turn client shell into interactive desktop

This commit is contained in:
Agent
2026-07-10 15:39:03 +00:00
parent 7f3d9accde
commit 16bb6793d9
4 changed files with 228 additions and 152 deletions

View File

@@ -1,107 +1,128 @@
import type { ReactNode } from 'react';
import { Activity, Archive, Compass, Grid2X2, Network, Radio, Sparkles } from 'lucide-react';
import { Link, NavLink } from 'react-router-dom';
import { useEffect, useMemo, useState, type ReactNode } from 'react';
import { Command, Grid2X2, Maximize2, Minimize2, Radio, X } from 'lucide-react';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import { LoginArea } from '@/components/auth/LoginArea';
import { cn } from '@/lib/utils';
const apps = [
{ name: 'Home', path: '/', icon: Grid2X2, short: 'Home' },
{ name: 'Discover', path: '/explore', icon: Compass, short: 'Find' },
{ name: 'Activity', path: '/dashboard', icon: Activity, short: 'Me' },
{ name: 'Studio', path: '/dashboard/events', icon: Sparkles, short: 'Studio' },
{ name: 'Archive', path: '/dashboard/export', icon: Archive, short: 'Save' },
{ name: 'Network', path: '/network', icon: Network, short: 'Relay' },
];
import { nostrApps } from '@/lib/nostrApps';
interface OsShellProps {
children: ReactNode;
title: string;
eyebrow?: string;
className?: string;
desktop?: boolean;
}
export function OsShell({ children, title, eyebrow = 'Nostr operating system', className }: OsShellProps) {
function useClock() {
const [time, setTime] = useState(() => new Date());
useEffect(() => {
const timer = window.setInterval(() => setTime(new Date()), 30_000);
return () => window.clearInterval(timer);
}, []);
return new Intl.DateTimeFormat(undefined, { hour: '2-digit', minute: '2-digit' }).format(time);
}
export function OsShell({ children, title, eyebrow = 'Nostr operating system', className, desktop = false }: OsShellProps) {
const location = useLocation();
const navigate = useNavigate();
const time = useClock();
const [launcherOpen, setLauncherOpen] = useState(false);
const [minimized, setMinimized] = useState(false);
const [maximized, setMaximized] = useState(false);
const [position, setPosition] = useState({ x: 0, y: 0 });
const currentApp = useMemo(
() => nostrApps.find((app) => location.pathname === app.path) ?? (desktop ? undefined : nostrApps[0]),
[desktop, location.pathname],
);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') {
event.preventDefault();
setLauncherOpen((open) => !open);
}
if (event.key === 'Escape') setLauncherOpen(false);
};
window.addEventListener('keydown', handleKeyDown);
const openLauncher = () => setLauncherOpen(true);
window.addEventListener('nostr-os:launcher', openLauncher);
return () => {
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('nostr-os:launcher', openLauncher);
};
}, []);
const launch = (path: string) => {
setLauncherOpen(false);
setMinimized(false);
navigate(path);
};
const beginDrag = (event: React.PointerEvent<HTMLDivElement>) => {
if (event.button !== 0 || window.innerWidth < 1024 || maximized) return;
const start = { x: event.clientX, y: event.clientY, position };
const move = (moveEvent: PointerEvent) => {
setPosition({ x: start.position.x + moveEvent.clientX - start.x, y: start.position.y + moveEvent.clientY - start.y });
};
const stop = () => {
window.removeEventListener('pointermove', move);
window.removeEventListener('pointerup', stop);
};
window.addEventListener('pointermove', move);
window.addEventListener('pointerup', stop);
};
return (
<div className="os-shell min-h-dvh bg-background text-foreground">
<aside className="os-rail fixed inset-y-0 left-0 z-30 hidden w-[232px] flex-col border-r border-white/10 px-3 py-4 lg:flex">
<Link to="/" className="group mb-8 flex items-center gap-3 px-2" aria-label="Open Nostr OS home">
<span className="grid h-10 w-10 place-items-center rounded-[14px] bg-primary text-primary-foreground shadow-[0_0_30px_hsl(var(--primary)/0.3)] transition-transform duration-300 group-hover:rotate-6">
<Radio className="h-5 w-5" aria-hidden="true" />
</span>
<span>
<span className="block font-mono text-[10px] uppercase tracking-[0.24em] text-primary">Layer systems</span>
<span className="block text-lg font-semibold tracking-tight">Nostr / OS</span>
</span>
</Link>
<p className="px-3 pb-3 font-mono text-[10px] uppercase tracking-[0.2em] text-muted-foreground">Applications</p>
<nav className="space-y-1" aria-label="Applications">
{apps.map((app) => (
<NavLink
key={app.path}
to={app.path}
end={app.path === '/'}
className={({ isActive }) => cn(
'group flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm text-muted-foreground transition-all hover:bg-white/[0.06] hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
isActive && 'bg-primary text-primary-foreground shadow-lg shadow-primary/20 hover:bg-primary hover:text-primary-foreground',
)}
>
<app.icon className="h-[18px] w-[18px]" aria-hidden="true" />
<span>{app.name}</span>
</NavLink>
))}
</nav>
<div className="mt-auto rounded-2xl border border-white/10 bg-white/[0.035] p-3">
<div className="mb-3 flex items-center gap-2 text-xs text-muted-foreground">
<span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-60" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-emerald-400" />
</span>
Preferred relay
</div>
<Link to="/network" className="block truncate font-mono text-[11px] text-foreground hover:text-primary">
relay.layer.systems
</Link>
<div className="signal-desktop min-h-dvh overflow-hidden bg-[#090b12] text-foreground">
<div className="signal-wallpaper pointer-events-none fixed inset-0" aria-hidden="true"><span className="signal-orb signal-orb-one" /><span className="signal-orb signal-orb-two" /><span className="signal-grid" /></div>
<header className="signal-menubar fixed inset-x-0 top-0 z-50 flex h-11 items-center justify-between border-b border-white/10 px-3 text-white sm:px-5">
<div className="flex min-w-0 items-center gap-2 sm:gap-4">
<button type="button" onClick={() => setLauncherOpen(true)} aria-label="Open application launcher" className="grid h-7 w-7 place-items-center rounded-lg bg-primary text-primary-foreground shadow-lg shadow-primary/20 transition-transform hover:scale-105 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white"><Radio className="h-4 w-4" /></button>
<Link to="/" className="hidden font-mono text-[11px] font-semibold tracking-[0.16em] text-white sm:block">NSTR / OS</Link>
<span className="hidden h-4 w-px bg-white/15 sm:block" />
<p className="truncate text-xs text-white/70">{desktop ? 'Desktop' : title}</p>
</div>
</aside>
<div className="flex items-center gap-2 sm:gap-3"><Link to="/network" className="hidden items-center gap-1.5 rounded-full bg-emerald-400/10 px-2.5 py-1 font-mono text-[10px] text-emerald-200 sm:flex"><span className="h-1.5 w-1.5 rounded-full bg-emerald-400" />relay.layer.systems</Link><span className="font-mono text-[11px] text-white/70">{time}</span><LoginArea className="max-w-[205px]" /></div>
</header>
<div className="min-h-dvh pb-24 lg:ml-[232px] lg:pb-0">
<header className="sticky top-0 z-20 border-b border-border/60 bg-background/80 px-4 py-3 backdrop-blur-xl sm:px-6 lg:px-8">
<div className="mx-auto flex max-w-[1600px] items-center justify-between gap-4">
<div className="min-w-0">
<p className="font-mono text-[10px] uppercase tracking-[0.2em] text-primary">{eyebrow}</p>
<h1 className="truncate text-lg font-semibold tracking-tight sm:text-xl">{title}</h1>
</div>
<div className="flex shrink-0 items-center gap-2">
<Link to="/network" className="hidden items-center gap-2 rounded-full border border-border/80 bg-card px-3 py-1.5 font-mono text-[10px] text-muted-foreground transition-colors hover:border-primary/50 hover:text-foreground sm:flex">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-400" />
relay.layer.systems
</Link>
<LoginArea className="max-w-[220px]" />
</div>
</div>
</header>
<main className={cn('mx-auto w-full max-w-[1600px] px-4 py-5 sm:px-6 sm:py-7 lg:px-8 lg:py-8', className)}>{children}</main>
<div className="relative z-10 flex min-h-dvh flex-col pb-24 pt-11 lg:pb-28">
<main className={cn('signal-workspace flex-1 px-3 py-4 sm:px-5 sm:py-6 lg:px-8 lg:py-8', className)}>
{desktop ? (
children
) : minimized ? (
<div className="grid min-h-[calc(100dvh-11rem)] place-items-center text-center text-white/60"><div><Minimize2 className="mx-auto mb-3 h-6 w-6 text-primary" /><p className="text-sm">{title} is minimized</p><button type="button" onClick={() => setMinimized(false)} className="mt-4 rounded-lg border border-white/15 bg-white/[0.06] px-3 py-2 text-xs font-medium text-white hover:bg-white/[0.12]">Restore window</button></div></div>
) : (
<section className={cn('signal-window mx-auto flex min-h-[calc(100dvh-9.5rem)] max-w-[1540px] flex-col overflow-hidden rounded-2xl border border-white/15 bg-background/95 shadow-[0_32px_100px_rgba(0,0,0,0.48)] backdrop-blur-2xl', maximized && 'max-w-none rounded-none')} style={maximized ? undefined : { transform: `translate(${position.x}px, ${position.y}px)` }}>
<div onPointerDown={beginDrag} className="signal-titlebar flex h-11 shrink-0 touch-none items-center justify-between border-b border-border/70 bg-card/80 px-3 sm:px-4" title="Drag window">
<div className="flex min-w-0 items-center gap-2"><span className="grid h-5 w-5 place-items-center rounded-md bg-primary/15 text-primary">{currentApp ? <currentApp.icon className="h-3 w-3" /> : <Grid2X2 className="h-3 w-3" />}</span><div className="min-w-0"><span className="block truncate text-xs font-medium">{title}</span><span className="hidden font-mono text-[9px] uppercase tracking-[0.13em] text-muted-foreground sm:block">{eyebrow}</span></div></div>
<div className="flex items-center gap-1"><button type="button" onClick={() => setMinimized(true)} className="grid h-7 w-7 place-items-center rounded-md text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground" aria-label="Minimize window"><Minimize2 className="h-3.5 w-3.5" /></button><button type="button" onClick={() => setMaximized((value) => !value)} className="hidden h-7 w-7 place-items-center rounded-md text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground sm:grid" aria-label="Maximize window"><Maximize2 className="h-3.5 w-3.5" /></button><button type="button" onClick={() => navigate('/')} className="grid h-7 w-7 place-items-center rounded-md text-muted-foreground transition-colors hover:bg-rose-500 hover:text-white" aria-label="Close window"><X className="h-3.5 w-3.5" /></button></div>
</div>
<div className="min-h-0 flex-1 overflow-y-auto">{children}</div>
</section>
)}
</main>
</div>
<nav className="fixed inset-x-3 bottom-3 z-30 grid grid-cols-6 rounded-2xl border border-white/10 bg-[hsl(220_18%_10%/0.92)] p-1.5 shadow-2xl shadow-black/30 backdrop-blur-xl lg:hidden" aria-label="Mobile applications">
{apps.map((app) => (
<NavLink
key={app.path}
to={app.path}
end={app.path === '/'}
className={({ isActive }) => cn(
'flex min-w-0 flex-col items-center gap-1 rounded-xl px-1 py-2 font-mono text-[9px] uppercase tracking-tight text-slate-400 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
isActive && 'bg-primary text-primary-foreground',
)}
>
<app.icon className="h-4 w-4" aria-hidden="true" />
<span className="truncate">{app.short}</span>
</NavLink>
))}
<nav className="signal-dock fixed bottom-3 left-1/2 z-40 flex max-w-[calc(100vw-1.5rem)] -translate-x-1/2 items-center gap-1 rounded-2xl border border-white/15 bg-[#111521]/85 p-1.5 shadow-2xl shadow-black/50 backdrop-blur-2xl" aria-label="Application dock">
<button type="button" onClick={() => setLauncherOpen(true)} className="grid h-10 w-10 shrink-0 place-items-center rounded-xl bg-primary text-primary-foreground shadow-lg shadow-primary/20 transition-transform hover:-translate-y-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white" aria-label="Open launcher"><Command className="h-4 w-4" /></button>
<span className="h-7 w-px shrink-0 bg-white/10" />
<button type="button" onClick={() => launch('/')} className={cn('dock-app', desktop && 'dock-app-active')} aria-label="Open desktop"><Grid2X2 className="h-[18px] w-[18px]" /><span className="dock-tooltip">Desktop</span></button>
{nostrApps.map((app) => <button key={app.path} type="button" onClick={() => launch(app.path)} className={cn('dock-app', location.pathname === app.path && !minimized && 'dock-app-active')} aria-label={`Open ${app.name}`}><app.icon className="h-[18px] w-[18px]" /><span className="dock-tooltip">{app.name}</span></button>)}
</nav>
{launcherOpen && (
<div className="signal-launcher fixed inset-0 z-[60] grid place-items-end bg-black/55 p-3 backdrop-blur-sm sm:place-items-center sm:p-6" role="dialog" aria-modal="true" aria-label="Application launcher" onMouseDown={() => setLauncherOpen(false)}>
<section className="w-full max-w-xl rounded-[26px] border border-white/15 bg-[#151a27]/95 p-4 shadow-2xl shadow-black/70 backdrop-blur-2xl sm:p-5" onMouseDown={(event) => event.stopPropagation()}>
<div className="mb-4 flex items-center justify-between"><div><p className="font-mono text-[10px] uppercase tracking-[0.2em] text-primary">Nostr OS</p><h2 className="mt-1 text-xl font-semibold text-white">Applications</h2></div><button type="button" onClick={() => setLauncherOpen(false)} className="rounded-lg p-2 text-white/60 hover:bg-white/10 hover:text-white" aria-label="Close launcher"><X className="h-4 w-4" /></button></div>
<button type="button" onClick={() => launch('/')} className="launcher-app mb-2"><span className="launcher-icon launcher-icon-slate"><Grid2X2 className="h-5 w-5" /></span><span><strong>Desktop</strong><small>Return to your signal desk</small></span></button>
<div className="grid gap-2 sm:grid-cols-2">{nostrApps.map((app) => <button key={app.path} type="button" onClick={() => launch(app.path)} className="launcher-app"><span className={`launcher-icon launcher-icon-${app.hue}`}><app.icon className="h-5 w-5" /></span><span><strong>{app.name}</strong><small>{app.detail}</small></span></button>)}</div>
<p className="mt-4 text-center font-mono text-[10px] text-white/35"> K to toggle launcher · ESC to close</p>
</section>
</div>
)}
</div>
);
}

