mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-13 08:57:04 +02:00
feat: add copy button and CSS variable-based syntax highlighting
- Add copy button next to file name in file viewer header (icon-only) - Use Shiki's createCssVariablesTheme for proper theme integration - Map Shiki CSS variables to our theme system variables - Syntax highlighting now works correctly across all themes (light/dark)
This commit is contained in:
@@ -1,7 +1,15 @@
|
||||
import { useState } from "react";
|
||||
import { FolderGit2, AlertCircle, FileQuestion, Binary } from "lucide-react";
|
||||
import {
|
||||
FolderGit2,
|
||||
AlertCircle,
|
||||
FileQuestion,
|
||||
Binary,
|
||||
Copy,
|
||||
Check,
|
||||
} from "lucide-react";
|
||||
import { useGitTree } from "@/hooks/useGitTree";
|
||||
import { useGitBlob } from "@/hooks/useGitBlob";
|
||||
import { useCopy } from "@/hooks/useCopy";
|
||||
import { FileTreeView } from "@/components/ui/FileTreeView";
|
||||
import { SyntaxHighlight } from "@/components/SyntaxHighlight";
|
||||
import { Skeleton } from "@/components/ui/skeleton/Skeleton";
|
||||
@@ -71,6 +79,9 @@ export function RepositoryFilesSection({
|
||||
? getExtension(selectedFile.name) || null
|
||||
: null;
|
||||
|
||||
// Copy functionality for file content
|
||||
const { copy, copied } = useCopy();
|
||||
|
||||
const handleFileSelect = (file: SelectedFile) => {
|
||||
setSelectedFile(file);
|
||||
};
|
||||
@@ -170,11 +181,25 @@ export function RepositoryFilesSection({
|
||||
) : fileContent ? (
|
||||
<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 className="flex items-center gap-2 min-w-0">
|
||||
<span className="text-xs font-mono text-muted-foreground truncate">
|
||||
{selectedFile.path}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copy(fileContent)}
|
||||
className="flex-shrink-0 p-1 rounded hover:bg-muted/50 text-muted-foreground hover:text-foreground transition-colors"
|
||||
title="Copy file content"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="size-3.5 text-success" />
|
||||
) : (
|
||||
<Copy className="size-3.5" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{rawContent && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
<span className="text-xs text-muted-foreground flex-shrink-0">
|
||||
{formatSize(rawContent.length)}
|
||||
</span>
|
||||
)}
|
||||
|
||||
155
src/lib/shiki.ts
155
src/lib/shiki.ts
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
createHighlighterCore,
|
||||
createCssVariablesTheme,
|
||||
type HighlighterCore,
|
||||
type ShikiTransformer,
|
||||
} from "shiki/core";
|
||||
import { createOnigurumaEngine } from "shiki/engine/oniguruma";
|
||||
|
||||
@@ -12,146 +12,16 @@ const loadedLanguages = new Set<string>();
|
||||
const failedLanguages = new Set<string>();
|
||||
|
||||
/**
|
||||
* Transformer that cleans up Shiki output for our styling
|
||||
* Keeps inline colors from the theme but removes backgrounds
|
||||
* CSS Variables theme for Shiki
|
||||
* This outputs CSS custom properties that we map to our theme variables.
|
||||
* The actual colors are defined in shiki-theme.css using our theme system.
|
||||
*/
|
||||
const cleanupTransformer: ShikiTransformer = {
|
||||
name: "cleanup-transformer",
|
||||
pre(node) {
|
||||
// Remove background color from pre, let CSS handle it
|
||||
if (node.properties?.style) {
|
||||
const style = node.properties.style as string;
|
||||
node.properties.style = style.replace(/background-color:[^;]+;?/g, "");
|
||||
}
|
||||
},
|
||||
code(node) {
|
||||
// Remove background from code element, keep color
|
||||
if (node.properties?.style) {
|
||||
const style = node.properties.style as string;
|
||||
node.properties.style = style.replace(/background-color:[^;]+;?/g, "");
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Minimal theme - we'll override colors via CSS
|
||||
* Using high-contrast colors as fallback if CSS fails
|
||||
*/
|
||||
const minimalTheme = {
|
||||
name: "grimoire",
|
||||
type: "dark" as const,
|
||||
colors: {
|
||||
"editor.background": "transparent",
|
||||
"editor.foreground": "#e6edf3",
|
||||
},
|
||||
tokenColors: [
|
||||
{
|
||||
scope: ["comment", "punctuation.definition.comment"],
|
||||
settings: { foreground: "#8b949e" },
|
||||
},
|
||||
{
|
||||
scope: ["string", "string.quoted"],
|
||||
settings: { foreground: "#a5d6ff" },
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"keyword",
|
||||
"storage",
|
||||
"storage.type",
|
||||
"storage.modifier",
|
||||
"keyword.operator",
|
||||
"keyword.control",
|
||||
],
|
||||
settings: { foreground: "#f0f0f0" },
|
||||
},
|
||||
{
|
||||
scope: ["entity.name.function", "support.function", "meta.function-call"],
|
||||
settings: { foreground: "#e6edf3" },
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"entity.name.class",
|
||||
"entity.name.type",
|
||||
"support.class",
|
||||
"support.type",
|
||||
],
|
||||
settings: { foreground: "#f0f0f0" },
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"constant",
|
||||
"constant.numeric",
|
||||
"constant.language",
|
||||
"constant.character",
|
||||
],
|
||||
settings: { foreground: "#79c0ff" },
|
||||
},
|
||||
{
|
||||
scope: ["variable", "variable.parameter", "variable.other"],
|
||||
settings: { foreground: "#e6edf3" },
|
||||
},
|
||||
{
|
||||
scope: ["punctuation", "meta.brace"],
|
||||
settings: { foreground: "#c9d1d9" },
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"variable.other.property",
|
||||
"entity.other.attribute-name",
|
||||
"support.type.property-name",
|
||||
],
|
||||
settings: { foreground: "#e6edf3" },
|
||||
},
|
||||
{
|
||||
scope: ["entity.name.tag", "support.class.component"],
|
||||
settings: { foreground: "#7ee787" },
|
||||
},
|
||||
{
|
||||
scope: ["support.type.property-name.json"],
|
||||
settings: { foreground: "#a5d6ff" },
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"markup.deleted",
|
||||
"punctuation.definition.deleted",
|
||||
"meta.diff.header.from-file",
|
||||
],
|
||||
settings: { foreground: "#ffa198" },
|
||||
},
|
||||
{
|
||||
scope: [
|
||||
"markup.inserted",
|
||||
"punctuation.definition.inserted",
|
||||
"meta.diff.header.to-file",
|
||||
],
|
||||
settings: { foreground: "#7ee787" },
|
||||
},
|
||||
{
|
||||
scope: ["markup.changed", "meta.diff.range", "meta.diff.header"],
|
||||
settings: { foreground: "#a5d6ff" },
|
||||
},
|
||||
{
|
||||
scope: ["markup.heading", "entity.name.section"],
|
||||
settings: { foreground: "#f0f0f0" },
|
||||
},
|
||||
{
|
||||
scope: ["markup.bold"],
|
||||
settings: { fontStyle: "bold" },
|
||||
},
|
||||
{
|
||||
scope: ["markup.italic"],
|
||||
settings: { fontStyle: "italic" },
|
||||
},
|
||||
{
|
||||
scope: ["markup.underline.link"],
|
||||
settings: { foreground: "#a5d6ff" },
|
||||
},
|
||||
{
|
||||
scope: ["markup.inline.raw", "markup.raw"],
|
||||
settings: { foreground: "#a5d6ff" },
|
||||
},
|
||||
],
|
||||
};
|
||||
const cssVarsTheme = createCssVariablesTheme({
|
||||
name: "css-variables",
|
||||
variablePrefix: "--shiki-",
|
||||
variableDefaults: {},
|
||||
fontStyle: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* Language alias mapping (file extensions and common names to Shiki IDs)
|
||||
@@ -308,7 +178,7 @@ export async function getHighlighter(): Promise<HighlighterCore> {
|
||||
|
||||
if (!highlighterPromise) {
|
||||
highlighterPromise = createHighlighterCore({
|
||||
themes: [minimalTheme],
|
||||
themes: [cssVarsTheme],
|
||||
langs: [
|
||||
import("shiki/langs/javascript.mjs"),
|
||||
import("shiki/langs/typescript.mjs"),
|
||||
@@ -371,8 +241,7 @@ export async function highlightCode(
|
||||
|
||||
return hl.codeToHtml(code, {
|
||||
lang: effectiveLang,
|
||||
theme: "grimoire",
|
||||
transformers: [cleanupTransformer],
|
||||
theme: "css-variables",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,40 @@
|
||||
/* ==========================================================================
|
||||
Shiki Syntax Highlighting - CSS Variable Based Theming
|
||||
|
||||
Token colors are defined using CSS variables from the theme system,
|
||||
Maps Shiki's CSS variables to our theme system variables,
|
||||
allowing automatic adaptation to light/dark mode.
|
||||
========================================================================== */
|
||||
|
||||
/* Base container styling */
|
||||
/* ==========================================================================
|
||||
Shiki CSS Variables -> Theme Variables Mapping
|
||||
These are set by Shiki's css-variables theme and we map them to our theme
|
||||
========================================================================== */
|
||||
|
||||
:root {
|
||||
/* Default/fallback - uses foreground */
|
||||
--shiki-color-text: hsl(var(--foreground));
|
||||
--shiki-color-background: transparent;
|
||||
|
||||
/* Token colors - mapped to our syntax theme variables */
|
||||
--shiki-token-constant: hsl(var(--syntax-constant));
|
||||
--shiki-token-string: hsl(var(--syntax-string));
|
||||
--shiki-token-comment: hsl(var(--syntax-comment));
|
||||
--shiki-token-keyword: hsl(var(--syntax-keyword));
|
||||
--shiki-token-parameter: hsl(var(--syntax-variable));
|
||||
--shiki-token-function: hsl(var(--syntax-function));
|
||||
--shiki-token-string-expression: hsl(var(--syntax-string));
|
||||
--shiki-token-punctuation: hsl(var(--syntax-punctuation));
|
||||
--shiki-token-link: hsl(var(--syntax-string));
|
||||
|
||||
/* Diff colors */
|
||||
--shiki-token-deleted: hsl(var(--diff-deleted));
|
||||
--shiki-token-inserted: hsl(var(--diff-inserted));
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Base container styling
|
||||
========================================================================== */
|
||||
|
||||
.shiki-container {
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
@@ -36,55 +65,6 @@
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Token Classes - Styled with CSS Variables
|
||||
========================================================================== */
|
||||
|
||||
/* Base token - inherits foreground color */
|
||||
.shiki-token {
|
||||
color: hsl(var(--syntax-variable));
|
||||
}
|
||||
|
||||
/* Comments */
|
||||
.shiki-comment {
|
||||
color: hsl(var(--syntax-comment));
|
||||
}
|
||||
|
||||
/* Strings */
|
||||
.shiki-string {
|
||||
color: hsl(var(--syntax-string));
|
||||
}
|
||||
|
||||
/* Keywords (if, else, return, function, etc.) */
|
||||
.shiki-keyword {
|
||||
color: hsl(var(--syntax-keyword));
|
||||
}
|
||||
|
||||
/* Constants and numbers */
|
||||
.shiki-constant {
|
||||
color: hsl(var(--syntax-constant));
|
||||
}
|
||||
|
||||
/* Punctuation */
|
||||
.shiki-punctuation {
|
||||
color: hsl(var(--syntax-punctuation));
|
||||
}
|
||||
|
||||
/* Tags (HTML/JSX) */
|
||||
.shiki-tag {
|
||||
color: hsl(var(--syntax-tag));
|
||||
}
|
||||
|
||||
/* Diff - deleted lines */
|
||||
.shiki-deleted {
|
||||
color: hsl(var(--diff-deleted));
|
||||
}
|
||||
|
||||
/* Diff - inserted lines */
|
||||
.shiki-inserted {
|
||||
color: hsl(var(--diff-inserted));
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Loading State
|
||||
========================================================================== */
|
||||
@@ -108,29 +88,20 @@
|
||||
========================================================================== */
|
||||
|
||||
/* Diff-specific styling - block-level backgrounds for inserted/deleted */
|
||||
.shiki-container .line:has(.shiki-deleted) {
|
||||
.shiki-container .line:has([style*="--shiki-token-deleted"]) {
|
||||
background: hsl(var(--diff-deleted-bg));
|
||||
display: block;
|
||||
margin: 0 -1rem;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.shiki-container .line:has(.shiki-inserted) {
|
||||
.shiki-container .line:has([style*="--shiki-token-inserted"]) {
|
||||
background: hsl(var(--diff-inserted-bg));
|
||||
display: block;
|
||||
margin: 0 -1rem;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
/* Hunk headers (@@ lines) */
|
||||
.shiki-container .line:has([class*="meta"]) {
|
||||
background: hsl(var(--diff-meta-bg));
|
||||
display: block;
|
||||
margin: 0 -1rem;
|
||||
padding: 0 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Line Numbers (optional)
|
||||
========================================================================== */
|
||||
|
||||
Reference in New Issue
Block a user