diff --git a/app/components/AppShell.tsx b/app/components/AppShell.tsx
index 1bd197d..b2c293e 100644
--- a/app/components/AppShell.tsx
+++ b/app/components/AppShell.tsx
@@ -10,6 +10,7 @@ import { getActiveParent, NAV_ITEMS, NavChild, NavIcon } from '../navigation';
import { BLUE, BORDER, DISABLED, INK, MUTED, SELECTED } from '../theme';
import { spectrum } from '../spectrum';
import { getChangeBySlug } from '../upgrades/data/changes';
+import { demoLabel } from '../demos/catalogue';
import { getUpgradeById } from '../upgrades/data/upgrades';
import { titleForPath } from '../navigation';
@@ -508,7 +509,7 @@ function GlobalBanner({ dismissed, onDismiss, className }: GlobalBannerProps) {
New!EIP-8130: Native Account Abstraction
-
+
Test on Vibenet
@@ -630,20 +631,20 @@ export function AppShell({ children }: PropsWithChildren) {
/>
);
}
+ const demoMatch = pathname.match(/^\/demos\/(.+)$/);
+ if (demoMatch) {
+ const slug = demoMatch[1].split('/')[0];
+ return (
+
+ );
+ }
if (pathname.startsWith('/vibenet') && pathname !== '/vibenet') {
let childLabel = title;
let middle: { label: string; href: string } | undefined;
- const demosMatch = pathname.match(/^\/vibenet\/demos(?:\/(.+))?$/);
- if (demosMatch) {
- if (!demosMatch[1]) {
- childLabel = 'Demos';
- } else {
- middle = { label: 'Demos', href: '/vibenet/demos' };
- const segments = demosMatch[1].split('/');
- const first = segments[0];
- childLabel = first.charAt(0).toUpperCase() + first.slice(1);
- }
- }
const explorerDetailMatch = pathname.match(/^\/vibenet\/explorer\/(tx|block|address)\/(.+)$/);
if (explorerDetailMatch) {
middle = { label: 'Explorer', href: '/vibenet/explorer' };
diff --git a/app/vibenet/demos/_components/AnimatedAmount.tsx b/app/demos/_components/AnimatedAmount.tsx
similarity index 97%
rename from app/vibenet/demos/_components/AnimatedAmount.tsx
rename to app/demos/_components/AnimatedAmount.tsx
index 5880128..0fc5905 100644
--- a/app/vibenet/demos/_components/AnimatedAmount.tsx
+++ b/app/demos/_components/AnimatedAmount.tsx
@@ -3,7 +3,7 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { animate, motion, useMotionValue, useTransform } from 'motion/react';
-import { cn } from '../../../components/ui/cn';
+import { cn } from '../../components/ui/cn';
type AnimatedAmountProps = {
text: string;
diff --git a/app/vibenet/demos/_components/DemoHeader.tsx b/app/demos/_components/DemoHeader.tsx
similarity index 81%
rename from app/vibenet/demos/_components/DemoHeader.tsx
rename to app/demos/_components/DemoHeader.tsx
index 204bd06..ed892a4 100644
--- a/app/vibenet/demos/_components/DemoHeader.tsx
+++ b/app/demos/_components/DemoHeader.tsx
@@ -1,6 +1,6 @@
import type { ReactNode } from 'react';
-import { Text } from '../../../components/ui/Text';
+import { Text } from '../../components/ui/Text';
type DemoHeaderProps = {
eyebrow?: string;
@@ -10,7 +10,9 @@ type DemoHeaderProps = {
};
export function DemoHeader({
- eyebrow = 'Base Vibenet · Demo',
+ // Network-neutral default: demos run on whichever test network carries the
+ // feature they show. A demo names its own network by passing `eyebrow`.
+ eyebrow = 'Base · Demo',
title,
description,
actions,
diff --git a/app/vibenet/demos/_components/DemoTabs.tsx b/app/demos/_components/DemoTabs.tsx
similarity index 97%
rename from app/vibenet/demos/_components/DemoTabs.tsx
rename to app/demos/_components/DemoTabs.tsx
index 1e81b0a..625bfcb 100644
--- a/app/vibenet/demos/_components/DemoTabs.tsx
+++ b/app/demos/_components/DemoTabs.tsx
@@ -2,7 +2,7 @@
import { motion } from 'motion/react';
-import { cn } from '../../../components/ui/cn';
+import { cn } from '../../components/ui/cn';
export type DemoTabItem = {
value: string;
diff --git a/app/vibenet/demos/_components/Stat.tsx b/app/demos/_components/Stat.tsx
similarity index 100%
rename from app/vibenet/demos/_components/Stat.tsx
rename to app/demos/_components/Stat.tsx
diff --git a/app/vibenet/demos/account/AccountDemo.tsx b/app/demos/account/AccountDemo.tsx
similarity index 99%
rename from app/vibenet/demos/account/AccountDemo.tsx
rename to app/demos/account/AccountDemo.tsx
index 5e17105..db1ae5a 100644
--- a/app/vibenet/demos/account/AccountDemo.tsx
+++ b/app/demos/account/AccountDemo.tsx
@@ -58,20 +58,19 @@ import Link from 'next/link';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { AnimatePresence, motion } from 'motion/react';
-import { Button } from '../../../components/ui/Button';
-import { Card } from '../../../components/ui/Card';
-import { cn } from '../../../components/ui/cn';
-import { AnimatedArrowIcon, CloseIcon } from '../../../components/ui/icons';
-import { Modal } from '../../../components/ui/Modal';
-import { Select } from '../../../components/ui/Select';
-import { Text } from '../../../components/ui/Text';
+import { Button } from '../../components/ui/Button';
+import { Card } from '../../components/ui/Card';
+import { cn } from '../../components/ui/cn';
+import { AnimatedArrowIcon, CloseIcon } from '../../components/ui/icons';
+import { Modal } from '../../components/ui/Modal';
+import { Select } from '../../components/ui/Select';
+import { Text } from '../../components/ui/Text';
import { toast } from 'sonner';
-import { vibenetApi } from '../../library/client';
-import { ACCOUNT_RPC_URL, VIBENET_EXPLORER_PATH } from '../../library/config';
+import { vibenetApi } from '../../vibenet/library/client';
+import { ACCOUNT_RPC_URL, VIBENET_EXPLORER_PATH } from '../../vibenet/library/config';
import { AnimatedAmount } from '../_components/AnimatedAmount';
-import { DemoHeader } from '../_components/DemoHeader';
-import { Spinner } from '../../../components/ui/Spinner';
-import { Tabs } from '../../../components/ui/Tabs';
+import { Spinner } from '../../components/ui/Spinner';
+import { Tabs } from '../../components/ui/Tabs';
import { Stat } from '../_components/Stat';
import { ActivityLog } from './components/ActivityLog';
import { AppsView } from './components/AppsView';
diff --git a/app/vibenet/demos/account/components/ActivityLog.tsx b/app/demos/account/components/ActivityLog.tsx
similarity index 97%
rename from app/vibenet/demos/account/components/ActivityLog.tsx
rename to app/demos/account/components/ActivityLog.tsx
index fa1e296..d94c7d7 100644
--- a/app/vibenet/demos/account/components/ActivityLog.tsx
+++ b/app/demos/account/components/ActivityLog.tsx
@@ -2,10 +2,10 @@ import Link from 'next/link';
import { useEffect, useRef, useState } from 'react';
import { motion, useReducedMotion } from 'motion/react';
-import { Button } from '../../../../components/ui/Button';
-import { cn } from '../../../../components/ui/cn';
-import { Text } from '../../../../components/ui/Text';
-import { VIBENET_EXPLORER_PATH } from '../../../library/config';
+import { Button } from '../../../components/ui/Button';
+import { cn } from '../../../components/ui/cn';
+import { Text } from '../../../components/ui/Text';
+import { VIBENET_EXPLORER_PATH } from '../../../vibenet/library/config';
import { type ActivityEntry, type StoredAccount, formatTime } from '../library/model';
import { short } from '../shared';
import { AccountIdentity, Badge } from './primitives';
diff --git a/app/vibenet/demos/account/components/AppsView.tsx b/app/demos/account/components/AppsView.tsx
similarity index 97%
rename from app/vibenet/demos/account/components/AppsView.tsx
rename to app/demos/account/components/AppsView.tsx
index 64864ce..212eea2 100644
--- a/app/vibenet/demos/account/components/AppsView.tsx
+++ b/app/demos/account/components/AppsView.tsx
@@ -1,9 +1,9 @@
'use client';
-import { Button } from '../../../../components/ui/Button';
-import { Card } from '../../../../components/ui/Card';
-import { cn } from '../../../../components/ui/cn';
-import { Text } from '../../../../components/ui/Text';
+import { Button } from '../../../components/ui/Button';
+import { Card } from '../../../components/ui/Card';
+import { cn } from '../../../components/ui/cn';
+import { Text } from '../../../components/ui/Text';
import type { DemoApp } from '../library/apps';
import type { DemoChain } from '../library/chains';
import { formatExpiry, type AppSessionKey, type AppSubAccount, type StoredAccount } from '../library/model';
diff --git a/app/vibenet/demos/account/components/ConfigView.tsx b/app/demos/account/components/ConfigView.tsx
similarity index 98%
rename from app/vibenet/demos/account/components/ConfigView.tsx
rename to app/demos/account/components/ConfigView.tsx
index f7d155c..82fd205 100644
--- a/app/vibenet/demos/account/components/ConfigView.tsx
+++ b/app/demos/account/components/ConfigView.tsx
@@ -5,14 +5,14 @@ import type { Address, Hex } from '@aa';
import Link from 'next/link';
import { AnimatePresence, motion } from 'motion/react';
-import { Button } from '../../../../components/ui/Button';
-import { Card } from '../../../../components/ui/Card';
-import { cn } from '../../../../components/ui/cn';
-import { CloseIcon } from '../../../../components/ui/icons';
-import { Spinner } from '../../../../components/ui/Spinner';
-import { Select } from '../../../../components/ui/Select';
-import { Text } from '../../../../components/ui/Text';
-import { VIBENET_EXPLORER_PATH } from '../../../library/config';
+import { Button } from '../../../components/ui/Button';
+import { Card } from '../../../components/ui/Card';
+import { cn } from '../../../components/ui/cn';
+import { CloseIcon } from '../../../components/ui/icons';
+import { Spinner } from '../../../components/ui/Spinner';
+import { Select } from '../../../components/ui/Select';
+import { Text } from '../../../components/ui/Text';
+import { VIBENET_EXPLORER_PATH } from '../../../vibenet/library/config';
import { AnimatedAmount } from '../../_components/AnimatedAmount';
import {
DEMO_CHAINS,
diff --git a/app/vibenet/demos/account/components/primitives.tsx b/app/demos/account/components/primitives.tsx
similarity index 98%
rename from app/vibenet/demos/account/components/primitives.tsx
rename to app/demos/account/components/primitives.tsx
index 7dbc5d9..1d5c6ad 100644
--- a/app/vibenet/demos/account/components/primitives.tsx
+++ b/app/demos/account/components/primitives.tsx
@@ -1,8 +1,8 @@
import type { ReactNode } from 'react';
import { AnimatePresence, motion } from 'motion/react';
-import { cn } from '../../../../components/ui/cn';
-import { Text } from '../../../../components/ui/Text';
+import { cn } from '../../../components/ui/cn';
+import { Text } from '../../../components/ui/Text';
import type { SignerKind } from '../library/model';
import { KIND_LABEL, short } from '../shared';
diff --git a/app/demos/account/layout.tsx b/app/demos/account/layout.tsx
new file mode 100644
index 0000000..01b9d9c
--- /dev/null
+++ b/app/demos/account/layout.tsx
@@ -0,0 +1,30 @@
+import type { Metadata } from 'next';
+import type { ReactNode } from 'react';
+
+import { MaintenanceBanner } from '../../vibenet/components/MaintenanceBanner';
+
+// The account demo page is a client component; this passthrough layout carries
+// its title/description.
+//
+// Title convention: a section root is "{Section} · Base Chain" and a page within
+// a section is "{Page} · {Section}" — e.g. "Faucet · Vibenet". Spelled out in
+// full rather than composed from a parent title.template, because the llms/agents
+// generator resolves a template to its `default` and would publish the bare page
+// title, disagreeing with what the browser renders.
+export const metadata: Metadata = {
+ title: 'Native Account Abstraction · Demos',
+ description:
+ 'Create native account abstraction accounts from in-browser keys, fund them from the faucet, and inspect balances on Vibenet.',
+};
+
+// This demo runs on Vibenet, so it carries Vibenet's maintenance notice — the
+// demo is unusable while the devnet is down. Scoped here rather than to the
+// /demos section, since other demos may run on other test networks.
+export default function AccountDemoLayout({ children }: { children: ReactNode }) {
+ return (
+ <>
+
+ {children}
+ >
+ );
+}
diff --git a/app/vibenet/demos/account/library/apps.ts b/app/demos/account/library/apps.ts
similarity index 100%
rename from app/vibenet/demos/account/library/apps.ts
rename to app/demos/account/library/apps.ts
diff --git a/app/vibenet/demos/account/library/calls.ts b/app/demos/account/library/calls.ts
similarity index 100%
rename from app/vibenet/demos/account/library/calls.ts
rename to app/demos/account/library/calls.ts
diff --git a/app/vibenet/demos/account/library/chains.ts b/app/demos/account/library/chains.ts
similarity index 99%
rename from app/vibenet/demos/account/library/chains.ts
rename to app/demos/account/library/chains.ts
index fcb60d9..411e4f9 100644
--- a/app/vibenet/demos/account/library/chains.ts
+++ b/app/demos/account/library/chains.ts
@@ -5,7 +5,7 @@ import {
vibenetDevnetDeployment,
} from "@aa";
-import { isAddress } from "../../../library/format";
+import { isAddress } from "../../../vibenet/library/format";
/**
* Last-known-good vibenet EIP-8130 system contracts — a STATIC FALLBACK only.
@@ -94,7 +94,7 @@ export function deploymentFromContracts(
import {
ACCOUNT_BUNDLER_URL,
ACCOUNT_PAYER_URL,
-} from "../../../library/config";
+} from "../../../vibenet/library/config";
/**
* How EIP-8130 accounts execute on a given chain:
diff --git a/app/vibenet/demos/account/library/model.ts b/app/demos/account/library/model.ts
similarity index 100%
rename from app/vibenet/demos/account/library/model.ts
rename to app/demos/account/library/model.ts
diff --git a/app/vibenet/demos/account/library/policy.ts b/app/demos/account/library/policy.ts
similarity index 99%
rename from app/vibenet/demos/account/library/policy.ts
rename to app/demos/account/library/policy.ts
index 728821e..3a520ba 100644
--- a/app/vibenet/demos/account/library/policy.ts
+++ b/app/demos/account/library/policy.ts
@@ -12,7 +12,7 @@ import {
parseUnits,
} from '@aa';
-import { vibenetApi } from '../../../library/client';
+import { vibenetApi } from '../../../vibenet/library/client';
import { BASE_SEPOLIA_USDC } from './chains';
import { SCOPE, scopeChips } from './model';
import { short } from '../shared';
diff --git a/app/vibenet/demos/account/library/templates.ts b/app/demos/account/library/templates.ts
similarity index 100%
rename from app/vibenet/demos/account/library/templates.ts
rename to app/demos/account/library/templates.ts
diff --git a/app/vibenet/demos/account/loading.tsx b/app/demos/account/loading.tsx
similarity index 97%
rename from app/vibenet/demos/account/loading.tsx
rename to app/demos/account/loading.tsx
index 7ab815f..4b358b7 100644
--- a/app/vibenet/demos/account/loading.tsx
+++ b/app/demos/account/loading.tsx
@@ -1,4 +1,4 @@
-import { Skeleton } from '../../../components/ui/Skeleton';
+import { Skeleton } from '../../components/ui/Skeleton';
export default function Loading() {
return (
diff --git a/app/vibenet/demos/account/page.tsx b/app/demos/account/page.tsx
similarity index 100%
rename from app/vibenet/demos/account/page.tsx
rename to app/demos/account/page.tsx
diff --git a/app/vibenet/demos/account/shared.ts b/app/demos/account/shared.ts
similarity index 95%
rename from app/vibenet/demos/account/shared.ts
rename to app/demos/account/shared.ts
index 879c87f..a230abb 100644
--- a/app/vibenet/demos/account/shared.ts
+++ b/app/demos/account/shared.ts
@@ -2,7 +2,7 @@
import type { Address, Hex } from '@aa';
-import type { AccountBalancesResponse } from '../../library/api-types';
+import type { AccountBalancesResponse } from '../../vibenet/library/api-types';
import type { ActivityEntry, SignerKind, StoredAccount } from './library/model';
/** An in-browser signer key held in the demo wallet. */
diff --git a/app/demos/catalogue.test.ts b/app/demos/catalogue.test.ts
new file mode 100644
index 0000000..60674c6
--- /dev/null
+++ b/app/demos/catalogue.test.ts
@@ -0,0 +1,44 @@
+import { describe, expect, it } from 'vitest';
+
+import { DEMOS, demoLabel } from './catalogue';
+
+describe('demoLabel', () => {
+ it('uses the catalogue entry so the crumb matches the demo name', () => {
+ expect(demoLabel('account')).toBe('Account');
+ });
+
+ it('prefers shortTitle over title when both are set', () => {
+ const account = DEMOS.find((d) => d.href === '/demos/account');
+ expect(account?.title).toBe('Native Account Abstraction');
+ expect(demoLabel('account')).toBe(account?.shortTitle);
+ });
+
+ it('title-cases a multi-word slug that has no catalogue entry', () => {
+ // The regression this guards: the previous implementation only uppercased
+ // the first character, rendering "smart-wallet" as "Smart-wallet".
+ expect(demoLabel('smart-wallet')).toBe('Smart Wallet');
+ expect(demoLabel('paymaster-v2')).toBe('Paymaster V2');
+ });
+
+ it('handles a single-word unknown slug', () => {
+ expect(demoLabel('bridge')).toBe('Bridge');
+ });
+
+ it('does not emit stray spaces for awkward slugs', () => {
+ expect(demoLabel('a--b')).toBe('A B');
+ expect(demoLabel('-lead')).toBe('Lead');
+ });
+});
+
+describe('DEMOS', () => {
+ it('gives every entry a /demos/ href, so demoLabel can resolve it', () => {
+ for (const demo of DEMOS) {
+ expect(demo.href.startsWith('/demos/')).toBe(true);
+ }
+ });
+
+ it('has no duplicate hrefs', () => {
+ const hrefs = DEMOS.map((d) => d.href);
+ expect(new Set(hrefs).size).toBe(hrefs.length);
+ });
+});
diff --git a/app/demos/catalogue.ts b/app/demos/catalogue.ts
new file mode 100644
index 0000000..eeed0ad
--- /dev/null
+++ b/app/demos/catalogue.ts
@@ -0,0 +1,58 @@
+// The demo catalogue — one place to register a demo. The index page renders
+// cards from it and the breadcrumb resolves its label from it, so the two can't
+// drift. Plain data with no server-only imports, so client components (AppShell)
+// can read it too.
+
+export type DemoEntry = {
+ /** Route for the demo, e.g. `/demos/account`. */
+ href: string;
+ /** Short tag shown beside the title on the card, usually the spec or network. */
+ eyebrow: string;
+ /**
+ * Full name of the demo. Also the page title, which by convention reads
+ * `{title} · Demos` — set it in the demo's own layout metadata, since this
+ * catalogue is data and does not drive `
`.
+ */
+ title: string;
+ /** Compact label for the breadcrumb; falls back to `title` when unset. */
+ shortTitle?: string;
+ summary: string;
+ points: string[];
+ available: boolean;
+};
+
+export const DEMOS: DemoEntry[] = [
+ {
+ href: '/demos/account',
+ eyebrow: 'EIP-8130',
+ title: 'Native Account Abstraction',
+ shortTitle: 'Account',
+ summary:
+ 'Create portable account-abstraction accounts from in-browser keys, fund them from the faucet, and inspect balances across networks.',
+ points: [
+ 'Smart & EOA accounts — deterministic addresses',
+ 'K1 / P-256 / passkey signers',
+ 'Live balances on Vibenet + Base Sepolia',
+ ],
+ available: true,
+ },
+];
+
+/** `smart-wallet` -> `Smart Wallet`. Fallback for a route with no catalogue entry. */
+function prettifySlug(slug: string): string {
+ return slug
+ .split('-')
+ .filter(Boolean)
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
+ .join(' ');
+}
+
+/**
+ * Breadcrumb label for the first path segment under /demos. Prefers the
+ * catalogue so the crumb matches the demo's real name, and degrades to a
+ * title-cased slug for anything not registered.
+ */
+export function demoLabel(slug: string): string {
+ const demo = DEMOS.find((entry) => entry.href === `/demos/${slug}`);
+ return demo?.shortTitle ?? demo?.title ?? prettifySlug(slug);
+}
diff --git a/app/demos/layout.tsx b/app/demos/layout.tsx
new file mode 100644
index 0000000..d805fe2
--- /dev/null
+++ b/app/demos/layout.tsx
@@ -0,0 +1,18 @@
+import type { ReactNode } from 'react';
+
+// Metadata lives on the routes themselves: app/demos/page.tsx for the index and
+// app/demos/account/layout.tsx for the demo.
+
+type DemosLayoutProps = {
+ children: ReactNode;
+};
+
+// Section chrome only. Demos run on whichever test network carries the feature
+// they show, so nothing network-specific belongs here — a demo that depends on
+// a particular network owns that concern (see app/demos/account/layout.tsx,
+// which carries Vibenet's maintenance notice).
+export default function DemosLayout({ children }: DemosLayoutProps) {
+ return (
+
{children}
+ );
+}
diff --git a/app/vibenet/demos/loading.tsx b/app/demos/loading.tsx
similarity index 93%
rename from app/vibenet/demos/loading.tsx
rename to app/demos/loading.tsx
index 543310e..ba7a398 100644
--- a/app/vibenet/demos/loading.tsx
+++ b/app/demos/loading.tsx
@@ -1,4 +1,4 @@
-import { Skeleton } from '../../components/ui/Skeleton';
+import { Skeleton } from '../components/ui/Skeleton';
export default function DemosLoading() {
return (
diff --git a/app/vibenet/demos/page.tsx b/app/demos/page.tsx
similarity index 65%
rename from app/vibenet/demos/page.tsx
rename to app/demos/page.tsx
index 6acdc0d..f65db13 100644
--- a/app/vibenet/demos/page.tsx
+++ b/app/demos/page.tsx
@@ -1,42 +1,16 @@
import type { Metadata } from 'next';
-import { Card, LinkCard } from '../../components/ui/Card';
-import { Text } from '../../components/ui/Text';
+import { Card, LinkCard } from '../components/ui/Card';
+import { Text } from '../components/ui/Text';
import { Badge } from './account/components/primitives';
+import { DEMOS, type DemoEntry } from './catalogue';
export const metadata: Metadata = {
- title: 'Demos · Vibenet',
- description: 'Interactive demos of in-flight Base features running on the vibenet developer network.',
+ title: 'Demos · Base Chain',
+ description:
+ 'Interactive demos of in-flight Base features. Each demo runs on the test network that carries the feature it shows.',
};
-type DemoEntry = {
- href: string;
- eyebrow: string;
- title: string;
- summary: string;
- points: string[];
- available: boolean;
-};
-
-// Demo catalogue. The account demo is the first (and, for now, only live)
-// entry; further demos are listed as upcoming so the surface reflects the
-// roadmap — matching how the home/features surfaces handle staged work.
-const DEMOS: DemoEntry[] = [
- {
- href: '/vibenet/demos/account',
- eyebrow: 'EIP-8130',
- title: 'Native Account Abstraction',
- summary:
- 'Create portable account-abstraction accounts from in-browser keys, fund them from the faucet, and inspect balances across networks.',
- points: [
- 'Smart & EOA accounts — deterministic addresses',
- 'K1 / P-256 / passkey signers',
- 'Live balances on Vibenet + Base Sepolia',
- ],
- available: true,
- },
-];
-
export default function DemosIndexPage() {
return (