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
31 changes: 15 additions & 16 deletions app/vibenet/demos/_components/AccountDemoShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
// - the collapsible ActivityDrawer pinned to the bottom, for demos that hand
// it activity (B20 keeps its log in the page flow instead, so it passes
// none and the drawer is skipped).
// Each demo owns one AccountEngine and passes it here, avoiding duplicate store
// instances and repeated account-settings wiring.
// Each demo renders this inside one AccountEngineProvider, avoiding duplicate
// store instances and repeated account-settings wiring.

import { useEffect, useState, type ReactNode } from 'react';
import { createPortal } from 'react-dom';
Expand All @@ -19,15 +19,10 @@ import { cn } from '../../../components/ui/cn';
import { AccountSwitcher } from '../_shared/AccountSwitcher';
import { ActivityDrawer } from '../_shared/ActivityDrawer';
import { DemoGate } from '../_shared/DemoGate';
import { AccountDetailsModal } from '../account/components/AccountDetailsModal';
import { CreateAccountModal } from '../account/components/CreateAccountModal';
import type { AccountEngine } from '../account/useAccountEngine';
import { useAccountEngine } from '../account/useAccountEngine';

type AccountDemoShellProps = {
engine: AccountEngine;
/** Page-specific navigation from Account Details. Omit when the demo has no
* transaction builder of its own (for example B20). */
onTransactFromDetails?: () => void;
// Empty-state copy.
gateTitle?: string;
gateDescription?: string;
Expand All @@ -41,8 +36,6 @@ type AccountDemoShellProps = {
};

export function AccountDemoShell({
engine,
onTransactFromDetails,
gateTitle,
gateDescription,
activity,
Expand All @@ -51,7 +44,11 @@ export function AccountDemoShell({
className,
children,
}: AccountDemoShellProps) {
const engine = useAccountEngine();
const [topbarSlot, setTopbarSlot] = useState<HTMLElement | null>(null);
// The switcher and the empty-state gate both open the create-account modal.
const [createOpen, setCreateOpen] = useState(false);
const onCreate = () => setCreateOpen(true);
useEffect(() => {
setTopbarSlot(document.getElementById('topbar-actions-slot'));
}, []);
Expand All @@ -61,9 +58,12 @@ export function AccountDemoShell({
accounts={engine.accounts}
activeAccountId={engine.activeAccountId}
onSelect={engine.setActiveAccountId}
onCreate={engine.openCreate}
onDelete={engine.removeAccount}
onDetails={engine.openAccountDetails}
onCreate={onCreate}
onDelete={engine.deleteAccount}
onDetails={(id) => {
const addr = engine.accounts.find((a) => a.id === id)?.address;
if (addr) window.open(`/vibenet/explorer/address/${addr}`, '_blank', 'noopener,noreferrer');
}}
/>
);

Expand All @@ -82,7 +82,7 @@ export function AccountDemoShell({
<DemoGate
accounts={engine.accounts}
hydrated={engine.hydrated}
onCreate={engine.openCreate}
onCreate={onCreate}
title={gateTitle}
description={gateDescription}
>
Expand All @@ -97,8 +97,7 @@ export function AccountDemoShell({
</DemoGate>
</div>

<AccountDetailsModal engine={engine} onTransact={onTransactFromDetails} />
<CreateAccountModal engine={engine} />
<CreateAccountModal open={createOpen} onClose={() => setCreateOpen(false)} />
</>
);
}
70 changes: 70 additions & 0 deletions app/vibenet/demos/_shared/ConfirmTrashButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
'use client';

// Two-click confirm icon button for a destructive row action (revoke an owner,
// revoke a session key, …): first click arms it, a second click within the
// window commits, and clicking anywhere else cancels it. Extracted from the
// inline delete button in AccountSwitcher, which still owns its own copy
// (it's list-indexed by account id rather than a single boolean).

import { useEffect, useRef, useState } from 'react';

import { cn } from '../../../components/ui/cn';
import { TrashIcon } from './primitives';

export function ConfirmTrashButton({
onConfirm,
label,
size = 15,
className,
disabled = false,
disabledTitle,
}: {
onConfirm: () => void;
/** Used in aria-label / title, e.g. "Revoke Owner 2". */
label: string;
size?: number;
className?: string;
disabled?: boolean;
/** Title shown while disabled, e.g. "An account needs at least one owner". */
disabledTitle?: string;
}) {
const [confirming, setConfirming] = useState(false);
const buttonRef = useRef<HTMLButtonElement | null>(null);

useEffect(() => {
if (!confirming) return;
const onDocMouseDown = (e: MouseEvent) => {
if (buttonRef.current?.contains(e.target as Node)) return;
setConfirming(false);
};
document.addEventListener('mousedown', onDocMouseDown);
return () => document.removeEventListener('mousedown', onDocMouseDown);
}, [confirming]);

return (
<button
type="button"
ref={buttonRef}
disabled={disabled}
onClick={() => {
if (confirming) {
onConfirm();
setConfirming(false);
} else {
setConfirming(true);
}
}}
aria-label={confirming ? `Confirm ${label}` : label}
title={disabled ? disabledTitle : confirming ? 'Click again to confirm' : label}
className={cn(
'rounded-md p-1.5 transition-colors disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent disabled:hover:text-bds-gray-40',
confirming
? 'bg-bds-red-0 text-bds-red-60'
: 'text-bds-gray-40 hover:bg-bds-red-0 hover:text-bds-red-60',
className,
)}
>
<TrashIcon size={size} />
</button>
);
}
8 changes: 2 additions & 6 deletions app/vibenet/demos/_shared/TransactionModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
// straight to it from a preset). Callers may fully override the review and
// submitted bodies, or supply a custom success renderer for the default one.

import Link from 'next/link';
import type { ReactNode } from 'react';

import { Button } from '../../../components/ui/Button';
import { Modal } from '../../../components/ui/Modal';
import { Spinner } from '../../../components/ui/Spinner';
import { Text } from '../../../components/ui/Text';
import { ViewTransactionButton } from './ViewTransactionButton';

export type TxStep = 'build' | 'review' | 'submitted';
export type TxResult = { txHash?: string } | null;
Expand Down Expand Up @@ -202,11 +202,7 @@ export function TransactionModal({
<>
{successExtra}
{result?.txHash && explorerTxPath ? (
<Link href={explorerTxPath(result.txHash)} target="_blank" rel="noopener noreferrer">
<Button variant="secondary" size="sm">
View Transaction
</Button>
</Link>
<ViewTransactionButton href={explorerTxPath(result.txHash)} />
) : null}
<Button variant="primary" size="sm" onClick={onDone}>
Done
Expand Down
25 changes: 25 additions & 0 deletions app/vibenet/demos/_shared/ViewTransactionButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Button } from '../../../components/ui/Button';

type ViewTransactionButtonProps = {
href: string;
label?: string;
};

/** Consistent explorer action for completed transactions. Explorer details open
* separately so closing the result dialog does not lose the current demo state. */
export function ViewTransactionButton({
href,
label = 'View Transaction',
}: ViewTransactionButtonProps) {
return (
<Button
href={href}
target="_blank"
rel="noopener noreferrer"
variant="secondary"
size="sm"
>
{label}
</Button>
);
}
Loading
Loading