mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-19 07:54:10 +00:00
Add metric fill motion to infrastructure views
This commit is contained in:
parent
a6c460daa0
commit
be7c1639e0
10 changed files with 89 additions and 10 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
|
|
|
|||
|
|
@ -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()');
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue