# Metric display

> One headline number with its label, an optional unit, a signed comparison such as Up 12% vs last month, and a source footnote. Formats with an explicit locale; says direction in words, tones by whether it is good news.

- ID: `cmp_metric_display_01`
- Slug: `metric-display-01`
- Version: `1.0.0` (current)
- Status: published
- Published: 2026-10-21T21:38:13Z
- Updated: 2026-10-21
- Available versions: `1.0.0`
- Kind: control
- Primary category: `typography`
- Detail page: https://pagesugar.com/components/metric-display-01
- Preview: https://pagesugar.com/preview/metric-display-01

## Variants

| Variant | Label | Default | Artifact digest |
| --- | --- | --- | --- |
| `neutral` | Inherited colour | yes | `sha256-ea66d7b2d1eaf68936394c7220dc667d2f2f5be8cfbc0f3adb2c5d6d69cd090d` |

## Runtime and compatibility

- Runtime: svelte
- Svelte: 5
- SvelteKit required: no (portable Svelte component)
- Imports: The source imports its own files by relative path only. Examples import from $lib/...; SvelteKit 3 projects have no $lib alias by default, so import from your #lib subpath (with the .svelte extension) or a relative path instead.
- Tailwind CSS: 4
- SSR: supported
- Requires client-side JavaScript: no
- Integration level: presentational
- Appearance modes: light
- Suggested directory: `src/lib/components/metric-display-01`

## Dependencies

No third-party runtime packages.

## Services

No external services required.

## Usage

Pass a label, the raw value and a locale; add a format, unit, change and source as needed. It renders one description list: label, formatted value, an optional comparison and footnote. It does not fetch or calculate metrics, animate or count up, update live, draw a chart, or check that the figure is true.

Required props: `label`, `value`, `locale`

```svelte
<script lang="ts">
	import MetricDisplay from '$lib/components/metric-display-01/MetricDisplay.svelte';
</script>

<MetricDisplay
	label="Weekly active members"
	value={12400}
	locale="en-US"
	size="lg"
	change={{ value: 0.082, period: 'vs Q2', goodWhen: 'up' }}
	source="Signed in at least once a week"
/>

<MetricDisplay
	label="Median time from card to Done"
	value={3.4}
	unit="days"
	locale="en-US"
	change={{ value: -0.12, period: 'vs Q2', goodWhen: 'down' }}
/>
```

Limitations:

- The change's value is a fraction under the default percent format (0.12 prints 12%). For an absolute change, such as 40 ms or 4 points, pass change.format; the component does not calculate a change from two values.
- Direction words are English by default. Pass the text prop with up, down, none and unavailable in the page's language; {value} marks where the magnitude goes.
- A locale the JavaScript engine does not support falls back to 'en' on both server and browser, rather than to the host's default.
- The positive and negative defaults clear 4.5:1 on white and light pages only. On a dark or tinted page set --metric-display-positive and --metric-display-negative to tones that clear it there.
- The value never wraps. A very long number at lg in a column narrower than about 280 px will overflow; use md or compact notation there.
- Formatting depends on the JavaScript engine's locale data. A server whose Node build lacks a locale falls back to 'en' and the browser may then print the figure differently on hydration; full-ICU Node (the default since Node 13) avoids it.
- Light mode is what is tested. Because ink and muted follow the surrounding text colour, a dark band works with the two tone variables set, but no dark mode is declared.

## Usage guide

### Metric display

One headline number with its label, an optional unit, a comparison and a footnote. Use it alone
on a card, or several in a row for a stat band, a dashboard header or a case study.

#### Values and changes

- `value` is the raw number. Formatting is `Intl.NumberFormat(locale, format)`, with the locale
  passed explicitly so the server render and the browser agree.
- `change.value` is a signed fraction under the default percent format: `0.12` prints `12%`,
  `-0.3` prints `30%` with the word Down. For an absolute change pass `change.format`, for
  example `{ style: 'unit', unit: 'millisecond', unitDisplay: 'short' }` with `value: 40`.
- The direction follows the rounded figure. A change that prints as `0%` says No change in the
  muted tone, whatever `goodWhen` says.
- `goodWhen: 'down'` is for figures where less is better: wait times, error rates, cost, churn.
  With `goodWhen` set, the change ends with `better` or `worse`, so the judgement is in words and
  not only in the green or red tone.

#### In a row

