From 6535efdd978266ccce3c600720192b4d0ba0d511 Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Wed, 25 Mar 2026 18:53:14 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20UI/UX=20polish=20=E2=80=94=20layout?= =?UTF-8?q?,=20sidebar,=20button,=20theme=20improvements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- .../(dashboard)/_components/app-sidebar.tsx | 105 ++++++++++------ apps/web/app/(dashboard)/agents/page.tsx | 20 ++-- apps/web/app/(dashboard)/inbox/page.tsx | 6 +- apps/web/app/(dashboard)/issues/[id]/page.tsx | 36 +++--- apps/web/app/(dashboard)/issues/page.tsx | 6 +- .../app/(dashboard)/knowledge-base/page.tsx | 2 +- apps/web/app/(dashboard)/layout.tsx | 2 +- apps/web/app/(dashboard)/settings/page.tsx | 6 +- apps/web/app/custom.css | 7 +- apps/web/app/globals.css | 2 +- apps/web/app/layout.tsx | 4 +- apps/web/app/pair/local/page.tsx | 2 +- apps/web/components/markdown/CodeBlock.tsx | 31 ++--- apps/web/components/theme-provider.tsx | 5 +- .../components/pickers/assignee-picker.tsx | 11 +- .../components/pickers/property-picker.tsx | 1 + apps/web/features/modals/create-issue.tsx | 2 + apps/web/features/modals/create-workspace.tsx | 113 ++++++++++++------ .../skills/components/skills-page.tsx | 22 ++-- .../workspace/components/workspace-avatar.tsx | 29 +++++ apps/web/features/workspace/index.ts | 1 + packages/ui/src/components/ui/button.tsx | 2 +- packages/ui/src/styles/custom.css | 5 - pnpm-lock.yaml | 45 +------ 24 files changed, 255 insertions(+), 210 deletions(-) create mode 100644 apps/web/features/workspace/components/workspace-avatar.tsx diff --git a/apps/web/app/(dashboard)/_components/app-sidebar.tsx b/apps/web/app/(dashboard)/_components/app-sidebar.tsx index 14e39c36..1b04cf31 100644 --- a/apps/web/app/(dashboard)/_components/app-sidebar.tsx +++ b/apps/web/app/(dashboard)/_components/app-sidebar.tsx @@ -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 ( - <> {/* Workspace Switcher */} - - - - - - - {workspace?.name ?? "Multica"} - - - - } - /> +
+ + + + + + + {workspace?.name ?? "Multica"} + + + + } + /> - + } + > + + Settings + + + + + Workspaces + + useModalStore.getState().open("create-workspace")} + > + + + + Create workspace + + {workspaces.map((ws) => ( - - {ws.name.charAt(0).toUpperCase()} - + {ws.name} {ws.id === workspace?.id && ( )} ))} - useModalStore.getState().open("create-workspace")} - > - - Create workspace - - } - > - - Settings - - Sign out + Log out - - - + + + +
+ + + + + Search + + + useModalStore.getState().open("create-issue")} + > + + + New issue + +
+
{/* Navigation */} - + {navItems.map((item) => { const isActive = pathname === item.href || @@ -160,6 +189,7 @@ export function AppSidebar() { } + className="text-muted-foreground hover:not-data-active:bg-sidebar-accent/70 data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground" > {item.label} @@ -198,6 +228,5 @@ export function AppSidebar() { )}
- ); } diff --git a/apps/web/app/(dashboard)/agents/page.tsx b/apps/web/app/(dashboard)/agents/page.tsx index 4d7b2608..b786b238 100644 --- a/apps/web/app/(dashboard)/agents/page.tsx +++ b/apps/web/app/(dashboard)/agents/page.tsx @@ -1002,7 +1002,7 @@ function AgentDetail({
-

{agent.name}

+

{agent.name}

{st.label} @@ -1017,7 +1017,7 @@ function AgentDetail({
{agent.description && ( -

{agent.description}

+

{agent.description}

)}
@@ -1091,15 +1091,15 @@ function AgentDetail({ { if (!v) setConfirmDelete(false); }}>
-
+
-
-

Delete agent?

-

+ + Delete agent? + This will permanently delete "{agent.name}" and all its configuration. -

-
+ +
+
{/* Code content */} diff --git a/apps/web/components/theme-provider.tsx b/apps/web/components/theme-provider.tsx index e40ded36..8438961f 100644 --- a/apps/web/components/theme-provider.tsx +++ b/apps/web/components/theme-provider.tsx @@ -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} > - {children} + + {children} + ) } diff --git a/apps/web/features/issues/components/pickers/assignee-picker.tsx b/apps/web/features/issues/components/pickers/assignee-picker.tsx index 0739b918..128c0691 100644 --- a/apps/web/features/issues/components/pickers/assignee-picker.tsx +++ b/apps/web/features/issues/components/pickers/assignee-picker.tsx @@ -57,15 +57,14 @@ export function AssigneePicker({ assigneeType && assigneeId ? ( <>
{assigneeType === "agent" ? ( - + ) : ( getActorInitials(assigneeType, assigneeId) )} @@ -128,8 +127,8 @@ export function AssigneePicker({ setOpen(false); }} > -
- +
+
{a.name} diff --git a/apps/web/features/issues/components/pickers/property-picker.tsx b/apps/web/features/issues/components/pickers/property-picker.tsx index 74096ead..4c734bb5 100644 --- a/apps/web/features/issues/components/pickers/property-picker.tsx +++ b/apps/web/features/issues/components/pickers/property-picker.tsx @@ -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" />
diff --git a/apps/web/features/modals/create-issue.tsx b/apps/web/features/modals/create-issue.tsx index 061273ef..fdbf346f 100644 --- a/apps/web/features/modals/create-issue.tsx +++ b/apps/web/features/modals/create-issue.tsx @@ -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 }) { New Issue + Create a new issue for the workspace.
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 ( { if (!v) onClose(); }}> - - - Create workspace - - Create a new workspace for your team. - - -
-
- - handleNameChange(e.target.value)} - placeholder="My Workspace" - className="mt-1" - /> + + + +
+
+ + Create a new workspace + + + Workspaces are shared environments where teams can work on + projects and issues. +
-
- - setSlug(e.target.value)} - placeholder="my-workspace" - className="mt-1" - /> -
-
- - + + + +
+ + handleNameChange(e.target.value)} + placeholder="My Workspace" + /> +
+
+ +
+ + multica.app/ + + setSlug(e.target.value)} + placeholder="my-workspace" + className="border-0 shadow-none focus-visible:ring-0" + /> +
+ {slugError && ( +

{slugError}

+ )} +
+
+
+ -
+
); diff --git a/apps/web/features/skills/components/skills-page.tsx b/apps/web/features/skills/components/skills-page.tsx index c607f16d..ae5d4632 100644 --- a/apps/web/features/skills/components/skills-page.tsx +++ b/apps/web/features/skills/components/skills-page.tsx @@ -298,17 +298,17 @@ function SkillDetail({ }; return ( -
+
{/* Header */}
-
-

{skill.name}

+
+

{skill.name}

{skill.description && ( -

{skill.description}

+

{skill.description}

)}
@@ -380,15 +380,15 @@ function SkillDetail({ { if (!v) setConfirmDelete(false); }}>
-
+
-
-

Delete skill?

-

+ + Delete skill? + This will permanently delete "{skill.name}" and remove it from all agents. -

-
+ +