fix: add KPI tile widget type to the dashboard (#26)
CI / build-and-test (pull_request) Successful in 2m24s
CI / build-and-test (pull_request) Successful in 2m24s
Adds a non-chart dashboard widget: a large value with its label, an optional signed delta versus the previous period, and an optional sparkline. Four tiles now sit above the charts, 3x1 each. - KpiTile draws no surface of its own and fills the grid item's, styled from the --dialect-* token layer; the delta's up/down colors are new tokens rather than literals, so they follow the light/dark toggle. - Value and delta share one row: stacking them costs a line the tile does not have at its default height of one grid cell. - SparklineChart is a chrome-free line chart built on DialectTheme, so it shares the palette and font with the real charts. - ApexCharts' sparkline.enabled drops the axes on the initial render but updateOptions (how the theme toggle recolors a chart) brings the y-axis labels back; they are hidden in CSS, which reaches the chart SVG since it renders into light DOM. The registry registration the issue asks for is left out: #21 is blocked, so there is no widget picker to register with yet. The tiles are wired into DashboardView the same way the chart widgets are, and their values come from the translation bundle until the data service (#20) lands. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0124BiJikbhbiEsNfJxdWM69
This commit is contained in:
@@ -11,6 +11,10 @@
|
||||
--dialect-ink: light-dark(#14171f, #e6eaf0);
|
||||
--dialect-border: light-dark(#e6eaf0, #2b303b);
|
||||
--dialect-radius: 14px;
|
||||
/* Delta direction (KpiTile). Lightened in dark mode so they keep contrast
|
||||
against the dark surface instead of going muddy. */
|
||||
--dialect-positive: light-dark(#1f9d63, #45c98a);
|
||||
--dialect-negative: light-dark(#d1453b, #f0736a);
|
||||
/* light-dark() only takes 2 args, so each shadow layer gets its own light-dark()
|
||||
call and the layers are joined below (var() is a text substitution, so the
|
||||
comma-separated shadow list still parses correctly). */
|
||||
@@ -123,6 +127,108 @@ vaadin-app-layout::part(content) {
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
|
||||
/* KpiTile (components/KpiTile.java): a one-row widget, so everything is sized
|
||||
to survive a 120px grid cell — the sparkline is the only flexible part and
|
||||
collapses first. */
|
||||
.dialect-kpi {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* border-box, or the padding is added on top of the 100% and the tile ends
|
||||
up taller than the grid item it is supposed to fill. */
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
gap: 2px;
|
||||
padding: 12px 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Value and delta on one line, baseline-aligned, so a one-cell-high tile still
|
||||
has room for the sparkline underneath. */
|
||||
.dialect-kpi__figure {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dialect-kpi__label {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: uppercase;
|
||||
color: var(--dialect-ink);
|
||||
opacity: 0.6;
|
||||
/* Long labels shorten rather than wrapping into a second line, which at
|
||||
h=1 would push the value out of the tile. */
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.dialect-kpi__value {
|
||||
font-size: clamp(1.4rem, 4cqw, 2rem);
|
||||
font-weight: 700;
|
||||
line-height: 1.15;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--dialect-ink);
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Never shrinks: a truncated delta would be unreadable, so the value gives way
|
||||
first (it ellipsises). */
|
||||
.dialect-kpi__delta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: none;
|
||||
gap: 6px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dialect-kpi__delta--up {
|
||||
color: var(--dialect-positive);
|
||||
}
|
||||
|
||||
.dialect-kpi__delta--down {
|
||||
color: var(--dialect-negative);
|
||||
}
|
||||
|
||||
.dialect-kpi__delta--flat {
|
||||
color: var(--dialect-ink);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* ApexCharts' `sparkline.enabled` drops the axes on the initial render, but a
|
||||
later updateOptions — which is how the theme toggle recolors a chart — brings
|
||||
the y-axis labels back, clipped to a smudge at the tile's left edge. Hide
|
||||
them for good; the chart renders into light DOM, so this rule reaches its SVG
|
||||
(see components/apex-chart.ts). */
|
||||
apex-chart.dialect-sparkline .apexcharts-yaxis,
|
||||
apex-chart.dialect-sparkline .apexcharts-xaxis {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* The only part allowed to shrink: at h=1 there is no room left for it and it
|
||||
collapses to nothing, at h≥2 it takes the slack. */
|
||||
.dialect-kpi__sparkline {
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* The tile draws no surface of its own — inside a grid item it fills
|
||||
grid-stack-item-content, which already provides background, radius and
|
||||
shadow. This also gives the value's `cqw` font size a container to resolve
|
||||
against, so a narrow tile scales its number down instead of clipping it. */
|
||||
.grid-stack-item-content > .dialect-kpi {
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
/* Secondary text (e.g. the layout status line in GridStackView). */
|
||||
.dialect-muted {
|
||||
color: var(--dialect-ink);
|
||||
|
||||
@@ -32,6 +32,15 @@ gridstack.widgetText=Frei platzierbare Karte.
|
||||
gridstack.dragHandle=Verschieben
|
||||
gridstack.close=Schließen
|
||||
|
||||
kpi.revenueTotal=Gesamtumsatz
|
||||
kpi.revenueTotal.value=1,24 Mio. €
|
||||
kpi.openOrders=Offene Vorgänge
|
||||
kpi.openOrders.value=38
|
||||
kpi.newCustomers=Neue Kunden
|
||||
kpi.newCustomers.value=112
|
||||
kpi.averageOrderValue=Ø Bestellwert
|
||||
kpi.averageOrderValue.value=486 €
|
||||
|
||||
chart.revenueSeries=Umsatz 2026
|
||||
chart.pointClick=Serie {0}, Punkt {1}
|
||||
chart.sliceClick=Slice {0}
|
||||
|
||||
@@ -32,6 +32,15 @@ gridstack.widgetText=Freely placeable card.
|
||||
gridstack.dragHandle=Move
|
||||
gridstack.close=Close
|
||||
|
||||
kpi.revenueTotal=Total revenue
|
||||
kpi.revenueTotal.value=€1.24M
|
||||
kpi.openOrders=Open orders
|
||||
kpi.openOrders.value=38
|
||||
kpi.newCustomers=New customers
|
||||
kpi.newCustomers.value=112
|
||||
kpi.averageOrderValue=Avg. order value
|
||||
kpi.averageOrderValue.value=€486
|
||||
|
||||
chart.revenueSeries=Revenue 2026
|
||||
chart.pointClick=Series {0}, point {1}
|
||||
chart.sliceClick=Slice {0}
|
||||
|
||||
@@ -32,6 +32,15 @@ gridstack.widgetText=Tarjeta de colocación libre.
|
||||
gridstack.dragHandle=Mover
|
||||
gridstack.close=Cerrar
|
||||
|
||||
kpi.revenueTotal=Ingresos totales
|
||||
kpi.revenueTotal.value=1,24 M€
|
||||
kpi.openOrders=Pedidos abiertos
|
||||
kpi.openOrders.value=38
|
||||
kpi.newCustomers=Nuevos clientes
|
||||
kpi.newCustomers.value=112
|
||||
kpi.averageOrderValue=Valor medio del pedido
|
||||
kpi.averageOrderValue.value=486 €
|
||||
|
||||
chart.revenueSeries=Ingresos 2026
|
||||
chart.pointClick=Serie {0}, punto {1}
|
||||
chart.sliceClick=Sector {0}
|
||||
|
||||
Reference in New Issue
Block a user