mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-15 10:59:32 +01:00
fix(story-mode): read right by a screen reader, and the last of review round 6
Accessibility (review6-c):
- A panel is named "Panel 2 of 7" and its caption is read once, as its
content (it was the name and the content both).
- Balloons say who speaks, visually hidden ("Claude says: ", "You say: ",
"Claude asks: ", "Notice: "); Save image leaves those words out.
- The overlay is named "Story Mode: <session>", the drawer by its kind and
title ("Diff 2 files"), the final panel's chips by what they open, and
the wire's panel labels are headings.
- While a file preview opened from a panel is up, the overlay is not
aria-modal (the preview sits beside it), and is again once focus returns.
- The focus ring on a click region gets a light band under its dashes; on
the loop's magenta halftone it vanished.
From review6-b:
- "Mission complete!" no longer shows while a helper or a background
command is still out ("To be continued…").
- A background command's drawer says "Started in the background…", then
"Finished in the background, exit code 0…", instead of the launch note
meant for Claude ("do not poll that file").
- `sed -n '5,25p' README.md` is a printed file, offered in the drawer.
- A report turn no longer counts the helper it answers as one it started.
- A helper's `cd /work/x && ls` keeps its folder as ".".
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
89c222086a
commit
6e45831909
7 files changed
+83
-17
No files matched your search
@@ -345,6 +345,10 @@ body.story-open {
|
||||
outline: 3px dashed var(--story-magenta) !important;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
/* A light band under the dashes: on a magenta halftone (the loop) the ring alone vanished. */
|
||||
.story-hit:focus-visible {
|
||||
box-shadow: 0 0 0 5px var(--story-balloon);
|
||||
}
|
||||
.story-cap {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
@@ -393,7 +397,8 @@ body.story-open {
|
||||
cursor: pointer;
|
||||
}
|
||||
/* Spoken, not shown: the status region a screen reader announces from. */
|
||||
.story-sr {
|
||||
.story-sr,
|
||||
.story-vh {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
@@ -702,6 +707,10 @@ body.story-open .toast-container {
|
||||
font: 16px/1.1 var(--story-letter);
|
||||
color: var(--story-muted);
|
||||
}
|
||||
.story-wire-group h3 {
|
||||
margin: 0;
|
||||
font: inherit;
|
||||
}
|
||||
.story-ev {
|
||||
display: grid;
|
||||
grid-template-columns: 62px minmax(0, 1fr);
|
||||
|
||||
@@ -1586,6 +1586,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
st.sessionLabel.textContent = session
|
||||
? this.getSessionName?.(session) || session.name || sessionId.slice(0, 8)
|
||||
: sessionId.slice(0, 8);
|
||||
st.root.setAttribute('aria-label', `Story Mode: ${st.sessionLabel.textContent}`);
|
||||
this._storyRender();
|
||||
this._storyStartRuntime();
|
||||
this._storyRefresh();
|
||||
@@ -1649,7 +1650,13 @@ Object.assign(CodemanApp.prototype, {
|
||||
root.hidden = true;
|
||||
root.setAttribute('role', 'dialog');
|
||||
root.setAttribute('aria-modal', 'true');
|
||||
root.setAttribute('aria-labelledby', 'storyTitle');
|
||||
root.setAttribute('aria-label', 'Story Mode');
|
||||
// A file preview opened from a panel sits beside the overlay, not in it: while it is up the
|
||||
// overlay is no longer modal, or a screen reader that honours aria-modal would hide it.
|
||||
root.addEventListener('focusin', () => {
|
||||
if (!document.getElementById('filePreviewOverlay')?.classList.contains('visible'))
|
||||
root.setAttribute('aria-modal', 'true');
|
||||
});
|
||||
|
||||
const bar = el('div', 'story-bar');
|
||||
const brand = el('div', 'story-brand');
|
||||
@@ -1731,7 +1738,8 @@ Object.assign(CodemanApp.prototype, {
|
||||
drawerMeta.append(drawerKind, drawerLive);
|
||||
const drawerTitle = el('p', 'story-drawer-title');
|
||||
drawerTitle.id = 'storyDrawerTitle';
|
||||
drawer.setAttribute('aria-labelledby', 'storyDrawerTitle');
|
||||
drawerKind.id = 'storyDrawerKind';
|
||||
drawer.setAttribute('aria-labelledby', 'storyDrawerKind storyDrawerTitle');
|
||||
const drawerSub = el('p', 'story-drawer-sub');
|
||||
drawerTitle.dataset.i18nSkip = '';
|
||||
drawerSub.dataset.i18nSkip = '';
|
||||
@@ -2370,7 +2378,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
const balloonText = p.kind === 'ask' && p.question?.text ? p.question.text : p.balloon?.text;
|
||||
rt.balloon.hidden = !balloonText;
|
||||
// The text sits in its own box so a long one can be clamped without clipping the tail.
|
||||
let balloonSpan = rt.balloon.firstElementChild;
|
||||
let balloonSpan = rt.balloon.querySelector('.story-balloon-text');
|
||||
if (!balloonSpan) {
|
||||
balloonSpan = document.createElement('span');
|
||||
balloonSpan.className = 'story-balloon-text';
|
||||
@@ -2380,6 +2388,20 @@ Object.assign(CodemanApp.prototype, {
|
||||
// A prompt somebody else sent (a background job, another session) is a notice, not your speech.
|
||||
const origin = page.events[p.events[0]]?.origin ?? (index === 0 ? page.origin : 'human');
|
||||
const notice = p.kind === 'mission' && origin && origin !== 'human';
|
||||
// Who speaks, for a screen reader: the art shows it, the text alone does not.
|
||||
let who = rt.balloon.querySelector('.story-vh');
|
||||
if (!who) {
|
||||
who = document.createElement('span');
|
||||
who.className = 'story-vh';
|
||||
rt.balloon.prepend(who);
|
||||
}
|
||||
who.textContent = notice
|
||||
? 'Notice: '
|
||||
: p.kind === 'mission' || p.balloon?.who === 'you'
|
||||
? 'You say: '
|
||||
: p.kind === 'ask'
|
||||
? 'Claude asks: '
|
||||
: 'Claude says: ';
|
||||
rt.balloon.onclick =
|
||||
p.kind === 'mission'
|
||||
? () =>
|
||||
@@ -2397,7 +2419,8 @@ Object.assign(CodemanApp.prototype, {
|
||||
}`;
|
||||
rt.liveTag.hidden = !p.live;
|
||||
rt.el.classList.toggle('is-live', !!p.live);
|
||||
rt.el.setAttribute('aria-label', `Panel ${index + 1}: ${p.caption.story}`);
|
||||
// Named by its place only: the caption inside is read as the panel's content, once.
|
||||
rt.el.setAttribute('aria-label', `Panel ${index + 1} of ${page.panels.length}`);
|
||||
if (rt.answer) {
|
||||
rt.answer.hidden = !p.question?.answer;
|
||||
const planned = page.events[p.events[0]]?.tool === 'ExitPlanMode';
|
||||
@@ -2416,7 +2439,9 @@ Object.assign(CodemanApp.prototype, {
|
||||
? 'To be continued…'
|
||||
: page.noneWorked
|
||||
? 'Turn over'
|
||||
: 'Mission complete!';
|
||||
: page.helpersRunning || page.events.some((e) => e.running && !e.helper)
|
||||
? 'To be continued…'
|
||||
: 'Mission complete!';
|
||||
const chips = rt.splash.querySelector('.story-chips');
|
||||
const items = [];
|
||||
if (page.durationMs !== undefined) items.push({ text: this._storyDuration(page.durationMs) });
|
||||
@@ -2443,6 +2468,10 @@ Object.assign(CodemanApp.prototype, {
|
||||
if (item.open) {
|
||||
inner.type = 'button';
|
||||
inner.dataset.storyOpen = item.open;
|
||||
inner.setAttribute(
|
||||
'aria-label',
|
||||
`${item.text}: ${item.open.startsWith('diff') ? 'see the changes' : 'list every step'}`
|
||||
);
|
||||
}
|
||||
inner.textContent = item.text;
|
||||
li.append(inner);
|
||||
@@ -2777,7 +2806,9 @@ Object.assign(CodemanApp.prototype, {
|
||||
}
|
||||
}
|
||||
// The text the reader sees (hidden spans, like the other reading level, are left out).
|
||||
const text = (el.innerText || el.textContent || '').trim();
|
||||
let text = (el.innerText || el.textContent || '').trim();
|
||||
const spoken = el.querySelector?.('.story-vh');
|
||||
if (spoken) text = text.replace(spoken.textContent.trim(), '').trim();
|
||||
if (!text) return;
|
||||
const size = parseFloat(cs.fontSize) || 14;
|
||||
const lh = parseFloat(cs.lineHeight) || size * 1.15;
|
||||
@@ -2857,7 +2888,9 @@ Object.assign(CodemanApp.prototype, {
|
||||
if (pi !== lastPanel && pi >= 0) {
|
||||
const g = document.createElement('li');
|
||||
g.className = 'story-wire-group';
|
||||
g.textContent = `Panel ${pi + 1} · ${STORY_KIND_NAMES[page.panels[pi].kind] || page.panels[pi].kind}`;
|
||||
const h = document.createElement('h3');
|
||||
h.textContent = `Panel ${pi + 1} · ${STORY_KIND_NAMES[page.panels[pi].kind] || page.panels[pi].kind}`;
|
||||
g.append(h);
|
||||
rows.push(g);
|
||||
lastPanel = pi;
|
||||
}
|
||||
@@ -2984,6 +3017,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
const st = this._story;
|
||||
if (!st || !path) return;
|
||||
st.previewReturn = trigger || document.activeElement;
|
||||
st.root.setAttribute('aria-modal', 'false');
|
||||
this.openFilePreview?.(path, st.sessionId);
|
||||
requestAnimationFrame(() =>
|
||||
document.querySelector('#filePreviewOverlay.visible button[title="Close"]')?.focus({ preventScroll: true })
|
||||
@@ -3334,7 +3368,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
const li = el('li', 'story-ev');
|
||||
li.dataset.actor = 'helper';
|
||||
const body = el('span', 'story-ev-body');
|
||||
const shown = this._storyShortPath(ev.label);
|
||||
const shown = this._storyShortPath(ev.label, { dot: ev.tool === 'Bash' });
|
||||
const label = ev.file ? el('button', 'story-link', shown) : el('span', null, shown);
|
||||
if (shown !== ev.label) label.title = ev.label;
|
||||
if (ev.file) {
|
||||
|
||||
Reference in new issue
Block a user