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