diff --git a/specs/spec10/10-tui.md b/specs/spec10/10-tui.md index a813841..340d411 100644 --- a/specs/spec10/10-tui.md +++ b/specs/spec10/10-tui.md @@ -348,11 +348,14 @@ clipped to end above the card (`waveRowsFor`), and either raster is deleted only when the card's own rows would collide with it (`stagePlan`, `cardTopRow` in `dock.ts` replaying the renderer's width/chrome math), because kitty images composite ABOVE text cells) and the head ask renders as -a centered rounded card (refs' ~55% content width) on the `CARD` ground in -the dock's slot — and the answer field moves INTO the card (user decision, -2026-08-11): the same single input, permanent focus intact, renders as the -card's last row while a question is open, and the bottom row keeps only its -quiet rule. The card is where you read AND where you answer. +a centered rounded card (refs' ~55% content width) on the `CARD` ground, +**floating over the room** (amended 2026-08-12, with §3.3's overlay +amendment): the card is absolutely positioned a gap row above the bottom +rule and takes no rows from the layout, so nothing behind it moves — the +room dims, it never rearranges — and the answer field moves INTO the card +(user decision, 2026-08-11): the same single input, permanent focus intact, +renders as the card's last row while a question is open, and the bottom row +keeps only its quiet rule. The card is where you read AND where you answer. Kind picks the frame: warm/ember for a question (with a client-side `#n` counter in the title), periwinkle for a consent (` · optional` in the title) — the listener's color, because the decision is theirs; a consent card @@ -438,9 +441,16 @@ now-playing stays in the status strip. Same four regions either way; only the composition moves, which is exactly the §6.1 licence. **Overlays and the band**: the settings pane reclaims the band's rows outright (a mode the listener opened is their own full attention); a spotlight card keeps the sky -on stage dimmed per §3.2-B, and the band steps off only when the card's top -row (the renderer's own math, `cardTopRow`) would climb into the scene — the -short-terminal case where both cannot fit. **The starfield is +on stage dimmed per §3.2-B ~~, and the band steps off only when the card's +top row (the renderer's own math, `cardTopRow`) would climb into the scene — +the short-terminal case where both cannot fit~~ — **amended 2026-08-12 (user +report: the card is a mask, not a sibling — a checklist card tall enough to +trip the yield collapsed the whole scene and threw the wordmark to the top +of the frame): the card floats (yoga absolute, above the text layer), takes +no rows from the composition, and the band never steps off while the pane is +closed; only the raster layers yield, per-rectangle, where the card's own +rows reach (`stagePlan` / `waveRowsFor` against `cardTopRow`), because kitty +images composite above text cells and would otherwise cover the card. **The starfield is retired** (2026-08-12 design session): at character resolution the scatter read as noise, so the night behind the wave and the figure stays empty and the scene's texture budget goes to the wave alone. diff --git a/tui/src/app.tsx b/tui/src/app.tsx index 174d330..ce32372 100644 --- a/tui/src/app.tsx +++ b/tui/src/app.tsx @@ -410,12 +410,11 @@ export function App({ subscribe, wire }: { subscribe: Subscribe; wire: Wire }): const { scene: sceneRows } = sceneSplit(Math.max(dims.height - 5, 10)) const sceneWidth = cols - 2 // Whether the scene band holds the stage. The settings pane always reclaims - // its rows (a mode the listener opened is their own full attention); a - // spotlight card keeps the sky dimmed beside it (§3.2-B: a sky going dark - // under every consent reads as broken) unless the terminal is too short to - // host both — the card's own top row, from the renderer's math, is the - // judge. The paint loops read this through a ref, like the card row. - const sceneShown = wide && !paneOpen && (cardTop === null || cardTop > 3 + sceneRows) + // its rows (a mode the listener opened is their own full attention). The + // spotlight card takes none: it floats over the room (§3.2-B), so the sky + // stays on stage dimmed beneath it — only the raster layers yield, and only + // where the card's own rows reach (stagePlan / waveRowsFor via the refs). + const sceneShown = wide && !paneOpen const sceneShownRef = useRef(sceneShown) sceneShownRef.current = sceneShown // The newest broadcast line carries the bullet (concept 04); older lines @@ -761,8 +760,11 @@ export function App({ subscribe, wire }: { subscribe: Subscribe; wire: Wire }): {/* The spotlight card (§3.2-B as built): the oldest pending ask grows into a centered rounded card while the room around it is hushed. - Kind picks the frame: warm for a question, periwinkle for a consent — - the listener's color, because the decision is theirs. */} + It FLOATS — absolutely positioned over the log, one gap row above + the bottom rule, taking no rows from the layout: the room behind it + dims but never rearranges. Kind picks the frame: warm for a + question, periwinkle for a consent — the listener's color, because + the decision is theirs. */} {asks.length > 0 && (() => { const head = asks[0]! @@ -786,13 +788,17 @@ export function App({ subscribe, wire }: { subscribe: Subscribe; wire: Wire }): borderColor: frame, titleColor: consent ? PERIWINKLE : EMBER, flexDirection: 'column', - alignSelf: 'center', + position: 'absolute', + // Yoga anchors absolute insets to the parent's border box, so + // the ultrawide gutter must be added back to stay centered. + left: gutter + Math.floor((cols - width) / 2), + bottom: 2, + zIndex: 100, width, paddingLeft: 2, paddingRight: 2, paddingTop: 1, paddingBottom: 1, - marginBottom: 1, backgroundColor: CARD, }} > diff --git a/tui/src/dock.ts b/tui/src/dock.ts index bcd795f..2480207 100644 --- a/tui/src/dock.ts +++ b/tui/src/dock.ts @@ -68,12 +68,13 @@ export function cardRows(text: string, cols: number): number { rows += 2 // the action row (its top margin + the line) rows += 2 // the in-card answer field (its top margin + the input) rows += 4 // border (2) + vertical padding (2) - rows += 1 // the card's bottom margin + rows += 1 // the gap row between the floating card and the bottom rule return rows } -// The first terminal row the card can touch: it stands on the window's bottom -// row (the quiet rule that keeps the frame closed). Rasters end above this. +// The first terminal row the card can touch: the card floats anchored to the +// window's bottom rule (the quiet line that keeps the frame closed), so its +// top is the window height minus its own rows. Rasters end above this. export function cardTopRow(text: string, cols: number, height: number): number { return Math.max(1, height - 1 - cardRows(text, cols)) }