From 44f0ff3c3dad7f18dff9e05bc4e33afcb61e3600 Mon Sep 17 00:00:00 2001 From: CarterPerez-dev Date: Sun, 3 May 2026 07:12:20 -0400 Subject: [PATCH] feat(monitor/frontend): shared Panel shell + KPI + Sparkline + StaleIndicator primitives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shared shell every operator panel uses. 28px header strip with UPPERCASE letter-spaced title (--type-label, --fg-2), optional muted subtitle (--fg-3), StaleIndicator dot, and an external-link "raw" icon → upstream source. Body fills remaining space and scrolls internally. Border-bottom 1px --fg-4 separates panels in the column. Primitives in shared/: KPI — value (mono, --type-num-l, tabular-nums, --fg-1) + label (UPPERCASE, --type-label, --fg-3). No card wrapper, no rounded corner, no background. The number IS the unit. Sparkline — d3-scale + d3-array driven inline SVG. Single stroke in currentColor, no axes / no fill / no animation. Used only when the shape IS the insight (per ethos). Returns null with <2 points so empty panels don't ship a flat line as a placeholder. StaleIndicator — 6px square dot. --ok green (fresh), --amber (stale, breached threshold), --fg-4 muted (unknown / no data yet). Title attribute carries last update UTC time for hover context. RawLink not extracted yet — Panel inlines the icon since one raw link per panel is the rule. Extract when a panel needs more. Adds d3-scale 4.0.2 + d3-array 3.2.4 + their @types — small d3 modules (not the full umbrella) per spec §10.3 to keep bundle lean. Plan 5 Task 13 Design QA: panel header 28px, title UPPERCASE letter-spaced, sparkline pure stroke (no axes / fill), stale dot 6px, no border-radius anywhere, no hover scale. --- .../frontend/package.json | 4 ++ .../frontend/pnpm-lock.yaml | 29 +++++++++ .../src/pages/panels/Panel.module.scss | 60 +++++++++++++++++++ .../frontend/src/pages/panels/Panel.tsx | 55 +++++++++++++++++ .../src/pages/panels/shared/KPI.module.scss | 24 ++++++++ .../frontend/src/pages/panels/shared/KPI.tsx | 21 +++++++ .../src/pages/panels/shared/Sparkline.tsx | 49 +++++++++++++++ .../panels/shared/StaleIndicator.module.scss | 21 +++++++ .../pages/panels/shared/StaleIndicator.tsx | 29 +++++++++ 9 files changed, 292 insertions(+) create mode 100644 PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/Panel.module.scss create mode 100644 PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/Panel.tsx create mode 100644 PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/shared/KPI.module.scss create mode 100644 PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/shared/KPI.tsx create mode 100644 PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/shared/Sparkline.tsx create mode 100644 PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/shared/StaleIndicator.module.scss create mode 100644 PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/shared/StaleIndicator.tsx diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/package.json b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/package.json index b2d6c562..e61195bb 100644 --- a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/package.json +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/package.json @@ -19,6 +19,8 @@ "dependencies": { "@tanstack/react-query": "^5.90.12", "axios": "^1.13.0", + "d3-array": "^3.2.4", + "d3-scale": "^4.0.2", "react": "^19.2.1", "react-dom": "^19.2.0", "react-error-boundary": "^6.0.0", @@ -34,6 +36,8 @@ "devDependencies": { "@biomejs/biome": "^2.3.8", "@tanstack/react-query-devtools": "^5.91.1", + "@types/d3-array": "^3.2.2", + "@types/d3-scale": "^4.0.9", "@types/node": "^24.10.2", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/pnpm-lock.yaml b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/pnpm-lock.yaml index 0d03bdab..54596339 100644 --- a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/pnpm-lock.yaml +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/pnpm-lock.yaml @@ -17,6 +17,12 @@ importers: axios: specifier: ^1.13.0 version: 1.13.2 + d3-array: + specifier: ^3.2.4 + version: 3.2.4 + d3-scale: + specifier: ^4.0.2 + version: 4.0.2 react: specifier: ^19.2.1 version: 19.2.1 @@ -57,6 +63,12 @@ importers: '@tanstack/react-query-devtools': specifier: ^5.91.1 version: 5.91.1(@tanstack/react-query@5.90.12(react@19.2.1))(react@19.2.1) + '@types/d3-array': + specifier: ^3.2.2 + version: 3.2.2 + '@types/d3-scale': + specifier: ^4.0.9 + version: 4.0.9 '@types/node': specifier: ^24.10.2 version: 24.10.2 @@ -572,6 +584,15 @@ packages: '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} @@ -2264,6 +2285,14 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/d3-array@3.2.2': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-time@3.0.4': {} + '@types/deep-eql@4.0.2': {} '@types/estree@1.0.8': {} diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/Panel.module.scss b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/Panel.module.scss new file mode 100644 index 00000000..1187f67e --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/Panel.module.scss @@ -0,0 +1,60 @@ +// ©AngelaMos | 2026 +// Panel.module.scss + +.panel { + display: flex; + flex-direction: column; + border-bottom: 1px solid var(--fg-4); + min-height: 0; +} + +.head { + display: flex; + align-items: center; + justify-content: space-between; + height: 28px; + padding: 0 8px; + background: var(--bg); + border-bottom: 1px solid var(--fg-4); +} + +.title { + font-size: var(--type-label); + letter-spacing: var(--letter-spacing-label); + text-transform: uppercase; + color: var(--fg-2); +} + +.subtitle { + color: var(--fg-3); +} + +.meta { + display: inline-flex; + align-items: center; + gap: 8px; + color: var(--fg-3); +} + +.rawLink { + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--fg-3); + + &:hover { + color: var(--fg-1); + } + + &:focus-visible { + outline: 1px solid var(--fg-2); + outline-offset: 1px; + } +} + +.body { + flex: 1; + overflow: auto; + padding: 4px 0; + min-height: 0; +} diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/Panel.tsx b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/Panel.tsx new file mode 100644 index 00000000..d9485b86 --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/Panel.tsx @@ -0,0 +1,55 @@ +// ©AngelaMos | 2026 +// Panel.tsx + +import type { ReactNode } from 'react' +import { FiExternalLink } from 'react-icons/fi' +import styles from './Panel.module.scss' +import { StaleIndicator } from './shared/StaleIndicator' + +export interface PanelProps { + title: string + subtitle?: string + rawHref?: string + rawLabel?: string + isStale?: boolean + lastTickAt?: number + children: ReactNode +} + +export function Panel({ + title, + subtitle, + rawHref, + rawLabel, + isStale, + lastTickAt, + children, +}: PanelProps): React.ReactElement { + return ( +
+
+ + {title} + {subtitle && · {subtitle}} + + + + {rawHref && ( + + + + )} + +
+
{children}
+
+ ) +} + +Panel.displayName = 'Panel' diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/shared/KPI.module.scss b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/shared/KPI.module.scss new file mode 100644 index 00000000..f97a0951 --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/shared/KPI.module.scss @@ -0,0 +1,24 @@ +// ©AngelaMos | 2026 +// KPI.module.scss + +.kpi { + display: flex; + flex-direction: column; + gap: 2px; + padding: 4px 8px; +} + +.value { + font-family: var(--font-mono); + font-size: var(--type-num-l); + font-variant-numeric: tabular-nums; + color: var(--fg-1); + line-height: 1; +} + +.label { + font-size: var(--type-label); + letter-spacing: var(--letter-spacing-label); + text-transform: uppercase; + color: var(--fg-3); +} diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/shared/KPI.tsx b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/shared/KPI.tsx new file mode 100644 index 00000000..34ed09e0 --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/shared/KPI.tsx @@ -0,0 +1,21 @@ +// ©AngelaMos | 2026 +// KPI.tsx + +import type { ReactNode } from 'react' +import styles from './KPI.module.scss' + +export interface KPIProps { + label: string + value: ReactNode +} + +export function KPI({ label, value }: KPIProps): React.ReactElement { + return ( +
+
{value}
+
{label}
+
+ ) +} + +KPI.displayName = 'KPI' diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/shared/Sparkline.tsx b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/shared/Sparkline.tsx new file mode 100644 index 00000000..df64c136 --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/shared/Sparkline.tsx @@ -0,0 +1,49 @@ +// ©AngelaMos | 2026 +// Sparkline.tsx + +import { extent } from 'd3-array' +import { scaleLinear } from 'd3-scale' + +const DEFAULT_WIDTH = 80 +const DEFAULT_HEIGHT = 16 +const STROKE_WIDTH = 1 +const MIN_DATA_POINTS = 2 + +export interface SparklineProps { + data: number[] + width?: number + height?: number +} + +export function Sparkline({ + data, + width = DEFAULT_WIDTH, + height = DEFAULT_HEIGHT, +}: SparklineProps): React.ReactElement | null { + if (data.length < MIN_DATA_POINTS) return null + const [min, max] = extent(data) + if (min === undefined || max === undefined) return null + + const x = scaleLinear() + .domain([0, data.length - 1]) + .range([0, width]) + const y = scaleLinear().domain([min, max]).range([height, 0]) + + const path = data + .map((v, i) => `${i === 0 ? 'M' : 'L'}${x(i)},${y(v)}`) + .join('') + + return ( + + Trend sparkline + + + ) +} + +Sparkline.displayName = 'Sparkline' diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/shared/StaleIndicator.module.scss b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/shared/StaleIndicator.module.scss new file mode 100644 index 00000000..f2597471 --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/shared/StaleIndicator.module.scss @@ -0,0 +1,21 @@ +// ©AngelaMos | 2026 +// StaleIndicator.module.scss + +.dot { + display: inline-block; + width: 6px; + height: 6px; + flex-shrink: 0; +} + +.live { + background: var(--ok); +} + +.stale { + background: var(--amber); +} + +.unknown { + background: var(--fg-4); +} diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/shared/StaleIndicator.tsx b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/shared/StaleIndicator.tsx new file mode 100644 index 00000000..7b981905 --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/shared/StaleIndicator.tsx @@ -0,0 +1,29 @@ +// ©AngelaMos | 2026 +// StaleIndicator.tsx + +import styles from './StaleIndicator.module.scss' + +export interface StaleIndicatorProps { + stale?: boolean + lastTickAt?: number +} + +export function StaleIndicator({ + stale, + lastTickAt, +}: StaleIndicatorProps): React.ReactElement { + const className = + stale === undefined + ? `${styles.dot} ${styles.unknown}` + : stale + ? `${styles.dot} ${styles.stale}` + : `${styles.dot} ${styles.live}` + + const title = lastTickAt + ? `Last update ${new Date(lastTickAt).toISOString().slice(11, 19)} UTC` + : undefined + + return +} + +StaleIndicator.displayName = 'StaleIndicator'