localharness 0.47.0

Agents that own themselves: one Rust crate that's both an agent SDK (streaming, tools, hooks, policies, triggers, MCP) and a wallet-owning, self-sovereign agent that runs in the browser.
Documentation
//! Apex fresh-visitor landing — the product front door at
//! `localharness.xyz` for a visitor with NO identity.
//!
//! Hoisted out of the wasm-gated `app/` tree (same pattern as `raster.rs`
//! / `compose.rs`) so the exact shipping markup renders NATIVELY: the
//! `landing_preview` test below writes `target/landing-preview.html`
//! (stylesheet linked relatively into `web/`) so the page can be
//! screenshot-reviewed without an identity-free browser profile.
//! Regenerate with:
//!
//! ```sh
//! cargo test --features browser-app landing_preview
//! # then open target/landing-preview.html
//! ```
//!
//! Funnel: ONE decision — create a wallet. The fresh-visitor front door is a
//! single `create wallet` CTA (the paid entry: it creates AND funds the
//! wallet, so there's no unfunded-wallet path and no 0-$LH name-squatting).
//! Invited users skip this entirely — an `?invite=CODE` link/QR auto-redeems
//! on mount (`app/mod.rs` → `try_redeem_pending_invite`). Redeeming a code and
//! importing a seed are recovery/edge paths and live in the admin panel, NOT
//! here. Explore is post-auth only (no account yet → nothing to browse).

use maud::{Markup, html};

/// The fresh-visitor front door: ONE CTA whose PRICE is on the button —
/// pay-first onboarding (`Action::CreateAccount`): it creates the identity then
/// opens the $2 checkout immediately, so there's no surprise paywall after the
/// user has invested in picking a name. The mint ($2 = 200 $LH) lands them on
/// the (now-funded) name-claim input. No code input or seed-import here (both
/// live in admin; `?invite=` links auto-redeem on mount). `#onboard-msg` is the
/// status slot; `data-action="create-account"` is load-bearing (MUST match a
/// `Action::parse` arm — a fresh visitor's only control).
/// The two-line offer pitch: a "limited time" label + the offer. Shown ABOVE the
/// create button AND kept at the top of the inline checkout card, so the offer
/// (and "limited time") does NOT vanish the moment the user taps create.
pub(crate) fn onboard_pitch() -> Markup {
    html! {
        p style="font-size:11px;letter-spacing:0.08em;text-transform:uppercase;color:var(--muted);margin:0 0 4px" {
            "limited time"
        }
        p style="font-size:14px;margin:0 0 12px" {
            "1 agent + 200 $LH for $2"
        }
    }
}

pub(crate) fn create_wallet_cta() -> Markup {
    html! {
        section #apex-onboard .apex-onboard {
            (onboard_pitch())
            button type="button" data-action="create-account" .apex-onboard-cta {
                "create agent"
            }
            div #onboard-msg .step-msg {}
        }
    }
}

/// The muted footer link(s) under the apex column. The home screen stays a
/// single front door — the public agent directory (`?explore=1`) is reachable
/// from the admin panel / direct link, not surfaced here (per request). Only
/// the agent-onboarding pointer remains.
pub(crate) fn apex_links(_fresh: bool) -> Markup {
    html! {
        nav.apex-links {
            a href="/skill.md" { "for agents →" }
        }
    }
}

#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
    use super::*;
    use maud::DOCTYPE;

    /// Writes the apex fresh-visitor page to `target/landing-preview.html`
    /// for screenshot review (no browser profile / wasm build needed).
    ///
    /// Run: `cargo test --features browser-app landing_preview`
    /// then open `target/landing-preview.html` (file:// works — the
    /// stylesheet is linked relatively as `../web/styles.css`; the IBM
    /// Plex Mono link needs network, fallback is ui-monospace).
    #[test]
    fn landing_preview() {
        let page = html! {
            (DOCTYPE)
            html lang="en" {
                head {
                    meta charset="utf-8";
                    meta name="viewport"
                        content="width=device-width,initial-scale=1";
                    link rel="preconnect" href="https://fonts.googleapis.com";
                    link rel="preconnect" href="https://fonts.gstatic.com"
                        crossorigin;
                    link rel="stylesheet"
                        href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&display=swap";
                    link rel="stylesheet" href="../web/styles.css";
                    title { "localharness — landing preview" }
                }
                body {
                    div #root {
                        // STATIC replica of `templates::site_header` (which
                        // is wasm-gated): brand + admin button, enough for a
                        // faithful screenshot. If the real header changes,
                        // refresh this replica.
                        header.site-header {
                            div.header-inner {
                                h1.header-brand { "localharness" }
                                div.header-admin {
                                    button type="button"
                                        .header-button.admin-button { "admin" }
                                }
                            }
                        }
                        // The REAL fresh-apex content path (`templates::apex`
                        // with no wallet) — not a copy.
                        main.apex-main {
                            div.col-chat {
                                div #status .terminal-status {}
                                (create_wallet_cta())
                                (apex_links(true))
                            }
                        }
                    }
                }
            }
        };
        let dir =
            std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("target");
        std::fs::create_dir_all(&dir).expect("create target/");
        let path = dir.join("landing-preview.html");
        std::fs::write(&path, page.into_string())
            .expect("write landing-preview.html");
        println!("wrote {}", path.display());
    }
}