@agent-native/core 0.181.0 → 0.181.1-nightly-20260916144018

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/clips/app/components/player/create-account-dialog.tsx +2 -1
  3. package/corpus/templates/clips/changelog/2026-09-15-google-sign-up-now-opens-reliably-in-embedded-browsers.md +6 -0
  4. package/corpus/templates/content/actions/resolve-preview-document-draft.ts +320 -60
  5. package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +3 -1
  6. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +349 -104
  7. package/corpus/templates/content/app/components/editor/DocumentReconcileRecovery.tsx +109 -0
  8. package/corpus/templates/content/app/components/editor/PageDraftRecovery.tsx +31 -153
  9. package/corpus/templates/content/app/components/editor/RecoveryComparison.tsx +230 -0
  10. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +46 -19
  11. package/corpus/templates/content/app/components/editor/editor-draft-save.ts +13 -0
  12. package/corpus/templates/content/app/components/editor/useDocumentReconcileRecovery.ts +248 -0
  13. package/corpus/templates/content/app/components/sidebar/NotionButton.tsx +2 -1
  14. package/corpus/templates/content/app/i18n-data.ts +13 -0
  15. package/corpus/templates/content/changelog/2026-09-15-oauth-connections-now-open-reliably-in-embedded-browsers.md +6 -0
  16. package/corpus/templates/content/docs/solutions/2026-09-02-content-suggested-edits-parity-shape.md +84 -2
  17. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +27 -7
  18. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +45 -15
  19. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +35 -8
  20. package/corpus/templates/design/app/components/design/design-canvas/element-payload.ts +14 -0
  21. package/corpus/templates/design/app/pages/DesignEditor.tsx +18 -5
  22. package/corpus/templates/design/app/pages/design-editor/commands/record-pending-live-structure-edit.ts +9 -4
  23. package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-structure-change.ts +3 -0
  24. package/corpus/templates/design/app/pages/design-editor/commands/start-sidebar-resize.ts +7 -2
  25. package/corpus/templates/design/app/pages/design-editor/commands/visual-structure-change.ts +2 -0
  26. package/corpus/templates/design/app/pages/design-editor/pending-edits.ts +4 -0
  27. package/corpus/templates/design/app/pages/design-editor/pending-structure-verification.ts +197 -147
  28. package/corpus/templates/design/changelog/2026-09-16-design-editor-agent-chat-uses-shared-conversation-tabs-and-s.md +6 -0
  29. package/corpus/templates/design/changelog/2026-09-16-visual-edits-stay-applied-after-replacements.md +5 -0
  30. package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +2 -1
  31. package/corpus/templates/mail/changelog/2026-09-15-google-connections-now-open-reliably-in-embedded-browsers.md +6 -0
  32. package/dist/client/AgentPanel.d.ts +1 -1
  33. package/dist/client/AgentPanel.js +4 -4
  34. package/dist/client/auth/AuthPage.js +6 -1
  35. package/dist/client/oauth-popup.d.ts +7 -0
  36. package/dist/client/oauth-popup.js +14 -0
  37. package/dist/client/ocean/OceanBackground.d.ts +3 -1
  38. package/dist/client/ocean/OceanBackground.js +4 -3
  39. package/dist/client/resources/mcp-integration-catalog.js +7 -3
  40. package/dist/client/settings/useBuilderStatus.js +4 -3
  41. package/dist/collab/routes.d.ts +1 -1
  42. package/dist/collab/struct-routes.d.ts +1 -1
  43. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  44. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  45. package/dist/notifications/routes.d.ts +3 -3
  46. package/dist/observability/routes.d.ts +3 -3
  47. package/dist/org/app-roles.d.ts +8 -0
  48. package/dist/org/app-roles.js +7 -0
  49. package/dist/org/index.d.ts +1 -1
  50. package/dist/org/index.js +1 -1
  51. package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
  52. package/dist/secrets/routes.d.ts +6 -6
  53. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  54. package/dist/server/auth.js +3 -0
  55. package/dist/server/core-routes-plugin.d.ts +3 -0
  56. package/dist/server/core-routes-plugin.js +17 -0
  57. package/dist/server/realtime-token.d.ts +1 -1
  58. package/package.json +5 -4
