mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-19 13:33:35 +08:00
Dark-mode Markdown text was still inheriting Tailwind Typography's default prose color, so normal and italic text could render dark while strong text used the app theme token. Map markdown prose variables to the desktop theme tokens, move code-viewer hover and line-number styling onto theme tokens, and make Mermaid/Diff rendering follow the active app theme. Write/Edit/tool result and workspace render paths were audited; their core code and diff surfaces already use theme tokens, with the DiffViewer theme handoff tightened here. Constraint: Mermaid theme customization requires the base theme and concrete color values, so CSS theme tokens are resolved to hex before rendering. Rejected: Add per-element prose utility overrides only | misses Tailwind Typography defaults for quotes, counters, tables, and future prose elements Confidence: high Scope-risk: moderate Directive: Keep markdown, code, diff, and diagram colors routed through shared desktop theme tokens; do not reintroduce fixed light backgrounds in chat renderers. Tested: cd desktop && bun run test -- --run src/theme/globals.test.ts src/components/markdown/MarkdownRenderer.test.tsx src/components/chat/CodeViewer.test.tsx src/components/chat/MermaidRenderer.test.tsx src/components/chat/DiffViewer.test.tsx src/components/chat/chatBlocks.test.tsx src/components/workspace/WorkspacePanel.test.tsx Tested: cd desktop && bun run lint Tested: cd desktop && bun run build Tested: git diff --check Not-tested: Full Tauri live desktop session with real provider output; browser broad-audit screenshot capture timed out, but computed colors were captured for all three themes.
160 lines
6.6 KiB
TypeScript
160 lines
6.6 KiB
TypeScript
import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer-continued'
|
|
import { Highlight, type PrismTheme } from 'prism-react-renderer'
|
|
import { CopyButton } from '../shared/CopyButton'
|
|
import { useUIStore } from '../../stores/uiStore'
|
|
|
|
type Props = {
|
|
filePath: string
|
|
oldString: string
|
|
newString: string
|
|
}
|
|
|
|
function inferLanguage(filePath: string): string {
|
|
const ext = filePath.split('.').pop()?.toLowerCase()
|
|
const langMap: Record<string, string> = {
|
|
ts: 'typescript', tsx: 'tsx', js: 'javascript', jsx: 'jsx',
|
|
py: 'python', rs: 'rust', go: 'go', rb: 'ruby',
|
|
json: 'json', yaml: 'yaml', yml: 'yaml', toml: 'toml',
|
|
md: 'markdown', css: 'css', html: 'markup', xml: 'markup',
|
|
sql: 'sql', sh: 'bash', bash: 'bash', zsh: 'bash',
|
|
}
|
|
return langMap[ext ?? ''] || 'text'
|
|
}
|
|
|
|
/** Shared warm syntax theme — must stay in sync with CodeViewer */
|
|
const warmSyntaxTheme: PrismTheme = {
|
|
plain: {
|
|
color: 'var(--color-code-fg)',
|
|
backgroundColor: 'transparent',
|
|
},
|
|
styles: [
|
|
{ types: ['comment', 'prolog', 'doctype', 'cdata'], style: { color: 'var(--color-code-comment)', fontStyle: 'italic' as const } },
|
|
{ types: ['string', 'attr-value', 'template-string'], style: { color: 'var(--color-code-string)' } },
|
|
{ types: ['keyword', 'selector', 'important', 'atrule'], style: { color: 'var(--color-code-keyword)' } },
|
|
{ types: ['function'], style: { color: 'var(--color-code-function)' } },
|
|
{ types: ['tag'], style: { color: 'var(--color-code-keyword)' } },
|
|
{ types: ['number', 'boolean'], style: { color: 'var(--color-code-number)' } },
|
|
{ types: ['operator'], style: { color: 'var(--color-code-fg)' } },
|
|
{ types: ['punctuation'], style: { color: 'var(--color-code-punctuation)' } },
|
|
{ types: ['variable', 'parameter'], style: { color: 'var(--color-code-fg)' } },
|
|
{ types: ['property', 'attr-name'], style: { color: 'var(--color-code-property)' } },
|
|
{ types: ['builtin', 'class-name', 'constant', 'symbol'], style: { color: 'var(--color-code-type)' } },
|
|
{ types: ['regex'], style: { color: 'var(--color-primary-container)' } },
|
|
{ types: ['inserted'], style: { color: 'var(--color-code-inserted)' } },
|
|
{ types: ['deleted'], style: { color: 'var(--color-code-deleted)' } },
|
|
],
|
|
}
|
|
|
|
function highlightSyntax(str: string, language: string) {
|
|
return (
|
|
<Highlight theme={warmSyntaxTheme} code={str} language={language}>
|
|
{({ tokens, getTokenProps }) => (
|
|
<>
|
|
{tokens.map((line, i) => (
|
|
<span key={i}>
|
|
{line.map((token, key) => (
|
|
<span key={key} {...getTokenProps({ token })} />
|
|
))}
|
|
</span>
|
|
))}
|
|
</>
|
|
)}
|
|
</Highlight>
|
|
)
|
|
}
|
|
|
|
const diffStyles = {
|
|
variables: {
|
|
light: {
|
|
diffViewerBackground: 'var(--color-code-bg)',
|
|
diffViewerColor: 'var(--color-code-fg)',
|
|
addedBackground: 'var(--color-diff-added-bg)',
|
|
addedColor: 'var(--color-code-fg)',
|
|
removedBackground: 'var(--color-diff-removed-bg)',
|
|
removedColor: 'var(--color-code-fg)',
|
|
wordAddedBackground: 'var(--color-diff-added-word)',
|
|
wordRemovedBackground: 'var(--color-diff-removed-word)',
|
|
addedGutterBackground: 'var(--color-diff-added-gutter)',
|
|
removedGutterBackground: 'var(--color-diff-removed-gutter)',
|
|
gutterBackground: 'var(--color-surface-container-low)',
|
|
gutterBackgroundDark: 'var(--color-surface-container)',
|
|
highlightBackground: 'var(--color-diff-highlight-bg)',
|
|
highlightGutterBackground: 'var(--color-diff-highlight-gutter)',
|
|
codeFoldGutterBackground: 'var(--color-surface-container-high)',
|
|
codeFoldBackground: 'var(--color-surface-container-highest)',
|
|
emptyLineBackground: 'var(--color-surface-container-low)',
|
|
gutterColor: 'var(--color-text-tertiary)',
|
|
addedGutterColor: 'var(--color-diff-added-text)',
|
|
removedGutterColor: 'var(--color-diff-removed-text)',
|
|
codeFoldContentColor: 'var(--color-text-tertiary)',
|
|
diffViewerTitleBackground: 'var(--color-diff-title-bg)',
|
|
diffViewerTitleColor: 'var(--color-diff-title-color)',
|
|
diffViewerTitleBorderColor: 'var(--color-diff-title-border)',
|
|
},
|
|
},
|
|
diffContainer: {
|
|
borderRadius: '0',
|
|
fontSize: '12px',
|
|
lineHeight: '1.45',
|
|
fontFamily: 'var(--font-mono)',
|
|
},
|
|
line: {
|
|
padding: '1px 0',
|
|
},
|
|
gutter: {
|
|
padding: '1px 8px',
|
|
minWidth: '40px',
|
|
fontSize: '11px',
|
|
},
|
|
wordDiff: {
|
|
padding: '1px 2px',
|
|
borderRadius: '2px',
|
|
},
|
|
}
|
|
|
|
export function DiffViewer({ filePath, oldString, newString }: Props) {
|
|
const theme = useUIStore((state) => state.theme)
|
|
const language = inferLanguage(filePath)
|
|
|
|
const oldLines = oldString.split('\n')
|
|
const newLines = newString.split('\n')
|
|
const additions = newLines.filter((l, i) => l !== (oldLines[i] ?? null)).length
|
|
const deletions = oldLines.filter((l, i) => l !== (newLines[i] ?? null)).length
|
|
|
|
return (
|
|
<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-[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-[var(--color-diff-added-bg)] px-2 py-0.5 text-[var(--color-diff-added-text)]">+{additions}</span>
|
|
<span className="rounded-full bg-[var(--color-diff-removed-bg)] px-2 py-0.5 text-[var(--color-diff-removed-text)]">-{deletions}</span>
|
|
</div>
|
|
</div>
|
|
<CopyButton
|
|
text={`--- ${filePath}\n+++ ${filePath}`}
|
|
label="Copy path"
|
|
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}
|
|
newValue={newString}
|
|
splitView={false}
|
|
compareMethod={DiffMethod.WORDS}
|
|
renderContent={(str) => highlightSyntax(str, language)}
|
|
hideLineNumbers={false}
|
|
styles={diffStyles}
|
|
useDarkTheme={theme === 'dark'}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|