Improve workspace/chat diff viewer with syntax highlighting & per-file view
Replace the raw single-blob diff dump (Monaco, language=diff) and the plain <pre> file diffs in chat with @git-diff-view/react: - Parse the unified git diff into structured per-file entries (status, +/- counts, binary detection) via parseDiffFiles() - Workspace Diff tab: collapsible per-file cards with status badges, line counts, syntax highlighting, and a Unified/Split toggle - Agent chat: render each change's diff highlighted instead of plain text - Theme follows next-themes resolvedTheme (light/dark)
This commit is contained in:
@@ -21,6 +21,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@convex-dev/auth": "catalog:convex",
|
"@convex-dev/auth": "catalog:convex",
|
||||||
|
"@git-diff-view/react": "^0.1.6",
|
||||||
"@monaco-editor/react": "latest",
|
"@monaco-editor/react": "latest",
|
||||||
"@sentry/nextjs": "^10.46.0",
|
"@sentry/nextjs": "^10.46.0",
|
||||||
"@spoon/backend": "workspace:*",
|
"@spoon/backend": "workspace:*",
|
||||||
@@ -31,6 +32,7 @@
|
|||||||
"monaco-vim": "latest",
|
"monaco-vim": "latest",
|
||||||
"next": "^16.2.1",
|
"next": "^16.2.1",
|
||||||
"next-plausible": "^3.12.5",
|
"next-plausible": "^3.12.5",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
"react": "catalog:react19",
|
"react": "catalog:react19",
|
||||||
"react-dom": "catalog:react19",
|
"react-dom": "catalog:react19",
|
||||||
"require-in-the-middle": "^7.5.2",
|
"require-in-the-middle": "^7.5.2",
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ import { toast } from 'sonner';
|
|||||||
import type { Doc } from '@spoon/backend/convex/_generated/dataModel.js';
|
import type { Doc } from '@spoon/backend/convex/_generated/dataModel.js';
|
||||||
import { Badge, Button, Textarea } from '@spoon/ui';
|
import { Badge, Button, Textarea } from '@spoon/ui';
|
||||||
|
|
||||||
import { extractFileDiff } from './diff-utils';
|
import { DiffFileView, useDiffTheme } from './diff-file-view';
|
||||||
|
import { parseDiffFileForPath } from './diff-utils';
|
||||||
|
|
||||||
type ActivityFilter = 'all' | 'chat' | 'activity' | 'files' | 'errors';
|
type ActivityFilter = 'all' | 'chat' | 'activity' | 'files' | 'errors';
|
||||||
|
|
||||||
@@ -67,6 +68,7 @@ export const AgentThread = ({
|
|||||||
const [sending, setSending] = useState(false);
|
const [sending, setSending] = useState(false);
|
||||||
const [replying, setReplying] = useState<string>();
|
const [replying, setReplying] = useState<string>();
|
||||||
const [filter, setFilter] = useState<ActivityFilter>('all');
|
const [filter, setFilter] = useState<ActivityFilter>('all');
|
||||||
|
const diffTheme = useDiffTheme();
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const chatMessages = useMemo(
|
const chatMessages = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -317,58 +319,80 @@ export const AgentThread = ({
|
|||||||
</pre>
|
</pre>
|
||||||
</article>
|
</article>
|
||||||
))}
|
))}
|
||||||
{visibleChanges.map((change) => (
|
{visibleChanges.map((change) => {
|
||||||
<article
|
const changedFile = parseDiffFileForPath(change.diff, change.path);
|
||||||
key={change._id}
|
const hasDiff = Boolean(changedFile && !changedFile.isBinary);
|
||||||
className='border-border bg-background rounded-md border p-3 text-sm'
|
const hasRenderableHunk = Boolean(
|
||||||
>
|
changedFile && hasDiff && changedFile.hunkText.includes('@@'),
|
||||||
<div className='flex items-center justify-between gap-3'>
|
);
|
||||||
<div className='min-w-0'>
|
return (
|
||||||
<div className='flex items-center gap-2'>
|
<article
|
||||||
<FilePenLine className='text-primary size-4 flex-none' />
|
key={change._id}
|
||||||
<span className='truncate font-mono text-xs'>
|
className='border-border bg-background rounded-md border p-3 text-sm'
|
||||||
{change.path}
|
>
|
||||||
</span>
|
<div className='flex items-center justify-between gap-3'>
|
||||||
|
<div className='min-w-0'>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<FilePenLine className='text-primary size-4 flex-none' />
|
||||||
|
<span className='truncate font-mono text-xs'>
|
||||||
|
{change.path}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className='text-muted-foreground mt-1 text-xs capitalize'>
|
||||||
|
{change.source} {change.changeType}
|
||||||
|
{changedFile ? (
|
||||||
|
<span className='ml-2 font-mono normal-case'>
|
||||||
|
<span className='text-emerald-500'>
|
||||||
|
+{changedFile.additions}
|
||||||
|
</span>{' '}
|
||||||
|
<span className='text-red-500'>
|
||||||
|
−{changedFile.deletions}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-none items-center gap-2'>
|
||||||
|
{hasDiff ? (
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='outline'
|
||||||
|
size='sm'
|
||||||
|
onClick={() => onOpenDiff(change.path)}
|
||||||
|
>
|
||||||
|
View diff
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
{change.path !== '.' ? (
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='outline'
|
||||||
|
size='sm'
|
||||||
|
onClick={() => onOpenFile(change.path)}
|
||||||
|
>
|
||||||
|
Open
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<p className='text-muted-foreground mt-1 text-xs capitalize'>
|
|
||||||
{change.source} {change.changeType}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-none items-center gap-2'>
|
{hasRenderableHunk && changedFile ? (
|
||||||
{extractFileDiff(change.diff, change.path) ? (
|
<details className='mt-3'>
|
||||||
<Button
|
<summary className='text-muted-foreground cursor-pointer text-xs'>
|
||||||
type='button'
|
File diff
|
||||||
variant='outline'
|
</summary>
|
||||||
size='sm'
|
<div className='border-border mt-2 max-h-72 overflow-auto rounded border'>
|
||||||
onClick={() => onOpenDiff(change.path)}
|
<DiffFileView
|
||||||
>
|
file={changedFile}
|
||||||
View diff
|
mode='unified'
|
||||||
</Button>
|
theme={diffTheme}
|
||||||
) : null}
|
fontSize={11}
|
||||||
{change.path !== '.' ? (
|
/>
|
||||||
<Button
|
</div>
|
||||||
type='button'
|
</details>
|
||||||
variant='outline'
|
) : null}
|
||||||
size='sm'
|
</article>
|
||||||
onClick={() => onOpenFile(change.path)}
|
);
|
||||||
>
|
})}
|
||||||
Open
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{extractFileDiff(change.diff, change.path) ? (
|
|
||||||
<details className='mt-3'>
|
|
||||||
<summary className='text-muted-foreground cursor-pointer text-xs'>
|
|
||||||
File diff
|
|
||||||
</summary>
|
|
||||||
<pre className='bg-muted mt-2 max-h-72 overflow-auto rounded p-2 text-xs whitespace-pre-wrap'>
|
|
||||||
{extractFileDiff(change.diff, change.path)}
|
|
||||||
</pre>
|
|
||||||
</details>
|
|
||||||
) : null}
|
|
||||||
</article>
|
|
||||||
))}
|
|
||||||
{visibleEvents.slice(-80).map((event) => (
|
{visibleEvents.slice(-80).map((event) => (
|
||||||
<article
|
<article
|
||||||
key={event._id}
|
key={event._id}
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { DiffModeEnum, DiffView } from '@git-diff-view/react';
|
||||||
|
import { useTheme } from 'next-themes';
|
||||||
|
|
||||||
|
import '@git-diff-view/react/styles/diff-view.css';
|
||||||
|
|
||||||
|
import type { ParsedDiffFile } from './diff-utils';
|
||||||
|
|
||||||
|
export type DiffMode = 'unified' | 'split';
|
||||||
|
|
||||||
|
/** Resolves the git-diff-view theme from next-themes, defaulting to dark. */
|
||||||
|
export const useDiffTheme = (): 'light' | 'dark' => {
|
||||||
|
const { resolvedTheme } = useTheme();
|
||||||
|
return resolvedTheme === 'light' ? 'light' : 'dark';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders one file's diff with syntax highlighting. Falls back to a short note
|
||||||
|
* for binary files and metadata-only changes (pure renames / mode changes) that
|
||||||
|
* have no hunks to display.
|
||||||
|
*/
|
||||||
|
export const DiffFileView = ({
|
||||||
|
file,
|
||||||
|
mode,
|
||||||
|
theme,
|
||||||
|
fontSize = 12,
|
||||||
|
wrap = false,
|
||||||
|
}: {
|
||||||
|
file: ParsedDiffFile;
|
||||||
|
mode: DiffMode;
|
||||||
|
theme: 'light' | 'dark';
|
||||||
|
fontSize?: number;
|
||||||
|
wrap?: boolean;
|
||||||
|
}) => {
|
||||||
|
if (file.isBinary) {
|
||||||
|
return (
|
||||||
|
<div className='text-muted-foreground px-3 py-2 text-xs'>
|
||||||
|
Binary file not shown.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!file.hunkText.includes('@@')) {
|
||||||
|
return (
|
||||||
|
<div className='text-muted-foreground px-3 py-2 text-xs'>
|
||||||
|
{file.status === 'renamed'
|
||||||
|
? 'Renamed with no content changes.'
|
||||||
|
: 'No content changes.'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<DiffView
|
||||||
|
data={{
|
||||||
|
oldFile: { fileName: file.oldPath || file.displayPath },
|
||||||
|
newFile: { fileName: file.newPath || file.displayPath },
|
||||||
|
hunks: [file.hunkText],
|
||||||
|
}}
|
||||||
|
diffViewMode={
|
||||||
|
mode === 'split' ? DiffModeEnum.Split : DiffModeEnum.Unified
|
||||||
|
}
|
||||||
|
diffViewTheme={theme}
|
||||||
|
diffViewHighlight
|
||||||
|
diffViewWrap={wrap}
|
||||||
|
diffViewFontSize={fontSize}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,3 +1,112 @@
|
|||||||
|
export type DiffFileStatus = 'added' | 'deleted' | 'modified' | 'renamed';
|
||||||
|
|
||||||
|
export type ParsedDiffFile = {
|
||||||
|
id: string;
|
||||||
|
oldPath: string;
|
||||||
|
newPath: string;
|
||||||
|
/** Path to show in the UI (new path, or old path for deletions). */
|
||||||
|
displayPath: string;
|
||||||
|
status: DiffFileStatus;
|
||||||
|
additions: number;
|
||||||
|
deletions: number;
|
||||||
|
isBinary: boolean;
|
||||||
|
/** The full per-file unified diff section, fed as-is to the diff renderer. */
|
||||||
|
hunkText: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const stripABPrefix = (value: string) => value.replace(/^[ab]\//, '');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splits a raw unified git diff into structured, per-file entries. Replaces the
|
||||||
|
* "one giant blob" rendering: each file can be shown, counted, and highlighted
|
||||||
|
* independently.
|
||||||
|
*/
|
||||||
|
export const parseDiffFiles = (diff: string | undefined): ParsedDiffFile[] => {
|
||||||
|
if (!diff?.trim()) return [];
|
||||||
|
const sections: string[][] = [];
|
||||||
|
let current: string[] | null = null;
|
||||||
|
for (const line of diff.split('\n')) {
|
||||||
|
if (line.startsWith('diff --git ')) {
|
||||||
|
if (current) sections.push(current);
|
||||||
|
current = [line];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
current?.push(line);
|
||||||
|
}
|
||||||
|
if (current) sections.push(current);
|
||||||
|
|
||||||
|
return sections.map((sectionLines, index) => {
|
||||||
|
const header = sectionLines[0] ?? '';
|
||||||
|
const gitMatch = /^diff --git a\/(.+?) b\/(.+)$/.exec(header);
|
||||||
|
let oldPath = gitMatch?.[1] ?? '';
|
||||||
|
let newPath = gitMatch?.[2] ?? oldPath;
|
||||||
|
let status: DiffFileStatus = 'modified';
|
||||||
|
let isBinary = false;
|
||||||
|
let additions = 0;
|
||||||
|
let deletions = 0;
|
||||||
|
let renameFrom = '';
|
||||||
|
let renameTo = '';
|
||||||
|
|
||||||
|
for (const line of sectionLines) {
|
||||||
|
if (line.startsWith('new file mode')) status = 'added';
|
||||||
|
else if (line.startsWith('deleted file mode')) status = 'deleted';
|
||||||
|
else if (line.startsWith('rename from ')) {
|
||||||
|
renameFrom = line.slice('rename from '.length);
|
||||||
|
} else if (line.startsWith('rename to ')) {
|
||||||
|
renameTo = line.slice('rename to '.length);
|
||||||
|
} else if (
|
||||||
|
line.startsWith('Binary files') ||
|
||||||
|
line.startsWith('GIT binary patch')
|
||||||
|
) {
|
||||||
|
isBinary = true;
|
||||||
|
} else if (line.startsWith('--- ')) {
|
||||||
|
const value = line.slice(4).trim();
|
||||||
|
if (value !== '/dev/null') oldPath = stripABPrefix(value);
|
||||||
|
} else if (line.startsWith('+++ ')) {
|
||||||
|
const value = line.slice(4).trim();
|
||||||
|
if (value !== '/dev/null') newPath = stripABPrefix(value);
|
||||||
|
} else if (line.startsWith('+') && !line.startsWith('+++')) {
|
||||||
|
additions += 1;
|
||||||
|
} else if (line.startsWith('-') && !line.startsWith('---')) {
|
||||||
|
deletions += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (renameFrom || renameTo) {
|
||||||
|
status = 'renamed';
|
||||||
|
oldPath = renameFrom || oldPath;
|
||||||
|
newPath = renameTo || newPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayPath = status === 'deleted' ? oldPath : newPath;
|
||||||
|
return {
|
||||||
|
id: `${index}-${displayPath}`,
|
||||||
|
oldPath,
|
||||||
|
newPath,
|
||||||
|
displayPath,
|
||||||
|
status,
|
||||||
|
additions,
|
||||||
|
deletions,
|
||||||
|
isBinary,
|
||||||
|
hunkText: sectionLines.join('\n'),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Returns the single parsed file matching a path, if present in the diff. */
|
||||||
|
export const parseDiffFileForPath = (
|
||||||
|
diff: string | undefined,
|
||||||
|
filePath: string,
|
||||||
|
): ParsedDiffFile | undefined => {
|
||||||
|
const normalized = filePath.replace(/^\.\/+/, '');
|
||||||
|
return parseDiffFiles(diff).find(
|
||||||
|
(file) =>
|
||||||
|
file.displayPath === normalized ||
|
||||||
|
file.newPath === normalized ||
|
||||||
|
file.oldPath === normalized,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const extractFileDiff = (diff: string | undefined, filePath: string) => {
|
export const extractFileDiff = (diff: string | undefined, filePath: string) => {
|
||||||
if (!diff?.trim() || filePath === '.') return '';
|
if (!diff?.trim() || filePath === '.') return '';
|
||||||
const lines = diff.split('\n');
|
const lines = diff.split('\n');
|
||||||
|
|||||||
@@ -1,25 +1,90 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import dynamic from 'next/dynamic';
|
import { useMemo, useState } from 'react';
|
||||||
|
import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||||
|
|
||||||
import { Button } from '@spoon/ui';
|
import { Button } from '@spoon/ui';
|
||||||
|
|
||||||
import { extractFileDiff } from './diff-utils';
|
import type { DiffMode } from './diff-file-view';
|
||||||
|
import type { DiffFileStatus, ParsedDiffFile } from './diff-utils';
|
||||||
|
import { DiffFileView, useDiffTheme } from './diff-file-view';
|
||||||
|
import { parseDiffFiles } from './diff-utils';
|
||||||
|
|
||||||
const MonacoEditor = dynamic(async () => await import('@monaco-editor/react'), {
|
const statusBadge: Record<
|
||||||
ssr: false,
|
DiffFileStatus,
|
||||||
});
|
{ label: string; className: string }
|
||||||
|
> = {
|
||||||
|
added: { label: 'Added', className: 'bg-emerald-500/15 text-emerald-500' },
|
||||||
|
deleted: { label: 'Deleted', className: 'bg-red-500/15 text-red-500' },
|
||||||
|
modified: { label: 'Modified', className: 'bg-amber-500/15 text-amber-500' },
|
||||||
|
renamed: { label: 'Renamed', className: 'bg-sky-500/15 text-sky-500' },
|
||||||
|
};
|
||||||
|
|
||||||
const diffStats = (diff: string) => {
|
const totals = (files: ParsedDiffFile[]) =>
|
||||||
const files = new Set<string>();
|
files.reduce(
|
||||||
let additions = 0;
|
(acc, file) => ({
|
||||||
let removals = 0;
|
additions: acc.additions + file.additions,
|
||||||
for (const line of diff.split('\n')) {
|
deletions: acc.deletions + file.deletions,
|
||||||
if (line.startsWith('diff --git ')) files.add(line);
|
}),
|
||||||
if (line.startsWith('+') && !line.startsWith('+++')) additions += 1;
|
{ additions: 0, deletions: 0 },
|
||||||
if (line.startsWith('-') && !line.startsWith('---')) removals += 1;
|
);
|
||||||
}
|
|
||||||
return { files: files.size, additions, removals };
|
const FileCard = ({
|
||||||
|
file,
|
||||||
|
mode,
|
||||||
|
theme,
|
||||||
|
defaultOpen,
|
||||||
|
}: {
|
||||||
|
file: ParsedDiffFile;
|
||||||
|
mode: DiffMode;
|
||||||
|
theme: 'light' | 'dark';
|
||||||
|
defaultOpen: boolean;
|
||||||
|
}) => {
|
||||||
|
const [open, setOpen] = useState(defaultOpen);
|
||||||
|
const badge = statusBadge[file.status];
|
||||||
|
return (
|
||||||
|
<div className='border-border overflow-hidden rounded-md border'>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
onClick={() => setOpen((value) => !value)}
|
||||||
|
className='bg-muted/40 hover:bg-muted/70 flex w-full items-center gap-2 px-3 py-2 text-left transition-colors'
|
||||||
|
>
|
||||||
|
{open ? (
|
||||||
|
<ChevronDown className='text-muted-foreground size-4 flex-none' />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className='text-muted-foreground size-4 flex-none' />
|
||||||
|
)}
|
||||||
|
<span className='min-w-0 flex-1 truncate font-mono text-xs'>
|
||||||
|
{file.status === 'renamed' && file.oldPath !== file.newPath ? (
|
||||||
|
<>
|
||||||
|
<span className='text-muted-foreground'>{file.oldPath} → </span>
|
||||||
|
{file.newPath}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
file.displayPath
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`flex-none rounded px-1.5 py-0.5 text-[10px] font-medium ${badge.className}`}
|
||||||
|
>
|
||||||
|
{badge.label}
|
||||||
|
</span>
|
||||||
|
<span className='flex-none font-mono text-xs'>
|
||||||
|
{file.additions > 0 ? (
|
||||||
|
<span className='text-emerald-500'>+{file.additions}</span>
|
||||||
|
) : null}{' '}
|
||||||
|
{file.deletions > 0 ? (
|
||||||
|
<span className='text-red-500'>−{file.deletions}</span>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{open ? (
|
||||||
|
<div className='overflow-x-auto'>
|
||||||
|
<DiffFileView file={file} mode={mode} theme={theme} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DiffViewer = ({
|
export const DiffViewer = ({
|
||||||
@@ -33,25 +98,65 @@ export const DiffViewer = ({
|
|||||||
onRefresh: () => Promise<void>;
|
onRefresh: () => Promise<void>;
|
||||||
onClearFocusedPath?: () => void;
|
onClearFocusedPath?: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const focusedDiff = focusedPath ? extractFileDiff(diff, focusedPath) : '';
|
const [mode, setMode] = useState<DiffMode>('unified');
|
||||||
const visibleDiff = focusedPath ? focusedDiff : diff;
|
const theme = useDiffTheme();
|
||||||
const stats = diffStats(visibleDiff);
|
|
||||||
|
const files = useMemo(() => parseDiffFiles(diff), [diff]);
|
||||||
|
const normalizedFocus = focusedPath?.replace(/^\.\/+/, '');
|
||||||
|
const visibleFiles = useMemo(
|
||||||
|
() =>
|
||||||
|
normalizedFocus
|
||||||
|
? files.filter(
|
||||||
|
(file) =>
|
||||||
|
file.displayPath === normalizedFocus ||
|
||||||
|
file.newPath === normalizedFocus ||
|
||||||
|
file.oldPath === normalizedFocus,
|
||||||
|
)
|
||||||
|
: files,
|
||||||
|
[files, normalizedFocus],
|
||||||
|
);
|
||||||
|
const stats = totals(visibleFiles);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex h-full min-h-0 flex-col'>
|
<div className='flex h-full min-h-0 flex-col'>
|
||||||
<div className='border-border flex h-12 items-center justify-between gap-3 border-b px-3'>
|
<div className='border-border flex h-12 items-center justify-between gap-3 border-b px-3'>
|
||||||
<div className='min-w-0'>
|
<div className='min-w-0'>
|
||||||
<p className='truncate text-sm font-medium'>
|
<p className='truncate text-sm font-medium'>
|
||||||
{focusedPath ? `Diff viewer: ${focusedPath}` : 'Diff viewer'}
|
{focusedPath ? `Diff: ${focusedPath}` : 'Diff viewer'}
|
||||||
</p>
|
</p>
|
||||||
<p className='text-muted-foreground truncate text-xs'>
|
<p className='text-muted-foreground truncate text-xs'>
|
||||||
{visibleDiff.trim()
|
{visibleFiles.length > 0
|
||||||
? `${stats.files} files, +${stats.additions} -${stats.removals}`
|
? `${visibleFiles.length} ${visibleFiles.length === 1 ? 'file' : 'files'}, `
|
||||||
: focusedPath
|
: ''}
|
||||||
? 'No diff for this file'
|
<span className='text-emerald-500'>+{stats.additions}</span>{' '}
|
||||||
: 'Current git diff'}
|
<span className='text-red-500'>−{stats.deletions}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-none items-center gap-2'>
|
<div className='flex flex-none items-center gap-2'>
|
||||||
|
<div className='border-border flex items-center rounded-md border p-0.5'>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
onClick={() => setMode('unified')}
|
||||||
|
className={`rounded px-2 py-1 text-xs font-medium transition-colors ${
|
||||||
|
mode === 'unified'
|
||||||
|
? 'bg-muted text-foreground'
|
||||||
|
: 'text-muted-foreground hover:text-foreground'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Unified
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
onClick={() => setMode('split')}
|
||||||
|
className={`rounded px-2 py-1 text-xs font-medium transition-colors ${
|
||||||
|
mode === 'split'
|
||||||
|
? 'bg-muted text-foreground'
|
||||||
|
: 'text-muted-foreground hover:text-foreground'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Split
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
{focusedPath ? (
|
{focusedPath ? (
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
@@ -67,22 +172,18 @@ export const DiffViewer = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{visibleDiff.trim() ? (
|
{visibleFiles.length > 0 ? (
|
||||||
<MonacoEditor
|
<div className='flex flex-1 flex-col gap-3 overflow-y-auto p-3'>
|
||||||
height='100%'
|
{visibleFiles.map((file, index) => (
|
||||||
width='100%'
|
<FileCard
|
||||||
language='diff'
|
key={file.id}
|
||||||
theme='vs-dark'
|
file={file}
|
||||||
value={visibleDiff}
|
mode={mode}
|
||||||
options={{
|
theme={theme}
|
||||||
readOnly: true,
|
defaultOpen={visibleFiles.length <= 10 || index < 5}
|
||||||
minimap: { enabled: false },
|
/>
|
||||||
fontSize: 13,
|
))}
|
||||||
scrollBeyondLastLine: false,
|
</div>
|
||||||
automaticLayout: true,
|
|
||||||
scrollbar: { alwaysConsumeMouseWheel: false },
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<div className='text-muted-foreground flex flex-1 items-center justify-center text-sm'>
|
<div className='text-muted-foreground flex flex-1 items-center justify-center text-sm'>
|
||||||
{focusedPath
|
{focusedPath
|
||||||
|
|||||||
@@ -97,6 +97,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@convex-dev/auth": "catalog:convex",
|
"@convex-dev/auth": "catalog:convex",
|
||||||
|
"@git-diff-view/react": "^0.1.6",
|
||||||
"@monaco-editor/react": "latest",
|
"@monaco-editor/react": "latest",
|
||||||
"@sentry/nextjs": "^10.46.0",
|
"@sentry/nextjs": "^10.46.0",
|
||||||
"@spoon/backend": "workspace:*",
|
"@spoon/backend": "workspace:*",
|
||||||
@@ -705,6 +706,12 @@
|
|||||||
|
|
||||||
"@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="],
|
"@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="],
|
||||||
|
|
||||||
|
"@git-diff-view/core": ["@git-diff-view/core@0.1.6", "", { "dependencies": { "@git-diff-view/lowlight": "^0.1.6", "fast-diff": "^1.3.0", "highlight.js": "^11.11.0", "lowlight": "^3.3.0" } }, "sha512-q2Ch8jURF6pL7VeNpOgHBRVY9gsGLXCOYpKXHG3BqpXe0kv6GNSUux8SmAYsDrakBzfgDClODxDtsM2rfiWpnA=="],
|
||||||
|
|
||||||
|
"@git-diff-view/lowlight": ["@git-diff-view/lowlight@0.1.6", "", { "dependencies": { "@types/hast": "^3.0.0", "highlight.js": "^11.11.0", "lowlight": "^3.3.0" } }, "sha512-YIsiAc2aWAePWaDNi3k8xI0Vs/ZItt5J6nrftTIFbMFN3GwDOsyJFm2L7o8XWKTJkV2yItaz28KUI9CWj0MVZA=="],
|
||||||
|
|
||||||
|
"@git-diff-view/react": ["@git-diff-view/react@0.1.6", "", { "dependencies": { "@git-diff-view/core": "^0.1.6", "@types/hast": "^3.0.0", "fast-diff": "^1.3.0", "highlight.js": "^11.11.0", "lowlight": "^3.3.0", "reactivity-store": "^0.4.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-koABBon5bNKh6/WnWSxggK9ojw+cvWAPnY2/ciOkwlR+8dm0h6A7Qa5kP2HFDxqYHwZ2imkGMcSLgXMOnWHRFA=="],
|
||||||
|
|
||||||
"@hookform/resolvers": ["@hookform/resolvers@5.2.2", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA=="],
|
"@hookform/resolvers": ["@hookform/resolvers@5.2.2", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA=="],
|
||||||
|
|
||||||
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
||||||
@@ -1525,6 +1532,8 @@
|
|||||||
|
|
||||||
"@types/hammerjs": ["@types/hammerjs@2.0.46", "", {}, "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw=="],
|
"@types/hammerjs": ["@types/hammerjs@2.0.46", "", {}, "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw=="],
|
||||||
|
|
||||||
|
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
|
||||||
|
|
||||||
"@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="],
|
"@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="],
|
||||||
|
|
||||||
"@types/istanbul-lib-report": ["@types/istanbul-lib-report@3.0.3", "", { "dependencies": { "@types/istanbul-lib-coverage": "*" } }, "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA=="],
|
"@types/istanbul-lib-report": ["@types/istanbul-lib-report@3.0.3", "", { "dependencies": { "@types/istanbul-lib-coverage": "*" } }, "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA=="],
|
||||||
@@ -1553,6 +1562,8 @@
|
|||||||
|
|
||||||
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
||||||
|
|
||||||
|
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||||
|
|
||||||
"@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="],
|
"@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="],
|
||||||
|
|
||||||
"@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="],
|
"@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="],
|
||||||
@@ -1605,6 +1616,10 @@
|
|||||||
|
|
||||||
"@vitest/utils": ["@vitest/utils@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA=="],
|
"@vitest/utils": ["@vitest/utils@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA=="],
|
||||||
|
|
||||||
|
"@vue/reactivity": ["@vue/reactivity@3.5.38", "", { "dependencies": { "@vue/shared": "3.5.38" } }, "sha512-pG6LV/NDNRbKizcUjFFLAfjaL8mcv4DmR9avNcUw2gDHBzZneuS2TWCmp633ynzxz9YYKNeEPK2I8Wraqy2HUQ=="],
|
||||||
|
|
||||||
|
"@vue/shared": ["@vue/shared@3.5.38", "", {}, "sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug=="],
|
||||||
|
|
||||||
"@webassemblyjs/ast": ["@webassemblyjs/ast@1.14.1", "", { "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ=="],
|
"@webassemblyjs/ast": ["@webassemblyjs/ast@1.14.1", "", { "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ=="],
|
||||||
|
|
||||||
"@webassemblyjs/floating-point-hex-parser": ["@webassemblyjs/floating-point-hex-parser@1.13.2", "", {}, "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA=="],
|
"@webassemblyjs/floating-point-hex-parser": ["@webassemblyjs/floating-point-hex-parser@1.13.2", "", {}, "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA=="],
|
||||||
@@ -1965,6 +1980,8 @@
|
|||||||
|
|
||||||
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
|
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
|
||||||
|
|
||||||
|
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
|
||||||
|
|
||||||
"doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
|
"doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
|
||||||
|
|
||||||
"dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="],
|
"dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="],
|
||||||
@@ -2157,6 +2174,8 @@
|
|||||||
|
|
||||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||||
|
|
||||||
|
"fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="],
|
||||||
|
|
||||||
"fast-glob": ["fast-glob@3.3.1", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="],
|
"fast-glob": ["fast-glob@3.3.1", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="],
|
||||||
|
|
||||||
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||||
@@ -2277,6 +2296,8 @@
|
|||||||
|
|
||||||
"hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
|
"hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
|
||||||
|
|
||||||
|
"highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="],
|
||||||
|
|
||||||
"hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="],
|
"hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="],
|
||||||
|
|
||||||
"hosted-git-info": ["hosted-git-info@7.0.2", "", { "dependencies": { "lru-cache": "^10.0.1" } }, "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w=="],
|
"hosted-git-info": ["hosted-git-info@7.0.2", "", { "dependencies": { "lru-cache": "^10.0.1" } }, "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w=="],
|
||||||
@@ -2535,6 +2556,8 @@
|
|||||||
|
|
||||||
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||||
|
|
||||||
|
"lowlight": ["lowlight@3.3.0", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.0.0", "highlight.js": "~11.11.0" } }, "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ=="],
|
||||||
|
|
||||||
"lru-cache": ["lru-cache@11.5.1", "", {}, "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A=="],
|
"lru-cache": ["lru-cache@11.5.1", "", {}, "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A=="],
|
||||||
|
|
||||||
"lucia": ["lucia@3.2.2", "", { "dependencies": { "@oslojs/crypto": "^1.0.1", "@oslojs/encoding": "^1.1.0" } }, "sha512-P1FlFBGCMPMXu+EGdVD9W4Mjm0DqsusmKgO7Xc33mI5X1bklmsQb0hfzPhXomQr9waWIBDsiOjvr1e6BTaUqpA=="],
|
"lucia": ["lucia@3.2.2", "", { "dependencies": { "@oslojs/crypto": "^1.0.1", "@oslojs/encoding": "^1.1.0" } }, "sha512-P1FlFBGCMPMXu+EGdVD9W4Mjm0DqsusmKgO7Xc33mI5X1bklmsQb0hfzPhXomQr9waWIBDsiOjvr1e6BTaUqpA=="],
|
||||||
@@ -2877,6 +2900,8 @@
|
|||||||
|
|
||||||
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
|
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
|
||||||
|
|
||||||
|
"reactivity-store": ["reactivity-store@0.4.0", "", { "dependencies": { "@vue/reactivity": "~3.5.30", "@vue/shared": "~3.5.30", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-uL9uoREOBg2o4zUa8vMU0AbvAOk0osPloizscmyZqMvJzcuuKX3ELFYYr1DX8gAcfvlhPduz4QuLZn1eChCu4Q=="],
|
||||||
|
|
||||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||||
|
|
||||||
"recharts": ["recharts@3.8.1", "", { "dependencies": { "@reduxjs/toolkit": "^1.9.0 || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg=="],
|
"recharts": ["recharts@3.8.1", "", { "dependencies": { "@reduxjs/toolkit": "^1.9.0 || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg=="],
|
||||||
|
|||||||
Reference in New Issue
Block a user