mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-15 12:53:31 +08:00
feat: migrate code blocks to shiki for VS Code-quality syntax highlighting
Replace prism-react-renderer with shiki/react-shiki in CodeViewer for dramatically better tokenization (50+ TextMate scopes vs ~15 Prism tokens). Custom warm-code theme with improved comment contrast (#5C6B7A, ~5:1 ratio) and warm off-white background (#FDFCF9). Update DiffViewer to use matching warm color palette with CSS variables instead of hardcoded GitHub colors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ad4ba88a62
commit
f81fe41797
@ -21,6 +21,8 @@
|
||||
"react": "^18.3.1",
|
||||
"react-diff-viewer-continued": "^4.2.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-shiki": "^0.9.2",
|
||||
"shiki": "^4.0.2",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
3832
desktop/pnpm-lock.yaml
generated
Normal file
3832
desktop/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,6 @@
|
||||
import { useState } from 'react'
|
||||
import { Highlight, themes } from 'prism-react-renderer'
|
||||
import { ShikiHighlighter } from 'react-shiki'
|
||||
import 'react-shiki/css'
|
||||
import { CopyButton } from '../shared/CopyButton'
|
||||
|
||||
type Props = {
|
||||
@ -9,6 +10,41 @@ type Props = {
|
||||
showLineNumbers?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom warm-toned TextMate theme — uses VS Code-quality tokenization
|
||||
* while harmonizing with the app's cream/terra-cotta design system.
|
||||
*/
|
||||
const warmCodeTheme = {
|
||||
name: 'warm-code',
|
||||
type: 'light' as const,
|
||||
fg: '#24201E',
|
||||
bg: 'transparent',
|
||||
tokenColors: [
|
||||
{ scope: ['comment', 'punctuation.definition.comment'], settings: { foreground: '#5C6B7A', fontStyle: 'italic' } },
|
||||
{ scope: ['string', 'string.quoted', 'string.template', 'string.other.link'], settings: { foreground: '#437220' } },
|
||||
{ scope: ['string.regexp'], settings: { foreground: '#C15F3C' } },
|
||||
{ scope: ['keyword', 'keyword.control', 'storage', 'storage.type', 'storage.modifier'], settings: { foreground: '#B8533B' } },
|
||||
{ scope: ['keyword.operator'], settings: { foreground: '#B8533B' } },
|
||||
{ scope: ['entity.name.function', 'support.function'], settings: { foreground: '#1D5A8C' } },
|
||||
{ scope: ['entity.name.type', 'support.type', 'support.class', 'entity.name.class', 'entity.other.inherited-class'], settings: { foreground: '#7E5520' } },
|
||||
{ scope: ['entity.name.type.parameter'], settings: { foreground: '#1B7A6A' } },
|
||||
{ scope: ['variable', 'variable.other', 'variable.other.readwrite'], settings: { foreground: '#24201E' } },
|
||||
{ scope: ['variable.parameter'], settings: { foreground: '#5C3D2E' } },
|
||||
{ scope: ['variable.other.property', 'support.type.property-name', 'meta.object-literal.key'], settings: { foreground: '#7A3E20' } },
|
||||
{ scope: ['variable.other.constant', 'variable.other.enummember'], settings: { foreground: '#7E5520' } },
|
||||
{ scope: ['constant.numeric', 'constant.language'], settings: { foreground: '#1B7A6A' } },
|
||||
{ scope: ['punctuation', 'meta.brace', 'meta.bracket'], settings: { foreground: '#5C504A' } },
|
||||
{ scope: ['entity.name.tag', 'punctuation.definition.tag'], settings: { foreground: '#B8533B' } },
|
||||
{ scope: ['entity.other.attribute-name'], settings: { foreground: '#7A3E20' } },
|
||||
{ scope: ['meta.decorator', 'punctuation.decorator'], settings: { foreground: '#7E5520' } },
|
||||
{ scope: ['markup.inserted', 'punctuation.definition.inserted'], settings: { foreground: '#1A7F37' } },
|
||||
{ scope: ['markup.deleted', 'punctuation.definition.deleted'], settings: { foreground: '#CF222E' } },
|
||||
{ scope: ['markup.heading', 'entity.name.section'], settings: { foreground: '#1D5A8C', fontStyle: 'bold' } },
|
||||
{ scope: ['markup.bold'], settings: { fontStyle: 'bold' } },
|
||||
{ scope: ['markup.italic'], settings: { fontStyle: 'italic' } },
|
||||
],
|
||||
}
|
||||
|
||||
export function CodeViewer({ code, language, maxLines = 20, showLineNumbers = true }: Props) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
|
||||
@ -22,71 +58,44 @@ export function CodeViewer({ code, language, maxLines = 20, showLineNumbers = tr
|
||||
const showExpandToggle = allLines.length > maxLines
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-lg border border-[#d0d7de] bg-[#f6f8fa] text-[#24292f]">
|
||||
<div className="flex items-center justify-between border-b border-[#d0d7de] bg-white px-3 py-1.5 text-[11px] text-[#57606a]">
|
||||
<div className="overflow-hidden rounded-[var(--radius-lg)] border border-[var(--color-outline-variant)]/50 bg-[var(--color-surface-container-low)]">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b border-[var(--color-outline-variant)]/40 bg-[var(--color-surface-container)] px-3 py-1.5 text-[11px] text-[var(--color-text-tertiary)]">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-semibold uppercase tracking-[0.14em] text-[#57606a]">{languageLabel}</span>
|
||||
<span className="font-semibold uppercase tracking-[0.14em]">{languageLabel}</span>
|
||||
<span>{lineCountLabel}</span>
|
||||
</div>
|
||||
<CopyButton
|
||||
text={code}
|
||||
className="rounded-md border border-[#d0d7de] bg-white px-2 py-1 text-[11px] text-[#57606a] transition-colors hover:bg-[#f3f4f6] hover:text-[#24292f]"
|
||||
className="rounded-md border border-[var(--color-outline-variant)]/40 bg-[var(--color-surface-container-lowest)] px-2 py-1 text-[11px] text-[var(--color-text-tertiary)] transition-colors hover:bg-[var(--color-surface-container-high)] hover:text-[var(--color-text-primary)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="max-h-[420px] overflow-auto">
|
||||
<Highlight theme={themes.github} code={visibleCode} language={language || 'text'}>
|
||||
{({ tokens, getLineProps, getTokenProps }) => (
|
||||
<pre className="m-0 bg-white p-0 font-[var(--font-mono)] text-[12px] leading-[1.3]">
|
||||
{tokens.map((line, i) => {
|
||||
const lineProps = getLineProps({ line })
|
||||
const hasLanguage = !!language && language !== 'text'
|
||||
return effectiveShowLineNumbers ? (
|
||||
<div
|
||||
key={i}
|
||||
{...lineProps}
|
||||
className="grid grid-cols-[3rem,minmax(0,1fr)] gap-0 hover:bg-[#f6f8fa]/50"
|
||||
style={undefined}
|
||||
>
|
||||
<span className="select-none border-r border-[#eaeef2] bg-[#fafbfc] px-2 py-px text-right text-[11px] text-[#8b949e]">
|
||||
{i + 1}
|
||||
</span>
|
||||
<span className="overflow-hidden px-3 py-px whitespace-pre-wrap break-words">
|
||||
{line.map((token, key) => (
|
||||
<span key={key} {...getTokenProps({ token })} />
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
key={i}
|
||||
{...lineProps}
|
||||
className="hover:bg-[#f6f8fa]/50"
|
||||
style={undefined}
|
||||
>
|
||||
<span className={`block px-3 py-px whitespace-pre-wrap break-words ${!hasLanguage ? 'text-[#24292f]' : ''}`}>
|
||||
{hasLanguage ? (
|
||||
line.map((token, key) => (
|
||||
<span key={key} {...getTokenProps({ token })} />
|
||||
))
|
||||
) : (
|
||||
line.map((token, key) => (
|
||||
<span key={key} {...getTokenProps({ token })} style={{ color: '#24292f' }} />
|
||||
))
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</pre>
|
||||
)}
|
||||
</Highlight>
|
||||
{/* Code area */}
|
||||
<div className="code-viewer-area max-h-[420px] overflow-auto bg-[#FDFCF9]">
|
||||
<ShikiHighlighter
|
||||
language={language || 'text'}
|
||||
theme={warmCodeTheme}
|
||||
showLineNumbers={effectiveShowLineNumbers}
|
||||
showLanguage={false}
|
||||
addDefaultStyles={false}
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: '0.5rem 0',
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontSize: '12px',
|
||||
lineHeight: '1.45',
|
||||
}}
|
||||
>
|
||||
{visibleCode}
|
||||
</ShikiHighlighter>
|
||||
</div>
|
||||
|
||||
{/* Expand/collapse toggle */}
|
||||
{showExpandToggle && (
|
||||
<button
|
||||
onClick={() => setExpanded((value) => !value)}
|
||||
className="w-full border-t border-[#d0d7de] bg-[#f6f8fa] py-1.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-[#57606a] transition-colors hover:bg-[#eaeef2] hover:text-[#24292f]"
|
||||
className="w-full border-t border-[var(--color-outline-variant)]/40 bg-[var(--color-surface-container)] py-1.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-[var(--color-text-tertiary)] transition-colors hover:bg-[var(--color-surface-container-high)] hover:text-[var(--color-text-primary)]"
|
||||
>
|
||||
{expanded ? 'Collapse' : `Show ${allLines.length - maxLines} more lines`}
|
||||
</button>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer-continued'
|
||||
import { Highlight, themes } from 'prism-react-renderer'
|
||||
import { Highlight, type PrismTheme } from 'prism-react-renderer'
|
||||
import { CopyButton } from '../shared/CopyButton'
|
||||
|
||||
type Props = {
|
||||
@ -20,9 +20,33 @@ function inferLanguage(filePath: string): string {
|
||||
return langMap[ext ?? ''] || 'text'
|
||||
}
|
||||
|
||||
/** Shared warm syntax theme — must stay in sync with CodeViewer */
|
||||
const warmSyntaxTheme: PrismTheme = {
|
||||
plain: {
|
||||
color: '#24201E',
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
styles: [
|
||||
{ types: ['comment', 'prolog', 'doctype', 'cdata'], style: { color: '#8C7E75', fontStyle: 'italic' as const } },
|
||||
{ types: ['string', 'attr-value', 'template-string'], style: { color: '#437220' } },
|
||||
{ types: ['keyword', 'selector', 'important', 'atrule'], style: { color: '#B8533B' } },
|
||||
{ types: ['function'], style: { color: '#1D5A8C' } },
|
||||
{ types: ['tag'], style: { color: '#B8533B' } },
|
||||
{ types: ['number', 'boolean'], style: { color: '#1B7A6A' } },
|
||||
{ types: ['operator'], style: { color: '#24201E' } },
|
||||
{ types: ['punctuation'], style: { color: '#5C504A' } },
|
||||
{ types: ['variable', 'parameter'], style: { color: '#24201E' } },
|
||||
{ types: ['property', 'attr-name'], style: { color: '#7A3E20' } },
|
||||
{ types: ['builtin', 'class-name', 'constant', 'symbol'], style: { color: '#7E5520' } },
|
||||
{ types: ['regex'], style: { color: '#C15F3C' } },
|
||||
{ types: ['inserted'], style: { color: '#1A7F37' } },
|
||||
{ types: ['deleted'], style: { color: '#CF222E' } },
|
||||
],
|
||||
}
|
||||
|
||||
function highlightSyntax(str: string, language: string) {
|
||||
return (
|
||||
<Highlight theme={themes.github} code={str} language={language}>
|
||||
<Highlight theme={warmSyntaxTheme} code={str} language={language}>
|
||||
{({ tokens, getTokenProps }) => (
|
||||
<>
|
||||
{tokens.map((line, i) => (
|
||||
@ -41,36 +65,36 @@ function highlightSyntax(str: string, language: string) {
|
||||
const diffStyles = {
|
||||
variables: {
|
||||
light: {
|
||||
diffViewerBackground: '#ffffff',
|
||||
diffViewerColor: '#24292f',
|
||||
addedBackground: '#dafbe1',
|
||||
addedColor: '#24292f',
|
||||
removedBackground: '#ffebe9',
|
||||
removedColor: '#24292f',
|
||||
wordAddedBackground: '#abf2bc',
|
||||
wordRemovedBackground: '#ff818266',
|
||||
addedGutterBackground: '#ccffd8',
|
||||
removedGutterBackground: '#ffd7d5',
|
||||
gutterBackground: '#f6f8fa',
|
||||
gutterBackgroundDark: '#f0f1f3',
|
||||
highlightBackground: '#fffbdd',
|
||||
highlightGutterBackground: '#fff5b1',
|
||||
codeFoldGutterBackground: '#dbedff',
|
||||
codeFoldBackground: '#f1f8ff',
|
||||
emptyLineBackground: '#fafbfc',
|
||||
gutterColor: '#8b949e',
|
||||
addedGutterColor: '#1a7f37',
|
||||
removedGutterColor: '#cf222e',
|
||||
codeFoldContentColor: '#57606a',
|
||||
diffViewerTitleBackground: '#fafbfc',
|
||||
diffViewerTitleColor: '#57606a',
|
||||
diffViewerTitleBorderColor: '#d0d7de',
|
||||
diffViewerBackground: '#FDFCF9',
|
||||
diffViewerColor: '#3B3330',
|
||||
addedBackground: '#E8F5E2',
|
||||
addedColor: '#3B3330',
|
||||
removedBackground: '#FDECEA',
|
||||
removedColor: '#3B3330',
|
||||
wordAddedBackground: '#B8E4A8',
|
||||
wordRemovedBackground: '#F5B8B4',
|
||||
addedGutterBackground: '#D4EDCA',
|
||||
removedGutterBackground: '#F9D4D0',
|
||||
gutterBackground: '#F4F4F0',
|
||||
gutterBackgroundDark: '#EFEEEA',
|
||||
highlightBackground: '#FFF5D6',
|
||||
highlightGutterBackground: '#FFECB3',
|
||||
codeFoldGutterBackground: '#E4EDF6',
|
||||
codeFoldBackground: '#EDF4FB',
|
||||
emptyLineBackground: '#F4F4F0',
|
||||
gutterColor: '#87736D',
|
||||
addedGutterColor: '#1A7F37',
|
||||
removedGutterColor: '#CF222E',
|
||||
codeFoldContentColor: '#87736D',
|
||||
diffViewerTitleBackground: '#F4F4F0',
|
||||
diffViewerTitleColor: '#87736D',
|
||||
diffViewerTitleBorderColor: '#DAC1BA',
|
||||
},
|
||||
},
|
||||
diffContainer: {
|
||||
borderRadius: '0',
|
||||
fontSize: '12px',
|
||||
lineHeight: '1.3',
|
||||
lineHeight: '1.45',
|
||||
fontFamily: 'var(--font-mono)',
|
||||
},
|
||||
line: {
|
||||
@ -96,24 +120,26 @@ export function DiffViewer({ filePath, oldString, newString }: Props) {
|
||||
const deletions = oldLines.filter((l, i) => l !== (newLines[i] ?? null)).length
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-lg border border-[#d0d7de] bg-[#f6f8fa] text-[#24292f]">
|
||||
<div className="flex items-center justify-between border-b border-[#d0d7de] bg-white px-3 py-1.5">
|
||||
<div className="overflow-hidden rounded-[var(--radius-lg)] border border-[var(--color-outline-variant)]/50 bg-[var(--color-surface-container-low)]">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b border-[var(--color-outline-variant)]/40 bg-[var(--color-surface-container)] px-3 py-1.5">
|
||||
<div className="min-w-0">
|
||||
<div className="truncate font-[var(--font-mono)] text-[11px] text-[#57606a]">
|
||||
<div className="truncate font-[var(--font-mono)] text-[11px] text-[var(--color-text-tertiary)]">
|
||||
{filePath}
|
||||
</div>
|
||||
<div className="mt-1 flex items-center gap-2 text-[10px] uppercase tracking-[0.14em]">
|
||||
<span className="rounded-full bg-[#dafbe1] px-2 py-0.5 text-[#1a7f37]">+{additions}</span>
|
||||
<span className="rounded-full bg-[#ffebe9] px-2 py-0.5 text-[#cf222e]">-{deletions}</span>
|
||||
<span className="rounded-full bg-[#E8F5E2] px-2 py-0.5 text-[#1A7F37]">+{additions}</span>
|
||||
<span className="rounded-full bg-[#FDECEA] px-2 py-0.5 text-[#CF222E]">-{deletions}</span>
|
||||
</div>
|
||||
</div>
|
||||
<CopyButton
|
||||
text={`--- ${filePath}\n+++ ${filePath}`}
|
||||
label="Copy path"
|
||||
className="rounded-md border border-[#d0d7de] bg-white px-2 py-1 text-[11px] text-[#57606a] transition-colors hover:bg-[#f3f4f6] hover:text-[#24292f]"
|
||||
className="rounded-md border border-[var(--color-outline-variant)]/40 bg-[var(--color-surface-container-lowest)] px-2 py-1 text-[11px] text-[var(--color-text-tertiary)] transition-colors hover:bg-[var(--color-surface-container-high)] hover:text-[var(--color-text-primary)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Diff area */}
|
||||
<div className="max-h-[400px] overflow-auto">
|
||||
<ReactDiffViewer
|
||||
oldValue={oldString}
|
||||
|
||||
@ -1,7 +1,30 @@
|
||||
@import "tailwindcss";
|
||||
@plugin "@tailwindcss/typography";
|
||||
|
||||
/* Code highlighting is handled by prism-react-renderer (inline styles) */
|
||||
/* Code highlighting is handled by shiki/react-shiki (inline styles + CSS line numbers) */
|
||||
|
||||
/* ─── Shiki code viewer ───────────────────────────────────────── */
|
||||
.code-viewer-area .line {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
padding: 1px 12px;
|
||||
}
|
||||
|
||||
.code-viewer-area .has-line-numbers .line {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.code-viewer-area .line:hover {
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.code-viewer-area .has-line-numbers {
|
||||
--line-numbers-foreground: rgba(135, 115, 109, 0.5);
|
||||
--line-numbers-width: 2.5ch;
|
||||
--line-numbers-padding-left: 1ch;
|
||||
--line-numbers-padding-right: 1.5ch;
|
||||
--line-numbers-font-size: 11px;
|
||||
}
|
||||
|
||||
/* ─── Tailwind Theme Override (matches prototype design system) ─── */
|
||||
@theme {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user