@saadnvd1/agent-os 0.3.761 → 0.3.762
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/app/api/artifacts/[id]/route.ts +13 -4
- package/app/api/artifacts/[id]/versions/route.ts +16 -0
- package/app/api/sessions/[id]/artifacts/route.ts +9 -1
- package/components/Artifacts/ArtifactActions.tsx +308 -0
- package/components/Artifacts/ArtifactFullscreen.tsx +58 -0
- package/components/Artifacts/ArtifactPreview.tsx +141 -18
- package/components/Artifacts/ArtifactRow.tsx +5 -0
- package/components/Artifacts/ArtifactsPanel.tsx +2 -0
- package/components/Artifacts/VersionSwitcher.tsx +216 -0
- package/components/Chat/Artifact.tsx +129 -89
- package/components/Share/DownloadMenu.tsx +63 -38
- package/components/Share/ShareDialog.tsx +2 -0
- package/data/session-artifacts/keys.ts +3 -0
- package/data/session-artifacts/queries.ts +25 -0
- package/lib/artifacts/actions.ts +75 -0
- package/lib/artifacts/store.ts +105 -10
- package/lib/artifacts/tools.ts +29 -6
- package/lib/artifacts/versions.ts +73 -0
- package/lib/chat/events.ts +4 -0
- package/lib/chat/worker/host.ts +2 -0
- package/lib/db/migrations/20261029T1200_artifact_versions.ts +28 -0
- package/lib/security/demo.ts +1 -0
- package/lib/session-artifacts/collect.ts +28 -6
- package/lib/session-artifacts/derive.ts +3 -1
- package/lib/session-artifacts/types.ts +9 -0
- package/package.json +1 -1
- package/stores/shareUi.ts +6 -2
|
@@ -7,26 +7,38 @@ import {
|
|
|
7
7
|
Copy,
|
|
8
8
|
ExternalLink,
|
|
9
9
|
FolderOpen,
|
|
10
|
+
History,
|
|
10
11
|
} from "lucide-react";
|
|
11
12
|
import { Button } from "@/components/ui/button";
|
|
12
13
|
import { Markdown } from "@/components/Chat/Markdown";
|
|
13
14
|
import { ArtifactFrame } from "@/components/Chat/ArtifactFrame";
|
|
14
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
artifactContentUrl,
|
|
17
|
+
useArtifactText,
|
|
18
|
+
useArtifactVersions,
|
|
19
|
+
} from "@/data/session-artifacts";
|
|
20
|
+
import { artifactActions, type ArtifactAction } from "@/lib/artifacts/actions";
|
|
15
21
|
import { formatBytes } from "@/lib/share/view";
|
|
16
22
|
import { relativeTime } from "@/lib/schedules/when";
|
|
17
23
|
import { openable, type SessionArtifact } from "@/lib/session-artifacts/types";
|
|
18
24
|
import { cn } from "@/lib/utils";
|
|
19
25
|
import { ArtifactIcon } from "./ArtifactRow";
|
|
20
26
|
import { stateLabel } from "./helpers";
|
|
27
|
+
import { ArtifactActions } from "./ArtifactActions";
|
|
28
|
+
import { ArtifactFullscreen } from "./ArtifactFullscreen";
|
|
29
|
+
import { VersionSwitcher } from "./VersionSwitcher";
|
|
21
30
|
|
|
22
31
|
function Body({
|
|
23
32
|
sessionId,
|
|
24
33
|
artifact,
|
|
25
34
|
pr,
|
|
35
|
+
versionId,
|
|
26
36
|
}: {
|
|
27
37
|
sessionId: string;
|
|
28
38
|
artifact: SessionArtifact;
|
|
29
39
|
pr: string | null;
|
|
40
|
+
// A page's version to show, served by its own id.
|
|
41
|
+
versionId?: string;
|
|
30
42
|
}) {
|
|
31
43
|
const url = artifactContentUrl(sessionId, artifact.id);
|
|
32
44
|
const textual =
|
|
@@ -50,8 +62,8 @@ function Body({
|
|
|
50
62
|
if (artifact.preview === "html")
|
|
51
63
|
return (
|
|
52
64
|
<ArtifactFrame
|
|
53
|
-
artifactId={artifact.id}
|
|
54
|
-
src={url}
|
|
65
|
+
artifactId={versionId ?? artifact.id}
|
|
66
|
+
src={versionId ? undefined : url}
|
|
55
67
|
title={artifact.name}
|
|
56
68
|
fill
|
|
57
69
|
className="bg-background"
|
|
@@ -103,6 +115,7 @@ export function ArtifactPreview({
|
|
|
103
115
|
artifact,
|
|
104
116
|
pr,
|
|
105
117
|
mobile,
|
|
118
|
+
remote = false,
|
|
106
119
|
onBack,
|
|
107
120
|
onOpenInFiles,
|
|
108
121
|
}: {
|
|
@@ -110,10 +123,34 @@ export function ArtifactPreview({
|
|
|
110
123
|
artifact: SessionArtifact;
|
|
111
124
|
pr: string | null;
|
|
112
125
|
mobile: boolean;
|
|
126
|
+
// On a linked machine: its pages are read through the session, one
|
|
127
|
+
// version only.
|
|
128
|
+
remote?: boolean;
|
|
113
129
|
onBack?: () => void;
|
|
114
130
|
onOpenInFiles?: (path: string) => void;
|
|
115
131
|
}) {
|
|
116
132
|
const [copied, setCopied] = useState(false);
|
|
133
|
+
const [full, setFull] = useState(false);
|
|
134
|
+
// The version picked; none means the latest, which follows new versions.
|
|
135
|
+
const [picked, setPicked] = useState<string | null>(null);
|
|
136
|
+
const page =
|
|
137
|
+
artifact.kind === "page" && !!artifact.artifactId && !remote
|
|
138
|
+
? artifact.artifactId
|
|
139
|
+
: null;
|
|
140
|
+
// Keyed by the latest version, so a new one refetches.
|
|
141
|
+
const versionsQuery = useArtifactVersions(
|
|
142
|
+
page ?? "",
|
|
143
|
+
page ?? "",
|
|
144
|
+
!!page && openable(artifact)
|
|
145
|
+
);
|
|
146
|
+
const versions = versionsQuery.data?.versions ?? [];
|
|
147
|
+
const shownId =
|
|
148
|
+
(picked && versions.some((v) => v.id === picked) && picked) || page;
|
|
149
|
+
const shown = versions.find((v) => v.id === shownId);
|
|
150
|
+
const latest = versions[versions.length - 1];
|
|
151
|
+
const many = versions.length > 1;
|
|
152
|
+
const older = many && !!shown && shown.id !== latest?.id;
|
|
153
|
+
|
|
117
154
|
const label = stateLabel(artifact, pr);
|
|
118
155
|
// Inside the worktree, the path from its top; the full one is copied.
|
|
119
156
|
const where =
|
|
@@ -125,8 +162,13 @@ export function ArtifactPreview({
|
|
|
125
162
|
const meta = [
|
|
126
163
|
where,
|
|
127
164
|
label,
|
|
128
|
-
|
|
129
|
-
|
|
165
|
+
many && shown && `v${shown.version} of ${versions.length}`,
|
|
166
|
+
shown?.turn && `turn ${shown.turn.index}`,
|
|
167
|
+
!shown && artifact.size !== undefined ? formatBytes(artifact.size) : null,
|
|
168
|
+
shown?.bytes !== undefined ? formatBytes(shown.bytes) : null,
|
|
169
|
+
(mobile || shown) &&
|
|
170
|
+
(shown?.createdAt || artifact.at) &&
|
|
171
|
+
relativeTime(shown?.createdAt || artifact.at),
|
|
130
172
|
]
|
|
131
173
|
.filter(Boolean)
|
|
132
174
|
.join(" · ");
|
|
@@ -137,14 +179,16 @@ export function ArtifactPreview({
|
|
|
137
179
|
setTimeout(() => setCopied(false), 1500);
|
|
138
180
|
});
|
|
139
181
|
};
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
(
|
|
145
|
-
|
|
182
|
+
const list = artifact.actions ?? artifactActions(artifact, { pdf: false });
|
|
183
|
+
const can = (id: ArtifactAction["id"]) => list.some((a) => a.id === id);
|
|
184
|
+
// A linked machine's page is read through the session: only full screen.
|
|
185
|
+
const pageList = page
|
|
186
|
+
? (versionsQuery.data?.actions ?? list)
|
|
187
|
+
: list.filter((a) => a.id === "fullscreen");
|
|
188
|
+
const canOpenInFiles = !!onOpenInFiles && can("open-in-files");
|
|
189
|
+
const fileActions = (
|
|
146
190
|
<>
|
|
147
|
-
{artifact.url && (
|
|
191
|
+
{artifact.url && can("open-pr") && (
|
|
148
192
|
<Button
|
|
149
193
|
variant="outline"
|
|
150
194
|
size="sm"
|
|
@@ -162,7 +206,7 @@ export function ArtifactPreview({
|
|
|
162
206
|
</a>
|
|
163
207
|
</Button>
|
|
164
208
|
)}
|
|
165
|
-
{artifact.path && (
|
|
209
|
+
{artifact.path && can("copy-path") && (
|
|
166
210
|
<Button
|
|
167
211
|
variant="outline"
|
|
168
212
|
size="sm"
|
|
@@ -178,7 +222,7 @@ export function ArtifactPreview({
|
|
|
178
222
|
<span className={cn(!mobile && "hidden @lg:inline")}>Copy path</span>
|
|
179
223
|
</Button>
|
|
180
224
|
)}
|
|
181
|
-
{canOpenInFiles && (
|
|
225
|
+
{canOpenInFiles && artifact.path && (
|
|
182
226
|
<Button
|
|
183
227
|
variant="outline"
|
|
184
228
|
size="sm"
|
|
@@ -194,7 +238,31 @@ export function ArtifactPreview({
|
|
|
194
238
|
)}
|
|
195
239
|
</>
|
|
196
240
|
);
|
|
197
|
-
const
|
|
241
|
+
const isPage = artifact.kind === "page" && openable(artifact);
|
|
242
|
+
const pageActionsFor = (variant: "labelled" | "bar") => (
|
|
243
|
+
<ArtifactActions
|
|
244
|
+
artifactId={shownId ?? artifact.id}
|
|
245
|
+
title={shown?.title ?? artifact.name}
|
|
246
|
+
version={many ? shown?.version : undefined}
|
|
247
|
+
actions={pageList}
|
|
248
|
+
variant={variant}
|
|
249
|
+
onFull={() => setFull(true)}
|
|
250
|
+
/>
|
|
251
|
+
);
|
|
252
|
+
const actions = isPage
|
|
253
|
+
? pageActionsFor(mobile ? "bar" : "labelled")
|
|
254
|
+
: fileActions;
|
|
255
|
+
const hasActions = isPage
|
|
256
|
+
? pageList.length > 0
|
|
257
|
+
: list.some((a) => a.id !== "open-in-files" || canOpenInFiles);
|
|
258
|
+
const switcher = many && shownId && (
|
|
259
|
+
<VersionSwitcher
|
|
260
|
+
versions={versions}
|
|
261
|
+
currentId={shownId}
|
|
262
|
+
onPick={(id) => setPicked(id === latest?.id ? null : id)}
|
|
263
|
+
mobile={mobile}
|
|
264
|
+
/>
|
|
265
|
+
);
|
|
198
266
|
|
|
199
267
|
return (
|
|
200
268
|
<div className="@container flex h-full min-w-0 flex-col">
|
|
@@ -214,7 +282,7 @@ export function ArtifactPreview({
|
|
|
214
282
|
)}
|
|
215
283
|
<div className="min-w-0 flex-1">
|
|
216
284
|
<div className="truncate text-[13px] font-medium">
|
|
217
|
-
{artifact.name}
|
|
285
|
+
{shown?.title ?? artifact.name}
|
|
218
286
|
</div>
|
|
219
287
|
{meta && (
|
|
220
288
|
<div className="text-muted-foreground truncate font-mono text-[11px]">
|
|
@@ -222,18 +290,73 @@ export function ArtifactPreview({
|
|
|
222
290
|
</div>
|
|
223
291
|
)}
|
|
224
292
|
</div>
|
|
293
|
+
{!mobile && switcher && (
|
|
294
|
+
<div className="hidden @3xl:flex">{switcher}</div>
|
|
295
|
+
)}
|
|
225
296
|
{!mobile && hasActions && (
|
|
226
297
|
<div className="flex shrink-0 gap-1.5">{actions}</div>
|
|
227
298
|
)}
|
|
228
299
|
</div>
|
|
300
|
+
{mobile && switcher && (
|
|
301
|
+
<div className="bg-card border-b px-1">{switcher}</div>
|
|
302
|
+
)}
|
|
303
|
+
{!mobile && switcher && (
|
|
304
|
+
<div className="flex border-b px-3 py-1.5 sm:px-4 @3xl:hidden">
|
|
305
|
+
{switcher}
|
|
306
|
+
</div>
|
|
307
|
+
)}
|
|
308
|
+
{older && latest && (
|
|
309
|
+
<div className="flex min-h-11 items-center gap-2 border-b border-amber-500/25 bg-amber-500/[0.08] px-3 text-[12.5px] sm:px-4">
|
|
310
|
+
<History className="h-3.5 w-3.5 shrink-0 text-amber-600 dark:text-amber-400" />
|
|
311
|
+
<span className="min-w-0 flex-1 truncate">
|
|
312
|
+
Older version
|
|
313
|
+
<span className="hidden @md:inline">
|
|
314
|
+
. The agent changed this page since.
|
|
315
|
+
</span>
|
|
316
|
+
</span>
|
|
317
|
+
<Button
|
|
318
|
+
variant="ghost"
|
|
319
|
+
size="sm"
|
|
320
|
+
className="h-11 shrink-0 md:h-7"
|
|
321
|
+
onClick={() => setPicked(null)}
|
|
322
|
+
>
|
|
323
|
+
Go to latest (v{latest.version})
|
|
324
|
+
</Button>
|
|
325
|
+
</div>
|
|
326
|
+
)}
|
|
229
327
|
<div className="min-h-0 flex-1 overflow-auto">
|
|
230
|
-
<Body
|
|
328
|
+
<Body
|
|
329
|
+
sessionId={sessionId}
|
|
330
|
+
artifact={artifact}
|
|
331
|
+
pr={pr}
|
|
332
|
+
versionId={shownId ?? undefined}
|
|
333
|
+
/>
|
|
231
334
|
</div>
|
|
232
335
|
{mobile && hasActions && (
|
|
233
336
|
<div className="flex gap-2 border-t px-3 pt-2.5 pb-[max(0.75rem,env(safe-area-inset-bottom))]">
|
|
234
|
-
{actions}
|
|
337
|
+
{isPage ? <div className="flex-1">{actions}</div> : actions}
|
|
235
338
|
</div>
|
|
236
339
|
)}
|
|
340
|
+
{isPage && (
|
|
341
|
+
<ArtifactFullscreen
|
|
342
|
+
open={full}
|
|
343
|
+
onOpenChange={setFull}
|
|
344
|
+
artifactId={shownId ?? artifact.id}
|
|
345
|
+
src={shownId ? undefined : artifactContentUrl(sessionId, artifact.id)}
|
|
346
|
+
title={shown?.title ?? artifact.name}
|
|
347
|
+
actions={
|
|
348
|
+
page ? (
|
|
349
|
+
<ArtifactActions
|
|
350
|
+
artifactId={shownId ?? artifact.id}
|
|
351
|
+
title={shown?.title ?? artifact.name}
|
|
352
|
+
version={many ? shown?.version : undefined}
|
|
353
|
+
actions={pageList.filter((a) => a.id !== "fullscreen")}
|
|
354
|
+
variant="icons"
|
|
355
|
+
/>
|
|
356
|
+
) : undefined
|
|
357
|
+
}
|
|
358
|
+
/>
|
|
359
|
+
)}
|
|
237
360
|
</div>
|
|
238
361
|
);
|
|
239
362
|
}
|
|
@@ -106,6 +106,11 @@ export function ArtifactRow({
|
|
|
106
106
|
)}
|
|
107
107
|
>
|
|
108
108
|
{artifact.name}
|
|
109
|
+
{(artifact.versionCount ?? 1) > 1 && (
|
|
110
|
+
<span className="bg-primary/15 text-primary ml-1.5 rounded-full px-1.5 py-px align-[1px] font-sans text-[10.5px]">
|
|
111
|
+
v{artifact.version}
|
|
112
|
+
</span>
|
|
113
|
+
)}
|
|
109
114
|
</span>
|
|
110
115
|
<span
|
|
111
116
|
className={cn(
|
|
@@ -121,6 +121,7 @@ export function ArtifactsPanel({
|
|
|
121
121
|
artifact={selected}
|
|
122
122
|
pr={pr}
|
|
123
123
|
mobile
|
|
124
|
+
remote={!!data?.host}
|
|
124
125
|
onBack={() => setSelectedId(null)}
|
|
125
126
|
onOpenInFiles={onOpenInFiles}
|
|
126
127
|
/>
|
|
@@ -231,6 +232,7 @@ export function ArtifactsPanel({
|
|
|
231
232
|
artifact={selected}
|
|
232
233
|
pr={pr}
|
|
233
234
|
mobile={false}
|
|
235
|
+
remote={!!data?.host}
|
|
234
236
|
onOpenInFiles={onOpenInFiles}
|
|
235
237
|
/>
|
|
236
238
|
) : (
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
Check,
|
|
6
|
+
ChevronDown,
|
|
7
|
+
ChevronLeft,
|
|
8
|
+
ChevronRight,
|
|
9
|
+
History,
|
|
10
|
+
} from "lucide-react";
|
|
11
|
+
import {
|
|
12
|
+
DropdownMenu,
|
|
13
|
+
DropdownMenuContent,
|
|
14
|
+
DropdownMenuItem,
|
|
15
|
+
DropdownMenuTrigger,
|
|
16
|
+
} from "@/components/ui/dropdown-menu";
|
|
17
|
+
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
|
18
|
+
import { SHEET } from "@/components/Hosts/sheet";
|
|
19
|
+
import type { ArtifactVersion } from "@/lib/artifacts/actions";
|
|
20
|
+
import { formatBytes } from "@/lib/share/view";
|
|
21
|
+
import { relativeTime } from "@/lib/schedules/when";
|
|
22
|
+
import { cn } from "@/lib/utils";
|
|
23
|
+
|
|
24
|
+
// "turn 9 · "split by plan" · 31m ago · 16 KB"
|
|
25
|
+
export function versionDetail(v: ArtifactVersion, full = true): string {
|
|
26
|
+
return [
|
|
27
|
+
v.turn && `turn ${v.turn.index}`,
|
|
28
|
+
full && v.turn?.prompt && `“${v.turn.prompt}”`,
|
|
29
|
+
relativeTime(v.createdAt),
|
|
30
|
+
full && v.bytes !== undefined && formatBytes(v.bytes),
|
|
31
|
+
]
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.join(" · ");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const summary = (v: ArtifactVersion) =>
|
|
37
|
+
v.note || (v.version === 1 ? "First version" : `Version ${v.version}`);
|
|
38
|
+
|
|
39
|
+
function Row({
|
|
40
|
+
v,
|
|
41
|
+
latest,
|
|
42
|
+
current,
|
|
43
|
+
full,
|
|
44
|
+
}: {
|
|
45
|
+
v: ArtifactVersion;
|
|
46
|
+
latest: boolean;
|
|
47
|
+
current: boolean;
|
|
48
|
+
full: boolean;
|
|
49
|
+
}) {
|
|
50
|
+
return (
|
|
51
|
+
<>
|
|
52
|
+
<span
|
|
53
|
+
className={cn(
|
|
54
|
+
"mt-0.5 inline-flex h-5 shrink-0 items-center rounded-full px-2 text-[11px]",
|
|
55
|
+
latest
|
|
56
|
+
? "bg-primary/15 text-primary"
|
|
57
|
+
: "bg-muted text-muted-foreground"
|
|
58
|
+
)}
|
|
59
|
+
>
|
|
60
|
+
v{v.version}
|
|
61
|
+
</span>
|
|
62
|
+
<span className="min-w-0 flex-1">
|
|
63
|
+
<span className="block truncate text-[13px]">
|
|
64
|
+
{summary(v)}
|
|
65
|
+
{latest && <span className="text-muted-foreground"> · latest</span>}
|
|
66
|
+
</span>
|
|
67
|
+
<span className="text-muted-foreground block truncate text-[11.5px]">
|
|
68
|
+
{versionDetail(v, full)}
|
|
69
|
+
</span>
|
|
70
|
+
</span>
|
|
71
|
+
{current && <Check className="text-primary h-4 w-4 shrink-0" />}
|
|
72
|
+
</>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ‹ v2 of 3 › with the list of versions: a menu on a wide screen, a sheet
|
|
77
|
+
// on a phone. Hidden for a page with one version.
|
|
78
|
+
export function VersionSwitcher({
|
|
79
|
+
versions,
|
|
80
|
+
currentId,
|
|
81
|
+
onPick,
|
|
82
|
+
mobile,
|
|
83
|
+
className,
|
|
84
|
+
}: {
|
|
85
|
+
versions: ArtifactVersion[];
|
|
86
|
+
currentId: string;
|
|
87
|
+
onPick: (id: string) => void;
|
|
88
|
+
mobile: boolean;
|
|
89
|
+
className?: string;
|
|
90
|
+
}) {
|
|
91
|
+
const [sheet, setSheet] = useState(false);
|
|
92
|
+
if (versions.length < 2) return null;
|
|
93
|
+
const i = Math.max(
|
|
94
|
+
0,
|
|
95
|
+
versions.findIndex((v) => v.id === currentId)
|
|
96
|
+
);
|
|
97
|
+
const current = versions[i];
|
|
98
|
+
const last = versions.length - 1;
|
|
99
|
+
const ordered = [...versions].reverse();
|
|
100
|
+
const step = (d: number) => {
|
|
101
|
+
const next = versions[i + d];
|
|
102
|
+
if (next) onPick(next.id);
|
|
103
|
+
};
|
|
104
|
+
const arrow = mobile ? "h-11 w-11" : "h-[30px] w-[30px]";
|
|
105
|
+
const label = (
|
|
106
|
+
<>
|
|
107
|
+
{!mobile && <History className="h-3.5 w-3.5" />}
|
|
108
|
+
<span>
|
|
109
|
+
<b className="font-semibold">v{current.version}</b>
|
|
110
|
+
<span className="text-muted-foreground"> of {versions.length}</span>
|
|
111
|
+
</span>
|
|
112
|
+
<ChevronDown className="h-3.5 w-3.5 opacity-60" />
|
|
113
|
+
</>
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<div
|
|
118
|
+
className={cn(
|
|
119
|
+
"flex items-center",
|
|
120
|
+
mobile ? "w-full" : "h-8 shrink-0 rounded-lg border",
|
|
121
|
+
className
|
|
122
|
+
)}
|
|
123
|
+
>
|
|
124
|
+
<button
|
|
125
|
+
type="button"
|
|
126
|
+
className={cn(
|
|
127
|
+
"text-muted-foreground hover:text-foreground inline-flex items-center justify-center disabled:opacity-30",
|
|
128
|
+
arrow
|
|
129
|
+
)}
|
|
130
|
+
onClick={() => step(-1)}
|
|
131
|
+
disabled={i === 0}
|
|
132
|
+
aria-label="Older version"
|
|
133
|
+
>
|
|
134
|
+
<ChevronLeft className="h-4 w-4" />
|
|
135
|
+
</button>
|
|
136
|
+
{mobile ? (
|
|
137
|
+
<>
|
|
138
|
+
<button
|
|
139
|
+
type="button"
|
|
140
|
+
className="flex h-11 flex-1 items-center justify-center gap-1 text-[13px]"
|
|
141
|
+
onClick={() => setSheet(true)}
|
|
142
|
+
aria-label="Versions"
|
|
143
|
+
>
|
|
144
|
+
{label}
|
|
145
|
+
</button>
|
|
146
|
+
<Dialog open={sheet} onOpenChange={setSheet}>
|
|
147
|
+
<DialogContent className={cn(SHEET, "gap-1 p-2 pt-4")}>
|
|
148
|
+
<DialogTitle className="px-3 pb-1 text-sm">Versions</DialogTitle>
|
|
149
|
+
{ordered.map((v) => (
|
|
150
|
+
<button
|
|
151
|
+
key={v.id}
|
|
152
|
+
type="button"
|
|
153
|
+
className={cn(
|
|
154
|
+
"hover:bg-accent flex min-h-14 items-start gap-2.5 rounded-lg px-3 py-2 text-left",
|
|
155
|
+
v.id === current.id && "bg-accent"
|
|
156
|
+
)}
|
|
157
|
+
onClick={() => {
|
|
158
|
+
onPick(v.id);
|
|
159
|
+
setSheet(false);
|
|
160
|
+
}}
|
|
161
|
+
>
|
|
162
|
+
<Row
|
|
163
|
+
v={v}
|
|
164
|
+
latest={v.id === versions[last].id}
|
|
165
|
+
current={v.id === current.id}
|
|
166
|
+
full={false}
|
|
167
|
+
/>
|
|
168
|
+
</button>
|
|
169
|
+
))}
|
|
170
|
+
</DialogContent>
|
|
171
|
+
</Dialog>
|
|
172
|
+
</>
|
|
173
|
+
) : (
|
|
174
|
+
<DropdownMenu>
|
|
175
|
+
<DropdownMenuTrigger asChild>
|
|
176
|
+
<button
|
|
177
|
+
type="button"
|
|
178
|
+
className="flex h-full items-center gap-1 border-x px-2 text-[12.5px]"
|
|
179
|
+
aria-label="Versions"
|
|
180
|
+
>
|
|
181
|
+
{label}
|
|
182
|
+
</button>
|
|
183
|
+
</DropdownMenuTrigger>
|
|
184
|
+
<DropdownMenuContent align="end" className="w-[22rem] max-w-[90vw]">
|
|
185
|
+
{ordered.map((v) => (
|
|
186
|
+
<DropdownMenuItem
|
|
187
|
+
key={v.id}
|
|
188
|
+
className="items-start gap-2.5 py-2"
|
|
189
|
+
onSelect={() => onPick(v.id)}
|
|
190
|
+
>
|
|
191
|
+
<Row
|
|
192
|
+
v={v}
|
|
193
|
+
latest={v.id === versions[last].id}
|
|
194
|
+
current={v.id === current.id}
|
|
195
|
+
full
|
|
196
|
+
/>
|
|
197
|
+
</DropdownMenuItem>
|
|
198
|
+
))}
|
|
199
|
+
</DropdownMenuContent>
|
|
200
|
+
</DropdownMenu>
|
|
201
|
+
)}
|
|
202
|
+
<button
|
|
203
|
+
type="button"
|
|
204
|
+
className={cn(
|
|
205
|
+
"text-muted-foreground hover:text-foreground inline-flex items-center justify-center disabled:opacity-30",
|
|
206
|
+
arrow
|
|
207
|
+
)}
|
|
208
|
+
onClick={() => step(1)}
|
|
209
|
+
disabled={i === last}
|
|
210
|
+
aria-label="Newer version"
|
|
211
|
+
>
|
|
212
|
+
<ChevronRight className="h-4 w-4" />
|
|
213
|
+
</button>
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
}
|