@agent-native/core 0.79.18 → 0.79.20
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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +13 -0
- package/corpus/core/package.json +2 -1
- package/corpus/core/src/client/i18n.tsx +77 -65
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -1
- package/corpus/templates/analytics/app/global.css +15 -0
- package/corpus/templates/analytics/app/pages/Ask.tsx +1 -1
- package/corpus/templates/analytics/changelog/2026-06-26-the-ask-tab-uses-a-softer-dark-gray-canvas-while-sidebar-cha.md +6 -0
- package/corpus/templates/clips/app/components/library/library-layout.tsx +325 -333
- package/corpus/templates/clips/app/components/player/delete-recording-menu.tsx +80 -40
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +44 -3
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +10 -2
- package/corpus/templates/clips/changelog/2026-06-26-recording-details-button-stays-at-the-far-right-on-small-screen.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-26-the-agent-sidebar-now-slides-in-smoothly-without-duplicate-e.md +6 -0
- package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +56 -4
- package/corpus/templates/plan/app/global.css +8 -3
- package/corpus/templates/plan/app/pages/PlansPage.tsx +1 -1
- package/corpus/templates/plan/changelog/2026-06-26-canvas-grid-panning-now-stays-smooth-and-fills-the-workspace.md +6 -0
- package/dist/client/i18n.d.ts.map +1 -1
- package/dist/client/i18n.js +20 -9
- package/dist/client/i18n.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +2 -2
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/package.json +2 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useActionMutation, useT } from "@agent-native/core/client";
|
|
2
|
-
import { IconDots, IconTrash } from "@tabler/icons-react";
|
|
2
|
+
import { IconDots, IconDownload, IconTrash } from "@tabler/icons-react";
|
|
3
3
|
import { useCallback, useState } from "react";
|
|
4
4
|
import { toast } from "sonner";
|
|
5
5
|
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
DropdownMenu,
|
|
19
19
|
DropdownMenuContent,
|
|
20
20
|
DropdownMenuItem,
|
|
21
|
+
DropdownMenuSeparator,
|
|
21
22
|
DropdownMenuTrigger,
|
|
22
23
|
} from "@/components/ui/dropdown-menu";
|
|
23
24
|
|
|
@@ -26,12 +27,29 @@ interface DeleteRecordingMenuProps {
|
|
|
26
27
|
onDeleted?: () => void;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
interface RecordingOptionsMenuProps extends DeleteRecordingMenuProps {
|
|
31
|
+
canDelete?: boolean;
|
|
32
|
+
canDownload?: boolean;
|
|
33
|
+
downloadPending?: boolean;
|
|
34
|
+
downloadLabel?: string;
|
|
35
|
+
downloadingLabel?: string;
|
|
36
|
+
onDownload?: () => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function RecordingOptionsMenu({
|
|
30
40
|
recordingId,
|
|
31
41
|
onDeleted,
|
|
32
|
-
|
|
42
|
+
canDelete = true,
|
|
43
|
+
canDownload = false,
|
|
44
|
+
downloadPending = false,
|
|
45
|
+
downloadLabel,
|
|
46
|
+
downloadingLabel,
|
|
47
|
+
onDownload,
|
|
48
|
+
}: RecordingOptionsMenuProps) {
|
|
33
49
|
const t = useT();
|
|
34
50
|
const [open, setOpen] = useState(false);
|
|
51
|
+
const showDownload = canDownload && Boolean(onDownload);
|
|
52
|
+
const showDelete = canDelete;
|
|
35
53
|
const trashRecording = useActionMutation<any, { id: string }>(
|
|
36
54
|
"trash-recording",
|
|
37
55
|
{
|
|
@@ -50,6 +68,8 @@ export function DeleteRecordingMenu({
|
|
|
50
68
|
trashRecording.mutate({ id: recordingId });
|
|
51
69
|
}, [recordingId, trashRecording]);
|
|
52
70
|
|
|
71
|
+
if (!showDownload && !showDelete) return null;
|
|
72
|
+
|
|
53
73
|
return (
|
|
54
74
|
<AlertDialog
|
|
55
75
|
open={open}
|
|
@@ -69,45 +89,65 @@ export function DeleteRecordingMenu({
|
|
|
69
89
|
</Button>
|
|
70
90
|
</DropdownMenuTrigger>
|
|
71
91
|
<DropdownMenuContent align="end" className="w-44">
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
92
|
+
{showDownload ? (
|
|
93
|
+
<DropdownMenuItem
|
|
94
|
+
onSelect={() => onDownload?.()}
|
|
95
|
+
disabled={downloadPending}
|
|
96
|
+
>
|
|
97
|
+
<IconDownload className="me-2 h-4 w-4" />
|
|
98
|
+
{downloadPending
|
|
99
|
+
? (downloadingLabel ?? t("sharePage.downloading"))
|
|
100
|
+
: (downloadLabel ?? t("sharePage.downloadMp4"))}
|
|
101
|
+
</DropdownMenuItem>
|
|
102
|
+
) : null}
|
|
103
|
+
{showDownload && showDelete ? <DropdownMenuSeparator /> : null}
|
|
104
|
+
{showDelete ? (
|
|
105
|
+
<DropdownMenuItem
|
|
106
|
+
onSelect={(event) => {
|
|
107
|
+
event.preventDefault();
|
|
108
|
+
setOpen(true);
|
|
109
|
+
}}
|
|
110
|
+
className="text-destructive focus:text-destructive"
|
|
111
|
+
>
|
|
112
|
+
<IconTrash className="me-2 h-4 w-4" />
|
|
113
|
+
{t("deleteRecordingMenu.delete")}
|
|
114
|
+
</DropdownMenuItem>
|
|
115
|
+
) : null}
|
|
82
116
|
</DropdownMenuContent>
|
|
83
117
|
</DropdownMenu>
|
|
84
|
-
|
|
85
|
-
<
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
<
|
|
95
|
-
{
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
event
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
118
|
+
{showDelete ? (
|
|
119
|
+
<AlertDialogContent>
|
|
120
|
+
<AlertDialogHeader>
|
|
121
|
+
<AlertDialogTitle>
|
|
122
|
+
{t("deleteRecordingMenu.moveTitle")}
|
|
123
|
+
</AlertDialogTitle>
|
|
124
|
+
<AlertDialogDescription>
|
|
125
|
+
{t("deleteRecordingMenu.moveDescription")}
|
|
126
|
+
</AlertDialogDescription>
|
|
127
|
+
</AlertDialogHeader>
|
|
128
|
+
<AlertDialogFooter>
|
|
129
|
+
<AlertDialogCancel disabled={trashRecording.isPending}>
|
|
130
|
+
{t("common.cancel")}
|
|
131
|
+
</AlertDialogCancel>
|
|
132
|
+
<AlertDialogAction
|
|
133
|
+
disabled={trashRecording.isPending}
|
|
134
|
+
onClick={(event) => {
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
handleTrashRecording();
|
|
137
|
+
}}
|
|
138
|
+
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
139
|
+
>
|
|
140
|
+
{trashRecording.isPending
|
|
141
|
+
? t("deleteRecordingMenu.deleting")
|
|
142
|
+
: t("deleteRecordingMenu.moveToTrash")}
|
|
143
|
+
</AlertDialogAction>
|
|
144
|
+
</AlertDialogFooter>
|
|
145
|
+
</AlertDialogContent>
|
|
146
|
+
) : null}
|
|
111
147
|
</AlertDialog>
|
|
112
148
|
);
|
|
113
149
|
}
|
|
150
|
+
|
|
151
|
+
export function DeleteRecordingMenu(props: DeleteRecordingMenuProps) {
|
|
152
|
+
return <RecordingOptionsMenu {...props} canDelete />;
|
|
153
|
+
}
|
|
@@ -34,7 +34,7 @@ import { toast } from "sonner";
|
|
|
34
34
|
import { EditableRecordingTitle } from "@/components/editable-recording-title";
|
|
35
35
|
import { EditorLayout } from "@/components/editor/editor-layout";
|
|
36
36
|
import { CommentsPanel } from "@/components/player/comments-panel";
|
|
37
|
-
import {
|
|
37
|
+
import { RecordingOptionsMenu } from "@/components/player/delete-recording-menu";
|
|
38
38
|
import { InsightsPanel } from "@/components/player/insights-panel";
|
|
39
39
|
import { ReactionsTray } from "@/components/player/reactions-tray";
|
|
40
40
|
import { SettingsPanel } from "@/components/player/settings-panel";
|
|
@@ -212,6 +212,7 @@ export default function RecordingPage() {
|
|
|
212
212
|
// can retry or report the issue instead of staring at a spinner.
|
|
213
213
|
const [processingTimeout, setProcessingTimeout] = useState(false);
|
|
214
214
|
const [retryingFinalize, setRetryingFinalize] = useState(false);
|
|
215
|
+
const [downloading, setDownloading] = useState(false);
|
|
215
216
|
|
|
216
217
|
useEffect(() => {
|
|
217
218
|
if (
|
|
@@ -306,6 +307,30 @@ export default function RecordingPage() {
|
|
|
306
307
|
const isLoomRecording = isLoomRecordingSource(recording);
|
|
307
308
|
const canUseNativeEditor = canEdit && !isLoomEmbedBacked;
|
|
308
309
|
const canDelete = role === "owner";
|
|
310
|
+
const canDownloadRecording = Boolean(
|
|
311
|
+
recording?.enableDownloads && recording.videoUrl && !isLoomEmbedBacked,
|
|
312
|
+
);
|
|
313
|
+
const downloadRecording = useCallback(async () => {
|
|
314
|
+
if (!recording?.videoUrl) return;
|
|
315
|
+
setDownloading(true);
|
|
316
|
+
try {
|
|
317
|
+
const res = await fetch(recording.videoUrl);
|
|
318
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
319
|
+
const blob = await res.blob();
|
|
320
|
+
const url = URL.createObjectURL(blob);
|
|
321
|
+
const a = document.createElement("a");
|
|
322
|
+
a.href = url;
|
|
323
|
+
a.download = `${sanitizeFilename(recording.title || "clip")}.mp4`;
|
|
324
|
+
document.body.appendChild(a);
|
|
325
|
+
a.click();
|
|
326
|
+
a.remove();
|
|
327
|
+
URL.revokeObjectURL(url);
|
|
328
|
+
} catch {
|
|
329
|
+
window.open(recording.videoUrl, "_blank", "noopener,noreferrer");
|
|
330
|
+
} finally {
|
|
331
|
+
setDownloading(false);
|
|
332
|
+
}
|
|
333
|
+
}, [recording?.title, recording?.videoUrl]);
|
|
309
334
|
const retryFinalizeAfterStorage = useCallback(async () => {
|
|
310
335
|
if (!recordingId) return;
|
|
311
336
|
setRetryingFinalize(true);
|
|
@@ -1011,9 +1036,15 @@ export default function RecordingPage() {
|
|
|
1011
1036
|
</Button>
|
|
1012
1037
|
</ShareRecordingPopover>
|
|
1013
1038
|
|
|
1014
|
-
{canDelete ? (
|
|
1015
|
-
<
|
|
1039
|
+
{canDelete || canDownloadRecording ? (
|
|
1040
|
+
<RecordingOptionsMenu
|
|
1016
1041
|
recordingId={recording.id}
|
|
1042
|
+
canDelete={canDelete}
|
|
1043
|
+
canDownload={canDownloadRecording}
|
|
1044
|
+
downloadPending={downloading}
|
|
1045
|
+
onDownload={() => {
|
|
1046
|
+
void downloadRecording();
|
|
1047
|
+
}}
|
|
1017
1048
|
onDeleted={() => navigate("/library", { replace: true })}
|
|
1018
1049
|
/>
|
|
1019
1050
|
) : null}
|
|
@@ -1284,6 +1315,16 @@ function displayRecordingTitle(title: string | null | undefined): string {
|
|
|
1284
1315
|
return isDefaultTitle(title) ? "Untitled Clip" : (title ?? "").trim();
|
|
1285
1316
|
}
|
|
1286
1317
|
|
|
1318
|
+
function sanitizeFilename(name: string): string {
|
|
1319
|
+
return (
|
|
1320
|
+
name
|
|
1321
|
+
.trim()
|
|
1322
|
+
.replace(/[^\w.-]+/g, "-")
|
|
1323
|
+
.replace(/^-+|-+$/g, "")
|
|
1324
|
+
.slice(0, 80) || "clip"
|
|
1325
|
+
);
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1287
1328
|
function shouldShowGeneratedTitleSkeleton(
|
|
1288
1329
|
recording: { title: string | null | undefined; createdAt?: string | null },
|
|
1289
1330
|
transcriptStatus?: string,
|
|
@@ -38,7 +38,7 @@ import { useLoaderData, useNavigate, useParams } from "react-router";
|
|
|
38
38
|
import { CaptureInstallButton } from "@/components/capture-install-options";
|
|
39
39
|
import { AccessPasswordPrompt } from "@/components/player/access-password-prompt";
|
|
40
40
|
import { CommentsPanel } from "@/components/player/comments-panel";
|
|
41
|
-
import {
|
|
41
|
+
import { RecordingOptionsMenu } from "@/components/player/delete-recording-menu";
|
|
42
42
|
import { ReactionsTray } from "@/components/player/reactions-tray";
|
|
43
43
|
import { ShareRecordingPopover } from "@/components/player/share-dialog";
|
|
44
44
|
import { SignInPromptDialog } from "@/components/player/sign-in-prompt-dialog";
|
|
@@ -797,8 +797,16 @@ export default function ShareRoute() {
|
|
|
797
797
|
</DropdownMenu>
|
|
798
798
|
) : null}
|
|
799
799
|
{viewerIsOwner ? (
|
|
800
|
-
<
|
|
800
|
+
<RecordingOptionsMenu
|
|
801
801
|
recordingId={recording.id}
|
|
802
|
+
canDelete
|
|
803
|
+
canDownload={canDownloadRecording}
|
|
804
|
+
downloadPending={downloading}
|
|
805
|
+
downloadLabel={t("sharePage.downloadMp4")}
|
|
806
|
+
downloadingLabel={t("sharePage.downloading")}
|
|
807
|
+
onDownload={() => {
|
|
808
|
+
void downloadRecording();
|
|
809
|
+
}}
|
|
802
810
|
onDeleted={() => navigate("/library", { replace: true })}
|
|
803
811
|
/>
|
|
804
812
|
) : null}
|
|
@@ -43,6 +43,8 @@ const WHEEL_ZOOM_STEP = 0.16;
|
|
|
43
43
|
const PINCH_ZOOM_SENSITIVITY = 0.01;
|
|
44
44
|
/** Base CSS grid cell, scaled by zoom. */
|
|
45
45
|
const GRID_CELL = 28;
|
|
46
|
+
/** Extra world-space grid on each side so the grid still fills overscrolled pans. */
|
|
47
|
+
const GRID_PADDING = 5000;
|
|
46
48
|
|
|
47
49
|
type CanvasView = typeof DEFAULT_VIEW;
|
|
48
50
|
export type CanvasViewport = CanvasView;
|
|
@@ -156,6 +158,9 @@ export function CanvasArea({
|
|
|
156
158
|
);
|
|
157
159
|
const latestViewportChangeRef = useRef<CanvasViewport>(initialView);
|
|
158
160
|
const viewportChangeFrameRef = useRef<number | null>(null);
|
|
161
|
+
// Kept current each render so the central pan clamp (in updateView) can read
|
|
162
|
+
// the live board size without widening updateView's dependencies.
|
|
163
|
+
const boardRef = useRef({ width: 0, height: 0 });
|
|
159
164
|
const queueViewportChange = useCallback(
|
|
160
165
|
(nextView: CanvasViewport) => {
|
|
161
166
|
latestViewportChangeRef.current = nextView;
|
|
@@ -171,7 +176,11 @@ export function CanvasArea({
|
|
|
171
176
|
const updateView = useCallback(
|
|
172
177
|
(resolve: (current: CanvasView) => CanvasView) => {
|
|
173
178
|
setView((current) => {
|
|
174
|
-
const next =
|
|
179
|
+
const next = clampPanToGrid(
|
|
180
|
+
resolve(current),
|
|
181
|
+
boardRef.current,
|
|
182
|
+
viewportRef.current?.getBoundingClientRect() ?? null,
|
|
183
|
+
);
|
|
175
184
|
if (sameCanvasView(current, next)) return current;
|
|
176
185
|
queueViewportChange(next);
|
|
177
186
|
return next;
|
|
@@ -332,6 +341,7 @@ export function CanvasArea({
|
|
|
332
341
|
);
|
|
333
342
|
return { width: maxX + 360, height: maxY + 280 };
|
|
334
343
|
}, [frames, annotations, legacyNotes]);
|
|
344
|
+
boardRef.current = board;
|
|
335
345
|
|
|
336
346
|
const lastAutoFitKeyRef = useRef<string | null>(null);
|
|
337
347
|
useEffect(() => {
|
|
@@ -385,6 +395,7 @@ export function CanvasArea({
|
|
|
385
395
|
}, [frameLayoutKey, frames, hasSavedViewport, updateView]);
|
|
386
396
|
|
|
387
397
|
const { zoom, pan } = view;
|
|
398
|
+
const worldTransform = `translate3d(${pan.x}px, ${pan.y}px, 0) scale(${zoom})`;
|
|
388
399
|
useEffect(() => {
|
|
389
400
|
queueViewportChange(view);
|
|
390
401
|
}, [queueViewportChange, view]);
|
|
@@ -656,8 +667,6 @@ export function CanvasArea({
|
|
|
656
667
|
tabIndex={0}
|
|
657
668
|
style={
|
|
658
669
|
{
|
|
659
|
-
backgroundPosition: `${pan.x}px ${pan.y}px`,
|
|
660
|
-
backgroundSize: `${GRID_CELL * zoom}px ${GRID_CELL * zoom}px`,
|
|
661
670
|
overscrollBehavior: "contain",
|
|
662
671
|
touchAction: "none",
|
|
663
672
|
} as CSSProperties
|
|
@@ -724,11 +733,25 @@ export function CanvasArea({
|
|
|
724
733
|
style={{
|
|
725
734
|
width: board.width,
|
|
726
735
|
height: board.height,
|
|
727
|
-
transform:
|
|
736
|
+
transform: worldTransform,
|
|
728
737
|
transformOrigin: "0 0",
|
|
729
738
|
willChange: "transform",
|
|
739
|
+
backfaceVisibility: "hidden",
|
|
730
740
|
}}
|
|
731
741
|
>
|
|
742
|
+
<div
|
|
743
|
+
aria-hidden="true"
|
|
744
|
+
className="plan-canvas-grid absolute"
|
|
745
|
+
data-plan-canvas-grid
|
|
746
|
+
style={{
|
|
747
|
+
left: -GRID_PADDING,
|
|
748
|
+
top: -GRID_PADDING,
|
|
749
|
+
width: board.width + GRID_PADDING * 2,
|
|
750
|
+
height: board.height + GRID_PADDING * 2,
|
|
751
|
+
backgroundSize: `${GRID_CELL}px ${GRID_CELL}px`,
|
|
752
|
+
}}
|
|
753
|
+
/>
|
|
754
|
+
|
|
732
755
|
{/* Section containers sit BEHIND the frames (lowest layer) so each
|
|
733
756
|
group reads as one bounded region the artboards rest inside. */}
|
|
734
757
|
{sectionRects.map(({ section, rect }) => (
|
|
@@ -2293,3 +2316,32 @@ function resolveMarkupComposerPosition(input: {
|
|
|
2293
2316
|
function clamp(value: number, min: number, max: number) {
|
|
2294
2317
|
return Math.max(min, Math.min(max, value));
|
|
2295
2318
|
}
|
|
2319
|
+
|
|
2320
|
+
/**
|
|
2321
|
+
* Keep the visible viewport inside the rendered grid. The grid is a fixed-size
|
|
2322
|
+
* child of the transformed world (board + GRID_PADDING on every side), so it is
|
|
2323
|
+
* large but finite; without this clamp a far-enough pan scrolls past the grid
|
|
2324
|
+
* edge into a blank void. Screen = pan + world * zoom, and the grid spans world
|
|
2325
|
+
* [-GRID_PADDING, board + GRID_PADDING], so bounding pan to the range below
|
|
2326
|
+
* guarantees the grid always fills the viewport while still allowing the full
|
|
2327
|
+
* GRID_PADDING of overscroll. A no-op until the viewport has been measured.
|
|
2328
|
+
*/
|
|
2329
|
+
function clampPanToGrid(
|
|
2330
|
+
view: CanvasView,
|
|
2331
|
+
board: { width: number; height: number },
|
|
2332
|
+
rect: DOMRect | null,
|
|
2333
|
+
): CanvasView {
|
|
2334
|
+
if (!rect) return view;
|
|
2335
|
+
const { zoom } = view;
|
|
2336
|
+
const minPanX = rect.width - (board.width + GRID_PADDING) * zoom;
|
|
2337
|
+
const maxPanX = GRID_PADDING * zoom;
|
|
2338
|
+
const minPanY = rect.height - (board.height + GRID_PADDING) * zoom;
|
|
2339
|
+
const maxPanY = GRID_PADDING * zoom;
|
|
2340
|
+
return {
|
|
2341
|
+
zoom,
|
|
2342
|
+
pan: {
|
|
2343
|
+
x: minPanX <= maxPanX ? clamp(view.pan.x, minPanX, maxPanX) : view.pan.x,
|
|
2344
|
+
y: minPanY <= maxPanY ? clamp(view.pan.y, minPanY, maxPanY) : view.pan.y,
|
|
2345
|
+
},
|
|
2346
|
+
};
|
|
2347
|
+
}
|
|
@@ -255,13 +255,18 @@
|
|
|
255
255
|
touch-action: none;
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
-
/* Infinite low-contrast grid
|
|
259
|
-
*
|
|
260
|
-
* inline by CanvasArea. */
|
|
258
|
+
/* Infinite low-contrast grid. The live grid is rendered inside the transformed
|
|
259
|
+
* canvas world so Chrome can composite grid + artboards together while panning. */
|
|
261
260
|
.plan-canvas-viewport {
|
|
261
|
+
isolation: isolate;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.plan-canvas-grid {
|
|
265
|
+
pointer-events: none;
|
|
262
266
|
background-image:
|
|
263
267
|
linear-gradient(var(--plan-grid-line) 1px, transparent 1px),
|
|
264
268
|
linear-gradient(90deg, var(--plan-grid-line) 1px, transparent 1px);
|
|
269
|
+
backface-visibility: hidden;
|
|
265
270
|
}
|
|
266
271
|
|
|
267
272
|
/* Fixed-size static artboard frame. No border / shadow / box around the
|
|
@@ -7113,7 +7113,7 @@ function PlanCanvasSkeleton() {
|
|
|
7113
7113
|
aria-hidden="true"
|
|
7114
7114
|
>
|
|
7115
7115
|
<div
|
|
7116
|
-
className="plan-canvas-
|
|
7116
|
+
className="plan-canvas-grid absolute inset-0"
|
|
7117
7117
|
style={{
|
|
7118
7118
|
backgroundPosition: "96px 64px",
|
|
7119
7119
|
backgroundSize: "20px 20px",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../../src/client/i18n.tsx"],"names":[],"mappings":"AAGA,OAAO,KAQN,MAAM,OAAO,CAAC;AAQf,OAAO,EAUL,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,sBAAsB,EAC5B,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../../src/client/i18n.tsx"],"names":[],"mappings":"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"}
|
package/dist/client/i18n.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import * as
|
|
2
|
+
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
|
3
3
|
import { IconCheck, IconChevronDown, IconLanguage } from "@tabler/icons-react";
|
|
4
4
|
import i18next from "i18next";
|
|
5
5
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react";
|
|
@@ -8,6 +8,7 @@ import defaultEnglishMessages from "../localization/default-messages.js";
|
|
|
8
8
|
import { DEFAULT_LOCALE, LOCALE_HYDRATION_GLOBAL, LOCALE_METADATA, LOCALE_STORAGE_KEY, SUPPORTED_LOCALES, localeDirection, normalizeLocalizationPreference, resolveLocaleFromCandidates, resolveLocaleFromPreference, } from "../localization/shared.js";
|
|
9
9
|
import { setClientAppState } from "./application-state.js";
|
|
10
10
|
import { callAction } from "./use-action.js";
|
|
11
|
+
import { cn } from "./utils.js";
|
|
11
12
|
export { DEFAULT_LOCALE, LOCALE_HYDRATION_GLOBAL, LOCALE_METADATA, LOCALE_STORAGE_KEY, SUPPORTED_LOCALES, localeDirection, normalizeLocaleCode, normalizeLocalePreference, normalizeLocalizationPreference, resolveLocaleFromCandidates, resolveLocaleFromPreference, } from "../localization/shared.js";
|
|
12
13
|
export { getLocaleInitScript } from "../localization/server.js";
|
|
13
14
|
const LocaleContext = createContext(null);
|
|
@@ -464,6 +465,7 @@ export function useFormatters() {
|
|
|
464
465
|
}
|
|
465
466
|
export function LanguagePicker({ className, includeSystem = true, label, variant = "select", }) {
|
|
466
467
|
const { locale, preference, setPreference } = useLocale();
|
|
468
|
+
const [open, setOpen] = useState(false);
|
|
467
469
|
const copy = LANGUAGE_PICKER_COPY[locale] ?? LANGUAGE_PICKER_COPY[DEFAULT_LOCALE];
|
|
468
470
|
const resolvedLabel = label ?? copy.label;
|
|
469
471
|
const options = [
|
|
@@ -478,17 +480,26 @@ export function LanguagePicker({ className, includeSystem = true, label, variant
|
|
|
478
480
|
: []),
|
|
479
481
|
...SUPPORTED_LOCALES.map((code) => ({
|
|
480
482
|
value: code,
|
|
481
|
-
label: LOCALE_METADATA[code].nativeName
|
|
482
|
-
? LOCALE_METADATA[code].nativeName
|
|
483
|
-
: `${LOCALE_METADATA[code].nativeName} (${LOCALE_METADATA[code].englishName})`,
|
|
483
|
+
label: `${LOCALE_METADATA[code].nativeName} (${code})`,
|
|
484
484
|
description: code,
|
|
485
485
|
})),
|
|
486
486
|
];
|
|
487
487
|
const selected = options.find((option) => option.value === preference);
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
488
|
+
const selectedLabel = selected?.label ?? preference;
|
|
489
|
+
const triggerLabel = `${resolvedLabel}: ${selectedLabel}`;
|
|
490
|
+
function handleOptionClick(value) {
|
|
491
|
+
setOpen(false);
|
|
492
|
+
void setPreference(normalizeLocalizationPreference(value).locale);
|
|
493
|
+
}
|
|
494
|
+
return (_jsx("div", { className: className, children: _jsxs(PopoverPrimitive.Root, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverPrimitive.Trigger, { asChild: true, children: _jsxs("button", { type: "button", "aria-label": triggerLabel, title: triggerLabel, "data-language-picker-trigger": true, className: cn("shrink-0 rounded-md border border-border bg-background text-foreground outline-none transition-colors hover:border-foreground/30 hover:bg-accent/40 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 data-[state=open]:border-foreground/30 data-[state=open]:bg-accent/40", variant === "icon"
|
|
495
|
+
? "flex h-8 w-8 items-center justify-center"
|
|
496
|
+
: "flex h-9 w-full items-center justify-between gap-2 px-3 text-start text-sm"), children: [_jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [_jsx(IconLanguage, { className: "h-4 w-4 shrink-0 text-muted-foreground" }), variant === "select" ? (_jsx("span", { className: "truncate", children: selectedLabel })) : (_jsx("span", { className: "sr-only", children: triggerLabel }))] }), variant === "select" ? (_jsx(IconChevronDown, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground", "aria-hidden": "true" })) : null] }) }), _jsx(PopoverPrimitive.Portal, { children: _jsx(PopoverPrimitive.Content, { align: variant === "icon" ? "end" : "start", sideOffset: 6, role: "menu", className: cn("z-[9999] max-h-[min(20rem,var(--radix-popover-content-available-height))] overflow-y-auto rounded-lg border border-border bg-popover p-1 text-popover-foreground shadow-lg outline-none will-change-[transform,opacity] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:duration-100 data-[state=open]:duration-150 data-[state=closed]:ease-in data-[state=open]:ease-out data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1", variant === "icon"
|
|
497
|
+
? "min-w-56"
|
|
498
|
+
: "w-[min(20rem,calc(100vw-2rem))] min-w-[var(--radix-popover-trigger-width)]"), children: options.map((option) => {
|
|
499
|
+
const optionSelected = option.value === preference;
|
|
500
|
+
return (_jsxs("button", { type: "button", role: "menuitemradio", "aria-checked": optionSelected, title: option.description, onClick: () => handleOptionClick(option.value), className: cn("flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-start text-sm outline-none transition-colors hover:bg-accent/60 hover:text-foreground focus-visible:bg-accent/60 focus-visible:text-foreground", optionSelected
|
|
501
|
+
? "bg-accent/40 text-foreground"
|
|
502
|
+
: "text-muted-foreground"), children: [_jsx(IconCheck, { className: cn("h-3.5 w-3.5 shrink-0", optionSelected ? "opacity-100" : "opacity-0"), "aria-hidden": "true" }), _jsx("span", { className: "truncate", children: option.label })] }, option.value));
|
|
503
|
+
}) }) })] }) }));
|
|
493
504
|
}
|
|
494
505
|
//# sourceMappingURL=i18n.js.map
|