@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.
Files changed (86) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +25 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/client/frame.ts +55 -0
  5. package/corpus/core/src/mcp/build-server.ts +30 -4
  6. package/corpus/core/src/mcp/embed-app.ts +33 -18
  7. package/corpus/core/src/server/core-routes-plugin.ts +4 -5
  8. package/corpus/core/src/server/create-server.ts +3 -5
  9. package/corpus/core/src/server/google-oauth.ts +4 -0
  10. package/corpus/templates/calendar/actions/create-event.ts +9 -2
  11. package/corpus/templates/calendar/actions/event-action-helpers.ts +8 -2
  12. package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +11 -0
  13. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +17 -0
  14. package/corpus/templates/calendar/app/hooks/use-google-auth.ts +70 -39
  15. package/corpus/templates/calendar/changelog/2026-06-30-google-calendar-connection-errors-now-stay-in-the-app-instea.md +6 -0
  16. package/corpus/templates/calendar/server/handlers/google-auth.ts +77 -21
  17. package/corpus/templates/calendar/server/lib/google-calendar.ts +19 -8
  18. package/corpus/templates/design/actions/apply-design-token-edit.ts +13 -6
  19. package/corpus/templates/design/actions/import-design-tokens.ts +17 -13
  20. package/corpus/templates/design/actions/preview-design-token-edit.ts +20 -3
  21. package/corpus/templates/design/actions/run-design-audit.ts +9 -2
  22. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +8 -1
  23. package/corpus/templates/design/app/components/design/LayersPanel.tsx +11 -2
  24. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +89 -16
  25. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +21 -14
  26. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +35 -14
  27. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +20 -3
  28. package/corpus/templates/design/app/pages/DesignEditor.tsx +794 -262
  29. package/corpus/templates/design/app/pages/VisualEdit.tsx +1 -1
  30. package/corpus/templates/design/changelog/2026-06-30-design-editor-undo-drag-drop-style-editing-and-visual-edit-s.md +6 -0
  31. package/corpus/templates/design/changelog/2026-06-30-design-studio-canvas-layers-can-be-copied-pasted-and-reparen.md +6 -0
  32. package/corpus/templates/design/e2e/global-setup.ts +3 -1
  33. package/corpus/templates/design/e2e/helpers.ts +4 -1
  34. package/corpus/templates/design/playwright.config.ts +4 -1
  35. package/corpus/templates/design/shared/resolve-tweaks.ts +29 -6
  36. package/corpus/templates/plan/actions/consume-plan-feedback.ts +1 -9
  37. package/corpus/templates/plan/actions/convert-visual-plan-to-prototype.ts +1 -1
  38. package/corpus/templates/plan/actions/create-plan-design.ts +1 -1
  39. package/corpus/templates/plan/actions/create-prototype-plan.ts +1 -1
  40. package/corpus/templates/plan/actions/create-ui-plan.ts +1 -1
  41. package/corpus/templates/plan/actions/create-visual-plan.ts +36 -19
  42. package/corpus/templates/plan/actions/create-visual-questions.ts +1 -1
  43. package/corpus/templates/plan/actions/create-visual-recap.ts +1 -1
  44. package/corpus/templates/plan/actions/delete-plan-comment.ts +1 -9
  45. package/corpus/templates/plan/actions/delete-visual-plan.ts +1 -9
  46. package/corpus/templates/plan/actions/get-plan-feedback.ts +1 -9
  47. package/corpus/templates/plan/actions/get-visual-plan.ts +1 -9
  48. package/corpus/templates/plan/actions/import-visual-plan-source.ts +1 -1
  49. package/corpus/templates/plan/actions/list-visual-plans.ts +1 -9
  50. package/corpus/templates/plan/actions/reply-to-plan-comment.ts +1 -9
  51. package/corpus/templates/plan/actions/resolve-plan-comment.ts +1 -9
  52. package/corpus/templates/plan/actions/update-visual-plan.ts +1 -1
  53. package/corpus/templates/plan/actions/visual-answer.ts +1 -1
  54. package/corpus/templates/plan/app/components/layout/Layout.tsx +15 -0
  55. package/corpus/templates/plan/app/entry.client.tsx +10 -0
  56. package/corpus/templates/plan/app/global.css +28 -0
  57. package/corpus/templates/plan/shared/plan-content.ts +10 -6
  58. package/dist/client/frame.d.ts.map +1 -1
  59. package/dist/client/frame.js +59 -0
  60. package/dist/client/frame.js.map +1 -1
  61. package/dist/collab/awareness.d.ts +2 -2
  62. package/dist/collab/awareness.d.ts.map +1 -1
  63. package/dist/collab/routes.d.ts +1 -1
  64. package/dist/mcp/build-server.d.ts.map +1 -1
  65. package/dist/mcp/build-server.js +31 -4
  66. package/dist/mcp/build-server.js.map +1 -1
  67. package/dist/mcp/embed-app.d.ts.map +1 -1
  68. package/dist/mcp/embed-app.js +33 -18
  69. package/dist/mcp/embed-app.js.map +1 -1
  70. package/dist/notifications/routes.d.ts +3 -3
  71. package/dist/observability/routes.d.ts +3 -3
  72. package/dist/progress/routes.d.ts +1 -1
  73. package/dist/resources/handlers.d.ts +3 -3
  74. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  75. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  76. package/dist/server/core-routes-plugin.js +1 -2
  77. package/dist/server/core-routes-plugin.js.map +1 -1
  78. package/dist/server/create-server.d.ts.map +1 -1
  79. package/dist/server/create-server.js +1 -2
  80. package/dist/server/create-server.js.map +1 -1
  81. package/dist/server/google-oauth.d.ts +2 -0
  82. package/dist/server/google-oauth.d.ts.map +1 -1
  83. package/dist/server/google-oauth.js +3 -0
  84. package/dist/server/google-oauth.js.map +1 -1
  85. package/dist/server/transcribe-voice.d.ts +1 -1
  86. package/package.json +1 -1
