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.
990 lines
39 KiB
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><Card></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> </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><Sparkline></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><Link></code> / <code><Button></code> / <code><DropdownMenu></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"><{part.defaultElement}></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><Card interactive></code>; the Actions row composes a real <code><Link></code> /
|
|
<code><Button></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>
|