
Sidebar-free admin shell with a sticky top bar over a working settings screen: vertical tabs with roving keyboard focus, a header search that filters every section and counts matches per tab, and rows that actually do their thing (inline edit, copy to clipboard, or a switch) with a live save status.
@hirael
registry/hirael/bases/radix/blocks/app-shell-02/app-shell-02.tsx
// App Shell 02 from Hirael <https://hirael.com/blocks/app-shell/app-shell-02>
// MIT · Mohammad Shehadeh · https://github.com/MohammadShehadeh/hirael
'use client';
import * as React from 'react';
import {
Bell,
CreditCard,
KeyRound,
LogOut,
Menu,
Plug,
Search,
Settings,
Shield,
User,
type LucideIcon,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { Badge } from '@/registry/hirael/bases/radix/ui/badge';
import { Button } from '@/registry/hirael/bases/radix/ui/button';
import { CopyButton } from '@/registry/hirael/bases/radix/components/copy-button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/registry/hirael/bases/radix/ui/dropdown-menu';
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from '@/registry/hirael/bases/radix/ui/empty';
import {
InlineEdit,
InlineEditControls,
InlineEditInput,
InlineEditPreview,
} from '@/registry/hirael/bases/radix/components/inline-edit';
import { InputGroup, InputGroupAddon, InputGroupInput } from '@/registry/hirael/bases/radix/ui/input-group';
import { Separator } from '@/registry/hirael/bases/radix/ui/separator';
import { Switch } from '@/registry/hirael/bases/radix/ui/switch';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/registry/hirael/bases/radix/ui/tabs';
const NAV = ['Overview', 'Projects', 'Activity', 'Settings'] as const;
const ENTER =
'animate-in fade-in slide-in-from-bottom-2 duration-500 ease-[cubic-bezier(0.22,1,0.36,1)] fill-mode-both motion-reduce:animate-none';
const SWAP =
'animate-in fade-in slide-in-from-bottom-1 duration-250 ease-[cubic-bezier(0.22,1,0.36,1)] fill-mode-both motion-reduce:animate-none';
type SectionId = 'profile' | 'security' | 'api' | 'billing' | 'integrations';
interface Section {
id: SectionId;
label: string;
icon: LucideIcon;
desc: string;
}
const SECTIONS: readonly Section[] = [
{ id: 'profile', label: 'Profile', icon: User, desc: 'Your personal details' },
{ id: 'security', label: 'Security', icon: Shield, desc: 'Password and 2FA' },
{ id: 'api', label: 'API keys', icon: KeyRound, desc: 'Tokens and access' },
{
id: 'billing',
label: 'Billing',
icon: CreditCard,
desc: 'Plan and invoices',
},
{
id: 'integrations',
label: 'Integrations',
icon: Plug,
desc: 'Connected apps',
},
];
type Field =
| { kind: 'text'; id: string; label: string; value: string; hint?: string }
| { kind: 'secret'; id: string; label: string; value: string; hint?: string }
| {
kind: 'toggle';
id: string;
label: string;
hint?: string;
onLabel: string;
offLabel: string;
enabled: boolean;
}
| {
kind: 'readonly';
id: string;
label: string;
value: string;
hint?: string;
};
const SECTION_FIELDS: Record<SectionId, readonly Field[]> = {
profile: [
{
kind: 'text',
id: 'name',
label: 'Full name',
value: 'Mohammad Shehadeh',
},
{
kind: 'text',
id: 'email',
label: 'Email',
value: 'hello@mohammadshehadeh.com',
hint: 'Used for sign-in',
},
{
kind: 'readonly',
id: 'role',
label: 'Role',
value: 'Workspace admin',
hint: 'Set by the workspace owner',
},
],
security: [
{
kind: 'readonly',
id: 'password',
label: 'Password',
value: 'Updated 12 days ago',
hint: 'Changed from the account recovery flow',
},
{
kind: 'toggle',
id: '2fa',
label: 'Two-factor auth',
hint: 'Authenticator app',
onLabel: 'Required at sign-in',
offLabel: 'Off',
enabled: true,
},
{
kind: 'readonly',
id: 'sessions',
label: 'Active sessions',
value: '3 devices',
hint: 'Oldest signed in 6 days ago',
},
],
api: [
{
kind: 'secret',
id: 'live',
label: 'Production key',
value: 'msh_live_4f81c2a9d7e08f2a',
hint: 'Last used 2h ago',
},
{
kind: 'secret',
id: 'test',
label: 'Development key',
value: 'msh_test_90ba17ce4d5b1c0d',
},
{
kind: 'secret',
id: 'webhook',
label: 'Webhook secret',
value: 'whsec_2c7f0e1a6b93d44b9',
},
],
billing: [
{
kind: 'readonly',
id: 'plan',
label: 'Current plan',
value: 'Pro, $24 a month',
hint: 'Renews 14 June 2026',
},
{
kind: 'readonly',
id: 'card',
label: 'Payment method',
value: 'Visa ending 4242',
hint: 'Expires 09/28',
},
{
kind: 'text',
id: 'ap-email',
label: 'Billing email',
value: 'ap@hirael.com',
hint: 'Invoices are sent here',
},
],
integrations: [
{
kind: 'toggle',
id: 'vercel',
label: 'Vercel',
hint: 'hirael',
onLabel: 'Deploys on push',
offLabel: 'Disconnected',
enabled: true,
},
{
kind: 'toggle',
id: 'slack',
label: 'Slack',
hint: '#product',
onLabel: 'Alerts on',
offLabel: 'Disconnected',
enabled: true,
},
{
kind: 'toggle',
id: 'linear',
label: 'Linear',
onLabel: 'Issues synced',
offLabel: 'Disconnected',
enabled: false,
},
],
};
const fieldMatches = (field: Field, query: string) => {
const haystack = [
field.label,
field.hint ?? '',
field.kind === 'toggle' ? `${field.onLabel} ${field.offLabel}` : field.value,
]
.join(' ')
.toLowerCase();
return haystack.includes(query);
};
interface BrandMarkProps {
className?: string;
}
const BrandMark = ({ className }: BrandMarkProps) => {
return (
<span
role="img"
aria-label="Hirael"
className={cn('flex aspect-square items-center justify-center rounded-sm text-foreground', className)}
>
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="size-[64%]">
<path d="M2.3 12h2.4v10.95h6.2V14.6h4.6v8.35h6.2V12h-2.4V1.05h-6.2V9.4H8.5V1.05H2.3Z" />
</svg>
</span>
);
};
type SaveState = 'idle' | 'saving' | 'saved';
interface FieldRowProps {
field: Field;
values: Record<string, string>;
toggles: Record<string, boolean>;
onCommit: (id: string, value: string) => Promise<void>;
onToggle: (id: string, next: boolean) => void;
}
const FieldRow = ({ field, values, toggles, onCommit, onToggle }: FieldRowProps) => {
const hintId = field.hint ? `${field.id}-hint` : undefined;
return (
<div className="flex flex-col gap-1 px-5 py-4 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<div className="flex flex-col">
<span className="text-sm font-medium" id={`${field.id}-label`}>
{field.label}
</span>
{field.hint && (
<span id={hintId} className="text-[11px] text-muted-foreground">
{field.hint}
</span>
)}
</div>
{field.kind === 'text' && (
<InlineEdit
value={values[field.id] ?? field.value}
onSubmit={(next) => onCommit(field.id, next)}
validate={(next) => (next.trim() === '' ? `${field.label} cannot be empty` : null)}
selectOnFocus
className="flex w-full flex-wrap items-center justify-start sm:w-72 sm:justify-end"
>
<InlineEditPreview aria-labelledby={`${field.id}-label`} aria-describedby={hintId} />
<InlineEditInput aria-labelledby={`${field.id}-label`} className="h-8 min-w-0 flex-1" />
<InlineEditControls />
</InlineEdit>
)}
{field.kind === 'secret' && (
<div className="flex items-center gap-2">
<span className="text-sm text-foreground tabular-nums">
{field.value.slice(0, 9)}
<span aria-hidden>{'•'.repeat(8)}</span>
<span className="sr-only"> hidden</span>
{field.value.slice(-4)}
</span>
<CopyButton
value={field.value}
variant="outline"
size="sm"
aria-label={`Copy ${field.label.toLowerCase()}`}
/>
</div>
)}
{field.kind === 'toggle' && (
<div className="flex items-center gap-3">
<span className={cn('text-xs uppercase', toggles[field.id] ? 'text-success' : 'text-muted-foreground')}>
{toggles[field.id] ? field.onLabel : field.offLabel}
</span>
<Switch
checked={toggles[field.id]}
onCheckedChange={(next) => onToggle(field.id, next)}
aria-labelledby={`${field.id}-label`}
aria-describedby={hintId}
/>
</div>
)}
{field.kind === 'readonly' && <span className="text-sm text-muted-foreground">{field.value}</span>}
</div>
);
};
const AppShell02 = () => {
const [section, setSection] = React.useState<SectionId>('profile');
const [query, setQuery] = React.useState('');
const [values, setValues] = React.useState<Record<string, string>>({});
const [toggles, setToggles] = React.useState<Record<string, boolean>>(() =>
Object.fromEntries(
Object.values(SECTION_FIELDS)
.flat()
.filter((f) => f.kind === 'toggle')
.map((f) => [f.id, f.enabled]),
),
);
const [saveState, setSaveState] = React.useState<SaveState>('idle');
const savedTimer = React.useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
React.useEffect(() => () => clearTimeout(savedTimer.current), []);
const markSaved = React.useCallback(() => {
setSaveState('saved');
clearTimeout(savedTimer.current);
savedTimer.current = setTimeout(() => setSaveState('idle'), 2000);
}, []);
const commitValue = React.useCallback(
async (id: string, value: string) => {
setSaveState('saving');
// Stands in for the request a real settings screen would fire.
await new Promise((resolve) => setTimeout(resolve, 400));
setValues((prev) => ({ ...prev, [id]: value }));
markSaved();
},
[markSaved],
);
const commitToggle = React.useCallback(
(id: string, next: boolean) => {
setToggles((prev) => ({ ...prev, [id]: next }));
markSaved();
},
[markSaved],
);
const normalized = query.trim().toLowerCase();
const matchesBySection = React.useMemo(() => {
return SECTIONS.reduce<Record<SectionId, readonly Field[]>>(
(acc, s) => {
const fields = SECTION_FIELDS[s.id];
acc[s.id] = normalized ? fields.filter((f) => fieldMatches(f, normalized)) : fields;
return acc;
},
{} as Record<SectionId, readonly Field[]>,
);
}, [normalized]);
const totalMatches = SECTIONS.reduce((sum, s) => sum + matchesBySection[s.id].length, 0);
return (
<div className="flex min-h-[640px] flex-col bg-background">
<header className="sticky top-0 z-20 border-b border-border bg-background/80 backdrop-blur">
<div className="mx-auto flex h-14 w-full max-w-[1480px] items-center gap-3 px-4">
<BrandMark className="size-7 shrink-0" />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon-sm" aria-label="Open navigation" className="md:hidden">
<Menu aria-hidden />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-48">
{NAV.map((item) => (
<DropdownMenuItem key={item} asChild>
<a href="#" aria-current={item === 'Settings' ? 'page' : undefined}>
{item}
</a>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<Separator orientation="vertical" className="mx-1 hidden data-[orientation=vertical]:h-5 sm:block" />
<nav aria-label="Primary" className="hidden items-center gap-1 md:flex">
{NAV.map((item) => (
<a
key={item}
href="#"
aria-current={item === 'Settings' ? 'page' : undefined}
className="rounded-md px-2.5 py-1.5 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-foreground aria-[current=page]:bg-accent aria-[current=page]:text-foreground"
>
{item}
</a>
))}
</nav>
<InputGroup className="ms-auto h-8 max-w-[220px]">
<InputGroupAddon align="inline-start">
<Search className="size-3.5" aria-hidden />
</InputGroupAddon>
<InputGroupInput
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Escape' && query) {
e.preventDefault();
setQuery('');
}
}}
placeholder="Search settings…"
aria-label="Search settings"
/>
</InputGroup>
<Button variant="outline" size="icon" aria-label="Notifications, 3 unread" className="relative size-8">
<Bell className="size-3.5" aria-hidden />
<span aria-hidden className="absolute end-1.5 top-1.5 size-1.5 rounded-full bg-foreground" />
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="secondary" size="icon-sm" aria-label="Account menu">
<span className="text-[10px]">MS</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuLabel>
<span className="block text-sm font-medium">Mohammad Shehadeh</span>
<span className="block truncate text-xs text-muted-foreground">hello@mohammadshehadeh.com</span>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Settings />
Workspace settings
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive">
<LogOut />
Sign out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</header>
<div data-slot="app-shell-main" className={cn(ENTER, 'mx-auto w-full max-w-[1480px] px-4 py-6 sm:py-8')}>
<div className="flex flex-col gap-1">
<span className="flex gap-1.5 text-xs text-muted-foreground uppercase">
<span>workspace</span>
<span className="text-border">|</span>
<span>hirael</span>
</span>
<h1 className="text-2xl font-semibold tracking-[-0.02em]">Settings</h1>
<p className="text-sm text-muted-foreground">
Manage your account, security, and workspace integrations. Changes save as you make them.
</p>
</div>
<p aria-live="polite" className="sr-only">
{normalized ? `${totalMatches} settings match ${query.trim()}` : 'Showing all settings'}
</p>
<Tabs
value={section}
onValueChange={(v) => setSection(v as SectionId)}
orientation="vertical"
className="mt-6 grid grid-cols-1 items-start lg:grid-cols-12"
>
<TabsList variant="line" className="h-auto w-full justify-start lg:col-span-3">
{SECTIONS.map((s) => {
const count = matchesBySection[s.id].length;
return (
<TabsTrigger key={s.id} value={s.id}>
<s.icon className="size-4 shrink-0 text-muted-foreground" aria-hidden />
<span className="whitespace-nowrap">{s.label}</span>
{normalized && (
<Badge variant={count ? 'secondary' : 'outline'} className="ms-auto">
<span className="tabular-nums">{count}</span>
<span className="sr-only"> matches</span>
</Badge>
)}
</TabsTrigger>
);
})}
</TabsList>
<div className="mt-4 lg:col-span-9 lg:ms-4 lg:mt-0">
{SECTIONS.map((s) => {
const fields = matchesBySection[s.id];
const elsewhere = SECTIONS.filter((other) => other.id !== s.id && matchesBySection[other.id].length > 0);
return (
<TabsContent key={s.id} value={s.id} className="mt-0">
<div className={cn(SWAP, 'overflow-hidden rounded-xl border bg-card text-card-foreground shadow-sm')}>
<div className="flex items-center justify-between gap-3 border-b border-border px-5 py-4">
<div className="flex items-center gap-3">
<s.icon className="mt-0.5 size-4 shrink-0 self-start text-muted-foreground" aria-hidden />
<div className="flex flex-col">
<h2 className="text-sm font-semibold">{s.label}</h2>
<p className="text-xs text-muted-foreground">{s.desc}</p>
</div>
</div>
<Badge variant="outline" className="hidden sm:inline-flex">
<span className="tabular-nums">
{fields.length}
{normalized ? ` of ${SECTION_FIELDS[s.id].length}` : ''} fields
</span>
</Badge>
</div>
{fields.length === 0 ? (
<Empty>
<EmptyHeader>
<EmptyMedia variant="icon">
<Search />
</EmptyMedia>
<EmptyTitle>Nothing here matches</EmptyTitle>
<EmptyDescription>
Nothing in {s.label} matches{' '}
<span className="text-foreground">“{query.trim()}”</span>
{elsewhere.length > 0 ? ', but other sections have hits.' : '.'}
</EmptyDescription>
</EmptyHeader>
<EmptyContent className="flex-row flex-wrap justify-center">
{elsewhere.map((other) => (
<Button key={other.id} variant="outline" size="sm" onClick={() => setSection(other.id)}>
<other.icon aria-hidden />
{other.label}
<Badge variant="secondary">
<span className="tabular-nums">{matchesBySection[other.id].length}</span>
</Badge>
</Button>
))}
<Button variant="ghost" size="sm" onClick={() => setQuery('')}>
Clear search
</Button>
</EmptyContent>
</Empty>
) : (
<div className="divide-y divide-border">
{fields.map((field) => (
<FieldRow
key={field.id}
field={field}
values={values}
toggles={toggles}
onCommit={commitValue}
onToggle={commitToggle}
/>
))}
</div>
)}
<div className="flex items-center justify-between border-t border-border bg-muted/30 px-5 py-3">
<span
aria-live="polite"
className={cn(
'text-xs uppercase',
saveState === 'saved' ? 'text-success' : 'text-muted-foreground',
)}
>
{saveState === 'saving'
? 'saving…'
: saveState === 'saved'
? 'all changes saved'
: 'changes save automatically'}
</span>
<Button variant="outline" size="sm" asChild>
<a href="#">Audit log</a>
</Button>
</div>
</div>
</TabsContent>
);
})}
</div>
</Tabs>
</div>
</div>
);
};
export default AppShell02;

































