@dyrected/admin 2.6.0 → 2.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -5,7 +5,7 @@ import { HashRouter, Link, MemoryRouter, Route, Routes, useLocation, useNavigate
5
5
  import { QueryClient, QueryClientProvider, keepPreviousData, useInfiniteQuery, useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/react-query";
6
6
  import { DyrectedError, PREVIEW_TOKEN_PARAM, createClient } from "@dyrected/sdk";
7
7
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
- import { AlertCircle, AlertTriangle, AlignCenter, AlignLeft, AlignRight, Archive, ArrowDown, ArrowLeft, ArrowRight, ArrowUp, ArrowUpDown, ArrowUpRight, Bold, Braces, Calendar, Check, CheckCircle, CheckCircle2, ChevronDown, ChevronLeft, ChevronLeftIcon, ChevronRight, ChevronRightIcon, ChevronUp, ChevronsUpDown, Circle, Clock, Clock3, Code, Copy, Database, Download, ExternalLink, Eye, EyeOff, FileDown, FileIcon, FileText, FileUp, Filter, Globe, GripVertical, Heading1, Heading2, Heading3, Heading4, Heading5, Heading6, History, Home, Image as Image$1, Info, Italic, KeyRound, Layers, LayoutDashboard, LayoutGrid, Library, Link as Link$1, List, ListOrdered, Loader2, Lock, LogOut, Mail, Menu, Monitor, Moon, MoreHorizontal, MousePointer2, PanelLeftClose, PanelLeftOpen, Pencil, Play, Plus, Quote, RotateCcw, Save, Scissors, Search, Settings, Settings2, Share2, Shield, Smartphone, Sparkles, Star, Strikethrough, Sun, Table, Trash2, Underline, Undo2, Upload, UploadCloud, Users, Video, Volume2, Workflow, X, XCircle, icons } from "lucide-react";
8
+ import { AlertCircle, AlertTriangle, AlignCenter, AlignLeft, AlignRight, Archive, ArrowDown, ArrowLeft, ArrowRight, ArrowUp, ArrowUpDown, ArrowUpRight, Bold, Braces, Calendar, Check, CheckCircle, CheckCircle2, ChevronDown, ChevronLeft, ChevronLeftIcon, ChevronRight, ChevronRightIcon, ChevronUp, ChevronsUpDown, Circle, Clock, Clock3, Code, Copy, Database, Download, ExternalLink, Eye, EyeOff, FileDown, FileIcon, FileText, FileUp, Filter, Globe, GripVertical, Heading1, Heading2, Heading3, Heading4, Heading5, Heading6, History, Home, Image as Image$1, Info, Italic, KeyRound, Layers, LayoutDashboard, LayoutGrid, Library, Link as Link$1, List, ListOrdered, Loader2, Lock, LogOut, Mail, Menu, MessageSquarePlus, Monitor, Moon, MoreHorizontal, MousePointer2, PanelLeftClose, PanelLeftOpen, Pencil, Play, Plus, Quote, RotateCcw, Save, Scissors, Search, Settings, Settings2, Share2, Shield, Smartphone, Sparkles, Star, Strikethrough, Sun, Table, Trash2, Underline, Undo2, Upload, UploadCloud, Users, Video, Volume2, Workflow, X, XCircle, icons } from "lucide-react";
9
9
  import { clsx } from "clsx";
10
10
  import { extendTailwindMerge } from "tailwind-merge";
11
11
  import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
@@ -1045,7 +1045,7 @@ function isNewerVersion(latest, current) {
1045
1045
  return false;
1046
1046
  }
1047
1047
  function useUpdateCheck() {
1048
- const currentVersion = "2.5.65";
1048
+ const currentVersion = "2.6.0";
1049
1049
  const [updateInfo, setUpdateInfo] = useState(() => {
1050
1050
  if (typeof window === "undefined") return null;
1051
1051
  const cacheKey = "dyrected_latest_release";
@@ -1271,7 +1271,7 @@ function getStatusLabel(doc) {
1271
1271
  }
1272
1272
  function Dashboard() {
1273
1273
  const { client, components, user } = useDyrected();
1274
- const currentVersion = "2.5.65";
1274
+ const currentVersion = "2.6.0";
1275
1275
  const [latestVersion, setLatestVersion] = useState(() => {
1276
1276
  if (typeof window === "undefined") return null;
1277
1277
  return localStorage.getItem("dyrected_latest_release");
@@ -9147,7 +9147,7 @@ function ArrayFieldRenderer({ schema, basePath, control, renderField }) {
9147
9147
  const { schemas } = useDyrected();
9148
9148
  const { drillInEnabled, drillInto, reconcileAfterMutation, activePath, registerFieldArray, unregisterFieldArray } = useNestedEditor();
9149
9149
  const [isBulkOpen, setIsBulkOpen] = React$1.useState(false);
9150
- const [expandedIds, setExpandedIds] = React$1.useState({});
9150
+ const [expandedIds, setExpandedIds] = React$1.useState(() => Object.fromEntries(fields.map((field) => [field.id, true])));
9151
9151
  const watchedItems = useWatch({
9152
9152
  control,
9153
9153
  name: basePath
@@ -9234,7 +9234,7 @@ function ArrayFieldRenderer({ schema, basePath, control, renderField }) {
9234
9234
  });
9235
9235
  nextIds.forEach((id) => {
9236
9236
  if (next[id] === void 0) {
9237
- next[id] = false;
9237
+ next[id] = true;
9238
9238
  changed = true;
9239
9239
  }
9240
9240
  });
@@ -9729,10 +9729,29 @@ function FormFieldRendererInner({ schema, basePath, control, collection, canUpda
9729
9729
  });
9730
9730
  }
9731
9731
  //#endregion
9732
+ //#region src/lib/workflow-autosave.ts
9733
+ var DEFAULT_WORKFLOW_AUTOSAVE_DELAY_MS = 1500;
9734
+ function isWorkflowEnabledCollection(collection) {
9735
+ return Boolean(collection?.workflow || collection?.drafts);
9736
+ }
9737
+ function resolveWorkflowAutosaveSettings(collection) {
9738
+ if (!isWorkflowEnabledCollection(collection)) return {
9739
+ enabled: false,
9740
+ delayMs: DEFAULT_WORKFLOW_AUTOSAVE_DELAY_MS
9741
+ };
9742
+ return {
9743
+ enabled: collection?.admin?.autosave !== false,
9744
+ delayMs: collection?.admin?.autosaveDelayMs ?? 1500
9745
+ };
9746
+ }
9747
+ function classifyWorkflowAutosaveError(error) {
9748
+ return (typeof error === "object" && error !== null && "statusCode" in error ? error.statusCode : void 0) === 409 ? "conflict" : "error";
9749
+ }
9750
+ //#endregion
9732
9751
  //#region src/components/forms/form-engine.tsx
9733
9752
  /** Query-string key holding the active form tab (replace-navigated). */
9734
9753
  var TAB_PARAM = "tab";
9735
- function FormEngineInner({ collection, fields, defaultValues = {}, onSubmit, onChange, isLoading, submitLabel = "Save", hideSubmit = false, readOnly, onDataChange, passwordChangeMode = null, documentId, defaultTabLabel }) {
9754
+ function FormEngineInner({ collection, fields, defaultValues = {}, onSubmit, onChange, isLoading, submitLabel = "Save", hideSubmit = false, readOnly, onDataChange, passwordChangeMode = null, documentId, defaultTabLabel, autosave }, ref) {
9736
9755
  const { activePath, navigateToPath, getStableId } = useNestedEditor();
9737
9756
  const [searchParams, setSearchParams] = useSearchParams();
9738
9757
  const isDrilledIn = activePath.length > 0;
@@ -9780,6 +9799,12 @@ function FormEngineInner({ collection, fields, defaultValues = {}, onSubmit, onC
9780
9799
  });
9781
9800
  const { isDirty } = form.formState;
9782
9801
  const flatErrors = getFlatErrors(form.formState.errors);
9802
+ const autosaveEnabled = Boolean(autosave?.enabled && autosave.onSave && !readOnly);
9803
+ const autosaveStateRef = React.useRef("idle");
9804
+ const emitAutosaveState = useCallback((state) => {
9805
+ autosaveStateRef.current = state;
9806
+ autosave?.onStatusChange?.(state);
9807
+ }, [autosave]);
9783
9808
  useEffect(() => {
9784
9809
  if (flatErrors.length > 0 && form.formState.submitCount > 0) console.warn("[Validation] Submission failed with errors:", form.formState.errors);
9785
9810
  }, [
@@ -9790,16 +9815,71 @@ function FormEngineInner({ collection, fields, defaultValues = {}, onSubmit, onC
9790
9815
  useEffect(() => {
9791
9816
  onChange?.(isDirty);
9792
9817
  }, [isDirty, onChange]);
9818
+ useEffect(() => {
9819
+ if (!autosaveEnabled) {
9820
+ emitAutosaveState("idle");
9821
+ return;
9822
+ }
9823
+ if (!isDirty) {
9824
+ if (autosaveStateRef.current !== "saving") emitAutosaveState("saved");
9825
+ return;
9826
+ }
9827
+ if (autosaveStateRef.current !== "saving") emitAutosaveState("dirty");
9828
+ }, [
9829
+ autosaveEnabled,
9830
+ emitAutosaveState,
9831
+ isDirty
9832
+ ]);
9793
9833
  const watchedValues = useWatch({ control: form.control });
9794
- const handleFormSubmit = useCallback(async (data) => {
9834
+ const persistFormData = useCallback(async (data, persist, options) => {
9795
9835
  const draftKey = `dyrected_draft:${collection}:${defaultValues?.id || "global"}`;
9796
- localStorage.removeItem(draftKey);
9797
- await onSubmit(data);
9836
+ if (options?.reportAutosaveStatus) emitAutosaveState("saving");
9837
+ try {
9838
+ localStorage.removeItem(draftKey);
9839
+ const result = await persist(data);
9840
+ form.reset(data);
9841
+ if (options?.reportAutosaveStatus) emitAutosaveState("saved");
9842
+ return result;
9843
+ } catch (error) {
9844
+ if (options?.reportAutosaveStatus) emitAutosaveState(classifyWorkflowAutosaveError(error));
9845
+ throw error;
9846
+ }
9798
9847
  }, [
9799
9848
  collection,
9800
9849
  defaultValues,
9801
- onSubmit
9850
+ emitAutosaveState,
9851
+ form
9852
+ ]);
9853
+ const handleFormSubmit = useCallback(async (data) => {
9854
+ return persistFormData(data, onSubmit, { reportAutosaveStatus: autosaveEnabled });
9855
+ }, [
9856
+ autosaveEnabled,
9857
+ onSubmit,
9858
+ persistFormData
9859
+ ]);
9860
+ const submitCurrentDraft = useCallback(() => {
9861
+ return new Promise((resolve, reject) => {
9862
+ form.handleSubmit(async (data) => {
9863
+ try {
9864
+ resolve(await handleFormSubmit(data));
9865
+ } catch (error) {
9866
+ reject(error);
9867
+ }
9868
+ }, () => {
9869
+ if (autosaveEnabled) emitAutosaveState("dirty");
9870
+ reject(/* @__PURE__ */ new Error("Please resolve validation errors before continuing."));
9871
+ })();
9872
+ });
9873
+ }, [
9874
+ autosaveEnabled,
9875
+ emitAutosaveState,
9876
+ form,
9877
+ handleFormSubmit
9802
9878
  ]);
9879
+ React.useImperativeHandle(ref, () => ({
9880
+ submitCurrentDraft,
9881
+ isDirty: () => form.formState.isDirty
9882
+ }), [form.formState.isDirty, submitCurrentDraft]);
9803
9883
  useEffect(() => {
9804
9884
  if (readOnly || isLoading) return;
9805
9885
  const handleKeyDown = (e) => {
@@ -9868,6 +9948,24 @@ function FormEngineInner({ collection, fields, defaultValues = {}, onSubmit, onC
9868
9948
  collection,
9869
9949
  defaultValues
9870
9950
  ]);
9951
+ useEffect(() => {
9952
+ if (!autosaveEnabled || !isDirty || isLoading) return;
9953
+ const handler = setTimeout(() => {
9954
+ form.handleSubmit(async (data) => {
9955
+ await persistFormData(data, autosave.onSave, { reportAutosaveStatus: true }).catch(() => {});
9956
+ }, () => emitAutosaveState("dirty"))();
9957
+ }, autosave?.delayMs ?? 1500);
9958
+ return () => clearTimeout(handler);
9959
+ }, [
9960
+ autosave,
9961
+ autosaveEnabled,
9962
+ emitAutosaveState,
9963
+ form,
9964
+ isDirty,
9965
+ isLoading,
9966
+ persistFormData,
9967
+ watchedValues
9968
+ ]);
9871
9969
  useEffect(() => {
9872
9970
  if (onDataChange) {
9873
9971
  const handler = setTimeout(() => {
@@ -10213,10 +10311,17 @@ function FormEngineInner({ collection, fields, defaultValues = {}, onSubmit, onC
10213
10311
  })
10214
10312
  })] });
10215
10313
  }
10216
- function FormEngine(props) {
10217
- if (React.useContext(NestedEditorContext)) return /* @__PURE__ */ jsx(FormEngineInner, { ...props });
10218
- return /* @__PURE__ */ jsx(NestedEditorProvider, { children: /* @__PURE__ */ jsx(FormEngineInner, { ...props }) });
10219
- }
10314
+ var ForwardedFormEngineInner = React.forwardRef(FormEngineInner);
10315
+ var FormEngine = React.forwardRef(function FormEngine(props, ref) {
10316
+ if (React.useContext(NestedEditorContext)) return /* @__PURE__ */ jsx(ForwardedFormEngineInner, {
10317
+ ...props,
10318
+ ref
10319
+ });
10320
+ return /* @__PURE__ */ jsx(NestedEditorProvider, { children: /* @__PURE__ */ jsx(ForwardedFormEngineInner, {
10321
+ ...props,
10322
+ ref
10323
+ }) });
10324
+ });
10220
10325
  //#endregion
10221
10326
  //#region src/components/forms/fields/relationship-picker.tsx
10222
10327
  var PAGE_SIZE = 50;
@@ -11090,6 +11195,449 @@ function SpreadsheetEditor({ slug, schema, data, onSave, isSaving }) {
11090
11195
  });
11091
11196
  }
11092
11197
  //#endregion
11198
+ //#region src/lib/workflow-ui.ts
11199
+ function resolveWorkflowState(workflowConfig, workflowMeta) {
11200
+ if (!workflowConfig || !workflowMeta?.state) return null;
11201
+ return workflowConfig.states.find((state) => state.name === workflowMeta.state) ?? null;
11202
+ }
11203
+ function resolveWorkflowStateFromDocument(workflowConfig, item) {
11204
+ return resolveWorkflowState(workflowConfig, item._workflow ?? null);
11205
+ }
11206
+ function resolvePublishingStatus(schema, item) {
11207
+ const hasPublishingState = !!(schema?.workflow || schema?.drafts);
11208
+ const workflowState = resolveWorkflowStateFromDocument(schema?.workflow ?? null, item);
11209
+ const workflowMeta = item._workflow;
11210
+ if (workflowState) {
11211
+ const rawWorkflowLabel = (workflowState.label || workflowState.name || "").trim();
11212
+ const workflowStateLabel = rawWorkflowLabel && !["published", "draft"].includes(rawWorkflowLabel.toLowerCase()) ? rawWorkflowLabel : null;
11213
+ if (workflowState.published) return {
11214
+ label: "Published",
11215
+ color: "success",
11216
+ workflowStateLabel
11217
+ };
11218
+ if (workflowMeta?.publishedRevision) return {
11219
+ label: "Changed",
11220
+ color: "info",
11221
+ workflowStateLabel
11222
+ };
11223
+ return {
11224
+ label: "Draft",
11225
+ color: "warning",
11226
+ workflowStateLabel
11227
+ };
11228
+ }
11229
+ if (!hasPublishingState) return null;
11230
+ const plainStatus = item.status;
11231
+ if (plainStatus === "published") return {
11232
+ label: "Published",
11233
+ color: "success",
11234
+ workflowStateLabel: null
11235
+ };
11236
+ if (plainStatus === "draft") return {
11237
+ label: "Draft",
11238
+ color: "warning",
11239
+ workflowStateLabel: null
11240
+ };
11241
+ return null;
11242
+ }
11243
+ function getAvailableWorkflowTransitions(workflowConfig, workflowMeta) {
11244
+ if (!workflowConfig || !workflowMeta) return [];
11245
+ const available = new Set(workflowMeta.availableTransitions ?? []);
11246
+ return workflowConfig.transitions.filter((transition) => available.has(transition.name));
11247
+ }
11248
+ function getPrimaryWorkflowTransition(transitions) {
11249
+ if (transitions.length === 0) return null;
11250
+ if (transitions.length === 1) return transitions[0] ?? null;
11251
+ return transitions.find((transition) => !transition.unpublish) ?? transitions[0] ?? null;
11252
+ }
11253
+ function groupWorkflowTransitions(transitions) {
11254
+ return transitions.reduce((groups, transition) => {
11255
+ if (transition.unpublish) groups.unpublish.push(transition);
11256
+ else groups.normal.push(transition);
11257
+ return groups;
11258
+ }, {
11259
+ normal: [],
11260
+ unpublish: []
11261
+ });
11262
+ }
11263
+ function getCommonWorkflowTransitions(workflowConfig, docs) {
11264
+ if (!workflowConfig || docs.length === 0) return [];
11265
+ const availableSets = docs.map((doc) => doc._workflow?.availableTransitions).filter((value) => Array.isArray(value) && value.length > 0).map((value) => new Set(value));
11266
+ if (availableSets.length !== docs.length) return [];
11267
+ return workflowConfig.transitions.filter((transition) => availableSets.every((set) => set.has(transition.name)));
11268
+ }
11269
+ //#endregion
11270
+ //#region src/components/workflow/workflow-transition-controls.tsx
11271
+ function formatSingleTransitionSuccessMessage(transitionLabel, documentLabel) {
11272
+ return documentLabel?.trim() ? `${documentLabel}: ${transitionLabel}` : `${transitionLabel} completed`;
11273
+ }
11274
+ function StateIcon$1({ color }) {
11275
+ if (color === "success") return /* @__PURE__ */ jsx(CheckCircle, { className: "dy-h-3.5 dy-w-3.5" });
11276
+ if (color === "warning") return /* @__PURE__ */ jsx(Clock, { className: "dy-h-3.5 dy-w-3.5" });
11277
+ if (color === "danger") return /* @__PURE__ */ jsx(XCircle, { className: "dy-h-3.5 dy-w-3.5" });
11278
+ if (color === "info") return /* @__PURE__ */ jsx(AlertCircle, { className: "dy-h-3.5 dy-w-3.5" });
11279
+ return /* @__PURE__ */ jsx(Clock, { className: "dy-h-3.5 dy-w-3.5" });
11280
+ }
11281
+ function WorkflowCommentDialog({ transition, pending, onOpenChange, onConfirm }) {
11282
+ const [comment, setComment] = React$1.useState("");
11283
+ React$1.useEffect(() => {
11284
+ if (!transition) setComment("");
11285
+ }, [transition]);
11286
+ return /* @__PURE__ */ jsx(Dialog, {
11287
+ open: !!transition,
11288
+ onOpenChange,
11289
+ children: /* @__PURE__ */ jsxs(DialogContent, {
11290
+ className: "sm:dy-max-w-md",
11291
+ children: [
11292
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [/* @__PURE__ */ jsx(DialogTitle, { children: transition?.label }), /* @__PURE__ */ jsx(DialogDescription, { children: "Add the required comment before applying this workflow transition." })] }),
11293
+ /* @__PURE__ */ jsxs("div", {
11294
+ className: "dy-space-y-2",
11295
+ children: [/* @__PURE__ */ jsx("label", {
11296
+ className: "dy-text-xs dy-font-semibold dy-text-muted-foreground",
11297
+ children: "Comment"
11298
+ }), /* @__PURE__ */ jsx("textarea", {
11299
+ className: "dy-min-h-28 dy-w-full dy-rounded-lg dy-border dy-border-border/60 dy-bg-background dy-px-3 dy-py-2 dy-text-sm dy-text-foreground placeholder:dy-text-muted-foreground focus:dy-outline-none focus:dy-ring-2 focus:dy-ring-primary/30",
11300
+ value: comment,
11301
+ onChange: (event) => setComment(event.target.value),
11302
+ placeholder: `Required for "${transition?.label ?? "this transition"}"`,
11303
+ autoFocus: true
11304
+ })]
11305
+ }),
11306
+ /* @__PURE__ */ jsxs(DialogFooter, {
11307
+ className: "dy-flex dy-justify-end dy-gap-2",
11308
+ children: [/* @__PURE__ */ jsx(Button, {
11309
+ variant: "outline",
11310
+ onClick: () => onOpenChange(false),
11311
+ disabled: pending,
11312
+ children: "Cancel"
11313
+ }), /* @__PURE__ */ jsx(Button, {
11314
+ onClick: () => onConfirm(comment.trim()),
11315
+ disabled: pending || !comment.trim(),
11316
+ children: pending ? /* @__PURE__ */ jsx(Loader2, { className: "dy-h-4 dy-w-4 dy-animate-spin" }) : transition?.label
11317
+ })]
11318
+ })
11319
+ ]
11320
+ })
11321
+ });
11322
+ }
11323
+ function WorkflowTransitionActionItems({ transitions, workflowConfig, onSelect, disabled }) {
11324
+ return transitions.map((transition) => {
11325
+ return /* @__PURE__ */ jsxs(DropdownMenuItem, {
11326
+ onClick: () => onSelect(transition),
11327
+ disabled,
11328
+ className: "dy-flex dy-items-center dy-gap-2",
11329
+ children: [
11330
+ /* @__PURE__ */ jsx(StateIcon$1, { color: workflowConfig.states.find((state) => state.name === transition.to)?.color }),
11331
+ /* @__PURE__ */ jsx("span", {
11332
+ className: "dy-flex-1",
11333
+ children: transition.label
11334
+ }),
11335
+ transition.requireComment && /* @__PURE__ */ jsx(MessageSquarePlus, { className: "dy-h-3.5 dy-w-3.5 dy-text-muted-foreground" })
11336
+ ]
11337
+ }, transition.name);
11338
+ });
11339
+ }
11340
+ function useWorkflowTransitionExecutor({ collection, documentIds, documentLabels, expectedRevisions, invalidateQueryKeys, onComplete, prepareTransition }) {
11341
+ const { client } = useDyrected();
11342
+ const queryClient = useQueryClient();
11343
+ const wfClient = client;
11344
+ const [commentTransition, setCommentTransition] = React$1.useState(null);
11345
+ const [isPreparing, setIsPreparing] = React$1.useState(false);
11346
+ const getDefaultContext = React$1.useCallback(() => ({
11347
+ documentIds,
11348
+ documentLabels,
11349
+ expectedRevisions,
11350
+ invalidateQueryKeys
11351
+ }), [
11352
+ documentIds,
11353
+ documentLabels,
11354
+ expectedRevisions,
11355
+ invalidateQueryKeys
11356
+ ]);
11357
+ const mutation = useMutation({
11358
+ mutationFn: async ({ transition, comment, context }) => {
11359
+ let successCount = 0;
11360
+ let failureCount = 0;
11361
+ let firstError;
11362
+ for (const id of context.documentIds) try {
11363
+ await wfClient.transition(collection, id, transition.name, {
11364
+ expectedRevision: context.expectedRevisions?.[id],
11365
+ comment
11366
+ });
11367
+ successCount += 1;
11368
+ } catch (error) {
11369
+ failureCount += 1;
11370
+ if (!firstError) firstError = error instanceof Error ? error : new Error(String(error));
11371
+ }
11372
+ return {
11373
+ transition,
11374
+ successCount,
11375
+ failureCount,
11376
+ firstError,
11377
+ context
11378
+ };
11379
+ },
11380
+ onSuccess: async (result) => {
11381
+ setCommentTransition(null);
11382
+ const queryKeys = [...invalidateQueryKeys ?? [], ...result.context.invalidateQueryKeys ?? []];
11383
+ if (queryKeys.length > 0) await Promise.all(queryKeys.map((queryKey) => queryClient.invalidateQueries({ queryKey: [...queryKey] })));
11384
+ if (result.failureCount === 0) if (result.context.documentIds.length === 1) {
11385
+ const singleDocumentId = result.context.documentIds[0] ?? "";
11386
+ toast.success(formatSingleTransitionSuccessMessage(result.transition.label, result.context.documentLabels?.[singleDocumentId]));
11387
+ } else toast.success(`Applied "${result.transition.label}" to ${result.successCount} ${result.successCount === 1 ? "entry" : "entries"}`);
11388
+ else if (result.successCount === 0) toast.error("Transition failed", { description: result.firstError?.message ?? "The workflow transition could not be completed." });
11389
+ else toast(`Applied "${result.transition.label}" to ${result.successCount} entries`, { description: `${result.failureCount} failed. ${result.firstError?.message ?? ""}`.trim() });
11390
+ onComplete?.(result);
11391
+ },
11392
+ onError: (error) => {
11393
+ setCommentTransition(null);
11394
+ toast.error("Transition failed", { description: error.message });
11395
+ }
11396
+ });
11397
+ return {
11398
+ requestTransition: React$1.useCallback(async (transition) => {
11399
+ try {
11400
+ setIsPreparing(true);
11401
+ const context = await (prepareTransition?.(transition) ?? Promise.resolve(getDefaultContext()));
11402
+ if (!context || context.documentIds.length === 0) return;
11403
+ if (transition.requireComment) {
11404
+ setCommentTransition({
11405
+ transition,
11406
+ context
11407
+ });
11408
+ return;
11409
+ }
11410
+ mutation.mutate({
11411
+ transition,
11412
+ context
11413
+ });
11414
+ } catch (error) {
11415
+ const message = error instanceof Error ? error.message : "The workflow action could not be prepared.";
11416
+ toast.error("Could not continue", { description: message });
11417
+ } finally {
11418
+ setIsPreparing(false);
11419
+ }
11420
+ }, [
11421
+ getDefaultContext,
11422
+ mutation,
11423
+ prepareTransition
11424
+ ]),
11425
+ commentTransition,
11426
+ setCommentTransition,
11427
+ confirmComment: React$1.useCallback((comment) => {
11428
+ if (!commentTransition) return;
11429
+ mutation.mutate({
11430
+ transition: commentTransition.transition,
11431
+ comment,
11432
+ context: commentTransition.context
11433
+ });
11434
+ }, [commentTransition, mutation]),
11435
+ mutation,
11436
+ isPreparing
11437
+ };
11438
+ }
11439
+ function WorkflowTransitionSplitButton({ collection, documentId, workflowConfig, workflowMeta, invalidateQueryKeys, onComplete, onPendingChange, onSaveDraft, saveDraftPending, documentLabel, className, prepareTransition }) {
11440
+ const transitions = React$1.useMemo(() => getAvailableWorkflowTransitions(workflowConfig, workflowMeta), [workflowConfig, workflowMeta]);
11441
+ const groupedTransitions = React$1.useMemo(() => groupWorkflowTransitions(transitions), [transitions]);
11442
+ const primaryTransition = React$1.useMemo(() => getPrimaryWorkflowTransition(transitions), [transitions]);
11443
+ const { requestTransition, commentTransition, setCommentTransition, confirmComment, mutation, isPreparing } = useWorkflowTransitionExecutor({
11444
+ collection,
11445
+ documentIds: [documentId],
11446
+ documentLabels: { [documentId]: documentLabel },
11447
+ expectedRevisions: { [documentId]: workflowMeta.revision },
11448
+ invalidateQueryKeys,
11449
+ onComplete,
11450
+ prepareTransition
11451
+ });
11452
+ const activeTransitionName = mutation.variables?.transition?.name;
11453
+ const isPrimaryLoading = mutation.isPending && activeTransitionName === primaryTransition?.name;
11454
+ const hasTransitions = transitions.length > 0;
11455
+ const hasMenuItems = hasTransitions || !!onSaveDraft;
11456
+ const isBusy = mutation.isPending || isPreparing || !!saveDraftPending;
11457
+ React$1.useEffect(() => {
11458
+ onPendingChange?.(mutation.isPending || isPreparing);
11459
+ }, [
11460
+ isPreparing,
11461
+ mutation.isPending,
11462
+ onPendingChange
11463
+ ]);
11464
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
11465
+ className: cn("dy-inline-flex dy-items-center", className),
11466
+ children: [/* @__PURE__ */ jsx("div", { className: "dy-mx-1 dy-h-6 dy-w-px dy-bg-border/60" }), /* @__PURE__ */ jsxs("div", {
11467
+ className: "dy-inline-flex dy-overflow-hidden dy-rounded-lg dy-border dy-border-primary/20 dy-shadow-sm",
11468
+ children: [/* @__PURE__ */ jsx(Button, {
11469
+ size: "sm",
11470
+ className: "dy-h-9 dy-rounded-none dy-border-0 dy-px-4 dy-font-bold dy-bg-primary dy-text-primary-foreground hover:dy-bg-primary/90",
11471
+ disabled: !primaryTransition || isBusy,
11472
+ onClick: () => {
11473
+ if (primaryTransition) requestTransition(primaryTransition);
11474
+ },
11475
+ title: primaryTransition?.label ?? "No workflow actions available",
11476
+ children: isPrimaryLoading || isPreparing ? /* @__PURE__ */ jsxs("span", {
11477
+ className: "dy-flex dy-items-center dy-gap-2",
11478
+ children: [/* @__PURE__ */ jsx(Loader2, { className: "dy-h-3.5 dy-w-3.5 dy-animate-spin" }), isPreparing ? "Saving…" : "Applying…"]
11479
+ }) : primaryTransition?.label ?? "No actions"
11480
+ }), /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
11481
+ asChild: true,
11482
+ children: /* @__PURE__ */ jsx(Button, {
11483
+ size: "sm",
11484
+ variant: "ghost",
11485
+ className: "dy-h-9 dy-rounded-none dy-border-0 dy-px-2 dy-text-primary-foreground hover:dy-bg-primary/90 hover:dy-text-primary-foreground",
11486
+ disabled: !hasMenuItems || isBusy,
11487
+ "aria-label": "More workflow actions",
11488
+ children: /* @__PURE__ */ jsx(ChevronDown, { className: "dy-h-4 dy-w-4" })
11489
+ })
11490
+ }), /* @__PURE__ */ jsxs(DropdownMenuContent, {
11491
+ align: "end",
11492
+ className: "dy-min-w-56",
11493
+ children: [onSaveDraft && /* @__PURE__ */ jsxs(DropdownMenuItem, {
11494
+ onClick: () => void onSaveDraft(),
11495
+ disabled: isBusy,
11496
+ className: "dy-flex dy-items-center dy-gap-2",
11497
+ children: [saveDraftPending ? /* @__PURE__ */ jsx(Loader2, { className: "dy-h-3.5 dy-w-3.5 dy-animate-spin" }) : /* @__PURE__ */ jsx(Save, { className: "dy-h-3.5 dy-w-3.5" }), /* @__PURE__ */ jsx("span", {
11498
+ className: "dy-flex-1",
11499
+ children: saveDraftPending ? "Saving draft..." : "Save draft"
11500
+ })]
11501
+ }), hasTransitions && /* @__PURE__ */ jsxs(Fragment, { children: [
11502
+ onSaveDraft && /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
11503
+ /* @__PURE__ */ jsx(WorkflowTransitionActionItems, {
11504
+ transitions: groupedTransitions.normal,
11505
+ workflowConfig,
11506
+ onSelect: (transition) => {
11507
+ requestTransition(transition);
11508
+ },
11509
+ disabled: isBusy
11510
+ }),
11511
+ groupedTransitions.unpublish.length > 0 && groupedTransitions.normal.length > 0 && /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
11512
+ /* @__PURE__ */ jsx(WorkflowTransitionActionItems, {
11513
+ transitions: groupedTransitions.unpublish,
11514
+ workflowConfig,
11515
+ onSelect: (transition) => {
11516
+ requestTransition(transition);
11517
+ },
11518
+ disabled: isBusy
11519
+ })
11520
+ ] })]
11521
+ })] })]
11522
+ })]
11523
+ }), /* @__PURE__ */ jsx(WorkflowCommentDialog, {
11524
+ transition: commentTransition?.transition ?? null,
11525
+ pending: mutation.isPending,
11526
+ onOpenChange: (open) => !open && setCommentTransition(null),
11527
+ onConfirm: confirmComment
11528
+ })] });
11529
+ }
11530
+ function WorkflowTransitionMenu({ collection, documentIds, workflowConfig, transitions, expectedRevisions, invalidateQueryKeys, onComplete, trigger, documentLabels, align = "end", sideOffset = 4, disabled }) {
11531
+ const { requestTransition, commentTransition, setCommentTransition, confirmComment, mutation, isPreparing } = useWorkflowTransitionExecutor({
11532
+ collection,
11533
+ documentIds,
11534
+ documentLabels,
11535
+ expectedRevisions,
11536
+ invalidateQueryKeys,
11537
+ onComplete
11538
+ });
11539
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
11540
+ asChild: true,
11541
+ disabled: disabled || transitions.length === 0,
11542
+ children: trigger
11543
+ }), /* @__PURE__ */ jsx(DropdownMenuContent, {
11544
+ align,
11545
+ sideOffset,
11546
+ className: "dy-min-w-56",
11547
+ children: /* @__PURE__ */ jsx(WorkflowTransitionActionItems, {
11548
+ transitions,
11549
+ workflowConfig,
11550
+ onSelect: (transition) => {
11551
+ requestTransition(transition);
11552
+ },
11553
+ disabled: mutation.isPending || isPreparing
11554
+ })
11555
+ })] }), /* @__PURE__ */ jsx(WorkflowCommentDialog, {
11556
+ transition: commentTransition?.transition ?? null,
11557
+ pending: mutation.isPending,
11558
+ onOpenChange: (open) => !open && setCommentTransition(null),
11559
+ onConfirm: confirmComment
11560
+ })] });
11561
+ }
11562
+ function WorkflowTransitionPanelActions({ collection, documentId, workflowConfig, workflowMeta, onSaveDraft, saveDraftPending, prepareTransition }) {
11563
+ const transitions = React$1.useMemo(() => getAvailableWorkflowTransitions(workflowConfig, workflowMeta), [workflowConfig, workflowMeta]);
11564
+ const groupedTransitions = React$1.useMemo(() => groupWorkflowTransitions(transitions), [transitions]);
11565
+ const { requestTransition, commentTransition, setCommentTransition, confirmComment, mutation, isPreparing } = useWorkflowTransitionExecutor({
11566
+ collection,
11567
+ documentIds: [documentId],
11568
+ expectedRevisions: { [documentId]: workflowMeta.revision },
11569
+ invalidateQueryKeys: [
11570
+ [
11571
+ "entry",
11572
+ collection,
11573
+ documentId
11574
+ ],
11575
+ ["collection", collection],
11576
+ [
11577
+ "workflow-history",
11578
+ collection,
11579
+ documentId
11580
+ ]
11581
+ ],
11582
+ prepareTransition
11583
+ });
11584
+ const activeTransitionName = mutation.variables?.transition?.name;
11585
+ if (transitions.length === 0 && !onSaveDraft) return /* @__PURE__ */ jsx("p", {
11586
+ className: "dy-text-xs dy-text-muted-foreground/50 dy-italic",
11587
+ children: "No transitions available from this state."
11588
+ });
11589
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
11590
+ className: "dy-space-y-2",
11591
+ children: [
11592
+ onSaveDraft && /* @__PURE__ */ jsxs(Button, {
11593
+ size: "sm",
11594
+ variant: "secondary",
11595
+ className: "dy-w-full dy-h-9 dy-rounded-lg dy-text-xs dy-font-semibold dy-justify-start dy-gap-2",
11596
+ disabled: mutation.isPending || isPreparing || saveDraftPending,
11597
+ onClick: () => void onSaveDraft(),
11598
+ children: [saveDraftPending ? /* @__PURE__ */ jsx(Loader2, { className: "dy-h-3.5 dy-w-3.5 dy-animate-spin" }) : /* @__PURE__ */ jsx(Save, { className: "dy-h-3.5 dy-w-3.5" }), saveDraftPending ? "Saving draft..." : "Save draft"]
11599
+ }),
11600
+ groupedTransitions.normal.map((transition) => {
11601
+ const isLoading = mutation.isPending && activeTransitionName === transition.name;
11602
+ const targetState = workflowConfig.states.find((state) => state.name === transition.to);
11603
+ return /* @__PURE__ */ jsxs(Button, {
11604
+ size: "sm",
11605
+ variant: "default",
11606
+ className: "dy-w-full dy-h-9 dy-rounded-lg dy-text-xs dy-font-semibold dy-justify-start dy-gap-2",
11607
+ disabled: mutation.isPending || isPreparing,
11608
+ onClick: () => {
11609
+ requestTransition(transition);
11610
+ },
11611
+ children: [isLoading ? /* @__PURE__ */ jsx(Loader2, { className: "dy-h-3.5 dy-w-3.5 dy-animate-spin" }) : /* @__PURE__ */ jsx(StateIcon$1, { color: targetState?.color }), transition.label]
11612
+ }, transition.name);
11613
+ }),
11614
+ groupedTransitions.unpublish.length > 0 && groupedTransitions.normal.length > 0 && /* @__PURE__ */ jsx("div", {
11615
+ className: "dy-px-1 dy-pt-2",
11616
+ children: /* @__PURE__ */ jsx("div", { className: "dy-h-px dy-bg-border/60" })
11617
+ }),
11618
+ groupedTransitions.unpublish.map((transition) => {
11619
+ const isLoading = mutation.isPending && activeTransitionName === transition.name;
11620
+ const targetState = workflowConfig.states.find((state) => state.name === transition.to);
11621
+ return /* @__PURE__ */ jsxs(Button, {
11622
+ size: "sm",
11623
+ variant: "outline",
11624
+ className: "dy-w-full dy-h-9 dy-rounded-lg dy-text-xs dy-font-semibold dy-justify-start dy-gap-2",
11625
+ disabled: mutation.isPending || isPreparing,
11626
+ onClick: () => {
11627
+ requestTransition(transition);
11628
+ },
11629
+ children: [isLoading ? /* @__PURE__ */ jsx(Loader2, { className: "dy-h-3.5 dy-w-3.5 dy-animate-spin" }) : /* @__PURE__ */ jsx(StateIcon$1, { color: targetState?.color }), transition.label]
11630
+ }, transition.name);
11631
+ })
11632
+ ]
11633
+ }), /* @__PURE__ */ jsx(WorkflowCommentDialog, {
11634
+ transition: commentTransition?.transition ?? null,
11635
+ pending: mutation.isPending,
11636
+ onOpenChange: (open) => !open && setCommentTransition(null),
11637
+ onConfirm: confirmComment
11638
+ })] });
11639
+ }
11640
+ //#endregion
11093
11641
  //#region src/pages/collections/list-page.tsx
