@young1lin/dsh-ui-gitworkbench 0.1.11 → 0.1.13

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 (36) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +28 -0
  3. package/CHANGELOG_EN.md +28 -0
  4. package/README.md +100 -50
  5. package/README_EN.md +2 -1
  6. package/lib/client.js +6902 -6256
  7. package/package.json +1 -1
  8. package/src/client/ChangesFileTree.tsx +550 -0
  9. package/src/client/ChromeGlyph.tsx +27 -0
  10. package/src/client/CodeEditor.tsx +129 -3
  11. package/src/client/CommitHistory.tsx +1001 -0
  12. package/src/client/DiffViews.tsx +1070 -0
  13. package/src/client/GitWorkbenchPanel.module.css +15 -2513
  14. package/src/client/GitWorkbenchPanel.tsx +37 -3959
  15. package/src/client/PaneDivider.tsx +74 -0
  16. package/src/client/WorkbenchControls.tsx +1047 -0
  17. package/src/client/WorktreeGlyph.tsx +24 -0
  18. package/src/client/cm-search-theme.ts +250 -0
  19. package/src/client/diff-nav.ts +59 -0
  20. package/src/client/git-workbench-types.ts +252 -0
  21. package/src/client/locales.ts +14 -8
  22. package/src/client/row-window.ts +23 -0
  23. package/src/client/search-count.ts +125 -0
  24. package/src/client/side-rows.ts +66 -0
  25. package/src/client/styles/changes.css +505 -0
  26. package/src/client/styles/controls.css +236 -0
  27. package/src/client/styles/environment.css +89 -0
  28. package/src/client/styles/files.css +113 -0
  29. package/src/client/styles/history-filters.css +400 -0
  30. package/src/client/styles/history.css +276 -0
  31. package/src/client/styles/image.css +67 -0
  32. package/src/client/styles/operations.css +179 -0
  33. package/src/client/styles/shell.css +431 -0
  34. package/src/client/styles/themes.css +224 -0
  35. package/src/client/use-change-nav.ts +6 -5
  36. package/src/client/use-row-window.ts +55 -0
