From 015cb432fb8429dd39b1346d77105323272edfce Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Sun, 19 Apr 2026 13:28:30 +0200 Subject: [PATCH] docs(plans): Editor-Split Implementierungsplan (Tier 4 Item B) 5-Task-Plan fuer 4 Sub-Components: ImageUploadBox, IngredientRow, StepList, TimeDisplay. Parent-owned state bleibt im Parent, Sub- Components rendern bare Content damit Parent-Scoped-CSS greift. Keine Component-Unit-Tests (etablierter Codebase-Stil), Manual- Smoke + existierende e2e-Specs decken Regression. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../plans/2026-04-19-editor-split.md | 897 ++++++++++++++++++ 1 file changed, 897 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-19-editor-split.md diff --git a/docs/superpowers/plans/2026-04-19-editor-split.md b/docs/superpowers/plans/2026-04-19-editor-split.md new file mode 100644 index 0000000..1d90c19 --- /dev/null +++ b/docs/superpowers/plans/2026-04-19-editor-split.md @@ -0,0 +1,897 @@ +# Editor-Split Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` (recommended) or `superpowers:executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Split the monolithic `RecipeEditor.svelte` (628 L) and pull one readability-oriented block out of `RecipeView.svelte` (398 L) by extracting 4 focused Svelte components: `ImageUploadBox`, `IngredientRow`, `StepList`, `TimeDisplay`. No behavior changes, just structure. + +**Architecture:** Parent-owned state stays in the parent (`RecipeEditor` still owns `ingredients: DraftIng[]`, `steps: DraftStep[]`). Sub-components receive props + callbacks and render their own template + scoped CSS. Shared draft types land in `src/lib/components/recipe-editor-types.ts` so sub-components and parent agree on the shape. `RecipeView.TimeDisplay` is pure presentational with no state. + +**Tech Stack:** Svelte 5 runes (`$props`, `$state`, `$derived`), TypeScript-strict, no new runtime deps. + +--- + +## Why this is worth doing + +- `RecipeEditor.svelte:42-89` (Bild-Upload) and `RecipeEditor.svelte:313-334` (Zubereitung) are each self-contained logic-islands with their own state and handlers. Extracting them caps the file a Claude can reason about in one shot. +- `IngredientRow` renders 10 lines of template with 5 ARIA labels and 6 grid-columns — a natural single-responsibility unit. +- `TimeDisplay` is pure formatting; owning it as a component lets future phases (preview, card hover) reuse it. + +## What we are NOT doing + +- No refactor of `RecipeView`'s tabs / servings-stepper / ingredient-display. Those work fine as-is; roadmap only names the 4 above. +- No component unit tests (kochwas has none for components; the e2e `recipe-detail.spec.ts` still covers View behavior, and edit-flow is manually smoked). +- No ` +``` + +- [ ] **Step 2: Wire up `RecipeEditor.svelte`** + +Remove lines 30–89 (imagePath/uploading/fileInput state, imageSrc derived, onFileChosen, removeImage). + +Remove these imports at the top: +```ts +import { Plus, Trash2, ChevronUp, ChevronDown, ImagePlus, ImageOff } from 'lucide-svelte'; +import { confirmAction } from '$lib/client/confirm.svelte'; +import { asyncFetch } from '$lib/client/api-fetch-wrapper'; +import { requireOnline } from '$lib/client/require-online'; +``` +Replace with (Task 1 needs only Plus + Trash2 + Chevrons — the image-specific imports move to the sub-component; `confirmAction`/`asyncFetch`/`requireOnline` stay for future tasks): +```ts +import { Plus, Trash2, ChevronUp, ChevronDown } from 'lucide-svelte'; +import ImageUploadBox from '$lib/components/ImageUploadBox.svelte'; +``` + +Remove the image-related CSS (`.image-row`, `.image-preview*`, `.image-actions`, `.image-actions .btn`, `.upload-status`, `.file-input`, `.image-hint`, `.image-block` — those live in the sub-component now). + +Replace the Bild section in the template: +```svelte +
+

Bild

