Widget
Widget — единица дашборда. Шапка: рукоятка перетаскивания, значок акцентного цвета, заголовок, справа ghost-кнопки «развернуть/свернуть», «изменить», «закрыть». Тело в компактной ячейке — столбик: показатель и график. В развёрнутой (две ячейки) слева колонка side шириной --widget-side-w: число с плашкой сверху, Stat List прижат к низу; справа основное содержимое, между ними линия. Содержимое получает { expanded } и подстраивает состав: спарклайн в компактной, полный график с подписью в широкой.
Статус: experimental · в системе с 2026-09 · интерактивность: conditional (по конфигурации)
Когда использовать
- Дашборд рекламодателя и паблишера: расход, клики, конверсии, кампании по статусам.
- Настраиваемая сетка виджетов с перетаскиванием и удалением.
Не использовать:
- Секция формы — Card или Island.
- Один показатель без графика и действий — Kpi в Island.
Альтернативы:
Конверсии: компактный и широкий
Один виджет в двух размерах: спарклайн под числом и полный график с колонкой показателей.
<!--
🟢
Виджет «Conversions» в двух размерах (галерея: 1 cell / 2 cells):
компактный — число с плашкой в строке, заметка и график от нуля без осей;
широкий — колонка показателей слева и полный график с подписью справа.
Кнопка в шапке переключает размер.
-->
<script>
import { t } from "$lib/spec/demo-lang.svelte.js";
import ChartLine from "$lib/ui/ChartLine.svelte";
import Kpi from "$lib/ui/Kpi.svelte";
import StatList from "$lib/ui/StatList.svelte";
import Widget from "$lib/ui/Widget.svelte";
const labels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
const values = [80, 140, 110, 160, 190, 220, 240];
const stats = [
{ label: "CR", value: "2.66%" },
{ label: "CPA", value: "$2.76" },
{ label: t("Revenue"), value: "$8 940" },
];
let expanded = $state(false);
</script>
<div style="width: {expanded ? 760 : 400}px; transition: width var(--t-control) var(--ease);">
<Widget title={t("Conversions")} icon="Target" bind:expanded draggable editable closable>
{#snippet side()}
<Kpi label={t("Conversions · 7 days")} value="1 284" delta="+8.7%" trend="up" />
<StatList items={stats} />
{/snippet}
{#snippet children({ expanded: wide })}
{#if wide}
<span style="color: var(--c-kpi-note); font-size: var(--kpi-note-fs);">{t("All campaigns")}</span>
<ChartLine {values} {labels} color="var(--c-chart-2)" label={t("Конверсии по дням")} />
{:else}
<Kpi value="1 284" delta="+8.7%" trend="up" deltaInline note={t("CR 2.66%")} size="m" />
<ChartLine {values} {labels} color="var(--c-chart-2)" axis={false} grid={false} zero />
{/if}
{/snippet}
</Widget>
</div>
Кампании по статусам
Кольцо с легендой и ряд показателей; в широком виде — строки показателей справа.
<!--
🟢
Виджет «Campaigns summary» (галерея: 1 cell / 2 cells): кольцо со статусами
и легендой с полосами долей; в компактной ячейке под ними ряд показателей,
в широкой — кольцо крупнее и строки показателей справа от линии.
-->
<script>
import { t } from "$lib/spec/demo-lang.svelte.js";
import ChartDonut from "$lib/ui/ChartDonut.svelte";
import Legend from "$lib/ui/Legend.svelte";
import StatList from "$lib/ui/StatList.svelte";
import Widget from "$lib/ui/Widget.svelte";
const items = [
{ label: t("Active"), value: 6, share: 50, color: "var(--c-chart-4)" },
{ label: t("Moderated"), value: 3, share: 25, color: "var(--c-chart-3)" },
{ label: t("Blocked"), value: 2, share: 16.7, color: "var(--c-status-rejected)" },
{ label: t("Stopped"), value: 1, share: 8.3, color: "var(--c-chart-5)" },
];
const columns = [
{ label: t("Spend · 7d"), value: "$12 408" },
{ label: t("Avg CTR"), value: "2.41%" },
{ label: t("Active rate"), value: "50%" },
];
const rows = [...columns, { label: t("Avg CPA"), value: "$2.76" }];
let expanded = $state(false);
</script>
<div style="width: {expanded ? 760 : 400}px; transition: width var(--t-control) var(--ease);">
<Widget title={t("Campaigns summary")} icon="Monitor" bind:expanded draggable editable closable>
{#snippet children({ expanded: wide })}
<div style="display: flex; gap: var(--sp-2); align-items: center;">
<ChartDonut {items} size={wide ? "l" : "m"} totalLabel={t("Total")} label={t("Кампании по статусам")} />
<div style="flex: 1;"><Legend {items} bars /></div>
{#if wide}
<div style="flex: 1; padding-inline-start: var(--sp-3); border-left: var(--bw-1) solid var(--c-widget-divider);"><StatList items={rows} /></div>
{/if}
</div>
{#if !wide}
<StatList items={columns} layout="columns" />
{/if}
{/snippet}
</Widget>
</div>
Расход по форматам
Стопка столбцов с легендой; в широком виде — разбивка слева.
<!--
🟢
Виджет «Spend by format» (галерея): стопка столбцов с легендой; в
компактной ячейке число с плашкой в строке, в широкой — слева число и
разбивка по форматам, справа подпись и график.
-->
<script>
import { t } from "$lib/spec/demo-lang.svelte.js";
import ChartBars from "$lib/ui/ChartBars.svelte";
import Kpi from "$lib/ui/Kpi.svelte";
import Legend from "$lib/ui/Legend.svelte";
import StatList from "$lib/ui/StatList.svelte";
import Widget from "$lib/ui/Widget.svelte";
const labels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
const series = [
{ id: "push", name: "Push", color: "var(--c-chart-1)", values: [62, 128, 96, 188, 146, 220, 282] },
{ id: "inpage", name: "In-page", color: "var(--c-chart-2)", values: [38, 74, 54, 108, 84, 126, 162] },
{ id: "native", name: "Native", color: "var(--c-chart-3)", values: [20, 38, 30, 64, 50, 74, 96] },
];
const legend = series.map((s) => ({ label: s.name, color: s.color }));
const stats = [
{ label: "Push", value: "$1 922.00" },
{ label: "In-page", value: "$1 274.00" },
{ label: "Native", value: "$346.18" },
];
const money = (value) => `$${value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }).replace(/,/g, " ")}`;
let expanded = $state(true);
</script>
<div style="width: {expanded ? 760 : 400}px; transition: width var(--t-control) var(--ease);">
<Widget title={t("Spend by format")} icon="ChartBar" bind:expanded draggable editable closable>
{#snippet side()}
<Kpi label={t("Spend · 7 days")} value="$3 542.18" delta="+12.4%" trend="up" />
<StatList items={stats} />
{/snippet}
{#snippet children({ expanded: wide })}
{#if !wide}
<Kpi value="$3 542.18" delta="+12.4%" trend="up" deltaInline size="m" />
{:else}
<span style="color: var(--c-kpi-note); font-size: var(--kpi-note-fs);">{t("Daily spend by ad format")}</span>
{/if}
<Legend items={legend} layout="inline" />
<ChartBars {series} {labels} format={money} size={wide ? "m" : "s"} label={t("Расход по форматам")} />
{/snippet}
</Widget>
</div>
Страны и источники
Топ стран строками; источники — столбцы в компактной ячейке и две колонки с номерами в широкой.
<!--
🟢
Два виджета галереи: «Top countries» — страны строками с долями от общего;
«Campaign by source» — в компактной ячейке столбцы восьми источников без
оси, в широкой две колонки с номерами и колонка показателей слева.
-->
<script>
import { t } from "$lib/spec/demo-lang.svelte.js";
import ChartBars from "$lib/ui/ChartBars.svelte";
import ChartRanked from "$lib/ui/ChartRanked.svelte";
import Kpi from "$lib/ui/Kpi.svelte";
import StatList from "$lib/ui/StatList.svelte";
import Widget from "$lib/ui/Widget.svelte";
const countries = [
{ label: t("United States"), value: 1284.4, share: 41 },
{ label: t("Brazil"), value: 642.1, share: 21 },
{ label: t("India"), value: 486.6, share: 16 },
{ label: t("Indonesia"), value: 372.2, share: 12 },
{ label: t("Germany"), value: 318.8, share: 10 },
];
const sources = [
{ label: "src-40219 · Push", value: 82.4 },
{ label: "src-38855 · In-page", value: 64.1 },
{ label: "src-41022 · Push", value: 48.6 },
{ label: "src-39710 · Native", value: 39.2 },
{ label: "src-40988 · Push", value: 36.8 },
{ label: "src-37421 · In-page", value: 28.1 },
{ label: "src-41560 · Native", value: 14.9 },
{ label: "src-40033 · Push", value: 8.3 },
];
const money = (value) => `$${value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }).replace(/,/g, " ")}`;
let expanded = $state(true);
</script>
<div style="display: flex; flex-direction: column; gap: var(--sp-3);">
<div style="width: 400px;">
<Widget title={t("Top countries")} icon="Globe" expandable={false} draggable editable closable>
<Kpi layout="side" value="32" label={t("countries running")} size="m" />
<ChartRanked items={countries} format={money} label={t("Топ стран")} />
</Widget>
</div>
<div style="width: {expanded ? 880 : 400}px; transition: width var(--t-control) var(--ease);">
<Widget title={t("Campaign by source")} icon="ChartBarHorizontal" bind:expanded draggable editable closable>
{#snippet side()}
<Kpi label={t("Sources today")} value="8" delta={t("+2 new")} trend="neutral" />
<StatList items={[{ label: t("Top source share"), value: "26%" }, { label: t("Spend"), value: "$318.40" }]} />
{/snippet}
{#snippet children({ expanded: wide })}
{#if wide}
<ChartRanked items={sources} layout="stacked" columns={2} rank share={false} format={money} label={t("Источники за день")} />
{:else}
<Kpi value={t("8 sources")} delta={t("Today")} trend="neutral" deltaInline size="m" />
<ChartBars values={sources.map((s) => s.value)} labels={sources.map((s) => s.label)} axis={false} format={money} size="s" label={t("Источники за день")} />
{/if}
{/snippet}
</Widget>
</div>
</div>
Трафик по устройствам
Круг с легендой строками; в широком виде — круг xl, легенда с полосами долей и показатели справа.
<!--
🟢
Виджет «Traffic by device» (галерея: 1 cell / 2 cells): круг с легендой
строками; в широкой ячейке круг xl, легенда с полосами долей от общего и
показатели справа от линии.
-->
<script>
import { t } from "$lib/spec/demo-lang.svelte.js";
import ChartDonut from "$lib/ui/ChartDonut.svelte";
import Legend from "$lib/ui/Legend.svelte";
import StatList from "$lib/ui/StatList.svelte";
import Widget from "$lib/ui/Widget.svelte";
const items = [
{ label: "Android", value: 28_940, color: "var(--c-chart-1)" },
{ label: "iOS", value: 12_060, color: "var(--c-chart-2)" },
{ label: t("Desktop"), value: 5_210, color: "var(--c-chart-3)" },
{ label: t("Tablet"), value: 2_007, color: "var(--c-chart-5)" },
];
const total = items.reduce((sum, item) => sum + item.value, 0);
// Значения легенды как в макете: от 10 000 — тысячи с «K»; доля — от общего
const legend = items.map((item) => ({
...item,
value: item.value >= 10_000 ? `${Math.round(item.value / 1000)}K` : item.value,
share: (item.value / total) * 100,
}));
const stats = [
{ label: t("Clicks · 7d"), value: "48 217" },
{ label: t("Mobile share"), value: "85%" },
{ label: t("Best CTR"), value: t("Android · 2.8%") },
];
let expanded = $state(false);
</script>
<div style="width: {expanded ? 760 : 400}px; transition: width var(--t-control) var(--ease);">
<Widget title={t("Traffic by device")} icon="ChartPie" bind:expanded draggable editable closable>
{#snippet children({ expanded: wide })}
<div style="display: flex; gap: var(--sp-2); align-items: center;">
<ChartDonut {items} donut={false} size={wide ? "xl" : "m"} label={t("Трафик по устройствам")} />
<div style="flex: 1;"><Legend items={legend} bars={wide} /></div>
{#if wide}
<div style="flex: 1; padding-inline-start: var(--sp-3); border-left: var(--bw-1) solid var(--c-widget-divider);"><StatList items={stats} /></div>
{/if}
</div>
{/snippet}
</Widget>
</div>
Реферальная программа
Число участников сбоку, два месяца с полосами, спарклайн дохода.
<!--
🟢
Виджет «Referral program» (галерея): участники сбоку с плашкой, два
месяца ячейками с полосами Meter, в широком виде — доход за полгода
графиком с подписями месяцев.
-->
<script>
import { t } from "$lib/spec/demo-lang.svelte.js";
import ChartLine from "$lib/ui/ChartLine.svelte";
import Kpi from "$lib/ui/Kpi.svelte";
import Meter from "$lib/ui/Meter.svelte";
import StatList from "$lib/ui/StatList.svelte";
import Widget from "$lib/ui/Widget.svelte";
const months = ["Mar", "Apr", "May", "Jun", "Jul", "Aug"];
const income = [420, 610, 745, 880, 1050, 1284];
const money = (value) => `$${value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }).replace(/,/g, " ")}`;
let expanded = $state(true);
</script>
<div style="width: {expanded ? 800 : 400}px; transition: width var(--t-control) var(--ease);">
<Widget title={t("Referral program")} icon="UsersThree" bind:expanded draggable editable closable>
{#snippet side()}
<Kpi label={t("Participants")} value="48" delta={t("+6 this month")} trend="up" />
<StatList items={[{ label: t("Active · idle"), value: "38 · 10" }, { label: t("Reward rate"), value: "5%" }]} />
{/snippet}
{#snippet children({ expanded: wide })}
{#if !wide}
<Kpi layout="side" value="48" label={t("Participants")} note={t("38 active · 10 idle")} delta={t("+6 this month")} />
{/if}
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-3);">
<div style="display: flex; flex-direction: column; gap: var(--sp-1);">
<Kpi label={t("Last month")} value="$1 284.00" size="m" note={t("August · closed")} />
<Meter label={t("Last month")} value={78} valueText="$1 284.00" tone="default" />
</div>
<div style="display: flex; flex-direction: column; gap: var(--sp-1); padding-inline-start: var(--sp-3); border-left: var(--bw-1) solid var(--c-widget-divider);">
<Kpi label={wide ? t("This month · expected") : t("This month")} value="$1 640.00" size="m" note={t("$1 112 accrued")} />
<Meter label={t("This month")} value={68} valueText={t("$1 112 accrued")} tone="success" />
</div>
</div>
{#if wide}
<span style="color: var(--c-kpi-label); font-size: var(--kpi-label-fs); font-weight: var(--fw-medium); letter-spacing: var(--tracking-wide); text-transform: uppercase;">{t("Referral income · last 6 months")}</span>
<ChartLine values={income} labels={months} color="var(--c-chart-4)" format={money} size="s" label={t("Доход за полгода")} />
{/if}
{/snippet}
</Widget>
</div>