refactor(web): findings 2/3/4 in page/design/custom-styles

Finding 2: Replace 4 number + 3 switch form.Field blocks with
pageNumberFields/pageSwitchFields array maps.
Finding 3: Extract ColorFormField helper for primary/text/background
color fields in ColorSectionForm.
Finding 4: Remove labelPrefix (was always component-specific), replace
local slugify with @reactive-resume/utils/string import; fix ariaLabel
template literals to keep test labels accurate.

Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
This commit is contained in:
Amruth Pillai
2026-07-04 21:28:47 +02:00
parent 15448cad6a
commit 9b9d5c833c
3 changed files with 169 additions and 355 deletions
@@ -15,6 +15,7 @@ import { Button } from "@reactive-resume/ui/components/button";
import { Input } from "@reactive-resume/ui/components/input";
import { Label } from "@reactive-resume/ui/components/label";
import { Separator } from "@reactive-resume/ui/components/separator";
import { slugify } from "@reactive-resume/utils/string";
import { cn } from "@reactive-resume/utils/style";
import { ColorPicker } from "@/components/input/color-picker";
import { Combobox } from "@/components/ui/combobox";
@@ -471,19 +472,16 @@ function RuleScopePill({ target, slot }: RuleScopePillProps) {
type RuleIntentEditorProps = {
idPrefix: string;
intent: StyleIntent;
labelPrefix?: string;
onChange: (patch: Partial<StyleIntent>) => void;
};
function RuleIntentEditor({ idPrefix, intent, labelPrefix, onChange }: RuleIntentEditorProps) {
const labelStart = labelPrefix ? `${labelPrefix} ` : "";
function RuleIntentEditor({ idPrefix, intent, onChange }: RuleIntentEditorProps) {
return (
<div className="space-y-3">
<ControlPanel title="Color">
<div className={exactFourControlGridClassName}>
<ColorField
label={`${labelStart}Text Color`}
label={"Text Color"}
id={`${idPrefix}-color`}
value={intent.color}
placeholder="rgba(0, 0, 0, 1)"
@@ -491,7 +489,7 @@ function RuleIntentEditor({ idPrefix, intent, labelPrefix, onChange }: RuleInten
onChange={(color) => onChange({ color })}
/>
<ColorField
label={`${labelStart}Background`}
label={"Background"}
id={`${idPrefix}-background`}
value={intent.backgroundColor}
placeholder="rgba(255, 255, 255, 1)"
@@ -499,7 +497,7 @@ function RuleIntentEditor({ idPrefix, intent, labelPrefix, onChange }: RuleInten
onChange={(backgroundColor) => onChange({ backgroundColor })}
/>
<ColorField
label={`${labelStart}Text Decoration Color`}
label={"Text Decoration Color"}
id={`${idPrefix}-text-decoration-color`}
value={intent.textDecorationColor}
placeholder="rgba(0, 0, 0, 1)"
@@ -507,7 +505,7 @@ function RuleIntentEditor({ idPrefix, intent, labelPrefix, onChange }: RuleInten
onChange={(textDecorationColor) => onChange({ textDecorationColor })}
/>
<NumberInput
label={`${labelStart}Opacity`}
label={"Opacity"}
id={`${idPrefix}-opacity`}
value={intent.opacity}
min={0}
@@ -521,7 +519,7 @@ function RuleIntentEditor({ idPrefix, intent, labelPrefix, onChange }: RuleInten
<ControlPanel title="Text">
<div className={controlGridClassName}>
<NumberInput
label={`${labelStart}Font Size`}
label={"Font Size"}
id={`${idPrefix}-font-size`}
value={intent.fontSize}
min={6}
@@ -529,20 +527,20 @@ function RuleIntentEditor({ idPrefix, intent, labelPrefix, onChange }: RuleInten
onChange={(fontSize) => onChange({ fontSize })}
/>
<FontWeightField
label={`${labelStart}Font Weight`}
label={"Font Weight"}
id={`${idPrefix}-font-weight`}
value={intent.fontWeight}
onChange={(fontWeight) => onChange({ fontWeight })}
/>
<IntentSelectField
label={`${labelStart}Font Style`}
label={"Font Style"}
id={`${idPrefix}-font-style`}
value={intent.fontStyle}
options={fontStyleOptions}
onChange={(fontStyle) => onChange({ fontStyle })}
/>
<NumberInput
label={`${labelStart}Line Height`}
label={"Line Height"}
id={`${idPrefix}-line-height`}
value={intent.lineHeight}
min={0.5}
@@ -551,7 +549,7 @@ function RuleIntentEditor({ idPrefix, intent, labelPrefix, onChange }: RuleInten
onChange={(lineHeight) => onChange({ lineHeight })}
/>
<NumberInput
label={`${labelStart}Letter Spacing`}
label={"Letter Spacing"}
id={`${idPrefix}-letter-spacing`}
value={intent.letterSpacing}
min={-16}
@@ -560,28 +558,28 @@ function RuleIntentEditor({ idPrefix, intent, labelPrefix, onChange }: RuleInten
onChange={(letterSpacing) => onChange({ letterSpacing })}
/>
<IntentSelectField
label={`${labelStart}Text Decoration`}
label={"Text Decoration"}
id={`${idPrefix}-text-decoration`}
value={intent.textDecoration}
options={textDecorationOptions}
onChange={(textDecoration) => onChange({ textDecoration })}
/>
<IntentSelectField
label={`${labelStart}Decoration Style`}
label={"Decoration Style"}
id={`${idPrefix}-text-decoration-style`}
value={intent.textDecorationStyle}
options={textDecorationStyleOptions}
onChange={(textDecorationStyle) => onChange({ textDecorationStyle })}
/>
<IntentSelectField
label={`${labelStart}Text Align`}
label={"Text Align"}
id={`${idPrefix}-text-align`}
value={intent.textAlign}
options={textAlignOptions}
onChange={(textAlign) => onChange({ textAlign })}
/>
<IntentSelectField
label={`${labelStart}Text Transform`}
label={"Text Transform"}
id={`${idPrefix}-text-transform`}
value={intent.textTransform}
options={textTransformOptions}
@@ -592,11 +590,11 @@ function RuleIntentEditor({ idPrefix, intent, labelPrefix, onChange }: RuleInten
<ControlPanel title="Spacing">
<div className="space-y-3">
<PaddingSideInputs idPrefix={idPrefix} intent={intent} labelPrefix={labelPrefix} onChange={onChange} />
<MarginSideInputs idPrefix={idPrefix} intent={intent} labelPrefix={labelPrefix} onChange={onChange} />
<PaddingSideInputs idPrefix={idPrefix} intent={intent} onChange={onChange} />
<MarginSideInputs idPrefix={idPrefix} intent={intent} onChange={onChange} />
<SpacingInputGroup label="Gap">
<CompactNumberInput
ariaLabel={`${labelStart}Row Gap`}
ariaLabel={"Row Gap"}
id={`${idPrefix}-row-gap`}
placeholder="row"
value={intent.rowGap}
@@ -605,7 +603,7 @@ function RuleIntentEditor({ idPrefix, intent, labelPrefix, onChange }: RuleInten
onChange={(rowGap) => onChange({ rowGap })}
/>
<CompactNumberInput
ariaLabel={`${labelStart}Column Gap`}
ariaLabel={"Column Gap"}
id={`${idPrefix}-column-gap`}
placeholder="column"
value={intent.columnGap}
@@ -620,14 +618,14 @@ function RuleIntentEditor({ idPrefix, intent, labelPrefix, onChange }: RuleInten
<ControlPanel title="Border">
<div className={exactFourControlGridClassName}>
<IntentSelectField
label={`${labelStart}Border Style`}
label={"Border Style"}
id={`${idPrefix}-border-style`}
value={intent.borderStyle}
options={borderStyleOptions}
onChange={(borderStyle) => onChange({ borderStyle })}
/>
<NumberInput
label={`${labelStart}Border Width`}
label={"Border Width"}
id={`${idPrefix}-border-width`}
value={intent.borderWidth}
min={0}
@@ -635,7 +633,7 @@ function RuleIntentEditor({ idPrefix, intent, labelPrefix, onChange }: RuleInten
onChange={(borderWidth) => onChange({ borderWidth })}
/>
<NumberInput
label={`${labelStart}Border Radius`}
label={"Border Radius"}
id={`${idPrefix}-border-radius`}
value={intent.borderRadius}
min={0}
@@ -643,7 +641,7 @@ function RuleIntentEditor({ idPrefix, intent, labelPrefix, onChange }: RuleInten
onChange={(borderRadius) => onChange({ borderRadius })}
/>
<ColorField
label={`${labelStart}Border Color`}
label={"Border Color"}
id={`${idPrefix}-border-color`}
value={intent.borderColor}
placeholder="rgba(0, 0, 0, 1)"
@@ -751,19 +749,16 @@ type MarginSideProperty = (typeof marginSideOptions)[number]["property"];
type PaddingSideInputsProps = {
idPrefix: string;
intent: StyleIntent;
labelPrefix?: string;
onChange: (patch: Partial<StyleIntent>) => void;
};
function PaddingSideInputs({ idPrefix, intent, labelPrefix, onChange }: PaddingSideInputsProps) {
const labelStart = labelPrefix ? `${labelPrefix} ` : "";
function PaddingSideInputs({ idPrefix, intent, onChange }: PaddingSideInputsProps) {
return (
<SpacingInputGroup label="Padding">
{paddingSideOptions.map((side) => (
<CompactNumberInput
key={side.property}
ariaLabel={`${labelStart}Padding ${side.label}`}
ariaLabel={`Padding ${side.label}`}
id={`${idPrefix}-${side.property}`}
placeholder={side.label.toLowerCase()}
value={getPaddingSideValue(intent, side.property)}
@@ -779,19 +774,16 @@ function PaddingSideInputs({ idPrefix, intent, labelPrefix, onChange }: PaddingS
type MarginSideInputsProps = {
idPrefix: string;
intent: StyleIntent;
labelPrefix?: string;
onChange: (patch: Partial<StyleIntent>) => void;
};
function MarginSideInputs({ idPrefix, intent, labelPrefix, onChange }: MarginSideInputsProps) {
const labelStart = labelPrefix ? `${labelPrefix} ` : "";
function MarginSideInputs({ idPrefix, intent, onChange }: MarginSideInputsProps) {
return (
<SpacingInputGroup label="Margin">
{marginSideOptions.map((side) => (
<CompactNumberInput
key={side.property}
ariaLabel={`${labelStart}Margin ${side.label}`}
ariaLabel={`Margin ${side.label}`}
id={`${idPrefix}-${side.property}`}
placeholder={side.label.toLowerCase()}
value={intent[side.property]}
@@ -1047,10 +1039,3 @@ function compactIntent(intent: Partial<StyleIntent>): StyleIntent {
function hasIntent(intent: StyleIntent | undefined) {
return Boolean(intent && Object.keys(intent).length > 0);
}
function slugify(value: string) {
return value
.replace(/[^a-zA-Z0-9]+/g, "-")
.replace(/(^-|-$)/g, "")
.toLowerCase();
}
@@ -1,3 +1,4 @@
import type { ReactNode } from "react";
import type z from "zod";
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
@@ -31,6 +32,20 @@ export function DesignSectionBuilder() {
type ColorValues = z.infer<typeof colorDesignSchema>;
function useColorSectionForm(colors: ColorValues, persist: (data: ColorValues) => void) {
const form = useAppForm({
defaultValues: colors,
validators: { onChange: colorDesignSchema },
onSubmit: ({ value }) => {
persist(value);
},
});
useSyncFormValues(form, colors);
return form;
}
type ColorSectionForm = ReturnType<typeof useColorSectionForm>;
function ColorSectionForm() {
const resume = useCurrentResume();
const colors = resume.data.metadata.design.colors;
@@ -42,14 +57,7 @@ function ColorSectionForm() {
});
};
const form = useAppForm({
defaultValues: colors,
validators: { onChange: colorDesignSchema },
onSubmit: ({ value }) => {
persist(value);
},
});
useSyncFormValues(form, colors);
const form = useColorSectionForm(colors, persist);
const handleAutoSave = () => {
persist(form.state.values);
@@ -85,108 +93,67 @@ function ColorSectionForm() {
)}
</form.Field>
<form.Field name="primary">
{(field) => (
<FormItem hasError={field.state.meta.isTouched && field.state.meta.errors.length > 0}>
<FormLabel>
<Trans>Primary Color</Trans>
</FormLabel>
<div className="flex items-center gap-3">
<ColorPicker
value={field.state.value}
onChange={(color) => {
field.handleChange(color);
handleAutoSave();
}}
/>
<FormControl
render={
<Input
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => {
field.handleChange(e.target.value);
handleAutoSave();
}}
/>
}
/>
</div>
<FormMessage errors={field.state.meta.errors} />
</FormItem>
)}
</form.Field>
<form.Field name="text">
{(field) => (
<FormItem hasError={field.state.meta.isTouched && field.state.meta.errors.length > 0}>
<FormLabel>
<Trans>Text Color</Trans>
</FormLabel>
<div className="flex items-center gap-3">
<ColorPicker
defaultValue={field.state.value}
onChange={(color) => {
field.handleChange(color);
handleAutoSave();
}}
/>
<FormControl
render={
<Input
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => {
field.handleChange(e.target.value);
handleAutoSave();
}}
/>
}
/>
</div>
<FormMessage errors={field.state.meta.errors} />
</FormItem>
)}
</form.Field>
<form.Field name="background">
{(field) => (
<FormItem hasError={field.state.meta.isTouched && field.state.meta.errors.length > 0}>
<FormLabel>
<Trans>Background Color</Trans>
</FormLabel>
<div className="flex items-center gap-3">
<ColorPicker
defaultValue={field.state.value}
onChange={(color) => {
field.handleChange(color);
handleAutoSave();
}}
/>
<FormControl
render={
<Input
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => {
field.handleChange(e.target.value);
handleAutoSave();
}}
/>
}
/>
</div>
<FormMessage errors={field.state.meta.errors} />
</FormItem>
)}
</form.Field>
<ColorFormField
form={form}
name="primary"
label={<Trans>Primary Color</Trans>}
controlled
handleAutoSave={handleAutoSave}
/>
<ColorFormField form={form} name="text" label={<Trans>Text Color</Trans>} handleAutoSave={handleAutoSave} />
<ColorFormField
form={form}
name="background"
label={<Trans>Background Color</Trans>}
handleAutoSave={handleAutoSave}
/>
</form>
);
}
type ColorFormFieldProps = {
form: ColorSectionForm;
name: keyof ColorValues;
label: ReactNode;
controlled?: boolean;
handleAutoSave: () => void;
};
function ColorFormField({ form, name, label, controlled, handleAutoSave }: ColorFormFieldProps) {
return (
<form.Field name={name}>
{(field) => (
<FormItem hasError={field.state.meta.isTouched && field.state.meta.errors.length > 0}>
<FormLabel>{label}</FormLabel>
<div className="flex items-center gap-3">
<ColorPicker
{...(controlled ? { value: field.state.value } : { defaultValue: field.state.value })}
onChange={(color) => {
field.handleChange(color);
handleAutoSave();
}}
/>
<FormControl
render={
<Input
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => {
field.handleChange(e.target.value);
handleAutoSave();
}}
/>
}
/>
</div>
<FormMessage errors={field.state.meta.errors} />
</FormItem>
)}
</form.Field>
);
}
const quickColorOptions = [
"rgba(231, 0, 11, 1)", // red-600
"rgba(245, 73, 0, 1)", // orange-600
@@ -53,6 +53,19 @@ function PageSectionForm() {
persist({ ...form.state.values, [name]: value });
};
const pageNumberFields = [
{ name: "marginX" as const, label: <Trans>Margin (Horizontal)</Trans>, min: 0, max: 100 as number | undefined },
{ name: "marginY" as const, label: <Trans>Margin (Vertical)</Trans>, min: 0, max: 100 as number | undefined },
{ name: "gapX" as const, label: <Trans>Spacing (Horizontal)</Trans>, min: 0, max: undefined },
{ name: "gapY" as const, label: <Trans>Spacing (Vertical)</Trans>, min: 0, max: undefined },
];
const pageSwitchFields = [
{ name: "hideLinkUnderline" as const, label: <Trans>Hide Link Underline</Trans> },
{ name: "hideIcons" as const, label: <Trans>Hide Icons</Trans> },
{ name: "hideSectionIcons" as const, label: <Trans>Hide Section Icons</Trans> },
];
return (
<form
className="grid @md:grid-cols-2 grid-cols-1 gap-4"
@@ -120,215 +133,64 @@ function PageSectionForm() {
)}
</form.Field>
<form.Field name="marginX">
{(field) => (
<FormItem hasError={field.state.meta.isTouched && field.state.meta.errors.length > 0}>
<FormLabel>
<Trans>Margin (Horizontal)</Trans>
</FormLabel>
<InputGroup>
<FormControl
render={
<InputGroupInput
name={field.name}
value={field.state.value}
min={0}
max={100}
step={1}
type="number"
onBlur={field.handleBlur}
onChange={(e) => {
const value = e.target.value;
const marginX = value === "" ? ("" as unknown as number) : Number(value);
field.handleChange(marginX);
handleAutoSave("marginX", marginX);
}}
/>
}
/>
<InputGroupAddon align="inline-end">
<InputGroupText>pt</InputGroupText>
</InputGroupAddon>
</InputGroup>
<FormMessage errors={field.state.meta.errors} />
</FormItem>
)}
</form.Field>
<form.Field name="marginY">
{(field) => (
<FormItem hasError={field.state.meta.isTouched && field.state.meta.errors.length > 0}>
<FormLabel>
<Trans>Margin (Vertical)</Trans>
</FormLabel>
<InputGroup>
<FormControl
render={
<InputGroupInput
name={field.name}
value={field.state.value}
min={0}
max={100}
step={1}
type="number"
onBlur={field.handleBlur}
onChange={(e) => {
const value = e.target.value;
const marginY = value === "" ? ("" as unknown as number) : Number(value);
field.handleChange(marginY);
handleAutoSave("marginY", marginY);
}}
/>
}
/>
<InputGroupAddon align="inline-end">
<InputGroupText>pt</InputGroupText>
</InputGroupAddon>
</InputGroup>
<FormMessage errors={field.state.meta.errors} />
</FormItem>
)}
</form.Field>
<form.Field name="gapX">
{(field) => (
<FormItem hasError={field.state.meta.isTouched && field.state.meta.errors.length > 0}>
<FormLabel>
<Trans>Spacing (Horizontal)</Trans>
</FormLabel>
<InputGroup>
<FormControl
render={
<InputGroupInput
name={field.name}
value={field.state.value}
min={0}
step={1}
type="number"
onBlur={field.handleBlur}
onChange={(e) => {
const value = e.target.value;
const gapX = value === "" ? ("" as unknown as number) : Number(value);
field.handleChange(gapX);
handleAutoSave("gapX", gapX);
}}
/>
}
/>
<InputGroupAddon align="inline-end">
<InputGroupText>pt</InputGroupText>
</InputGroupAddon>
</InputGroup>
<FormMessage errors={field.state.meta.errors} />
</FormItem>
)}
</form.Field>
<form.Field name="gapY">
{(field) => (
<FormItem hasError={field.state.meta.isTouched && field.state.meta.errors.length > 0}>
<FormLabel>
<Trans>Spacing (Vertical)</Trans>
</FormLabel>
<InputGroup>
<FormControl
render={
<InputGroupInput
name={field.name}
value={field.state.value}
min={0}
step={1}
type="number"
onBlur={field.handleBlur}
onChange={(e) => {
const value = e.target.value;
const gapY = value === "" ? ("" as unknown as number) : Number(value);
field.handleChange(gapY);
handleAutoSave("gapY", gapY);
}}
/>
}
/>
<InputGroupAddon align="inline-end">
<InputGroupText>pt</InputGroupText>
</InputGroupAddon>
</InputGroup>
<FormMessage errors={field.state.meta.errors} />
</FormItem>
)}
</form.Field>
<form.Field name="hideLinkUnderline">
{(field) => (
<FormItem
className="col-span-full flex items-center gap-x-3 py-1"
hasError={field.state.meta.isTouched && field.state.meta.errors.length > 0}
>
<FormControl
render={
<Switch
checked={field.state.value}
onCheckedChange={(checked) => {
field.handleChange(checked);
handleAutoSave("hideLinkUnderline", checked);
}}
{pageNumberFields.map(({ name, label, min, max }) => (
<form.Field key={name} name={name}>
{(field) => (
<FormItem hasError={field.state.meta.isTouched && field.state.meta.errors.length > 0}>
<FormLabel>{label}</FormLabel>
<InputGroup>
<FormControl
render={
<InputGroupInput
name={field.name}
value={field.state.value}
min={min}
{...(max !== undefined ? { max } : {})}
step={1}
type="number"
onBlur={field.handleBlur}
onChange={(e) => {
const v = e.target.value;
const num = v === "" ? ("" as unknown as number) : Number(v);
field.handleChange(num);
handleAutoSave(name, num);
}}
/>
}
/>
}
/>
<FormLabel>
<Trans>Hide Link Underline</Trans>
</FormLabel>
</FormItem>
)}
</form.Field>
<InputGroupAddon align="inline-end">
<InputGroupText>pt</InputGroupText>
</InputGroupAddon>
</InputGroup>
<FormMessage errors={field.state.meta.errors} />
</FormItem>
)}
</form.Field>
))}
<form.Field name="hideIcons">
{(field) => (
<FormItem
className="col-span-full flex items-center gap-x-3 py-1"
hasError={field.state.meta.isTouched && field.state.meta.errors.length > 0}
>
<FormControl
render={
<Switch
checked={field.state.value}
onCheckedChange={(checked) => {
field.handleChange(checked);
handleAutoSave("hideIcons", checked);
}}
/>
}
/>
<FormLabel>
<Trans>Hide Icons</Trans>
</FormLabel>
</FormItem>
)}
</form.Field>
<form.Field name="hideSectionIcons">
{(field) => (
<FormItem
className="col-span-full flex items-center gap-x-3 py-1"
hasError={field.state.meta.isTouched && field.state.meta.errors.length > 0}
>
<FormControl
render={
<Switch
checked={field.state.value}
onCheckedChange={(checked) => {
field.handleChange(checked);
handleAutoSave("hideSectionIcons", checked);
}}
/>
}
/>
<FormLabel>
<Trans>Hide Section Icons</Trans>
</FormLabel>
</FormItem>
)}
</form.Field>
{pageSwitchFields.map(({ name, label }) => (
<form.Field key={name} name={name}>
{(field) => (
<FormItem
className="col-span-full flex items-center gap-x-3 py-1"
hasError={field.state.meta.isTouched && field.state.meta.errors.length > 0}
>
<FormControl
render={
<Switch
checked={field.state.value}
onCheckedChange={(checked) => {
field.handleChange(checked);
handleAutoSave(name, checked);
}}
/>
}
/>
<FormLabel>{label}</FormLabel>
</FormItem>
)}
</form.Field>
))}
</form>
);
}