diff --git a/components/Layout.js b/components/Layout.js
index e53a219..6d73922 100644
--- a/components/Layout.js
+++ b/components/Layout.js
@@ -4,6 +4,8 @@ import Link from 'next/link';
import { useTheme } from '../lib/theme-context';
import MobileNavigation from './MobileNavigation';
import DailyEmberWidget from './DailyEmberWidget';
+import TopSearchBar from './ui/TopSearchBar';
+import CommandPaletteModal from './ui/CommandPaletteModal';
// User Profile Dropdown Component
function UserProfileDropdown({ user, onMobileMenuClose }) {
@@ -606,6 +608,28 @@ export default function Layout({ children, user = null, showSearch = false }) {
const { theme, toggleTheme } = useTheme();
const [searchQuery, setSearchQuery] = useState('');
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
+ // Command palette (redesign-v2 sub-convoy #3, 2026-06-04). Opens
+ // globally on ⌘K / Ctrl+K for authenticated users. The keyboard
+ // listener is attached at Layout-scope (not _app.js) so it doesn't
+ // fire on the public marketing pages that don't mount .
+ const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useState(false);
+
+ useEffect(() => {
+ if (!user) return undefined;
+ const handler = (event) => {
+ const isShortcut =
+ (event.metaKey || event.ctrlKey) &&
+ !event.shiftKey &&
+ !event.altKey &&
+ event.key.toLowerCase() === 'k';
+ if (isShortcut) {
+ event.preventDefault();
+ setIsCommandPaletteOpen((prev) => !prev);
+ }
+ };
+ document.addEventListener('keydown', handler);
+ return () => document.removeEventListener('keydown', handler);
+ }, [user]);
return (
@@ -871,39 +895,22 @@ export default function Layout({ children, user = null, showSearch = false }) {
{/* Main Content */}
- {/* Top Header - Only show search on dashboard */}
- {showSearch && (
-
-
-
-
- setSearchQuery(e.target.value)}
- />
-
- ⌘F
-
-
-
-
-
-
-
-
+ {/* Global top bar — redesign-v2 sub-convoy #3 (2026-06-04).
+ Rendered for ALL authenticated pages so the search +
+ notifications + user-menu chrome is consistent everywhere
+ (operator decision § 7.3 of the umbrella convoy: "sweep to
+ all authenticated pages in this convoy"). Unauthenticated
+ visitors see no top bar — public landing has its own
+ marketing header. The legacy `showSearch` prop is honored
+ via fall-through but no longer drives visibility; it can
+ be removed in a cleanup convoy along with the dead
+ `searchQuery` state. */}
+ {user && (
+ setIsCommandPaletteOpen(true)}
+ />
)}
{/* Page Content */}
@@ -911,6 +918,11 @@ export default function Layout({ children, user = null, showSearch = false }) {
{children}
+
+ setIsCommandPaletteOpen(false)}
+ />
);
}
\ No newline at end of file
diff --git a/components/ui/CommandPaletteModal.js b/components/ui/CommandPaletteModal.js
new file mode 100644
index 0000000..cb3cad6
--- /dev/null
+++ b/components/ui/CommandPaletteModal.js
@@ -0,0 +1,139 @@
+import { useRef, useState } from 'react';
+import { useRouter } from 'next/router';
+import Modal from './Modal';
+
+/**
+ * CommandPaletteModal — the surface that opens on Cmd+K (or Ctrl+K).
+ *
+ * From the redesign-v2 mockup's top-bar Cmd+K hint. Operator decided
+ * (§ 7.2 of the umbrella convoy) to ship the keyboard handler + a
+ * minimum-viable palette in this convoy, deferring federated search
+ * across cards/decks/lists to a follow-up.
+ *
+ * What it does today:
+ * - Single search input, auto-focused on open
+ * - Submit on Enter → routes to /cards?q= (the closest
+ * existing search surface). Closes the palette.
+ * - 3 placeholder "Quick actions" rows below the input, each
+ * routing to an existing page (Dashboard, Cards, Scanner).
+ * - Escape closes (delegated to the underlying ).
+ *
+ * What it does NOT do (deferred):
+ * - Live result preview as you type
+ * - "Recent searches" from a backend store
+ * - Cross-resource ranked results (cards + decks + lists + users)
+ *
+ * Props:
+ * open boolean — controls visibility
+ * onClose () => void
+ */
+const QUICK_ACTIONS = [
+ { label: 'Go to Dashboard', href: '/dashboard', keywords: ['home', 'dash'] },
+ { label: 'Browse Cards', href: '/cards', keywords: ['catalog', 'all'] },
+ { label: 'Open Scanner', href: '/scanner', keywords: ['scan', 'camera'] },
+];
+
+export default function CommandPaletteModal({ open, onClose }) {
+ const router = useRouter();
+ const inputRef = useRef(null);
+ // Reset query when the modal opens. We track the previous `open` and
+ // clear synchronously during render (per the react-hooks/set-state-
+ // in-effect lint rule — the same pattern used elsewhere in the
+ // codebase, e.g. NavigationContent's path-change resetter).
+ const [query, setQuery] = useState('');
+ const [wasOpen, setWasOpen] = useState(open);
+ if (open && !wasOpen) {
+ setWasOpen(true);
+ setQuery('');
+ } else if (!open && wasOpen) {
+ setWasOpen(false);
+ }
+
+ const handleSubmit = (event) => {
+ event.preventDefault();
+ const trimmed = query.trim();
+ if (trimmed.length === 0) return;
+ onClose?.();
+ router.push({ pathname: '/cards', query: { q: trimmed } });
+ };
+
+ const handleQuickAction = (href) => {
+ onClose?.();
+ router.push(href);
+ };
+
+ return (
+
+
+
+ );
+}
diff --git a/components/ui/TopSearchBar.js b/components/ui/TopSearchBar.js
new file mode 100644
index 0000000..650413f
--- /dev/null
+++ b/components/ui/TopSearchBar.js
@@ -0,0 +1,233 @@
+import { useState } from 'react';
+import { useRouter } from 'next/router';
+import Link from 'next/link';
+
+/**
+ * TopSearchBar — the top horizontal chrome strip from the operator's
+ * redesign-v2 mockup.
+ *
+ * Layout: prominent search input on the left (with magnifier + Cmd+K
+ * hint pill), then 3 right-side widgets — notification bell (with red
+ * badge when there are unread items), mail/inbox icon, and a compact
+ * user-menu chip (avatar + name + chevron).
+ *
+ * Mounted globally inside for authenticated users; renders
+ * as a no-op `null` for unauthenticated visitors (the public landing
+ * uses its own marketing header).
+ *
+ * Props:
+ * user { username, email, avatar_url, ... }
+ * notificationCount number — drives the bell badge; 0 hides it
+ * onOpenCommandPalette () => void — fires when user clicks the
+ * search input or presses ⌘K (the global
+ * keydown listener lives in ).
+ *
+ * Implementation notes:
+ * - The search input is intentionally read-only-ish: clicking or
+ * focusing it opens the CommandPaletteModal instead of typing
+ * inline. This matches Linear / Notion / Vercel's pattern and
+ * avoids two competing search affordances on the page.
+ * - The "Cmd+K" hint adapts to the user's platform: ⌘K on Mac,
+ * Ctrl+K elsewhere. Detection is best-effort via navigator.platform
+ * and gracefully defaults to ⌘ on the server (matches the Mac
+ * audience's default expectation).
+ * - User menu (avatar + name) is a Link that routes to /settings;
+ * a follow-up convoy can replace this with a real dropdown if
+ * the operator wants the full Notion-style menu.
+ */
+function isMacPlatform() {
+ if (typeof window === 'undefined' || !window.navigator) return true;
+ return /Mac|iPhone|iPad|iPod/.test(window.navigator.platform);
+}
+
+export default function TopSearchBar({
+ user,
+ notificationCount = 0,
+ onOpenCommandPalette,
+}) {
+ const router = useRouter();
+ const [hovered, setHovered] = useState(false);
+
+ if (!user) return null;
+
+ const isMac = isMacPlatform();
+ const shortcutLabel = isMac ? '⌘K' : 'Ctrl+K';
+ const initial = (user.username || user.email || 'U').charAt(0).toUpperCase();
+ const displayName =
+ user.username || user.email?.split('@')[0] || 'Account';
+
+ const openPalette = () => {
+ onOpenCommandPalette?.();
+ };
+
+ return (
+
+ {/* Search affordance — read-only-ish input that opens the
+ command palette on click. */}
+
+
+