svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+export {
+ Avatar,
+ AvatarImage,
+ AvatarFallback,
+ AvatarBadge,
+ AvatarGroup,
+ AvatarGroupCount,
+}
diff --git a/ui/src/components/ui/breadcrumb.tsx b/ui/src/components/ui/breadcrumb.tsx
new file mode 100644
index 00000000..542e7620
--- /dev/null
+++ b/ui/src/components/ui/breadcrumb.tsx
@@ -0,0 +1,109 @@
+import * as React from "react"
+import { ChevronRight, MoreHorizontal } from "lucide-react"
+import { Slot } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+
+function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
+ return
+}
+
+function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
+ return (
+
+ )
+}
+
+function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
+ return (
+
+ )
+}
+
+function BreadcrumbLink({
+ asChild,
+ className,
+ ...props
+}: React.ComponentProps<"a"> & {
+ asChild?: boolean
+}) {
+ const Comp = asChild ? Slot.Root : "a"
+
+ return (
+
+ )
+}
+
+function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
+ return (
+
+ )
+}
+
+function BreadcrumbSeparator({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<"li">) {
+ return (
+
svg]:size-3.5", className)}
+ {...props}
+ >
+ {children ?? }
+
+ )
+}
+
+function BreadcrumbEllipsis({
+ className,
+ ...props
+}: React.ComponentProps<"span">) {
+ return (
+
+
+ More
+
+ )
+}
+
+export {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis,
+}
diff --git a/ui/src/components/ui/checkbox.tsx b/ui/src/components/ui/checkbox.tsx
new file mode 100644
index 00000000..a3ec1841
--- /dev/null
+++ b/ui/src/components/ui/checkbox.tsx
@@ -0,0 +1,32 @@
+"use client"
+
+import * as React from "react"
+import { CheckIcon } from "lucide-react"
+import { Checkbox as CheckboxPrimitive } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+
+function Checkbox({
+ className,
+ ...props
+}: React.ComponentProps
) {
+ return (
+
+
+
+
+
+ )
+}
+
+export { Checkbox }
diff --git a/ui/src/components/ui/collapsible.tsx b/ui/src/components/ui/collapsible.tsx
new file mode 100644
index 00000000..2f7a4e7f
--- /dev/null
+++ b/ui/src/components/ui/collapsible.tsx
@@ -0,0 +1,33 @@
+"use client"
+
+import { Collapsible as CollapsiblePrimitive } from "radix-ui"
+
+function Collapsible({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function CollapsibleTrigger({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function CollapsibleContent({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Collapsible, CollapsibleTrigger, CollapsibleContent }
diff --git a/ui/src/components/ui/command.tsx b/ui/src/components/ui/command.tsx
new file mode 100644
index 00000000..8cb4ca7a
--- /dev/null
+++ b/ui/src/components/ui/command.tsx
@@ -0,0 +1,184 @@
+"use client"
+
+import * as React from "react"
+import { Command as CommandPrimitive } from "cmdk"
+import { SearchIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog"
+
+function Command({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function CommandDialog({
+ title = "Command Palette",
+ description = "Search for a command to run...",
+ children,
+ className,
+ showCloseButton = true,
+ ...props
+}: React.ComponentProps & {
+ title?: string
+ description?: string
+ className?: string
+ showCloseButton?: boolean
+}) {
+ return (
+
+ )
+}
+
+function CommandInput({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+ )
+}
+
+function CommandList({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function CommandEmpty({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function CommandGroup({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function CommandSeparator({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function CommandItem({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function CommandShortcut({
+ className,
+ ...props
+}: React.ComponentProps<"span">) {
+ return (
+
+ )
+}
+
+export {
+ Command,
+ CommandDialog,
+ CommandInput,
+ CommandList,
+ CommandEmpty,
+ CommandGroup,
+ CommandItem,
+ CommandShortcut,
+ CommandSeparator,
+}
diff --git a/ui/src/components/ui/dialog.tsx b/ui/src/components/ui/dialog.tsx
new file mode 100644
index 00000000..d8a06aaa
--- /dev/null
+++ b/ui/src/components/ui/dialog.tsx
@@ -0,0 +1,156 @@
+import * as React from "react"
+import { XIcon } from "lucide-react"
+import { Dialog as DialogPrimitive } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+import { Button } from "@/components/ui/button"
+
+function Dialog({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DialogTrigger({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DialogPortal({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DialogClose({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DialogOverlay({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DialogContent({
+ className,
+ children,
+ showCloseButton = true,
+ ...props
+}: React.ComponentProps & {
+ showCloseButton?: boolean
+}) {
+ return (
+
+
+
+ {children}
+ {showCloseButton && (
+
+
+ Close
+
+ )}
+
+
+ )
+}
+
+function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function DialogFooter({
+ className,
+ showCloseButton = false,
+ children,
+ ...props
+}: React.ComponentProps<"div"> & {
+ showCloseButton?: boolean
+}) {
+ return (
+
+ {children}
+ {showCloseButton && (
+
+
+
+ )}
+
+ )
+}
+
+function DialogTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DialogDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogOverlay,
+ DialogPortal,
+ DialogTitle,
+ DialogTrigger,
+}
diff --git a/ui/src/components/ui/dropdown-menu.tsx b/ui/src/components/ui/dropdown-menu.tsx
new file mode 100644
index 00000000..bffc3276
--- /dev/null
+++ b/ui/src/components/ui/dropdown-menu.tsx
@@ -0,0 +1,257 @@
+"use client"
+
+import * as React from "react"
+import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
+import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+
+function DropdownMenu({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DropdownMenuPortal({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DropdownMenuTrigger({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DropdownMenuContent({
+ className,
+ sideOffset = 4,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+function DropdownMenuGroup({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DropdownMenuItem({
+ className,
+ inset,
+ variant = "default",
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean
+ variant?: "default" | "destructive"
+}) {
+ return (
+
+ )
+}
+
+function DropdownMenuCheckboxItem({
+ className,
+ children,
+ checked,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+
+
+ {children}
+
+ )
+}
+
+function DropdownMenuRadioGroup({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DropdownMenuRadioItem({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+
+
+ {children}
+
+ )
+}
+
+function DropdownMenuLabel({
+ className,
+ inset,
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean
+}) {
+ return (
+
+ )
+}
+
+function DropdownMenuSeparator({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DropdownMenuShortcut({
+ className,
+ ...props
+}: React.ComponentProps<"span">) {
+ return (
+
+ )
+}
+
+function DropdownMenuSub({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DropdownMenuSubTrigger({
+ className,
+ inset,
+ children,
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean
+}) {
+ return (
+
+ {children}
+
+
+ )
+}
+
+function DropdownMenuSubContent({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export {
+ DropdownMenu,
+ DropdownMenuPortal,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuLabel,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuSub,
+ DropdownMenuSubTrigger,
+ DropdownMenuSubContent,
+}
diff --git a/ui/src/components/ui/label.tsx b/ui/src/components/ui/label.tsx
new file mode 100644
index 00000000..f752f82b
--- /dev/null
+++ b/ui/src/components/ui/label.tsx
@@ -0,0 +1,22 @@
+import * as React from "react"
+import { Label as LabelPrimitive } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+
+function Label({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Label }
diff --git a/ui/src/components/ui/popover.tsx b/ui/src/components/ui/popover.tsx
new file mode 100644
index 00000000..103bec3e
--- /dev/null
+++ b/ui/src/components/ui/popover.tsx
@@ -0,0 +1,87 @@
+import * as React from "react"
+import { Popover as PopoverPrimitive } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+
+function Popover({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function PopoverTrigger({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function PopoverContent({
+ className,
+ align = "center",
+ sideOffset = 4,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+function PopoverAnchor({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function PopoverTitle({ className, ...props }: React.ComponentProps<"h2">) {
+ return (
+
+ )
+}
+
+function PopoverDescription({
+ className,
+ ...props
+}: React.ComponentProps<"p">) {
+ return (
+
+ )
+}
+
+export {
+ Popover,
+ PopoverTrigger,
+ PopoverContent,
+ PopoverAnchor,
+ PopoverHeader,
+ PopoverTitle,
+ PopoverDescription,
+}
diff --git a/ui/src/components/ui/scroll-area.tsx b/ui/src/components/ui/scroll-area.tsx
new file mode 100644
index 00000000..f71026ba
--- /dev/null
+++ b/ui/src/components/ui/scroll-area.tsx
@@ -0,0 +1,56 @@
+import * as React from "react"
+import { ScrollArea as ScrollAreaPrimitive } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+
+function ScrollArea({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+ {children}
+
+
+
+
+ )
+}
+
+function ScrollBar({
+ className,
+ orientation = "vertical",
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+export { ScrollArea, ScrollBar }
diff --git a/ui/src/components/ui/sheet.tsx b/ui/src/components/ui/sheet.tsx
new file mode 100644
index 00000000..59630905
--- /dev/null
+++ b/ui/src/components/ui/sheet.tsx
@@ -0,0 +1,143 @@
+"use client"
+
+import * as React from "react"
+import { XIcon } from "lucide-react"
+import { Dialog as SheetPrimitive } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+
+function Sheet({ ...props }: React.ComponentProps) {
+ return
+}
+
+function SheetTrigger({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function SheetClose({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function SheetPortal({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function SheetOverlay({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function SheetContent({
+ className,
+ children,
+ side = "right",
+ showCloseButton = true,
+ ...props
+}: React.ComponentProps & {
+ side?: "top" | "right" | "bottom" | "left"
+ showCloseButton?: boolean
+}) {
+ return (
+
+
+
+ {children}
+ {showCloseButton && (
+
+
+ Close
+
+ )}
+
+
+ )
+}
+
+function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SheetTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function SheetDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export {
+ Sheet,
+ SheetTrigger,
+ SheetClose,
+ SheetContent,
+ SheetHeader,
+ SheetFooter,
+ SheetTitle,
+ SheetDescription,
+}
diff --git a/ui/src/components/ui/skeleton.tsx b/ui/src/components/ui/skeleton.tsx
new file mode 100644
index 00000000..32ea0ef7
--- /dev/null
+++ b/ui/src/components/ui/skeleton.tsx
@@ -0,0 +1,13 @@
+import { cn } from "@/lib/utils"
+
+function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+export { Skeleton }
diff --git a/ui/src/components/ui/tabs.tsx b/ui/src/components/ui/tabs.tsx
new file mode 100644
index 00000000..7bf18aa7
--- /dev/null
+++ b/ui/src/components/ui/tabs.tsx
@@ -0,0 +1,89 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+import { Tabs as TabsPrimitive } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+
+function Tabs({
+ className,
+ orientation = "horizontal",
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+const tabsListVariants = cva(
+ "rounded-lg p-[3px] group-data-[orientation=horizontal]/tabs:h-9 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col",
+ {
+ variants: {
+ variant: {
+ default: "bg-muted",
+ line: "gap-1 bg-transparent",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+function TabsList({
+ className,
+ variant = "default",
+ ...props
+}: React.ComponentProps &
+ VariantProps) {
+ return (
+
+ )
+}
+
+function TabsTrigger({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function TabsContent({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
diff --git a/ui/src/components/ui/textarea.tsx b/ui/src/components/ui/textarea.tsx
new file mode 100644
index 00000000..7f21b5e7
--- /dev/null
+++ b/ui/src/components/ui/textarea.tsx
@@ -0,0 +1,18 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
+ return (
+
+ )
+}
+
+export { Textarea }
diff --git a/ui/src/components/ui/tooltip.tsx b/ui/src/components/ui/tooltip.tsx
new file mode 100644
index 00000000..d80a1446
--- /dev/null
+++ b/ui/src/components/ui/tooltip.tsx
@@ -0,0 +1,57 @@
+"use client"
+
+import * as React from "react"
+import { Tooltip as TooltipPrimitive } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+
+function TooltipProvider({
+ delayDuration = 0,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function Tooltip({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function TooltipTrigger({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function TooltipContent({
+ className,
+ sideOffset = 0,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+ {children}
+
+
+
+ )
+}
+
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
diff --git a/ui/src/context/BreadcrumbContext.tsx b/ui/src/context/BreadcrumbContext.tsx
new file mode 100644
index 00000000..4d7c563b
--- /dev/null
+++ b/ui/src/context/BreadcrumbContext.tsx
@@ -0,0 +1,35 @@
+import { createContext, useCallback, useContext, useState, type ReactNode } from "react";
+
+export interface Breadcrumb {
+ label: string;
+ href?: string;
+}
+
+interface BreadcrumbContextValue {
+ breadcrumbs: Breadcrumb[];
+ setBreadcrumbs: (crumbs: Breadcrumb[]) => void;
+}
+
+const BreadcrumbContext = createContext(null);
+
+export function BreadcrumbProvider({ children }: { children: ReactNode }) {
+ const [breadcrumbs, setBreadcrumbsState] = useState([]);
+
+ const setBreadcrumbs = useCallback((crumbs: Breadcrumb[]) => {
+ setBreadcrumbsState(crumbs);
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useBreadcrumbs() {
+ const ctx = useContext(BreadcrumbContext);
+ if (!ctx) {
+ throw new Error("useBreadcrumbs must be used within BreadcrumbProvider");
+ }
+ return ctx;
+}
diff --git a/ui/src/context/DialogContext.tsx b/ui/src/context/DialogContext.tsx
new file mode 100644
index 00000000..206d0b08
--- /dev/null
+++ b/ui/src/context/DialogContext.tsx
@@ -0,0 +1,45 @@
+import { createContext, useCallback, useContext, useState, type ReactNode } from "react";
+
+interface NewIssueDefaults {
+ status?: string;
+ priority?: string;
+ projectId?: string;
+}
+
+interface DialogContextValue {
+ newIssueOpen: boolean;
+ newIssueDefaults: NewIssueDefaults;
+ openNewIssue: (defaults?: NewIssueDefaults) => void;
+ closeNewIssue: () => void;
+}
+
+const DialogContext = createContext(null);
+
+export function DialogProvider({ children }: { children: ReactNode }) {
+ const [newIssueOpen, setNewIssueOpen] = useState(false);
+ const [newIssueDefaults, setNewIssueDefaults] = useState({});
+
+ const openNewIssue = useCallback((defaults: NewIssueDefaults = {}) => {
+ setNewIssueDefaults(defaults);
+ setNewIssueOpen(true);
+ }, []);
+
+ const closeNewIssue = useCallback(() => {
+ setNewIssueOpen(false);
+ setNewIssueDefaults({});
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useDialog() {
+ const ctx = useContext(DialogContext);
+ if (!ctx) {
+ throw new Error("useDialog must be used within DialogProvider");
+ }
+ return ctx;
+}
diff --git a/ui/src/context/PanelContext.tsx b/ui/src/context/PanelContext.tsx
new file mode 100644
index 00000000..96d4cd51
--- /dev/null
+++ b/ui/src/context/PanelContext.tsx
@@ -0,0 +1,35 @@
+import { createContext, useCallback, useContext, useState, type ReactNode } from "react";
+
+interface PanelContextValue {
+ panelContent: ReactNode | null;
+ openPanel: (content: ReactNode) => void;
+ closePanel: () => void;
+}
+
+const PanelContext = createContext(null);
+
+export function PanelProvider({ children }: { children: ReactNode }) {
+ const [panelContent, setPanelContent] = useState(null);
+
+ const openPanel = useCallback((content: ReactNode) => {
+ setPanelContent(content);
+ }, []);
+
+ const closePanel = useCallback(() => {
+ setPanelContent(null);
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function usePanel() {
+ const ctx = useContext(PanelContext);
+ if (!ctx) {
+ throw new Error("usePanel must be used within PanelProvider");
+ }
+ return ctx;
+}
diff --git a/ui/src/hooks/useKeyboardShortcuts.ts b/ui/src/hooks/useKeyboardShortcuts.ts
new file mode 100644
index 00000000..6120da80
--- /dev/null
+++ b/ui/src/hooks/useKeyboardShortcuts.ts
@@ -0,0 +1,40 @@
+import { useEffect } from "react";
+
+interface ShortcutHandlers {
+ onNewIssue?: () => void;
+ onToggleSidebar?: () => void;
+ onTogglePanel?: () => void;
+}
+
+export function useKeyboardShortcuts({ onNewIssue, onToggleSidebar, onTogglePanel }: ShortcutHandlers) {
+ useEffect(() => {
+ function handleKeyDown(e: KeyboardEvent) {
+ // Don't fire shortcuts when typing in inputs
+ const target = e.target as HTMLElement;
+ if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable) {
+ return;
+ }
+
+ // C → New Issue
+ if (e.key === "c" && !e.metaKey && !e.ctrlKey && !e.altKey) {
+ e.preventDefault();
+ onNewIssue?.();
+ }
+
+ // [ → Toggle Sidebar
+ if (e.key === "[" && !e.metaKey && !e.ctrlKey) {
+ e.preventDefault();
+ onToggleSidebar?.();
+ }
+
+ // ] → Toggle Panel
+ if (e.key === "]" && !e.metaKey && !e.ctrlKey) {
+ e.preventDefault();
+ onTogglePanel?.();
+ }
+ }
+
+ document.addEventListener("keydown", handleKeyDown);
+ return () => document.removeEventListener("keydown", handleKeyDown);
+ }, [onNewIssue, onToggleSidebar, onTogglePanel]);
+}
diff --git a/ui/src/lib/groupBy.ts b/ui/src/lib/groupBy.ts
new file mode 100644
index 00000000..cbdbf1ce
--- /dev/null
+++ b/ui/src/lib/groupBy.ts
@@ -0,0 +1,11 @@
+export function groupBy(items: T[], keyFn: (item: T) => string): Record {
+ const result: Record = {};
+ for (const item of items) {
+ const key = keyFn(item);
+ if (!result[key]) {
+ result[key] = [];
+ }
+ result[key].push(item);
+ }
+ return result;
+}
diff --git a/ui/src/lib/timeAgo.ts b/ui/src/lib/timeAgo.ts
new file mode 100644
index 00000000..88888bd9
--- /dev/null
+++ b/ui/src/lib/timeAgo.ts
@@ -0,0 +1,31 @@
+const MINUTE = 60;
+const HOUR = 60 * MINUTE;
+const DAY = 24 * HOUR;
+const WEEK = 7 * DAY;
+const MONTH = 30 * DAY;
+
+export function timeAgo(date: Date | string): string {
+ const now = Date.now();
+ const then = new Date(date).getTime();
+ const seconds = Math.round((now - then) / 1000);
+
+ if (seconds < MINUTE) return "just now";
+ if (seconds < HOUR) {
+ const m = Math.floor(seconds / MINUTE);
+ return `${m}m ago`;
+ }
+ if (seconds < DAY) {
+ const h = Math.floor(seconds / HOUR);
+ return `${h}h ago`;
+ }
+ if (seconds < WEEK) {
+ const d = Math.floor(seconds / DAY);
+ return `${d}d ago`;
+ }
+ if (seconds < MONTH) {
+ const w = Math.floor(seconds / WEEK);
+ return `${w}w ago`;
+ }
+ const mo = Math.floor(seconds / MONTH);
+ return `${mo}mo ago`;
+}
diff --git a/ui/src/main.tsx b/ui/src/main.tsx
index 62f4e649..f056297d 100644
--- a/ui/src/main.tsx
+++ b/ui/src/main.tsx
@@ -3,13 +3,25 @@ import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import { App } from "./App";
import { CompanyProvider } from "./context/CompanyContext";
+import { BreadcrumbProvider } from "./context/BreadcrumbContext";
+import { PanelProvider } from "./context/PanelContext";
+import { DialogProvider } from "./context/DialogContext";
+import { TooltipProvider } from "@/components/ui/tooltip";
import "./index.css";
createRoot(document.getElementById("root")!).render(
-
+
+
+
+
+
+
+
+
+