fix(story-mode): a three-row bar on phones, buttons for panels out of view, loop words that match the marks

- The bar on a phone took four rows (216 px of a 664 px screen) with the
  close button alone on the last; it now takes three (170 px): title and
  close, the turn, the view and Save image. The drawer starts exactly
  under it and follows it when the phone turns (the bar is observed).
- A panel below the fold had no buttons until it was scrolled to (only
  drawn panels get click regions), so Tab skipped it. A quiet frame now
  draws one such panel, a band at a time; its entrance still plays when
  it comes into view.
- "Fix and test: the first run fails, the second passes." was chosen by
  rounds, so a loop with three runs (a failing test, then ruff, then a
  pass) said it under ✗ ✗ ✓. It now needs exactly two runs; that loop
  reads "Fix and test, 2 rounds: 2 runs fail, then it passes.", and the
  corpus pins every loop's marks.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-18 12:50:40 +02:00
1 parent 3f1ecfe83a
commit 76d5c61eda
14 files changed
+225 -6

No files matched your search

+32 -1
View File
@@ -218,6 +218,37 @@ body.story-open {
}
}
/* A phone: three rows (title and close, the turn, the view and Save image), not four with the
close button alone at the bottom. Tab order stays the DOM's; focus starts on close anyway. */
@media (max-width: 600px) {
.story-bar {
gap: 8px 10px;
padding-block: 8px;
}
.story-brand {
order: 0;
flex: 1 1 0;
}
.story-title {
font-size: 24px;
}
.story-close {
order: 1;
}
.story-nav {
order: 2;
flex: 1 1 100%;
}
.story-turns {
max-width: none;
}
.story-seg,
.story-live,
.story-save {
order: 3;
}
}
/* Main: the sheet and the wire */
.story-main {
overflow: auto;
@@ -731,7 +762,7 @@ body.story-open .toast-container {
padding-right: max(16px, env(safe-area-inset-right, 0px));
}
.story-drawer {
top: calc(var(--story-bar-h, 18vh) + 6px);
top: var(--story-bar-h, 18vh);
left: 0;
width: auto;
height: auto;
+14
View File
@@ -1894,12 +1894,16 @@ Object.assign(CodemanApp.prototype, {
_storyStartRuntime() {
const st = this._story;
const bar = st.root.querySelector('.story-bar');
st.ro = new ResizeObserver((entries) => {
for (const entry of entries) {
// The bar's height places the phone drawer below it, and changes when the phone turns.
if (entry.target === bar) st.root.style.setProperty('--story-bar-h', `${bar.offsetHeight}px`);
const rt = st.panels.find((p) => p.el === entry.target);
if (rt) this._storySizePanel(rt);
}
});
if (bar) st.ro.observe(bar);
st.io = new IntersectionObserver((entries) => {
for (const entry of entries) {
const rt = st.panels.find((p) => p.el === entry.target);
@@ -1947,6 +1951,16 @@ Object.assign(CodemanApp.prototype, {
drawn++;
}
st.drawCursor = n ? ((st.drawCursor || 0) + 1) % n : 0;
// A quiet frame draws a panel out of view (one band of it), so its buttons exist for the
// keyboard and a screen reader before anyone scrolls to it. Its clock waits, so the entrance
// still plays when it comes into view.
if (!drawn) {
const off = st.panels.find((p) => !p.visible && p.pending);
if (off) {
off.pending = false;
this._storyDrawPanel(off);
}
}
}
st.raf = requestAnimationFrame(loop);
};