Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
8bf5d08
docs: UI rework spec, implementation plan, and product brief
shiftedx Jun 10, 2026
b35abf8
ui-rework: calm-console + PS-blue design tokens with legacy aliases
shiftedx Jun 10, 2026
ae302fe
ui-rework: annotate legacy token block for safe cleanup
shiftedx Jun 10, 2026
d74ba83
ui-rework: status/tuning/advanced navigation model with legacy hash r…
shiftedx Jun 10, 2026
64cad79
ui-rework: sidebar shell, smoke test retargeted to new routes
shiftedx Jun 11, 2026
56b0c49
ui-rework: contain overlay scroll, prune dead glyph rule, cleanup bre…
shiftedx Jun 11, 2026
89bf4dd
ui-rework: status page with sentence-of-truth and profile resolution
shiftedx Jun 11, 2026
20c52e1
ui-rework: stable finding keys and aria-hidden decorative dots on Status
shiftedx Jun 11, 2026
18383b9
ui-rework: tuning header with game dropdown, profile menu, bespoke ar…
shiftedx Jun 11, 2026
2604db2
ui-rework: tuning menus close on tab-out and scroll; aria-label refresh
shiftedx Jun 11, 2026
506260d
ui-rework: semantic tuning columns (brake/throttle/road feel/lights)
shiftedx Jun 11, 2026
b7ce8f7
ui-rework: reword comments to keep source-audit at baseline
shiftedx Jun 11, 2026
ee53ac2
ui-rework: typed effect-column mapping and semantic strip classes
shiftedx Jun 11, 2026
505970f
ui-rework: sticky saved rail with tweak-vs-saved diff and mobile bar
shiftedx Jun 11, 2026
9fb34d2
ui-rework: coherent save/discard gating, shared default constants, cl…
shiftedx Jun 11, 2026
c66516d
ui-rework: per-game setup guide with passive verification and chip re…
shiftedx Jun 11, 2026
0a4f8ef
ui-rework: setup guide review fixes (deep-link race, chip affordance,…
shiftedx Jun 11, 2026
596e59c
ui-rework: advanced section (controller details, edge slots, button m…
shiftedx Jun 11, 2026
494edff
ui-rework: centralize mono font token, drop dead power-metrics helper
shiftedx Jun 11, 2026
1b2871c
ui-rework: remove HUD-era shell, styles, and routed views
shiftedx Jun 11, 2026
67b69fa
Fix spurious unsaved-changes state on first profile load
shiftedx Jun 11, 2026
5f8eabb
ui-rework: toolbar alignment, strip overlap fix, DualSense brand glyph
shiftedx Jun 11, 2026
8081cfc
ui-rework: pair Everyday header title with Global Profile
shiftedx Jun 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,4 @@ pnpm-debug.log*

# Upstream research clone kept out of this clean-room implementation repo.
/Forza-Horizon-DualSense-Python/
.superpowers/
67 changes: 67 additions & 0 deletions PRODUCT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Product

## Register

product

## Users

Non-technical Windows gamers ("No Python, no scripts, no command line") who own a
PlayStation DualSense or DualSense Edge and play on PC — mostly racing games.
They open DSCC in two modes: a one-time **setup** moment (connect controller,
check it works) and a recurring **play** moment (pick a game, tune how it feels).
Between those moments the app should answer one question at a glance:
"is everything working?"

They are not sim-rig tinkerers. Density, jargon, and diagnostics-first layouts
read as risk, not power.

## Product Purpose

DSCC changes how a DualSense controller feels on PC: adaptive trigger
resistance, rich haptics, lights, and live racing-game telemetry turned into
feel. Success looks like: a user connects a controller, starts a supported
game, and feels the difference — without ever wondering whether the app is
about to do something scary to their hardware.

## Brand Personality

Calm, trustworthy, capable. Quiet confidence: a well-made tool that writes to
real hardware and never makes that feel dangerous. Plain language over jargon
(domain terms come from CONTEXT.md and are law). Microcopy reassures rather
than performs.

## Anchor References

- **Linear** — soft dark surfaces, quiet hierarchy, restrained accent use,
speed as a feeling.
- **Raycast** — disciplined density, muted accents, exemplary settings panels.

## Anti-references

- The "gamer cockpit" HUD: grid textures, glows, glassmorphism, scanline
energy, all-caps telemetry labels everywhere. (This is what the current UI
does and what the rework moves away from.)
- RGB-gamer aesthetic (Razer/ROG software): loud, busy, salesy.
- Diagnostics-first layouts that lead with raw numbers instead of the task.

## Design Principles

1. **Status before controls.** Every screen answers "is everything OK?" before
it offers anything to change.
2. **The task is the navigation.** Lead with what the user is doing (set up my
controller, make my game feel great), not with tool categories.
3. **Advanced is opt-in.** Button mapping, calibration readouts, Edge slots,
and raw telemetry live behind a clear "Advanced" door — reachable, never
ambient.
4. **Safety is visible, not scary.** Always distinguish dry-run/test effects
from live Hardware Output, and Global Profile from an active Game Profile,
in plain words.
5. **Quiet surfaces, one voice.** One accent color doing real work (state,
selection, primary action), consistent component vocabulary on every screen.

## Accessibility & Inclusion

WCAG AA contrast (≥4.5:1 body text), full `prefers-reduced-motion` support,
keyboard navigable. The 390px mobile viewport is an enforced layout target via
the visual smoke test.
409 changes: 409 additions & 0 deletions docs/superpowers/plans/2026-06-10-ui-rework.md

Large diffs are not rendered by default.

206 changes: 206 additions & 0 deletions docs/superpowers/specs/2026-06-10-ui-rework-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
# DSCC Web UI Rework — Design Brief

**Date:** 2026-06-10 · **Branch:** `ui-improvements` · **Status:** awaiting approval, no implementation yet
**Inputs:** brainstorm + 12 browser-validated mockups (`.superpowers/brainstorm/9273-1781129906/content/`), PRODUCT.md, CONTEXT.md, impeccable skill (product register)

## 1. Feature Summary

Reimagine the DSCC web UI as a calm, task-oriented app for non-technical Windows
gamers. The four parallel tool routes become a three-destination shell — Status,
Tuning, Advanced — where the primary surface is a game-led tuning canvas and
power tools are demoted behind an Advanced door. Visual system moves from the
"gamer cockpit" HUD to a Calm Console aesthetic (Linear/Raycast lineage) with a
PlayStation-blue accent.

## 2. Primary User Action

Pick a game (or Everyday), tune how the controller feels, and always know two
things at a glance: *is everything working* and *what's saved vs. what I've
tweaked*.

## 3. Design Direction

- **Color strategy:** Restrained. Neutral dark surfaces; one accent doing real
work (actions, selection, live/edited state).
- **Scene sentence:** An evening PC gamer at a desk or couch in a dim room,
controller in hand, game about to launch — glancing at DSCC to confirm
everything works, then nudging feel. → Dark theme, low-glare, quiet.
- **Anchors:** Linear (soft dark surfaces, quiet hierarchy), Raycast
(disciplined density, settings craft).
- **Anti-references:** current HUD (grid textures, glows, glass), RGB-gamer
software, diagnostics-first layouts.

## 4. Scope

- **Fidelity:** production-ready (the mockups are direction; implementation is
real Svelte 5 + CSS, no component library).
- **Breadth:** whole web UI — shell, all routes, tokens.
- **Interactivity:** shipped-quality; behavior preserved except where this
brief intentionally changes UX.
- **Time intent:** iterate route-by-route on `ui-improvements`; nothing merges
until the gates pass and the user signs off in the running app.

## 5. Information Architecture

```
Sidebar (slim, persistent; icon rail <760px)
├─ Status ← default route on launch
├─ Tuning ← game-led canvas (the heart of the app)
├─ ADVANCED ▸ ← collapsed group, expands in place
│ ├─ Controller details (live input, calibration, connection)
│ ├─ Button mapping (existing workflow, restyled only)
│ └─ Edge onboard slots
└─ ⚙ Settings
```

- **No Games page.** Game selection is a dropdown in the Tuning header:
Running now (detected) → Everyday (Global Profile) → Supported games →
"Setup guide for <game>…" → "+ Add a game manually…".
- **Profiles live in the canvas**: profile selector under the game title;
create/duplicate/import/export inside that menu.
- Old routes `#/games`, `#/controllers`, `#/adaptive-triggers-haptics`,
`#/button-mapping` map to `#/status`, `#/tuning`,
`#/advanced/controller`, `#/advanced/button-mapping` (old hashes redirect).

## 6. The Tuning Canvas (locked pattern, mockup v9)

**Header band (per game, bespoke):** Steam `library_hero` behind a slim ~80px
band, double-scrimmed into the page bg; `library_600x900` cover thumbnail
anchors the game dropdown; profile selector + unsaved-changes count beneath the
title; controller name/battery right-aligned; clickable telemetry status chip.
Everyday (Global Profile) uses a neutral band, no game art.

**Working surface — semantic columns, not control-type rows:**
- **Brake · L2** — trigger curve editor + brake effects (ABS pulse, lockup rumble)
- **Throttle · R2** — trigger curve editor + throttle effects (gear-shift kick,
rev-limiter buzz)
- **Road feel** — road texture rumble, surface detail; game-provided effects
appear here
- **Lights** — lightbar mode/RPM colors, brightness, player LEDs

**Saved rail (furniture):** fixed ~260px panel docked right, sticky on scroll,
excluded from wrapping. Shows the active profile's saved values; edited rows
render `saved → current` with strikethrough; curve editors echo with a dashed
saved-curve ghost. Contains Preview feel ("3s · nothing saved") above
Save changes / Discard. Below ~900px it becomes a docked bottom bar
("2 unsaved changes · Save · Discard", expands to the list on tap).

**Layout contract (the no-wasted-space law):**
- Controls have intrinsic sizes; extra width adds columns or grows
*instruments only* — never stretches sliders/rows.
- Curve editors: flex 280→460px (instruments earn size). Sliders: ~220px cap.
- Columns flex-wrap: 5-across on wide, Road feel/Lights wrap under
Brake/Throttle near 720p widths, single column <760px.
- Saved rail is outside the wrap container at all times (until the <900px bar).
- Smoke test enforces no horizontal overflow at 390px.

## 7. Status (default route)

1. **One sentence of truth:** green/yellow/red dot + "Everything is working." +
one plain-language clause naming game and controller.
2. **Controller** block: alias, family, connection/transport, battery; Rename
inline; hint line for adding another controller.
3. **What's active, and why:** Profile Resolution as plain-words rows — game
detected, profile in use, telemetry freshness, "when the game closes →
back to Global Profile".
4. **Needs attention:** the only home for warnings; states "Nothing else needs
you" when empty. Links into game setup if telemetry goes quiet.

## 8. Per-Game Setup (canvas state, not a pop-up)

- Each Game Module declares its requirements. Selecting an unverified game
renders the walkthrough *in the canvas*; once verified, the canvas shows
tuning controls permanently.
- Forza-style flow: numbered steps (a real sequence): ① game found
(auto-verified) ② enable Data Out with exact menu path + copy buttons for
IP/port ③ drive. A "Listening on port 5300…" box flips green passively when
packets arrive — completion requires zero clicks.
- Zero-setup games (Assetto Corsa Rally): "No setup needed" reassurance +
offer to pre-tune with base feel.
- **Re-entry:** the header telemetry chip ("● TELEMETRY FRESH · setup ↗") opens
the guide anytime; turns yellow and deep-links to the fix when telemetry is
quiet. Fallback entry in the game dropdown. Telemetry loss never yanks the
canvas — it flags Status → Needs attention.

## 9. Advanced

Sidebar group expands in place. Controller details = live input meters,
connection facts (transport, input path, polling, firmware), stick-drift
plain-words readout, Support Bundle download. Framed as "for checking, not for
everyday tuning." Button mapping keeps its current workflow and copy
(read-only mirror messaging preserved for the smoke test), restyled to the new
tokens. Edge onboard slots move here.

## 10. Visual System (tokens.css rewrite)

Neutrals (Calm Console): bg `#141417` · sidebar/header `#18181c` · surface
`#1d1d22` · raised `#26262c` · hairline `#232329` / `#2e2e36` · ink `#d6d6dc`
· muted `#8b8b96` (large/secondary text only; verify 4.5:1 where body-sized).

Accent (PlayStation blue): `--accent #0070CC` (primary buttons) ·
`--accent-bright #1f8fff` (slider fills, live lines, meters) ·
`--accent-text #5db2ff` (accent text on dark, ≥4.5:1) · `--accent-tint #12273d`
(edited/unsaved bg) · `--accent-outline #1d4f80` (edited outlines).
Semantic: green = working, yellow = attention, red = errors only.
Final values to be expressed in OKLCH with contrast verified at implementation.

Type: Inter only (drop Space Grotesk display pairing); fixed rem scale, ratio
~1.2; JetBrains Mono retained solely for literal values (ports, IPs).
Surfaces: flat fills + hairlines; no glassmorphism, grid textures, glows, or
side-stripe borders. Radii ~6–10px. Motion: 150–250ms ease-out state
transitions only; full `prefers-reduced-motion` support.

## 11. Key States

- Tuning: setup-needed (walkthrough) · waiting-for-detection · active-clean ·
active-with-unsaved (count in header, diff in rail, ghost curves) ·
Everyday/Global (neutral band) · no-controller (point to Status).
- Status: all-good · attention (yellow) · no-controller-yet (first-run:
connect instructions) · telemetry-quiet.
- Saved rail: clean (values only) · dirty (diffs + actions) · mobile bar.
- Empty states teach ("Plug in or pair a controller and it appears here").

## 12. Safety Framing (constraint #4)

Preview feel is always labeled "Nothing is saved"; unsaved drift is always
visible (header count + rail diff); plain-words distinction between what's
saved, what the controller is using right now, and what happens on
Save/Discard. Global vs Game Profile state is always named in the header and
on Status.

## 13. Constraints & Gates

- Domain language from CONTEXT.md is law; no _Avoid_ terms in copy.
"Everyday" is a presentation label only and always appears with its domain
term ("Everyday · Global Profile"); the term of record in copy, code, and
docs remains Global Profile.
- Mock/real API contracts unchanged (`web/src/lib/mock/`, `web/src/lib/api/`).
Steam art assets come via existing game artwork fields; if hero/cover grades
are missing for a title, fall back to a neutral band + InitialBadge.
- Behavior preserved except UX changes specified here.
- Gates before any "done": `npm run typecheck`, `npm run build`,
`npm run test:visual-smoke` (route-text assertions will need updating to the
new IA in the same change), `npm run test:source-audit`, plus eyes-on
`dev:mock` at desktop and 390px.
- Coordinate with the architecture-decomposition effort: this rework
effectively decomposes App.svelte's route shells; note overlaps in PRs.

## 14. Implementation Shape (route-by-route, each step green)

1. Tokens + shell: new tokens.css, sidebar, hash redirects (old routes still
render existing views inside the new shell).
2. Status page (new).
3. Tuning canvas: header band + game dropdown + profile menu; semantic
columns wrapping existing trigger/haptics controls; saved rail.
4. Per-game setup state + telemetry chip re-entry.
5. Advanced: move controllers detail + button mapping + Edge slots; restyle.
6. Delete dead styles (HUD, ribbon, games grid); update smoke-test
expectations; full gate run.

## 15. Open Questions (deliberately few)

- Sidebar at 390px: icon rail vs. bottom tab bar — decide in implementation
against the smoke test; mockups assume collapse, either satisfies the
contract.
- Onboarding tutorial and LAN settings panel: keep current behavior, restyle
only; revisit copy in a later pass.
35 changes: 29 additions & 6 deletions web/scripts/visual-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,15 @@ const port = requestedPort > 0 ? requestedPort : await findOpenPort(5174);
const baseUrl = `http://${host}:${port}`;
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
const routeChecks = [
{ hash: '#/games', pattern: /Profiles|Games|Selected Game/i },
{ hash: '#/controllers', pattern: /Controllers|Live Input|Input Path/i },
{ hash: '#/adaptive-triggers-haptics', pattern: /Trigger Curves|Base Haptics|Adaptive/i },
{ hash: '#/button-mapping', pattern: /Customize Button Assignments|Button Mapping|Default mirror/i }
{ hash: '#/status', pattern: /Status|Everything|controller/i },
{ hash: '#/tuning', pattern: /Tuning|Profile/i },
{ hash: '#/advanced/controller', pattern: /Controller details|Live input/i },
// Button mapping needs a game scope first; main() selects one via the
// toolbar on the previously loaded page, so this check must stay LAST.
{ hash: '#/advanced/button-mapping', pattern: /Button Mapping|Default mirror/i }
];
// Old routes keep working forever; each lands on the new home for its content.
const legacyRedirectChecks = [{ from: '#/games', to: '#/tuning' }];
const viewports = [
{ width: 1366, height: 768 },
{ width: 1440, height: 900 },
Expand Down Expand Up @@ -114,6 +118,16 @@ async function main() {
});

for (const check of routeChecks) {
if (check.hash === '#/advanced/button-mapping') {
// Button mapping is guarded behind a game scope; pick the mock
// running game from the Tuning header's game dropdown so the route
// does not bounce.
await page.goto(`${baseUrl}/#/tuning`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(300);
await page.click('.tuning-header-game');
await page.click('.tuning-menu .tuning-menu-item:has(.tuning-running-dot)');
await page.waitForTimeout(500);
}
await page.goto(`${baseUrl}/${check.hash}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(300);
const snapshot = await routeSnapshot(page);
Expand All @@ -122,8 +136,17 @@ async function main() {
if (!check.pattern.test(snapshot.text)) failures.push(`${label}: expected route text was missing`);
if (!snapshot.canReachBottom) failures.push(`${label}: page content could not scroll to the bottom`);
if (snapshot.scrollWidth > snapshot.clientWidth + 2) failures.push(`${label}: horizontal overflow ${snapshot.scrollWidth - snapshot.clientWidth}px`);
if (check.hash === '#/button-mapping' && !/Default mirror only|No writable|read-only|Global Profile/i.test(snapshot.text)) {
failures.push(`${label}: read-only/default-mirror mapping copy was missing`);
if (check.hash === '#/advanced/button-mapping' && !/Default mirror only|No writable|read-only|inputs mapped/i.test(snapshot.text)) {
failures.push(`${label}: mapping session copy (read-only or live layout) was missing`);
}
}

for (const redirect of legacyRedirectChecks) {
await page.goto(`${baseUrl}/${redirect.from}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(300);
const finalHash = await page.evaluate(() => location.hash);
if (finalHash !== redirect.to) {
failures.push(`${viewport.width}x${viewport.height} ${redirect.from}: redirected to ${finalHash}, expected ${redirect.to}`);
}
}
if (consoleErrors.length) {
Expand Down
Loading
Loading