git-trace 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/.tracerc.example +38 -0
  2. package/README.md +136 -0
  3. package/bun.lock +511 -0
  4. package/bunchee.config.ts +11 -0
  5. package/cli/index.ts +251 -0
  6. package/cli/parser.ts +76 -0
  7. package/cli/tsconfig.json +6 -0
  8. package/dist/cli/index.d.ts +1 -0
  9. package/dist/cli/index.js +858 -0
  10. package/dist/config.cjs +66 -0
  11. package/dist/config.d.ts +15 -0
  12. package/dist/config.js +63 -0
  13. package/dist/highlight/index.cjs +770 -0
  14. package/dist/highlight/index.d.ts +26 -0
  15. package/dist/highlight/index.js +766 -0
  16. package/dist/index.cjs +849 -0
  17. package/dist/index.d.ts +52 -0
  18. package/dist/index.js +845 -0
  19. package/examples/demo/App.tsx +78 -0
  20. package/examples/demo/index.html +12 -0
  21. package/examples/demo/main.tsx +10 -0
  22. package/examples/demo/mockData.ts +170 -0
  23. package/examples/demo/styles.css +103 -0
  24. package/examples/demo/tsconfig.json +21 -0
  25. package/examples/demo/tsconfig.node.json +10 -0
  26. package/examples/demo/vite.config.ts +20 -0
  27. package/package.json +58 -0
  28. package/src/Trace.tsx +717 -0
  29. package/src/cache.ts +118 -0
  30. package/src/config.ts +51 -0
  31. package/src/entries/config.ts +7 -0
  32. package/src/entries/gitea.ts +4 -0
  33. package/src/entries/github.ts +5 -0
  34. package/src/entries/gitlab.ts +4 -0
  35. package/src/gitea.ts +58 -0
  36. package/src/github.ts +100 -0
  37. package/src/gitlab.ts +65 -0
  38. package/src/highlight/highlight.ts +119 -0
  39. package/src/highlight/index.ts +4 -0
  40. package/src/host.ts +32 -0
  41. package/src/index.ts +6 -0
  42. package/src/patterns.ts +6 -0
  43. package/src/shared.ts +108 -0
  44. package/src/themes.ts +98 -0
  45. package/src/types.ts +72 -0
  46. package/test/e2e.html +424 -0
  47. package/tsconfig.json +18 -0
  48. package/vercel.json +4 -0
