From 2d0f4b2958394cb4360951045b5111a3f490fa72 Mon Sep 17 00:00:00 2001 From: "claude@clouddev1" Date: Thu, 11 Jun 2026 08:40:07 +0000 Subject: [PATCH] feat(ui): flat filled rectangles for demo overlays (#22, ADR-0047 D4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Render the keystroke badge and step caption as a solid yellow rectangle with no border glyphs and a one-cell text margin, instead of a rounded-border box — deliberately unlike the app's bordered panels so the demo overlays read as a distinct, eye-catching callout. Shared fill_overlay_rect helper (borderless Block fill + inset Paragraph). Snapshots regenerated; ADR-0047 D4 wording updated. --- docs/adr/0047-demonstration-overlay-layer.md | 41 +++++++----- ..._demo_badge_and_caption_stacked_90x26.snap | 12 ++-- ...__tests__demo_badge_enter_light_90x26.snap | 6 +- ..._ui__tests__demo_badge_tab_dark_90x26.snap | 6 +- ...d__ui__tests__demo_caption_dark_90x26.snap | 6 +- ...ui__tests__demo_caption_wrapped_90x26.snap | 10 +-- src/ui.rs | 66 +++++++++---------- 7 files changed, 76 insertions(+), 71 deletions(-) diff --git a/docs/adr/0047-demonstration-overlay-layer.md b/docs/adr/0047-demonstration-overlay-layer.md index 754bb03..3d7eba9 100644 --- a/docs/adr/0047-demonstration-overlay-layer.md +++ b/docs/adr/0047-demonstration-overlay-layer.md @@ -186,12 +186,21 @@ confirmed by test if live `--demo` on Windows is exercised.)* ### D4 — Both overlays are floating boxes at the output panel's inner bottom-right -The badge and the caption both render as **floating, bordered boxes -anchored to the inside of the output panel's bottom-right corner** -(inset one cell from the panel's inner edge), drawn **last over the base -render** — after modals, so they remain visible while the load-picker -(the `#24` cast) or any modal is up, and with **no layout reflow** -(consistent with the modal / nav-overlay precedent; honours R8). +The badge and the caption both render as **floating, flat filled +rectangles anchored to the inside of the output panel's bottom-right +corner** (inset one cell from the panel's inner edge), drawn **last over +the base render** — after modals, so they remain visible while the +load-picker (the `#24` cast) or any modal is up, and with **no layout +reflow** (consistent with the modal / nav-overlay precedent; honours +R8). + +**Flat rectangle, not a bordered box (user decision, post-build).** The +overlays draw as a **solid yellow rectangle with no border glyphs** and +a one-cell margin around the text — deliberately *unlike* the app's +rounded-border panels, so they read as a distinct callout that "stands +out nicely" rather than as another panel. Implemented with a borderless +`Block` fill (the `paint_background` mechanism) plus a `Paragraph` inset +into a one-cell `Margin`. The top-level `render()` does not currently know the output-panel rect (it is computed inside `render_right_column`), so a **new field @@ -204,19 +213,19 @@ When **both** are present, the **keystroke badge stacks directly above the caption box** (both right-aligned in the corner) so they never overlap. -**Styling — deliberately high-contrast:** **black text on a yellow -background**, bold, bordered — hard to overlook, identical in light and -dark themes (a fixed high-contrast pair centralised in `theme.rs`, not -theme-derived). +**Styling — deliberately high-contrast:** **bold black text on a yellow +fill** — hard to overlook, identical in light and dark themes (a fixed +high-contrast pair centralised in `theme.rs`, not theme-derived). **Caption sizing (user-confirmed).** The caption is **word-wrapped to at most 3 lines** within a content width of `min(40, output_inner_width − -6)` columns, ellipsised beyond the third line. So the caption box is -**3–5 rows** tall (1–3 text rows + 2 border), its height varying with -the text — a full sentence fits without forcing the author to split it, -while the 3-line cap keeps it corner-sized. The **badge** box is always -a single short token (`[TAB]` … `[SHIFT-TAB]`), so it is a fixed **3 -rows** (1 text + 2 border), narrow. +4)` columns, ellipsised beyond the third line. So the caption rectangle +is **3–5 rows** tall (1–3 text rows + a one-cell margin top and bottom), +its height varying with the text — a full sentence fits without forcing +the author to split it, while the 3-line cap keeps it corner-sized. The +**badge** rectangle is always a single short token (`[TAB]` … +`[SHIFT-TAB]`), so it is a fixed **3 rows** (1 text row + the margin), +narrow. **Clamping (runda finding).** Stacked, the two boxes are up to 8 rows (5 caption + 3 badge); the output panel's inner height is only `Min(5)`, diff --git a/src/snapshots/rdbms_playground__ui__tests__demo_badge_and_caption_stacked_90x26.snap b/src/snapshots/rdbms_playground__ui__tests__demo_badge_and_caption_stacked_90x26.snap index f1de558..7f82289 100644 --- a/src/snapshots/rdbms_playground__ui__tests__demo_badge_and_caption_stacked_90x26.snap +++ b/src/snapshots/rdbms_playground__ui__tests__demo_badge_and_caption_stacked_90x26.snap @@ -11,12 +11,12 @@ expression: snapshot │ │ │ │ │ │ -│ ╭───────╮ │ -│ │ [TAB] │ │ -│ ╰───────╯ │ -│ ╭─────────────────────╮ │ -│ │ Completing the name │ │ -│ ╰─────────────────────╯ │ +│ │ +│ [TAB] │ +│ │ +│ │ +│ Completing the name │ +│ │ │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭ SIMPLE ────────────────────────────────────────────────────────────────────────────────╮ diff --git a/src/snapshots/rdbms_playground__ui__tests__demo_badge_enter_light_90x26.snap b/src/snapshots/rdbms_playground__ui__tests__demo_badge_enter_light_90x26.snap index 290d0cb..7120bbd 100644 --- a/src/snapshots/rdbms_playground__ui__tests__demo_badge_enter_light_90x26.snap +++ b/src/snapshots/rdbms_playground__ui__tests__demo_badge_enter_light_90x26.snap @@ -14,9 +14,9 @@ expression: snapshot │ │ │ │ │ │ -│ ╭─────────╮ │ -│ │ [ENTER] │ │ -│ ╰─────────╯ │ +│ │ +│ [ENTER] │ +│ │ │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭ SIMPLE ────────────────────────────────────────────────────────────────────────────────╮ diff --git a/src/snapshots/rdbms_playground__ui__tests__demo_badge_tab_dark_90x26.snap b/src/snapshots/rdbms_playground__ui__tests__demo_badge_tab_dark_90x26.snap index ce45304..d6358c1 100644 --- a/src/snapshots/rdbms_playground__ui__tests__demo_badge_tab_dark_90x26.snap +++ b/src/snapshots/rdbms_playground__ui__tests__demo_badge_tab_dark_90x26.snap @@ -14,9 +14,9 @@ expression: snapshot │ │ │ │ │ │ -│ ╭───────╮ │ -│ │ [TAB] │ │ -│ ╰───────╯ │ +│ │ +│ [TAB] │ +│ │ │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭ SIMPLE ────────────────────────────────────────────────────────────────────────────────╮ diff --git a/src/snapshots/rdbms_playground__ui__tests__demo_caption_dark_90x26.snap b/src/snapshots/rdbms_playground__ui__tests__demo_caption_dark_90x26.snap index 1907566..b132bbd 100644 --- a/src/snapshots/rdbms_playground__ui__tests__demo_caption_dark_90x26.snap +++ b/src/snapshots/rdbms_playground__ui__tests__demo_caption_dark_90x26.snap @@ -14,9 +14,9 @@ expression: snapshot │ │ │ │ │ │ -│ ╭──────────────────────────────────────────╮ │ -│ │ Now press Tab to complete the table name │ │ -│ ╰──────────────────────────────────────────╯ │ +│ │ +│ Now press Tab to complete the table name │ +│ │ │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭ SIMPLE ────────────────────────────────────────────────────────────────────────────────╮ diff --git a/src/snapshots/rdbms_playground__ui__tests__demo_caption_wrapped_90x26.snap b/src/snapshots/rdbms_playground__ui__tests__demo_caption_wrapped_90x26.snap index 7863da9..9d2184d 100644 --- a/src/snapshots/rdbms_playground__ui__tests__demo_caption_wrapped_90x26.snap +++ b/src/snapshots/rdbms_playground__ui__tests__demo_caption_wrapped_90x26.snap @@ -12,11 +12,11 @@ expression: snapshot │ │ │ │ │ │ -│ ╭──────────────────────────────────────────╮ │ -│ │ This is a deliberately long step caption │ │ -│ │ that must wrap onto several lines and │ │ -│ │ then be clipped to three with an… │ │ -│ ╰──────────────────────────────────────────╯ │ +│ │ +│ This is a deliberately long step caption │ +│ that must wrap onto several lines and │ +│ then be clipped to three with an… │ +│ │ │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭ SIMPLE ────────────────────────────────────────────────────────────────────────────────╮ diff --git a/src/ui.rs b/src/ui.rs index f2596a0..e4df268 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -152,15 +152,31 @@ fn render_demo_overlays(app: &App, frame: &mut Frame<'_>) { } } +/// Paint a flat filled overlay rectangle — a solid yellow block with no +/// border glyphs (ADR-0047 D4) — and lay `body` inside a one-cell +/// margin. The borderless solid block is deliberately *unlike* the app's +/// bordered panels, so the demo overlays read as a distinct callout. +fn fill_overlay_rect(rect: Rect, body: String, frame: &mut Frame<'_>) { + frame.render_widget(ratatui::widgets::Clear, rect); + // `Block` with no borders fills the whole rect with the overlay + // background (same mechanism as `paint_background`). + frame.render_widget(Block::default().style(demo_overlay_style()), rect); + let inner = rect.inner(Margin { + horizontal: 1, + vertical: 1, + }); + frame.render_widget(Paragraph::new(body).style(demo_overlay_style()), inner); +} + /// A small high-contrast keystroke badge (`[TAB]`, `[ENTER]`, …) inset -/// one cell from the bottom-right of `area` (ADR-0047 D2/D4). When a -/// caption box is present (`above`), the badge sits directly on top of -/// it, right-aligned; otherwise it takes the bottom-right corner. -/// Skipped rather than overflowing if it cannot fit. +/// one cell from the bottom-right of `area` (ADR-0047 D2/D4) — the label +/// on a flat yellow rectangle with a one-cell margin. When a caption box +/// is present (`above`), the badge sits directly on top of it, right +/// edges aligned; otherwise it takes the bottom-right corner. Skipped +/// rather than overflowing if it cannot fit. fn render_badge_box(label: &str, area: Rect, above: Option, frame: &mut Frame<'_>) { - // ` [LABEL] ` (one pad each side) inside a rounded border. - let box_w = label.chars().count() as u16 + 4; - let box_h = 3; + let box_w = label.chars().count() as u16 + 2; // one-cell margin each side + let box_h = 3; // text row + a margin row above and below if box_w + 1 > area.width { return; } @@ -180,34 +196,25 @@ fn render_badge_box(label: &str, area: Rect, above: Option, frame: &mut Fr area.y + area.height - box_h - 1 } }; - let rect = Rect { x, y, width: box_w, height: box_h }; - let block = Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .style(demo_overlay_style()); - let para = Paragraph::new(format!(" {label} ")) - .style(demo_overlay_style()) - .block(block); - frame.render_widget(ratatui::widgets::Clear, rect); - frame.render_widget(para, rect); + fill_overlay_rect(Rect { x, y, width: box_w, height: box_h }, label.to_string(), frame); } /// A step-caption box inset one cell from the bottom-right of `area` /// (ADR-0047 D3/D4): the text word-wrapped to at most 3 lines within a -/// corner-sized width, bold black on yellow. Returns the rect it drew, -/// or `None` if it was too small to place (so the badge can fall back to -/// the bottom-right corner). +/// corner-sized width, bold black on a flat yellow rectangle. Returns +/// the rect it drew, or `None` if it was too small to place (so the +/// badge can fall back to the bottom-right corner). fn render_caption_box(text: &str, area: Rect, frame: &mut Frame<'_>) -> Option { // Content width capped so the box stays corner-sized; the caption // wraps to ≤ 3 lines and ellipsises beyond (D4). - let content_w = 40.min(area.width.saturating_sub(6)) as usize; + let content_w = 40.min(area.width.saturating_sub(4)) as usize; if content_w < 4 { return None; // output too narrow for a useful caption } let lines = clamp_wrapped(text, content_w, 3); let inner_w = lines.iter().map(|l| l.chars().count()).max().unwrap_or(0); - let box_w = inner_w as u16 + 4; // 2 border + 1 pad each side - let box_h = lines.len() as u16 + 2; // 2 border + let box_w = inner_w as u16 + 2; // one-cell margin each side + let box_h = lines.len() as u16 + 2; // text rows + a margin row above and below if box_w + 1 > area.width || box_h + 1 > area.height { return None; } @@ -217,18 +224,7 @@ fn render_caption_box(text: &str, area: Rect, frame: &mut Frame<'_>) -> Option>() - .join("\n"); - let block = Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .style(demo_overlay_style()); - let para = Paragraph::new(body).style(demo_overlay_style()).block(block); - frame.render_widget(ratatui::widgets::Clear, rect); - frame.render_widget(para, rect); + fill_overlay_rect(rect, lines.join("\n"), frame); Some(rect) }