@@ -0,0 +1,109 @@
1
+ import { useT } from "@agent-native/core/client/i18n";
2
+ import { useState } from "react";
3
+
4
+ import { Button } from "@/components/ui/button";
5
+ import {
6
+ Dialog,
7
+ DialogContent,
8
+ DialogTitle,
9
+ DialogTrigger,
10
+ } from "@/components/ui/dialog";
11
+
12
+ import { RecoveryComparison } from "./RecoveryComparison";
13
+ import type {
14
+ DocumentReconcileRecoveryState,
15
+ ReconcileSaveBase,
16
+ } from "./useDocumentReconcileRecovery";
17
+
18
+ export function DocumentReconcileRecovery({
19
+ state,
20
+ server,
21
+ canEdit,
22
+ onKeepMine,
23
+ onUseSaved,
24
+ onSaveSeparately,
25
+ onCopy,
26
+ onClose,
27
+ }: {
28
+ state: DocumentReconcileRecoveryState;
29
+ server: ReconcileSaveBase;
30
+ canEdit: boolean;
31
+ onKeepMine: (base: ReconcileSaveBase) => Promise<boolean>;
32
+ onUseSaved: (base: ReconcileSaveBase) => Promise<boolean>;
33
+ onSaveSeparately: (base: ReconcileSaveBase) => Promise<boolean>;
34
+ onCopy: () => Promise<void>;
35
+ onClose: () => void;
36
+ }) {
37
+ const t = useT();
38
+ const [reviewed, setReviewed] = useState<ReconcileSaveBase | null>(null);
39
+ const stale =
40
+ reviewed !== null &&
41
+ (reviewed.updatedAt !== server.updatedAt ||
42
+ reviewed.revision !== server.revision ||
43
+ reviewed.title !== server.title ||
44
+ reviewed.content !== server.content);
45
+ const message = state.saving
46
+ ? t("editor.reconcileSaving")
47
+ : state.reason === "conflict"
48
+ ? t("editor.reconcileConflict")
49
+ : state.reason === "save-failed"
50
+ ? t("editor.reconcileSaveFailed")
51
+ : t("editor.reconcileFailed");
52
+
53
+ async function finish(action: (base: ReconcileSaveBase) => Promise<boolean>) {
54
+ if (!reviewed || stale || state.saving) return;
55
+ if (await action(reviewed)) setReviewed(null);
56
+ }
57
+
58
+ return (
59
+ <Dialog
60
+ open={reviewed !== null}
61
+ onOpenChange={(open) => {
62
+ if (open) return;
63
+ if (state.saving) return;
64
+ setReviewed(null);
65
+ onClose();
66
+ }}
67
+ >
68
+ <div
69
+ className="flex flex-wrap items-center gap-2 border-b bg-muted/40 px-4 py-2 text-sm"
70
+ data-document-reconcile-conflict
71
+ >
72
+ <span className="me-auto min-w-0 flex-1 basis-56" role="status">
73
+ {message}
74
+ </span>
75
+ <DialogTrigger asChild>
76
+ <Button
77
+ type="button"
78
+ size="sm"
79
+ disabled={state.saving}
80
+ onClick={() => setReviewed({ ...server })}
81
+ >
82
+ {t("editor.reconcileReview")}
83
+ </Button>
84
+ </DialogTrigger>
85
+ </div>
86
+ <DialogContent className="flex h-[min(46rem,calc(100dvh-2rem))] max-w-[min(72rem,calc(100vw-2rem))] flex-col overflow-hidden p-0 sm:max-w-[min(72rem,calc(100vw-2rem))]">
87
+ <DialogTitle className="sr-only">
88
+ {t("editor.reconcileReview")}
89
+ </DialogTitle>
90
+ <RecoveryComparison
91
+ mine={{ title: state.localTitle, content: state.localDraft }}
92
+ saved={{
93
+ title: reviewed?.title ?? server.title ?? "",
94
+ content: reviewed?.content ?? "",
95
+ }}
96
+ busy={state.saving}
97
+ keepMineDisabled={!canEdit || !reviewed?.updatedAt}
98
+ failure={stale ? t("editor.reconcileReviewStale") : message}
99
+ stale={stale}
100
+ onRefresh={() => setReviewed({ ...server })}
101
+ onKeepMine={() => void finish(onKeepMine)}
102
+ onUseSaved={() => void finish(onUseSaved)}
103
+ onSaveSeparately={() => void finish(onSaveSeparately)}
104
+ onCopy={() => void onCopy()}
105
+ />
106
+ </DialogContent>
107
+ </Dialog>
108
+ );
109
+ }
@@ -2,13 +2,11 @@ import { writeClipboardText } from "@agent-native/core/client/clipboard";
2
2
  import { useT } from "@agent-native/core/client/i18n";
