badge: add a quiet variant for icon-only header triggers

Badge's default look is a filled pill, right for status/picker chips
but wrong for an icon-only header button like the settings gear or
agent links trigger, which should read as chrome. Add variant='quiet'
(same naming/shape as Button's ButtonVariant) that drops the idle
background, keeping the existing hover/expanded feedback.

Apply it to the two icon-only Badge triggers: shared SettingsMenu
(used by both swarm-ui and the agent page) and the agent page's own
MetaNav links trigger. Add a components-page showcase sample so the
variant has a visible regression check going forward.
This commit is contained in:
iris 2026-08-29 20:23:00 +02:00 committed by mara
commit e1aae29068
5 changed files with 68 additions and 23 deletions

View file

@ -30,6 +30,14 @@
opacity: 0.6;
cursor: default;
}
/* `variant="quiet"` icon-only header triggers (settings/links), not
status chips: no permanent fill, so it reads as chrome next to the
rest of the header rather than another data pill. The `:hover` and
`[aria-expanded='true']` rules above still apply quiet only drops
the idle background, which is what mara's report was actually about. */
.ui-badge-quiet {
background: none;
}
.ui-badge-interactive[aria-expanded='true'] {
background: var(--bg-elev);
outline: 1px solid var(--purple);

View file

@ -23,6 +23,19 @@ import type { ComponentChildren } from 'preact';
import './Badge.css';
export type BadgeTone = 'neutral' | 'positive' | 'warning' | 'negative' | 'accent';
/**
* `'default'` the filled pill every status/picker badge has always
* been. `'quiet'` no permanent fill, only a background on hover/
* `expanded`; for an icon-only trigger that should read as chrome (a
* header button) rather than a status chip mara: "link and settings
* button should not have the badge bg" against both the agent page's
* `MetaNav` and the shared `SettingsMenu` (swarm-ui + agent both use
* it). Same `variant` naming/shape as `Button`'s
* `ButtonVariant` (`../../swarm-ui/src/ui/button/Button.tsx`) visual
* weight is orthogonal to `tone` (color semantics), so it's its own prop
* rather than a new `tone` value.
*/
export type BadgeVariant = 'default' | 'quiet';
export interface BadgeProps {
/** Dim prefix text, e.g. "model". Omit for a single-value badge like "alive". */
@ -30,6 +43,7 @@ export interface BadgeProps {
/** The value itself, e.g. "sonnet". Required — a badge always shows something. */
value: ComponentChildren;
tone?: BadgeTone;
variant?: BadgeVariant;
/** Decorative glyph before the label; `aria-hidden`, the text is still the real label. */
icon?: ComponentChildren;
/**
@ -49,6 +63,7 @@ export function Badge({
label,
value,
tone = 'neutral',
variant = 'default',
icon,
onClick,
expanded,
@ -56,7 +71,13 @@ export function Badge({
class: extraClass,
title,
}: BadgeProps) {
const classes = ['ui-badge', `ui-badge-${tone}`, onClick && 'ui-badge-interactive', extraClass]
const classes = [
'ui-badge',
`ui-badge-${tone}`,
variant === 'quiet' && 'ui-badge-quiet',
onClick && 'ui-badge-interactive',
extraClass,
]
.filter(Boolean)
.join(' ');
const content = (