@@ -3,6 +3,7 @@ import { EditorState, Extension, Prec, Range as CMRange, RangeSet, StateField }
33import { formatDistanceToNowStrict } from "date-fns" ;
44import type { FileBlameResponse } from "@/features/git" ;
55import { cn } from "@/lib/utils" ;
6+ import { BLAME_AGE_BG_CLASSES , computeAgeBucket } from "./blameAgeColors" ;
67
78type 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
157174const 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 }
0 commit comments