- 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>
134 lines
3.6 KiB
TypeScript
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>
|
|
);
|
|
}
|