diff --git a/docs/superpowers/plans/2026-06-11-p3-polish.md b/docs/superpowers/plans/2026-06-11-p3-polish.md new file mode 100644 index 0000000..f6d898a --- /dev/null +++ b/docs/superpowers/plans/2026-06-11-p3-polish.md @@ -0,0 +1,669 @@ +# P3 Polish Backlog 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:** Land the ten deliberately-deferred P3 items from the 2026-06-11 critique (`.impeccable/critique/2026-06-11T05-34-28Z__web-src.md`): orphaned asset cleanup, saved-rail typography/contrast, Edge-slots plain-words copy, status-page desktop layout, emoji→glyph swap, color swatches, mobile-bar occlusion, toolbar scope readout, and the two `$:` hidden-dep smells (only if cheap). + +**Architecture:** All work is in `web/` except one read-only grep already done (crates/ has zero asset-path references to the focus PNGs — confirmed). Every change is small and local; the one M-effort item (status desktop layout) is prepared as two screenshot-able treatments and decided at a single batched user CHECKPOINT together with the other visual items. `cd web && npm run check` is the regression rail after every task. + +**Tech Stack:** Svelte 5 (App.svelte and tuning panels are LEGACY `$:` mode — do not introduce runes there; SavedRail.svelte is already runes mode), Vite, Playwright (devDep). + +**Hard rules (user-enforced; violations failed prior reviews):** +- Never touch `web/src/lib/api/*`, `web/src/lib/mock/*`, `web/src/lib/types.ts` (the Edge-slots dev-voiced error string lives in `web/src/lib/api/controllers.ts` — map it in the VIEW, do not edit the api file). +- Copy law: no Device/HID/gamepad/plugin/backend/bus in user copy; "Everyday" always pairs with "Global Profile"; `/legacy/i` must not match anything in production source (source-audit enforces). +- Never commit to `main`. This plan's branch: `p3-polish` off `main`. +- No local Rust toolchain; nothing here touches `crates/`. +- Gates: `cd web && npm run check` green at every commit. +- Visual items (Tasks 2, 4, 5, 7, 8) get ONE batched user CHECKPOINT (Task 10) before the PR — the user reviews in dev:mock; commits before the checkpoint are fine (branch is unpushed and amendable). + +--- + +### Task 0: Branch + baseline + +**Files:** none (git + gates only) + +- [ ] **Step 0.1: Create the branch** + +```bash +cd /Users/kmcdowell/Documents/repos/dualsense-command +git checkout main && git pull +git checkout -b p3-polish +``` + +- [ ] **Step 0.2: Confirm the gate baseline** + +Run: `cd web && npm run check` +Expected: all gates green (typecheck, source-audit, button-map, snapshot-map, haptics-graph, build, release-size, visual-smoke). If anything is red, STOP and report. + +--- + +### Task 1: Delete the orphaned HUD-era focus PNGs (+ their dead CSS hook) + +20 PNGs (156KB) in `web/public/dualsense/focus/` have zero references anywhere in `web/` or `crates/` (verified by exhaustive grep: the only image lookups are `ButtonMappingView.svelte:316` → `/dualsense/controller_front.png` and `buttonMapping.ts:251` → `/dualsense/icons/iconid_controller_key_*.png`). The companion CSS class `.dm-controller-focus` (`web/src/styles/button-mapping/layout/controller-art.css:88-99`) is defined but never applied to any element. + +**Files:** +- Delete: `web/public/dualsense/focus/` (entire directory, 20 PNGs) +- Modify: `web/src/styles/button-mapping/layout/controller-art.css:88-99` + +- [ ] **Step 1.1: Re-verify zero references (cheap insurance)** + +```bash +cd /Users/kmcdowell/Documents/repos/dualsense-command +grep -rn "dualsense/focus\|focus_" web/src web/scripts web/index.html web/vite.config.ts crates/ || echo "ZERO REFS" +``` +Expected: `ZERO REFS` (or only hits that are clearly not asset paths, e.g. `focus-visible`, `focus_` absent). If a real asset reference appears, STOP and report. + +- [ ] **Step 1.2: Delete the directory and the dead CSS block** + +```bash +git rm -r web/public/dualsense/focus +``` + +Then in `web/src/styles/button-mapping/layout/controller-art.css` delete the entire `.dm-controller-focus { ... }` rule block (lines 88–99: opacity/filter/transition styling for a focus overlay image that is never rendered). Verify nothing else references the class first: + +```bash +grep -rn "dm-controller-focus" web/src || echo "only the css rule" +``` +Expected: only the rule being deleted (in controller-art.css). If a Svelte file uses it, STOP — leave the CSS and report. + +- [ ] **Step 1.3: Verify build + size gate** + +Run: `cd web && npm run build && npm run test:release-size && (grep -r "focus_" dist/ && echo "FAIL: focus refs in dist" || echo "clean")` +Expected: build green, release-size green (smaller), `clean`. + +- [ ] **Step 1.4: Commit** + +```bash +git add -A web/public/dualsense web/src/styles/button-mapping/layout/controller-art.css +git commit -m "p3-polish: delete 156KB of orphaned HUD-era focus PNGs and their dead CSS hook" +``` + +--- + +### Task 2: Saved rail typography + raised-surface contrast — **CHECKPOINT (batched, Task 10)** + +The rail title is 9px and rows are 11px; `--ink-muted` (#8b8b96) on `--surface-raised` (#26262c) computes ≈4.46:1 — a marginal AA fail. Raise rows to 12px, title to 10px, and add a one-step-brighter muted token for raised surfaces. + +**Files:** +- Modify: `web/src/styles/tokens.css` (after `--ink-muted`, line ~10) +- Modify: `web/src/styles/tuning.css` (`.saved-rail-title` ~505, `.saved-row` ~519, `.saved-row-saved`/`.saved-row-was` ~546) + +- [ ] **Step 2.1: Add the raised-surface muted token** + +In `tokens.css`, directly after the `--ink-muted: #8b8b96;` line, add: + +```css + /* Muted ink for raised surfaces: #8b8b96 on --surface-raised is ≈4.46:1 + (marginal AA fail); this lands ≈4.9:1. */ + --ink-muted-raised: #93939d; +``` + +- [ ] **Step 2.2: Apply in tuning.css** + +Change `.saved-rail-title` from: +```css +.saved-rail-title { + font-size: 9px; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--ink-muted); +} +``` +to: +```css +.saved-rail-title { + font-size: 10px; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--ink-muted-raised); +} +``` + +In `.saved-row`, change `font-size: 11px;` to `font-size: 12px;` (rest of the rule unchanged). + +Change: +```css +.saved-row-saved, +.saved-row-was { + color: var(--ink-muted); +} +``` +to: +```css +.saved-row-saved, +.saved-row-was { + color: var(--ink-muted-raised); +} +``` + +- [ ] **Step 2.3: Verify** + +Run: `cd web && npm run check` +Expected: green (visual-smoke covers 1366/1440/390; the rail is fixed-width furniture so +1px text must not break layout — if visual-smoke fails on rail overflow, report rather than shrinking the font back). + +- [ ] **Step 2.4: Commit** + +```bash +git add web/src/styles/tokens.css web/src/styles/tuning.css +git commit -m "p3-polish: saved rail 12px rows, 10px title, AA-passing muted ink on raised surfaces" +``` + +--- + +### Task 3: Edge onboard slots — plain words, teach Fn slots + +The error state renders the raw API message "DualSense Edge onboard profile read/write requires the real DSCC agent." (thrown in `web/src/lib/api/controllers.ts` — FORBIDDEN file, map in the view). The empty state says "No slot data / unavailable". Both are dev register. + +**Files:** +- Modify: `web/src/lib/features/controllers/EdgeSlotsView.svelte` (script + error note + empty-state block) + +- [ ] **Step 3.1: Map the dev-voiced error in the view's script** + +In the ` +{#snippet rowValue(value: string, isColor: boolean)} + {#if isColor && value.startsWith('#')}{/if}{value} +{/snippet} + {#snippet diffRows()} {#each rows as item (item.id)}
{item.label} {#if item.dirty && item.savedValue !== item.currentValue} - {item.savedValue} - → {item.currentValue} + {@render rowValue(item.savedValue, item.kind === 'color')} + → {@render rowValue(item.currentValue, item.kind === 'color')} {:else if item.dirty} - {item.currentValue} + {@render rowValue(item.currentValue, item.kind === 'color')} {:else} - {item.savedValue} + {@render rowValue(item.savedValue, item.kind === 'color')} {/if}
diff --git a/web/src/lib/features/tuning/savedDiff.ts b/web/src/lib/features/tuning/savedDiff.ts index fc379eb..d228de5 100644 --- a/web/src/lib/features/tuning/savedDiff.ts +++ b/web/src/lib/features/tuning/savedDiff.ts @@ -41,6 +41,8 @@ export type SavedDiffRow = { savedValue: string; currentValue: string; dirty: boolean; + /** 'color' rows carry raw hex values; the rail pairs them with a swatch. */ + kind?: 'color'; }; /** The saved baseline: the editable slice of a controller configuration. */ @@ -135,8 +137,9 @@ const row = ( label: string, savedValue: string, currentValue: string, - dirty = savedValue !== currentValue -): SavedDiffRow => ({ id, label, savedValue, currentValue, dirty }); + dirty = savedValue !== currentValue, + kind?: 'color' +): SavedDiffRow => ({ id, label, savedValue, currentValue, dirty, kind }); /** A generic per-field comparison for the deep telemetry tuning groups. */ const tuningGroupRow = ( @@ -249,13 +252,17 @@ export const savedDiffRows = ( 'lightbar-color', 'Lightbar color', (saved.lightbar?.color ?? DEFAULT_LIGHTBAR_COLOR).toLowerCase(), - draft.lightbarColor.toLowerCase() + draft.lightbarColor.toLowerCase(), + undefined, + 'color' ), row( 'redline-color', 'Redline color', (saved.lightbar?.rpmColor ?? DEFAULT_REDLINE_COLOR).toLowerCase(), - draft.rpmColor.toLowerCase() + draft.rpmColor.toLowerCase(), + undefined, + 'color' ), row( 'left-deadzone', diff --git a/web/src/styles/button-mapping/layout/controller-art.css b/web/src/styles/button-mapping/layout/controller-art.css index 5532ca6..4357df9 100644 --- a/web/src/styles/button-mapping/layout/controller-art.css +++ b/web/src/styles/button-mapping/layout/controller-art.css @@ -64,8 +64,7 @@ z-index: 1; } -.dm-controller-base, -.dm-controller-focus { +.dm-controller-base { position: absolute; inset: 4% 4% 6%; width: 92%; @@ -85,19 +84,6 @@ opacity: 0.46; } -.dm-controller-focus { - z-index: 3; - opacity: 0; - filter: - drop-shadow(0 0 6px rgba(0, 112, 204, 0.55)) - drop-shadow(0 0 18px rgba(0, 112, 204, 0.4)); - transition: opacity 200ms ease-out, filter 220ms ease-out; -} - -.dm-controller-focus.visible { - opacity: 1; -} - .dm-controller-focus-card { position: absolute; left: 50%; diff --git a/web/src/styles/feedback.css b/web/src/styles/feedback.css index 9d9aba9..320d143 100644 --- a/web/src/styles/feedback.css +++ b/web/src/styles/feedback.css @@ -84,9 +84,8 @@ display: grid; gap: 5px; min-width: 0; - padding: 11px 13px 12px; - border: 0; - border-left: 3px solid var(--toast-accent); + padding: 11px 13px 12px 15px; + border: 1px solid var(--toast-accent); color: #FFFFFF; background: linear-gradient(90deg, var(--toast-bg), rgba(18, 18, 20, 0.96) 46%), diff --git a/web/src/styles/status.css b/web/src/styles/status.css index 8088646..8d8defa 100644 --- a/web/src/styles/status.css +++ b/web/src/styles/status.css @@ -110,11 +110,22 @@ display: flex; align-items: center; justify-content: center; - font-size: 16px; border-radius: var(--radius-m); background: var(--surface-raised); } +/* Two-class selector to out-specify .dm-controller-glyph's `mask` shorthand + (which would otherwise reset mask-size back to `contain`, same as the + sidebar brand glyph does in shell-v2.css). */ +.dm-controller-glyph.status-controller-glyph { + width: 30px; + height: 22px; + /* Square SVG canvas with the artwork in a central band — zoom the mask + past the box so the controller renders at ~22px tall (see shell-v2.css). */ + mask-size: 36px 36px; + -webkit-mask-size: 36px 36px; +} + .status-controller-main { flex: 1; min-width: 0; @@ -237,3 +248,14 @@ .status-ok { color: var(--ok); } .status-warn { color: var(--warn); } .status-mut { color: var(--ink-muted); } + +/* Wide desktops: the band keeps its scale but sits centered in the viewport + instead of hugging the top-left corner. */ +@media (min-width: 1200px) and (min-height: 720px) { + .status-view { + display: flex; + flex-direction: column; + justify-content: center; + min-height: calc(100dvh - 130px); /* viewport minus measured app chrome */ + } +} diff --git a/web/src/styles/tokens.css b/web/src/styles/tokens.css index 8f9bd53..92daa68 100644 --- a/web/src/styles/tokens.css +++ b/web/src/styles/tokens.css @@ -8,6 +8,10 @@ --hairline-strong: #2e2e36; --ink: #d6d6dc; --ink-muted: #8b8b96; /* secondary text only; not body-sized prose */ + /* Brighter muted ink for small text needing AA headroom: #8b8b96 computes + ≈4.46:1 on --surface-raised (a marginal fail); #93939d clears 4.5:1 on + every app surface. */ + --ink-muted-raised: #93939d; /* accent — PlayStation blue */ --accent: #0070cc; /* primary buttons */ diff --git a/web/src/styles/tuning.css b/web/src/styles/tuning.css index c1a2473..f99ca39 100644 --- a/web/src/styles/tuning.css +++ b/web/src/styles/tuning.css @@ -503,11 +503,11 @@ } .saved-rail-title { - font-size: 9px; + font-size: 10px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; - color: var(--ink-muted); + color: var(--ink-muted-raised); } .saved-rail-rows { @@ -523,7 +523,7 @@ gap: 10px; padding: 4px 0; border-bottom: 1px solid var(--hairline); - font-size: 11px; + font-size: 12px; } .saved-row:last-child { @@ -545,7 +545,7 @@ .saved-row-saved, .saved-row-was { - color: var(--ink-muted); + color: var(--ink-muted-raised); } .saved-row-now { @@ -553,6 +553,19 @@ font-weight: 600; } +/* Hex color values get a swatch so the color is readable at a glance. + (Atomic inline boxes don't inherit the strikethrough line, so the saved + swatch stays clean inside .) */ +.saved-swatch { + display: inline-block; + width: 10px; + height: 10px; + margin-right: 5px; + border-radius: 3px; + border: 1px solid var(--hairline-strong); + vertical-align: -1px; +} + .saved-rail-foot { margin-top: 12px; padding-top: 10px; @@ -588,7 +601,7 @@ .saved-preview-note { font-size: 9px; - color: var(--ink-muted); + color: var(--ink-muted-raised); white-space: nowrap; } @@ -614,7 +627,7 @@ .saved-discard-button { flex: 0 0 auto; background: var(--surface-raised); - color: var(--ink-muted); + color: var(--ink-muted-raised); border: 0; border-radius: var(--radius-s); padding: 5px 12px; @@ -671,7 +684,7 @@ padding: 0; font-family: inherit; font-size: 11px; - color: var(--ink-muted); + color: var(--ink-muted-raised); cursor: pointer; min-width: 0; text-align: left; @@ -705,6 +718,13 @@ .saved-mobile-bar { display: block; } + + /* The fixed bar floats over the document; reserve room beneath the canvas + only while it's shown so the last row stays reachable. Anchored at the + tuning wrapper to keep :has() invalidation local. */ + .work-and-rail:has(.saved-mobile-bar) .canvas-grid { + padding-bottom: 84px; + } } /* Content parked below the grid until Tasks 8-10 re-home it. */