Skip to content

Commit 1e293ec

Browse files
color gradient
1 parent 8e719ed commit 1e293ec

4 files changed

Lines changed: 109 additions & 1 deletion

File tree

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Shared color ramp for the age-of-commit indicator. Used by the blame gutter
2+
// (left border of each cell) and the legend rendered next to the toolbar.
3+
//
4+
// Tailwind's JIT scanner reads class names from source, so each class must
5+
// appear as a complete literal string. Don't try to construct these via
6+
// template strings.
7+
8+
export const BLAME_AGE_BUCKET_COUNT = 10;
9+
10+
// In dark mode the ramp is flipped: pale shades (amber-50/100) are
11+
// high-contrast against a dark background, dark shades blend in. We want
12+
// "newer" to pop visually in both themes, so the dark-mode bucket-0 (oldest)
13+
// is amber-900 (low contrast → fades) and dark-mode bucket-9 (newest) is
14+
// amber-50 (high contrast → pops). The light-mode ramp stays unchanged.
15+
export const BLAME_AGE_BG_CLASSES = [
16+
'bg-slate-50 dark:bg-slate-900',
17+
'bg-slate-100 dark:bg-slate-800',
18+
'bg-slate-200 dark:bg-slate-700',
19+
'bg-slate-300 dark:bg-slate-600',
20+
'bg-slate-400 dark:bg-slate-500',
21+
'bg-slate-500 dark:bg-slate-400',
22+
'bg-slate-600 dark:bg-slate-300',
23+
'bg-slate-700 dark:bg-slate-200',
24+
'bg-slate-800 dark:bg-slate-100',
25+
'bg-slate-900 dark:bg-slate-50',
26+
] as const;
27+
28+
/**
29+
* Linear time mapping: given a commit date (ISO 8601) and the file's overall
30+
* date range, returns a bucket 0..9 (palest..darkest). Clamps out-of-range
31+
* inputs (e.g., clock-skewed future dates) to the endpoints.
32+
*/
33+
export const computeAgeBucket = (
34+
isoDate: string,
35+
oldestMs: number,
36+
newestMs: number,
37+
): number => {
38+
const max = BLAME_AGE_BUCKET_COUNT - 1;
39+
if (newestMs === oldestMs) {
40+
return max;
41+
}
42+
const t = new Date(isoDate).getTime();
43+
const ratio = (t - oldestMs) / (newestMs - oldestMs);
44+
const bucket = Math.floor(ratio * max);
45+
return Math.max(0, Math.min(max, bucket));
46+
};
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { BLAME_AGE_BG_CLASSES } from "./blameAgeColors";
2+
import { cn } from "@/lib/utils";
3+
4+
export const BlameAgeLegend = () => {
5+
return (
6+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
7+
<span>Older</span>
8+
<div className="flex items-center gap-px">
9+
{BLAME_AGE_BG_CLASSES.map((bg, i) => (
10+
<div
11+
key={i}
12+
className={cn('h-2 w-2', bg)}
13+
/>
14+
))}
15+
</div>
16+
<span>Newer</span>
17+
</div>
18+
);
19+
};

packages/web/src/app/(app)/browse/[...path]/components/codePreviewPanel/blameGutterExtension.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { EditorState, Extension, Prec, Range as CMRange, RangeSet, StateField }
33
import { formatDistanceToNowStrict } from "date-fns";
44
import type { FileBlameResponse } from "@/features/git";
55
import { cn } from "@/lib/utils";
6+
import { BLAME_AGE_BG_CLASSES, computeAgeBucket } from "./blameAgeColors";
67

