kimi-code / apps /vis /web /src /components /layout /AppShell.tsx
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
7c5ed63 verified
Raw
History Blame Contribute Delete
4.99 kB
import type { ReactNode } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { Link } from 'react-router-dom';
import { SessionRail } from '../sessions/SessionRail';
import { ZipDropOverlay } from '../shared/ZipDropOverlay';
import { useTheme, type ThemeChoice, type ResolvedTheme } from '../../hooks/useTheme';
interface AppShellProps {
children: ReactNode;
}
export function AppShell({ children }: AppShellProps) {
const qc = useQueryClient();
const { choice, resolved, cycle } = useTheme();
return (
<div className="flex h-full flex-col">
<header className="flex h-10 shrink-0 items-center justify-between border-b border-border bg-surface-1 px-4">
<Link to="/" className="flex items-center gap-2">
<LogoMark />
<span className="font-mono text-[12px] uppercase tracking-[0.12em] text-fg-0">
kimi <span className="text-fg-2">vis</span>
</span>
<span className="ml-2 font-mono text-[10px] uppercase tracking-[0.12em] text-fg-3">
debug · local files
</span>
</Link>
<div className="flex items-center gap-2 text-[11px] text-fg-2">
<ThemeToggle choice={choice} resolved={resolved} onCycle={cycle} />
<button
onClick={() => {
void qc.invalidateQueries();
}}
className="flex items-center gap-1.5 border border-border px-2 py-0.5 font-mono text-[11px] text-fg-1 transition-colors hover:border-border-strong hover:text-fg-0"
title="Refresh — re-read all session data from disk"
>
<RefreshIcon />
refresh
</button>
</div>
</header>
<div className="flex min-h-0 flex-1">
<SessionRail />
{/* min-w-0 lets the main column shrink below its content's intrinsic
width; without it a flex child defaults to min-width:auto and wide
tab content (e.g. the Timeline's flex-wrap rows) blows the layout
out horizontally instead of wrapping. */}
<main className="flex min-h-0 min-w-0 flex-1 flex-col">{children}</main>
</div>
<ZipDropOverlay />
</div>
);
}
function ThemeToggle({
choice,
resolved,
onCycle,
}: {
choice: ThemeChoice;
resolved: ResolvedTheme;
onCycle: () => void;
}) {
const label = choice === 'auto' ? `auto · ${resolved}` : choice;
const title = `Theme: ${label}. Click to cycle (auto → light → dark → auto).`;
return (
<button
onClick={onCycle}
className="flex items-center gap-1.5 border border-border px-2 py-0.5 font-mono text-[11px] text-fg-1 transition-colors hover:border-border-strong hover:text-fg-0"
title={title}
aria-label={`Theme ${label}`}
>
{choice === 'auto' ? <AutoIcon /> : resolved === 'light' ? <SunIcon /> : <MoonIcon />}
<span className="tabular">{label}</span>
</button>
);
}
function LogoMark() {
return (
<svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true">
<rect x="1" y="1" width="6" height="6" fill="var(--color-cat-conversation)" />
<rect x="9" y="1" width="6" height="6" fill="var(--color-cat-subagent)" />
<rect x="1" y="9" width="6" height="6" fill="var(--color-cat-ephemeral)" />
<rect x="9" y="9" width="6" height="6" fill="var(--color-cat-approval)" />
</svg>
);
}
function RefreshIcon() {
return (
<svg width="10" height="10" viewBox="0 0 12 12" aria-hidden="true">
<path
d="M10 3 A5 5 0 1 0 10.8 7"
stroke="currentColor"
strokeWidth="1.2"
fill="none"
strokeLinecap="square"
/>
<path d="M7 1 L10 3 L7 5" stroke="currentColor" strokeWidth="1.2" fill="none" strokeLinecap="square" strokeLinejoin="miter" />
</svg>
);
}
function SunIcon() {
return (
<svg width="11" height="11" viewBox="0 0 12 12" aria-hidden="true">
<circle cx="6" cy="6" r="2" fill="currentColor" />
<g stroke="currentColor" strokeWidth="1" strokeLinecap="round">
<line x1="6" y1="0.5" x2="6" y2="2" />
<line x1="6" y1="10" x2="6" y2="11.5" />
<line x1="0.5" y1="6" x2="2" y2="6" />
<line x1="10" y1="6" x2="11.5" y2="6" />
<line x1="2" y1="2" x2="3" y2="3" />
<line x1="9" y1="9" x2="10" y2="10" />
<line x1="10" y1="2" x2="9" y2="3" />
<line x1="3" y1="9" x2="2" y2="10" />
</g>
</svg>
);
}
function MoonIcon() {
return (
<svg width="11" height="11" viewBox="0 0 12 12" aria-hidden="true">
<path
d="M9 7.5 A4.5 4.5 0 1 1 4.5 3 A3.6 3.6 0 0 0 9 7.5 Z"
fill="currentColor"
/>
</svg>
);
}
function AutoIcon() {
return (
<svg width="11" height="11" viewBox="0 0 12 12" aria-hidden="true">
<path
d="M6 1 A5 5 0 1 1 6 11 Z"
fill="currentColor"
/>
<circle cx="6" cy="6" r="4.5" fill="none" stroke="currentColor" strokeWidth="1" />
</svg>
);
}