refactor(webui): unify floating controls

This commit is contained in:
Xubin Ren
2026-08-04 18:05:34 +08:00
parent faff0ac2fa
commit 7819cef7bd
16 changed files with 777 additions and 257 deletions
@@ -10,10 +10,10 @@ import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { useSessionAutomationJobs } from "@/hooks/useSessionAutomationJobs";
import { currentLocale } from "@/i18n";
import { fmtDateTime } from "@/lib/format";
@@ -63,8 +63,8 @@ export function SessionInfoPopover({ sessionKey, token, title }: SessionInfoPopo
);
return (
<DropdownMenu modal={false} open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="ghost"
size="icon"
@@ -76,11 +76,11 @@ export function SessionInfoPopover({ sessionKey, token, title }: SessionInfoPopo
>
<ListTodo className="h-4 w-4 stroke-[1.75]" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
</PopoverTrigger>
<PopoverContent
align="end"
sideOffset={8}
className="w-[min(23rem,calc(100vw-1.5rem))] rounded-[24px] p-0"
className="w-[min(23rem,calc(100vw-1.5rem))] p-0"
>
<div className="space-y-3 px-4 py-3.5">
<div className="min-w-0">
@@ -108,8 +108,8 @@ export function SessionInfoPopover({ sessionKey, token, title }: SessionInfoPopo
{automationContent}
</div>
</DropdownMenuContent>
</DropdownMenu>
</PopoverContent>
</Popover>
);
}
+12 -8
View File
@@ -50,6 +50,10 @@ import {
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import {
floatingItemClassName,
floatingSurfaceVisualClassName,
} from "@/components/ui/floating-surface";
import {
Tooltip,
TooltipContent,
@@ -2688,10 +2692,9 @@ function CliAppMentionPalette({
aria-label={t("thread.composer.mentions.ariaLabel")}
style={{ maxHeight: layout.maxHeight }}
className={cn(
"absolute left-1/2 z-30 w-[calc(100%-0.5rem)] -translate-x-1/2 overflow-hidden rounded-[22px] border",
floatingSurfaceVisualClassName,
"absolute left-1/2 z-30 w-[calc(100%-0.5rem)] -translate-x-1/2 overflow-hidden",
layout.placement === "above" ? "bottom-full mb-2" : "top-full mt-2",
"border-border/70 bg-popover p-2 text-popover-foreground shadow-[0_20px_60px_rgba(15,23,42,0.12)]",
"dark:border-white/10 dark:shadow-[0_24px_60px_rgba(0,0,0,0.42)]",
isHero ? "max-w-[58rem]" : "max-w-[49.5rem]",
)}
>
@@ -2728,7 +2731,8 @@ function CliAppMentionPalette({
onChoose(candidate);
}}
className={cn(
"flex min-h-10 w-full items-center gap-2.5 rounded-[13px] px-2.5 py-1.5 text-left transition-colors",
floatingItemClassName,
"flex min-h-10 w-full items-center gap-2.5 px-2.5 py-1.5 text-left transition-colors",
selected
? "bg-foreground/[0.055] text-foreground"
: "text-foreground/90 hover:bg-foreground/[0.04]",
@@ -2836,10 +2840,9 @@ function SlashCommandPalette({
aria-label={t("thread.composer.slash.ariaLabel")}
style={{ maxHeight: layout.maxHeight }}
className={cn(
"absolute left-1/2 z-30 w-[calc(100%-0.5rem)] -translate-x-1/2 overflow-hidden rounded-[18px] border",
floatingSurfaceVisualClassName,
"absolute left-1/2 z-30 w-[calc(100%-0.5rem)] -translate-x-1/2 overflow-hidden",
layout.placement === "above" ? "bottom-full mb-2" : "top-full mt-2",
"border-border/65 bg-popover p-1.5 text-popover-foreground shadow-[0_18px_55px_rgba(15,23,42,0.16)]",
"dark:border-white/10 dark:shadow-[0_22px_55px_rgba(0,0,0,0.45)]",
isHero ? "max-w-[58rem]" : "max-w-[49.5rem]",
)}
>
@@ -2868,7 +2871,8 @@ function SlashCommandPalette({
onChoose(command);
}}
className={cn(
"flex min-h-[44px] w-full items-center gap-3 rounded-[13px] px-3 py-2 text-left transition-colors",
floatingItemClassName,
"flex min-h-[44px] w-full items-center gap-3 px-3 py-2 text-left transition-colors",
selected
? "bg-foreground/[0.065] text-foreground dark:bg-white/[0.09]"
: "text-foreground/86 hover:bg-foreground/[0.045] dark:hover:bg-white/[0.065]",
@@ -6,10 +6,20 @@ import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
floatingItemClassName,
floatingItemFocusClassName,
} from "@/components/ui/floating-surface";
import { Input } from "@/components/ui/input";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import type {
WorkspaceAccessMode,
WorkspaceScopePayload,
@@ -134,8 +144,8 @@ export function WorkspaceProjectPicker({
return (
<div className="flex min-w-0 items-center rounded-b-[28px] bg-muted/45 px-3 py-1.5 dark:bg-white/[0.045] sm:px-4">
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<button
type="button"
disabled={disabled}
@@ -151,16 +161,21 @@ export function WorkspaceProjectPicker({
<span className="truncate">{projectLabel}</span>
<ChevronDown className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent
</PopoverTrigger>
<PopoverContent
align="start"
side="bottom"
sideOffset={8}
className="w-[min(25rem,calc(100vw-2rem))] rounded-[22px]"
className="w-[min(25rem,calc(100vw-2rem))]"
>
<DropdownMenuItem
onSelect={() => applyProjectPath(defaultScope.project_path, defaultScope.project_name)}
className="flex min-h-[48px] cursor-default gap-3 rounded-[16px] px-3 py-2.5 focus:bg-muted/55"
<button
type="button"
onClick={() => applyProjectPath(defaultScope.project_path, defaultScope.project_name)}
className={cn(
floatingItemClassName,
floatingItemFocusClassName,
"flex min-h-[48px] w-full cursor-default gap-3 px-3 py-2.5 focus:bg-muted/55",
)}
>
<span className="grid h-8 w-8 shrink-0 place-items-center rounded-[12px] bg-muted text-foreground/80">
<Folder className="h-4 w-4" />
@@ -174,14 +189,9 @@ export function WorkspaceProjectPicker({
</span>
</span>
{!currentProjectScope ? <Check className="h-4 w-4 text-foreground/80" /> : null}
</DropdownMenuItem>
</button>
<div className="my-1 h-px bg-border/45" />
<div
className="space-y-1.5 px-1.5 py-1.5"
onKeyDown={(event) => {
if (event.key !== "Escape") event.stopPropagation();
}}
>
<div className="space-y-1.5 px-1.5 py-1.5">
<form
className="flex items-center gap-2"
onSubmit={(event) => {
@@ -217,8 +227,8 @@ export function WorkspaceProjectPicker({
</p>
) : null}
</div>
</DropdownMenuContent>
</DropdownMenu>
</PopoverContent>
</Popover>
</div>
);
}
@@ -284,46 +294,51 @@ export function WorkspaceAccessMenu({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-56">
<AccessMenuItem
icon={<Hand className="h-4 w-4" />}
label={t("thread.composer.workspace.default")}
selected={mode === "restricted"}
onSelect={() => setMode("restricted")}
/>
<AccessMenuItem
icon={<AlertTriangle className="h-4 w-4" />}
label={t("thread.composer.workspace.full")}
selected={mode === "full"}
disabled={!canUseFullAccess}
warning
onSelect={() => setMode("full")}
/>
<DropdownMenuRadioGroup
value={mode}
onValueChange={(value) => {
if (value === "restricted" || value === "full") setMode(value);
}}
>
<AccessMenuItem
value="restricted"
icon={<Hand className="h-4 w-4" />}
label={t("thread.composer.workspace.default")}
/>
<AccessMenuItem
value="full"
icon={<AlertTriangle className="h-4 w-4" />}
label={t("thread.composer.workspace.full")}
disabled={!canUseFullAccess}
warning
/>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
);
}
function AccessMenuItem({
value,
icon,
label,
selected,
disabled,
warning,
onSelect,
}: {
value: WorkspaceAccessMode;
icon: ReactNode;
label: string;
selected: boolean;
disabled?: boolean;
warning?: boolean;
onSelect: () => void;
}) {
return (
<DropdownMenuItem
<DropdownMenuRadioItem
value={value}
indicator="check"
indicatorPosition="end"
disabled={disabled}
onSelect={onSelect}
className={cn(
"flex h-10 items-center gap-3 rounded-xl px-3 text-[13.5px] font-semibold",
"flex h-10 items-center gap-3 rounded-xl pl-3 text-[13.5px] font-semibold",
warning && "text-orange-600 focus:text-orange-600 dark:text-orange-300 dark:focus:text-orange-300",
)}
>
@@ -331,7 +346,6 @@ function AccessMenuItem({
{icon}
</span>
<span className="min-w-0 flex-1 truncate">{label}</span>
{selected ? <Check className="h-4 w-4 shrink-0" aria-hidden /> : null}
</DropdownMenuItem>
</DropdownMenuRadioItem>
);
}