Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
669 changes: 669 additions & 0 deletions docs/superpowers/plans/2026-06-11-p3-polish.md

Large diffs are not rendered by default.

Binary file removed web/public/dualsense/focus/focus_L1.png
Binary file not shown.
Binary file removed web/public/dualsense/focus/focus_L2.png
Binary file not shown.
Binary file removed web/public/dualsense/focus/focus_Lstick.png
Binary file not shown.
Binary file removed web/public/dualsense/focus/focus_PS.png
Binary file not shown.
Binary file removed web/public/dualsense/focus/focus_R1.png
Binary file not shown.
Binary file removed web/public/dualsense/focus/focus_R2.png
Binary file not shown.
Binary file removed web/public/dualsense/focus/focus_Rstick.png
Binary file not shown.
Binary file removed web/public/dualsense/focus/focus_circle.png
Binary file not shown.
Binary file removed web/public/dualsense/focus/focus_create.png
Binary file not shown.
Binary file removed web/public/dualsense/focus/focus_cross.png
Binary file not shown.
Binary file removed web/public/dualsense/focus/focus_down.png
Binary file not shown.
Binary file removed web/public/dualsense/focus/focus_left.png
Binary file not shown.
Binary file removed web/public/dualsense/focus/focus_options.png
Binary file not shown.
Binary file removed web/public/dualsense/focus/focus_rear_L.png
Binary file not shown.
Binary file removed web/public/dualsense/focus/focus_rear_R.png
Binary file not shown.
Binary file removed web/public/dualsense/focus/focus_right.png
Binary file not shown.
Binary file removed web/public/dualsense/focus/focus_square.png
Binary file not shown.
Binary file removed web/public/dualsense/focus/focus_touchpad.png
Binary file not shown.
Binary file removed web/public/dualsense/focus/focus_triangle.png
Binary file not shown.
Binary file removed web/public/dualsense/focus/focus_up.png
Binary file not shown.
20 changes: 13 additions & 7 deletions web/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
edgeSlotWriteLabel as edgeOnboardSlotWriteLabel,
edgeSlotWriteTooltip as edgeOnboardSlotWriteTooltip,
emptyEdgeOnboardProfileState,
friendlyEdgeSlotsError,
isEdgeTargetController,
shouldReadEdgeOnboardProfiles,
shouldResetEdgeOnboardProfiles
Expand Down Expand Up @@ -464,8 +465,10 @@
$: telemetry = snapshot?.telemetry ?? [];
$: telemetryByName = new Map(telemetry.map((item) => [item.name, item]));
$: effectState = snapshot?.effectState;
$: l2LivePress = controllerInputFresh ? l2ControllerPress : selectedTuningScope === 'global' ? 0 : telemetryUnitValue('input.brake');
$: r2LivePress = controllerInputFresh ? r2ControllerPress : selectedTuningScope === 'global' ? 0 : telemetryUnitValue('input.throttle');
// Stale telemetry must read as "no press", not as the last frozen value.
$: gameTelemetryUsable = selectedTuningScope !== 'global' && selectedGameTelemetryFresh;
$: l2LivePress = controllerInputFresh ? l2ControllerPress : gameTelemetryUsable ? telemetryUnitValue('input.brake') : 0;
$: r2LivePress = controllerInputFresh ? r2ControllerPress : gameTelemetryUsable ? telemetryUnitValue('input.throttle') : 0;
$: triggerCurveDisplayMode = selectedTuningScope === 'game' && usesForzaRuntimeProfile(selectedTuningGame) ? 'forza' : 'base';
$: appSettings = snapshot?.appSettings;
$: forzaGlyphs = appSettings?.settings.forzaPlaystationGlyphs;
Expand Down Expand Up @@ -631,11 +634,14 @@
};
$: telemetryRateText = `${telemetryPacketRate >= 100 ? telemetryPacketRate.toFixed(0) : telemetryPacketRate.toFixed(1)} Hz`;
$: telemetryRateDetail = telemetryRateStatusText(adapter);
$: systemReadoutTitle = selectedTuningScope === 'global' ? 'Profile Scope' : 'Telemetry Rate';
$: systemReadoutValue = selectedTuningScope === 'global' ? 'Global' : telemetryRateText;
// "Tuning Scope", not "Profile Scope": this readout tracks what the tuning
// view edits, while Status's sentence tracks profile resolution — the two can
// legitimately differ, so the label must not claim to be the same thing.
$: systemReadoutTitle = selectedTuningScope === 'global' ? 'Tuning Scope' : 'Telemetry Rate';
$: systemReadoutValue = selectedTuningScope === 'global' ? 'Everyday' : telemetryRateText;
$: systemReadoutDetail =
selectedTuningScope === 'global'
? 'Controller-only tuning'
? 'Global Profile · controller-only tuning'
: telemetryRateDetail;
$: overrideScope = profileWorkspace.overrideScope;
// Sync the override dropdown when the ACTIVE profile changes (server-side
Expand Down Expand Up @@ -1495,7 +1501,7 @@
edgeProfiles = await getEdgeProfiles(controllerId);
} catch (caught) {
edgeProfiles = null;
edgeProfilesError = caught instanceof Error ? caught.message : 'Unable to read Edge onboard slots.';
edgeProfilesError = friendlyEdgeSlotsError(caught, 'Unable to read Edge onboard slots.');
} finally {
edgeProfilesLoading = false;
}
Expand Down Expand Up @@ -1534,7 +1540,7 @@
showToast(response.message, response.accepted ? 'success' : 'error');
await loadEdgeProfiles(controller.id, true);
} catch (caught) {
edgeProfilesError = caught instanceof Error ? caught.message : 'Unable to write Edge onboard slot.';
edgeProfilesError = friendlyEdgeSlotsError(caught, 'Unable to write Edge onboard slot.');
showToast(edgeProfilesError, 'error');
} finally {
edgeProfilesBusySlot = '';
Expand Down
14 changes: 14 additions & 0 deletions web/src/app/edgeOnboardProfiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,20 @@ export function edgeSlotWriteLabel(edgeProfiles: EdgeProfilesResponse | null): s
return edgeProfiles?.supportState === 'read_write' ? 'Write' : 'Stage';
}

