@agent-native/core 0.81.1 → 0.81.3
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 +25 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/frame.ts +55 -0
- package/corpus/core/src/mcp/build-server.ts +30 -4
- package/corpus/core/src/mcp/embed-app.ts +33 -18
- package/corpus/core/src/server/core-routes-plugin.ts +4 -5
- package/corpus/core/src/server/create-server.ts +3 -5
- package/corpus/core/src/server/google-oauth.ts +4 -0
- package/corpus/templates/calendar/actions/create-event.ts +9 -2
- package/corpus/templates/calendar/actions/event-action-helpers.ts +8 -2
- package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +11 -0
- package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +17 -0
- package/corpus/templates/calendar/app/hooks/use-google-auth.ts +70 -39
- package/corpus/templates/calendar/changelog/2026-06-30-google-calendar-connection-errors-now-stay-in-the-app-instea.md +6 -0
- package/corpus/templates/calendar/server/handlers/google-auth.ts +77 -21
- package/corpus/templates/calendar/server/lib/google-calendar.ts +19 -8
- package/corpus/templates/design/actions/apply-design-token-edit.ts +13 -6
- package/corpus/templates/design/actions/import-design-tokens.ts +17 -13
- package/corpus/templates/design/actions/preview-design-token-edit.ts +20 -3
- package/corpus/templates/design/actions/run-design-audit.ts +9 -2
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +8 -1
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +11 -2
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +89 -16
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +21 -14
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +35 -14
- package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +20 -3
- package/corpus/templates/design/app/pages/DesignEditor.tsx +794 -262
- package/corpus/templates/design/app/pages/VisualEdit.tsx +1 -1
- package/corpus/templates/design/changelog/2026-06-30-design-editor-undo-drag-drop-style-editing-and-visual-edit-s.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-studio-canvas-layers-can-be-copied-pasted-and-reparen.md +6 -0
- package/corpus/templates/design/e2e/global-setup.ts +3 -1
- package/corpus/templates/design/e2e/helpers.ts +4 -1
- package/corpus/templates/design/playwright.config.ts +4 -1
- package/corpus/templates/design/shared/resolve-tweaks.ts +29 -6
- package/corpus/templates/plan/actions/consume-plan-feedback.ts +1 -9
- package/corpus/templates/plan/actions/convert-visual-plan-to-prototype.ts +1 -1
- package/corpus/templates/plan/actions/create-plan-design.ts +1 -1
- package/corpus/templates/plan/actions/create-prototype-plan.ts +1 -1
- package/corpus/templates/plan/actions/create-ui-plan.ts +1 -1
- package/corpus/templates/plan/actions/create-visual-plan.ts +36 -19
- package/corpus/templates/plan/actions/create-visual-questions.ts +1 -1
- package/corpus/templates/plan/actions/create-visual-recap.ts +1 -1
- package/corpus/templates/plan/actions/delete-plan-comment.ts +1 -9
- package/corpus/templates/plan/actions/delete-visual-plan.ts +1 -9
- package/corpus/templates/plan/actions/get-plan-feedback.ts +1 -9
- package/corpus/templates/plan/actions/get-visual-plan.ts +1 -9
- package/corpus/templates/plan/actions/import-visual-plan-source.ts +1 -1
- package/corpus/templates/plan/actions/list-visual-plans.ts +1 -9
- package/corpus/templates/plan/actions/reply-to-plan-comment.ts +1 -9
- package/corpus/templates/plan/actions/resolve-plan-comment.ts +1 -9
- package/corpus/templates/plan/actions/update-visual-plan.ts +1 -1
- package/corpus/templates/plan/actions/visual-answer.ts +1 -1
- package/corpus/templates/plan/app/components/layout/Layout.tsx +15 -0
- package/corpus/templates/plan/app/entry.client.tsx +10 -0
- package/corpus/templates/plan/app/global.css +28 -0
- package/corpus/templates/plan/shared/plan-content.ts +10 -6
- package/dist/client/frame.d.ts.map +1 -1
- package/dist/client/frame.js +59 -0
- package/dist/client/frame.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +31 -4
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/mcp/embed-app.d.ts.map +1 -1
- package/dist/mcp/embed-app.js +33 -18
- package/dist/mcp/embed-app.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +1 -2
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/create-server.d.ts.map +1 -1
- package/dist/server/create-server.js +1 -2
- package/dist/server/create-server.js.map +1 -1
- package/dist/server/google-oauth.d.ts +2 -0
- package/dist/server/google-oauth.d.ts.map +1 -1
- package/dist/server/google-oauth.js +3 -0
- package/dist/server/google-oauth.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
|
@@ -3365,15 +3365,26 @@ export function MultiScreenCanvas({
|
|
|
3365
3365
|
[flushPendingWheelGesture],
|
|
3366
3366
|
);
|
|
3367
3367
|
|
|
3368
|
-
const
|
|
3369
|
-
(
|
|
3368
|
+
const enqueueWheelGestureFromClient = useCallback(
|
|
3369
|
+
(args: {
|
|
3370
|
+
deltaX: number;
|
|
3371
|
+
deltaY: number;
|
|
3372
|
+
deltaMode: number;
|
|
3373
|
+
clientX: number;
|
|
3374
|
+
clientY: number;
|
|
3375
|
+
ctrlKey: boolean;
|
|
3376
|
+
metaKey: boolean;
|
|
3377
|
+
shiftKey: boolean;
|
|
3378
|
+
}) => {
|
|
3370
3379
|
const rect = surfaceRef.current?.getBoundingClientRect();
|
|
3371
3380
|
if (!rect) return;
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3381
|
+
const delta = getWheelDeltaFromValues(
|
|
3382
|
+
args.deltaX,
|
|
3383
|
+
args.deltaY,
|
|
3384
|
+
args.deltaMode,
|
|
3385
|
+
);
|
|
3375
3386
|
|
|
3376
|
-
if (
|
|
3387
|
+
if (args.ctrlKey || args.metaKey) {
|
|
3377
3388
|
const zoomDeltaY = clamp(
|
|
3378
3389
|
delta.y,
|
|
3379
3390
|
-MAX_WHEEL_ZOOM_DELTA,
|
|
@@ -3382,20 +3393,23 @@ export function MultiScreenCanvas({
|
|
|
3382
3393
|
enqueueWheelGesture({
|
|
3383
3394
|
mode: "zoom",
|
|
3384
3395
|
deltaY: zoomDeltaY,
|
|
3385
|
-
cursor: {
|
|
3386
|
-
|
|
3387
|
-
|
|
3396
|
+
cursor: {
|
|
3397
|
+
x: args.clientX - rect.left,
|
|
3398
|
+
y: args.clientY - rect.top,
|
|
3399
|
+
},
|
|
3400
|
+
clientX: args.clientX,
|
|
3401
|
+
clientY: args.clientY,
|
|
3388
3402
|
});
|
|
3389
3403
|
return;
|
|
3390
3404
|
}
|
|
3391
3405
|
|
|
3392
3406
|
const deltaX = clamp(
|
|
3393
|
-
|
|
3407
|
+
args.shiftKey && delta.x === 0 ? delta.y : delta.x,
|
|
3394
3408
|
-MAX_WHEEL_PAN_DELTA,
|
|
3395
3409
|
MAX_WHEEL_PAN_DELTA,
|
|
3396
3410
|
);
|
|
3397
3411
|
const deltaY = clamp(
|
|
3398
|
-
|
|
3412
|
+
args.shiftKey && delta.x === 0 ? 0 : delta.y,
|
|
3399
3413
|
-MAX_WHEEL_PAN_DELTA,
|
|
3400
3414
|
MAX_WHEEL_PAN_DELTA,
|
|
3401
3415
|
);
|
|
@@ -3404,6 +3418,24 @@ export function MultiScreenCanvas({
|
|
|
3404
3418
|
[enqueueWheelGesture],
|
|
3405
3419
|
);
|
|
3406
3420
|
|
|
3421
|
+
const handleWheelEvent = useCallback(
|
|
3422
|
+
(event: WheelEvent) => {
|
|
3423
|
+
event.preventDefault();
|
|
3424
|
+
event.stopPropagation();
|
|
3425
|
+
enqueueWheelGestureFromClient({
|
|
3426
|
+
deltaX: event.deltaX,
|
|
3427
|
+
deltaY: event.deltaY,
|
|
3428
|
+
deltaMode: event.deltaMode,
|
|
3429
|
+
clientX: event.clientX,
|
|
3430
|
+
clientY: event.clientY,
|
|
3431
|
+
ctrlKey: event.ctrlKey,
|
|
3432
|
+
metaKey: event.metaKey,
|
|
3433
|
+
shiftKey: event.shiftKey,
|
|
3434
|
+
});
|
|
3435
|
+
},
|
|
3436
|
+
[enqueueWheelGestureFromClient],
|
|
3437
|
+
);
|
|
3438
|
+
|
|
3407
3439
|
useEffect(() => {
|
|
3408
3440
|
const surface = surfaceRef.current;
|
|
3409
3441
|
if (!surface) return;
|
|
@@ -3418,6 +3450,44 @@ export function MultiScreenCanvas({
|
|
|
3418
3450
|
};
|
|
3419
3451
|
}, [handleWheelEvent]);
|
|
3420
3452
|
|
|
3453
|
+
useEffect(() => {
|
|
3454
|
+
const handleEmbeddedWheelMessage = (event: MessageEvent) => {
|
|
3455
|
+
if (!event.data || event.data.type !== "embedded-canvas-wheel") return;
|
|
3456
|
+
const surface = surfaceRef.current;
|
|
3457
|
+
if (!surface) return;
|
|
3458
|
+
const sourceIframe = Array.from(
|
|
3459
|
+
surface.querySelectorAll<HTMLIFrameElement>(
|
|
3460
|
+
"iframe[data-design-preview-iframe]",
|
|
3461
|
+
),
|
|
3462
|
+
).find((iframe) => iframe.contentWindow === event.source);
|
|
3463
|
+
if (!sourceIframe) return;
|
|
3464
|
+
|
|
3465
|
+
const rect = sourceIframe.getBoundingClientRect();
|
|
3466
|
+
const scaleX =
|
|
3467
|
+
sourceIframe.clientWidth > 0
|
|
3468
|
+
? rect.width / sourceIframe.clientWidth
|
|
3469
|
+
: 1;
|
|
3470
|
+
const scaleY =
|
|
3471
|
+
sourceIframe.clientHeight > 0
|
|
3472
|
+
? rect.height / sourceIframe.clientHeight
|
|
3473
|
+
: 1;
|
|
3474
|
+
enqueueWheelGestureFromClient({
|
|
3475
|
+
deltaX: Number(event.data.deltaX) || 0,
|
|
3476
|
+
deltaY: Number(event.data.deltaY) || 0,
|
|
3477
|
+
deltaMode: Number(event.data.deltaMode) || WheelEvent.DOM_DELTA_PIXEL,
|
|
3478
|
+
clientX: rect.left + (Number(event.data.clientX) || 0) * scaleX,
|
|
3479
|
+
clientY: rect.top + (Number(event.data.clientY) || 0) * scaleY,
|
|
3480
|
+
ctrlKey: Boolean(event.data.ctrlKey),
|
|
3481
|
+
metaKey: Boolean(event.data.metaKey),
|
|
3482
|
+
shiftKey: Boolean(event.data.shiftKey),
|
|
3483
|
+
});
|
|
3484
|
+
};
|
|
3485
|
+
|
|
3486
|
+
window.addEventListener("message", handleEmbeddedWheelMessage);
|
|
3487
|
+
return () =>
|
|
3488
|
+
window.removeEventListener("message", handleEmbeddedWheelMessage);
|
|
3489
|
+
}, [enqueueWheelGestureFromClient]);
|
|
3490
|
+
|
|
3421
3491
|
useEffect(() => {
|
|
3422
3492
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
3423
3493
|
if (activePenPathRef.current) return;
|
|
@@ -6060,12 +6130,15 @@ function getSelectableBounds(geometry: FrameGeometry): BoundsRect {
|
|
|
6060
6130
|
};
|
|
6061
6131
|
}
|
|
6062
6132
|
|
|
6063
|
-
function
|
|
6064
|
-
|
|
6065
|
-
|
|
6133
|
+
function getWheelDeltaFromValues(
|
|
6134
|
+
deltaX: number,
|
|
6135
|
+
deltaY: number,
|
|
6136
|
+
deltaMode: number,
|
|
6137
|
+
) {
|
|
6138
|
+
const multiplier = deltaMode === 1 ? 16 : deltaMode === 2 ? 800 : 1;
|
|
6066
6139
|
return {
|
|
6067
|
-
x:
|
|
6068
|
-
y:
|
|
6140
|
+
x: deltaX * multiplier,
|
|
6141
|
+
y: deltaY * multiplier,
|
|
6069
6142
|
};
|
|
6070
6143
|
}
|
|
6071
6144
|
|
|
@@ -3468,17 +3468,20 @@ declare var __EDITOR_CHROME_SCALE_Y__: string;
|
|
|
3468
3468
|
}
|
|
3469
3469
|
}
|
|
3470
3470
|
|
|
3471
|
-
function beginTextEditingFromEvent(e) {
|
|
3471
|
+
function beginTextEditingFromEvent(e, forceTextEditing) {
|
|
3472
3472
|
if (activeTextEditEl && e.target && activeTextEditEl.contains(e.target))
|
|
3473
3473
|
return;
|
|
3474
|
-
if (!textEditingEnabled) {
|
|
3474
|
+
if (!textEditingEnabled && !forceTextEditing) {
|
|
3475
3475
|
stopNativeInteraction(e);
|
|
3476
3476
|
return;
|
|
3477
3477
|
}
|
|
3478
3478
|
stopNativeInteraction(e);
|
|
3479
|
-
var
|
|
3480
|
-
|
|
3481
|
-
|
|
3479
|
+
var eventTarget =
|
|
3480
|
+
e && e.target && e.target.nodeType === 1 ? e.target : null;
|
|
3481
|
+
var target =
|
|
3482
|
+
findTextEditTarget(elementFromEditorPoint(e.clientX, e.clientY)) ||
|
|
3483
|
+
findTextEditTarget(eventTarget) ||
|
|
3484
|
+
eventTarget;
|
|
3482
3485
|
if (!target || target.nodeType !== 1) return;
|
|
3483
3486
|
// Anchor the selection identity to the nearest source-backed element. Text
|
|
3484
3487
|
// editing still operates on the actual target text node, but a later
|
|
@@ -3725,7 +3728,8 @@ declare var __EDITOR_CHROME_SCALE_Y__: string;
|
|
|
3725
3728
|
// text-element creation so the user can type right away and the autosize
|
|
3726
3729
|
// CSS (width:max-content or similar) takes effect from the first keystroke.
|
|
3727
3730
|
if (e.data.type === "begin-text-edit") {
|
|
3728
|
-
|
|
3731
|
+
var forceBeginTextEdit = e.data.force === true;
|
|
3732
|
+
if ((readOnly || !textEditingEnabled) && !forceBeginTextEdit) return;
|
|
3729
3733
|
var nodeId: string =
|
|
3730
3734
|
typeof e.data.nodeId === "string" ? e.data.nodeId : "";
|
|
3731
3735
|
if (!nodeId) return;
|
|
@@ -3749,14 +3753,17 @@ declare var __EDITOR_CHROME_SCALE_Y__: string;
|
|
|
3749
3753
|
var bteCenterY = bteRect.top + bteRect.height / 2;
|
|
3750
3754
|
// Delegate to the canonical path so all state, events, and postMessages
|
|
3751
3755
|
// stay consistent with a normal double-click text edit.
|
|
3752
|
-
beginTextEditingFromEvent(
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3756
|
+
beginTextEditingFromEvent(
|
|
3757
|
+
{
|
|
3758
|
+
clientX: bteCenterX,
|
|
3759
|
+
clientY: bteCenterY,
|
|
3760
|
+
target: textTarget,
|
|
3761
|
+
preventDefault: function () {},
|
|
3762
|
+
stopPropagation: function () {},
|
|
3763
|
+
stopImmediatePropagation: function () {},
|
|
3764
|
+
} as unknown as MouseEvent,
|
|
3765
|
+
forceBeginTextEdit,
|
|
3766
|
+
);
|
|
3760
3767
|
return;
|
|
3761
3768
|
}
|
|
3762
3769
|
if (e.data.type === "set-editor-chrome-scale") {
|
|
@@ -755,6 +755,7 @@ export function DesignColorPicker({
|
|
|
755
755
|
|
|
756
756
|
const [mode, setMode] = useState<DesignColorMode>("hex");
|
|
757
757
|
const [hexDraft, setHexDraft] = useState(() => toDisplayHex(color));
|
|
758
|
+
const hexDraftRef = useRef(hexDraft);
|
|
758
759
|
const [open, setOpen] = useState(false);
|
|
759
760
|
const [picking, setPicking] = useState(false);
|
|
760
761
|
const skipNextHexBlurCommitRef = useRef(false);
|
|
@@ -814,7 +815,9 @@ export function DesignColorPicker({
|
|
|
814
815
|
: null;
|
|
815
816
|
|
|
816
817
|
useEffect(() => {
|
|
817
|
-
|
|
818
|
+
const nextHex = toDisplayHex(color);
|
|
819
|
+
hexDraftRef.current = nextHex;
|
|
820
|
+
setHexDraft(nextHex);
|
|
818
821
|
}, [color.r, color.g, color.b]);
|
|
819
822
|
|
|
820
823
|
// The local override (the user's explicit paint-type click) persists for the
|
|
@@ -863,17 +866,20 @@ export function DesignColorPicker({
|
|
|
863
866
|
};
|
|
864
867
|
|
|
865
868
|
const commitHex = () => {
|
|
866
|
-
const
|
|
869
|
+
const currentDraft = hexDraftRef.current;
|
|
870
|
+
const parsed = parseCssColor(`#${currentDraft.replace(/^#/, "")}`);
|
|
867
871
|
if (!parsed) {
|
|
868
|
-
|
|
872
|
+
const reverted = toDisplayHex(activeGradient ? fieldColor : color);
|
|
873
|
+
hexDraftRef.current = reverted;
|
|
874
|
+
setHexDraft(reverted);
|
|
869
875
|
return;
|
|
870
876
|
}
|
|
871
877
|
if (activeGradient) {
|
|
872
|
-
const hexIncludesAlpha = hasHexAlpha(
|
|
878
|
+
const hexIncludesAlpha = hasHexAlpha(currentDraft);
|
|
873
879
|
emitStopColor(hexIncludesAlpha ? parsed : { ...parsed, a: fieldColor.a });
|
|
874
880
|
return;
|
|
875
881
|
}
|
|
876
|
-
const hexIncludesAlpha = hasHexAlpha(
|
|
882
|
+
const hexIncludesAlpha = hasHexAlpha(currentDraft);
|
|
877
883
|
const nextOpacity = hexIncludesAlpha
|
|
878
884
|
? alphaToOpacity(parsed.a)
|
|
879
885
|
: effectiveOpacity;
|
|
@@ -1080,7 +1086,10 @@ export function DesignColorPicker({
|
|
|
1080
1086
|
aria-label={copy.hex}
|
|
1081
1087
|
spellCheck={false}
|
|
1082
1088
|
className="h-6 min-w-0 rounded-md border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-2 text-[11px] tabular-nums uppercase"
|
|
1083
|
-
onChange={(e) =>
|
|
1089
|
+
onChange={(e) => {
|
|
1090
|
+
hexDraftRef.current = e.target.value;
|
|
1091
|
+
setHexDraft(e.target.value);
|
|
1092
|
+
}}
|
|
1084
1093
|
onFocus={(e) => e.target.select()}
|
|
1085
1094
|
onKeyDown={(e) => {
|
|
1086
1095
|
if (e.key === "Enter") {
|
|
@@ -1090,7 +1099,9 @@ export function DesignColorPicker({
|
|
|
1090
1099
|
e.currentTarget.blur();
|
|
1091
1100
|
}
|
|
1092
1101
|
if (e.key === "Escape") {
|
|
1093
|
-
|
|
1102
|
+
const reverted = toDisplayHex(color);
|
|
1103
|
+
hexDraftRef.current = reverted;
|
|
1104
|
+
setHexDraft(reverted);
|
|
1094
1105
|
skipNextHexBlurCommitRef.current = true;
|
|
1095
1106
|
e.currentTarget.blur();
|
|
1096
1107
|
}
|
|
@@ -1978,16 +1989,21 @@ function ScrubbyNumberInput({
|
|
|
1978
1989
|
compact?: boolean;
|
|
1979
1990
|
}) {
|
|
1980
1991
|
const [draft, setDraft] = useState<string>(() => String(value));
|
|
1992
|
+
const draftRef = useRef(draft);
|
|
1981
1993
|
const skipBlurRef = useRef(false);
|
|
1982
1994
|
|
|
1983
1995
|
useEffect(() => {
|
|
1984
|
-
|
|
1996
|
+
const nextDraft = String(value);
|
|
1997
|
+
draftRef.current = nextDraft;
|
|
1998
|
+
setDraft(nextDraft);
|
|
1985
1999
|
}, [value]);
|
|
1986
2000
|
|
|
1987
2001
|
const commit = () => {
|
|
1988
|
-
const parsed = Number(
|
|
2002
|
+
const parsed = Number(draftRef.current);
|
|
1989
2003
|
if (!Number.isFinite(parsed)) {
|
|
1990
|
-
|
|
2004
|
+
const reverted = String(value);
|
|
2005
|
+
draftRef.current = reverted;
|
|
2006
|
+
setDraft(reverted);
|
|
1991
2007
|
return;
|
|
1992
2008
|
}
|
|
1993
2009
|
onChange(clamp(parsed, min, max));
|
|
@@ -2007,7 +2023,10 @@ function ScrubbyNumberInput({
|
|
|
2007
2023
|
compact && "border-0 shadow-none focus-visible:ring-0",
|
|
2008
2024
|
className,
|
|
2009
2025
|
)}
|
|
2010
|
-
onChange={(e) =>
|
|
2026
|
+
onChange={(e) => {
|
|
2027
|
+
draftRef.current = e.target.value;
|
|
2028
|
+
setDraft(e.target.value);
|
|
2029
|
+
}}
|
|
2011
2030
|
onFocus={(e) => e.target.select()}
|
|
2012
2031
|
onKeyDown={(e) => {
|
|
2013
2032
|
if (e.key === "Enter") {
|
|
@@ -2017,21 +2036,23 @@ function ScrubbyNumberInput({
|
|
|
2017
2036
|
e.currentTarget.blur();
|
|
2018
2037
|
}
|
|
2019
2038
|
if (e.key === "Escape") {
|
|
2020
|
-
|
|
2039
|
+
const reverted = String(value);
|
|
2040
|
+
draftRef.current = reverted;
|
|
2041
|
+
setDraft(reverted);
|
|
2021
2042
|
skipBlurRef.current = true;
|
|
2022
2043
|
e.currentTarget.blur();
|
|
2023
2044
|
}
|
|
2024
2045
|
if (e.key === "ArrowUp") {
|
|
2025
2046
|
e.preventDefault();
|
|
2026
2047
|
const step = e.shiftKey ? 10 : 1;
|
|
2027
|
-
const parsed = Number(
|
|
2048
|
+
const parsed = Number(draftRef.current);
|
|
2028
2049
|
const base = Number.isFinite(parsed) ? parsed : value;
|
|
2029
2050
|
onChange(clamp(base + step, min, max));
|
|
2030
2051
|
}
|
|
2031
2052
|
if (e.key === "ArrowDown") {
|
|
2032
2053
|
e.preventDefault();
|
|
2033
2054
|
const step = e.shiftKey ? 10 : 1;
|
|
2034
|
-
const parsed = Number(
|
|
2055
|
+
const parsed = Number(draftRef.current);
|
|
2035
2056
|
const base = Number.isFinite(parsed) ? parsed : value;
|
|
2036
2057
|
onChange(clamp(base - step, min, max));
|
|
2037
2058
|
}
|
|
@@ -238,6 +238,14 @@ export function parseImageFillCss(
|
|
|
238
238
|
return { url, fit };
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
+
export function mergeImageFitDraft(
|
|
242
|
+
value: ImageFillValue,
|
|
243
|
+
urlDraft: string,
|
|
244
|
+
fit: ImageFitMode,
|
|
245
|
+
): ImageFillValue {
|
|
246
|
+
return { ...value, url: urlDraft.trim(), fit };
|
|
247
|
+
}
|
|
248
|
+
|
|
241
249
|
// ─── Component ─────────────────────────────────────────────────────────────────
|
|
242
250
|
|
|
243
251
|
export interface ImageFillControlsProps {
|
|
@@ -255,13 +263,15 @@ export function ImageFillControls({
|
|
|
255
263
|
}: ImageFillControlsProps) {
|
|
256
264
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
257
265
|
const [urlDraft, setUrlDraft] = useState(value.url);
|
|
266
|
+
const urlDraftRef = useRef(value.url);
|
|
258
267
|
|
|
259
268
|
useEffect(() => {
|
|
269
|
+
urlDraftRef.current = value.url;
|
|
260
270
|
setUrlDraft(value.url);
|
|
261
271
|
}, [value.url]);
|
|
262
272
|
|
|
263
273
|
const commitUrl = () => {
|
|
264
|
-
onChange({ ...value, url:
|
|
274
|
+
onChange({ ...value, url: urlDraftRef.current.trim() });
|
|
265
275
|
};
|
|
266
276
|
|
|
267
277
|
const handleFilePick = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
@@ -338,7 +348,10 @@ export function ImageFillControls({
|
|
|
338
348
|
aria-label={"Image URL" /* i18n-ignore */}
|
|
339
349
|
spellCheck={false}
|
|
340
350
|
className="h-6 min-w-0 flex-1 rounded-md border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] px-2 text-[11px]"
|
|
341
|
-
onChange={(event) =>
|
|
351
|
+
onChange={(event) => {
|
|
352
|
+
urlDraftRef.current = event.target.value;
|
|
353
|
+
setUrlDraft(event.target.value);
|
|
354
|
+
}}
|
|
342
355
|
onBlur={commitUrl}
|
|
343
356
|
onKeyDown={(event) => {
|
|
344
357
|
if (event.key === "Enter") {
|
|
@@ -378,7 +391,11 @@ export function ImageFillControls({
|
|
|
378
391
|
{/* ── Fit mode dropdown ─────────────────────────────────────────────── */}
|
|
379
392
|
<Select
|
|
380
393
|
value={value.fit}
|
|
381
|
-
onValueChange={(v) =>
|
|
394
|
+
onValueChange={(v) =>
|
|
395
|
+
onChange(
|
|
396
|
+
mergeImageFitDraft(value, urlDraftRef.current, v as ImageFitMode),
|
|
397
|
+
)
|
|
398
|
+
}
|
|
382
399
|
disabled={disabled}
|
|
383
400
|
>
|
|
384
401
|
<SelectTrigger
|