View File

@@ -179,3 +179,95 @@
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@layer components {
.signal-wallpaper {
background:
radial-gradient(circle at 12% 18%, hsl(187 90% 55% / 0.17), transparent 28rem),
radial-gradient(circle at 85% 22%, hsl(39 96% 55% / 0.13), transparent 23rem),
radial-gradient(circle at 50% 105%, hsl(270 75% 58% / 0.14), transparent 35rem),
linear-gradient(145deg, #080b14 0%, #10152a 47%, #090d17 100%);
}
.signal-grid {
position: absolute;
inset: 0;
opacity: 0.32;
background-image:
linear-gradient(hsl(0 0% 100% / 0.035) 1px, transparent 1px),
linear-gradient(90deg, hsl(0 0% 100% / 0.035) 1px, transparent 1px);
background-size: 42px 42px;
mask-image: linear-gradient(to bottom, black, transparent 80%);
}
.signal-orb {
position: absolute;
border-radius: 999px;
filter: blur(1px);
opacity: 0.7;
animation: signal-drift 16s ease-in-out infinite alternate;
}
.signal-orb-one { top: 18%; left: 37%; width: 7px; height: 7px; background: #fbbf24; box-shadow: 0 0 32px 12px hsl(42 100% 62% / 0.4); }
.signal-orb-two { top: 57%; right: 18%; width: 5px; height: 5px; background: #5eead4; box-shadow: 0 0 28px 10px hsl(172 75% 57% / 0.35); animation-delay: -7s; }
.signal-menubar { background: hsl(225 27% 8% / 0.78); backdrop-filter: blur(20px); }
.signal-workspace { min-height: calc(100dvh - 2.75rem); }
.signal-window { transition: transform 180ms ease, box-shadow 180ms ease; }
.signal-titlebar { cursor: grab; }
.signal-titlebar:active { cursor: grabbing; }
.signal-home {
background:
radial-gradient(circle at 67% 22%, hsl(214 92% 62% / 0.16), transparent 22rem),
radial-gradient(circle at 14% 83%, hsl(38 96% 60% / 0.1), transparent 22rem),
hsl(225 23% 12% / 0.58);
backdrop-filter: blur(16px);
}
.desktop-icon { display: block; opacity: 0; animation: desktop-icon-in 520ms cubic-bezier(0.16, 1, 0.3, 1) forwards; }
.desktop-icon-mark { display: grid; width: 68px; height: 68px; margin: 0 auto; place-items: center; border: 1px solid hsl(0 0% 100% / 0.16); border-radius: 21px; color: white; box-shadow: inset 0 1px 0 hsl(0 0% 100% / 0.22), 0 14px 28px hsl(225 40% 2% / 0.3); transition: transform 180ms ease, filter 180ms ease; }
.desktop-icon:hover .desktop-icon-mark, .desktop-icon:focus-visible .desktop-icon-mark { transform: translateY(-4px) scale(1.04); filter: brightness(1.1); }
.desktop-icon:focus-visible { outline: 2px solid hsl(var(--primary)); outline-offset: 6px; border-radius: 16px; }
.desktop-icon-cyan, .launcher-icon-cyan { background: linear-gradient(140deg, #14b8c5, #1d4ed8); }
.desktop-icon-amber, .launcher-icon-amber { background: linear-gradient(140deg, #fbbf24, #ea580c); }
.desktop-icon-violet, .launcher-icon-violet { background: linear-gradient(140deg, #c084fc, #6d28d9); }
.desktop-icon-emerald, .launcher-icon-emerald { background: linear-gradient(140deg, #34d399, #047857); }
.desktop-icon-rose, .launcher-icon-rose { background: linear-gradient(140deg, #fb7185, #be123c); }
.signal-dock { min-height: 56px; }
.dock-app { position: relative; display: grid; width: 40px; height: 40px; flex: 0 0 auto; place-items: center; border-radius: 12px; color: hsl(220 13% 78%); transition: transform 150ms ease, background-color 150ms ease, color 150ms ease; }
.dock-app:hover, .dock-app:focus-visible { transform: translateY(-5px) scale(1.08); background: hsl(0 0% 100% / 0.11); color: white; outline: none; }
.dock-app-active { background: hsl(0 0% 100% / 0.14); color: hsl(var(--primary)); }
.dock-app-active::after { position: absolute; right: 50%; bottom: -3px; width: 4px; height: 4px; border-radius: 999px; background: hsl(var(--primary)); content: ''; transform: translateX(50%); }
.dock-tooltip { pointer-events: none; position: absolute; bottom: calc(100% + 10px); display: none; white-space: nowrap; border: 1px solid hsl(0 0% 100% / 0.12); border-radius: 8px; background: #151927; padding: 4px 7px; font-family: var(--font-mono); font-size: 10px; color: white; }
.dock-app:hover .dock-tooltip { display: block; }
.launcher-app { display: flex; width: 100%; align-items: center; gap: 12px; border-radius: 15px; padding: 10px; text-align: left; color: white; transition: background-color 150ms ease, transform 150ms ease; }
.launcher-app:hover, .launcher-app:focus-visible { background: hsl(0 0% 100% / 0.09); transform: translateX(2px); outline: none; }
.launcher-app strong { display: block; font-size: 14px; font-weight: 600; }
.launcher-app small { display: block; margin-top: 2px; font-size: 11px; color: hsl(220 15% 70%); }
.launcher-icon { display: grid; width: 40px; height: 40px; flex: 0 0 auto; place-items: center; border: 1px solid hsl(0 0% 100% / 0.12); border-radius: 13px; color: white; }
.launcher-icon-slate { background: linear-gradient(140deg, #475569, #0f172a); }
}
@media (max-width: 640px) {
.signal-dock { gap: 0; padding: 4px; }
.dock-app { width: 34px; height: 38px; }
.dock-app svg { width: 16px; height: 16px; }
.signal-home { border-radius: 16px; }
}
@media (prefers-reduced-motion: reduce) {
.signal-orb, .desktop-icon { animation: none; opacity: 1; }
.signal-window, .desktop-icon-mark, .dock-app, .launcher-app { transition: none; }
}
@keyframes signal-drift {
from { transform: translate3d(-12px, -8px, 0); }
to { transform: translate3d(18px, 15px, 0); }
}
@keyframes desktop-icon-in {
from { opacity: 0; transform: translateY(10px) scale(0.96); }
to { opacity: 1; transform: translateY(0) scale(1); }
}

9
src/lib/nostrApps.ts Normal file
View File

@@ -0,0 +1,9 @@
import { Activity, Archive, Compass, Network, Sparkles } from 'lucide-react';
export const nostrApps = [
{ name: 'Discover', path: '/explore', icon: Compass, short: 'Find', detail: 'Explore the public network', hue: 'cyan' },
{ name: 'Activity', path: '/dashboard', icon: Activity, short: 'Me', detail: 'See your Nostr footprint', hue: 'amber' },
{ name: 'Studio', path: '/dashboard/events', icon: Sparkles, short: 'Studio', detail: 'Inspect your published events', hue: 'violet' },
{ name: 'Archive', path: '/dashboard/export', icon: Archive, short: 'Save', detail: 'Keep your social graph portable', hue: 'emerald' },
{ name: 'Network', path: '/network', icon: Network, short: 'Relay', detail: 'Manage signal routes', hue: 'rose' },
] as const;

View File

@@ -1,85 +1,39 @@
import { useSeoMeta } from '@unhead/react';
import { ArrowUpRight, Boxes, Copy, Fingerprint, Network, Orbit, Sparkles, Terminal, Waves } from 'lucide-react';
import { ArrowUpRight, Command, Fingerprint, Radio, Sparkles } from 'lucide-react';
import { Link } from 'react-router-dom';
import { useState } from 'react';
import { OsShell } from '@/components/navigation/OsShell';
import { useToast } from '@/hooks/useToast';
import { useCurrentUser } from '@/hooks/useCurrentUser';
const workspaces = [
{ title: 'Discover', detail: 'Live notes & people', path: '/explore', icon: Orbit, tone: 'from-cyan-400/20 to-blue-500/5', index: '01' },
{ title: 'Activity', detail: 'Your Nostr footprint', path: '/dashboard', icon: Waves, tone: 'from-amber-300/20 to-orange-500/5', index: '02' },
{ title: 'Studio', detail: 'Browse your events', path: '/dashboard/events', icon: Sparkles, tone: 'from-fuchsia-400/20 to-violet-500/5', index: '03' },
{ title: 'Archive', detail: 'Export your follows', path: '/dashboard/export', icon: Boxes, tone: 'from-emerald-300/20 to-teal-500/5', index: '04' },
];
import { nostrApps } from '@/lib/nostrApps';
const Index = () => {
const [copied, setCopied] = useState(false);
const { toast } = useToast();
const { user } = useCurrentUser();
const relayUrl = 'wss://relay.layer.systems';
useSeoMeta({
title: 'Nostr OS · Layer Systems',
description: 'A beautiful operating system for your Nostr identity.',
description: 'A real desktop environment for your Nostr identity.',
});
const copyRelay = async () => {
try {
await navigator.clipboard.writeText(relayUrl);
setCopied(true);
window.setTimeout(() => setCopied(false), 1600);
toast({ title: 'Relay address copied', description: 'Paste it into any Nostr client.' });
} catch {
toast({ title: 'Could not copy relay address', description: relayUrl, variant: 'destructive' });
}
};
return (
<OsShell title="Home" eyebrow="Layer Systems / Nostr OS">
<section className="os-hero relative isolate overflow-hidden rounded-[28px] border border-white/10 px-5 py-7 shadow-2xl shadow-black/10 sm:px-8 sm:py-10 lg:px-10 lg:py-12">
<div className="absolute -right-16 -top-16 h-64 w-64 rounded-full bg-primary/20 blur-3xl" />
<div className="absolute -bottom-24 left-1/3 h-56 w-56 rounded-full bg-cyan-400/10 blur-3xl" />
<div className="relative grid gap-8 lg:grid-cols-[1.35fr_.65fr] lg:items-end">
<div>
<div className="mb-5 inline-flex items-center gap-2 rounded-full border border-white/10 bg-black/15 px-3 py-1.5 font-mono text-[10px] uppercase tracking-[0.18em] text-slate-300">
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-emerald-400" /> Personal signal online
</div>
<p className="max-w-xl text-balance text-4xl font-semibold leading-[0.98] tracking-[-0.055em] text-white sm:text-5xl lg:text-6xl">One place for the network that belongs to you.</p>
<p className="mt-5 max-w-lg text-pretty text-sm leading-6 text-slate-300 sm:text-base">A calm control surface for your Nostr identity: discover public conversations, inspect your signal, and keep your network portable.</p>
<div className="mt-7 flex flex-wrap gap-3">
<Link to={user ? '/dashboard' : '/explore'} className="inline-flex items-center gap-2 rounded-xl bg-primary px-4 py-3 text-sm font-semibold text-primary-foreground shadow-lg shadow-primary/20 transition-transform hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white">
{user ? 'Open my activity' : 'Enter discover'} <ArrowUpRight className="h-4 w-4" />
</Link>
<Link to="/network" className="inline-flex items-center gap-2 rounded-xl border border-white/15 bg-white/[0.06] px-4 py-3 text-sm font-medium text-white transition-colors hover:bg-white/[0.12] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white">
<Network className="h-4 w-4" /> Network routes
</Link>
</div>
</div>
<div className="relative overflow-hidden rounded-2xl border border-white/10 bg-black/20 p-5 backdrop-blur-sm">
<div className="mb-8 flex items-center justify-between font-mono text-[10px] uppercase tracking-[0.16em] text-slate-400"><span>Identity</span><Fingerprint className="h-4 w-4 text-primary" /></div>
<p className="text-lg font-medium text-white">{user ? 'Signer connected' : 'Your Nostr key is waiting'}</p>
<p className="mt-2 font-mono text-xs leading-5 text-slate-400">{user ? `${user.pubkey.slice(0, 12)}${user.pubkey.slice(-8)}` : 'Use a signer extension or nsec login to begin.'}</p>
<div className="mt-7 flex items-center gap-2 text-xs text-emerald-300"><span className="h-2 w-2 rounded-full bg-emerald-400" /> Your identity never leaves your signer</div>
</div>
</div>
</section>
<OsShell title="Desktop" eyebrow="Layer Systems / Nostr OS" desktop className="flex min-h-0 flex-col">
<section className="signal-home relative flex min-h-[calc(100dvh-9.5rem)] flex-1 flex-col overflow-hidden rounded-2xl border border-white/10 p-4 shadow-[0_32px_100px_rgba(0,0,0,0.26)] sm:p-6 lg:p-8">
<div className="pointer-events-none absolute right-[-4%] top-[12%] select-none font-mono text-[clamp(6rem,20vw,20rem)] font-bold leading-none tracking-[-0.12em] text-white/[0.035]">NSTR</div>
<div className="relative flex items-start justify-between gap-4"><div className="rounded-2xl border border-white/10 bg-black/15 px-3 py-2 backdrop-blur-sm"><p className="font-mono text-[10px] uppercase tracking-[0.18em] text-primary">Signal desk</p><p className="mt-0.5 text-xs text-white/65">Your independent social computer</p></div><button type="button" onClick={() => window.dispatchEvent(new Event('nostr-os:launcher'))} className="hidden items-center gap-2 rounded-xl border border-white/10 bg-black/15 px-3 py-2 font-mono text-[10px] text-white/70 backdrop-blur-sm transition-colors hover:bg-white/10 sm:flex"><Command className="h-3.5 w-3.5" /> Launch app <kbd className="rounded bg-white/10 px-1.5 py-0.5 text-white"> K</kbd></button></div>
<section className="mt-8">
<div className="mb-4 flex items-end justify-between gap-4"><div><p className="font-mono text-[10px] uppercase tracking-[0.2em] text-primary">Your desktop</p><h2 className="mt-1 text-2xl font-semibold tracking-tight">Open an app</h2></div><span className="hidden font-mono text-[10px] text-muted-foreground sm:block">04 MODULES READY</span></div>
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
{workspaces.map((workspace) => (
<Link key={workspace.path} to={workspace.path} className="group relative min-h-[178px] overflow-hidden rounded-2xl border border-border/70 bg-card/80 p-5 shadow-sm transition-all duration-300 hover:-translate-y-1 hover:border-primary/40 hover:shadow-xl hover:shadow-primary/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary">
<div className={`absolute inset-0 bg-gradient-to-br ${workspace.tone} opacity-0 transition-opacity duration-300 group-hover:opacity-100`} />
<div className="relative flex h-full flex-col"><div className="flex items-start justify-between"><span className="grid h-10 w-10 place-items-center rounded-xl border border-border bg-background/80"><workspace.icon className="h-5 w-5 text-primary" /></span><span className="font-mono text-[10px] text-muted-foreground">{workspace.index}</span></div><div className="mt-auto"><h3 className="font-semibold">{workspace.title}</h3><p className="mt-1 text-sm text-muted-foreground">{workspace.detail}</p></div></div>
<div className="relative mt-8 grid max-w-4xl grid-cols-3 gap-x-2 gap-y-5 sm:grid-cols-5 sm:gap-x-5 lg:mt-12 lg:grid-cols-5 xl:gap-x-10">
{nostrApps.map((app, index) => (
<Link key={app.path} to={app.path} className="desktop-icon group" style={{ animationDelay: `${90 + index * 55}ms` }}>
<span className={`desktop-icon-mark desktop-icon-${app.hue}`}><app.icon className="h-7 w-7" /></span>
<span className="mt-2 block text-center text-xs font-medium text-white drop-shadow-md">{app.name}</span>
<span className="mt-0.5 block text-center font-mono text-[9px] uppercase tracking-wider text-white/45">App</span>
</Link>
))}
</div>
</section>
<section className="mt-8 grid gap-4 lg:grid-cols-[1.35fr_.65fr]">
<div className="rounded-2xl border border-border/70 bg-card/70 p-5 sm:p-6"><div className="flex items-center gap-3"><span className="grid h-10 w-10 place-items-center rounded-xl bg-primary/10 text-primary"><Terminal className="h-5 w-5" /></span><div><h2 className="font-semibold">Bring Layer Systems with you</h2><p className="text-sm text-muted-foreground">A preferred relay, ready for any Nostr client.</p></div></div><div className="mt-5 flex flex-col gap-3 rounded-xl border border-border bg-background/60 p-3 sm:flex-row sm:items-center sm:justify-between"><code className="min-w-0 truncate font-mono text-sm text-foreground">{relayUrl}</code><button type="button" onClick={copyRelay} className="inline-flex shrink-0 items-center justify-center gap-2 rounded-lg bg-secondary px-3 py-2 text-xs font-semibold transition-colors hover:bg-primary hover:text-primary-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"><Copy className="h-3.5 w-3.5" />{copied ? 'Copied' : 'Copy relay'}</button></div></div>
<Link to="/network" className="group rounded-2xl border border-border/70 bg-card/70 p-5 transition-colors hover:border-primary/40 sm:p-6"><p className="font-mono text-[10px] uppercase tracking-[0.18em] text-primary">Your route table</p><p className="mt-3 text-lg font-semibold">Control what carries your signal.</p><p className="mt-2 text-sm leading-6 text-muted-foreground">Manage read and write relays from one focused network app.</p><span className="mt-5 inline-flex items-center gap-2 text-sm font-medium text-primary">Open Network <ArrowUpRight className="h-4 w-4 transition-transform group-hover:translate-x-0.5 group-hover:-translate-y-0.5" /></span></Link>
<div className="relative mt-auto grid gap-3 pt-10 lg:grid-cols-[minmax(0,1fr)_320px]">
<div className="rounded-2xl border border-white/10 bg-black/20 p-4 backdrop-blur-md sm:p-5"><div className="flex items-start gap-3"><span className="grid h-10 w-10 shrink-0 place-items-center rounded-xl bg-primary text-primary-foreground shadow-lg shadow-primary/20"><Fingerprint className="h-5 w-5" /></span><div><p className="font-mono text-[10px] uppercase tracking-[0.18em] text-primary">Your identity</p><p className="mt-1 text-sm font-medium text-white">{user ? 'Signer connected' : 'Ready when you are'}</p><p className="mt-1 max-w-lg text-xs leading-5 text-white/60">{user ? `${user.pubkey.slice(0, 16)}${user.pubkey.slice(-8)}` : 'Log in with a Nostr signer from the menu bar. Your key stays in your wallet or extension.'}</p></div></div></div>
<Link to="/network" className="group rounded-2xl border border-white/10 bg-black/20 p-4 backdrop-blur-md transition-colors hover:bg-white/[0.08] sm:p-5"><div className="flex items-center justify-between"><div className="flex items-center gap-2 text-xs font-medium text-white"><Radio className="h-4 w-4 text-emerald-300" /> Network online</div><ArrowUpRight className="h-4 w-4 text-white/50 transition-transform group-hover:translate-x-0.5 group-hover:-translate-y-0.5" /></div><p className="mt-4 font-mono text-[11px] text-emerald-200">wss://relay.layer.systems</p><p className="mt-1 text-xs text-white/55">Preferred signal route</p></Link>
</div>
<div className="pointer-events-none absolute bottom-4 right-5 hidden items-center gap-2 font-mono text-[10px] uppercase tracking-[0.16em] text-white/30 lg:flex"><Sparkles className="h-3 w-3" /> Make the network yours</div>
</section>
</OsShell>
);