feat(dashboard): Home view as default landing
The full operator dashboard (sidebar + scene + inspector + console + REPL) is dense by design — that's the power-user surface. New users said it felt overwhelming on first load. Add a clean <nv-home> view as the default landing: - Hero with NV badge, plain-language title, single-paragraph explainer - Three CTAs: ▶ Run the simulation · ★ Take the tour · ? Help - Live status pill (Idle / Live · 1.79 kHz · witness verified ✓) - 4 quick-jump cards: Live scene · App Store · Determinism gate · Ghost Murmur - Full keyboard accessibility (tabindex, Enter/Space activation) - Footnote with a 'Take the 60-second guided tour' link Rail gets a Home button as the new first nav item. View union extended to include 'home'; default view is 'home'. Click any rail icon (Scene, Apps, Inspector, Witness, Ghost Murmur) to drop straight into the power-user views. Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
parent
f25b82ddb5
commit
b34e5ee8cf
|
|
@ -19,12 +19,13 @@ import './nv-settings-drawer';
|
|||
import './nv-onboarding';
|
||||
import './nv-ghost-murmur';
|
||||
import './nv-help';
|
||||
import './nv-home';
|
||||
|
||||
export type View = 'scene' | 'apps' | 'inspector' | 'witness' | 'ghost-murmur';
|
||||
export type View = 'home' | 'scene' | 'apps' | 'inspector' | 'witness' | 'ghost-murmur';
|
||||
|
||||
@customElement('nv-app')
|
||||
export class NvApp extends LitElement {
|
||||
@state() private view: View = 'scene';
|
||||
@state() private view: View = 'home';
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
|
|
@ -99,15 +100,17 @@ export class NvApp extends LitElement {
|
|||
<nv-topbar></nv-topbar>
|
||||
<nv-sidebar></nv-sidebar>
|
||||
<main class="main" id="main-content" tabindex="-1" role="main" aria-label="Main view">
|
||||
${this.view === 'apps'
|
||||
? html`<nv-app-store></nv-app-store>`
|
||||
: this.view === 'ghost-murmur'
|
||||
? html`<nv-ghost-murmur></nv-ghost-murmur>`
|
||||
: this.view === 'inspector'
|
||||
? html`<nv-inspector expanded .pinTab=${'signal'}></nv-inspector>`
|
||||
: this.view === 'witness'
|
||||
? html`<nv-inspector expanded .pinTab=${'witness'}></nv-inspector>`
|
||||
: html`<nv-scene></nv-scene>`}
|
||||
${this.view === 'home'
|
||||
? html`<nv-home></nv-home>`
|
||||
: this.view === 'apps'
|
||||
? html`<nv-app-store></nv-app-store>`
|
||||
: this.view === 'ghost-murmur'
|
||||
? html`<nv-ghost-murmur></nv-ghost-murmur>`
|
||||
: this.view === 'inspector'
|
||||
? html`<nv-inspector expanded .pinTab=${'signal'}></nv-inspector>`
|
||||
: this.view === 'witness'
|
||||
? html`<nv-inspector expanded .pinTab=${'witness'}></nv-inspector>`
|
||||
: html`<nv-scene></nv-scene>`}
|
||||
</main>
|
||||
<nv-inspector
|
||||
.pinTab=${this.view === 'inspector' ? 'signal'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,270 @@
|
|||
/* Home view — friendly landing surface for new users.
|
||||
*
|
||||
* The full-power scene + sidebar + inspector + console are intentionally
|
||||
* dense; that's the operator surface. Home is for first-time visitors:
|
||||
* a single hero CTA, four quick-jump action cards, and a 1-paragraph
|
||||
* explanation of what this dashboard is. No jargon above the fold.
|
||||
*/
|
||||
|
||||
import { LitElement, html, css } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
import { effect } from '@preact/signals-core';
|
||||
import { running, getClient, witnessVerified, fps, pushLog } from '../store/appStore';
|
||||
|
||||
export type Action = 'scene' | 'apps' | 'witness' | 'ghost-murmur' | 'help' | 'tour';
|
||||
|
||||
@customElement('nv-home')
|
||||
export class NvHome extends LitElement {
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
background: radial-gradient(ellipse at 50% 30%, var(--bg-2) 0%, var(--bg-0) 70%);
|
||||
padding: 28px clamp(16px, 6vw, 56px) 60px;
|
||||
}
|
||||
.hero {
|
||||
max-width: 800px;
|
||||
margin: 16px auto 28px;
|
||||
text-align: center;
|
||||
}
|
||||
.hero .icon {
|
||||
width: 56px; height: 56px;
|
||||
margin: 0 auto 18px;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(135deg, oklch(0.78 0.14 70) 0%, oklch(0.55 0.16 30) 100%);
|
||||
display: grid; place-items: center;
|
||||
font-family: var(--mono);
|
||||
font-weight: 700;
|
||||
font-size: 18px;
|
||||
color: #1a0f00;
|
||||
box-shadow: 0 8px 24px -6px oklch(0.55 0.16 30 / 0.4);
|
||||
}
|
||||
.hero h1 {
|
||||
margin: 0 0 8px;
|
||||
font-size: clamp(24px, 4vw, 34px);
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--ink);
|
||||
line-height: 1.15;
|
||||
}
|
||||
.hero .tag {
|
||||
font-size: clamp(13px, 1.6vw, 15px);
|
||||
color: var(--ink-2);
|
||||
margin: 0 0 22px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.hero .ctas {
|
||||
display: flex; flex-wrap: wrap; gap: 8px;
|
||||
justify-content: center;
|
||||
}
|
||||
.cta {
|
||||
padding: 11px 20px;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--bg-2);
|
||||
color: var(--ink);
|
||||
transition: transform 0.12s, border-color 0.12s, filter 0.12s;
|
||||
}
|
||||
.cta:hover { transform: translateY(-1px); border-color: var(--line-2); }
|
||||
.cta.primary {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: #1a0f00;
|
||||
}
|
||||
.cta.primary:hover { filter: brightness(1.08); }
|
||||
.status {
|
||||
display: inline-flex; align-items: center; gap: 8px;
|
||||
padding: 6px 12px;
|
||||
background: var(--bg-2);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-family: var(--mono);
|
||||
color: var(--ink-2);
|
||||
margin-top: 18px;
|
||||
}
|
||||
.status .dot {
|
||||
width: 8px; height: 8px; border-radius: 50%;
|
||||
background: var(--ink-3);
|
||||
}
|
||||
.status.live .dot {
|
||||
background: var(--ok);
|
||||
box-shadow: 0 0 8px var(--ok);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
@keyframes pulse { 50% { opacity: 0.5; } }
|
||||
|
||||
.grid {
|
||||
max-width: 980px;
|
||||
margin: 36px auto 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
.card {
|
||||
background: var(--bg-2);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
padding: 18px 20px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.12s, border-color 0.12s, background 0.12s;
|
||||
display: flex; flex-direction: column; gap: 6px;
|
||||
text-align: left;
|
||||
color: inherit;
|
||||
}
|
||||
.card:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: var(--accent);
|
||||
background: linear-gradient(180deg, var(--bg-2) 0%, oklch(0.78 0.14 70 / 0.04) 100%);
|
||||
}
|
||||
.card .ico {
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.card h3 {
|
||||
margin: 0;
|
||||
font-size: 14.5px;
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.card p {
|
||||
margin: 0;
|
||||
font-size: 12.5px;
|
||||
color: var(--ink-2);
|
||||
line-height: 1.55;
|
||||
}
|
||||
.card .arrow {
|
||||
color: var(--accent);
|
||||
font-family: var(--mono);
|
||||
font-size: 11.5px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.footnote {
|
||||
max-width: 800px;
|
||||
margin: 36px auto 0;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: var(--ink-3);
|
||||
line-height: 1.55;
|
||||
}
|
||||
.footnote code {
|
||||
font-family: var(--mono);
|
||||
background: var(--bg-3);
|
||||
padding: 1px 5px;
|
||||
border-radius: 4px;
|
||||
color: var(--accent);
|
||||
font-size: 11px;
|
||||
}
|
||||
.footnote a {
|
||||
color: var(--accent-2);
|
||||
text-decoration: underline dotted;
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
effect(() => { running.value; witnessVerified.value; fps.value; this.requestUpdate(); });
|
||||
}
|
||||
|
||||
private go(action: Action): void {
|
||||
if (action === 'tour') { window.dispatchEvent(new CustomEvent('nv-show-tour')); return; }
|
||||
if (action === 'help') { window.dispatchEvent(new CustomEvent('nv-show-help')); return; }
|
||||
this.dispatchEvent(new CustomEvent('navigate', { detail: action, bubbles: true, composed: true }));
|
||||
}
|
||||
|
||||
private async runDemo(): Promise<void> {
|
||||
const c = getClient(); if (!c) return;
|
||||
if (running.value) return;
|
||||
await c.run();
|
||||
running.value = true;
|
||||
pushLog('ok', 'demo started · streaming MagFrames');
|
||||
}
|
||||
|
||||
override render() {
|
||||
const isRunning = running.value;
|
||||
const wasVerified = witnessVerified.value === 'ok';
|
||||
return html`
|
||||
<div class="hero">
|
||||
<div class="icon" aria-hidden="true">NV</div>
|
||||
<h1>An open-source quantum-magnetometer simulator, in your browser.</h1>
|
||||
<p class="tag">
|
||||
nvsim runs a real Rust simulator (the same code that
|
||||
<code style="font-family:var(--mono); background:var(--bg-3); padding:1px 5px; border-radius:4px; color:var(--accent); font-size:12px;">cargo test</code>
|
||||
uses) entirely in WebAssembly. No server, no upload, no telemetry.
|
||||
Press the button to start the live magnetic-field simulation, or
|
||||
take the 60-second tour first.
|
||||
</p>
|
||||
<div class="ctas">
|
||||
<button class="cta primary" id="home-run-btn" @click=${() => this.runDemo()}>
|
||||
${isRunning ? '✓ Demo running' : '▶ Run the simulation'}
|
||||
</button>
|
||||
<button class="cta" id="home-tour-btn" @click=${() => this.go('tour')}>
|
||||
★ Take the 60-second tour
|
||||
</button>
|
||||
<button class="cta" id="home-help-btn" @click=${() => this.go('help')}>
|
||||
? Help center
|
||||
</button>
|
||||
</div>
|
||||
<div class="status ${isRunning ? 'live' : ''}">
|
||||
<span class="dot"></span>
|
||||
${isRunning
|
||||
? html`Live · ${fps.value > 0 ? (fps.value / 1000).toFixed(2) + ' kHz' : 'starting…'}${wasVerified ? ' · witness verified ✓' : ''}`
|
||||
: html`Idle${wasVerified ? ' · witness verified ✓' : ''}`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card" tabindex="0" role="button"
|
||||
@click=${() => this.go('scene')}
|
||||
@keydown=${(e: KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.go('scene'); } }}>
|
||||
<div class="ico">🌐</div>
|
||||
<h3>Live scene</h3>
|
||||
<p>Drag magnetic sources, watch the recovered field update in real time, and tweak sample rate / noise / integration.</p>
|
||||
<div class="arrow">Open scene →</div>
|
||||
</div>
|
||||
|
||||
<div class="card" tabindex="0" role="button"
|
||||
@click=${() => this.go('apps')}
|
||||
@keydown=${(e: KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.go('apps'); } }}>
|
||||
<div class="ico">🛍</div>
|
||||
<h3>App Store · 66 edge apps</h3>
|
||||
<p>Browse 65 hot-loadable WASM sensing modules across medical, security, building, retail, industrial, learning. Six run live in the browser.</p>
|
||||
<div class="arrow">Browse the catalogue →</div>
|
||||
</div>
|
||||
|
||||
<div class="card" tabindex="0" role="button"
|
||||
@click=${() => this.go('witness')}
|
||||
@keydown=${(e: KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.go('witness'); } }}>
|
||||
<div class="ico">✓</div>
|
||||
<h3>Determinism gate</h3>
|
||||
<p>Re-derive the SHA-256 witness for the canonical reference scene right here in your browser. Same inputs → same hash, every time.</p>
|
||||
<div class="arrow">Verify the witness →</div>
|
||||
</div>
|
||||
|
||||
<div class="card" tabindex="0" role="button"
|
||||
@click=${() => this.go('ghost-murmur')}
|
||||
@keydown=${(e: KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.go('ghost-murmur'); } }}>
|
||||
<div class="ico">👻</div>
|
||||
<h3>Ghost Murmur reality check</h3>
|
||||
<p>Audit the publicly-reported April 2026 CIA NV-diamond program against published physics. Live distance/moment sliders.</p>
|
||||
<div class="arrow">Read the spec →</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="footnote">
|
||||
New here? <a @click=${() => this.go('tour')}>Take the 60-second guided tour</a>
|
||||
— every panel is explained. Or press <code>?</code> for the help center
|
||||
(quickstart, glossary, FAQ, shortcuts) any time.<br>
|
||||
Open source · Apache-2.0 OR MIT · <code>github.com/ruvnet/RuView</code>
|
||||
</p>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
@ -64,6 +64,12 @@ export class NvRail extends LitElement {
|
|||
<div class="logo" aria-hidden="true">NV</div>
|
||||
<nav role="navigation" aria-label="Primary"
|
||||
style="display:flex; flex-direction:column; align-items:center; gap:4px; flex:1;">
|
||||
<button class="btn ${this.view === 'home' ? 'active' : ''}"
|
||||
data-id="home-btn" title="Home" aria-label="Home"
|
||||
aria-current=${this.view === 'home' ? 'page' : 'false'}
|
||||
@click=${() => this.navigate('home')}>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 12L12 4l9 8M5 10v10h14V10"/></svg>
|
||||
</button>
|
||||
<button class="btn ${this.view === 'scene' ? 'active' : ''}"
|
||||
data-id="scene-btn" title="Scene" aria-label="Scene"
|
||||
aria-current=${this.view === 'scene' ? 'page' : 'false'}
|
||||
|
|
|
|||
Loading…
Reference in New Issue