diff --git a/public/devices/README.md b/public/devices/README.md
index d8e0889f..ce8f7107 100644
--- a/public/devices/README.md
+++ b/public/devices/README.md
@@ -274,3 +274,14 @@ package:
**Needs a maintainer upload** to the `openmouse-devices` R2 bucket; the mapping in
`src/ui/device-images.ts` is in place and falls back cleanly to the placeholder until then.
+
+`unknown-keyboard.png` is the keyboard placeholder, the keyboard twin of
+`unknown-device.png`: a flat black silhouette with a white Arial Bold question
+mark, on a transparent 1428×716 canvas. Every device labelled a keyboard that
+has no art of its own resolves to it, and keyboard images fall back to it on a
+load error. Only the outline derives from a source image, a transparent pngtree
+stock render of a 60% keyboard (`pngtree-compact-60-mechanical-gaming-keyboard-with-retro-carbon-pbt-keycaps-png-image_18714675.webp`);
+no colour, keycaps, legends or shading carried over. It is committed at
+`public/devices/unknown-keyboard.png` and served from the repo until a
+maintainer uploads it to the bucket; then drop its `LOCAL_OVERRIDES` entry in
+`src/shared/device-images.ts` and point the tests at `CDN + "unknown-keyboard.png"`.
diff --git a/public/devices/keychron-keyboard.svg b/public/devices/keychron-keyboard.svg
deleted file mode 100644
index 4d149bea..00000000
--- a/public/devices/keychron-keyboard.svg
+++ /dev/null
@@ -1,65 +0,0 @@
-
diff --git a/public/devices/unknown-keyboard.png b/public/devices/unknown-keyboard.png
new file mode 100644
index 00000000..de43f8a2
Binary files /dev/null and b/public/devices/unknown-keyboard.png differ
diff --git a/src/app/AppSidebar.tsx b/src/app/AppSidebar.tsx
index 957987dc..4aff2351 100644
--- a/src/app/AppSidebar.tsx
+++ b/src/app/AppSidebar.tsx
@@ -14,7 +14,7 @@ import {
import { memo, useCallback, type ReactNode } from "react";
import type { ControlSnapshot, SidebarDevice } from "../device/types";
import type { MouseStatus } from "@openmouse/protocol/drivers/mouse-types";
-import { deviceImage } from "../shared/device-images";
+import { deviceImage, placeholderImage } from "../shared/device-images";
import { BridgeStatusButton } from "../features/bridge/BridgeStatus";
import { BatteryIcon } from "../shared/ui";
import { useUiLocale } from "../shared/ui-locale";
@@ -82,7 +82,7 @@ const SidebarDeviceRow = memo(function SidebarDeviceRow({
localeTableRevision: number;
}): ReactNode {
const locale = useUiLocale();
- const image = deviceImage({ vendorId: device.vendorId, productId: device.productId } as HIDDevice, device.name);
+ const image = deviceImage({ vendorId: device.vendorId, productId: device.productId } as HIDDevice, device.name, device.kind);
const openDevice = useCallback(() => open(device.index), [open, device.index]);
return (
@@ -100,7 +100,7 @@ const SidebarDeviceRow = memo(function SidebarDeviceRow({
draggable={false}
onError={(event) => {
event.currentTarget.onerror = null;
- event.currentTarget.src = deviceImage(null);
+ event.currentTarget.src = placeholderImage(device.kind);
}}
/>
{live ? : null}
diff --git a/src/features/devices/DeviceHero.tsx b/src/features/devices/DeviceHero.tsx
index 37e080fd..58da7530 100644
--- a/src/features/devices/DeviceHero.tsx
+++ b/src/features/devices/DeviceHero.tsx
@@ -4,8 +4,7 @@ import { useMemo, useState, type KeyboardEvent, type ReactNode } from "react";
import * as control from "../../device/controller";
import { cachedBatterySamples, type BatterySample } from "./battery-history";
import type { ControlSnapshot, WorkspaceTab } from "../../device/types";
-import { deviceImage, isUnknownDevice } from "../../shared/device-images";
-import { KeyboardArt } from "../../shared/KeyboardArt";
+import { deviceImage, isUnknownDevice, placeholderImage } from "../../shared/device-images";
import { ConnectionIcon } from "./DeviceStats";
const ARTWORK_DISCUSSION_URL =
@@ -31,10 +30,9 @@ export function DeviceHero({
const active = control.getActiveDevice();
const activeEntry = snapshot.devices.find((device) => device.selected) ?? null;
const name = status?.name ?? keyboard?.name ?? activeEntry?.name ?? "";
- // Arbiter keyboards have no product render; draw a keyboard rather than the unknown-device mouse.
- const keyboardArt = keyboard?.family === "arbiter" && !snapshot.deviceArtwork;
+ // A keyboard with no render of its own gets the keyboard placeholder, never the mouse one.
const image = snapshot.deviceArtwork
- ?? (keyboard && !keyboardArt ? deviceImage({ vendorId: keyboard.vendorId, productId: keyboard.productId } as HIDDevice, keyboard.name) : null);
+ ?? (keyboard ? deviceImage({ vendorId: keyboard.vendorId, productId: keyboard.productId } as HIDDevice, keyboard.name, "keyboard") : null);
const needsArtwork = Boolean(status && active && (imageFailed || isUnknownDevice(
{ vendorId: active.vendorId, productId: active.productId } as HIDDevice,
status.name,
@@ -84,13 +82,12 @@ export function DeviceHero({
draggable={false}
onError={(event) => {
event.currentTarget.onerror = null;
- event.currentTarget.src = deviceImage(null);
+ event.currentTarget.src = placeholderImage(keyboard && !snapshot.deviceArtwork ? "keyboard" : "mouse");
setImageFailed(true);
}}
/>
) : null}
- {keyboardArt ?