package/dist/index.js ADDED
@@ -0,0 +1,845 @@
1
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
+ import { useState, useRef, useMemo, useCallback, useEffect, memo } from 'react';
3
+
4
+ // Default AI detection patterns — browser-safe, no Node.js dependencies
5
+ const DEFAULT_PATTERNS = {
6
+ emails: [
7
+ 'noreply@cursor.sh',
8
+ 'claude@anthropic.com',
9
+ 'bot@github.com',
10
+ 'copilot',
11
+ 'cursor'
12
+ ],
13
+ messages: [
14
+ 'Co-Authored-By: Claude',
15
+ 'Co-Authored-By: Cursor',
16
+ 'Generated-by:',
17
+ '[skip-human-review]',
18
+ 'AI-generated'
19
+ ]
20
+ };
21
+
22
+ // Shared utilities for git adapters — parse diffs, detect AI authors, format dates
23
+ // Compile string arrays to lowercase Sets for fast O(1) lookup
24
+ function compilePatterns(patterns) {
25
+ return {
26
+ emails: new Set(patterns.emails.map((p)=>p.toLowerCase())),
27
+ messages: new Set(patterns.messages.map((p)=>p.toLowerCase()))
28
+ };
29
+ }
30
+ // Cached compiled defaults
31
+ compilePatterns(DEFAULT_PATTERNS);
32
+ // Escape HTML to prevent XSS attacks
33
+ function escapeHtml(unsafe) {
34
+ return unsafe.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;');
35
+ }
36
+
37
+ // 2026 preset themes — modern, accessible, trend-aligned color schemes
38
+ const themes = {
39
+ // Dark theme (default) — classic developer dark mode
40
+ dark: {
41
+ bg: '#09090b',
42
+ fg: '#e4e4e7',
43
+ dim: '#71717a',
44
+ human: '#22c55e',
45
+ ai: '#a855f7',
46
+ add: 'rgba(34, 102, 68, 0.25)',
47
+ remove: 'rgba(185, 28, 28, 0.2)',
48
+ border: '#27272a',
49
+ borderSubtle: '#18181b'
50
+ },
51
+ // Light theme — clean, minimal
52
+ light: {
53
+ bg: '#ffffff',
54
+ fg: '#18181b',
55
+ dim: '#71717a',
56
+ human: '#16a34a',
57
+ ai: '#9333ea',
58
+ add: 'rgba(22, 163, 74, 0.15)',
59
+ remove: 'rgba(220, 38, 38, 0.12)',
60
+ border: '#e4e4e7',
61
+ borderSubtle: '#f4f4f5'
62
+ },
63
+ // Midnight — deep blue-black, cyberpunk adjacent
64
+ midnight: {
65
+ bg: '#030712',
66
+ fg: '#e2e8f0',
67
+ dim: '#64748b',
68
+ human: '#06b6d4',
69
+ ai: '#f472b6',
70
+ add: 'rgba(6, 182, 212, 0.2)',
71
+ remove: 'rgba(244, 114, 182, 0.15)',
72
+ border: '#1e293b',
73
+ borderSubtle: '#0f172a'
74
+ },
75
+ // Cyber — neon accents on near-black
76
+ cyber: {
77
+ bg: '#050505',
78
+ fg: '#f0f0f0',
79
+ dim: '#6b6b6b',
80
+ human: '#00ff9f',
81
+ ai: '#ff00ff',
82
+ add: 'rgba(0, 255, 159, 0.2)',
83
+ remove: 'rgba(255, 0, 255, 0.15)',
84
+ border: '#2a2a2a',
85
+ borderSubtle: '#0a0a0a'
86
+ },
87
+ // Forest — nature-inspired greens
88
+ forest: {
89
+ bg: '#0a1208',
90
+ fg: '#d4e5d4',
91
+ dim: '#5a6a5a',
92
+ human: '#4ade80',
93
+ ai: '#2dd4bf',
94
+ add: 'rgba(74, 222, 128, 0.2)',
95
+ remove: 'rgba(248, 113, 113, 0.15)',
96
+ border: '#1a2a1a',
97
+ borderSubtle: '#0a180a'
98
+ },
99
+ // Sunset — warm tones, easy on eyes
100
+ sunset: {
101
+ bg: '#0f0808',
102
+ fg: '#f5e6e6',
103
+ dim: '#7a5a5a',
104
+ human: '#fb923c',
105
+ ai: '#f43f5e',
106
+ add: 'rgba(251, 146, 60, 0.2)',
107
+ remove: 'rgba(244, 63, 94, 0.15)',
108
+ border: '#2a1a1a',
109
+ borderSubtle: '#1a0a0a'
110
+ }
111
+ };
112
+ // Generate CSS variables from theme colors
113
+ function themeToVars(theme) {
114
+ return {
115
+ '--trace-bg': theme.bg,
116
+ '--trace-fg': theme.fg,
117
+ '--trace-dim': theme.dim,
118
+ '--trace-human': theme.human,
119
+ '--trace-ai': theme.ai,
120
+ '--trace-add': theme.add,
121
+ '--trace-remove': theme.remove,
122
+ '--trace-border': theme.border,
123
+ '--trace-border-subtle': theme.borderSubtle
124
+ };
125
+ }
126
+
127
+ const styles = `
128
+ .trace-root {
129
+ --trace-bg: #09090b;
130
+ --trace-fg: #e4e4e7;
131
+ --trace-dim: #71717a;
132
+ --trace-human: #22c55e;
133
+ --trace-ai: #a855f7;
134
+ --trace-add: rgba(34, 102, 68, 0.2);
135
+ --trace-remove: rgba(185, 28, 28, 0.2);
136
+ --trace-border: #27272a;
137
+ --trace-border-subtle: #18181b;
138
+ --trace-font-code: 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace;
139
+ --trace-line-height: 1.6;
140
+ --trace-radius: 0;
141
+ --trace-duration: 0.18s;
142
+ --trace-stagger: 35ms;
143
+ display: block;
144
+ font-family: var(--trace-font-code);
145
+ background: var(--trace-bg);
146
+ color: var(--trace-fg);
147
+ border-radius: var(--trace-radius);
148
+ overflow: hidden;
149
+ font-size: 13px;
150
+ }
151
+ .trace-container {
152
+ display: flex;
153
+ height: 100%;
154
+ min-height: 420px;
155
+ }
156
+ .trace-timeline {
157
+ width: 240px;
158
+ border-right: 1px solid var(--trace-border-subtle);
159
+ padding: 0;
160
+ overflow-y: auto;
161
+ flex-shrink: 0;
162
+ position: relative;
163
+ background: var(--trace-bg);
164
+ /* Performance: isolate layout and paint */
165
+ contain: layout style paint;
166
+ will-change: scroll-position;
167
+ }
168
+ .trace-progress {
169
+ position: absolute;
170
+ left: 0;
171
+ top: 0;
172
+ bottom: 0;
173
+ width: 2px;
174
+ background: var(--trace-commit-color, var(--trace-human));
175
+ opacity: 0.6;
176
+ pointer-events: none;
177
+ transition: height 0.25s ease-out;
178
+ }
179
+ .trace-commit {
180
+ position: relative;
181
+ padding: 12px 16px;
182
+ cursor: pointer;
183
+ transition: background 0.12s ease;
184
+ border-left: 2px solid transparent;
185
+ }
186
+ .trace-commit:hover {
187
+ background: rgba(255, 255, 255, 0.02);
188
+ }
189
+ .trace-commit.active {
190
+ background: rgba(255, 255, 255, 0.03);
191
+ border-left-color: var(--trace-commit-color, var(--trace-human));
192
+ }
193
+ .trace-commit-header {
194
+ display: flex;
195
+ align-items: center;
196
+ gap: 8px;
197
+ }
198
+ .trace-commit-dot {
199
+ width: 6px;
200
+ height: 6px;
201
+ border-radius: 50%;
202
+ background: var(--trace-commit-color, var(--trace-human));
203
+ flex-shrink: 0;
204
+ }
205
+ .trace-commit-message {
206
+ font-size: 12px;
207
+ font-weight: 400;
208
+ white-space: nowrap;
209
+ overflow: hidden;
210
+ text-overflow: ellipsis;
211
+ flex: 1;
212
+ color: var(--trace-fg);
213
+ }
214
+ .trace-commit-meta {
215
+ font-size: 11px;
216
+ color: var(--trace-dim);
217
+ margin-top: 4px;
218
+ padding-left: 14px;
219
+ }
220
+ .trace-badge {
221
+ font-size: 10px;
222
+ color: var(--trace-dim);
223
+ font-weight: 400;
224
+ text-transform: none;
225
+ letter-spacing: -0.02em;
226
+ }
227
+ .trace-diff {
228
+ flex: 1;
229
+ padding: 0;
230
+ overflow-y: auto;
231
+ font-size: 13px;
232
+ line-height: var(--trace-line-height);
233
+ background: var(--trace-bg);
234
+ /* Performance: isolate layout and paint */
235
+ contain: layout style paint;
236
+ will-change: scroll-position;
237
+ }
238
+ .trace-diff-header {
239
+ padding: 12px 16px;
240
+ border-bottom: 1px solid var(--trace-border-subtle);
241
+ background: var(--trace-bg);
242
+ position: sticky;
243
+ top: 0;
244
+ z-index: 1;
245
+ }
246
+ .trace-diff-header-message {
247
+ font-size: 13px;
248
+ font-weight: 500;
249
+ color: var(--trace-fg);
250
+ margin-bottom: 4px;
251
+ }
252
+ .trace-diff-header-meta {
253
+ font-size: 11px;
254
+ color: var(--trace-dim);
255
+ }
256
+ .trace-line {
257
+ padding: 1px 16px;
258
+ /* 2026: content-visibility for lazy rendering off-screen lines */
259
+ content-visibility: auto;
260
+ contain-intrinsic-size: auto 22px;
261
+ min-height: 22px;
262
+ display: flex;
263
+ align-items: flex-start;
264
+ /* Performance: isolate layout changes */
265
+ contain: layout style paint;
266
+ }
267
+ .trace-line-prefix {
268
+ width: 16px;
269
+ flex-shrink: 0;
270
+ color: var(--trace-dim);
271
+ opacity: 0.5;
272
+ font-size: 11px;
273
+ user-select: none;
274
+ text-align: center;
275
+ }
276
+ .trace-line-content {
277
+ flex: 1;
278
+ white-space: pre;
279
+ overflow-x: auto;
280
+ }
281
+ @keyframes trace-line-in {
282
+ from {
283
+ opacity: 0;
284
+ transform: translateX(-2px);
285
+ }
286
+ to {
287
+ opacity: 1;
288
+ transform: translateX(0);
289
+ }
290
+ }
291
+ .trace-line.add {
292
+ background: var(--trace-add);
293
+ color: #86efac;
294
+ }
295
+ .trace-line.add .trace-line-prefix {
296
+ color: #22c55e;
297
+ }
298
+ .trace-line.remove {
299
+ background: var(--trace-remove);
300
+ color: #fca5a5;
301
+ }
302
+ .trace-line.remove .trace-line-prefix {
303
+ color: #ef4444;
304
+ }
305
+ .trace-line.ctx {
306
+ opacity: 0.5;
307
+ }
308
+ .trace-controls {
309
+ display: flex;
310
+ gap: 4px;
311
+ padding: 8px 16px;
312
+ border-bottom: 1px solid var(--trace-border-subtle);
313
+ align-items: center;
314
+ background: var(--trace-bg);
315
+ }
316
+ .trace-btn {
317
+ background: transparent;
318
+ border: 1px solid var(--trace-border);
319
+ color: var(--trace-fg);
320
+ padding: 4px 10px;
321
+ border-radius: var(--trace-radius);
322
+ font-size: 11px;
323
+ cursor: pointer;
324
+ transition: all 0.12s ease;
325
+ font-family: inherit;
326
+ }
327
+ .trace-btn:hover:not(:disabled) {
328
+ background: rgba(255, 255, 255, 0.05);
329
+ border-color: var(--trace-dim);
330
+ }
331
+ .trace-btn:disabled {
332
+ opacity: 0.3;
333
+ cursor: not-allowed;
334
+ }
335
+ .trace-info {
336
+ font-size: 11px;
337
+ color: var(--trace-dim);
338
+ margin-left: auto;
339
+ }
340
+ .trace-empty {
341
+ display: flex;
342
+ align-items: center;
343
+ justify-content: center;
344
+ height: 400px;
345
+ color: var(--trace-dim);
346
+ }
347
+ .trace-filter-bar {
348
+ display: flex;
349
+ gap: 8px;
350
+ padding: 8px 16px;
351
+ border-bottom: 1px solid var(--trace-border-subtle);
352
+ align-items: center;
353
+ background: var(--trace-bg);
354
+ flex-wrap: wrap;
355
+ }
356
+ .trace-filter-input {
357
+ flex: 1;
358
+ min-width: 140px;
359
+ background: transparent;
360
+ border: 1px solid var(--trace-border);
361
+ color: var(--trace-fg);
362
+ padding: 6px 10px;
363
+ border-radius: var(--trace-radius);
364
+ font-size: 12px;
365
+ font-family: inherit;
366
+ outline: none;
367
+ transition: border-color 0.12s ease;
368
+ }
369
+ .trace-filter-input:focus {
370
+ border-color: var(--trace-dim);
371
+ }
372
+ .trace-filter-input::placeholder {
373
+ color: var(--trace-dim);
374
+ }
375
+ .trace-filter-group {
376
+ display: flex;
377
+ gap: 4px;
378
+ align-items: center;
379
+ }
380
+ .trace-filter-btn {
381
+ background: transparent;
382
+ border: 1px solid var(--trace-border);
383
+ color: var(--trace-dim);
384
+ padding: 4px 10px;
385
+ border-radius: var(--trace-radius);
386
+ font-size: 11px;
387
+ cursor: pointer;
388
+ transition: all 0.12s ease;
389
+ font-family: inherit;
390
+ }
391
+ .trace-filter-btn:hover {
392
+ color: var(--trace-fg);
393
+ border-color: var(--trace-dim);
394
+ }
395
+ .trace-filter-btn.active {
396
+ background: rgba(168, 85, 247, 0.15);
397
+ color: var(--trace-ai);
398
+ border-color: var(--trace-ai);
399
+ }
400
+ .trace-filter-btn.active[data-filter="human"] {
401
+ background: rgba(34, 197, 94, 0.15);
402
+ color: var(--trace-human);
403
+ border-color: var(--trace-human);
404
+ }
405
+ .trace-filter-clear {
406
+ background: transparent;
407
+ border: none;
408
+ color: var(--trace-dim);
409
+ padding: 4px 8px;
410
+ border-radius: var(--trace-radius);
411
+ font-size: 11px;
412
+ cursor: pointer;
413
+ transition: color 0.12s ease;
414
+ font-family: inherit;
415
+ }
416
+ .trace-filter-clear:hover {
417
+ color: var(--trace-fg);
418
+ }
419
+ .trace-filter-stats {
420
+ font-size: 11px;
421
+ color: var(--trace-dim);
422
+ margin-left: auto;
423
+ }
424
+ `;
425
+ // Line prefix lookup - constant outside render
426
+ const LINE_PREFIX = {
427
+ add: '+',
428
+ remove: '-',
429
+ ctx: ' '
430
+ };
431
+ const STYLE_ID = 'trace-styles';
432
+ // Filter bar component with search and filters
433
+ const FilterBar = /*#__PURE__*/ memo(function FilterBar({ filter, onFilterChange, totalCount, filteredCount }) {
434
+ const authorType = filter.authorType ?? 'all';
435
+ const setAuthorType = (type)=>{
436
+ onFilterChange({
437
+ ...filter,
438
+ authorType: type === authorType ? 'all' : type
439
+ });
440
+ };
441
+ const setSearch = (search)=>{
442
+ onFilterChange({
443
+ ...filter,
444
+ search: search || undefined
445
+ });
446
+ };
447
+ const clearFilter = ()=>{
448
+ onFilterChange({});
449
+ };
450
+ const hasActiveFilter = filter.search || filter.authorType && filter.authorType !== 'all';
451
+ return /*#__PURE__*/ jsxs("div", {
452
+ className: "trace-filter-bar",
453
+ children: [
454
+ /*#__PURE__*/ jsx("input", {
455
+ type: "text",
456
+ className: "trace-filter-input",
457
+ placeholder: "Search commits...",
458
+ value: filter.search ?? '',
459
+ onChange: (e)=>setSearch(e.target.value),
460
+ "aria-label": "Search commits"
461
+ }),
462
+ /*#__PURE__*/ jsxs("div", {
463
+ className: "trace-filter-group",
464
+ children: [
465
+ /*#__PURE__*/ jsx("button", {
466
+ className: `trace-filter-btn ${authorType === 'ai' ? 'active' : ''}`,
467
+ "data-filter": "ai",
468
+ onClick: ()=>setAuthorType('ai'),
469
+ "aria-label": "Filter by AI commits",
470
+ children: "AI"
471
+ }),
472
+ /*#__PURE__*/ jsx("button", {
473
+ className: `trace-filter-btn ${authorType === 'human' ? 'active' : ''}`,
474
+ "data-filter": "human",
475
+ onClick: ()=>setAuthorType('human'),
476
+ "aria-label": "Filter by Human commits",
477
+ children: "Human"
478
+ })
479
+ ]
480
+ }),
481
+ hasActiveFilter && /*#__PURE__*/ jsx("button", {
482
+ className: "trace-filter-clear",
483
+ onClick: clearFilter,
484
+ "aria-label": "Clear filters",
485
+ children: "clear"
486
+ }),
487
+ /*#__PURE__*/ jsxs("span", {
488
+ className: "trace-filter-stats",
489
+ children: [
490
+ filteredCount,
491
+ " / ",
492
+ totalCount
493
+ ]
494
+ })
495
+ ]
496
+ });
497
+ });
498
+ // Memoized line component - only re-renders when line content changes
499
+ const DiffLine = /*#__PURE__*/ memo(function DiffLine({ line, prefix }) {
500
+ return /*#__PURE__*/ jsxs("div", {
501
+ className: `trace-line ${line.type}`,
502
+ children: [
503
+ /*#__PURE__*/ jsx("span", {
504
+ className: "trace-line-prefix",
505
+ children: prefix
506
+ }),
507
+ /*#__PURE__*/ jsx("span", {
508
+ className: "trace-line-content",
509
+ dangerouslySetInnerHTML: {
510
+ __html: escapeHtml(line.content)
511
+ }
512
+ })
513
+ ]
514
+ });
515
+ });
516
+ // Memoized commit item - only re-renders when active state or commit changes
517
+ const CommitItem = /*#__PURE__*/ memo(function CommitItem({ commit, index, isActive, onClick }) {
518
+ return /*#__PURE__*/ jsxs("div", {
519
+ className: `trace-commit ${isActive ? 'active' : ''}`,
520
+ style: {
521
+ '--trace-commit-color': commit.authorType === 'ai' ? 'var(--trace-ai)' : 'var(--trace-human)'
522
+ },
523
+ onClick: onClick,
524
+ children: [
525
+ /*#__PURE__*/ jsxs("div", {
526
+ className: "trace-commit-header",
527
+ children: [
528
+ /*#__PURE__*/ jsx("span", {
529
+ className: "trace-commit-dot",
530
+ "aria-hidden": "true"
531
+ }),
532
+ /*#__PURE__*/ jsx("span", {
533
+ className: "trace-commit-message",
534
+ children: commit.message
535
+ }),
536
+ /*#__PURE__*/ jsx("span", {
537
+ className: `trace-badge ${commit.authorType}`,
538
+ "aria-label": `Author type: ${commit.authorType}`,
539
+ children: commit.authorType
540
+ })
541
+ ]
542
+ }),
543
+ /*#__PURE__*/ jsxs("div", {
544
+ className: "trace-commit-meta",
545
+ children: [
546
+ commit.author,
547
+ " · ",
548
+ commit.time
549
+ ]
550
+ })
551
+ ]
552
+ });
553
+ });
554
+ // Memoized diff content - only re-renders when active commit changes
555
+ const DiffContent = /*#__PURE__*/ memo(function DiffContent({ commit, activeIndex, linePrefix }) {
556
+ return /*#__PURE__*/ jsxs(Fragment, {
557
+ children: [
558
+ /*#__PURE__*/ jsxs("div", {
559
+ className: "trace-diff-header",
560
+ children: [
561
+ /*#__PURE__*/ jsx("div", {
562
+ className: "trace-diff-header-message",
563
+ children: commit.message
564
+ }),
565
+ /*#__PURE__*/ jsxs("div", {
566
+ className: "trace-diff-header-meta",
567
+ children: [
568
+ commit.hash,
569
+ " · ",
570
+ commit.author
571
+ ]
572
+ })
573
+ ]
574
+ }),
575
+ commit.lines.map((line, index)=>/*#__PURE__*/ jsx(DiffLine, {
576
+ line: line,
577
+ prefix: linePrefix[line.type] || ' '
578
+ }, `${activeIndex}-${index}-${line.type}`))
579
+ ]
580
+ });
581
+ });
582
+ function injectStyles() {
583
+ if (typeof document === 'undefined') return;
584
+ if (document.getElementById(STYLE_ID)) return;
585
+ const style = document.createElement('style');
586
+ style.id = STYLE_ID;
587
+ style.textContent = styles;
588
+ document.head.appendChild(style);
589
+ }
590
+ function Trace({ commits = [], autoPlay = false, interval = 2000, onCommit, className = '', theme = 'dark', filterable = false, defaultFilter = {}, onFilterChange }) {
591
+ const themeVars = themeToVars(themes[theme]);
592
+ const [activeIndex, setActiveIndex] = useState(0);
593
+ const [isPlaying, setIsPlaying] = useState(autoPlay);
594
+ const [filter, setFilter] = useState(defaultFilter);
595
+ const intervalRef = useRef(undefined);
596
+ const commitsRef = useRef(commits);
597
+ const onCommitRef = useRef(onCommit);
598
+ // Filter commits based on filter options
599
+ const filteredCommits = useMemo(()=>{
600
+ let result = commits;
601
+ if (filter.search) {
602
+ const searchLower = filter.search.toLowerCase();
603
+ result = result.filter((c)=>c.message.toLowerCase().includes(searchLower) || c.author.toLowerCase().includes(searchLower) || c.hash.toLowerCase().includes(searchLower));
604
+ }
605
+ if (filter.authorType && filter.authorType !== 'all') {
606
+ result = result.filter((c)=>c.authorType === filter.authorType);
607
+ }
608
+ return result;
609
+ }, [
610
+ commits,
611
+ filter
612
+ ]);
613
+ // Handle filter change
614
+ const handleFilterChange = useCallback((newFilter)=>{
615
+ setFilter(newFilter);
616
+ setActiveIndex(0);
617
+ setIsPlaying(false);
618
+ onFilterChange?.(newFilter);
619
+ }, [
620
+ onFilterChange
621
+ ]);
622
+ useEffect(()=>{
623
+ injectStyles();
624
+ }, []);
625
+ useEffect(()=>{
626
+ commitsRef.current = commits;
627
+ onCommitRef.current = onCommit;
628
+ }, [
629
+ commits,
630
+ onCommit
631
+ ]);
632
+ useEffect(()=>{
633
+ if (!isPlaying) {
634
+ if (intervalRef.current) {
635
+ clearInterval(intervalRef.current);
636
+ intervalRef.current = undefined;
637
+ }
638
+ return;
639
+ }
640
+ intervalRef.current = window.setInterval(()=>{
641
+ setActiveIndex((i)=>{
642
+ const next = i + 1;
643
+ const maxIndex = filteredCommits.length - 1;
644
+ if (next >= maxIndex) {
645
+ setIsPlaying(false);
646
+ return maxIndex;
647
+ }
648
+ return next;
649
+ });
650
+ }, interval);
651
+ return ()=>{
652
+ if (intervalRef.current) {
653
+ clearInterval(intervalRef.current);
654
+ }
655
+ };
656
+ }, [
657
+ isPlaying,
658
+ interval,
659
+ filteredCommits.length
660
+ ]);
661
+ useEffect(()=>{
662
+ if (filteredCommits[activeIndex]) {
663
+ onCommitRef.current?.(filteredCommits[activeIndex]);
664
+ }
665
+ }, [
666
+ activeIndex,
667
+ filteredCommits
668
+ ]);
669
+ // Helper to navigate with View Transitions API (2026: native smooth transitions)
670
+ const navigateToIndex = useCallback((newIndex)=>{
671
+ const setActive = ()=>setActiveIndex(newIndex);
672
+ // Use View Transitions API for smooth native transitions (Chrome 111+)
673
+ // Disabled for performance - can be enabled via prop if needed
674
+ // if (document.startViewTransition) {
675
+ // try {
676
+ // document.startViewTransition(setActive)
677
+ // } catch (err: unknown) {
678
+ // setActive()
679
+ // console.error('View transition failed:', err)
680
+ // }
681
+ // } else {
682
+ setActive();
683
+ // }
684
+ }, []);
685
+ const handlePrev = useCallback(()=>{
686
+ navigateToIndex(Math.max(0, activeIndex - 1));
687
+ }, [
688
+ activeIndex,
689
+ navigateToIndex
690
+ ]);
691
+ const handleNext = useCallback(()=>{
692
+ navigateToIndex(Math.min(filteredCommits.length - 1, activeIndex + 1));
693
+ }, [
694
+ activeIndex,
695
+ navigateToIndex,
696
+ filteredCommits.length
697
+ ]);
698
+ const togglePlay = useCallback(()=>{
699
+ setIsPlaying((v)=>!v);
700
+ }, []);
701
+ // Keyboard navigation handler
702
+ const handleKeyDown = useCallback((e)=>{
703
+ switch(e.key){
704
+ case 'ArrowUp':
705
+ case 'ArrowLeft':
706
+ e.preventDefault();
707
+ handlePrev();
708
+ break;
709
+ case 'ArrowDown':
710
+ case 'ArrowRight':
711
+ e.preventDefault();
712
+ handleNext();
713
+ break;
714
+ case ' ':
715
+ e.preventDefault();
716
+ togglePlay();
717
+ break;
718
+ case 'Escape':
719
+ e.preventDefault();
720
+ navigateToIndex(0);
721
+ setIsPlaying(false);
722
+ break;
723
+ case 'Home':
724
+ e.preventDefault();
725
+ navigateToIndex(0);
726
+ break;
727
+ case 'End':
728
+ e.preventDefault();
729
+ navigateToIndex(filteredCommits.length - 1);
730
+ break;
731
+ }
732
+ }, [
733
+ handlePrev,
734
+ handleNext,
735
+ togglePlay,
736
+ navigateToIndex,
737
+ filteredCommits.length
738
+ ]);
739
+ if (commits.length === 0) {
740
+ return /*#__PURE__*/ jsx("div", {
741
+ className: `trace-root ${className}`,
742
+ children: /*#__PURE__*/ jsx("div", {
743
+ className: "trace-empty",
744
+ children: "no commits to display"
745
+ })
746
+ });
747
+ }
748
+ if (filteredCommits.length === 0) {
749
+ return /*#__PURE__*/ jsx("div", {
750
+ className: `trace-root ${className}`,
751
+ children: /*#__PURE__*/ jsx("div", {
752
+ className: "trace-empty",
753
+ children: "no commits match your filter"
754
+ })
755
+ });
756
+ }
757
+ const activeCommit = filteredCommits[activeIndex];
758
+ const progressHeight = filteredCommits.length > 0 ? (activeIndex + 1) / filteredCommits.length * 100 : 0;
759
+ // Guard against out-of-bounds activeIndex (can happen with rapid navigation)
760
+ if (!activeCommit) {
761
+ return /*#__PURE__*/ jsx("div", {
762
+ className: `trace-root ${className}`,
763
+ children: /*#__PURE__*/ jsx("div", {
764
+ className: "trace-empty",
765
+ children: "commit not found"
766
+ })
767
+ });
768
+ }
769
+ return /*#__PURE__*/ jsxs("div", {
770
+ className: `trace-root ${className}`,
771
+ style: themeVars,
772
+ onKeyDown: handleKeyDown,
773
+ tabIndex: 0,
774
+ children: [
775
+ filterable && /*#__PURE__*/ jsx(FilterBar, {
776
+ filter: filter,
777
+ onFilterChange: handleFilterChange,
778
+ totalCount: commits.length,
779
+ filteredCount: filteredCommits.length
780
+ }),
781
+ /*#__PURE__*/ jsxs("div", {
782
+ className: "trace-controls",
783
+ children: [
784
+ /*#__PURE__*/ jsx("button", {
785
+ className: "trace-btn",
786
+ onClick: handlePrev,
787
+ disabled: activeIndex === 0,
788
+ children: "prev"
789
+ }),
790
+ /*#__PURE__*/ jsx("button", {
791
+ className: "trace-btn",
792
+ onClick: togglePlay,
793
+ children: isPlaying ? 'pause' : 'play'
794
+ }),
795
+ /*#__PURE__*/ jsx("button", {
796
+ className: "trace-btn",
797
+ onClick: handleNext,
798
+ disabled: activeIndex >= filteredCommits.length - 1,
799
+ children: "next"
800
+ }),
801
+ /*#__PURE__*/ jsxs("span", {
802
+ className: "trace-info",
803
+ children: [
804
+ activeIndex + 1,
805
+ " / ",
806
+ filteredCommits.length
807
+ ]
808
+ })
809
+ ]
810
+ }),
811
+ /*#__PURE__*/ jsxs("div", {
812
+ className: "trace-container",
813
+ children: [
814
+ /*#__PURE__*/ jsxs("div", {
815
+ className: "trace-timeline",
816
+ children: [
817
+ /*#__PURE__*/ jsx("div", {
818
+ className: "trace-progress",
819
+ style: {
820
+ height: `${progressHeight}%`
821
+ }
822
+ }),
823
+ filteredCommits.map((commit, index)=>/*#__PURE__*/ jsx(CommitItem, {
824
+ commit: commit,
825
+ index: index,
826
+ isActive: index === activeIndex,
827
+ onClick: ()=>navigateToIndex(index)
828
+ }, commit.hash))
829
+ ]
830
+ }),
831
+ /*#__PURE__*/ jsx("div", {
832
+ className: "trace-diff",
833
+ children: /*#__PURE__*/ jsx(DiffContent, {
834
+ commit: activeCommit,
835
+ activeIndex: activeIndex,
836
+ linePrefix: LINE_PREFIX
837
+ })
838
+ })
839
+ ]
840
+ })
841
+ ]
842
+ });
843
+ }
844
+
845
+ export { Trace, themeToVars, themes };