```svelte
<ul class="grid border-t border-black/10 sm:grid-cols-3">
	{#each metrics as metric, i}
		<li
			class={[
				'py-8 sm:px-8 sm:first:ps-0',
				i > 0 && 'border-t border-black/10 sm:border-s sm:border-t-0'
			]}
		>
			<MetricDisplay {...metric} locale="en-US" size="lg" />
		</li>
	{/each}
</ul>
```

Keep every metric in a row at the same size, and the same period wording, so they compare.

#### Other languages

Pass `text` with the six phrases. `{value}` marks where the magnitude goes, so word order can
change: `{ up: '{value}増', down: '{value}減', none: '変化なし', better: '改善', worse: '悪化', unavailable: 'データなし' }`.

## Props

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `label` | `string` | yes |  | What the number measures, e.g. 'Weekly active members'. Read before the value. |
| `value` | number \| null | yes |  | The raw number. null, NaN or Infinity draws a dash, reads the unavailable text and hides the change. |
| `locale` | `string` | yes |  | BCP 47 locale for Intl.NumberFormat, e.g. 'en-GB' or 'de-DE'. Explicit so server and browser agree. |
| `format` | `Intl.NumberFormatOptions` | no | `{}` | Formatting for the value: compact notation, currency, percent, unit style or fraction digits. |
| `unit` | `string` | no |  | Unit text after the value when the format does not supply one, e.g. 'days'. Set smaller and muted. |
| `change` | `MetricDisplayChange` | no |  | { value, period, goodWhen?, format? }. value is the signed change (a fraction by default: 0.12 is 12%); period follows it, e.g. 'vs last month'; goodWhen 'up' or 'down' picks the positive or negative tone and adds 'better' or 'worse' after the period, omitted it stays muted with no judgement; format overrides the percent default. |
| `source` | `string` | no |  | Footnote naming the data source or date, set small and muted under the comparison. |
| `size` | 'md' \| 'lg' | no | `'md'` | md sets the value at 36 px for dashboards and cards; lg at 48 to 60 px for stat rows on landing pages. |
| `text` | `Partial<MetricDisplayText>` | no |  | Replaces the English words: up ('Up {value}'), down ('Down {value}'), none ('No change'), better and worse (the judgement after the period when goodWhen is set) and unavailable ('Not available'). |
| `class` | `string` | no |  | Extra classes on the root, for margins or a width. |

## Customization

Set format, unit, size and text through props. Retone with five --metric-display-\* variables: ink and muted follow the surrounding text colour by default, so most pages need none of them.

- Content: label says what is measured, change.period says against what ('vs September', 'vs the same week last year'). Keep the footnote to the source and date.
- Numbers: pass Intl options in format, e.g. { notation: 'compact', maximumFractionDigits: 1 }, { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 } or { style: 'unit', unit: 'minute', unitDisplay: 'short' }. The currency symbol is raised, and suffixes such as %, M or min are set at half size.
- Good news: set goodWhen to 'down' for wait times, error rates, costs and churn, so a fall takes the positive tone. Leave it out for a figure that is neither good nor bad. The change then ends with 'better' or 'worse', so the judgement is in words as well as tone.
- Colours: --metric-display-positive and --metric-display-negative colour the change; --metric-display-accent colours the value (defaults to the ink); --metric-display-ink and --metric-display-muted set the text tones.
- Dark or tinted page: set the band's text colour and the component follows it. Then set --metric-display-positive to a light green such as #6ee7b7 and --metric-display-negative to #fca5a5, which clear 4.5:1 on zinc-950.
- Stat row: place several metrics in a grid or flex row with a hairline between them (border-s on all but the first) and the same size on each.
- Other languages: pass the locale and the text prop, e.g. { up: 'Plus {value}', down: 'Minus {value}', none: 'Unverändert', better: 'besser', worse: 'schlechter', unavailable: 'Nicht verfügbar' }.

| Token | Public CSS variable |
| --- | --- |
| `accent` | `--metric-display-accent` |
| `ink` | `--metric-display-ink` |
| `muted` | `--metric-display-muted` |
| `positive` | `--metric-display-positive` |
| `negative` | `--metric-display-negative` |

## Accessibility

