plans/20-section-restoration.md
Follow the verification gates in order. This is a plan for a later executor, not a claim that all three reports have the same cause. Stop when the source or reproduction contradicts the stated assumptions. Use the repository's execution and review skills when available.
7a98f6662, 2026-09-05.useUpdateResumeData for mutations so autosave and undo remain intact. No new persisted visibility flag and no automatic repair of imported layouts.| Issue | Requested behavior | Verified facts and limits | Required final evidence |
|---|---|---|---|
| #3378 | Re-add an existing section after removal in Layout without Undo. | Current Layout has no section-delete action; deleting a page moves its IDs to another page. Show toggles visibility but does not recreate a missing layout reference. Reporter has not supplied version or affected JSON. | Reproduce the reported action or inspect its JSON; prove IDs/content survive and explicit placement restores output without duplicates. |
| #3265 | Missing drag targets in Leafish Layout. | Layout intentionally excludes hidden sections and sections without a visible item with a primary title. A screenshot alone does not distinguish those states from missing IDs. | Match affected section IDs and content to the screenshot, then test the exact visibility/placement cause. |
| #2921 | Separate hidden sections in the left sidebar and distinguish them in Layout. | Left sidebar still mounts every section editor; Layout already filters hidden items. The existing hidden flag controls resume output, not just editor visibility. | A hidden built-in, summary, and custom section each appears in a compact recovery area, stays absent from output, and returns to its original position on Show. |
The owner previously accepted investigating the combined hidden-section UX in #2921. There is no authorization to reinterpret hidden as a builder-only preference.
Paths are repository-relative. Run this drift check before changing code:
rtk proxy git diff --stat 7a98f6662..HEAD -- packages/resume apps/web/src/features/resume/builder apps/web/src/routes/builder apps/web/src/libs/resume
Inspect changed source before using this plan; stop if section identity, ownership, or layout semantics have changed.
apps/web/src/routes/builder/$resumeId/-sidebar/left/index.tsx, BuilderSidebarLeft, around lines 78–84, renders all entries:
{leftSidebarSections.map((section) => (
<Fragment key={section}>
{getSectionComponent(section)}
<Separator />
</Fragment>
))}
apps/web/src/routes/builder/$resumeId/-sidebar/right/sections/layout/pages.tsx, around lines 250–260, passes filtered arrays to PageContainer:
main: filterVisibleLayoutSectionIds(page.main, resume.data),
sidebar: filterVisibleLayoutSectionIds(page.sidebar, resume.data),
Its visibility.ts checks summary content and item primary titles. hasVisibleItems returns !section.hidden && section.items.some((item) => !item.hidden && hasValidPrimaryTitle(item, sectionType)). This explains why an empty section may be absent without being lost. Do not use this content filter to decide whether a section exists.
left/shared/section-menu.tsx, onToggleVisibility, changes only summary.hidden or sections[type].hidden. Reset removes content after confirmation. Preserve that distinction.
left/sections/custom.tsx owns custom-section editor cards and their menu. Custom IDs come from data.customSections; the left sidebar's custom entry is an editor container, not a printable section ID.
apps/web/src/features/resume/builder/draft.ts owns useUpdateResumeData, undo/redo, and save scheduling. Call its public hook; do not mutate a fetched Query cache or add an independent save request.
packages/resume/src/section-availability.ts and .test.ts, with an explicit ./section-availability export in packages/resume/package.json if no equivalent helper exists at execution time.apps/web/src/features/resume/builder/section-recovery.tsx and .test.tsx.tests/e2e/specs/section-editing.spec.ts, or add a focused section-recovery.spec.ts using the same authenticated fixture from tests/e2e/fixtures/test.ts.sampleResumeData in a test. Construct cases with: visible placed section; hidden placed section; visible unplaced section; hidden unplaced section; empty placed section; custom section; unknown layout ID; and an existing ID on a later page's sidebar.metadata.layout.pages[*].main/sidebar array without changing its section record.getSectionAvailability(data) returns known printable section IDs, their hidden state, and zero or more { pageIndex, columnId } locations. A separate placement operation validates the target and appends only when the ID is currently unplaced. Unknown IDs and invalid targets produce an explicit failure and leave input unchanged. Repeated placement must be a no-op, not a duplicate.Run rtk proxy pnpm --filter @reactive-resume/resume exec vitest run src/section-availability.test.ts. New assertions must fail before the helper exists and pass after implementing it. Use existing packages/resume/src/export-sections.test.ts as the data-driven Vitest style exemplar, not as evidence of placement correctness.
Use this proposed helper contract so the later UI does not invent a second definition of placement:
type SectionLocation = { pageIndex: number; columnId: "main" | "sidebar" };
type SectionAvailability = {
sectionId: string;
hidden: boolean;
locations: SectionLocation[];
};
// Include summary, every built-in section, and each real custom section.
// Exclude picture, basics, and the UI-only "custom" container.
function getSectionAvailability(data: ResumeData): SectionAvailability[];
The declaration is a proposed interface, not an existing export. Derive locations from the saved arrays, never the filtered Layout UI arrays. Keep localized titles in the web component rather than introducing Lingui into the pure helper. A concrete regression assertion is:
const data = structuredClone(sampleResumeData);
data.sections.experience.hidden = true;
for (const page of data.metadata.layout.pages) {
page.main = page.main.filter((id) => id !== "experience");
page.sidebar = page.sidebar.filter((id) => id !== "experience");
}
const before = structuredClone(data);
expect(getSectionAvailability(data).find((entry) => entry.sectionId === "experience"))
.toEqual({ sectionId: "experience", hidden: true, locations: [] });
expect(data).toEqual(before);
Wrap this assertion in a Vitest test, importing the sample from @reactive-resume/schema/resume/sample. Add a second test that places the same section on an existing later page and expects that exact location even while hidden. These assertions distinguish content availability from presentation filtering.
hidden flag through useUpdateResumeData.Run rtk proxy pnpm --filter web exec vitest run src/features/resume/builder/section-recovery.test.tsx. Assert user-visible behavior and emitted data, not source-string presence. Use left/shared/section-menu.test.tsx as the existing provider/menu test pattern.
Run the new helper and DOM tests, plus the existing Layout visibility tests:
rtk proxy pnpm --filter web exec vitest run 'src/routes/builder/$resumeId/-sidebar/right/sections/layout/visibility.test.ts'
rtk proxy pnpm --filter web typecheck
rtk proxy pnpm --filter @reactive-resume/resume typecheck
rtk proxy pnpm exec turbo boundaries
Each command must exit 0. If implementing this step requires changing import behavior or persisted schema, stop and revise the plan with the maintainer first.
Run production E2E with a dedicated database and repository environment setup:
rtk proxy pnpm build
rtk proxy dotenvx run -f .env.local -- pnpm exec playwright test tests/e2e/specs/section-recovery.spec.ts --reporter=list
The test fixture owns its disposable account/data. Never point this command at a production database. Run pnpm check only after acknowledging that it writes files, and inspect the resulting diff before committing.
Stop if the source section record is actually gone; layout placement cannot recover deleted content. Stop if the requested UX requires treating hidden sections as visible output. Stop if an external update changes page/section identity during implementation. Preserve those findings for the maintainer instead of inventing a migration.