mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-23 23:02:17 +10:00
280 lines
14 KiB
Markdown
280 lines
14 KiB
Markdown
# Plan 007: Extract a shared template page-shell — spike + one-template pilot
|
||
|
||
> **Executor instructions**: This is a **spike + pilot**, not a 15-file
|
||
> rewrite. You will build a parity net, refactor exactly ONE template behind it,
|
||
> and then STOP and report. Do NOT migrate the other templates in this plan.
|
||
> Run every verification. Honor "STOP conditions". When done, update the status
|
||
> row in `plans/README.md`.
|
||
>
|
||
> **Drift check (run first)**:
|
||
> `git diff --stat 73daf22b2..HEAD -- packages/pdf/src/templates/`
|
||
> If the templates changed since this plan, compare "Current state" against
|
||
> live code before proceeding; on mismatch, STOP.
|
||
|
||
## Status
|
||
|
||
- **Priority**: P2
|
||
- **Effort**: L
|
||
- **Risk**: HIGH
|
||
- **Depends on**: 001 recommended as the characterization-testing exemplar/
|
||
discipline (not a technical blocker — the PDF layer needs its own parity net,
|
||
which is Step 1 here).
|
||
- **Category**: tech-debt
|
||
- **Planned at**: commit `73daf22b2`, 2026-07-08
|
||
|
||
## Why this matters
|
||
|
||
All 15 resume templates
|
||
(`packages/pdf/src/templates/<name>/<Name>Page.tsx`) independently reimplement
|
||
the **same page-shell orchestration**: compute `getTemplateMetrics`,
|
||
`getTemplatePageSize`, `getTemplatePageMinHeightStyle`, `shouldShowResumeHeader`,
|
||
`hasTemplatePicture`, `filterSections(page.main/sidebar)`, then render
|
||
`<Page>` → `<TemplateProvider>` → a two-column `layout` → sidebar/main columns
|
||
that `.map` over `<Section>`. Git history proves the cost: a single header
|
||
line-height change touched 14 template files (commit `1b0bb067b`); the free-form
|
||
layout feature touched 14 (`2cd774dab`). Every new template copies this again,
|
||
and drift between copies is invisible until someone diffs all 15.
|
||
|
||
The goal is a shared, prop-/callback-driven `TemplatePageShell` that owns the
|
||
orchestration while each template keeps its own styles and decorative choices.
|
||
**Because the visual risk is high and no render-parity test net exists for
|
||
templates today, this plan proves the abstraction on ONE template (Pikachu)
|
||
behind a characterization snapshot, then stops for review before any rollout.**
|
||
|
||
## Current state
|
||
|
||
- `packages/pdf/src/templates/pikachu/PikachuPage.tsx` — the pilot. Structure
|
||
(lines 58–110): the `PikachuPage` component computes the shared metrics/flags
|
||
and renders the shell; a local `Header` component (lines 112–161) renders
|
||
`basics.name`/`headline` + the shared contact-item components; a
|
||
`usePikachuTemplate` hook (lines 163–292) builds the per-template `StyleSheet`.
|
||
The orchestration block, verbatim:
|
||
|
||
```tsx
|
||
export const PikachuPage = ({ page, pageIndex }: TemplatePageProps) => {
|
||
const data = useRender();
|
||
const { metadata, picture } = data;
|
||
const { colors, styles } = usePikachuTemplate();
|
||
const metrics = getTemplateMetrics(metadata.page);
|
||
const pageSize = getTemplatePageSize(metadata.page.format);
|
||
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
|
||
const showHeader = shouldShowResumeHeader(data, pageIndex);
|
||
const showSidebar = !page.fullWidth;
|
||
const hasPicture = hasTemplatePicture(picture);
|
||
const mainSections = filterSections(page.main, data);
|
||
const sidebarSections = filterSections(page.sidebar, data);
|
||
|
||
return (
|
||
<Page size={pageSize} style={composeStyles(styles.page, pageMinHeightStyle)}>
|
||
<TemplateProvider styles={styles} colors={colors} features={pikachuFeatures}>
|
||
<View style={styles.layout}>
|
||
{showSidebar && (
|
||
<View style={composeStyles(styles.sidebarColumn, { width: `${metadata.layout.sidebarWidth}%`, rowGap: metrics.sectionGap })}>
|
||
{showHeader && showSidebar && hasPicture && <Image src={picture.url} style={styles.picture} />}
|
||
<View style={composeStyles(styles.sidebarContent, { rowGap: metrics.sectionGap })}>
|
||
{sidebarSections.map((s) => <Section key={s} section={s} placement="sidebar" />)}
|
||
</View>
|
||
</View>
|
||
)}
|
||
<View style={composeStyles(styles.mainColumn, { rowGap: metrics.sectionGap })}>
|
||
{showHeader && (
|
||
<View style={styles.headerRow}>
|
||
{showHeader && !showSidebar && hasPicture && <Image src={picture.url} style={styles.picture} />}
|
||
<Header styles={styles} colors={colors} />
|
||
</View>
|
||
)}
|
||
<View style={{ rowGap: metrics.sectionGap }}>
|
||
{mainSections.map((s) => <Section key={s} section={s} placement="main" />)}
|
||
</View>
|
||
</View>
|
||
</View>
|
||
</TemplateProvider>
|
||
</Page>
|
||
);
|
||
};
|
||
```
|
||
|
||
**Template-specific variation you must preserve** (this is why extraction is
|
||
risky — the shell cannot assume one layout): Pikachu wraps its `Header` in a
|
||
colored box and places the picture *beside* the header in the main column when
|
||
there's no sidebar, but *above* the sidebar sections when there is. Other
|
||
templates differ: Azurill (`AzurillPage.tsx`) uses `flexBasis` for sidebar
|
||
width instead of a `width` percentage; Onyx (`OnyxPage.tsx`) is single-column
|
||
header-on-top; Gengar (`GengarPage.tsx`) calls `getFeaturedSummaryLayout`.
|
||
The shared shell must expose enough seams (render callbacks / slots) that each
|
||
template keeps these differences — do not flatten them into one hardcoded
|
||
layout.
|
||
|
||
- Shared helpers already exist under `packages/pdf/src/templates/shared/`:
|
||
`metrics.ts`, `page-size.ts`, `cover-letter.ts` (`shouldShowResumeHeader`),
|
||
`filtering.ts`, `picture.ts`, `contact-item.tsx`, `context.tsx`
|
||
(`TemplateProvider`), `sections.tsx` (`Section`), `styles.ts` (`composeStyles`).
|
||
The extraction consolidates the *orchestration that wires these together*, not
|
||
the helpers themselves.
|
||
|
||
- **No render-parity test exists per template.** The only per-template test is
|
||
`templates/scizor/ScizorPage.test.ts`, which greps source text — not a
|
||
structural snapshot. You will build a real parity net in Step 1.
|
||
|
||
- `packages/pdf/src/document.ts` — defines `TemplatePageProps` and
|
||
`TemplatePage`; `templates/index.ts` maps template name → page component.
|
||
|
||
## Commands you will need
|
||
|
||
| Purpose | Command | Expected |
|
||
|----------------|---------------------------------------------------------------|--------------------|
|
||
| Typecheck | `pnpm --filter @reactive-resume/pdf typecheck` | exit 0 |
|
||
| PDF tests | `pnpm --filter @reactive-resume/pdf test` | all pass |
|
||
| Pilot snapshot | `pnpm --filter @reactive-resume/pdf test -- pikachu` | all pass |
|
||
|
||
(Do NOT run `pnpm check`.)
|
||
|
||
## Scope
|
||
|
||
**In scope**:
|
||
- A new shared shell, e.g. `packages/pdf/src/templates/shared/page-shell.tsx`
|
||
(create) and its type additions in `shared/types.ts` if needed.
|
||
- `packages/pdf/src/templates/pikachu/PikachuPage.tsx` (the ONE pilot migration)
|
||
- A new parity snapshot test:
|
||
`packages/pdf/src/templates/pikachu/PikachuPage.test.tsx` (create)
|
||
- `plans/README.md` (status row + a note on the pilot outcome)
|
||
|
||
**Out of scope (hard stop)**:
|
||
- The other 14 `*Page.tsx` templates — **do not touch them in this plan.** Their
|
||
migration is explicit follow-up, gated on this pilot's review.
|
||
- The shared helpers' internals (`metrics.ts`, `filtering.ts`, etc.).
|
||
- `templates/index.ts` mapping — Pikachu's export name/signature must not change.
|
||
- Any visual/styling change — this is a pure structural extraction; the pilot's
|
||
rendered output must be byte-identical to before.
|
||
|
||
## Git workflow
|
||
|
||
- Branch: `advisor/007-template-shell-spike`
|
||
- Commit style: conventional commits, e.g.
|
||
`refactor(pdf): extract shared template page-shell; pilot on pikachu`.
|
||
- Do NOT push or open a PR unless instructed.
|
||
|
||
## Steps
|
||
|
||
### Step 1: Build the parity net for Pikachu (BEFORE refactoring)
|
||
|
||
Create `packages/pdf/src/templates/pikachu/PikachuPage.test.tsx` that renders
|
||
`PikachuPage` with a fixed sample resume and metadata and snapshots the produced
|
||
element tree. Approach:
|
||
|
||
- Use `react-test-renderer` (already available transitively; if not, use the
|
||
approach the existing `packages/pdf/src/browser.test.tsx` / `server.test.tsx`
|
||
use to exercise components) to render the template wrapped in whatever context
|
||
`useRender()` needs. Read `packages/pdf/src/context.tsx` to see how to provide
|
||
the render context in a test (there may be a provider/helper the existing
|
||
tests use — reuse it).
|
||
- Feed it `sampleResumeData` from `@reactive-resume/schema/resume/sample` (used
|
||
by `server.test.tsx`) and a representative `page` prop (one with a sidebar,
|
||
one full-width — two snapshots).
|
||
- Snapshot the tree with `expect(tree.toJSON()).toMatchSnapshot()`.
|
||
|
||
Run it to generate the baseline snapshot **against the current, un-refactored
|
||
PikachuPage**.
|
||
|
||
**Verify**: `pnpm --filter @reactive-resume/pdf test -- pikachu` → passes and
|
||
writes a `__snapshots__` file. Commit this snapshot as the baseline.
|
||
|
||
> If you cannot render the template in a test without excessive/fragile mocking
|
||
> (react-pdf host primitives don't cooperate with the test renderer), STOP and
|
||
> report — a structural refactor with no parity net is exactly what this plan
|
||
> refuses to ship. Do not proceed to Step 2 without a working baseline.
|
||
|
||
### Step 2: Extract the shared shell
|
||
|
||
Create `shared/page-shell.tsx` exporting a `TemplatePageShell` component (or a
|
||
hook + component pair) that encapsulates the orchestration from the excerpt:
|
||
computing `metrics`/`pageSize`/`pageMinHeightStyle`/`showHeader`/`showSidebar`/
|
||
`hasPicture`/`mainSections`/`sidebarSections`, and rendering the
|
||
`<Page><TemplateProvider>...<View layout>` scaffold. Expose the per-template
|
||
variation through props/render-callbacks, at minimum:
|
||
- `styles`, `colors`, `features` (passed through to `TemplateProvider` and used
|
||
by the scaffold).
|
||
- `renderHeader` / `renderPicture` callbacks (or slot props) so Pikachu can keep
|
||
its colored-box header and beside/above picture placement.
|
||
- The sidebar-width strategy as data (Pikachu passes a `width` %; Azurill will
|
||
later pass `flexBasis`) — model it so both fit without the shell hardcoding one.
|
||
|
||
Keep the shell's public surface minimal and documented with a short comment.
|
||
|
||
**Verify**: `pnpm --filter @reactive-resume/pdf typecheck` → exit 0.
|
||
|
||
### Step 3: Migrate Pikachu onto the shell
|
||
|
||
Rewrite `PikachuPage.tsx` so `PikachuPage` delegates its orchestration to
|
||
`TemplatePageShell`, passing `usePikachuTemplate()`'s styles/colors, the
|
||
`pikachuFeatures`, and its `Header`/picture rendering via the callbacks. Keep
|
||
`usePikachuTemplate` and the `Header` component in the Pikachu file (styles stay
|
||
template-owned). The export name and `TemplatePageProps` signature must not
|
||
change.
|
||
|
||
**Verify**:
|
||
- `pnpm --filter @reactive-resume/pdf test -- pikachu` → the Step 1 snapshot
|
||
**still matches** (identical tree). If the snapshot changed, the refactor
|
||
altered output — investigate and reconcile; do NOT blindly update the
|
||
snapshot.
|
||
- `pnpm --filter @reactive-resume/pdf test` → the full PDF suite passes.
|
||
- `pnpm --filter @reactive-resume/pdf typecheck` → exit 0.
|
||
|
||
### Step 4: STOP and report — do not roll out
|
||
|
||
Write a short note in `plans/README.md`'s status area (or the PR description)
|
||
covering: whether the shell's seams were sufficient for Pikachu without style
|
||
regressions, what the shell's public API ended up being, and which of the other
|
||
14 templates look like clean fits vs. which have layouts (e.g. Onyx single
|
||
column, Gengar featured-summary) that will need extra seams. This note is the
|
||
input for the reviewed rollout plans. **Do not migrate any other template.**
|
||
|
||
## Test plan
|
||
|
||
- New `PikachuPage.test.tsx` with two snapshots (with-sidebar, full-width),
|
||
established as a baseline in Step 1 and asserted unchanged after Step 3.
|
||
- Full `packages/pdf` suite must remain green.
|
||
- Verification: `pnpm --filter @reactive-resume/pdf test` → all pass; the
|
||
Pikachu snapshot is unchanged between baseline and post-refactor.
|
||
|
||
## Done criteria
|
||
|
||
Machine-checkable. ALL must hold:
|
||
|
||
- [ ] `packages/pdf/src/templates/shared/page-shell.tsx` exists and is imported by `PikachuPage.tsx`
|
||
- [ ] `packages/pdf/src/templates/pikachu/PikachuPage.test.tsx` + its `__snapshots__` exist
|
||
- [ ] `pnpm --filter @reactive-resume/pdf test` exits 0 (snapshot unchanged post-refactor)
|
||
- [ ] `pnpm --filter @reactive-resume/pdf typecheck` exits 0
|
||
- [ ] `git diff --name-only` shows **exactly one** `*Page.tsx` changed (pikachu); no other template file modified
|
||
- [ ] `plans/README.md` status row for 007 updated with the pilot-outcome note
|
||
- [ ] `grep -rL page-shell packages/pdf/src/templates/*/[A-Z]*Page.tsx` still lists 14 templates (i.e. only Pikachu adopted it)
|
||
|
||
## STOP conditions
|
||
|
||
Stop and report back (do not improvise) if:
|
||
|
||
- You cannot build a working render-parity snapshot in Step 1 (Step 1's own
|
||
stop clause) — no net, no refactor.
|
||
- The Pikachu snapshot changes after Step 3 and you cannot make it identical —
|
||
the extraction is not behavior-preserving; report the diff instead of
|
||
updating the snapshot to match.
|
||
- Making the shell fit Pikachu forces the abstraction to also encode a second
|
||
template's layout (you find yourself designing for Onyx/Gengar mid-pilot) —
|
||
stop; the pilot's job is to prove one clean seam set, not to pre-solve all 15.
|
||
- Any change would touch a template other than Pikachu — that's the rollout,
|
||
which is out of scope.
|
||
|
||
## Maintenance notes
|
||
|
||
- Rollout is deliberately deferred. After this pilot is reviewed and merged,
|
||
create follow-up plans that migrate the remaining templates in small reviewed
|
||
batches (group by layout family: full-width/single-column like Onyx; sidebar-%
|
||
like Pikachu; flexBasis like Azurill; featured-summary like Gengar). Each
|
||
batch gets its own baseline snapshot first.
|
||
- The shell's public API is load-bearing for 14 future migrations — a reviewer
|
||
should scrutinize its seam design (are the render callbacks expressive enough?)
|
||
more than the Pikachu diff itself.
|
||
- Prior maintainer guidance: template rewrites should be spike-gated and must
|
||
not break the react-pdf/DOCX/JSON export pipeline. This plan honors that by
|
||
keeping styles template-owned and asserting identical render output.
|