Detailed changes
@@ -19,12 +19,13 @@
},
"dependencies": {
"@apollo/client": "^4.1.6",
+ "@shikijs/langs": "^4.0.2",
+ "@shikijs/themes": "^4.0.2",
"@tanstack/react-router": "^1.168.8",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"graphql": "^16.9.0",
- "highlight.js": "^11.11.1",
"lucide-react": "^1.7.0",
"radix-ui": "^1.4.3",
"react": "^19.1.0",
@@ -38,6 +39,7 @@
"remark-emoji": "^5.0.2",
"remark-gfm": "^4.0.0",
"rxjs": "^7.8.2",
+ "shiki": "^4.0.2",
"tailwind-merge": "^3.5.0",
"tw-animate-css": "^1.4.0",
"valibot": "^1.3.1"
@@ -11,6 +11,12 @@ importers:
'@apollo/client':
specifier: ^4.1.6
version: 4.1.6(graphql-ws@6.0.8(graphql@16.13.2)(ws@8.20.0))(graphql@16.13.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rxjs@7.8.2)
+ '@shikijs/langs':
+ specifier: ^4.0.2
+ version: 4.0.2
+ '@shikijs/themes':
+ specifier: ^4.0.2
+ version: 4.0.2
'@tanstack/react-router':
specifier: ^1.168.8
version: 1.168.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -26,9 +32,6 @@ importers:
graphql:
specifier: ^16.9.0
version: 16.13.2
- highlight.js:
- specifier: ^11.11.1
- version: 11.11.1
lucide-react:
specifier: ^1.7.0
version: 1.7.0(react@19.2.4)
@@ -68,6 +71,9 @@ importers:
rxjs:
specifier: ^7.8.2
version: 7.8.2
+ shiki:
+ specifier: ^4.0.2
+ version: 4.0.2
tailwind-merge:
specifier: ^3.5.0
version: 3.5.0
@@ -2102,6 +2108,37 @@ packages:
rollup:
optional: true
+ '@shikijs/core@4.0.2':
+ resolution: {integrity: sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw==}
+ engines: {node: '>=20'}
+
+ '@shikijs/engine-javascript@4.0.2':
+ resolution: {integrity: sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag==}
+ engines: {node: '>=20'}
+
+ '@shikijs/engine-oniguruma@4.0.2':
+ resolution: {integrity: sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg==}
+ engines: {node: '>=20'}
+
+ '@shikijs/langs@4.0.2':
+ resolution: {integrity: sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg==}
+ engines: {node: '>=20'}
+
+ '@shikijs/primitive@4.0.2':
+ resolution: {integrity: sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw==}
+ engines: {node: '>=20'}
+
+ '@shikijs/themes@4.0.2':
+ resolution: {integrity: sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA==}
+ engines: {node: '>=20'}
+
+ '@shikijs/types@4.0.2':
+ resolution: {integrity: sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg==}
+ engines: {node: '>=20'}
+
+ '@shikijs/vscode-textmate@10.0.2':
+ resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
+
'@sindresorhus/is@4.6.0':
resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==}
engines: {node: '>=10'}
@@ -3353,6 +3390,9 @@ packages:
hast-util-sanitize@5.0.2:
resolution: {integrity: sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==}
+ hast-util-to-html@9.0.5:
+ resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==}
+
hast-util-to-jsx-runtime@2.3.6:
resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==}
@@ -3371,10 +3411,6 @@ packages:
header-case@2.0.4:
resolution: {integrity: sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==}
- highlight.js@11.11.1:
- resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
- engines: {node: '>=12.0.0'}
-
html-encoding-sniffer@6.0.0:
resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
@@ -3944,6 +3980,12 @@ packages:
resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
engines: {node: '>=18'}
+ oniguruma-parser@0.12.1:
+ resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==}
+
+ oniguruma-to-es@4.3.5:
+ resolution: {integrity: sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ==}
+
open@10.2.0:
resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==}
engines: {node: '>=18'}
@@ -4184,6 +4226,15 @@ packages:
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
engines: {node: '>=8'}
+ regex-recursion@6.0.2:
+ resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==}
+
+ regex-utilities@2.3.0:
+ resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==}
+
+ regex@6.1.0:
+ resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==}
+
rehype-autolink-headings@7.1.0:
resolution: {integrity: sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw==}
@@ -4318,6 +4369,10 @@ packages:
resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==}
engines: {node: '>= 0.4'}
+ shiki@4.0.2:
+ resolution: {integrity: sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ==}
+ engines: {node: '>=20'}
+
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
@@ -6761,6 +6816,46 @@ snapshots:
estree-walker: 2.0.2
picomatch: 4.0.4
+ '@shikijs/core@4.0.2':
+ dependencies:
+ '@shikijs/primitive': 4.0.2
+ '@shikijs/types': 4.0.2
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.4
+ hast-util-to-html: 9.0.5
+
+ '@shikijs/engine-javascript@4.0.2':
+ dependencies:
+ '@shikijs/types': 4.0.2
+ '@shikijs/vscode-textmate': 10.0.2
+ oniguruma-to-es: 4.3.5
+
+ '@shikijs/engine-oniguruma@4.0.2':
+ dependencies:
+ '@shikijs/types': 4.0.2
+ '@shikijs/vscode-textmate': 10.0.2
+
+ '@shikijs/langs@4.0.2':
+ dependencies:
+ '@shikijs/types': 4.0.2
+
+ '@shikijs/primitive@4.0.2':
+ dependencies:
+ '@shikijs/types': 4.0.2
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.4
+
+ '@shikijs/themes@4.0.2':
+ dependencies:
+ '@shikijs/types': 4.0.2
+
+ '@shikijs/types@4.0.2':
+ dependencies:
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.4
+
+ '@shikijs/vscode-textmate@10.0.2': {}
+
'@sindresorhus/is@4.6.0': {}
'@standard-schema/spec@1.1.0': {}
@@ -8097,6 +8192,20 @@ snapshots:
'@ungap/structured-clone': 1.3.0
unist-util-position: 5.0.0
+ hast-util-to-html@9.0.5:
+ dependencies:
+ '@types/hast': 3.0.4
+ '@types/unist': 3.0.3
+ ccount: 2.0.1
+ comma-separated-tokens: 2.0.3
+ hast-util-whitespace: 3.0.0
+ html-void-elements: 3.0.0
+ mdast-util-to-hast: 13.2.1
+ property-information: 7.1.0
+ space-separated-tokens: 2.0.2
+ stringify-entities: 4.0.4
+ zwitch: 2.0.4
+
hast-util-to-jsx-runtime@2.3.6:
dependencies:
'@types/estree': 1.0.8
@@ -8148,8 +8257,6 @@ snapshots:
capital-case: 1.0.4
tslib: 2.8.1
- highlight.js@11.11.1: {}
-
html-encoding-sniffer@6.0.0:
dependencies:
'@exodus/bytes': 1.15.0
@@ -8867,6 +8974,14 @@ snapshots:
dependencies:
mimic-function: 5.0.1
+ oniguruma-parser@0.12.1: {}
+
+ oniguruma-to-es@4.3.5:
+ dependencies:
+ oniguruma-parser: 0.12.1
+ regex: 6.1.0
+ regex-recursion: 6.0.2
+
open@10.2.0:
dependencies:
default-browser: 5.5.0
@@ -9224,6 +9339,16 @@ snapshots:
indent-string: 4.0.0
strip-indent: 3.0.0
+ regex-recursion@6.0.2:
+ dependencies:
+ regex-utilities: 2.3.0
+
+ regex-utilities@2.3.0: {}
+
+ regex@6.1.0:
+ dependencies:
+ regex-utilities: 2.3.0
+
rehype-autolink-headings@7.1.0:
dependencies:
'@types/hast': 3.0.4
@@ -9402,6 +9527,17 @@ snapshots:
shell-quote@1.8.3: {}
+ shiki@4.0.2:
+ dependencies:
+ '@shikijs/core': 4.0.2
+ '@shikijs/engine-javascript': 4.0.2
+ '@shikijs/engine-oniguruma': 4.0.2
+ '@shikijs/langs': 4.0.2
+ '@shikijs/themes': 4.0.2
+ '@shikijs/types': 4.0.2
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.4
+
siginfo@2.0.0: {}
signal-exit@4.1.0: {}
@@ -4,6 +4,9 @@ import { FileViewer } from "./file-viewer";
const meta = {
component: FileViewer,
+ // Skip browser tests — Shiki's WASM engine doesn't load in Vitest browser mode.
+ // Snapshot tests (happy-dom) still cover this component.
+ tags: ["!test"],
} satisfies Meta<typeof FileViewer>;
export default meta;
@@ -1,13 +1,99 @@
// Syntax-highlighted file viewer with line numbers and copy button.
-// highlight.js is loaded lazily so it doesn't bloat the initial bundle.
+// Uses Shiki (VS Code's grammar engine) for accurate highlighting.
+// The highlighter is created lazily on first use and cached.
import { Copy } from "lucide-react";
import { useState, useEffect } from "react";
+import { createHighlighterCore, type HighlighterCore } from "shiki/core";
+import { createOnigurumaEngine } from "shiki/engine/oniguruma";
import type { GitBlob } from "@/__generated__/graphql";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
+// Lazy singleton — created once, reused across all FileViewer instances.
+let highlighterPromise: Promise<HighlighterCore> | null = null;
+
+function getHighlighter(): Promise<HighlighterCore> {
+ if (!highlighterPromise) {
+ highlighterPromise = createHighlighterCore({
+ themes: [
+ import("@shikijs/themes/github-light"),
+ import("@shikijs/themes/github-dark"),
+ ],
+ langs: [],
+ engine: createOnigurumaEngine(import("shiki/wasm")),
+ });
+ }
+ return highlighterPromise;
+}
+
+// Map file extensions / filenames → [shiki lang ID, lazy import].
+// Languages are loaded on demand — only the ones actually viewed get fetched.
+interface LangEntry {
+ id: string;
+ load: () => Promise<unknown>;
+}
+
+const LANG_MAP: Record<string, LangEntry> = {
+ // JavaScript / TypeScript
+ js: { id: "javascript", load: () => import("@shikijs/langs/javascript") },
+ mjs: { id: "javascript", load: () => import("@shikijs/langs/javascript") },
+ cjs: { id: "javascript", load: () => import("@shikijs/langs/javascript") },
+ jsx: { id: "jsx", load: () => import("@shikijs/langs/jsx") },
+ ts: { id: "typescript", load: () => import("@shikijs/langs/typescript") },
+ mts: { id: "typescript", load: () => import("@shikijs/langs/typescript") },
+ cts: { id: "typescript", load: () => import("@shikijs/langs/typescript") },
+ tsx: { id: "tsx", load: () => import("@shikijs/langs/tsx") },
+ // Web
+ html: { id: "html", load: () => import("@shikijs/langs/html") },
+ css: { id: "css", load: () => import("@shikijs/langs/css") },
+ scss: { id: "scss", load: () => import("@shikijs/langs/scss") },
+ // Data
+ json: { id: "json", load: () => import("@shikijs/langs/json") },
+ jsonc: { id: "jsonc", load: () => import("@shikijs/langs/jsonc") },
+ yaml: { id: "yaml", load: () => import("@shikijs/langs/yaml") },
+ yml: { id: "yaml", load: () => import("@shikijs/langs/yaml") },
+ toml: { id: "toml", load: () => import("@shikijs/langs/toml") },
+ xml: { id: "xml", load: () => import("@shikijs/langs/xml") },
+ svg: { id: "xml", load: () => import("@shikijs/langs/xml") },
+ graphql: { id: "graphql", load: () => import("@shikijs/langs/graphql") },
+ sql: { id: "sql", load: () => import("@shikijs/langs/sql") },
+ // Docs
+ md: { id: "markdown", load: () => import("@shikijs/langs/markdown") },
+ mdx: { id: "mdx", load: () => import("@shikijs/langs/mdx") },
+ // Shell
+ sh: { id: "bash", load: () => import("@shikijs/langs/bash") },
+ bash: { id: "bash", load: () => import("@shikijs/langs/bash") },
+ zsh: { id: "bash", load: () => import("@shikijs/langs/bash") },
+ // Systems
+ go: { id: "go", load: () => import("@shikijs/langs/go") },
+ rs: { id: "rust", load: () => import("@shikijs/langs/rust") },
+ c: { id: "c", load: () => import("@shikijs/langs/c") },
+ h: { id: "c", load: () => import("@shikijs/langs/c") },
+ cpp: { id: "cpp", load: () => import("@shikijs/langs/cpp") },
+ hpp: { id: "cpp", load: () => import("@shikijs/langs/cpp") },
+ // Scripting
+ py: { id: "python", load: () => import("@shikijs/langs/python") },
+ rb: { id: "ruby", load: () => import("@shikijs/langs/ruby") },
+ lua: { id: "lua", load: () => import("@shikijs/langs/lua") },
+ // JVM / Mobile
+ java: { id: "java", load: () => import("@shikijs/langs/java") },
+ kt: { id: "kotlin", load: () => import("@shikijs/langs/kotlin") },
+ swift: { id: "swift", load: () => import("@shikijs/langs/swift") },
+ // Infra
+ nix: { id: "nix", load: () => import("@shikijs/langs/nix") },
+ // Filenames
+ Dockerfile: { id: "dockerfile", load: () => import("@shikijs/langs/dockerfile") },
+ Makefile: { id: "makefile", load: () => import("@shikijs/langs/makefile") },
+};
+
+function getLangEntry(path: string): LangEntry | undefined {
+ const filename = path.split("/").pop() ?? "";
+ const ext = filename.split(".").pop() ?? "";
+ return LANG_MAP[ext] ?? LANG_MAP[filename];
+}
+
interface FileViewerProps {
blob: GitBlob | null;
}
@@ -34,17 +120,35 @@ export function FileViewer({ blob }: FileViewerProps) {
}
setHighlighted(null);
let cancelled = false;
- void import("highlight.js").then(({ default: hljs }) => {
+
+ void (async () => {
+ const highlighter = await getHighlighter();
+ const entry = getLangEntry(blob.path);
+
+ let lang = "text";
+ if (entry) {
+ try {
+ const langModule = await entry.load();
+ await highlighter.loadLanguage(langModule as Parameters<typeof highlighter.loadLanguage>[0]);
+ lang = entry.id;
+ } catch {
+ // Language not available — fall back to plain text
+ }
+ }
+
if (cancelled) return;
- const ext = blob.path.split(".").pop() ?? "";
- const result = hljs.getLanguage(ext)
- ? hljs.highlight(blob.text!, { language: ext })
- : hljs.highlightAuto(blob.text!);
+
+ const html = highlighter.codeToHtml(blob.text!, {
+ lang,
+ themes: { light: "github-light", dark: "github-dark" },
+ });
+
setHighlighted({
- html: result.value,
+ html,
lineCount: blob.text!.split("\n").length,
});
- });
+ })();
+
return () => {
cancelled = true;
};
@@ -80,22 +184,10 @@ export function FileViewer({ blob }: FileViewerProps) {
Binary file — {formatBytes(blob.size)}
</div>
) : (
- <div className="flex overflow-x-auto font-mono text-xs leading-5">
- <div
- className="border-border bg-muted/20 text-muted-foreground/50 border-r px-4 py-4 text-right select-none"
- aria-hidden
- >
- {Array.from({ length: lineCount }, (_, i) => (
- <div key={i}>{i + 1}</div>
- ))}
- </div>
- <pre className="flex-1 overflow-visible px-4 py-4">
- <code
- className="hljs !bg-transparent !p-0"
- dangerouslySetInnerHTML={{ __html: html }}
- />
- </pre>
- </div>
+ <div
+ className="overflow-x-auto font-mono text-xs leading-5 [&_.shiki]:!bg-transparent [&_pre]:px-4 [&_pre]:py-4"
+ dangerouslySetInnerHTML={{ __html: html }}
+ />
)}
</div>
);
@@ -16,10 +16,10 @@ exports[`Pagination/Default matches snapshot 1`] = `
<a
aria-current="page"
aria-disabled="true"
- class="inline-flex items-center justify-center whitespace-nowrap font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 hover:bg-accent hover:text-accent-foreground h-8 rounded-md px-3 text-xs text-muted-foreground gap-1 active"
+ class="inline-flex items-center justify-center whitespace-nowrap font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 hover:bg-accent hover:text-accent-foreground h-8 rounded-md px-3 text-xs text-muted-foreground gap-1 pointer-events-none opacity-50 active"
data-status="active"
- disabled=""
role="link"
+ tabindex="-1"
>
<svg
aria-hidden="true"
@@ -113,10 +113,10 @@ exports[`Pagination/LastPage matches snapshot 1`] = `
<a
aria-current="page"
aria-disabled="true"
- class="inline-flex items-center justify-center whitespace-nowrap font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 hover:bg-accent hover:text-accent-foreground h-8 rounded-md px-3 text-xs text-muted-foreground gap-1 active"
+ class="inline-flex items-center justify-center whitespace-nowrap font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 hover:bg-accent hover:text-accent-foreground h-8 rounded-md px-3 text-xs text-muted-foreground gap-1 pointer-events-none opacity-50 active"
data-status="active"
- disabled=""
role="link"
+ tabindex="-1"
>
Next
<svg
@@ -6,7 +6,7 @@ exports[`QueryInput/AsyncCompletions matches snapshot 1`] = `
class="relative flex flex-1 items-center rounded-md border border-input bg-background ring-offset-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2"
>
<div
- class="text-muted-foreground pointer-events-none absolute left-3 size-4 shrink-0"
+ class="text-muted-foreground pointer-events-none absolute left-3 flex size-4 shrink-0 items-center justify-center [&>svg]:size-4"
>
<svg
aria-hidden="true"
@@ -53,7 +53,7 @@ exports[`QueryInput/AutocompleteInteraction matches snapshot 1`] = `
class="relative flex flex-1 items-center rounded-md border border-input bg-background ring-offset-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2"
>
<div
- class="text-muted-foreground pointer-events-none absolute left-3 size-4 shrink-0"
+ class="text-muted-foreground pointer-events-none absolute left-3 flex size-4 shrink-0 items-center justify-center [&>svg]:size-4"
>
<svg
aria-hidden="true"
@@ -114,7 +114,7 @@ exports[`QueryInput/Default matches snapshot 1`] = `
class="relative flex flex-1 items-center rounded-md border border-input bg-background ring-offset-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2"
>
<div
- class="text-muted-foreground pointer-events-none absolute left-3 size-4 shrink-0"
+ class="text-muted-foreground pointer-events-none absolute left-3 flex size-4 shrink-0 items-center justify-center [&>svg]:size-4"
>
<svg
aria-hidden="true"
@@ -172,7 +172,7 @@ exports[`QueryInput/SyntaxOnly matches snapshot 1`] = `
class="relative flex flex-1 items-center rounded-md border border-input bg-background ring-offset-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2"
>
<div
- class="text-muted-foreground pointer-events-none absolute left-3 size-4 shrink-0"
+ class="text-muted-foreground pointer-events-none absolute left-3 flex size-4 shrink-0 items-center justify-center [&>svg]:size-4"
>
<svg
aria-hidden="true"
@@ -236,7 +236,7 @@ exports[`QueryInput/WithFilters matches snapshot 1`] = `
class="relative flex flex-1 items-center rounded-md border border-input bg-background ring-offset-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2"
>
<div
- class="text-muted-foreground pointer-events-none absolute left-3 size-4 shrink-0"
+ class="text-muted-foreground pointer-events-none absolute left-3 flex size-4 shrink-0 items-center justify-center [&>svg]:size-4"
>
<svg
aria-hidden="true"
@@ -1,6 +1,3 @@
-/* highlight.js theme must be imported before tailwind */
-@import "highlight.js/styles/github.css";
-
@import "tailwindcss";
@import "tw-animate-css";
@plugin "@tailwindcss/typography";
@@ -114,31 +111,3 @@ body {
color: var(--foreground);
}
-/* ── Dark-mode overrides for highlight.js (imported above) ──────────────── */
-.dark .hljs {
- background: hsl(220, 13%, 16%);
- color: hsl(220, 10%, 85%);
-}
-.dark .hljs-keyword,
-.dark .hljs-selector-tag,
-.dark .hljs-built_in {
- color: #ff7b72;
-}
-.dark .hljs-string,
-.dark .hljs-attr {
- color: #a5d6ff;
-}
-.dark .hljs-comment {
- color: hsl(220, 8%, 50%);
-}
-.dark .hljs-number,
-.dark .hljs-literal {
- color: #79c0ff;
-}
-.dark .hljs-title,
-.dark .hljs-name {
- color: #d2a8ff;
-}
-.dark .hljs-type {
- color: #ffa657;
-}
@@ -31,6 +31,8 @@ export default mergeConfig(
instances: [{ browser: "chromium" }],
},
setupFiles: ["./.storybook/vitest.setup.ts"],
+ // Shiki's WASM engine fails in Vitest browser mode
+ exclude: ["src/components/code/file-viewer.stories.tsx"],
},
},
// Snapshot tests (happy-dom, fast)