feat(web): stack entry fields in one column and let dates fill the row

The write panel is narrow, so an entry's fields now take the whole
column. Start and end dates still share a line and now span it, with
Present below. A new entry's badge just says Draft.
This commit is contained in:
Amruth Pillai
2026-09-29 14:25:53 +02:00
parent 3d65aea58c
commit 0a5323520f
5 changed files with 23 additions and 18 deletions
@@ -49,7 +49,7 @@ function MonthYearInput({
const formatted = value ? formatYearMonth(value, { locale, format }) : "";
return (
<div className="relative w-[132px]">
<div className="relative min-w-0 flex-1">
<Icon
name="calendar_month"
size={18}
@@ -109,7 +109,7 @@ export function DatesField({ dates, single = false, locale, format, onChange, cl
{single ? <Trans>Date</Trans> : <Trans>Dates</Trans>}
</legend>
<div className="flex flex-wrap items-center gap-2">
<div className="flex items-center gap-2">
<MonthYearInput
label={single ? t`Date` : t`Start`}
value={dates.start}
@@ -134,18 +134,23 @@ export function DatesField({ dates, single = false, locale, format, onChange, cl
onCommit={(end) => onChange({ ...current, end })}
onInvalid={setInvalidFor("end")}
/>
{/* biome-ignore lint/a11y/noLabelWithoutControl: Base UI's Switch is the control; wrapping it in a label is its documented pattern. */}
<label className="flex cursor-pointer items-center gap-2 p-1 text-sm">
<Switch
checked={dates.present}
onCheckedChange={(present) => onChange({ ...current, present, end: present ? null : dates.end })}
/>
<Trans>Present</Trans>
</label>
</>
)}
</div>
{!single && (
<>
{/* biome-ignore lint/a11y/noLabelWithoutControl: Base UI's Switch is the control; wrapping it in a label is its documented pattern. */}
<label className="flex w-fit cursor-pointer items-center gap-2 p-1 text-sm">
<Switch
checked={dates.present}
onCheckedChange={(present) => onChange({ ...current, present, end: present ? null : dates.end })}
/>
<Trans>Present</Trans>
</label>
</>
)}
<div id={`${id}-note`}>
{hasError ? (
<p className="flex items-start gap-1 text-danger-text text-xs leading-4">
@@ -142,7 +142,7 @@ export function EntryCard({ section, entryId, index, count, page, locked, onMove
</span>
{draft && (
<Badge variant="neutral" className="shrink-0">
<Trans>Draft · not printed</Trans>
<Trans>Draft</Trans>
</Badge>
)}
{entry.hidden && (
@@ -171,7 +171,7 @@ export function EntryCard({ section, entryId, index, count, page, locked, onMove
<CollapsibleContent>
<fieldset
disabled={locked}
className="m-0 grid min-w-0 grid-cols-2 gap-x-3 gap-y-2.5 border-0 border-line border-t px-3 pt-3 pb-3.5"
className="m-0 grid min-w-0 gap-y-2.5 border-0 border-line border-t px-3 pt-3 pb-3.5"
>
{fields}
</fieldset>
@@ -237,7 +237,7 @@ function PhoneEntryScreen({ section, title, locked, onBack, onDelete, children }
</div>
<fieldset
disabled={locked}
className="m-0 grid min-w-0 flex-1 auto-rows-min grid-cols-2 gap-x-3 gap-y-3 overflow-y-auto border-0 p-4 [&_input]:h-11"
className="m-0 grid min-w-0 flex-1 auto-rows-min gap-y-3 overflow-y-auto border-0 p-4 [&_input]:h-11"
>
{children}
</fieldset>
@@ -167,7 +167,7 @@ export function MoreOptions({ children }: MoreOptionsProps) {
<Trans>More options</Trans>
</CollapsibleTrigger>
<CollapsibleContent hiddenUntilFound>
<div className="grid grid-cols-2 gap-x-3 gap-y-2.5 pt-3">{children}</div>
<div className="grid gap-y-2.5 pt-3">{children}</div>
</CollapsibleContent>
</Collapsible>
);
@@ -95,12 +95,12 @@ describe("WritePanel", () => {
fireEvent.click(within(experience).getByRole("button", { name: "Add experience" }));
await waitFor(() => expect(within(experience).getByText("Draft · not printed")).toBeInTheDocument());
await waitFor(() => expect(within(experience).getByText("Draft", { exact: true })).toBeInTheDocument());
expect(document.activeElement).toHaveAttribute("data-entry-field", "position");
fireEvent.change(within(experience).getByRole("textbox", { name: "Company" }), { target: { value: "Lumen" } });
expect(within(experience).queryByText("Draft · not printed")).not.toBeInTheDocument();
expect(within(experience).queryByText("Draft", { exact: true })).not.toBeInTheDocument();
expect(data().sections.experience.items.at(-1)).toMatchObject({ company: "Lumen" });
});
+2 -2
View File
@@ -12,7 +12,7 @@ test("adds an experience item and persists it across reloads", async ({ authPage
// The new draft opens in place with its first field focused; it saves as you type.
const entry = page.locator("#sidebar-experience [data-entry-id]").last();
await expect(entry.getByText("Draft · not printed")).toBeVisible();
await expect(entry.getByText("Draft", { exact: true })).toBeVisible();
await expect(entry.getByRole("textbox", { name: "Position" })).toBeFocused();
const savePromise = page.waitForResponse((response) => {
@@ -26,7 +26,7 @@ test("adds an experience item and persists it across reloads", async ({ authPage
await savePromise;
// With a company it's no longer a draft, and its card reads "company · …"
await expect(entry.getByText("Draft · not printed")).toHaveCount(0);
await expect(entry.getByText("Draft", { exact: true })).toHaveCount(0);
await expect(page.getByText(company).filter({ visible: true }).first()).toBeVisible();
// And it survives a full reload