@@ -0,0 +1,224 @@
1
+ /* ============================ theme palettes ============================ */
2
+
3
+ /* GitHub — the default, and the reference the other palettes are balanced against. */
4
+ .overlay[data-gs-theme='github-dark'] {
5
+ --gs-bg: #0d1117; --gs-panel: #161b22; --gs-raise: #21262d;
6
+ --gs-border: #30363d; --gs-border-soft: #21262d;
7
+ --gs-fg: #e6edf3; --gs-fg-muted: #c9d1d9; --gs-fg-dim: #8b949e; --gs-fg-faint: #6e7681; --gs-fg-fainter: #484f58;
8
+ --gs-accent: #58a6ff; --gs-accent-bg: rgba(56, 139, 253, 0.14); --gs-accent-border: rgba(56, 139, 253, 0.55);
9
+ --gs-add: #3fb950; --gs-del: #f85149; --gs-warn: #d29922; --gs-info: #79c0ff;
10
+ --gs-add-bg: rgba(46, 160, 67, 0.16); --gs-del-bg: rgba(248, 81, 73, 0.16);
11
+ --gs-warn-bg: rgba(187, 128, 9, 0.16); --gs-info-bg: rgba(56, 139, 253, 0.16);
12
+ /* Primer's own diffBlob tokens, flattened over --gs-bg (#0d1117). Opaque
13
+ rather than the alpha hexes Primer ships, because here the number cells
14
+ paint ON TOP of the row tint — two alphas would composite to roughly twice
15
+ GitHub's strength. Line 15% / num 30% / word 40% of the base hue. */
16
+ --gs-add-line: #12261e; --gs-del-line: #25181c;
17
+ --gs-add-word: #1a4a29; --gs-del-word: #6b2b2b;
18
+ --gs-add-num: #1c4428; --gs-del-num: #542426;
19
+ --gs-hunk: #111d2e; --gs-hunk-num: #0c2d6b;
20
+ --gs-neutral-bg: rgba(110, 118, 129, 0.18);
21
+ --gs-backdrop: rgba(1, 4, 9, 0.6); --gs-shadow: rgba(1, 4, 9, 0.55);
22
+ }
23
+ .overlay[data-gs-theme='github-light'] {
24
+ --gs-bg: #ffffff; --gs-panel: #f6f8fa; --gs-raise: #eaeef2;
25
+ --gs-border: #d0d7de; --gs-border-soft: #eaeef2;
26
+ --gs-fg: #1f2328; --gs-fg-muted: #32383f; --gs-fg-dim: #656d76; --gs-fg-faint: #8c959f; --gs-fg-fainter: #afb8c1;
27
+ --gs-accent: #0969da; --gs-accent-bg: rgba(9, 105, 218, 0.1); --gs-accent-border: rgba(9, 105, 218, 0.45);
28
+ --gs-add: #1a7f37; --gs-del: #cf222e; --gs-warn: #9a6700; --gs-info: #0550ae;
29
+ --gs-add-bg: rgba(26, 127, 55, 0.12); --gs-del-bg: rgba(207, 34, 46, 0.12);
30
+ --gs-warn-bg: rgba(154, 103, 0, 0.12); --gs-info-bg: rgba(9, 105, 218, 0.12);
31
+ /* Primer's light diffBlob tokens, which are already opaque upstream. */
32
+ --gs-add-line: #dafbe1; --gs-del-line: #ffebe9;
33
+ --gs-add-word: #aceebb; --gs-del-word: #ffcecb;
34
+ --gs-add-num: #aceebb; --gs-del-num: #ffcecb;
35
+ --gs-hunk: #ddf4ff; --gs-hunk-num: #b6e3ff;
36
+ --gs-neutral-bg: rgba(110, 118, 129, 0.12);
37
+ --gs-backdrop: rgba(31, 35, 40, 0.28); --gs-shadow: rgba(31, 35, 40, 0.22);
38
+ }
39
+
40
+ /* IntelliJ IDEA — the New UI (2022.3+, the default since 2024.1), not Darcula.
41
+ Its greys are the giveaway: the New UI dropped Darcula's warm #3C3F41 chrome
42
+ for the cooler #1E1F22 editor over a #2B2D30 tool window, and moved the
43
+ accent to #3574F0.
44
+
45
+ The semantic four are the IntelliJ platform's own VCS colours (the keys the
46
+ log and the diff gutter paint with), except that dark's green is the New UI's
47
+ brighter #5FAD65 rather than the platform #499C54 — the latter is a fill
48
+ colour there, and as 12px TEXT on #1E1F22 it lands under 4:1.
49
+
50
+ Unlike the GitHub palettes below, these are not transcribed from a published
51
+ token file, so there is no test pinning them to a source of truth; they are
52
+ read off the New UI's own chrome and diff. */
53
+ .overlay[data-gs-theme='idea-dark'] {
54
+ --gs-bg: #1e1f22; --gs-panel: #2b2d30; --gs-raise: #393b40;
55
+ --gs-border: #393b40; --gs-border-soft: #2b2d30;
56
+ --gs-fg: #dfe1e5; --gs-fg-muted: #ced0d6; --gs-fg-dim: #9da0a8; --gs-fg-faint: #6f737a; --gs-fg-fainter: #4e5157;
57
+ --gs-accent: #548af7; --gs-accent-bg: rgba(84, 138, 247, 0.16); --gs-accent-border: rgba(84, 138, 247, 0.55);
58
+ --gs-add: #5fad65; --gs-del: #e26e6e; --gs-warn: #f0a732; --gs-info: #3592c4;
59
+ --gs-add-bg: rgba(95, 173, 101, 0.16); --gs-del-bg: rgba(226, 110, 110, 0.16);
60
+ --gs-warn-bg: rgba(240, 167, 50, 0.16); --gs-info-bg: rgba(53, 146, 196, 0.16);
61
+ /* IDEA paints diff as solid blocks rather than a wash, and the word-level
62
+ highlight as a distinctly stronger block over it — so these are opaque,
63
+ like GitHub's, instead of the alpha tints the other families use. */
64
+ --gs-add-line: #293c2e; --gs-del-line: #3d2b2b;
65
+ --gs-add-word: #3d6640; --gs-del-word: #6b3838;
66
+ --gs-hunk: rgba(84, 138, 247, 0.10); --gs-neutral-bg: rgba(157, 160, 168, 0.18);
67
+ --gs-backdrop: rgba(16, 17, 19, 0.6); --gs-shadow: rgba(0, 0, 0, 0.5);
68
+ }
69
+ .overlay[data-gs-theme='idea-light'] {
70
+ --gs-bg: #ffffff; --gs-panel: #f7f8fa; --gs-raise: #ebecf0;
71
+ --gs-border: #d3d5db; --gs-border-soft: #ebecf0;
72
+ --gs-fg: #1e1f22; --gs-fg-muted: #3c3f44; --gs-fg-dim: #6c707e; --gs-fg-faint: #818594; --gs-fg-fainter: #a8adbd;
73
+ --gs-accent: #3574f0; --gs-accent-bg: rgba(53, 116, 240, 0.1); --gs-accent-border: rgba(53, 116, 240, 0.45);
74
+ --gs-add: #398a4a; --gs-del: #cc4a4a; --gs-warn: #a8760b; --gs-info: #2b7fb8;
75
+ --gs-add-bg: rgba(57, 138, 74, 0.12); --gs-del-bg: rgba(204, 74, 74, 0.12);
76
+ --gs-warn-bg: rgba(168, 118, 11, 0.12); --gs-info-bg: rgba(43, 127, 184, 0.12);
77
+ --gs-add-line: #e6f5e9; --gs-del-line: #fbe9ea;
78
+ --gs-add-word: #c2e5c9; --gs-del-word: #f5c9cc;
79
+ --gs-hunk: rgba(53, 116, 240, 0.07); --gs-neutral-bg: rgba(108, 112, 126, 0.12);
80
+ --gs-backdrop: rgba(30, 31, 34, 0.26); --gs-shadow: rgba(30, 31, 34, 0.18);
81
+ }
82
+
83
+ /* VS Code — Dark+ / Light+, the default editor themes. */
84
+ .overlay[data-gs-theme='vscode-dark'] {
85
+ --gs-bg: #1e1e1e; --gs-panel: #252526; --gs-raise: #2d2d30;
86
+ --gs-border: #3e3e42; --gs-border-soft: #2d2d30;
87
+ --gs-fg: #d4d4d4; --gs-fg-muted: #cccccc; --gs-fg-dim: #9d9d9d; --gs-fg-faint: #808080; --gs-fg-fainter: #5a5a5a;
88
+ --gs-accent: #0098ff; --gs-accent-bg: rgba(0, 122, 204, 0.24); --gs-accent-border: rgba(0, 152, 255, 0.6);
89
+ --gs-add: #89d185; --gs-del: #f14c4c; --gs-warn: #cca700; --gs-info: #75beff;
90
+ --gs-add-bg: rgba(137, 209, 133, 0.18); --gs-del-bg: rgba(241, 76, 76, 0.18);
91
+ --gs-warn-bg: rgba(204, 167, 0, 0.18); --gs-info-bg: rgba(117, 190, 255, 0.18);
92
+ --gs-add-line: rgba(155, 185, 85, 0.18); --gs-del-line: rgba(255, 0, 0, 0.16);
93
+ --gs-add-word: rgba(155, 185, 85, 0.4); --gs-del-word: rgba(255, 0, 0, 0.36);
94
+ --gs-hunk: rgba(0, 122, 204, 0.14); --gs-neutral-bg: rgba(190, 190, 190, 0.14);
95
+ --gs-backdrop: rgba(0, 0, 0, 0.55); --gs-shadow: rgba(0, 0, 0, 0.6);
96
+ }
97
+ .overlay[data-gs-theme='vscode-light'] {
98
+ --gs-bg: #ffffff; --gs-panel: #f3f3f3; --gs-raise: #e8e8e8;
99
+ --gs-border: #cecece; --gs-border-soft: #e8e8e8;
100
+ --gs-fg: #1e1e1e; --gs-fg-muted: #333333; --gs-fg-dim: #616161; --gs-fg-faint: #767676; --gs-fg-fainter: #a0a0a0;
101
+ --gs-accent: #005fb8; --gs-accent-bg: rgba(0, 95, 184, 0.1); --gs-accent-border: rgba(0, 95, 184, 0.45);
102
+ --gs-add: #10793f; --gs-del: #b5200d; --gs-warn: #855b00; --gs-info: #005fb8;
103
+ --gs-add-bg: rgba(16, 121, 63, 0.12); --gs-del-bg: rgba(181, 32, 13, 0.12);
104
+ --gs-warn-bg: rgba(133, 91, 0, 0.12); --gs-info-bg: rgba(0, 95, 184, 0.12);
105
+ --gs-add-line: rgba(155, 185, 85, 0.22); --gs-del-line: rgba(255, 0, 0, 0.13);
106
+ --gs-add-word: rgba(155, 185, 85, 0.45); --gs-del-word: rgba(255, 0, 0, 0.3);
107
+ --gs-hunk: rgba(0, 95, 184, 0.07); --gs-neutral-bg: rgba(97, 97, 97, 0.12);
108
+ --gs-backdrop: rgba(0, 0, 0, 0.25); --gs-shadow: rgba(0, 0, 0, 0.18);
109
+ }
110
+
111
+ /* Cyberpunk — neon on deep violet; the light variant keeps the magenta/cyan pair
112
+ over a near-white ground so the theme survives a bright room. */
113
+ .overlay[data-gs-theme='cyberpunk-dark'] {
114
+ --gs-bg: #0b0417; --gs-panel: #14082a; --gs-raise: #1f0d3d;
115
+ --gs-border: #3a1f6b; --gs-border-soft: #26124a;
116
+ --gs-fg: #f0e6ff; --gs-fg-muted: #d5c2f5; --gs-fg-dim: #a98fd6; --gs-fg-faint: #7d64a8; --gs-fg-fainter: #56427a;
117
+ --gs-accent: #00f0ff; --gs-on-accent: #0b0417; --gs-accent-bg: rgba(0, 240, 255, 0.14); --gs-accent-border: rgba(0, 240, 255, 0.5);
118
+ --gs-add: #39ff88; --gs-del: #ff2e88; --gs-warn: #ffcc00; --gs-info: #00f0ff;
119
+ --gs-add-bg: rgba(57, 255, 136, 0.14); --gs-del-bg: rgba(255, 46, 136, 0.14);
120
+ --gs-warn-bg: rgba(255, 204, 0, 0.14); --gs-info-bg: rgba(0, 240, 255, 0.14);
121
+ --gs-add-line: rgba(57, 255, 136, 0.12); --gs-del-line: rgba(255, 46, 136, 0.12);
122
+ --gs-add-word: rgba(57, 255, 136, 0.34); --gs-del-word: rgba(255, 46, 136, 0.34);
123
+ --gs-hunk: rgba(0, 240, 255, 0.1); --gs-neutral-bg: rgba(169, 143, 214, 0.16);
124
+ --gs-backdrop: rgba(6, 2, 14, 0.68); --gs-shadow: rgba(0, 240, 255, 0.14);
125
+ }
126
+ .overlay[data-gs-theme='cyberpunk-light'] {
127
+ --gs-bg: #fdfbff; --gs-panel: #f4eeff; --gs-raise: #e9dfff;
128
+ --gs-border: #c9b3f0; --gs-border-soft: #e0d2f7;
129
+ --gs-fg: #1a0b2e; --gs-fg-muted: #33195c; --gs-fg-dim: #6b4ba0; --gs-fg-faint: #8f74bd; --gs-fg-fainter: #b9a5d6;
130
+ --gs-accent: #00a6b8; --gs-accent-bg: rgba(0, 166, 184, 0.12); --gs-accent-border: rgba(0, 166, 184, 0.5);
131
+ --gs-add: #00875a; --gs-del: #d6006e; --gs-warn: #b37a00; --gs-info: #00a6b8;
132
+ --gs-add-bg: rgba(0, 135, 90, 0.12); --gs-del-bg: rgba(214, 0, 110, 0.12);
133
+ --gs-warn-bg: rgba(179, 122, 0, 0.12); --gs-info-bg: rgba(0, 166, 184, 0.12);
134
+ --gs-add-line: rgba(0, 200, 130, 0.14); --gs-del-line: rgba(255, 46, 136, 0.12);
135
+ --gs-add-word: rgba(0, 200, 130, 0.34); --gs-del-word: rgba(255, 46, 136, 0.3);
136
+ --gs-hunk: rgba(0, 166, 184, 0.08); --gs-neutral-bg: rgba(107, 75, 160, 0.12);
137
+ --gs-backdrop: rgba(26, 11, 46, 0.28); --gs-shadow: rgba(26, 11, 46, 0.16);
138
+ }
139
+
140
+ /* One — Atom's One Dark / One Light. */
141
+ .overlay[data-gs-theme='one-dark'] {
142
+ --gs-bg: #282c34; --gs-panel: #21252b; --gs-raise: #2c313a;
143
+ --gs-border: #3e4451; --gs-border-soft: #2c313a;
144
+ --gs-fg: #abb2bf; --gs-fg-muted: #b6bdca; --gs-fg-dim: #7f848e; --gs-fg-faint: #636d83; --gs-fg-fainter: #4b5263;
145
+ --gs-accent: #61afef; --gs-accent-bg: rgba(97, 175, 239, 0.14); --gs-accent-border: rgba(97, 175, 239, 0.55);
146
+ --gs-add: #98c379; --gs-del: #e06c75; --gs-warn: #e5c07b; --gs-info: #56b6c2;
147
+ --gs-add-bg: rgba(152, 195, 121, 0.16); --gs-del-bg: rgba(224, 108, 117, 0.16);
148
+ --gs-warn-bg: rgba(229, 192, 123, 0.16); --gs-info-bg: rgba(86, 182, 194, 0.16);
149
+ --gs-add-line: rgba(152, 195, 121, 0.14); --gs-del-line: rgba(224, 108, 117, 0.14);
150
+ --gs-add-word: rgba(152, 195, 121, 0.38); --gs-del-word: rgba(224, 108, 117, 0.38);
151
+ --gs-hunk: rgba(97, 175, 239, 0.1); --gs-neutral-bg: rgba(127, 132, 142, 0.18);
152
+ --gs-backdrop: rgba(23, 26, 33, 0.6); --gs-shadow: rgba(15, 17, 21, 0.55);
153
+ }
154
+ .overlay[data-gs-theme='one-light'] {
155
+ --gs-bg: #fafafa; --gs-panel: #f0f0f0; --gs-raise: #e5e5e6;
156
+ --gs-border: #d4d4d4; --gs-border-soft: #e5e5e6;
157
+ --gs-fg: #383a42; --gs-fg-muted: #4a4c53; --gs-fg-dim: #696c77; --gs-fg-faint: #909196; --gs-fg-fainter: #b8b9bd;
158
+ --gs-accent: #4078f2; --gs-accent-bg: rgba(64, 120, 242, 0.1); --gs-accent-border: rgba(64, 120, 242, 0.45);
159
+ --gs-add: #50a14f; --gs-del: #e45649; --gs-warn: #c18401; --gs-info: #0184bc;
160
+ --gs-add-bg: rgba(80, 161, 79, 0.12); --gs-del-bg: rgba(228, 86, 73, 0.12);
161
+ --gs-warn-bg: rgba(193, 132, 1, 0.12); --gs-info-bg: rgba(1, 132, 188, 0.12);
162
+ --gs-add-line: rgba(80, 161, 79, 0.14); --gs-del-line: rgba(228, 86, 73, 0.13);
163
+ --gs-add-word: rgba(80, 161, 79, 0.34); --gs-del-word: rgba(228, 86, 73, 0.32);
164
+ --gs-hunk: rgba(64, 120, 242, 0.07); --gs-neutral-bg: rgba(105, 108, 119, 0.12);
165
+ --gs-backdrop: rgba(56, 58, 66, 0.26); --gs-shadow: rgba(56, 58, 66, 0.18);
166
+ }
167
+
168
+ /* Solarized — Ethan Schoonover's fixed sixteen, both official grounds. */
169
+ .overlay[data-gs-theme='solarized-dark'] {
170
+ --gs-bg: #002b36; --gs-panel: #073642; --gs-raise: #0a4553;
171
+ --gs-border: #0f5666; --gs-border-soft: #073642;
172
+ --gs-fg: #93a1a1; --gs-fg-muted: #839496; --gs-fg-dim: #657b83; --gs-fg-faint: #586e75; --gs-fg-fainter: #45636b;
173
+ --gs-accent: #268bd2; --gs-accent-bg: rgba(38, 139, 210, 0.16); --gs-accent-border: rgba(38, 139, 210, 0.55);
174
+ --gs-add: #859900; --gs-del: #dc322f; --gs-warn: #b58900; --gs-info: #2aa198;
175
+ --gs-add-bg: rgba(133, 153, 0, 0.2); --gs-del-bg: rgba(220, 50, 47, 0.18);
176
+ --gs-warn-bg: rgba(181, 137, 0, 0.18); --gs-info-bg: rgba(42, 161, 152, 0.18);
177
+ --gs-add-line: rgba(133, 153, 0, 0.16); --gs-del-line: rgba(220, 50, 47, 0.15);
178
+ --gs-add-word: rgba(133, 153, 0, 0.4); --gs-del-word: rgba(220, 50, 47, 0.38);
179
+ --gs-hunk: rgba(38, 139, 210, 0.12); --gs-neutral-bg: rgba(101, 123, 131, 0.2);
180
+ --gs-backdrop: rgba(0, 20, 26, 0.62); --gs-shadow: rgba(0, 20, 26, 0.55);
181
+ }
182
+ .overlay[data-gs-theme='solarized-light'] {
183
+ --gs-bg: #fdf6e3; --gs-panel: #eee8d5; --gs-raise: #e3ddca;
184
+ --gs-border: #d5cfbb; --gs-border-soft: #eee8d5;
185
+ --gs-fg: #586e75; --gs-fg-muted: #657b83; --gs-fg-dim: #839496; --gs-fg-faint: #93a1a1; --gs-fg-fainter: #b5b0a0;
186
+ --gs-accent: #268bd2; --gs-accent-bg: rgba(38, 139, 210, 0.12); --gs-accent-border: rgba(38, 139, 210, 0.45);
187
+ --gs-add: #6c7c00; --gs-del: #cb2825; --gs-warn: #9a7400; --gs-info: #21867f;
188
+ --gs-add-bg: rgba(133, 153, 0, 0.18); --gs-del-bg: rgba(220, 50, 47, 0.14);
189
+ --gs-warn-bg: rgba(181, 137, 0, 0.16); --gs-info-bg: rgba(42, 161, 152, 0.14);
190
+ --gs-add-line: rgba(133, 153, 0, 0.16); --gs-del-line: rgba(220, 50, 47, 0.12);
191
+ --gs-add-word: rgba(133, 153, 0, 0.38); --gs-del-word: rgba(220, 50, 47, 0.3);
192
+ --gs-hunk: rgba(38, 139, 210, 0.08); --gs-neutral-bg: rgba(88, 110, 117, 0.14);
193
+ --gs-backdrop: rgba(88, 110, 117, 0.26); --gs-shadow: rgba(88, 110, 117, 0.2);
194
+ }
195
+
196
+ /* Nord — Polar Night for dark, Snow Storm for light. Nord's aurora greens and
197
+ reds are tuned for a dark ground, so the light variant darkens the diff pair
198
+ rather than washing it out. */
199
+ .overlay[data-gs-theme='nord-dark'] {
200
+ --gs-bg: #2e3440; --gs-panel: #3b4252; --gs-raise: #434c5e;
201
+ --gs-border: #4c566a; --gs-border-soft: #3b4252;
202
+ --gs-fg: #eceff4; --gs-fg-muted: #d8dee9; --gs-fg-dim: #9aa5b8; --gs-fg-faint: #7b88a1; --gs-fg-fainter: #5d6a82;
203
+ --gs-accent: #88c0d0; --gs-on-accent: #2e3440; --gs-accent-bg: rgba(136, 192, 208, 0.14); --gs-accent-border: rgba(136, 192, 208, 0.55);
204
+ --gs-add: #a3be8c; --gs-del: #bf616a; --gs-warn: #ebcb8b; --gs-info: #81a1c1;
205
+ --gs-add-bg: rgba(163, 190, 140, 0.16); --gs-del-bg: rgba(191, 97, 106, 0.18);
206
+ --gs-warn-bg: rgba(235, 203, 139, 0.16); --gs-info-bg: rgba(129, 161, 193, 0.16);
207
+ --gs-add-line: rgba(163, 190, 140, 0.14); --gs-del-line: rgba(191, 97, 106, 0.15);
208
+ --gs-add-word: rgba(163, 190, 140, 0.38); --gs-del-word: rgba(191, 97, 106, 0.4);
209
+ --gs-hunk: rgba(136, 192, 208, 0.1); --gs-neutral-bg: rgba(154, 165, 184, 0.16);
210
+ --gs-backdrop: rgba(29, 35, 45, 0.62); --gs-shadow: rgba(20, 25, 33, 0.55);
211
+ }
212
+ .overlay[data-gs-theme='nord-light'] {
213
+ --gs-bg: #eceff4; --gs-panel: #e5e9f0; --gs-raise: #d8dee9;
214
+ --gs-border: #c3cbd8; --gs-border-soft: #dfe4ec;
215
+ --gs-fg: #2e3440; --gs-fg-muted: #3b4252; --gs-fg-dim: #4c566a; --gs-fg-faint: #6b7689; --gs-fg-fainter: #9aa4b5;
216
+ --gs-accent: #5e81ac; --gs-accent-bg: rgba(94, 129, 172, 0.12); --gs-accent-border: rgba(94, 129, 172, 0.5);
217
+ --gs-add: #5e7d47; --gs-del: #a5424c; --gs-warn: #97701f; --gs-info: #4c7a8c;
218
+ --gs-add-bg: rgba(94, 125, 71, 0.14); --gs-del-bg: rgba(165, 66, 76, 0.13);
219
+ --gs-warn-bg: rgba(151, 112, 31, 0.14); --gs-info-bg: rgba(76, 122, 140, 0.13);
220
+ --gs-add-line: rgba(94, 125, 71, 0.14); --gs-del-line: rgba(165, 66, 76, 0.12);
221
+ --gs-add-word: rgba(94, 125, 71, 0.34); --gs-del-word: rgba(165, 66, 76, 0.32);
222
+ --gs-hunk: rgba(94, 129, 172, 0.09); --gs-neutral-bg: rgba(76, 86, 106, 0.13);
223
+ --gs-backdrop: rgba(46, 52, 64, 0.26); --gs-shadow: rgba(46, 52, 64, 0.18);
224
+ }
@@ -22,8 +22,8 @@ import { anchorFrom, scrollTopFor, stepToBlock, type BlockTop, type NavMemory }
22
22
 
