feat(story-mode): the turn picker and the composer are one prompt bar

The owner asked to merge the turn picker with the new input field. The prompts you already sent
(the picker and its arrows) now sit on the left of the bottom bar, the next message beside them,
the hint under the field; on a narrow screen the turns get their own row above it. The top bar
keeps the title, the reading levels, Save image and close, so a phone's bar is two rows. On a
touch screen the ready hint (Enter and Shift+Enter) is hidden: it means nothing on a touch
keyboard, and the hints that matter still show.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-19 06:04:16 +02:00
1 parent acf0567c1a
commit 4e937c44b4
3 files changed
+66 -22

No files matched your search

+43 -19
View File
@@ -251,8 +251,9 @@ 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. */
/* A phone: two rows (title and close, then the view and Save image), not three with the close
button alone at the bottom. Tab order stays the DOM's; focus starts on close anyway. The turn
picker is in the composer at the bottom. */
@media (max-width: 600px) {
.story-bar {
gap: 8px 10px;
@@ -268,13 +269,6 @@ body.story-open {
.story-close {
order: 1;
}
.story-nav {
order: 2;
flex: 1 1 100%;
}
.story-turns {
max-width: none;
}
.story-seg,
.story-live,
.story-save {
@@ -1157,17 +1151,41 @@ body.story-open .toast-container {
background: var(--story-sheet);
border-top: 2px solid var(--story-ink);
}
.story-compose-inner {
max-width: 1240px;
margin: 0 auto;
}
/* One prompt bar: the turns you sent (picker and arrows) on the left, the next message beside them,
the hint under the field. On a narrow screen the turns take a row of their own above it. */
.story-compose-row {
display: flex;
align-items: flex-end;
gap: 8px;
display: grid;
grid-template-columns: minmax(220px, 400px) minmax(0, 1fr) auto;
grid-template-areas:
'nav field send'
'. hint .';
align-items: end;
gap: 4px 8px;
}
.story-compose .story-nav {
grid-area: nav;
min-width: 0;
}
/* As tall as the field beside it. */
.story-compose .story-nav .story-btn {
min-width: 44px;
height: 44px;
}
.story-compose .story-turns {
max-width: none;
height: 44px;
}
@media (max-width: 760px) {
.story-compose-row {
grid-template-columns: minmax(0, 1fr) auto;
grid-template-areas:
'nav nav'
'field send'
'hint hint';
}
}
.story-compose .story-compose-field {
flex: 1 1 auto;
grid-area: field;
min-width: 0;
box-sizing: border-box;
/* One line to about five (story-mode.js grows it to its text), then it scrolls inside. 16px keeps
@@ -1193,7 +1211,7 @@ body.story-open .toast-container {
border-style: dashed;
}
.story-compose .story-compose-send {
flex: none;
grid-area: send;
min-width: 76px;
height: 44px;
padding: 0 16px;
@@ -1208,7 +1226,8 @@ body.story-open .toast-container {
cursor: default;
}
.story-compose-hint {
margin: 4px 0 0;
grid-area: hint;
margin: 0;
font: 12px/1.35 var(--story-body);
color: var(--story-muted);
}
@@ -1242,6 +1261,11 @@ body.story-open .toast-container {
.story-overlay .story-wire-pill {
min-height: 44px;
}
/* "Enter sends. Shift+Enter adds a line." means nothing on a touch keyboard: the bar keeps its
height for the page, and the hints that matter (Claude waits, the session is gone) still show. */
.story-compose[data-state='ready'] .story-compose-hint {
display: none;
}
.story-cap-dir,
.story-ev,
.story-code,
+7 -3
View File
@@ -1859,7 +1859,9 @@ Object.assign(CodemanApp.prototype, {
const saveBtn = btn('story-btn story-save', 'Save image', 'Save this page as an image');
saveBtn.title = 'Save this page as a PNG to share';
const closeBtn = btn('story-btn story-close', '×', 'Close Story Mode');
bar.append(brand, nav, seg, live, saveBtn, closeBtn);
// The turn picker lives in the composer at the bottom (_storyComposeBuild): the prompts you sent and
// the one you write next, in one bar.
bar.append(brand, seg, live, saveBtn, closeBtn);
// What a screen reader hears as the page changes: a new turn, a finished one.
const announcer = el('p', 'story-sr');
announcer.dataset.i18nSkip = ''; // it reads out the agent's prompts
@@ -1964,6 +1966,7 @@ Object.assign(CodemanApp.prototype, {
wireCount,
wireList,
wirePill,
navEl: nav,
drawerEl: drawer,
drawerKind,
drawerLive,
@@ -2107,8 +2110,9 @@ Object.assign(CodemanApp.prototype, {
const t = typeof window.CodemanI18n?.t === 'function' ? (s) => window.CodemanI18n.t(s) : (s) => s;
field.setAttribute('aria-label', t('Message Claude'));
field.placeholder = t('Tell Claude what to do next');
row.append(field, send);
inner.append(row, hint);
// The turn picker on the left (the prompts already sent), then the next one: one prompt bar.
row.append(st.navEl, field, send, hint);
inner.append(row);
box.append(inner);
// Between the page and the drawer: Tab walks bar, page, composer, then an open drawer.
st.root.insertBefore(box, st.drawerEl);
+16
View File
@@ -1638,6 +1638,22 @@ describe('story-mode.js composer', () => {
expect($('.story-page')!.contains($('.story-compose'))).toBe(false);
expect($('.story-main')!.contains($('.story-compose'))).toBe(false);
});
it('the turn picker sits in the prompt bar beside the field, not in the top bar', async () => {
boot(done());
app.openStoryMode(SID);
await settle();
const row = $('.story-compose-row')!;
expect($('.story-bar .story-turns')).toBeNull();
expect([...row.children].map((n) => n.className)).toEqual([
'story-nav',
'story-compose-field',
'story-compose-send',
'story-compose-hint',
]);
expect(row.querySelector('.story-prev')).not.toBeNull();
expect(row.querySelector('.story-next')).not.toBeNull();
});
});
describe('story-mode.js scenes', () => {