- Renders a description list: the label is the dt and the value the first dd, so a screen reader reads the label first ('Weekly active members, 12,400') even though the value is drawn above it.
- The visible value is hidden from assistive technology and replaced by one visually hidden phrase in standard notation, so compact '1.2M' is read as '1,240,000' and the unit follows it.
- The change states its direction in words ('Up 8.2% vs Q2') and, when goodWhen is set, its judgement too ('· better'). The arrow and middle dot are aria-hidden and the tone repeats what the words say, so nothing relies on colour alone.
- The default positive and negative tones and the muted text clear 4.5:1 on white and zinc-50. Check your own tones when you set them.
- Not interactive and not a live region. If the figure updates while the page is open, announcing it is the consumer's job.
- Pass the page's language through text when it is not English, so the direction words match the lang of the page.

Known limitations:

- The better and worse words are English by default; pass them through text with the other phrases when the page is in another language.

## License

- Declared source: MIT
- Default license approval is pending. See https://pagesugar.com/docs/license.

## Source

- Palette: Inherited colour (`neutral`)
- Entry: `MetricDisplay.svelte`
- Suggested directory: `src/lib/components/metric-display-01`
- Files: 1
- Artifact digest: `sha256-ea66d7b2d1eaf68936394c7220dc667d2f2f5be8cfbc0f3adb2c5d6d69cd090d`

Paths below are relative to the suggested directory. Copy the files as they are;
they import nothing from this site.

#### `MetricDisplay.svelte`

Role: entry · 13610 bytes · SHA-256 `c858f1d67c6b3b65d1665335f4e4796195cc437971c131ba5de359cb10920e2f`

