diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 80835d2..4c46eb6 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -7,6 +7,7 @@ import { ThemeProvider } from "next-themes"; import { ModeToggle } from "@/components/mode-toggle"; import SignIn from "@/components/sign-in"; import { Toaster } from "@/components/ui/sonner"; +import { TooltipProvider } from "@/components/ui/tooltip"; const geistSans = Geist({ subsets: ["latin", "cyrillic"], @@ -34,12 +35,13 @@ export default function RootLayout({ - + +
- - + + + ); diff --git a/src/components/ai-toolbar.tsx b/src/components/ai-toolbar.tsx index eab5f58..6b4a396 100644 --- a/src/components/ai-toolbar.tsx +++ b/src/components/ai-toolbar.tsx @@ -7,21 +7,82 @@ import { Download, FileUp, ImageIcon, + Info, Loader2, + LogIn, Sparkles, Trash2, X, } from "lucide-react"; import Image from "next/image"; +import { useEffect, useRef, useState } from "react"; import { IcsFilePicker } from "@/components/ics-file-picker"; import { ImagePicker } from "@/components/image-picker"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { Separator } from "@/components/ui/separator"; +import { + HoverCard, + HoverCardContent, + HoverCardTrigger, +} from "@/components/ui/hover-card"; +import { Kbd, KbdGroup } from "@/components/ui/kbd"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; import { Skeleton } from "@/components/ui/skeleton"; import { Textarea } from "@/components/ui/textarea"; +import { + SHORTCUT_DEFINITIONS, + detectOs, + resolveKeys, + type Os, +} from "@/lib/keyboard-shortcuts"; import type { CalendarEvent } from "@/lib/types"; +// ─── OS detection hook ──────────────────────────────────────────────────────── + +function useOs(): Os { + // Start with "unknown" for SSR — effect sets the real value after hydration + const [os, setOs] = useState("unknown"); + useEffect(() => { + setOs(detectOs()); + }, []); + return os; +} + +// ─── Shared shortcuts list (rendered in both HoverCard and Popover) ─────────── + +function ShortcutsList({ os }: { os: Os }) { + return ( + <> +

+ Keyboard shortcuts +

+
    + {SHORTCUT_DEFINITIONS.map((shortcut) => ( +
  • + + {shortcut.label} + + + {resolveKeys(shortcut.modifiers, os).map((key) => ( + {key} + ))} + +
  • + ))} +
+ + ); +} + +// ─── Types ──────────────────────────────────────────────────────────────────── + interface AIToolbarProps { isAuthenticated: boolean; isPending: boolean; @@ -44,6 +105,8 @@ interface AIToolbarProps { onClearAll: () => void; } +// ─── Component ──────────────────────────────────────────────────────────────── + export const AIToolbar = ({ isAuthenticated, isPending, @@ -64,77 +127,185 @@ export const AIToolbar = ({ onExport, onClearAll, }: AIToolbarProps) => { + // Ref to imperatively open the file picker from the keyboard shortcut + const imageTriggerRef = useRef<{ open: () => void }>(null); + + // When the popover is pinned open, suppress the hover card so they don't overlap + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + // Detect OS after hydration for keyboard shortcut glyphs + const os = useOs(); + if (isPending) { return ( -
- +
+ +
); } return ( -
-
- {/* AI command — only shown when authenticated */} +
+ {/* ── Zone 1: AI ───────────────────────────────────────────────────────── */} +
{isAuthenticated ? ( - <> - {/* Textarea fills full width; action icons sit below on mobile, to the right on sm+ */} -
-
-
- - - AI Command - -
-