From 4e937c44b41901207a7da0263ca6fc3f0747244a Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Mon, 19 Oct 2026 06:04:16 +0200 Subject: [PATCH] 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) --- src/web/public/story-mode.css | 62 ++++++++++++++++++++++++----------- src/web/public/story-mode.js | 10 ++++-- test/story-mode-ui.test.ts | 16 +++++++++ 3 files changed, 66 insertions(+), 22 deletions(-) diff --git a/src/web/public/story-mode.css b/src/web/public/story-mode.css index 48614106..fcb94895 100644 --- a/src/web/public/story-mode.css +++ b/src/web/public/story-mode.css @@ -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, diff --git a/src/web/public/story-mode.js b/src/web/public/story-mode.js index 114f9359..b4734d5e 100644 --- a/src/web/public/story-mode.js +++ b/src/web/public/story-mode.js @@ -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); diff --git a/test/story-mode-ui.test.ts b/test/story-mode-ui.test.ts index 2c7b5cfc..138e9d3e 100644 --- a/test/story-mode-ui.test.ts +++ b/test/story-mode-ui.test.ts @@ -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', () => {