multica/apps/web/features/issues/components/pickers/property-picker.tsx
Naiyuan Qing 2cf088ddf6 feat: resizable sidebar, issue detail rewrite, package consolidation
- Add drag-to-resize sidebar with localStorage persistence
- Rewrite issue detail page with Tiptap rich text editor, due date picker, acceptance criteria
- Redesign create-issue modal with pill-based property toolbar and expand/collapse
- Consolidate @multica/sdk and @multica/types into apps/web/shared/
- Simplify auth: remove verification codes, PATs, email service (dev-only login)
- Add 401 unauthorized handler to redirect expired sessions to login
- Fix due date format to send full RFC3339 timestamps
- Increase description editor debounce to 1500ms
- Remove arbitrary Tailwind values in create-issue modal
- Renumber migrations (inbox_actor 012→009), remove unused migrations
- UI polish across agents, settings, inbox, knowledge-base pages

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

134 lines
3.6 KiB
TypeScript

"use client";
import { useState, useCallback } from "react";
import { Check } from "lucide-react";
import {
Popover,
PopoverTrigger,
PopoverContent,
} from "@/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 overflow-hidden">
{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}
aria-label="Filter options"
className="w-full bg-transparent text-sm 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-sm ${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-xs 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-sm text-muted-foreground">
No results
</div>
);
}