a dock tile can be bare, so artwork is not framed in a swatch

The tile background exists so a white lucide glyph has something to sit on —
on nothing it is invisible. Artwork does not need it, and a logo framed in an
arbitrary coloured square reads as a mistake.

  badge  rounded square filled with `color`, icon inset   (what a glyph needs)
  bare   no background, artwork fills the tile            (what a mark wants)

DEFAULTED from whether the plugin ships assets/icon.png — artwork gets bare, a
glyph gets badge — with `tile` on the manifest to override either way. Presence
is the declaration, as everywhere else here; the field exists only for the
plugin whose artwork genuinely wants a backdrop. Music sets nothing and gets
bare.

`color` is still required either way. It does four jobs and only one of them is
the square: the active glow, the indicator dot and the plugins-list tint all
read it too.

The icon was also smaller than it looked. The source carried ~8% transparent
margin on every side and the artwork is 233x221 inside a square frame, so with
the old `h-7 w-7` inset inside a `w-10` box the duck occupied roughly 39% of
the tile's area. Cropped to its alpha bounding box, padded back to square to
keep aspect, and rescaled: it now fills 95% of the frame instead of 79%, and
`bare` gives it the whole tile instead of half.

Measured rather than eyeballed — the bounding box came from extracting the
alpha plane and scanning it, then the crop was applied to the 1254px original
so nothing was resampled twice.

tsgo clean, 797 tests, 787 pass, same 7. Verified live: music reports
tile=bare, example and offscale still badge.
This commit is contained in:
2026-08-15 15:21:25 +00:00
parent f78abbe05a
commit 9b1c0a75b2
5 changed files with 58 additions and 5 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 123 KiB

@@ -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) => {
<div
className="w-10 h-10 md:w-12 md:h-12 rounded-xl flex items-center justify-center transition-all"
style={{
background: item.color,
// `bare` keeps the box — it is what sizes the row and carries the glow — and simply
// does not paint it.
background: item.tile === 'bare' ? 'transparent' : item.color,
boxShadow: isActive ? `0 0 12px ${item.color}40` : 'none',
}}
>
{item.image ? (
<img src={item.image} alt="" className="h-7 w-7 md:h-8 md:w-8 object-contain" />
<img
src={item.image}
alt=""
className={
item.tile === 'bare' ? 'h-full w-full object-contain' : 'h-7 w-7 md:h-8 md:w-8 object-contain'
}
/>
) : (
item.icon && <item.icon className="h-5 w-5 md:h-6 md:w-6 text-white" />
)}
@@ -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),
];
});
+18 -1
View File
@@ -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[];
+3
View File
@@ -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 <img> 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],
@@ -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[];