fix: improve repository tree visualization UX

- Collapse directories by default in file tree
- Hide files section on tree loading error
- Add code-like skeleton loader with file header
- Fix syntax highlight size jump between loading/loaded states
- Replace purple accent with grayscale theme
- Preload Rust and Markdown languages for reliable highlighting
This commit is contained in:
Claude
2026-01-02 12:03:09 +00:00
parent 8183c4e798
commit a7eac1f1b2
4 changed files with 54 additions and 53 deletions

View File

@@ -29,42 +29,39 @@ export function SyntaxHighlight({
}: SyntaxHighlightProps) {
const { html, loading, error } = useHighlightedCode(code, language);
// Use consistent wrapper structure for all states to avoid size jumps
const wrapperClasses = cn(
"shiki-container overflow-x-auto max-w-full [&_pre]:!bg-transparent [&_pre]:!m-0 [&_code]:text-xs [&_code]:font-mono",
showLineNumbers && "line-numbers",
className,
);
// Loading state - show code without highlighting
if (loading) {
return (
<pre
className={cn(
"shiki-loading overflow-x-auto max-w-full font-mono text-xs",
className,
)}
>
<code className="text-foreground/70">{code}</code>
</pre>
<div className={cn(wrapperClasses, "shiki-loading")}>
<pre className="!bg-transparent !m-0">
<code className="text-foreground/70">{code}</code>
</pre>
</div>
);
}
// Error state - fallback to plain code
if (error || !html) {
return (
<pre
className={cn(
"overflow-x-auto max-w-full font-mono text-xs",
className,
)}
>
<code>{code}</code>
</pre>
<div className={wrapperClasses}>
<pre className="!bg-transparent !m-0">
<code>{code}</code>
</pre>
</div>
);
}
// Render highlighted HTML
return (
<div
className={cn(
"shiki-container overflow-x-auto max-w-full [&_pre]:!bg-transparent [&_code]:text-xs [&_code]:font-mono",
showLineNumbers && "line-numbers",
className,
)}
className={wrapperClasses}
dangerouslySetInnerHTML={{ __html: html }}
/>
);

View File

@@ -97,23 +97,9 @@ export function RepositoryFilesSection({
);
}
// Error state
// Error state - silently hide section
if (treeError) {
return (
<section className={cn("flex flex-col gap-4", className)}>
<h2 className="text-xl font-semibold flex items-center gap-2">
<FolderGit2 className="size-5" />
Files
</h2>
<div className="flex items-start gap-3 p-4 bg-destructive/10 border border-destructive/20 rounded text-sm">
<AlertCircle className="size-5 text-destructive flex-shrink-0 mt-0.5" />
<div className="flex flex-col gap-1">
<p className="font-medium">Unable to load repository files</p>
<p className="text-muted-foreground text-xs">{treeError.message}</p>
</div>
</div>
</section>
);
return null;
}
// No tree available
@@ -147,8 +133,22 @@ export function RepositoryFilesSection({
<div className="border border-border rounded max-h-96 overflow-auto bg-muted/20">
{selectedFile ? (
contentLoading ? (
<div className="p-4">
<Skeleton className="h-48" />
<div className="relative">
<div className="sticky top-0 bg-muted/80 backdrop-blur-sm px-3 py-1.5 border-b border-border/50 flex items-center justify-between">
<span className="text-xs font-mono text-muted-foreground truncate">
{selectedFile.path}
</span>
</div>
<div className="p-3 space-y-2">
<Skeleton className="h-3 w-3/4" />
<Skeleton className="h-3 w-1/2" />
<Skeleton className="h-3 w-5/6" />
<Skeleton className="h-3 w-2/3" />
<Skeleton className="h-3 w-4/5" />
<Skeleton className="h-3 w-1/3" />
<Skeleton className="h-3 w-3/4" />
<Skeleton className="h-3 w-1/2" />
</div>
</div>
) : contentError ? (
<div className="flex items-start gap-3 p-4 text-sm">

View File

@@ -153,7 +153,7 @@ function TreeNode({
selectedPath,
depth,
}: TreeNodeProps) {
const [isOpen, setIsOpen] = useState(depth === 0);
const [isOpen, setIsOpen] = useState(false);
const isSelected = selectedPath === path;
const handleClick = () => {

View File

@@ -11,8 +11,8 @@ let highlighterPromise: Promise<HighlighterCore> | null = null;
const loadedLanguages = new Set<string>();
/**
* Grimoire dark theme matching current minimalistic Prism styles
* Uses muted colors with primary accent for keywords/functions
* Grimoire dark theme - minimalistic grayscale with semantic colors
* Uses muted grays for syntax with color only for diff semantics
*/
const grimoireTheme: ThemeRegistration = {
name: "grimoire-dark",
@@ -32,7 +32,7 @@ const grimoireTheme: ThemeRegistration = {
scope: ["string", "string.quoted"],
settings: { foreground: "#9ca3af" },
},
// Keywords, operators - primary color
// Keywords, operators - emphasized gray
{
scope: [
"keyword",
@@ -42,14 +42,14 @@ const grimoireTheme: ThemeRegistration = {
"keyword.operator",
"keyword.control",
],
settings: { foreground: "#a855f7" },
settings: { foreground: "#d4d4d4" },
},
// Functions, methods - primary bold
// Functions, methods - foreground bold
{
scope: ["entity.name.function", "support.function", "meta.function-call"],
settings: { foreground: "#a855f7", fontStyle: "bold" },
settings: { foreground: "#e5e5e5", fontStyle: "bold" },
},
// Classes, types - primary bold
// Classes, types - foreground bold
{
scope: [
"entity.name.class",
@@ -57,9 +57,9 @@ const grimoireTheme: ThemeRegistration = {
"support.class",
"support.type",
],
settings: { foreground: "#a855f7", fontStyle: "bold" },
settings: { foreground: "#e5e5e5", fontStyle: "bold" },
},
// Numbers, constants - primary
// Numbers, constants - emphasized gray
{
scope: [
"constant",
@@ -67,7 +67,7 @@ const grimoireTheme: ThemeRegistration = {
"constant.language",
"constant.character",
],
settings: { foreground: "#a855f7" },
settings: { foreground: "#d4d4d4" },
},
// Variables, parameters - foreground
{
@@ -91,7 +91,7 @@ const grimoireTheme: ThemeRegistration = {
// Tags (HTML/JSX)
{
scope: ["entity.name.tag", "support.class.component"],
settings: { foreground: "#a855f7" },
settings: { foreground: "#d4d4d4" },
},
// JSON keys
{
@@ -124,7 +124,7 @@ const grimoireTheme: ThemeRegistration = {
// Markdown headings
{
scope: ["markup.heading", "entity.name.section"],
settings: { foreground: "#a855f7", fontStyle: "bold" },
settings: { foreground: "#e5e5e5", fontStyle: "bold" },
},
// Markdown bold/italic
{
@@ -138,7 +138,7 @@ const grimoireTheme: ThemeRegistration = {
// Markdown links
{
scope: ["markup.underline.link"],
settings: { foreground: "#a855f7" },
settings: { foreground: "#93c5fd" },
},
],
};
@@ -277,6 +277,8 @@ const CORE_LANGUAGES = [
"json",
"diff",
"bash",
"rust",
"markdown",
] as const;
/**
@@ -303,6 +305,8 @@ export async function getHighlighter(): Promise<HighlighterCore> {
import("shiki/langs/json.mjs"),
import("shiki/langs/diff.mjs"),
import("shiki/langs/bash.mjs"),
import("shiki/langs/rust.mjs"),
import("shiki/langs/markdown.mjs"),
],
engine: createOnigurumaEngine(import("shiki/wasm")),
}).then((hl) => {