11094
11642
  function SortableColumnItem({ id, label, visible, onToggleVisible }) {
11095
11643
  const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
@@ -11119,24 +11667,74 @@ function SortableColumnItem({ id, label, visible, onToggleVisible }) {
11119
11667
  ]
11120
11668
  });
11121
11669
  }
11122
- function resolvePublishedStateName(schema, item) {
11123
- const hasPublishingState = !!(schema?.workflow || schema?.drafts);
11124
- const workflowConfig = schema?.workflow;
11125
- const workflowMeta = item?._workflow;
11126
- const workflowState = workflowConfig && workflowMeta ? workflowConfig.states?.find((state) => state.name === workflowMeta.state) : null;
11127
- if (workflowState) return workflowState.published ? "Published" : "Draft";
11128
- if (!hasPublishingState) return null;
11129
- const plainStatus = item.status;
11130
- if (plainStatus === "published") return "Published";
11131
- if (plainStatus === "draft") return "Draft";
11132
- return null;
11133
- }
11134
- function resolveWorkflowState(schema, item) {
11135
- const workflowConfig = schema?.workflow;
11136
- const workflowMeta = item?._workflow;
11137
- return workflowConfig && workflowMeta ? workflowConfig.states?.find((state) => state.name === workflowMeta.state) ?? null : null;
11670
+ function WorkflowStatusCell({ schema, slug, item }) {
11671
+ const workflowConfig = schema.workflow ?? null;
11672
+ const workflowMeta = item._workflow ?? null;
11673
+ const publishingStatus = resolvePublishingStatus(schema, item);
11674
+ const documentLabel = resolveDocumentTitle({
11675
+ entry: item,
11676
+ collection: schema,
11677
+ collections: [schema]
11678
+ });
11679
+ if (!publishingStatus) return /* @__PURE__ */ jsx("span", {
11680
+ className: "dy-text-xs dy-text-muted-foreground",
11681
+ children: "-"
11682
+ });
11683
+ const workflowState = resolveWorkflowStateFromDocument(workflowConfig, item);
11684
+ const badgePresentation = {
11685
+ className: WORKFLOW_BADGE_COLORS[publishingStatus.color],
11686
+ style: void 0
11687
+ };
11688
+ const workflowStagePresentation = workflowState && publishingStatus.workflowStateLabel ? getWorkflowBadgePresentation(workflowState.color) : null;
11689
+ const transitions = getAvailableWorkflowTransitions(workflowConfig, workflowMeta);
11690
+ const badge = /* @__PURE__ */ jsxs("div", {
11691
+ className: "dy-inline-flex dy-items-center dy-gap-1.5",
11692
+ children: [/* @__PURE__ */ jsx(Badge, {
11693
+ variant: "outline",
11694
+ className: cn("dy-px-2 dy-py-0 dy-rounded-full dy-text-[10px] dy-font-bold dy-uppercase dy-tracking-wider dy-shrink-0", transitions.length > 0 && "hover:dy-opacity-85", badgePresentation.className),
11695
+ style: badgePresentation.style,
11696
+ children: publishingStatus.label
11697
+ }), publishingStatus.workflowStateLabel && workflowStagePresentation && /* @__PURE__ */ jsx(Badge, {
11698
+ variant: "outline",
11699
+ className: cn("dy-px-2 dy-py-0 dy-rounded-full dy-text-[10px] dy-font-semibold dy-tracking-wide dy-shrink-0", transitions.length > 0 && "hover:dy-opacity-85", workflowStagePresentation.className),
11700
+ style: workflowStagePresentation.style,
11701
+ children: publishingStatus.workflowStateLabel
11702
+ })]
11703
+ });
11704
+ if (!workflowConfig || !workflowMeta || transitions.length === 0 || typeof item.id !== "string") return badge;
11705
+ return /* @__PURE__ */ jsx(WorkflowTransitionMenu, {
11706
+ collection: slug,
11707
+ documentIds: [item.id],
11708
+ documentLabels: { [item.id]: documentLabel },
11709
+ workflowConfig,
11710
+ transitions,
11711
+ expectedRevisions: { [item.id]: workflowMeta.revision },
11712
+ invalidateQueryKeys: [
11713
+ ["collection", slug],
11714
+ [
11715
+ "entry",
11716
+ slug,
11717
+ item.id
11718
+ ],
11719
+ [
11720
+ "workflow-history",
11721
+ slug,
11722
+ item.id
11723
+ ]
11724
+ ],
11725
+ trigger: /* @__PURE__ */ jsx("button", {
11726
+ type: "button",
11727
+ className: "dy-inline-flex",
11728
+ title: "Change workflow state",
11729
+ "aria-label": "Change workflow state",
11730
+ children: badge
11731
+ })
11732
+ });
11138
11733
  }
11139
11734
  function CollectionListPage({ slug }) {
11735
+ return /* @__PURE__ */ jsx(CollectionListPageContent, { slug }, slug);
11736
+ }
11737
+ function CollectionListPageContent({ slug }) {
11140
11738
  const { client, components, user } = useDyrected();
11141
11739
  const queryClient = useQueryClient();
11142
11740
  const [page, setPage] = React$1.useState(1);
@@ -11162,12 +11760,6 @@ function CollectionListPage({ slug }) {
11162
11760
  }, { replace: true });
11163
11761
  setPage(1);
11164
11762
  }, [setSearchParams]);
