@agent-native/core 0.137.6 → 0.137.8
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/templates/clips/app/components/recorder/pre-record-panel.tsx +65 -18
- package/corpus/templates/clips/app/i18n/en-US.ts +4 -0
- package/corpus/templates/clips/desktop/package.json +1 -0
- package/corpus/templates/clips/desktop/src/app.tsx +70 -6
- package/corpus/templates/clips/desktop/src/components/AlertDialog.tsx +131 -0
- package/corpus/templates/clips/desktop/src/styles.css +129 -0
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +2 -0
- package/corpus/templates/design/actions/generate-design.ts +3 -2
- package/corpus/templates/design/actions/update-design.ts +35 -14
- package/corpus/templates/design/app/components/design/DesignImportPanel.tsx +7 -2
- package/corpus/templates/design/app/components/design/FigmaHydrationDialog.tsx +11 -2
- package/corpus/templates/design/app/components/editor/PromptDialog.tsx +8 -1
- package/corpus/templates/design/app/i18n-data.ts +53 -22
- package/corpus/templates/design/app/lib/design-file-upload.ts +2 -3
- package/corpus/templates/design/app/lib/upload-limits.ts +4 -0
- package/corpus/templates/design/changelog/2026-08-05-chat-attachments-now-warn-before-upload-when-they-exceed-the.md +6 -0
- package/corpus/templates/design/changelog/2026-08-05-figma-fig-uploads-now-show-a-clear-4-mb-limit-up-front-inste.md +6 -0
- package/corpus/templates/design/server/handlers/import-design-file.ts +9 -5
- package/corpus/templates/design/server/handlers/uploads.ts +13 -9
- package/corpus/templates/design/server/lib/fig-file-limits.ts +2 -1
- package/corpus/templates/design/server/lib/figma-image-hydration.ts +1 -1
- package/corpus/templates/design/server/lib/request-body-limits.ts +11 -0
- package/corpus/templates/design/shared/canvas-frames.ts +81 -0
- package/dist/client/AssistantChat.js +1 -0
- package/dist/client/chat/message-components.d.ts +10 -1
- package/dist/client/chat/message-components.js +13 -4
- package/dist/client/chat/runtime.js +20 -9
- package/dist/client/chat/tool-call-display.d.ts +7 -0
- package/dist/client/chat/tool-call-display.js +9 -1
- package/dist/client/require-session.js +13 -4
- package/dist/client/sse-event-processor.d.ts +2 -0
- package/dist/client/sse-event-processor.js +39 -1
- package/dist/client/use-session.d.ts +11 -0
- package/dist/client/use-session.js +34 -6
- package/dist/server/realtime-token.d.ts +1 -1
- package/package.json +1 -1
package/corpus/README.md
CHANGED
|
@@ -14,6 +14,16 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
14
14
|
|
|
15
15
|
import { CaptureInstallInlineLink } from "@/components/capture-install-options";
|
|
16
16
|
import { ImportMenu } from "@/components/import-menu";
|
|
17
|
+
import {
|
|
18
|
+
AlertDialog,
|
|
19
|
+
AlertDialogAction,
|
|
20
|
+
AlertDialogCancel,
|
|
21
|
+
AlertDialogContent,
|
|
22
|
+
AlertDialogDescription,
|
|
23
|
+
AlertDialogFooter,
|
|
24
|
+
AlertDialogHeader,
|
|
25
|
+
AlertDialogTitle,
|
|
26
|
+
} from "@/components/ui/alert-dialog";
|
|
17
27
|
import { Button } from "@/components/ui/button";
|
|
18
28
|
import {
|
|
19
29
|
Collapsible,
|
|
@@ -426,6 +436,35 @@ export function PreRecordPanel({
|
|
|
426
436
|
);
|
|
427
437
|
}, [micId, micLabel, mics, t]);
|
|
428
438
|
|
|
439
|
+
const [micWarningOpen, setMicWarningOpen] = useState(false);
|
|
440
|
+
|
|
441
|
+
const buildStartOpts = useCallback(
|
|
442
|
+
() => ({
|
|
443
|
+
// If the user toggled off the camera inside screen+camera mode,
|
|
444
|
+
// downgrade to screen-only so the recorder engine doesn't try
|
|
445
|
+
// to acquire a webcam stream.
|
|
446
|
+
mode: (mode === "screen+camera" && !needsCamera
|
|
447
|
+
? "screen"
|
|
448
|
+
: mode) as RecordingMode,
|
|
449
|
+
displaySurface: normalizeDisplaySurfaceForRuntime(displaySurface),
|
|
450
|
+
micDeviceId: micId === "default" ? null : micId,
|
|
451
|
+
micDeviceLabel:
|
|
452
|
+
micId === "default" || micId === NO_MIC_DEVICE_ID
|
|
453
|
+
? null
|
|
454
|
+
: selectedMicLabel,
|
|
455
|
+
cameraDeviceId: needsCamera && cameraId !== "default" ? cameraId : null,
|
|
456
|
+
}),
|
|
457
|
+
[mode, needsCamera, displaySurface, micId, selectedMicLabel, cameraId],
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
const handleStartClick = useCallback(() => {
|
|
461
|
+
if (micId === NO_MIC_DEVICE_ID) {
|
|
462
|
+
setMicWarningOpen(true);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
onStart(buildStartOpts());
|
|
466
|
+
}, [micId, buildStartOpts, onStart]);
|
|
467
|
+
|
|
429
468
|
const selectedCameraLabel = useMemo(() => {
|
|
430
469
|
if (!needsCamera) return null;
|
|
431
470
|
if (cameraId === "default") return t("preRecord.defaultCamera");
|
|
@@ -893,24 +932,7 @@ export function PreRecordPanel({
|
|
|
893
932
|
)}
|
|
894
933
|
<Button
|
|
895
934
|
disabled={startDisabled}
|
|
896
|
-
onClick={
|
|
897
|
-
onStart({
|
|
898
|
-
// If the user toggled off the camera inside screen+camera mode,
|
|
899
|
-
// downgrade to screen-only so the recorder engine doesn't try
|
|
900
|
-
// to acquire a webcam stream.
|
|
901
|
-
mode:
|
|
902
|
-
mode === "screen+camera" && !needsCamera ? "screen" : mode,
|
|
903
|
-
displaySurface:
|
|
904
|
-
normalizeDisplaySurfaceForRuntime(displaySurface),
|
|
905
|
-
micDeviceId: micId === "default" ? null : micId,
|
|
906
|
-
micDeviceLabel:
|
|
907
|
-
micId === "default" || micId === NO_MIC_DEVICE_ID
|
|
908
|
-
? null
|
|
909
|
-
: selectedMicLabel,
|
|
910
|
-
cameraDeviceId:
|
|
911
|
-
needsCamera && cameraId !== "default" ? cameraId : null,
|
|
912
|
-
})
|
|
913
|
-
}
|
|
935
|
+
onClick={handleStartClick}
|
|
914
936
|
className={cn("h-12", onCancel ? "flex-1" : "w-full")}
|
|
915
937
|
>
|
|
916
938
|
{t("preRecord.startRecording")}
|
|
@@ -943,6 +965,31 @@ export function PreRecordPanel({
|
|
|
943
965
|
</>
|
|
944
966
|
)}
|
|
945
967
|
</div>
|
|
968
|
+
|
|
969
|
+
<AlertDialog open={micWarningOpen} onOpenChange={setMicWarningOpen}>
|
|
970
|
+
<AlertDialogContent>
|
|
971
|
+
<AlertDialogHeader>
|
|
972
|
+
<AlertDialogTitle>
|
|
973
|
+
{t("preRecord.micOffConfirmTitle")}
|
|
974
|
+
</AlertDialogTitle>
|
|
975
|
+
<AlertDialogDescription>
|
|
976
|
+
{t("preRecord.micOffConfirmDescription")}
|
|
977
|
+
</AlertDialogDescription>
|
|
978
|
+
</AlertDialogHeader>
|
|
979
|
+
<AlertDialogFooter>
|
|
980
|
+
<AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
|
|
981
|
+
<AlertDialogAction
|
|
982
|
+
onClick={(event) => {
|
|
983
|
+
event.preventDefault();
|
|
984
|
+
setMicWarningOpen(false);
|
|
985
|
+
onStart(buildStartOpts());
|
|
986
|
+
}}
|
|
987
|
+
>
|
|
988
|
+
{t("preRecord.startWithoutMic")}
|
|
989
|
+
</AlertDialogAction>
|
|
990
|
+
</AlertDialogFooter>
|
|
991
|
+
</AlertDialogContent>
|
|
992
|
+
</AlertDialog>
|
|
946
993
|
</div>
|
|
947
994
|
);
|
|
948
995
|
}
|
|
@@ -1274,6 +1274,10 @@ All notable user-facing changes to Clips are documented here. Open it any time f
|
|
|
1274
1274
|
cameraOff: "Camera off",
|
|
1275
1275
|
includeCameraAria: "Include camera in this recording",
|
|
1276
1276
|
startRecording: "Start recording",
|
|
1277
|
+
micOffConfirmTitle: "Record without a microphone?",
|
|
1278
|
+
micOffConfirmDescription:
|
|
1279
|
+
"Your mic is off, so this recording won't capture any audio. Turn it on before starting if you want narration.",
|
|
1280
|
+
startWithoutMic: "Start anyway",
|
|
1277
1281
|
uploadVideo: "Upload video",
|
|
1278
1282
|
importLoom: "Import Loom",
|
|
1279
1283
|
importing: "Importing...",
|
|
@@ -35,6 +35,16 @@ import {
|
|
|
35
35
|
useState,
|
|
36
36
|
} from "react";
|
|
37
37
|
|
|
38
|
+
import {
|
|
39
|
+
AlertDialog,
|
|
40
|
+
AlertDialogAction,
|
|
41
|
+
AlertDialogCancel,
|
|
42
|
+
AlertDialogContent,
|
|
43
|
+
AlertDialogDescription,
|
|
44
|
+
AlertDialogFooter,
|
|
45
|
+
AlertDialogHeader,
|
|
46
|
+
AlertDialogTitle,
|
|
47
|
+
} from "./components/AlertDialog";
|
|
38
48
|
import { FeedbackButton } from "./components/FeedbackButton";
|
|
39
49
|
import {
|
|
40
50
|
CamIcon,
|
|
@@ -873,6 +883,9 @@ export function App() {
|
|
|
873
883
|
loadBool(CAM_ON_KEY, false),
|
|
874
884
|
);
|
|
875
885
|
const [micOn, setMicOn] = useState<boolean>(() => loadBool(MIC_ON_KEY, true));
|
|
886
|
+
const [micOffConfirmOpen, setMicOffConfirmOpen] = useState(false);
|
|
887
|
+
const pendingStartOptionsRef =
|
|
888
|
+
useRef<Parameters<typeof handleStartRecording>[0]>(undefined);
|
|
876
889
|
const [systemAudioOn, setSystemAudioOn] = useState<boolean>(() =>
|
|
877
890
|
loadBool(SYSTEM_AUDIO_KEY, true),
|
|
878
891
|
);
|
|
@@ -3051,6 +3064,26 @@ export function App() {
|
|
|
3051
3064
|
// include a function that is recreated every render.
|
|
3052
3065
|
handleStartRecordingRef.current = handleStartRecording;
|
|
3053
3066
|
|
|
3067
|
+
// Gates every start-recording gesture (button, global shortcut, permission
|
|
3068
|
+
// retry) on the mic toggle. When the mic is off we hold the actual
|
|
3069
|
+
// getDisplayMedia/getUserMedia call until the user confirms in
|
|
3070
|
+
// micOffConfirmOpen — the confirm button's own click supplies the user
|
|
3071
|
+
// activation handleStartRecording needs, same as the direct gesture would.
|
|
3072
|
+
function beginRecording(
|
|
3073
|
+
options?: Parameters<typeof handleStartRecording>[0],
|
|
3074
|
+
beginOptions?: { revealPopoverIfMicOff?: boolean },
|
|
3075
|
+
) {
|
|
3076
|
+
if (!micOn) {
|
|
3077
|
+
pendingStartOptionsRef.current = options;
|
|
3078
|
+
if (beginOptions?.revealPopoverIfMicOff) {
|
|
3079
|
+
invoke("show_popover").catch(() => {});
|
|
3080
|
+
}
|
|
3081
|
+
setMicOffConfirmOpen(true);
|
|
3082
|
+
return;
|
|
3083
|
+
}
|
|
3084
|
+
void handleStartRecording(options);
|
|
3085
|
+
}
|
|
3086
|
+
|
|
3054
3087
|
recordShortcutHandlerRef.current = () => {
|
|
3055
3088
|
if (recorder) {
|
|
3056
3089
|
emit("clips:recorder-stop").catch(() => {});
|
|
@@ -3078,7 +3111,10 @@ export function App() {
|
|
|
3078
3111
|
return;
|
|
3079
3112
|
}
|
|
3080
3113
|
|
|
3081
|
-
|
|
3114
|
+
beginRecording(
|
|
3115
|
+
{ ignoreActiveRecorder: true },
|
|
3116
|
+
{ revealPopoverIfMicOff: true },
|
|
3117
|
+
);
|
|
3082
3118
|
};
|
|
3083
3119
|
|
|
3084
3120
|
useEffect(() => {
|
|
@@ -3903,9 +3939,7 @@ export function App() {
|
|
|
3903
3939
|
disabled={
|
|
3904
3940
|
localRecordingMode === "off" && videoStorageStatus === "checking"
|
|
3905
3941
|
}
|
|
3906
|
-
onClick={() =>
|
|
3907
|
-
void handleStartRecording();
|
|
3908
|
-
}}
|
|
3942
|
+
onClick={() => beginRecording()}
|
|
3909
3943
|
>
|
|
3910
3944
|
{localRecordingMode === "off" && videoStorageStatus === "checking"
|
|
3911
3945
|
? "Checking storage..."
|
|
@@ -3914,6 +3948,36 @@ export function App() {
|
|
|
3914
3948
|
: "Start local recording"}
|
|
3915
3949
|
</button>
|
|
3916
3950
|
) : null}
|
|
3951
|
+
|
|
3952
|
+
<AlertDialog
|
|
3953
|
+
open={micOffConfirmOpen}
|
|
3954
|
+
onOpenChange={(open) => {
|
|
3955
|
+
setMicOffConfirmOpen(open);
|
|
3956
|
+
if (!open) pendingStartOptionsRef.current = undefined;
|
|
3957
|
+
}}
|
|
3958
|
+
>
|
|
3959
|
+
<AlertDialogContent>
|
|
3960
|
+
<AlertDialogHeader>
|
|
3961
|
+
<AlertDialogTitle>Record without a microphone?</AlertDialogTitle>
|
|
3962
|
+
<AlertDialogDescription>
|
|
3963
|
+
Your mic is off, so this recording won't capture any audio.
|
|
3964
|
+
Turn it on before starting if you want narration.
|
|
3965
|
+
</AlertDialogDescription>
|
|
3966
|
+
</AlertDialogHeader>
|
|
3967
|
+
<AlertDialogFooter>
|
|
3968
|
+
<AlertDialogAction
|
|
3969
|
+
onClick={() => {
|
|
3970
|
+
const options = pendingStartOptionsRef.current;
|
|
3971
|
+
pendingStartOptionsRef.current = undefined;
|
|
3972
|
+
void handleStartRecording(options);
|
|
3973
|
+
}}
|
|
3974
|
+
>
|
|
3975
|
+
Start anyway
|
|
3976
|
+
</AlertDialogAction>
|
|
3977
|
+
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
3978
|
+
</AlertDialogFooter>
|
|
3979
|
+
</AlertDialogContent>
|
|
3980
|
+
</AlertDialog>
|
|
3917
3981
|
{recError ? (
|
|
3918
3982
|
recError === MACOS_UPDATE_RESTART_MESSAGE ? (
|
|
3919
3983
|
<UpdateRestartBanner message={recError} />
|
|
@@ -3928,14 +3992,14 @@ export function App() {
|
|
|
3928
3992
|
? ["screen"]
|
|
3929
3993
|
: permissionPanesForRecording(mode, cameraOn, micOn)
|
|
3930
3994
|
}
|
|
3931
|
-
onRetry={
|
|
3995
|
+
onRetry={() => beginRecording()}
|
|
3932
3996
|
/>
|
|
3933
3997
|
) : recError === MACOS_SPEECH_PERMISSION_MESSAGE ? (
|
|
3934
3998
|
<PermissionRecoveryBanner
|
|
3935
3999
|
kind="speech"
|
|
3936
4000
|
message={recError}
|
|
3937
4001
|
panes={["speech", "microphone"]}
|
|
3938
|
-
onRetry={
|
|
4002
|
+
onRetry={() => beginRecording()}
|
|
3939
4003
|
/>
|
|
3940
4004
|
) : isStorageSetupFailureMessage(recError) ? (
|
|
3941
4005
|
<StorageConnectionBanner
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* shadcn-style AlertDialog for the Tauri tray app.
|
|
6
|
+
*
|
|
7
|
+
* Mirrors shadcn/ui's alert-dialog API, but styled with the desktop app's
|
|
8
|
+
* plain-CSS theme tokens instead of Tailwind — this app has no Tailwind/
|
|
9
|
+
* shadcn build. Deliberately skips Radix's `Portal`: the popover window is
|
|
10
|
+
* sized to fit `.app`'s measured content (see `resize_popover`) and clipped
|
|
11
|
+
* to its own rounded corners, so a body-portaled overlay would render past
|
|
12
|
+
* the window's actual bounds. Overlay/Content use `position: absolute`
|
|
13
|
+
* instead of `fixed` so they stay inside `.app`'s rounded, clipped bounds.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const AlertDialog = AlertDialogPrimitive.Root;
|
|
17
|
+
const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
|
|
18
|
+
|
|
19
|
+
const AlertDialogOverlay = React.forwardRef<
|
|
20
|
+
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
|
21
|
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
|
22
|
+
>(({ className, ...props }, ref) => (
|
|
23
|
+
<AlertDialogPrimitive.Overlay
|
|
24
|
+
ref={ref}
|
|
25
|
+
className={["alert-dialog-overlay", className].filter(Boolean).join(" ")}
|
|
26
|
+
{...props}
|
|
27
|
+
/>
|
|
28
|
+
));
|
|
29
|
+
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
|
|
30
|
+
|
|
31
|
+
const AlertDialogContent = React.forwardRef<
|
|
32
|
+
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
|
33
|
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
|
34
|
+
>(({ className, ...props }, ref) => (
|
|
35
|
+
<>
|
|
36
|
+
<AlertDialogOverlay />
|
|
37
|
+
<AlertDialogPrimitive.Content
|
|
38
|
+
ref={ref}
|
|
39
|
+
className={["alert-dialog-content", className].filter(Boolean).join(" ")}
|
|
40
|
+
{...props}
|
|
41
|
+
/>
|
|
42
|
+
</>
|
|
43
|
+
));
|
|
44
|
+
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
|
|
45
|
+
|
|
46
|
+
function AlertDialogHeader({
|
|
47
|
+
className,
|
|
48
|
+
...props
|
|
49
|
+
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
50
|
+
return (
|
|
51
|
+
<div
|
|
52
|
+
className={["alert-dialog-header", className].filter(Boolean).join(" ")}
|
|
53
|
+
{...props}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function AlertDialogFooter({
|
|
59
|
+
className,
|
|
60
|
+
...props
|
|
61
|
+
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
62
|
+
return (
|
|
63
|
+
<div
|
|
64
|
+
className={["alert-dialog-footer", className].filter(Boolean).join(" ")}
|
|
65
|
+
{...props}
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const AlertDialogTitle = React.forwardRef<
|
|
71
|
+
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
|
72
|
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
|
73
|
+
>(({ className, ...props }, ref) => (
|
|
74
|
+
<AlertDialogPrimitive.Title
|
|
75
|
+
ref={ref}
|
|
76
|
+
className={["alert-dialog-title", className].filter(Boolean).join(" ")}
|
|
77
|
+
{...props}
|
|
78
|
+
/>
|
|
79
|
+
));
|
|
80
|
+
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
|
|
81
|
+
|
|
82
|
+
const AlertDialogDescription = React.forwardRef<
|
|
83
|
+
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
|
84
|
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
|
85
|
+
>(({ className, ...props }, ref) => (
|
|
86
|
+
<AlertDialogPrimitive.Description
|
|
87
|
+
ref={ref}
|
|
88
|
+
className={["alert-dialog-description", className]
|
|
89
|
+
.filter(Boolean)
|
|
90
|
+
.join(" ")}
|
|
91
|
+
{...props}
|
|
92
|
+
/>
|
|
93
|
+
));
|
|
94
|
+
AlertDialogDescription.displayName =
|
|
95
|
+
AlertDialogPrimitive.Description.displayName;
|
|
96
|
+
|
|
97
|
+
const AlertDialogAction = React.forwardRef<
|
|
98
|
+
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
|
99
|
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
|
100
|
+
>(({ className, ...props }, ref) => (
|
|
101
|
+
<AlertDialogPrimitive.Action
|
|
102
|
+
ref={ref}
|
|
103
|
+
className={["primary", className].filter(Boolean).join(" ")}
|
|
104
|
+
{...props}
|
|
105
|
+
/>
|
|
106
|
+
));
|
|
107
|
+
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
|
|
108
|
+
|
|
109
|
+
const AlertDialogCancel = React.forwardRef<
|
|
110
|
+
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
|
111
|
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
|
112
|
+
>(({ className, ...props }, ref) => (
|
|
113
|
+
<AlertDialogPrimitive.Cancel
|
|
114
|
+
ref={ref}
|
|
115
|
+
className={["secondary", className].filter(Boolean).join(" ")}
|
|
116
|
+
{...props}
|
|
117
|
+
/>
|
|
118
|
+
));
|
|
119
|
+
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
|
|
120
|
+
|
|
121
|
+
export {
|
|
122
|
+
AlertDialog,
|
|
123
|
+
AlertDialogTrigger,
|
|
124
|
+
AlertDialogContent,
|
|
125
|
+
AlertDialogHeader,
|
|
126
|
+
AlertDialogFooter,
|
|
127
|
+
AlertDialogTitle,
|
|
128
|
+
AlertDialogDescription,
|
|
129
|
+
AlertDialogAction,
|
|
130
|
+
AlertDialogCancel,
|
|
131
|
+
};
|
|
@@ -156,6 +156,7 @@ body[data-clips-route="recording-pill"] #root {
|
|
|
156
156
|
content exceeds the restored popover height. Only the content region may
|
|
157
157
|
scroll; the window shell and bottom destinations stay put. */
|
|
158
158
|
.app-recorder {
|
|
159
|
+
position: relative;
|
|
159
160
|
gap: 0;
|
|
160
161
|
padding: 0;
|
|
161
162
|
overflow: hidden;
|
|
@@ -2612,6 +2613,134 @@ body[data-clips-route="recording-pill"] #root {
|
|
|
2612
2613
|
}
|
|
2613
2614
|
}
|
|
2614
2615
|
|
|
2616
|
+
/* ------------------------------------------------------------------------- */
|
|
2617
|
+
/* Alert dialog */
|
|
2618
|
+
/* ------------------------------------------------------------------------- */
|
|
2619
|
+
|
|
2620
|
+
/* `position: absolute`, not `fixed` — see AlertDialog.tsx: this must stay
|
|
2621
|
+
inside `.app-recorder`'s clipped, rounded bounds rather than the raw
|
|
2622
|
+
(square-cornered) Tauri window. */
|
|
2623
|
+
.alert-dialog-overlay {
|
|
2624
|
+
position: absolute;
|
|
2625
|
+
inset: 0;
|
|
2626
|
+
z-index: 70;
|
|
2627
|
+
/* guard:allow-raw-color — modal scrim is theme-invariant black, same as shadcn's own bg-black/80 overlay */
|
|
2628
|
+
background: rgba(0, 0, 0, 0.45);
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
.alert-dialog-overlay[data-state="open"] {
|
|
2632
|
+
animation: alert-dialog-overlay-in 120ms ease-out;
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
.alert-dialog-overlay[data-state="closed"] {
|
|
2636
|
+
animation: alert-dialog-overlay-out 100ms ease-in;
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
.alert-dialog-content {
|
|
2640
|
+
position: absolute;
|
|
2641
|
+
z-index: 71;
|
|
2642
|
+
left: 50%;
|
|
2643
|
+
top: 50%;
|
|
2644
|
+
transform: translate(-50%, -50%);
|
|
2645
|
+
width: calc(100% - 32px);
|
|
2646
|
+
max-width: 320px;
|
|
2647
|
+
border-radius: var(--radius);
|
|
2648
|
+
border: 1px solid var(--border);
|
|
2649
|
+
background: var(--bg);
|
|
2650
|
+
box-shadow: var(--shadow-md);
|
|
2651
|
+
padding: 16px;
|
|
2652
|
+
display: flex;
|
|
2653
|
+
flex-direction: column;
|
|
2654
|
+
gap: 14px;
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
.alert-dialog-content[data-state="open"] {
|
|
2658
|
+
animation: alert-dialog-content-in 120ms ease-out;
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
.alert-dialog-content[data-state="closed"] {
|
|
2662
|
+
animation: alert-dialog-content-out 100ms ease-in;
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
.alert-dialog-header {
|
|
2666
|
+
display: flex;
|
|
2667
|
+
flex-direction: column;
|
|
2668
|
+
gap: 6px;
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
.alert-dialog-title {
|
|
2672
|
+
font-size: 14px;
|
|
2673
|
+
font-weight: 600;
|
|
2674
|
+
color: var(--fg);
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
.alert-dialog-description {
|
|
2678
|
+
font-size: 12px;
|
|
2679
|
+
line-height: 1.5;
|
|
2680
|
+
color: var(--fg-muted);
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
.alert-dialog-footer {
|
|
2684
|
+
display: flex;
|
|
2685
|
+
flex-direction: row-reverse;
|
|
2686
|
+
gap: 8px;
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
.alert-dialog-footer .primary,
|
|
2690
|
+
.alert-dialog-footer .secondary {
|
|
2691
|
+
width: auto;
|
|
2692
|
+
flex: 1;
|
|
2693
|
+
height: 36px;
|
|
2694
|
+
padding: 0 12px;
|
|
2695
|
+
border-radius: var(--radius-sm);
|
|
2696
|
+
font-size: 13px;
|
|
2697
|
+
white-space: nowrap;
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
@keyframes alert-dialog-overlay-in {
|
|
2701
|
+
from {
|
|
2702
|
+
opacity: 0;
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
to {
|
|
2706
|
+
opacity: 1;
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
@keyframes alert-dialog-overlay-out {
|
|
2711
|
+
from {
|
|
2712
|
+
opacity: 1;
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
to {
|
|
2716
|
+
opacity: 0;
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
@keyframes alert-dialog-content-in {
|
|
2721
|
+
from {
|
|
2722
|
+
opacity: 0;
|
|
2723
|
+
transform: translate(-50%, -50%) scale(0.96);
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
to {
|
|
2727
|
+
opacity: 1;
|
|
2728
|
+
transform: translate(-50%, -50%) scale(1);
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
@keyframes alert-dialog-content-out {
|
|
2733
|
+
from {
|
|
2734
|
+
opacity: 1;
|
|
2735
|
+
transform: translate(-50%, -50%) scale(1);
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
to {
|
|
2739
|
+
opacity: 0;
|
|
2740
|
+
transform: translate(-50%, -50%) scale(0.96);
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2615
2744
|
.setup-toggle-row {
|
|
2616
2745
|
display: flex;
|
|
2617
2746
|
align-items: center;
|
|
@@ -330,6 +330,8 @@ canonical sizes to reuse instead of guessing:
|
|
|
330
330
|
- **Social**: Instagram Post 1080×1080, Instagram Story 1080×1920, X Post
|
|
331
331
|
1200×675, Facebook Cover 820×312, LinkedIn Cover 1584×396.
|
|
332
332
|
|
|
333
|
+
Frame geometry is always numbers — `"width": 800`, never `"800"` or `"800px"`. String dimensions are rejected.
|
|
334
|
+
|
|
333
335
|
At small ad-unit sizes (320×50, 160×600), text commonly runs 9-11px — smaller
|
|
334
336
|
than this skill's general 16px body-text floor — because there is no room to
|
|
335
337
|
reflow. That is expected for these formats; it stays legible in @2x+ exports
|
|
@@ -439,7 +439,7 @@ const generateDesignAgentParameters = {
|
|
|
439
439
|
type: "string",
|
|
440
440
|
description:
|
|
441
441
|
"Optional JSON array of overview-canvas placements keyed by filename or fileId. " +
|
|
442
|
-
"Pass explicit x/y/width/height for every generated screen; desktop is 1440x900.",
|
|
442
|
+
"Pass explicit x/y/width/height for every generated screen as numbers; desktop is 1440x900.",
|
|
443
443
|
},
|
|
444
444
|
contextPackId: {
|
|
445
445
|
type: "string",
|
|
@@ -504,7 +504,8 @@ const generateDesignAction = defineAction({
|
|
|
504
504
|
"for a mobile- or tablet-primary design when not passing `devices`. " +
|
|
505
505
|
"Do not report a design as ready until this action succeeds. " +
|
|
506
506
|
"When adding multiple screens or states, pass canvasFrames with filenames " +
|
|
507
|
-
"and x/y/width/height so the new screens appear placed on the
|
|
507
|
+
"and numeric x/y/width/height so the new screens appear placed on the " +
|
|
508
|
+
"overview canvas.",
|
|
508
509
|
schema: z.object({
|
|
509
510
|
designId: z.string().describe("Design project ID to save content to"),
|
|
510
511
|
prompt: z.string().describe("The generation prompt (stored for reference)"),
|
|
@@ -4,6 +4,7 @@ import { and, eq, isNull } from "drizzle-orm";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
|
|
6
6
|
import { getDb, schema } from "../server/db/index.js";
|
|
7
|
+
import { numericDesignDataWriteError } from "../shared/canvas-frames.js";
|
|
7
8
|
|
|
8
9
|
const MAX_DATA_CAS_ATTEMPTS = 5;
|
|
9
10
|
const MAX_DATA_OPERATION_SOURCES = 128;
|
|
@@ -26,17 +27,28 @@ const dataPathSchema = z
|
|
|
26
27
|
.min(1)
|
|
27
28
|
.max(8);
|
|
28
29
|
|
|
29
|
-
const dataOperationSchema = z
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
30
|
+
const dataOperationSchema = z
|
|
31
|
+
.discriminatedUnion("op", [
|
|
32
|
+
z.object({
|
|
33
|
+
op: z.literal("set"),
|
|
34
|
+
path: dataPathSchema,
|
|
35
|
+
value: z.json(),
|
|
36
|
+
}),
|
|
37
|
+
z.object({
|
|
38
|
+
op: z.literal("delete"),
|
|
39
|
+
path: dataPathSchema,
|
|
40
|
+
}),
|
|
41
|
+
])
|
|
42
|
+
.superRefine((operation, context) => {
|
|
43
|
+
if (operation.op !== "set") return;
|
|
44
|
+
const message = numericDesignDataWriteError(
|
|
45
|
+
operation.path,
|
|
46
|
+
operation.value,
|
|
47
|
+
);
|
|
48
|
+
if (message) {
|
|
49
|
+
context.addIssue({ code: "custom", path: ["value"], message });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
40
52
|
|
|
41
53
|
type DataOperation = z.infer<typeof dataOperationSchema>;
|
|
42
54
|
|
|
@@ -195,7 +207,9 @@ export default defineAction({
|
|
|
195
207
|
"Update an existing design project. Requires editor access. " +
|
|
196
208
|
"Only provided fields are updated; omitted fields are left unchanged. " +
|
|
197
209
|
"For map entries such as canvasFrames, use dataOperations " +
|
|
198
|
-
"with explicit set/delete paths instead of a full data snapshot."
|
|
210
|
+
"with explicit set/delete paths instead of a full data snapshot. " +
|
|
211
|
+
"Dimensions and positions (x, y, width, height, rotation, z) are " +
|
|
212
|
+
"numbers. String values are rejected.",
|
|
199
213
|
schema: z
|
|
200
214
|
.object({
|
|
201
215
|
id: z.string().describe("Design ID"),
|
|
@@ -213,7 +227,7 @@ export default defineAction({
|
|
|
213
227
|
.max(500)
|
|
214
228
|
.optional()
|
|
215
229
|
.describe(
|
|
216
|
-
"Atomic path-addressed set/delete operations for design data. Safe to CAS-retry across concurrent writers.",
|
|
230
|
+
"Atomic path-addressed set/delete operations for design data. Safe to CAS-retry across concurrent writers. Geometry values must be numbers, not strings.",
|
|
217
231
|
),
|
|
218
232
|
operationSource: z
|
|
219
233
|
.string()
|
|
@@ -284,11 +298,18 @@ export default defineAction({
|
|
|
284
298
|
designSystemId,
|
|
285
299
|
}) => {
|
|
286
300
|
if (data !== undefined) {
|
|
301
|
+
let parsedSnapshot: unknown;
|
|
287
302
|
try {
|
|
288
|
-
JSON.parse(data);
|
|
303
|
+
parsedSnapshot = JSON.parse(data);
|
|
289
304
|
} catch {
|
|
290
305
|
throw new Error("data must be a valid JSON string");
|
|
291
306
|
}
|
|
307
|
+
if (isRecord(parsedSnapshot)) {
|
|
308
|
+
for (const [key, value] of Object.entries(parsedSnapshot)) {
|
|
309
|
+
const message = numericDesignDataWriteError([key], value);
|
|
310
|
+
if (message) throw new Error(message);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
292
313
|
}
|
|
293
314
|
|
|
294
315
|
await assertAccess("design", id, "editor");
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
getFigmaConnectionStatus,
|
|
38
38
|
saveFigmaAccessToken,
|
|
39
39
|
} from "@/lib/figma-connection";
|
|
40
|
+
import { MAX_UPLOAD_MB } from "@/lib/upload-limits";
|
|
40
41
|
import { cn } from "@/lib/utils";
|
|
41
42
|
|
|
42
43
|
import type { DesignExtensionSlotContext } from "./DesignExtensionsPanel";
|
|
@@ -307,7 +308,9 @@ export function DesignImportPanel(p: DesignImportPanelProps) {
|
|
|
307
308
|
}
|
|
308
309
|
if (validationError === "too-large") {
|
|
309
310
|
toast.error(t("designEditor.import.errors.uploadFailed"), {
|
|
310
|
-
description: t("designEditor.import.errors.figFileTooLarge"
|
|
311
|
+
description: t("designEditor.import.errors.figFileTooLarge", {
|
|
312
|
+
max: MAX_UPLOAD_MB,
|
|
313
|
+
}),
|
|
311
314
|
});
|
|
312
315
|
if (figFileInputRef.current) figFileInputRef.current.value = "";
|
|
313
316
|
return;
|
|
@@ -566,7 +569,9 @@ export function DesignImportPanel(p: DesignImportPanelProps) {
|
|
|
566
569
|
>
|
|
567
570
|
<div className="space-y-2 p-2">
|
|
568
571
|
<p className="text-[11px] leading-snug text-muted-foreground">
|
|
569
|
-
{t("designEditor.import.figUploadDescription"
|
|
572
|
+
{t("designEditor.import.figUploadDescription", {
|
|
573
|
+
max: MAX_UPLOAD_MB,
|
|
574
|
+
})}
|
|
570
575
|
</p>
|
|
571
576
|
<input
|
|
572
577
|
ref={figFileInputRef}
|