Add metric fill motion to infrastructure views

This commit is contained in:
rcourtman 2026-05-14 12:10:23 +01:00
parent a6c460daa0
commit be7c1639e0
10 changed files with 89 additions and 10 deletions

View file

@ -288,6 +288,13 @@ prompt explain the same operator-facing priority.
alert threshold tables, and Infrastructure Settings source-manager tables;
feature owners may own group content and behavior, but not duplicate the
subgroup band styling.
Shared progress and metric-fill motion belongs to the frontend primitive
CSS contract in `frontend-modern/src/index.css`. Generic progress bars must
keep the CSP-safe `ProgressBar` / `foreignObject` shape and use the shared
`.progress-fill-frame` and `.progress-fill` classes for width and color
transitions instead of inline styles or page-local animation wrappers. The
same global CSS owner must provide the `prefers-reduced-motion` disable path
for these fills so feature surfaces inherit one accessibility policy.
Shared primitives must not reintroduce app-shell monitored-system capacity
banners. Monitored-system grouping and ledger presentation belongs in the
owned settings surfaces, while commercial plan explanation belongs in

View file

@ -262,6 +262,13 @@ regression protection.
override resolution belongs to the alerts-owned activation store and
`frontend-modern/src/utils/metricThresholds.ts`, while the hot-path bar
models remain presentation-only consumers.
Metric-fill motion for these hot-path bars must stay CSS-owned and
transform-free for layout stability: `StackedDiskBar`, `StackedMemoryBar`,
and `EnhancedCPUBar` may attach the shared `.metric-fill-geometry` and
`.metric-fill-divider` classes to existing SVG geometry, but they must not
add per-row timers, animation signals, measurement loops, or page-local
motion state. The global frontend primitive CSS owns the easing,
color/geometry transition timing, and `prefers-reduced-motion` fallback.
23. Extend grouped workload row windowing, reveal-index clamping, overscan math, and per-group visible-slice derivation through `frontend-modern/src/components/Workloads/useGroupedTableWindowing.ts`, and extend viewport event wiring through `frontend-modern/src/components/Workloads/useWorkloadViewportSync.ts` rather than rebuilding scroll handlers, mounted-row budgets, viewport listeners, or group-slice math inside `frontend-modern/src/components/Workloads/useWorkloadsDerivedState.ts`
24. Extend Workloads shell rendering through `frontend-modern/src/components/Workloads/WorkloadsStateCards.tsx`, `frontend-modern/src/components/Workloads/WorkloadsTable.tsx`, and `frontend-modern/src/components/Workloads/WorkloadsStatsStrip.tsx` rather than accreting loading cards, workload table markup, or stats-strip presentation back into `frontend-modern/src/components/Workloads/WorkloadsSurface.tsx`
25. Extend workload table shell ownership through `frontend-modern/src/components/Workloads/WorkloadTableHeader.tsx` and `frontend-modern/src/components/Workloads/WorkloadPanel.tsx` rather than rebuilding sortable header markup, grouped node rows, row expansion, or guest-drawer rendering inside `frontend-modern/src/components/Workloads/WorkloadsTable.tsx`

View file

