difit 2.0.10 → 2.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 (32) hide show
  1. package/README.ja.md +192 -0
  2. package/README.ko.md +192 -0
  3. package/README.md +61 -44
  4. package/README.zh.md +192 -0
  5. package/dist/cli/index.js +50 -0
  6. package/dist/cli/index.test.js +141 -12
  7. package/dist/cli/utils.js +18 -3
  8. package/dist/cli/utils.test.js +10 -1
  9. package/dist/client/assets/index-BtavrLIu.css +1 -0
  10. package/dist/client/assets/index-Bx2n4Aep.js +210 -0
  11. package/dist/client/assets/{prism-csharp-68c6WkNx.js → prism-csharp-BTkEzOdP.js} +1 -1
  12. package/dist/client/assets/{prism-java-C8EIlB8E.js → prism-java-B6gV82l4.js} +1 -1
  13. package/dist/client/assets/{prism-php-DHZyM8JV.js → prism-php-gnpy0VQF.js} +1 -1
  14. package/dist/client/assets/prism-protobuf-DiQ_z8B5.js +1 -0
  15. package/dist/client/assets/{prism-ruby-MnFNFfyf.js → prism-ruby-CMkpRodx.js} +1 -1
  16. package/dist/client/assets/{prism-solidity-CIeB0O-m.js → prism-solidity-BDXCWkss.js} +1 -1
  17. package/dist/client/index.html +2 -2
  18. package/dist/server/file-watcher.d.ts +23 -0
  19. package/dist/server/file-watcher.js +236 -0
  20. package/dist/server/file-watcher.test.d.ts +1 -0
  21. package/dist/server/file-watcher.test.js +225 -0
  22. package/dist/server/git-diff.d.ts +2 -0
  23. package/dist/server/git-diff.js +47 -4
  24. package/dist/server/git-diff.test.js +209 -0
  25. package/dist/server/server.d.ts +5 -2
  26. package/dist/server/server.js +66 -16
  27. package/dist/server/server.test.js +3 -3
  28. package/dist/types/watch.d.ts +30 -0
  29. package/dist/types/watch.js +8 -0
  30. package/package.json +3 -2
  31. package/dist/client/assets/index-DMBW6MaM.css +0 -1
  32. package/dist/client/assets/index-EuLpHPLj.js +0 -200
@@ -5,7 +5,9 @@ export declare class GitDiffParser {
5
5
  parseDiff(targetCommitish: string, baseCommitish: string, ignoreWhitespace?: boolean): Promise<DiffResponse>;
6
6
  private parseUnifiedDiff;
7
7
  private parseFileBlock;
8
+ private countLinesFromChunks;
8
9
  private parseChunks;
9
10
  validateCommit(commitish: string): Promise<boolean>;
11
+ parseStdinDiff(diffContent: string): DiffResponse;
10
12
  getBlobContent(filepath: string, ref: string): Promise<Buffer>;
11
13
  }