/**
* The missing-agent failure arrives as dev-voiced API text (thrown by
* getEdgeProfiles/writeEdgeProfile when no real agent is serving); speak
* product before it reaches a note or toast. Direction-neutral: the same
* helper handles both the read and the write path, so the copy can't claim a
* direction.
*/
export const friendlyEdgeSlotsError = (caught: unknown, fallback: string): string => {
const message = caught instanceof Error ? caught.message : fallback;
return message.includes('requires the real DSCC agent')
? 'Onboard slots need DSCC running.'
: message;
};

export function edgeProfileNameForSlot(slot: EdgeProfileSlot, profileName: string): string {
const sourceName = profileName || 'DSCC Profile';
return `${sourceName} ${slot.shortcut.replace('Fn + ', '')}`.trim().slice(0, 64);
Expand Down
10 changes: 7 additions & 3 deletions web/src/lib/features/controllers/EdgeSlotsView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,13 @@
{:else}
<div class="edge-slot-row disabled">
<div class="edge-slot-copy">
<span class="lbl">Fn Slots</span>
<strong>{edgeProfilesLoading ? 'Reading slots' : 'No slot data'}</strong>
<small>{edgeProfilesLoading ? 'controller scan' : 'unavailable'}</small>
<span class="lbl">Fn slots</span>
<strong>{edgeProfilesLoading ? 'Reading slots' : 'Nothing read yet'}</strong>
<small
>{edgeProfilesLoading
? 'checking the controller'
: 'Fn slots are shortcuts stored on the controller — hold Fn and press an action button to switch. Press Read to see what each slot holds.'}</small
>
</div>
</div>
{/if}
Expand Down
4 changes: 3 additions & 1 deletion web/src/lib/features/status/StatusView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,9 @@
<div class="status-surf">
{#each controllers as item (item.id)}
<div class="status-controller-card">
<div class="status-controller-icon" aria-hidden="true">🎮</div>
<div class="status-controller-icon" aria-hidden="true">
<span class="dm-controller-glyph status-controller-glyph"></span>
</div>
<div class="status-controller-main">
{#if renameActiveId === item.id}
<div class="status-rename">
Expand Down
12 changes: 8 additions & 4 deletions web/src/lib/features/tuning/SavedRail.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -61,19 +61,23 @@
);
</script>

{#snippet rowValue(value: string, isColor: boolean)}
{#if isColor && value.startsWith('#')}<span class="saved-swatch" style:background={value}></span>{/if}{value}
{/snippet}

{#snippet diffRows()}
{#each rows as item (item.id)}
<div class="saved-row" class:dirty={item.dirty}>
<span class="saved-row-label">{item.label}</span>
<span class="saved-row-value">
{#if item.dirty && item.savedValue !== item.currentValue}
<s class="saved-row-was">{item.savedValue}</s>
<span class="saved-row-now">→ {item.currentValue}</span>
<s class="saved-row-was">{@render rowValue(item.savedValue, item.kind === 'color')}</s>
<span class="saved-row-now">→ {@render rowValue(item.currentValue, item.kind === 'color')}</span>
{:else if item.dirty}
<!-- Group summaries ("2 of 5 edited") have no single saved value to strike. -->
<span class="saved-row-now">{item.currentValue}</span>
<span class="saved-row-now">{@render rowValue(item.currentValue, item.kind === 'color')}</span>
{:else}
<span class="saved-row-saved">{item.savedValue}</span>
<span class="saved-row-saved">{@render rowValue(item.savedValue, item.kind === 'color')}</span>
{/if}
</span>
</div>
Expand Down
15 changes: 11 additions & 4 deletions web/src/lib/features/tuning/savedDiff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -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 = <T extends object>(
Expand Down Expand Up @@ -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',
Expand Down
16 changes: 1 addition & 15 deletions web/src/styles/button-mapping/layout/controller-art.css
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,7 @@
z-index: 1;
}

.dm-controller-base,
.dm-controller-focus {
.dm-controller-base {
position: absolute;
inset: 4% 4% 6%;
width: 92%;
Expand All @@ -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%;
Expand Down
5 changes: 2 additions & 3 deletions web/src/styles/feedback.css
Original file line number Diff line number Diff line change
Expand Up @@ -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%),
Expand Down
24 changes: 23 additions & 1 deletion web/src/styles/status.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 */
}
}
4 changes: 4 additions & 0 deletions web/src/styles/tokens.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
34 changes: 27 additions & 7 deletions web/src/styles/tuning.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -545,14 +545,27 @@

.saved-row-saved,
.saved-row-was {
color: var(--ink-muted);
color: var(--ink-muted-raised);
}

.saved-row-now {
color: var(--accent-text);
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 <s>.) */
.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;
Expand Down Expand Up @@ -588,7 +601,7 @@

.saved-preview-note {
font-size: 9px;
color: var(--ink-muted);
color: var(--ink-muted-raised);
white-space: nowrap;
}

Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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. */
Expand Down
Loading