diff --git a/frontend/src/components/Dashboard/PanelsDialog.tsx b/frontend/src/components/Dashboard/PanelsDialog.tsx
index e9de859..dda5281 100644
--- a/frontend/src/components/Dashboard/PanelsDialog.tsx
+++ b/frontend/src/components/Dashboard/PanelsDialog.tsx
@@ -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 (
@@ -264,13 +273,25 @@ function PanelRow({
{panel.id}
diff --git a/frontend/src/components/Dashboard/panels.tsx b/frontend/src/components/Dashboard/panels.tsx
index 2f948f3..c824185 100644
--- a/frontend/src/components/Dashboard/panels.tsx
+++ b/frontend/src/components/Dashboard/panels.tsx
@@ -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 (
-
+ {
+ setOpen(next)
+ setFilter("")
+ }}
+ >
-
-
- {ICON_NAMES.map((name) => {
+ {/* Fixed width rather than auto: the grid would otherwise shrink under
+ the cursor as the filter narrows it. */}
+
+ setFilter(event.target.value)}
+ />
+