You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/web/routes/uix/components/badge/+page.svelte

850 lines
31 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<script lang="ts">
import {
Badge,
type BadgeSize,
type BadgeColor,
type BadgeVariant,
type BadgeRounded
} from '$uix/eidos/components/badge';
import { SHAPE_FAMILIES, type ShapeFamily } from '$uix/eidos/lib/types';
import { Box } from '$uix/eidos/components/box';
import { Stack } from '$uix/eidos/components/stack';
import { compileMorfo } from '$uix/morfo';
import { badgeMorfo } from '@/uix/morfo/components/badge';
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
type TraceEntry = { event: string; family: string; intent?: string; at: number };
let tab = $state<Tab>('live');
let trace = $state<TraceEntry[]>([]);
let stageRef = $state<HTMLElement | null>(null);
// ── Live state ────────────────────────────────────────────────────────
let variant = $state<BadgeVariant>('soft');
let size = $state<BadgeSize>('md');
let color = $state<BadgeColor>('primary');
let rounded = $state<BadgeRounded>('full');
let shape = $state<ShapeFamily>('rounded');
let labelText = $state('New');
let withDot = $state(false);
let withIcon = $state(false);
let removable = $state(false);
let removeCount = $state(0);
const variants: BadgeVariant[] = ['soft', 'solid', 'outline', 'ghost'];
const sizes: BadgeSize[] = ['xs', 'sm', 'md', 'lg'];
const colors: BadgeColor[] = [
'primary',
'secondary',
'neutral',
'affirm',
'fulfill',
'risk',
'threat',
'loss'
];
const roundeds: BadgeRounded[] = ['sm', 'md', 'lg', 'full'];
// ── Compiled morfo ────────────────────────────────────────────────────
const compiled = compileMorfo(badgeMorfo);
const partsList = $derived([...compiled.parts.byKebab.values()]);
const events = $derived([...compiled.actions.byName.values()]);
$effect(() => {
const el = stageRef;
if (!el) return;
const obs = new MutationObserver((mutations) => {
for (const m of mutations) {
if (m.attributeName !== 'data-event') continue;
const target = m.target as Element;
const ev = target.getAttribute('data-event');
if (!ev) continue;
trace = [
{
event: ev,
family: target.getAttribute('data-event-family') ?? '—',
intent: target.getAttribute('data-event-intent') ?? undefined,
at: Date.now()
},
...trace
].slice(0, 6);
}
});
obs.observe(el, { attributes: true, subtree: true, attributeFilter: ['data-event'] });
return () => obs.disconnect();
});
// ── Code snippet ──────────────────────────────────────────────────────
const eidosSnippet = $derived(
[
"<script lang='ts'>",
" import { Badge } from '$uix/eidos/components/badge';",
'</' + 'script>',
'',
'<Badge',
variant !== 'soft' && ` variant="${variant}"`,
size !== 'md' && ` size="${size}"`,
color !== 'primary' && ` color="${color}"`,
rounded !== 'full' && ` rounded="${rounded}"`,
shape !== 'rounded' && ` shape="${shape}"`,
withDot && ' dot',
removable && ' removable',
removable && ' onRemove={() => removeTag(id)}',
'>',
` ${labelText || ''}`,
'</Badge>'
]
.filter(Boolean)
.join('\n')
);
function handleRemove() {
removeCount += 1;
trace = [
{ event: 'onRemove (consumer callback)', family: 'consumer', at: Date.now() },
...trace
].slice(0, 6);
}
</script>
{#snippet sparkIcon()}
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
style="inline-size: 100%; block-size: 100%;"
aria-hidden="true"
>
<path
d="M12 2l2.39 5.95L20 9.27l-4 4.13.94 6.04L12 16.77l-4.94 2.67L8 13.4 4 9.27l5.61-1.32L12 2z"
/>
</svg>
{/snippet}
<div data-uix-canvas-inner>
<header>
<div data-uix-eyebrow>Status · Badge</div>
<h1 data-uix-page-title>Badge</h1>
<p data-uix-page-lede>
Small inline label / status pill. Four visual variants
(<code>soft</code>, <code>solid</code>, <code>outline</code>, <code>ghost</code>), four
sizes, eight color accents, four rounded scales, and optional
<code>dot</code> · <code>icon</code> · <code>remove</code> slots. Eidos-native: no semantic
events, no soma split — the morfo declares five parts and the recipe paints the chip from
the canonical color palette.
</p>
<div data-uix-page-meta>
<span data-uix-meta-pill>
<span data-uix-meta-key>parts</span>{compiled.parts.order.length}
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>events</span>0
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>variants</span>{variants.length}
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>sizes</span>{sizes.length}
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>colors</span>{colors.length}
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>scope</span>eidos
</span>
</div>
</header>
<!-- Live preview always rendered (between header and tabs). -->
<div data-uix-stage>
<div data-uix-stage-area bind:this={stageRef}>
<Box
style="padding: var(--uix-space-6); display: flex; align-items: center; justify-content: center; inline-size: 100%; gap: var(--uix-space-3); flex-wrap: wrap;"
>
<Badge
{variant}
{size}
{color}
{rounded}
{shape}
dot={withDot}
{removable}
onRemove={handleRemove}
icon={withIcon ? sparkIcon : undefined}
>
{labelText || 'Badge'}
</Badge>
</Box>
</div>
<div data-uix-stage-trace>
<span data-uix-stage-trace-key>trace</span>
<span>{trace.length === 0 ? 'no semantic events' : `${trace.length} event(s)`}</span>
<span style="color: var(--uix-text-faint)">·</span>
<span data-uix-stage-trace-key>removes</span>
<span>{removeCount}</span>
<span style="margin-inline-start: auto;">
<span data-uix-stage-trace-key>variant</span> {variant} ·
<span data-uix-stage-trace-key>size</span> {size} ·
<span data-uix-stage-trace-key>color</span> {color}
</span>
</div>
</div>
<div data-uix-tabs role="tablist">
<button data-uix-tab data-active={tab === 'live'} onclick={() => (tab = 'live')}>Live</button>
<button data-uix-tab data-active={tab === 'api'} onclick={() => (tab = 'api')}>
API <span data-uix-tab-count>9</span>
</button>
<button data-uix-tab data-active={tab === 'morfo'} onclick={() => (tab = 'morfo')}>
<span data-uix-layer-badge="morfo">morfo</span>
<span data-uix-tab-count>{partsList.length}p · 0e</span>
</button>
<button data-uix-tab data-active={tab === 'sema'} onclick={() => (tab = 'sema')}>
<span data-uix-layer-badge="sema">sema</span>
<span data-uix-tab-count>{events.length}</span>
</button>
<button data-uix-tab data-active={tab === 'recipe'} onclick={() => (tab = 'recipe')}>Recipe</button
>
<button data-uix-tab data-active={tab === 'a11y'} onclick={() => (tab = 'a11y')}>A11y</button>
</div>
{#if tab === 'live'}
<section data-uix-section>
<h2 data-uix-section-title>Controls</h2>
<p data-uix-section-desc>
Badge is eidos-native — no <span data-uix-layer-badge="soma">soma</span> split. Variants
stretch over the same chrome (padding, radius, font). The optional
<code>Dot</code> / <code>Icon</code> / <code>Remove</code> slots compose inline next to
the label; toggle them independently.
</p>
<div data-uix-subsection-head>
<span data-uix-layer-badge="eidos">eidos</span> props · visual treatment
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>variant</span>
<span data-uix-chips role="radiogroup">
{#each variants as v (v)}
<button data-uix-chip data-active={variant === v} onclick={() => (variant = v)}
>{v}</button
>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>size</span>
<span data-uix-chips role="radiogroup">
{#each sizes as s (s)}
<button data-uix-chip data-active={size === s} onclick={() => (size = s)}>{s}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>color</span>
<span data-uix-chips role="radiogroup">
{#each colors as c (c)}
<button data-uix-chip data-active={color === c} onclick={() => (color = c)}
>{c}</button
>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>rounded</span>
<span data-uix-chips role="radiogroup">
{#each roundeds as r (r)}
<button data-uix-chip data-active={rounded === r} onclick={() => (rounded = r)}
>{r}</button
>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>shape</span>
<span data-uix-chips role="radiogroup">
{#each SHAPE_FAMILIES as sh (sh)}
<button data-uix-chip data-active={shape === sh} onclick={() => (shape = sh)}
>{sh}</button
>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>label</span>
<input
data-uix-text-input
type="text"
bind:value={labelText}
placeholder="Badge"
style="inline-size: 12rem;"
/>
</label>
<label data-uix-control>
<span data-uix-control-label>dot</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={withDot} />
<span data-uix-switch-label>{withDot ? 'on' : 'off'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>icon</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={withIcon} />
<span data-uix-switch-label>{withIcon ? 'on' : 'off'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>removable</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={removable} />
<span data-uix-switch-label>{removable ? 'on' : 'off'}</span>
</span>
</label>
</div>
<!-- ── Code snippet (eidos-native — no soma layer) ──────────────── -->
<div data-uix-code>
<div data-uix-code-head>
<span data-uix-layer-badge="eidos">eidos</span>
<span>visual · slots compose inline</span>
<span data-uix-code-lang>svelte</span>
</div>
<pre><code>{eidosSnippet}</code></pre>
</div>
<div data-uix-subsection-head>Patterns · common shapes</div>
<Stack gap="var(--uix-space-4)">
<Box
style="padding: var(--uix-space-4); border: 1px solid var(--uix-border); border-radius: var(--uix-radius-md); display: flex; flex-wrap: wrap; gap: var(--uix-space-2); align-items: center;"
>
<code style="min-inline-size: 8rem; color: var(--uix-text-faint);">status pill</code>
<Badge variant="soft" color="affirm" dot>Active</Badge>
<Badge variant="soft" color="risk" dot>Pending</Badge>
<Badge variant="soft" color="threat" dot>Failed</Badge>
<Badge variant="soft" color="loss" dot>Archived</Badge>
<Badge variant="soft" color="neutral" dot>Draft</Badge>
</Box>
<Box
style="padding: var(--uix-space-4); border: 1px solid var(--uix-border); border-radius: var(--uix-radius-md); display: flex; flex-wrap: wrap; gap: var(--uix-space-2); align-items: center;"
>
<code style="min-inline-size: 8rem; color: var(--uix-text-faint);">shape families</code>
<Badge variant="soft" color="primary" rounded="md" shape="rounded">rounded</Badge>
<Badge variant="soft" color="primary" rounded="md" shape="continuous">continuous</Badge>
<Badge variant="solid" color="primary" rounded="md" shape="cut">cut</Badge>
<Badge variant="solid" color="primary" rounded="md" shape="scoop">scoop</Badge>
</Box>
<Box
style="padding: var(--uix-space-4); border: 1px solid var(--uix-border); border-radius: var(--uix-radius-md); display: flex; flex-wrap: wrap; gap: var(--uix-space-2); align-items: center;"
>
<code style="min-inline-size: 8rem; color: var(--uix-text-faint);">count chip</code>
<Badge variant="solid" color="primary" size="xs">3</Badge>
<Badge variant="solid" color="threat" size="xs">12</Badge>
<Badge variant="solid" color="affirm" size="xs">99+</Badge>
<Badge variant="solid" color="neutral" size="xs">new</Badge>
</Box>
<Box
style="padding: var(--uix-space-4); border: 1px solid var(--uix-border); border-radius: var(--uix-radius-md); display: flex; flex-wrap: wrap; gap: var(--uix-space-2); align-items: center;"
>
<code style="min-inline-size: 8rem; color: var(--uix-text-faint);">filter tags</code>
<Badge variant="outline" color="primary" rounded="md" removable onRemove={handleRemove}>
Engineering
</Badge>
<Badge variant="outline" color="primary" rounded="md" removable onRemove={handleRemove}>
Design
</Badge>
<Badge variant="outline" color="primary" rounded="md" removable onRemove={handleRemove}>
Product
</Badge>
</Box>
<Box
style="padding: var(--uix-space-4); border: 1px solid var(--uix-border); border-radius: var(--uix-radius-md); display: flex; flex-wrap: wrap; gap: var(--uix-space-2); align-items: center;"
>
<code style="min-inline-size: 8rem; color: var(--uix-text-faint);">with icon</code>
<Badge variant="soft" color="affirm" icon={sparkIcon}>Premium</Badge>
<Badge variant="solid" color="fulfill" icon={sparkIcon}>Beta</Badge>
<Badge variant="outline" color="risk" icon={sparkIcon}>Limited</Badge>
</Box>
<Box
style="padding: var(--uix-space-4); border: 1px solid var(--uix-border); border-radius: var(--uix-radius-md); display: flex; flex-wrap: wrap; gap: var(--uix-space-2); align-items: center;"
>
<code style="min-inline-size: 8rem; color: var(--uix-text-faint);">inline label</code>
<span style="display: inline-flex; align-items: center; gap: var(--uix-space-2);">
Subscription <Badge variant="soft" color="affirm" size="sm">Active</Badge>
</span>
<span style="display: inline-flex; align-items: center; gap: var(--uix-space-2);">
Build #4218 <Badge variant="solid" color="primary" size="sm">latest</Badge>
</span>
<span style="display: inline-flex; align-items: center; gap: var(--uix-space-2);">
API v2 <Badge variant="outline" color="loss" size="sm">deprecated</Badge>
</span>
</Box>
</Stack>
</section>
{/if}
{#if tab === 'api'}
<section data-uix-section>
<h2 data-uix-section-title>API reference</h2>
<p data-uix-section-desc>
Badge composes through <code>&lt;Box&gt;</code> so consumers can apply layout primitives
(gridColumn, margin, …) without wrapping. The five Badge-specific props are independent —
you can render a label-only badge, a dot-only chip, an icon + label, or a removable tag
without touching the others.
</p>
<div data-uix-subsection-head>Badge-specific props</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Prop</th><th>Type</th><th>Notes</th></tr></thead>
<tbody>
<tr>
<td class="name">variant</td>
<td class="type">'soft' | 'solid' | 'outline' | 'ghost'</td>
<td>
<code>soft</code> (default) — tinted bg + text. <code>solid</code> — filled bg
+ contrast text. <code>outline</code> — border-only. <code>ghost</code> — text
only.
</td>
</tr>
<tr>
<td class="name">size</td>
<td class="type">'xs' | 'sm' | 'md' | 'lg'</td>
<td>
Drives padding · gap · font-size. Default <code>md</code>. No <code>xl</code> —
badges are inline labels, a larger size competes with body type.
</td>
</tr>
<tr>
<td class="name">color</td>
<td class="type">ColorRole</td>
<td>
Full 8-step palette. Map status semantics directly:
<code>affirm</code> → success, <code>risk</code> → warning,
<code>threat</code> → error, <code>loss</code> → archived / muted.
</td>
</tr>
<tr>
<td class="name">rounded</td>
<td class="type">'sm' | 'md' | 'lg' | 'full'</td>
<td>
Corner radius. Default <code>full</code> (pill). <code>md</code>/<code>lg</code>
for a soft-rect look at any size.
</td>
</tr>
<tr>
<td class="name">dot</td>
<td class="type">boolean</td>
<td>
Render a small coloured circle before the label — status-pill style. Uses the
solid palette step so it reads even on soft / outline / ghost variants.
</td>
</tr>
<tr>
<td class="name">icon</td>
<td class="type">Snippet?</td>
<td>
Optional snippet rendered before the label. Sized via
<code>--badge-icon-size</code> (1em — scales with the badge font-size).
</td>
</tr>
<tr>
<td class="name">removable</td>
<td class="type">boolean</td>
<td>
Render an X button at the trailing edge. Use for filter chips / dismissable
tags.
</td>
</tr>
<tr>
<td class="name">onRemove</td>
<td class="type">() =&gt; void</td>
<td>Callback fired when the user activates the remove button.</td>
</tr>
<tr>
<td class="name">removeLabel</td>
<td class="type">string?</td>
<td>
Override the remove button's accessible label. Defaults to the translated
<code>'Remove'</code>.
</td>
</tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Reference comparison</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Library</th><th>Closest equivalent</th><th>Difference</th></tr></thead>
<tbody>
<tr>
<td class="name">shadcn/ui</td>
<td><code>&lt;Badge&gt;</code></td>
<td>
shadcn ships 4 variants (default/secondary/destructive/outline). UIX adds the
8-color matrix, <code>ghost</code> variant, and built-in
<code>dot</code> / <code>icon</code> / <code>removable</code> slots.
</td>
</tr>
<tr>
<td class="name">radix-themes</td>
<td><code>&lt;Badge&gt;</code></td>
<td>
Radix has variant + radius + color + high-contrast. UIX drops high-contrast
(handled by <code>solid</code>) and adds <code>dot</code> + <code>removable</code>
as first-class props instead of consumer-composed children.
</td>
</tr>
<tr>
<td class="name">chakra-ui</td>
<td><code>&lt;Tag&gt;</code> / <code>&lt;Badge&gt;</code></td>
<td>
Chakra splits Tag (with CloseButton) and Badge (label only). UIX collapses both
into one primitive — <code>removable</code> turns a badge into a tag.
</td>
</tr>
<tr>
<td class="name">mantine</td>
<td><code>&lt;Badge&gt;</code></td>
<td>
Mantine has <code>leftSection</code> / <code>rightSection</code> for icons. UIX
uses a typed <code>icon</code> snippet for the leading slot; the trailing slot
is reserved for <code>removable</code>.
</td>
</tr>
<tr>
<td class="name">ark-ui</td>
<td>—</td>
<td>
Ark doesn't ship a Badge — it's pure visual without a behavior surface (no
open / close / select to coordinate). UIX places it in eidos for the same
reason.
</td>
</tr>
</tbody>
</table>
</div>
</section>
{/if}
{#if tab === 'morfo'}
<section data-uix-section>
<h2 data-uix-section-title>Morfo contract</h2>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Field</th><th>Value</th></tr></thead>
<tbody>
<tr><td class="name">name</td><td>{badgeMorfo.name}</td></tr>
<tr><td class="name">kebab</td><td><code>{badgeMorfo.kebab}</code></td></tr>
<tr><td class="name">scope</td><td>{badgeMorfo.scope.join(', ')}</td></tr>
<tr><td class="name">parts</td><td>{partsList.length}</td></tr>
<tr><td class="name">events</td><td>0</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Parts</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead>
<tr>
<th>kebab</th>
<th>marker</th>
<th>element</th>
<th>archetype</th>
<th>role</th>
<th>optional</th>
</tr>
</thead>
<tbody>
{#each partsList as part (part.kebab)}
<tr>
<td class="name">{part.kebab}</td>
<td><code data-uix-part-marker>[{part.marker}]</code></td>
<td class="type">&lt;{part.defaultElement}&gt;</td>
<td class="type">{part.archetype ?? '—'}</td>
<td class="type">{part.role ?? '—'}</td>
<td class="default">{part.optional ? 'yes' : 'no'}</td>
</tr>
{/each}
</tbody>
</table>
</div>
<div data-uix-subsection-head>Dot · aria</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>attr</th><th>value</th><th>severity</th></tr></thead>
<tbody>
<tr>
<td class="name"><code>aria-hidden</code></td>
<td>"true"</td>
<td class="type">literal</td>
</tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Icon · aria</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>attr</th><th>value</th><th>severity</th></tr></thead>
<tbody>
<tr>
<td class="name"><code>aria-hidden</code></td>
<td>"true"</td>
<td class="type">literal</td>
</tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Remove · aria</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>attr</th><th>value</th><th>severity</th></tr></thead>
<tbody>
<tr>
<td class="name"><code>role</code></td>
<td>button</td>
<td class="type">part default</td>
</tr>
<tr>
<td class="name"><code>type</code></td>
<td>"button"</td>
<td class="type">literal</td>
</tr>
<tr>
<td class="name"><code>aria-label</code></td>
<td>translationRef('components.badge.remove')</td>
<td class="type">recommended</td>
</tr>
</tbody>
</table>
</div>
</section>
{/if}
{#if tab === 'sema'}
<section data-uix-section>
<h2 data-uix-section-title>
<span data-uix-layer-badge="sema">sema</span> · events
</h2>
<p data-uix-section-desc>
Badge declares <strong>no semantic events</strong>. The chip is perceptual chrome — a
label, status pill, or tag — not a behavior surface. The optional
<code>Remove</code> button emits a click but the canonical shape for "user clicked the X"
is the consumer's <code>onRemove</code> handler, not a sema event the engine should
route.
</p>
<p data-uix-section-desc>
If you need a perceptual cue when a badge appears or removes (e.g. a tag added to a
filter list), attach it to the parent action — the <code>TagsInput</code>'s
<code>commit-add</code> / <code>commit-remove</code>, the search field's filter update,
etc.
</p>
</section>
{/if}
{#if tab === 'recipe'}
<section data-uix-section>
<h2 data-uix-section-title>Eidos recipe</h2>
<p data-uix-section-desc>
Recipe lives in <code>src/uix/eidos/components/badge/badge.css</code>. All variants share
private tokens (<code>--_badge-bg</code>, <code>--_badge-fg</code>,
<code>--_badge-border-color</code>) set per variant + color cascade so the rules below
stay flat. The recipe surface (<code>--badge-*</code>) feeds the canonical accent slots
(track / border / solid / text / contrast).
</p>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Selector</th><th>Owner</th><th>Purpose</th></tr></thead>
<tbody>
<tr>
<td class="name"><code>[data-badge]</code></td>
<td><span data-uix-tag data-kind="morfo">morfo</span></td>
<td>Provider marker. Inline-flex chrome (padding, radius, font, border).</td>
</tr>
<tr>
<td class="name"><code>[data-badge][data-size='xs'..'lg']</code></td>
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
<td>
Sets <code>--_badge-px</code> · <code>--_badge-py</code> ·
<code>--_badge-gap</code> · <code>--_badge-font-size</code>.
</td>
</tr>
<tr>
<td class="name"><code>[data-badge][data-rounded='sm'..'full']</code></td>
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
<td>Sets <code>--_badge-radius</code>.</td>
</tr>
<tr>
<td class="name"><code>[data-badge][data-color='primary'..'loss']</code></td>
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
<td>
Cascades track / border / solid / text / contrast from the canonical 8-color
palette.
</td>
</tr>
<tr>
<td class="name"><code>[data-badge][data-variant='soft'..'ghost']</code></td>
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
<td>
Picks which palette slot becomes <code>--_badge-bg</code> /
<code>--_badge-fg</code> / <code>--_badge-border-color</code>.
</td>
</tr>
<tr>
<td class="name"><code>[data-badge-dot]</code></td>
<td><span data-uix-tag data-kind="morfo">morfo</span></td>
<td>Status dot — small filled circle, always uses the solid palette step.</td>
</tr>
<tr>
<td class="name"><code>[data-badge-icon]</code></td>
<td><span data-uix-tag data-kind="morfo">morfo</span></td>
<td>Icon slot — sized 1em so it scales with the badge font-size.</td>
</tr>
<tr>
<td class="name"><code>[data-badge-label]</code></td>
<td><span data-uix-tag data-kind="morfo">morfo</span></td>
<td>Label wrapper around the children text.</td>
</tr>
<tr>
<td class="name"><code>[data-badge-remove]</code></td>
<td><span data-uix-tag data-kind="morfo">morfo</span></td>
<td>
Remove button — circular X with hover tint. Focus-visible paints the global
focus ring.
</td>
</tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>All variants × colors (size = md)</div>
<Stack gap="var(--uix-space-3)">
{#each variants as v (v)}
<Box
style="display: flex; align-items: center; gap: var(--uix-space-3); padding: var(--uix-space-3); border: 1px solid var(--uix-border); border-radius: var(--uix-radius-md); flex-wrap: wrap;"
>
<code style="min-inline-size: 5rem; color: var(--uix-text-faint);">{v}</code>
{#each colors as c (c)}
<Badge variant={v} color={c}>{c}</Badge>
{/each}
</Box>
{/each}
</Stack>
<div data-uix-subsection-head>All sizes × rounded (variant = soft, color = primary)</div>
<Box
style="display: flex; flex-wrap: wrap; gap: var(--uix-space-4);"
>
{#each sizes as s (s)}
<Box
style="display: flex; align-items: center; gap: var(--uix-space-2); padding: var(--uix-space-3); border: 1px solid var(--uix-border); border-radius: var(--uix-radius-md);"
>
<code style="min-inline-size: 2rem; color: var(--uix-text-faint);">{s}</code>
{#each roundeds as r (r)}
<Badge size={s} rounded={r} color="primary">{r}</Badge>
{/each}
</Box>
{/each}
</Box>
<div data-uix-subsection-head>Slot matrix · dot · icon · removable</div>
<Box
style="display: flex; flex-wrap: wrap; gap: var(--uix-space-3); padding: var(--uix-space-3); border: 1px solid var(--uix-border); border-radius: var(--uix-radius-md);"
>
<Badge color="affirm">label only</Badge>
<Badge color="affirm" dot>dot</Badge>
<Badge color="affirm" icon={sparkIcon}>icon</Badge>
<Badge color="affirm" removable onRemove={handleRemove}>removable</Badge>
<Badge color="affirm" dot icon={sparkIcon}>dot + icon</Badge>
<Badge color="affirm" icon={sparkIcon} removable onRemove={handleRemove}>
icon + remove
</Badge>
<Badge color="affirm" dot removable onRemove={handleRemove}>dot + remove</Badge>
</Box>
</section>
{/if}
{#if tab === 'a11y'}
<section data-uix-section>
<h2 data-uix-section-title>Accessibility</h2>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Concern</th><th>Contract</th></tr></thead>
<tbody>
<tr>
<td class="name">Provider role</td>
<td>
Default <code>&lt;span&gt;</code>, no implicit role. Screen readers read the
inline text alongside surrounding context — the badge is read as part of the
sentence it sits in, which is the right default for status / count chips.
</td>
</tr>
<tr>
<td class="name">Decorative slots</td>
<td>
<code>data-badge-dot</code> and <code>data-badge-icon</code> are
<code>aria-hidden="true"</code>. The label text alone carries semantic
meaning. Don't communicate state through the dot color only — pair it with a
word ("Active", "Failed").
</td>
</tr>
<tr>
<td class="name">Remove button</td>
<td>
<code>&lt;button type="button"&gt;</code> with translated
<code>aria-label="Remove"</code>. Activates via Enter / Space.
<code>stopPropagation</code> prevents the click from bubbling to a parent
interactive element (e.g. the row a filter tag sits in).
</td>
</tr>
<tr>
<td class="name">Focus visible</td>
<td>
Only the remove button is focusable. Focus ring uses the global
<code>--focus-ring-*</code> tokens.
</td>
</tr>
<tr>
<td class="name">Color contrast</td>
<td>
Each color × variant pair is auto-balanced by the canonical palette
(<code>track</code> + <code>text</code> for soft / outline / ghost,
<code>solid</code> + <code>contrast</code> for solid). Tested against WCAG AA
for normal text at all sizes ≥ <code>sm</code>.
</td>
</tr>
<tr>
<td class="name">High-contrast mode</td>
<td>
<code>outline</code> variant survives forced-colors (border + text use
<code>currentColor</code>). <code>ghost</code> degrades to text-only —
acceptable for label use, not for status pills (use <code>soft</code> or
<code>solid</code> there).
</td>
</tr>
<tr>
<td class="name">When to use</td>
<td>
Status pills, count chips, tags, version markers. For interactive selection use
<a href="/uix/components/tag-group">&lt;TagGroup&gt;</a>; for user-typed tags use
<a href="/uix/components/tags-input">&lt;TagsInput&gt;</a>; for filter chips use
<a href="/uix/components/toggle-group">&lt;ToggleGroup&gt;</a> with
<code>variant="chip"</code>.
</td>
</tr>
</tbody>
</table>
</div>
</section>
{/if}
</div>

Powered by TurnKey Linux.