+ onimagechange?.(p)} + /> +
+``` + +- [ ] **Step 3: Run checks** + +```bash +npm run check +npm test +``` + +Expected: 0 errors, 196/196 tests pass. + +- [ ] **Step 4: Manual smoke** + +```bash +npm run dev +``` + +Open any saved recipe → edit → upload an image → verify it shows up and `onimagechange` fires (parent's state updates). Remove the image → confirms the confirm-dialog and removes. Bail out if either flow breaks. + +- [ ] **Step 5: Commit** + +```bash +git add src/lib/components/ImageUploadBox.svelte src/lib/components/RecipeEditor.svelte +git commit -m "$(cat <<'EOF' +refactor(editor): ImageUploadBox als eigenstaendige Component + +Isoliert den Bild-Upload-Flow (File-Input, Preview, Entfernen-Dialog) +aus dem RecipeEditor. Parent haelt nur noch den
-Wrapper und +reicht recipe.id + image_path rein, kriegt Aenderungen per onchange +callback zurueck. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +## Task 2: Extract types + `IngredientRow` + +**Files:** +- Create: `src/lib/components/recipe-editor-types.ts` +- Create: `src/lib/components/IngredientRow.svelte` +- Modify: `src/lib/components/RecipeEditor.svelte` + +- [ ] **Step 1: Types file** + +```ts +// src/lib/components/recipe-editor-types.ts +export type DraftIng = { + qty: string; + unit: string; + name: string; + note: string; +}; + +export type DraftStep = { text: string }; +``` + +- [ ] **Step 2: IngredientRow component** + +```svelte + + + +
  • +
    + + +
    + + + + + +
  • + + +``` + +- [ ] **Step 3: Wire up `RecipeEditor.svelte`** + +Replace the local `DraftIng` / `DraftStep` type declarations (lines 100–106) with: +```ts +import type { DraftIng, DraftStep } from '$lib/components/recipe-editor-types'; +import IngredientRow from '$lib/components/IngredientRow.svelte'; +``` + +In the template, swap the `
  • ` block for: +```svelte +{#each ingredients as ing, idx (idx)} + moveIngredient(idx, dir)} + onremove={() => removeIngredient(idx)} + /> +{/each} +``` + +Remove the CSS for `.ing-row`, `.move`, `.move-btn`, `.ing-row input`, `.del`, and the `@media (max-width: 560px)` block — all now live in `IngredientRow.svelte`. + +Remove the unused imports `ChevronUp`, `ChevronDown`, `Trash2` from RecipeEditor (they moved to the sub-component, but wait — `Trash2` is also used for step-remove. Keep `Trash2`, remove the two Chevrons). + +- [ ] **Step 4: Run checks** + +```bash +npm run check +npm test +``` + +- [ ] **Step 5: Manual smoke** + +Open any recipe in edit mode. Add an ingredient, type into all 4 fields, reorder up/down, remove one. Verify save persists the ordering. + +- [ ] **Step 6: Commit** + +```bash +git add src/lib/components/recipe-editor-types.ts src/lib/components/IngredientRow.svelte src/lib/components/RecipeEditor.svelte +git commit -m "$(cat <<'EOF' +refactor(editor): IngredientRow + shared types + +IngredientRow rendert eine einzelne editierbare Zutat-Zeile. DraftIng +und DraftStep sind jetzt in recipe-editor-types.ts, damit Parent und +Sub-Components auf dieselbe Form referenzieren. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +## Task 3: Extract `StepList` + +**Files:** +- Create: `src/lib/components/StepList.svelte` +- Modify: `src/lib/components/RecipeEditor.svelte` + +- [ ] **Step 1: StepList component** + +```svelte + + + +
      + {#each steps as step, idx (idx)} +
    1. + {idx + 1} + + +
    2. + {/each} +
    + + + +``` + +- [ ] **Step 2: Wire up `RecipeEditor.svelte`** + +Add import: +```ts +import StepList from '$lib/components/StepList.svelte'; +``` + +Replace the entire Zubereitung `
    ` template block (starting `
    ` with `

    Zubereitung

    ` through the add-step button): + +```svelte +
    +

    Zubereitung

    + +
    +``` + +**CSS audit — what stays and what goes in the parent:** + +Parent's template after Tasks 1–3 still contains: +- `

    Bild

    ` — no `.block` inner styles needed beyond what's in parent. +- `
    ` — still here. Keep `.meta`, `.field`, `.row`, `.small`, `.lbl`. +- `

    Zutaten

      {#each ..}{/each}
    ` — still uses `.ing-list` and `.add`. +- `

    Zubereitung

    ` — no inner CSS. +- `
    ` — keeps `.foot`, `.btn`, `.btn.ghost`, `.btn.primary`, `.btn:disabled`. + +So parent CSS after Task 3 keeps: `.editor`, `.meta`, `.field`, `.lbl`, `.row`, `.small`, `.block`, `.block h2`, `.ing-list` (the `
      ` wrapper), `.add` (for "Zutat hinzufügen"), `.foot`, `.btn` and variants. + +Drop from parent CSS in Task 3: `.step-list`, `.step-row`, `.num`, `.step-row textarea`, `.del`. + +- [ ] **Step 3: Run checks** + +```bash +npm run check +npm test +``` + +- [ ] **Step 4: Manual smoke** + +Open any recipe → edit → add a step, type, remove, save. Verify steps persist with correct ordering. + +- [ ] **Step 5: Commit** + +```bash +git add src/lib/components/StepList.svelte src/lib/components/RecipeEditor.svelte +git commit -m "$(cat <<'EOF' +refactor(editor): StepList als eigenstaendige Component + +Zubereitungs-Liste mit Add + Remove als Sub-Component. Parent steuert +nur noch den Wrapper und reicht steps + die zwei Callbacks rein. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +## Task 4: Extract `TimeDisplay` (RecipeView) + +**Files:** +- Create: `src/lib/components/TimeDisplay.svelte` +- Modify: `src/lib/components/RecipeView.svelte` + +- [ ] **Step 1: TimeDisplay component** + +```svelte + + + +{#if summary} +

      {summary}

      +{/if} + + +``` + +- [ ] **Step 2: Wire up `RecipeView.svelte`** + +Add import: +```ts +import TimeDisplay from '$lib/components/TimeDisplay.svelte'; +``` + +Remove the local `timeSummary()` function (lines 45–52). + +Replace the `{#if timeSummary()}

      ...

      {/if}` block in the template with: +```svelte + +``` + +Remove the `.times` CSS from RecipeView (it's in the sub-component now). + +- [ ] **Step 3: Run checks** + +```bash +npm run check +npm test +``` + +- [ ] **Step 4: Manual smoke** + +Open any recipe → verify the time line still shows the same content (Vorb. / Kochen / Gesamt). + +- [ ] **Step 5: Commit** + +```bash +git add src/lib/components/TimeDisplay.svelte src/lib/components/RecipeView.svelte +git commit -m "$(cat <<'EOF' +refactor(view): TimeDisplay als eigenstaendige Component + +timeSummary-Formatierung in eine wiederverwendbare Component +gezogen. RecipeView liefert nur noch die drei Werte — zukuenftige +Call-Sites (Preview, Hover-Cards) koennen dieselbe Logik reusen. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +## Task 5: Self-review + push + +- [ ] **Step 1: Line-count audit** + +```bash +wc -l src/lib/components/RecipeEditor.svelte src/lib/components/RecipeView.svelte src/lib/components/ImageUploadBox.svelte src/lib/components/IngredientRow.svelte src/lib/components/StepList.svelte src/lib/components/TimeDisplay.svelte +``` + +Expected shape (approximate, ±10%): +- `RecipeEditor.svelte`: 628 → ~330–370 +- `RecipeView.svelte`: 398 → ~380 +- `ImageUploadBox.svelte`: ~160 +- `IngredientRow.svelte`: ~110 +- `StepList.svelte`: ~100 +- `TimeDisplay.svelte`: ~30 + +- [ ] **Step 2: Full test + typecheck** + +```bash +npm test +npm run check +``` + +Both green. + +- [ ] **Step 3: Git log review** + +```bash +git log --oneline main..HEAD +``` + +Expected 4 commits: +1. `refactor(editor): ImageUploadBox als eigenstaendige Component` +2. `refactor(editor): IngredientRow + shared types` +3. `refactor(editor): StepList als eigenstaendige Component` +4. `refactor(view): TimeDisplay als eigenstaendige Component` + +- [ ] **Step 4: Remote E2E after push** + +```bash +git push -u origin editor-split +``` + +CI builds branch-tagged image. After deploy to `kochwas-dev.siegeln.net`: + +```bash +npm run test:e2e:remote +``` + +Expected: 40/42 green (same as Search-State-Store baseline). `recipe-detail.spec.ts` (6 tests) specifically exercises the View side — must be clean. + +Manual UAT pass on `https://kochwas-dev.siegeln.net/`: +- Edit a recipe → upload + remove image. +- Add / reorder / remove an ingredient → save → verify persistence on reload. +- Add / remove a step → save → verify. +- Check time-summary rendering on any recipe with prep/cook/total times set. + +- [ ] **Step 5: Merge to main** + +Once UAT is clean: +```bash +git checkout main +git merge --no-ff editor-split +git push origin main +``` + +--- + +## Risk Notes + +- **Prop-reference mutability.** `IngredientRow` and `StepList` receive `ing` / `steps` by reference and use `bind:value` on their own `` / `