-
Activate
-
Hire {agent.name}
-
+
+
+
+ Activate
+
Hire {agent.name}
+
{local
? "No wallet needed. We’ll mark this hire as complete."
: "Confirm in your wallet. We’ll tell you when it’s done."}
-
+
- {!local ?
: null}
+ {!local ? (
+
+
+
+
+
+
+
+
+ ) : null}
{job && phase === "funded" ? (
-
Hire complete
+
+
+ Hire complete
+
{job.txHashes[0] ? (
-
Receipt {job.txHashes[0]}
+
Receipt {job.txHashes[0]}
) : null}
{job.session ? (
-
+
Access
-
Spend cap {job.session.spendCapWei} wei
-
Expires {new Date(job.session.expiry).toLocaleString()}
+
Spend cap {weiToU(job.session.spendCapWei) || job.session.spendCapWei} $U
+
Expires {formatDateTime(job.session.expiry)}
{job.session.revoked ? "Access stopped" : "Access active"}
{!job.session.revoked ? (
-
) : (
-
)}
+
+
+ {busy ? phaseLabel(phase) : ""}
+
+
+
setGate(null)}
+ details={gate === "fields" ? fieldErrors.task || fieldErrors.budget : error ?? undefined}
+ onConnect={() => {
+ setGate(null);
+ openConnectModal?.();
+ }}
+ onSwitch={
+ target && switchChain
+ ? () => switchChain({ chainId: target.id })
+ : undefined
+ }
+ switching={switching}
+ />
+ setConfirmOpen(false)}
+ onConfirm={() => void executeHire()}
+ agent={agent}
+ task={task.trim()}
+ budgetWei={budgetWei ?? ""}
+ local={local}
+ busy={busy}
+ />
+ setRevokeOpen(false)}
+ onConfirm={() => void onRevoke()}
+ busy={busy}
+ />
);
}
+
+export function HireStickyBar({
+ agent,
+ visible,
+}: {
+ agent: AgentListing;
+ visible: boolean;
+}) {
+ if (!visible) return null;
+ return (
+
+
+
+
{agent.name}
+
Hire with $U
+
+
+ document.getElementById("hire")?.scrollIntoView({ behavior: "smooth", block: "start" })
+ }
+ >
+ Hire
+
+
+
+ );
+}
diff --git a/apps/web/src/features/signals/category-signal.tsx b/apps/web/src/features/signals/category-signal.tsx
index 7c078d7..951e765 100644
--- a/apps/web/src/features/signals/category-signal.tsx
+++ b/apps/web/src/features/signals/category-signal.tsx
@@ -1,58 +1,91 @@
import { CATEGORY_JOBS, CATEGORY_LABELS, type AgentSignal, type Category } from "@era/domain";
import { Badge } from "@/components/ui";
+import { formatDateTime } from "@/lib/format";
function Metric({ label, value }: { label: string; value: string }) {
return (
-
+
{label}
-
{value}
+
{value}
);
}
-export function CategorySignal({ signal }: { signal: AgentSignal }) {
+function metricsFor(signal: AgentSignal, compact: boolean): Array<{ label: string; value: string }> {
if (signal.category === "rebalancing") {
- return (
-
-
-
-
-
- );
+ const rows = [
+ { label: "In range", value: `${signal.inRangePct.toFixed(1)}%` },
+ {
+ label: "Fees vs IL",
+ value: `$${signal.feesUsd.toFixed(0)} / $${signal.impermanentLossUsd.toFixed(0)}`,
+ },
+ ];
+ if (!compact) {
+ rows.push({ label: "Last rebalance", value: formatDateTime(signal.lastRebalanceAt) });
+ }
+ return rows;
}
if (signal.category === "grid_trading") {
- return (
-
-
-
-
-
-
- );
+ if (compact) {
+ return [
+ { label: "Fill rate", value: `${signal.fillRatePct.toFixed(1)}%` },
+ { label: "Realized PnL", value: `$${signal.realizedPnlUsd.toFixed(1)}` },
+ ];
+ }
+ return [
+ { label: "Bounds", value: `${signal.lowerBound} – ${signal.upperBound}` },
+ { label: "Fill rate", value: `${signal.fillRatePct.toFixed(1)}%` },
+ { label: "Realized PnL", value: `$${signal.realizedPnlUsd.toFixed(1)}` },
+ { label: "Window", value: `${signal.windowHours}h` },
+ ];
}
if (signal.category === "yield") {
- return (
-
-
-
-
-
-
- );
+ if (compact) {
+ return [
+ { label: "Net APR", value: `${signal.netAprPct.toFixed(2)}%` },
+ { label: "Venue", value: signal.venue },
+ ];
+ }
+ return [
+ { label: "Venue", value: signal.venue },
+ { label: "Net APR", value: `${signal.netAprPct.toFixed(2)}%` },
+ { label: "Allocation", value: `${signal.allocationPct}%` },
+ { label: "Last hop", value: formatDateTime(signal.lastHopAt) },
+ ];
}
+ if (compact) {
+ return [
+ { label: "Health factor", value: signal.healthFactor.toFixed(2) },
+ { label: "Liq. price", value: `$${signal.liquidationPrice.toFixed(1)}` },
+ ];
+ }
+ return [
+ { label: "Health factor", value: signal.healthFactor.toFixed(2) },
+ { label: "Liq. price", value: `$${signal.liquidationPrice.toFixed(1)}` },
+ { label: "Protocol", value: signal.protocol },
+ { label: "Last action", value: formatDateTime(signal.lastActionAt) },
+ ];
+}
+
+export function CategorySignal({
+ signal,
+ compact = false,
+}: {
+ signal: AgentSignal;
+ compact?: boolean;
+}) {
return (
-
-
-
-
+ {metricsFor(signal, compact).map((metric) => (
+
+ ))}
);
}
export function CategoryPitch({ category }: { category: Category }) {
return (
-
+
{CATEGORY_LABELS[category]}
{CATEGORY_JOBS[category]}
diff --git a/apps/web/src/features/signals/signal-chart.tsx b/apps/web/src/features/signals/signal-chart.tsx
new file mode 100644
index 0000000..76124d6
--- /dev/null
+++ b/apps/web/src/features/signals/signal-chart.tsx
@@ -0,0 +1,296 @@
+import { useId, useMemo, useState, type PointerEvent } from "react";
+import { RefreshCw } from "lucide-react";
+import type { AgentSignal } from "@era/domain";
+import { Button, ChartSkeleton, EmptyState } from "@/components/ui";
+import { Icon } from "@/components/ui/icon";
+
+type Point = { label: string; value: number; max: number; display: string };
+
+export function SignalChart({
+ signal,
+ loading = false,
+ refreshing = false,
+ onRefresh,
+}: {
+ signal: AgentSignal | null;
+ loading?: boolean;
+ refreshing?: boolean;
+ onRefresh?: () => void;
+}) {
+ if (loading) return
;
+ if (!signal) {
+ return (
+
+ Reload
+
+ ) : undefined
+ }
+ >
+ This agent has no live snapshot right now.
+
+ );
+ }
+
+ const points = pointsFor(signal);
+ if (points.length === 0) {
+ return (
+
+ This signal has no numeric snapshot to plot.
+
+ );
+ }
+
+ return
;
+}
+
+function BinancePanel({
+ points,
+ refreshing,
+ onRefresh,
+}: {
+ points: Point[];
+ refreshing: boolean;
+ onRefresh?: () => void;
+}) {
+ const gid = useId().replaceAll(":", "");
+ const [hover, setHover] = useState
(null);
+ const primary = points[0];
+ const layout = useMemo(() => layoutPoints(points), [points]);
+ const active = (hover !== null ? points[hover] : primary) ?? primary;
+ if (!primary || !active) return null;
+
+ function onMove(event: PointerEvent) {
+ const svg = event.currentTarget;
+ const box = svg.getBoundingClientRect();
+ const x = ((event.clientX - box.left) / box.width) * 360;
+ const index = nearestIndex(layout.xs, x);
+ setHover(index);
+ }
+
+ return (
+
+
+
+
Live snapshot
+
+ {active.display}
+
+
{active.label}
+
+ {onRefresh ? (
+
+
+
+ ) : null}
+
+
+
+
+
+
+
+ {points.map((point, index) => (
+ -
+
+ {point.label}: {point.display}
+
+ ))}
+
+ Now · no historical range is stored
+
+ );
+}
+
+function layoutPoints(points: Point[]): { xs: number[]; ys: number[]; line: string; area: string } {
+ const left = 40;
+ const right = 352;
+ const top = 16;
+ const height = 120;
+ const span = right - left;
+ const xs = points.map((_, index) =>
+ points.length === 1 ? (left + right) / 2 : left + (span * index) / (points.length - 1),
+ );
+ const ys = points.map((point) => top + (1 - clamp(point.value / point.max, 0, 1)) * height);
+ const line = smoothPath(xs, ys);
+ return { xs, ys, line, area: line };
+}
+
+function smoothPath(xs: number[], ys: number[]): string {
+ const firstX = xs[0];
+ const firstY = ys[0];
+ if (firstX === undefined || firstY === undefined) return "";
+ if (xs.length === 1) return `M ${firstX} ${firstY}`;
+ const secondX = xs[1];
+ const secondY = ys[1];
+ if (xs.length === 2 && secondX !== undefined && secondY !== undefined) {
+ return `M ${firstX} ${firstY} L ${secondX} ${secondY}`;
+ }
+ let d = `M ${firstX} ${firstY}`;
+ for (let i = 0; i < xs.length - 1; i += 1) {
+ const x0 = xs[i];
+ const y0 = ys[i];
+ const x1 = xs[i + 1];
+ const y1 = ys[i + 1];
+ if (x0 === undefined || y0 === undefined || x1 === undefined || y1 === undefined) continue;
+ const c = (x1 - x0) / 2;
+ d += ` C ${x0 + c} ${y0}, ${x1 - c} ${y1}, ${x1} ${y1}`;
+ }
+ return d;
+}
+
+function nearestIndex(xs: number[], x: number): number {
+ let best = 0;
+ let dist = Infinity;
+ xs.forEach((value, index) => {
+ const next = Math.abs(value - x);
+ if (next < dist) {
+ dist = next;
+ best = index;
+ }
+ });
+ return best;
+}
+
+function axisLabel(max: number, t: number): string {
+ const value = max * t;
+ if (max >= 100) return value.toFixed(0);
+ if (max >= 10) return value.toFixed(1);
+ return value.toFixed(2);
+}
+
+function clamp(value: number, min: number, max: number): number {
+ return Math.min(max, Math.max(min, value));
+}
+
+function pointsFor(signal: AgentSignal): Point[] {
+ if (signal.category === "rebalancing") {
+ return [
+ { label: "In range", value: signal.inRangePct, max: 100, display: `${signal.inRangePct.toFixed(1)}%` },
+ {
+ label: "Fees",
+ value: signal.feesUsd,
+ max: Math.max(signal.feesUsd, signal.impermanentLossUsd, 1),
+ display: `$${signal.feesUsd.toFixed(0)}`,
+ },
+ {
+ label: "IL",
+ value: signal.impermanentLossUsd,
+ max: Math.max(signal.feesUsd, signal.impermanentLossUsd, 1),
+ display: `$${signal.impermanentLossUsd.toFixed(0)}`,
+ },
+ ];
+ }
+ if (signal.category === "grid_trading") {
+ return [
+ { label: "Fill", value: signal.fillRatePct, max: 100, display: `${signal.fillRatePct.toFixed(1)}%` },
+ {
+ label: "PnL",
+ value: Math.abs(signal.realizedPnlUsd),
+ max: Math.max(Math.abs(signal.realizedPnlUsd), 1),
+ display: `$${signal.realizedPnlUsd.toFixed(1)}`,
+ },
+ ];
+ }
+ if (signal.category === "yield") {
+ return [
+ {
+ label: "APR",
+ value: signal.netAprPct,
+ max: Math.max(signal.netAprPct, 20),
+ display: `${signal.netAprPct.toFixed(2)}%`,
+ },
+ { label: "Alloc", value: signal.allocationPct, max: 100, display: `${signal.allocationPct}%` },
+ ];
+ }
+ return [
+ {
+ label: "HF",
+ value: signal.healthFactor,
+ max: Math.max(signal.healthFactor, 3),
+ display: signal.healthFactor.toFixed(2),
+ },
+ {
+ label: "Liq",
+ value: signal.liquidationPrice,
+ max: Math.max(signal.liquidationPrice, 1),
+ display: `$${signal.liquidationPrice.toFixed(1)}`,
+ },
+ ];
+}
diff --git a/apps/web/src/lib/catalog.ts b/apps/web/src/lib/catalog.ts
new file mode 100644
index 0000000..596ee35
--- /dev/null
+++ b/apps/web/src/lib/catalog.ts
@@ -0,0 +1,111 @@
+import type { AgentListing } from "@era/domain";
+
+export type AvailabilityFilter = "all" | "live" | "offline";
+export type SortKey = "featured" | "newest" | "name";
+
+export type CatalogQuery = {
+ search: string;
+ availability: AvailabilityFilter;
+ featuredOnly: boolean;
+ x402Only: boolean;
+ sort: SortKey;
+};
+
+export const DEFAULT_CATALOG_QUERY: CatalogQuery = {
+ search: "",
+ availability: "all",
+ featuredOnly: false,
+ x402Only: false,
+ sort: "featured",
+};
+
+export const CATALOG_PAGE_SIZE = 6;
+
+export function activeFilterCount(query: CatalogQuery): number {
+ let count = 0;
+ if (query.availability !== "all") count += 1;
+ if (query.featuredOnly) count += 1;
+ if (query.x402Only) count += 1;
+ return count;
+}
+
+export function catalogCapabilities(agents: AgentListing[]): {
+ showFeatured: boolean;
+ showX402: boolean;
+ showOffline: boolean;
+} {
+ return {
+ showFeatured: agents.some((agent) => agent.featured) && agents.some((agent) => !agent.featured),
+ showX402: agents.some((agent) => agent.commerce.x402),
+ showOffline: agents.some((agent) => !agent.live),
+ };
+}
+
+export function applyCatalogQuery(
+ agents: AgentListing[],
+ query: CatalogQuery,
+): AgentListing[] {
+ const needle = query.search.trim().toLowerCase();
+ let rows = agents;
+
+ if (needle) {
+ rows = rows.filter((agent) => {
+ const category = agent.category.replaceAll("_", " ");
+ return (
+ agent.name.toLowerCase().includes(needle) ||
+ agent.description.toLowerCase().includes(needle) ||
+ category.includes(needle)
+ );
+ });
+ }
+
+ if (query.availability === "live") {
+ rows = rows.filter((agent) => agent.live);
+ } else if (query.availability === "offline") {
+ rows = rows.filter((agent) => !agent.live);
+ }
+
+ if (query.featuredOnly) {
+ rows = rows.filter((agent) => agent.featured);
+ }
+
+ if (query.x402Only) {
+ rows = rows.filter((agent) => agent.commerce.x402);
+ }
+
+ return [...rows].sort((a, b) => {
+ if (query.sort === "name") return a.name.localeCompare(b.name);
+ if (query.sort === "newest") return b.createdAt.localeCompare(a.createdAt);
+ const score = (agent: AgentListing) =>
+ (agent.featured ? 2 : 0) + (agent.live ? 1 : 0);
+ const delta = score(b) - score(a);
+ if (delta !== 0) return delta;
+ return a.name.localeCompare(b.name);
+ });
+}
+
+export function paginate(
+ items: T[],
+ page: number,
+ pageSize: number,
+): {
+ page: number;
+ pageCount: number;
+ slice: T[];
+ start: number;
+ end: number;
+ total: number;
+} {
+ const total = items.length;
+ const pageCount = Math.max(1, Math.ceil(total / pageSize) || 1);
+ const safePage = Math.min(Math.max(1, page), pageCount);
+ const startIndex = (safePage - 1) * pageSize;
+ return {
+ page: safePage,
+ pageCount,
+ slice: items.slice(startIndex, startIndex + pageSize),
+ start: total === 0 ? 0 : startIndex + 1,
+ end: Math.min(startIndex + pageSize, total),
+ total,
+ };
+}
diff --git a/apps/web/src/lib/format.ts b/apps/web/src/lib/format.ts
new file mode 100644
index 0000000..62d1127
--- /dev/null
+++ b/apps/web/src/lib/format.ts
@@ -0,0 +1,65 @@
+const U_DECIMALS = 18;
+
+export function formatAddress(value: string): string {
+ if (value.length < 12) return value;
+ return `${value.slice(0, 6)}…${value.slice(-4)}`;
+}
+
+export function agentInitials(name: string): string {
+ const parts = name.trim().split(/\s+/).filter(Boolean);
+ const first = parts[0]?.[0] ?? "?";
+ const second = parts.length > 1 ? parts[parts.length - 1]?.[0] : parts[0]?.[1];
+ return `${first}${second ?? ""}`.toUpperCase();
+}
+
+export function chainLabel(chainId: 56 | 97): string {
+ return chainId === 56 ? "BNB Smart Chain" : "BSC Testnet";
+}
+
+export function formatDate(iso: string): string {
+ const date = new Date(iso);
+ if (Number.isNaN(date.getTime())) return iso;
+ return new Intl.DateTimeFormat(undefined, { dateStyle: "medium" }).format(date);
+}
+
+export function formatDateTime(iso: string): string {
+ const date = new Date(iso);
+ if (Number.isNaN(date.getTime())) return iso;
+ return new Intl.DateTimeFormat(undefined, {
+ dateStyle: "medium",
+ timeStyle: "short",
+ }).format(date);
+}
+
+/** Convert a $U wei string (18 decimals) into a compact decimal for display. */
+export function weiToU(wei: string): string {
+ if (!/^\d+$/.test(wei)) return "";
+ const value = BigInt(wei);
+ const base = 10n ** BigInt(U_DECIMALS);
+ const whole = value / base;
+ const frac = value % base;
+ if (frac === 0n) return whole.toString();
+ const fracStr = frac.toString().padStart(U_DECIMALS, "0").replace(/0+$/, "");
+ return `${whole.toString()}.${fracStr}`;
+}
+
+/**
+ * Parse a $U decimal into wei. Returns null when the value is empty,
+ * not a number, has too many decimals, or is not strictly positive.
+ */
+export function uToWei(amount: string): string | null {
+ const trimmed = amount.trim();
+ if (!trimmed) return null;
+ if (!/^\d+(\.\d+)?$/.test(trimmed)) return null;
+ const [wholeRaw, fracRaw = ""] = trimmed.split(".");
+ if (fracRaw.length > U_DECIMALS) return null;
+ const whole = wholeRaw && wholeRaw.length > 0 ? wholeRaw : "0";
+ const fracPadded = fracRaw.padEnd(U_DECIMALS, "0");
+ const wei = BigInt(whole) * 10n ** BigInt(U_DECIMALS) + BigInt(fracPadded);
+ if (wei <= 0n) return null;
+ return wei.toString();
+}
+
+export function friendlyLoadError(): string {
+ return "Check your connection and try again. If this keeps happening, the catalog may be offline.";
+}
diff --git a/apps/web/src/lib/inbox.ts b/apps/web/src/lib/inbox.ts
new file mode 100644
index 0000000..ede39a3
--- /dev/null
+++ b/apps/web/src/lib/inbox.ts
@@ -0,0 +1,91 @@
+export type NoticeKind =
+ | "hire_success"
+ | "hire_failed"
+ | "tx_pending"
+ | "wallet_connected"
+ | "wallet_disconnected"
+ | "agent_update"
+ | "system"
+ | "error"
+ | "warning";
+
+export type Notice = {
+ id: string;
+ kind: NoticeKind;
+ title: string;
+ description: string;
+ createdAt: string;
+ read: boolean;
+ href?: string;
+};
+
+export type ActivityEvent = {
+ id: string;
+ type: "hire" | "revoke" | "wallet" | "copy" | "system";
+ title: string;
+ description: string;
+ status?: string;
+ createdAt: string;
+ href?: string;
+};
+
+export type StoredHire = {
+ jobId: string;
+ agentId: string;
+ agentName: string;
+ category: string;
+ status: string;
+ budgetWei: string;
+ createdAt: string;
+ txHash?: string;
+};
+
+const NOTICE_KEY = "pulse:notices";
+const ACTIVITY_KEY = "pulse:activity";
+const HIRES_KEY = "pulse:hires";
+
+function readJson(key: string, fallback: T): T {
+ try {
+ const raw = localStorage.getItem(key);
+ if (!raw) return fallback;
+ return JSON.parse(raw) as T;
+ } catch {
+ return fallback;
+ }
+}
+
+function writeJson(key: string, value: unknown): void {
+ try {
+ localStorage.setItem(key, JSON.stringify(value));
+ } catch {
+ /* private mode */
+ }
+}
+
+export function loadNotices(): Notice[] {
+ return readJson(NOTICE_KEY, []);
+}
+
+export function saveNotices(items: Notice[]): void {
+ writeJson(NOTICE_KEY, items.slice(0, 80));
+}
+
+export function loadActivity(): ActivityEvent[] {
+ return readJson(ACTIVITY_KEY, []);
+}
+
+export function saveActivity(items: ActivityEvent[]): void {
+ writeJson(ACTIVITY_KEY, items.slice(0, 80));
+}
+
+export function loadHires(): StoredHire[] {
+ return readJson(HIRES_KEY, []);
+}
+
+export function saveHires(items: StoredHire[]): void {
+ writeJson(HIRES_KEY, items.slice(0, 80));
+}
+
+export function newId(): string {
+ return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
+}
diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx
index 205f1e4..2cac867 100644
--- a/apps/web/src/main.tsx
+++ b/apps/web/src/main.tsx
@@ -17,3 +17,9 @@ createRoot(root).render(
,
);
+
+if (import.meta.env.PROD && "serviceWorker" in navigator) {
+ window.addEventListener("load", () => {
+ void navigator.serviceWorker.register("/sw.js");
+ });
+}
diff --git a/apps/web/src/providers/wrong-network-banner.tsx b/apps/web/src/providers/wrong-network-banner.tsx
index 4fbf27c..b32dd8e 100644
--- a/apps/web/src/providers/wrong-network-banner.tsx
+++ b/apps/web/src/providers/wrong-network-banner.tsx
@@ -13,20 +13,21 @@ export function WrongNetworkBanner() {
return (
-
+
Wrong network. Switch to {target.name} to hire on this demo.
switchChain({ chainId: target.id })}
+ loading={isPending}
+ disabled={!switchChain}
+ onClick={() => switchChain?.({ chainId: target.id })}
>
{isPending ? "Switching…" : `Switch to ${target.name}`}
{error ? (
-
{error.message}
+
Unable to continue. Try the switch again.
) : null}
diff --git a/apps/web/src/router.tsx b/apps/web/src/router.tsx
index dce8a58..8bbcb99 100644
--- a/apps/web/src/router.tsx
+++ b/apps/web/src/router.tsx
@@ -9,6 +9,27 @@ import { HomePage } from "@/routes/index";
import { BrowsePage } from "@/routes/browse/$category";
import { AgentDetailPage } from "@/routes/agents/$agentId";
import { LockupsPage } from "@/routes/lockups";
+import { ProfilePage } from "@/routes/profile";
+
+export type CatalogSearch = {
+ q?: string;
+};
+
+export type ProfileSearch = {
+ tab?: string;
+};
+
+function catalogSearch(search: Record
): CatalogSearch {
+ return {
+ q: typeof search.q === "string" && search.q.length > 0 ? search.q : undefined,
+ };
+}
+
+function profileSearch(search: Record): ProfileSearch {
+ return {
+ tab: typeof search.tab === "string" ? search.tab : undefined,
+ };
+}
const rootRoute = createRootRoute({
component: AppShell,
@@ -17,12 +38,14 @@ const rootRoute = createRootRoute({
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/",
+ validateSearch: catalogSearch,
component: HomePage,
});
const browseRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/browse/$category",
+ validateSearch: catalogSearch,
component: BrowsePage,
});
@@ -38,11 +61,19 @@ const lockupsRoute = createRoute({
component: LockupsPage,
});
+const profileRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: "/profile",
+ validateSearch: profileSearch,
+ component: ProfilePage,
+});
+
const routeTree = rootRoute.addChildren([
indexRoute,
browseRoute,
agentRoute,
lockupsRoute,
+ profileRoute,
]);
export const router = createRouter({
diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx
index 06461ba..f111aed 100644
--- a/apps/web/src/routes/__root.tsx
+++ b/apps/web/src/routes/__root.tsx
@@ -1,8 +1,9 @@
-import { Outlet, Link, useRouterState } from "@tanstack/react-router";
-import { ConnectButton } from "@rainbow-me/rainbowkit";
+import { Outlet, useRouterState } from "@tanstack/react-router";
import { BnbChainLockup } from "@/components/brand/bnb-chain-lockup";
-import { PulseWordmark } from "@/components/brand/pulse-wordmark";
-import { chainStatusLabel } from "@/providers/network";
+import { AppBottomNav } from "@/components/layout/app-bottom-nav";
+import { AppHeader } from "@/components/layout/app-header";
+import { ToastProvider } from "@/components/ui";
+import { InboxProvider } from "@/features/account/inbox";
import { WrongNetworkBanner } from "@/providers/wrong-network-banner";
export function AppShell() {
@@ -12,47 +13,46 @@ export function AppShell() {
}
return (
-
-
-
-
-
-
-
-
- {chainStatusLabel()}
-
-
-
-
-
-
-
-
-
-