feat: Polish review tab — viewed tracking, file tree, syntax highlighting, better UX
- Add file "viewed" checkmarks with progress tracking (X/26 viewed in header + sidebar) - Add directory-grouped file tree in sidebar with review progress bar - Add sticky file headers that stay visible when scrolling through long diffs - Add file change type badges (NEW/DELETED/RENAMED) with colored left borders - Add syntax highlighting via shiki with lazy loading and progressive enhancement - Add merge confirmation dropdown showing unresolved comments + viewed progress - Make Approve & Merge button prominently green, Request Changes styled with error tokens - Show full branch names instead of aggressively truncated text - Add always-visible comment dots on lines with comments, subtle hover on others - Improve hunk headers with two-tone @@ display and context function highlighting - Add review progress bar to sidebar with file-level viewed state
This commit is contained in:
@@ -12,6 +12,9 @@ interface DiffViewerProps {
|
||||
) => void;
|
||||
onResolveComment: (commentId: string) => void;
|
||||
onUnresolveComment: (commentId: string) => void;
|
||||
viewedFiles?: Set<string>;
|
||||
onToggleViewed?: (filePath: string) => void;
|
||||
onRegisterRef?: (filePath: string, el: HTMLDivElement | null) => void;
|
||||
}
|
||||
|
||||
export function DiffViewer({
|
||||
@@ -20,18 +23,24 @@ export function DiffViewer({
|
||||
onAddComment,
|
||||
onResolveComment,
|
||||
onUnresolveComment,
|
||||
viewedFiles,
|
||||
onToggleViewed,
|
||||
onRegisterRef,
|
||||
}: DiffViewerProps) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{files.map((file) => (
|
||||
<FileCard
|
||||
key={file.newPath}
|
||||
file={file}
|
||||
comments={comments.filter((c) => c.filePath === file.newPath)}
|
||||
onAddComment={onAddComment}
|
||||
onResolveComment={onResolveComment}
|
||||
onUnresolveComment={onUnresolveComment}
|
||||
/>
|
||||
<div key={file.newPath} ref={(el) => onRegisterRef?.(file.newPath, el)}>
|
||||
<FileCard
|
||||
file={file}
|
||||
comments={comments.filter((c) => c.filePath === file.newPath)}
|
||||
onAddComment={onAddComment}
|
||||
onResolveComment={onResolveComment}
|
||||
onUnresolveComment={onUnresolveComment}
|
||||
isViewed={viewedFiles?.has(file.newPath) ?? false}
|
||||
onToggleViewed={() => onToggleViewed?.(file.newPath)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,45 @@
|
||||
import { useState } from "react";
|
||||
import { ChevronDown, ChevronRight, Plus, Minus } from "lucide-react";
|
||||
import { useState, useMemo } from "react";
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Plus,
|
||||
Minus,
|
||||
CheckCircle2,
|
||||
Circle,
|
||||
} from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import type { FileDiff, DiffLine, ReviewComment } from "./types";
|
||||
import type { FileDiff, FileChangeType, DiffLine, ReviewComment } from "./types";
|
||||
import { HunkRows } from "./HunkRows";
|
||||
import { useHighlightedFile } from "./use-syntax-highlight";
|
||||
|
||||
const changeTypeBadge: Record<
|
||||
FileChangeType,
|
||||
{ label: string; classes: string } | null
|
||||
> = {
|
||||
added: {
|
||||
label: "NEW",
|
||||
classes:
|
||||
"bg-status-success-bg text-status-success-fg border-status-success-border",
|
||||
},
|
||||
deleted: {
|
||||
label: "DELETED",
|
||||
classes:
|
||||
"bg-status-error-bg text-status-error-fg border-status-error-border",
|
||||
},
|
||||
renamed: {
|
||||
label: "RENAMED",
|
||||
classes:
|
||||
"bg-status-active-bg text-status-active-fg border-status-active-border",
|
||||
},
|
||||
modified: null,
|
||||
};
|
||||
|
||||
const leftBorderClass: Record<FileChangeType, string> = {
|
||||
added: "border-l-2 border-l-status-success-fg",
|
||||
deleted: "border-l-2 border-l-status-error-fg",
|
||||
renamed: "border-l-2 border-l-status-active-fg",
|
||||
modified: "border-l-2 border-l-primary/40",
|
||||
};
|
||||
|
||||
interface FileCardProps {
|
||||
file: FileDiff;
|
||||
@@ -15,6 +52,8 @@ interface FileCardProps {
|
||||
) => void;
|
||||
onResolveComment: (commentId: string) => void;
|
||||
onUnresolveComment: (commentId: string) => void;
|
||||
isViewed?: boolean;
|
||||
onToggleViewed?: () => void;
|
||||
}
|
||||
|
||||
export function FileCard({
|
||||
@@ -23,15 +62,25 @@ export function FileCard({
|
||||
onAddComment,
|
||||
onResolveComment,
|
||||
onUnresolveComment,
|
||||
isViewed = false,
|
||||
onToggleViewed = () => {},
|
||||
}: FileCardProps) {
|
||||
const [expanded, setExpanded] = useState(true);
|
||||
const commentCount = comments.length;
|
||||
const badge = changeTypeBadge[file.changeType];
|
||||
|
||||
// Flatten all hunk lines for syntax highlighting
|
||||
const allLines = useMemo(
|
||||
() => file.hunks.flatMap((h) => h.lines),
|
||||
[file.hunks],
|
||||
);
|
||||
const tokenMap = useHighlightedFile(file.newPath, allLines);
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-border overflow-hidden">
|
||||
{/* File header */}
|
||||
{/* File header — sticky so it stays visible when scrolling */}
|
||||
<button
|
||||
className="flex w-full items-center gap-2 px-3 py-2 bg-muted/50 hover:bg-muted text-left text-sm font-mono transition-colors"
|
||||
className={`sticky top-0 z-10 flex w-full items-center gap-2 px-3 py-2 bg-muted hover:bg-muted/90 text-left text-sm font-mono transition-colors ${leftBorderClass[file.changeType]}`}
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
{expanded ? (
|
||||
@@ -39,8 +88,41 @@ export function FileCard({
|
||||
) : (
|
||||
<ChevronRight className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
)}
|
||||
<span className="truncate flex-1">{file.newPath}</span>
|
||||
<span className="truncate flex-1 flex items-center gap-2">
|
||||
{file.newPath}
|
||||
{badge && (
|
||||
<span
|
||||
className={`inline-flex text-[9px] font-semibold uppercase px-1.5 py-0 rounded border leading-4 ${badge.classes}`}
|
||||
>
|
||||
{badge.label}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="flex items-center gap-2 shrink-0 text-xs">
|
||||
{/* Viewed toggle */}
|
||||
<span
|
||||
role="checkbox"
|
||||
aria-checked={isViewed}
|
||||
tabIndex={0}
|
||||
className="cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggleViewed();
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
onToggleViewed();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isViewed ? (
|
||||
<CheckCircle2 className="h-4 w-4 text-status-success-fg" />
|
||||
) : (
|
||||
<Circle className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
</span>
|
||||
{file.additions > 0 && (
|
||||
<span className="flex items-center gap-0.5 text-diff-add-fg">
|
||||
<Plus className="h-3 w-3" />
|
||||
@@ -75,6 +157,7 @@ export function FileCard({
|
||||
onAddComment={onAddComment}
|
||||
onResolveComment={onResolveComment}
|
||||
onUnresolveComment={onUnresolveComment}
|
||||
tokenMap={tokenMap}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import type { DiffLine, ReviewComment } from "./types";
|
||||
import { LineWithComments } from "./LineWithComments";
|
||||
import type { LineTokenMap } from "./use-syntax-highlight";
|
||||
|
||||
interface HunkRowsProps {
|
||||
hunk: { header: string; lines: DiffLine[] };
|
||||
@@ -14,6 +15,7 @@ interface HunkRowsProps {
|
||||
) => void;
|
||||
onResolveComment: (commentId: string) => void;
|
||||
onUnresolveComment: (commentId: string) => void;
|
||||
tokenMap?: LineTokenMap | null;
|
||||
}
|
||||
|
||||
export function HunkRows({
|
||||
@@ -23,6 +25,7 @@ export function HunkRows({
|
||||
onAddComment,
|
||||
onResolveComment,
|
||||
onUnresolveComment,
|
||||
tokenMap,
|
||||
}: HunkRowsProps) {
|
||||
const [commentingLine, setCommentingLine] = useState<{
|
||||
lineNumber: number;
|
||||
@@ -49,9 +52,26 @@ export function HunkRows({
|
||||
<tr>
|
||||
<td
|
||||
colSpan={3}
|
||||
className="px-3 py-1 text-muted-foreground bg-diff-hunk-bg text-[11px] select-none"
|
||||
className="px-3 py-1 text-muted-foreground bg-diff-hunk-bg text-[11px] select-none border-l-2 border-l-primary/20"
|
||||
>
|
||||
{hunk.header}
|
||||
{(() => {
|
||||
const match = hunk.header.match(/^(@@[^@]+@@)(.*)$/);
|
||||
if (match) {
|
||||
const prefix = match[1];
|
||||
const context = match[2].trim();
|
||||
return (
|
||||
<>
|
||||
<span className="text-muted-foreground/60">{prefix}</span>
|
||||
{context && (
|
||||
<span className="ml-1 text-muted-foreground/80 font-medium">
|
||||
{context}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return hunk.header;
|
||||
})()}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -78,6 +98,11 @@ export function HunkRows({
|
||||
onSubmitComment={handleSubmitComment}
|
||||
onResolveComment={onResolveComment}
|
||||
onUnresolveComment={onUnresolveComment}
|
||||
tokens={
|
||||
line.newLineNumber !== null
|
||||
? tokenMap?.get(line.newLineNumber) ?? undefined
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { MessageSquarePlus } from "lucide-react";
|
||||
import type { DiffLine, ReviewComment } from "./types";
|
||||
import { CommentThread } from "./CommentThread";
|
||||
import { CommentForm } from "./CommentForm";
|
||||
import type { TokenizedLine } from "./use-syntax-highlight";
|
||||
|
||||
interface LineWithCommentsProps {
|
||||
line: DiffLine;
|
||||
@@ -14,11 +15,13 @@ interface LineWithCommentsProps {
|
||||
onSubmitComment: (body: string) => void;
|
||||
onResolveComment: (commentId: string) => void;
|
||||
onUnresolveComment: (commentId: string) => void;
|
||||
/** Syntax-highlighted tokens for this line (if available) */
|
||||
tokens?: TokenizedLine;
|
||||
}
|
||||
|
||||
export function LineWithComments({
|
||||
line,
|
||||
lineKey,
|
||||
lineKey: _lineKey,
|
||||
lineComments,
|
||||
isCommenting,
|
||||
onStartComment,
|
||||
@@ -26,6 +29,7 @@ export function LineWithComments({
|
||||
onSubmitComment,
|
||||
onResolveComment,
|
||||
onUnresolveComment,
|
||||
tokens,
|
||||
}: LineWithCommentsProps) {
|
||||
const formRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
@@ -62,7 +66,7 @@ export function LineWithComments({
|
||||
return (
|
||||
<>
|
||||
<tr
|
||||
className={`group ${bgClass} hover:brightness-95 dark:hover:brightness-110`}
|
||||
className={`group ${bgClass} hover:ring-1 hover:ring-inset hover:ring-primary/10`}
|
||||
>
|
||||
{/* Line numbers */}
|
||||
<td
|
||||
@@ -80,24 +84,48 @@ export function LineWithComments({
|
||||
|
||||
{/* Comment button gutter */}
|
||||
<td className={`w-6 min-w-6 ${gutterBgClass} align-top`}>
|
||||
<button
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity p-0.5 hover:text-primary"
|
||||
onClick={onStartComment}
|
||||
title="Add comment"
|
||||
>
|
||||
<MessageSquarePlus className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{lineComments.length > 0 ? (
|
||||
<div className="relative flex items-center justify-center h-5">
|
||||
<button
|
||||
className="flex items-center justify-center group/comment"
|
||||
onClick={onStartComment}
|
||||
title={`${lineComments.length} comment${lineComments.length > 1 ? "s" : ""} — click to reply`}
|
||||
>
|
||||
<span className="h-2 w-2 rounded-full bg-primary group-hover/comment:hidden" />
|
||||
<MessageSquarePlus className="h-3.5 w-3.5 text-primary hidden group-hover/comment:block" />
|
||||
{lineComments.length > 1 && (
|
||||
<span className="absolute -top-0.5 -right-0.5 text-[8px] text-primary font-bold leading-none">
|
||||
{lineComments.length}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
className="opacity-0 group-hover:opacity-50 transition-opacity p-0.5 hover:!opacity-100 hover:text-primary text-muted-foreground"
|
||||
onClick={onStartComment}
|
||||
title="Add comment"
|
||||
>
|
||||
<MessageSquarePlus className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
|
||||
{/* Code content */}
|
||||
<td className="pl-1 pr-3 align-top">
|
||||
<pre
|
||||
className={`leading-5 whitespace-pre-wrap break-all ${textColorClass}`}
|
||||
className={`leading-5 whitespace-pre-wrap break-all ${tokens ? "" : textColorClass}`}
|
||||
>
|
||||
<span className="select-none text-muted-foreground/60">
|
||||
{prefix}
|
||||
</span>
|
||||
{line.content}
|
||||
{tokens
|
||||
? tokens.map((token, i) => (
|
||||
<span key={i} style={{ color: token.color }}>
|
||||
{token.content}
|
||||
</span>
|
||||
))
|
||||
: line.content}
|
||||
</pre>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import {
|
||||
Check,
|
||||
X,
|
||||
@@ -11,6 +12,9 @@ import {
|
||||
CircleDot,
|
||||
RotateCcw,
|
||||
ArrowRight,
|
||||
Eye,
|
||||
AlertCircle,
|
||||
GitMerge,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -43,6 +47,8 @@ interface ReviewHeaderProps {
|
||||
onApprove: () => void;
|
||||
onRequestChanges: () => void;
|
||||
preview: PreviewState | null;
|
||||
viewedCount?: number;
|
||||
totalCount?: number;
|
||||
}
|
||||
|
||||
export function ReviewHeader({
|
||||
@@ -58,9 +64,31 @@ export function ReviewHeader({
|
||||
onApprove,
|
||||
onRequestChanges,
|
||||
preview,
|
||||
viewedCount,
|
||||
totalCount,
|
||||
}: ReviewHeaderProps) {
|
||||
const totalAdditions = files.reduce((s, f) => s + f.additions, 0);
|
||||
const totalDeletions = files.reduce((s, f) => s + f.deletions, 0);
|
||||
const [showConfirmation, setShowConfirmation] = useState(false);
|
||||
const confirmRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Click-outside handler to dismiss confirmation
|
||||
useEffect(() => {
|
||||
if (!showConfirmation) return;
|
||||
function handleClickOutside(e: MouseEvent) {
|
||||
if (
|
||||
confirmRef.current &&
|
||||
!confirmRef.current.contains(e.target as Node)
|
||||
) {
|
||||
setShowConfirmation(false);
|
||||
}
|
||||
}
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, [showConfirmation]);
|
||||
|
||||
const viewed = viewedCount ?? 0;
|
||||
const total = totalCount ?? 0;
|
||||
|
||||
return (
|
||||
<div className="border-b border-border bg-card/80 backdrop-blur-sm">
|
||||
@@ -108,14 +136,14 @@ export function ReviewHeader({
|
||||
</h2>
|
||||
|
||||
{sourceBranch && (
|
||||
<div className="flex items-center gap-1 text-[11px] text-muted-foreground font-mono shrink-0">
|
||||
<div className="flex items-center gap-1 text-[11px] text-muted-foreground font-mono min-w-0">
|
||||
<GitBranch className="h-3 w-3 shrink-0" />
|
||||
<span className="truncate max-w-[140px]" title={sourceBranch}>
|
||||
{truncateBranch(sourceBranch)}
|
||||
<span className="truncate" title={sourceBranch}>
|
||||
{sourceBranch}
|
||||
</span>
|
||||
<ArrowRight className="h-2.5 w-2.5 shrink-0 text-muted-foreground/50" />
|
||||
<span className="truncate max-w-[140px]" title={targetBranch}>
|
||||
{truncateBranch(targetBranch)}
|
||||
<span className="truncate" title={targetBranch}>
|
||||
{targetBranch}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -136,8 +164,18 @@ export function ReviewHeader({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Center: review progress */}
|
||||
{total > 0 && (
|
||||
<div className="flex items-center gap-1.5 text-[11px] text-muted-foreground shrink-0">
|
||||
<Eye className="h-3 w-3" />
|
||||
<span>
|
||||
{viewed}/{total} viewed
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Right: preview + actions */}
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<div className="flex items-center gap-3 shrink-0">
|
||||
{/* Preview controls */}
|
||||
{preview && <PreviewControls preview={preview} />}
|
||||
|
||||
@@ -148,22 +186,82 @@ export function ReviewHeader({
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onRequestChanges}
|
||||
className="h-7 text-xs"
|
||||
className="h-8 text-xs px-3 border-status-error-border/50 text-status-error-fg hover:bg-status-error-bg/50 hover:border-status-error-border"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
Request Changes
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={onApprove}
|
||||
disabled={unresolvedCount > 0}
|
||||
className="h-7 text-xs"
|
||||
>
|
||||
<Check className="h-3 w-3" />
|
||||
{unresolvedCount > 0
|
||||
? `${unresolvedCount} unresolved`
|
||||
: "Approve & Merge"}
|
||||
</Button>
|
||||
<div className="relative" ref={confirmRef}>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (unresolvedCount > 0) return;
|
||||
setShowConfirmation(true);
|
||||
}}
|
||||
disabled={unresolvedCount > 0}
|
||||
className={
|
||||
unresolvedCount > 0
|
||||
? "h-9 px-5 text-sm font-semibold"
|
||||
: "bg-status-success-fg text-white hover:opacity-90 h-9 px-5 text-sm font-semibold shadow-sm"
|
||||
}
|
||||
>
|
||||
{unresolvedCount > 0 ? (
|
||||
<>
|
||||
<AlertCircle className="h-3.5 w-3.5" />
|
||||
{unresolvedCount} unresolved
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<GitMerge className="h-3.5 w-3.5" />
|
||||
Approve & Merge
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* Merge confirmation dropdown */}
|
||||
{showConfirmation && (
|
||||
<div className="absolute right-0 top-full mt-1 z-20 w-64 rounded-lg border border-border bg-card shadow-lg p-4">
|
||||
<p className="text-sm font-semibold mb-3">
|
||||
Ready to merge?
|
||||
</p>
|
||||
<div className="space-y-1.5 mb-4">
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<Check className="h-3.5 w-3.5 text-status-success-fg" />
|
||||
<span className="text-muted-foreground">
|
||||
0 unresolved comments
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<Eye className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">
|
||||
{viewed}/{total} files viewed
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowConfirmation(false)}
|
||||
className="h-8 text-xs"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setShowConfirmation(false);
|
||||
onApprove();
|
||||
}}
|
||||
className="bg-status-success-fg text-white hover:opacity-90 h-8 px-4 text-xs font-semibold shadow-sm"
|
||||
>
|
||||
<GitMerge className="h-3.5 w-3.5" />
|
||||
Merge Now
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{status === "approved" && (
|
||||
@@ -247,9 +345,3 @@ function PreviewControls({ preview }: { preview: PreviewState }) {
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function truncateBranch(branch: string): string {
|
||||
const parts = branch.split("/");
|
||||
if (parts.length <= 2) return branch;
|
||||
return parts.slice(-2).join("/");
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import {
|
||||
MessageSquare,
|
||||
FileCode,
|
||||
FolderOpen,
|
||||
Plus,
|
||||
Minus,
|
||||
Circle,
|
||||
@@ -21,6 +22,7 @@ interface ReviewSidebarProps {
|
||||
activeFiles: FileDiff[];
|
||||
commits: CommitInfo[];
|
||||
onSelectCommit: (hash: string | null) => void;
|
||||
viewedFiles?: Set<string>;
|
||||
}
|
||||
|
||||
export function ReviewSidebar({
|
||||
@@ -31,6 +33,7 @@ export function ReviewSidebar({
|
||||
activeFiles,
|
||||
commits,
|
||||
onSelectCommit,
|
||||
viewedFiles = new Set(),
|
||||
}: ReviewSidebarProps) {
|
||||
const [view, setView] = useState<SidebarView>("files");
|
||||
|
||||
@@ -45,6 +48,7 @@ export function ReviewSidebar({
|
||||
onFileClick={onFileClick}
|
||||
selectedCommit={selectedCommit}
|
||||
activeFiles={activeFiles}
|
||||
viewedFiles={viewedFiles}
|
||||
/>
|
||||
) : (
|
||||
<CommitsView
|
||||
@@ -115,25 +119,96 @@ function IconTab({
|
||||
|
||||
/* ─── Files View ───────────────────────────────────────── */
|
||||
|
||||
interface DirectoryGroup {
|
||||
directory: string;
|
||||
files: FileDiff[];
|
||||
}
|
||||
|
||||
function groupFilesByDirectory(files: FileDiff[]): DirectoryGroup[] {
|
||||
const groups = new Map<string, FileDiff[]>();
|
||||
|
||||
for (const file of files) {
|
||||
const lastSlash = file.newPath.lastIndexOf("/");
|
||||
const directory = lastSlash >= 0 ? file.newPath.slice(0, lastSlash + 1) : "";
|
||||
const existing = groups.get(directory);
|
||||
if (existing) {
|
||||
existing.push(file);
|
||||
} else {
|
||||
groups.set(directory, [file]);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort directories alphabetically, sort files within each directory
|
||||
const sorted = Array.from(groups.entries())
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([directory, dirFiles]) => ({
|
||||
directory,
|
||||
files: dirFiles.sort((a, b) => {
|
||||
const aName = a.newPath.slice(a.newPath.lastIndexOf("/") + 1);
|
||||
const bName = b.newPath.slice(b.newPath.lastIndexOf("/") + 1);
|
||||
return aName.localeCompare(bName);
|
||||
}),
|
||||
}));
|
||||
|
||||
return sorted;
|
||||
}
|
||||
|
||||
function getFileName(path: string): string {
|
||||
const lastSlash = path.lastIndexOf("/");
|
||||
return lastSlash >= 0 ? path.slice(lastSlash + 1) : path;
|
||||
}
|
||||
|
||||
const changeTypeDotColor: Record<string, string> = {
|
||||
added: "bg-status-success-fg",
|
||||
deleted: "bg-status-error-fg",
|
||||
renamed: "bg-status-active-fg",
|
||||
};
|
||||
|
||||
function FilesView({
|
||||
files,
|
||||
comments,
|
||||
onFileClick,
|
||||
selectedCommit,
|
||||
activeFiles,
|
||||
viewedFiles,
|
||||
}: {
|
||||
files: FileDiff[];
|
||||
comments: ReviewComment[];
|
||||
onFileClick: (filePath: string) => void;
|
||||
selectedCommit: string | null;
|
||||
activeFiles: FileDiff[];
|
||||
viewedFiles: Set<string>;
|
||||
}) {
|
||||
const unresolvedCount = comments.filter((c) => !c.resolved).length;
|
||||
const resolvedCount = comments.filter((c) => c.resolved).length;
|
||||
const activeFilePaths = new Set(activeFiles.map((f) => f.newPath));
|
||||
|
||||
const directoryGroups = useMemo(() => groupFilesByDirectory(files), [files]);
|
||||
|
||||
const viewedCount = files.filter((f) => viewedFiles.has(f.newPath)).length;
|
||||
const totalCount = files.length;
|
||||
const progressPercent = totalCount > 0 ? (viewedCount / totalCount) * 100 : 0;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Review progress */}
|
||||
{totalCount > 0 && (
|
||||
<div className="space-y-1.5">
|
||||
<h4 className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wider">
|
||||
Review Progress
|
||||
</h4>
|
||||
<div className="h-1 rounded-full bg-muted w-full">
|
||||
<div
|
||||
className="h-full rounded-full bg-status-success-fg transition-all duration-300"
|
||||
style={{ width: `${progressPercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{viewedCount}/{totalCount} files viewed
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Comment summary */}
|
||||
{comments.length > 0 && (
|
||||
<div className="space-y-1.5">
|
||||
@@ -161,8 +236,8 @@ function FilesView({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* File list */}
|
||||
<div className="space-y-0.5">
|
||||
{/* Directory-grouped file tree */}
|
||||
<div>
|
||||
<h4 className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wider mb-1.5">
|
||||
Files
|
||||
{selectedCommit && (
|
||||
@@ -171,46 +246,74 @@ function FilesView({
|
||||
</span>
|
||||
)}
|
||||
</h4>
|
||||
{files.map((file) => {
|
||||
const fileCommentCount = comments.filter(
|
||||
(c) => c.filePath === file.newPath,
|
||||
).length;
|
||||
const isInView = activeFilePaths.has(file.newPath);
|
||||
const dimmed = selectedCommit && !isInView;
|
||||
{directoryGroups.map((group) => (
|
||||
<div key={group.directory}>
|
||||
{/* Directory header */}
|
||||
{group.directory && (
|
||||
<div className="text-[10px] font-mono text-muted-foreground/70 mt-2 first:mt-0 px-2 py-0.5 flex items-center gap-1">
|
||||
<FolderOpen className="h-3 w-3 shrink-0" />
|
||||
<span className="truncate">{group.directory}</span>
|
||||
</div>
|
||||
)}
|
||||
{/* Files in directory */}
|
||||
<div className="space-y-0.5">
|
||||
{group.files.map((file) => {
|
||||
const fileCommentCount = comments.filter(
|
||||
(c) => c.filePath === file.newPath,
|
||||
).length;
|
||||
const isInView = activeFilePaths.has(file.newPath);
|
||||
const dimmed = selectedCommit && !isInView;
|
||||
const isViewed = viewedFiles.has(file.newPath);
|
||||
const dotColor = changeTypeDotColor[file.changeType];
|
||||
|
||||
return (
|
||||
<button
|
||||
key={file.newPath}
|
||||
className={`
|
||||
flex w-full items-center gap-1.5 rounded px-2 py-1 text-left text-[11px]
|
||||
hover:bg-accent/50 transition-colors group
|
||||
${dimmed ? "opacity-35" : ""}
|
||||
`}
|
||||
onClick={() => onFileClick(file.newPath)}
|
||||
>
|
||||
<FileCode className="h-3 w-3 text-muted-foreground shrink-0" />
|
||||
<span className="truncate flex-1 font-mono">
|
||||
{formatFilePath(file.newPath)}
|
||||
</span>
|
||||
<span className="flex items-center gap-1 shrink-0">
|
||||
{fileCommentCount > 0 && (
|
||||
<span className="flex items-center gap-0.5 text-muted-foreground">
|
||||
<MessageSquare className="h-2.5 w-2.5" />
|
||||
{fileCommentCount}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-diff-add-fg text-[10px]">
|
||||
<Plus className="h-2.5 w-2.5 inline" />
|
||||
{file.additions}
|
||||
</span>
|
||||
<span className="text-diff-remove-fg text-[10px]">
|
||||
<Minus className="h-2.5 w-2.5 inline" />
|
||||
{file.deletions}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
return (
|
||||
<button
|
||||
key={file.newPath}
|
||||
className={`
|
||||
flex w-full items-center gap-1.5 rounded py-1 text-left text-[11px]
|
||||
hover:bg-accent/50 transition-colors group
|
||||
${group.directory ? "pl-4 pr-2" : "px-2"}
|
||||
${dimmed ? "opacity-35" : ""}
|
||||
`}
|
||||
onClick={() => onFileClick(file.newPath)}
|
||||
>
|
||||
{isViewed ? (
|
||||
<CheckCircle2 className="h-3 w-3 text-status-success-fg shrink-0" />
|
||||
) : (
|
||||
<FileCode className="h-3 w-3 text-muted-foreground shrink-0" />
|
||||
)}
|
||||
{dotColor && (
|
||||
<span className={`h-1.5 w-1.5 rounded-full shrink-0 ${dotColor}`} />
|
||||
)}
|
||||
<span className="truncate flex-1 font-mono">
|
||||
{getFileName(file.newPath)}
|
||||
</span>
|
||||
<span className="flex items-center gap-1 shrink-0">
|
||||
{fileCommentCount > 0 && (
|
||||
<span className="flex items-center gap-0.5 text-muted-foreground">
|
||||
<MessageSquare className="h-2.5 w-2.5" />
|
||||
{fileCommentCount}
|
||||
</span>
|
||||
)}
|
||||
{file.additions > 0 && (
|
||||
<span className="text-diff-add-fg text-[10px]">
|
||||
<Plus className="h-2.5 w-2.5 inline" />
|
||||
{file.additions}
|
||||
</span>
|
||||
)}
|
||||
{file.deletions > 0 && (
|
||||
<span className="text-diff-remove-fg text-[10px]">
|
||||
<Minus className="h-2.5 w-2.5 inline" />
|
||||
{file.deletions}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -304,12 +407,6 @@ function CommitsView({
|
||||
|
||||
/* ─── Helpers ──────────────────────────────────────────── */
|
||||
|
||||
function formatFilePath(path: string): string {
|
||||
const parts = path.split("/");
|
||||
if (parts.length <= 2) return path;
|
||||
return parts.slice(-2).join("/");
|
||||
}
|
||||
|
||||
function truncateMessage(msg: string): string {
|
||||
const firstLine = msg.split("\n")[0];
|
||||
return firstLine.length > 50 ? firstLine.slice(0, 47) + "..." : firstLine;
|
||||
|
||||
@@ -15,8 +15,29 @@ interface ReviewTabProps {
|
||||
export function ReviewTab({ initiativeId }: ReviewTabProps) {
|
||||
const [status, setStatus] = useState<ReviewStatus>("pending");
|
||||
const [selectedCommit, setSelectedCommit] = useState<string | null>(null);
|
||||
const [viewedFiles, setViewedFiles] = useState<Set<string>>(new Set());
|
||||
const fileRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
||||
|
||||
const toggleViewed = useCallback((filePath: string) => {
|
||||
setViewedFiles(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(filePath)) {
|
||||
next.delete(filePath);
|
||||
} else {
|
||||
next.add(filePath);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const registerFileRef = useCallback((filePath: string, el: HTMLDivElement | null) => {
|
||||
if (el) {
|
||||
fileRefs.current.set(filePath, el);
|
||||
} else {
|
||||
fileRefs.current.delete(filePath);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Fetch phases for this initiative
|
||||
const phasesQuery = trpc.listPhases.useQuery({ initiativeId });
|
||||
const pendingReviewPhases = useMemo(
|
||||
@@ -129,7 +150,7 @@ export function ReviewTab({ initiativeId }: ReviewTabProps) {
|
||||
lineType: c.lineType as "added" | "removed" | "context",
|
||||
body: c.body,
|
||||
author: c.author,
|
||||
createdAt: c.createdAt instanceof Date ? c.createdAt.toISOString() : String(c.createdAt),
|
||||
createdAt: typeof c.createdAt === 'string' ? c.createdAt : String(c.createdAt),
|
||||
resolved: c.resolved,
|
||||
}));
|
||||
}, [commentsQuery.data]);
|
||||
@@ -222,6 +243,7 @@ export function ReviewTab({ initiativeId }: ReviewTabProps) {
|
||||
setSelectedPhaseId(id);
|
||||
setSelectedCommit(null);
|
||||
setStatus("pending");
|
||||
setViewedFiles(new Set());
|
||||
}, []);
|
||||
|
||||
const unresolvedCount = comments.filter((c) => !c.resolved).length;
|
||||
@@ -261,6 +283,8 @@ export function ReviewTab({ initiativeId }: ReviewTabProps) {
|
||||
onApprove={handleApprove}
|
||||
onRequestChanges={handleRequestChanges}
|
||||
preview={previewState}
|
||||
viewedCount={viewedFiles.size}
|
||||
totalCount={allFiles.length}
|
||||
/>
|
||||
|
||||
{/* Main content area — sidebar always rendered to preserve state */}
|
||||
@@ -285,6 +309,9 @@ export function ReviewTab({ initiativeId }: ReviewTabProps) {
|
||||
onAddComment={handleAddComment}
|
||||
onResolveComment={handleResolveComment}
|
||||
onUnresolveComment={handleUnresolveComment}
|
||||
viewedFiles={viewedFiles}
|
||||
onToggleViewed={toggleViewed}
|
||||
onRegisterRef={registerFileRef}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -300,6 +327,7 @@ export function ReviewTab({ initiativeId }: ReviewTabProps) {
|
||||
activeFiles={files}
|
||||
commits={commits}
|
||||
onSelectCommit={setSelectedCommit}
|
||||
viewedFiles={viewedFiles}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { FileDiff, DiffHunk, DiffLine } from "./types";
|
||||
import type { FileDiff, FileChangeType, DiffHunk, DiffLine } from "./types";
|
||||
|
||||
/**
|
||||
* Parse a unified diff string into structured FileDiff objects.
|
||||
@@ -20,9 +20,13 @@ export function parseUnifiedDiff(raw: string): FileDiff[] {
|
||||
let additions = 0;
|
||||
let deletions = 0;
|
||||
|
||||
// Scan header lines (between "diff --git" and first "@@") for /dev/null markers
|
||||
let hasOldDevNull = false;
|
||||
let hasNewDevNull = false;
|
||||
let i = 1;
|
||||
// Skip to first hunk header
|
||||
while (i < lines.length && !lines[i].startsWith("@@")) {
|
||||
if (lines[i].startsWith("--- /dev/null")) hasOldDevNull = true;
|
||||
if (lines[i].startsWith("+++ /dev/null")) hasNewDevNull = true;
|
||||
i++;
|
||||
}
|
||||
|
||||
@@ -86,7 +90,19 @@ export function parseUnifiedDiff(raw: string): FileDiff[] {
|
||||
hunks.push({ header, oldStart, oldCount, newStart, newCount, lines: hunkLines });
|
||||
}
|
||||
|
||||
files.push({ oldPath, newPath, hunks, additions, deletions });
|
||||
// Derive changeType from header markers and path comparison
|
||||
let changeType: FileChangeType;
|
||||
if (hasOldDevNull) {
|
||||
changeType = "added";
|
||||
} else if (hasNewDevNull) {
|
||||
changeType = "deleted";
|
||||
} else if (oldPath !== newPath) {
|
||||
changeType = "renamed";
|
||||
} else {
|
||||
changeType = "modified";
|
||||
}
|
||||
|
||||
files.push({ oldPath, newPath, hunks, additions, deletions, changeType });
|
||||
}
|
||||
|
||||
return files;
|
||||
|
||||
@@ -14,12 +14,15 @@ export interface DiffLine {
|
||||
newLineNumber: number | null;
|
||||
}
|
||||
|
||||
export type FileChangeType = 'added' | 'modified' | 'deleted' | 'renamed';
|
||||
|
||||
export interface FileDiff {
|
||||
oldPath: string;
|
||||
newPath: string;
|
||||
hunks: DiffHunk[];
|
||||
additions: number;
|
||||
deletions: number;
|
||||
changeType: FileChangeType;
|
||||
}
|
||||
|
||||
export interface ReviewComment {
|
||||
|
||||
163
apps/web/src/components/review/use-syntax-highlight.ts
Normal file
163
apps/web/src/components/review/use-syntax-highlight.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import type { ThemedToken } from "shiki";
|
||||
|
||||
/* ── Lazy singleton highlighter ─────────────────────────── */
|
||||
|
||||
let highlighterPromise: Promise<Awaited<
|
||||
ReturnType<typeof import("shiki")["createHighlighter"]>
|
||||
> | null> | null = null;
|
||||
|
||||
const LANGS = [
|
||||
"typescript",
|
||||
"javascript",
|
||||
"tsx",
|
||||
"jsx",
|
||||
"json",
|
||||
"css",
|
||||
"html",
|
||||
"yaml",
|
||||
"markdown",
|
||||
"bash",
|
||||
"python",
|
||||
"go",
|
||||
"rust",
|
||||
"sql",
|
||||
"toml",
|
||||
"xml",
|
||||
] as const;
|
||||
|
||||
function getHighlighter() {
|
||||
if (!highlighterPromise) {
|
||||
highlighterPromise = import("shiki")
|
||||
.then(({ createHighlighter }) =>
|
||||
createHighlighter({
|
||||
themes: ["github-dark-default"],
|
||||
langs: [...LANGS],
|
||||
}),
|
||||
)
|
||||
.catch(() => null);
|
||||
}
|
||||
return highlighterPromise;
|
||||
}
|
||||
|
||||
// Pre-warm on module load (non-blocking)
|
||||
getHighlighter();
|
||||
|
||||
/* ── Language detection ──────────────────────────────────── */
|
||||
|
||||
const EXT_TO_LANG: Record<string, string> = {
|
||||
ts: "typescript",
|
||||
tsx: "tsx",
|
||||
js: "javascript",
|
||||
jsx: "jsx",
|
||||
mjs: "javascript",
|
||||
cjs: "javascript",
|
||||
json: "json",
|
||||
css: "css",
|
||||
scss: "css",
|
||||
html: "html",
|
||||
htm: "html",
|
||||
yml: "yaml",
|
||||
yaml: "yaml",
|
||||
md: "markdown",
|
||||
mdx: "markdown",
|
||||
sh: "bash",
|
||||
bash: "bash",
|
||||
zsh: "bash",
|
||||
py: "python",
|
||||
go: "go",
|
||||
rs: "rust",
|
||||
sql: "sql",
|
||||
toml: "toml",
|
||||
xml: "xml",
|
||||
};
|
||||
|
||||
function detectLang(path: string): string | null {
|
||||
const ext = path.split(".").pop()?.toLowerCase() ?? "";
|
||||
return EXT_TO_LANG[ext] ?? null;
|
||||
}
|
||||
|
||||
/* ── Types ───────────────────────────────────────────────── */
|
||||
|
||||
export type TokenizedLine = ThemedToken[];
|
||||
/** Maps newLineNumber → highlighted tokens for that line */
|
||||
export type LineTokenMap = Map<number, TokenizedLine>;
|
||||
|
||||
interface DiffLineInput {
|
||||
content: string;
|
||||
newLineNumber: number | null;
|
||||
type: "added" | "removed" | "context";
|
||||
}
|
||||
|
||||
/* ── Hook ────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* Highlights the "new-side" content of a file diff.
|
||||
* Returns null until highlighting is ready (progressive enhancement).
|
||||
* Only context + added lines are highlighted (removed lines fall back to plain text).
|
||||
*/
|
||||
export function useHighlightedFile(
|
||||
filePath: string,
|
||||
allLines: DiffLineInput[],
|
||||
): LineTokenMap | null {
|
||||
const [tokenMap, setTokenMap] = useState<LineTokenMap | null>(null);
|
||||
|
||||
// Build stable code string + line number mapping
|
||||
const { code, lineNums, cacheKey } = useMemo(() => {
|
||||
const entries: { lineNum: number; content: string }[] = [];
|
||||
for (const line of allLines) {
|
||||
if (
|
||||
line.newLineNumber !== null &&
|
||||
(line.type === "context" || line.type === "added")
|
||||
) {
|
||||
entries.push({ lineNum: line.newLineNumber, content: line.content });
|
||||
}
|
||||
}
|
||||
entries.sort((a, b) => a.lineNum - b.lineNum);
|
||||
const c = entries.map((e) => e.content).join("\n");
|
||||
return {
|
||||
code: c,
|
||||
lineNums: entries.map((e) => e.lineNum),
|
||||
cacheKey: `${filePath}:${c.length}:${entries.length}`,
|
||||
};
|
||||
}, [filePath, allLines]);
|
||||
|
||||
useEffect(() => {
|
||||
const lang = detectLang(filePath);
|
||||
if (!lang || !code) {
|
||||
setTokenMap(null);
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
getHighlighter().then((highlighter) => {
|
||||
if (cancelled || !highlighter) return;
|
||||
|
||||
try {
|
||||
const result = highlighter.codeToTokens(code, {
|
||||
lang: lang as Parameters<typeof highlighter.codeToTokens>[1]["lang"],
|
||||
theme: "github-dark-default",
|
||||
});
|
||||
const map: LineTokenMap = new Map();
|
||||
|
||||
result.tokens.forEach((lineTokens: ThemedToken[], idx: number) => {
|
||||
if (idx < lineNums.length) {
|
||||
map.set(lineNums[idx], lineTokens);
|
||||
}
|
||||
});
|
||||
|
||||
if (!cancelled) setTokenMap(map);
|
||||
} catch {
|
||||
// Language not loaded or parse error — no highlighting
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [cacheKey]);
|
||||
|
||||
return tokenMap;
|
||||
}
|
||||
559
package-lock.json
generated
559
package-lock.json
generated
@@ -28,6 +28,7 @@
|
||||
"motion": "^12.34.5",
|
||||
"nanoid": "^5.1.6",
|
||||
"pino": "^10.3.0",
|
||||
"shiki": "^4.0.1",
|
||||
"simple-git": "^3.30.0",
|
||||
"unique-names-generator": "^4.7.1",
|
||||
"zod": "^4.3.6"
|
||||
@@ -3974,6 +3975,106 @@
|
||||
"integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@shikijs/core": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.0.1.tgz",
|
||||
"integrity": "sha512-vWvqi9JNgz1dRL9Nvog5wtx7RuNkf7MEPl2mU/cyUUxJeH1CAr3t+81h8zO8zs7DK6cKLMoU9TvukWIDjP4Lzg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/primitive": "4.0.1",
|
||||
"@shikijs/types": "4.0.1",
|
||||
"@shikijs/vscode-textmate": "^10.0.2",
|
||||
"@types/hast": "^3.0.4",
|
||||
"hast-util-to-html": "^9.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/engine-javascript": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.0.1.tgz",
|
||||
"integrity": "sha512-DJK9NiwtGYqMuKCRO4Ip0FKNDQpmaiS+K5bFjJ7DWFn4zHueDWgaUG8kAofkrnXF6zPPYYQY7J5FYVW9MbZyBg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "4.0.1",
|
||||
"@shikijs/vscode-textmate": "^10.0.2",
|
||||
"oniguruma-to-es": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/engine-oniguruma": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.0.1.tgz",
|
||||
"integrity": "sha512-oCWdCTDch3J8Kc0OZJ98KuUPC02O1VqIE3W/e2uvrHqTxYRR21RGEJMtchrgrxhsoJJCzmIciKsqG+q/yD+Cxg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "4.0.1",
|
||||
"@shikijs/vscode-textmate": "^10.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/langs": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.0.1.tgz",
|
||||
"integrity": "sha512-v/mluaybWdnGJR4GqAR6zh8qAZohW9k+cGYT28Y7M8+jLbC0l4yG085O1A+WkseHTn+awd+P3UBymb2+MXFc8w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/primitive": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.0.1.tgz",
|
||||
"integrity": "sha512-ns0hHZc5eWZuvuIEJz2pTx3Qecz0aRVYumVQJ8JgWY2tq/dH8WxdcVM49Fc2NsHEILNIT6vfdW9MF26RANWiTA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "4.0.1",
|
||||
"@shikijs/vscode-textmate": "^10.0.2",
|
||||
"@types/hast": "^3.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/themes": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.0.1.tgz",
|
||||
"integrity": "sha512-FW41C/D6j/yKQkzVdjrRPiJCtgeDaYRJFEyCKFCINuRJRj9WcmubhP4KQHPZ4+9eT87jruSrYPyoblNRyDFzvA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/types": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.0.1.tgz",
|
||||
"integrity": "sha512-EaygPEn57+jJ76mw+nTLvIpJMAcMPokFbrF8lufsZP7Ukk+ToJYEcswN1G0e49nUZAq7aCQtoeW219A8HK1ZOw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/vscode-textmate": "^10.0.2",
|
||||
"@types/hast": "^3.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/vscode-textmate": {
|
||||
"version": "10.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz",
|
||||
"integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@sindresorhus/merge-streams": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz",
|
||||
@@ -4905,6 +5006,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/hast": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
|
||||
"integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/js-yaml": {
|
||||
"version": "4.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
|
||||
@@ -4928,6 +5038,15 @@
|
||||
"@types/mdurl": "^2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mdast": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
|
||||
"integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mdurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
|
||||
@@ -4961,6 +5080,12 @@
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/unist": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
||||
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/use-sync-external-store": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||
@@ -4984,6 +5109,12 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@ungap/structured-clone": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
|
||||
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
||||
@@ -5514,6 +5645,16 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/ccount": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
|
||||
"integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/chai": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
|
||||
@@ -5524,6 +5665,26 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/character-entities-html4": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
|
||||
"integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/character-entities-legacy": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
|
||||
"integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
@@ -5603,6 +5764,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/comma-separated-tokens": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
|
||||
"integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "12.1.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
|
||||
@@ -5715,6 +5886,15 @@
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dequal": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
@@ -5730,6 +5910,19 @@
|
||||
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/devlop": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
|
||||
"integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dequal": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/didyoumean": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||
@@ -6930,6 +7123,42 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-to-html": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz",
|
||||
"integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/unist": "^3.0.0",
|
||||
"ccount": "^2.0.0",
|
||||
"comma-separated-tokens": "^2.0.0",
|
||||
"hast-util-whitespace": "^3.0.0",
|
||||
"html-void-elements": "^3.0.0",
|
||||
"mdast-util-to-hast": "^13.0.0",
|
||||
"property-information": "^7.0.0",
|
||||
"space-separated-tokens": "^2.0.0",
|
||||
"stringify-entities": "^4.0.0",
|
||||
"zwitch": "^2.0.4"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-whitespace": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
|
||||
"integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/help-me": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
|
||||
@@ -6944,6 +7173,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/html-void-elements": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
|
||||
"integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/human-signals": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz",
|
||||
@@ -7341,6 +7580,27 @@
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-to-hast": {
|
||||
"version": "13.2.1",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
|
||||
"integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/mdast": "^4.0.0",
|
||||
"@ungap/structured-clone": "^1.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"micromark-util-sanitize-uri": "^2.0.0",
|
||||
"trim-lines": "^3.0.0",
|
||||
"unist-util-position": "^5.0.0",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"vfile": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
|
||||
@@ -7357,6 +7617,95 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-util-character": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
|
||||
"integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-util-encode": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
|
||||
"integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/micromark-util-sanitize-uri": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
|
||||
"integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-encode": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-util-symbol": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
|
||||
"integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/micromark-util-types": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
|
||||
"integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
@@ -7728,6 +8077,23 @@
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/oniguruma-parser": {
|
||||
"version": "0.12.1",
|
||||
"resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz",
|
||||
"integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/oniguruma-to-es": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.4.tgz",
|
||||
"integrity": "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"oniguruma-parser": "^0.12.1",
|
||||
"regex": "^6.0.1",
|
||||
"regex-recursion": "^6.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/orderedmap": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz",
|
||||
@@ -8176,6 +8542,16 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/property-information": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
|
||||
"integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-changeset": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.3.1.tgz",
|
||||
@@ -8608,6 +8984,30 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/regex": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz",
|
||||
"integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"regex-utilities": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/regex-recursion": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz",
|
||||
"integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"regex-utilities": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/regex-utilities": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz",
|
||||
"integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.11",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
||||
@@ -8910,6 +9310,25 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shiki": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/shiki/-/shiki-4.0.1.tgz",
|
||||
"integrity": "sha512-EkAEhDTN5WhpoQFXFw79OHIrSAfHhlImeCdSyg4u4XvrpxKEmdo/9x/HWSowujAnUrFsGOwWiE58a6GVentMnQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/core": "4.0.1",
|
||||
"@shikijs/engine-javascript": "4.0.1",
|
||||
"@shikijs/engine-oniguruma": "4.0.1",
|
||||
"@shikijs/langs": "4.0.1",
|
||||
"@shikijs/themes": "4.0.1",
|
||||
"@shikijs/types": "4.0.1",
|
||||
"@shikijs/vscode-textmate": "^10.0.2",
|
||||
"@types/hast": "^3.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/siginfo": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
|
||||
@@ -9038,6 +9457,16 @@
|
||||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/space-separated-tokens": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
|
||||
"integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/split2": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||
@@ -9076,6 +9505,20 @@
|
||||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stringify-entities": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
|
||||
"integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"character-entities-html4": "^2.0.0",
|
||||
"character-entities-legacy": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-bom-string": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
|
||||
@@ -9402,6 +9845,16 @@
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/trim-lines": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
|
||||
"integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-interface-checker": {
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
|
||||
@@ -9493,6 +9946,74 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/unist-util-is": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
|
||||
"integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/unist-util-position": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
|
||||
"integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/unist-util-stringify-position": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
|
||||
"integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/unist-util-visit": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz",
|
||||
"integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^3.0.0",
|
||||
"unist-util-is": "^6.0.0",
|
||||
"unist-util-visit-parents": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/unist-util-visit-parents": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
|
||||
"integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^3.0.0",
|
||||
"unist-util-is": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/unplugin": {
|
||||
"version": "2.3.11",
|
||||
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz",
|
||||
@@ -9598,6 +10119,34 @@
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vfile": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
|
||||
"integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^3.0.0",
|
||||
"vfile-message": "^4.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/vfile-message": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
|
||||
"integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^3.0.0",
|
||||
"unist-util-stringify-position": "^4.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
||||
@@ -9862,6 +10411,16 @@
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/zwitch": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
|
||||
"integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"packages/shared": {
|
||||
"name": "@codewalk-district/shared",
|
||||
"version": "0.0.1"
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
"motion": "^12.34.5",
|
||||
"nanoid": "^5.1.6",
|
||||
"pino": "^10.3.0",
|
||||
"shiki": "^4.0.1",
|
||||
"simple-git": "^3.30.0",
|
||||
"unique-names-generator": "^4.7.1",
|
||||
"zod": "^4.3.6"
|
||||
|
||||
Reference in New Issue
Block a user