etaf-playground/design/operations-console.html
2026-08-22 06:19:10 +08:00

212 lines
24 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ETAF Operations Console — visual prototype</title>
<style>
:root {
--ink: #142235;
--ink-soft: #526174;
--paper: #f6f1e8;
--paper-strong: #fffdf8;
--line: #c8c1b6;
--teal: #2e8b83;
--teal-soft: #d9eeea;
--coral: #e26d5a;
--coral-soft: #f9ded7;
--amber: #d99a3d;
--amber-soft: #f8e9c9;
--success: #3e9b72;
--shadow: 0 12px 28px rgba(20, 34, 53, .09);
--radius: 14px;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: var(--ink);
background: var(--paper);
}
:root[data-theme="dark"] {
--ink: #edf2f4;
--ink-soft: #aab8c2;
--paper: #111923;
--paper-strong: #182433;
--line: #344455;
--teal: #61c6ba;
--teal-soft: #173b3b;
--coral: #ff957f;
--coral-soft: #4b2c31;
--amber: #efc06c;
--amber-soft: #4a3922;
--success: #73d29d;
--shadow: 0 14px 32px rgba(0, 0, 0, .25);
}
* { box-sizing: border-box; }
body { margin: 0; min-width: 320px; background: var(--paper); }
button { font: inherit; }
.app-shell {
width: min(1240px, calc(100% - 40px));
min-height: 100vh;
margin: 0 auto;
padding: 22px 0 30px;
display: grid;
grid-template-columns: 210px minmax(0, 1fr);
grid-template-rows: auto 1fr auto;
gap: 18px 24px;
}
.topbar { grid-column: 1 / -1; display: flex; align-items: center; justify-content: space-between; gap: 18px; }
.brand { display: flex; align-items: center; gap: 12px; }
.brand-mark { width: 38px; height: 38px; border-radius: 12px; display: grid; place-items: center; color: #fff; background: var(--ink); font-weight: 800; letter-spacing: -.08em; }
.brand small, .eyebrow, .mono { font-family: "SFMono-Regular", Consolas, monospace; letter-spacing: .08em; text-transform: uppercase; }
.brand small { color: var(--ink-soft); font-size: 10px; }
.brand strong { display: block; font-size: 16px; letter-spacing: -.02em; }
.top-actions { display: flex; align-items: center; gap: 10px; }
.status-pill { display: inline-flex; align-items: center; gap: 7px; padding: 8px 11px; border: 1px solid var(--line); border-radius: 999px; background: var(--paper-strong); font-size: 12px; }
.status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--success); box-shadow: 0 0 0 4px color-mix(in srgb, var(--success) 18%, transparent); }
.icon-button, .nav-button, .primary-button, .quiet-button { border: 1px solid var(--line); border-radius: 10px; cursor: pointer; color: var(--ink); background: var(--paper-strong); }
.icon-button { width: 36px; height: 36px; }
.primary-button { padding: 10px 14px; color: #fff; border-color: var(--ink); background: var(--ink); }
.quiet-button { padding: 9px 12px; }
button:focus-visible { outline: 3px solid color-mix(in srgb, var(--teal) 60%, transparent); outline-offset: 2px; }
.sidebar { grid-row: 2; display: flex; flex-direction: column; gap: 18px; }
.sidebar-heading { margin: 0 0 4px; color: var(--ink-soft); font: 10px "SFMono-Regular", Consolas, monospace; text-transform: uppercase; letter-spacing: .14em; }
.nav { display: grid; gap: 6px; }
.nav-button { padding: 11px 12px; text-align: left; display: flex; justify-content: space-between; align-items: center; }
.nav-button[aria-current="page"] { color: #fff; border-color: var(--ink); background: var(--ink); }
.nav-kicker { font: 10px "SFMono-Regular", Consolas, monospace; opacity: .7; }
.side-card { margin-top: auto; padding: 15px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--paper-strong); box-shadow: var(--shadow); }
.side-card strong { display: block; margin-bottom: 6px; }
.side-card p { margin: 0; color: var(--ink-soft); font-size: 12px; line-height: 1.45; }
.main { grid-row: 2; min-width: 0; }
.page { display: none; gap: 18px; }
.page.active { display: grid; }
.hero { min-height: 190px; padding: 24px; display: flex; align-items: flex-end; justify-content: space-between; gap: 22px; color: #fff; border-radius: var(--radius); background: linear-gradient(135deg, #17283d 0%, #1b4f5c 60%, #2e8b83 100%); box-shadow: var(--shadow); overflow: hidden; position: relative; }
.hero::after { content: ""; position: absolute; width: 300px; height: 300px; right: -80px; top: -140px; border: 1px solid rgba(255,255,255,.22); border-radius: 50%; box-shadow: 0 0 0 28px rgba(255,255,255,.05), 0 0 0 58px rgba(255,255,255,.04); }
.hero-copy, .hero-actions { position: relative; z-index: 1; }
.eyebrow { margin: 0 0 10px; font-size: 10px; opacity: .72; }
h1, h2, h3, p { margin-top: 0; }
h1 { max-width: 620px; margin-bottom: 10px; font-size: clamp(24px, 4vw, 40px); line-height: 1.04; letter-spacing: -.05em; }
.hero p { max-width: 570px; margin-bottom: 0; color: rgba(255,255,255,.78); line-height: 1.5; }
.hero-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.hero .primary-button { color: var(--ink); border-color: #fff; background: #fff; }
.hero .quiet-button { color: #fff; border-color: rgba(255,255,255,.35); background: rgba(255,255,255,.08); }
.metrics { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; }
.metric { padding: 15px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--paper-strong); box-shadow: var(--shadow); }
.metric-label { color: var(--ink-soft); font-size: 12px; }
.metric-value { margin: 8px 0 4px; font: 700 27px "SFMono-Regular", Consolas, monospace; letter-spacing: -.08em; }
.metric-delta { color: var(--success); font-size: 12px; }
.workspace { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(260px, .75fr); gap: 14px; }
.card { min-width: 0; padding: 18px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--paper-strong); box-shadow: var(--shadow); }
.card-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 15px; }
.card-header h2, .card-header h3 { margin: 0; font-size: 15px; letter-spacing: -.02em; }
.card-header p { margin: 4px 0 0; color: var(--ink-soft); font-size: 12px; }
.activity-list, .timeline { display: grid; gap: 10px; }
.activity-row { display: grid; grid-template-columns: 34px minmax(0, 1fr) auto; align-items: center; gap: 10px; padding: 10px 0; border-bottom: 1px solid color-mix(in srgb, var(--line) 70%, transparent); }
.activity-row:last-child { border-bottom: 0; }
.activity-icon { width: 30px; height: 30px; display: grid; place-items: center; border-radius: 9px; color: var(--teal); background: var(--teal-soft); }
.activity-row strong { display: block; font-size: 12px; }
.activity-row small { color: var(--ink-soft); font-size: 11px; }
.activity-time { color: var(--ink-soft); font: 10px "SFMono-Regular", Consolas, monospace; }
.capabilities { display: grid; gap: 8px; }
.capability { display: grid; grid-template-columns: 22px minmax(0, 1fr); gap: 9px; align-items: start; padding: 10px; border-radius: 10px; background: color-mix(in srgb, var(--paper) 70%, var(--paper-strong)); }
.capability-mark { color: var(--success); font-weight: 800; }
.capability strong { display: block; font-size: 12px; }
.capability span { display: block; margin-top: 3px; color: var(--ink-soft); font-size: 11px; line-height: 1.35; }
.timeline-card { padding-bottom: 12px; }
.timeline-item { display: grid; grid-template-columns: 10px minmax(0, 1fr) auto; gap: 10px; align-items: start; }
.timeline-line { position: relative; display: grid; justify-items: center; }
.timeline-line::before { content: ""; width: 1px; height: 100%; background: var(--line); position: absolute; top: 10px; }
.timeline-dot { position: relative; width: 9px; height: 9px; margin-top: 4px; border-radius: 50%; background: var(--coral); }
.timeline-item strong { font-size: 12px; }
.timeline-item p { margin: 3px 0 0; color: var(--ink-soft); font-size: 11px; }
.timeline-item time { color: var(--ink-soft); font: 10px "SFMono-Regular", Consolas, monospace; }
.data-layout, .resource-layout { display: grid; gap: 14px; }
.data-toolbar { display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; align-items: center; }
.data-toolbar strong { font-size: 14px; }
.data-toolbar span { color: var(--ink-soft); font-size: 12px; }
.table { overflow: hidden; border: 1px solid var(--line); border-radius: 10px; }
.table-row { display: grid; grid-template-columns: 70px minmax(0, 1fr) 130px 100px; gap: 10px; align-items: center; padding: 11px 13px; border-bottom: 1px solid var(--line); font-size: 12px; }
.table-row:last-child { border-bottom: 0; }
.table-head { color: var(--ink-soft); background: color-mix(in srgb, var(--paper) 65%, var(--paper-strong)); font: 10px "SFMono-Regular", Consolas, monospace; text-transform: uppercase; letter-spacing: .08em; }
.table-row[data-selected="true"] { color: var(--teal); background: var(--teal-soft); }
.row-button { all: unset; cursor: pointer; display: contents; }
.tag { display: inline-flex; width: max-content; padding: 4px 7px; border-radius: 999px; color: var(--teal); background: var(--teal-soft); font: 10px "SFMono-Regular", Consolas, monospace; }
.resource-grid { display: grid; grid-template-columns: 1.15fr .85fr; gap: 14px; }
.resource-status { display: flex; align-items: center; gap: 10px; padding: 13px; border-radius: 10px; background: var(--teal-soft); }
.resource-status strong { font-size: 13px; }
.resource-status small { display: block; margin-top: 3px; color: var(--ink-soft); font-size: 11px; }
.resource-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
.boundary { padding: 14px; border: 1px dashed var(--amber); border-radius: 10px; background: var(--amber-soft); }
.boundary strong { display: block; margin-bottom: 4px; font-size: 12px; }
.boundary p { margin: 0; color: var(--ink-soft); font-size: 12px; line-height: 1.45; }
.footer { grid-column: 1 / -1; display: flex; justify-content: space-between; gap: 12px; padding-top: 14px; border-top: 1px solid var(--line); color: var(--ink-soft); font-size: 11px; }
.footer strong { color: var(--ink); }
.toast { position: fixed; right: 22px; bottom: 22px; max-width: 340px; padding: 12px 14px; color: #fff; border-radius: 10px; background: var(--ink); box-shadow: var(--shadow); opacity: 0; transform: translateY(8px); pointer-events: none; transition: opacity .15s ease, transform .15s ease; font-size: 12px; }
.toast.show { opacity: 1; transform: translateY(0); }
@media (max-width: 920px) {
.app-shell { width: min(100% - 24px, 760px); grid-template-columns: 1fr; gap: 14px; }
.sidebar { grid-row: auto; display: grid; grid-template-columns: auto 1fr; align-items: center; gap: 10px; }
.sidebar-heading { display: none; }
.nav { grid-template-columns: repeat(3, 1fr); }
.nav-button { padding: 9px; }
.side-card { display: none; }
.main { grid-row: auto; }
.metrics { grid-template-columns: repeat(2, 1fr); }
.workspace, .resource-grid { grid-template-columns: 1fr; }
}
@media (max-width: 560px) {
.app-shell { width: calc(100% - 18px); padding-top: 12px; }
.topbar { align-items: flex-start; }
.status-pill { display: none; }
.hero { padding: 18px; display: block; }
.hero-actions { margin-top: 18px; }
.table-row { grid-template-columns: 48px minmax(0, 1fr) 80px; }
.table-row > :last-child { display: none; }
.footer { display: block; line-height: 1.6; }
}
</style>
</head>
<body>
<div class="app-shell" id="app">
<header class="topbar">
<div class="brand"><div class="brand-mark">OC</div><div><small>ETAF / reference workspace</small><strong>Operations Console</strong></div></div>
<div class="top-actions"><span class="status-pill"><i class="status-dot"></i><span id="system-status">All systems nominal</span></span><button class="icon-button" id="theme-toggle" aria-label="Toggle theme"></button></div>
</header>
<aside class="sidebar">
<div><p class="sidebar-heading">Workspace</p><nav class="nav" aria-label="Primary navigation">
<button class="nav-button" data-page="overview" aria-current="page"><span>Overview</span><span class="nav-kicker">01</span></button>
<button class="nav-button" data-page="data"><span>Data desk</span><span class="nav-kicker">02</span></button>
<button class="nav-button" data-page="resource"><span>Resource lab</span><span class="nav-kicker">03</span></button>
</nav></div>
<div class="side-card"><strong>Owned surface</strong><p>One stable shell, several independently reactive panels, and a status line that never loses the source of truth.</p></div>
</aside>
<main class="main">
<section class="page active" id="page-overview" data-page-view="overview">
<div class="hero"><div class="hero-copy"><p class="eyebrow">Tuesday / 09:42 / live review</p><h1>Make complex state feel calm.</h1><p>A single surface for components, context, behavior, data, and recovery — designed to make every ETAF update visible and explainable.</p></div><div class="hero-actions"><button class="primary-button" id="hero-action">Run health check</button><button class="quiet-button" id="hero-note">View event trail</button></div></div>
<div class="metrics" aria-label="Key metrics"><article class="metric"><span class="metric-label">Throughput</span><div class="metric-value" id="metric-throughput">2.4k</div><span class="metric-delta">↑ 18% this week</span></article><article class="metric"><span class="metric-label">Active flows</span><div class="metric-value" id="metric-flows">18</div><span class="metric-delta">+3 since 08:00</span></article><article class="metric"><span class="metric-label">Response</span><div class="metric-value">42ms</div><span class="metric-delta">p95 within target</span></article><article class="metric"><span class="metric-label">Reliability</span><div class="metric-value">99.8%</div><span class="metric-delta">steady</span></article></div>
<div class="workspace"><article class="card"><div class="card-header"><div><h2>Activity stream</h2><p>Reactive events, rendered as a readable story.</p></div><span class="tag" id="activity-count">4 events</span></div><div class="activity-list" id="activity-list"><div class="activity-row"><span class="activity-icon"></span><div><strong>Theme provider promoted</strong><small>Context defaults were refreshed without losing focus.</small></div><span class="activity-time">09:41</span></div><div class="activity-row"><span class="activity-icon"></span><div><strong>Counter alpha crossed 2k</strong><small>One Component-owned update, one visible publication.</small></div><span class="activity-time">09:36</span></div><div class="activity-row"><span class="activity-icon"></span><div><strong>Resource boundary recovered</strong><small>Handled failure left the shell and routes intact.</small></div><span class="activity-time">09:28</span></div></div></article><article class="card"><div class="card-header"><div><h2>Capability map</h2><p>What this surface is proving.</p></div><span class="tag">ETAF</span></div><div class="capabilities"><div class="capability"><b class="capability-mark"></b><div><strong>Component ownership</strong><span>Props, slots, setup state, and retained identity.</span></div></div><div class="capability"><b class="capability-mark"></b><div><strong>Context + Theme</strong><span>Provider defaults, dark mode, and explicit feedback.</span></div></div><div class="capability"><b class="capability-mark"></b><div><strong>Behavior + Actions</strong><span>Keyboard-ready interactions with stable refs.</span></div></div><div class="capability"><b class="capability-mark"></b><div><strong>Data + Resource</strong><span>Selection, loading, failure, boundary, and cleanup.</span></div></div></div></article></div>
<article class="card timeline-card"><div class="card-header"><div><h2>Event trail</h2><p>Every meaningful action leaves a compact, inspectable trace.</p></div><button class="quiet-button" id="clear-trail">Clear trail</button></div><div class="timeline" id="timeline"><div class="timeline-item"><div class="timeline-line"><i class="timeline-dot"></i></div><div><strong>Surface mounted</strong><p>Operations Console is ready for review.</p></div><time>09:22</time></div><div class="timeline-item"><div class="timeline-line"><i class="timeline-dot"></i></div><div><strong>Data controller loaded</strong><p>3 records are available in the Data desk.</p></div><time>09:23</time></div></div></article>
</section>
<section class="page" id="page-data" data-page-view="data"><article class="card data-layout"><div class="data-toolbar"><div><strong>Data desk</strong><span> / a small table with real selection state</span></div><span class="tag" id="selection-status">No row selected</span></div><div class="table" role="grid"><div class="table-row table-head"><span>ID</span><span>NAME</span><span>STATUS</span><span>OWNER</span></div><button class="row-button" data-row="1"><div class="table-row" data-selected="false"><span>01</span><strong>Atlas</strong><span><i class="tag">active</i></span><span>Core</span></div></button><button class="row-button" data-row="2"><div class="table-row" data-selected="false"><span>02</span><strong>Beacon</strong><span><i class="tag">review</i></span><span>Ops</span></div></button><button class="row-button" data-row="3"><div class="table-row" data-selected="false"><span>03</span><strong>Cirrus</strong><span><i class="tag">done</i></span><span>Data</span></div></button></div><div class="resource-status"><i class="status-dot"></i><div><strong id="data-detail-title">Select a row to inspect it.</strong><small id="data-detail-copy">Selection updates the status without rebuilding the shell.</small></div></div></article></section>
<section class="page" id="page-resource" data-page-view="resource"><div class="resource-grid"><article class="card"><div class="card-header"><div><h2>Resource lab</h2><p>Loading, failure, recovery, and cleanup are first-class states.</p></div><span class="tag" id="resource-tag">ready</span></div><div class="resource-status"><i class="status-dot" id="resource-dot"></i><div><strong id="resource-title">Loaded value 7</strong><small id="resource-copy">Owned by the current application mount.</small></div></div><div class="resource-actions"><button class="primary-button" id="resource-reload">Reload resource</button><button class="quiet-button" id="resource-fail">Fail next load</button><label class="status-pill"><input type="checkbox" id="resource-check"> fail next</label></div></article><article class="card"><div class="card-header"><div><h2>Error boundary</h2><p>Failure is handled in place.</p></div><span class="tag">safe</span></div><div class="boundary"><strong id="boundary-title">No boundary run yet</strong><p id="boundary-copy">Run the boundary to see a handled error while the rest of the application stays mounted.</p></div><div class="resource-actions"><button class="quiet-button" id="run-boundary">Run handled boundary</button></div></article></div><article class="card"><div class="card-header"><div><h2>Lifecycle ledger</h2><p>Resource cleanup is visible, not inferred.</p></div><span class="metric-value" id="cleanup-count">0</span></div><p class="mono">cleanup calls after mount / reload / unmount</p></article></section>
</main>
<footer class="footer"><span><strong id="footer-status">Ready for review</strong> · state is owned by the current generation</span><span class="mono">operations-console / v0.1</span></footer>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script>
const root = document.documentElement;
const toast = document.getElementById('toast');
const showToast = message => { toast.textContent = message; toast.classList.add('show'); clearTimeout(showToast.timer); showToast.timer = setTimeout(() => toast.classList.remove('show'), 2200); };
document.getElementById('theme-toggle').addEventListener('click', () => { root.dataset.theme = root.dataset.theme === 'dark' ? 'light' : 'dark'; showToast(root.dataset.theme === 'dark' ? 'Dark theme enabled' : 'Light theme enabled'); });
document.querySelectorAll('[data-page]').forEach(button => button.addEventListener('click', () => { const page = button.dataset.page; document.querySelectorAll('[data-page]').forEach(item => item.setAttribute('aria-current', item.dataset.page === page ? 'page' : 'false')); document.querySelectorAll('[data-page-view]').forEach(view => view.classList.toggle('active', view.dataset.pageView === page)); document.getElementById('footer-status').textContent = `Viewing ${page === 'data' ? 'Data desk' : page === 'resource' ? 'Resource lab' : 'Overview'}`; }));
document.getElementById('hero-action').addEventListener('click', () => { document.getElementById('system-status').textContent = 'Health check complete'; document.getElementById('footer-status').textContent = 'Health check completed'; showToast('All owned surfaces responded'); });
document.getElementById('hero-note').addEventListener('click', () => document.getElementById('timeline').scrollIntoView({ behavior: 'smooth', block: 'center' }));
document.querySelectorAll('[data-row]').forEach(button => button.addEventListener('click', () => { const row = button.querySelector('.table-row'); document.querySelectorAll('.table-row[data-selected]').forEach(item => item.dataset.selected = 'false'); row.dataset.selected = 'true'; const name = row.querySelector('strong').textContent; document.getElementById('selection-status').textContent = `Selected ${name}`; document.getElementById('data-detail-title').textContent = `${name} is selected`; document.getElementById('data-detail-copy').textContent = 'The Data Controller and DataGrid share one stable row ref.'; showToast(`Selected ${name}`); }));
document.getElementById('clear-trail').addEventListener('click', () => { document.getElementById('timeline').innerHTML = '<div class="timeline-item"><div class="timeline-line"><i class="timeline-dot"></i></div><div><strong>Trail cleared</strong><p>New actions will append here.</p></div><time>now</time></div>'; showToast('Event trail cleared'); });
let resourceValue = 7; let shouldFail = false; let cleanups = 0;
document.getElementById('resource-check').addEventListener('change', event => { shouldFail = event.target.checked; showToast(shouldFail ? 'Next resource load will fail' : 'Failure flag cleared'); });
document.getElementById('resource-fail').addEventListener('click', () => { shouldFail = true; document.getElementById('resource-check').checked = true; showToast('Next resource load will fail'); });
document.getElementById('resource-reload').addEventListener('click', () => { if (shouldFail) { shouldFail = false; document.getElementById('resource-check').checked = false; document.getElementById('resource-tag').textContent = 'error'; document.getElementById('resource-title').textContent = 'Handled load failure'; document.getElementById('resource-copy').textContent = 'Retry remains available; the shell stayed mounted.'; showToast('Resource error handled'); return; } resourceValue += 1; document.getElementById('resource-tag').textContent = 'ready'; document.getElementById('resource-title').textContent = `Loaded value ${resourceValue}`; document.getElementById('resource-copy').textContent = 'Fresh value promoted through the owned resource.'; showToast('Resource reloaded'); });
document.getElementById('run-boundary').addEventListener('click', () => { document.getElementById('boundary-title').textContent = 'Handled boundary result'; document.getElementById('boundary-copy').textContent = 'The failure was caught and rendered here; navigation and state remain available.'; showToast('Boundary handled safely'); });
</script>
</body>
</html>