refactor(desktop): dock market wallet in drawer

Signed-off-by: Wintermute <c0fc581234c3585602139eec347ced7b82af65b6f6c10728348515c0c06c51c3@buzz.block.builderlab.xyz>
This commit is contained in:
Wintermute
2026-08-31 22:08:40 -04:00
parent 36167bb90c
commit 033bed70ef
3 changed files with 150 additions and 75 deletions

View File

@@ -0,0 +1,102 @@
import { WalletCards } from "lucide-react";
import type { MarketScenarioId } from "@/features/market/lib/marketPrototypeData";
import { ProjectContextRail } from "@/features/projects/ui/ProjectContextRail";
import { ProjectHomeColumn } from "@/features/projects/ui/ProjectHomeColumn";
import { useThreadPanelWidth } from "@/shared/hooks/useThreadPanelWidth";
import { SIDEBAR_WIDTH_MIN } from "@/shared/layout/sidebarLayout";
const MARKET_WALLET_WIDTH_KEY = "buzz.desktop.market-wallet-width";
const WALLET_DETAILS: Record<
MarketScenarioId,
{ account: string; balance: string; settlement: string }
> = {
finite: {
account: "escrow1report…13c8",
balance: "150 sats funded",
settlement: "1 paid · 2 reserved",
},
unlimited: {
account: "wallet1mapper…c241",
balance: "80 sats escrowed",
settlement: "760 sats paid",
},
auction: {
account: "escrow1strings…9f10",
balance: "600 sats funded",
settlement: "Held until award",
},
tender: {
account: "escrow1tender…1b73",
balance: "2,000 sats funded",
settlement: "Held during selection",
},
awarded: {
account: "escrow1tender…1b73",
balance: "1,750 sats escrowed",
settlement: "Release on signed receipt",
},
};
export function AgentWalletPanel({
open,
scenarioId,
}: {
open: boolean;
scenarioId: MarketScenarioId;
}) {
const width = useThreadPanelWidth(undefined, {
minWidthPx: SIDEBAR_WIDTH_MIN,
sessionKey: MARKET_WALLET_WIDTH_KEY,
});
const wallet = WALLET_DETAILS[scenarioId];
return (
<ProjectContextRail
open={open}
panelWidthPx={width.widthPx}
resizing={width.isResizing}
rounded={false}
testId="market-wallet-rail"
>
<ProjectHomeColumn
bodyClassName="overflow-hidden"
canResetWidth={width.canReset}
onResetWidth={width.onResetWidth}
onResizeStart={width.onResizeStart}
testId="market-agent-wallet"
widthPx={width.widthPx}
>
<div className="flex h-full min-h-0 flex-col bg-sidebar text-sidebar-foreground">
<header className="flex h-14 shrink-0 items-center gap-2 px-5">
<WalletCards className="h-4 w-4 text-sidebar-foreground/65" />
<h2 className="text-sm font-semibold">Agent wallet</h2>
</header>
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto px-5 pb-6">
<p className="text-xs text-sidebar-foreground/60">
Settlement for this listing
</p>
<dl className="mt-6 space-y-5">
<WalletValue label="Balance" value={wallet.balance} />
<WalletValue label="Settlement" value={wallet.settlement} />
<WalletValue label="Network" value="Sandbox Lightning" />
</dl>
<p className="mt-auto pt-6 font-mono text-xs text-sidebar-foreground/60">
{wallet.account}
</p>
</div>
</div>
</ProjectHomeColumn>
</ProjectContextRail>
);
}
function WalletValue({ label, value }: { label: string; value: string }) {
return (
<div>
<dt className="text-xs text-sidebar-foreground/60">{label}</dt>
<dd className="mt-1 text-sm font-medium">{value}</dd>
</div>
);
}

View File

