File size: 4,989 Bytes
7c5ed63 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | 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>
);
}
|