Filter the icon grid, and ask before removing a panel
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UytviPMJbXzD8P84nLvXcq
This commit is contained in:
@@ -27,6 +27,7 @@ import { Input } from "@/components/ui/input"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import useAuth from "@/hooks/useAuth"
|
||||
import useCustomToast from "@/hooks/useCustomToast"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { handleError } from "@/utils"
|
||||
|
||||
/** As many characters as a device puts on the wall. */
|
||||
@@ -207,6 +208,9 @@ function PanelRow({
|
||||
// ponytail: a two-step button rather than a dialog, since this one already
|
||||
// lives inside a dialog.
|
||||
const [confirmUnpair, setConfirmUnpair] = useState(false)
|
||||
// Removing the panel throws away more than unpairing does, so it asks the
|
||||
// same way — the icon turns destructive and only the second press fires.
|
||||
const [confirmRemove, setConfirmRemove] = useState(false)
|
||||
const unpair = useUnpairPanel(panel.id)
|
||||
const assigned = panel.dashboards ?? []
|
||||
const typed = code.trim().toUpperCase()
|
||||
@@ -247,6 +251,11 @@ function PanelRow({
|
||||
|
||||
const link = host ? `${host}/panel/${panel.id}` : ""
|
||||
const remoteLink = remoteHost ? `${remoteHost}/panel` : ""
|
||||
// The trash icon carries no text of its own, so the confirm step says what
|
||||
// the next press does through the label a reader or a hover gets.
|
||||
const removeLabel = confirmRemove
|
||||
? `Confirm — remove ${panel.id}`
|
||||
: `Remove ${panel.id}`
|
||||
|
||||
return (
|
||||
<div className="grid gap-3" data-testid={`panel-${panel.id}`}>
|
||||
@@ -264,13 +273,25 @@ function PanelRow({
|
||||
{panel.id}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant={confirmRemove ? "destructive" : "ghost"}
|
||||
size="icon"
|
||||
className="size-11 shrink-0 text-muted-foreground md:size-8"
|
||||
aria-label={`Remove ${panel.id}`}
|
||||
className={cn(
|
||||
"size-11 shrink-0 md:size-8",
|
||||
!confirmRemove && "text-muted-foreground",
|
||||
)}
|
||||
aria-label={removeLabel}
|
||||
title={removeLabel}
|
||||
data-testid={`remove-panel-${panel.id}`}
|
||||
disabled={!canEdit}
|
||||
onClick={onRemove}
|
||||
onBlur={() => setConfirmRemove(false)}
|
||||
onClick={() => {
|
||||
if (!confirmRemove) {
|
||||
setConfirmRemove(true)
|
||||
return
|
||||
}
|
||||
setConfirmRemove(false)
|
||||
onRemove()
|
||||
}}
|
||||
>
|
||||
<Trash2 />
|
||||
</Button>
|
||||
|
||||
@@ -307,8 +307,8 @@ const CHART_SOURCES = [
|
||||
* Radix forbids an empty `SelectItem` value — which is why the selects this
|
||||
* replaces could set an icon but never take one back.
|
||||
*
|
||||
* ponytail: no filter field. `ICONS` is a few dozen and the grid shows all of
|
||||
* it without scrolling; add one when the map outgrows a popover.
|
||||
* The field at the top narrows the grid by name, so the glyphs stay findable
|
||||
* as the map grows past what a popover can show at once.
|
||||
*/
|
||||
function IconPicker({
|
||||
value,
|
||||
@@ -328,10 +328,21 @@ function IconPicker({
|
||||
onChange: (icon: string) => void
|
||||
}) {
|
||||
const [open, setOpen] = useState(false)
|
||||
// Cleared whenever the popover opens or closes: a filter left over from last
|
||||
// time hides icons that are still there.
|
||||
const [filter, setFilter] = useState("")
|
||||
const Current = ICONS[value]
|
||||
const needle = filter.trim().toLowerCase()
|
||||
const shown = ICON_NAMES.filter((name) => name.toLowerCase().includes(needle))
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={(next) => {
|
||||
setOpen(next)
|
||||
setFilter("")
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -351,9 +362,18 @@ function IconPicker({
|
||||
<ChevronDown className="ml-auto opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="start" className="w-auto p-2">
|
||||
<div className="grid grid-cols-6 gap-1">
|
||||
{ICON_NAMES.map((name) => {
|
||||
{/* Fixed width rather than auto: the grid would otherwise shrink under
|
||||
the cursor as the filter narrows it. */}
|
||||
<PopoverContent align="start" className="w-60 p-2">
|
||||
<Input
|
||||
value={filter}
|
||||
placeholder="Filter"
|
||||
aria-label="Filter icons"
|
||||
className="mb-2 h-8 text-sm"
|
||||
onChange={(event) => setFilter(event.target.value)}
|
||||
/>
|
||||
<div className="grid grid-cols-6 justify-items-center gap-1">
|
||||
{shown.map((name) => {
|
||||
const Glyph = ICONS[name]
|
||||
return (
|
||||
<Button
|
||||
@@ -376,6 +396,11 @@ function IconPicker({
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{shown.length === 0 ? (
|
||||
<p className="px-1 text-sm text-muted-foreground">
|
||||
No icon by that name.
|
||||
</p>
|
||||
) : null}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
|
||||
Reference in New Issue
Block a user