feat(FN-403): Phase-1 parts panel selector & route manifest for FN-368 #21

Merged
root merged 1 commits from merge-fn403-manifest into main 2026-05-16 11:24:12 +03:00

164
MANIFEST-FN-403.md Normal file
View File

@@ -0,0 +1,164 @@
# FN-368 Parts Panel Selector Manifest
Source: FN-403 recon on worktree branch `fusion/fn-403` rebased onto `origin/dev`,
HEAD `d684e7f` (fix(pl24): pass Ford VIN BOM detail URL through fetchP4Page baseUrl prefix).
`apps/web/src/components/schema/parts-panel.tsx` content is **unchanged** from the FN-368 ship
commit `0ce68a7` (feat(FN-368): parts panel loading state, inert unavailable rows, escape hatch).
Sibling to `.fusion/tasks/FN-400/MANIFEST.md` (FN-367 / VIN-decode half).
Conventions: see project memory `frontend-testability-hook-convention``apps/web` does **not**
use `data-testid`; hook priority is (1) `data-faro-user-action-name`, (2) ARIA role + accessible
name, (3) visible text.
`<PartsPanel>` is always mounted via `<SchemaViewer>`. SchemaViewer mounts PartsPanel at two
sites:
- `apps/web/src/components/schema/schema-viewer.tsx:100` — no-schema fallback branch
(`if (!schemaPic)`, e.g. PL24 bomOverviewlist pages).
- `apps/web/src/components/schema/schema-viewer.tsx:168` — split-pane right column (default).
`grep -n data-testid apps/web/src/components/schema/parts-panel.tsx`**0 matches**.
---
## Route 1 — primary post-VIN-decode flow
```
Route: /dashboard/vehicles_/$id/categories_/$categoryId
(file: apps/web/src/routes/dashboard/vehicles_/$id/categories_/$categoryId.tsx, line: 44)
Live example URL: https://sase.tr/dashboard/vehicles/<vehicleId>/categories/<categoryId>
(slug discovery: go to https://sase.tr/dashboard/search, paste a known-good
VIN, the decoded landing page links each category leaf via this route)
Container: getByRole('heading', { name: 'Parcalar' }) → closest div.flex.h-full.flex-col.overflow-hidden
(file: apps/web/src/components/schema/parts-panel.tsx, line: 95 for loading-state heading inside
div at line 94; line 112 for normal-state heading inside div at line 110)
Interactive elements:
- Parts row, available — selector: [data-faro-user-action-name="select-part"][role="button"],
role: button (Turkish accessible name is part.name + optional remark/modelCodes),
expected on click: useSchemaStore.setSelectedGroup(group) toggles selection,
scrolls the matched row into view via row.scrollIntoView({ behavior: "smooth", block: "center" }),
triggers hotspot highlight on the schema image,
mobile hit-target: ≈36 px (td has px-3 py-2, text-sm) — FLAG <44 px
(file: apps/web/src/components/schema/parts-panel.tsx, line: 146 for data-faro attr, 148 for role, 149 for tabIndex, 85-88 for scrollIntoView, 174-179 for onClick)
- Parts row, unavailable (FN-368 inert, MUST NOT be rage-clickable) — selector:
[data-faro-user-action-name="select-part"][aria-disabled="true"],
role: (no role attribute — omitted when unavailable), tabIndex={-1},
expected on click: NOTHING — onClick / onKeyDown / onMouseEnter / onMouseLeave are all undefined for unavailable rows,
mobile hit-target: ≈36 px (still rendered, opacity-40) — N/A for rage-click verification (inert)
(file: apps/web/src/components/schema/parts-panel.tsx, line: 146 for data-faro attr, 147 for aria-disabled, 148-149 for role/tabIndex, 158 for opacity-40 class, 171-187 for stripped handlers)
- OEM copy button — selector (scope to a parts row first): getByRole('button') matching the inline
<button type="button"> that contains a <Copy> lucide icon (no Faro hook, no aria-label),
role: button (no accessible name; icon-only),
expected on click: e.stopPropagation() → navigator.clipboard.writeText(part.oemCode) →
setCopiedId(part.id) flips the icon from <Copy> to <Check className="text-green-500"> for 1500 ms →
api.post('/analytics/oem-copy', {oemCode, partId, vehicleId, categoryId}) (errors swallowed) →
capture('oem_code_copied', {oem_code, part_id, vehicle_id, category_id}) PostHog event,
mobile hit-target: ≈18×18 px (p-0.5 + size-3.5) — FLAG <44 px
(file: apps/web/src/components/schema/parts-panel.tsx, line: 198 for <button>, 200-204 for onClick, 205-210 for icon swap, 56-79 for copyOemCode handler)
- Empty-state escape (FN-368) — selector: getByRole('button', { name: 'Geri don' }),
role: button (accessible name "Geri don", variant="outline"),
expected on click: window.history.back() — only renders when parts.length === 0,
mobile hit-target: ≈40 px (Button default sm h-9 ≈ 36 px; with outline border + py-2 effective ≈40 px) — borderline, document
(file: apps/web/src/components/schema/parts-panel.tsx, line: 120 for <Button onClick>, 121 for label, 117 for parts.length===0 branch guard)
Loading state: getByText('Yukleniyor...') (no diacritics) + 8 sibling <Skeleton className="h-10 w-full">
keyed s0..s7. Branch entered when isLoading prop is true. Note: <SchemaViewer>'s page-level
skeleton at schema-viewer.tsx:72-86 usually fires before PartsPanel's own loading branch.
(file: apps/web/src/components/schema/parts-panel.tsx, line: 96 for "Yukleniyor...", 16 for SKELETON_ROW_KEYS, 99-103 for Skeleton map, 89 for `if (isLoading)` guard)
Empty state: getByText('Bu kategori icin parca bulunamadi.') (no diacritics) + "Geri don" button.
Branch entered when parts.length === 0 after isLoading clears.
(file: apps/web/src/components/schema/parts-panel.tsx, line: 119 for text, 121 for button label, 117 for parts.length===0 guard)
Feedback affordance (FN-368 fix): NONE — failures bubble to SchemaViewer skeleton at schema-viewer.tsx:72-86.
FN-368 (commit 0ce68a7) shipped (a) aria-disabled="true" + stripped handlers on unavailable rows,
(b) "Yukleniyor..." loading branch + 8 skeleton rows, (c) "Geri don" empty-state escape. No toast,
no inline error UI. `use-parts` fetch failures bubble up to <SchemaViewer>'s page-level
`if (isLoading)` skeleton block. `grep -n toast apps/web/src/components/schema/*.tsx
apps/web/src/hooks/use-parts.ts` → 0 matches; PartsPanel has no toast call site.
(file: apps/web/src/components/schema/schema-viewer.tsx, line: 72 for `if (isLoading)`, 73-85 for skeleton return, 86 for closing brace)
data-testid introduced by FN-368: NONE (sase/web convention — see project memory
`frontend-testability-hook-convention`).
(verified: `grep -n data-testid apps/web/src/components/schema/parts-panel.tsx` → 0 matches at HEAD d684e7f)
```
---
## Route 2 — catalog drill-down (brand → model → categories)
```
Route: /dashboard/catalog_/$brandName_/$modelId/categories_/$categoryId
(file: apps/web/src/routes/dashboard/catalog_/$brandName_/$modelId/categories_/$categoryId.tsx, line: 36)
Live example URL: https://sase.tr/dashboard/catalog/<brand>/<modelId>/categories/<categoryId>
[optional search params: ?body=&engine=&gearbox=&mgp=]
(slug discovery: start at https://sase.tr/dashboard/catalog, drill brand →
model → category)
Container: same — getByRole('heading', { name: 'Parcalar' })
(file: apps/web/src/components/schema/parts-panel.tsx, line: 95 / 112)
Interactive elements: identical to Route 1 (same <SchemaViewer><PartsPanel> children).
Mount site in this route: SchemaViewer rendered at line 225 inside `<Suspense fallback={<SchemaViewerFallback />}>`.
(file: apps/web/src/routes/dashboard/catalog_/$brandName_/$modelId/categories_/$categoryId.tsx, line: 225)
Loading state: same — see Route 1 block; this route additionally renders a route-level
<SchemaViewerFallback /> while the lazy chunk loads (line 224 Suspense + line 121 also).
(file: apps/web/src/routes/dashboard/catalog_/$brandName_/$modelId/categories_/$categoryId.tsx, line: 224 for Suspense, 121 for in-page fallback usage)
Empty state: same as Route 1.
Feedback affordance (FN-368 fix): NONE — failures bubble to SchemaViewer skeleton at schema-viewer.tsx:72-86.
data-testid introduced by FN-368: NONE (sase/web convention — see project memory
`frontend-testability-hook-convention`).
```
---
## Route 3 — emex catalog group leaf
```
Route: /dashboard/catalog_/emex/$catalogCode_/$vehicleId_/groups/$groupId
(file: apps/web/src/routes/dashboard/catalog_/emex/$catalogCode_/$vehicleId_/groups/$groupId.tsx, line: 17)
Live example URL: https://sase.tr/dashboard/catalog/emex/<catalogCode>/<vehicleId>/groups/<groupId>
(slug discovery: start at https://sase.tr/dashboard/catalog, pick an
emex-backed brand → model → vehicle → group)
Container: same — getByRole('heading', { name: 'Parcalar' })
(file: apps/web/src/components/schema/parts-panel.tsx, line: 95 / 112)
Interactive elements: identical to Route 1.
Mount site in this route: SchemaViewer rendered at line 83.
(file: apps/web/src/routes/dashboard/catalog_/emex/$catalogCode_/$vehicleId_/groups/$groupId.tsx, line: 83)
Loading state: same as Route 1.
Empty state: same as Route 1.
Feedback affordance (FN-368 fix): NONE — failures bubble to SchemaViewer skeleton at schema-viewer.tsx:72-86.
data-testid introduced by FN-368: NONE (sase/web convention — see project memory
`frontend-testability-hook-convention`).
```
---
## Route 4 — pcat catalog group leaf
```
Route: /dashboard/catalog_/pcat/$catalogId_/$modelId_/$carId_/groups/$groupId
(file: apps/web/src/routes/dashboard/catalog_/pcat/$catalogId_/$modelId_/$carId_/groups/$groupId.tsx, line: 17)
Live example URL: https://sase.tr/dashboard/catalog/pcat/<catalogId>/<modelId>/<carId>/groups/<groupId>
(slug discovery: start at https://sase.tr/dashboard/catalog, pick a
pcat-backed brand → model → car → group)
Container: same — getByRole('heading', { name: 'Parcalar' })
(file: apps/web/src/components/schema/parts-panel.tsx, line: 95 / 112)
Interactive elements: identical to Route 1.
Mount site in this route: SchemaViewer rendered at line 100.
(file: apps/web/src/routes/dashboard/catalog_/pcat/$catalogId_/$modelId_/$carId_/groups/$groupId.tsx, line: 100)
Loading state: same as Route 1.
Empty state: same as Route 1.
Feedback affordance (FN-368 fix): NONE — failures bubble to SchemaViewer skeleton at schema-viewer.tsx:72-86.
data-testid introduced by FN-368: NONE (sase/web convention — see project memory
`frontend-testability-hook-convention`).
```
---
## Drift vs FN-400 baseline
FN-400 captured selectors against `main @ 08dbde7`. Re-verifying on current worktree HEAD `d684e7f`:
- All `parts-panel.tsx` line numbers unchanged (146 select-part attr, 147 aria-disabled, 96 "Yukleniyor...", 119 empty-state text, 121 "Geri don" button).
- `schema-viewer.tsx` skeleton block: FN-400 noted `:72-86`; the `if (isLoading)` branch on current worktree returns through line 94 (block opens at `:72` and the closing brace is at `:94`). Use **`schema-viewer.tsx:72-86`** going forward.
- All four route files (mount sites in step 3 of PROMPT.md) confirmed present and mounting `<SchemaViewer>`.
## Cross-references
- Project memory: `frontend-testability-hook-convention` (sase/web no-`data-testid` convention).
- Project memory `MEMORY.md` (≈ lines 257268 of root index): `Canonical FN-367/FN-368 selectors` block — kept in sync via the same `fn_memory_append` call that wrote the compact version of this manifest.
- Sibling: `.fusion/tasks/FN-400/MANIFEST.md` (FN-367 / VIN-decode half).
- Consumer: FN-393 (Playwright verification — to be re-specified against this manifest).
- Source commit for the fix: `0ce68a7``feat(FN-368): parts panel loading state, inert unavailable rows, escape hatch (gitea #10)`.