feat: add ui shell sections

This commit is contained in:
dev-agent
2026-04-28 16:15:24 +00:00
parent 99878bae4c
commit 5aac6d69c5
5 changed files with 364 additions and 70 deletions

View File

@@ -27,11 +27,6 @@ npm run lint
``` ```
## Current scope ## Current scope
- Minimal frontend shell only. - App shell slice for `timeline`, `audit feed`, and `environment summary`.
- No live API wiring in the baseline commit. - Local typed fixtures only. No live API wiring.
- `environment summary` stays mock-only until a worker-visible route exists in the current backend runtime. - `environment summary` stays explicitly mock-only until a worker-visible route exists in the current backend runtime.
## Next development slice
1. Add app shell sections for `timeline`, `audit feed`, and `environment summary`.
2. Introduce typed DTO/fixture layer from the approved first-slice contracts.
3. Keep `environment summary` behind mock data until the live route is available.

View File

@@ -1,7 +1,7 @@
:root { :root {
color: #e5eef7; color: #ebf1ea;
background: #0b1220; background: #101611;
font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-family: 'IBM Plex Sans', 'Segoe UI', sans-serif;
} }
body { body {
@@ -9,8 +9,9 @@ body {
min-width: 320px; min-width: 320px;
min-height: 100vh; min-height: 100vh;
background: background:
radial-gradient(circle at top, rgba(56, 189, 248, 0.18), transparent 35%), radial-gradient(circle at top left, rgba(201, 255, 148, 0.14), transparent 32%),
linear-gradient(180deg, #020617 0%, #0f172a 100%); radial-gradient(circle at right, rgba(102, 163, 255, 0.1), transparent 28%),
linear-gradient(180deg, #0b100c 0%, #161d17 100%);
} }
#root { #root {
@@ -18,23 +19,38 @@ body {
} }
.app-shell { .app-shell {
max-width: 1120px; max-width: 1180px;
margin: 0 auto; margin: 0 auto;
padding: 48px 24px 64px; padding: 40px 20px 56px;
} }
.hero-card, .hero-card,
.next-slice-card, .overview-strip article,
.status-grid article { .panel,
background: rgba(15, 23, 42, 0.8); .summary-card,
border: 1px solid rgba(148, 163, 184, 0.18); .timeline-item,
border-radius: 24px; .audit-item {
box-shadow: 0 20px 45px rgba(2, 6, 23, 0.35); background: rgba(18, 25, 20, 0.84);
border: 1px solid rgba(207, 234, 212, 0.1);
box-shadow: 0 18px 40px rgba(5, 8, 6, 0.3);
} }
.hero-card, .hero-card,
.next-slice-card { .panel,
.overview-strip article,
.summary-card,
.timeline-item,
.audit-item {
border-radius: 22px;
}
.hero-card {
display: grid;
grid-template-columns: minmax(0, 1.1fr) minmax(280px, 0.9fr);
gap: 24px;
align-items: end;
padding: 28px; padding: 28px;
margin-bottom: 20px;
} }
.eyebrow { .eyebrow {
@@ -42,52 +58,177 @@ body {
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.12em; letter-spacing: 0.12em;
font-size: 0.8rem; font-size: 0.8rem;
color: #38bdf8; color: #b8f36b;
} }
h1, h1,
h2, h2,
p, h3,
ul { p {
margin-top: 0; margin-top: 0;
} }
h1 { h1 {
font-size: clamp(2.4rem, 5vw, 4rem); font-size: clamp(2.4rem, 4vw, 3.5rem);
margin-bottom: 16px; margin-bottom: 0;
line-height: 0.95;
letter-spacing: -0.04em;
}
h2 {
margin-bottom: 0;
font-size: 1.2rem;
}
h3 {
margin-bottom: 8px;
font-size: 1rem;
} }
.lede { .lede {
font-size: 1.1rem; margin-bottom: 0;
align-self: stretch;
font-size: 1rem;
line-height: 1.7; line-height: 1.7;
max-width: 68ch; color: #c8d4c8;
color: #cbd5e1;
} }
.status-grid { .overview-strip {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 20px; gap: 16px;
margin: 28px 0; margin-bottom: 20px;
} }
.status-grid article { .overview-strip article {
padding: 18px 20px;
}
.kicker,
.section-label,
.item-meta {
display: inline-block;
margin: 0 0 8px;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: #93a393;
}
.overview-strip strong {
display: block;
font-size: 1rem;
color: #f5f8f4;
}
.content-grid {
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
gap: 20px;
}
.panel {
grid-column: span 4;
padding: 22px; padding: 22px;
} }
.status-grid h2, .panel-wide {
.next-slice-card h2 { grid-column: span 8;
margin-bottom: 12px;
font-size: 1.1rem;
} }
.status-grid p, .panel-heading {
.next-slice-card li { display: flex;
color: #cbd5e1; align-items: flex-start;
line-height: 1.6; justify-content: space-between;
gap: 16px;
margin-bottom: 18px;
} }
.next-slice-card ul { .panel-badge {
padding-left: 20px; border-radius: 999px;
padding: 7px 12px;
font-size: 0.8rem;
color: #d8f4af;
background: rgba(184, 243, 107, 0.12);
border: 1px solid rgba(184, 243, 107, 0.18);
}
.panel-badge.muted {
color: #c6d0c6;
background: rgba(198, 208, 198, 0.08);
border-color: rgba(198, 208, 198, 0.12);
}
.timeline-list,
.audit-list,
.summary-grid {
display: grid;
gap: 14px;
}
.timeline-list {
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
.timeline-item,
.audit-item,
.summary-card {
padding: 18px;
}
.timeline-item h3,
.audit-item h3,
.summary-card h3 {
color: #f5f8f4;
}
.timeline-item p,
.audit-item p,
.summary-card p {
margin-bottom: 0; margin-bottom: 0;
line-height: 1.55;
color: #c8d4c8;
}
.timeline-stable {
border-color: rgba(157, 255, 172, 0.16);
}
.timeline-active {
border-color: rgba(255, 211, 122, 0.18);
}
.timeline-attention {
border-color: rgba(255, 143, 143, 0.18);
}
.audit-target {
color: #b8f36b;
}
.summary-grid {
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
@media (max-width: 900px) {
.hero-card,
.content-grid {
grid-template-columns: 1fr;
}
.panel,
.panel-wide {
grid-column: auto;
}
}
@media (max-width: 640px) {
.app-shell {
padding-inline: 16px;
}
.hero-card,
.panel {
padding: 20px;
}
} }

View File

@@ -1,45 +1,101 @@
import './App.css' import './App.css'
import {
const nextSlice = [ auditEntries,
'Timeline block wired to approved DTO fixtures', environmentSummaryItems,
'Audit feed block wired to approved DTO fixtures', timelineEntries,
'Environment summary placeholder kept mock-only', } from './fixtures/dashboard'
]
function App() { function App() {
return ( return (
<main className="app-shell"> <main className="app-shell">
<section className="hero-card"> <header className="hero-card">
<p className="eyebrow">Mission Control / devopspanel</p> <div>
<h1>DevOps orchestration admin panel</h1> <p className="eyebrow">Mission Control / devopspanel</p>
<h1>Operations shell</h1>
</div>
<p className="lede"> <p className="lede">
Baseline frontend scaffold for the first implementation slice. This commit Local mock slice for shell structure only. Timeline, audit feed, and
intentionally sets the stack and shell contract before live integrations. environment summary are rendered from typed fixtures with no live API wiring.
</p> </p>
</section> </header>
<section className="status-grid"> <section className="overview-strip" aria-label="App slice overview">
<article> <article>
<h2>Baseline stack</h2> <span className="kicker">Stack</span>
<p>React + TypeScript + Vite</p> <strong>React + TypeScript + Vite</strong>
</article> </article>
<article> <article>
<h2>Current state</h2> <span className="kicker">Data mode</span>
<p>Scaffold only, no runtime data wiring yet</p> <strong>Local typed fixtures</strong>
</article> </article>
<article> <article>
<h2>Guardrail</h2> <span className="kicker">Guardrail</span>
<p>Environment summary remains mock-only until live worker-visible route exists</p> <strong>Environment summary is mock-only</strong>
</article> </article>
</section> </section>
<section className="next-slice-card"> <section className="content-grid">
<h2>Next development slice</h2> <article className="panel panel-wide">
<ul> <div className="panel-heading">
{nextSlice.map((item) => ( <div>
<li key={item}>{item}</li> <p className="section-label">Timeline</p>
))} <h2>Planned operator moments</h2>
</ul> </div>
<span className="panel-badge">{timelineEntries.length} items</span>
</div>
<div className="timeline-list">
{timelineEntries.map((entry) => (
<div key={entry.id} className={`timeline-item timeline-${entry.tone}`}>
<p className="item-meta">{entry.windowLabel}</p>
<h3>{entry.title}</h3>
<p>{entry.detail}</p>
</div>
))}
</div>
</article>
<article className="panel">
<div className="panel-heading">
<div>
<p className="section-label">Audit Feed</p>
<h2>Recent shell events</h2>
</div>
</div>
<div className="audit-list">
{auditEntries.map((entry) => (
<div key={entry.id} className="audit-item">
<p className="item-meta">{entry.occurredAt}</p>
<h3>
{entry.actor} {entry.action}
</h3>
<p className="audit-target">{entry.target}</p>
<p>{entry.summary}</p>
</div>
))}
</div>
</article>
<article className="panel panel-wide">
<div className="panel-heading">
<div>
<p className="section-label">Environment Summary</p>
<h2>Mock-only environment snapshot</h2>
</div>
<span className="panel-badge muted">mock only</span>
</div>
<div className="summary-grid">
{environmentSummaryItems.map((item) => (
<div key={item.id} className="summary-card">
<p className="item-meta">{item.label}</p>
<h3>{item.value}</h3>
<p>{item.note}</p>
</div>
))}
</div>
</article>
</section> </section>
</main> </main>
) )

77
src/fixtures/dashboard.ts Normal file
View File

@@ -0,0 +1,77 @@
import type {
AuditEntry,
EnvironmentSummaryItem,
TimelineEntry,
} from '../types/dashboard'
export const timelineEntries: TimelineEntry[] = [
{
id: 'tl-1',
title: 'Morning maintenance window',
windowLabel: '08:30-09:00 UTC',
detail: 'Patch validation and worker drift review staged for operator sign-off.',
tone: 'stable',
},
{
id: 'tl-2',
title: 'Release rehearsal',
windowLabel: '11:15-12:00 UTC',
detail: 'Dry-run checklist for deployment sequencing and rollback notes.',
tone: 'active',
},
{
id: 'tl-3',
title: 'Credential rotation follow-up',
windowLabel: '15:00-15:30 UTC',
detail: 'Pending verification items remain isolated to the mock shell.',
tone: 'attention',
},
]
export const auditEntries: AuditEntry[] = [
{
id: 'af-1',
actor: 'ops.release',
action: 'approved',
target: 'frontend-shell-slice',
occurredAt: '10:42 UTC',
summary: 'Scope held to local fixtures and presentational shell changes only.',
},
{
id: 'af-2',
actor: 'runtime.guardrail',
action: 'flagged',
target: 'environment-summary',
occurredAt: '09:18 UTC',
summary: 'Section copy must stay explicitly mock-only until backend visibility exists.',
},
{
id: 'af-3',
actor: 'design.review',
action: 'recorded',
target: 'layout-baseline',
occurredAt: '08:05 UTC',
summary: 'Three-panel shell accepted as the next UI slice for iteration.',
},
]
export const environmentSummaryItems: EnvironmentSummaryItem[] = [
{
id: 'env-1',
label: 'Mock worker coverage',
value: '12 workers',
note: 'Mock dataset only. Not sourced from runtime inventory.',
},
{
id: 'env-2',
label: 'Mock alert posture',
value: '2 advisory notices',
note: 'Mock status text for layout validation only.',
},
{
id: 'env-3',
label: 'Mock change queue',
value: '5 queued actions',
note: 'Mock placeholder until live API contracts are introduced.',
},
]

25
src/types/dashboard.ts Normal file
View File

@@ -0,0 +1,25 @@
export type TimelineTone = 'stable' | 'active' | 'attention'
export interface TimelineEntry {
id: string
title: string
windowLabel: string
detail: string
tone: TimelineTone
}
export interface AuditEntry {
id: string
actor: string
action: string
target: string
occurredAt: string
summary: string
}
export interface EnvironmentSummaryItem {
id: string
label: string
value: string
note: string
}