3
3
  import type { Document } from "@shared/api";
4
4
  import { useQueryClient } from "@tanstack/react-query";
5
- import DiffMatchPatch, { DIFF_DELETE, DIFF_INSERT } from "diff-match-patch";
6
- import { useEffect, useMemo, useState, type ReactNode } from "react";
5
+ import { useEffect, useState, type ReactNode } from "react";
7
6
  import { useNavigate } from "react-router";
8
7
  import { toast } from "sonner";
9
8
 
10
9
  import { QueryErrorState } from "@/components/QueryErrorState";
11
- import { Button } from "@/components/ui/button";
12
10
  import {
13
11
  documentQueryFilter,
14
12
  isDocumentUpdateConflict,
@@ -21,34 +19,10 @@ import { isDocumentCreationPending } from "@/lib/optimistic-document";
21
19
 
22
20
  import { documentBodyHydrationIsPending } from "./body-hydration";
23
21
  import { DocumentEditorSkeleton } from "./DocumentEditorSkeleton";
22
+ import { RecoveryComparison } from "./RecoveryComparison";
24
23
 
25
24
  type DraftRecoveryFailure = "conflict" | "error";
26
25
 
27
- function DiffValue({ value, other }: { value: string; other: string }) {
28
- const segments = useMemo(() => {
29
- const differ = new DiffMatchPatch();
30
- const result = differ.diff_main(value, other);
31
- differ.diff_cleanupSemantic(result);
32
- return result;
33
- }, [other, value]);
34
- return (
35
- <>
36
- {segments.map(([operation, text], index) =>
37
- operation === DIFF_INSERT ? null : operation === DIFF_DELETE ? (
38
- <mark
39
- key={`${index}:${text}`}
40
- className="rounded-sm bg-accent text-accent-foreground"
41
- >
42
- {text}
43
- </mark>
44
- ) : (
45
- <span key={`${index}:${text}`}>{text}</span>
46
- ),
47
- )}
48
- </>
49
- );
50
- }
51
-
52
26
  export function PageDraftRecovery({
53
27
  document,
54
28
  children,
@@ -149,12 +123,8 @@ export function PageDraftRecovery({
149
123
  expectedDraftVersion: draft.version,
150
124
  expectedDraftTitle: draft.title,
151
125
  expectedDraftContent: draft.content,
152
- ...(choice === "keep_mine"
153
- ? {
154
- expectedDocumentUpdatedAt:
155
- conflictDocument?.updatedAt ?? document.updatedAt,
156
- }
157
- : {}),
126
+ expectedDocumentUpdatedAt:
127
+ conflictDocument?.updatedAt ?? document.updatedAt,
158
128
  });
159
129
  if (result.status === "document_conflict") {
160
130
  setFailure("conflict");
@@ -192,126 +162,34 @@ export function PageDraftRecovery({
192
162
  );
193
163
  if (!drafts.data) return <DocumentEditorSkeleton title={document.title} />;
194
164
  if (!draft) return children;
195
- const comparing =
196
- failure === "conflict" || draft.deferredReason === "conflict";
197
165
  const savedVersion = conflictDocument ?? document;
198
166
  return (
199
- <div className="flex min-h-0 flex-1 flex-col gap-4 overflow-auto p-6">
200
- <h2 className="text-sm font-semibold">
201
- {comparing
202
- ? t("editor.previewDraftCompare")
203
- : t("editor.previewDraftRecovery")}
204
- </h2>
205
- <div className={comparing ? "grid gap-3 md:grid-cols-2" : "grid gap-3"}>
206
- <section className="min-w-0 rounded-md border p-3">
207
- {comparing ? (
208
- <p className="mb-2 text-xs font-medium text-muted-foreground">
209
- {t("editor.previewDraftYourEdits")}
210
- </p>
211
- ) : null}
212
- <p className="font-medium break-words">
213
- {comparing ? (
214
- <DiffValue value={draft.title} other={savedVersion.title} />
215
- ) : (
216
- draft.title
217
- )}
218
- </p>
219
- <pre className="mt-2 whitespace-pre-wrap break-words text-sm">
220
- {comparing ? (
221
- <DiffValue value={draft.content} other={savedVersion.content} />
222
- ) : (
223
- draft.content
224
- )}
225
- </pre>
226
- </section>
227
- {comparing ? (
228
- <section className="min-w-0 rounded-md border p-3">
229
- <p className="mb-2 text-xs font-medium text-muted-foreground">
230
- {t("editor.previewDraftSavedVersion")}
231
- </p>
232
- <p className="font-medium break-words">
233
- <DiffValue value={savedVersion.title} other={draft.title} />
234
- </p>
235
- <pre className="mt-2 whitespace-pre-wrap break-words text-sm">
236
- <DiffValue value={savedVersion.content} other={draft.content} />
237
- </pre>
238
- </section>
239
- ) : null}
240
- </div>
241
- {failure ? (
242
- <p role="alert" className="text-sm text-destructive">
243
- {failure === "conflict"
244
- ? t("editor.previewDraftConflict")
245
- : t("empty.genericError")}
246
- </p>
247
- ) : null}
248
- <div className="flex flex-wrap gap-2">
249
- {comparing ? (
250
- <>
251
- <Button
252
- type="button"
253
- size="sm"
254
- disabled={busy}
255
- onClick={() => void resolveConflict("keep_mine")}
256
- >
257
- {t("editor.previewDraftKeepMine")}
258
- </Button>
259
- <Button
260
- type="button"
261
- size="sm"
262
- variant="outline"
263
- disabled={busy}
264
- onClick={() => void resolveConflict("use_saved")}
265
- >
266
- {t("editor.previewDraftUseSaved")}
267
- </Button>
268
- <Button
269
- type="button"
270
- size="sm"
271
- variant="outline"
272
- disabled={busy}
273
- onClick={() => void resolveConflict("save_separately")}
274
- >
275
- {t("editor.previewDraftSaveSeparately")}
276
- </Button>
277
- <Button
278
- type="button"
279
- size="sm"
280
- variant="ghost"
281
- disabled={busy}
282
- onClick={() => {
283
- void writeClipboardText(draft.content).then((copied) => {
284
- if (copied) toast.success(t("editor.unsavedTextCopied"));
285
- else
286
- toast.error(t("editor.toolbar.clipboardAccessUnavailable"));
287
- });
288
- }}
289
- >
290
- {t("editor.copyUnsavedText")}
291
- </Button>
292
- </>
293
- ) : (
294
- <Button
295
- type="button"
296
- size="sm"
297
- disabled={busy || documentBodyHydrationIsPending(document)}
298
- onClick={() => void settleDraft(true)}
299
- >
300
- {t("editor.restorePreviewDraft")}
301
- </Button>
302
- )}
303
- {!comparing ? (
304
- <Button
305
- type="button"
306
- size="sm"
307
- variant="outline"
308
- disabled={busy}
309
- onClick={() => void resolveConflict("use_saved")}
310
- >
311
- {t("editor.previewDraftUseSaved")}
312
- </Button>
313
- ) : null}
314
- </div>
315
- </div>
167
+ <RecoveryComparison
168
+ mine={{ title: draft.title, content: draft.content }}
169
+ saved={{ title: savedVersion.title, content: savedVersion.content }}
170
+ busy={busy}
171
+ keepMineDisabled={documentBodyHydrationIsPending(document)}
172
+ failure={
173
+ failure === "conflict"
174
+ ? t("editor.previewDraftConflict")
175
+ : failure === "error"
176
+ ? t("empty.genericError")
177
+ : null
178
+ }
179
+ onKeepMine={() => {
180
+ if (documentBodyHydrationIsPending(document)) return;
181
+ if (failure === "conflict" || draft.deferredReason === "conflict")
182
+ void resolveConflict("keep_mine");
183
+ else void settleDraft(true);
184
+ }}
185
+ onUseSaved={() => void resolveConflict("use_saved")}
186
+ onSaveSeparately={() => void resolveConflict("save_separately")}
187
+ onCopy={() => {
188
+ void writeClipboardText(draft.content).then((copied) => {
189
+ if (copied) toast.success(t("editor.unsavedTextCopied"));
190
+ else toast.error(t("editor.toolbar.clipboardAccessUnavailable"));
191
+ });
192
+ }}
193
+ />
316
194
  );
317
195
  }
@@ -0,0 +1,230 @@
1
+ import { useT } from "@agent-native/core/client/i18n";
2
+ import { SharedRichEditor } from "@agent-native/toolkit/editor";
3
+ import { IconChevronDown, IconLoader2 } from "@tabler/icons-react";
4
+ import DiffMatchPatch, {
5
+ DIFF_DELETE,
6
+ DIFF_EQUAL,
7
+ DIFF_INSERT,
8
+ type Diff,
9
+ } from "diff-match-patch";
10
+ import { useMemo, useState } from "react";
11
+
12
+ import { Button } from "@/components/ui/button";
13
+ import {
14
+ DropdownMenu,
15
+ DropdownMenuContent,
16
+ DropdownMenuItem,
17
+ DropdownMenuTrigger,
18
+ } from "@/components/ui/dropdown-menu";
19
+
20
+ const CONTEXT_LENGTH = 120;
21
+
22
+ function contextualize(segments: Diff[]): Diff[] {
23
+ return segments.flatMap(([operation, text]) => {
24
+ if (operation !== DIFF_EQUAL || text.length <= CONTEXT_LENGTH * 2)
25
+ return [[operation, text] satisfies Diff];
26
+ return [
27
+ [operation, text.slice(0, CONTEXT_LENGTH)] satisfies Diff,
28
+ [operation, "\n…\n"] satisfies Diff,
29
+ [operation, text.slice(-CONTEXT_LENGTH)] satisfies Diff,
30
+ ];
31
+ });
32
+ }
33
+
34
+ function DiffValue({ value, other }: { value: string; other: string }) {
35
+ const segments = useMemo(() => {
36
+ const differ = new DiffMatchPatch();
37
+ const result = differ.diff_main(value, other);
38
+ differ.diff_cleanupSemantic(result);
39
+ return contextualize(result);
40
+ }, [other, value]);
41
+
42
+ return segments.map(([operation, text], index) =>
43
+ operation === DIFF_INSERT ? null : operation === DIFF_DELETE ? (
44
+ <mark
45
+ key={`${index}:${text}`}
46
+ className="rounded-sm bg-accent text-accent-foreground"
47
+ >
48
+ {text}
49
+ </mark>
50
+ ) : (
51
+ <span key={`${index}:${text}`}>{text}</span>
52
+ ),
53
+ );
54
+ }
55
+
56
+ interface RecoveryVersion {
57
+ title: string;
58
+ content: string;
59
+ }
60
+
61
+ export function RecoveryComparison({
62
+ mine,
63
+ saved,
64
+ busy,
65
+ keepMineDisabled = false,
66
+ failure,
67
+ stale = false,
68
+ onRefresh,
69
+ onKeepMine,
70
+ onUseSaved,
71
+ onSaveSeparately,
72
+ onCopy,
73
+ }: {
74
+ mine: RecoveryVersion;
75
+ saved: RecoveryVersion;
76
+ busy: boolean;
77
+ keepMineDisabled?: boolean;
78
+ failure?: string | null;
79
+ stale?: boolean;
80
+ onRefresh?: () => void;
81
+ onKeepMine: () => void;
82
+ onUseSaved: () => void;
83
+ onSaveSeparately: () => void;
84
+ onCopy: () => void;
85
+ }) {
86
+ const t = useT();
87
+ const [fullVersions, setFullVersions] = useState(false);
88
+
89
+ return (
90
+ <div
91
+ className="flex min-h-0 flex-1 flex-col overflow-hidden [container-type:inline-size]"
92
+ data-recovery-comparison
93
+ >
94
+ <div className="min-h-0 flex-1 overflow-y-auto px-4 py-5 sm:px-6">
95
+ <h2 className="text-sm font-semibold">
96
+ {t("editor.previewDraftCompare")}
97
+ </h2>
98
+ <div className="mt-4 grid min-w-0 gap-3 @min-[48rem]:grid-cols-2">
99
+ {[
100
+ {
101
+ key: "mine",
102
+ label: t("editor.previewDraftYourEdits"),
103
+ value: mine,
104
+ other: saved,
105
+ },
106
+ {
107
+ key: "saved",
108
+ label: t("editor.previewDraftSavedVersion"),
109
+ value: saved,
110
+ other: mine,
111
+ },
112
+ ].map((version) => (
113
+ <section
114
+ key={version.key}
115
+ className="min-w-0 rounded-md border bg-background p-3"
116
+ >
117
+ <h3 className="mb-3 text-xs font-medium text-muted-foreground">
118
+ {version.label}
119
+ </h3>
120
+ {fullVersions ? (
121
+ <>
122
+ <p className="mb-3 font-medium break-words">
123
+ {version.value.title}
124
+ </p>
125
+ <SharedRichEditor
126
+ key={`${version.key}:${version.value.content}`}
127
+ value={version.value.content}
128
+ onChange={() => undefined}
129
+ editable={false}
130
+ dragHandle={false}
131
+ preset="content"
132
+ ariaLabel={version.label}
133
+ />
134
+ </>
135
+ ) : (
136
+ <>
137
+ <p className="font-medium break-words">
138
+ <DiffValue
139
+ value={version.value.title}
140
+ other={version.other.title}
141
+ />
142
+ </p>
143
+ <pre className="mt-2 overflow-x-auto whitespace-pre-wrap break-words font-sans text-sm">
144
+ <DiffValue
145
+ value={version.value.content}
146
+ other={version.other.content}
147
+ />
148
+ </pre>
149
+ </>
150
+ )}
151
+ </section>
152
+ ))}
153
+ </div>
154
+ <Button
155
+ type="button"
156
+ variant="ghost"
157
+ size="sm"
158
+ className="mt-2"
159
+ aria-expanded={fullVersions}
160
+ onClick={() => setFullVersions((value) => !value)}
161
+ >
162
+ <IconChevronDown
163
+ aria-hidden="true"
164
+ className={fullVersions ? "rotate-180" : undefined}
165
+ />
166
+ {fullVersions
167
+ ? t("editor.previewDraftShowChanges")
168
+ : t("editor.previewDraftViewFullVersions")}
169
+ </Button>
170
+ {failure ? (
171
+ <p role="alert" className="mt-3 text-sm text-destructive">
172
+ {failure}
173
+ </p>
174
+ ) : null}
175
+ </div>
176
+ <div className="border-t bg-background px-4 pt-3 pb-[max(0.75rem,env(safe-area-inset-bottom))] sm:px-6">
177
+ {stale && onRefresh ? (
178
+ <Button type="button" onClick={onRefresh}>
179
+ {t("editor.reconcileRefresh")}
180
+ </Button>
181
+ ) : (
182
+ <>
183
+ <div className="grid gap-2 @min-[32rem]:grid-cols-2">
184
+ <Button
185
+ type="button"
186
+ variant="outline"
187
+ disabled={busy || keepMineDisabled}
188
+ onClick={onKeepMine}
189
+ >
190
+ {busy ? (
191
+ <IconLoader2 aria-hidden="true" className="animate-spin" />
192
+ ) : null}
193
+ {t("editor.previewDraftKeepMine")}
194
+ </Button>
195
+ <Button
196
+ type="button"
197
+ variant="outline"
198
+ disabled={busy}
199
+ onClick={onUseSaved}
200
+ >
201
+ {t("editor.previewDraftUseSaved")}
202
+ </Button>
203
+ </div>
204
+ <DropdownMenu>
205
+ <DropdownMenuTrigger asChild>
206
+ <Button
207
+ type="button"
208
+ variant="ghost"
209
+ size="sm"
210
+ className="mt-1"
211
+ disabled={busy}
212
+ >
213
+ {t("editor.previewDraftMoreOptions")}
214
+ </Button>
215
+ </DropdownMenuTrigger>
216
+ <DropdownMenuContent align="start">
217
+ <DropdownMenuItem onSelect={onSaveSeparately}>
218
+ {t("editor.previewDraftSaveSeparately")}
219
+ </DropdownMenuItem>
220
+ <DropdownMenuItem onSelect={onCopy}>
221
+ {t("editor.copyUnsavedText")}
222
+ </DropdownMenuItem>
223
+ </DropdownMenuContent>
224
+ </DropdownMenu>
225
+ </>
226
+ )}
227
+ </div>
228
+ </div>
229
+ );
230
+ }