feat: add main page
This commit is contained in:
@@ -1,2 +1,244 @@
|
||||
<h1>🦆 Rubber Duck Debugging as a Service</h1>
|
||||
<p class="muted">Главная страница в разработке. А пока — смотрите <a href="/reports">отчёты тестирования</a> и <a href="/mcp">MCP-сервер</a>.</p>
|
||||
<script lang="ts">
|
||||
import DuckAvatar from '$lib/components/duck/DuckAvatar.svelte';
|
||||
import ChatPanel from '$lib/components/duck/ChatPanel.svelte';
|
||||
import WardrobePanel from '$lib/components/duck/WardrobePanel.svelte';
|
||||
import SettingsPanel from '$lib/components/duck/SettingsPanel.svelte';
|
||||
import { outfit, saveOutfit } from '$lib/duck/state.svelte';
|
||||
|
||||
const TABS = [
|
||||
{ id: 'chat', label: 'Чат' },
|
||||
{ id: 'wardrobe', label: 'Гардероб' },
|
||||
{ id: 'settings', label: 'Настройки' }
|
||||
] as const;
|
||||
|
||||
type Tab = (typeof TABS)[number]['id'];
|
||||
|
||||
let tab = $state<Tab>('chat');
|
||||
|
||||
// сохраняем образ при любом изменении
|
||||
$effect(() => {
|
||||
saveOutfit();
|
||||
});
|
||||
|
||||
const outfitLine = $derived(
|
||||
`{"cap":"${outfit.cap}","cap_logo":${JSON.stringify(outfit.capLogo)},` +
|
||||
`"tee":"${outfit.tee}","tee_logo":${JSON.stringify(outfit.teeLogo)}}`
|
||||
);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Rubber Duck — отладка с уткой</title>
|
||||
<meta name="description" content="Резиновая утка для отладки: переодевай, вешай логотипы, рассказывай о проблеме." />
|
||||
</svelte:head>
|
||||
|
||||
<section class="hero">
|
||||
<div>
|
||||
<h1>Rubber Duck<span class="cursor mono">_</span></h1>
|
||||
<p class="sub">
|
||||
Проговори проблему вслух — утка выслушает. Пока говоришь, решение часто находится само.
|
||||
</p>
|
||||
</div>
|
||||
<div class="chips mono">
|
||||
<span class="chip">debugging-as-a-service</span>
|
||||
<span class="chip">local-first</span>
|
||||
<span class="chip">quack · симуляция</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="grid">
|
||||
<section class="stage" aria-label="Сцена с уткой">
|
||||
<div class="stage-top mono">
|
||||
<span>duck_id: RU-1337</span>
|
||||
<span>status: listening</span>
|
||||
<span>uptime: ∞</span>
|
||||
</div>
|
||||
<div class="duck-wrap">
|
||||
<DuckAvatar />
|
||||
</div>
|
||||
<div class="term mono">
|
||||
<div class="term-line"><span class="prompt">$</span> duck outfit --json</div>
|
||||
<div class="term-out">{outfitLine}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside class="side" aria-label="Чат и настройки">
|
||||
<div class="tabs mono" role="tablist">
|
||||
{#each TABS as t (t.id)}
|
||||
<button
|
||||
role="tab"
|
||||
class:active={tab === t.id}
|
||||
aria-selected={tab === t.id}
|
||||
onclick={() => (tab = t.id)}
|
||||
>
|
||||
{t.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="tab-body chat-body" class:hidden={tab !== 'chat'}>
|
||||
<ChatPanel />
|
||||
</div>
|
||||
<div class="tab-body" class:hidden={tab !== 'wardrobe'}>
|
||||
<WardrobePanel />
|
||||
</div>
|
||||
<div class="tab-body" class:hidden={tab !== 'settings'}>
|
||||
<SettingsPanel />
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.hero {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
margin-bottom: 22px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 34px;
|
||||
line-height: 1.15;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.cursor {
|
||||
color: #f5883c;
|
||||
animation: caret 1.1s steps(1) infinite;
|
||||
font-weight: 400;
|
||||
}
|
||||
@keyframes caret {
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
.sub {
|
||||
margin: 8px 0 0;
|
||||
color: var(--muted);
|
||||
max-width: 520px;
|
||||
font-size: 15px;
|
||||
}
|
||||
.chips {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.chip {
|
||||
font-size: 11.5px;
|
||||
color: var(--muted);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--panel);
|
||||
border-radius: 999px;
|
||||
padding: 4px 10px;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 400px;
|
||||
gap: 20px;
|
||||
align-items: stretch;
|
||||
}
|
||||
.stage {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
background:
|
||||
radial-gradient(420px 320px at 50% 46%, rgba(255, 217, 59, 0.09), transparent 70%),
|
||||
linear-gradient(rgba(148, 163, 184, 0.06) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(148, 163, 184, 0.06) 1px, transparent 1px),
|
||||
var(--panel);
|
||||
background-size: auto, 28px 28px, 28px 28px, auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.stage-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 10px 16px;
|
||||
font-size: 11.5px;
|
||||
color: var(--muted);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.duck-wrap {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 22px 20px 10px;
|
||||
min-height: 420px;
|
||||
}
|
||||
.term {
|
||||
border-top: 1px solid var(--border);
|
||||
background: rgba(2, 6, 23, 0.45);
|
||||
padding: 10px 16px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.term-line {
|
||||
color: var(--muted);
|
||||
}
|
||||
.prompt {
|
||||
color: var(--ok);
|
||||
}
|
||||
.term-out {
|
||||
color: #fbbf24;
|
||||
margin-top: 2px;
|
||||
word-break: break-all;
|
||||
}
|
||||
.side {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
background: var(--panel);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 640px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.tabs button {
|
||||
flex: 1;
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
color: var(--muted);
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
padding: 12px 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tabs button:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
.tabs button.active {
|
||||
color: var(--text);
|
||||
border-bottom-color: #3b82f6;
|
||||
}
|
||||
.tab-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 14px;
|
||||
min-height: 0;
|
||||
}
|
||||
.chat-body {
|
||||
display: flex;
|
||||
}
|
||||
.tab-body.hidden {
|
||||
display: none;
|
||||
}
|
||||
@media (max-width: 1020px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.side {
|
||||
height: auto;
|
||||
}
|
||||
.chat-body {
|
||||
height: 520px;
|
||||
}
|
||||
.duck-wrap {
|
||||
min-height: 320px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user