mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-09-04 14:45:41 +00:00
feat(ui): redesign top bar as chrome-glass masthead (#696)
Extract the app header into a dedicated TopBar component and apply the same translucent chrome language used by the sidebar: bg-sidebar + backdrop-blur-md + inset top highlight. A new --chrome-top-highlight token drives the highlight in both themes via the shadow-chrome-top utility alias. Restore the pulsing-dot unread indicator on the notification bell (replaces the temporary count pill) and keep its aria-label dynamic so screen readers still hear the unread count. Tighten nav a11y with aria-current on the active item, aria-label on icon-only buttons, and a proper <nav> landmark around the desktop Highlight group.
This commit is contained in:
@@ -17,10 +17,9 @@ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, Di
|
|||||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from './ui/alert-dialog';
|
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from './ui/alert-dialog';
|
||||||
import { Tabs, TabsList, TabsTrigger, TabsHighlight, TabsHighlightItem } from './ui/tabs';
|
import { Tabs, TabsList, TabsTrigger, TabsHighlight, TabsHighlightItem } from './ui/tabs';
|
||||||
import { springs } from '@/lib/motion';
|
import { springs } from '@/lib/motion';
|
||||||
import { Highlight, HighlightItem } from './animate-ui/primitives/effects/highlight';
|
|
||||||
import { CursorProvider, Cursor, CursorContainer, CursorFollow } from '@/components/animate-ui/primitives/animate/cursor';
|
import { CursorProvider, Cursor, CursorContainer, CursorFollow } from '@/components/animate-ui/primitives/animate/cursor';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
||||||
import { Plus, Trash2, Play, Square, Save, Terminal, RotateCw, CloudDownload, Pencil, X, Home, ExternalLink, MoreVertical, BellRing, Rocket, HardDrive, ScrollText, Activity, Radar, Undo2, RefreshCw, Download, Clock, Menu, FolderSearch, Loader2, Tag, Check, ChevronDown, GitBranch, FileCode2, ShieldCheck, ArrowUpRight, Copy } from 'lucide-react';
|
import { Plus, Trash2, Play, Square, Save, Terminal, RotateCw, CloudDownload, Pencil, X, Home, ExternalLink, MoreVertical, BellRing, Rocket, HardDrive, ScrollText, Activity, Radar, Undo2, RefreshCw, Download, Clock, FolderSearch, Loader2, Tag, Check, ChevronDown, GitBranch, FileCode2, ShieldCheck, ArrowUpRight, Copy } from 'lucide-react';
|
||||||
import type { LucideIcon } from 'lucide-react';
|
import type { LucideIcon } from 'lucide-react';
|
||||||
import { LabelPill, LabelDot } from './LabelPill';
|
import { LabelPill, LabelDot } from './LabelPill';
|
||||||
import { type Label as StackLabel } from './label-types';
|
import { type Label as StackLabel } from './label-types';
|
||||||
@@ -39,7 +38,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ui/t
|
|||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from './ui/dropdown-menu';
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from './ui/dropdown-menu';
|
||||||
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger } from './ui/context-menu';
|
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger } from './ui/context-menu';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { Sheet, SheetContent, SheetTrigger } from './ui/sheet';
|
import { TopBar } from './TopBar';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { SettingsModal } from './SettingsModal';
|
import { SettingsModal } from './SettingsModal';
|
||||||
import { StackAlertSheet } from './StackAlertSheet';
|
import { StackAlertSheet } from './StackAlertSheet';
|
||||||
@@ -2482,60 +2481,14 @@ export default function EditorLayout() {
|
|||||||
|
|
||||||
{/* Main Content Area */}
|
{/* Main Content Area */}
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
{/* Top Header Bar — Three-zone layout: Node Pill | Navigation | Utilities */}
|
<TopBar
|
||||||
<div className="h-14 flex items-center px-4 border-b border-border gap-3">
|
activeView={activeView}
|
||||||
{/* LEFT ZONE: Node Context Pill */}
|
navItems={navItems}
|
||||||
<div className="flex-shrink-0">
|
navTabValue={navTabValue}
|
||||||
{activeNode?.type === 'remote' ? (
|
onNavigate={handleNavigate}
|
||||||
<div className="flex items-center gap-2 px-3 py-1.5 rounded-full bg-info-muted border border-info/20 text-info text-sm font-medium">
|
mobileNavOpen={mobileNavOpen}
|
||||||
<span className="w-2 h-2 rounded-full bg-info animate-pulse shrink-0" />
|
onMobileNavOpenChange={setMobileNavOpen}
|
||||||
{activeNode.name}
|
notifications={
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center gap-2 px-3 py-1.5 rounded-full bg-muted/50 border border-border text-muted-foreground text-sm">
|
|
||||||
<span className="w-2 h-2 rounded-full bg-success animate-pulse shrink-0" />
|
|
||||||
{activeNode?.name ?? 'Local'}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* CENTER ZONE: Navigation Group (hidden on mobile) */}
|
|
||||||
<div className="flex-1 hidden md:flex justify-center">
|
|
||||||
<Highlight
|
|
||||||
className="inset-0 rounded-md bg-accent"
|
|
||||||
value={navTabValue}
|
|
||||||
controlledItems
|
|
||||||
mode="children"
|
|
||||||
click={false}
|
|
||||||
transition={springs.snappy}
|
|
||||||
>
|
|
||||||
<div className="inline-flex items-center rounded-lg p-1 gap-0.5">
|
|
||||||
{navItems.map(({ value, label, icon: Icon }) => (
|
|
||||||
<HighlightItem key={value} value={value}>
|
|
||||||
<button
|
|
||||||
onClick={() => handleNavigate(value)}
|
|
||||||
className={cn(
|
|
||||||
'relative inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md transition-colors',
|
|
||||||
activeView === value
|
|
||||||
? 'text-foreground after:absolute after:bottom-0 after:left-1/4 after:right-1/4 after:h-[2px] after:rounded-full after:bg-brand after:blur-[2px]'
|
|
||||||
: 'text-muted-foreground hover:text-foreground'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon className="w-4 h-4 shrink-0" />
|
|
||||||
<span className="hidden xl:inline">{label}</span>
|
|
||||||
</button>
|
|
||||||
</HighlightItem>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Highlight>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Spacer for mobile (when center nav is hidden) */}
|
|
||||||
<div className="flex-1 md:hidden" />
|
|
||||||
|
|
||||||
{/* RIGHT ZONE: Utilities */}
|
|
||||||
<div className="flex-shrink-0 flex items-center gap-2">
|
|
||||||
{/* Notifications */}
|
|
||||||
<NotificationPanel
|
<NotificationPanel
|
||||||
notifications={notifications}
|
notifications={notifications}
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
@@ -2544,47 +2497,15 @@ export default function EditorLayout() {
|
|||||||
onDelete={deleteNotification}
|
onDelete={deleteNotification}
|
||||||
onNavigate={navigateToNotification}
|
onNavigate={navigateToNotification}
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
|
userMenu={
|
||||||
{/* User Profile Dropdown */}
|
|
||||||
<UserProfileDropdown
|
<UserProfileDropdown
|
||||||
theme={theme}
|
theme={theme}
|
||||||
setTheme={setTheme}
|
setTheme={setTheme}
|
||||||
onOpenSettings={() => setSettingsModalOpen(true)}
|
onOpenSettings={() => setSettingsModalOpen(true)}
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
{/* Mobile Navigation Trigger */}
|
/>
|
||||||
<Sheet open={mobileNavOpen} onOpenChange={setMobileNavOpen}>
|
|
||||||
<SheetTrigger asChild>
|
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8 rounded-lg md:hidden">
|
|
||||||
<Menu className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</SheetTrigger>
|
|
||||||
<SheetContent side="right" className="w-64 p-0">
|
|
||||||
<div className="p-4 border-b">
|
|
||||||
<p className="text-sm font-medium">Navigation</p>
|
|
||||||
</div>
|
|
||||||
<nav className="flex flex-col p-2 gap-1">
|
|
||||||
{navItems.map(({ value, label, icon: Icon }) => (
|
|
||||||
<button
|
|
||||||
key={value}
|
|
||||||
onClick={() => { handleNavigate(value); setMobileNavOpen(false); }}
|
|
||||||
className={cn(
|
|
||||||
'flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm transition-colors',
|
|
||||||
activeView === value
|
|
||||||
? 'bg-glass-highlight font-medium text-foreground'
|
|
||||||
: 'text-muted-foreground hover:bg-glass-highlight hover:text-foreground'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon className="w-4 h-4" />
|
|
||||||
{label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
</SheetContent>
|
|
||||||
</Sheet>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main Workspace */}
|
{/* Main Workspace */}
|
||||||
<div key={activeView} className="flex-1 overflow-y-auto p-6 animate-fade-up">
|
<div key={activeView} className="flex-1 overflow-y-auto p-6 animate-fade-up">
|
||||||
|
|||||||
@@ -132,11 +132,9 @@ export function NotificationPanel({
|
|||||||
|
|
||||||
const bellBadge =
|
const bellBadge =
|
||||||
unreadCount > 0 ? (
|
unreadCount > 0 ? (
|
||||||
<span
|
<span aria-hidden="true" className="absolute -right-1 -top-1 flex h-2.5 w-2.5">
|
||||||
aria-hidden="true"
|
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-destructive opacity-75" />
|
||||||
className="absolute -right-1 -top-1 flex min-h-[16px] min-w-[16px] items-center justify-center rounded-full bg-destructive px-1 font-mono text-[9px] font-semibold tabular-nums leading-none text-destructive-foreground"
|
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-destructive" />
|
||||||
>
|
|
||||||
{unreadCount > 99 ? '99+' : unreadCount}
|
|
||||||
</span>
|
</span>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
@@ -154,7 +152,7 @@ export function NotificationPanel({
|
|||||||
size="icon"
|
size="icon"
|
||||||
className="relative h-8 w-8 rounded-lg"
|
className="relative h-8 w-8 rounded-lg"
|
||||||
title="Notifications"
|
title="Notifications"
|
||||||
aria-label="Notifications"
|
aria-label={unreadCount > 0 ? `Notifications, ${unreadCount} unread` : 'Notifications'}
|
||||||
>
|
>
|
||||||
<Bell className="h-4 w-4" strokeWidth={1.5} />
|
<Bell className="h-4 w-4" strokeWidth={1.5} />
|
||||||
{bellBadge}
|
{bellBadge}
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import type { LucideIcon } from 'lucide-react';
|
||||||
|
import { Menu } from 'lucide-react';
|
||||||
|
import { Button } from './ui/button';
|
||||||
|
import { Sheet, SheetContent, SheetTrigger } from './ui/sheet';
|
||||||
|
import { Highlight, HighlightItem } from './animate-ui/primitives/effects/highlight';
|
||||||
|
import { springs } from '@/lib/motion';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
export interface TopBarNavItem {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
icon: LucideIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TopBarProps {
|
||||||
|
activeView: string;
|
||||||
|
navItems: TopBarNavItem[];
|
||||||
|
navTabValue: string | undefined;
|
||||||
|
onNavigate: (value: string) => void;
|
||||||
|
mobileNavOpen: boolean;
|
||||||
|
onMobileNavOpenChange: (open: boolean) => void;
|
||||||
|
notifications: ReactNode;
|
||||||
|
userMenu: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TopBar({
|
||||||
|
activeView,
|
||||||
|
navItems,
|
||||||
|
navTabValue,
|
||||||
|
onNavigate,
|
||||||
|
mobileNavOpen,
|
||||||
|
onMobileNavOpenChange,
|
||||||
|
notifications,
|
||||||
|
userMenu,
|
||||||
|
}: TopBarProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'relative flex h-14 items-center gap-3 px-4',
|
||||||
|
'border-b border-glass-border bg-sidebar backdrop-blur-md',
|
||||||
|
'shadow-chrome-top',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* LEFT ZONE: reserved spacer (keeps nav visually centered) */}
|
||||||
|
<div className="flex-1 min-w-0" />
|
||||||
|
|
||||||
|
{/* CENTER ZONE: Navigation (hidden on mobile) */}
|
||||||
|
<nav aria-label="Primary" className="hidden md:flex justify-center">
|
||||||
|
<Highlight
|
||||||
|
className="inset-0 rounded-md bg-accent"
|
||||||
|
value={navTabValue}
|
||||||
|
controlledItems
|
||||||
|
mode="children"
|
||||||
|
click={false}
|
||||||
|
transition={springs.snappy}
|
||||||
|
>
|
||||||
|
<div className="inline-flex items-center rounded-lg p-1 gap-0.5">
|
||||||
|
{navItems.map(({ value, label, icon: Icon }) => (
|
||||||
|
<HighlightItem key={value} value={value}>
|
||||||
|
<button
|
||||||
|
onClick={() => onNavigate(value)}
|
||||||
|
aria-label={label}
|
||||||
|
aria-current={activeView === value ? 'page' : undefined}
|
||||||
|
className={cn(
|
||||||
|
'relative inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md transition-colors',
|
||||||
|
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/50',
|
||||||
|
activeView === value
|
||||||
|
? 'text-foreground after:absolute after:bottom-0 after:left-1/4 after:right-1/4 after:h-[2px] after:rounded-full after:bg-brand after:blur-[2px]'
|
||||||
|
: 'text-muted-foreground hover:text-foreground',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon className="w-4 h-4 shrink-0" strokeWidth={1.5} />
|
||||||
|
<span className="hidden xl:inline">{label}</span>
|
||||||
|
</button>
|
||||||
|
</HighlightItem>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Highlight>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* RIGHT ZONE: Utilities + identity pin */}
|
||||||
|
<div className="flex flex-1 min-w-0 items-center justify-end gap-2">
|
||||||
|
{notifications}
|
||||||
|
{userMenu}
|
||||||
|
|
||||||
|
{/* Mobile nav trigger */}
|
||||||
|
<Sheet open={mobileNavOpen} onOpenChange={onMobileNavOpenChange}>
|
||||||
|
<SheetTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
aria-label="Open navigation menu"
|
||||||
|
className="h-8 w-8 rounded-lg md:hidden"
|
||||||
|
>
|
||||||
|
<Menu className="w-4 h-4" strokeWidth={1.5} />
|
||||||
|
</Button>
|
||||||
|
</SheetTrigger>
|
||||||
|
<SheetContent side="right" className="w-64 p-0">
|
||||||
|
<div className="p-4 border-b">
|
||||||
|
<p className="text-sm font-medium">Navigation</p>
|
||||||
|
</div>
|
||||||
|
<nav className="flex flex-col p-2 gap-1">
|
||||||
|
{navItems.map(({ value, label, icon: Icon }) => (
|
||||||
|
<button
|
||||||
|
key={value}
|
||||||
|
onClick={() => {
|
||||||
|
onNavigate(value);
|
||||||
|
onMobileNavOpenChange(false);
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm transition-colors',
|
||||||
|
activeView === value
|
||||||
|
? 'bg-glass-highlight font-medium text-foreground'
|
||||||
|
: 'text-muted-foreground hover:bg-glass-highlight hover:text-foreground',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon className="w-4 h-4" strokeWidth={1.5} />
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -148,6 +148,7 @@
|
|||||||
/* Material simulation — inherent depth */
|
/* Material simulation — inherent depth */
|
||||||
--card-bevel: inset 0 1px 0 0 oklch(1 0 0 / 0.04), 0 1px 2px 0 oklch(0 0 0 / 0.05);
|
--card-bevel: inset 0 1px 0 0 oklch(1 0 0 / 0.04), 0 1px 2px 0 oklch(0 0 0 / 0.05);
|
||||||
--button-inner-glow: inset 0 1px 0 0 oklch(1 0 0 / 0.04);
|
--button-inner-glow: inset 0 1px 0 0 oklch(1 0 0 / 0.04);
|
||||||
|
--chrome-top-highlight: inset 0 1px 0 0 oklch(1 0 0 / 0.45);
|
||||||
|
|
||||||
/* Motion timing */
|
/* Motion timing */
|
||||||
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
|
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
@@ -304,6 +305,7 @@
|
|||||||
/* Material simulation — inherent depth */
|
/* Material simulation — inherent depth */
|
||||||
--card-bevel: inset 0 1px 0 0 oklch(1 0 0 / 0.05);
|
--card-bevel: inset 0 1px 0 0 oklch(1 0 0 / 0.05);
|
||||||
--button-inner-glow: inset 0 1px 0 0 oklch(1 0 0 / 0.06);
|
--button-inner-glow: inset 0 1px 0 0 oklch(1 0 0 / 0.06);
|
||||||
|
--chrome-top-highlight: inset 0 1px 0 0 oklch(1 0 0 / 0.08);
|
||||||
|
|
||||||
/* Shadows — near-zero, depth comes from surface tone */
|
/* Shadows — near-zero, depth comes from surface tone */
|
||||||
--shadow-x: 0px;
|
--shadow-x: 0px;
|
||||||
@@ -413,6 +415,7 @@
|
|||||||
--shadow-2xl: var(--shadow-2xl);
|
--shadow-2xl: var(--shadow-2xl);
|
||||||
--shadow-card-bevel: var(--card-bevel);
|
--shadow-card-bevel: var(--card-bevel);
|
||||||
--shadow-btn-glow: var(--button-inner-glow);
|
--shadow-btn-glow: var(--button-inner-glow);
|
||||||
|
--shadow-chrome-top: var(--chrome-top-highlight);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─────────────────────────────────────────────────────────────
|
/* ─────────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user