23
23
  /** What a pane needs to offer the walk. */
24
24
  export interface ChangeNav {
25
- /** Move to the next (1) or previous (-1) change, wrapping at both ends. */
26
- readonly goToChange: (direction: 1 | -1) => void
25
+ /** Move to the next (1) or previous (-1) change and report its block id. */
26
+ readonly goToChange: (direction: 1 | -1) => number | null
27
27
  }
28
28
 
29
29
  /**
@@ -76,17 +76,18 @@ export function useChangeNav(
76
76
  return tops
77
77
  }
78
78
 
79
- const goToChange = (direction: 1 | -1): void => {
79
+ const goToChange = (direction: 1 | -1): number | null => {
80
80
  const scroller = scrollRef.current
81
- if (scroller === null) return
81
+ if (scroller === null) return null
82
82
  const measured = tops === undefined ? blockTops() : tops()
83
83
  const target = stepToBlock(measured, anchorFrom(measured, scroller.scrollTop, navMemory.current), direction)
84
- if (target === null) return
84
+ if (target === null) return null
85
85
  scroller.scrollTop = scrollTopFor(target.top)
86
86
  // Read back rather than storing what was asked for: the browser clamps at
87
87
  // the end of the content, and the clamped value is what the next press
88
88
  // compares against to tell "still here" from "the reader scrolled".
89
89
  navMemory.current = { block: target.block, scrollTop: scroller.scrollTop }
90
+ return target.block
90
91
  }
91
92
 
92
93
  return { goToChange }
@@ -0,0 +1,55 @@
1
+ import { useEffect, useState } from 'react'
2
+
3
+ import { rowWindow, rowWindowForMount, sameRowWindow, type HeldRowWindow, type RowWindow } from './row-window.ts'
4
+
5
+ /* ---------- shared helpers ---------- */
6
+
7
+ /**
8
+ * The rows a diff actually has to put in the DOM, tracked against its scroller.
9
+ *
10
+ * Rendering a whole file cost the pane 3.9 seconds of main thread and a
11
+ * 3.6-second frozen frame at 4,000 lines — for a one-line change, because the
12
+ * side-by-side view draws every line of the file whether it changed or not.
13
+ * This is the fix that removes the length from the cost rather than capping it.
14
+ *
15
+ * The window is held in state rather than the raw scroll offset so a scroll
16
+ * that does not move it renders nothing: `start` only changes once a whole row
17
+ * has passed under the viewport's edge.
18
+ *
19
+ * @param scrollRef - the element that scrolls the rows.
20
+ * @param rowCount - how many rows the diff has.
21
+ * @returns the rows to render and the spacer heights standing in for the rest.
22
+ */
23
+ export function useRowWindow(scrollRef: { current: HTMLElement | null }, rowCount: number, mountKey: string): RowWindow {
24
+ const count = Number.isFinite(rowCount) ? Math.max(0, Math.trunc(rowCount)) : 0
25
+ const [held, setHeld] = useState<HeldRowWindow>(() => ({
26
+ mountKey, rowCount: count, win: rowWindow(0, 0, count),
27
+ }))
28
+ // A file can change while the scroller is temporarily unmounted by loading.
29
+ // Do not paint the previous file's spacer for one frame — derive a fresh top
30
+ // window immediately; the effect below replaces it with the measured one.
31
+ const visible = rowWindowForMount(held, count, mountKey)
32
+ useEffect(() => {
33
+ const el = scrollRef.current
34
+ if (el === null) return
35
+ const read = (): void => {
36
+ const next = rowWindow(el.scrollTop, el.clientHeight, count)
37
+ setHeld(prev => prev.mountKey === mountKey && prev.rowCount === count && sameRowWindow(prev.win, next)
38
+ ? prev
39
+ : { mountKey, rowCount: count, win: next })
40
+ }
41
+ read()
42
+ // Passive: this listener never calls preventDefault, and saying so keeps
43
+ // it off the scroll's critical path.
44
+ el.addEventListener('scroll', read, { passive: true })
45
+ // The drawer resizes without the page doing so — a dragged edge, the
46
+ // maximize button — and a taller pane needs more rows.
47
+ const observer = new ResizeObserver(read)
48
+ observer.observe(el)
49
+ return () => {
50
+ el.removeEventListener('scroll', read)
51
+ observer.disconnect()
52
+ }
53
+ }, [scrollRef, count, mountKey])
54
+ return visible
55
+ }