11165
- const [prevSlug, setPrevSlug] = React$1.useState(slug);
11166
- if (prevSlug !== slug) {
11167
- setPrevSlug(slug);
11168
- setPage(1);
11169
- setRowSelection({});
11170
- }
11171
11763
  const { data: schemas } = useQuery({
11172
11764
  queryKey: ["schemas"],
11173
11765
  queryFn: () => client.getSchemas(),
@@ -11475,6 +12067,10 @@ function CollectionListPage({ slug }) {
11475
12067
  }
11476
12068
  if (window.confirm(`Delete ${cleanIds.length} entries? This cannot be undone.`)) bulkDeleteMutation.mutate(cleanIds);
11477
12069
  }
12070
+ const workflowConfig = schema?.workflow ?? null;
12071
+ const selectedWorkflowDocs = React$1.useMemo(() => Object.keys(rowSelection).filter((id) => rowSelection[id]).map((id) => response?.docs?.find((doc) => String(doc.id) === id)).filter((doc) => !!doc && typeof doc.id === "string" && !!doc._workflow), [rowSelection, response]);
12072
+ const sharedWorkflowTransitions = React$1.useMemo(() => getCommonWorkflowTransitions(workflowConfig, selectedWorkflowDocs), [workflowConfig, selectedWorkflowDocs]);
12073
+ const selectedWorkflowRevisions = React$1.useMemo(() => Object.fromEntries(selectedWorkflowDocs.map((doc) => [doc.id, doc._workflow?.revision])), [selectedWorkflowDocs]);
11478
12074
  const columns = React$1.useMemo(() => {
11479
12075
  if (!schema) return [];
11480
12076
  const allDisplayFields = schema.fields.filter((f) => f.name && f.name !== "password" && !f.admin?.hidden && f.type !== "row" && f.type !== "join");
@@ -11632,21 +12228,10 @@ function CollectionListPage({ slug }) {
11632
12228
  header: "Status",
11633
12229
  enableHiding: false,
11634
12230
  cell: ({ row }) => {
11635
- const status = resolvePublishedStateName(schema, row.original);
11636
- if (!status) return /* @__PURE__ */ jsx("span", {
11637
- className: "dy-text-xs dy-text-muted-foreground",
11638
- children: "-"
11639
- });
11640
- const workflowState = resolveWorkflowState(schema, row.original);
11641
- const badgePresentation = workflowState ? getWorkflowBadgePresentation(workflowState.color) : {
11642
- className: status === "Published" ? WORKFLOW_BADGE_COLORS.success : WORKFLOW_BADGE_COLORS.warning,
11643
- style: void 0
11644
- };
11645
- return /* @__PURE__ */ jsx(Badge, {
11646
- variant: "outline",
11647
- className: cn("dy-px-2 dy-py-0 dy-rounded-full dy-text-[10px] dy-font-bold dy-uppercase dy-tracking-wider dy-shrink-0", badgePresentation.className),
11648
- style: badgePresentation.style,
11649
- children: status
12231
+ return /* @__PURE__ */ jsx(WorkflowStatusCell, {
12232
+ schema,
12233
+ slug,
12234
+ item: row.original
11650
12235
  });
11651
12236
  }
11652
12237
  });
@@ -12060,48 +12645,80 @@ function CollectionListPage({ slug }) {
12060
12645
  }
12061
12646
  return canDelete;
12062
12647
  });
12063
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(Button, {
12064
- variant: "outline",
12065
- size: "sm",
12066
- className: "dy-h-8",
12067
- onClick: () => handleExportSelected(selectedIds),
12068
- disabled: selectedIds.length === 0,
12069
- children: [
12070
- /* @__PURE__ */ jsx(FileDown, { className: "dy-h-4 dy-w-4 dy-mr-2" }),
12071
- "Export Selected (",
12072
- selectedIds.length,
12073
- ")"
12074
- ]
12075
- }), /* @__PURE__ */ jsxs(Button, {
12076
- variant: "destructive",
12077
- size: "sm",
12078
- className: "dy-h-8",
12079
- onClick: () => handleBulkDelete(deletableIds),
12080
- disabled: bulkDeleteMutation.isPending || deletableIds.length === 0,
12081
- children: [
12082
- /* @__PURE__ */ jsx(Trash2, { className: "dy-h-4 dy-w-4 dy-mr-2" }),
12083
- "Delete Selected (",
12084
- deletableIds.length,
12085
- ")"
12086
- ]
12087
- })] });
12088
- }
12089
- }, slug),
12090
- /* @__PURE__ */ jsx(AdminComponentSlot, {
12091
- slot: "afterListTable",
12092
- componentKeys: collectionSlots?.afterListTable,
12093
- registry: components?.collectionList,
12094
- componentProps: collectionComponentProps
12095
- }),
12096
- /* @__PURE__ */ jsx(Pagination, {
12097
- page,
12098
- totalPages,
12099
- total: response?.total,
12100
- hasPrevPage,
12101
- hasNextPage,
12102
- onPageChange: setPage
12103
- }),
12104
- /* @__PURE__ */ jsx(AdminComponentSlot, {
12648
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
12649
+ workflowConfig && selectedWorkflowDocs.length > 0 && sharedWorkflowTransitions.length > 0 && /* @__PURE__ */ jsx(WorkflowTransitionMenu, {
12650
+ collection: slug,
12651
+ documentIds: selectedWorkflowDocs.map((doc) => doc.id),
12652
+ documentLabels: Object.fromEntries(selectedWorkflowDocs.map((doc) => [doc.id, resolveDocumentTitle({
12653
+ entry: doc,
12654
+ collection: schema,
12655
+ collections: [schema]
12656
+ })])),
12657
+ workflowConfig,
12658
+ transitions: sharedWorkflowTransitions,
12659
+ expectedRevisions: selectedWorkflowRevisions,
12660
+ invalidateQueryKeys: [["collection", slug]],
12661
+ onComplete: () => setRowSelection({}),
12662
+ trigger: /* @__PURE__ */ jsxs(Button, {
12663
+ variant: "outline",
12664
+ size: "sm",
12665
+ className: "dy-h-8",
12666
+ disabled: selectedWorkflowDocs.length === 0,
12667
+ children: [
12668
+ "Change state (",
12669
+ selectedWorkflowDocs.length,
12670
+ ")"
12671
+ ]
12672
+ })
12673
+ }),
12674
+ workflowConfig && selectedWorkflowDocs.length > 0 && sharedWorkflowTransitions.length === 0 && /* @__PURE__ */ jsx("span", {
12675
+ className: "dy-text-xs dy-text-muted-foreground",
12676
+ children: "No shared workflow action for this selection."
12677
+ }),
12678
+ /* @__PURE__ */ jsxs(Button, {
12679
+ variant: "outline",
12680
+ size: "sm",
12681
+ className: "dy-h-8",
12682
+ onClick: () => handleExportSelected(selectedIds),
12683
+ disabled: selectedIds.length === 0,
12684
+ children: [
12685
+ /* @__PURE__ */ jsx(FileDown, { className: "dy-h-4 dy-w-4 dy-mr-2" }),
12686
+ "Export Selected (",
12687
+ selectedIds.length,
12688
+ ")"
12689
+ ]
12690
+ }),
12691
+ /* @__PURE__ */ jsxs(Button, {
12692
+ variant: "destructive",
12693
+ size: "sm",
12694
+ className: "dy-h-8",
12695
+ onClick: () => handleBulkDelete(deletableIds),
12696
+ disabled: bulkDeleteMutation.isPending || deletableIds.length === 0,
12697
+ children: [
12698
+ /* @__PURE__ */ jsx(Trash2, { className: "dy-h-4 dy-w-4 dy-mr-2" }),
12699
+ "Delete Selected (",
12700
+ deletableIds.length,
12701
+ ")"
12702
+ ]
12703
+ })
12704
+ ] });
12705
+ }
12706
+ }, slug),
12707
+ /* @__PURE__ */ jsx(AdminComponentSlot, {
12708
+ slot: "afterListTable",
12709
+ componentKeys: collectionSlots?.afterListTable,
12710
+ registry: components?.collectionList,
12711
+ componentProps: collectionComponentProps
12712
+ }),
12713
+ /* @__PURE__ */ jsx(Pagination, {
12714
+ page,
12715
+ totalPages,
12716
+ total: response?.total,
12717
+ hasPrevPage,
12718
+ hasNextPage,
12719
+ onPageChange: setPage
12720
+ }),
12721
+ /* @__PURE__ */ jsx(AdminComponentSlot, {
12105
12722
  slot: "afterList",
12106
12723
  componentKeys: collectionSlots?.afterList,
12107
12724
  registry: components?.collectionList,
@@ -12358,55 +12975,11 @@ function StateIcon({ color }) {
12358
12975
  if (color === "info") return /* @__PURE__ */ jsx(AlertCircle, { className: "dy-h-3.5 dy-w-3.5" });
12359
12976
  return /* @__PURE__ */ jsx(Clock, { className: "dy-h-3.5 dy-w-3.5" });
12360
12977
  }
12361
- function CommentDialog({ transitionLabel, onConfirm, onCancel, isPending }) {
12362
- const [comment, setComment] = useState("");
12363
- return /* @__PURE__ */ jsxs("div", {
12364
- className: "dy-mt-3 dy-space-y-2",
12365
- children: [
12366
- /* @__PURE__ */ jsxs("label", {
12367
- className: "dy-text-xs dy-font-medium dy-text-muted-foreground",
12368
- children: ["Comment ", /* @__PURE__ */ jsx("span", {
12369
- className: "dy-text-destructive",
12370
- children: "*"
12371
- })]
12372
- }),
12373
- /* @__PURE__ */ jsx("textarea", {
12374
- className: "dy-w-full dy-rounded-lg dy-border dy-border-border/60 dy-bg-background dy-px-3 dy-py-2 dy-text-sm dy-text-foreground placeholder:dy-text-muted-foreground focus:dy-outline-none focus:dy-ring-2 focus:dy-ring-primary/30 dy-resize-none",
12375
- rows: 3,
12376
- placeholder: `Required for "${transitionLabel}"…`,
12377
- value: comment,
12378
- onChange: (e) => setComment(e.target.value),
12379
- autoFocus: true
12380
- }),
12381
- /* @__PURE__ */ jsxs("div", {
12382
- className: "dy-flex dy-gap-2",
12383
- children: [/* @__PURE__ */ jsx(Button, {
12384
- size: "sm",
12385
- className: "dy-h-8 dy-px-3 dy-rounded-lg dy-text-xs dy-font-semibold",
12386
- disabled: !comment.trim() || isPending,
12387
- onClick: () => onConfirm(comment.trim()),
12388
- children: isPending ? /* @__PURE__ */ jsx(Loader2, { className: "dy-h-3 dy-w-3 dy-animate-spin" }) : transitionLabel
12389
- }), /* @__PURE__ */ jsx(Button, {
12390
- size: "sm",
12391
- variant: "ghost",
12392
- className: "dy-h-8 dy-px-3 dy-rounded-lg dy-text-xs",
12393
- onClick: onCancel,
12394
- disabled: isPending,
12395
- children: "Cancel"
12396
- })]
12397
- })
12398
- ]
12399
- });
12400
- }
12401
- function WorkflowPanel({ collection, documentId, workflowMeta, workflowConfig }) {
12978
+ function WorkflowPanel({ collection, documentId, workflowMeta, workflowConfig, onCompareToLive, compareToLiveEnabled = false, compareToLiveReason = null, onSaveDraft, saveDraftPending, prepareTransition }) {
12402
12979
  const { client } = useDyrected();
12403
- const queryClient = useQueryClient();
12404
12980
  const [showHistory, setShowHistory] = useState(false);
12405
- const [pendingComment, setPendingComment] = useState(null);
12406
12981
  const currentState = workflowConfig.states.find((s) => s.name === workflowMeta.state);
12407
12982
  const colors = stateColors(currentState?.color);
12408
- const available = workflowMeta.availableTransitions ?? [];
12409
- const availableTransitions = workflowConfig.transitions.filter((t) => available.includes(t.name));
12410
12983
  const wfClient = client;
12411
12984
  const { data: historyResult, isLoading: historyLoading } = useQuery({
12412
12985
  queryKey: [
@@ -12418,34 +12991,6 @@ function WorkflowPanel({ collection, documentId, workflowMeta, workflowConfig })
12418
12991
  enabled: showHistory && !!client
12419
12992
  });
12420
12993
  const history = historyResult?.docs ?? [];
12421
- const transitionMutation = useMutation({
12422
- mutationFn: ({ name, comment }) => wfClient.transition(collection, documentId, name, {
12423
- expectedRevision: workflowMeta.revision,
12424
- comment
12425
- }),
12426
- onSuccess: () => {
12427
- setPendingComment(null);
12428
- queryClient.invalidateQueries({ queryKey: [
12429
- "entry",
12430
- collection,
12431
- documentId
12432
- ] });
12433
- queryClient.invalidateQueries({ queryKey: [
12434
- "workflow-history",
12435
- collection,
12436
- documentId
12437
- ] });
12438
- toast.success("Transition applied");
12439
- },
12440
- onError: (err) => {
12441
- setPendingComment(null);
12442
- toast.error("Transition failed", { description: err.message });
12443
- }
12444
- });
12445
- function handleTransitionClick(t) {
12446
- if (t.requireComment) setPendingComment(t.name);
12447
- else transitionMutation.mutate({ name: t.name });
12448
- }
12449
12994
  return /* @__PURE__ */ jsxs("div", {
12450
12995
  className: "dy-rounded-2xl dy-border dy-border-border/50 dy-bg-muted/10 dy-p-4 dy-space-y-4",
12451
12996
  children: [
@@ -12463,32 +13008,32 @@ function WorkflowPanel({ collection, documentId, workflowMeta, workflowConfig })
12463
13008
  className: "dy-flex dy-gap-4 dy-text-[11px] dy-text-muted-foreground/60",
12464
13009
  children: [/* @__PURE__ */ jsxs("span", { children: ["Revision ", workflowMeta.revision] }), workflowMeta.publishedAt && /* @__PURE__ */ jsxs("span", { children: ["Published ", new Date(workflowMeta.publishedAt).toLocaleDateString()] })]
12465
13010
  }),
12466
- availableTransitions.length > 0 && /* @__PURE__ */ jsx("div", {
12467
- className: "dy-space-y-2",
12468
- children: availableTransitions.map((t) => {
12469
- const isWaiting = pendingComment === t.name;
12470
- const isLoading = transitionMutation.isPending && transitionMutation.variables?.name === t.name;
12471
- return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs(Button, {
12472
- size: "sm",
12473
- variant: t.unpublish || t.name === "reject" ? "outline" : "default",
12474
- className: cn("dy-w-full dy-h-9 dy-rounded-lg dy-text-xs dy-font-semibold dy-justify-start dy-gap-2", isWaiting && "dy-ring-2 dy-ring-primary/30"),
12475
- disabled: transitionMutation.isPending,
12476
- onClick: () => handleTransitionClick(t),
12477
- children: [isLoading ? /* @__PURE__ */ jsx(Loader2, { className: "dy-h-3.5 dy-w-3.5 dy-animate-spin" }) : /* @__PURE__ */ jsx(StateIcon, { color: workflowConfig.states.find((s) => s.name === t.to)?.color }), t.label]
12478
- }), isWaiting && /* @__PURE__ */ jsx(CommentDialog, {
12479
- transitionLabel: t.label,
12480
- isPending: transitionMutation.isPending,
12481
- onConfirm: (comment) => transitionMutation.mutate({
12482
- name: t.name,
12483
- comment
12484
- }),
12485
- onCancel: () => setPendingComment(null)
12486
- })] }, t.name);
12487
- })
13011
+ /* @__PURE__ */ jsx(WorkflowTransitionPanelActions, {
13012
+ collection,
13013
+ documentId,
13014
+ workflowConfig,
13015
+ workflowMeta,
13016
+ onSaveDraft,
13017
+ saveDraftPending,
13018
+ prepareTransition
12488
13019
  }),
12489
- available.length === 0 && /* @__PURE__ */ jsx("p", {
12490
- className: "dy-text-xs dy-text-muted-foreground/50 dy-italic",
12491
- children: "No transitions available from this state."
13020
+ onCompareToLive && /* @__PURE__ */ jsx(Button, {
13021
+ type: "button",
13022
+ variant: "outline",
13023
+ className: "dy-w-full dy-justify-between dy-rounded-xl dy-border-border/60 dy-bg-background/70 dy-text-left",
13024
+ onClick: onCompareToLive,
13025
+ disabled: !compareToLiveEnabled,
13026
+ title: !compareToLiveEnabled && compareToLiveReason ? compareToLiveReason : "Compare draft to live",
13027
+ children: /* @__PURE__ */ jsxs("span", {
13028
+ className: "dy-flex dy-flex-col dy-items-start",
13029
+ children: [/* @__PURE__ */ jsx("span", {
13030
+ className: "dy-text-sm dy-font-semibold dy-text-foreground",
13031
+ children: "Compare to live"
13032
+ }), /* @__PURE__ */ jsx("span", {
13033
+ className: "dy-text-xs dy-font-normal dy-text-muted-foreground",
13034
+ children: compareToLiveEnabled ? "See exactly what will change on publish." : compareToLiveReason ?? "Publish this entry once to unlock live comparisons."
13035
+ })]
13036
+ })
12492
13037
  }),
12493
13038
  /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("button", {
12494
13039
  className: "dy-flex dy-items-center dy-gap-1 dy-text-[11px] dy-font-medium dy-text-muted-foreground/60 hover:dy-text-muted-foreground dy-transition-colors",
@@ -12545,6 +13090,216 @@ function WorkflowPanel({ collection, documentId, workflowMeta, workflowConfig })
12545
13090
  });
12546
13091
  }
12547
13092
  //#endregion
13093
+ //#region src/components/ui/card.tsx
13094
+ var Card = React$1.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", {
13095
+ ref,
13096
+ className: cn("dy-rounded-lg dy-border dy-bg-card dy-text-card-foreground dy-shadow-sm", className),
13097
+ ...props
13098
+ }));
13099
+ Card.displayName = "Card";
13100
+ var CardHeader = React$1.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", {
13101
+ ref,
13102
+ className: cn("dy-flex dy-flex-col dy-space-y-1.5 dy-p-6", className),
13103
+ ...props
13104
+ }));
13105
+ CardHeader.displayName = "CardHeader";
13106
+ var CardTitle = React$1.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", {
13107
+ ref,
13108
+ className: cn("dy-text-2xl dy-font-semibold dy-leading-none dy-tracking-tight", className),
13109
+ ...props
13110
+ }));
13111
+ CardTitle.displayName = "CardTitle";
13112
+ var CardDescription = React$1.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", {
13113
+ ref,
13114
+ className: cn("dy-text-sm dy-text-muted-foreground", className),
13115
+ ...props
13116
+ }));
13117
+ CardDescription.displayName = "CardDescription";
13118
+ var CardContent = React$1.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", {
13119
+ ref,
13120
+ className: cn("dy-p-6 dy-pt-0", className),
13121
+ ...props
13122
+ }));
13123
+ CardContent.displayName = "CardContent";
13124
+ var CardFooter = React$1.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", {
13125
+ ref,
13126
+ className: cn("dy-flex dy-items-center dy-p-6 dy-pt-0", className),
13127
+ ...props
13128
+ }));
13129
+ CardFooter.displayName = "CardFooter";
13130
+ //#endregion
13131
+ //#region src/components/workflow/draft-live-compare-sheet.tsx
13132
+ function statusBadgeClass(status) {
13133
+ if (status === "Added") return "dy-bg-emerald-50 dy-text-emerald-700 dy-border-emerald-200";
13134
+ if (status === "Removed") return "dy-bg-rose-50 dy-text-rose-700 dy-border-rose-200";
13135
+ return "dy-bg-amber-50 dy-text-amber-700 dy-border-amber-200";
13136
+ }
13137
+ function DraftLiveCompareSheet({ open, onOpenChange, comparison }) {
13138
+ return /* @__PURE__ */ jsx(Sheet, {
13139
+ open,
13140
+ onOpenChange,
13141
+ children: /* @__PURE__ */ jsxs(SheetContent, {
13142
+ side: "right",
13143
+ className: "dy-w-[96vw] sm:dy-max-w-4xl lg:dy-max-w-6xl dy-border-l dy-border-border/60",
13144
+ children: [/* @__PURE__ */ jsx(SheetHeader, {
13145
+ className: "dy-border-b dy-border-border/50 dy-bg-background/95 dy-px-6 dy-py-5 dy-backdrop-blur",
13146
+ children: /* @__PURE__ */ jsxs("div", {
13147
+ className: "dy-flex dy-flex-wrap dy-items-start dy-justify-between dy-gap-4",
13148
+ children: [/* @__PURE__ */ jsxs("div", {
13149
+ className: "dy-space-y-1",
13150
+ children: [/* @__PURE__ */ jsx(SheetTitle, {
13151
+ className: "dy-text-2xl dy-font-serif dy-font-bold dy-tracking-tight",
13152
+ children: "Compare draft to live"
13153
+ }), /* @__PURE__ */ jsx(SheetDescription, { children: "These changes are in your draft and are not live yet." })]
13154
+ }), /* @__PURE__ */ jsxs("div", {
13155
+ className: "dy-flex dy-flex-wrap dy-gap-2",
13156
+ children: [
13157
+ /* @__PURE__ */ jsx(SummaryChip, { label: `${comparison.fieldChangeCount} fields changed` }),
13158
+ /* @__PURE__ */ jsx(SummaryChip, { label: `${comparison.sectionsAdded} sections added` }),
13159
+ /* @__PURE__ */ jsx(SummaryChip, { label: `${comparison.sectionsRemoved} sections removed` })
13160
+ ]
13161
+ })]
13162
+ })
13163
+ }), /* @__PURE__ */ jsx(ScrollArea, {
13164
+ className: "dy-h-[calc(100vh-132px)]",
13165
+ children: /* @__PURE__ */ jsxs("div", {
13166
+ className: "dy-space-y-8 dy-p-6",
13167
+ children: [!comparison.hasChanges && /* @__PURE__ */ jsx(Card, {
13168
+ className: "dy-rounded-2xl dy-border-border/60",
13169
+ children: /* @__PURE__ */ jsxs(CardContent, {
13170
+ className: "dy-p-8 dy-text-center",
13171
+ children: [/* @__PURE__ */ jsx("p", {
13172
+ className: "dy-text-base dy-font-semibold dy-text-foreground",
13173
+ children: "Draft matches the live site."
13174
+ }), /* @__PURE__ */ jsx("p", {
13175
+ className: "dy-mt-2 dy-text-sm dy-text-muted-foreground",
13176
+ children: "There are no unpublished changes in this document right now."
13177
+ })]
13178
+ })
13179
+ }), comparison.groups.map((group) => /* @__PURE__ */ jsxs("section", {
13180
+ className: "dy-space-y-4",
13181
+ children: [/* @__PURE__ */ jsx("div", {
13182
+ className: "dy-flex dy-items-center dy-justify-between",
13183
+ children: /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h3", {
13184
+ className: "dy-text-sm dy-font-bold dy-uppercase dy-tracking-[0.18em] dy-text-muted-foreground/70",
13185
+ children: group.title
13186
+ }), /* @__PURE__ */ jsxs("p", {
13187
+ className: "dy-mt-1 dy-text-sm dy-text-muted-foreground",
13188
+ children: [
13189
+ group.cards.length,
13190
+ " ",
13191
+ group.cards.length === 1 ? "change" : "changes"
13192
+ ]
13193
+ })] })
13194
+ }), /* @__PURE__ */ jsx("div", {
13195
+ className: "dy-space-y-4",
13196
+ children: group.cards.map((card) => /* @__PURE__ */ jsx(ChangeCard, { card }, card.id))
13197
+ })]
13198
+ }, group.title))]
13199
+ })
13200
+ })]
13201
+ })
13202
+ });
13203
+ }
13204
+ function SummaryChip({ label }) {
13205
+ return /* @__PURE__ */ jsx("div", {
13206
+ className: "dy-rounded-full dy-border dy-border-border/60 dy-bg-muted/40 dy-px-3 dy-py-1.5 dy-text-xs dy-font-semibold dy-text-foreground/80",
13207
+ children: label
13208
+ });
13209
+ }
13210
+ function ChangeCard({ card }) {
13211
+ return /* @__PURE__ */ jsxs(Card, {
13212
+ className: "dy-overflow-hidden dy-rounded-2xl dy-border-border/60",
13213
+ children: [/* @__PURE__ */ jsx(CardHeader, {
13214
+ className: "dy-gap-3 dy-border-b dy-border-border/50 dy-bg-muted/20 dy-px-5 dy-py-4",
13215
+ children: /* @__PURE__ */ jsxs("div", {
13216
+ className: "dy-flex dy-flex-wrap dy-items-center dy-justify-between dy-gap-3",
13217
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(CardTitle, {
13218
+ className: "dy-text-base dy-font-semibold",
13219
+ children: card.label
13220
+ }), /* @__PURE__ */ jsx("p", {
13221
+ className: "dy-mt-1 dy-text-xs dy-text-muted-foreground",
13222
+ children: card.path
13223
+ })] }), /* @__PURE__ */ jsx(Badge, {
13224
+ variant: "outline",
13225
+ className: cn("dy-rounded-full dy-border dy-px-2.5 dy-py-1 dy-text-[10px] dy-font-bold dy-uppercase dy-tracking-[0.18em]", statusBadgeClass(card.status)),
13226
+ children: card.status
13227
+ })]
13228
+ })
13229
+ }), /* @__PURE__ */ jsxs(CardContent, {
13230
+ className: "dy-grid dy-gap-4 dy-p-5 lg:dy-grid-cols-2",
13231
+ children: [/* @__PURE__ */ jsx(CompareSide, {
13232
+ title: "Live",
13233
+ text: card.liveText,
13234
+ media: card.liveMedia
13235
+ }), /* @__PURE__ */ jsx(CompareSide, {
13236
+ title: "Draft",
13237
+ text: card.draftText,
13238
+ media: card.draftMedia,
13239
+ emphasize: true
13240
+ })]
13241
+ })]
13242
+ });
13243
+ }
13244
+ function CompareSide({ title, text, media, emphasize = false }) {
13245
+ return /* @__PURE__ */ jsxs("div", {
13246
+ className: cn("dy-space-y-3 dy-rounded-2xl dy-border dy-p-4", emphasize ? "dy-border-primary/20 dy-bg-primary/5" : "dy-border-border/60 dy-bg-background"),
13247
+ children: [
13248
+ /* @__PURE__ */ jsx("div", {
13249
+ className: "dy-flex dy-items-center dy-justify-between",
13250
+ children: /* @__PURE__ */ jsx("p", {
13251
+ className: "dy-text-[11px] dy-font-bold dy-uppercase dy-tracking-[0.18em] dy-text-muted-foreground/70",
13252
+ children: title
13253
+ })
13254
+ }),
13255
+ media && /* @__PURE__ */ jsx(MediaPreview, { media }),
13256
+ /* @__PURE__ */ jsx("div", {
13257
+ className: "dy-rounded-xl dy-bg-muted/35 dy-p-3",
13258
+ children: /* @__PURE__ */ jsx("p", {
13259
+ className: "dy-whitespace-pre-wrap dy-break-words dy-text-sm dy-leading-6 dy-text-foreground/90",
13260
+ children: text
13261
+ })
13262
+ })
13263
+ ]
13264
+ });
13265
+ }
13266
+ function MediaPreview({ media }) {
13267
+ return /* @__PURE__ */ jsxs("div", {
13268
+ className: "dy-flex dy-gap-3",
13269
+ children: [/* @__PURE__ */ jsx("div", {
13270
+ className: "dy-flex dy-h-20 dy-w-20 dy-shrink-0 dy-items-center dy-justify-center dy-overflow-hidden dy-rounded-xl dy-border dy-border-border/60 dy-bg-muted/30",
13271
+ children: media.url ? /* @__PURE__ */ jsx("img", {
13272
+ src: media.url,
13273
+ alt: media.alt || media.filename || "Media preview",
13274
+ className: "dy-h-full dy-w-full dy-object-cover"
13275
+ }) : /* @__PURE__ */ jsx("div", {
13276
+ className: "dy-text-[11px] dy-font-semibold dy-text-muted-foreground",
13277
+ children: "No preview"
13278
+ })
13279
+ }), /* @__PURE__ */ jsxs("div", {
13280
+ className: "dy-min-w-0 dy-space-y-1",
13281
+ children: [
13282
+ media.filename && /* @__PURE__ */ jsx("p", {
13283
+ className: "dy-text-sm dy-font-semibold dy-text-foreground dy-break-all",
13284
+ children: media.filename
13285
+ }),
13286
+ media.alt && /* @__PURE__ */ jsx("p", {
13287
+ className: "dy-text-xs dy-text-muted-foreground",
13288
+ children: media.alt
13289
+ }),
13290
+ media.url && /* @__PURE__ */ jsx(Button, {
13291
+ type: "button",
13292
+ variant: "ghost",
13293
+ size: "sm",
13294
+ className: "dy-h-auto dy-justify-start dy-p-0 dy-text-xs dy-text-primary hover:dy-bg-transparent hover:dy-text-primary/80",
13295
+ onClick: () => window.open(media.url, "_blank", "noopener,noreferrer"),
13296
+ children: "Open asset"
13297
+ })
13298
+ ]
13299
+ })]
13300
+ });
13301
+ }
13302
+ //#endregion
12548
13303
  //#region src/hooks/useLayoutPreference.ts
