diff --git a/plugins/music/assets/icon.png b/plugins/music/assets/icon.png
index 65981284..93b10cf2 100644
Binary files a/plugins/music/assets/icon.png and b/plugins/music/assets/icon.png differ
diff --git a/src/apps/officer-web/Screens/Dashboard/Layout/Dock.tsx b/src/apps/officer-web/Screens/Dashboard/Layout/Dock.tsx
index d9ee1cf7..05656b3b 100644
--- a/src/apps/officer-web/Screens/Dashboard/Layout/Dock.tsx
+++ b/src/apps/officer-web/Screens/Dashboard/Layout/Dock.tsx
@@ -11,6 +11,14 @@ export type DockItem = {
// Either a lucide glyph (rendered white on the coloured tile) or an image asset (e.g. an app favicon).
icon?: LucideIcon;
image?: string;
+ /**
+ * `badge` (the default) fills a rounded square with `color` and insets the icon — which is what a
+ * white lucide glyph needs, because on nothing it is invisible. `bare` draws no background and lets
+ * artwork fill the tile, because a logo framed in an arbitrary swatch reads as a mistake.
+ *
+ * `color` is still used either way: the active glow and the indicator dot are drawn from it.
+ */
+ tile?: 'badge' | 'bare';
};
type DockProps = {
@@ -101,12 +109,20 @@ export const Dock = ({ items, className, boundaryRef }: DockProps) => {
{item.image ? (
-

+

) : (
item.icon &&
)}
@@ -182,15 +198,30 @@ export const CORE_DOCK_ITEMS: DockItem[] = [
*/
export function dockItemsFromPlugins(plugins: PluginManifest[]): DockItem[] {
return plugins.flatMap((plugin) => {
- const tile = (t: { name: string; icon?: string; image?: string; color: string; route: string }): DockItem => ({
+ const tile = (t: {
+ name: string;
+ icon?: string;
+ image?: string;
+ tile?: 'badge' | 'bare';
+ color: string;
+ route: string;
+ }): DockItem => ({
label: t.name,
to: t.route,
color: t.color,
+ ...(t.tile ? { tile: t.tile } : {}),
...(t.image ? { image: t.image } : { icon: resolveIcon(t.icon ?? 'Box') }),
});
return [
- tile({ name: plugin.name, icon: plugin.icon, image: plugin.image, color: plugin.color, route: plugin.rootRoute }),
+ tile({
+ name: plugin.name,
+ icon: plugin.icon,
+ image: plugin.image,
+ tile: plugin.tile,
+ color: plugin.color,
+ route: plugin.rootRoute,
+ }),
...(plugin.extraTiles ?? []).map(tile),
];
});
diff --git a/src/servers/plugins/manifest.ts b/src/servers/plugins/manifest.ts
index c57927b0..e2737467 100644
--- a/src/servers/plugins/manifest.ts
+++ b/src/servers/plugins/manifest.ts
@@ -102,8 +102,25 @@ export type PluginManifest = {
* `assets/icon.png`. Optional for that reason: a plugin with its own artwork has nothing to say here.
*/
icon?: string;
- /** Tile colour. */
+ /**
+ * Accent colour. Four jobs, not one: the dock tile's background in `badge` style, the active glow,
+ * the active indicator dot, and the tint on the plugins list. Required even for a plugin whose tile
+ * draws no background, because the other three still use it.
+ */
color: string;
+ /**
+ * How the dock draws this plugin's tile.
+ *
+ * badge a rounded square filled with `color`, the icon inset — what a lucide GLYPH needs, since a
+ * white glyph on nothing is invisible.
+ * bare no background; the artwork fills the tile — what a brand mark wants, since a logo framed
+ * in an arbitrary swatch reads as a mistake.
+ *
+ * DEFAULTED from whether the plugin ships `assets/icon.png`: artwork gets `bare`, a glyph gets
+ * `badge`. Presence is the declaration here as everywhere else, and this field only exists to
+ * override that — a plugin whose artwork genuinely wants a coloured backdrop can say `badge`.
+ */
+ tile?: 'badge' | 'bare';
permissions: PluginPermission[];
diff --git a/src/servers/plugins/mount.ts b/src/servers/plugins/mount.ts
index cc5168da..dfa04be8 100644
--- a/src/servers/plugins/mount.ts
+++ b/src/servers/plugins/mount.ts
@@ -184,6 +184,7 @@ export async function pluginDockManifests(): Promise<
name: string;
icon?: string;
image?: string;
+ tile?: 'badge' | 'bare';
color: string;
rootRoute: string;
routes: string[];
@@ -202,6 +203,8 @@ export async function pluginDockManifests(): Promise<
// artwork of its own. The dock renders `image` as an
![]()
and `icon` through `resolveIcon`,
// and it already preferred the image — this is the first thing to give it one.
...(plugin.icon ? { image: iconUrl(plugin.appName) } : { icon: plugin.manifest.icon }),
+ // Artwork defaults to `bare`, a glyph to `badge`; the manifest overrides either way.
+ tile: plugin.manifest.tile ?? (plugin.icon ? 'bare' : 'badge'),
color: plugin.manifest.color,
rootRoute: prefix,
routes: [prefix],
diff --git a/src/workspaces/hooks/src/useCapabilities.ts b/src/workspaces/hooks/src/useCapabilities.ts
index ba78a645..50b8e2f5 100644
--- a/src/workspaces/hooks/src/useCapabilities.ts
+++ b/src/workspaces/hooks/src/useCapabilities.ts
@@ -46,6 +46,8 @@ export type PluginManifest = {
name: string;
icon?: string;
image?: string;
+ /** `bare` draws no tile background — for a plugin whose icon is artwork rather than a glyph. */
+ tile?: 'badge' | 'bare';
color: string;
rootRoute: string;
routes: string[];