Skip to content

Commit 46c22ca

Browse files
committed
redesign flags sheet
1 parent 4955412 commit 46c22ca

File tree

5 files changed

+1204
-1220
lines changed

5 files changed

+1204
-1220
lines changed
Lines changed: 144 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,168 @@
11
"use client";
22

3-
import { XIcon } from "@phosphor-icons/react";
3+
import {
4+
CheckCircleIcon,
5+
CircleIcon,
6+
PlusIcon,
7+
XIcon,
8+
} from "@phosphor-icons/react";
9+
import { AnimatePresence, motion } from "framer-motion";
10+
import { useState } from "react";
411
import { Button } from "@/components/ui/button";
12+
import { Input } from "@/components/ui/input";
513
import {
6-
Select,
7-
SelectContent,
8-
SelectItem,
9-
SelectTrigger,
10-
SelectValue,
11-
} from "@/components/ui/select";
12-
import type { DependencySelectorProps } from "./types";
14+
Popover,
15+
PopoverContent,
16+
PopoverTrigger,
17+
} from "@/components/ui/popover";
18+
import { cn } from "@/lib/utils";
19+
import type { DependencySelectorProps, Flag } from "./types";
1320

1421
export function DependencySelector({
1522
value = [],
1623
onChange,
1724
availableFlags = [],
1825
currentFlagKey,
1926
}: DependencySelectorProps) {
27+
const [isOpen, setIsOpen] = useState(false);
28+
const [search, setSearch] = useState("");
29+
2030
const selectableFlags = availableFlags.filter(
2131
(flag) => flag.key !== currentFlagKey && !value.includes(flag.key)
2232
);
2333

24-
const getFlagName = (flagKey: string) =>
25-
availableFlags.find((f) => f.key === flagKey)?.name ?? flagKey;
34+
const filteredFlags = selectableFlags.filter(
35+
(flag) =>
36+
flag.name?.toLowerCase().includes(search.toLowerCase()) ||
37+
flag.key.toLowerCase().includes(search.toLowerCase())
38+
);
39+
40+
const selectedFlags = value
41+
.map((key) => availableFlags.find((f) => f.key === key))
42+
.filter(Boolean) as Flag[];
43+
44+
const handleSelect = (flagKey: string) => {
45+
onChange([...value, flagKey]);
46+
setSearch("");
47+
};
48+
49+
const handleRemove = (flagKey: string) => {
50+
onChange(value.filter((k) => k !== flagKey));
51+
};
52+
53+
if (selectableFlags.length === 0 && value.length === 0) {
54+
return (
55+
<p className="py-4 text-center text-muted-foreground text-sm">
56+
No other flags available
57+
</p>
58+
);
59+
}
2660

2761
return (
2862
<div className="space-y-2">
29-
<Select
30-
disabled={selectableFlags.length === 0}
31-
onValueChange={(flagKey) => onChange([...value, flagKey])}
32-
value=""
33-
>
34-
<SelectTrigger className="h-8">
35-
<SelectValue placeholder="Select a flag dependency..." />
36-
</SelectTrigger>
37-
<SelectContent>
38-
{selectableFlags.map((flag) => (
39-
<SelectItem key={flag.key} value={flag.key}>
40-
{flag.name || flag.key}
41-
</SelectItem>
42-
))}
43-
</SelectContent>
44-
</Select>
63+
{/* Selected */}
64+
{selectedFlags.length > 0 && (
65+
<div className="flex flex-wrap gap-1.5">
66+
<AnimatePresence mode="popLayout">
67+
{selectedFlags.map((flag) => {
68+
const isActive = flag.status === "active";
69+
return (
70+
<motion.div
71+
animate={{ opacity: 1, scale: 1 }}
72+
className="group flex items-center gap-1.5 rounded bg-secondary px-2 py-1"
73+
exit={{ opacity: 0, scale: 0.9 }}
74+
initial={{ opacity: 0, scale: 0.9 }}
75+
key={flag.key}
76+
layout
77+
>
78+
<div
79+
className={cn(
80+
"size-1.5 rounded-full",
81+
isActive ? "bg-green-500" : "bg-amber-500"
82+
)}
83+
/>
84+
<span className="text-sm">{flag.name || flag.key}</span>
85+
<button
86+
aria-label={`Remove ${flag.name || flag.key}`}
87+
className="text-muted-foreground opacity-0 transition-opacity hover:text-destructive group-hover:opacity-100"
88+
onClick={() => handleRemove(flag.key)}
89+
type="button"
90+
>
91+
<XIcon size={12} />
92+
</button>
93+
</motion.div>
94+
);
95+
})}
96+
</AnimatePresence>
97+
</div>
98+
)}
4599

46-
{value.length > 0 ? (
47-
<div className="flex flex-wrap gap-2">
48-
{value.map((flagKey) => (
49-
<div
50-
className="flex items-center gap-1 rounded border bg-secondary px-2 py-1 text-sm"
51-
key={flagKey}
100+
{/* Add */}
101+
{selectableFlags.length > 0 && (
102+
<Popover onOpenChange={setIsOpen} open={isOpen}>
103+
<PopoverTrigger asChild>
104+
<Button
105+
className="h-8 gap-1.5 text-muted-foreground"
106+
size="sm"
107+
type="button"
108+
variant="ghost"
52109
>
53-
<span>{getFlagName(flagKey)}</span>
54-
<Button
55-
aria-label={`Remove ${getFlagName(flagKey)} dependency`}
56-
className="h-4 w-4 text-muted-foreground hover:text-destructive"
57-
onClick={() => onChange(value.filter((k) => k !== flagKey))}
58-
size="icon"
59-
type="button"
60-
variant="ghost"
61-
>
62-
<XIcon className="h-3 w-3" />
63-
</Button>
110+
<PlusIcon size={14} />
111+
Add dependency
112+
</Button>
113+
</PopoverTrigger>
114+
<PopoverContent align="start" className="w-64 p-2">
115+
<Input
116+
className="mb-2 h-8"
117+
onChange={(e) => setSearch(e.target.value)}
118+
placeholder="Search…"
119+
value={search}
120+
/>
121+
<div className="max-h-40 space-y-0.5 overflow-y-auto">
122+
{filteredFlags.length > 0 ? (
123+
filteredFlags.map((flag) => {
124+
const isActive = flag.status === "active";
125+
return (
126+
<button
127+
className="flex w-full items-center gap-2 rounded p-2 text-left text-sm transition-colors hover:bg-accent"
128+
key={flag.key}
129+
onClick={() => {
130+
handleSelect(flag.key);
131+
setIsOpen(false);
132+
}}
133+
type="button"
134+
>
135+
{isActive ? (
136+
<CheckCircleIcon
137+
className="shrink-0 text-green-500"
138+
size={14}
139+
weight="fill"
140+
/>
141+
) : (
142+
<CircleIcon
143+
className="shrink-0 text-amber-500"
144+
size={14}
145+
/>
146+
)}
147+
<span className="truncate">{flag.name || flag.key}</span>
148+
</button>
149+
);
150+
})
151+
) : (
152+
<p className="py-2 text-center text-muted-foreground text-xs">
153+
No flags found
154+
</p>
155+
)}
64156
</div>
65-
))}
66-
</div>
67-
) : null}
157+
</PopoverContent>
158+
</Popover>
159+
)}
160+
161+
{value.length > 0 && (
162+
<p className="text-muted-foreground text-xs">
163+
This flag requires all dependencies to be active
164+
</p>
165+
)}
68166
</div>
69167
);
70168
}

0 commit comments

Comments
 (0)