ae0d888ddf
Palette overhaul: - Warm cream canvas (#faf8f4) + near-white surfaces (#fdfdfb) + deep teal accent (#0f766e) - Unified on stone-warm neutrals via @theme block in index.css Typography: - Inter Variable + JetBrains Mono Variable via fontsource (self-hosted) - Monaco editor set to JetBrains Mono for consistency Icons: - lucide-react replaces all 8 hand-rolled SVGs - Leaf brand mark beside wordmark Shared primitives (src/components/ui/): - Icon, Button (4 variants, 2 sizes), IconButton, TextInput, SectionHeader, StatusDot, Chip Custom Monaco theme (src/theme/monaco-theme.ts): - typst-leaf-light: warm near-white background, restrained rainbow-free syntax - Keywords teal, strings warm green, comments stone italic, most tokens ink Component reworks: - TopBar: Leaf icon, h-12, chip-based status, Button/IconButton primitives - Sidebar: w-72, icon+label tabs with teal bottom border - StatusBar: lucide GitBranch/ArrowUp/ArrowDown, chip-based status - FileTree: lucide Plus/FolderPlus/Pencil/Trash2, teal active state - GitPanel: lucide status icons, restored staged/unstaged bg tint - Preview: RotateCw reload icon, chip status - EmptyState: muted Leaf icon - ToastHost: StatusDot + lucide X Fixes: - GitPanel staged/unstaged/untracked rows now have distinct bg tints - Button defaults to type="button" to prevent accidental form submission - Editor error fallback from red-400 to rose-600 - DESIGN.md: removed emoji from layout skeleton, fixed Modals/Monaco typo Build: pnpm -r typecheck + pnpm build pass.
66 lines
2.4 KiB
TypeScript
66 lines
2.4 KiB
TypeScript
import { useRef } from "react";
|
|
import { RotateCw } from "lucide-react";
|
|
import { useApp } from "../app-state";
|
|
import { Chip } from "./ui/Chip";
|
|
import { Icon } from "./ui/Icon";
|
|
|
|
export function Preview() {
|
|
const { state, lsp } = useApp();
|
|
const iframeRef = useRef<HTMLIFrameElement | null>(null);
|
|
|
|
const statusChip = () => {
|
|
if (!state.currentProject) return <Chip tone="neutral">no project</Chip>;
|
|
if (!state.openFile) return <Chip tone="neutral">open a file</Chip>;
|
|
if (lsp.status !== "ready") return <Chip tone="amber" dot dotPulse>LSP {lsp.status}</Chip>;
|
|
if (!lsp.previewReady) return <Chip tone="neutral">starting...</Chip>;
|
|
return <Chip tone="teal" dot>live</Chip>;
|
|
};
|
|
|
|
return (
|
|
<div className="h-full flex flex-col bg-surface">
|
|
{/* Toolbar */}
|
|
<div className="flex items-center gap-2 px-3 py-1.5 border-b border-line text-xs text-ink-muted shrink-0">
|
|
<span className="font-medium">Preview</span>
|
|
<span className="ml-auto flex items-center gap-1">
|
|
{statusChip()}
|
|
</span>
|
|
{lsp.previewReady && (
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
iframeRef.current?.contentWindow?.location.reload();
|
|
}}
|
|
className="p-1 rounded-md text-ink-muted hover:bg-surface-hover transition-colors"
|
|
aria-label="Reload preview"
|
|
>
|
|
<Icon icon={RotateCw} className="size-3.5" />
|
|
</button>
|
|
)}
|
|
<span className="text-ink-subtle text-[11px] hidden sm:inline">Click preview to jump to source</span>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="flex-1 bg-surface relative min-h-0">
|
|
{state.currentProject && state.openFile && lsp.status === "ready" && lsp.previewReady ? (
|
|
<iframe
|
|
ref={iframeRef}
|
|
key={state.currentProject}
|
|
src="/preview-app"
|
|
title="Typst preview"
|
|
className="absolute inset-0 w-full h-full bg-white"
|
|
/>
|
|
) : (
|
|
<div className="absolute inset-0 flex flex-col items-center justify-center text-ink-subtle text-sm p-6 gap-2">
|
|
<span>{statusChip()}</span>
|
|
{lsp.status === "error" && lsp.error && (
|
|
<pre className="mt-2 max-w-full overflow-auto text-xs text-rose-600 bg-rose-50 p-3 rounded whitespace-pre-wrap break-all">
|
|
{lsp.error}
|
|
</pre>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|