multica/apps/web/features/issues/components/pickers/property-picker.tsx
Naiyuan Qing a2d7501d57 refactor(web): restructure to feature-based architecture with zustand stores
- Remove tab system entirely (tab-store, tab-bar, tab-link)
- Split monolithic AuthContext into zustand auth + workspace stores
- Move issue components/config to features/issues/
- Move WebSocket provider to features/realtime/
- Move api.ts to shared/
- Migrate all consumers from useAuth() to direct store imports
- Simplify sidebar: replace hand-built dropdown with shadcn DropdownMenu,
  replace custom layout wrapper with SidebarInset
- Remove unused @multica/store and @multica/hooks dependencies
- Add @/ path alias and zustand dependency
- Update CLAUDE.md with feature-based architecture conventions

Net change: +293 / -2435 lines

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 16:08:36 +08:00

133 lines
3.6 KiB
TypeScript

"use client";
import { useState, useCallback } from "react";
import { Check } from "lucide-react";
import {
Popover,
PopoverTrigger,
PopoverContent,
} from "@multica/ui/components/ui/popover";
// ---------------------------------------------------------------------------
// PropertyPicker — generic Popover shell with optional search
// ---------------------------------------------------------------------------
export function PropertyPicker({
open,
onOpenChange,
trigger,
width = "w-48",
align = "end",
searchable = false,
searchPlaceholder = "Filter...",
onSearchChange,
children,
}: {
open: boolean;
onOpenChange: (v: boolean) => void;
trigger: React.ReactNode;
width?: string;
align?: "start" | "center" | "end";
searchable?: boolean;
searchPlaceholder?: string;
onSearchChange?: (query: string) => void;
children: React.ReactNode;
}) {
const [query, setQuery] = useState("");
const handleOpenChange = useCallback(
(v: boolean) => {
onOpenChange(v);
if (!v) {
setQuery("");
onSearchChange?.("");
}
},
[onOpenChange, onSearchChange],
);
return (
<Popover open={open} onOpenChange={handleOpenChange}>
<PopoverTrigger className="flex items-center gap-1.5 cursor-pointer rounded px-1 -mx-1 hover:bg-accent/30 transition-colors">
{trigger}
</PopoverTrigger>
<PopoverContent align={align} className={`${width} gap-0 p-0`}>
{searchable && (
<div className="px-2 py-1.5 border-b">
<input
type="text"
value={query}
onChange={(e) => {
setQuery(e.target.value);
onSearchChange?.(e.target.value);
}}
placeholder={searchPlaceholder}
className="w-full bg-transparent text-[13px] placeholder:text-muted-foreground outline-none"
/>
</div>
)}
<div className="p-1 max-h-60 overflow-y-auto">{children}</div>
</PopoverContent>
</Popover>
);
}
// ---------------------------------------------------------------------------
// PickerItem — single selectable row
// ---------------------------------------------------------------------------
export function PickerItem({
selected,
onClick,
hoverClassName,
children,
}: {
selected: boolean;
onClick: () => void;
hoverClassName?: string;
children: React.ReactNode;
}) {
return (
<button
type="button"
onClick={onClick}
className={`flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-[13px] ${hoverClassName ?? "hover:bg-accent"} transition-colors`}
>
<span className="flex flex-1 items-center gap-2">{children}</span>
{selected && <Check className="h-3.5 w-3.5 text-muted-foreground" />}
</button>
);
}
// ---------------------------------------------------------------------------
// PickerSection — group header
// ---------------------------------------------------------------------------
export function PickerSection({
label,
children,
}: {
label: string;
children: React.ReactNode;
}) {
return (
<div>
<div className="px-2 pt-2 pb-1 text-[11px] font-medium text-muted-foreground uppercase tracking-wider">
{label}
</div>
{children}
</div>
);
}
// ---------------------------------------------------------------------------
// PickerEmpty — no results state
// ---------------------------------------------------------------------------
export function PickerEmpty() {
return (
<div className="px-2 py-3 text-center text-[13px] text-muted-foreground">
No results
</div>
);
}