Docs / docs (push) Successful in 22s
Playwright Tests / test-playwright (1, 2) (push) Failing after 1m9s
Playwright Tests / test-playwright (2, 2) (push) Failing after 11s
pre-commit / pre-commit (push) Failing after 1m59s
Test Backend / test-backend (push) Failing after 2m28s
Compose Smoke Test / test-compose (push) Failing after 11s
Playwright Tests / merge-reports (push) Failing after 2m19s
It described the wrong object. A dashboard is a document that may hang on a
hallway tablet and in a desk browser at the same time, and only one of those
has fingers on it — so the flag moves off `DashboardDef.settings` and onto
`PanelDef` as a plain bool, ticked in the Panels dialog. `useCanvasRoot` takes
it as an argument rather than reading the document, and `/panel/{id}` is the
only surface with a panel to ask.
Dropping the message binding with it is deliberate: nothing drove it, and a
flow deciding whether a screen has fingers on it was never the point. A stored
`settings.touch` is inert rather than migrated, which `_check_settings`
skipping unknown names already guaranteed.
The rail was the other half. It had no touch behaviour at all and its 40px
buttons met neither branch of the 44/32 rule. `[data-touch] .dui-rail{-item}`
in `ui/core/core.css` spends the padding and the gap on the buttons instead,
so they reach the 44px target and the rail comes out taller at exactly the
same width — `RAIL_INSET` never moves, and the arrangement under it does not
either.
Also closes the panels-dialog icon gap: `DashboardSummary` carries the `icon`
now, so the dialog draws each assigned dashboard's rail glyph beside its
checkbox. `initials()` went from three identical copies in the looks to one in
`Dashboard/icons.ts`, so the dialog and the rail fall back the same way.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Va7ExQDtuwKN7kNpHhWWNQ
155 lines
3.9 KiB
TypeScript
155 lines
3.9 KiB
TypeScript
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<string, LucideIcon> = {
|
|
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<string, string> = {
|
|
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<string>()
|
|
|
|
function warnOnce(message: string) {
|
|
if (warned.has(message)) return
|
|
warned.add(message)
|
|
console.warn(message)
|
|
}
|