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
17 changes: 8 additions & 9 deletions src/components/layout/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { AnnouncementBanner } from './AnnouncementBanner';
import { CommandPalette } from './CommandPalette';
import { MobileTopBar } from './MobileTopBar';
import { Sidebar } from './Sidebar';
import { TopBar } from './TopBar';

/**
* The persistent application shell, mounted once as a layout route for the
Expand Down Expand Up @@ -55,7 +56,7 @@ export function AppShell() {
<div className="flex min-h-0 flex-1">
{/* Desktop sidebar */}
<div className="hidden w-[272px] shrink-0 lg:block">
<Sidebar onSearch={openSearch} />
<Sidebar />
</div>

{/* Mobile drawer + backdrop */}
Expand Down Expand Up @@ -84,24 +85,22 @@ export function AppShell() {
drawerOpen ? 'translate-x-0' : '-translate-x-full',
)}
>
<Sidebar
onNavigate={() => setDrawerOpen(false)}
onSearch={(seed) => {
setDrawerOpen(false);
openSearch(seed);
}}
/>
<Sidebar onNavigate={() => setDrawerOpen(false)} />
</div>
</div>

{/* Content column: mobile top bar + floating canvas */}
{/* Content column: top bar (desktop) / mobile top bar + floating canvas */}
<div className="flex min-w-0 flex-1 flex-col">
<MobileTopBar
ref={menuButtonRef}
drawerOpen={drawerOpen}
onMenu={() => setDrawerOpen(true)}
onSearch={() => openSearch()}
/>
{/* Desktop header strip on the exposed outer shell: centered search,
bell far right. A shrink-0 sibling of the canvas, so the canvas
gives up exactly its height and stays the only scroll region. */}
<TopBar onSearch={openSearch} />
<div className="flex min-h-0 flex-1 flex-col p-2 pt-0 lg:p-3 lg:pl-0 lg:pt-3">
<main className="min-h-0 flex-1 overflow-y-auto rounded border border-steel/20 bg-canvas">
<div className="mx-auto max-w-6xl p-4 sm:p-6 lg:p-8">
Expand Down
15 changes: 11 additions & 4 deletions src/components/layout/CommandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,17 @@ export function CommandPalette({ open, onOpenChange, initialQuery }: CommandPale
const navigate = useNavigate();
const [q, setQ] = useState('');

useHotkeys('mod+k', () => onOpenChange(!open), {
enableOnFormTags: true,
preventDefault: true,
});
useHotkeys(
'mod+k',
() => onOpenChange(!open),
{
enableOnFormTags: true,
preventDefault: true,
},
// Without deps the callback keeps the first render's `open` (false), so
// the shortcut could open the palette but never toggle it closed.
[open, onOpenChange],
);

// Seed (or clear) the query whenever the palette opens; typing in the
// sidebar input while open appends through the same channel.
Expand Down
37 changes: 3 additions & 34 deletions src/components/layout/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { Search } from 'lucide-react';
import { useMe } from '../../features/auth/hooks/useAuth';
import { NotificationBell } from '../../features/notifications/components/NotificationBell';
import { Logo } from '../brand/Logo';
import { SidebarNav } from './SidebarNav';
import { UserFooter } from './UserFooter';
Expand All @@ -9,17 +7,16 @@ import { WorkspaceSwitcher } from './WorkspaceSwitcher';
interface SidebarProps {
/** Called after a nav link is activated (closes the mobile drawer). */
onNavigate?: () => void;
/** Opens the command palette, optionally seeded with typed text. */
onSearch: (seed?: string) => void;
}

/**
* The persistent navigation frame: workspace identity + switcher fixed at the
* top, an independently scrollable nav list in the middle, and the user
* footer fixed at the bottom. Rendered both as the desktop rail and inside
* the mobile drawer.
* the mobile drawer. Search and the notification bell live in the shell's
* top bar (TopBar on desktop, MobileTopBar below lg), not here.
*/
export function Sidebar({ onNavigate, onSearch }: SidebarProps) {
export function Sidebar({ onNavigate }: SidebarProps) {
const { data: me } = useMe();
if (!me?.workspace) return null; // guarded by WorkspaceRoute; satisfies types

Expand All @@ -29,34 +26,6 @@ export function Sidebar({ onNavigate, onSearch }: SidebarProps) {
<div className="flex items-center gap-1.5">
<Logo size="sm" withWordmark={false} className="shrink-0 px-1 text-charcoal" />
<WorkspaceSwitcher workspace={me.workspace} />
{/* Operational inbox: right edge of the shell header, before the
user's own controls — the "top of the app" position. */}
<div className="shrink-0">
<NotificationBell />
</div>
</div>
{/* Real search input, centered at the top of the sidebar. Focusing
or typing hands off to the floating palette (seeded with the
typed text) — one search surface, so the input itself stays
empty and the palette owns the query. */}
<div className="relative mx-auto mt-3 w-full">
<Search
size={14}
aria-hidden="true"
className="pointer-events-none absolute left-2.5 top-1/2 -translate-y-1/2 text-steel"
/>
<input
type="search"
aria-label="Search workspace"
placeholder="Search…"
value=""
onFocus={() => onSearch()}
onChange={(event) => onSearch(event.target.value)}
className="w-full rounded border border-steel/20 bg-canvas py-1.5 pl-8 pr-14 text-sm text-charcoal transition-colors placeholder:text-steel hover:border-steel/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
/>
<kbd className="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 rounded border border-steel/20 bg-surface px-1.5 font-sans text-[11px] text-steel">
Ctrl K
</kbd>
</div>
</div>
<nav aria-label="Workspace" className="mt-4 min-h-0 flex-1 overflow-y-auto px-3 pb-4">
Expand Down
51 changes: 51 additions & 0 deletions src/components/layout/TopBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Search } from 'lucide-react';
import { NotificationBell } from '../../features/notifications/components/NotificationBell';

interface TopBarProps {
/** Opens the command palette, optionally seeded with typed text. */
onSearch: (seed?: string) => void;
}

const IS_MAC = /Mac|iPhone|iPad/.test(navigator.platform);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 Deprecated API: navigator.platform is deprecated and may not work in all browsers. Use navigator.userAgentData or navigator.userAgent for more reliable platform detection.

Suggested change
const IS_MAC = /Mac|iPhone|iPad/.test(navigator.platform);
const IS_MAC = typeof navigator !== 'undefined' &&
(navigator.userAgentData?.platform?.toLowerCase().includes('mac') ??
/Mac|iPhone|iPad/.test(navigator.userAgent));


/**
* Desktop-only header strip on the outer shell surface, above the floating
* content canvas: workspace search centered, the notification bell pinned to
* the far right. Below lg these controls live in MobileTopBar instead.
*/
export function TopBar({ onSearch }: TopBarProps) {
return (
<div className="hidden shrink-0 grid-cols-[1fr_minmax(0,28rem)_1fr] items-center gap-2 px-3 pt-3 lg:grid">
{/* Left spacer keeps the search truly centered despite the bell. */}
<div aria-hidden="true" />
{/* Real search input, centered on the shell. Focusing or typing hands
off to the floating palette (seeded with the typed text) — one
search surface, so the input itself stays empty and the palette
owns the query. */}
<div className="relative w-full">
<Search
size={14}
aria-hidden="true"
className="pointer-events-none absolute left-2.5 top-1/2 -translate-y-1/2 text-steel"
/>
<input
type="search"
aria-label="Search workspace"
placeholder="Search…"
value=""
onFocus={() => onSearch()}
onChange={(event) => onSearch(event.target.value)}
className="w-full rounded border border-steel/20 bg-canvas py-1.5 pl-8 pr-14 text-sm text-charcoal transition-colors placeholder:text-steel hover:border-steel/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
/>
<kbd className="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 rounded border border-steel/20 bg-surface px-1.5 font-sans text-[11px] text-steel">
{IS_MAC ? '⌘ K' : 'Ctrl K'}
</kbd>
</div>
{/* Operational inbox at the far right of the outer shell; the bell's
popover is portaled and end-aligned, so no extra wiring needed. */}
<div className="flex justify-end">
<NotificationBell />
</div>
</div>
);
}
Loading