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/metrics/+page.svelte

990 lines
39 KiB

<script lang="ts">
import { Metrics } from '$uix/eidos/components/metrics';
import type {
MetricsSize,
MetricsAlign,
MetricsLayout,
MetricsTrend,
MetricsIntent,
MetricsDeltaVariant,
MetricsIconVariant,
MetricsIconShape,
MetricsChartPlacement
} from '$uix/eidos/components/metrics';
import { Card } from '$uix/eidos/components/card';
import { Button } from '$uix/eidos/components/button';
import { Skeleton } from '$uix/eidos/components/skeleton';
import { Separator } from '$uix/eidos/components/separator';
import { Link } from '$uix/eidos/components/link';
import {
DollarSign,
Database,
Users,
Activity,
ShoppingCart,
Clock,
Eye,
ArrowUpRight
} from '$uix/eidos/components/icon';
import type { ColorRole } from '$uix/eidos/lib/types';
import { compileMorfo } from '$uix/morfo';
import { metricsMorfo } from '@/uix/morfo/components/metrics';
import { getActiveUix } from '$active-uix';
const uix = getActiveUix();
type Tab = 'live' | 'gallery' | '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);
// ── Root props ─────────────────────────────────────────────────────────
let size = $state<MetricsSize>('lg');
let align = $state<MetricsAlign>('start');
let layout = $state<MetricsLayout>('icon-start');
let live = $state(true);
// ── Value ──────────────────────────────────────────────────────────────
let value = $state(72540);
const display = $derived('$' + value.toLocaleString('en-US'));
// ── Delta ──────────────────────────────────────────────────────────────
let trend = $state<MetricsTrend>('up');
let goodTrend = $state<'up' | 'down'>('up');
let intentProp = $state<MetricsIntent | 'auto'>('auto');
let deltaVariant = $state<MetricsDeltaVariant>('soft');
let deltaValue = $state('12%');
// ── Icon (featured) + chart + actions + content ──────────────────────────
let showIcon = $state(true);
let iconVariant = $state<MetricsIconVariant>('soft');
let iconShape = $state<MetricsIconShape>('rounded');
let iconColor = $state<ColorRole>('primary');
let showChart = $state(true);
let chartPlacement = $state<MetricsChartPlacement>('full');
let showActions = $state(false);
let labelText = $state('Total revenue');
let descText = $state('vs. last month');
// Sample sparkline series (12 points, gently trending up).
const series = [
{ day: 1, amount: 38 },
{ day: 2, amount: 41 },
{ day: 3, amount: 39 },
{ day: 4, amount: 47 },
{ day: 5, amount: 52 },
{ day: 6, amount: 49 },
{ day: 7, amount: 58 },
{ day: 8, amount: 63 },
{ day: 9, amount: 60 },
{ day: 10, amount: 71 },
{ day: 11, amount: 76 },
{ day: 12, amount: 84 }
];
// A gently falling series — for "down is good" KPIs (churn, latency, cost).
const seriesDown = [
{ day: 1, amount: 41 },
{ day: 2, amount: 38 },
{ day: 3, amount: 39 },
{ day: 4, amount: 33 },
{ day: 5, amount: 30 },
{ day: 6, amount: 31 },
{ day: 7, amount: 26 },
{ day: 8, amount: 24 },
{ day: 9, amount: 22 },
{ day: 10, amount: 19 },
{ day: 11, amount: 18 },
{ day: 12, amount: 14 }
];
const sizes: MetricsSize[] = ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'];
const aligns: MetricsAlign[] = ['start', 'center'];
const layouts: MetricsLayout[] = ['icon-start', 'stack'];
const iconShapes: MetricsIconShape[] = ['circle', 'rounded', 'square'];
const trends: MetricsTrend[] = ['up', 'down', 'flat'];
const goodTrends = ['up', 'down'] as const;
const intentOptions = ['auto', 'neutral', 'affirm', 'fulfill', 'risk', 'threat', 'loss'] as const;
const deltaVariants: MetricsDeltaVariant[] = ['soft', 'solid', 'outline', 'ghost'];
const iconVariants: MetricsIconVariant[] = ['plain', 'soft', 'solid', 'outline'];
const chartPlacements: MetricsChartPlacement[] = ['full', 'below', 'right'];
const iconColors: ColorRole[] = [
'primary',
'secondary',
'neutral',
'affirm',
'fulfill',
'risk',
'threat',
'loss'
];
// Mirror the Delta's intent derivation so the trace + ▶ play match the chip.
const resolvedIntent = $derived<MetricsIntent>(
intentProp !== 'auto'
? intentProp
: trend === 'flat'
? 'neutral'
: trend === goodTrend
? 'affirm'
: 'risk'
);
const deltaIntentProp = $derived<MetricsIntent | undefined>(
intentProp === 'auto' ? undefined : intentProp
);
const compiled = compileMorfo(metricsMorfo);
const partsList = $derived([...compiled.parts.byKebab.values()]);
const events = $derived([...compiled.actions.byName.values()]);
function fmtTime(at: number): string {
const d = new Date(at);
return `${String(d.getSeconds()).padStart(2, '0')}.${String(d.getMilliseconds()).padStart(3, '0')}`;
}
function simulate(): void {
const step = trend === 'down' ? -1240 : trend === 'flat' ? 0 : 1240;
value = Math.max(0, value + step);
}
function playTarget(part: string): HTMLElement | null {
return (stageRef?.querySelector(`[data-metrics-${part}]`) ?? stageRef) as HTMLElement | null;
}
$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();
});
const eidosSnippet = $derived(
[
"<script lang='ts'>",
" import { Metrics } from '$uix/eidos/components/metrics';",
" import { Card } from '$uix/eidos/components/card';",
" import { DollarSign } from '$uix/eidos/components/icon';",
` let value = $state(${value});`,
'</' + 'script>',
'',
'<Card variant="outline">',
' <Metrics' +
(size !== 'md' ? ` size="${size}"` : '') +
(align !== 'start' ? ` align="${align}"` : '') +
(layout !== 'icon-start' ? ` layout="${layout}"` : '') +
(live ? ' live' : '') +
'>',
showIcon &&
` <Metrics.Icon variant="${iconVariant}" shape="${iconShape}"${iconVariant !== 'plain' ? ` color="${iconColor}"` : ''}><DollarSign /></Metrics.Icon>`,
` <Metrics.Label>${labelText}</Metrics.Label>`,
' <Metrics.Value {value}>$' + '{value.toLocaleString()}</Metrics.Value>',
' <Metrics.Trend>',
' <Metrics.Delta' +
` trend="${trend}" value="${deltaValue}"` +
(goodTrend !== 'up' ? ` goodTrend="${goodTrend}"` : '') +
(deltaIntentProp ? ` intent="${deltaIntentProp}"` : '') +
(deltaVariant !== 'soft' ? ` variant="${deltaVariant}"` : '') +
' />',
descText && ` <Metrics.Description>${descText}</Metrics.Description>`,
' </Metrics.Trend>',
showChart &&
` <Metrics.Chart data={series} x="day" y="amount"${chartPlacement !== 'full' ? ` placement="${chartPlacement}"` : ''} />`,
showActions &&
' <Metrics.Actions><Button variant="ghost" size="sm">View report</Button></Metrics.Actions>',
' </Metrics>',
'</Card>'
]
.filter(Boolean)
.join('\n')
);
</script>
<div data-uix-canvas-inner>
<header>
<div data-uix-eyebrow>Data · Metrics</div>
<h1 data-uix-page-title>Metrics</h1>
<p data-uix-page-lede>
A metric / KPI readout — label, headline value and a change delta, with an optional featured
icon, trend sparkline and actions row. Covers the Untitled-UI <strong>Simple</strong>,
<strong>Icon</strong>
and <strong>Chart</strong> families (each ± actions). Surfaceless: compose it inside
<code>&lt;Card&gt;</code>. The Delta decouples direction (the arrow) from valence (the colour) —
set <code>goodTrend="down"</code> where falling is good. Opt into <code>live</code> and a value
change fires <code>signal-notify-update</code> (flash + sema ping + announce).
</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>{events.length}</span>
<span data-uix-meta-pill><span data-uix-meta-key>families</span>3</span>
<span data-uix-meta-pill><span data-uix-meta-key>intents</span>6</span>
<span data-uix-meta-pill><span data-uix-meta-key>scope</span>sema · eidos</span>
</div>
</header>
<div data-uix-stage>
<div data-uix-stage-area bind:this={stageRef}>
<Card variant="outline" style="inline-size: min(22rem, 100%);">
<Metrics {size} {align} {layout} {live}>
{#if showIcon}
<Metrics.Icon variant={iconVariant} shape={iconShape} color={iconColor}>
<DollarSign />
</Metrics.Icon>
{/if}
<Metrics.Label>{labelText}</Metrics.Label>
<Metrics.Value {value}>{display}</Metrics.Value>
<Metrics.Trend>
<Metrics.Delta
{trend}
value={deltaValue}
{goodTrend}
intent={deltaIntentProp}
variant={deltaVariant}
/>
{#if descText}<Metrics.Description>{descText}</Metrics.Description>{/if}
</Metrics.Trend>
{#if showChart}<Metrics.Chart data={series} x="day" y="amount" placement={chartPlacement} />{/if}
{#if showActions}
<Metrics.Actions><Button variant="ghost" size="sm">View report</Button></Metrics.Actions>
{/if}
</Metrics>
</Card>
</div>
<div data-uix-stage-trace>
<span data-uix-stage-trace-key>trace</span>
{#if trace.length === 0}
<span>turn on <code>live</code> + press “Simulate update” to see the signal</span>
{:else}
{#each trace.slice(0, 3) as entry}
<span
><span data-uix-stage-trace-event>{entry.event}</span> · {entry.family}{entry.intent
? ' · ' + entry.intent
: ''}</span
>
<span style="color: var(--uix-text-faint)">{fmtTime(entry.at)}</span>
{/each}
{/if}
<span style="margin-inline-start: auto;">
<span data-uix-stage-trace-key>intent</span>
{resolvedIntent}
<span data-uix-stage-trace-key style="margin-inline-start: var(--uix-space-2);">value</span>
{value}
</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 === 'gallery'} onclick={() => (tab = 'gallery')}
>Gallery</button
>
<button data-uix-tab data-active={tab === 'api'} onclick={() => (tab = 'api')}
>API <span data-uix-tab-count>16</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 · {events.length}e</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>
Controls are grouped by owner. <span data-uix-layer-badge="eidos">eidos</span>
owns layout, the delta chip, the featured icon and the chart; the <span
data-uix-layer-badge="sema">sema</span
>
signal (Sema tab) fires on a live value change. Toggle icon / chart / actions to see the
Untitled families.
</p>
<div data-uix-subsection-head>
<span data-uix-layer-badge="eidos">eidos</span> props · layout
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>size</span>
<span data-uix-chips role="radiogroup">
{#each sizes as 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>align</span>
<span data-uix-chips role="radiogroup">
{#each aligns as a}
<button data-uix-chip data-active={align === a} onclick={() => (align = a)}>{a}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>layout <span data-uix-control-hint>icon position</span></span>
<span data-uix-chips role="radiogroup">
{#each layouts as l}
<button data-uix-chip data-active={layout === l} onclick={() => (layout = l)}>{l}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>label text</span>
<input type="text" bind:value={labelText} />
</label>
<label data-uix-control>
<span data-uix-control-label>description</span>
<input type="text" bind:value={descText} placeholder="(empty to omit)" />
</label>
</div>
<div data-uix-subsection-head>
<span data-uix-layer-badge="eidos">eidos</span> props · delta (direction ⟂ valence)
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>trend <span data-uix-control-hint>arrow</span></span>
<span data-uix-chips role="radiogroup">
{#each trends as t}
<button data-uix-chip data-active={trend === t} onclick={() => (trend = t)}>{t}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>goodTrend <span data-uix-control-hint>which way is good</span></span
>
<span data-uix-chips role="radiogroup">
{#each goodTrends as g}
<button data-uix-chip data-active={goodTrend === g} onclick={() => (goodTrend = g)}
>{g}</button
>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>intent <span data-uix-control-hint>auto = derived</span></span>
<span data-uix-chips role="radiogroup">
{#each intentOptions as i}
<button data-uix-chip data-active={intentProp === i} onclick={() => (intentProp = i)}
>{i}</button
>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>variant</span>
<span data-uix-chips role="radiogroup">
{#each deltaVariants as v}
<button data-uix-chip data-active={deltaVariant === v} onclick={() => (deltaVariant = v)}
>{v}</button
>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>delta value</span>
<input type="text" bind:value={deltaValue} />
</label>
</div>
<div data-uix-subsection-head>
<span data-uix-layer-badge="eidos">eidos</span> props · featured icon
</div>
<div data-uix-controls>
<label data-uix-control data-uix-control-inline>
<input type="checkbox" bind:checked={showIcon} />
<span data-uix-control-label>show icon</span>
</label>
<label data-uix-control>
<span data-uix-control-label>icon variant</span>
<span data-uix-chips role="radiogroup">
{#each iconVariants as v}
<button data-uix-chip data-active={iconVariant === v} onclick={() => (iconVariant = v)}
>{v}</button
>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>icon shape</span>
<span data-uix-chips role="radiogroup">
{#each iconShapes as sh}
<button data-uix-chip data-active={iconShape === sh} onclick={() => (iconShape = sh)}
>{sh}</button
>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>icon color</span>
<span data-uix-chips role="radiogroup">
{#each iconColors as c}
<button data-uix-chip data-active={iconColor === c} onclick={() => (iconColor = c)}
>{c}</button
>
{/each}
</span>
</label>
</div>
<div data-uix-subsection-head>
<span data-uix-layer-badge="eidos">eidos</span> props · chart + actions
</div>
<div data-uix-controls>
<label data-uix-control data-uix-control-inline>
<input type="checkbox" bind:checked={showChart} />
<span data-uix-control-label>show chart <span data-uix-control-hint>sparkline</span></span>
</label>
<label data-uix-control>
<span data-uix-control-label>chart placement</span>
<span data-uix-chips role="radiogroup">
{#each chartPlacements as p}
<button
data-uix-chip
data-active={chartPlacement === p}
onclick={() => (chartPlacement = p)}>{p}</button
>
{/each}
</span>
</label>
<label data-uix-control data-uix-control-inline>
<input type="checkbox" bind:checked={showActions} />
<span data-uix-control-label>show actions <span data-uix-control-hint>View report</span></span>
</label>
</div>
<div data-uix-subsection-head>
<span data-uix-layer-badge="sema">sema</span> · live signal
</div>
<div data-uix-controls>
<label data-uix-control data-uix-control-inline>
<input type="checkbox" bind:checked={live} />
<span data-uix-control-label>live <span data-uix-control-hint>fire on value change</span></span
>
</label>
<label data-uix-control>
<span data-uix-control-label>value <span data-uix-control-hint>{value}</span></span>
<input type="number" bind:value step="100" />
</label>
<label data-uix-control>
<span data-uix-control-label>&nbsp;</span>
<button data-uix-chip onclick={simulate}>Simulate update</button>
</label>
</div>
<div data-uix-subsection-head>
<span data-uix-layer-badge="eidos">eidos</span> · more variants
<span data-uix-control-hint>compose Progress · Gauge · Skeleton</span>
</div>
<div
style="display:grid; grid-template-columns:repeat(auto-fit,minmax(190px,1fr)); gap:var(--uix-space-3);"
>
<Card variant="outline">
<Metrics size="sm">
<Metrics.Label>Quarterly target</Metrics.Label>
<Metrics.Value value={72540}>$72.5k</Metrics.Value>
<Metrics.Progress value={72540} max={100000} ariaLabel="Quarterly target"
>72% to goal</Metrics.Progress
>
</Metrics>
</Card>
<Card variant="outline">
<Metrics size="sm">
<Metrics.Label>Storage used</Metrics.Label>
<div style="display:flex; align-items:center; gap:var(--uix-space-3);">
<Metrics.Gauge value={68} max={100} ariaLabel="Storage used" />
<Metrics.Value>6.8 TB</Metrics.Value>
</div>
</Metrics>
</Card>
<Card variant="outline">
<div style="display:flex; gap:var(--uix-space-3);">
<Skeleton shape="circle" size="md" />
<div style="flex:1; display:grid; gap:var(--uix-space-2);">
<Skeleton shape="text" width="55%" />
<Skeleton width="75%" height={24} />
</div>
</div>
</Card>
<Card variant="outline">
<Metrics size="sm">
<Metrics.Icon variant="soft" color="neutral"><Database /></Metrics.Icon>
<Metrics.Label>Revenue</Metrics.Label>
<Metrics.Value>—</Metrics.Value>
<Metrics.Description>No data yet</Metrics.Description>
</Metrics>
</Card>
</div>
<div data-uix-code style="margin-top: var(--uix-space-4);">
<div data-uix-code-head>
<span data-uix-layer-badge="eidos">eidos</span>
<span>composed Card + Metrics + Badge + Icon + Sparkline</span>
<span data-uix-code-lang>svelte</span>
</div>
<pre><code>{eidosSnippet}</code></pre>
</div>
</section>
{/if}
{#if tab === 'gallery'}
<section data-uix-section>
<h2 data-uix-section-title>Gallery — possibilities</h2>
<p data-uix-section-intro>
Every tile is the <em>same</em> compound — `Metrics` + its parts — wrapped in
`Card`. Nothing bespoke: the icon, delta, sparkline, progress bar and gauge are
all composed framework components. Direction (the arrow) is decoupled from valence
(the colour), so "down is good" KPIs read correctly.
</p>
<!-- Dashboard row — N metrics + vertical Separator (composition) -->
<div data-uix-subsection-head>Dashboard row — metrics + vertical `Separator`</div>
<Card variant="outline">
<div class="gallery-row">
<Metrics size="sm" layout="stack" style="flex:1; min-inline-size:7rem;">
<Metrics.Label>Revenue</Metrics.Label>
<Metrics.Value value={72540}>$72.5k</Metrics.Value>
<Metrics.Delta trend="up" value="12%" />
</Metrics>
<Separator orientation="vertical" />
<Metrics size="sm" layout="stack" style="flex:1; min-inline-size:7rem;">
<Metrics.Label>Orders</Metrics.Label>
<Metrics.Value value={1240}>1,240</Metrics.Value>
<Metrics.Delta trend="up" value="8%" />
</Metrics>
<Separator orientation="vertical" />
<Metrics size="sm" layout="stack" style="flex:1; min-inline-size:7rem;">
<Metrics.Label>Churn</Metrics.Label>
<Metrics.Value value={2.1}>2.1%</Metrics.Value>
<Metrics.Delta trend="down" value="0.4pp" goodTrend="down" />
</Metrics>
</div>
</Card>
<!-- Grid of individual possibilities -->
<div class="gallery-grid">
<div class="gallery-tile">
<Card variant="outline">
<Metrics size="md" layout="stack">
<Metrics.Label>Active users</Metrics.Label>
<Metrics.Value value={8420}>8,420</Metrics.Value>
</Metrics>
</Card>
<span class="gallery-cap">Simple — label + value</span>
</div>
<div class="gallery-tile">
<Card variant="outline">
<Metrics size="md" layout="stack">
<Metrics.Label>New signups</Metrics.Label>
<Metrics.Value value={1240}>1,240</Metrics.Value>
<Metrics.Delta trend="up" value="18%" />
</Metrics>
</Card>
<span class="gallery-cap">+ change delta</span>
</div>
<div class="gallery-tile">
<Card variant="outline">
<Metrics size="md" layout="stack">
<Metrics.Icon variant="soft" color="primary"><Users /></Metrics.Icon>
<Metrics.Label>Team members</Metrics.Label>
<Metrics.Value value={48}>48</Metrics.Value>
<Metrics.Delta trend="up" value="6" />
</Metrics>
</Card>
<span class="gallery-cap">Featured icon (stack)</span>
</div>
<div class="gallery-tile">
<Card variant="outline">
<Metrics size="md" layout="icon-start">
<Metrics.Icon variant="soft" color="primary"><DollarSign /></Metrics.Icon>
<Metrics.Label>Total revenue</Metrics.Label>
<Metrics.Value value={72540}>$72,540</Metrics.Value>
<Metrics.Delta trend="up" value="12%" />
<Metrics.Chart data={series} x="day" y="amount" />
</Metrics>
</Card>
<span class="gallery-cap">Icon-start + full sparkline (default)</span>
</div>
<div class="gallery-tile">
<Card variant="outline">
<Metrics size="md" layout="stack">
<Metrics.Icon variant="soft" color="secondary"><Activity /></Metrics.Icon>
<Metrics.Label>API latency</Metrics.Label>
<Metrics.Value value={128}>128ms</Metrics.Value>
<Metrics.Delta trend="down" value="14ms" goodTrend="down" />
<Metrics.Chart data={seriesDown} x="day" y="amount" placement="below" color="affirm" />
</Metrics>
</Card>
<span class="gallery-cap">Sparkline below</span>
</div>
<div class="gallery-tile">
<Card variant="outline">
<Metrics size="md" layout="stack">
<Metrics.Label>Sales today</Metrics.Label>
<Metrics.Value value={3120}>3,120</Metrics.Value>
<Metrics.Delta trend="up" value="9%" />
<Metrics.Chart data={series} x="day" y="amount" placement="right" />
</Metrics>
</Card>
<span class="gallery-cap">Sparkline right</span>
</div>
<div class="gallery-tile">
<Card variant="outline">
<Metrics size="md" layout="stack">
<Metrics.Label>Quarterly target</Metrics.Label>
<Metrics.Value value={72540}>$72.5k</Metrics.Value>
<Metrics.Progress value={72540} max={100000} ariaLabel="Quarterly target"
>72% of $100k goal</Metrics.Progress
>
</Metrics>
</Card>
<span class="gallery-cap">Progress to goal (Meter)</span>
</div>
<div class="gallery-tile">
<Card variant="outline">
<Metrics size="md" layout="stack">
<Metrics.Label>Storage used</Metrics.Label>
<div style="display:flex; align-items:center; gap:var(--uix-space-3);">
<Metrics.Gauge value={68} max={100} ariaLabel="Storage used" />
<Metrics.Value>6.8 / 10 TB</Metrics.Value>
</div>
</Metrics>
</Card>
<span class="gallery-cap">Radial gauge (Meter)</span>
</div>
<div class="gallery-tile">
<Card variant="outline">
<Metrics size="md" layout="stack">
<Metrics.Icon variant="soft" color="affirm"><Clock /></Metrics.Icon>
<Metrics.Label>Churn rate</Metrics.Label>
<Metrics.Value value={2.1}>2.1%</Metrics.Value>
<Metrics.Trend>
<Metrics.Delta trend="down" value="0.4pp" goodTrend="down" />
<Metrics.Description>↓ is good — affirm</Metrics.Description>
</Metrics.Trend>
</Metrics>
</Card>
<span class="gallery-cap">"Down is good" — ▼ affirm</span>
</div>
<div class="gallery-tile">
<Card variant="outline">
<Metrics size="md" layout="stack">
<Metrics.Label>Refunds</Metrics.Label>
<Metrics.Value value={3210}>$3,210</Metrics.Value>
<Metrics.Trend>
<Metrics.Delta trend="up" value="8%" />
<Metrics.Description>↑ is bad — risk</Metrics.Description>
</Metrics.Trend>
</Metrics>
</Card>
<span class="gallery-cap">Negative — ▲ risk (default)</span>
</div>
<div class="gallery-tile">
<Card variant="outline">
<Metrics size="md" layout="stack" align="center">
<Metrics.Icon variant="solid" shape="circle" color="fulfill"
><ShoppingCart /></Metrics.Icon
>
<Metrics.Label>Conversion</Metrics.Label>
<Metrics.Value value={3.8}>3.8%</Metrics.Value>
<Metrics.Delta trend="up" value="0.5pp" />
</Metrics>
</Card>
<span class="gallery-cap">Centered + solid circle icon</span>
</div>
<div class="gallery-tile">
<Card interactive variant="outline">
<Metrics size="md" layout="icon-start">
<Metrics.Icon variant="soft" color="primary"><Eye /></Metrics.Icon>
<Metrics.Label>Page views</Metrics.Label>
<Metrics.Value value={48200}>48.2k</Metrics.Value>
<Metrics.Delta trend="up" value="11%" />
<Metrics.Actions>
<Link href="#gallery">View report <ArrowUpRight /></Link>
</Metrics.Actions>
</Metrics>
</Card>
<span class="gallery-cap">Interactive card + actions</span>
</div>
<div class="gallery-tile">
<Card variant="outline">
<div style="display:flex; gap:var(--uix-space-3);">
<Skeleton shape="circle" size="md" />
<div style="flex:1; display:grid; gap:var(--uix-space-2);">
<Skeleton shape="text" width="55%" />
<Skeleton width="75%" height={24} />
<Skeleton width="40%" height={14} />
</div>
</div>
</Card>
<span class="gallery-cap">Loading — Skeleton</span>
</div>
<div class="gallery-tile">
<Card variant="outline">
<Metrics size="md" layout="stack">
<Metrics.Icon variant="soft" color="neutral"><Database /></Metrics.Icon>
<Metrics.Label>Revenue</Metrics.Label>
<Metrics.Value>—</Metrics.Value>
<Metrics.Description>No data yet</Metrics.Description>
</Metrics>
</Card>
<span class="gallery-cap">Empty state</span>
</div>
</div>
</section>
{/if}
{#if tab === 'api'}
<section data-uix-section>
<h2 data-uix-section-title>API reference</h2>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Part · Prop</th><th>Type</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td class="name">Metrics · size</td><td class="type">{`ResponsiveProp<'xs'…'xxl'>`}</td><td class="default">'md'</td><td>Whole-block scale (6 steps); value 1:1 with the type scale, every sub-part scales. Theme-overridable token.</td></tr>
<tr><td class="name">Metrics · align</td><td class="type">'start' | 'center'</td><td class="default">'start'</td><td>Cross-axis alignment.</td></tr>
<tr><td class="name">Metrics · layout</td><td class="type">'icon-start' | 'stack'</td><td class="default">'icon-start'</td><td>Featured-icon position — left column vs. stacked on top.</td></tr>
<tr><td class="name">Metrics · live</td><td class="type">boolean</td><td class="default">false</td><td>Fire <code>signal-notify-update</code> on a Value change.</td></tr>
<tr><td class="name">Metrics.Value · value</td><td class="type">string | number</td><td class="default empty">—</td><td>The figure. Rendered when no children; drives live detection.</td></tr>
<tr><td class="name">Metrics.Delta · trend</td><td class="type">'up' | 'down' | 'flat'</td><td class="default empty">—</td><td>Direction — drives the arrow glyph.</td></tr>
<tr><td class="name">Metrics.Delta · value</td><td class="type">string | number</td><td class="default empty">—</td><td>Change magnitude; used for the a11y label.</td></tr>
<tr><td class="name">Metrics.Delta · goodTrend</td><td class="type">'up' | 'down'</td><td class="default">'up'</td><td>Which direction reads as affirmative.</td></tr>
<tr><td class="name">Metrics.Delta · intent</td><td class="type">MetricsIntent</td><td class="default empty">— (derived)</td><td>Override the trend→colour derivation.</td></tr>
<tr><td class="name">Metrics.Delta · variant</td><td class="type">'soft'|'solid'|'outline'|'ghost'</td><td class="default">'soft'</td><td>Chip treatment (composed Badge).</td></tr>
<tr><td class="name">Metrics.Icon · variant</td><td class="type">'plain'|'soft'|'solid'|'outline'</td><td class="default">'soft'</td><td>Featured-icon container treatment.</td></tr>
<tr><td class="name">Metrics.Icon · shape</td><td class="type">'circle'|'rounded'|'square'</td><td class="default">'rounded'</td><td>Featured-icon silhouette.</td></tr>
<tr><td class="name">Metrics.Chart · placement</td><td class="type">'full'|'below'|'right'</td><td class="default">'full'</td><td>Sparkline position: full-width (breaks out past the icon) · in-column · pinned right.</td></tr>
<tr><td class="name">Metrics.Icon · color</td><td class="type">ColorRole</td><td class="default">'primary'</td><td>Featured-icon tint.</td></tr>
<tr><td class="name">Metrics.Chart · data / x / y</td><td class="type">{`Row[] / string / string`}</td><td class="default empty">—</td><td>Sparkline series + accessor keys (composed <code>&lt;Sparkline&gt;</code>).</td></tr>
<tr><td class="name">Metrics.Chart · color</td><td class="type">ColorRole</td><td class="default empty">— (delta intent)</td><td>Line/area colour; defaults to the trend's valence.</td></tr>
<tr><td class="name">Metrics.Actions</td><td class="type">Snippet</td><td class="default empty">—</td><td>Footer row — compose <code>&lt;Link&gt;</code> / <code>&lt;Button&gt;</code> / <code>&lt;DropdownMenu&gt;</code>.</td></tr>
<tr><td class="name">Metrics.Label / Description</td><td class="type">Snippet</td><td class="default empty">—</td><td>Metric name · period / context.</td></tr>
</tbody>
</table>
</div>
</section>
{/if}
{#if tab === 'morfo'}
<section data-uix-section>
<h2 data-uix-section-title>
<span data-uix-layer-badge="morfo">morfo</span> · declarative contract
</h2>
<p data-uix-section-desc>Source: <code>src/uix/morfo/components/metrics.ts</code>.</p>
<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 class="type">"{metricsMorfo.name}"</td></tr>
<tr><td class="name">kebab</td><td class="type">"{metricsMorfo.kebab}"</td></tr>
<tr><td class="name">scope</td><td class="type">[{metricsMorfo.scope.map((s) => `"${s}"`).join(', ')}]</td></tr>
<tr><td class="name">expression</td><td class="type">"{metricsMorfo.expression}"</td></tr>
<tr><td class="name">parts.length</td><td class="default">{metricsMorfo.parts.length}</td></tr>
<tr><td class="name">events.length</td><td class="default">{events.length}</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Parts</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Part</th><th>Marker</th><th>Element</th><th>Archetype</th><th>Optional</th></tr></thead>
<tbody>
{#each partsList as part}
<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="default">{part.archetype ?? '—'}</td>
<td class="default">{part.optional ? 'yes' : 'no'}</td>
</tr>
{/each}
</tbody>
</table>
</div>
<div data-uix-subsection-head>Events</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>name</th><th>family</th><th>verb</th><th>sequence</th><th>intent</th><th>target</th></tr></thead>
<tbody>
{#each metricsMorfo.events as ev}
{@const sem = ev.semantic as { family: string; verb?: string; sequence?: string; intent?: unknown; target?: { ref?: string } }}
<tr>
<td class="name">{ev.name}</td>
<td class="type">{sem.family}</td>
<td class="default">{sem.verb ?? '—'}</td>
<td class="default">{sem.sequence ?? '—'}</td>
<td class="default">{typeof sem.intent === 'string' ? sem.intent : 'fromProp: intent'}</td>
<td class="default">{sem.target?.ref ?? '—'}</td>
</tr>
{/each}
</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>
One event: <code>signal-notify-update</code> (signal · notify), fired only in
<code>live</code> mode when the Value changes. Its intent follows the Delta's resolved
valence. ▶ play emits onto the live <code>[data-metrics-value]</code> target.
</p>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>name</th><th>family</th><th>verb</th><th>sequence</th><th>intent</th><th>play</th></tr></thead>
<tbody>
{#each metricsMorfo.events as ev}
{@const sem = ev.semantic as { family: string; verb?: string; sequence?: string; intent?: unknown; target?: { ref?: string } }}
<tr>
<td class="name">{ev.name}</td>
<td class="type">{sem.family}</td>
<td class="default">{sem.verb ?? '—'}</td>
<td class="default">{sem.sequence ?? '—'}</td>
<td class="default">{resolvedIntent}</td>
<td>
<button
data-uix-play
onclick={() => {
const target = playTarget(sem.target?.ref ?? 'value');
if (!target) return;
void uix.events?.emit({
name: ev.name,
family: sem.family as never,
target,
intent: resolvedIntent as never
});
}}>▶ play</button
>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</section>
{/if}
{#if tab === 'recipe'}
<section data-uix-section>
<h2 data-uix-section-title>Eidos recipe</h2>
<p data-uix-section-desc>
Selectors at <code>src/uix/eidos/components/metrics/metrics.css</code>. Surfaceless: layout
+ typography only. The Delta's colour comes from the composed Badge; the chart from the
composed Sparkline.
</p>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Selector</th><th>Source</th></tr></thead>
<tbody>
<tr><td class="name">[data-metrics][data-size]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td></tr>
<tr><td class="name">[data-metrics][data-align]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td></tr>
<tr><td class="name">[data-metrics][data-live]</td><td><span data-uix-tag data-kind="soma">morfo</span></td></tr>
<tr><td class="name">[data-metrics-label] / [data-metrics-value] / [data-metrics-description]</td><td><span data-uix-tag data-kind="soma">morfo</span></td></tr>
<tr><td class="name">[data-metrics-delta][data-trend]</td><td><span data-uix-tag data-kind="soma">morfo</span></td></tr>
<tr><td class="name">[data-metrics-icon][data-variant][data-color]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td></tr>
<tr><td class="name">[data-metrics-chart] / [data-metrics-actions]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td></tr>
<tr><td class="name">[data-metrics-value][data-event-family='signal']</td><td><span data-uix-tag data-kind="sema">sema</span></td></tr>
</tbody>
</table>
</div>
</section>
{/if}
{#if tab === 'a11y'}
<section data-uix-section>
<h2 data-uix-section-title>Accessibility</h2>
<p data-uix-section-desc>
Metrics is a display block read linearly by a screen reader (label, value, then delta). The
Delta's arrow is <code>aria-hidden</code>; its accessible name comes from the trend phrase
(<code>components.metrics.trend-*</code>), so a reader hears “Increased by 12%”. The featured
icon and sparkline are <code>aria-hidden</code> (decorative). In <code>live</code> mode the new
value is announced via the shared live region. Interactive drill-down is delegated to
<code>&lt;Card interactive&gt;</code>; the Actions row composes a real <code>&lt;Link&gt;</code> /
<code>&lt;Button&gt;</code>.
</p>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Concern</th><th>Handling</th></tr></thead>
<tbody>
<tr><td>Delta name</td><td class="type">aria-label from <code>trend-up/down/flat</code> + value</td></tr>
<tr><td>Decorative glyphs</td><td class="type">arrow, featured icon + sparkline are aria-hidden</td></tr>
<tr><td>Live updates</td><td class="type">announced to the live region (requiresLiveRegion)</td></tr>
<tr><td>Numerals</td><td class="type">tabular + lining (digits align as values change)</td></tr>
<tr><td>Reduced motion</td><td class="type">flash capped to 1ms; sound/haptic still convey the signal</td></tr>
</tbody>
</table>
</div>
</section>
{/if}
</div>
<style>
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: var(--uix-space-5);
margin-top: var(--uix-space-4);
}
.gallery-tile {
display: flex;
flex-direction: column;
gap: var(--uix-space-2);
min-inline-size: 0;
}
.gallery-cap {
font-size: 0.75rem;
color: var(--color-content-muted);
letter-spacing: 0.01em;
}
.gallery-row {
display: flex;
align-items: stretch;
gap: var(--uix-space-4);
flex-wrap: wrap;
}
</style>

Powered by TurnKey Linux.