@agent-native/core 0.81.1 → 0.81.2
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 +6 -0
- package/corpus/core/package.json +1 -1
- 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/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 +237 -51
- 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/shared/resolve-tweaks.ts +29 -6
- package/dist/collab/routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +7 -7
- 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
|
@@ -10,7 +10,11 @@ import {
|
|
|
10
10
|
INLINE_DEFAULT_CAPABILITIES,
|
|
11
11
|
hasCapability,
|
|
12
12
|
} from "../shared/design-source-capabilities.js";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
isSafeCssTokenValue,
|
|
15
|
+
isSafeCssVarName,
|
|
16
|
+
resolveTweaksToCssVars,
|
|
17
|
+
} from "../shared/resolve-tweaks.js";
|
|
14
18
|
|
|
15
19
|
// ---------------------------------------------------------------------------
|
|
16
20
|
// Token-edit patch schema (mirrors preview-design-token-edit)
|
|
@@ -23,17 +27,20 @@ const tokenEditSchema = z.object({
|
|
|
23
27
|
.startsWith("--")
|
|
24
28
|
// The token name is spliced raw into a `:root { … }` rule, so constrain it
|
|
25
29
|
// to a safe CSS custom-property ident (leading "--" plus ident chars only).
|
|
26
|
-
.
|
|
27
|
-
|
|
30
|
+
.refine(
|
|
31
|
+
isSafeCssVarName,
|
|
28
32
|
"cssVar must be a valid CSS custom property name (-- followed by letters, digits, hyphens, or underscores).",
|
|
29
33
|
)
|
|
30
34
|
.describe("CSS custom property to edit"),
|
|
31
35
|
/** New value string, e.g. "#3B82F6" or "0.75rem". */
|
|
32
36
|
value: z
|
|
33
37
|
.string()
|
|
34
|
-
// The value is
|
|
35
|
-
//
|
|
36
|
-
.refine(
|
|
38
|
+
// The value is rendered into CSS custom-property declarations; reject
|
|
39
|
+
// declaration terminators and style-tag breakout characters.
|
|
40
|
+
.refine(
|
|
41
|
+
isSafeCssTokenValue,
|
|
42
|
+
'Token value may not contain ";", "{", "}", "<", ">", CSS comments, or control characters.',
|
|
43
|
+
)
|
|
37
44
|
.describe("New value for the token"),
|
|
38
45
|
});
|
|
39
46
|
|
|
@@ -17,7 +17,11 @@ import { z } from "zod";
|
|
|
17
17
|
|
|
18
18
|
import { getDb, schema } from "../server/db/index.js";
|
|
19
19
|
import "../server/db/index.js"; // ensure registerShareableResource runs
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
isSafeCssTokenValue,
|
|
22
|
+
isSafeCssVarName,
|
|
23
|
+
resolveTweaksToCssVars,
|
|
24
|
+
} from "../shared/resolve-tweaks.js";
|
|
21
25
|
|
|
22
26
|
type ImportedTokenType =
|
|
23
27
|
| "color"
|
|
@@ -174,10 +178,6 @@ function normalizeValue(value: string): string {
|
|
|
174
178
|
.trim();
|
|
175
179
|
}
|
|
176
180
|
|
|
177
|
-
function isSafeTokenValue(value: string): boolean {
|
|
178
|
-
return value.length > 0 && value.length <= 300 && !/[}<]/.test(value);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
181
|
function tokenVar(prefix: string, key: string): string {
|
|
182
182
|
if (key.startsWith("--")) return key;
|
|
183
183
|
const stem = toKebab(key);
|
|
@@ -224,7 +224,7 @@ function parseNamedLines(
|
|
|
224
224
|
|
|
225
225
|
const label = match[1].replace(/\*\*/g, "").trim();
|
|
226
226
|
const value = normalizeValue(match[2]);
|
|
227
|
-
if (!
|
|
227
|
+
if (!isSafeCssTokenValue(value)) continue;
|
|
228
228
|
|
|
229
229
|
const lower = label.toLowerCase();
|
|
230
230
|
let cssVar: string | null = null;
|
|
@@ -261,7 +261,7 @@ function uniqueTokenList(tokens: ImportedDesignToken[]): ImportedDesignToken[] {
|
|
|
261
261
|
|
|
262
262
|
for (const token of tokens) {
|
|
263
263
|
if (!/^--[-_a-zA-Z0-9]+$/.test(token.cssVar)) continue;
|
|
264
|
-
if (!
|
|
264
|
+
if (!isSafeCssTokenValue(token.value)) continue;
|
|
265
265
|
const previous = byVar.get(token.cssVar);
|
|
266
266
|
byVar.set(token.cssVar, token);
|
|
267
267
|
if (!previous && !/^--color-imported-\d+$/.test(token.cssVar)) {
|
|
@@ -404,7 +404,11 @@ export default defineAction({
|
|
|
404
404
|
}
|
|
405
405
|
|
|
406
406
|
const { filesAnalyzed, tokens } = tokensFromFiles(importFiles);
|
|
407
|
-
|
|
407
|
+
const safeTokens = tokens.filter(
|
|
408
|
+
(token) =>
|
|
409
|
+
isSafeCssVarName(token.cssVar) && isSafeCssTokenValue(token.value),
|
|
410
|
+
);
|
|
411
|
+
if (safeTokens.length === 0) {
|
|
408
412
|
return {
|
|
409
413
|
designId,
|
|
410
414
|
source,
|
|
@@ -452,7 +456,7 @@ export default defineAction({
|
|
|
452
456
|
: {};
|
|
453
457
|
const nextSelections = { ...prevSelections };
|
|
454
458
|
|
|
455
|
-
for (const token of
|
|
459
|
+
for (const token of safeTokens) {
|
|
456
460
|
const tweakId = cssVarToTweakId.get(token.cssVar);
|
|
457
461
|
nextSelections[tweakId ?? token.cssVar] = token.value;
|
|
458
462
|
}
|
|
@@ -464,7 +468,7 @@ export default defineAction({
|
|
|
464
468
|
? (prevData.tokenImportSources as Record<string, string>)
|
|
465
469
|
: {};
|
|
466
470
|
const tokenImportSources = { ...previousSources };
|
|
467
|
-
for (const token of
|
|
471
|
+
for (const token of safeTokens) {
|
|
468
472
|
tokenImportSources[token.cssVar] =
|
|
469
473
|
token.source === "CSS variables" || token.source === "Colors"
|
|
470
474
|
? sourceLabel
|
|
@@ -484,7 +488,7 @@ export default defineAction({
|
|
|
484
488
|
{
|
|
485
489
|
source,
|
|
486
490
|
sourceLabel,
|
|
487
|
-
tokenCount:
|
|
491
|
+
tokenCount: safeTokens.length,
|
|
488
492
|
filesAnalyzed,
|
|
489
493
|
importedAt: now,
|
|
490
494
|
},
|
|
@@ -500,10 +504,10 @@ export default defineAction({
|
|
|
500
504
|
return {
|
|
501
505
|
designId,
|
|
502
506
|
source,
|
|
503
|
-
importedCount:
|
|
507
|
+
importedCount: safeTokens.length,
|
|
504
508
|
skippedCount: importFiles.length - filesAnalyzed.length,
|
|
505
509
|
filesAnalyzed,
|
|
506
|
-
tokens,
|
|
510
|
+
tokens: safeTokens,
|
|
507
511
|
resolvedCssVars: resolveTweaksToCssVars(tweaks, nextSelections),
|
|
508
512
|
deepLink: designDeepLink(designId),
|
|
509
513
|
};
|
|
@@ -3,7 +3,11 @@ import { resolveAccess } from "@agent-native/core/sharing";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
|
|
5
5
|
import "../server/db/index.js"; // ensure registerShareableResource runs
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
isSafeCssTokenValue,
|
|
8
|
+
isSafeCssVarName,
|
|
9
|
+
resolveTweaksToCssVars,
|
|
10
|
+
} from "../shared/resolve-tweaks.js";
|
|
7
11
|
|
|
8
12
|
// ---------------------------------------------------------------------------
|
|
9
13
|
// Token-edit patch schema
|
|
@@ -11,9 +15,22 @@ import { resolveTweaksToCssVars } from "../shared/resolve-tweaks.js";
|
|
|
11
15
|
|
|
12
16
|
const tokenEditSchema = z.object({
|
|
13
17
|
/** The CSS custom property to update, e.g. "--primary-color". */
|
|
14
|
-
cssVar: z
|
|
18
|
+
cssVar: z
|
|
19
|
+
.string()
|
|
20
|
+
.startsWith("--")
|
|
21
|
+
.refine(
|
|
22
|
+
isSafeCssVarName,
|
|
23
|
+
"cssVar must be a valid CSS custom property name (-- followed by letters, digits, hyphens, or underscores).",
|
|
24
|
+
)
|
|
25
|
+
.describe("CSS custom property to edit"),
|
|
15
26
|
/** New value string, e.g. "#3B82F6" or "0.75rem". */
|
|
16
|
-
value: z
|
|
27
|
+
value: z
|
|
28
|
+
.string()
|
|
29
|
+
.refine(
|
|
30
|
+
isSafeCssTokenValue,
|
|
31
|
+
'Token value may not contain ";", "{", "}", "<", ">", CSS comments, or control characters.',
|
|
32
|
+
)
|
|
33
|
+
.describe("New value for the token"),
|
|
17
34
|
});
|
|
18
35
|
|
|
19
36
|
// ---------------------------------------------------------------------------
|
|
@@ -196,13 +196,20 @@ export function checkTapTargets(html: string): A11yFinding[] {
|
|
|
196
196
|
const findings: A11yFinding[] = [];
|
|
197
197
|
// Heuristic: buttons/links with explicit tiny size classes (h-4, h-5, w-4, w-5, size-4, size-5)
|
|
198
198
|
// and no explicit larger override or sr-only are flagged.
|
|
199
|
-
const interactivePattern =
|
|
199
|
+
const interactivePattern =
|
|
200
|
+
/<(button|a|input|select|textarea)\b[^>]*(?:\/>|>)/gi;
|
|
200
201
|
const tinyPattern = /\b(?:h|w|size)-[345]\b/;
|
|
201
202
|
const largePattern = /\b(?:h|w|size)-(?:[6-9]|[1-9]\d)/;
|
|
202
203
|
const srOnlyPattern = /\bsr-only\b/;
|
|
203
204
|
let idx = 0;
|
|
204
205
|
for (const m of html.matchAll(interactivePattern)) {
|
|
205
206
|
const tag = m[0];
|
|
207
|
+
const tagName = (m[1] ?? "button").toLowerCase();
|
|
208
|
+
const typeMatch = tag.match(/\btype\s*=\s*(?:"([^"]*?)"|'([^']*?)')/i);
|
|
209
|
+
if (tagName === "input") {
|
|
210
|
+
const type = (typeMatch?.[1] ?? typeMatch?.[2] ?? "text").toLowerCase();
|
|
211
|
+
if (type === "hidden") continue;
|
|
212
|
+
}
|
|
206
213
|
if (
|
|
207
214
|
tinyPattern.test(tag) &&
|
|
208
215
|
!largePattern.test(tag) &&
|
|
@@ -217,7 +224,7 @@ export function checkTapTargets(html: string): A11yFinding[] {
|
|
|
217
224
|
detail:
|
|
218
225
|
"Minimum recommended tap target size is 44×44 px (WCAG 2.5.5). Consider increasing padding or size.",
|
|
219
226
|
nodeId: extractNodeId(tag),
|
|
220
|
-
selector: extractSelector(tag,
|
|
227
|
+
selector: extractSelector(tag, tagName),
|
|
221
228
|
wcag: "2.5.5",
|
|
222
229
|
fixAvailable: true,
|
|
223
230
|
});
|
|
@@ -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
|