@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.
Files changed (27) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +13 -0
  3. package/corpus/core/package.json +2 -1
  4. package/corpus/core/src/client/i18n.tsx +77 -65
  5. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -1
  6. package/corpus/templates/analytics/app/global.css +15 -0
  7. package/corpus/templates/analytics/app/pages/Ask.tsx +1 -1
  8. package/corpus/templates/analytics/changelog/2026-06-26-the-ask-tab-uses-a-softer-dark-gray-canvas-while-sidebar-cha.md +6 -0
  9. package/corpus/templates/clips/app/components/library/library-layout.tsx +325 -333
  10. package/corpus/templates/clips/app/components/player/delete-recording-menu.tsx +80 -40
  11. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +44 -3
  12. package/corpus/templates/clips/app/routes/share.$shareId.tsx +10 -2
  13. package/corpus/templates/clips/changelog/2026-06-26-recording-details-button-stays-at-the-far-right-on-small-screen.md +6 -0
  14. package/corpus/templates/clips/changelog/2026-06-26-the-agent-sidebar-now-slides-in-smoothly-without-duplicate-e.md +6 -0
  15. package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +56 -4
  16. package/corpus/templates/plan/app/global.css +8 -3
  17. package/corpus/templates/plan/app/pages/PlansPage.tsx +1 -1
  18. package/corpus/templates/plan/changelog/2026-06-26-canvas-grid-panning-now-stays-smooth-and-fills-the-workspace.md +6 -0
  19. package/dist/client/i18n.d.ts.map +1 -1
  20. package/dist/client/i18n.js +20 -9
  21. package/dist/client/i18n.js.map +1 -1
  22. package/dist/collab/routes.d.ts +1 -1
  23. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  24. package/dist/notifications/routes.d.ts +1 -1
  25. package/dist/observability/routes.d.ts +2 -2
  26. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  27. 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
