Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

61 changes: 59 additions & 2 deletions crates/deckard-app/src/money.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,27 @@
//! only* (`text.muted`) — never by a size step, which would produce the
//! superscript look DESIGN rejects. The integer carries `text.primary`.
//!
//! Two entry points wrap the single canonical formatter
//! Three entry points wrap the single canonical formatter
//! `deckard_core::format_amount(raw, decimals, max_frac)`:
//! - [`money`] — an asset amount with an optional trailing ticker (`1,934.5 ETH`).
//! - [`usd`] — a USD figure carrying the `$` prefix; zero renders `$0`, never
//! `$0.0` (the `$` discipline + the zero rule).
//! - [`money_cell`] — a decimal-point-aligned ledger cell (integer right-aligned to a
//! fixed seam, `.decimals` left-aligned after it) so a holdings column's points line up.

use gpui::{div, prelude::FluentBuilder, Hsla, IntoElement, ParentElement, SharedString, Styled};
use gpui::{
div, prelude::FluentBuilder, px, Hsla, IntoElement, ParentElement, Pixels, SharedString, Styled,
};
use gpui_component::h_flex;

use deckard_core::U256;

/// The fixed width of a ledger money cell's fractional sub-column — it holds `.decimals` (up to the
/// four-digit hero precision) left-aligned. The integer sub-column takes the rest of the cell and is
/// right-aligned, so the decimal seam sits at a constant `x` across every row in a column and the
/// points line up (the E4 "decimals aligned on the point" rule, #184). Sized to clear `.0000`.
const FRAC_SEAM_W: Pixels = px(52.0);

/// The fixed-length privacy mask: **always six bullets**, never `real.len()`, so a
/// masked figure leaks neither its value nor its digit count (the magnitude-safe rule,
/// per MetaMask's `SensitiveText`). One glyph for every money surface.
Expand Down Expand Up @@ -91,6 +101,53 @@ pub fn usd(
spans(format!("${int_part}"), frac, None, mono, primary, dim)
}

/// A **decimal-point-aligned** money cell for a holdings / ledger column (E4, #184). The integer
/// part is **right-aligned** up to a fixed seam and the `.decimals` are **left-aligned** after it, so
/// the decimal points line up down a column no matter how many fractional digits each row carries.
/// Dimming stays color-only (the decimals in `dim`), size held flat — never a superscript step. When
/// `masked`, renders the fixed [`MASK_BULLETS`] right-aligned to the seam (no value, no decimals).
///
/// The caller sets the column's width (this fills it, `w_full`); the seam is at `col_w -
/// FRAC_SEAM_W`, identical for every row, which is what makes the points align.
pub fn money_cell(
raw: U256,
decimals: u8,
max_frac: usize,
masked: bool,
mono: SharedString,
primary: Hsla,
dim: Hsla,
) -> impl IntoElement {
let (int_part, frac, int_color) = if masked {
(MASK_BULLETS.to_string(), String::new(), dim)
} else {
let s = deckard_core::format_amount(raw, decimals, max_frac);
let (int_part, frac) = split_amount(&s);
(int_part, frac, primary)
};

h_flex()
.w_full()
.font_family(mono)
// Integer: fills the row up to the seam, right-aligned so its ones-digit meets the point.
.child(
h_flex()
.flex_1()
.min_w_0()
.justify_end()
.child(div().flex_shrink_0().text_color(int_color).child(int_part)),
)
// Fractional seam: a fixed-width, left-aligned sub-column so the point starts at a constant x.
.child(
h_flex()
.w(FRAC_SEAM_W)
.flex_shrink_0()
.when(!frac.is_empty(), |e| {
e.child(div().text_color(dim).child(format!(".{frac}")))
}),
)
}

/// Split a formatted amount (`"1,934.5"`) into its integer and (possibly empty)
/// fractional parts. `format_amount` already strips trailing zeros, so the frac
/// is absent for whole numbers (zero renders `"0"` → `("0", "")`).
Expand Down
32 changes: 21 additions & 11 deletions crates/deckard-app/src/shell_rail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ impl Shell {
.w_full()
.gap_2()
.child(kv("Balance", KvValue::Mono(&balance)))
// Value: HONEST "—". The app has no price feed, so the rail's fiat row is an explicit
// dash — never a fabricated number (E4, #184; DESIGN §Trust), mirroring the hero meta.
.child(kv("Value", KvValue::Sans("—")))
.child(kv("Synced", KvValue::Sans(&synced)))
.child(kv("Status", status_kv))
.child(kv("Network", KvValue::Sans(network)))
Expand Down Expand Up @@ -126,19 +129,26 @@ impl Shell {
.child(kv("Per-transaction", per_tx_val))
.into_any_element();
col = col.child(meta_section(Some("Agent caps"), caps, theme));

// The golden ref's trailing composition metasec. Only "Agents" is engine-backed today —
// the app models one agent, armed unless a STOP revoked it — so the "Connections" count
// is omitted (not invented) until the browser bridge lands (ADR-0001 / #44).
let agents = if p.revoked { "1 stopped" } else { "1 active" };
let summary = v_flex()
.w_full()
.gap_2()
.child(kv("Agents", KvValue::Sans(agents)))
.into_any_element();
col = col.child(meta_section(None, summary, theme));
}

// The golden ref's trailing composition metasec — ALWAYS present (independent of whether the
// policy has landed). Connections: the reserved/empty state — the browser bridge (dapp
// connections) is deferred (ADR-0001 / #44), so we render the honest empty "none", NEVER a
// fabricated count. Agents: the app models one agent on this wallet; its state follows the
// live policy (idle until it lands, active unless a STOP revoked it).
let agents = match self.agent_policy.as_ref() {
Some(p) if p.revoked => "1 stopped",
Some(_) => "1 active",
None => "idle",
};
let summary = v_flex()
.w_full()
.gap_2()
.child(kv("Connections", KvValue::Sans("none")))
.child(kv("Agents", KvValue::Sans(agents)))
.into_any_element();
col = col.child(meta_section(None, summary, theme));

col.into_any_element()
}

Expand Down
Loading
Loading