Draw something for an icon or colour name nothing maps to
An unknown name indexed the map, got undefined and drew nothing — the same as no icon at all, so a typo was invisible. It resolves to a placeholder glyph and the default tint now, and warns once per bad name: both lookups sit in a render, so a bound widget would otherwise warn per value. An empty name still draws nothing, which is what "no icon" means. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KYM38KSb4V4v2T71eifnZv
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { useBoundValue } from "./dataContext"
|
import { useBoundValue } from "./dataContext"
|
||||||
import { ICON_COLORS, ICONS } from "./icons"
|
import { resolveIcon, resolveIconColor } from "./icons"
|
||||||
import { config } from "./ui/core/config"
|
import { config } from "./ui/core/config"
|
||||||
import type { WidgetProps } from "./widgets"
|
import type { WidgetProps } from "./widgets"
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ export function ForecastWidget({ widget }: WidgetProps) {
|
|||||||
return (
|
return (
|
||||||
<div className="flex min-h-0 items-stretch justify-between gap-2">
|
<div className="flex min-h-0 items-stretch justify-between gap-2">
|
||||||
{items.map((item, index) => {
|
{items.map((item, index) => {
|
||||||
const Glyph = ICONS[item.icon ?? ""] ?? null
|
const Glyph = resolveIcon(item.icon ?? "")
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
// Two steps can carry the same label and reading; position is the
|
// Two steps can carry the same label and reading; position is the
|
||||||
@@ -66,7 +66,7 @@ export function ForecastWidget({ widget }: WidgetProps) {
|
|||||||
<Glyph
|
<Glyph
|
||||||
className={cn(
|
className={cn(
|
||||||
"dui-glyph-sm shrink-0",
|
"dui-glyph-sm shrink-0",
|
||||||
ICON_COLORS[item.color ?? ""] ?? ICON_COLORS.default,
|
resolveIconColor(item.color ?? ""),
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { useBoundValue } from "./dataContext"
|
import { useBoundValue } from "./dataContext"
|
||||||
import { ICON_COLORS, ICONS } from "./icons"
|
import { resolveIcon, resolveIconColor } from "./icons"
|
||||||
import { config, text } from "./ui/core/config"
|
import { config, text } from "./ui/core/config"
|
||||||
import type { WidgetProps } from "./widgets"
|
import type { WidgetProps } from "./widgets"
|
||||||
|
|
||||||
@@ -41,15 +41,19 @@ export function IconWidget({ widget }: WidgetProps) {
|
|||||||
value === null || value === undefined
|
value === null || value === undefined
|
||||||
? undefined
|
? undefined
|
||||||
: rules.find((rule) => matches(value, coerce(text(rule.at))))
|
: rules.find((rule) => matches(value, coerce(text(rule.at))))
|
||||||
const Glyph = ICONS[matched?.icon ?? text(cfg.icon)] ?? null
|
const Glyph = resolveIcon(matched?.icon ?? text(cfg.icon))
|
||||||
|
|
||||||
// Unbound, unmatched, or pointed at a glyph that is not in the map.
|
// Unbound or unmatched — a named-but-unknown glyph resolves to the
|
||||||
|
// placeholder above rather than falling through to here.
|
||||||
if (!Glyph) return <p className="dui-clock text-muted-foreground">—</p>
|
if (!Glyph) return <p className="dui-clock text-muted-foreground">—</p>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-w-0 flex-col items-center justify-center gap-1">
|
<div className="flex min-w-0 flex-col items-center justify-center gap-1">
|
||||||
<Glyph
|
<Glyph
|
||||||
className={cn("dui-glyph", ICON_COLORS[matched?.color ?? "default"])}
|
className={cn(
|
||||||
|
"dui-glyph",
|
||||||
|
resolveIconColor(matched?.color ?? "default"),
|
||||||
|
)}
|
||||||
// State is never carried by colour alone.
|
// State is never carried by colour alone.
|
||||||
aria-label={matched?.label || text(value, "—")}
|
aria-label={matched?.label || text(value, "—")}
|
||||||
data-testid="icon-glyph"
|
data-testid="icon-glyph"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
Bed,
|
Bed,
|
||||||
Check,
|
Check,
|
||||||
CircleCheck,
|
CircleCheck,
|
||||||
|
CircleHelp,
|
||||||
CloudDrizzle,
|
CloudDrizzle,
|
||||||
CloudFog,
|
CloudFog,
|
||||||
CloudLightning,
|
CloudLightning,
|
||||||
@@ -81,6 +82,24 @@ export const ICONS: Record<string, LucideIcon> = {
|
|||||||
|
|
||||||
export const ICON_NAMES = Object.keys(ICONS)
|
export const ICON_NAMES = Object.keys(ICONS)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A tile's glyph, or the neutral placeholder for a name nothing maps to.
|
||||||
|
*
|
||||||
|
* A flow author names an icon by guess as often as by picking one from the
|
||||||
|
* panel, so a name outside {@link ICONS} is a typo, not "no icon" — unlike an
|
||||||
|
* empty name, which stays undrawn rather than warning. The warning is what
|
||||||
|
* makes the typo findable; the placeholder is what makes it visible.
|
||||||
|
*/
|
||||||
|
export function resolveIcon(name: string): LucideIcon | null {
|
||||||
|
if (!name) return null
|
||||||
|
const icon = ICONS[name]
|
||||||
|
if (icon) return icon
|
||||||
|
warnOnce(
|
||||||
|
`Dashboard: unknown icon "${name}", expected one of: ${ICON_NAMES.join(", ")}`,
|
||||||
|
)
|
||||||
|
return CircleHelp
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* What an icon may be tinted, by name.
|
* What an icon may be tinted, by name.
|
||||||
*
|
*
|
||||||
@@ -94,3 +113,28 @@ export const ICON_COLORS: Record<string, string> = {
|
|||||||
success: "text-status-success",
|
success: "text-status-success",
|
||||||
danger: "text-destructive",
|
danger: "text-destructive",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A tile's tint, or the widget's own default for a name nothing maps to. */
|
||||||
|
export function resolveIconColor(name: string): string {
|
||||||
|
const color = ICON_COLORS[name]
|
||||||
|
if (color) return color
|
||||||
|
if (name) {
|
||||||
|
warnOnce(
|
||||||
|
`Dashboard: unknown icon color "${name}", expected one of: ${Object.keys(ICON_COLORS).join(", ")}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return ICON_COLORS.default
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Both lookups sit in a render, and a bound widget renders as often as its
|
||||||
|
* value arrives — so a typo would otherwise be a warning a second, for as long
|
||||||
|
* as the panel is open.
|
||||||
|
*/
|
||||||
|
const warned = new Set<string>()
|
||||||
|
|
||||||
|
function warnOnce(message: string) {
|
||||||
|
if (warned.has(message)) return
|
||||||
|
warned.add(message)
|
||||||
|
console.warn(message)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user