- export function DeleteRecordingMenu({
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
- }: DeleteRecordingMenuProps) {
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
- <DropdownMenuItem
73
- onSelect={(event) => {
74
- event.preventDefault();
75
- setOpen(true);
76
- }}
77
- className="text-destructive focus:text-destructive"
78
- >
79
- <IconTrash className="mr-2 h-4 w-4" />
80
- {t("deleteRecordingMenu.delete")}
81
- </DropdownMenuItem>
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
- <AlertDialogContent>
85
- <AlertDialogHeader>
86
- <AlertDialogTitle>
87
- {t("deleteRecordingMenu.moveTitle")}
88
- </AlertDialogTitle>
89
- <AlertDialogDescription>
90
- {t("deleteRecordingMenu.moveDescription")}
91
- </AlertDialogDescription>
92
- </AlertDialogHeader>
93
- <AlertDialogFooter>
94
- <AlertDialogCancel disabled={trashRecording.isPending}>
95
- {t("common.cancel")}
96
- </AlertDialogCancel>
97
- <AlertDialogAction
98
- disabled={trashRecording.isPending}
99
- onClick={(event) => {
100
- event.preventDefault();
101
- handleTrashRecording();
102
- }}
103
- className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
104
- >
105
- {trashRecording.isPending
106
- ? t("deleteRecordingMenu.deleting")
107
- : t("deleteRecordingMenu.moveToTrash")}
108
- </AlertDialogAction>
109
- </AlertDialogFooter>
110
- </AlertDialogContent>
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 { DeleteRecordingMenu } from "@/components/player/delete-recording-menu";
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
- <DeleteRecordingMenu
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 { DeleteRecordingMenu } from "@/components/player/delete-recording-menu";
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
- <DeleteRecordingMenu
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}
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-06-26
4
+ ---
5
+
6
+ The recording details button stays at the far right on small screens.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-06-26
4
+ ---
5
+
6
+ The agent sidebar now slides in smoothly without duplicate edge borders.
@@ -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 = resolve(current);
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: `translate(${pan.x}px, ${pan.y}px) scale(${zoom})`,
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 (slightly darker than the document bg) that
259
- * moves on pan via background-position and scales with zoom both driven
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-viewport absolute inset-0"
7116
+ className="plan-canvas-grid absolute inset-0"
7117
7117
  style={{
7118
7118
  backgroundPosition: "96px 64px",
7119
7119
  backgroundSize: "20px 20px",
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-06-26
4
+ ---
5
+
6
+ Canvas grid panning now stays smooth and fills the workspace while navigating large plans.
@@ -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;AAInC,OAAO,EACL,cAAc,EACd,uBAAuB,EACvB,eAAe,EACf,kBAAkB,EAClB,iBAAiB,EACjB,eAAe,EACf,mBAAmB,EACnB,yBAAyB,EACzB,+BAA+B,EAC/B,2BAA2B,EAC3B,2BAA2B,EAC3B,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,sBAAsB,GAC5B,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAEhE,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAErD,MAAM,WAAW,sBAAsB;IACrC,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,UAAU,CAAC,EAAE,sBAAsB,CAAC;IACpC,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;IACpB,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B;AAED,MAAM,WAAW,sBAAsB;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;CACvE;AAED,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,OAAO,CAAC,EAAE,sBAAsB,CAAC;IACjC,aAAa,CAAC,EAAE,UAAU,CAAC;IAC3B,iBAAiB,CAAC,EAAE,sBAAsB,GAAG,gBAAgB,CAAC;IAC9D,eAAe,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;IACxC,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,UAAU,kBAAkB;IAC1B,MAAM,EAAE,UAAU,CAAC;IACnB,YAAY,EAAE,UAAU,CAAC;IACzB,UAAU,EAAE,gBAAgB,CAAC;IAC7B,GAAG,EAAE,KAAK,GAAG,KAAK,CAAC;IACnB,QAAQ,EAAE,cAAc,CAAC;IACzB,aAAa,EAAE,CAAC,UAAU,EAAE,gBAAgB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/D,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,uBAAuB,CAAC,EAAE,sBAAsB,CAAC;KAClD;CACF;AA+JD,wBAAgB,uBAAuB,CAAC,EACtC,QAAQ,EACR,OAAO,EACP,aAAa,EACb,iBAAiB,EACjB,eAAe,EACf,iBAAwB,GACzB,EAAE,4BAA4B,qBAyM9B;AAED,wBAAgB,SAAS,IAAI,kBAAkB,CAM9C;AAED,wBAAgB,iBAAiB,IAAI,kBAAkB,GAAG,IAAI,CAE7D;AA8ID,wBAAgB,IAAI,UAKV,MAAM,YAAY,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,YAYlD;AAED,wBAAgB,aAAa;sBAMd,IAAI,GAAG,MAAM,GAAG,MAAM,YACnB,IAAI,CAAC,qBAAqB;wBAMlB,MAAM,YAAY,IAAI,CAAC,mBAAmB;8BAIrD,MAAM,QACP,IAAI,CAAC,sBAAsB,YACvB,IAAI,CAAC,yBAAyB;sBAIxB,MAAM,EAAE,YAAY,IAAI,CAAC,iBAAiB;EAMjE;AAED,wBAAgB,cAAc,CAAC,EAC7B,SAAS,EACT,aAAoB,EACpB,KAAK,EACL,OAAkB,GACnB,EAAE;IACD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;CAC7B,qBAiGA"}
1
+ {"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../../src/client/i18n.tsx"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import * as SelectPrimitive from "@radix-ui/react-select";
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 === LOCALE_METADATA[code].englishName
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
- return (_jsx("div", { className: className, children: _jsxs(SelectPrimitive.Root, { value: preference, onValueChange: (value) => void setPreference(normalizeLocalizationPreference(value).locale), children: [_jsxs(SelectPrimitive.Trigger, { className: variant === "icon"
489
- ? "flex h-8 w-8 items-center justify-center rounded-md border border-border bg-background text-foreground outline-none transition-colors hover:bg-accent/40 data-[placeholder]:text-muted-foreground"
490
- : "flex h-9 w-full items-center justify-between rounded-md border border-border bg-background px-3 text-start text-[12px] text-foreground outline-none transition-colors hover:bg-accent/40 data-[placeholder]:text-muted-foreground", "aria-label": resolvedLabel, title: selected?.label ?? resolvedLabel, 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(SelectPrimitive.Value, { children: _jsx("span", { className: "truncate", children: selected?.label ?? preference }) })) : null] }), variant === "select" ? (_jsx(SelectPrimitive.Icon, { asChild: true, children: _jsx(IconChevronDown, { className: "h-3.5 w-3.5 text-muted-foreground" }) })) : null] }), _jsx(SelectPrimitive.Portal, { children: _jsx(SelectPrimitive.Content, { position: "popper", sideOffset: 6, className: variant === "icon"
491
- ? "z-[9999] min-w-56 overflow-hidden rounded-lg border border-border bg-popover shadow-lg"
492
- : "z-[9999] w-[var(--radix-select-trigger-width)] overflow-hidden rounded-lg border border-border bg-popover shadow-lg", children: _jsx(SelectPrimitive.Viewport, { className: "p-1", children: options.map((option) => (_jsxs(SelectPrimitive.Item, { value: option.value, className: "relative flex w-full cursor-pointer select-none items-start gap-2 rounded-md px-8 py-2.5 text-[12px] outline-none data-[highlighted]:bg-accent/60 data-[state=checked]:bg-accent/40", children: [_jsx("span", { className: "absolute start-2 top-2.5 flex h-4 w-4 items-center justify-center text-muted-foreground", children: _jsx(SelectPrimitive.ItemIndicator, { children: _jsx(IconCheck, { className: "h-3.5 w-3.5" }) }) }), _jsxs("div", { className: "flex min-w-0 flex-col", children: [_jsx(SelectPrimitive.ItemText, { children: _jsx("span", { className: "text-foreground", children: option.label }) }), _jsx("span", { className: "mt-0.5 text-[11px] leading-relaxed text-muted-foreground", children: option.description })] })] }, option.value))) }) }) })] }) }));
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