12549
13304
  function useLayoutPreference({ key, defaultKeys }) {
12550
13305
  const { client } = useDyrected();
@@ -12639,6 +13394,292 @@ function useLayoutPreference({ key, defaultKeys }) {
12639
13394
  };
12640
13395
  }
12641
13396
  //#endregion
13397
+ //#region src/lib/draft-live-compare.ts
13398
+ var GROUP_ORDER = [
13399
+ "Text changes",
13400
+ "Media changes",
13401
+ "Layout / sections changed",
13402
+ "Settings changed"
13403
+ ];
13404
+ var SYSTEM_KEYS = /* @__PURE__ */ new Set([
13405
+ "id",
13406
+ "_workflow",
13407
+ "__workflow",
13408
+ "__published",
13409
+ "createdAt",
13410
+ "createdBy",
13411
+ "updatedAt",
13412
+ "updatedBy",
13413
+ "password",
13414
+ "oldPassword",
13415
+ "newPassword",
13416
+ "confirmPassword"
13417
+ ]);
13418
+ function buildDraftLiveComparison(args) {
13419
+ const draft = args.draft ?? {};
13420
+ const live = args.live ?? {};
13421
+ const cards = [];
13422
+ const counters = {
13423
+ sectionsAdded: 0,
13424
+ sectionsRemoved: 0
13425
+ };
13426
+ compareFields({
13427
+ fields: args.fields,
13428
+ draft,
13429
+ live,
13430
+ pathPrefix: "",
13431
+ cards,
13432
+ counters
13433
+ });
13434
+ const groups = GROUP_ORDER.map((title) => ({
13435
+ title,
13436
+ cards: cards.filter((card) => card.group === title)
13437
+ })).filter((group) => group.cards.length > 0);
13438
+ return {
13439
+ hasPublishedVersion: !isStructurallyEmpty(live),
13440
+ hasChanges: cards.length > 0,
13441
+ fieldChangeCount: cards.length,
13442
+ sectionsAdded: counters.sectionsAdded,
13443
+ sectionsRemoved: counters.sectionsRemoved,
13444
+ groups
13445
+ };
13446
+ }
13447
+ function compareFields(args) {
13448
+ const draftRecord = args.draft ?? {};
13449
+ const liveRecord = args.live ?? {};
13450
+ for (const field of args.fields) {
13451
+ if (!field?.name || SYSTEM_KEYS.has(field.name) || field.admin?.hidden) continue;
13452
+ const path = args.pathPrefix ? `${args.pathPrefix}.${field.name}` : field.name;
13453
+ const draftValue = draftRecord[field.name];
13454
+ const liveValue = liveRecord[field.name];
13455
+ if (field.type === "object" && Array.isArray(field.fields)) {
13456
+ compareFields({
13457
+ fields: field.fields,
13458
+ draft: asRecord(draftValue),
13459
+ live: asRecord(liveValue),
13460
+ pathPrefix: path,
13461
+ cards: args.cards,
13462
+ counters: args.counters
13463
+ });
13464
+ continue;
13465
+ }
13466
+ if (field.type === "array" || field.type === "blocks") {
13467
+ compareArrayField({
13468
+ field,
13469
+ path,
13470
+ draftValue,
13471
+ liveValue,
13472
+ cards: args.cards,
13473
+ counters: args.counters
13474
+ });
13475
+ continue;
13476
+ }
13477
+ if (deepEqual(draftValue, liveValue)) continue;
13478
+ args.cards.push(buildCard({
13479
+ field,
13480
+ label: field.label || humanizeKey(field.name),
13481
+ path,
13482
+ draftValue,
13483
+ liveValue
13484
+ }));
13485
+ }
13486
+ }
13487
+ function compareArrayField(args) {
13488
+ const draftItems = Array.isArray(args.draftValue) ? args.draftValue : [];
13489
+ const liveItems = Array.isArray(args.liveValue) ? args.liveValue : [];
13490
+ const max = Math.max(draftItems.length, liveItems.length);
13491
+ for (let index = 0; index < max; index += 1) {
13492
+ const draftItem = draftItems[index];
13493
+ const liveItem = liveItems[index];
13494
+ if (deepEqual(draftItem, liveItem)) continue;
13495
+ const label = describeArrayItem(args.field, draftItem, liveItem, index);
13496
+ const status = resolveStatus(liveItem, draftItem);
13497
+ if (status === "Added") args.counters.sectionsAdded += 1;
13498
+ if (status === "Removed") args.counters.sectionsRemoved += 1;
13499
+ args.cards.push({
13500
+ id: `${args.path}.${index}`,
13501
+ group: "Layout / sections changed",
13502
+ label,
13503
+ status,
13504
+ path: `${args.path}.${index}`,
13505
+ kind: "array",
13506
+ liveValue: liveItem,
13507
+ draftValue: draftItem,
13508
+ liveText: summarizeArrayItem(liveItem),
13509
+ draftText: summarizeArrayItem(draftItem)
13510
+ });
13511
+ }
13512
+ }
13513
+ function buildCard(args) {
13514
+ const kind = resolveKind(args.field, args.draftValue, args.liveValue);
13515
+ const status = resolveStatus(args.liveValue, args.draftValue);
13516
+ const liveMedia = kind === "media" ? extractMediaPreview(args.liveValue) : null;
13517
+ const draftMedia = kind === "media" ? extractMediaPreview(args.draftValue) : null;
13518
+ return {
13519
+ id: args.path,
13520
+ group: resolveGroup(args.field, kind),
13521
+ label: args.label,
13522
+ status,
13523
+ path: args.path,
13524
+ kind,
13525
+ liveValue: args.liveValue,
13526
+ draftValue: args.draftValue,
13527
+ liveText: formatValueForPreview(args.liveValue, kind),
13528
+ draftText: formatValueForPreview(args.draftValue, kind),
13529
+ liveMedia,
13530
+ draftMedia
13531
+ };
13532
+ }
13533
+ function resolveKind(field, draftValue, liveValue) {
13534
+ if (field.type === "richText" || field.type === "json") return "richText";
13535
+ if (field.type === "image" || isMediaLike(draftValue) || isMediaLike(liveValue)) return "media";
13536
+ if (field.type === "relationship" && (field.hasMany || Array.isArray(draftValue) || Array.isArray(liveValue))) return "array";
13537
+ if (isLikelySettingField(field)) return "setting";
13538
+ return "text";
13539
+ }
13540
+ function resolveGroup(field, kind) {
13541
+ if (kind === "media") return "Media changes";
13542
+ if (kind === "array") return "Layout / sections changed";
13543
+ if (kind === "setting" || isLikelySettingField(field)) return "Settings changed";
13544
+ return "Text changes";
13545
+ }
13546
+ function resolveStatus(liveValue, draftValue) {
13547
+ if (isStructurallyEmpty(liveValue) && !isStructurallyEmpty(draftValue)) return "Added";
13548
+ if (!isStructurallyEmpty(liveValue) && isStructurallyEmpty(draftValue)) return "Removed";
13549
+ return "Changed";
13550
+ }
13551
+ function describeArrayItem(field, draftItem, liveItem, index) {
13552
+ const sample = asRecord(draftItem) || asRecord(liveItem);
13553
+ if (field.type === "blocks") {
13554
+ const blockType = typeof sample?.blockType === "string" ? sample.blockType : null;
13555
+ const blockLabel = blockType ? field.blocks?.find((block) => block.slug === blockType)?.labels?.singular ?? humanizeKey(blockType) : `Section ${index + 1}`;
13556
+ return blockLabel.includes("section") || blockLabel.includes("Section") ? blockLabel : `${blockLabel} section`;
13557
+ }
13558
+ const title = extractTitle(sample);
13559
+ if (title) return title;
13560
+ return `${field.label?.replace(/s$/, "") || "Section"} ${index + 1}`;
13561
+ }
13562
+ function summarizeArrayItem(value) {
13563
+ if (isStructurallyEmpty(value)) return "No content";
13564
+ const record = asRecord(value);
13565
+ if (!record) return formatValueForPreview(value, "text");
13566
+ const summaryParts = [];
13567
+ const title = extractTitle(record);
13568
+ if (title) summaryParts.push(title);
13569
+ const textSnippet = extractTextContent(record);
13570
+ if (textSnippet && textSnippet !== title) summaryParts.push(textSnippet);
13571
+ if (summaryParts.length === 0) {
13572
+ const keys = Object.keys(record).filter((key) => !SYSTEM_KEYS.has(key));
13573
+ if (keys.length > 0) summaryParts.push(`${keys.length} fields`);
13574
+ }
13575
+ return summaryParts.join(" • ") || "No content";
13576
+ }
13577
+ function formatValueForPreview(value, kind) {
13578
+ if (isStructurallyEmpty(value)) return "Empty";
13579
+ if (kind === "media") {
13580
+ const media = extractMediaPreview(value);
13581
+ return media?.filename || media?.url || media?.alt || "Media updated";
13582
+ }
13583
+ if (kind === "richText") return extractTextContent(value) || "Rich text updated";
13584
+ if (Array.isArray(value)) return value.map((item) => summarizeArrayItem(item)).filter(Boolean).slice(0, 3).join("\n") || `${value.length} items`;
13585
+ if (typeof value === "object") return extractTextContent(value) || JSON.stringify(value);
13586
+ return String(value);
13587
+ }
13588
+ function extractMediaPreview(value) {
13589
+ const record = asRecord(value);
13590
+ if (!record) return null;
13591
+ const filename = readString(record.filename) || readString(record.name);
13592
+ const url = readString(record.url) || readString(record.src);
13593
+ const alt = readString(record.alt) || readString(record.caption);
13594
+ if (!filename && !url && !alt) return null;
13595
+ return {
13596
+ filename,
13597
+ url,
13598
+ alt
13599
+ };
13600
+ }
13601
+ function isMediaLike(value) {
13602
+ return !!extractMediaPreview(value);
13603
+ }
13604
+ function isLikelySettingField(field) {
13605
+ const source = `${field.name ?? ""} ${field.label ?? ""}`.toLowerCase();
13606
+ return [
13607
+ "status",
13608
+ "slug",
13609
+ "seo",
13610
+ "settings",
13611
+ "theme",
13612
+ "variant",
13613
+ "url",
13614
+ "path",
13615
+ "template"
13616
+ ].some((token) => source.includes(token));
13617
+ }
13618
+ function extractTitle(value) {
13619
+ if (!value) return "";
13620
+ for (const key of [
13621
+ "title",
13622
+ "heading",
13623
+ "headline",
13624
+ "label",
13625
+ "name",
13626
+ "question",
13627
+ "slug"
13628
+ ]) {
13629
+ const next = readString(value[key]);
13630
+ if (next) return next;
13631
+ }
13632
+ return "";
13633
+ }
13634
+ function extractTextContent(value) {
13635
+ const parts = [];
13636
+ collectText(value, parts);
13637
+ return truncateText(parts.join(" ").replace(/\s+/g, " ").trim(), 220);
13638
+ }
13639
+ function collectText(value, parts) {
13640
+ if (value === null || value === void 0) return;
13641
+ if (typeof value === "string") {
13642
+ const cleaned = value.replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").trim();
13643
+ if (cleaned) parts.push(cleaned);
13644
+ return;
13645
+ }
13646
+ if (typeof value === "number" || typeof value === "boolean") {
13647
+ parts.push(String(value));
13648
+ return;
13649
+ }
13650
+ if (Array.isArray(value)) {
13651
+ for (const item of value) collectText(item, parts);
13652
+ return;
13653
+ }
13654
+ if (typeof value === "object") for (const [key, nested] of Object.entries(value)) {
13655
+ if (SYSTEM_KEYS.has(key) || key === "blockType") continue;
13656
+ collectText(nested, parts);
13657
+ }
13658
+ }
13659
+ function truncateText(value, max) {
13660
+ if (value.length <= max) return value;
13661
+ return `${value.slice(0, max - 1).trimEnd()}…`;
13662
+ }
13663
+ function asRecord(value) {
13664
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
13665
+ }
13666
+ function readString(value) {
13667
+ return typeof value === "string" && value.trim() ? value.trim() : null;
13668
+ }
13669
+ function deepEqual(left, right) {
13670
+ return JSON.stringify(left) === JSON.stringify(right);
13671
+ }
13672
+ function isStructurallyEmpty(value) {
13673
+ if (value === null || value === void 0) return true;
13674
+ if (typeof value === "string") return value.trim().length === 0;
13675
+ if (Array.isArray(value)) return value.length === 0;
13676
+ if (typeof value === "object") return Object.keys(value).length === 0;
13677
+ return false;
13678
+ }
13679
+ function humanizeKey(value) {
13680
+ return value.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim().replace(/^./, (char) => char.toUpperCase());
13681
+ }
13682
+ //#endregion
12642
13683
  //#region src/pages/collections/edit-page.tsx
