import { ArrowDown, ArrowUp, BatteryCharging, Bed, Check, CircleCheck, CircleHelp, CloudDrizzle, CloudFog, CloudLightning, CloudRain, CloudSnow, Cloudy, DoorOpen, Droplets, Fan, Flame, House, Lightbulb, type LucideIcon, Moon, Music, Plug, Snowflake, Sun, SunSnow, Thermometer, ThermometerSun, TriangleAlert, Umbrella, Volume2, VolumeX, Wind, Zap, } from "lucide-react" /** * The icons a tile may be drawn with, by name. * * Curated rather than lucide's `dynamicIconImports`: the panel has to offer * these in a list, and the dynamic map makes the bundler emit a lazy chunk per * icon — some fifteen hundred of them — so a wall panel would fetch one request * per tile over its LAN before it could draw anything. A few dozen weather, * room and status glyphs cover what a dashboard says, and the map is cheap to * extend. */ export const ICONS: Record = { sun: Sun, moon: Moon, cloudy: Cloudy, "cloud-rain": CloudRain, "cloud-drizzle": CloudDrizzle, "cloud-snow": CloudSnow, "cloud-lightning": CloudLightning, "cloud-fog": CloudFog, wind: Wind, umbrella: Umbrella, snowflake: Snowflake, droplets: Droplets, thermometer: Thermometer, "thermometer-sun": ThermometerSun, flame: Flame, fan: Fan, "sun-snow": SunSnow, "door-open": DoorOpen, house: House, bed: Bed, lightbulb: Lightbulb, plug: Plug, music: Music, "volume-2": Volume2, "volume-x": VolumeX, zap: Zap, "battery-charging": BatteryCharging, "arrow-up": ArrowUp, "arrow-down": ArrowDown, "triangle-alert": TriangleAlert, check: Check, "circle-check": CircleCheck, } 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 } /** * Two letters off a title, so a rail of four reads as four different things. * * The fallback wherever a glyph is drawn from a name that may be empty: the * rail in all three looks, and the panels dialog showing what the rail will * draw. */ export function initials(label: string): string { const words = label.split(/[\s_-]+/).filter(Boolean) if (words.length === 0) return "?" if (words.length === 1) return words[0].slice(0, 2).toUpperCase() return (words[0][0] + words[1][0]).toUpperCase() } /** * What an icon may be tinted, by name. * * Literal classes so Tailwind's scanner sees them. No terracotta: that is the * one affordance a view gets, and a panel is many tiles. */ export const ICON_COLORS: Record = { default: "text-foreground", muted: "text-muted-foreground", primary: "text-primary", success: "text-status-success", 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() function warnOnce(message: string) { if (warned.has(message)) return warned.add(message) console.warn(message) }