@@ -5,11 +5,11 @@ import {
Radio,
Store,
TriangleAlert,
WalletCards,
} from "lucide-react";
import * as React from "react";
import { ChatHeader } from "@/features/chat/ui/ChatHeader";
import { AgentWalletPanel } from "@/features/market/ui/AgentWalletPanel";
import {
MARKET_SCENARIOS,
type MarketActivity,
@@ -28,6 +28,7 @@ import {
DialogHeader,
DialogTitle,
} from "@/shared/ui/dialog";
import { DrawerPanelIcon } from "@/shared/ui/DrawerPanelIcon";
import { Input } from "@/shared/ui/input";
import { useOptionalSidebar } from "@/shared/ui/sidebar";
import { Textarea } from "@/shared/ui/textarea";
@@ -38,37 +39,6 @@ const REPRESENTING_AGENTS = [
{ name: "Sentinel", role: "Operations & safety" },
] as const;
const WALLET_DETAILS: Record<
MarketScenarioId,
{ account: string; balance: string; settlement: string }
> = {
finite: {
account: "escrow1report…13c8",
balance: "150 sats funded",
settlement: "1 paid · 2 reserved",
},
unlimited: {
account: "wallet1mapper…c241",
balance: "80 sats escrowed",
settlement: "760 sats paid",
},
auction: {
account: "escrow1strings…9f10",
balance: "600 sats funded",
settlement: "Held until award",
},
tender: {
account: "escrow1tender…1b73",
balance: "2,000 sats funded",
settlement: "Held during selection",
},
awarded: {
account: "escrow1tender…1b73",
balance: "1,750 sats escrowed",
settlement: "Release on signed receipt",
},
};
const COMMERCIAL_TERM_LABELS = new Set([
"Price",
"Initial quantity",
@@ -108,6 +78,7 @@ export function MarketScreen({ scenarioId }: { scenarioId: MarketScenarioId }) {
const scenario = MARKET_SCENARIOS[scenarioId];
const sidebar = useOptionalSidebar();
const [createOpen, setCreateOpen] = React.useState(false);
const [walletOpen, setWalletOpen] = React.useState(true);
const isTerminal = scenario.status !== "Open";
return (
@@ -121,6 +92,27 @@ export function MarketScreen({ scenarioId }: { scenarioId: MarketScenarioId }) {
>
<section className="mb-2 ml-px mt-px flex min-h-0 min-w-60 flex-1 flex-col overflow-hidden rounded-2xl bg-background">
<ChatHeader
actions={
<Button
aria-label={
walletOpen ? "Hide agent wallet" : "Show agent wallet"
}
aria-pressed={walletOpen}
className="h-7 w-7 text-sidebar-foreground hover:bg-sidebar-accent"
data-testid="market-wallet-toggle"
onClick={() => setWalletOpen((open) => !open)}
size="icon"
title={walletOpen ? "Hide agent wallet" : "Show agent wallet"}
type="button"
variant="ghost"
>
<DrawerPanelIcon
className="-scale-x-100"
side={walletOpen ? "left" : "right"}
testId="market-wallet-toggle-icon"
/>
</Button>
}
channelType="stream"
description={scenario.summary}
leadingContent={<Store className="h-4 w-4 text-muted-foreground" />}
@@ -132,7 +124,6 @@ export function MarketScreen({ scenarioId }: { scenarioId: MarketScenarioId }) {
<OfferCard
onCreate={() => setCreateOpen(true)}
scenario={scenario}
scenarioId={scenarioId}
/>
<section className="pt-5" data-testid="market-agent-timeline">
@@ -187,6 +178,8 @@ export function MarketScreen({ scenarioId }: { scenarioId: MarketScenarioId }) {
</div>
</footer>
</section>
<AgentWalletPanel open={walletOpen} scenarioId={scenarioId} />
</main>
);
}
@@ -194,11 +187,9 @@ export function MarketScreen({ scenarioId }: { scenarioId: MarketScenarioId }) {
function OfferCard({
onCreate,
scenario,
scenarioId,
}: {
onCreate: () => void;
scenario: MarketScenario;
scenarioId: MarketScenarioId;
}) {
const author =
scenario.terms.find(
@@ -216,7 +207,7 @@ function OfferCard({
className="overflow-hidden rounded-2xl border bg-card"
data-testid="market-offer-card"
>
<div className="grid sm:grid-cols-[minmax(12rem,22rem)_minmax(0,1fr)_17rem]">
<div className="grid sm:grid-cols-[minmax(12rem,22rem)_minmax(0,1fr)]">
<div
className="flex aspect-square items-center justify-center border-b bg-muted/60 text-muted-foreground sm:border-b-0 sm:border-r"
data-testid="market-product-image"
@@ -266,47 +257,11 @@ function OfferCard({
</Button>
</div>
</div>
<AgentWallet scenarioId={scenarioId} />
</div>
</section>
);
}
function AgentWallet({ scenarioId }: { scenarioId: MarketScenarioId }) {
const wallet = WALLET_DETAILS[scenarioId];
return (
<aside
className="flex flex-col border-t bg-muted/25 p-5 sm:border-l sm:border-t-0"
data-testid="market-agent-wallet"
>
<div className="flex items-center gap-2">
<WalletCards className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-semibold">Agent wallet</h3>
</div>
<p className="mt-1 text-xs text-muted-foreground">
Settlement for this listing
</p>
<dl className="mt-5 space-y-4">
<WalletValue label="Balance" value={wallet.balance} />
<WalletValue label="Settlement" value={wallet.settlement} />
<WalletValue label="Network" value="Sandbox Lightning" />
</dl>
<p className="mt-auto pt-5 font-mono text-xs text-muted-foreground">
{wallet.account}
</p>
</aside>
);
}
function WalletValue({ label, value }: { label: string; value: string }) {
return (
<div>
<dt className="text-xs text-muted-foreground">{label}</dt>
<dd className="mt-0.5 text-sm font-medium">{value}</dd>
</div>
);
}
function CreateMarketDialog({
onOpenChange,
open,

View File

@@ -49,7 +49,7 @@ test.describe("market channel prototype", () => {
const offerCardWidth = await market
.getByTestId("market-offer-card")
.evaluate((element) => element.getBoundingClientRect().width);
expect(offerCardWidth).toBeGreaterThan(1100);
expect(offerCardWidth).toBeGreaterThan(850);
const imageAspectRatio = await market
.getByTestId("market-product-image")
.evaluate((element) => {
@@ -57,10 +57,28 @@ test.describe("market channel prototype", () => {
return bounds.width / bounds.height;
});
expect(imageAspectRatio).toBeCloseTo(1, 1);
await expect(market.getByTestId("market-agent-wallet")).toBeVisible();
const wallet = market.getByTestId("market-agent-wallet");
await expect(wallet).toBeVisible();
await expect(wallet).toHaveCSS(
"background-color",
await page
.locator('[data-sidebar="sidebar"]')
.evaluate(
(element) => window.getComputedStyle(element).backgroundColor,
),
);
await expect(
market.getByText("Agent wallet", { exact: true }),
market.getByRole("button", { name: "Hide agent wallet" }).first(),
).toBeVisible();
const walletRail = market.getByTestId("market-wallet-rail");
await market.getByTestId("market-wallet-toggle").click();
await expect(walletRail).toHaveAttribute("aria-hidden", "true");
await expect(walletRail).toHaveCSS("width", "0px");
await expect(
market.getByRole("button", { name: "Show agent wallet" }),
).toBeVisible();
await market.getByRole("button", { name: "Show agent wallet" }).click();
await expect(wallet).toBeVisible();
await expect(market.getByText("Agent market channel")).toBeVisible();
await expect(market.getByTestId("market-agent-avatar")).toHaveCount(
scenario === "unlimited" ? 3 : 4,