mirror of
https://github.com/block/buzz.git
synced 2026-10-29 13:31:44 +00:00
refactor(desktop): dock market wallet in drawer
Signed-off-by: Wintermute <c0fc581234c3585602139eec347ced7b82af65b6f6c10728348515c0c06c51c3@buzz.block.builderlab.xyz>
This commit is contained in:
102
desktop/src/features/market/ui/AgentWalletPanel.tsx
Normal file
102
desktop/src/features/market/ui/AgentWalletPanel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user