feat: establish albedo dashboard shell

This commit is contained in:
2026-10-25 03:57:56 +01:00
parent 5298c6870b
commit 66fe3e4fea
9 changed files with 405 additions and 142 deletions
+36 -32
View File
@@ -1,54 +1,58 @@
<script lang="ts"> <script lang="ts">
let serverAddress = 'http://127.0.0.1:8787'; let serverAddress = 'http://127.0.0.1:8787';
let connectionState = 'Not connected'; let connectionState = 'OFFLINE';
let serverVersion = '—'; let serverVersion = '—';
let activeView = 'Live dashboard';
async function checkServer() { async function checkServer() {
connectionState = 'Connecting…'; connectionState = 'CONNECTING';
try { try {
const response = await fetch(`${serverAddress}/api/v1/server/info`); const response = await fetch(`${serverAddress}/api/v1/server/info`);
if (!response.ok) throw new Error(`HTTP ${response.status}`); if (!response.ok) throw new Error(`HTTP ${response.status}`);
const info: { server_version: string } = await response.json(); const info: { server_version: string } = await response.json();
serverVersion = info.server_version; serverVersion = info.server_version;
connectionState = 'Connected'; connectionState = 'CONNECTED';
} catch { } catch {
connectionState = 'Server unavailable'; connectionState = 'SERVER UNAVAILABLE';
} }
} }
</script> </script>
<svelte:head> <svelte:head><title>Trixie</title></svelte:head>
<meta name="theme-color" content="#2575e6" />
</svelte:head>
<main class="shell"> <div class="app-shell">
<header class="topbar"> <header class="app-bar">
<div> <div class="brand"><span class="brand-mark">T</span><span>TRIXIE</span></div>
<p class="eyebrow">TRIXIE</p> <nav class="top-nav" aria-label="Primary navigation">
<h1>Twitch control center</h1> {#each ['Live dashboard', 'Chat', 'Bot', 'Overlays'] as item}
</div> <button class:active={activeView === item} class="nav-item" onclick={() => (activeView = item)}>{item}</button>
<span class:connected={connectionState === 'Connected'} class="status">{connectionState}</span> {/each}
</nav>
<div class="app-actions"><span class:online={connectionState === 'CONNECTED'} class="connection-state">{connectionState}</span><button class="icon-button" aria-label="Settings">⚙</button></div>
</header> </header>
<section class="hero panel"> <main class="workspace">
<div> <aside class="side-panel">
<p class="eyebrow">SERVER CONNECTION</p> <div class="panel-header"><span>TRIXIE</span><span class="panel-kicker">CONTROL</span></div>
<h2>Ready to build the dashboard</h2> <div class="server-card">
<p class="muted">The client and server now share a versioned readiness contract.</p> <div class="micro">SERVER CONNECTION</div>
<div class="server-status"><span class:good={connectionState === 'CONNECTED'} class="status-dot"></span>{connectionState}</div>
<label for="server-address" class="micro">ADDRESS</label>
<input id="server-address" class="control" bind:value={serverAddress} spellcheck="false" />
<button class="primary control-button" onclick={checkServer}>TEST CONNECTION</button>
</div> </div>
<div class="connection-form"> <div class="side-section"><div class="micro">CHANNEL</div><div class="empty-row">No Twitch account connected</div></div>
<label for="server-address">Server address</label> <div class="side-section side-footer"><div class="micro">SERVER VERSION</div><div class="mono">{serverVersion}</div></div>
<div class="input-row"> </aside>
<input id="server-address" bind:value={serverAddress} spellcheck="false" />
<button on:click={checkServer}>Test connection</button>
</div>
<small>Server version: {serverVersion}</small>
</div>
</section>
<section class="dashboard-grid"> <section class="content-area">
<article class="panel card"><span class="icon">◈</span><h3>Live dashboard</h3><p>Stream preview, activity, chat, and controls will live here.</p></article> <div class="content-heading"><div><div class="micro">LIVE DASHBOARD / {activeView.toUpperCase()}</div><h1>{activeView}</h1></div><button class="secondary">⋮</button></div>
<article class="panel card"><span class="icon">⌁</span><h3>Bot and plugins</h3><p>Modular commands and safe WASM extensions are planned next.</p></article> <div class="dashboard-grid">
<article class="panel card"><span class="icon">◎</span><h3>Overlays</h3><p>Browser-source alerts will be editable and previewable.</p></article> <section class="panel preview-panel"><div class="panel-header"><span>STREAM PREVIEW</span><span class="panel-menu">⋮</span></div><div class="preview-stage"><div class="offline-mark">STREAM OFFLINE</div><div class="offline-subtitle">CONNECT TWITCH TO BEGIN</div></div><div class="panel-footer"><span class="micro">VIDEO PLAYER</span><span class="badge">OFFLINE</span></div></section>
<section class="panel activity-panel"><div class="panel-header"><span>ACTIVITY FEED</span><span class="panel-menu">⋮</span></div><div class="panel-tabs"><button class="tab active">ALL ACTIVITY</button><button class="tab">MODERATION</button></div><div class="empty-state"><div class="empty-glyph">◌</div><strong>Nothing here yet</strong><span>Follows, subscriptions, raids, and server events will appear here.</span></div></section>
<section class="panel chat-panel"><div class="panel-header"><span>MY CHAT</span><span class="panel-menu">⋮</span></div><div class="chat-empty"><div class="empty-glyph">⌁</div><strong>Chat is waiting</strong><span>Connect a streamer or bot account to begin.</span></div><div class="chat-composer"><input class="control" placeholder="Send a message" disabled /><button class="primary" disabled>SEND</button></div></section>
<section class="panel quick-panel"><div class="panel-header"><span>QUICK ACTIONS</span><span class="panel-menu">⋮</span></div><div class="quick-grid"><button class="quick-action accent">EDIT STREAM INFO</button><button class="quick-action">MODERATION</button><button class="quick-action">BOT COMMANDS</button><button class="quick-action">OVERLAYS</button></div></section>
</div>
</section> </section>
</main> </main>
</div>
+2 -1
View File
@@ -1,7 +1,8 @@
import './styles.css'; import './styles.css';
import App from './App.svelte'; import App from './App.svelte';
import { mount } from 'svelte';
const app = new App({ const app = mount(App, {
target: document.getElementById('app')!, target: document.getElementById('app')!,
}); });
+365 -107
View File
@@ -1,154 +1,412 @@
@font-face {
font-family: 'Rajdhani';
src: url('/fonts/Rajdhani-Regular.ttf') format('truetype');
font-weight: 400;
}
@font-face {
font-family: 'Rajdhani';
src: url('/fonts/Rajdhani-SemiBold.ttf') format('truetype');
font-weight: 600;
}
@font-face {
font-family: 'Rajdhani';
src: url('/fonts/Rajdhani-Bold.ttf') format('truetype');
font-weight: 700;
}
@font-face {
font-family: 'Inter';
src: url('/fonts/Inter-Variable.ttf') format('truetype');
}
@font-face {
font-family: 'JetBrains Mono';
src: url('/fonts/JetBrainsMono-Variable.ttf') format('truetype');
}
:root { :root {
font-family: Inter, ui-sans-serif, system-ui, sans-serif; --ed-accent: #00b3ff;
color: #f3f5fb; --ed-accent2: #ff7f00;
background: #10131a; --ed-bg: #06080c;
font-synthesis: none; --ed-panel: #0d1117;
--ed-edge: #1d2630;
--ed-fg: #dde6ec;
--ed-dim: #7d8b96;
--ed-success: #35d073;
--ed-warn: #ffb400;
--ed-danger: #ff5436;
--ed-info: #00b3ff;
--ed-love: #ff5ca8;
--font-display: 'Rajdhani', 'Inter', sans-serif;
--font-data: 'JetBrains Mono', monospace;
--font-body: 'Inter', sans-serif;
--panel-radius: 2px;
--ui-line: color-mix(in srgb, var(--ed-accent) 28%, var(--ed-edge));
--ui-line-soft: color-mix(in srgb, var(--ed-accent) 16%, var(--ed-edge));
--ui-line-focus: color-mix(in srgb, var(--ed-accent) 62%, var(--ed-edge));
--ui-surface: color-mix(in srgb, var(--ed-accent) 8%, var(--ed-bg));
--ui-surface-hi: color-mix(in srgb, var(--ed-accent) 14%, var(--ed-bg));
--ui-divider: color-mix(in srgb, var(--ed-fg) 7%, transparent);
} }
* { * {
box-sizing: border-box; box-sizing: border-box;
} }
html,
body { body {
margin: 0; margin: 0;
min-width: 320px; min-height: 100%;
min-height: 100vh; background: var(--ed-bg);
background: radial-gradient(circle at 85% 0%, #17325e 0, #10131a 42rem); color: var(--ed-fg);
font-family: var(--font-body);
font-size: 13px;
} }
button, button,
input { input {
font: inherit; font: inherit;
color: inherit;
} }
button { button {
border: 0; border: 0;
border-radius: 8px;
padding: 0.7rem 1rem;
color: white;
background: #2575e6;
cursor: pointer; cursor: pointer;
background: none;
} }
button:hover { .app-shell {
background: #4189ed; min-height: 100vh;
background: linear-gradient(
135deg,
var(--ed-bg),
color-mix(in srgb, var(--ed-bg) 90%, var(--ed-accent))
);
} }
.shell { .app-bar {
width: min(1180px, calc(100% - 3rem)); height: 54px;
margin: 0 auto; display: flex;
padding: 2rem 0; align-items: center;
gap: 24px;
padding: 0 18px;
border-bottom: 1px solid var(--ed-edge);
background: var(--ed-panel);
} }
.topbar, .brand {
.input-row { display: flex;
align-items: center;
gap: 9px;
min-width: 170px;
font: 600 18px var(--font-display);
letter-spacing: 0.12em;
}
.brand-mark {
display: grid;
place-items: center;
width: 26px;
height: 26px;
color: var(--ed-bg);
background: var(--ed-accent);
font-weight: 700;
}
.top-nav {
display: flex;
align-self: stretch;
gap: 4px;
}
.nav-item {
position: relative;
padding: 0 14px;
color: var(--ed-dim);
font: 600 13px var(--font-display);
}
.nav-item:hover,
.nav-item.active {
color: var(--ed-fg);
background: var(--ui-surface);
}
.nav-item.active::after {
position: absolute;
right: 10px;
bottom: 0;
left: 10px;
height: 2px;
background: var(--ed-accent);
content: '';
}
.app-actions {
display: flex;
align-items: center;
gap: 10px;
margin-left: auto;
}
.connection-state,
.badge {
padding: 3px 7px;
border: 1px solid var(--ed-warn);
color: var(--ed-warn);
font: 10px var(--font-data);
letter-spacing: 0.04em;
}
.connection-state.online {
border-color: var(--ed-success);
color: var(--ed-success);
}
.icon-button,
.secondary {
color: var(--ed-dim);
font-size: 18px;
}
.workspace {
display: grid;
grid-template-columns: 220px 1fr;
min-height: calc(100vh - 54px);
}
.side-panel {
border-right: 1px solid var(--ed-edge);
background: color-mix(in srgb, var(--ed-panel) 80%, var(--ed-bg));
}
.panel-header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 1rem; min-height: 34px;
padding: 0 10px;
border-bottom: 1px solid var(--ed-edge);
color: var(--ed-fg);
font: 600 13px var(--font-display);
letter-spacing: 0.04em;
} }
.topbar { .panel-kicker,
margin-bottom: 2rem; .micro {
color: var(--ed-dim);
font: 10px var(--font-data);
letter-spacing: 0.08em;
} }
.eyebrow { .server-card,
margin: 0 0 0.35rem; .side-section {
color: #78aef5; padding: 16px 12px;
font-size: 0.72rem; border-bottom: 1px solid var(--ed-edge);
font-weight: 800;
letter-spacing: 0.14em;
} }
h1, .server-status {
h2, display: flex;
h3, align-items: center;
p { gap: 7px;
margin-top: 0; margin: 13px 0 18px;
font: 11px var(--font-data);
} }
h1 { .status-dot {
margin-bottom: 0; width: 7px;
font-size: clamp(1.6rem, 3vw, 2.4rem); height: 7px;
border-radius: 50%;
background: var(--ed-danger);
} }
h2 { .status-dot.good {
font-size: clamp(1.4rem, 3vw, 2.1rem); background: var(--ed-success);
}
.status {
border: 1px solid #3a4354;
border-radius: 999px;
padding: 0.45rem 0.8rem;
color: #a9b1c2;
font-size: 0.85rem;
}
.status.connected {
border-color: #2f9d70;
color: #77e0ae;
}
.panel {
border: 1px solid #2b3342;
border-radius: 14px;
background: rgba(24, 29, 39, 0.88);
box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.18);
}
.hero {
display: grid;
grid-template-columns: 1.2fr 1fr;
gap: 2rem;
padding: 2rem;
}
.muted,
small {
color: #9aa5b8;
}
.connection-form {
align-self: center;
} }
label { label {
display: block; display: block;
margin-bottom: 0.5rem; margin-bottom: 5px;
color: #c9d2e3;
font-size: 0.85rem;
} }
input { .control {
width: 100%;
padding: 7px 8px;
border: 1px solid var(--ui-line);
border-radius: 2px;
outline: none;
background: var(--ed-bg);
color: var(--ed-fg);
font: 11px var(--font-data);
}
.control:focus {
border-color: var(--ui-line-focus);
}
.control-button {
width: 100%;
margin-top: 8px;
}
.primary {
padding: 7px 10px;
border: 1px solid var(--ed-accent);
background: color-mix(in srgb, var(--ed-accent) 20%, var(--ed-bg));
color: var(--ed-fg);
font: 600 11px var(--font-data);
}
.primary:hover {
background: color-mix(in srgb, var(--ed-accent) 34%, var(--ed-bg));
}
.empty-row,
.mono {
margin-top: 8px;
color: var(--ed-dim);
font-size: 11px;
}
.mono {
font-family: var(--font-data);
}
.side-footer {
margin-top: auto;
border-bottom: 0;
}
.content-area {
min-width: 0; min-width: 0;
flex: 1; padding: 16px;
border: 1px solid #3a4354;
border-radius: 8px;
padding: 0.7rem 0.8rem;
color: #f3f5fb;
background: #10131a;
} }
.input-row { .content-heading {
justify-content: stretch; display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
} }
small { .content-heading h1 {
display: block; margin: 4px 0 0;
margin-top: 0.55rem; font: 600 24px var(--font-display);
} }
.dashboard-grid { .dashboard-grid {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: 1.25fr 1fr 1fr;
gap: 1rem; grid-template-rows: minmax(280px, 1fr) 180px;
margin-top: 1rem; gap: 10px;
min-height: calc(100vh - 110px);
} }
.card { .panel {
min-height: 180px; min-width: 0;
padding: 1.4rem; border: 1px solid var(--ed-edge);
border-radius: var(--panel-radius);
background: var(--ed-panel);
} }
.card p { .preview-panel {
color: #9aa5b8; grid-column: span 2;
}
.chat-panel {
grid-row: span 2;
}
.preview-stage {
display: grid;
place-content: center;
min-height: 220px;
border-bottom: 1px solid var(--ed-edge);
background: radial-gradient(
circle,
color-mix(in srgb, var(--ed-accent) 10%, var(--ed-panel)),
var(--ed-bg) 70%
);
text-align: center;
}
.offline-mark {
color: var(--ed-fg);
font: 700 24px var(--font-display);
letter-spacing: 0.18em;
}
.offline-subtitle {
margin-top: 5px;
color: var(--ed-dim);
font: 10px var(--font-data);
letter-spacing: 0.1em;
}
.panel-footer {
display: flex;
justify-content: space-between;
padding: 9px 10px;
}
.panel-tabs {
display: flex;
gap: 14px;
padding: 9px 10px;
border-bottom: 1px solid var(--ed-edge);
}
.tab {
padding: 0 0 5px;
color: var(--ed-dim);
font: 10px var(--font-data);
}
.tab.active {
border-bottom: 2px solid var(--ed-accent);
color: var(--ed-accent);
}
.empty-state,
.chat-empty {
display: grid;
place-items: center;
gap: 6px;
height: calc(100% - 75px);
padding: 24px;
color: var(--ed-dim);
text-align: center;
}
.empty-state strong,
.chat-empty strong {
color: var(--ed-fg);
font: 600 16px var(--font-display);
}
.empty-state span,
.chat-empty span {
max-width: 220px;
line-height: 1.5; line-height: 1.5;
} }
.icon { .empty-glyph {
display: inline-block; color: var(--ed-accent);
margin-bottom: 1rem; font-size: 28px;
color: #5d9bf0;
font-size: 1.8rem;
} }
@media (max-width: 760px) { .chat-composer {
.shell { display: flex;
width: min(100% - 1.5rem, 600px); gap: 5px;
padding-top: 1rem; padding: 8px;
border-top: 1px solid var(--ed-edge);
}
.chat-composer .control {
min-width: 0;
}
.chat-composer .primary {
opacity: 0.5;
}
.quick-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 6px;
padding: 10px;
}
.quick-action {
min-height: 52px;
border: 1px solid var(--ui-line-soft);
background: var(--ui-surface);
color: var(--ed-dim);
font: 10px var(--font-data);
}
.quick-action:hover,
.quick-action.accent {
border-color: var(--ui-line-focus);
color: var(--ed-fg);
background: var(--ui-surface-hi);
}
@media (max-width: 900px) {
.workspace {
grid-template-columns: 170px 1fr;
} }
.hero,
.dashboard-grid { .dashboard-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr 1fr;
grid-template-rows: auto auto auto;
} }
.hero { .preview-panel {
padding: 1.25rem; grid-column: span 2;
} }
.topbar { .chat-panel {
align-items: flex-start; grid-row: span 1;
grid-column: span 2;
}
}
@media (max-width: 640px) {
.top-nav {
display: none;
}
.workspace {
display: block;
}
.side-panel {
display: none;
}
.content-area {
padding: 10px;
}
.dashboard-grid {
display: block;
}
.panel {
margin-bottom: 10px;
}
.chat-panel {
min-height: 280px;
} }
} }
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.