@@ -65,8 +65,14 @@ export class GitDiffParser {
65
65
  for (let i = 0; i < fileBlocks.length; i++) {
66
66
  const block = `diff --git ${fileBlocks[i]}`;
67
67
  const summaryItem = summary[i];
68
- if (!summaryItem)
68
+ // For stdin diff, we don't have summary
69
+ if (!summaryItem) {
70
+ const file = this.parseFileBlock(block, null);
71
+ if (file) {
72
+ files.push(file);
73
+ }
69
74
  continue;
75
+ }
70
76
  const file = this.parseFileBlock(block, summaryItem);
71
77
  if (file) {
72
78
  files.push(file);
@@ -105,8 +111,20 @@ export class GitDiffParser {
105
111
  oldPath: oldPath !== newPath ? oldPath : undefined,
106
112
  status,
107
113
  };
114
+ // Parse chunks
115
+ const chunks = this.parseChunks(lines);
108
116
  // Handle different summary types
109
- if ('binary' in summary && summary.binary) {
117
+ if (!summary) {
118
+ // No summary - count additions/deletions from chunks
119
+ const { additions, deletions } = this.countLinesFromChunks(chunks);
120
+ return {
121
+ ...baseFile,
122
+ additions,
123
+ deletions,
124
+ chunks,
125
+ };
126
+ }
127
+ else if ('binary' in summary && summary.binary) {
110
128
  // Binary file
111
129
  return {
112
130
  ...baseFile,
@@ -116,15 +134,28 @@ export class GitDiffParser {
116
134
  };
117
135
  }
118
136
  else {
119
- // Text file
137
+ // Text file with summary
120
138
  return {
121
139
  ...baseFile,
122
140
  additions: summary.insertions,
123
141
  deletions: summary.deletions,
124
- chunks: this.parseChunks(lines),
142
+ chunks,
125
143
  };
126
144
  }
127
145
  }
146
+ countLinesFromChunks(chunks) {
147
+ let additions = 0;
148
+ let deletions = 0;
149
+ for (const chunk of chunks) {
150
+ for (const line of chunk.lines) {
151
+ if (line.type === 'add')
152
+ additions++;
153
+ else if (line.type === 'delete')
154
+ deletions++;
155
+ }
156
+ }
157
+ return { additions, deletions };
158
+ }
128
159
  parseChunks(lines) {
129
160
  const chunks = [];
130
161
  let currentChunk = null;
@@ -190,6 +221,18 @@ export class GitDiffParser {
190
221
  return false;
191
222
  }
192
223
  }
224
+ parseStdinDiff(diffContent) {
225
+ // For stdin diff, we pass an empty summary array
226
+ // parseUnifiedDiff will handle it by counting additions/deletions from the actual diff content
227
+ const emptySummary = [];
228
+ // Use the existing parseUnifiedDiff method
229
+ const files = this.parseUnifiedDiff(diffContent, emptySummary);
230
+ return {
231
+ commit: 'stdin diff',
232
+ files,
233
+ isEmpty: files.length === 0,
234
+ };
235
+ }
193
236
  async getBlobContent(filepath, ref) {
194
237
  try {
195
238
  // For working directory, read directly from filesystem
@@ -314,4 +314,213 @@ describe('GitDiffParser', () => {
314
314
  expect(result.status).toBe('deleted');
315
315
  });
316
316
  });
317
+ describe('parseStdinDiff', () => {
318
+ it('should parse a simple unified diff', async () => {
319
+ const diffContent = `diff --git a/test.txt b/test.txt
320
+ index abc123..def456 100644
321
+ --- a/test.txt
322
+ +++ b/test.txt
323
+ @@ -1,3 +1,3 @@
324
+ line1
325
+ -line2
326
+ +line2 modified
327
+ line3`;
328
+ const result = parser.parseStdinDiff(diffContent);
329
+ expect(result).toMatchObject({
330
+ commit: 'stdin diff',
331
+ isEmpty: false,
332
+ files: [
333
+ {
334
+ path: 'test.txt',
335
+ status: 'modified',
336
+ additions: 1,
337
+ deletions: 1,
338
+ chunks: expect.any(Array),
339
+ },
340
+ ],
341
+ });
342
+ expect(result.files[0].chunks).toHaveLength(1);
343
+ expect(result.files[0].chunks[0].lines).toHaveLength(4);
344
+ });
345
+ it('should parse multiple files', async () => {
346
+ const diffContent = `diff --git a/file1.txt b/file1.txt
347
+ index abc123..def456 100644
348
+ --- a/file1.txt
349
+ +++ b/file1.txt
350
+ @@ -1 +1 @@
351
+ -old content
352
+ +new content
353
+ diff --git a/file2.js b/file2.js
354
+ new file mode 100644
355
+ index 0000000..1234567
356
+ --- /dev/null
357
+ +++ b/file2.js
358
+ @@ -0,0 +1,3 @@
359
+ +function hello() {
360
+ + console.log('Hello');
361
+ +}`;
362
+ const result = parser.parseStdinDiff(diffContent);
363
+ expect(result.files).toHaveLength(2);
364
+ expect(result.files[0]).toMatchObject({
365
+ path: 'file1.txt',
366
+ status: 'modified',
367
+ additions: 1,
368
+ deletions: 1,
369
+ });
370
+ expect(result.files[1]).toMatchObject({
371
+ path: 'file2.js',
372
+ status: 'added',
373
+ additions: 3,
374
+ deletions: 0,
375
+ });
376
+ });
377
+ it('should handle deleted files', async () => {
378
+ const diffContent = `diff --git a/deleted.txt b/deleted.txt
379
+ deleted file mode 100644
380
+ index abc123..0000000
381
+ --- a/deleted.txt
382
+ +++ /dev/null
383
+ @@ -1,2 +0,0 @@
384
+ -line1
385
+ -line2`;
386
+ const result = parser.parseStdinDiff(diffContent);
387
+ expect(result.files[0]).toMatchObject({
388
+ path: 'deleted.txt',
389
+ status: 'deleted',
390
+ additions: 0,
391
+ deletions: 2,
392
+ });
393
+ });
394
+ it('should handle renamed files', async () => {
395
+ const diffContent = `diff --git a/old-name.txt b/new-name.txt
396
+ similarity index 100%
397
+ rename from old-name.txt
398
+ rename to new-name.txt`;
399
+ const result = parser.parseStdinDiff(diffContent);
400
+ expect(result.files[0]).toMatchObject({
401
+ path: 'new-name.txt',
402
+ oldPath: 'old-name.txt',
403
+ status: 'renamed',
404
+ additions: 0,
405
+ deletions: 0,
406
+ });
407
+ });
408
+ it('should handle empty diff', async () => {
409
+ const result = parser.parseStdinDiff('');
410
+ expect(result).toMatchObject({
411
+ commit: 'stdin diff',
412
+ isEmpty: true,
413
+ files: [],
414
+ });
415
+ });
416
+ it('should count additions and deletions correctly', async () => {
417
+ const diffContent = `diff --git a/test.txt b/test.txt
418
+ index abc123..def456 100644
419
+ --- a/test.txt
420
+ +++ b/test.txt
421
+ @@ -1,5 +1,6 @@
422
+ unchanged line
423
+ -deleted line 1
424
+ -deleted line 2
425
+ +added line 1
426
+ +added line 2
427
+ +added line 3
428
+ another unchanged
429
+ final unchanged`;
430
+ const result = parser.parseStdinDiff(diffContent);
431
+ expect(result.files[0]).toMatchObject({
432
+ additions: 3,
433
+ deletions: 2,
434
+ });
435
+ });
436
+ it('should handle diffs with multiple chunks', async () => {
437
+ const diffContent = `diff --git a/test.txt b/test.txt
438
+ index abc123..def456 100644
439
+ --- a/test.txt
440
+ +++ b/test.txt
441
+ @@ -1,3 +1,3 @@
442
+ line1
443
+ -line2
444
+ +line2 modified
445
+ line3
446
+ @@ -10,3 +10,4 @@
447
+ line10
448
+ line11
449
+ line12
450
+ +line13 added`;
451
+ const result = parser.parseStdinDiff(diffContent);
452
+ expect(result.files[0].chunks).toHaveLength(2);
453
+ expect(result.files[0]).toMatchObject({
454
+ additions: 2,
455
+ deletions: 1,
456
+ });
457
+ });
458
+ it('should handle binary files', async () => {
459
+ const diffContent = `diff --git a/image.png b/image.png
460
+ new file mode 100644
461
+ index 0000000..1234567
462
+ Binary files /dev/null and b/image.png differ`;
463
+ const result = parser.parseStdinDiff(diffContent);
464
+ expect(result.files[0]).toMatchObject({
465
+ path: 'image.png',
466
+ status: 'added',
467
+ additions: 0,
468
+ deletions: 0,
469
+ chunks: [],
470
+ });
471
+ });
472
+ it('should handle diffs with context lines', async () => {
473
+ const diffContent = `diff --git a/test.txt b/test.txt
474
+ index abc123..def456 100644
475
+ --- a/test.txt
476
+ +++ b/test.txt
477
+ @@ -1,7 +1,7 @@
478
+ context before 1
479
+ context before 2
480
+ context before 3
481
+ -old line
482
+ +new line
483
+ context after 1
484
+ context after 2
485
+ context after 3`;
486
+ const result = parser.parseStdinDiff(diffContent);
487
+ const lines = result.files[0].chunks[0].lines;
488
+ expect(lines.filter((l) => l.type === 'normal')).toHaveLength(6);
489
+ expect(lines.filter((l) => l.type === 'delete')).toHaveLength(1);
490
+ expect(lines.filter((l) => l.type === 'add')).toHaveLength(1);
491
+ });
492
+ });
493
+ describe('countLinesFromChunks', () => {
494
+ it('should count additions and deletions correctly', () => {
495
+ const chunks = [
496
+ {
497
+ header: '@@ -1,3 +1,3 @@',
498
+ oldStart: 1,
499
+ oldLines: 3,
500
+ newStart: 1,
501
+ newLines: 3,
502
+ lines: [
503
+ { type: 'normal', content: 'line1' },
504
+ { type: 'delete', content: '-line2' },
505
+ { type: 'add', content: '+line2 modified' },
506
+ { type: 'normal', content: 'line3' },
507
+ ],
508
+ },
509
+ {
510
+ header: '@@ -5,2 +5,3 @@',
511
+ oldStart: 5,
512
+ oldLines: 2,
513
+ newStart: 5,
514
+ newLines: 3,
515
+ lines: [
516
+ { type: 'normal', content: 'line5' },
517
+ { type: 'add', content: '+new line' },
518
+ { type: 'normal', content: 'line6' },
519
+ ],
520
+ },
521
+ ];
522
+ const result = parser.countLinesFromChunks(chunks);
523
+ expect(result).toEqual({ additions: 2, deletions: 1 });
524
+ });
525
+ });
317
526
  });
@@ -1,13 +1,16 @@
1
1
  import { type Server } from 'http';
2
+ import { type DiffMode } from '../types/watch.js';
2
3
  interface ServerOptions {
3
- targetCommitish: string;
4
- baseCommitish: string;
4
+ targetCommitish?: string;
5
+ baseCommitish?: string;
6
+ stdinDiff?: string;
5
7
  preferredPort?: number;
6
8
  host?: string;
7
9
  openBrowser?: boolean;
8
10
  mode?: string;
9
11
  ignoreWhitespace?: boolean;
10
12
  clearComments?: boolean;
13
+ diffMode?: DiffMode;
11
14
  }
12
15
  export declare function startServer(options: ServerOptions): Promise<{
13
16
  port: number;
@@ -5,11 +5,13 @@ import open from 'open';
5
5
  const __filename = fileURLToPath(import.meta.url);
6
6
  const __dirname = dirname(__filename);
7
7
  import { getFileExtension } from '../utils/fileUtils.js';
8
+ import { FileWatcherService } from './file-watcher.js';
8
9
  import { GitDiffParser } from './git-diff.js';
9
10
  export async function startServer(options) {
10
11
  const app = express();
11
12
  const parser = new GitDiffParser();
12
- let diffData;
13
+ const fileWatcher = new FileWatcherService();
14
+ let diffDataCache = null;
13
15
  let currentIgnoreWhitespace = options.ignoreWhitespace || false;
14
16
  const diffMode = options.mode || 'side-by-side';
15
17
  app.use(express.json());
@@ -20,28 +22,48 @@ export async function startServer(options) {
20
22
  res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
21
23
  next();
22
24
  });
23
- const isValidCommit = await parser.validateCommit(options.targetCommitish);
24
- if (!isValidCommit) {
25
- throw new Error(`Invalid or non-existent commit: ${options.targetCommitish}`);
25
+ // Skip validation if using stdin diff
26
+ if (!options.stdinDiff) {
27
+ const isValidCommit = await parser.validateCommit(options.targetCommitish ?? '');
28
+ if (!isValidCommit) {
29
+ throw new Error(`Invalid or non-existent commit: ${options.targetCommitish}`);
30
+ }
31
+ }
32
+ // Generate initial diff data for isEmpty check
33
+ if (options.stdinDiff) {
34
+ // Parse stdin diff directly
35
+ diffDataCache = parser.parseStdinDiff(options.stdinDiff);
26
36
  }
27
- diffData = await parser.parseDiff(options.targetCommitish, options.baseCommitish, currentIgnoreWhitespace);
37
+ else {
38
+ diffDataCache = await parser.parseDiff(options.targetCommitish ?? '', options.baseCommitish ?? '', currentIgnoreWhitespace);
39
+ }
40
+ // Function to invalidate cache when file changes are detected
41
+ const invalidateCache = () => {
42
+ diffDataCache = null;
43
+ };
28
44
  app.get('/api/diff', async (req, res) => {
29
45
  const ignoreWhitespace = req.query.ignoreWhitespace === 'true';
30
- if (ignoreWhitespace !== currentIgnoreWhitespace) {
46
+ // Regenerate diff data if cache is invalid or whitespace setting changed
47
+ if (!diffDataCache || (ignoreWhitespace !== currentIgnoreWhitespace && !options.stdinDiff)) {
31
48
  currentIgnoreWhitespace = ignoreWhitespace;
32
- diffData = await parser.parseDiff(options.targetCommitish, options.baseCommitish, ignoreWhitespace);
49
+ diffDataCache = await parser.parseDiff(options.targetCommitish ?? '', options.baseCommitish ?? '', ignoreWhitespace);
33
50
  }
34
51
  res.json({
35
- ...diffData,
52
+ ...diffDataCache,
36
53
  ignoreWhitespace,
37
54
  mode: diffMode,
38
- baseCommitish: options.baseCommitish,
39
- targetCommitish: options.targetCommitish,
55
+ baseCommitish: options.baseCommitish || 'stdin',
56
+ targetCommitish: options.targetCommitish || 'stdin',
40
57
  clearComments: options.clearComments,
41
58
  });
42
59
  });
43
60
  app.get(/^\/api\/blob\/(.*)$/, async (req, res) => {
44
61
  try {
62
+ // If using stdin diff, blob content is not available
63
+ if (options.stdinDiff) {
64
+ res.status(404).json({ error: 'Blob content not available for stdin diff' });
65
+ return;
66
+ }
45
67
  const filepath = req.params[0];
46
68
  const ref = req.query.ref || 'HEAD';
47
69
  const blob = await parser.getBlobContent(filepath, ref);
@@ -121,6 +143,19 @@ export async function startServer(options) {
121
143
  console.log(formatCommentsOutput(finalComments));
122
144
  }
123
145
  }
146
+ // SSE endpoint for file watching
147
+ app.get('/api/watch', (req, res) => {
148
+ res.writeHead(200, {
149
+ 'Content-Type': 'text/event-stream',
150
+ 'Cache-Control': 'no-cache',
151
+ Connection: 'keep-alive',
152
+ 'Access-Control-Allow-Origin': '*',
153
+ });
154
+ fileWatcher.addClient(res);
155
+ req.on('close', () => {
156
+ fileWatcher.removeClient(res);
157
+ });
158
+ });
124
159
  // SSE endpoint to detect when tab is closed
125
160
  app.get('/api/heartbeat', (req, res) => {
126
161
  res.writeHead(200, {
@@ -138,9 +173,14 @@ export async function startServer(options) {
138
173
  // When client disconnects (tab closed, navigation, etc.)
139
174
  req.on('close', () => {
140
175
  clearInterval(heartbeatInterval);
141
- console.log('Client disconnected, shutting down server...');
142
- outputFinalComments();
143
- process.exit(0);
176
+ // Add a small delay to ensure any pending sendBeacon requests are processed
177
+ setTimeout(async () => {
178
+ console.log('Client disconnected, shutting down server...');
179
+ // Stop file watcher
180
+ await fileWatcher.stop();
181
+ outputFinalComments();
182
+ process.exit(0);
183
+ }, 100);
144
184
  });
145
185
  });
146
186
  // Always runs in production mode when distributed as a CLI tool
@@ -172,15 +212,25 @@ export async function startServer(options) {
172
212
  `);
173
213
  });
174
214
  }
175
- const { port, url, server } = await startServerWithFallback(app, options.preferredPort || 3000, options.host || 'localhost');
215
+ const { port, url, server } = await startServerWithFallback(app, options.preferredPort || 4966, options.host || 'localhost');
176
216
  // Security warning for non-localhost binding
177
217
  if (options.host && options.host !== '127.0.0.1' && options.host !== 'localhost') {
178
218
  console.warn('\n⚠️ WARNING: Server is accessible from external network!');
179
219
  console.warn(` Binding to: ${options.host}:${port}`);
180
220
  console.warn(' Make sure this is intended and your network is secure.\n');
181
221
  }
222
+ // Start file watcher
223
+ if (options.diffMode) {
224
+ try {
225
+ await fileWatcher.start(options.diffMode, process.cwd(), 300, invalidateCache);
226
+ }
227
+ catch (error) {
228
+ console.warn('⚠️ File watcher failed to start:', error);
229
+ console.warn(' Continuing without file watching...');
230
+ }
231
+ }
182
232
  // Check if diff is empty and skip browser opening
183
- if (diffData.isEmpty) {
233
+ if (diffDataCache?.isEmpty) {
184
234
  // Don't open browser if no differences found
185
235
  }
186
236
  else if (options.openBrowser) {
@@ -191,7 +241,7 @@ export async function startServer(options) {
191
241
  console.warn('Failed to open browser automatically');
192
242
  }
193
243
  }
194
- return { port, url, isEmpty: diffData.isEmpty, server };
244
+ return { port, url, isEmpty: diffDataCache?.isEmpty || false, server };
195
245
  }
196
246
  async function startServerWithFallback(app, preferredPort, host) {
197
247
  return new Promise((resolve, reject) => {
@@ -260,7 +260,7 @@ describe('Server Integration Tests', () => {
260
260
  mode: 'inline',
261
261
  });
262
262
  servers.push(result.server);
263
- expect(result.port).toBeGreaterThanOrEqual(3000);
263
+ expect(result.port).toBeGreaterThanOrEqual(4966);
264
264
  expect(result.url).toContain('http://localhost:');
265
265
  });
266
266
  it('accepts different mode values', async () => {
@@ -276,8 +276,8 @@ describe('Server Integration Tests', () => {
276
276
  mode: 'side-by-side',
277
277
  });
278
278
  servers.push(sideBySideResult.server);
279
- expect(inlineResult.port).toBeGreaterThanOrEqual(3000);
280
- expect(sideBySideResult.port).toBeGreaterThanOrEqual(3000);
279
+ expect(inlineResult.port).toBeGreaterThanOrEqual(4966);
280
+ expect(sideBySideResult.port).toBeGreaterThanOrEqual(4966);
281
281
  });
282
282
  it('mode option should be included in diff response', async () => {
283
283
  const result = await startServer({
@@ -0,0 +1,30 @@
1
+ export declare enum DiffMode {
2
+ DEFAULT = "default",// HEAD^ vs HEAD
3
+ WORKING = "working",// staged vs working
4
+ STAGED = "staged",// HEAD vs staged
5
+ DOT = "dot",// HEAD vs working (all changes)
6
+ SPECIFIC = "specific"
7
+ }
8
+ export interface FileWatchConfig {
9
+ watchPath: string;
10
+ diffMode: DiffMode;
11
+ ignore: string[];
12
+ debounceMs: number;
13
+ backend?: 'fs-events' | 'watchman' | 'windows' | 'linux';
14
+ }
15
+ export interface WatchEvent {
16
+ type: 'reload' | 'error' | 'connected';
17
+ diffMode: DiffMode;
18
+ changeType: 'file' | 'commit' | 'staging';
19
+ timestamp: string;
20
+ message?: string;
21
+ }
22
+ export interface ClientWatchState {
23
+ isWatchEnabled: boolean;
24
+ diffMode: DiffMode;
25
+ shouldReload: boolean;
26
+ isReloading: boolean;
27
+ lastChangeTime: Date | null;
28
+ lastChangeType: 'file' | 'commit' | 'staging' | null;
29
+ connectionStatus: 'connected' | 'disconnected' | 'reconnecting';
30
+ }
@@ -0,0 +1,8 @@
1
+ export var DiffMode;
2
+ (function (DiffMode) {
3
+ DiffMode["DEFAULT"] = "default";
4
+ DiffMode["WORKING"] = "working";
5
+ DiffMode["STAGED"] = "staged";
6
+ DiffMode["DOT"] = "dot";
7
+ DiffMode["SPECIFIC"] = "specific";
8
+ })(DiffMode || (DiffMode = {}));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "difit",
3
- "version": "2.0.10",
3
+ "version": "2.1.0",
4
4
  "description": "A lightweight command-line tool that spins up a local web server to display Git commit diffs in a GitHub-like Files changed view",
5
5
  "type": "module",
6
6
  "engines": {
@@ -35,6 +35,7 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@octokit/rest": "^22.0.0",
38
+ "@parcel/watcher": "^2.5.1",
38
39
  "commander": "^14.0.0",
39
40
  "dracula-prism": "^2.1.16",
40
41
  "express": "^5.1.0",
@@ -46,7 +47,7 @@
46
47
  "prismjs": "^1.30.0",
47
48
  "react": "^19.1.0",
48
49
  "react-dom": "^19.1.0",
49
- "shiki": "^3.7.0",
50
+ "react-hotkeys-hook": "^5.1.0",
50
51
  "simple-git": "^3.28.0"
51
52
  },
52
53
  "devDependencies": {
@@ -1 +0,0 @@
1
- /*! tailwindcss v4.1.11 | MIT License | https://tailwindcss.com */@layer properties{@supports ((-webkit-hyphens:none) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial;--tw-ease:initial;--tw-content:"";--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-100:oklch(93.6% .032 17.717);--color-yellow-400:oklch(85.2% .199 91.936);--color-yellow-600:oklch(68.1% .162 75.834);--color-green-100:oklch(96.2% .044 156.743);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-blue-100:oklch(93.2% .032 255.585);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-slate-400:oklch(70.4% .04 256.788);--color-slate-500:oklch(55.4% .046 257.417);--color-slate-600:oklch(44.6% .043 257.281);--color-white:#fff;--spacing:.25rem;--container-md:28rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--font-weight-medium:500;--font-weight-semibold:600;--radius-md:.375rem;--radius-lg:.5rem;--ease-out:cubic-bezier(0,0,.2,1);--ease-in-out:cubic-bezier(.4,0,.2,1);--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-github-bg-primary:#0d1117;--color-github-bg-secondary:#161b22;--color-github-bg-tertiary:#21262d;--color-github-border:#30363d;--color-github-text-primary:#f0f6fc;--color-github-text-secondary:#8b949e;--color-github-text-muted:#6e7681;--color-github-accent:#238636;--color-github-danger:#da3633;--color-github-warning:#d29922;--color-diff-addition-bg:#0d4429;--color-diff-addition-border:#1b7c3d;--color-diff-deletion-bg:#67060c;--color-diff-deletion-border:#da3633;--color-diff-neutral-bg:#21262d;--color-diff-selected-bg:#ae7c1426;--color-diff-selected-border:#ae7c1466;--color-comment-bg:#1c2128;--color-comment-border:#373e47;--color-comment-text:#e6edf3}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::-moz-placeholder{opacity:1}::placeholder{opacity:1}@supports (not (-webkit-appearance:-apple-pay-button)) or (contain-intrinsic-size:1px){::-moz-placeholder{color:currentColor}::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::-moz-placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing)*0)}.top-0{top:calc(var(--spacing)*0)}.top-1\/2{top:50%}.-right-2{right:calc(var(--spacing)*-2)}.right-0{right:calc(var(--spacing)*0)}.left-0{left:calc(var(--spacing)*0)}.left-3{left:calc(var(--spacing)*3)}.z-10{z-index:10}.z-50{z-index:50}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.m-0{margin:calc(var(--spacing)*0)}.m-2{margin:calc(var(--spacing)*2)}.mx-3{margin-inline:calc(var(--spacing)*3)}.mx-4{margin-inline:calc(var(--spacing)*4)}.mx-auto{margin-inline:auto}.mt-2{margin-top:calc(var(--spacing)*2)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.ml-auto{margin-left:auto}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.h-2{height:calc(var(--spacing)*2)}.h-4{height:calc(var(--spacing)*4)}.h-7{height:calc(var(--spacing)*7)}.h-full{height:100%}.h-screen{height:100vh}.max-h-96{max-height:calc(var(--spacing)*96)}.min-h-\[16px\]{min-height:16px}.min-h-\[20px\]{min-height:20px}.min-h-\[60px\]{min-height:60px}.w-1\/2{width:50%}.w-4{width:calc(var(--spacing)*4)}.w-5{width:calc(var(--spacing)*5)}.w-7{width:calc(var(--spacing)*7)}.w-8{width:calc(var(--spacing)*8)}.w-\[50px\]{width:50px}.w-\[60px\]{width:60px}.w-full{width:100%}.max-w-full{max-width:100%}.max-w-md{max-width:var(--container-md)}.min-w-0{min-width:calc(var(--spacing)*0)}.flex-1{flex:1}.flex-shrink-0{flex-shrink:0}.border-collapse{border-collapse:collapse}.-translate-y-1\/2{--tw-translate-y: -50% ;translate:var(--tw-translate-x)var(--tw-translate-y)}.rotate-180{rotate:180deg}.transform{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.cursor-col-resize{cursor:col-resize}.cursor-pointer{cursor:pointer}.resize{resize:both}.resize-none{resize:none}.resize-y{resize:vertical}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.justify-start{justify-content:flex-start}.gap-1{gap:calc(var(--spacing)*1)}.gap-1\.5{gap:calc(var(--spacing)*1.5)}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*6)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-y-reverse)))}.overflow-hidden{overflow:hidden}.overflow-visible{overflow:visible}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-l{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-r{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.rounded-b{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-t-2{border-top-style:var(--tw-border-style);border-top-width:2px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-4{border-left-style:var(--tw-border-style);border-left-width:4px}.border-none{--tw-border-style:none;border-style:none}.border-\[var\(--border-muted\)\]{border-color:var(--border-muted)}.border-github-accent{border-color:var(--color-github-accent)}.border-github-border{border-color:var(--color-github-border)}.border-github-text-muted{border-color:var(--color-github-text-muted)}.border-yellow-600\/50{border-color:#cd890080}@supports (color:color-mix(in lab,red,red)){.border-yellow-600\/50{border-color:color-mix(in oklab,var(--color-yellow-600)50%,transparent)}}.border-t-github-accent{border-top-color:var(--color-github-accent)}.border-l-yellow-400{border-left-color:var(--color-yellow-400)}.bg-\[var\(--bg-secondary\)\]{background-color:var(--bg-secondary)}.bg-blue-600{background-color:var(--color-blue-600)}.bg-diff-addition-bg{background-color:var(--color-diff-addition-bg)}.bg-diff-deletion-bg{background-color:var(--color-diff-deletion-bg)}.bg-github-accent{background-color:var(--color-github-accent)}.bg-github-bg-primary{background-color:var(--color-github-bg-primary)}.bg-github-bg-secondary{background-color:var(--color-github-bg-secondary)}.bg-github-bg-tertiary{background-color:var(--color-github-bg-tertiary)}.bg-github-border{background-color:var(--color-github-border)}.bg-green-100\/10{background-color:#dcfce71a}@supports (color:color-mix(in lab,red,red)){.bg-green-100\/10{background-color:color-mix(in oklab,var(--color-green-100)10%,transparent)}}.bg-red-100\/10{background-color:#ffe2e21a}@supports (color:color-mix(in lab,red,red)){.bg-red-100\/10{background-color:color-mix(in oklab,var(--color-red-100)10%,transparent)}}.bg-transparent{background-color:#0000}.p-0{padding:calc(var(--spacing)*0)}.p-1{padding:calc(var(--spacing)*1)}.p-1\.5{padding:calc(var(--spacing)*1.5)}.p-2{padding:calc(var(--spacing)*2)}.p-3{padding:calc(var(--spacing)*3)}.p-4{padding:calc(var(--spacing)*4)}.px-1{padding-inline:calc(var(--spacing)*1)}.px-1\.5{padding-inline:calc(var(--spacing)*1.5)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-5{padding-inline:calc(var(--spacing)*5)}.py-0\.5{padding-block:calc(var(--spacing)*.5)}.py-1{padding-block:calc(var(--spacing)*1)}.py-1\.5{padding-block:calc(var(--spacing)*1.5)}.py-2{padding-block:calc(var(--spacing)*2)}.py-3{padding-block:calc(var(--spacing)*3)}.py-4{padding-block:calc(var(--spacing)*4)}.pr-2{padding-right:calc(var(--spacing)*2)}.pr-3{padding-right:calc(var(--spacing)*3)}.pr-5{padding-right:calc(var(--spacing)*5)}.pb-px{padding-bottom:1px}.pl-9{padding-left:calc(var(--spacing)*9)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.align-top{vertical-align:top}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-5{--tw-leading:calc(var(--spacing)*5);line-height:calc(var(--spacing)*5)}.leading-6{--tw-leading:calc(var(--spacing)*6);line-height:calc(var(--spacing)*6)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.break-all{word-break:break-all}.text-ellipsis{text-overflow:ellipsis}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-github-accent{color:var(--color-github-accent)}.text-github-danger{color:var(--color-github-danger)}.text-github-text-muted{color:var(--color-github-text-muted)}.text-github-text-primary{color:var(--color-github-text-primary)}.text-github-text-secondary{color:var(--color-github-text-secondary)}.text-github-warning{color:var(--color-github-warning)}.text-green-600{color:var(--color-green-600)}.text-white{color:var(--color-white)}.lowercase{text-transform:lowercase}.italic{font-style:italic}.line-through{text-decoration-line:line-through}.placeholder-github-text-muted::-moz-placeholder{color:var(--color-github-text-muted)}.placeholder-github-text-muted::placeholder{color:var(--color-github-text-muted)}.accent-github-accent{accent-color:var(--color-github-accent)}.opacity-70{opacity:.7}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.\!transition-all{transition-property:all!important;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function))!important;transition-duration:var(--tw-duration,var(--default-transition-duration))!important}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.\!duration-300{--tw-duration:.3s!important;transition-duration:.3s!important}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.\!ease-in-out{--tw-ease:var(--ease-in-out)!important;transition-timing-function:var(--ease-in-out)!important}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.select-text{-webkit-user-select:text;-moz-user-select:text;user-select:text}.after\:pointer-events-none:after{content:var(--tw-content);pointer-events:none}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:inset-0:after{content:var(--tw-content);inset:calc(var(--spacing)*0)}.after\:border-t-2:after{content:var(--tw-content);border-top-style:var(--tw-border-style);border-top-width:2px}.after\:border-b-2:after{content:var(--tw-content);border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.after\:border-l-4:after{content:var(--tw-content);border-left-style:var(--tw-border-style);border-left-width:4px}.after\:border-l-5:after{content:var(--tw-content);border-left-style:var(--tw-border-style);border-left-width:5px}.after\:border-blue-500:after{content:var(--tw-content);border-color:var(--color-blue-500)}.after\:border-l-diff-selected-border:after{content:var(--tw-content);border-left-color:var(--color-diff-selected-border)}.after\:bg-blue-100:after{content:var(--tw-content);background-color:var(--color-blue-100)}.after\:bg-diff-selected-bg:after{content:var(--tw-content);background-color:var(--color-diff-selected-bg)}.after\:opacity-30:after{content:var(--tw-content);opacity:.3}@media (hover:hover){.hover\:scale-110:hover{--tw-scale-x:110%;--tw-scale-y:110%;--tw-scale-z:110%;scale:var(--tw-scale-x)var(--tw-scale-y)}.hover\:border-github-accent\/50:hover{border-color:#23863680}@supports (color:color-mix(in lab,red,red)){.hover\:border-github-accent\/50:hover{border-color:color-mix(in oklab,var(--color-github-accent)50%,transparent)}}.hover\:border-github-text-muted:hover{border-color:var(--color-github-text-muted)}.hover\:border-green-600:hover{border-color:var(--color-green-600)}.hover\:bg-github-bg-primary:hover{background-color:var(--color-github-bg-primary)}.hover\:bg-github-bg-tertiary:hover{background-color:var(--color-github-bg-tertiary)}.hover\:bg-github-text-muted:hover{background-color:var(--color-github-text-muted)}.hover\:bg-green-500\/10:hover{background-color:#00c7581a}@supports (color:color-mix(in lab,red,red)){.hover\:bg-green-500\/10:hover{background-color:color-mix(in oklab,var(--color-green-500)10%,transparent)}}.hover\:bg-green-600:hover{background-color:var(--color-green-600)}.hover\:text-github-text-primary:hover{color:var(--color-github-text-primary)}.hover\:opacity-80:hover{opacity:.8}}.focus\:min-h-\[80px\]:focus{min-height:80px}.focus\:border-blue-600:focus{border-color:var(--color-blue-600)}.focus\:border-github-accent:focus{border-color:var(--color-github-accent)}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-blue-600\/30:focus{--tw-ring-color:#155dfc4d}@supports (color:color-mix(in lab,red,red)){.focus\:ring-blue-600\/30:focus{--tw-ring-color:color-mix(in oklab,var(--color-blue-600)30%,transparent)}}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.disabled\:opacity-50:disabled{opacity:.5}@media (prefers-color-scheme:dark){.dark\:border-slate-500{border-color:var(--color-slate-500)}.dark\:bg-slate-600{background-color:var(--color-slate-600)}.dark\:text-white{color:var(--color-white)}@media (hover:hover){.dark\:hover\:border-slate-400:hover{border-color:var(--color-slate-400)}.dark\:hover\:bg-slate-500:hover{background-color:var(--color-slate-500)}}}.\[\&_code\]\:\!bg-transparent code{background-color:#0000!important}.\[\&_code\]\:text-inherit code{color:inherit}.\[\&_pre\]\:m-0 pre{margin:calc(var(--spacing)*0)}.\[\&_pre\]\:\!bg-transparent pre{background-color:#0000!important}.\[\&_pre\]\:p-0 pre{padding:calc(var(--spacing)*0)}.\[\&_pre\]\:text-inherit pre{color:inherit}}:root{--app-font-size:14px;--app-font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif}html,body{background-color:var(--color-github-bg-primary);color:var(--color-github-text-primary);font-family:var(--app-font-family);line-height:1.5;font-size:var(--app-font-size)}:root{interpolate-size:allow-keywords}button{cursor:pointer}@keyframes sparkle-rise{0%{opacity:0;transform:translateY(20px)scale(.5)}20%{opacity:1;transform:translateY(10px)scale(1)}80%{opacity:1;transform:translateY(-30px)scale(1)}to{opacity:0;transform:translateY(-40px)scale(.8)}}.animate-sparkle-rise{animation:.8s ease-out both sparkle-rise}html,body,.bg-github-bg-primary,.bg-github-bg-secondary,.bg-github-bg-tertiary,[class*=bg-github],[class*=text-github],[class*=border-github],[class*=bg-diff],[class*=border-diff]{transition:background-color .3s,color .3s,border-color .3s}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-content{syntax:"*";inherits:false;initial-value:""}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}