@ -144,6 +144,9 @@ describe('App architecture', () => {
expect(appStylesSource).toContain('--color-grouped-table-row-bg');
expect(appStylesSource).toContain('--color-grouped-table-row-bg: rgba(226, 232, 240, 0.72);');
expect(appStylesSource).toContain('--color-grouped-table-row-bg: rgba(51, 65, 85, 0.58);');
expect(appStylesSource).toContain('.progress-fill-frame');
expect(appStylesSource).toContain('.metric-fill-geometry');
expect(appStylesSource).toContain('@media (prefers-reduced-motion: reduce)');
expect(appStylesSource).not.toContain('--color-grouped-table-row-bg: theme(');
expect(appStylesSource).not.toContain('@keyframes pulse-brand-wordmark');
expect(appStylesSource).not.toContain('text-shadow');

View file

@ -27,6 +27,7 @@ export function EnhancedCPUBar(props: EnhancedCPUBarProps) {
>
<rect
data-enhanced-cpu-fill="true"
class="metric-fill-geometry"
x="0"
y="0"
width={barPercent()}

View file

@ -36,6 +36,7 @@ export function StackedDiskBar(props: StackedDiskBarProps) {
<>
<rect
data-stacked-disk-fill="segment"
class="metric-fill-geometry"
x={String(
presentation()
.segments.slice(0, idx())
@ -49,6 +50,7 @@ export function StackedDiskBar(props: StackedDiskBarProps) {
/>
<Show when={idx() < presentation().segments.length - 1}>
<line
class="metric-fill-divider"
x1={String(
presentation()
.segments.slice(0, idx() + 1)
@ -81,6 +83,7 @@ export function StackedDiskBar(props: StackedDiskBarProps) {
>
<rect
data-stacked-disk-fill="single"
class="metric-fill-geometry"
x="0"
y="0"
width={clampPercent(presentation().barPercent)}
@ -96,7 +99,10 @@ export function StackedDiskBar(props: StackedDiskBarProps) {
<span class="max-w-full min-w-0 whitespace-nowrap overflow-hidden text-ellipsis px-0.5 text-center">
<span>{presentation().displayLabel}</span>
<Show when={presentation().showMaxLabel}>
<span class="text-[8px] font-normal text-muted" title={presentation().maxLabelFull}>
<span
class="text-[8px] font-normal text-muted"
title={presentation().maxLabelFull}
>
{' '}
{presentation().maxLabelShort}
</span>
@ -130,7 +136,11 @@ export function StackedDiskBar(props: StackedDiskBarProps) {
</div>
}
>
<div class="w-full" onMouseEnter={state.handleMouseEnter} onMouseLeave={state.handleMouseLeave}>
<div
class="w-full"
onMouseEnter={state.handleMouseEnter}
onMouseLeave={state.handleMouseLeave}
>
<div class="flex items-stretch gap-1">
<For each={presentation().miniDisks}>
{(disk) => (
@ -147,6 +157,7 @@ export function StackedDiskBar(props: StackedDiskBarProps) {
>
<rect
data-stacked-disk-fill="mini"
class="metric-fill-geometry"
x="0"
y="0"
width={clampPercent(disk.percent)}
@ -164,11 +175,7 @@ export function StackedDiskBar(props: StackedDiskBarProps) {
</Show>
{/* Tooltip for disk breakdown */}
<TooltipPortal
when={state.tooltipVisible()}
x={state.tip.pos().x}
y={state.tip.pos().y}
>
<TooltipPortal when={state.tooltipVisible()} x={state.tip.pos().x} y={state.tip.pos().y}>
<div class="min-w-[140px]">
<div class="font-medium mb-1 text-slate-300 border-b border-border pb-1">
{presentation().tooltipTitle}
@ -199,6 +206,7 @@ export function StackedDiskBar(props: StackedDiskBarProps) {
>
<rect
data-stacked-disk-fill="tooltip"
class="metric-fill-geometry"
x="0"
y="0"
width={parsePercent(item.percent)}

View file

@ -11,7 +11,10 @@ export function StackedMemoryBar(props: StackedMemoryBarProps) {
String(Math.max(0, Math.min(leftPercent + widthPercent, 100)));
return (
<div ref={state.setContainerRef} class="metric-text w-full h-4 flex items-center justify-center">
<div
ref={state.setContainerRef}
class="metric-text w-full h-4 flex items-center justify-center"
>
<div
class="relative w-full h-full overflow-hidden bg-surface-hover rounded"
onMouseEnter={state.handleMouseEnter}
@ -28,6 +31,7 @@ export function StackedMemoryBar(props: StackedMemoryBarProps) {
<>
<rect
data-stacked-memory-segment="true"
class="metric-fill-geometry"
x={String(Math.max(0, Math.min(segment.leftPercent, 100)))}
y="0"
width={String(Math.max(0, segment.widthPercent))}
@ -37,6 +41,7 @@ export function StackedMemoryBar(props: StackedMemoryBarProps) {
/>
<Show when={idx() < presentation().segments.length - 1}>
<line
class="metric-fill-divider"
x1={segmentEdge(segment.leftPercent, segment.widthPercent)}
x2={segmentEdge(segment.leftPercent, segment.widthPercent)}
y1="0"
@ -51,6 +56,7 @@ export function StackedMemoryBar(props: StackedMemoryBarProps) {
<Show when={presentation().showSwapBar}>
<rect
data-stacked-memory-swap="true"
class="metric-fill-geometry"
x="0"
y="82"
width={swapBarWidth()}

View file

@ -931,6 +931,8 @@ describe('Workloads performance contract', () => {
it('keeps stacked disk bar runtime and derivations in canonical owners', () => {
expect(stackedDiskBarSource).toContain('useStackedDiskBarState');
expect(stackedDiskBarSource).toContain('metric-fill-geometry');
expect(stackedDiskBarSource).toContain('metric-fill-divider');
expect(stackedDiskBarSource).not.toContain('style={{');
expect(stackedDiskBarSource).not.toContain('style={');
expect(stackedDiskBarSource).not.toContain('const [containerWidth, setContainerWidth] =');
@ -947,6 +949,8 @@ describe('Workloads performance contract', () => {
it('keeps stacked memory bar runtime and derivations in canonical owners', () => {
expect(stackedMemoryBarSource).toContain('useStackedMemoryBarState');
expect(stackedMemoryBarSource).toContain('metric-fill-geometry');
expect(stackedMemoryBarSource).toContain('metric-fill-divider');
expect(stackedMemoryBarSource).not.toContain('style={{');
expect(stackedMemoryBarSource).not.toContain('style={');
expect(stackedMemoryBarSource).not.toContain('const [containerWidth, setContainerWidth] =');
@ -974,6 +978,7 @@ describe('Workloads performance contract', () => {
it('keeps enhanced CPU bar runtime and derivations in canonical owners', () => {
expect(enhancedCpuBarSource).toContain('useEnhancedCPUBarState');
expect(enhancedCpuBarSource).toContain('metric-fill-geometry');
expect(enhancedCpuBarSource).not.toContain('style={{');
expect(enhancedCpuBarSource).not.toContain('style={');
expect(enhancedCpuBarSource).not.toContain('const tip = useTooltip()');

View file

@ -30,7 +30,14 @@ export const ProgressBar: Component<ProgressBarProps> = (props) => {
preserveAspectRatio="none"
aria-hidden="true"
>
<foreignObject data-progress-fill="true" x="0" y="0" width={width()} height="100">
<foreignObject
data-progress-fill="true"
class="progress-fill-frame"
x="0"
y="0"
width={width()}
height="100"
>
<div class={`progress-fill h-full w-full ${props.fillClass ?? ''}`} />
</foreignObject>
</svg>

View file

@ -1222,8 +1222,11 @@ describe('shared primitive guardrails', () => {
it('keeps progress bars CSP-safe in the shared primitive owner', () => {
expect(progressBarSource).toContain('data-progress-fill');
expect(progressBarSource).toContain('foreignObject');
expect(progressBarSource).toContain('progress-fill-frame');
expect(progressBarSource).not.toContain('style={{');
expect(progressBarSource).not.toContain('style={');
expect(frontendIndexCssSource).toContain('.progress-fill-frame');
expect(frontendIndexCssSource).toContain('@media (prefers-reduced-motion: reduce)');
});
it('keeps search field on shell, runtime, and model owners', () => {

View file

@ -300,9 +300,41 @@
}
/* Progress bars */
.progress-fill-frame {
transition: width 320ms cubic-bezier(0.22, 1, 0.36, 1);
will-change: width;
}
.progress-fill {
@apply h-full;
transition: width 300ms ease-out;
transition:
width 320ms cubic-bezier(0.22, 1, 0.36, 1),
background-color 180ms ease-out;
}
.metric-fill-geometry {
transition:
x 320ms cubic-bezier(0.22, 1, 0.36, 1),
width 320ms cubic-bezier(0.22, 1, 0.36, 1),
fill 180ms ease-out;
will-change: x, width, fill;
}
.metric-fill-divider {
transition:
x1 320ms cubic-bezier(0.22, 1, 0.36, 1),
x2 320ms cubic-bezier(0.22, 1, 0.36, 1);
will-change: x1, x2;
}
@media (prefers-reduced-motion: reduce) {
.progress-fill-frame,
.progress-fill,
.metric-fill-geometry,
.metric-fill-divider {
transition: none;
will-change: auto;
}
}
/* Custom scrollbar - subtle styling */