@@ -3365,15 +3365,26 @@ export function MultiScreenCanvas({
3365
3365
  [flushPendingWheelGesture],
3366
3366
  );
3367
3367
 
3368
- const handleWheelEvent = useCallback(
3369
- (event: WheelEvent) => {
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
- event.preventDefault();
3373
- event.stopPropagation();
3374
- const delta = getWheelDelta(event);
3381
+ const delta = getWheelDeltaFromValues(
3382
+ args.deltaX,
3383
+ args.deltaY,
3384
+ args.deltaMode,
3385
+ );
3375
3386
 
3376
- if (event.ctrlKey || event.metaKey) {
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: { x: event.clientX - rect.left, y: event.clientY - rect.top },
3386
- clientX: event.clientX,
3387
- clientY: event.clientY,
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
- event.shiftKey && delta.x === 0 ? delta.y : delta.x,
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
- event.shiftKey && delta.x === 0 ? 0 : delta.y,
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 getWheelDelta(event: WheelEvent) {
6064
- const multiplier =
6065
- event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? 800 : 1;
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: event.deltaX * multiplier,
6068
- y: event.deltaY * multiplier,
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 target = findTextEditTarget(
3480
- elementFromEditorPoint(e.clientX, e.clientY),
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
- if (readOnly || !textEditingEnabled) return;
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
- clientX: bteCenterX,
3754
- clientY: bteCenterY,
3755
- target: textTarget,
3756
- preventDefault: function () {},
3757
- stopPropagation: function () {},
3758
- stopImmediatePropagation: function () {},
3759
- } as unknown as MouseEvent);
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
- setHexDraft(toDisplayHex(color));
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 parsed = parseCssColor(`#${hexDraft.replace(/^#/, "")}`);
869
+ const currentDraft = hexDraftRef.current;
870
+ const parsed = parseCssColor(`#${currentDraft.replace(/^#/, "")}`);
867
871
  if (!parsed) {
868
- setHexDraft(toDisplayHex(activeGradient ? fieldColor : color));
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(hexDraft);
878
+ const hexIncludesAlpha = hasHexAlpha(currentDraft);
873
879
  emitStopColor(hexIncludesAlpha ? parsed : { ...parsed, a: fieldColor.a });
874
880
  return;
875
881
  }
876
- const hexIncludesAlpha = hasHexAlpha(hexDraft);
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) => setHexDraft(e.target.value)}
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
- setHexDraft(toDisplayHex(color));
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
- setDraft(String(value));
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(draft);
2002
+ const parsed = Number(draftRef.current);
1989
2003
  if (!Number.isFinite(parsed)) {
1990
- setDraft(String(value));
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) => setDraft(e.target.value)}
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
- setDraft(String(value));
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(draft);
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(draft);
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: urlDraft.trim() });
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) => setUrlDraft(event.target.value)}
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) => onChange({ ...value, fit: v as ImageFitMode })}
394
+ onValueChange={(v) =>
395
+ onChange(
396
+ mergeImageFitDraft(value, urlDraftRef.current, v as ImageFitMode),
397
+ )
398
+ }
382
399
  disabled={disabled}
383
400
  >
384
401
  <SelectTrigger