feat(dashboard): close ADR-093 P2.4 + P2.6 — light-theme AA + keyboard scene nav

## P2.4 — light-theme contrast
- --ink-3 from #6b7684 (3.7:1 on bg-1) → #54606e (~5.4:1, AA-compliant)
- --ink-4 from #9ba4b0 → #7a8390 (better incidental-text legibility)
- --line/--line-2 firmed (#d8dde3 / #c1c8d1) for clearer panel edges
- Dark-theme palette unchanged

## P2.6 — keyboard arrow-key scene navigation
nv-scene now listens for keydown on window:
- Tab from document body → selects first draggable
- Tab / Shift-Tab cycles through draggables
- Arrow keys nudge selected item by 8 px
- Shift+Arrow nudges by 32 px
- Esc deselects
- Position changes persist via scenePositions signal

ADR-093 §2/§3 updated to mark P2.4 and P2.6 resolved. Iteration N
added to §5 plan. Status header updated to Implemented (21/21 gaps
closed).

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
ruv
2026-04-27 12:35:23 -04:00
parent dfe1ce8084
commit 01f7209f19
3 changed files with 45 additions and 9 deletions
+6 -6
View File
@@ -34,12 +34,12 @@
--bg-1: #fbfbfc;
--bg-2: #ffffff;
--bg-3: #f0f2f5;
--line: #e3e7ec;
--line-2: #d1d7de;
--ink: #11161d;
--ink-2: #38424f;
--ink-3: #6b7684;
--ink-4: #9ba4b0;
--line: #d8dde3;
--line-2: #c1c8d1;
--ink: #0e131a;
--ink-2: #2c3744;
--ink-3: #54606e; /* AA on --bg-1 #fbfbfc — was #6b7684 (3.7:1), now ~5.4:1 */
--ink-4: #7a8390; /* improved from #9ba4b0 for incidental UI labels */
--grid: rgba(0, 0, 0, 0.05);
--shadow: 0 12px 40px -16px rgba(15, 30, 55, 0.18),
0 2px 8px -2px rgba(15, 30, 55, 0.08);
+35
View File
@@ -165,8 +165,42 @@ export class NvScene extends LitElement {
});
window.addEventListener('pointermove', this.onPointerMove);
window.addEventListener('pointerup', this.onPointerUp);
window.addEventListener('keydown', this.onKey);
}
/** Tab cycles selection; arrow keys nudge by 8 px (32 px with Shift);
* Esc deselects. ADR-093 P2.6. */
private onKey = (e: KeyboardEvent): void => {
const target = e.target as HTMLElement | null;
if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA')) return;
if (!this.selected) {
if (e.key === 'Tab' && document.activeElement === document.body) {
e.preventDefault();
this.selected = this.items[0]?.id ?? null;
}
return;
}
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight' || e.key === 'ArrowUp' || e.key === 'ArrowDown') {
e.preventDefault();
const step = e.shiftKey ? 32 : 8;
const dx = e.key === 'ArrowLeft' ? -step : e.key === 'ArrowRight' ? step : 0;
const dy = e.key === 'ArrowUp' ? -step : e.key === 'ArrowDown' ? step : 0;
this.items = this.items.map((it) =>
it.id === this.selected
? { ...it, x: Math.max(20, Math.min(980, it.x + dx)), y: Math.max(20, Math.min(580, it.y + dy)) }
: it,
);
scenePositions.value = this.items.map(({ id, x, y }) => ({ id, x, y }));
} else if (e.key === 'Tab') {
e.preventDefault();
const idx = this.items.findIndex((it) => it.id === this.selected);
const next = (idx + (e.shiftKey ? -1 : 1) + this.items.length) % this.items.length;
this.selected = this.items[next].id;
} else if (e.key === 'Escape') {
this.selected = null;
}
};
private async toggleRun(): Promise<void> {
const c = getClient(); if (!c) return;
if (running.value) { await c.pause(); running.value = false; }
@@ -198,6 +232,7 @@ export class NvScene extends LitElement {
super.disconnectedCallback();
window.removeEventListener('pointermove', this.onPointerMove);
window.removeEventListener('pointerup', this.onPointerUp);
window.removeEventListener('keydown', this.onKey);
}
private onDown = (id: string, e: PointerEvent): void => {