diffstory 0.2.1__tar.gz → 0.3.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diffstory
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: Transform Git diffs into rich, interactive, self-contained HTML reports
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/lakshayjindal/diffstory
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "diffstory"
7
- version = "0.2.1"
7
+ version = "0.3.0"
8
8
  description = "Transform Git diffs into rich, interactive, self-contained HTML reports"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -1,3 +1,3 @@
1
1
  """DiffStory — Transform Git diffs into rich, interactive HTML reports."""
2
2
 
3
- __version__ = "0.2.1"
3
+ __version__ = "0.3.0"
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import argparse
6
6
  import sys
7
+ import webbrowser
7
8
  from pathlib import Path
8
9
  from typing import Optional
9
10
 
@@ -14,6 +15,7 @@ from diffstory.git_utils import (
14
15
  check_git_repo,
15
16
  get_diff,
16
17
  get_diff_with_renames,
18
+ get_git_root,
17
19
  )
18
20
  from diffstory.html_generator import generate_report
19
21
 
@@ -141,6 +143,13 @@ def build_parser() -> argparse.ArgumentParser:
141
143
  help="Generate report from a diff file directly (no git repository needed)",
142
144
  )
143
145
 
146
+ parser.add_argument(
147
+ "--no-open",
148
+ action="store_true",
149
+ default=False,
150
+ help="Do not open the report in a browser after generation",
151
+ )
152
+
144
153
  parser.add_argument(
145
154
  "--verbose", "-v",
146
155
  action="store_true",
@@ -279,6 +288,16 @@ def _export_csv(files, output_path: Path) -> None:
279
288
  print(f" CSV: {output_path}")
280
289
 
281
290
 
291
+ def _open_in_browser(path: str) -> None:
292
+ """Open the generated report in the default browser."""
293
+ try:
294
+ file_url = "file://" + path
295
+ webbrowser.open(file_url)
296
+ print(f" Opened in browser: {file_url}")
297
+ except Exception as e:
298
+ print(f" Could not open browser: {e}", file=sys.stderr)
299
+
300
+
282
301
  def _read_diff_from_file(path: str) -> str:
283
302
  """Read diff content from a file."""
284
303
  try:
@@ -291,6 +310,31 @@ def _read_diff_from_file(path: str) -> str:
291
310
  sys.exit(1)
292
311
 
293
312
 
313
+ def _resolve_output_path(given_path: str) -> str:
314
+ """Resolve the output file path.
315
+
316
+ If the given path is the default and we're inside a git repo,
317
+ place it in a 'stories/' directory outside the git working tree
318
+ so that git does not track it.
319
+ """
320
+ given = Path(given_path)
321
+ # Only redirect the default path — if the user explicitly passed -o, use as-is
322
+ if given.name != "diffstory-report.html":
323
+ return str(given.resolve())
324
+
325
+ try:
326
+ git_root = get_git_root()
327
+ if git_root:
328
+ git_root = Path(git_root).resolve()
329
+ stories_dir = git_root.parent / "stories"
330
+ stories_dir.mkdir(parents=True, exist_ok=True)
331
+ return str(stories_dir / given.name)
332
+ except Exception:
333
+ pass
334
+
335
+ return str(given.resolve())
336
+
337
+
294
338
  def main() -> None:
295
339
  """Main entry point for the diffstory CLI."""
296
340
  parser = build_parser()
@@ -304,6 +348,9 @@ def main() -> None:
304
348
  if debug:
305
349
  verbose = True # debug implies verbose
306
350
 
351
+ # Resolve output path — for the default, put it outside the git repo
352
+ output_path = _resolve_output_path(args.output)
353
+
307
354
  # Handle --diff flag (read diff from file, no git needed)
308
355
  if args.diff:
309
356
  if verbose:
@@ -317,9 +364,9 @@ def main() -> None:
317
364
  sys.exit(0)
318
365
  has_exports = args.json or args.md or args.csv
319
366
  if has_exports:
320
- generate_exports(files, args.output, args.json, args.md, args.csv)
367
+ generate_exports(files, output_path, args.json, args.md, args.csv)
321
368
  try:
322
- report_path = generate_report(files, output_path=args.output, repo_name="diff", verbose=verbose)
369
+ report_path = generate_report(files, output_path=output_path, repo_name="diff", verbose=verbose)
323
370
  except Exception as e:
324
371
  if debug:
325
372
  import traceback
@@ -328,6 +375,10 @@ def main() -> None:
328
375
  sys.exit(1)
329
376
  print(f"\\n HTML: {report_path}")
330
377
  print(" Report generated successfully!")
378
+
379
+ # Open in browser unless --no-open
380
+ if not args.no_open:
381
+ _open_in_browser(report_path)
331
382
  return
332
383
 
333
384
  # Validate Git repository
@@ -378,13 +429,13 @@ def main() -> None:
378
429
  # Generate exports if requested
379
430
  has_exports = args.json or args.md or args.csv
380
431
  if has_exports:
381
- generate_exports(files, args.output, args.json, args.md, args.csv)
432
+ generate_exports(files, output_path, args.json, args.md, args.csv)
382
433
 
383
434
  # Always generate HTML report
384
435
  try:
385
436
  report_path = generate_report(
386
437
  files,
387
- output_path=args.output,
438
+ output_path=output_path,
388
439
  staged=args.staged,
389
440
  commit_a=commit_a,
390
441
  commit_b=commit_b,
@@ -399,3 +450,7 @@ def main() -> None:
399
450
 
400
451
  print(f"\\n HTML: {report_path}")
401
452
  print(" Report generated successfully!")
453
+
454
+ # Open in browser unless --no-open
455
+ if not args.no_open:
456
+ _open_in_browser(report_path)
@@ -45,6 +45,14 @@ def check_git_repo(cwd: Optional[Path] = None) -> bool:
45
45
  return False
46
46
 
47
47
 
48
+ def get_git_root(cwd: Optional[Path] = None) -> Optional[str]:
49
+ """Get the absolute path to the root of the Git repository."""
50
+ try:
51
+ return _run_git(["rev-parse", "--show-toplevel"], cwd=cwd).strip()
52
+ except GitError:
53
+ return None
54
+
55
+
48
56
  def get_diff(
49
57
  staged: bool = False,
50
58
  commit_a: Optional[str] = None,
@@ -362,29 +362,61 @@ def _collect_blame_data(
362
362
  Returns a dict with:
363
363
  - line_blame: dict mapping "file_idx:lineno" to blame entry
364
364
  - commits: dict mapping commit_hash to commit metadata
365
+
366
+ Handles renamed files by using the old path for deletion blame
367
+ and the new path for addition/context blame. Skips blame when
368
+ no revision info is available (e.g. --diff mode).
365
369
  """
370
+ # If no commit info at all, skip blame entirely (e.g. --diff mode)
371
+ if not staged and commit_a is None and commit_b is None:
372
+ return {"line_blame": {}, "commits": {}}
373
+
366
374
  line_blame: dict = {}
367
375
  all_commits: set = set()
368
376
 
369
377
  for fi, file in enumerate(files):
370
- filepath = file.display_path
371
- if filepath == "/dev/null":
378
+ new_filepath = file.display_path
379
+ old_filepath = file.old_path if file.old_path != "/dev/null" else file.display_path
380
+
381
+ if new_filepath == "/dev/null" and old_filepath == "/dev/null":
372
382
  continue
373
383
 
374
- # Determine which revision to blame
384
+ # Determine which revisions to blame
375
385
  new_revision = None # None means working tree
376
386
  if commit_b:
377
387
  new_revision = commit_b
378
388
  elif staged:
379
- # For staged, blame working tree to capture staged additions
380
- pass # blame working tree
389
+ pass # blame working tree for staged additions
381
390
 
382
- # Get blame for current (new) version
383
- # TODO: handle renamed files — for renames, file.display_path is the new path
384
- # but blame on the old revision needs the old path
385
- blame_new = get_blame_for_revision(filepath, revision=new_revision)
386
-
387
- # Map blame by new line number for additions and context
391
+ old_revision = None
392
+ if commit_a:
393
+ old_revision = commit_a
394
+ elif staged:
395
+ old_revision = "HEAD"
396
+
397
+ # Get blame for current (new) version — skip if file doesn't exist at revision
398
+ blame_new: dict = {}
399
+ if new_filepath != "/dev/null":
400
+ try:
401
+ blame_new = get_blame_for_revision(new_filepath, revision=new_revision)
402
+ except Exception:
403
+ pass
404
+
405
+ # Get blame for old version if different from new (e.g. renames)
406
+ blame_old: dict = blame_new
407
+ if old_revision and old_filepath != new_filepath:
408
+ try:
409
+ blame_old = get_blame_for_revision(old_filepath, revision=old_revision)
410
+ except Exception:
411
+ blame_old = {}
412
+ elif old_revision and old_filepath == new_filepath and old_revision != new_revision:
413
+ # Same path but different revision — re-blame
414
+ try:
415
+ blame_old = get_blame_for_revision(old_filepath, revision=old_revision)
416
+ except Exception:
417
+ pass
418
+
419
+ # Map blame by line number for additions and context
388
420
  for hunk in file.hunks:
389
421
  for line in hunk.lines:
390
422
  if line.line_type in ("addition", "context") and line.new_lineno:
@@ -395,31 +427,22 @@ def _collect_blame_data(
395
427
  all_commits.add(entry["commit"])
396
428
 
397
429
  elif line.line_type == "deletion" and line.old_lineno:
398
- # For deletions, try to blame the old version of the file
399
- # commit_a is the old version being diffed
400
- old_revision = None
401
- if commit_a:
402
- old_revision = commit_a
403
- elif staged:
404
- old_revision = "HEAD"
405
-
406
- if old_revision:
407
- try:
408
- blame_old = get_blame_for_revision(filepath, revision=old_revision)
409
- entry = blame_old.get(line.old_lineno)
410
- if entry:
411
- key = str(fi) + ":" + str(line.old_lineno)
412
- line_blame[key] = entry
413
- all_commits.add(entry["commit"])
414
- except Exception:
415
- pass
430
+ if blame_old:
431
+ entry = blame_old.get(line.old_lineno)
432
+ if entry:
433
+ key = str(fi) + ":" + str(line.old_lineno)
434
+ line_blame[key] = entry
435
+ all_commits.add(entry["commit"])
416
436
 
417
437
  # Collect commit metadata for all unique commits
418
438
  commits: dict = {}
419
439
  for chash in all_commits:
420
440
  if chash and len(chash) == 40:
421
- info = get_commit_info(chash)
422
- commits[chash] = info
441
+ try:
442
+ info = get_commit_info(chash)
443
+ commits[chash] = info
444
+ except Exception:
445
+ pass
423
446
 
424
447
  return {
425
448
  "line_blame": line_blame,
@@ -620,10 +643,27 @@ def _build_html_template(
620
643
  ' <button class="view-btn" data-view="inline" onclick="switchView(\'inline\')" title="Inline Edit (I)">Inline</button>\n'
621
644
  ' </div>\n'
622
645
  ' <div class="toolbar-right">\n'
623
- ' <button class="tool-btn" onclick="focusSearch()" id="search-btn" title="Search (F or /)">\U0001f50d</button>\n'
624
- ' <button class="tool-btn" onclick="toggleTheme()" id="theme-btn" title="Toggle Theme (D)">\U0001f319</button>\n'
625
- ' <button class="tool-btn" onclick="toggleStats()" id="stats-btn" title="Statistics">\U0001f4ca</button>\n'
626
- ' <button class="tool-btn" onclick="toggleSidebar()" id="sidebar-btn" title="File List">\U0001f4c1</button>\n'
646
+ ' <button class="tool-btn" onclick="focusSearch()" id="search-btn" title="Search (F or /)">\n'
647
+ ' <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" class="tool-icon">\n'
648
+ ' <path d="M10.68 11.74a6 6 0 0 1-7.922-8.982 6 6 0 0 1 8.982 7.922l3.04 3.04a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215ZM11.5 7a4.499 4.499 0 1 0-8.997 0A4.499 4.499 0 0 0 11.5 7Z"/>\n'
649
+ ' </svg>\n'
650
+ ' </button>\n'
651
+ ' <button class="tool-btn" onclick="toggleTheme()" id="theme-btn" title="Toggle Theme (D)">\n'
652
+ ' <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" class="tool-icon" id="theme-icon">\n'
653
+ ' <path d="M8 12a4 4 0 1 1 0-8 4 4 0 0 1 0 8Zm0-1.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5Zm5.657-8.157a.75.75 0 0 1 0 1.061l-1.061 1.06a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734l1.06-1.06a.75.75 0 0 1 1.06 0Zm-9.193 9.193a.75.75 0 0 1 0 1.06l-1.06 1.061a.75.75 0 1 1-1.061-1.06l1.06-1.061a.75.75 0 0 1 1.061 0ZM8 0a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0V.75A.75.75 0 0 1 8 0ZM3 8a.75.75 0 0 1-.75.75H.75a.75.75 0 0 1 0-1.5h1.5A.75.75 0 0 1 3 8Zm13 0a.75.75 0 0 1-.75.75h-1.5a.75.75 0 0 1 0-1.5h1.5A.75.75 0 0 1 16 8Zm-8 5a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 8 13Zm3.536-1.464a.75.75 0 0 1 1.06 0l1.061 1.06a.75.75 0 0 1-1.06 1.061l-1.061-1.06a.75.75 0 0 1 0-1.061ZM2.343 2.343a.75.75 0 0 1 1.061 0l1.06 1.061a.751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018l-1.06-1.06a.75.75 0 0 1 0-1.06Z"/>\n'
654
+ ' </svg>\n'
655
+ ' </button>\n'
656
+ ' <button class="tool-btn" onclick="toggleStats()" id="stats-btn" title="Statistics">\n'
657
+ ' <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" class="tool-icon">\n'
658
+ ' <path d="M1.5 1.75V13.5h13.75a.75.75 0 0 1 0 1.5H.75a.75.75 0 0 1-.75-.75V1.75a.75.75 0 0 1 1.5 0Zm14.28 2.53-5.25 5.25a.75.75 0 0 1-1.06 0L7 7.06 4.28 9.78a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042l3.25-3.25a.75.75 0 0 1 1.06 0L10 7.94l4.72-4.72a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z"/>\n'
659
+ ' </svg>\n'
660
+ ' </button>\n'
661
+ ' <button class="tool-btn" onclick="toggleSidebar()" id="sidebar-btn" title="File List">\n'
662
+ ' <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" class="tool-icon">\n'
663
+ ' <path d="M6.823 7.823a.25.25 0 0 1 0 .354l-2.396 2.396A.25.25 0 0 1 4 10.396V5.604a.25.25 0 0 1 .427-.177Z"/>\n'
664
+ ' <path d="M1.75 0h12.5C15.216 0 16 .784 16 1.75v12.5A1.75 1.75 0 0 1 14.25 16H1.75A1.75 1.75 0 0 1 0 14.25V1.75C0 .784.784 0 1.75 0ZM1.5 1.75v12.5c0 .138.112.25.25.25H9.5v-13H1.75a.25.25 0 0 0-.25.25ZM11 14.5h3.25a.25.25 0 0 0 .25-.25V1.75a.25.25 0 0 0-.25-.25H11Z"/>\n'
665
+ ' </svg>\n'
666
+ ' </button>\n'
627
667
  ' </div>\n'
628
668
  ' </header>\n'
629
669
  ' <!-- Global Search Bar -->\n'
@@ -756,6 +796,8 @@ def _get_css() -> str:
756
796
  --text-secondary: #656d76;
757
797
  --border: #d0d7de;
758
798
  --border-light: #e0e4e8;
799
+ --line-number-color: #6e7681;
800
+ --diff-context-color: #656d76;
759
801
  --accent: #0969da;
760
802
  --accent-hover: #0550ae;
761
803
  --add-bg: #e6ffec;
@@ -788,6 +830,8 @@ def _get_css() -> str:
788
830
  --text-secondary: #8b949e;
789
831
  --border: #30363d;
790
832
  --border-light: #21262d;
833
+ --line-number-color: #484f58;
834
+ --diff-context-color: #8b949e;
791
835
  --accent: #58a6ff;
792
836
  --accent-hover: #79c0ff;
793
837
  --add-bg: #12262b;
@@ -813,7 +857,7 @@ def _get_css() -> str:
813
857
  }
814
858
 
815
859
  body {
816
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
860
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif, 'Apple Color Emoji';
817
861
  font-size: 14px;
818
862
  line-height: 1.5;
819
863
  color: var(--text);
@@ -828,6 +872,15 @@ body {
828
872
  height: 100vh;
829
873
  }
830
874
 
875
+ .tool-icon {
876
+ display: block;
877
+ fill: currentColor;
878
+ }
879
+
880
+ .tool-btn svg.tool-icon {
881
+ pointer-events: none;
882
+ }
883
+
831
884
  /* Toolbar */
832
885
  #toolbar {
833
886
  display: flex;
@@ -1075,7 +1128,7 @@ body {
1075
1128
  flex: 1;
1076
1129
  font-size: 14px;
1077
1130
  font-weight: 600;
1078
- font-family: 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', monospace;
1131
+ font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
1079
1132
  overflow: hidden;
1080
1133
  text-overflow: ellipsis;
1081
1134
  white-space: nowrap;
@@ -1130,7 +1183,7 @@ body {
1130
1183
  padding: 6px 14px;
1131
1184
  background: var(--hunk-header-bg);
1132
1185
  color: var(--hunk-header-text);
1133
- font-family: 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', monospace;
1186
+ font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
1134
1187
  font-size: 12px;
1135
1188
  border-bottom: 1px solid var(--border-light);
1136
1189
  }
@@ -1143,7 +1196,7 @@ body {
1143
1196
  .diff-line {
1144
1197
  display: flex;
1145
1198
  align-items: stretch;
1146
- font-family: 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', monospace;
1199
+ font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
1147
1200
  font-size: 12px;
1148
1201
  line-height: 1.5;
1149
1202
  min-height: 22px;
@@ -1214,7 +1267,7 @@ body {
1214
1267
  width: 50%;
1215
1268
  display: flex;
1216
1269
  align-items: stretch;
1217
- font-family: 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', monospace;
1270
+ font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
1218
1271
  font-size: 12px;
1219
1272
  line-height: 1.5;
1220
1273
  min-height: 22px;
@@ -1366,7 +1419,7 @@ body {
1366
1419
  .stats-table td {
1367
1420
  padding: 6px 8px;
1368
1421
  border-bottom: 1px solid var(--border-light);
1369
- font-family: 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', monospace;
1422
+ font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
1370
1423
  font-size: 12px;
1371
1424
  }
1372
1425
 
@@ -1428,7 +1481,7 @@ body {
1428
1481
  }
1429
1482
 
1430
1483
  .tooltip-commit {
1431
- font-family: 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', monospace;
1484
+ font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
1432
1485
  font-size: 11px;
1433
1486
  color: var(--text-secondary);
1434
1487
  }
@@ -1517,7 +1570,7 @@ body {
1517
1570
  }
1518
1571
 
1519
1572
  .drawer-commit-hash {
1520
- font-family: 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', monospace;
1573
+ font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
1521
1574
  font-size: 13px;
1522
1575
  color: var(--accent);
1523
1576
  }
@@ -1760,7 +1813,7 @@ body {
1760
1813
  font-size: 14px;
1761
1814
  font-weight: 600;
1762
1815
  color: var(--text);
1763
- font-family: 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', monospace;
1816
+ font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
1764
1817
  }
1765
1818
 
1766
1819
  .binary-note {
@@ -2030,13 +2083,19 @@ function switchView(viewName) {
2030
2083
  });
2031
2084
  }
2032
2085
 
2033
- // Theme Toggle
2086
+ // Theme Toggle — swap sun/moon SVG
2087
+ const sunPath = 'M8 12a4 4 0 1 1 0-8 4 4 0 0 1 0 8Zm0-1.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5Zm5.657-8.157a.75.75 0 0 1 0 1.061l-1.061 1.06a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734l1.06-1.06a.75.75 0 0 1 1.06 0Zm-9.193 9.193a.75.75 0 0 1 0 1.06l-1.06 1.061a.75.75 0 1 1-1.061-1.06l1.06-1.061a.75.75 0 0 1 1.061 0ZM8 0a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0V.75A.75.75 0 0 1 8 0ZM3 8a.75.75 0 0 1-.75.75H.75a.75.75 0 0 1 0-1.5h1.5A.75.75 0 0 1 3 8Zm13 0a.75.75 0 0 1-.75.75h-1.5a.75.75 0 0 1 0-1.5h1.5A.75.75 0 0 1 16 8Zm-8 5a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 8 13Zm3.536-1.464a.75.75 0 0 1 1.06 0l1.061 1.06a.75.75 0 0 1-1.06 1.061l-1.061-1.06a.75.75 0 0 1 0-1.061ZM2.343 2.343a.75.75 0 0 1 1.061 0l1.06 1.061a.751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018l-1.06-1.06a.75.75 0 0 1 0-1.06Z';
2088
+ const moonPath = 'M9.598 1.591a.749.749 0 0 1 .785-.175 7.001 7.001 0 1 1-8.967 8.967.75.75 0 0 1 .961-.96 5.5 5.5 0 0 0 7.046-7.046.75.75 0 0 1 .175-.786Zm1.616 1.945a7 7 0 0 1-7.678 7.678 5.499 5.499 0 1 0 7.678-7.678Z';
2089
+
2034
2090
  function toggleTheme() {
2035
2091
  var html = document.documentElement;
2036
2092
  var isDark = html.getAttribute('data-theme') === 'dark';
2037
- html.setAttribute('data-theme', isDark ? 'light' : 'dark');
2038
- document.getElementById('theme-btn').textContent = isDark ? '\\u{1F319}' : '\\u{1F31E}';
2039
- localStorage.setItem('diffstory-theme', isDark ? 'light' : 'dark');
2093
+ var newTheme = isDark ? 'light' : 'dark';
2094
+ html.setAttribute('data-theme', newTheme);
2095
+ localStorage.setItem('diffstory-theme', newTheme);
2096
+ // Swap icon: after toggling light→new moon, dark→new sun
2097
+ var icon = document.querySelector('#theme-btn path');
2098
+ if (icon) icon.setAttribute('d', newTheme === 'dark' ? sunPath : moonPath);
2040
2099
  }
2041
2100
 
2042
2101
  // Load saved theme
@@ -2044,7 +2103,9 @@ function toggleTheme() {
2044
2103
  var saved = localStorage.getItem('diffstory-theme');
2045
2104
  if (saved) {
2046
2105
  document.documentElement.setAttribute('data-theme', saved);
2047
- document.getElementById('theme-btn').textContent = saved === 'dark' ? '\\u{1F319}' : '\\u{1F31E}';
2106
+ // Set initial icon
2107
+ var icon = document.querySelector('#theme-btn path');
2108
+ if (icon) icon.setAttribute('d', saved === 'dark' ? sunPath : moonPath);
2048
2109
  }
2049
2110
  })();
2050
2111
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diffstory
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: Transform Git diffs into rich, interactive, self-contained HTML reports
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/lakshayjindal/diffstory
File without changes
File without changes