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:
2026-08-25 18:33:28 +02:00
co-authored by Claude Opus 5
parent 476dc6a1e9
commit 055f4d7d97
2 changed files with 56 additions and 10 deletions
@@ -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>
+31 -6
View File
@@ -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"