12643
13684
  function SortableFieldItem({ id, label, type, width, onChangeWidth }) {
12644
13685
  const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
@@ -12712,19 +13753,59 @@ function SortableFieldItem({ id, label, type, width, onChangeWidth }) {
12712
13753
  });
12713
13754
  }
12714
13755
  /**
12715
- * A single top-bar action: a compact square icon button with active / disabled /
12716
- * busy states. Groups into the horizontal action cluster on the right of the
12717
- * editor header (Storyblok-style top bar rather than a far-right vertical rail).
13756
+ * A single top-bar action. It stays icon-only on smaller screens and expands to
13757
+ * an icon + label button on desktop so action meaning is visible without relying
13758
+ * on tooltips.
12718
13759
  */
12719
13760
  function HeaderAction({ icon: Icon, label, onClick, active, disabled, busy, title, className }) {
12720
- return /* @__PURE__ */ jsx("button", {
13761
+ return /* @__PURE__ */ jsxs("button", {
12721
13762
  type: "button",
12722
13763
  onClick,
12723
13764
  disabled,
12724
13765
  title: title || label,
12725
13766
  "aria-label": title || label,
12726
- className: cn("dy-flex dy-h-9 dy-w-9 dy-items-center dy-justify-center dy-rounded-lg dy-transition-all", active ? "dy-bg-muted dy-text-primary" : "dy-text-muted-foreground hover:dy-bg-muted/60 hover:dy-text-foreground", disabled && "dy-opacity-50 dy-pointer-events-none", className),
12727
- children: busy ? /* @__PURE__ */ jsx("span", { className: "dy-h-4 dy-w-4 dy-animate-spin dy-rounded-full dy-border-2 dy-border-current dy-border-t-transparent" }) : /* @__PURE__ */ jsx(Icon, { className: "dy-h-4 dy-w-4" })
13767
+ className: cn("dy-inline-flex dy-h-9 dy-w-9 lg:dy-w-auto dy-items-center dy-justify-center lg:dy-justify-start lg:dy-gap-2 dy-rounded-lg lg:dy-px-3 dy-transition-all", active ? "dy-bg-muted dy-text-primary" : "dy-text-muted-foreground hover:dy-bg-muted/60 hover:dy-text-foreground", disabled && "dy-opacity-50 dy-pointer-events-none", className),
13768
+ children: [busy ? /* @__PURE__ */ jsx("span", { className: "dy-h-4 dy-w-4 dy-animate-spin dy-rounded-full dy-border-2 dy-border-current dy-border-t-transparent" }) : /* @__PURE__ */ jsx(Icon, { className: "dy-h-4 dy-w-4" }), /* @__PURE__ */ jsx("span", {
13769
+ className: "dy-hidden lg:dy-inline dy-text-xs dy-font-medium",
13770
+ children: label
13771
+ })]
13772
+ });
13773
+ }
13774
+ function DraftSaveStatusBadge({ state }) {
13775
+ const presentation = (() => {
13776
+ switch (state) {
13777
+ case "dirty": return {
13778
+ icon: Save,
13779
+ label: "Unsaved changes",
13780
+ className: "dy-text-amber-700 dy-bg-amber-50 dy-border-amber-200"
13781
+ };
13782
+ case "saving": return {
13783
+ icon: Loader2,
13784
+ label: "Saving...",
13785
+ className: "dy-text-sky-700 dy-bg-sky-50 dy-border-sky-200"
13786
+ };
13787
+ case "saved":
13788
+ case "idle": return {
13789
+ icon: Save,
13790
+ label: "Changes saved",
13791
+ className: "dy-text-emerald-700 dy-bg-emerald-50 dy-border-emerald-200"
13792
+ };
13793
+ case "conflict": return {
13794
+ icon: AlertCircle,
13795
+ label: "Refresh required",
13796
+ className: "dy-text-rose-700 dy-bg-rose-50 dy-border-rose-200"
13797
+ };
13798
+ default: return {
13799
+ icon: AlertCircle,
13800
+ label: "Save failed",
13801
+ className: "dy-text-rose-700 dy-bg-rose-50 dy-border-rose-200"
13802
+ };
13803
+ }
13804
+ })();
13805
+ const Icon = presentation.icon;
13806
+ return /* @__PURE__ */ jsxs("div", {
13807
+ className: cn("dy-inline-flex dy-items-center dy-gap-1.5 dy-rounded-full dy-border dy-px-2.5 dy-py-1 dy-text-[11px] dy-font-semibold", presentation.className),
13808
+ children: [/* @__PURE__ */ jsx(Icon, { className: cn("dy-h-3.5 dy-w-3.5", state === "saving" && "dy-animate-spin") }), /* @__PURE__ */ jsx("span", { children: presentation.label })]
12728
13809
  });
12729
13810
  }
12730
13811
  /**
@@ -12796,6 +13877,11 @@ function EditEntryPage() {
12796
13877
  const [mobilePreview, setMobilePreview] = useState(false);
12797
13878
  const [isDirty, setIsDirty] = useState(false);
12798
13879
  const [previewData, setPreviewData] = useState(null);
13880
+ const [workflowAutosaveState, setWorkflowAutosaveState] = useState("idle");
13881
+ const [workflowTransitionPending, setWorkflowTransitionPending] = useState(false);
13882
+ const [compareSheetOpen, setCompareSheetOpen] = useState(false);
13883
+ const formEngineRef = useRef(null);
13884
+ const draftSavePromiseRef = useRef(null);
12799
13885
  const isEdit = !!id;
12800
13886
  const [isConfiguringView, setIsConfiguringView] = useState(false);
12801
13887
  const { data: schemas } = useQuery({
@@ -12878,16 +13964,6 @@ function EditEntryPage() {
12878
13964
  window.addEventListener("beforeunload", handleBeforeUnload);
12879
13965
  return () => window.removeEventListener("beforeunload", handleBeforeUnload);
12880
13966
  }, [isDirty]);
12881
- useEffect(() => {
12882
- const handleSave = (e) => {
12883
- if ((e.metaKey || e.ctrlKey) && e.key === "s") {
12884
- e.preventDefault();
12885
- document.getElementById("dyrected-form-submit")?.click();
12886
- }
12887
- };
12888
- window.addEventListener("keydown", handleSave);
12889
- return () => window.removeEventListener("keydown", handleSave);
12890
- }, []);
12891
13967
  const schemaSlug = schema?.slug;
12892
13968
  const schemaPreviewUrl = schema?.admin?.previewUrl;
12893
13969
  const syncShowPreview = useState(() => (next) => {
@@ -12901,18 +13977,6 @@ function EditEntryPage() {
12901
13977
  schemaPreviewUrl,
12902
13978
  syncShowPreview
12903
13979
  ]);
12904
- const hasWorkflow = !!schema?.workflow;
12905
- const syncShowWorkflow = useState(() => (next) => {
12906
- setActiveTab((prev) => next ? "workflow" : prev === "workflow" ? "edit" : prev);
12907
- })[0];
12908
- useEffect(() => {
12909
- if (!hasWorkflow || schemaPreviewUrl) return;
12910
- syncShowWorkflow(true);
12911
- }, [
12912
- hasWorkflow,
12913
- schemaPreviewUrl,
12914
- syncShowWorkflow
12915
- ]);
12916
13980
  const { data: entry, isLoading: isEntryLoading } = useQuery({
12917
13981
  queryKey: [
12918
13982
  "entry",
@@ -12954,7 +14018,7 @@ function EditEntryPage() {
12954
14018
  const isSelf = !!user && !!id && (user.id === id || user.sub === id);
12955
14019
  const passwordChangeMode = schema?.auth ? isSelf ? "self" : isAdminUser ? "admin" : null : null;
12956
14020
  const saveMutation = useMutation({
12957
- mutationFn: async (data) => {
14021
+ mutationFn: async ({ data }) => {
12958
14022
  const { oldPassword, newPassword, confirmPassword, ...rest } = data;
12959
14023
  const results = {};
12960
14024
  if (isEdit) {
@@ -12970,7 +14034,7 @@ function EditEntryPage() {
12970
14034
  } else results.doc = await client.collection(slug).create(rest);
12971
14035
  return results;
12972
14036
  },
12973
- onSuccess: (results) => {
14037
+ onSuccess: (results, variables) => {
12974
14038
  setIsDirty(false);
12975
14039
  queryClient.invalidateQueries({ queryKey: ["collection", slug] });
12976
14040
  if (isEdit) queryClient.invalidateQueries({ queryKey: [
@@ -12978,45 +14042,26 @@ function EditEntryPage() {
12978
14042
  slug,
12979
14043
  id
12980
14044
  ] });
12981
- toast.success(isEdit ? "Entry updated successfully" : "Entry created successfully", { description: `${schema?.labels?.singular || schema?.slug} has been saved.` });
12982
- if (results.passwordChanged) toast.success("Password changed successfully");
14045
+ if (variables.mode === "manual") toast.success(isEdit ? "Entry updated successfully" : "Entry created successfully", { description: `${schema?.labels?.singular || schema?.slug} has been saved.` });
14046
+ if (results.passwordChanged && variables.mode === "manual") toast.success("Password changed successfully");
12983
14047
  const doc = results.doc;
12984
14048
  if (!isEdit && doc?.id) navigate(`/collections/${slug}/edit/${doc.id}`, { replace: true });
12985
14049
  },
12986
- onError: (error) => {
14050
+ onError: (error, variables) => {
14051
+ if (variables.mode === "autosave") return;
12987
14052
  toast.error("Failed to save entry", { description: error.message || "An unexpected error occurred." });
12988
14053
  }
12989
14054
  });
12990
- const [sendingReset, setSendingReset] = useState(false);
12991
- const handleSendResetLink = async () => {
12992
- if (!entry?.email) return;
12993
- setSendingReset(true);
12994
- try {
12995
- await client.collection(slug).sendResetLink(entry.email);
12996
- toast.success("Reset link sent", { description: `A password reset email has been sent to ${entry.email}.` });
12997
- } catch (err) {
12998
- toast.error("Failed to send reset link", { description: err instanceof Error ? err.message : "An unexpected error occurred." });
12999
- } finally {
13000
- setSendingReset(false);
13001
- }
13002
- };
13003
- if (!schema) return /* @__PURE__ */ jsx("div", { children: "Collection not found" });
13004
- if (isEdit && isEntryLoading) return /* @__PURE__ */ jsx("div", { children: "Loading entry..." });
13005
- const workflowConfig = schema.workflow ?? null;
14055
+ const workflowConfig = schema?.workflow ?? null;
13006
14056
  const workflowMeta = isEdit && entry ? entry._workflow ?? null : null;
13007
14057
  const hasStatus = schema?.fields.some((f) => f.name === "status");
13008
- const currentStatus = entry?.status || "draft";
13009
- const workflowState = workflowConfig && workflowMeta ? workflowConfig.states?.find((s) => s.name === workflowMeta.state) : null;
13010
- const isPublished = workflowState ? !!workflowState.published : currentStatus === "published";
13011
- const statusBadgeLabel = workflowState ? workflowState.label || workflowState.name : currentStatus === "published" ? "Published" : currentStatus === "draft" ? "Draft" : String(currentStatus);
13012
- const workflowBadgePresentation = workflowState ? getWorkflowBadgePresentation(workflowState.color) : {
13013
- className: isPublished ? WORKFLOW_BADGE_COLORS.success : WORKFLOW_BADGE_COLORS.warning,
13014
- style: void 0
13015
- };
14058
+ const workflowState = resolveWorkflowState(workflowConfig, workflowMeta);
14059
+ const publishingStatus = resolvePublishingStatus(schema, entry ?? {});
14060
+ const workflowStagePresentation = workflowState && publishingStatus?.workflowStateLabel ? getWorkflowBadgePresentation(workflowState.color) : null;
13016
14061
  const showStatusBadge = workflowConfig ? !!workflowMeta : hasStatus;
13017
14062
  const siteUrl = getSiteUrl(schemas?.admin?.siteUrl);
13018
- const previewUrl = resolvePreviewUrl(schema.admin?.previewUrl, previewData || entry, siteUrl);
13019
- const readAccess = schema.access?.read;
14063
+ const previewUrl = resolvePreviewUrl(schema?.admin?.previewUrl, previewData || entry, siteUrl);
14064
+ const readAccess = (schema?.access)?.read;
13020
14065
  let canRead = true;
13021
14066
  if (readAccess === false) canRead = false;
13022
14067
  else if (typeof readAccess === "string") try {
@@ -13027,27 +14072,7 @@ function EditEntryPage() {
13027
14072
  } catch (e) {
13028
14073
  console.warn("Read access eval failed:", e);
13029
14074
  }
13030
- if (!canRead) return /* @__PURE__ */ jsx("div", {
13031
- className: "dy-flex dy-items-center dy-justify-center dy-h-[calc(100vh-200px)]",
13032
- children: /* @__PURE__ */ jsxs("div", {
13033
- className: "dy-text-center dy-space-y-3",
13034
- children: [
13035
- /* @__PURE__ */ jsx("div", {
13036
- className: "dy-p-3 dy-bg-destructive/10 dy-text-destructive dy-rounded-full dy-w-12 dy-h-12 dy-mx-auto dy-flex dy-items-center dy-justify-center",
13037
- children: /* @__PURE__ */ jsx(Archive, { className: "dy-h-6 dy-w-6" })
13038
- }),
13039
- /* @__PURE__ */ jsx("h3", {
13040
- className: "dy-text-lg dy-font-bold",
13041
- children: "Access Denied"
13042
- }),
13043
- /* @__PURE__ */ jsx("p", {
13044
- className: "dy-text-sm dy-text-muted-foreground",
13045
- children: "You do not have permission to view this entry."
13046
- })
13047
- ]
13048
- })
13049
- });
13050
- const createAccess = schema.access?.create;
14075
+ const createAccess = (schema?.access)?.create;
13051
14076
  let canCreate = true;
13052
14077
  if (createAccess === false) canCreate = false;
13053
14078
  else if (typeof createAccess === "string") try {
@@ -13055,7 +14080,7 @@ function EditEntryPage() {
13055
14080
  } catch (e) {
13056
14081
  console.warn("Create access eval failed:", e);
13057
14082
  }
13058
- const updateAccess = schema.access?.update;
14083
+ const updateAccess = (schema?.access)?.update;
13059
14084
  let canUpdate = true;
13060
14085
  if (updateAccess === false) canUpdate = false;
13061
14086
  else if (typeof updateAccess === "string") try {
@@ -13066,7 +14091,7 @@ function EditEntryPage() {
13066
14091
  } catch (e) {
13067
14092
  console.warn("Update access eval failed:", e);
13068
14093
  }
13069
- const auditAccess = schema.access?.readAudit ?? readAccess;
14094
+ const auditAccess = (schema?.access)?.readAudit ?? readAccess;
13070
14095
  let canReadAudit = true;
13071
14096
  if (auditAccess === false) canReadAudit = false;
13072
14097
  else if (typeof auditAccess === "string") try {
@@ -13078,11 +14103,158 @@ function EditEntryPage() {
13078
14103
  canReadAudit = false;
13079
14104
  }
13080
14105
  const workflowAvailable = !!(workflowConfig && isEdit && workflowMeta);
14106
+ const liveSnapshot = entry?.__published && typeof entry.__published === "object" ? entry.__published : null;
14107
+ const compareToLiveEnabled = workflowAvailable && !!liveSnapshot;
14108
+ const compareToLiveReason = !workflowAvailable ? "Workflow is not enabled for this entry." : liveSnapshot ? null : "No live snapshot exists yet. Publish this entry once to compare future draft changes.";
14109
+ const draftLiveComparison = useMemo(() => buildDraftLiveComparison({
14110
+ fields: orderedFields,
14111
+ draft: previewData || entry || null,
14112
+ live: liveSnapshot
14113
+ }), [
14114
+ entry,
14115
+ liveSnapshot,
14116
+ orderedFields,
14117
+ previewData
14118
+ ]);
14119
+ const workflowAutosaveConfig = resolveWorkflowAutosaveSettings(schema);
14120
+ const workflowAutosaveEnabled = Boolean(isEdit && workflowAutosaveConfig.enabled && (schema?.workflow || schema?.drafts) && canUpdate);
14121
+ const documentLabel = useMemo(() => entry ? resolveDocumentTitle({
14122
+ entry,
14123
+ collection: schema,
14124
+ collections: schemas?.collections
14125
+ }) : void 0, [
14126
+ entry,
14127
+ schema,
14128
+ schemas?.collections
14129
+ ]);
14130
+ const persistDraft = useCallback((data, mode) => {
14131
+ const promise = saveMutation.mutateAsync({
14132
+ data,
14133
+ mode
14134
+ });
14135
+ draftSavePromiseRef.current = promise.finally(() => {
14136
+ if (draftSavePromiseRef.current === promise) draftSavePromiseRef.current = null;
14137
+ });
14138
+ return promise;
14139
+ }, [saveMutation]);
14140
+ const handleManualSave = useCallback((data) => {
14141
+ return persistDraft(data, "manual");
14142
+ }, [persistDraft]);
14143
+ const handleWorkflowAutosave = useCallback(async (data) => {
14144
+ await persistDraft(data, "autosave");
14145
+ }, [persistDraft]);
14146
+ const autosaveConfig = useMemo(() => {
14147
+ if (!workflowAutosaveEnabled) return void 0;
14148
+ return {
14149
+ enabled: true,
14150
+ delayMs: workflowAutosaveConfig.delayMs,
14151
+ onSave: handleWorkflowAutosave,
14152
+ onStatusChange: setWorkflowAutosaveState
14153
+ };
14154
+ }, [
14155
+ handleWorkflowAutosave,
14156
+ workflowAutosaveConfig.delayMs,
14157
+ workflowAutosaveEnabled
14158
+ ]);
14159
+ const showWorkflowAutosaveStatus = workflowAutosaveEnabled && activeTab === "edit";
14160
+ const showManualSaveChrome = !workflowAutosaveEnabled;
13081
14161
  const showLivePreview = activeTab === "edit" && showPreview && !!previewUrl;
14162
+ const resolveTransitionContextFromDoc = useCallback((doc) => {
14163
+ const resolvedDocumentId = String(doc?.id ?? id ?? "");
14164
+ const resolvedWorkflowMeta = doc?._workflow ?? workflowMeta ?? null;
14165
+ const resolvedDocumentLabel = doc ? resolveDocumentTitle({
14166
+ entry: doc,
14167
+ collection: schema,
14168
+ collections: schemas?.collections
14169
+ }) : documentLabel;
14170
+ return {
14171
+ documentIds: resolvedDocumentId ? [resolvedDocumentId] : [],
14172
+ documentLabels: resolvedDocumentId ? { [resolvedDocumentId]: resolvedDocumentLabel } : void 0,
14173
+ expectedRevisions: resolvedDocumentId && typeof resolvedWorkflowMeta?.revision === "number" ? { [resolvedDocumentId]: resolvedWorkflowMeta.revision } : void 0,
14174
+ invalidateQueryKeys: resolvedDocumentId ? [
14175
+ [
14176
+ "entry",
14177
+ slug,
14178
+ resolvedDocumentId
14179
+ ],
14180
+ ["collection", slug],
14181
+ [
14182
+ "workflow-history",
14183
+ slug,
14184
+ resolvedDocumentId
14185
+ ]
14186
+ ] : [["collection", slug]]
14187
+ };
14188
+ }, [
14189
+ documentLabel,
14190
+ id,
14191
+ schema,
14192
+ schemas?.collections,
14193
+ slug,
14194
+ workflowMeta
14195
+ ]);
14196
+ const saveCurrentDraftNow = useCallback(async (mode = "manual") => {
14197
+ if (draftSavePromiseRef.current) await draftSavePromiseRef.current;
14198
+ if (!formEngineRef.current) throw new Error("The editor is not ready yet.");
14199
+ const result = await formEngineRef.current.submitCurrentDraft();
14200
+ const savedDoc = result?.doc ?? null;
14201
+ return {
14202
+ result,
14203
+ context: resolveTransitionContextFromDoc(savedDoc),
14204
+ mode
14205
+ };
14206
+ }, [resolveTransitionContextFromDoc]);
14207
+ const prepareWorkflowTransition = useCallback(async (_transition) => {
14208
+ if (draftSavePromiseRef.current) await draftSavePromiseRef.current;
14209
+ if (formEngineRef.current?.isDirty()) {
14210
+ const { context } = await saveCurrentDraftNow("transition");
14211
+ return context;
14212
+ }
14213
+ return resolveTransitionContextFromDoc(entry ?? null);
14214
+ }, [
14215
+ entry,
14216
+ resolveTransitionContextFromDoc,
14217
+ saveCurrentDraftNow
14218
+ ]);
14219
+ const [sendingReset, setSendingReset] = useState(false);
14220
+ const handleSendResetLink = async () => {
14221
+ if (!entry?.email) return;
14222
+ setSendingReset(true);
14223
+ try {
14224
+ await client.collection(slug).sendResetLink(entry.email);
14225
+ toast.success("Reset link sent", { description: `A password reset email has been sent to ${entry.email}.` });
14226
+ } catch (err) {
14227
+ toast.error("Failed to send reset link", { description: err instanceof Error ? err.message : "An unexpected error occurred." });
14228
+ } finally {
14229
+ setSendingReset(false);
14230
+ }
14231
+ };
14232
+ if (!schema) return /* @__PURE__ */ jsx("div", { children: "Collection not found" });
14233
+ if (isEdit && isEntryLoading) return /* @__PURE__ */ jsx("div", { children: "Loading entry..." });
14234
+ if (!canRead) return /* @__PURE__ */ jsx("div", {
14235
+ className: "dy-flex dy-items-center dy-justify-center dy-h-[calc(100vh-200px)]",
14236
+ children: /* @__PURE__ */ jsxs("div", {
14237
+ className: "dy-text-center dy-space-y-3",
14238
+ children: [
14239
+ /* @__PURE__ */ jsx("div", {
14240
+ className: "dy-p-3 dy-bg-destructive/10 dy-text-destructive dy-rounded-full dy-w-12 dy-h-12 dy-mx-auto dy-flex dy-items-center dy-justify-center",
14241
+ children: /* @__PURE__ */ jsx(Archive, { className: "dy-h-6 dy-w-6" })
14242
+ }),
14243
+ /* @__PURE__ */ jsx("h3", {
14244
+ className: "dy-text-lg dy-font-bold",
14245
+ children: "Access Denied"
14246
+ }),
14247
+ /* @__PURE__ */ jsx("p", {
14248
+ className: "dy-text-sm dy-text-muted-foreground",
14249
+ children: "You do not have permission to view this entry."
14250
+ })
14251
+ ]
14252
+ })
14253
+ });
13082
14254
  const docsToDisplay = debouncedSearchQuery ? siblingEntries?.docs || [] : siblingEntries?.docs?.filter((d) => String(d.id) !== id).slice(0, 4) || [];
13083
- return /* @__PURE__ */ jsx(NestedEditorProvider, {
14255
+ return /* @__PURE__ */ jsxs(NestedEditorProvider, {
13084
14256
  drillInEnabled: !!showLivePreview,
13085
- children: /* @__PURE__ */ jsxs("div", {
14257
+ children: [/* @__PURE__ */ jsxs("div", {
13086
14258
  className: cn("dy-flex dy-flex-col dy--mt-6 dy--mb-6 dy--mx-4 lg:dy--mt-10 lg:dy--mb-10 lg:dy--mx-6", showLivePreview ? "dy-h-screen" : ""),
13087
14259
  children: [/* @__PURE__ */ jsxs("div", {
13088
14260
  className: "dy-flex dy-flex-wrap dy-shrink-0 dy-items-center dy-gap-2 dy-border-b dy-border-border/50 dy-bg-background dy-px-3 dy-py-2",
@@ -13147,12 +14319,12 @@ function EditEntryPage() {
13147
14319
  })] }) : /* @__PURE__ */ jsxs("h1", {
13148
14320
  className: "dy-text-base dy-font-serif dy-font-bold dy-tracking-tight dy-text-foreground dy-truncate",
13149
14321
  children: ["New ", schema?.labels?.singular || schema?.slug]
13150
- }), showStatusBadge && /* @__PURE__ */ jsx(Badge, {
13151
- className: cn("dy-px-2 dy-py-0 dy-rounded-full dy-text-[10px] dy-font-bold dy-uppercase dy-tracking-wider dy-shrink-0", workflowBadgePresentation.className),
13152
- style: workflowBadgePresentation.style,
14322
+ }), showStatusBadge && /* @__PURE__ */ jsxs(Fragment, { children: [publishingStatus?.workflowStateLabel && workflowStagePresentation && /* @__PURE__ */ jsx(Badge, {
14323
+ className: cn("dy-px-2 dy-py-0 dy-rounded-full dy-text-[10px] dy-font-semibold dy-tracking-wide dy-shrink-0", workflowStagePresentation.className),
14324
+ style: workflowStagePresentation.style,
13153
14325
  variant: "outline",
13154
- children: statusBadgeLabel
13155
- })]
14326
+ children: publishingStatus.workflowStateLabel
14327
+ }), showWorkflowAutosaveStatus && /* @__PURE__ */ jsx(DraftSaveStatusBadge, { state: workflowAutosaveState })] })]
13156
14328
  }),
13157
14329
  /* @__PURE__ */ jsxs("div", {
13158
14330
  className: "dy-ml-auto dy-flex dy-items-center dy-gap-1",
@@ -13180,9 +14352,9 @@ function EditEntryPage() {
13180
14352
  }),
13181
14353
  workflowAvailable && /* @__PURE__ */ jsx(HeaderAction, {
13182
14354
  icon: Workflow,
13183
- label: "Workflow",
14355
+ label: "Workflow details",
13184
14356
  active: activeTab === "workflow",
13185
- title: activeTab === "workflow" ? "Hide workflow transitions" : "Show workflow transitions",
14357
+ title: activeTab === "workflow" ? "Hide workflow details" : "Show workflow details",
13186
14358
  onClick: () => setActiveTab((tab) => tab === "workflow" ? "edit" : "workflow")
13187
14359
  }),
13188
14360
  schema?.audit && isEdit && canReadAudit && /* @__PURE__ */ jsx(HeaderAction, {
@@ -13250,12 +14422,35 @@ function EditEntryPage() {
13250
14422
  })]
13251
14423
  })] })]
13252
14424
  })] }),
