feat: add ui shell sections
This commit is contained in:
11
README.md
11
README.md
@@ -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.
|
|
||||||
|
|||||||
213
src/App.css
213
src/App.css
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
108
src/App.tsx
108
src/App.tsx
@@ -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
77
src/fixtures/dashboard.ts
Normal 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
25
src/types/dashboard.ts
Normal 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user