```svelte
<script lang="ts" module>
	export type MetricDisplaySize = 'md' | 'lg';
	export type MetricDisplayDirection = 'up' | 'down' | 'none';

	export interface MetricDisplayChange {
		/**
		 * The change since the comparison period. With the default percent format this is a
		 * fraction: 0.12 is "12%" and -0.3 is "30%". The sign sets the direction.
		 */
		value: number;
		/** What the change is measured against, e.g. 'vs last month'. Shown after the change. */
		period: string;
		/** Which direction is good news. Omitted, the change is shown in the muted tone. */
		goodWhen?: 'up' | 'down';
		/** Format for the change's magnitude. Defaults to a percent with up to one decimal. */
		format?: Intl.NumberFormatOptions;
	}

	/** The words around the numbers, so another language can replace them. */
	export interface MetricDisplayText {
		/** Change upwards; {value} is the formatted magnitude. */
		up: string;
		/** Change downwards; {value} is the formatted magnitude. */
		down: string;
		/** A change that rounds to zero. */
		none: string;
		/** Said after the period when the change is good news, so the judgement is not colour alone. */
		better: string;
		/** Said after the period when the change is bad news. */
		worse: string;
		/** Read in place of a missing value, which is drawn as a dash. */
		unavailable: string;
	}
</script>

<script lang="ts">
	interface Props {
		/** What the number measures, e.g. 'Weekly active members'. */
		label: string;
		/** The raw number. null, NaN or Infinity renders a dash and no change. */
		value: number | null;
		/** BCP 47 locale for Intl.NumberFormat, e.g. 'en-GB'. Explicit so server and browser agree. */
		locale: string;
		/** Formatting for the value: compact notation, currency, percent, fraction digits. */
		format?: Intl.NumberFormatOptions;
		/** Unit text after the value when the format does not supply one, e.g. 'days'. */
		unit?: string;
		/** The comparison: a signed change, its period and which direction is good. */
		change?: MetricDisplayChange;
		/** Footnote naming the data source or date. */
		source?: string;
		/** md for dashboards and cards; lg for stat rows on landing pages and case studies. */
		size?: MetricDisplaySize;
		/** Replaces the English direction words, the better/worse judgement and the unavailable text. */
		text?: Partial<MetricDisplayText>;
		/** Extra classes on the root, for margins or a width. */
		class?: string;
	}

	let {
		label,
		value,
		locale,
		format = {},
		unit,
		change,
		source,
		size = 'md',
		text = {},
		class: className
	}: Props = $props();

	const TEXT: MetricDisplayText = {
		up: 'Up {value}',
		down: 'Down {value}',
		none: 'No change',
		better: 'better',
		worse: 'worse',
		unavailable: 'Not available'
	};

	/*
	 * One entry per size, as complete class strings. The value leads; the label sits close under
	 * it so the pair reads as one, and the comparison stands three times as far off, so it reads
	 * as context for the pair rather than the next line of a stack.
	 */
	const SIZES: Record<
		MetricDisplaySize,
		{ value: string; unit: string; label: string; change: string; source: string }
	> = {
		md: {
			value: 'text-4xl/[1.1] tracking-[-0.02em]',
			unit: 'text-base/none',
			label: 'mt-2 text-sm/5',
			change: 'mt-6 text-sm/5',
			source: 'mt-3'
		},
		lg: {
			value: 'text-5xl/[1.1] tracking-[-0.03em] sm:text-6xl/[1.1]',
			unit: 'text-lg/none sm:text-xl/none',
			label: 'mt-3 text-base/6',
			change: 'mt-9 text-base/6',
			source: 'mt-4'
		}
	};

	/* Tone per meaning: good news, bad news, or neither. Complete class names, never built. */
	const TONES = {
		good: '[--_tone:var(--_positive)]',
		bad: '[--_tone:var(--_negative)]',
		neutral: '[--_tone:var(--_muted)]'
	} as const;

	type Part = { type: string; value: string };

	/** Locale used when the one passed is not supported, so the host's default never leaks in. */
	const FALLBACK_LOCALE = 'en';

	/*
	 * ICU versions disagree about which space goes where (U+0020, U+00A0, U+2009, U+202F), the
	 * commonest reason a server and a browser print different bytes for one number. Every space
	 * in a literal is pinned to a no-break space and every space group separator to a narrow one.
	 */
	const NBSP = String.fromCharCode(0x00a0);
	const NARROW_NBSP = String.fromCharCode(0x202f);
	const SPACES = new RegExp(`[ ${NBSP}${String.fromCharCode(0x2009)}${NARROW_NBSP}]`, 'g');
	const normalise = (part: Part): Part => {
		if (part.type === 'group') return { ...part, value: part.value.replace(SPACES, NARROW_NBSP) };
		if (part.type === 'literal') return { ...part, value: part.value.replace(SPACES, NBSP) };
		return part;
	};

	/** The locale actually used: the one passed when the engine supports it, otherwise 'en'. */
	const resolvedLocale = $derived.by(() => {
		try {
			return Intl.NumberFormat.supportedLocalesOf(locale).length ? locale : FALLBACK_LOCALE;
		} catch {
			return FALLBACK_LOCALE;
		}
	});

	const toParts = (n: number, options: Intl.NumberFormatOptions): Part[] => {
		try {
			return new Intl.NumberFormat(resolvedLocale, options).formatToParts(n).map(normalise);
		} catch {
			// Options the engine rejects (an unknown currency code): show the plain number.
			return [{ type: 'integer', value: String(n) }];
		}
	};
	const join = (parts: Part[]) => parts.map((part) => part.value).join('');

	/** Digits and their punctuation are the numeral; everything else is an affix set smaller. */
	const NUMERAL = new Set(['integer', 'group', 'decimal', 'fraction', 'minusSign', 'plusSign']);

	const available = $derived(typeof value === 'number' && Number.isFinite(value));
	/* Each phrase falls back on its own, so { up: undefined } still reads 'Up'. */
	const words = $derived({
		up: text.up ?? TEXT.up,
		down: text.down ?? TEXT.down,
		none: text.none ?? TEXT.none,
		better: text.better ?? TEXT.better,
		worse: text.worse ?? TEXT.worse,
		unavailable: text.unavailable ?? TEXT.unavailable
	});

	/*
	 * The formatted value runs in its locale's own direction, so on a right-to-left page an
	 * en-US "-12,400" keeps its minus in front, and an Arabic figure keeps its order on an
	 * English page.
	 */
	const RTL_LANGUAGES = new Set([
		'ar',
		'he',
		'iw',
		'fa',
		'ur',
		'ps',
		'yi',
		'dv',
		'ckb',
		'sd',
		'ug'
	]);
	const valueDir = $derived(
		RTL_LANGUAGES.has(resolvedLocale.split('-')[0].toLowerCase()) ? 'rtl' : 'ltr'
	);

	/*
	 * The visible value is split into the numeral and its affixes. A currency symbol before the
	 * digits is raised to their cap height; a percent sign, a compact suffix (M, 万) or a symbol
	 * after them sits on the baseline. Spaces touching an affix take its size.
	 */
	const parts = $derived(available ? toParts(value as number, format) : []);
	const first = $derived(parts.findIndex((part) => NUMERAL.has(part.type)));
	/*
	 * A unit the format writes itself ("mins", "ms") is set like the unit prop: smaller, muted and
	 * untracked, with the space before it at the same size, so "6 mins" and "3.4 days" match.
	 */
	const isUnit = (index: number) =>
		parts[index]?.type === 'unit' ||
		(parts[index]?.type === 'literal' &&
			(parts[index - 1]?.type === 'unit' || parts[index + 1]?.type === 'unit'));
	const placed = $derived(
		parts.map((part, index) => ({
			...part,
			role: NUMERAL.has(part.type)
				? 'numeral'
				: isUnit(index)
					? 'unit'
					: index < first
						? 'prefix'
						: 'suffix'
		}))
	);

	/*
	 * What a screen reader hears: the whole number in standard notation, so compact "1.2M" is read
	 * as "1,240,000", followed by the unit. The rounding that suited the compact form is dropped.
	 */
	const COMPACT_ONLY = new Set([
		'notation',
		'compactDisplay',
		'minimumFractionDigits',
		'maximumFractionDigits',
		'minimumSignificantDigits',
		'maximumSignificantDigits',
		'roundingPriority',
		'roundingIncrement'
	]);
	const full = $derived.by(() => {
		if (!available) return words.unavailable;
		// A currency keeps its own minor units; anything else keeps every digit it was given.
		const options: Intl.NumberFormatOptions =
			format.notation === 'compact'
				? {
						...Object.fromEntries(Object.entries(format).filter(([key]) => !COMPACT_ONLY.has(key))),
						...(format.style === 'currency' ? {} : { maximumFractionDigits: 20 })
					}
				: format;
		return [join(toParts(value as number, options)), unit].filter(Boolean).join(' ');
	});

	/*
	 * The change's magnitude is formatted without a sign, because the word carries the direction.
	 * Its direction follows the rounded figure: a change that prints as 0% says "No change".
	 */
	const comparison = $derived.by(() => {
		if (!change || !available || !Number.isFinite(change.value)) return undefined;
		const own = change.format ?? {};
		const digits = Object.keys(own).some((key) => /Digits$|^rounding/.test(key));
		const options: Intl.NumberFormatOptions = {
			style: 'percent',
			// The one-decimal default only applies when the consumer set no precision of their own.
			...(digits ? {} : { maximumFractionDigits: 1 }),
			...own,
			signDisplay: 'never'
		};
		const magnitude = join(toParts(Math.abs(change.value), options));
		const zero = magnitude === join(toParts(0, options));
		const direction: MetricDisplayDirection = zero ? 'none' : change.value > 0 ? 'up' : 'down';
		const tone =
			direction === 'none' || !change.goodWhen
				? 'neutral'
				: direction === change.goodWhen
					? 'good'
					: 'bad';
		return {
			direction,
			tone: tone as keyof typeof TONES,
			phrase: words[direction].replace('{value}', magnitude),
			period: change.period,
			// The tone's meaning in words: green or red alone would leave it to colour.
			judgement: tone === 'good' ? words.better : tone === 'bad' ? words.worse : ''
		};
	});

	const step = $derived(SIZES[size] ?? SIZES.md);
</script>

<dl class={['metric-display flex min-w-0 flex-col text-(--_ink)', className]} data-size={size}>
	<dt
		class={[
			'metric-display__label min-w-0 font-medium text-pretty break-words text-(--_muted)',
			step.label
		]}
	>
		{label}
	</dt>
	<dd class="order-first flex min-w-0 flex-wrap items-baseline gap-x-2">
		<span class="sr-only">{full}</span>
		<span
			class={[
				'metric-display__value font-semibold whitespace-nowrap tabular-nums [unicode-bidi:isolate] rtl:tracking-normal',
				available ? 'text-(--_accent)' : 'text-(--_muted)',
				step.value
			]}
			lang={resolvedLocale}
			dir={valueDir}
			aria-hidden="true"
			>{#if available}{#each placed as part, index (index)}{#if part.role === 'prefix'}<span
							class="align-[0.72em] text-[0.5em]">{part.value}</span
						>{:else if part.role === 'unit'}<span
							class={['font-medium tracking-normal text-(--_muted)', step.unit]}>{part.value}</span
						>{:else if part.role === 'suffix'}<span class="text-[0.5em]">{part.value}</span
						>{:else}{part.value}{/if}{/each}{:else}<span class="font-normal">&mdash;</span
				>{/if}</span
		>
		{#if unit && available}
			<span
				class={['min-w-0 font-medium break-words text-(--_muted)', step.unit]}
				aria-hidden="true">{unit}</span
			>
		{/if}
	</dd>
	{#if comparison}
		<dd
			class={['flex min-w-0 items-start gap-2', TONES[comparison.tone], step.change]}
			data-direction={comparison.direction}
			data-tone={comparison.tone}
		>
			<span class="flex h-lh shrink-0 items-center text-(--_tone)" aria-hidden="true">
				<svg
					class="size-4"
					viewBox="0 0 16 16"
					fill="none"
					stroke="currentColor"
					stroke-width="1.75"
					stroke-linecap="round"
					stroke-linejoin="round"
				>
					{#if comparison.direction === 'up'}
						<path d="M8 13V3M4 7l4-4 4 4" />
					{:else if comparison.direction === 'down'}
						<path d="M8 3v10M4 9l4 4 4-4" />
					{:else}
						<path d="M3.5 8h9" />
					{/if}
				</svg>
			</span>
			<span class="min-w-0 text-pretty break-words">
				<span class="font-medium whitespace-nowrap text-(--_tone) tabular-nums"
					>{comparison.phrase}</span
				>
				<!-- The judgement is held to the period's last word, so it never wraps onto a line alone. -->
				<span class="text-(--_muted)">{comparison.period}</span>{#if comparison.judgement}<span
						class="whitespace-nowrap text-(--_tone)"
						>&nbsp;<span aria-hidden="true">&middot;</span> {comparison.judgement}</span
					>{/if}
			</span>
		</dd>
	{/if}
	{#if source}
		<dd
			class={[
				'metric-display__source min-w-0 text-xs/4 text-pretty break-words text-(--_muted)',
				step.source
			]}
		>
			{source}
		</dd>
	{/if}
</dl>

<style>
	/*
	 * Public tokens: set them on the component or any ancestor. Ink defaults to the surrounding
	 * text colour and is set on the root, so currentColor below is the ink: the value takes the
	 * accent, else the ink, and the muted tone is a 72% mix of the ink. The metric then sits on a
	 * white card, a cream page or a dark band unchanged. Positive
	 * and negative colour the change by whether it is good news; both clear 4.5:1 on white.
	 */
	.metric-display {
		--_ink: var(--metric-display-ink, currentColor);
		--_accent: var(--metric-display-accent, currentColor);
		--_muted: var(--metric-display-muted, color-mix(in srgb, currentColor 72%, transparent));
		--_positive: var(--metric-display-positive, #047857);
		--_negative: var(--metric-display-negative, #b91c1c);
	}

	/* Japanese labels and footnotes break at phrases, Korean at spaces, never mid-word. */
	.metric-display__label:lang(ko),
	.metric-display__source:lang(ko) {
		word-break: keep-all;
	}

	@supports (word-break: auto-phrase) {
		.metric-display__label:lang(ja),
		.metric-display__source:lang(ja) {
			word-break: auto-phrase;
		}
	}
</style>
```

## Artifacts

### Inherited colour (`neutral`) (default)

- Artifact digest: `sha256-ea66d7b2d1eaf68936394c7220dc667d2f2f5be8cfbc0f3adb2c5d6d69cd090d`
- Entry: `MetricDisplay.svelte`
- Receipt: https://pagesugar.com/artifacts/cmp_metric_display_01/1.0.0/neutral/sha256-ea66d7b2d1eaf68936394c7220dc667d2f2f5be8cfbc0f3adb2c5d6d69cd090d/manifest.json
- Bundle: https://pagesugar.com/artifacts/cmp_metric_display_01/1.0.0/neutral/sha256-ea66d7b2d1eaf68936394c7220dc667d2f2f5be8cfbc0f3adb2c5d6d69cd090d/bundle.zip (8706 bytes, sha256 `3fd316e0689f527c3f65f51569737118475d6d1233bae1480e7090ab32ba37fb`)

Files:

- `MetricDisplay.svelte` (entry, 13610 bytes): https://pagesugar.com/artifacts/cmp_metric_display_01/1.0.0/neutral/sha256-ea66d7b2d1eaf68936394c7220dc667d2f2f5be8cfbc0f3adb2c5d6d69cd090d/source/MetricDisplay.svelte