13253
- (isEdit ? canUpdate : canCreate) && activeTab === "edit" && /* @__PURE__ */ jsxs("div", {
14425
+ (isEdit ? canUpdate : canCreate) && activeTab === "edit" && /* @__PURE__ */ jsxs(Fragment, { children: [workflowAvailable && /* @__PURE__ */ jsx(WorkflowTransitionSplitButton, {
14426
+ collection: slug,
14427
+ documentId: id,
14428
+ documentLabel,
14429
+ workflowConfig,
14430
+ workflowMeta,
14431
+ onPendingChange: setWorkflowTransitionPending,
14432
+ onSaveDraft: () => saveCurrentDraftNow("manual").then(() => void 0),
14433
+ saveDraftPending: saveMutation.isPending,
14434
+ prepareTransition: prepareWorkflowTransition,
14435
+ invalidateQueryKeys: [
14436
+ [
14437
+ "entry",
14438
+ slug,
14439
+ id
14440
+ ],
14441
+ ["collection", slug],
14442
+ [
14443
+ "workflow-history",
14444
+ slug,
14445
+ id
14446
+ ]
14447
+ ]
14448
+ }), showManualSaveChrome && /* @__PURE__ */ jsxs("div", {
13254
14449
  className: "dy-hidden md:dy-flex dy-items-center",
13255
14450
  children: [/* @__PURE__ */ jsx("div", { className: "dy-mx-1 dy-h-6 dy-w-px dy-bg-border/60" }), /* @__PURE__ */ jsx(Button, {
13256
14451
  size: "sm",
13257
14452
  className: "dy-h-9 dy-rounded-lg dy-px-4 dy-font-bold dy-bg-primary dy-text-primary-foreground hover:dy-bg-primary/90 dy-shadow-sm dy-shrink-0",
13258
- onClick: () => document.getElementById("dyrected-form-submit")?.click(),
14453
+ onClick: () => void saveCurrentDraftNow("manual"),
13259
14454
  disabled: saveMutation.isPending,
13260
14455
  title: isEdit ? "Save Changes (⌘S)" : "Create Entry (⌘S)",
13261
14456
  children: saveMutation.isPending ? /* @__PURE__ */ jsxs("span", {
@@ -13266,7 +14461,7 @@ function EditEntryPage() {
13266
14461
  children: [/* @__PURE__ */ jsx(Save, { className: "dy-h-3.5 dy-w-3.5" }), isEdit ? "Save" : "Create"]
13267
14462
  })
13268
14463
  })]
13269
- })
14464
+ })] })
13270
14465
  ]
