feat(ui): UI/UX polish — layout, sidebar, button, theme improvements

- Fix global scrollbar overflow by removing h-svh from html element
- Add h-full overflow-hidden to html/body for proper app-like layout
- Fix default button variant: add shadow-sm and hover:bg-primary/90
- Update sidebar create-issue button to bg-background with shadow
- Add WorkspaceAvatar component and search/new-issue actions to sidebar header
- Improve theme provider with TooltipProvider wrapper
- Polish various page layouts, pickers, modals, and code block styling
- Clean up custom.css unused styles

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Naiyuan Qing 2026-03-25 18:53:14 +08:00
parent f150b39f1e
commit 6535efdd97
24 changed files with 255 additions and 210 deletions

View file

@ -13,8 +13,10 @@ import {
Plus,
Check,
Sparkles,
Search,
SquarePen,
} from "lucide-react";
import { MulticaIcon } from "@/components/multica-icon";
import { WorkspaceAvatar } from "@/features/workspace";
import {
Sidebar,
SidebarContent,
@ -35,6 +37,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
import { useAuthStore } from "@/features/auth";
import { useWorkspaceStore } from "@/features/workspace";
import { useInboxStore } from "@/features/inbox";
@ -68,24 +71,24 @@ export function AppSidebar() {
};
return (
<>
<Sidebar variant="inset">
{/* Workspace Switcher */}
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger
render={
<SidebarMenuButton size="lg">
<MulticaIcon className="size-4" noSpin />
<span className="flex-1 truncate font-semibold">
{workspace?.name ?? "Multica"}
</span>
<ChevronDown className="size-4" />
</SidebarMenuButton>
}
/>
<div className="flex items-center gap-4">
<SidebarMenu className="min-w-0 flex-1">
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger
render={
<SidebarMenuButton>
<WorkspaceAvatar name={workspace?.name ?? "M"} size="sm" />
<span className="flex-1 truncate font-medium">
{workspace?.name ?? "Multica"}
</span>
<ChevronDown className="size-3 text-muted-foreground" />
</SidebarMenuButton>
}
/>
<DropdownMenuContent
className="w-52"
align="start"
@ -99,8 +102,28 @@ export function AppSidebar() {
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuLabel className="text-xs text-muted-foreground">
<DropdownMenuItem
render={<Link href="/settings" />}
>
<Settings className="h-3.5 w-3.5" />
Settings
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup className="group/ws-section">
<DropdownMenuLabel className="flex items-center text-xs text-muted-foreground">
Workspaces
<Tooltip>
<TooltipTrigger
className="ml-auto opacity-0 group-hover/ws-section:opacity-100 transition-opacity rounded hover:bg-accent p-0.5"
onClick={() => useModalStore.getState().open("create-workspace")}
>
<Plus className="h-3.5 w-3.5" />
</TooltipTrigger>
<TooltipContent side="right">
Create workspace
</TooltipContent>
</Tooltip>
</DropdownMenuLabel>
{workspaces.map((ws) => (
<DropdownMenuItem
@ -111,46 +134,52 @@ export function AppSidebar() {
}
}}
>
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded bg-muted text-[10px] font-semibold">
{ws.name.charAt(0).toUpperCase()}
</span>
<WorkspaceAvatar name={ws.name} size="sm" />
<span className="flex-1 truncate">{ws.name}</span>
{ws.id === workspace?.id && (
<Check className="h-3.5 w-3.5 text-primary" />
)}
</DropdownMenuItem>
))}
<DropdownMenuItem
onClick={() => useModalStore.getState().open("create-workspace")}
>
<Plus className="h-3.5 w-3.5" />
Create workspace
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
render={<Link href="/settings" />}
>
<Settings className="h-3.5 w-3.5" />
Settings
</DropdownMenuItem>
<DropdownMenuItem variant="destructive" onClick={logout}>
<LogOut className="h-3.5 w-3.5" />
Sign out
Log out
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
<div className="flex items-center gap-1">
<Tooltip>
<TooltipTrigger
className="flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
>
<Search className="size-4" />
</TooltipTrigger>
<TooltipContent side="bottom">Search</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger
className="flex h-7 w-7 items-center justify-center rounded-lg bg-background text-foreground shadow-sm hover:bg-accent"
onClick={() => useModalStore.getState().open("create-issue")}
>
<SquarePen className="size-3.5" />
</TooltipTrigger>
<TooltipContent side="bottom">New issue</TooltipContent>
</Tooltip>
</div>
</div>
</SidebarHeader>
{/* Navigation */}
<SidebarContent>
<SidebarGroup>
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenu className="gap-0.5">
{navItems.map((item) => {
const isActive =
pathname === item.href ||
@ -160,6 +189,7 @@ export function AppSidebar() {
<SidebarMenuButton
isActive={isActive}
render={<Link href={item.href} />}
className="text-muted-foreground hover:not-data-active:bg-sidebar-accent/70 data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground"
>
<item.icon />
<span>{item.label}</span>
@ -198,6 +228,5 @@ export function AppSidebar() {
)}
</SidebarFooter>
</Sidebar>
</>
);
}

View file

@ -1002,7 +1002,7 @@ function AgentDetail({
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-3">
<h2 className="text-base font-semibold">{agent.name}</h2>
<h2 className="text-base font-semibold truncate">{agent.name}</h2>
<span className={`flex items-center gap-1.5 text-xs ${st.color}`}>
<span className={`h-1.5 w-1.5 rounded-full ${st.dot}`} />
{st.label}
@ -1017,7 +1017,7 @@ function AgentDetail({
</span>
</div>
{agent.description && (
<p className="mt-1 text-sm text-muted-foreground">{agent.description}</p>
<p className="mt-1 text-sm text-muted-foreground line-clamp-2">{agent.description}</p>
)}
</div>
<div className="relative">
@ -1091,15 +1091,15 @@ function AgentDetail({
<Dialog open onOpenChange={(v) => { if (!v) setConfirmDelete(false); }}>
<DialogContent className="max-w-sm" showCloseButton={false}>
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-destructive/10">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-destructive/10">
<AlertCircle className="h-5 w-5 text-destructive" />
</div>
<div>
<h3 className="text-sm font-semibold">Delete agent?</h3>
<p className="mt-0.5 text-xs text-muted-foreground">
<DialogHeader className="flex-1 gap-1">
<DialogTitle className="text-sm font-semibold">Delete agent?</DialogTitle>
<DialogDescription className="text-xs">
This will permanently delete &quot;{agent.name}&quot; and all its configuration.
</p>
</div>
</DialogDescription>
</DialogHeader>
</div>
<DialogFooter>
<Button variant="ghost" onClick={() => setConfirmDelete(false)}>
@ -1184,14 +1184,14 @@ export default function AgentsPage() {
if (isLoading) {
return (
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
<div className="flex flex-1 min-h-0 items-center justify-center text-sm text-muted-foreground">
Loading...
</div>
);
}
return (
<div className="flex h-full">
<div className="flex flex-1 min-h-0">
{/* Left column — agent list */}
<div className="w-72 shrink-0 overflow-y-auto border-r">
<div className="flex h-12 items-center justify-between border-b px-4">

View file

@ -122,7 +122,7 @@ function InboxDetail({
<div className="flex items-start gap-3">
<Icon className={`mt-1 h-5 w-5 shrink-0 ${colorClass}`} />
<div className="min-w-0 flex-1">
<h2 className="text-lg font-semibold">{item.title}</h2>
<h2 className="text-lg font-semibold truncate">{item.title}</h2>
<div className="mt-1 flex items-center gap-3 text-sm text-muted-foreground">
<span className={colorClass}>{severityLabel[item.severity]}</span>
<span>·</span>
@ -223,7 +223,7 @@ export default function InboxPage() {
if (loading) {
return (
<div className="flex h-full">
<div className="flex flex-1 min-h-0">
<div className="w-80 shrink-0 border-r">
<div className="flex h-12 items-center border-b px-4">
<Skeleton className="h-5 w-16" />
@ -250,7 +250,7 @@ export default function InboxPage() {
}
return (
<div className="flex h-full">
<div className="flex flex-1 min-h-0">
{/* Left column — inbox list */}
<div className="w-80 shrink-0 overflow-y-auto border-r">
<div className="flex h-12 items-center border-b px-4">

View file

@ -36,7 +36,6 @@ import {
Tooltip,
TooltipTrigger,
TooltipContent,
TooltipProvider,
} from "@/components/ui/tooltip";
import { ActorAvatar } from "@/components/common/actor-avatar";
import type { Issue, Comment, UpdateIssueRequest } from "@multica/types";
@ -204,6 +203,7 @@ function AcceptanceCriteriaEditor({
onChange={(e) => setNewItem(e.target.value)}
onBlur={() => { if (!newItem.trim()) setAdding(false); }}
placeholder="Add criteria..."
aria-label="Add acceptance criteria"
className="flex-1 text-sm bg-transparent outline-none placeholder:text-muted-foreground"
/>
</form>
@ -286,6 +286,7 @@ function ContextRefsEditor({
onChange={(e) => setNewRef(e.target.value)}
onBlur={() => { if (!newRef.trim()) setAdding(false); }}
placeholder="Add reference URL..."
aria-label="Add context reference URL"
className="flex-1 text-sm bg-transparent outline-none placeholder:text-muted-foreground"
/>
</form>
@ -467,7 +468,7 @@ export default function IssueDetailPage({
if (loading) {
return (
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
<div className="flex flex-1 min-h-0 items-center justify-center text-sm text-muted-foreground">
Loading...
</div>
);
@ -475,14 +476,14 @@ export default function IssueDetailPage({
if (!issue) {
return (
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
<div className="flex flex-1 min-h-0 items-center justify-center text-sm text-muted-foreground">
Issue not found
</div>
);
}
return (
<div className="flex h-full">
<div className="flex flex-1 min-h-0">
{/* LEFT: Content area */}
<div className="flex-1 overflow-y-auto">
{/* Header bar */}
@ -617,20 +618,18 @@ export default function IssueDetailPage({
<span className="text-[13px] font-medium">
{getActorName(comment.author_type, comment.author_id)}
</span>
<TooltipProvider>
<Tooltip>
<TooltipTrigger
render={
<span className="text-[12px] text-muted-foreground cursor-default">
{timeAgo(comment.created_at)}
</span>
}
/>
<TooltipContent side="top">
{new Date(comment.created_at).toLocaleString()}
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Tooltip>
<TooltipTrigger
render={
<span className="text-[12px] text-muted-foreground cursor-default">
{timeAgo(comment.created_at)}
</span>
}
/>
<TooltipContent side="top">
{new Date(comment.created_at).toLocaleString()}
</TooltipContent>
</Tooltip>
{isOwn && (
<div className="ml-auto flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<Button
@ -658,6 +657,7 @@ export default function IssueDetailPage({
autoFocus
value={editContent}
onChange={(e) => setEditContent(e.target.value)}
aria-label="Edit comment"
className="w-full text-[13px] bg-transparent border-b outline-none"
onKeyDown={(e) => { if (e.key === "Escape") setEditingCommentId(null); }}
/>

View file

@ -231,7 +231,7 @@ function BoardView({
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<div className="flex h-full gap-3 overflow-x-auto p-4">
<div className="flex flex-1 min-h-0 gap-3 overflow-x-auto p-4">
{visibleStatuses.map((status) => (
<DroppableColumn
key={status}
@ -360,7 +360,7 @@ export default function IssuesPage() {
if (loading) {
return (
<div className="flex h-full flex-col">
<div className="flex flex-1 min-h-0 flex-col">
<div className="flex h-11 shrink-0 items-center justify-between border-b px-4">
<Skeleton className="h-5 w-24" />
<Skeleton className="h-8 w-24" />
@ -379,7 +379,7 @@ export default function IssuesPage() {
}
return (
<div className="flex h-full flex-col">
<div className="flex flex-1 min-h-0 flex-col">
{/* Toolbar */}
<div className="flex h-11 shrink-0 items-center justify-between border-b px-4">
<div className="flex items-center gap-2">

View file

@ -290,7 +290,7 @@ export default function KnowledgeBasePage() {
const selected = documents.find((d) => d.id === selectedId) ?? null;
return (
<div className="flex h-full">
<div className="flex flex-1 min-h-0">
{/* Left: Document list */}
<div className="w-72 shrink-0 overflow-y-auto border-r">
<div className="flex h-11 items-center justify-between border-b px-4">

View file

@ -37,7 +37,7 @@ export default function DashboardLayout({
return (
<SidebarProvider>
<AppSidebar />
<SidebarInset className="overflow-hidden">{children}</SidebarInset>
<SidebarInset>{children}</SidebarInset>
</SidebarProvider>
);
}

View file

@ -68,8 +68,8 @@ function MemberRow({
.slice(0, 2)}
</div>
<div className="min-w-0 flex-1">
<div className="text-sm font-medium">{member.name}</div>
<div className="text-xs text-muted-foreground">{member.email}</div>
<div className="text-sm font-medium truncate">{member.name}</div>
<div className="text-xs text-muted-foreground truncate">{member.email}</div>
</div>
{canEditRole ? (
<Select value={member.role} onValueChange={(value) => onRoleChange(value as MemberRole)} disabled={busy}>
@ -274,6 +274,7 @@ export default function SettingsPage() {
if (!workspace) return null;
return (
<div className="flex-1 min-h-0 overflow-y-auto">
<div className="mx-auto max-w-2xl p-6 space-y-8">
{/* Page header */}
<div className="flex items-center gap-2">
@ -523,5 +524,6 @@ export default function SettingsPage() {
</AlertDialogContent>
</AlertDialog>
</div>
</div>
);
}

View file

@ -25,7 +25,8 @@
animation: entrance-spin 0.6s ease-out forwards;
}
/* Shadcn sidebar: remove default padding from inset container */
[data-slot="sidebar-container"] {
padding: 0 !important;
/* Sidebar: open triggers (dropdown/popover) get active background */
[data-sidebar="menu-button"][data-popup-open] {
background-color: var(--sidebar-accent);
color: var(--sidebar-accent-foreground);
}

View file

@ -82,7 +82,7 @@
--sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(0.21 0.006 285.885);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.967 0.001 286.375);
--sidebar-accent: oklch(0.95 0.002 286.375);
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.705 0.015 286.067);

View file

@ -29,9 +29,9 @@ export default function RootLayout({
<html
lang="en"
suppressHydrationWarning
className={cn("antialiased font-sans", geist.variable, geistMono.variable)}
className={cn("antialiased font-sans h-full overflow-hidden", geist.variable, geistMono.variable)}
>
<body>
<body className="h-full overflow-hidden">
<ThemeProvider>
<AuthInitializer>
<WSProvider>{children}</WSProvider>

View file

@ -142,7 +142,7 @@ function LocalDaemonPairPageContent() {
<Label className="mb-2">Workspace</Label>
<Select value={selectedWorkspaceId} onValueChange={(v) => setSelectedWorkspaceId(v ?? "")}>
<SelectTrigger className="w-full">
<span className="flex flex-1 text-left">
<span className="flex flex-1 min-w-0 truncate text-left">
{selectedWorkspace?.name ?? "Select workspace"}
</span>
</SelectTrigger>

View file

@ -1,5 +1,7 @@
import * as React from 'react'
import { codeToHtml, bundledLanguages, type BundledLanguage } from 'shiki'
import { Copy, Check } from "lucide-react"
import { Button } from "@/components/ui/button"
import { cn } from '@/lib/utils'
export interface CodeBlockProps {
@ -177,36 +179,19 @@ export function CodeBlock({
<span className="text-muted-foreground font-medium uppercase tracking-wide">
{resolvedLang !== 'text' ? resolvedLang : 'plain text'}
</span>
<button
<Button
variant="ghost"
size="icon-xs"
onClick={handleCopy}
className="opacity-0 group-hover:opacity-100 transition-opacity text-muted-foreground hover:text-foreground"
aria-label="Copy code"
>
{copied ? (
<svg
className="w-4 h-4 text-green-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
<Check className="size-3.5 text-success" />
) : (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
<Copy className="size-3.5" />
)}
</button>
</Button>
</div>
{/* Code content */}

View file

@ -2,6 +2,7 @@
import * as React from "react"
import { ThemeProvider as NextThemesProvider, useTheme } from "next-themes"
import { TooltipProvider } from "@/components/ui/tooltip"
function ThemeProvider({
children,
@ -16,7 +17,9 @@ function ThemeProvider({
{...props}
>
<ThemeHotkey />
{children}
<TooltipProvider>
{children}
</TooltipProvider>
</NextThemesProvider>
)
}

View file

@ -57,15 +57,14 @@ export function AssigneePicker({
assigneeType && assigneeId ? (
<>
<div
className={`inline-flex shrink-0 items-center justify-center rounded-full font-medium text-[8px] ${
className={`inline-flex shrink-0 items-center justify-center rounded-full font-medium text-[8px] size-[18px] ${
assigneeType === "agent"
? "bg-purple-100 text-purple-700 dark:bg-purple-950 dark:text-purple-300"
? "bg-info/10 text-info"
: "bg-muted text-muted-foreground"
}`}
style={{ width: 18, height: 18 }}
>
{assigneeType === "agent" ? (
<Bot style={{ width: 10, height: 10 }} />
<Bot className="size-2.5" />
) : (
getActorInitials(assigneeType, assigneeId)
)}
@ -128,8 +127,8 @@ export function AssigneePicker({
setOpen(false);
}}
>
<div className="inline-flex h-[18px] w-[18px] shrink-0 items-center justify-center rounded-full bg-purple-100 text-purple-700 dark:bg-purple-950 dark:text-purple-300">
<Bot style={{ width: 10, height: 10 }} />
<div className="inline-flex h-[18px] w-[18px] shrink-0 items-center justify-center rounded-full bg-info/10 text-info">
<Bot className="size-2.5" />
</div>
<span>{a.name}</span>
</PickerItem>

View file

@ -62,6 +62,7 @@ export function PropertyPicker({
onSearchChange?.(e.target.value);
}}
placeholder={searchPlaceholder}
aria-label="Filter options"
className="w-full bg-transparent text-[13px] placeholder:text-muted-foreground outline-none"
/>
</div>

View file

@ -9,6 +9,7 @@ import {
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
@ -60,6 +61,7 @@ export function CreateIssueModal({ onClose }: { onClose: () => void }) {
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>New Issue</DialogTitle>
<DialogDescription className="sr-only">Create a new issue for the workspace.</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-2">
<Input

View file

@ -2,24 +2,33 @@
import { useState } from "react";
import { toast } from "sonner";
import { ArrowLeft } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from "@/components/ui/dialog";
import { Card, CardContent } from "@/components/ui/card";
import { useWorkspaceStore } from "@/features/workspace";
const SLUG_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
export function CreateWorkspaceModal({ onClose }: { onClose: () => void }) {
const [name, setName] = useState("");
const [slug, setSlug] = useState("");
const [creating, setCreating] = useState(false);
const slugError =
slug.length > 0 && !SLUG_REGEX.test(slug)
? "Only lowercase letters, numbers, and hyphens"
: null;
const canSubmit = name.trim().length > 0 && slug.trim().length > 0 && !slugError;
const handleNameChange = (value: string) => {
setName(value);
setSlug(
@ -31,7 +40,7 @@ export function CreateWorkspaceModal({ onClose }: { onClose: () => void }) {
};
const handleCreate = async () => {
if (!name.trim() || !slug.trim()) return;
if (!canSubmit) return;
setCreating(true);
try {
const { createWorkspace, switchWorkspace } =
@ -51,47 +60,73 @@ export function CreateWorkspaceModal({ onClose }: { onClose: () => void }) {
return (
<Dialog open onOpenChange={(v) => { if (!v) onClose(); }}>
<DialogContent>
<DialogHeader>
<DialogTitle>Create workspace</DialogTitle>
<DialogDescription>
Create a new workspace for your team.
</DialogDescription>
</DialogHeader>
<div className="space-y-3">
<div>
<Label className="text-xs text-muted-foreground">Name</Label>
<Input
autoFocus
type="text"
value={name}
onChange={(e) => handleNameChange(e.target.value)}
placeholder="My Workspace"
className="mt-1"
/>
<DialogContent
showCloseButton={false}
className="inset-0 flex h-full w-full max-w-none sm:max-w-none translate-0 flex-col items-center justify-center rounded-none bg-background ring-0 shadow-none"
>
<Button
variant="ghost"
size="sm"
className="absolute top-6 left-6 text-muted-foreground"
onClick={onClose}
>
<ArrowLeft className="h-4 w-4" />
Back
</Button>
<div className="flex w-full max-w-md flex-col items-center gap-6">
<div className="text-center">
<DialogTitle className="text-2xl font-semibold">
Create a new workspace
</DialogTitle>
<DialogDescription className="mt-2">
Workspaces are shared environments where teams can work on
projects and issues.
</DialogDescription>
</div>
<div>
<Label className="text-xs text-muted-foreground">Slug</Label>
<Input
type="text"
value={slug}
onChange={(e) => setSlug(e.target.value)}
placeholder="my-workspace"
className="mt-1"
/>
</div>
</div>
<DialogFooter>
<Button variant="ghost" onClick={onClose}>
Cancel
</Button>
<Card className="w-full">
<CardContent className="space-y-4 pt-6">
<div className="space-y-1.5">
<Label>Workspace Name</Label>
<Input
autoFocus
type="text"
value={name}
onChange={(e) => handleNameChange(e.target.value)}
placeholder="My Workspace"
/>
</div>
<div className="space-y-1.5">
<Label>Workspace URL</Label>
<div className="flex items-center gap-0 rounded-md border bg-background focus-within:ring-2 focus-within:ring-ring">
<span className="pl-3 text-sm text-muted-foreground select-none">
multica.app/
</span>
<Input
type="text"
value={slug}
onChange={(e) => setSlug(e.target.value)}
placeholder="my-workspace"
className="border-0 shadow-none focus-visible:ring-0"
/>
</div>
{slugError && (
<p className="text-xs text-destructive">{slugError}</p>
)}
</div>
</CardContent>
</Card>
<Button
className="w-full"
size="lg"
onClick={handleCreate}
disabled={creating || !name.trim() || !slug.trim()}
disabled={creating || !canSubmit}
>
{creating ? "Creating..." : "Create"}
{creating ? "Creating..." : "Create workspace"}
</Button>
</DialogFooter>
</div>
</DialogContent>
</Dialog>
);

View file

@ -298,17 +298,17 @@ function SkillDetail({
};
return (
<div className="flex h-full flex-col">
<div className="flex flex-1 min-h-0 flex-col">
{/* Header */}
<div className="flex items-center justify-between border-b px-6 py-4">
<div className="flex items-center gap-3">
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-muted">
<Sparkles className="h-4 w-4 text-muted-foreground" />
</div>
<div>
<h2 className="text-sm font-semibold">{skill.name}</h2>
<div className="min-w-0">
<h2 className="text-sm font-semibold truncate">{skill.name}</h2>
{skill.description && (
<p className="text-xs text-muted-foreground">{skill.description}</p>
<p className="text-xs text-muted-foreground truncate">{skill.description}</p>
)}
</div>
</div>
@ -380,15 +380,15 @@ function SkillDetail({
<Dialog open onOpenChange={(v) => { if (!v) setConfirmDelete(false); }}>
<DialogContent className="max-w-sm" showCloseButton={false}>
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-destructive/10">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-destructive/10">
<AlertCircle className="h-5 w-5 text-destructive" />
</div>
<div>
<h3 className="text-sm font-semibold">Delete skill?</h3>
<p className="mt-0.5 text-xs text-muted-foreground">
<DialogHeader className="flex-1 gap-1">
<DialogTitle className="text-sm font-semibold">Delete skill?</DialogTitle>
<DialogDescription className="text-xs">
This will permanently delete &quot;{skill.name}&quot; and remove it from all agents.
</p>
</div>
</DialogDescription>
</DialogHeader>
</div>
<DialogFooter>
<Button variant="ghost" onClick={() => setConfirmDelete(false)}>
@ -469,7 +469,7 @@ export default function SkillsPage() {
}
return (
<div className="flex h-full">
<div className="flex flex-1 min-h-0">
{/* Left column — skill list */}
<div className="w-72 shrink-0 overflow-y-auto border-r">
<div className="flex h-12 items-center justify-between border-b px-4">

View file

@ -0,0 +1,29 @@
import { cn } from "@/lib/utils";
const sizeMap = {
sm: "h-5 w-5 text-[10px] rounded",
md: "h-7 w-7 text-xs rounded-md",
lg: "h-9 w-9 text-sm rounded-md",
} as const;
interface WorkspaceAvatarProps {
name: string;
size?: keyof typeof sizeMap;
className?: string;
}
function WorkspaceAvatar({ name, size = "sm", className }: WorkspaceAvatarProps) {
return (
<span
className={cn(
"inline-flex shrink-0 items-center justify-center border bg-muted font-semibold text-muted-foreground",
sizeMap[size],
className
)}
>
{name.charAt(0).toUpperCase()}
</span>
);
}
export { WorkspaceAvatar, type WorkspaceAvatarProps };

View file

@ -1,2 +1,3 @@
export { useWorkspaceStore } from "./store";
export { useActorName } from "./hooks";
export { WorkspaceAvatar } from "./components/workspace-avatar";

View file

@ -10,7 +10,7 @@ const buttonVariants = cva(
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
default: "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90",
outline:
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary:

View file

@ -107,11 +107,6 @@
@apply w-full max-w-4xl mx-auto;
}
/* Sidebar: remove default padding from inset container */
[data-slot="sidebar-container"] {
padding: 0 !important;
}
/* Tool status: running glow pulse */
@keyframes glow-pulse {
0%, 100% { box-shadow: 0 0 0 0 var(--tool-running); }

45
pnpm-lock.yaml generated
View file

@ -113,7 +113,7 @@ importers:
version: 1.0.1(react@19.2.3)
next:
specifier: ^16.1.6
version: 16.2.0(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
version: 16.2.0(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
next-themes:
specifier: ^0.4.6
version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@ -197,28 +197,6 @@ importers:
specifier: ^4.1.0
version: 4.1.0(@types/node@25.5.0)(jsdom@29.0.1(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@25.5.0)(typescript@5.9.3))(vite@8.0.1(@types/node@25.5.0)(jiti@2.6.1))
packages/hooks:
dependencies:
'@multica/sdk':
specifier: workspace:*
version: link:../sdk
'@multica/store':
specifier: workspace:*
version: link:../store
'@multica/types':
specifier: workspace:*
version: link:../types
react:
specifier: 'catalog:'
version: 19.2.3
devDependencies:
'@types/react':
specifier: ^19.2.0
version: 19.2.14
typescript:
specifier: 'catalog:'
version: 5.9.3
packages/sdk:
dependencies:
'@multica/types':
@ -229,19 +207,6 @@ importers:
specifier: 'catalog:'
version: 5.9.3
packages/store:
dependencies:
'@multica/types':
specifier: workspace:*
version: link:../types
zustand:
specifier: 'catalog:'
version: 5.0.12(@types/react@19.2.14)(immer@11.1.4)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3))
devDependencies:
typescript:
specifier: 'catalog:'
version: 5.9.3
packages/types:
devDependencies:
typescript:
@ -6163,7 +6128,7 @@ snapshots:
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
next@16.2.0(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
next@16.2.0(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
dependencies:
'@next/env': 16.2.0
'@swc/helpers': 0.5.15
@ -6172,7 +6137,7 @@ snapshots:
postcss: 8.4.31
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.3)
styled-jsx: 5.1.6(react@19.2.3)
optionalDependencies:
'@next/swc-darwin-arm64': 16.2.0
'@next/swc-darwin-x64': 16.2.0
@ -6838,12 +6803,10 @@ snapshots:
dependencies:
inline-style-parser: 0.2.7
styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.3):
styled-jsx@5.1.6(react@19.2.3):
dependencies:
client-only: 0.0.1
react: 19.2.3
optionalDependencies:
'@babel/core': 7.29.0
symbol-tree@3.2.4: {}