feat(website): rebuild dashboard visual faithful to twenty-front (#22218)

Rebuilds the product-feature **DashboardVisual** to mirror
twenty-front's dashboard widgets, with colors traced to twenty-front's
actual source rather than eyeballed.

## Widgets
- **Bar — "Deals by month"**: single-series `blue8` (twenty-front's
`GRAPH_DEFAULT_COLOR`), dashed `4 4` gridlines, value labels, nice
rounded Y-ticks. Replaces the old stacked bar (stacked bars aren't used
in twenty-front).
- **Donut — "Deals by stage"**: the real opportunity pipeline
(New/Screening/Meeting/Proposal/Customer), each segment colored by that
stage option's own color from the metadata
(`red/purple/sky/turquoise/yellow`), with a center total and a paginated
horizontal legend.
- **KPIs**: big-number cards (Revenue YTD / Avg deal size / Win rate).

## Responsive — `mediaUp('md')`
The dashboard is the full-width spotlight tile, whose frame is short
below md and grows to 420px at md+. So the layout keys off md: below it
collapses to **2-up KPIs + a full-width bar** (donut and the 3rd KPI
hidden, smaller breadcrumb), and the spotlight frame's mobile min-height
is bumped so the bar has room; at md+ the full 3-KPI + side-by-side
layout returns. The donut caps at its size and shrinks with its
container.

## Notes
- Follows the `product-feature` conventions (per-visual folder,
one-export-per-file, split types).
- `Tiles.tsx`: one-line spotlight mobile min-height bump (only the
dashboard uses the spotlight tile).
- typecheck + lint + build all green.

<img width="1148" height="638" alt="image"
src="https://github.com/user-attachments/assets/1eb8f120-88c2-45d1-adf9-ec00abe11006"
/>
This commit is contained in:
Abdullah.
2026-06-26 20:57:21 +05:00
committed by GitHub
parent 8a257e0de3
commit fea2b8736f
10 changed files with 342 additions and 185 deletions
@@ -1,10 +1,13 @@
'use client';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { styled } from '@linaria/react';
import { IconDotsVertical, IconLayoutDashboard } from '@tabler/icons-react';
import { THEME_LIGHT } from 'twenty-ui/theme';
import { previewFontSize } from '@/app-preview/preview-font-size';
import { mediaUp } from '@/tokens';
import { BarChart } from './components/BarChart';
import { DonutChart } from './components/DonutChart';
@@ -45,13 +48,21 @@ const BreadcrumbIcon = styled.span`
const BreadcrumbText = styled.span`
color: ${THEME_LIGHT.font.color.secondary};
font-size: ${previewFontSize(THEME_LIGHT.font.size.md)};
font-size: ${previewFontSize(THEME_LIGHT.font.size.sm)};
${mediaUp('md')} {
font-size: ${previewFontSize(THEME_LIGHT.font.size.md)};
}
`;
const BreadcrumbCurrent = styled.span`
color: ${THEME_LIGHT.font.color.primary};
font-size: ${previewFontSize(THEME_LIGHT.font.size.md)};
font-size: ${previewFontSize(THEME_LIGHT.font.size.sm)};
font-weight: ${THEME_LIGHT.font.weight.medium};
${mediaUp('md')} {
font-size: ${previewFontSize(THEME_LIGHT.font.size.md)};
}
`;
const Actions = styled.div`
@@ -83,15 +94,40 @@ const SearchChip = styled.span`
const Body = styled.div`
background-color: ${THEME_LIGHT.background.primary};
display: grid;
display: flex;
flex: 1;
flex-direction: column;
gap: 8px;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: auto 1fr;
min-height: 0;
padding: 12px;
`;
const KpiRow = styled.div`
display: grid;
flex-shrink: 0;
gap: 8px;
grid-template-columns: repeat(2, 1fr);
& > :nth-child(3) {
display: none;
}
${mediaUp('md')} {
grid-template-columns: repeat(3, 1fr);
& > :nth-child(3) {
display: flex;
}
}
`;
const ChartRow = styled.div`
display: flex;
flex: 1;
gap: 8px;
min-height: 0;
`;
const WidgetCard = styled.div`
background-color: ${THEME_LIGHT.background.secondary};
border: 1px solid ${THEME_LIGHT.border.color.light};
@@ -99,15 +135,24 @@ const WidgetCard = styled.div`
display: flex;
flex-direction: column;
min-height: 0;
min-width: 0;
padding: 8px;
&[data-cell='bar'] {
grid-column: 1 / 3;
grid-row: 2;
flex: 1;
}
&[data-cell='donut'] {
grid-column: 3;
grid-row: 2;
display: none;
}
${mediaUp('md')} {
&[data-cell='bar'] {
flex: 1.4;
}
&[data-cell='donut'] {
display: flex;
flex: 1;
}
}
`;
@@ -122,6 +167,8 @@ const WidgetHeader = styled.span`
`;
export function DashboardVisual({ active }: { active: boolean }) {
const { i18n } = useLingui();
return (
<Window>
<Topbar>
@@ -129,8 +176,10 @@ export function DashboardVisual({ active }: { active: boolean }) {
<BreadcrumbIcon>
<IconLayoutDashboard size={16} stroke={2} />
</BreadcrumbIcon>
<BreadcrumbText>Dashboards /</BreadcrumbText>
<BreadcrumbCurrent>Sales performance</BreadcrumbCurrent>
<BreadcrumbText>{i18n._(msg`Dashboards`)} /</BreadcrumbText>
<BreadcrumbCurrent>
{i18n._(msg`Sales performance`)}
</BreadcrumbCurrent>
</Breadcrumb>
<Actions>
<IconButton>
@@ -140,28 +189,23 @@ export function DashboardVisual({ active }: { active: boolean }) {
</Actions>
</Topbar>
<Body>
{DASHBOARD_VISUAL_DATA.kpis.map((kpi) => (
<WidgetCard key={kpi.label}>
<WidgetHeader>{kpi.label}</WidgetHeader>
<KpiCard kpi={kpi} />
<KpiRow>
{DASHBOARD_VISUAL_DATA.kpis.map((kpi) => (
<WidgetCard key={kpi.id}>
<KpiCard kpi={kpi} />
</WidgetCard>
))}
</KpiRow>
<ChartRow>
<WidgetCard data-cell="bar">
<WidgetHeader>{i18n._(msg`Deals by month`)}</WidgetHeader>
<BarChart active={active} months={DASHBOARD_VISUAL_DATA.byMonth} />
</WidgetCard>
))}
<WidgetCard data-cell="bar">
<WidgetHeader>Deals by month</WidgetHeader>
<BarChart
active={active}
months={DASHBOARD_VISUAL_DATA.byMonth}
stages={DASHBOARD_VISUAL_DATA.stages}
/>
</WidgetCard>
<WidgetCard data-cell="donut">
<WidgetHeader>Deals by stage</WidgetHeader>
<DonutChart
active={active}
stages={DASHBOARD_VISUAL_DATA.stages}
values={DASHBOARD_VISUAL_DATA.stageTotals}
/>
</WidgetCard>
<WidgetCard data-cell="donut">
<WidgetHeader>{i18n._(msg`Deals by stage`)}</WidgetHeader>
<DonutChart active={active} stages={DASHBOARD_VISUAL_DATA.stages} />
</WidgetCard>
</ChartRow>
</Body>
</Window>
);
@@ -1,5 +1,6 @@
'use client';
import { useLingui } from '@lingui/react';
import { styled } from '@linaria/react';
import { THEME_LIGHT } from 'twenty-ui/theme';
@@ -7,10 +8,10 @@ import { previewFontSize } from '@/app-preview/preview-font-size';
import { EASING } from '@/tokens';
import { type DashboardMonth } from '../types/dashboard-month';
import { type DashboardStage } from '../types/dashboard-stage';
const Y_TICK_COUNT = 5;
const X_LABEL_ROW_HEIGHT = 18;
const TARGET_TICK_COUNT = 4;
const BAR_MAX_WIDTH = 26;
const Root = styled.div`
display: flex;
@@ -48,12 +49,25 @@ const Bars = styled.div`
min-width: 0;
`;
const PlotArea = styled.div`
flex: 1;
min-height: 0;
position: relative;
`;
const GridLine = styled.div`
border-top: 1px dashed ${THEME_LIGHT.border.color.light};
left: 0;
position: absolute;
right: 0;
`;
const BarsRow = styled.div`
align-items: flex-end;
display: flex;
flex: 1;
gap: 6px;
min-height: 0;
inset: 0;
position: absolute;
`;
const BarColumn = styled.div`
@@ -62,41 +76,35 @@ const BarColumn = styled.div`
flex: 1;
height: 100%;
justify-content: center;
position: relative;
`;
const Stack = styled.div`
border-radius: 3px 3px 0 0;
display: flex;
flex-direction: column-reverse;
min-height: 2px;
overflow: hidden;
const Bar = styled.div`
background-color: ${THEME_LIGHT.color.blue8};
border-radius: ${THEME_LIGHT.border.radius.sm} ${THEME_LIGHT.border.radius.sm}
0 0;
max-width: ${BAR_MAX_WIDTH}px;
transform-origin: bottom;
transition:
transform 0.8s ${EASING.standard},
filter 0.15s ease;
width: 68%;
width: 58%;
&:hover {
filter: brightness(1.08);
}
`;
const Segment = styled.div`
flex-shrink: 0;
width: 100%;
&[data-tone='blue'] {
background-color: ${THEME_LIGHT.color.blue8};
}
&[data-tone='purple'] {
background-color: ${THEME_LIGHT.color.purple8};
}
&[data-tone='turquoise'] {
background-color: ${THEME_LIGHT.color.turquoise8};
}
&[data-tone='orange'] {
background-color: ${THEME_LIGHT.color.orange8};
}
const ValueLabel = styled.span`
color: ${THEME_LIGHT.font.color.light};
font-size: ${previewFontSize(THEME_LIGHT.font.size.xs)};
font-variant-numeric: tabular-nums;
left: 0;
position: absolute;
right: 0;
text-align: center;
transform: translateY(-3px);
transition: opacity 0.4s ${EASING.standard};
`;
const XLabels = styled.div`
@@ -113,67 +121,83 @@ const XLabel = styled.span`
text-align: center;
`;
function getNiceScale(maxValue: number): { maxTick: number; ticks: number[] } {
if (!(maxValue > 0)) {
return { maxTick: 1, ticks: [0, 1] };
}
const rawStep = maxValue / TARGET_TICK_COUNT;
const magnitude = 10 ** Math.floor(Math.log10(rawStep));
const normalized = rawStep / magnitude;
const niceNormalized =
normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 5 ? 5 : 10;
const step = niceNormalized * magnitude;
const tickCount = Math.ceil(maxValue / step);
return {
maxTick: step * tickCount,
ticks: Array.from({ length: tickCount + 1 }, (_, index) => index * step),
};
}
export function BarChart({
active,
months,
stages,
}: {
active: boolean;
months: DashboardMonth[];
stages: DashboardStage[];
}) {
const monthlyTotals = months.map((month) =>
month.values.reduce((sum, value) => sum + value, 0),
);
const tickStep = Math.ceil(
(Math.max(...monthlyTotals) * 1.05) / Y_TICK_COUNT,
);
const topTick = tickStep * Y_TICK_COUNT;
const yTicks = Array.from(
{ length: Y_TICK_COUNT + 1 },
(_, tickNumber) => tickNumber * tickStep,
);
const { i18n } = useLingui();
const maxValue = Math.max(...months.map((month) => month.value));
const { maxTick, ticks } = getNiceScale(maxValue);
return (
<Root>
<Plot>
<YAxis>
{yTicks.toReversed().map((tick) => (
{ticks.toReversed().map((tick) => (
<YLabel key={tick}>{tick}</YLabel>
))}
</YAxis>
<Bars>
<BarsRow>
{months.map((month, columnNumber) => {
const total = monthlyTotals[columnNumber];
return (
<BarColumn key={month.label}>
<Stack
style={{
height: `${(total / topTick) * 100}%`,
transform: active ? 'scaleY(1)' : 'scaleY(0)',
transitionDelay: active
? `${columnNumber * 50}ms`
: '0ms',
}}
>
{stages.map((stage, stageNumber) => (
<Segment
key={stage.label}
data-tone={stage.tone}
style={{
height: `${(month.values[stageNumber] / total) * 100}%`,
}}
/>
))}
</Stack>
</BarColumn>
);
})}
</BarsRow>
<PlotArea>
{ticks.map((tick) => (
<GridLine
key={tick}
style={{ bottom: `${(tick / maxTick) * 100}%` }}
/>
))}
<BarsRow>
{months.map((month, columnNumber) => {
const heightPercent = (month.value / maxTick) * 100;
return (
<BarColumn key={month.id}>
<ValueLabel
style={{
bottom: `${heightPercent}%`,
opacity: active ? 1 : 0,
transitionDelay: active
? `${300 + columnNumber * 50}ms`
: '0ms',
}}
>
{month.value}
</ValueLabel>
<Bar
style={{
height: `${heightPercent}%`,
transform: active ? 'scaleY(1)' : 'scaleY(0)',
transitionDelay: active
? `${columnNumber * 50}ms`
: '0ms',
}}
/>
</BarColumn>
);
})}
</BarsRow>
</PlotArea>
<XLabels>
{months.map((month) => (
<XLabel key={month.label}>{month.label}</XLabel>
<XLabel key={month.id}>{i18n._(month.label)}</XLabel>
))}
</XLabels>
</Bars>
@@ -1,6 +1,10 @@
'use client';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { styled } from '@linaria/react';
import { IconChevronLeft, IconChevronRight } from '@tabler/icons-react';
import { useState } from 'react';
import { THEME_LIGHT } from 'twenty-ui/theme';
import { previewFontSize } from '@/app-preview/preview-font-size';
@@ -8,33 +12,41 @@ import { EASING } from '@/tokens';
import { type DashboardStage } from '../types/dashboard-stage';
const SIZE = 96;
const STROKE = 16;
const SIZE = 160;
const STROKE = 18;
const RADIUS = (SIZE - STROKE) / 2;
const CENTER = SIZE / 2;
const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
const GAP = CIRCUMFERENCE * 0.008;
const LEGEND_PAGE_SIZE = 3;
const Root = styled.div`
align-items: center;
display: flex;
flex: 1;
flex-direction: column;
gap: 14px;
gap: 16px;
justify-content: center;
min-height: 0;
`;
const ChartArea = styled.div`
flex-shrink: 0;
aspect-ratio: 1;
max-height: 100%;
max-width: ${SIZE}px;
min-height: 0;
position: relative;
width: 100%;
`;
const Ring = styled.svg`
display: block;
height: 100%;
transform-origin: center;
transition:
opacity 0.5s ${EASING.standard},
transform 0.5s ${EASING.standard};
width: 100%;
`;
const Slice = styled.circle`
@@ -46,17 +58,20 @@ const Slice = styled.circle`
filter: brightness(1.1);
}
&[data-tone='blue'] {
stroke: ${THEME_LIGHT.color.blue8};
&[data-tone='red'] {
stroke: ${THEME_LIGHT.color.red8};
}
&[data-tone='purple'] {
stroke: ${THEME_LIGHT.color.purple8};
}
&[data-tone='sky'] {
stroke: ${THEME_LIGHT.color.sky8};
}
&[data-tone='turquoise'] {
stroke: ${THEME_LIGHT.color.turquoise8};
}
&[data-tone='orange'] {
stroke: ${THEME_LIGHT.color.orange8};
&[data-tone='yellow'] {
stroke: ${THEME_LIGHT.color.yellow8};
}
`;
@@ -78,113 +93,159 @@ const CenterValue = styled.span`
`;
const CenterLabel = styled.span`
color: ${THEME_LIGHT.font.color.light};
color: ${THEME_LIGHT.font.color.tertiary};
font-size: ${previewFontSize(THEME_LIGHT.font.size.xs)};
margin-top: 2px;
`;
const Legend = styled.div`
align-items: center;
display: flex;
flex-direction: column;
gap: 6px;
gap: 48px;
overflow: hidden;
width: 100%;
`;
const LegendRow = styled.div`
const Pager = styled.div`
align-items: center;
display: flex;
gap: 6px;
flex-shrink: 0;
gap: 2px;
`;
const LegendDot = styled.span`
const PagerArrow = styled.span`
align-items: center;
color: ${THEME_LIGHT.font.color.light};
cursor: pointer;
display: inline-flex;
transition: color 0.15s ease;
&:hover {
color: ${THEME_LIGHT.font.color.secondary};
}
`;
const PagerText = styled.span`
color: ${THEME_LIGHT.font.color.light};
font-size: ${previewFontSize(THEME_LIGHT.font.size.sm)};
font-variant-numeric: tabular-nums;
`;
const Item = styled.div`
align-items: center;
display: flex;
flex-shrink: 0;
gap: 5px;
`;
const ItemDot = styled.span`
border-radius: ${THEME_LIGHT.border.radius.xs};
flex-shrink: 0;
height: 8px;
width: 8px;
height: 9px;
width: 9px;
&[data-tone='blue'] {
background-color: ${THEME_LIGHT.color.blue8};
&[data-tone='red'] {
background-color: ${THEME_LIGHT.color.red8};
}
&[data-tone='purple'] {
background-color: ${THEME_LIGHT.color.purple8};
}
&[data-tone='sky'] {
background-color: ${THEME_LIGHT.color.sky8};
}
&[data-tone='turquoise'] {
background-color: ${THEME_LIGHT.color.turquoise8};
}
&[data-tone='orange'] {
background-color: ${THEME_LIGHT.color.orange8};
&[data-tone='yellow'] {
background-color: ${THEME_LIGHT.color.yellow8};
}
`;
const LegendLabel = styled.span`
color: ${THEME_LIGHT.font.color.secondary};
flex: 1;
font-size: ${previewFontSize(THEME_LIGHT.font.size.md)};
`;
const LegendValue = styled.span`
color: ${THEME_LIGHT.font.color.tertiary};
font-size: ${previewFontSize(THEME_LIGHT.font.size.md)};
font-variant-numeric: tabular-nums;
const ItemLabel = styled.span`
color: ${THEME_LIGHT.font.color.primary};
font-size: ${previewFontSize(THEME_LIGHT.font.size.sm)};
font-weight: ${THEME_LIGHT.font.weight.medium};
white-space: nowrap;
`;
export function DonutChart({
active,
stages,
values,
}: {
active: boolean;
stages: DashboardStage[];
values: number[];
}) {
const total = values.reduce((sum, value) => sum + value, 0);
const { i18n } = useLingui();
const [page, setPage] = useState(0);
const total = stages.reduce((sum, stage) => sum + stage.value, 0);
const pageCount = Math.ceil(stages.length / LEGEND_PAGE_SIZE);
const visibleStages = stages.slice(
page * LEGEND_PAGE_SIZE,
page * LEGEND_PAGE_SIZE + LEGEND_PAGE_SIZE,
);
let cumulative = 0;
const slices = stages.map((stage, stageNumber) => {
const value = values[stageNumber];
const arc = (value / total) * CIRCUMFERENCE;
const startAngle = -90 + (cumulative / total) * 360;
cumulative += value;
return { arc, stage, startAngle, value };
const slices = stages.map((stage) => {
const arc = total > 0 ? (stage.value / total) * CIRCUMFERENCE : 0;
const startAngle = -90 + (total > 0 ? cumulative / total : 0) * 360;
cumulative += stage.value;
return { arc, stage, startAngle };
});
return (
<Root>
<ChartArea>
<Ring
height={SIZE}
style={{
opacity: active ? 1 : 0,
transform: active ? 'scale(1)' : 'scale(0.85)',
}}
viewBox={`0 0 ${SIZE} ${SIZE}`}
width={SIZE}
>
{slices.map(({ arc, stage, startAngle }) => (
<Slice
key={stage.label}
cx={CENTER}
cy={CENTER}
data-tone={stage.tone}
r={RADIUS}
strokeDasharray={`${arc} ${CIRCUMFERENCE - arc}`}
strokeWidth={STROKE}
transform={`rotate(${startAngle} ${CENTER} ${CENTER})`}
/>
))}
{slices.map(({ arc, stage, startAngle }) => {
const visibleArc = stage.value > 0 ? Math.max(arc - GAP, 0.5) : 0;
return (
<Slice
key={stage.id}
cx={CENTER}
cy={CENTER}
data-tone={stage.tone}
r={RADIUS}
strokeDasharray={`${visibleArc} ${CIRCUMFERENCE - visibleArc}`}
strokeWidth={STROKE}
transform={`rotate(${startAngle} ${CENTER} ${CENTER})`}
/>
);
})}
</Ring>
<Center>
<CenterValue>{total}</CenterValue>
<CenterLabel>Deals</CenterLabel>
<CenterLabel>{i18n._(msg`Total`)}</CenterLabel>
</Center>
</ChartArea>
<Legend>
{slices.map(({ stage, value }) => (
<LegendRow key={stage.label}>
<LegendDot data-tone={stage.tone} />
<LegendLabel>{stage.label}</LegendLabel>
<LegendValue>{value}</LegendValue>
</LegendRow>
<Pager>
<PagerArrow
onClick={() =>
setPage((current) => (current - 1 + pageCount) % pageCount)
}
>
<IconChevronLeft size={14} stroke={1.8} />
</PagerArrow>
<PagerText>
{page + 1}/{pageCount}
</PagerText>
<PagerArrow
onClick={() => setPage((current) => (current + 1) % pageCount)}
>
<IconChevronRight size={14} stroke={1.8} />
</PagerArrow>
</Pager>
{visibleStages.map((stage) => (
<Item key={stage.id}>
<ItemDot data-tone={stage.tone} />
<ItemLabel>{i18n._(stage.label)}</ItemLabel>
</Item>
))}
</Legend>
</Root>
@@ -1,3 +1,4 @@
import { useLingui } from '@lingui/react';
import { styled } from '@linaria/react';
import { IconTrendingDown, IconTrendingUp } from '@tabler/icons-react';
import { THEME_LIGHT } from 'twenty-ui/theme';
@@ -15,6 +16,11 @@ const Body = styled.div`
justify-content: center;
`;
const Label = styled.span`
color: ${THEME_LIGHT.font.color.secondary};
font-size: ${previewFontSize(THEME_LIGHT.font.size.md)};
`;
const Value = styled.span`
color: ${THEME_LIGHT.font.color.primary};
font-size: ${previewFontSize(THEME_LIGHT.font.size.xxl)};
@@ -42,13 +48,16 @@ const TrendIcon = styled.span`
`;
const TrendPercent = styled.span`
color: ${THEME_LIGHT.font.color.secondary};
color: ${THEME_LIGHT.font.color.light};
font-size: ${previewFontSize(THEME_LIGHT.font.size.xs)};
`;
export function KpiCard({ kpi }: { kpi: DashboardKpi }) {
const { i18n } = useLingui();
return (
<Body>
<Label>{i18n._(kpi.label)}</Label>
<Value>{kpi.value}</Value>
<Trend>
<TrendIcon data-direction={kpi.trendDirection}>
@@ -1,3 +1,5 @@
import { msg } from '@lingui/core/macro';
import { type DashboardKpi } from '../types/dashboard-kpi';
import { type DashboardMonth } from '../types/dashboard-month';
import { type DashboardStage } from '../types/dashboard-stage';
@@ -6,39 +8,41 @@ export const DASHBOARD_VISUAL_DATA: {
byMonth: DashboardMonth[];
kpis: DashboardKpi[];
stages: DashboardStage[];
stageTotals: number[];
} = {
stages: [
{ label: 'New', tone: 'blue' },
{ label: 'Qualified', tone: 'purple' },
{ label: 'Proposal', tone: 'turquoise' },
{ label: 'Won', tone: 'orange' },
{ id: 'new', label: msg`New`, tone: 'red', value: 47 },
{ id: 'screening', label: msg`Screening`, tone: 'purple', value: 34 },
{ id: 'meeting', label: msg`Meeting`, tone: 'sky', value: 27 },
{ id: 'proposal', label: msg`Proposal`, tone: 'turquoise', value: 20 },
{ id: 'customer', label: msg`Customer`, tone: 'yellow', value: 12 },
],
stageTotals: [47, 34, 27, 20],
byMonth: [
{ label: 'Jan', values: [7, 5, 4, 2] },
{ label: 'Feb', values: [9, 6, 5, 3] },
{ label: 'Mar', values: [8, 6, 4, 2] },
{ label: 'Apr', values: [12, 8, 6, 4] },
{ label: 'May', values: [15, 11, 8, 5] },
{ label: 'Jun', values: [13, 9, 7, 4] },
{ label: 'Jul', values: [17, 12, 9, 6] },
{ id: 'jan', label: msg`Jan`, value: 12 },
{ id: 'feb', label: msg`Feb`, value: 16 },
{ id: 'mar', label: msg`Mar`, value: 14 },
{ id: 'apr', label: msg`Apr`, value: 22 },
{ id: 'may', label: msg`May`, value: 27 },
{ id: 'jun', label: msg`Jun`, value: 24 },
{ id: 'jul', label: msg`Jul`, value: 31 },
],
kpis: [
{
label: 'Revenue (YTD)',
id: 'revenue',
label: msg`Revenue (YTD)`,
trendDirection: 'up',
trendPercent: 12,
value: '$1.2M',
},
{
label: 'Avg deal size',
id: 'avg-deal-size',
label: msg`Avg deal size`,
trendDirection: 'up',
trendPercent: 5,
value: '$9.4K',
},
{
label: 'Win rate',
id: 'win-rate',
label: msg`Win rate`,
trendDirection: 'down',
trendPercent: 3,
value: '34%',
@@ -1,5 +1,8 @@
import { type MessageDescriptor } from '@lingui/core';
export type DashboardKpi = {
label: string;
id: string;
label: MessageDescriptor;
trendDirection: 'down' | 'up';
trendPercent: number;
value: string;
@@ -1,4 +1,7 @@
import { type MessageDescriptor } from '@lingui/core';
export type DashboardMonth = {
label: string;
values: number[];
id: string;
label: MessageDescriptor;
value: number;
};
@@ -1 +1,6 @@
export type DashboardStageTone = 'blue' | 'orange' | 'purple' | 'turquoise';
export type DashboardStageTone =
| 'purple'
| 'red'
| 'sky'
| 'turquoise'
| 'yellow';
@@ -1,6 +1,10 @@
import { type MessageDescriptor } from '@lingui/core';
import { type DashboardStageTone } from './dashboard-stage-tone';
export type DashboardStage = {
label: string;
id: string;
label: MessageDescriptor;
tone: DashboardStageTone;
value: number;
};
@@ -74,7 +74,7 @@ const SpotlightVisual = styled.div`
border: 1px solid ${color('black-20')};
border-radius: ${radius(2)};
margin: ${spacing(4)};
min-height: 260px;
min-height: 300px;
overflow: hidden;
${mediaUp('md')} {