|
|
<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><Box></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">() => 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><Badge></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><Badge></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><Tag></code> / <code><Badge></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><Badge></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"><{part.defaultElement}></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><span></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><button type="button"></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"><TagGroup></a>; for user-typed tags use
|
|
|
<a href="/uix/components/tags-input"><TagsInput></a>; for filter chips use
|
|
|
<a href="/uix/components/toggle-group"><ToggleGroup></a> with
|
|
|
<code>variant="chip"</code>.
|
|
|
</td>
|
|
|
</tr>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
</section>
|
|
|
{/if}
|
|
|
</div>
|