feat: enhance frontend UI — command palette, admin redesign, API coverage

Types + API:
- Add User, TemplatePreview, QueueCustomField types
- Add getUsers, getTemplates, createTemplate, updateTemplate,
  previewTemplate, updateQueue, updateLifecycle, updateCustomField API functions

UI:
- Command palette: keyboard-first navigation with fuzzy ticket search
- Admin: comprehensive redesign with tab-based layout (Queues, Lifecycles,
  Scrips, Custom Fields, Templates, Users)
- Ticket list: improved inbox-style rows with quick actions
- Ticket detail: enhanced conversation thread and properties sidebar
- App shell: sidebar visual refinement with active indicator bar
- Theme toggle: smoother transitions

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Gjermund Høsøien Wiggen
2026-06-09 10:43:28 +02:00
parent b96ba21e99
commit 06cc7c79a3
14 changed files with 3987 additions and 1331 deletions

View File

@@ -11,6 +11,7 @@ import {
SettingsIcon,
PanelLeftCloseIcon,
PanelLeftIcon,
CommandIcon,
} from "lucide-react";
import { getTickets, getQueues } from "@/lib/api";
import type { Queue } from "@/lib/types";
@@ -51,11 +52,11 @@ function SidebarNavItem({
href={href}
title={collapsed ? label : undefined}
className={cn(
"flex items-center px-2 py-1.5 rounded-md text-[13px] transition-all duration-150 mb-0.5",
"group flex items-center px-2 py-1.5 rounded-md text-[13px] transition-all duration-150 mb-0.5",
collapsed ? "justify-center w-full" : "justify-between",
active
? "bg-accent text-foreground font-medium"
: "text-muted-foreground hover:text-foreground hover:bg-accent font-normal"
? "bg-sidebar-primary text-sidebar-primary-foreground font-semibold shadow-[inset_3px_0_0_color-mix(in_oklch,var(--sidebar-primary-foreground)_55%,transparent)]"
: "text-sidebar-foreground/70 hover:text-sidebar-foreground hover:bg-sidebar-accent font-normal"
)}
>
<span className={cn("flex items-center", collapsed ? "" : "gap-2.5")}>
@@ -63,7 +64,10 @@ function SidebarNavItem({
{!collapsed && label}
</span>
{!collapsed && count !== undefined && count > 0 && (
<span className="text-xs tabular-nums text-muted-foreground">
<span className={cn(
"min-w-5 rounded px-1 text-right text-[11px] tabular-nums",
active ? "text-sidebar-primary-foreground/80" : "text-sidebar-foreground/45"
)}>
{count}
</span>
)}
@@ -171,7 +175,7 @@ function SidebarNav() {
{queues.length > 0 && (
<div>
{!collapsed && (
<div className="px-2 py-1.5 text-[11px] font-semibold text-muted-foreground uppercase tracking-wider">
<div className="px-2 py-1.5 text-[11px] font-semibold text-sidebar-foreground/45 uppercase">
Queues
</div>
)}
@@ -179,7 +183,7 @@ function SidebarNav() {
const active =
pathname === "/" && searchParams.get("queue") === queue.id;
const QueueIcon = () => (
<span className="w-2 h-2 rounded-full bg-muted-foreground flex-shrink-0" />
<span className="w-2 h-2 rounded-full bg-sidebar-primary flex-shrink-0 shadow-[0_0_0_3px_color-mix(in_oklch,var(--sidebar-primary)_18%,transparent)]" />
);
return (
<SidebarNavItem
@@ -203,7 +207,7 @@ function SidebarBottom() {
const collapsed = useSidebarCollapsed();
return (
<div className="border-t border-border p-2">
<div className="border-t border-sidebar-border p-2">
<SidebarNavItem
href="/admin"
icon={SettingsIcon}
@@ -217,13 +221,13 @@ function SidebarBottom() {
)}
title={collapsed ? "User" : undefined}
>
<div className="w-5 h-5 rounded-full bg-primary flex items-center justify-center flex-shrink-0">
<span className="text-primary-foreground text-[10px] font-semibold">
<div className="w-5 h-5 rounded-md bg-sidebar-primary flex items-center justify-center flex-shrink-0">
<span className="text-sidebar-primary-foreground text-[10px] font-semibold">
U
</span>
</div>
{!collapsed && (
<span className="text-[13px] text-muted-foreground truncate">
<span className="text-[13px] text-sidebar-foreground/65 truncate">
User
</span>
)}
@@ -259,39 +263,54 @@ export function AppShell({ children }: { children: React.ReactNode }) {
return (
<SidebarCollapsedContext.Provider value={sidebarCollapsed}>
<div className="flex h-screen overflow-hidden">
<div className="flex h-screen overflow-hidden bg-background">
{/* Sidebar */}
<aside
className={cn(
"flex-shrink-0 flex flex-col bg-sidebar border-r border-border transition-all duration-150",
"flex-shrink-0 flex flex-col bg-sidebar border-r border-sidebar-border transition-all duration-150 shadow-[16px_0_42px_color-mix(in_oklch,var(--sidebar)_18%,transparent)]",
sidebarCollapsed ? "w-[60px]" : "w-60"
)}
>
{/* Brand */}
<div className="h-11 flex items-center px-3 border-b border-border">
<div className="h-14 flex items-center justify-between gap-2 px-3 border-b border-sidebar-border">
<Link href="/" className="flex items-center gap-2">
<div className="w-5 h-5 rounded-md bg-primary flex items-center justify-center">
<span className="text-primary-foreground text-[11px] font-semibold">
<div className="w-7 h-7 rounded-md bg-sidebar-primary flex items-center justify-center shadow-[0_0_0_1px_color-mix(in_oklch,var(--sidebar-primary)_55%,white_20%)]">
<span className="text-sidebar-primary-foreground text-[12px] font-bold">
T
</span>
</div>
{!sidebarCollapsed && (
<span className="font-semibold text-foreground text-sm tracking-tight">
Tessera
<span className="leading-tight">
<span className="block font-semibold text-sidebar-foreground text-sm">
Tessera
</span>
<span className="block text-[10px] text-sidebar-foreground/45">
ScripFoundry
</span>
</span>
)}
</Link>
{!sidebarCollapsed && (
<button
onClick={() => setCommandOpen(true)}
className="flex h-7 items-center gap-1 rounded-md border border-sidebar-border px-2 text-[11px] text-sidebar-foreground/55 transition-colors hover:bg-sidebar-accent hover:text-sidebar-foreground"
aria-label="Open command palette"
>
<CommandIcon className="h-3.5 w-3.5" />
K
</button>
)}
</div>
{/* Nav */}
<nav className="flex-1 overflow-y-auto py-2 px-2">
<nav className="flex-1 overflow-y-auto py-3 px-2">
<Suspense
fallback={
<div className="space-y-1.5 px-2">
{Array.from({ length: 6 }).map((_, i) => (
<div
key={i}
className="h-7 bg-muted rounded-md animate-pulse"
className="h-7 bg-sidebar-accent rounded-md animate-pulse"
/>
))}
</div>
@@ -306,7 +325,7 @@ export function AppShell({ children }: { children: React.ReactNode }) {
</aside>
{/* Main */}
<main className="flex-1 overflow-hidden">{children}</main>
<main className="flex-1 overflow-hidden bg-background/88">{children}</main>
{/* Command Palette */}
<CommandPalette open={commandOpen} onOpenChange={setCommandOpen} />
@@ -315,7 +334,7 @@ export function AppShell({ children }: { children: React.ReactNode }) {
{/* Collapse toggle */}
<button
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
className="fixed bottom-4 left-0 z-40 w-6 h-6 flex items-center justify-center rounded-r-md bg-sidebar border border-border border-l-0 text-muted-foreground hover:text-foreground transition-all duration-150"
className="fixed bottom-4 left-0 z-40 w-6 h-6 flex items-center justify-center rounded-r-md bg-sidebar border border-sidebar-border border-l-0 text-sidebar-foreground/55 hover:text-sidebar-foreground transition-all duration-150"
style={{ left: sidebarCollapsed ? 60 : 240 }}
aria-label={sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
>