78
type LineEntry = {
89
hash: string;
@@ -17,6 +18,9 @@ type LineEntry = {
1718
// True for first-line cells except line 1 of the file, so the divider
1819
// border doesn't render at the very top of the gutter.
1920
showStartBorder: boolean;
21+
// 0..9 bucket for the age-of-commit indicator stripe. Same value across
22+
// every line of a region (continuation lines included).
23+
ageBucket: number;
2024
};
2125

2226
// @see: https://lucide.dev/icons/file-stack
@@ -29,11 +33,23 @@ const buildCellDom = (
2933
onReblameClick: (previous: { hash: string; path: string }) => void,
3034
): HTMLElement => {
3135
const cell = document.createElement('div');
36+
// `relative` so the absolutely-positioned age stripe inside has a
37+
// positioning context. The stripe is a child <div> rather than a
38+
// border-left because tailwind-merge collapses any same-group border-color
39+
// class (e.g. `border-border` on the region divider) with the per-side
40+
// amber color, dropping the stripe on first-line cells.
3241
cell.className = cn(
33-
'flex items-start h-full px-2 overflow-hidden text-xs text-muted-foreground',
42+
'relative flex items-start h-full pl-2 pr-2 overflow-hidden text-xs text-muted-foreground',
3443
entry.showStartBorder && 'border-t border-border',
3544
);
3645

46+
const stripe = document.createElement('div');
47+
stripe.className = cn(
48+
'absolute inset-y-0 left-0 w-0.5',
49+
BLAME_AGE_BG_CLASSES[entry.ageBucket],
50+
);
51+
cell.appendChild(stripe);
52+
3753
if (entry.message === null || entry.date === null) {
3854
// Continuation line — empty cell with a non-breaking space so the row
3955
// still occupies its full line height.
@@ -105,6 +121,7 @@ class BlameMarker extends GutterMarker {
105121
a.date === b.date &&
106122
a.authorEmail === b.authorEmail &&
107123
a.showStartBorder === b.showStartBorder &&
124+
a.ageBucket === b.ageBucket &&
108125
a.previous?.hash === b.previous?.hash &&
109126
a.previous?.path === b.previous?.path
110127
);
@@ -155,9 +172,26 @@ const computeActive = (
155172
};
156173

157174
const buildLineIndex = (blame: FileBlameResponse): Map<number, LineEntry> => {
175+
// Compute the file's overall date range so each commit's age can be
176+
// mapped to a 0..9 bucket. We assume blame.commits' `date` fields are
177+
// ISO 8601 strings.
178+
const dateMs = Object.values(blame.commits)
179+
.map(c => new Date(c.date).getTime())
180+
.filter(t => Number.isFinite(t));
181+
const oldestMs = dateMs.length > 0 ? Math.min(...dateMs) : 0;
182+
const newestMs = dateMs.length > 0 ? Math.max(...dateMs) : 0;
183+
184+
// Per-commit bucket cache so every line of a region gets the same value
185+
// (and we don't recompute for each line).
186+
const bucketByHash = new Map<string, number>();
187+
for (const [hash, commit] of Object.entries(blame.commits)) {
188+
bucketByHash.set(hash, computeAgeBucket(commit.date, oldestMs, newestMs));
189+
}
190+
158191
const index = new Map<number, LineEntry>();
159192
for (const range of blame.ranges) {
160193
const commit = blame.commits[range.hash];
194+
const ageBucket = bucketByHash.get(range.hash) ?? 0;
161195
for (let i = 0; i < range.lineCount; i++) {
162196
const lineNumber = range.startLine + i;
163197
const isFirstLineOfRange = i === 0;
@@ -170,6 +204,7 @@ const buildLineIndex = (blame: FileBlameResponse): Map<number, LineEntry> => {
170204
authorEmail: commit.authorEmail,
171205
previous: commit.previous ?? null,
172206
showStartBorder,
207+
ageBucket,
173208
});
174209
} else {
175210
index.set(lineNumber, {
@@ -179,6 +214,7 @@ const buildLineIndex = (blame: FileBlameResponse): Map<number, LineEntry> => {
179214
authorEmail: null,
180215
previous: null,
181216
showStartBorder,
217+
ageBucket,
182218
});
183219
}
184220
}

packages/web/src/app/(app)/browse/[...path]/components/codePreviewPanel/codePreviewPanel.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { X } from "lucide-react";
88
import Image from "next/image";
99
import Link from "next/link";
1010
import { getBrowsePath } from "../../../hooks/utils";
11+
import { BlameAgeLegend } from "./blameAgeLegend";
1112
import { BlameViewToggle } from "./blameViewToggle";
1213
import { PureCodePreviewPanel } from "./pureCodePreviewPanel";
1314
import { getFileBlame, getFileSource } from '@/features/git';
@@ -127,6 +128,12 @@ export const CodePreviewPanel = async ({ path, repoName, revisionName, previewRe
127128
<span className="text-sm text-muted-foreground">
128129
{lineCount.toLocaleString()} lines · {fileSize}
129130
</span>
131+
{blame && (
132+
<>
133+
<Separator orientation="vertical" className="h-4" />
134+
<BlameAgeLegend />
135+
</>
136+
)}
130137
</div>
131138
)}
132139
{previewRef && (

0 commit comments

Comments
 (0)