From 5ada19353ff3afa5d6b2093899e713ad04bd885b Mon Sep 17 00:00:00 2001 From: hellno Date: Tue, 9 Jun 2026 20:53:45 +0200 Subject: [PATCH 01/11] =?UTF-8?q?fix(app):=20stretch=20content=20pane=20to?= =?UTF-8?q?=20full=20height=20=E2=80=94=20fixes=20layout=20collapse/overla?= =?UTF-8?q?p?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two-pane row is an h_flex (children centered vertically). The sidebar opts into full height with .h_full(), but the content column didn't — so any view shorter than the viewport collapsed to its intrinsic height and floated mid-pane, bunching the breadcrumb, body, and bottom status strip into an overlapping stack. Give the content column .h_full()/.min_h_0() to match the sidebar. --- crates/deckard-app/src/shell.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/deckard-app/src/shell.rs b/crates/deckard-app/src/shell.rs index 1540c5a..4d4323b 100644 --- a/crates/deckard-app/src/shell.rs +++ b/crates/deckard-app/src/shell.rs @@ -1444,9 +1444,16 @@ impl Render for Shell { .child(title_bar) .child( h_flex().size_full().child(self.render_sidebar(cx)).child( + // Fill the full pane height (like the sidebar's `.h_full()`): `h_flex` + // centers its children vertically, so without this the content column + // collapses to its intrinsic height and floats mid-pane — the + // breadcrumb, content, and bottom status strip then bunch up and overlap + // whenever a view is shorter than the viewport. v_flex() .flex_1() + .h_full() .min_w_0() + .min_h_0() .child(self.render_breadcrumb(cx)) .child(div().flex_1().min_h_0().child(content)) .child(self.render_status_strip(cx)), From edcbaf678fc790f3c078e2e8d44a08834e074d63 Mon Sep 17 00:00:00 2001 From: hellno Date: Tue, 9 Jun 2026 20:53:45 +0200 Subject: [PATCH 02/11] fix(app): wrap text in side-by-side rows so it stops clipping Text columns sitting next to a control/glyph had no width bound, so long copy laid out on one line and ran off the right edge: Settings toggles + RPC input pushed off-card, and the Shield/Receive descriptions clipped. Bound each text column with .flex_1()/.min_w_0() so it wraps within the available width. --- crates/deckard-app/src/receive.rs | 4 ++++ crates/deckard-app/src/settings_view.rs | 4 ++++ crates/deckard-app/src/shield_view.rs | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/crates/deckard-app/src/receive.rs b/crates/deckard-app/src/receive.rs index 4074aba..f80b1c6 100644 --- a/crates/deckard-app/src/receive.rs +++ b/crates/deckard-app/src/receive.rs @@ -105,7 +105,11 @@ impl Shell { .flex_shrink_0(), ) .child( + // Bound + flex the text so the warning wraps inside the card + // instead of being clipped at the right edge. div() + .flex_1() + .min_w_0() .text_xs() .text_color(fg) .child("Only send Ethereum-network assets to this address. Funds sent on the wrong network may be lost."), diff --git a/crates/deckard-app/src/settings_view.rs b/crates/deckard-app/src/settings_view.rs index 4f87c5f..2d57663 100644 --- a/crates/deckard-app/src/settings_view.rs +++ b/crates/deckard-app/src/settings_view.rs @@ -37,7 +37,11 @@ impl Shell { .items_center() .justify_between() .child( + // Bound the text column so a long description wraps instead of widening + // the row past the card and shoving the control off the right edge. v_flex() + .flex_1() + .min_w_0() .gap_0p5() .child( div() diff --git a/crates/deckard-app/src/shield_view.rs b/crates/deckard-app/src/shield_view.rs index 0440fbb..1862798 100644 --- a/crates/deckard-app/src/shield_view.rs +++ b/crates/deckard-app/src/shield_view.rs @@ -443,7 +443,11 @@ impl Shell { .flex_shrink_0(), ) .child( + // Bound the text column so the title/subtitle wrap within the card rather than + // running off the right edge next to the fixed-width glyph. v_flex() + .flex_1() + .min_w_0() .gap_1() .child( div() From d02fbf4ee728ede2c8a03d160f933be513b04e94 Mon Sep 17 00:00:00 2001 From: hellno Date: Wed, 10 Jun 2026 12:43:38 +0200 Subject: [PATCH 03/11] fix(app): scroll the content pane so tall views don't underlap the status strip The content slot clipped/overflowed instead of scrolling (a TODO had disabled it), so a view taller than the pane (Settings, a funded Wallet) slid under the bottom status strip. Wrap the slot in gpui-component's Scrollable (overflow_y_scrollbar, which owns its own scroll handle) so it clips to the pane and scrolls. --- crates/deckard-app/src/shell.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/crates/deckard-app/src/shell.rs b/crates/deckard-app/src/shell.rs index 4d4323b..19a4e98 100644 --- a/crates/deckard-app/src/shell.rs +++ b/crates/deckard-app/src/shell.rs @@ -13,6 +13,7 @@ use gpui::{ use gpui_component::{ h_flex, input::{InputEvent, InputState}, + scroll::ScrollableElement, v_flex, ActiveTheme, TitleBar, }; @@ -1455,7 +1456,19 @@ impl Render for Shell { .min_w_0() .min_h_0() .child(self.render_breadcrumb(cx)) - .child(div().flex_1().min_h_0().child(content)) + // Scrollable content slot: a bounded flex item wrapping a + // gpui-component Scrollable (which owns its own scroll handle), so a + // view taller than the pane scrolls instead of underlapping the + // status strip below it. + .child( + div().flex_1().min_h_0().child( + div() + .id("content-scroll") + .size_full() + .overflow_y_scrollbar() + .child(content), + ), + ) .child(self.render_status_strip(cx)), ), ) From 06a5a6232cbbdc46cf17fd46df9649b51c91c852 Mon Sep 17 00:00:00 2001 From: hellno Date: Wed, 10 Jun 2026 12:43:38 +0200 Subject: [PATCH 04/11] =?UTF-8?q?fix(app):=20correct=20stale=20Shield=20re?= =?UTF-8?q?cipient=20copy=20=E2=80=94=20it=20auto-fills=20now?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The compose note still claimed the 0zk address 'auto-fills in a later release', but Wave-2 shipped the auto-fill and the field is pre-filled. Show honest, state-aware copy keyed off whether the recipient field has content. --- crates/deckard-app/src/shield_view.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/deckard-app/src/shield_view.rs b/crates/deckard-app/src/shield_view.rs index 1862798..2cf5058 100644 --- a/crates/deckard-app/src/shield_view.rs +++ b/crates/deckard-app/src/shield_view.rs @@ -117,10 +117,13 @@ impl Shell { ), ) .child( - div() - .text_xs() - .text_color(muted) - .child("Your own 0zk address auto-fills in a later release."), + div().text_xs().text_color(muted).child( + if recipient_raw.trim().is_empty() { + "Enter the 0zk address that will receive the private balance." + } else { + "Pre-filled with your own 0zk address — edit it to shield to a different recipient." + }, + ), ) .into_any_element(), ) From f15f0ed636771d04e37fba58727d59693ac13b3f Mon Sep 17 00:00:00 2001 From: hellno Date: Wed, 10 Jun 2026 12:43:38 +0200 Subject: [PATCH 05/11] fix(app): drop redundant 'Personal > Personal' breadcrumb on Project home Project Home's view label is itself 'Personal', so the breadcrumb repeated it. Skip the trailing '> ' segment when it would just repeat the project name. --- crates/deckard-app/src/shell_chrome.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/deckard-app/src/shell_chrome.rs b/crates/deckard-app/src/shell_chrome.rs index 1348aac..460c33a 100644 --- a/crates/deckard-app/src/shell_chrome.rs +++ b/crates/deckard-app/src/shell_chrome.rs @@ -298,8 +298,15 @@ impl Shell { .gap_2() .child(div().size(px(16.0)).rounded(px(4.0)).bg(id_square)) .child(div().text_sm().text_color(fg).child("Personal")) - .child(div().text_sm().text_color(muted).child("›")) - .child(div().text_sm().text_color(fg).child(self.view_label())), + // Skip the trailing "› " when it would just repeat the project name + // (Project Home's label is "Personal" → avoid "Personal › Personal"). + .when( + !(self.surface == Surface::Home && self.selection == Selection::Project), + |el| { + el.child(div().text_sm().text_color(muted).child("›")) + .child(div().text_sm().text_color(fg).child(self.view_label())) + }, + ), ) .child( h_flex() From d9a9882d7412a4abc91417dacaf497c639dd7587 Mon Sep 17 00:00:00 2001 From: hellno Date: Wed, 10 Jun 2026 13:18:06 +0200 Subject: [PATCH 06/11] fix(app): scope scrolling per-surface + keep cards centered (codex P2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shared scroll wrapper on the content slot caused two regressions codex flagged: (1) gpui-component's Scrollable keys its offset by call site, so one wrapper shared a single offset across every surface — scrolling a long page then navigating opened the next one pre-scrolled with its header hidden; (2) the centered Receive/Shield cards lost their full-height parent and rendered top-aligned. Move scrolling into each surface's own match arm (distinct call sites -> independent offsets) and leave Receive/Shield unwrapped so they stay centered against the full-height slot. Drops the now-stale TODO(scroll) notes. --- crates/deckard-app/src/shell.rs | 55 ++-- crates/deckard-app/src/welcome.rs | 470 ++++++++++++++---------------- 2 files changed, 259 insertions(+), 266 deletions(-) diff --git a/crates/deckard-app/src/shell.rs b/crates/deckard-app/src/shell.rs index 19a4e98..1dac25e 100644 --- a/crates/deckard-app/src/shell.rs +++ b/crates/deckard-app/src/shell.rs @@ -1428,17 +1428,39 @@ impl Render for Shell { // (sidebar | [breadcrumb / content / status strip]) + command palette. self.prepare_shield_inputs(window, cx); let title_bar = self.render_title_bar(cx); + // Scrollable content surfaces wrap their view in a Scrollable. Each arm inlines its + // OWN `.overflow_y_scrollbar()` (not a shared helper) on purpose: gpui-component keys + // the scroll offset by call site, so a per-arm call gives each surface an independent + // offset — otherwise scrolling one long page would leave the next one opened + // pre-scrolled with its header hidden. Receive and Shield are short, centered + // single-action cards, so they get NO scroll wrapper and stay vertically centered. let content = match (self.selection, self.surface) { - (_, Surface::Settings) => self.render_settings(window, cx).into_any_element(), + (_, Surface::Settings) => div() + .id("scroll-settings") + .size_full() + .overflow_y_scrollbar() + .child(self.render_settings(window, cx)) + .into_any_element(), (_, Surface::Receive) => self.render_receive(cx).into_any_element(), (_, Surface::Shield) => self.render_shield(cx).into_any_element(), - (Selection::Wallet, Surface::Home) => { - self.render_wallet_home(cx).into_any_element() - } - (Selection::Project, Surface::Home) => { - self.render_project_home(cx).into_any_element() - } - (Selection::Agent, Surface::Home) => self.render_agent_home(cx).into_any_element(), + (Selection::Wallet, Surface::Home) => div() + .id("scroll-wallet") + .size_full() + .overflow_y_scrollbar() + .child(self.render_wallet_home(cx)) + .into_any_element(), + (Selection::Project, Surface::Home) => div() + .id("scroll-project") + .size_full() + .overflow_y_scrollbar() + .child(self.render_project_home(cx)) + .into_any_element(), + (Selection::Agent, Surface::Home) => div() + .id("scroll-agent") + .size_full() + .overflow_y_scrollbar() + .child(self.render_agent_home(cx)) + .into_any_element(), }; v_flex() .size_full() @@ -1456,19 +1478,10 @@ impl Render for Shell { .min_w_0() .min_h_0() .child(self.render_breadcrumb(cx)) - // Scrollable content slot: a bounded flex item wrapping a - // gpui-component Scrollable (which owns its own scroll handle), so a - // view taller than the pane scrolls instead of underlapping the - // status strip below it. - .child( - div().flex_1().min_h_0().child( - div() - .id("content-scroll") - .size_full() - .overflow_y_scrollbar() - .child(content), - ), - ) + // The content slot just fills the space between the breadcrumb and the + // status strip; per-surface scrolling is applied where `content` is + // built (the match above), so each surface owns its own scroll offset. + .child(div().flex_1().min_h_0().child(content)) .child(self.render_status_strip(cx)), ), ) diff --git a/crates/deckard-app/src/welcome.rs b/crates/deckard-app/src/welcome.rs index b98103b..b4019ee 100644 --- a/crates/deckard-app/src/welcome.rs +++ b/crates/deckard-app/src/welcome.rs @@ -128,102 +128,94 @@ impl Shell { "Personal".to_string() }; - div() - .size_full() - .p_8() - // TODO(scroll): restore a scrollable main pane via a Stateful - // `div().id(..).overflow_y_scroll()` (the agent draft mis-ordered - // gpui-component's `overflow_y_scrollbar`). Content is short for now. - .child( - v_flex() - .items_start() - .max_w(px(680.0)) - .gap_6() - // Page header (DESIGN §Page header): identity square + wallet-name - // H1 (text.primary, weight 600 — NEVER cyan) + a muted mono, - // middle-truncated address subtitle. - .child( - h_flex() - .w_full() - .items_center() - .justify_between() - .child( - h_flex() - .items_center() - .gap_3() - .child(div().size(px(28.0)).rounded(px(6.0)).bg(id_square)) - .child( - v_flex() - .gap_0p5() - .child( - div() - .text_xl() - .font_weight(FontWeight::SEMIBOLD) - .text_color(fg) - .child(wallet_name), - ) - .child( - div() - .font_family(mono.clone()) - .text_xs() - .text_color(muted) - .child(account_pill), - ), - ), - ) - .child( - Button::new("refresh") - .ghost() - .icon(IconName::Replace) - .on_click( - cx.listener(|this, _, _, cx| this.refresh_portfolio(cx)), - ), - ), - ) - // Balance hero: the merged Total (public + private), a Private/Public - // allocation bar, and the composition lines (Wave 2 T10). - .child(self.render_shielded_hero(native_wei, cx)) - // Primary actions. Shield (the privacy hero) is the one live, primary - // CTA; Send + Swap are gated to the next release (Chunk 4, testnet-first) - // and shown disabled rather than inert-but-active. - .child( - h_flex() - .w_full() - .gap_2() - // Shield signs from YOUR wallet, so it's disabled while viewing a - // watched read-only account (don't show a funds-moving action in a - // someone-else's-address context). - .child( - Button::new("shield") - .primary() - .label("Shield") - .disabled(self.viewing_watch) - .on_click(cx.listener(|this, _, _, cx| this.open_shield(cx))), - ) - .child(Button::new("receive").ghost().label("Receive").on_click( - cx.listener(|this, _, _, cx| this.open(Surface::Receive, cx)), - )) - .child(Button::new("send").ghost().label("Send").disabled(true)) - .child(Button::new("swap").ghost().label("Swap").disabled(true)), - ) - .child( - div() - .text_xs() - .text_color(muted) - .child("Send & Swap arrive in the next release."), - ) - // Holdings, or a state. - .child(self.render_holdings(first_sync, has_tokens, holdings, cx)) - // Keyboard hints — the Superhuman/Linear signal. - .child( - h_flex() - .gap_4() - .pt_1() - .child(chip(format!("{MOD}K"), "Command palette".into())) - .child(chip(format!("{MOD}["), "Back".into())) - .child(chip(format!("{MOD},"), "Settings".into())), - ), - ) + div().size_full().p_8().child( + v_flex() + .items_start() + .max_w(px(680.0)) + .gap_6() + // Page header (DESIGN §Page header): identity square + wallet-name + // H1 (text.primary, weight 600 — NEVER cyan) + a muted mono, + // middle-truncated address subtitle. + .child( + h_flex() + .w_full() + .items_center() + .justify_between() + .child( + h_flex() + .items_center() + .gap_3() + .child(div().size(px(28.0)).rounded(px(6.0)).bg(id_square)) + .child( + v_flex() + .gap_0p5() + .child( + div() + .text_xl() + .font_weight(FontWeight::SEMIBOLD) + .text_color(fg) + .child(wallet_name), + ) + .child( + div() + .font_family(mono.clone()) + .text_xs() + .text_color(muted) + .child(account_pill), + ), + ), + ) + .child( + Button::new("refresh") + .ghost() + .icon(IconName::Replace) + .on_click(cx.listener(|this, _, _, cx| this.refresh_portfolio(cx))), + ), + ) + // Balance hero: the merged Total (public + private), a Private/Public + // allocation bar, and the composition lines (Wave 2 T10). + .child(self.render_shielded_hero(native_wei, cx)) + // Primary actions. Shield (the privacy hero) is the one live, primary + // CTA; Send + Swap are gated to the next release (Chunk 4, testnet-first) + // and shown disabled rather than inert-but-active. + .child( + h_flex() + .w_full() + .gap_2() + // Shield signs from YOUR wallet, so it's disabled while viewing a + // watched read-only account (don't show a funds-moving action in a + // someone-else's-address context). + .child( + Button::new("shield") + .primary() + .label("Shield") + .disabled(self.viewing_watch) + .on_click(cx.listener(|this, _, _, cx| this.open_shield(cx))), + ) + .child(Button::new("receive").ghost().label("Receive").on_click( + cx.listener(|this, _, _, cx| this.open(Surface::Receive, cx)), + )) + .child(Button::new("send").ghost().label("Send").disabled(true)) + .child(Button::new("swap").ghost().label("Swap").disabled(true)), + ) + .child( + div() + .text_xs() + .text_color(muted) + .child("Send & Swap arrive in the next release."), + ) + // Holdings, or a state. + .child(self.render_holdings(first_sync, has_tokens, holdings, cx)) + // Keyboard hints — the Superhuman/Linear signal. + .child( + h_flex() + .gap_4() + .pt_1() + .child(chip(format!("{MOD}K"), "Command palette".into())) + .child(chip(format!("{MOD}["), "Back".into())) + .child(chip(format!("{MOD},"), "Settings".into())), + ), + ) } /// The merged Total hero (Wave 2 T10): `Total = public + private` when both are known, a @@ -468,79 +460,72 @@ impl Shell { let native_wei = self.portfolio.as_ref().map(|p| p.native_wei); - div() - .size_full() - .p_8() - // TODO(scroll): restore a scrollable main pane via a Stateful - // `div().id(..).overflow_y_scroll()` (the agent draft mis-ordered - // gpui-component's `overflow_y_scrollbar`). Content is short for now. - .child( - v_flex() - .items_start() - .max_w(px(680.0)) - .gap_6() - .child( - h_flex() - .items_center() - .gap_3() - .child(div().size(px(28.0)).rounded(px(6.0)).bg(id_square)) - .child( - div() - .text_xl() - .font_weight(FontWeight::SEMIBOLD) - .text_color(fg) - .child("Personal"), - ), - ) - .child( - v_flex() - .w_full() - .gap_3() - .child( - div() - .id("project-balance-hero") - .cursor_pointer() - .text_3xl() - .font_weight(FontWeight::SEMIBOLD) - .map(|el| match native_wei { - Some(wei) => el.child(money( - wei, - 18, - 4, - Some("ETH"), - masked, - mono.clone(), - fg, - muted, - )), - None => el - .font_family(mono.clone()) - .text_color(muted) - .child("—"), - }) - .on_click(cx.listener(|this, _, _, cx| this.toggle_mask(cx))), + div().size_full().p_8().child( + v_flex() + .items_start() + .max_w(px(680.0)) + .gap_6() + .child( + h_flex() + .items_center() + .gap_3() + .child(div().size(px(28.0)).rounded(px(6.0)).bg(id_square)) + .child( + div() + .text_xl() + .font_weight(FontWeight::SEMIBOLD) + .text_color(fg) + .child("Personal"), + ), + ) + .child( + v_flex() + .w_full() + .gap_3() + .child( + div() + .id("project-balance-hero") + .cursor_pointer() + .text_3xl() + .font_weight(FontWeight::SEMIBOLD) + .map(|el| match native_wei { + Some(wei) => el.child(money( + wei, + 18, + 4, + Some("ETH"), + masked, + mono.clone(), + fg, + muted, + )), + None => { + el.font_family(mono.clone()).text_color(muted).child("—") + } + }) + .on_click(cx.listener(|this, _, _, cx| this.toggle_mask(cx))), + ) + .children(native_wei.map(|_| { + allocation_bar( + vec![AllocSegment { + label: "Public".into(), + fraction: 1.0, + tone: id_square, + }], + masked, + border, + muted, + fg, ) - .children(native_wei.map(|_| { - allocation_bar( - vec![AllocSegment { - label: "Public".into(), - fraction: 1.0, - tone: id_square, - }], - masked, - border, - muted, - fg, - ) - })), - ) - .child( - div() - .text_sm() - .text_color(muted) - .child("1 wallet · 1 agent"), - ), - ) + })), + ) + .child( + div() + .text_sm() + .text_color(muted) + .child("1 wallet · 1 agent"), + ), + ) } /// Agent home — a static, demo-scoped policy-card placeholder (DESIGN §Policy @@ -578,84 +563,79 @@ impl Shell { ) }; - div() - .size_full() - .p_8() - // TODO(scroll): restore a scrollable main pane via a Stateful - // `div().id(..).overflow_y_scroll()` (the agent draft mis-ordered - // gpui-component's `overflow_y_scrollbar`). Content is short for now. - .child( - v_flex() - .items_start() - .max_w(px(680.0)) - .gap_6() - // Header: cyan squircle monogram (the ONLY cyan on the surface, - // breathing while Atlas acts) + agent name H1 (text.primary, NEVER cyan). - .child( - h_flex() - .items_center() - .gap_3() - .child(agent_squircle( - px(28.0), - px(6.0), - self.agent_acting, - agent, - agent_tint, - "agent-pulse-home", - )) - .child( - v_flex() - .gap_0p5() - .child( - div() - .text_xl() - .font_weight(FontWeight::SEMIBOLD) - .text_color(fg) - .child("Atlas"), - ) - .child(div().text_xs().text_color(muted).child( - if self.agent_acting { - "Delegated agent · acting now" - } else { - "Delegated agent · idle" - }, - )), - ), - ) - // Policy card: one faint frame, no interior grid lines. - .child( - v_flex() - .w_full() - .gap_0() - .p_4() - .rounded_lg() - .border_1() - .border_color(border) - .bg(surface) - .child(policy_row("Per-transaction cap", "0.10 ETH")) - .child(policy_row("Period budget", "1.00 ETH / week")) - .child(policy_row("Allowed assets", "ETH")) - .child(policy_row("Session key", "expires in 6d")) - .child(policy_row("Autonomy", "act < $50 · ask above")), - ) - // Demo control: narrate Atlas "acting" to show the one ambient motion - // (the breathing squircle). Real activity arrives with the MCP agent. - .child( - Button::new("toggle-agent-acting") - .ghost() - .label(if self.agent_acting { - "Stop activity (demo)" - } else { - "Simulate activity (demo)" - }) - .on_click(cx.listener(|this, _, _, cx| this.toggle_agent_acting(cx))), - ) - .child( - div().text_xs().text_color(muted).child( - "Atlas is a manual stand-in for the demo. Controls land with MCP.", + div().size_full().p_8().child( + v_flex() + .items_start() + .max_w(px(680.0)) + .gap_6() + // Header: cyan squircle monogram (the ONLY cyan on the surface, + // breathing while Atlas acts) + agent name H1 (text.primary, NEVER cyan). + .child( + h_flex() + .items_center() + .gap_3() + .child(agent_squircle( + px(28.0), + px(6.0), + self.agent_acting, + agent, + agent_tint, + "agent-pulse-home", + )) + .child( + v_flex() + .gap_0p5() + .child( + div() + .text_xl() + .font_weight(FontWeight::SEMIBOLD) + .text_color(fg) + .child("Atlas"), + ) + .child(div().text_xs().text_color(muted).child( + if self.agent_acting { + "Delegated agent · acting now" + } else { + "Delegated agent · idle" + }, + )), ), - ), - ) + ) + // Policy card: one faint frame, no interior grid lines. + .child( + v_flex() + .w_full() + .gap_0() + .p_4() + .rounded_lg() + .border_1() + .border_color(border) + .bg(surface) + .child(policy_row("Per-transaction cap", "0.10 ETH")) + .child(policy_row("Period budget", "1.00 ETH / week")) + .child(policy_row("Allowed assets", "ETH")) + .child(policy_row("Session key", "expires in 6d")) + .child(policy_row("Autonomy", "act < $50 · ask above")), + ) + // Demo control: narrate Atlas "acting" to show the one ambient motion + // (the breathing squircle). Real activity arrives with the MCP agent. + .child( + Button::new("toggle-agent-acting") + .ghost() + .label(if self.agent_acting { + "Stop activity (demo)" + } else { + "Simulate activity (demo)" + }) + .on_click(cx.listener(|this, _, _, cx| this.toggle_agent_acting(cx))), + ) + .child( + div() + .text_xs() + .text_color(muted) + .child("Atlas is a manual stand-in for the demo. Controls land with MCP."), + ), + ) } } From 7a39b350b9f387e84514ad6461c09d0aae68f90d Mon Sep 17 00:00:00 2001 From: hellno Date: Wed, 10 Jun 2026 13:18:07 +0200 Subject: [PATCH 07/11] fix(app): only call the Shield recipient 'your own' when it matches your 0zk (codex P2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The helper line said 'Pre-filled with your own 0zk address' for any non-empty recipient, including a user-typed/edited one — misrepresenting where the deposit goes. Key the copy off whether the recipient equals the wallet's auto-filled railgun address; a manual recipient now gets neutral 'double-check it' copy. --- crates/deckard-app/src/shield_view.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/crates/deckard-app/src/shield_view.rs b/crates/deckard-app/src/shield_view.rs index 2cf5058..9a9cf41 100644 --- a/crates/deckard-app/src/shield_view.rs +++ b/crates/deckard-app/src/shield_view.rs @@ -117,13 +117,21 @@ impl Shell { ), ) .child( - div().text_xs().text_color(muted).child( - if recipient_raw.trim().is_empty() { + // Only call the recipient "your own 0zk address" when it actually matches the + // wallet's auto-filled address — a user-typed/edited recipient gets neutral copy + // so the line never misrepresents where the deposit is going. + div().text_xs().text_color(muted).child({ + let recipient = recipient_raw.trim(); + let is_own_address = + self.railgun_address.as_deref().map(str::trim) == Some(recipient); + if recipient.is_empty() { "Enter the 0zk address that will receive the private balance." - } else { + } else if is_own_address { "Pre-filled with your own 0zk address — edit it to shield to a different recipient." - }, - ), + } else { + "Shielding to the 0zk address above — double-check it before you continue." + } + }), ) .into_any_element(), ) From 3603b9642bc64df36789c2bf7717b51714c16f2e Mon Sep 17 00:00:00 2001 From: hellno Date: Wed, 10 Jun 2026 13:31:10 +0200 Subject: [PATCH 08/11] fix(app): center Receive/Shield by making the content slot a real flex column MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gpui's Style::default() is display:block (verified style.rs), so the prior div().flex_1().min_h_0() content slot never let the centered cards' flex_1 take effect — Receive/Shield sized to their card and sat under the breadcrumb. Make the slot a v_flex (flex column) so flex_1 + justify_center actually fills + centers. Verified live: Receive now centers; scrolling surfaces (size_full children) unaffected. --- crates/deckard-app/src/shell.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/deckard-app/src/shell.rs b/crates/deckard-app/src/shell.rs index 1dac25e..1b6229b 100644 --- a/crates/deckard-app/src/shell.rs +++ b/crates/deckard-app/src/shell.rs @@ -1478,10 +1478,13 @@ impl Render for Shell { .min_w_0() .min_h_0() .child(self.render_breadcrumb(cx)) - // The content slot just fills the space between the breadcrumb and the - // status strip; per-surface scrolling is applied where `content` is - // built (the match above), so each surface owns its own scroll offset. - .child(div().flex_1().min_h_0().child(content)) + // The content slot is a full-height flex COLUMN (not a plain `div`, + // which gpui defaults to `display: block`): the centered single-action + // surfaces (Receive/Shield) use `flex_1` + `justify_center`, which only + // fills + centers inside a flex parent — a block slot would let them + // shrink to their card height and sit under the breadcrumb. Scrolling + // surfaces wrap themselves (the match above), so each owns its offset. + .child(v_flex().flex_1().min_h_0().child(content)) .child(self.render_status_strip(cx)), ), ) From d6efcf9b1680f27673713b51f8f4c685cd80ce0b Mon Sep 17 00:00:00 2001 From: hellno Date: Wed, 10 Jun 2026 13:36:02 +0200 Subject: [PATCH 09/11] style(app): keep welcome.rs formatting stable after the scroll rework Removing the stale TODO(scroll) comments let rustfmt collapse the home-view roots and re-indent ~460 lines. Restore the original layout with a one-line note (scrolling now lives in shell.rs's per-surface wrapper), shrinking the welcome.rs diff from ~470 lines of churn to the handful that actually changed. --- crates/deckard-app/src/welcome.rs | 467 ++++++++++++++++-------------- 1 file changed, 242 insertions(+), 225 deletions(-) diff --git a/crates/deckard-app/src/welcome.rs b/crates/deckard-app/src/welcome.rs index b4019ee..e63d3d2 100644 --- a/crates/deckard-app/src/welcome.rs +++ b/crates/deckard-app/src/welcome.rs @@ -128,94 +128,101 @@ impl Shell { "Personal".to_string() }; - div().size_full().p_8().child( - v_flex() - .items_start() - .max_w(px(680.0)) - .gap_6() - // Page header (DESIGN §Page header): identity square + wallet-name - // H1 (text.primary, weight 600 — NEVER cyan) + a muted mono, - // middle-truncated address subtitle. - .child( - h_flex() - .w_full() - .items_center() - .justify_between() - .child( - h_flex() - .items_center() - .gap_3() - .child(div().size(px(28.0)).rounded(px(6.0)).bg(id_square)) - .child( - v_flex() - .gap_0p5() - .child( - div() - .text_xl() - .font_weight(FontWeight::SEMIBOLD) - .text_color(fg) - .child(wallet_name), - ) - .child( - div() - .font_family(mono.clone()) - .text_xs() - .text_color(muted) - .child(account_pill), - ), - ), - ) - .child( - Button::new("refresh") - .ghost() - .icon(IconName::Replace) - .on_click(cx.listener(|this, _, _, cx| this.refresh_portfolio(cx))), - ), - ) - // Balance hero: the merged Total (public + private), a Private/Public - // allocation bar, and the composition lines (Wave 2 T10). - .child(self.render_shielded_hero(native_wei, cx)) - // Primary actions. Shield (the privacy hero) is the one live, primary - // CTA; Send + Swap are gated to the next release (Chunk 4, testnet-first) - // and shown disabled rather than inert-but-active. - .child( - h_flex() - .w_full() - .gap_2() - // Shield signs from YOUR wallet, so it's disabled while viewing a - // watched read-only account (don't show a funds-moving action in a - // someone-else's-address context). - .child( - Button::new("shield") - .primary() - .label("Shield") - .disabled(self.viewing_watch) - .on_click(cx.listener(|this, _, _, cx| this.open_shield(cx))), - ) - .child(Button::new("receive").ghost().label("Receive").on_click( - cx.listener(|this, _, _, cx| this.open(Surface::Receive, cx)), - )) - .child(Button::new("send").ghost().label("Send").disabled(true)) - .child(Button::new("swap").ghost().label("Swap").disabled(true)), - ) - .child( - div() - .text_xs() - .text_color(muted) - .child("Send & Swap arrive in the next release."), - ) - // Holdings, or a state. - .child(self.render_holdings(first_sync, has_tokens, holdings, cx)) - // Keyboard hints — the Superhuman/Linear signal. - .child( - h_flex() - .gap_4() - .pt_1() - .child(chip(format!("{MOD}K"), "Command palette".into())) - .child(chip(format!("{MOD}["), "Back".into())) - .child(chip(format!("{MOD},"), "Settings".into())), - ), - ) + div() + .size_full() + .p_8() + // Scrolling for this surface is handled by its per-surface wrapper in + // `shell.rs` (the content match), so the body here is plain content. + .child( + v_flex() + .items_start() + .max_w(px(680.0)) + .gap_6() + // Page header (DESIGN §Page header): identity square + wallet-name + // H1 (text.primary, weight 600 — NEVER cyan) + a muted mono, + // middle-truncated address subtitle. + .child( + h_flex() + .w_full() + .items_center() + .justify_between() + .child( + h_flex() + .items_center() + .gap_3() + .child(div().size(px(28.0)).rounded(px(6.0)).bg(id_square)) + .child( + v_flex() + .gap_0p5() + .child( + div() + .text_xl() + .font_weight(FontWeight::SEMIBOLD) + .text_color(fg) + .child(wallet_name), + ) + .child( + div() + .font_family(mono.clone()) + .text_xs() + .text_color(muted) + .child(account_pill), + ), + ), + ) + .child( + Button::new("refresh") + .ghost() + .icon(IconName::Replace) + .on_click( + cx.listener(|this, _, _, cx| this.refresh_portfolio(cx)), + ), + ), + ) + // Balance hero: the merged Total (public + private), a Private/Public + // allocation bar, and the composition lines (Wave 2 T10). + .child(self.render_shielded_hero(native_wei, cx)) + // Primary actions. Shield (the privacy hero) is the one live, primary + // CTA; Send + Swap are gated to the next release (Chunk 4, testnet-first) + // and shown disabled rather than inert-but-active. + .child( + h_flex() + .w_full() + .gap_2() + // Shield signs from YOUR wallet, so it's disabled while viewing a + // watched read-only account (don't show a funds-moving action in a + // someone-else's-address context). + .child( + Button::new("shield") + .primary() + .label("Shield") + .disabled(self.viewing_watch) + .on_click(cx.listener(|this, _, _, cx| this.open_shield(cx))), + ) + .child(Button::new("receive").ghost().label("Receive").on_click( + cx.listener(|this, _, _, cx| this.open(Surface::Receive, cx)), + )) + .child(Button::new("send").ghost().label("Send").disabled(true)) + .child(Button::new("swap").ghost().label("Swap").disabled(true)), + ) + .child( + div() + .text_xs() + .text_color(muted) + .child("Send & Swap arrive in the next release."), + ) + // Holdings, or a state. + .child(self.render_holdings(first_sync, has_tokens, holdings, cx)) + // Keyboard hints — the Superhuman/Linear signal. + .child( + h_flex() + .gap_4() + .pt_1() + .child(chip(format!("{MOD}K"), "Command palette".into())) + .child(chip(format!("{MOD}["), "Back".into())) + .child(chip(format!("{MOD},"), "Settings".into())), + ), + ) } /// The merged Total hero (Wave 2 T10): `Total = public + private` when both are known, a @@ -460,72 +467,78 @@ impl Shell { let native_wei = self.portfolio.as_ref().map(|p| p.native_wei); - div().size_full().p_8().child( - v_flex() - .items_start() - .max_w(px(680.0)) - .gap_6() - .child( - h_flex() - .items_center() - .gap_3() - .child(div().size(px(28.0)).rounded(px(6.0)).bg(id_square)) - .child( - div() - .text_xl() - .font_weight(FontWeight::SEMIBOLD) - .text_color(fg) - .child("Personal"), - ), - ) - .child( - v_flex() - .w_full() - .gap_3() - .child( - div() - .id("project-balance-hero") - .cursor_pointer() - .text_3xl() - .font_weight(FontWeight::SEMIBOLD) - .map(|el| match native_wei { - Some(wei) => el.child(money( - wei, - 18, - 4, - Some("ETH"), - masked, - mono.clone(), - fg, - muted, - )), - None => { - el.font_family(mono.clone()).text_color(muted).child("—") - } - }) - .on_click(cx.listener(|this, _, _, cx| this.toggle_mask(cx))), - ) - .children(native_wei.map(|_| { - allocation_bar( - vec![AllocSegment { - label: "Public".into(), - fraction: 1.0, - tone: id_square, - }], - masked, - border, - muted, - fg, + div() + .size_full() + .p_8() + // Scrolling for this surface is handled by its per-surface wrapper in + // `shell.rs` (the content match), so the body here is plain content. + .child( + v_flex() + .items_start() + .max_w(px(680.0)) + .gap_6() + .child( + h_flex() + .items_center() + .gap_3() + .child(div().size(px(28.0)).rounded(px(6.0)).bg(id_square)) + .child( + div() + .text_xl() + .font_weight(FontWeight::SEMIBOLD) + .text_color(fg) + .child("Personal"), + ), + ) + .child( + v_flex() + .w_full() + .gap_3() + .child( + div() + .id("project-balance-hero") + .cursor_pointer() + .text_3xl() + .font_weight(FontWeight::SEMIBOLD) + .map(|el| match native_wei { + Some(wei) => el.child(money( + wei, + 18, + 4, + Some("ETH"), + masked, + mono.clone(), + fg, + muted, + )), + None => el + .font_family(mono.clone()) + .text_color(muted) + .child("—"), + }) + .on_click(cx.listener(|this, _, _, cx| this.toggle_mask(cx))), ) - })), - ) - .child( - div() - .text_sm() - .text_color(muted) - .child("1 wallet · 1 agent"), - ), - ) + .children(native_wei.map(|_| { + allocation_bar( + vec![AllocSegment { + label: "Public".into(), + fraction: 1.0, + tone: id_square, + }], + masked, + border, + muted, + fg, + ) + })), + ) + .child( + div() + .text_sm() + .text_color(muted) + .child("1 wallet · 1 agent"), + ), + ) } /// Agent home — a static, demo-scoped policy-card placeholder (DESIGN §Policy @@ -563,79 +576,83 @@ impl Shell { ) }; - div().size_full().p_8().child( - v_flex() - .items_start() - .max_w(px(680.0)) - .gap_6() - // Header: cyan squircle monogram (the ONLY cyan on the surface, - // breathing while Atlas acts) + agent name H1 (text.primary, NEVER cyan). - .child( - h_flex() - .items_center() - .gap_3() - .child(agent_squircle( - px(28.0), - px(6.0), - self.agent_acting, - agent, - agent_tint, - "agent-pulse-home", - )) - .child( - v_flex() - .gap_0p5() - .child( - div() - .text_xl() - .font_weight(FontWeight::SEMIBOLD) - .text_color(fg) - .child("Atlas"), - ) - .child(div().text_xs().text_color(muted).child( - if self.agent_acting { - "Delegated agent · acting now" - } else { - "Delegated agent · idle" - }, - )), + div() + .size_full() + .p_8() + // Scrolling for this surface is handled by its per-surface wrapper in + // `shell.rs` (the content match), so the body here is plain content. + .child( + v_flex() + .items_start() + .max_w(px(680.0)) + .gap_6() + // Header: cyan squircle monogram (the ONLY cyan on the surface, + // breathing while Atlas acts) + agent name H1 (text.primary, NEVER cyan). + .child( + h_flex() + .items_center() + .gap_3() + .child(agent_squircle( + px(28.0), + px(6.0), + self.agent_acting, + agent, + agent_tint, + "agent-pulse-home", + )) + .child( + v_flex() + .gap_0p5() + .child( + div() + .text_xl() + .font_weight(FontWeight::SEMIBOLD) + .text_color(fg) + .child("Atlas"), + ) + .child(div().text_xs().text_color(muted).child( + if self.agent_acting { + "Delegated agent · acting now" + } else { + "Delegated agent · idle" + }, + )), + ), + ) + // Policy card: one faint frame, no interior grid lines. + .child( + v_flex() + .w_full() + .gap_0() + .p_4() + .rounded_lg() + .border_1() + .border_color(border) + .bg(surface) + .child(policy_row("Per-transaction cap", "0.10 ETH")) + .child(policy_row("Period budget", "1.00 ETH / week")) + .child(policy_row("Allowed assets", "ETH")) + .child(policy_row("Session key", "expires in 6d")) + .child(policy_row("Autonomy", "act < $50 · ask above")), + ) + // Demo control: narrate Atlas "acting" to show the one ambient motion + // (the breathing squircle). Real activity arrives with the MCP agent. + .child( + Button::new("toggle-agent-acting") + .ghost() + .label(if self.agent_acting { + "Stop activity (demo)" + } else { + "Simulate activity (demo)" + }) + .on_click(cx.listener(|this, _, _, cx| this.toggle_agent_acting(cx))), + ) + .child( + div().text_xs().text_color(muted).child( + "Atlas is a manual stand-in for the demo. Controls land with MCP.", ), - ) - // Policy card: one faint frame, no interior grid lines. - .child( - v_flex() - .w_full() - .gap_0() - .p_4() - .rounded_lg() - .border_1() - .border_color(border) - .bg(surface) - .child(policy_row("Per-transaction cap", "0.10 ETH")) - .child(policy_row("Period budget", "1.00 ETH / week")) - .child(policy_row("Allowed assets", "ETH")) - .child(policy_row("Session key", "expires in 6d")) - .child(policy_row("Autonomy", "act < $50 · ask above")), - ) - // Demo control: narrate Atlas "acting" to show the one ambient motion - // (the breathing squircle). Real activity arrives with the MCP agent. - .child( - Button::new("toggle-agent-acting") - .ghost() - .label(if self.agent_acting { - "Stop activity (demo)" - } else { - "Simulate activity (demo)" - }) - .on_click(cx.listener(|this, _, _, cx| this.toggle_agent_acting(cx))), - ) - .child( - div() - .text_xs() - .text_color(muted) - .child("Atlas is a manual stand-in for the demo. Controls land with MCP."), - ), - ) + ), + ) } } From 942bf57845d8ae9a634915b605897452f5cba7b2 Mon Sep 17 00:00:00 2001 From: hellno Date: Wed, 10 Jun 2026 13:45:31 +0200 Subject: [PATCH 10/11] chore(app): remove AI-slop comments, keep substantive gotcha notes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the redundant flex_1/min_w_0 'wraps text' comments (3 files — a standard gpui idiom), tighten two over-long shell.rs comments, and revert welcome.rs to main (its only change was formatting-motivated notes that triggered a 460-line rustfmt reflow). Keep the comments that document real footguns (gpui div=block, Scrollable call-site keying) and the shield honesty rationale. --- crates/deckard-app/src/receive.rs | 2 -- crates/deckard-app/src/settings_view.rs | 2 -- crates/deckard-app/src/shell.rs | 18 ++++++------------ crates/deckard-app/src/shield_view.rs | 2 -- crates/deckard-app/src/welcome.rs | 15 +++++++++------ 5 files changed, 15 insertions(+), 24 deletions(-) diff --git a/crates/deckard-app/src/receive.rs b/crates/deckard-app/src/receive.rs index f80b1c6..6be6a4d 100644 --- a/crates/deckard-app/src/receive.rs +++ b/crates/deckard-app/src/receive.rs @@ -105,8 +105,6 @@ impl Shell { .flex_shrink_0(), ) .child( - // Bound + flex the text so the warning wraps inside the card - // instead of being clipped at the right edge. div() .flex_1() .min_w_0() diff --git a/crates/deckard-app/src/settings_view.rs b/crates/deckard-app/src/settings_view.rs index 2d57663..a5f4bbd 100644 --- a/crates/deckard-app/src/settings_view.rs +++ b/crates/deckard-app/src/settings_view.rs @@ -37,8 +37,6 @@ impl Shell { .items_center() .justify_between() .child( - // Bound the text column so a long description wraps instead of widening - // the row past the card and shoving the control off the right edge. v_flex() .flex_1() .min_w_0() diff --git a/crates/deckard-app/src/shell.rs b/crates/deckard-app/src/shell.rs index 1b6229b..8ea2150 100644 --- a/crates/deckard-app/src/shell.rs +++ b/crates/deckard-app/src/shell.rs @@ -1428,12 +1428,9 @@ impl Render for Shell { // (sidebar | [breadcrumb / content / status strip]) + command palette. self.prepare_shield_inputs(window, cx); let title_bar = self.render_title_bar(cx); - // Scrollable content surfaces wrap their view in a Scrollable. Each arm inlines its - // OWN `.overflow_y_scrollbar()` (not a shared helper) on purpose: gpui-component keys - // the scroll offset by call site, so a per-arm call gives each surface an independent - // offset — otherwise scrolling one long page would leave the next one opened - // pre-scrolled with its header hidden. Receive and Shield are short, centered - // single-action cards, so they get NO scroll wrapper and stay vertically centered. + // Each scrollable surface inlines its OWN `.overflow_y_scrollbar()` (don't factor into + // a helper): gpui-component keys the scroll offset by call site, so per-arm calls give + // each surface an independent offset. Receive/Shield are short centered cards — no wrapper. let content = match (self.selection, self.surface) { (_, Surface::Settings) => div() .id("scroll-settings") @@ -1478,12 +1475,9 @@ impl Render for Shell { .min_w_0() .min_h_0() .child(self.render_breadcrumb(cx)) - // The content slot is a full-height flex COLUMN (not a plain `div`, - // which gpui defaults to `display: block`): the centered single-action - // surfaces (Receive/Shield) use `flex_1` + `justify_center`, which only - // fills + centers inside a flex parent — a block slot would let them - // shrink to their card height and sit under the breadcrumb. Scrolling - // surfaces wrap themselves (the match above), so each owns its offset. + // The slot is a `v_flex`, not a plain `div` (gpui defaults to + // `display: block`): the centered Receive/Shield roots use `flex_1` + + // `justify_center`, which only fill + center inside a flex parent. .child(v_flex().flex_1().min_h_0().child(content)) .child(self.render_status_strip(cx)), ), diff --git a/crates/deckard-app/src/shield_view.rs b/crates/deckard-app/src/shield_view.rs index 9a9cf41..c7d2735 100644 --- a/crates/deckard-app/src/shield_view.rs +++ b/crates/deckard-app/src/shield_view.rs @@ -454,8 +454,6 @@ impl Shell { .flex_shrink_0(), ) .child( - // Bound the text column so the title/subtitle wrap within the card rather than - // running off the right edge next to the fixed-width glyph. v_flex() .flex_1() .min_w_0() diff --git a/crates/deckard-app/src/welcome.rs b/crates/deckard-app/src/welcome.rs index e63d3d2..b98103b 100644 --- a/crates/deckard-app/src/welcome.rs +++ b/crates/deckard-app/src/welcome.rs @@ -131,8 +131,9 @@ impl Shell { div() .size_full() .p_8() - // Scrolling for this surface is handled by its per-surface wrapper in - // `shell.rs` (the content match), so the body here is plain content. + // TODO(scroll): restore a scrollable main pane via a Stateful + // `div().id(..).overflow_y_scroll()` (the agent draft mis-ordered + // gpui-component's `overflow_y_scrollbar`). Content is short for now. .child( v_flex() .items_start() @@ -470,8 +471,9 @@ impl Shell { div() .size_full() .p_8() - // Scrolling for this surface is handled by its per-surface wrapper in - // `shell.rs` (the content match), so the body here is plain content. + // TODO(scroll): restore a scrollable main pane via a Stateful + // `div().id(..).overflow_y_scroll()` (the agent draft mis-ordered + // gpui-component's `overflow_y_scrollbar`). Content is short for now. .child( v_flex() .items_start() @@ -579,8 +581,9 @@ impl Shell { div() .size_full() .p_8() - // Scrolling for this surface is handled by its per-surface wrapper in - // `shell.rs` (the content match), so the body here is plain content. + // TODO(scroll): restore a scrollable main pane via a Stateful + // `div().id(..).overflow_y_scroll()` (the agent draft mis-ordered + // gpui-component's `overflow_y_scrollbar`). Content is short for now. .child( v_flex() .items_start() From ae05073a39ab41124173b9897492c36ee14598ae Mon Sep 17 00:00:00 2001 From: hellno Date: Wed, 10 Jun 2026 13:58:24 +0200 Subject: [PATCH 11/11] fix(app): flatten the Receive network warning, drop the amber left-keyline Per design review the 2px amber left-keyline on caution cards read as generic alert-card slop. Flatten the Receive warning to an inline amber icon + risk text (no banner box), and remove the keyline from the Shield honesty box (keep its calm fill). Update DESIGN.md to drop the keyline rule (caution = amber icon + text). --- DESIGN.md | 8 ++++---- crates/deckard-app/src/receive.rs | 11 ++--------- crates/deckard-app/src/shield_view.rs | 6 +----- 3 files changed, 7 insertions(+), 18 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index a7ad676..a6ac13c 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -118,8 +118,8 @@ do not go lighter) · `accent #A8650C` (deepened for AA) · `agent #0C7E75`. 5. **Allocation/category bars** use neutral/low-chroma tonal steps, never amber as a category. 6. **Danger stays loud red, early.** Even though the app is otherwise near-colorless, unlimited approvals, unknown contracts, fresh-address sends, and over-cap states surface in `error`. -7. **Caution banners** = neutral surface + a 2px amber left keyline + amber icon/text. Not a filled - warm block. +7. **Caution** = an amber alert icon + the risk text, inline. No left keyline or banner box — the + amber icon carries the signal and the risk word carries the emphasis. 8. **Budget/utilization bars** = thin (4px), neutral track, neutral/cyan fill at rest; amber only ≥90%, red at ≥100%. Never a saturated amber slab. @@ -232,8 +232,8 @@ data components must define **empty / loading / error**. Defaults: - **Seed reveal** — blurred by default, **hold-to-reveal**, auto-hides after a few seconds, a "make sure nobody is watching" caution, **never auto-copied** (and Copy is visually demoted below Hold-to-reveal). The index numbers stay legible so the grid reads as "present but hidden." -- **Network warning** on Receive — the one caution moment; neutral surface + amber keyline, the risk - word emphasized, not the network chip. +- **Network warning** on Receive — the one caution moment; an amber alert icon + the risk text inline + (no keyline or banner box), the risk word emphasized, not the network chip. - **Kill switch / revocation** — Pause / Revoke / Rotate always one deliberate action away on any agent; a master "Pause all agents" belongs in Settings (agent governance), styled deliberate. diff --git a/crates/deckard-app/src/receive.rs b/crates/deckard-app/src/receive.rs index 6be6a4d..ac3e66e 100644 --- a/crates/deckard-app/src/receive.rs +++ b/crates/deckard-app/src/receive.rs @@ -85,20 +85,13 @@ impl Shell { .text_color(fg) .child(address), ) - // Network warning — the one caution moment (DESIGN §236): a - // neutral surface with a 2px amber LEFT keyline + amber icon/text. - // Not a filled warm block; the risk word carries the emphasis. + // Network warning — the one caution moment: an amber alert icon + the risk + // text, inline. No banner box or keyline; the icon carries the signal. .child( h_flex() .w_full() .items_start() .gap_2() - .px_3() - .py_2p5() - .rounded_lg() - .bg(surface) - .border_l_2() - .border_color(amber) .child( Icon::new(IconName::TriangleAlert) .text_color(amber) diff --git a/crates/deckard-app/src/shield_view.rs b/crates/deckard-app/src/shield_view.rs index c7d2735..dcdf5a1 100644 --- a/crates/deckard-app/src/shield_view.rs +++ b/crates/deckard-app/src/shield_view.rs @@ -300,14 +300,12 @@ impl Shell { ) } - /// The three honesty lines, in DESIGN's caution frame (neutral surface + a 2px amber - /// left keyline). Calm, not a filled warm block. + /// The three honesty lines in a calm neutral surface (no keyline). fn shield_honesty(&self, cx: &mut Context) -> impl IntoElement { let theme = cx.theme(); let fg = theme.foreground; let muted = theme.muted_foreground; let surface = theme.secondary; - let amber = theme::amber(theme.is_dark()); v_flex() .w_full() @@ -316,8 +314,6 @@ impl Shell { .py_2p5() .rounded_lg() .bg(surface) - .border_l_2() - .border_color(amber) .child( div() .text_xs()