From 4ac19f81b3d290dd063764ff6b0fa3dd94d34632 Mon Sep 17 00:00:00 2001 From: EMRAN <139449306+Emran-Y@users.noreply.github.com> Date: Wed, 29 Jul 2026 19:13:42 +0300 Subject: [PATCH] fix: clamp page margin values to [0, 100] to prevent crash on paste (#3277) Co-authored-by: Amruth Pillai Co-authored-by: Cursor Agent --- .../-sidebar/right/sections/page.tsx | 29 +++++++++++++++---- packages/schema/src/resume/data.test.ts | 13 +++++++++ packages/schema/src/resume/data.ts | 4 +-- 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/apps/web/src/routes/builder/$resumeId/-sidebar/right/sections/page.tsx b/apps/web/src/routes/builder/$resumeId/-sidebar/right/sections/page.tsx index d1b91bea5..06f6bdcbb 100644 --- a/apps/web/src/routes/builder/$resumeId/-sidebar/right/sections/page.tsx +++ b/apps/web/src/routes/builder/$resumeId/-sidebar/right/sections/page.tsx @@ -29,6 +29,11 @@ const formSchema = pageSchema; type FormValues = z.infer; +const CLAMP_MIN = 0; +const CLAMP_MAX = 100; + +const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max); + function PageSectionForm() { const resume = useResume(); const page = resume?.data.metadata.page; @@ -50,12 +55,17 @@ function PageSectionForm() { useSyncFormValues(form, page); const handleAutoSave = (name: K, value: FormValues[K]) => { - persist({ ...form.state.values, [name]: value }); + const next = { ...form.state.values, [name]: value }; + // Keep last-saved numeric page fields when the form holds a transient empty/NaN value + for (const key of ["marginX", "marginY", "gapX", "gapY"] as const) { + if (!Number.isFinite(next[key])) next[key] = page?.[key] ?? 0; + } + persist(next); }; const pageNumberFields = [ - { name: "marginX" as const, label: Margin (Horizontal), min: 0, max: 100 as number | undefined }, - { name: "marginY" as const, label: Margin (Vertical), min: 0, max: 100 as number | undefined }, + { name: "marginX" as const, label: Margin (Horizontal), min: CLAMP_MIN, max: CLAMP_MAX }, + { name: "marginY" as const, label: Margin (Vertical), min: CLAMP_MIN, max: CLAMP_MAX }, { name: "gapX" as const, label: Spacing (Horizontal), min: 0, max: undefined }, { name: "gapY" as const, label: Spacing (Vertical), min: 0, max: undefined }, ]; @@ -143,7 +153,7 @@ function PageSectionForm() { render={ { const v = e.target.value; - const num = v === "" ? ("" as unknown as number) : Number(v); + if (v === "") { + // Allow clearing the controlled input without persisting invalid metadata + field.handleChange(Number.NaN); + return; + } + + const raw = Number(v); + if (!Number.isFinite(raw)) return; + + const num = max !== undefined ? clamp(raw, min ?? 0, max) : Math.max(raw, min ?? 0); field.handleChange(num); handleAutoSave(name, num); }} diff --git a/packages/schema/src/resume/data.test.ts b/packages/schema/src/resume/data.test.ts index 6c1bfa392..70de04218 100644 --- a/packages/schema/src/resume/data.test.ts +++ b/packages/schema/src/resume/data.test.ts @@ -260,6 +260,19 @@ describe("pageSchema", () => { expect(pageSchema.safeParse(invalid).success).toBe(false); }); + it("falls back to default margins when out of range via .catch", () => { + const result = pageSchema.safeParse({ + ...defaultResumeData.metadata.page, + marginX: 1224, + marginY: 999, + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.marginX).toBe(14); + expect(result.data.marginY).toBe(12); + } + }); + it("falls back to 'a4' for unknown format via .catch", () => { const invalid = { ...defaultResumeData.metadata.page, format: "huge" }; const result = pageSchema.safeParse(invalid); diff --git a/packages/schema/src/resume/data.ts b/packages/schema/src/resume/data.ts index e7603cb87..a06317d11 100644 --- a/packages/schema/src/resume/data.ts +++ b/packages/schema/src/resume/data.ts @@ -444,8 +444,8 @@ export const layoutSchema = z.object({ export const pageSchema = z.object({ gapX: z.number().min(0).describe("The horizontal gap between the sections of the page, defined in points (pt)."), gapY: z.number().min(0).describe("The vertical gap between the sections of the page, defined in points (pt)."), - marginX: z.number().min(0).describe("The horizontal margin of the page, defined in points (pt)."), - marginY: z.number().min(0).describe("The vertical margin of the page, defined in points (pt)."), + marginX: z.number().min(0).max(100).catch(14).describe("The horizontal margin of the page, defined in points (pt)."), + marginY: z.number().min(0).max(100).catch(12).describe("The vertical margin of the page, defined in points (pt)."), format: z .enum(["a4", "letter", "free-form"]) .describe("The format of the page. Can be 'a4', 'letter', or 'free-form'.")