13271
14466
  })
13272
14467
  ]
@@ -13298,7 +14493,13 @@ function EditEntryPage() {
13298
14493
  collection: slug,
13299
14494
  documentId: id,
13300
14495
  workflowMeta,
13301
- workflowConfig
14496
+ workflowConfig,
14497
+ compareToLiveEnabled,
14498
+ compareToLiveReason,
14499
+ onCompareToLive: () => setCompareSheetOpen(true),
14500
+ onSaveDraft: () => saveCurrentDraftNow("manual").then(() => void 0),
14501
+ saveDraftPending: saveMutation.isPending,
14502
+ prepareTransition: prepareWorkflowTransition
13302
14503
  })
13303
14504
  }),
13304
14505
  activeTab === "audit" && /* @__PURE__ */ jsx("div", {
@@ -13475,19 +14676,21 @@ function EditEntryPage() {
13475
14676
  queryParamsDefaults[key] = value;
13476
14677
  });
13477
14678
  return /* @__PURE__ */ jsx(FormEngine, {
14679
+ ref: formEngineRef,
13478
14680
  collection: slug,
13479
14681
  fields: orderedFields,
13480
14682
  defaultValues: isEdit ? entry : {
13481
14683
  ...queryParamsDefaults,
13482
14684
  ...entry
13483
14685
  },
13484
- onSubmit: (data) => saveMutation.mutate(data),
14686
+ onSubmit: handleManualSave,
14687
+ autosave: autosaveConfig,
13485
14688
  onDataChange: (newData) => setPreviewData({
13486
14689
  ...entry,
13487
14690
  ...newData
13488
14691
  }),
13489
14692
  onChange: (dirty) => setIsDirty(dirty),
13490
- isLoading: saveMutation.isPending || isPreferenceLoading,
14693
+ isLoading: saveMutation.isPending || workflowTransitionPending || isPreferenceLoading,
13491
14694
  submitLabel: isEdit ? "Save Changes" : "Create Entry",
13492
14695
  hideSubmit: true,
13493
14696
  readOnly: isEdit ? !canUpdate : !canCreate,
@@ -13502,7 +14705,7 @@ function EditEntryPage() {
13502
14705
  form: "dyrected-edit-form",
13503
14706
  className: "dy-hidden"
13504
14707
  }),
13505
- (isDirty || !isEdit) && (isEdit ? canUpdate : canCreate) && /* @__PURE__ */ jsx("div", {
14708
+ showManualSaveChrome && (isDirty || !isEdit) && (isEdit ? canUpdate : canCreate) && /* @__PURE__ */ jsx("div", {
13506
14709
  className: "dy-hidden md:dy-block dy-sticky dy-bottom-0 dy-left-0 dy-right-0 dy-z-20 dy-pointer-events-none",
13507
14710
  children: /* @__PURE__ */ jsx("div", {
13508
14711
  className: "dy-pointer-events-auto dy-mx-auto dy-max-w-2xl dy-px-4 dy-pb-4",
@@ -13514,7 +14717,7 @@ function EditEntryPage() {
13514
14717
  }), /* @__PURE__ */ jsx(Button, {
13515
14718
  size: "sm",
13516
14719
  className: "dy-h-9 dy-px-5 dy-rounded-xl dy-font-bold dy-bg-primary dy-text-primary-foreground hover:dy-bg-primary/90 dy-shadow-sm dy-shrink-0",
13517
- onClick: () => document.getElementById("dyrected-form-submit")?.click(),
14720
+ onClick: () => void saveCurrentDraftNow("manual"),
13518
14721
  disabled: saveMutation.isPending,
13519
14722
  children: saveMutation.isPending ? /* @__PURE__ */ jsxs("div", {
13520
14723
  className: "dy-flex dy-items-center dy-gap-2",
@@ -13527,11 +14730,11 @@ function EditEntryPage() {
13527
14730
  })
13528
14731
  })
13529
14732
  }),
13530
- (isEdit ? canUpdate : canCreate) && /* @__PURE__ */ jsx("div", {
14733
+ showManualSaveChrome && (isEdit ? canUpdate : canCreate) && /* @__PURE__ */ jsx("div", {
13531
14734
  className: "md:dy-hidden dy-sticky dy-bottom-0 dy-left-0 dy-right-0 dy-z-20 dy-border-t dy-border-border dy-bg-background/95 dy-backdrop-blur-sm dy-px-4 dy-py-3",
13532
14735
  children: /* @__PURE__ */ jsx(Button, {
13533
14736
  className: "dy-w-full dy-h-11 dy-rounded-xl dy-font-bold dy-bg-primary dy-text-primary-foreground hover:dy-bg-primary/90 dy-shadow-sm",
13534
- onClick: () => document.getElementById("dyrected-form-submit")?.click(),
14737
+ onClick: () => void saveCurrentDraftNow("manual"),
13535
14738
  disabled: saveMutation.isPending || !(isDirty || !isEdit),
13536
14739
  children: saveMutation.isPending ? /* @__PURE__ */ jsxs("span", {
13537
14740
  className: "dy-flex dy-items-center dy-gap-2",
@@ -13548,7 +14751,11 @@ function EditEntryPage() {
13548
14751
  })
13549
14752
  })]
13550
14753
  })]
13551
- }, id || "new")
14754
+ }, id || "new"), workflowAvailable && compareToLiveEnabled && /* @__PURE__ */ jsx(DraftLiveCompareSheet, {
14755
+ open: compareSheetOpen,
14756
+ onOpenChange: setCompareSheetOpen,
14757
+ comparison: draftLiveComparison
14758
+ })]
13552
14759
  });
13553
14760
  }
13554
14761
  function AuditPanel({ collection, documentId }) {
@@ -13679,44 +14886,6 @@ function AuditPanel({ collection, documentId }) {
13679
14886
  });
13680
14887
  }
13681
14888
  //#endregion
13682
- //#region src/components/ui/card.tsx
13683
- var Card = React$1.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", {
13684
- ref,
13685
- className: cn("dy-rounded-lg dy-border dy-bg-card dy-text-card-foreground dy-shadow-sm", className),
13686
- ...props
13687
- }));
13688
- Card.displayName = "Card";
13689
- var CardHeader = React$1.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", {
13690
- ref,
13691
- className: cn("dy-flex dy-flex-col dy-space-y-1.5 dy-p-6", className),
13692
- ...props
13693
- }));
13694
- CardHeader.displayName = "CardHeader";
13695
- var CardTitle = React$1.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", {
13696
- ref,
13697
- className: cn("dy-text-2xl dy-font-semibold dy-leading-none dy-tracking-tight", className),
13698
- ...props
13699
- }));
13700
- CardTitle.displayName = "CardTitle";
13701
- var CardDescription = React$1.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", {
13702
- ref,
13703
- className: cn("dy-text-sm dy-text-muted-foreground", className),
13704
- ...props
13705
- }));
13706
- CardDescription.displayName = "CardDescription";
13707
- var CardContent = React$1.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", {
13708
- ref,
13709
- className: cn("dy-p-6 dy-pt-0", className),
13710
- ...props
13711
- }));
13712
- CardContent.displayName = "CardContent";
13713
- var CardFooter = React$1.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", {
13714
- ref,
13715
- className: cn("dy-flex dy-items-center dy-p-6 dy-pt-0", className),
13716
- ...props
13717
- }));
13718
- CardFooter.displayName = "CardFooter";
13719
- //#endregion
13720
14889
  //#region src/components/ui/aspect-ratio.tsx
13721
14890
  var AspectRatio = AspectRatioPrimitive.Root;
13722
14891
  //#endregion
@@ -14996,7 +16165,7 @@ function SetupPromptUI({ config }) {
14996
16165
  setCopied(true);
14997
16166
  window.setTimeout(() => setCopied(false), 1800);
14998
16167
  }
14999
- const currentVersion = "2.5.65";
16168
+ const currentVersion = "2.6.0";
15000
16169
  const [latestVersion, setLatestVersion] = useState(() => {
15001
16170
  if (typeof window === "undefined") return null;
15002
16171
  return localStorage.getItem("dyrected_latest_release");