@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
@@ -1,2515 +1,17 @@
1
- /* Change inspector.
1
+ /**
2
+ * Git workbench CSS Module manifest.
2
3
  *
3
- * The session-header card lives in dsh chrome and keeps dsh's own --dsw-* tokens,
4
- * so it is themed by the app and needs nothing here. The DRAWER paints its own
5
- * surface a diff needs an add/delete/word-level/syntax palette dsh does not
6
- * define — so it carries a self-contained palette instead.
7
- *
8
- * Every drawer colour resolves through a --gs-* token. The palettes below are the
9
- * only place a literal colour appears; each theme sets the same token names, so a
10
- * theme is a palette swap and nothing else. `data-gs-theme` on the overlay picks
11
- * one, and the panel computes it from the theme setting (`system` follows dsh's
12
- * `body[data-ds-dark-theme]`; light/dark pin the drawer).
13
- *
14
- * One exception is deliberate: the mode picker's own light/dark chips are fixed
15
- * white and near-black, because those buttons name colours rather than wear them.
16
- *
17
- * `data-gs-part` attributes on the overlay, card, header, tabs and three panes are
18
- * the stable hooks user CSS targets — class names are hashed by CSS Modules and
19
- * change on every build, so they are not addressable from outside.
20
- */
21
-
22
- /* ---- environment card (lives in dsh chrome — keep dsw tokens) ---- */
23
- .card {
24
- display: inline-flex;
25
- align-items: center;
26
- gap: 7px;
27
- min-height: 28px;
28
- padding: 3px 10px;
29
- border: 1px solid var(--dsw-alias-border-l2);
30
- border-radius: 8px;
31
- background: transparent;
32
- color: var(--dsw-alias-label-secondary);
33
- font-size: 12px;
34
- line-height: 18px;
35
- font-variant-numeric: tabular-nums;
36
- cursor: pointer;
37
- transition: background 120ms ease, border-color 120ms ease;
38
- }
39
- .card:hover, .card:focus-visible {
40
- background: var(--dsw-alias-interactive-bg-hover);
41
- border-color: var(--dsw-alias-border-l3);
42
- }
43
- .cardBranch {
44
- display: inline-flex;
45
- align-items: center;
46
- gap: 4px;
47
- max-width: 220px;
48
- overflow: hidden;
49
- color: var(--dsw-alias-label-primary);
50
- font-weight: 550;
51
- }
52
- .cardBranchName { min-width: 0; overflow: hidden; }
53
- .cardGlyph { flex: none; opacity: 0.7; }
54
- .cardDetached {
55
- padding: 0 6px;
56
- border-radius: 999px;
57
- background: var(--dsw-alias-state-warning-bg, rgba(187, 128, 9, 0.15));
58
- color: var(--dsw-alias-state-warning-primary, #d29922);
59
- font-size: 10px;
60
- line-height: 16px;
61
- letter-spacing: 0.02em;
62
- }
63
- /* Bound-worktree badge: the session is attached to an agent worktree. */
64
- .cardWt {
65
- display: inline-flex;
66
- align-items: center;
67
- gap: 3px;
68
- max-width: 150px;
69
- overflow: hidden;
70
- padding: 0 6px;
71
- border-radius: 999px;
72
- background: var(--dsw-alias-state-business-bg, rgba(56, 139, 253, 0.16));
73
- color: var(--dsw-alias-state-business-primary, #58a6ff);
74
- font-size: 10px;
75
- line-height: 16px;
76
- letter-spacing: 0.02em;
77
- white-space: nowrap;
78
- text-overflow: ellipsis;
79
- }
80
- .cardAhead, .cardBehind {
81
- color: var(--dsw-alias-label-tertiary);
82
- font-size: 11px;
83
- }
84
- .cardAhead { color: var(--dsw-alias-state-success-primary); }
85
- .cardBehind { color: var(--dsw-alias-state-attention-primary, #d29922); }
86
- .cardSep { width: 1px; height: 14px; background: var(--dsw-alias-border-l2); }
87
- .cardAdded { color: var(--dsw-alias-state-success-primary); }
88
- .cardDeleted { color: var(--dsw-alias-state-error-primary); }
89
- .cardFiles { color: var(--dsw-alias-label-tertiary); }
90
-
91
- /* ============================ theme palettes ============================ */
92
-
93
- /* GitHub — the default, and the reference the other palettes are balanced against. */
94
- .overlay[data-gs-theme='github-dark'] {
95
- --gs-bg: #0d1117; --gs-panel: #161b22; --gs-raise: #21262d;
96
- --gs-border: #30363d; --gs-border-soft: #21262d;
97
- --gs-fg: #e6edf3; --gs-fg-muted: #c9d1d9; --gs-fg-dim: #8b949e; --gs-fg-faint: #6e7681; --gs-fg-fainter: #484f58;
98
- --gs-accent: #58a6ff; --gs-accent-bg: rgba(56, 139, 253, 0.14); --gs-accent-border: rgba(56, 139, 253, 0.55);
99
- --gs-add: #3fb950; --gs-del: #f85149; --gs-warn: #d29922; --gs-info: #79c0ff;
100
- --gs-add-bg: rgba(46, 160, 67, 0.16); --gs-del-bg: rgba(248, 81, 73, 0.16);
101
- --gs-warn-bg: rgba(187, 128, 9, 0.16); --gs-info-bg: rgba(56, 139, 253, 0.16);
102
- /* Primer's own diffBlob tokens, flattened over --gs-bg (#0d1117). Opaque
103
- rather than the alpha hexes Primer ships, because here the number cells
104
- paint ON TOP of the row tint — two alphas would composite to roughly twice
105
- GitHub's strength. Line 15% / num 30% / word 40% of the base hue. */
106
- --gs-add-line: #12261e; --gs-del-line: #25181c;
107
- --gs-add-word: #1a4a29; --gs-del-word: #6b2b2b;
108
- --gs-add-num: #1c4428; --gs-del-num: #542426;
109
- --gs-hunk: #111d2e; --gs-hunk-num: #0c2d6b;
110
- --gs-neutral-bg: rgba(110, 118, 129, 0.18);
111
- --gs-backdrop: rgba(1, 4, 9, 0.6); --gs-shadow: rgba(1, 4, 9, 0.55);
112
- }
113
- .overlay[data-gs-theme='github-light'] {
114
- --gs-bg: #ffffff; --gs-panel: #f6f8fa; --gs-raise: #eaeef2;
115
- --gs-border: #d0d7de; --gs-border-soft: #eaeef2;
116
- --gs-fg: #1f2328; --gs-fg-muted: #32383f; --gs-fg-dim: #656d76; --gs-fg-faint: #8c959f; --gs-fg-fainter: #afb8c1;
117
- --gs-accent: #0969da; --gs-accent-bg: rgba(9, 105, 218, 0.1); --gs-accent-border: rgba(9, 105, 218, 0.45);
118
- --gs-add: #1a7f37; --gs-del: #cf222e; --gs-warn: #9a6700; --gs-info: #0550ae;
119
- --gs-add-bg: rgba(26, 127, 55, 0.12); --gs-del-bg: rgba(207, 34, 46, 0.12);
120
- --gs-warn-bg: rgba(154, 103, 0, 0.12); --gs-info-bg: rgba(9, 105, 218, 0.12);
121
- /* Primer's light diffBlob tokens, which are already opaque upstream. */
122
- --gs-add-line: #dafbe1; --gs-del-line: #ffebe9;
123
- --gs-add-word: #aceebb; --gs-del-word: #ffcecb;
124
- --gs-add-num: #aceebb; --gs-del-num: #ffcecb;
125
- --gs-hunk: #ddf4ff; --gs-hunk-num: #b6e3ff;
126
- --gs-neutral-bg: rgba(110, 118, 129, 0.12);
127
- --gs-backdrop: rgba(31, 35, 40, 0.28); --gs-shadow: rgba(31, 35, 40, 0.22);
128
- }
129
-
130
- /* IntelliJ IDEA — the New UI (2022.3+, the default since 2024.1), not Darcula.
131
- Its greys are the giveaway: the New UI dropped Darcula's warm #3C3F41 chrome
132
- for the cooler #1E1F22 editor over a #2B2D30 tool window, and moved the
133
- accent to #3574F0.
134
-
135
- The semantic four are the IntelliJ platform's own VCS colours (the keys the
136
- log and the diff gutter paint with), except that dark's green is the New UI's
137
- brighter #5FAD65 rather than the platform #499C54 — the latter is a fill
138
- colour there, and as 12px TEXT on #1E1F22 it lands under 4:1.
139
-
140
- Unlike the GitHub palettes below, these are not transcribed from a published
141
- token file, so there is no test pinning them to a source of truth; they are
142
- read off the New UI's own chrome and diff. */
143
- .overlay[data-gs-theme='idea-dark'] {
144
- --gs-bg: #1e1f22; --gs-panel: #2b2d30; --gs-raise: #393b40;
145
- --gs-border: #393b40; --gs-border-soft: #2b2d30;
146
- --gs-fg: #dfe1e5; --gs-fg-muted: #ced0d6; --gs-fg-dim: #9da0a8; --gs-fg-faint: #6f737a; --gs-fg-fainter: #4e5157;
147
- --gs-accent: #548af7; --gs-accent-bg: rgba(84, 138, 247, 0.16); --gs-accent-border: rgba(84, 138, 247, 0.55);
148
- --gs-add: #5fad65; --gs-del: #e26e6e; --gs-warn: #f0a732; --gs-info: #3592c4;
149
- --gs-add-bg: rgba(95, 173, 101, 0.16); --gs-del-bg: rgba(226, 110, 110, 0.16);
150
- --gs-warn-bg: rgba(240, 167, 50, 0.16); --gs-info-bg: rgba(53, 146, 196, 0.16);
151
- /* IDEA paints diff as solid blocks rather than a wash, and the word-level
152
- highlight as a distinctly stronger block over it — so these are opaque,
153
- like GitHub's, instead of the alpha tints the other families use. */
154
- --gs-add-line: #293c2e; --gs-del-line: #3d2b2b;
155
- --gs-add-word: #3d6640; --gs-del-word: #6b3838;
156
- --gs-hunk: rgba(84, 138, 247, 0.10); --gs-neutral-bg: rgba(157, 160, 168, 0.18);
157
- --gs-backdrop: rgba(16, 17, 19, 0.6); --gs-shadow: rgba(0, 0, 0, 0.5);
158
- }
159
- .overlay[data-gs-theme='idea-light'] {
160
- --gs-bg: #ffffff; --gs-panel: #f7f8fa; --gs-raise: #ebecf0;
161
- --gs-border: #d3d5db; --gs-border-soft: #ebecf0;
162
- --gs-fg: #1e1f22; --gs-fg-muted: #3c3f44; --gs-fg-dim: #6c707e; --gs-fg-faint: #818594; --gs-fg-fainter: #a8adbd;
163
- --gs-accent: #3574f0; --gs-accent-bg: rgba(53, 116, 240, 0.1); --gs-accent-border: rgba(53, 116, 240, 0.45);
164
- --gs-add: #398a4a; --gs-del: #cc4a4a; --gs-warn: #a8760b; --gs-info: #2b7fb8;
165
- --gs-add-bg: rgba(57, 138, 74, 0.12); --gs-del-bg: rgba(204, 74, 74, 0.12);
166
- --gs-warn-bg: rgba(168, 118, 11, 0.12); --gs-info-bg: rgba(43, 127, 184, 0.12);
167
- --gs-add-line: #e6f5e9; --gs-del-line: #fbe9ea;
168
- --gs-add-word: #c2e5c9; --gs-del-word: #f5c9cc;
169
- --gs-hunk: rgba(53, 116, 240, 0.07); --gs-neutral-bg: rgba(108, 112, 126, 0.12);
170
- --gs-backdrop: rgba(30, 31, 34, 0.26); --gs-shadow: rgba(30, 31, 34, 0.18);
171
- }
172
-
173
- /* VS Code — Dark+ / Light+, the default editor themes. */
174
- .overlay[data-gs-theme='vscode-dark'] {
175
- --gs-bg: #1e1e1e; --gs-panel: #252526; --gs-raise: #2d2d30;
176
- --gs-border: #3e3e42; --gs-border-soft: #2d2d30;
177
- --gs-fg: #d4d4d4; --gs-fg-muted: #cccccc; --gs-fg-dim: #9d9d9d; --gs-fg-faint: #808080; --gs-fg-fainter: #5a5a5a;
178
- --gs-accent: #0098ff; --gs-accent-bg: rgba(0, 122, 204, 0.24); --gs-accent-border: rgba(0, 152, 255, 0.6);
179
- --gs-add: #89d185; --gs-del: #f14c4c; --gs-warn: #cca700; --gs-info: #75beff;
180
- --gs-add-bg: rgba(137, 209, 133, 0.18); --gs-del-bg: rgba(241, 76, 76, 0.18);
181
- --gs-warn-bg: rgba(204, 167, 0, 0.18); --gs-info-bg: rgba(117, 190, 255, 0.18);
182
- --gs-add-line: rgba(155, 185, 85, 0.18); --gs-del-line: rgba(255, 0, 0, 0.16);
183
- --gs-add-word: rgba(155, 185, 85, 0.4); --gs-del-word: rgba(255, 0, 0, 0.36);
184
- --gs-hunk: rgba(0, 122, 204, 0.14); --gs-neutral-bg: rgba(190, 190, 190, 0.14);
185
- --gs-backdrop: rgba(0, 0, 0, 0.55); --gs-shadow: rgba(0, 0, 0, 0.6);
186
- }
187
- .overlay[data-gs-theme='vscode-light'] {
188
- --gs-bg: #ffffff; --gs-panel: #f3f3f3; --gs-raise: #e8e8e8;
189
- --gs-border: #cecece; --gs-border-soft: #e8e8e8;
190
- --gs-fg: #1e1e1e; --gs-fg-muted: #333333; --gs-fg-dim: #616161; --gs-fg-faint: #767676; --gs-fg-fainter: #a0a0a0;
191
- --gs-accent: #005fb8; --gs-accent-bg: rgba(0, 95, 184, 0.1); --gs-accent-border: rgba(0, 95, 184, 0.45);
192
- --gs-add: #10793f; --gs-del: #b5200d; --gs-warn: #855b00; --gs-info: #005fb8;
193
- --gs-add-bg: rgba(16, 121, 63, 0.12); --gs-del-bg: rgba(181, 32, 13, 0.12);
194
- --gs-warn-bg: rgba(133, 91, 0, 0.12); --gs-info-bg: rgba(0, 95, 184, 0.12);
195
- --gs-add-line: rgba(155, 185, 85, 0.22); --gs-del-line: rgba(255, 0, 0, 0.13);
196
- --gs-add-word: rgba(155, 185, 85, 0.45); --gs-del-word: rgba(255, 0, 0, 0.3);
197
- --gs-hunk: rgba(0, 95, 184, 0.07); --gs-neutral-bg: rgba(97, 97, 97, 0.12);
198
- --gs-backdrop: rgba(0, 0, 0, 0.25); --gs-shadow: rgba(0, 0, 0, 0.18);
199
- }
200
-
201
- /* Cyberpunk — neon on deep violet; the light variant keeps the magenta/cyan pair
202
- over a near-white ground so the theme survives a bright room. */
203
- .overlay[data-gs-theme='cyberpunk-dark'] {
204
- --gs-bg: #0b0417; --gs-panel: #14082a; --gs-raise: #1f0d3d;
205
- --gs-border: #3a1f6b; --gs-border-soft: #26124a;
206
- --gs-fg: #f0e6ff; --gs-fg-muted: #d5c2f5; --gs-fg-dim: #a98fd6; --gs-fg-faint: #7d64a8; --gs-fg-fainter: #56427a;
207
- --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);
208
- --gs-add: #39ff88; --gs-del: #ff2e88; --gs-warn: #ffcc00; --gs-info: #00f0ff;
209
- --gs-add-bg: rgba(57, 255, 136, 0.14); --gs-del-bg: rgba(255, 46, 136, 0.14);
210
- --gs-warn-bg: rgba(255, 204, 0, 0.14); --gs-info-bg: rgba(0, 240, 255, 0.14);
211
- --gs-add-line: rgba(57, 255, 136, 0.12); --gs-del-line: rgba(255, 46, 136, 0.12);
212
- --gs-add-word: rgba(57, 255, 136, 0.34); --gs-del-word: rgba(255, 46, 136, 0.34);
213
- --gs-hunk: rgba(0, 240, 255, 0.1); --gs-neutral-bg: rgba(169, 143, 214, 0.16);
214
- --gs-backdrop: rgba(6, 2, 14, 0.68); --gs-shadow: rgba(0, 240, 255, 0.14);
215
- }
216
- .overlay[data-gs-theme='cyberpunk-light'] {
217
- --gs-bg: #fdfbff; --gs-panel: #f4eeff; --gs-raise: #e9dfff;
218
- --gs-border: #c9b3f0; --gs-border-soft: #e0d2f7;
219
- --gs-fg: #1a0b2e; --gs-fg-muted: #33195c; --gs-fg-dim: #6b4ba0; --gs-fg-faint: #8f74bd; --gs-fg-fainter: #b9a5d6;
220
- --gs-accent: #00a6b8; --gs-accent-bg: rgba(0, 166, 184, 0.12); --gs-accent-border: rgba(0, 166, 184, 0.5);
221
- --gs-add: #00875a; --gs-del: #d6006e; --gs-warn: #b37a00; --gs-info: #00a6b8;
222
- --gs-add-bg: rgba(0, 135, 90, 0.12); --gs-del-bg: rgba(214, 0, 110, 0.12);
223
- --gs-warn-bg: rgba(179, 122, 0, 0.12); --gs-info-bg: rgba(0, 166, 184, 0.12);
224
- --gs-add-line: rgba(0, 200, 130, 0.14); --gs-del-line: rgba(255, 46, 136, 0.12);
225
- --gs-add-word: rgba(0, 200, 130, 0.34); --gs-del-word: rgba(255, 46, 136, 0.3);
226
- --gs-hunk: rgba(0, 166, 184, 0.08); --gs-neutral-bg: rgba(107, 75, 160, 0.12);
227
- --gs-backdrop: rgba(26, 11, 46, 0.28); --gs-shadow: rgba(26, 11, 46, 0.16);
228
- }
229
-
230
- /* One — Atom's One Dark / One Light. */
231
- .overlay[data-gs-theme='one-dark'] {
232
- --gs-bg: #282c34; --gs-panel: #21252b; --gs-raise: #2c313a;
233
- --gs-border: #3e4451; --gs-border-soft: #2c313a;
234
- --gs-fg: #abb2bf; --gs-fg-muted: #b6bdca; --gs-fg-dim: #7f848e; --gs-fg-faint: #636d83; --gs-fg-fainter: #4b5263;
235
- --gs-accent: #61afef; --gs-accent-bg: rgba(97, 175, 239, 0.14); --gs-accent-border: rgba(97, 175, 239, 0.55);
236
- --gs-add: #98c379; --gs-del: #e06c75; --gs-warn: #e5c07b; --gs-info: #56b6c2;
237
- --gs-add-bg: rgba(152, 195, 121, 0.16); --gs-del-bg: rgba(224, 108, 117, 0.16);
238
- --gs-warn-bg: rgba(229, 192, 123, 0.16); --gs-info-bg: rgba(86, 182, 194, 0.16);
239
- --gs-add-line: rgba(152, 195, 121, 0.14); --gs-del-line: rgba(224, 108, 117, 0.14);
240
- --gs-add-word: rgba(152, 195, 121, 0.38); --gs-del-word: rgba(224, 108, 117, 0.38);
241
- --gs-hunk: rgba(97, 175, 239, 0.1); --gs-neutral-bg: rgba(127, 132, 142, 0.18);
242
- --gs-backdrop: rgba(23, 26, 33, 0.6); --gs-shadow: rgba(15, 17, 21, 0.55);
243
- }
244
- .overlay[data-gs-theme='one-light'] {
245
- --gs-bg: #fafafa; --gs-panel: #f0f0f0; --gs-raise: #e5e5e6;
246
- --gs-border: #d4d4d4; --gs-border-soft: #e5e5e6;
247
- --gs-fg: #383a42; --gs-fg-muted: #4a4c53; --gs-fg-dim: #696c77; --gs-fg-faint: #909196; --gs-fg-fainter: #b8b9bd;
248
- --gs-accent: #4078f2; --gs-accent-bg: rgba(64, 120, 242, 0.1); --gs-accent-border: rgba(64, 120, 242, 0.45);
249
- --gs-add: #50a14f; --gs-del: #e45649; --gs-warn: #c18401; --gs-info: #0184bc;
250
- --gs-add-bg: rgba(80, 161, 79, 0.12); --gs-del-bg: rgba(228, 86, 73, 0.12);
251
- --gs-warn-bg: rgba(193, 132, 1, 0.12); --gs-info-bg: rgba(1, 132, 188, 0.12);
252
- --gs-add-line: rgba(80, 161, 79, 0.14); --gs-del-line: rgba(228, 86, 73, 0.13);
253
- --gs-add-word: rgba(80, 161, 79, 0.34); --gs-del-word: rgba(228, 86, 73, 0.32);
254
- --gs-hunk: rgba(64, 120, 242, 0.07); --gs-neutral-bg: rgba(105, 108, 119, 0.12);
255
- --gs-backdrop: rgba(56, 58, 66, 0.26); --gs-shadow: rgba(56, 58, 66, 0.18);
256
- }
257
-
258
- /* Solarized — Ethan Schoonover's fixed sixteen, both official grounds. */
259
- .overlay[data-gs-theme='solarized-dark'] {
260
- --gs-bg: #002b36; --gs-panel: #073642; --gs-raise: #0a4553;
261
- --gs-border: #0f5666; --gs-border-soft: #073642;
262
- --gs-fg: #93a1a1; --gs-fg-muted: #839496; --gs-fg-dim: #657b83; --gs-fg-faint: #586e75; --gs-fg-fainter: #45636b;
263
- --gs-accent: #268bd2; --gs-accent-bg: rgba(38, 139, 210, 0.16); --gs-accent-border: rgba(38, 139, 210, 0.55);
264
- --gs-add: #859900; --gs-del: #dc322f; --gs-warn: #b58900; --gs-info: #2aa198;
265
- --gs-add-bg: rgba(133, 153, 0, 0.2); --gs-del-bg: rgba(220, 50, 47, 0.18);
266
- --gs-warn-bg: rgba(181, 137, 0, 0.18); --gs-info-bg: rgba(42, 161, 152, 0.18);
267
- --gs-add-line: rgba(133, 153, 0, 0.16); --gs-del-line: rgba(220, 50, 47, 0.15);
268
- --gs-add-word: rgba(133, 153, 0, 0.4); --gs-del-word: rgba(220, 50, 47, 0.38);
269
- --gs-hunk: rgba(38, 139, 210, 0.12); --gs-neutral-bg: rgba(101, 123, 131, 0.2);
270
- --gs-backdrop: rgba(0, 20, 26, 0.62); --gs-shadow: rgba(0, 20, 26, 0.55);
271
- }
272
- .overlay[data-gs-theme='solarized-light'] {
273
- --gs-bg: #fdf6e3; --gs-panel: #eee8d5; --gs-raise: #e3ddca;
274
- --gs-border: #d5cfbb; --gs-border-soft: #eee8d5;
275
- --gs-fg: #586e75; --gs-fg-muted: #657b83; --gs-fg-dim: #839496; --gs-fg-faint: #93a1a1; --gs-fg-fainter: #b5b0a0;
276
- --gs-accent: #268bd2; --gs-accent-bg: rgba(38, 139, 210, 0.12); --gs-accent-border: rgba(38, 139, 210, 0.45);
277
- --gs-add: #6c7c00; --gs-del: #cb2825; --gs-warn: #9a7400; --gs-info: #21867f;
278
- --gs-add-bg: rgba(133, 153, 0, 0.18); --gs-del-bg: rgba(220, 50, 47, 0.14);
279
- --gs-warn-bg: rgba(181, 137, 0, 0.16); --gs-info-bg: rgba(42, 161, 152, 0.14);
280
- --gs-add-line: rgba(133, 153, 0, 0.16); --gs-del-line: rgba(220, 50, 47, 0.12);
281
- --gs-add-word: rgba(133, 153, 0, 0.38); --gs-del-word: rgba(220, 50, 47, 0.3);
282
- --gs-hunk: rgba(38, 139, 210, 0.08); --gs-neutral-bg: rgba(88, 110, 117, 0.14);
283
- --gs-backdrop: rgba(88, 110, 117, 0.26); --gs-shadow: rgba(88, 110, 117, 0.2);
284
- }
285
-
286
- /* Nord — Polar Night for dark, Snow Storm for light. Nord's aurora greens and
287
- reds are tuned for a dark ground, so the light variant darkens the diff pair
288
- rather than washing it out. */
289
- .overlay[data-gs-theme='nord-dark'] {
290
- --gs-bg: #2e3440; --gs-panel: #3b4252; --gs-raise: #434c5e;
291
- --gs-border: #4c566a; --gs-border-soft: #3b4252;
292
- --gs-fg: #eceff4; --gs-fg-muted: #d8dee9; --gs-fg-dim: #9aa5b8; --gs-fg-faint: #7b88a1; --gs-fg-fainter: #5d6a82;
293
- --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);
294
- --gs-add: #a3be8c; --gs-del: #bf616a; --gs-warn: #ebcb8b; --gs-info: #81a1c1;
295
- --gs-add-bg: rgba(163, 190, 140, 0.16); --gs-del-bg: rgba(191, 97, 106, 0.18);
296
- --gs-warn-bg: rgba(235, 203, 139, 0.16); --gs-info-bg: rgba(129, 161, 193, 0.16);
297
- --gs-add-line: rgba(163, 190, 140, 0.14); --gs-del-line: rgba(191, 97, 106, 0.15);
298
- --gs-add-word: rgba(163, 190, 140, 0.38); --gs-del-word: rgba(191, 97, 106, 0.4);
299
- --gs-hunk: rgba(136, 192, 208, 0.1); --gs-neutral-bg: rgba(154, 165, 184, 0.16);
300
- --gs-backdrop: rgba(29, 35, 45, 0.62); --gs-shadow: rgba(20, 25, 33, 0.55);
301
- }
302
- .overlay[data-gs-theme='nord-light'] {
303
- --gs-bg: #eceff4; --gs-panel: #e5e9f0; --gs-raise: #d8dee9;
304
- --gs-border: #c3cbd8; --gs-border-soft: #dfe4ec;
305
- --gs-fg: #2e3440; --gs-fg-muted: #3b4252; --gs-fg-dim: #4c566a; --gs-fg-faint: #6b7689; --gs-fg-fainter: #9aa4b5;
306
- --gs-accent: #5e81ac; --gs-accent-bg: rgba(94, 129, 172, 0.12); --gs-accent-border: rgba(94, 129, 172, 0.5);
307
- --gs-add: #5e7d47; --gs-del: #a5424c; --gs-warn: #97701f; --gs-info: #4c7a8c;
308
- --gs-add-bg: rgba(94, 125, 71, 0.14); --gs-del-bg: rgba(165, 66, 76, 0.13);
309
- --gs-warn-bg: rgba(151, 112, 31, 0.14); --gs-info-bg: rgba(76, 122, 140, 0.13);
310
- --gs-add-line: rgba(94, 125, 71, 0.14); --gs-del-line: rgba(165, 66, 76, 0.12);
311
- --gs-add-word: rgba(94, 125, 71, 0.34); --gs-del-word: rgba(165, 66, 76, 0.32);
312
- --gs-hunk: rgba(94, 129, 172, 0.09); --gs-neutral-bg: rgba(76, 86, 106, 0.13);
313
- --gs-backdrop: rgba(46, 52, 64, 0.26); --gs-shadow: rgba(46, 52, 64, 0.18);
314
- }
315
-
316
- /* ============================ drawer shell ============================ */
317
-
318
- /* ---- geometry and type scale ----
319
- *
320
- * The drawer is a plugin surface inside dsh, so it borrows dsh's own control
321
- * vocabulary rather than inventing a second one. The numbers below are measured
322
- * from the running app, not guessed: dsh settles on a 28px control height, a
323
- * 12px radius for its rows and panes, a full pill for its dropdown triggers,
324
- * and the `--dsw-font-*` ladder for type.
325
- *
326
- * Every control in this stylesheet resolves its height, radius, padding and
327
- * size through these. That is the whole mechanism keeping the drawer coherent —
328
- * before it, eight classes each spelled out their own padding and radius, and
329
- * the same "small secondary button" rendered at four different heights.
330
- */
331
- .overlay {
332
- --gs-inset: 14px;
333
-
334
- /* Control geometry — dsh's own. */
335
- --gs-h-control: 28px; /* chrome: header actions, chips, ref pickers */
336
- --gs-h-compact: 24px; /* in-pane tools, where vertical space belongs to the diff */
337
- --gs-pad-control: 0 12px;
338
- --gs-pad-compact: 0 10px;
339
-
340
- /* Readable against a solid --gs-accent fill. White for every palette whose
341
- accent is a mid-to-dark hue; the two palettes with a light accent override
342
- it, because white on #00f0ff is not a colour anyone can read. */
343
- --gs-on-accent: #ffffff;
344
-
345
- /* Commit-graph lanes. Deliberately NOT per-palette: a lane's colour carries no
346
- meaning beyond "not the same as the lane beside it", so it has nothing to
347
- follow the theme for. What it does need is to stay legible on every ground
348
- the drawer can paint, from #0d1117 to Solarized's cream — these are mid
349
- hues chosen to hold on both ends rather than tuned to either. */
350
- --gs-graph-0: #58a6ff;
351
- --gs-graph-1: #3fb950;
352
- --gs-graph-2: #d29922;
353
- --gs-graph-3: #bc8cff;
354
- --gs-graph-4: #f778ba;
355
- --gs-graph-5: #39c5cf;
356
-
357
- /* Radii. Four values, each with one job; nothing else may invent a fifth. */
358
- --gs-r-pill: 999px; /* drawer chrome: header actions, ref chips, triggers */
359
- --gs-r-control: 8px; /* controls inside a panel or pane; inputs; badges */
360
- --gs-r-surface: 12px; /* rows, popovers, panes */
361
- --gs-r-drawer: 14px; /* the drawer and its companion card */
362
-
363
- /* Type — dsh's --dsw-font-* ladder, which tops out at 11px for meta text. */
364
- --gs-t-meta: 11px; /* --dsw-font-xxxs-11: counters, captions */
365
- --gs-t-dense: 12px; /* --dsw-font-xxs-12: dense list rows */
366
- --gs-t-ui: 13px; /* --dsw-font-xs-13: controls, tabs, body */
367
-
368
- /* Two gutters, one per container. Chrome bars that span the drawer — header,
369
- tabs, worktree chips, path line, compare bar — share `--gs-gutter`; they
370
- used to sit at 20/20/14/20/20 and the drawer's left edge read as ragged.
371
- Inside a pane the gutter is tighter, because a pane is already inset by its
372
- divider and its rows carry indentation of their own. */
373
- --gs-gutter: 16px;
374
- --gs-gutter-pane: 12px;
375
-
376
- position: fixed; inset: 0; z-index: 1000;
377
- display: flex; justify-content: flex-end; align-items: stretch; gap: 12px;
378
- padding: var(--gs-inset);
379
- box-sizing: border-box;
380
- background: var(--gs-backdrop);
381
- animation: gsFade 160ms ease-out;
382
- }
383
- .drawer {
384
- position: relative;
385
- display: flex; flex-direction: column; box-sizing: border-box;
386
- width: min(1600px, 94vw); height: 100%;
387
- color: var(--gs-fg);
388
- background: var(--gs-bg);
389
- border: 1px solid var(--gs-border);
390
- border-radius: var(--gs-r-drawer);
391
- /* Rounded corners only hold if the panes cannot paint past them. This also
392
- clips the scaled-up background layer below. */
393
- overflow: hidden;
394
- box-shadow: 0 24px 64px var(--gs-shadow);
395
- font-family: var(--dsw-font-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif);
396
- animation: gsSlide 180ms cubic-bezier(0.22, 1, 0.36, 1);
397
- /* Surfaces the panes paint with. Identical to the palette's own colours until
398
- a background image is set, at which point they thin out to let it show. Every
399
- large filled area uses these; small chips and hovers keep the solid tokens,
400
- since a translucent 20px pill only reads as muddy. */
401
- --gs-surface: var(--gs-bg);
402
- --gs-surface-2: var(--gs-panel);
403
- }
404
- /* `--gs-bg-image`, `--gs-bg-blur` and `--gs-veil` are set inline by the panel
405
- from the winning style scope; the attribute is what switches the surfaces. */
406
- .drawer[data-gs-bg] {
407
- --gs-surface: color-mix(in srgb, var(--gs-bg) var(--gs-veil), transparent);
408
- --gs-surface-2: color-mix(in srgb, var(--gs-panel) var(--gs-veil), transparent);
409
- }
410
- /* Background image layer. Scaled past the edges because a blur samples beyond
411
- its own box and would otherwise fade to transparent at the border. */
412
- .drawer[data-gs-bg]::before {
413
- content: '';
414
- position: absolute; inset: 0; z-index: 0;
415
- background-image: var(--gs-bg-image);
416
- background-size: cover; background-position: center;
417
- filter: blur(var(--gs-bg-blur));
418
- transform: scale(1.12);
419
- pointer-events: none;
420
- }
421
- /* Everything real sits above that layer. The drag handle is excluded because it
422
- is absolutely positioned and carries its own stacking order; matching it here
423
- would leave the two rules deciding by source order.
424
- Chrome that hosts a popover must sit ABOVE `.body`: later siblings with the
425
- same z-index paint on top, which buried Theme and Branch menus under the
426
- panes. A menu that renders but cannot be clicked looks like a dead control.
427
- Popovers also drop DOWNWARD, so a bar's list can reach past every bar below
428
- it — which means the ranks must strictly DECREASE down the DOM, or a lower
429
- bar at the same z-index absorbs the rows (the worktree picker's list was
430
- unclickable behind the sync bar until they were separated).
431
- STRICTLY decreasing, including between the header and the tabs. Those two
432
- shared rank 23 for as long as the header's only menu was Appearance, which
433
- portals to the rail outside the drawer and so never dropped through anything.
434
- The worktree picker does drop, and the tab bar — a later sibling at the same
435
- rank — swallowed its filter box on the first try. */
436
- .drawer > *:not(.resizer) { position: relative; z-index: 1; }
437
- .drawer > .header { z-index: 24; }
438
- .drawer > .tabs { z-index: 23; }
439
- .drawer > .compareBar { z-index: 22; }
440
- .drawer > .syncBar { z-index: 20; }
441
- /* The roll-back confirmation, and the one deliberate exception to the
442
- strictly-decreasing rule above: it is the LAST child and outranks every
443
- other, because it is not a popover dropping through its siblings but a
444
- modal that has to cover them. It also has to be declared here — the
445
- `> *:not(.resizer)` rule out-specifies a bare `.confirmScrim`, which is
446
- what pinned it to the bottom of the drawer as an in-flow flex item. */
447
- .drawer > .confirmScrim { position: absolute; inset: 0; z-index: 40; }
448
- /* Maximized drops the inset and the rounding: at full bleed they would only
449
- waste edge pixels the diff wants. */
450
- .overlayMax { padding: 0; gap: 0; }
451
- .overlayMax .drawer { flex: 1 1 auto; width: auto; min-width: 0; border-radius: 0; border-width: 0 0 0 1px; }
452
- @keyframes gsFade { from { opacity: 0; } }
453
- @keyframes gsSlide { from { transform: translateX(24px); opacity: 0; } }
454
-
455
- /* Drag handle on the leading edge: a 10px hit area that is easy to grab, showing
456
- a short centred grip in the card's own border colour. A full-height accent bar
457
- was read as a second scrollbar and pulled the eye off the diff. */
458
- .resizer {
459
- position: absolute; top: 0; bottom: 0; left: 0; z-index: 30;
460
- display: flex; align-items: center; justify-content: center;
461
- width: 10px;
462
- cursor: col-resize;
463
- touch-action: none;
464
- }
465
- .resizer::after {
466
- content: '';
467
- width: 3px; height: 32px;
468
- border-radius: 999px;
469
- background: var(--gs-fg-fainter);
470
- opacity: 0;
471
- transition: opacity 140ms ease, height 140ms ease;
472
- }
473
- .resizer:hover::after { opacity: 0.6; }
474
- .resizerActive::after { opacity: 0.9; height: 56px; }
475
-
476
- /* Divider between two panes. It carries the 1px rule the panes used to draw as
477
- their own border, so the visible line and the drag target are one element and
478
- cannot drift apart. */
479
- .paneDivider {
480
- position: relative; z-index: 5;
481
- flex: none;
482
- display: flex; align-items: center; justify-content: center;
483
- width: 7px;
484
- cursor: col-resize;
485
- touch-action: none;
486
- }
487
- .paneDivider::before {
488
- content: '';
489
- position: absolute; top: 0; bottom: 0; left: 3px;
490
- width: 1px;
491
- background: var(--gs-border-soft);
492
- }
493
- .paneDivider::after {
494
- content: '';
495
- position: relative;
496
- width: 3px; height: 28px;
497
- border-radius: 999px;
498
- background: var(--gs-fg-fainter);
499
- opacity: 0;
500
- transition: opacity 140ms ease, height 140ms ease;
501
- }
502
- .paneDivider:hover::after { opacity: 0.6; }
503
- .paneDividerActive::after { opacity: 0.9; height: 48px; }
504
- /* The same handle turned through a right angle. Both modifier rules below beat
505
- the shared ones on specificity, so their order relative to them does not
506
- matter — the stylesheet's usual "same value loses" trap does not apply. */
507
- .paneDividerY {
508
- width: auto; height: 7px;
509
- cursor: row-resize;
510
- }
511
- .paneDividerY::before { top: 3px; bottom: auto; left: 0; right: 0; width: auto; height: 1px; }
512
- .paneDividerY::after { width: 28px; height: 3px; transition: opacity 140ms ease, width 140ms ease; }
513
- .paneDividerY.paneDividerActive::after { width: 48px; height: 3px; }
514
-
515
- .header {
516
- display: flex; align-items: center; justify-content: space-between; gap: 12px;
517
- flex: none;
518
- padding: 10px var(--gs-gutter);
519
- border-bottom: 1px solid var(--gs-border);
520
- background: var(--gs-surface-2);
521
- font-family: var(--dsw-font-ui, system-ui, sans-serif);
522
- font-size: var(--gs-t-ui);
523
- }
524
- .headerLeft { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; min-width: 0; }
525
- /* The subject of the drawer: which worktree. Worn by the picker's trigger when
526
- there is a choice (`.headerPicker` below) and by a plain span when there is
527
- not, so a one-worktree repository still states its identity in the same
528
- place, at the same weight. */
529
- .headerBranch {
530
- display: inline-flex; align-items: center; gap: 6px;
531
- padding: 2px 10px;
532
- border: 1px solid var(--gs-accent-border);
533
- border-radius: 999px;
534
- background: var(--gs-accent-bg);
535
- color: var(--gs-accent);
536
- font-weight: 600;
537
- }
538
- /* Colour only — the geometry stays the button vocabulary's, so the trigger is
539
- the same pill as every other menu in the drawer and only its role differs.
540
- Qualified with `.refButton` deliberately: the shared vocabulary is declared
541
- at the FOOT of this file, so a bare `.headerPicker` up here loses every
542
- property they both set to source order, and the chip came out grey. */
543
- .refButton.headerPicker {
544
- /* The drawer's subject, in a row with 1400px to spare — the 260px the small
545
- pickers share clipped `feature/nested/deep/scheduler` at a point where the
546
- name needed 205. Still capped, so one absurd branch cannot push the totals
547
- off the row. */
548
- max-width: 420px;
549
- border-color: var(--gs-accent-border);
550
- background: var(--gs-accent-bg);
551
- color: var(--gs-accent);
552
- font-weight: 600;
553
- }
554
- .refButton.headerPicker:hover { background: var(--gs-accent-bg); color: var(--gs-accent); border-color: var(--gs-accent); }
555
-
556
- /* Slash-separated names — paths and branches alike — that give up their HEAD
557
- when the row runs short. Plain `text-overflow: ellipsis` eats the leaf, and
558
- the leaf is the half that answers "which one": `…/worktrees/fixture-07` vs
559
- `…/worktrees/fixture-14`, `feature/deep/parser` vs `feature/deep/lexer`.
560
-
561
- The shrink is WEIGHTED rather than pinned. `flex: none` on the tail read
562
- correctly for a path, but a name with no head at all — a bare
563
- `some-very-long-branch-name` — would then refuse to shrink and simply
564
- overflow its row. At 999 the head absorbs effectively all of it, and the
565
- tail only starts giving ground once the head has nothing left. */
566
- .elide { display: inline-flex; align-items: baseline; flex: 0 1 auto; min-width: 0; }
567
- .elideHead {
568
- flex: 0 999 auto; min-width: 0;
569
- overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
570
- }
571
- .elideTail {
572
- flex: 0 1 auto; min-width: 0;
573
- overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
574
- }
575
- /* The header's path: quiet, with the leaf a shade brighter than its directory,
576
- and free to take whatever the totals beside it leave. */
577
- .headerPathMain { color: var(--gs-fg-faint); font-size: var(--gs-t-meta); }
578
- .headerPathMain .elideTail { color: var(--gs-fg-dim); }
579
- /* Either end of the compare pill, so a long branch elides there too rather
580
- than stretching the pill across the header. */
581
- .headerViewRef { max-width: 160px; }
582
-
583
- /* What the view is about when it is NOT the working tree: a commit hash, or a
584
- comparison's two ends. Quieter than the worktree chip — the worktree is where
585
- you are, this is only what you are looking at inside it. */
586
- .headerView {
587
- flex: none;
588
- padding: 1px 8px;
589
- border-radius: 999px;
590
- background: var(--gs-neutral-bg);
591
- color: var(--gs-fg-muted);
592
- font-size: var(--gs-t-meta);
593
- font-variant-numeric: tabular-nums;
594
- }
595
- .headerDetached {
596
- padding: 1px 8px;
597
- border-radius: 999px;
598
- background: var(--gs-warn-bg);
599
- color: var(--gs-warn);
600
- font-size: var(--gs-t-meta);
601
- letter-spacing: 0.02em;
602
- }
603
- .headerTotals { font-variant-numeric: tabular-nums; }
604
- .headerTotalsAdd { color: var(--gs-add); font-weight: 600; }
605
- .headerTotalsDel { color: var(--gs-del); font-weight: 600; }
606
- .headerTotalsDim { color: var(--gs-fg-dim); font-size: var(--gs-t-dense); }
607
- /* The gap is the rule, not each button's margin. `.btn` used to carry its own
608
- `margin-left`, which meant the spacing lived in the button and vanished the
609
- moment the button's geometry moved into the shared vocabulary. */
610
- .headerRight { display: flex; flex: none; gap: 8px; position: relative; }
611
-
612
- /* Theme button in the header. The panel is a companion card in the overlay,
613
- to the LEFT of the drawer — not a column stolen from the diff, and not a
614
- popover painted over the code. */
615
- .theme { position: relative; display: inline-flex; }
616
- /* Settings hangs from its own gear like every other menu in the drawer, right
617
- edge aligned to the button because the button is the last thing in the row
618
- and a left-aligned card would run off the drawer. Wider than a ref list: it
619
- holds a palette grid, an image preview and a CSS editor, none of which fit a
620
- menu-width column. */
621
- .refPop.settingsPop {
622
- top: calc(100% + 6px); right: 0; left: auto;
623
- width: 320px; max-width: calc(100vw - 32px);
624
- max-height: min(560px, calc(100vh - 120px));
625
- }
626
- .themeRail {
627
- flex: 1; min-height: 0;
628
- display: flex; flex-direction: column; gap: 14px;
629
- padding: 12px 14px 16px;
630
- overflow-y: auto;
631
- font-family: var(--dsw-font-ui, system-ui, sans-serif);
632
- }
633
- .themeGroup { display: flex; flex-direction: column; gap: 6px; }
634
- .themeLabel {
635
- color: var(--gs-fg-faint);
636
- font-size: var(--gs-t-meta); font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase;
637
- }
638
- .themeRowSplit { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
639
-
640
- /* Mode picker. Each option is a painted swatch over its label, and the swatch
641
- colours are FIXED rather than palette-derived: "light" and "dark" name the
642
- colours themselves, so a dark-palette button rendering the light option in
643
- dark grey would be telling the user the opposite of what it does. */
644
- /* The mode picker is the one control that is taller than the scale: it stacks a
645
- painted swatch over its label, because "light" and "dark" are better shown
646
- than named. Everything except the height comes from the vocabulary. */
647
- .segmented { display: flex; gap: 6px; }
648
- .segment {
649
- flex: 1 1 0;
650
- display: flex; flex-direction: column; gap: 4px; align-items: stretch;
651
- box-sizing: border-box;
652
- padding: 4px;
653
- border: 1px solid var(--gs-border); border-radius: var(--gs-r-control);
654
- background: transparent; color: var(--gs-fg-dim);
655
- font-family: inherit; font-size: var(--gs-t-meta);
656
- cursor: pointer;
657
- transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
658
- }
659
- .segment:hover { background: var(--gs-raise); border-color: var(--gs-fg-fainter); color: var(--gs-fg); }
660
- .segment:focus-visible { outline: 2px solid var(--gs-accent); outline-offset: 1px; }
661
- .segmentChip {
662
- height: 20px;
663
- border: 1px solid rgba(128, 128, 128, 0.45);
664
- border-radius: calc(var(--gs-r-control) - 4px);
665
- }
666
- .chipLight { background: #ffffff; }
667
- .chipDark { background: #0b0b0d; }
668
- /* System takes both halves, which is how every OS appearance control shows it. */
669
- .chipSystem { background: linear-gradient(105deg, #ffffff 0 50%, #0b0b0d 50% 100%); }
670
-
671
- .paletteRow {
672
- display: flex; align-items: center; gap: 8px;
673
- width: 100%; box-sizing: border-box;
674
- padding: 5px 8px;
675
- border: 0; border-radius: var(--gs-r-control);
676
- background: transparent; color: var(--gs-fg-muted);
677
- font-family: inherit; font-size: var(--gs-t-dense);
678
- text-align: left; cursor: pointer;
679
- }
680
- .paletteRow:hover { background: var(--gs-raise); color: var(--gs-fg); }
681
- /* .paletteRowActive: see the shared selected-state rule at the foot of the file. */
682
- /* Three-stop swatch: ground, accent, and the add colour the diff leans on. */
683
- .swatch {
684
- flex: none;
685
- display: inline-flex; overflow: hidden;
686
- width: 26px; height: 12px;
687
- border: 1px solid var(--gs-border); border-radius: 3px;
688
- }
689
- .swatch span { flex: 1; }
690
-
691
- /* Scope switch: which of the two stores an edit lands in. */
692
- .scopeRow { display: flex; gap: 6px; }
693
- .scopeHint { color: var(--gs-fg-faint); font-size: var(--gs-t-meta); overflow-wrap: anywhere; }
694
-
695
- .bgPreview {
696
- height: 64px;
697
- border: 1px solid var(--gs-border); border-radius: var(--gs-r-control);
698
- background-color: var(--gs-bg);
699
- background-size: cover; background-position: center;
700
- }
701
- .bgEmpty {
702
- display: flex; align-items: center; justify-content: center;
703
- color: var(--gs-fg-faint); font-size: var(--gs-t-meta);
704
- }
705
- .sliderRow { display: flex; align-items: center; gap: 8px; font-size: var(--gs-t-meta); color: var(--gs-fg-dim); }
706
- .sliderRow input { flex: 1; min-width: 0; accent-color: var(--gs-accent); }
707
- .sliderValue { flex: none; width: 38px; text-align: right; font-variant-numeric: tabular-nums; }
708
-
709
- .cssArea {
710
- width: 100%; box-sizing: border-box;
711
- min-height: 96px; max-height: 220px;
712
- padding: 7px 8px;
713
- border: 1px solid var(--gs-border); border-radius: var(--gs-r-control);
714
- background: var(--gs-bg); color: var(--gs-fg);
715
- font-family: var(--dsw-font-mono, ui-monospace, Consolas, monospace); font-size: var(--gs-t-meta);
716
- resize: vertical;
717
- outline: none;
718
- }
719
- .cssArea:focus { border-color: var(--gs-accent-border); }
720
- .themeNote { color: var(--gs-warn); font-size: var(--gs-t-meta); }
721
- .themeDirty { color: var(--gs-accent); font-size: var(--gs-t-meta); }
722
-
723
- /* Worktree rows give their whole width to the branch; `.refRowName` already
724
- flexes and ellipsises, so nothing else here competes for it. The path is on
725
- the row's `title`. */
726
- /* Marks the session's OWN worktree, independent of which one is being viewed. */
727
- .wtCurrent { color: var(--gs-add); font-size: 8px; line-height: 1; }
728
-
729
- .commitCopy { margin-left: auto; }
730
-
731
- .commitPop {
732
- position: fixed; z-index: 40;
733
- box-sizing: border-box;
734
- width: min(380px, calc(100vw - 24px));
735
- display: flex; flex-direction: column; gap: 6px;
736
- padding: 10px 12px 12px;
737
- overflow: auto;
738
- border: 1px solid var(--gs-border); border-radius: var(--gs-r-surface);
739
- background: var(--gs-panel); color: var(--gs-fg);
740
- box-shadow: 0 16px 40px var(--gs-shadow);
741
- font-family: var(--dsw-font-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif);
742
- font-size: var(--gs-t-dense);
743
- }
744
- .commitPopTop { display: flex; align-items: center; gap: 8px; }
745
- .commitPopTop .commitWhen { margin-right: auto; }
746
- /* Author / committer / exact date — the precise answers the row's summary
747
- ("3 weeks ago") deliberately defers to the hover card. */
748
- .commitPopMeta {
749
- display: flex; flex-direction: column; gap: 2px;
750
- color: var(--gs-fg-muted);
751
- font-size: var(--gs-t-dense); line-height: 16px;
752
- }
753
- /* The filter box in the history pane head — it flexes to fill the head beside
754
- the title (the funnel button is flex:none), so a wide pane gets a wide box
755
- and a narrow pane still leaves room to type. */
756
- .commitFilter {
757
- flex: 1 1 auto; min-width: 120px;
758
- height: var(--gs-h-control); box-sizing: border-box;
759
- padding: 0 10px;
760
- border: 1px solid var(--gs-border-soft); border-radius: var(--gs-r-control);
761
- background: var(--gs-bg); color: var(--gs-fg);
762
- font-family: inherit; font-size: var(--gs-t-dense);
763
- outline: none;
764
- }
765
- .commitFilter:focus { border-color: var(--gs-accent); }
766
- .commitFilter::placeholder { color: var(--gs-fg-faint); }
767
- /* Active criteria under the head: one removable chip each, grammar tokens as
768
- their own labels — the chip IS the query it would take to retype it. */
769
- .filterChips {
770
- display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
771
- flex: none;
772
- padding: 6px 10px;
773
- border-bottom: 1px solid var(--gs-border-soft);
774
- }
775
- .filterChip {
776
- display: inline-flex; align-items: center; gap: 2px;
777
- max-width: 220px;
778
- border: 1px solid var(--gs-border-soft); border-radius: var(--gs-r-control);
779
- background: var(--gs-surface-2);
780
- font-size: var(--gs-t-dense); line-height: 18px;
781
- }
782
- .filterChipLabel {
783
- padding: 2px 4px 2px 8px;
784
- overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
785
- color: var(--gs-fg-muted);
786
- }
787
- .filterChipRemove {
788
- flex: none;
789
- padding: 0 6px;
790
- border: none; border-left: 1px solid var(--gs-border-soft);
791
- background: transparent; color: var(--gs-fg-dim);
792
- font-family: inherit; font-size: var(--gs-t-dense); line-height: 20px;
793
- cursor: pointer;
794
- }
795
- .filterChipRemove:hover { color: var(--gs-danger, var(--gs-fg)); }
796
- .filterClear {
797
- border: none; background: transparent;
798
- padding: 2px 4px;
799
- color: var(--gs-fg-faint);
800
- font-family: inherit; font-size: var(--gs-t-dense); line-height: 18px;
801
- cursor: pointer;
802
- }
803
- .filterClear:hover { color: var(--gs-fg); }
804
- /* The funnel dropdown — RefPicker's positioning pattern (relative anchor,
805
- absolute panel, dismiss-on-outside). Grows leftward: it sits at the pane's
806
- right edge. */
807
- .funnel { position: relative; display: inline-flex; }
808
- /* The button itself carries no geometry of its own — it joined the shared
809
- control vocabulary above (`.btn, .refButton, …`) so it reads as a sibling of
810
- every other button in the drawer, not a foreign element. */
811
- .funnelButton.funnelButtonActive { color: var(--gs-accent); border-color: var(--gs-accent); }
812
-
813
- /* The panel is a flex COLUMN with exactly ONE scrolling child. It used to
814
- scroll itself AND cap its list at 240px: two nested scroll containers in a
815
- 331px box that had ~700px of anchor space to spend, so a long roster got a
816
- fat inner scrollbar instead of the height already available. The tab strip
817
- and the footer are `flex: none`; the list takes what is left. */
818
- .funnelPop {
819
- position: fixed; z-index: 40;
820
- display: flex; flex-direction: column;
821
- width: 320px; max-width: calc(100vw - 24px);
822
- box-sizing: border-box;
823
- overflow: hidden;
824
- border: 1px solid var(--gs-border); border-radius: var(--gs-r-surface);
825
- background: var(--gs-panel);
826
- box-shadow: 0 16px 40px var(--gs-shadow);
827
- font-size: var(--gs-t-dense);
828
- }
829
- /* One section at a time — the panel's height is a property of its design,
830
- not of how many authors the repository happens to have. The strip is a
831
- segmented control (inset track, raised active chip) rather than one
832
- outlined pill beside two bare words, which read as a button and two labels
833
- instead of one three-way choice. */
834
- .funnelTabs {
835
- flex: none;
836
- display: flex; gap: 2px;
837
- margin: 8px 8px 0;
838
- padding: 2px;
839
- border: 1px solid var(--gs-border-soft); border-radius: var(--gs-r-pill);
840
- background: var(--gs-bg);
841
- }
842
- .funnelTab {
843
- flex: 1; min-width: 0;
844
- height: 22px; padding: 0 8px;
845
- border: none; border-radius: var(--gs-r-pill);
846
- background: transparent;
847
- color: var(--gs-fg-dim);
848
- font-family: inherit; font-size: var(--gs-t-dense); line-height: 1;
849
- white-space: nowrap;
850
- cursor: pointer;
851
- transition: background 120ms ease, color 120ms ease;
852
- }
853
- .funnelTab:hover { color: var(--gs-fg); }
854
- .funnelTab.funnelTabActive {
855
- color: var(--gs-fg);
856
- background: var(--gs-raise);
857
- box-shadow: inset 0 0 0 1px var(--gs-border);
858
- }
859
- .funnelTab:focus-visible { outline: 2px solid var(--gs-accent); outline-offset: 1px; }
860
- /* A tab's own criteria count, so the two sections nobody is looking at still
861
- say they hold something. */
862
- .funnelTabCount { margin-left: 4px; color: var(--gs-accent); font-variant-numeric: tabular-nums; }
863
-
864
- /* The body of the active section. Everything inside is `flex: none` except
865
- the list, which is the one thing allowed to scroll. */
866
- .funnelPane {
867
- flex: 1 1 auto; min-height: 0;
868
- display: flex; flex-direction: column; gap: 6px;
869
- padding: 8px;
870
- }
871
- .funnelCaption {
872
- flex: none;
873
- color: var(--gs-fg-faint);
874
- font-size: 11px; line-height: 14px;
875
- letter-spacing: 0.04em; text-transform: uppercase;
876
- }
877
-
878
- /* Which bound a picked day lands in. Deliberately NOT the tab strip's shape:
879
- a rect track with an accent-tinted chip under a caption, because two
880
- identical pill rows stacked six pixels apart said nothing about meaning two
881
- different things. */
882
- .funnelBounds {
883
- flex: none;
884
- display: flex; gap: 2px;
885
- padding: 2px;
886
- border: 1px solid var(--gs-border-soft); border-radius: var(--gs-r-control);
887
- background: var(--gs-bg);
888
- }
889
- .funnelBoundBtn {
890
- flex: 1; min-width: 0;
891
- height: 20px; padding: 0 6px;
892
- border: none; border-radius: calc(var(--gs-r-control) - 3px);
893
- background: transparent;
894
- color: var(--gs-fg-dim);
895
- font-family: inherit; font-size: var(--gs-t-dense); line-height: 1;
896
- cursor: pointer;
897
- transition: background 120ms ease, color 120ms ease;
898
- }
899
- .funnelBoundBtn:hover { color: var(--gs-fg); }
900
- .funnelBoundBtn.funnelBoundBtnActive {
901
- color: var(--gs-accent);
902
- background: var(--gs-accent-bg);
903
- box-shadow: inset 0 0 0 1px var(--gs-accent-border);
904
- }
905
-
906
- /* The calendar — modern-grid look in the drawer's own tokens. */
907
- .cal { flex: none; display: flex; flex-direction: column; gap: 4px; }
908
- .calHead { display: flex; align-items: center; gap: 4px; }
909
- .calTitle {
910
- flex: 1; text-align: center;
911
- color: var(--gs-fg);
912
- font-size: var(--gs-t-dense); line-height: 20px;
913
- }
914
- .calNav {
915
- flex: none;
916
- width: 22px; height: 20px; padding: 0;
917
- border: none; border-radius: var(--gs-r-control);
918
- background: transparent;
919
- color: var(--gs-fg-dim);
920
- font-family: inherit; font-size: var(--gs-t-ui);
921
- cursor: pointer;
922
- }
923
- .calNav:hover { color: var(--gs-fg); background: var(--gs-raise); }
924
- .calWeek, .calGrid {
925
- display: grid;
926
- grid-template-columns: repeat(7, 1fr);
927
- gap: 2px;
928
- }
929
- .calWeek span {
930
- text-align: center;
931
- color: var(--gs-fg-faint);
932
- font-size: 11px; line-height: 14px;
933
- }
934
- .calGrid button {
935
- height: 26px;
936
- border: none; border-radius: var(--gs-r-control);
937
- background: transparent;
938
- color: var(--gs-fg-muted);
939
- font-family: inherit; font-size: var(--gs-t-dense);
940
- font-variant-numeric: tabular-nums;
941
- cursor: pointer;
942
- }
943
- .calGrid button:hover { background: var(--gs-raise); color: var(--gs-fg); }
944
- .calGrid button.calOut { color: var(--gs-fg-fainter); }
945
- .calGrid button.calToday { color: var(--gs-accent); font-weight: 600; }
946
- /* Days strictly between the two bounds: the span the filter actually means,
947
- which two lone marked endpoints never showed. */
948
- .calGrid button.calIn { background: var(--gs-accent-bg); color: var(--gs-fg); }
949
- .calGrid button.calMark {
950
- background: var(--gs-accent);
951
- color: var(--gs-on-accent);
952
- font-weight: 600;
953
- }
954
-
955
- /* The two bound readouts, side by side under the grid. */
956
- .funnelBoundRows { flex: none; display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
957
- .funnelBoundRow {
958
- display: flex; align-items: center; gap: 4px; min-width: 0;
959
- box-sizing: border-box;
960
- padding: 3px 6px;
961
- border: 1px solid var(--gs-border-soft); border-radius: var(--gs-r-control);
962
- background: var(--gs-bg);
963
- font-size: 11px; line-height: 16px;
964
- }
965
- .funnelBoundKey { flex: none; color: var(--gs-fg-faint); }
966
- .funnelBoundVal {
967
- flex: 1; min-width: 0;
968
- overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
969
- color: var(--gs-fg-faint);
970
- font-variant-numeric: tabular-nums;
971
- }
972
- .funnelBoundValSet { color: var(--gs-fg); }
973
- .funnelBoundClear {
974
- flex: none;
975
- padding: 0 2px;
976
- border: none; background: transparent;
977
- color: var(--gs-fg-dim);
978
- font-family: inherit; font-size: var(--gs-t-dense); line-height: 1;
979
- cursor: pointer;
980
- }
981
- .funnelBoundClear:hover { color: var(--gs-danger, var(--gs-fg)); }
982
-
983
- .funnelSearch {
984
- flex: none;
985
- box-sizing: border-box;
986
- height: 26px; padding: 0 8px;
987
- border: 1px solid var(--gs-border-soft); border-radius: var(--gs-r-control);
988
- background: var(--gs-bg); color: var(--gs-fg);
989
- font-family: inherit; font-size: var(--gs-t-dense);
990
- outline: none;
991
- }
992
- .funnelSearch::placeholder { color: var(--gs-fg-faint); }
993
- .funnelSearch:focus { border-color: var(--gs-accent); box-shadow: 0 0 0 2px var(--gs-accent-bg); }
994
- .funnelList {
995
- flex: 1 1 auto; min-height: 0;
996
- overflow-y: auto; overscroll-behavior: contain;
997
- display: flex; flex-direction: column; gap: 1px;
998
- margin: 0 -4px; padding: 0 4px;
999
- scrollbar-width: thin; scrollbar-color: var(--gs-fg-fainter) transparent;
1000
- }
1001
- .funnelList::-webkit-scrollbar { width: 10px; }
1002
- .funnelList::-webkit-scrollbar-track { background: transparent; }
1003
- .funnelList::-webkit-scrollbar-thumb {
1004
- background: var(--gs-fg-fainter);
1005
- border: 3px solid transparent; border-radius: 5px;
1006
- background-clip: content-box;
1007
- }
1008
- /* Every row is the same four slots — chevron, tick, glyph, name — then the
1009
- count. Directories used to skip the glyph that files carried, so a folder
1010
- name and the file names under it started at different x and the list had a
1011
- ragged left edge. */
1012
- .funnelRow {
1013
- display: flex; align-items: center; gap: 6px;
1014
- box-sizing: border-box;
1015
- min-height: 24px; padding: 0 4px;
1016
- border-radius: var(--gs-r-control);
1017
- cursor: pointer;
1018
- }
1019
- .funnelRow:hover { background: var(--gs-raise); }
1020
- .funnelRow:has(input[type='checkbox']:checked) .funnelName,
1021
- .funnelRow:has(input[type='checkbox']:indeterminate) .funnelName { color: var(--gs-fg); }
1022
- /* The tick IS the input — no wrapper span, no visually-hidden twin: the live
1023
- probes select `[class*="funnelRow"] input[type=checkbox]` and the directory
1024
- row's DIRECT child, so the markup has to stay flat. `appearance: none`
1025
- hands us the box; the mark is a rotated border, so nothing ships as an
1026
- asset and every state recolors with the theme. */
1027
- .funnelRow input[type='checkbox'] {
1028
- appearance: none; -webkit-appearance: none;
1029
- flex: none; position: relative;
1030
- box-sizing: border-box;
1031
- width: 14px; height: 14px; margin: 0;
1032
- border: 1px solid var(--gs-fg-fainter); border-radius: 3px;
1033
- background: transparent;
1034
- cursor: pointer;
1035
- transition: background 120ms ease, border-color 120ms ease;
1036
- }
1037
- .funnelRow input[type='checkbox']:hover { border-color: var(--gs-fg-dim); }
1038
- .funnelRow input[type='checkbox']:checked,
1039
- .funnelRow input[type='checkbox']:indeterminate {
1040
- background: var(--gs-accent); border-color: var(--gs-accent);
1041
- }
1042
- .funnelRow input[type='checkbox']:focus-visible { outline: 2px solid var(--gs-accent); outline-offset: 1px; }
1043
- .funnelRow input[type='checkbox']:checked::after {
1044
- content: ''; position: absolute;
1045
- left: 4px; top: 1px; width: 3px; height: 7px;
1046
- border: solid var(--gs-on-accent); border-width: 0 1.5px 1.5px 0;
1047
- transform: rotate(42deg);
1048
- }
1049
- /* Partial — a folder some of whose subtree is ticked. */
1050
- .funnelRow input[type='checkbox']:indeterminate::after {
1051
- content: ''; position: absolute;
1052
- left: 2px; top: 5px; width: 8px; height: 2px;
1053
- border-radius: 1px;
1054
- background: var(--gs-on-accent);
1055
- }
1056
- /* Tree chrome in the drawer's own visual language: a directory folds with the
1057
- chevron the file tree uses, and both glyphs are inline SVG in the shape
1058
- IntelliJ's New UI icon set uses — a 16px grid, 1px strokes, no fill,
1059
- rounded joins, outlines rather than the filled silhouettes the old UI
1060
- shipped. Drawn in `PathDirGlyph` / `PathFileGlyph` rather than imported:
1061
- the bundle purity gate forbids an icon package, and the picker needs
1062
- exactly these two. Indentation is the row's own padding ALONE; the nested
1063
- rail this used to draw was `--gs-border-soft` over `--gs-panel` (the same
1064
- value as `--gs-raise` in the GitHub palettes, so it vanished under hover)
1065
- and its margin double-counted every level. */
1066
- .pathNode { display: flex; flex-direction: column; }
1067
- .pathChildren { display: flex; flex-direction: column; }
1068
- /* One shared slot so a folder and a file start their names at the same x —
1069
- New UI draws both on the same 16px grid, and the column only lines up if
1070
- the box does too. `--gs-fg-dim` is the neutral the tree icons sit at there:
1071
- present, but never louder than the filename beside them. */
1072
- .pathFileGlyph, .pathDirGlyph {
1073
- flex: none;
1074
- width: 16px; height: 16px;
1075
- color: var(--gs-fg-dim);
1076
- }
1077
- .funnelRow:hover .pathFileGlyph,
1078
- .funnelRow:hover .pathDirGlyph { color: var(--gs-fg-muted); }
1079
- .funnelName {
1080
- flex: 1; min-width: 0;
1081
- overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
1082
- color: var(--gs-fg-muted);
1083
- }
1084
- .funnelCount {
1085
- flex: none;
1086
- color: var(--gs-fg-faint);
1087
- font-size: 11px;
1088
- font-variant-numeric: tabular-nums;
1089
- }
1090
- .funnelMore { color: var(--gs-fg-faint); padding: 4px 6px; }
1091
- .funnelChevron {
1092
- flex: none;
1093
- width: 14px; padding: 0;
1094
- border: none; background: transparent;
1095
- color: var(--gs-fg-faint);
1096
- font-family: inherit; font-size: 9px; line-height: 16px;
1097
- cursor: pointer;
1098
- }
1099
- .funnelChevron:hover:not(:disabled) { color: var(--gs-fg); }
1100
- .funnelChevron:disabled { cursor: default; }
1101
- .funnelPresets { flex: none; display: flex; gap: 4px; }
1102
- /* `.funnelPreset` shipped with `flex: none` and nothing else, so it never
1103
- joined the shared button vocabulary and the three date presets rendered as
1104
- the browser's default grey buttons — the one patch of unthemed chrome in
1105
- the drawer. It is in both selector lists now; this only sizes them to share
1106
- one row. */
1107
- .funnelPreset { flex: 1 1 0; min-width: 0; padding: 0 6px; }
1108
- /* Qualified, like `.funnelButtonActive`: the shared vocabulary is declared
1109
- several hundred lines BELOW this one, so a bare modifier would lose its
1110
- background to that rule's `background: transparent`. */
1111
- .funnelPreset.funnelPresetActive {
1112
- color: var(--gs-accent);
1113
- border-color: var(--gs-accent);
1114
- background: var(--gs-accent-bg);
1115
- }
1116
-
1117
- /* The footer says what the panel is currently asking git for. Without it the
1118
- only feedback lived on the chip row BEHIND the panel, so a reader had to
1119
- close the thing to find out whether their ticks took. */
1120
- .funnelFoot {
1121
- flex: none;
1122
- display: flex; align-items: center; justify-content: space-between; gap: 8px;
1123
- padding: 6px 10px;
1124
- border-top: 1px solid var(--gs-border-soft);
1125
- background: var(--gs-bg);
1126
- font-size: 11px; line-height: 16px;
1127
- }
1128
- .funnelFootCount { color: var(--gs-fg-faint); font-variant-numeric: tabular-nums; }
1129
- .funnelFootCountOn { color: var(--gs-fg-muted); }
1130
- .funnelFootClear {
1131
- flex: none;
1132
- padding: 2px 6px;
1133
- border: none; border-radius: var(--gs-r-control);
1134
- background: transparent;
1135
- color: var(--gs-fg-dim);
1136
- font-family: inherit; font-size: 11px; line-height: 16px;
1137
- cursor: pointer;
1138
- }
1139
- .funnelFootClear:hover:not(:disabled) { color: var(--gs-fg); background: var(--gs-raise); }
1140
- .funnelFootClear:disabled { color: var(--gs-fg-fainter); cursor: default; }
1141
-
1142
- .commitPopSubject {
1143
- color: var(--gs-fg);
1144
- font-size: var(--gs-t-ui); font-weight: 600; line-height: 18px;
1145
- white-space: pre-wrap; overflow-wrap: anywhere;
1146
- user-select: text;
1147
- }
1148
- .commitPopBody {
1149
- margin: 0;
1150
- color: var(--gs-fg-muted);
1151
- font-family: inherit;
1152
- font-size: var(--gs-t-dense); line-height: 18px;
1153
- white-space: pre-wrap; overflow-wrap: anywhere;
1154
- user-select: text;
1155
- }
1156
-
1157
- /* Drawer tabs — mirrors the app's own tab bar: 13/16 text at weight 500, a
1158
- tertiary label, the selected one riding the accent over a 2px bar. */
1159
- .tabs {
1160
- display: flex; gap: 24px;
1161
- flex: none;
1162
- padding: 0 var(--gs-gutter);
1163
- border-bottom: 1px solid var(--gs-border-soft);
1164
- background: var(--gs-surface-2);
1165
- font-family: inherit;
1166
- }
1167
- .tab {
1168
- position: relative;
1169
- padding: 8px 0 9px;
1170
- border: none;
1171
- background: transparent;
1172
- font-family: inherit;
1173
- font-size: var(--gs-t-ui); line-height: 16px; font-weight: 500;
1174
- color: var(--gs-fg-dim);
1175
- cursor: pointer;
1176
- }
1177
- .tab::after {
1178
- content: '';
1179
- position: absolute; right: 0; bottom: 1px; left: 0;
1180
- height: 2px; border-radius: 2px;
1181
- background: transparent;
1182
- }
1183
- .tabActive { color: var(--gs-accent); }
1184
- .tabActive::after { background: var(--gs-accent); }
1185
-
1186
- /* Compare tab: the base/head ref pickers. */
1187
- .compareBar {
1188
- display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
1189
- flex: none;
1190
- padding: 8px var(--gs-gutter);
1191
- border-bottom: 1px solid var(--gs-border-soft);
1192
- background: var(--gs-surface-2);
1193
- color: var(--gs-fg-dim);
1194
- font-family: var(--dsw-font-ui, system-ui, sans-serif);
1195
- font-size: var(--gs-t-dense);
1196
- }
1197
- .compareArrow { color: var(--gs-fg-faint); }
1198
-
1199
- /* Ref picker: a button that opens a filter box over a scrolling list. Sized for
1200
- a repository with hundreds of branches, where a chip row or a plain select
1201
- stops being usable. */
1202
- .refPicker { position: relative; display: inline-flex; align-items: center; gap: 6px; }
1203
- .refLabel { flex: none; color: var(--gs-fg-faint); }
1204
- .refButton { max-width: 260px; justify-content: space-between; }
1205
- .refValue { min-width: 0; overflow: hidden; }
1206
- .refCaret { flex: none; color: var(--gs-fg-faint); font-size: 9px; }
1207
- .refPop {
1208
- position: absolute; top: calc(100% + 4px); left: 0; z-index: 10;
1209
- display: flex; flex-direction: column;
1210
- width: 300px; max-width: 80vw;
1211
- border: 1px solid var(--gs-border); border-radius: var(--gs-r-control);
1212
- background: var(--gs-panel);
1213
- box-shadow: 0 12px 32px var(--gs-shadow);
1214
- overflow: hidden;
1215
- }
1216
- /* A short fixed menu rather than a branch list: no search box, no footer, and
1217
- sized to its own labels instead of the 300px a few hundred refs need. Anchored
1218
- right because the pickers that use it sit in a bar's right-hand cluster, where
1219
- a left-anchored popover would open off the drawer. */
1220
- .menuPop {
1221
- width: auto; min-width: 100%;
1222
- left: auto; right: 0;
1223
- padding: 4px;
1224
- }
1225
- .refSearch {
1226
- flex: none;
1227
- padding: 7px 10px;
1228
- border: 0; border-bottom: 1px solid var(--gs-border-soft);
1229
- background: var(--gs-bg); color: var(--gs-fg);
1230
- font-family: inherit; font-size: var(--gs-t-dense);
1231
- outline: none;
1232
- }
1233
- .refSearch::placeholder { color: var(--gs-fg-faint); }
1234
- /* Bounded so the popover never runs off the drawer, whatever the branch count. */
1235
- .refList { flex: 1; max-height: 280px; overflow-y: auto; padding: 4px; }
1236
- .refGroup {
1237
- padding: 6px 8px 3px;
1238
- color: var(--gs-fg-faint);
1239
- font-size: var(--gs-t-meta); font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase;
1240
- }
1241
- .refRow {
1242
- display: flex; align-items: center; gap: 6px;
1243
- box-sizing: border-box; width: 100%;
1244
- padding: 5px 8px;
1245
- border: 0; border-radius: var(--gs-r-control);
1246
- background: transparent; color: var(--gs-fg-muted);
1247
- font-family: inherit; font-size: var(--gs-t-dense);
1248
- text-align: left; cursor: pointer;
1249
- }
1250
- .refRow:hover { background: var(--gs-raise); color: var(--gs-fg); }
1251
- /* .refRowActive: see the shared selected-state rule at the foot of the file. */
1252
- /* Keeps a branch without a worktree aligned with one that has the glyph. */
1253
- .refRowSpacer { flex: none; width: 12px; }
1254
- .refRowName { flex: 1 1 auto; min-width: 0; overflow: hidden; }
1255
- .refEmpty { padding: 14px 10px; color: var(--gs-fg-faint); font-size: var(--gs-t-dense); }
1256
- .refFoot {
1257
- flex: none;
1258
- padding: 5px 10px;
1259
- border-top: 1px solid var(--gs-border-soft);
1260
- color: var(--gs-fg-faint); font-size: var(--gs-t-meta);
1261
- }
1262
-
1263
- /* Commit log — its own full-height pane beside the file tree. Rows are stacked
1264
- so each subject gets the pane's full width instead of a 340px ellipsis. */
1265
- .commitsPane {
1266
- /* Shrinkable, with the floor as a min-width rather than a JS clamp alone: a
1267
- width dragged in one tab must not squeeze the diff to nothing when a third
1268
- pane appears in another. The floors come from the panel's own constants, so
1269
- the clamp and the stylesheet cannot drift apart. */
1270
- flex: 0 1 auto; box-sizing: border-box;
1271
- display: flex; flex-direction: column; min-height: 0;
1272
- /* Capped, so every pixel past the cap goes to the diff — the pane whose
1273
- content cannot wrap. */
1274
- width: 26%; min-width: var(--gs-min-commits); max-width: 340px;
1275
- background: var(--gs-surface);
1276
- }
1277
- /* Shared header bar for the commit and tree panes. Its own height comes from
1278
- the compact control it hosts, so a pane with tools and a pane without one
1279
- still line up across the divider. */
1280
- .paneHead {
1281
- /* Wraps rather than overflows. Dragged narrow the pane cannot hold a title,
1282
- a funnel and a search box on one line, and what it used to do was clip the
1283
- search box at the pane's edge — a control half off the screen, with no
1284
- sign that the rest of it existed. */
1285
- display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
1286
- flex: none;
1287
- min-height: 37px; box-sizing: border-box;
1288
- padding: 6px var(--gs-gutter-pane);
1289
- border-bottom: 1px solid var(--gs-border-soft);
1290
- color: var(--gs-fg-dim);
1291
- font-family: var(--dsw-font-ui, system-ui, sans-serif);
1292
- font-size: var(--gs-t-dense);
1293
- }
1294
- .paneTitle { font-weight: 600; }
1295
- /* Zero-height probe below the last row; scrolling it into view fetches the next
1296
- page. It carries no pixels of its own so it cannot disturb the list. */
1297
- .commitsSentinel { flex: none; height: 1px; }
1298
- .commitsFoot {
1299
- flex: none;
1300
- padding: 8px;
1301
- color: var(--gs-fg-faint);
1302
- font-family: var(--dsw-font-ui, system-ui, sans-serif);
1303
- font-size: var(--gs-t-meta); text-align: center;
1304
- /* Reserved whether or not there is text, so arriving at the end of the list
1305
- does not shift the rows above it. */
1306
- min-height: 16px;
1307
- }
1308
-
1309
- /* `flex: 1` with `min-height: 0` gives this scroller a DEFINITE height from the
1310
- pane. A percentage max-height cannot: its parent is sized by content, so the
1311
- percentage has nothing to resolve against and the list grows without bound —
1312
- which is what pushed the file tree off screen and the footer out of reach. */
1313
- /* No gap between lines and no left padding: the graph column runs down the left
1314
- edge and a lane has to reach the next row to stay one line. The rows' own
1315
- breathing room is inside `.commit`, which keeps its inset and rounding. */
1316
- .commits {
1317
- display: flex; flex-direction: column;
1318
- flex: 1; min-height: 0;
1319
- overflow-y: auto;
1320
- padding: 4px 6px 4px 0;
1321
- }
1322
- /* One list line: the graph slice, then the row. Fixed height, because the SVG
1323
- is drawn at exactly this height and the lanes only join if they agree. */
1324
- .commitLine {
1325
- display: flex; align-items: stretch;
1326
- flex: none;
1327
- /* Published by the panel from `COMMIT_ROW_H`, which the lane graph also draws
1328
- each row's segment at — one source, because a row and its segment that
1329
- disagree leave the lanes short of the seam between rows. The two
1330
- arrangements want different rows, which is why this is a property and not
1331
- a number. Guarded by tests/commit-row-height.test.ts. */
1332
- height: var(--gs-commit-row);
1333
- }
1334
- .graphCell { flex: none; display: block; }
1335
- /* An inset row, like dsh's own session rows: 12px radius, filled when active. */
1336
- .commit {
1337
- display: flex; align-items: center; gap: 10px;
1338
- flex: 1 1 auto; min-width: 0; box-sizing: border-box;
1339
- height: 100%;
1340
- margin: 1px 0;
1341
- padding: 4px 8px;
1342
- border: 1px solid transparent;
1343
- border-radius: var(--gs-r-surface);
1344
- background: transparent;
1345
- font-family: inherit;
1346
- text-align: left;
1347
- cursor: pointer;
1348
- font-size: var(--gs-t-dense);
1349
- overflow: hidden;
1350
- transition: background 120ms ease, border-color 120ms ease;
1351
- }
1352
- /* Beside the diff the row turns into two lines: a pane of about 340px has no
1353
- width for the hash, the author, the date AND the subject on one, and the
1354
- subject is the part that would have given way. Above the diff it stays one
1355
- line — see the row's two shapes in `CommitRow`. */
1356
- .commitsPane[data-layout="columns"] .commit {
1357
- flex-direction: column; align-items: stretch; justify-content: center; gap: 2px;
1358
- }
1359
- /* The subject line must not absorb the row's spare height, or the two lines
1360
- stop sitting together in the middle of the row. */
1361
- .commitsPane[data-layout="columns"] .commitSubjectRow { flex: none; }
1362
- .commitTop {
1363
- display: flex; align-items: center; justify-content: space-between; gap: 8px;
1364
- min-width: 0;
1365
- }
1366
- .commit:hover { background: var(--gs-raise); }
1367
- .commitActive { border-color: var(--gs-accent-border); background: var(--gs-accent-bg); }
1368
- /* Pushed to the right end of the row, and never squeezed: the subject is what
1369
- gives way when the pane narrows, because it is the part with an ellipsis to
1370
- give. */
1371
- .commitMeta { flex: none; display: flex; align-items: baseline; gap: 8px; }
1372
- .commitHash { flex: none; color: var(--gs-info); }
1373
- .commitSubjectRow {
1374
- display: flex; align-items: baseline; gap: 4px;
1375
- flex: 1 1 auto; min-width: 0;
1376
- }
1377
- /* Shrinks, never grows. Growing was invisible while the list was a 26%%
1378
- column — the subject filled the row either way — and became a defect the
1379
- moment the list spanned the drawer: a subject that takes ALL the free space
1380
- pushes the has-a-body marker a thousand pixels right, where it reads as an
1381
- unexplained ellipsis next to the author rather than as "there is more to
1382
- this message". The row still pushes author and date to the right end;
1383
- .commitSubjectRow is what absorbs the slack. */
1384
- .commitSubject {
1385
- flex: 0 1 auto; min-width: 0;
1386
- overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
1387
- color: var(--gs-fg-muted);
1388
- }
1389
- .commitHasBody { flex: none; color: var(--gs-fg-faint); letter-spacing: 0.04em; }
1390
- /* Branch and tag names pointing at this commit. Capped and ellipsised: a release
1391
- commit can carry a dozen refs, and none of them is worth the subject's room. */
1392
- .commitRef {
1393
- flex: none;
1394
- max-width: 96px;
1395
- overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
1396
- padding: 0 5px;
1397
- border: 1px solid var(--gs-accent-border); border-radius: var(--gs-r-pill);
1398
- background: var(--gs-accent-bg);
1399
- color: var(--gs-accent);
1400
- font-size: var(--gs-t-meta); line-height: 15px;
1401
- }
1402
- .commitWhen { flex: none; color: var(--gs-fg-faint); }
1403
- /* Who wrote it — sits between hash and relative time, capped so a long name
1404
- cannot evict the time from the row. */
1405
- .commitAuthor {
1406
- flex: none; max-width: 120px;
1407
- overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
1408
- color: var(--gs-fg-dim);
1409
- }
1410
-
1411
- .body { display: flex; flex: 1; min-height: 0; z-index: 1; }
1412
- /* History stacks: the commit list over the tree-and-diff row. `.bodyRow` is
1413
- rendered on every tab, so the unstacked tabs are a row inside a row — which
1414
- costs nothing and keeps one JSX shape instead of two. */
1415
- .body[data-stacked] { flex-direction: column; }
1416
- .bodyRow { display: flex; flex: 1 1 0; min-width: 0; min-height: 0; }
1417
- /* Stacked, the list is sized by HEIGHT and spans the drawer; the width rules
1418
- it carries for the column layout would otherwise pin it to 26%. */
1419
- .body[data-stacked] .commitsPane {
1420
- width: auto; max-width: none; min-width: 0;
1421
- height: 34%; flex: none;
1422
- /* The drag's clamp, restated where a RESIZE can also reach it. An inline
1423
- `height` in pixels survives the window getting shorter, and then the list
1424
- is taller than the body: the lower half goes to zero and the handle ends
1425
- up below the bottom edge with nothing to grab. Percentages re-resolve on
1426
- every layout, which is exactly what the stored pixel height cannot do.
1427
- Both numbers come from the panel's own constants, so there is still one
1428
- home for them. `max()` keeps the range from inverting in a drawer too
1429
- short to hold both floors — the list then simply takes its minimum. */
1430
- min-height: var(--gs-min-commits-tall);
1431
- max-height: max(var(--gs-min-commits-tall), calc(100% - var(--gs-min-stacked-lower)));
1432
- }
1433
-
1434
- /* File tree column. A peer of the commit pane and the diff, never their parent:
1435
- each of the three scrolls on its own and keeps its full height. */
1436
- .treeCol {
1437
- flex: 0 1 auto; box-sizing: border-box;
1438
- display: flex; flex-direction: column; min-height: 0;
1439
- width: 28%; min-width: var(--gs-min-tree); max-width: 400px;
1440
- background: var(--gs-surface);
1441
- }
1442
-
1443
- /* file tree */
1444
- .treeWrap {
1445
- flex: 1; box-sizing: border-box;
1446
- display: flex; flex-direction: column;
1447
- min-width: 0; min-height: 0;
1448
- }
1449
- /* Two items, not five: the count and one group of buttons. The pane drags down
1450
- to 170px, so the row does have to wrap — but it wraps between the count and
1451
- the group, never inside it. Wrapping the buttons individually is what left a
1452
- lone "Collapse all" stranded on a second row under a right-aligned
1453
- "Expand all". `space-between` then aligns whichever way the row broke. */
1454
- .treeTools {
1455
- display: flex; align-items: center; flex-wrap: wrap; gap: 6px;
1456
- justify-content: space-between;
1457
- flex: none;
1458
- min-height: 37px; box-sizing: border-box;
1459
- padding: 6px var(--gs-gutter-pane);
1460
- border-bottom: 1px solid var(--gs-border-soft);
1461
- }
1462
- /* Wraps only when the group alone cannot fit the pane — five controls do not
1463
- go into 170px by any arrangement, and a clipped button is worse than a
1464
- second row. Above ~250px this never fires. */
1465
- .treeActions { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }
1466
- /* The root tick sits in the same column the rows read down, so it is pulled out
1467
- of the toolbar's gutter to the pane edge rather than aligned to the label. */
1468
- .treeLead { display: flex; align-items: center; gap: 4px; min-width: 0; }
1469
- .treeLabel {
1470
- flex: none;
1471
- white-space: nowrap;
1472
- color: var(--gs-fg-dim); font-size: var(--gs-t-dense);
1473
- }
1474
- /* The file filter, revealed under the tools rather than squeezed into them:
1475
- the pane drags down to 170px, where the tick, the count and two icon buttons
1476
- already own the row. It costs a row only while it is being used. */
1477
- .treeFilter {
1478
- position: relative;
1479
- flex: none;
1480
- display: flex; align-items: center;
1481
- padding: 6px var(--gs-gutter-pane);
1482
- border-bottom: 1px solid var(--gs-border-soft);
1483
- }
1484
- .treeFilterInput {
1485
- flex: 1 1 auto; min-width: 0;
1486
- height: var(--gs-h-compact); box-sizing: border-box;
1487
- padding: 0 24px 0 8px;
1488
- border: 1px solid var(--gs-border-soft); border-radius: var(--gs-r-control);
1489
- background: var(--gs-bg); color: var(--gs-fg);
1490
- font-family: inherit; font-size: var(--gs-t-dense);
1491
- outline: none;
1492
- }
1493
- .treeFilterInput:focus { border-color: var(--gs-accent); }
1494
- .treeFilterInput::placeholder { color: var(--gs-fg-faint); }
1495
- /* Inside the field's right padding, so clearing does not move the field or
1496
- change its width as the button comes and goes. */
1497
- .treeFilterClear {
1498
- position: absolute; right: calc(var(--gs-gutter-pane) + 6px);
1499
- display: flex; align-items: center; justify-content: center;
1500
- width: 16px; height: 16px;
1501
- padding: 0;
1502
- border: 0; border-radius: var(--gs-r-pill);
1503
- background: transparent; color: var(--gs-fg-faint);
1504
- font-size: var(--gs-t-ui); line-height: 1;
1505
- cursor: pointer;
1506
- }
1507
- .treeFilterClear:hover { color: var(--gs-fg); }
1508
- .tree {
1509
- flex: 1; min-height: 0;
1510
- margin: 0; padding: 6px 0 16px;
1511
- overflow: auto; list-style: none;
1512
- }
1513
- /* The list's stand-in while the view's file list is in flight: centred and
1514
- quiet, never a claim about how many changes exist. */
1515
- .treeEmpty {
1516
- flex: 1; min-height: 0;
1517
- display: flex; align-items: center; justify-content: center;
1518
- color: var(--gs-fg-faint); font-size: var(--gs-t-ui);
1519
- }
1520
- /* Nested lists are NOT .tree: only the root list scrolls, otherwise every level
1521
- would grow its own scrollbar. */
1522
- .treeSub {
1523
- position: relative;
1524
- margin: 0; padding: 0;
1525
- list-style: none;
1526
- }
1527
- /* One hairline per nesting level at the parent's chevron column, so a deep tree
1528
- reads as a tree rather than a ragged left edge. The offset comes from the row
1529
- that owns the level (see TREE_RAIL_OFFSET). */
1530
- .treeSub::before {
1531
- content: '';
1532
- position: absolute;
1533
- top: 0; bottom: 0; left: var(--gs-rail, 0);
1534
- width: 1px;
1535
- background: var(--gs-border-soft);
1536
- }
1537
- .treeDirLi { position: relative; }
1538
- .treeDir {
1539
- display: flex; align-items: center; gap: 6px;
1540
- box-sizing: border-box; width: 100%;
1541
- padding: 4px 12px 4px 8px;
1542
- border: 0;
1543
- background: transparent;
1544
- color: var(--gs-fg-dim);
1545
- font-family: inherit;
1546
- font-size: var(--gs-t-dense); font-weight: 600;
1547
- text-align: left;
1548
- cursor: pointer;
1549
- }
1550
- .treeDir:hover { background: var(--gs-panel); color: var(--gs-fg-muted); }
1551
- .treeDirActive { color: var(--gs-fg-muted); }
1552
- .chevron {
1553
- flex: none;
1554
- display: inline-block;
1555
- width: 10px;
1556
- color: var(--gs-fg-fainter);
1557
- font-size: var(--gs-t-meta);
1558
- transition: transform 120ms ease;
1559
- }
1560
- .chevronOpen { transform: rotate(90deg); }
1561
- .treeDirName {
1562
- flex: 1; min-width: 0;
1563
- overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
1564
- }
1565
- .treeDirCount {
1566
- flex: none;
1567
- padding: 0 6px;
1568
- border-radius: 999px;
1569
- background: var(--gs-neutral-bg);
1570
- color: var(--gs-fg-dim);
1571
- font-size: var(--gs-t-meta); font-weight: 600; line-height: 16px;
1572
- }
1573
- .treeDirCounts { flex: none; font-size: var(--gs-t-meta); font-variant-numeric: tabular-nums; }
1574
- .file {
1575
- display: flex; align-items: center; gap: 8px;
1576
- box-sizing: border-box; width: 100%;
1577
- padding: 5px 14px;
1578
- border: 0;
1579
- border-left: 2px solid transparent;
1580
- background: transparent;
1581
- color: var(--gs-fg-muted);
1582
- font-family: inherit;
1583
- font-size: var(--gs-t-dense);
1584
- text-align: left;
1585
- cursor: pointer;
1586
- }
1587
- .file:hover { background: var(--gs-panel); }
1588
- .fileActive {
1589
- background: var(--gs-panel);
1590
- border-left-color: var(--gs-accent);
1591
- color: var(--gs-fg);
1592
- }
1593
-
1594
- .fileStatus {
1595
- flex: none;
1596
- display: inline-flex; align-items: center; justify-content: center;
1597
- width: 18px; height: 18px;
1598
- border-radius: 5px;
1599
- font-weight: 700; font-size: var(--gs-t-meta); line-height: 1;
1600
- }
1601
- .stAdded, .stUntracked { color: var(--gs-add); background: var(--gs-add-bg); }
1602
- .stDeleted { color: var(--gs-del); background: var(--gs-del-bg); }
1603
- .stModified { color: var(--gs-warn); background: var(--gs-warn-bg); }
1604
- .stRenamed { color: var(--gs-info); background: var(--gs-info-bg); }
1605
-
1606
- .filePath { flex: 1; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
1607
- .fileBinary {
1608
- flex: none;
1609
- padding: 0 5px;
1610
- border-radius: 4px;
1611
- background: var(--gs-neutral-bg);
1612
- color: var(--gs-fg-dim);
1613
- font-size: 9px; font-weight: 700; letter-spacing: 0.06em;
1614
- }
1615
- .fileCounts { flex: none; font-size: var(--gs-t-meta); font-variant-numeric: tabular-nums; }
1616
- .fileCountAdd { color: var(--gs-add); }
1617
- .fileCountDel { color: var(--gs-del); }
1618
-
1619
- /* diff pane */
1620
- /* A column, so a header can sit above the scrolled diff instead of inside it.
1621
- `overflow: auto` stays as the outer guard — the rename line and the
1622
- side-by-side pane are also children here, and one of them growing must
1623
- scroll rather than be clipped. */
1624
- .diffPane {
1625
- flex: 1 1 0; min-width: var(--gs-min-diff);
1626
- display: flex; flex-direction: column;
1627
- overflow: auto; background: var(--gs-surface);
1628
- }
1629
- /* The unified view: its own header and its own scroller, filling whatever the
1630
- pane has left after a rename line. */
1631
- .diffWrap { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; width: 100%; }
1632
- .diffScroll { flex: 1 1 0; min-height: 0; overflow: auto; outline: none; }
1633
- /* The walk's controls. Right-aligned and quiet: they belong to the diff, and
1634
- the diff is what the reader came for. Same shape as `.sideNav` in the
1635
- side-by-side pane, because it is the same control. */
1636
- .diffNav {
1637
- flex: none;
1638
- display: flex; justify-content: flex-end; align-items: center; gap: 4px;
1639
- padding: 3px 12px 3px 0;
1640
- border-bottom: 1px solid var(--gs-border-soft);
1641
- }
1642
- .diffNav .blockBtn { padding-inline: 6px; line-height: 0; }
1643
- /* The pre is as wide as its longest line (`max-content`) and at least the pane
1644
- (`min-width: 100%`). Each row then stretches to that width, so add/delete
1645
- tints survive horizontal scroll instead of stopping at the viewport edge. */
1646
- .diffPre {
1647
- display: flex; flex-direction: column;
1648
- margin: 0; padding: 8px 0 16px;
1649
- min-width: 100%; width: max-content;
1650
- /* GitHub's blob stack — Cascadia ligatures and a 16px leading were why this
1651
- read as a terminal dump instead of a pull-request diff. */
1652
- font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
1653
- font-size: var(--gs-t-dense); line-height: 20px; font-weight: 400;
1654
- font-variant-ligatures: none;
1655
- font-feature-settings: 'liga' 0, 'calt' 0;
1656
- tab-size: 4;
1657
- }
1658
- .renameLine {
1659
- flex: none;
1660
- padding: 8px 16px;
1661
- border-bottom: 1px solid var(--gs-border-soft);
1662
- color: var(--gs-info);
1663
- font-size: var(--gs-t-dense);
1664
- }
1665
- /* The old path is literal content, not prose: give it the body colour so it
1666
- reads louder than the sentence around it. */
1667
- .renameLine code { color: var(--gs-fg); }
1668
-
1669
- /* Each line: [old#][new#][sign][code]. Stretch, don't baseline — empty line-number
1670
- cells plus nested syntax spans used to inflate the row into prose leading. */
1671
- .line {
1672
- display: flex; align-items: stretch;
1673
- width: max-content; min-width: 100%;
1674
- min-height: 20px; line-height: 20px;
1675
- white-space: nowrap;
1676
- }
1677
- .lnOld, .lnNew {
1678
- flex: none;
1679
- box-sizing: border-box;
1680
- width: 3.2em;
1681
- padding: 0 8px;
1682
- text-align: right;
1683
- color: var(--gs-fg-fainter);
1684
- user-select: none;
1685
- font-variant-numeric: tabular-nums;
1686
- }
1687
- .line:hover .lnOld, .line:hover .lnNew { color: var(--gs-fg-dim); }
1688
- /* No rule down the right edge: GitHub carries the +/- as the first glyph of the
1689
- code cell and draws nothing between the columns. The rule that used to be
1690
- here cut every tinted row in half, which is most of why a full-file addition
1691
- read as two stacked green bands rather than one block of new code. */
1692
- .gutter {
1693
- flex: none;
1694
- width: 1.4em;
1695
- text-align: center;
1696
- user-select: none;
1697
- }
1698
- .code { flex: 1 0 auto; white-space: pre; padding: 0 16px 0 10px; }
1699
-
1700
- .lineAdd { background: var(--gs-add-line); }
1701
- .lineDel { background: var(--gs-del-line); }
1702
- .lineAdd .lnOld, .lineAdd .lnNew, .lineAdd .gutter { background: var(--gs-add-num, var(--gs-add-line)); }
1703
- .lineDel .lnOld, .lineDel .lnNew, .lineDel .gutter { background: var(--gs-del-num, var(--gs-del-line)); }
1704
- .lineContext { color: var(--gs-fg-muted); }
1705
- /* GitHub's hunk header is quiet: a faint accent wash on the row, muted grey
1706
- text, and the saturated blue confined to the number cells. It used to run the
1707
- accent colour across the whole line, which made every hunk boundary shout
1708
- louder than the code it introduces. */
1709
- .lineHunk { background: var(--gs-hunk); }
1710
- .lineHunk .code { color: var(--gs-fg-dim); }
1711
- .lineHunk .lnOld, .lineHunk .lnNew, .lineHunk .gutter { background: var(--gs-hunk-num, var(--gs-hunk)); }
1712
- .signAdd { color: var(--gs-add); }
1713
- .signDel { color: var(--gs-del); }
1714
-
1715
- .wordAdd { background: var(--gs-add-word); border-radius: 2px; }
1716
- .wordDel { background: var(--gs-del-word); border-radius: 2px; }
1717
-
1718
- /* Side-by-side diff pane. One grid holds EVERY row's four cells — a per-row
1719
- grid would let the column boundary drift with each row's content, and a
1720
- fixed split would clip long lines. Equal 1fr code tracks keep the two
1721
- columns identically wide however wide the widest line is, and the whole pane
1722
- (this view's single shared vertical scroll) carries the horizontal one. */
1723
- /* The pane is a HEADER plus a scroller, not one wide column: the tab row
1724
- carries Save/Revert, and a row sized to the widest line of code pushes them
1725
- off the right edge of a long file — reachable only by scrolling sideways,
1726
- which is not where anyone looks for a toolbar. Only `.sideScroll` scrolls;
1727
- the tabs and the notices above it stay put on both axes. */
1728
- .sidePane { display: flex; flex-direction: column; width: 100%; min-width: 0; height: 100%; }
1729
- /* Vertical scrolling belongs to the pane; horizontal belongs to each column,
1730
- which is what lets the divider mean something. A single scroller for both
1731
- axes would tie the two sides' horizontal position together and size the
1732
- whole grid to the widest line in the file. */
1733
- .sideScroll { flex: 1 1 0; min-height: 0; overflow-y: auto; overflow-x: hidden; }
1734
- .sideCols { display: flex; align-items: stretch; min-height: 100%; }
1735
- /* `min-width: 0` is what lets a flex column be narrower than its content —
1736
- without it the column refuses to shrink and the divider stops moving. */
1737
- .sideCol { flex: 0 0 auto; min-width: 0; overflow-x: auto; }
1738
- .sideColRight { flex: 1 1 0; }
1739
- .sideColGrid {
1740
- display: grid;
1741
- grid-template-columns: 3.2em 1fr;
1742
- align-content: start;
1743
- padding: 8px 0 16px;
1744
- min-width: 100%; width: max-content;
1745
- font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
1746
- font-size: var(--gs-t-dense); line-height: 20px; font-weight: 400;
1747
- font-variant-ligatures: none;
1748
- font-feature-settings: 'liga' 0, 'calt' 0;
1749
- tab-size: 4;
1750
- }
1751
- /* The blame gutter, on the working-tree column only. Fixed width so the code
1752
- beside it does not shift line to line, and clipped rather than wrapped: a
1753
- gutter row that grew to two lines would break the alignment the whole
1754
- side-by-side view rests on. */
1755
- .sideColGridBlame { grid-template-columns: 13em 3.2em 1fr; }
1756
- .blameCell {
1757
- box-sizing: border-box;
1758
- padding: 0 8px 0 10px;
1759
- border-right: 1px solid var(--gs-border-soft);
1760
- color: var(--gs-fg-faint);
1761
- font-size: var(--gs-t-meta);
1762
- white-space: pre; overflow: hidden; text-overflow: ellipsis;
1763
- user-select: none;
1764
- }
1765
- .sideTabs {
1766
- z-index: 5;
1767
- display: flex; gap: 16px;
1768
- flex: none;
1769
- padding: 6px 16px 0;
1770
- border-bottom: 1px solid var(--gs-border-soft);
1771
- background: var(--gs-surface-2);
1772
- }
1773
- .sideTab {
1774
- position: relative;
1775
- padding: 2px 0 5px;
1776
- border: none;
1777
- background: transparent;
1778
- font-family: inherit;
1779
- font-size: var(--gs-t-meta); line-height: 16px; font-weight: 500;
1780
- color: var(--gs-fg-dim);
1781
- cursor: pointer;
1782
- }
1783
- .sideTab::after {
1784
- content: '';
1785
- position: absolute; right: 0; bottom: 1px; left: 0;
1786
- height: 2px; border-radius: 2px;
1787
- background: transparent;
1788
- }
1789
- .sideTabActive { color: var(--gs-accent); }
1790
- .sideTabActive::after { background: var(--gs-accent); }
1791
- .sideNotice {
1792
- flex: none;
1793
- padding: 8px 16px;
1794
- border-bottom: 1px solid var(--gs-border-soft);
1795
- color: var(--gs-info);
1796
- font-size: var(--gs-t-dense);
1797
- }
1798
- .sideNum {
1799
- box-sizing: border-box;
1800
- padding: 0 8px;
1801
- text-align: right;
1802
- color: var(--gs-fg-fainter);
1803
- user-select: none;
1804
- font-variant-numeric: tabular-nums;
1805
- }
1806
- .sideNumAdd { background: var(--gs-add-num, var(--gs-add-line)); }
1807
- .sideNumDel { background: var(--gs-del-num, var(--gs-del-line)); }
1808
- .sideCode { white-space: pre; padding: 0 16px 0 10px; min-height: 20px; }
1809
- /* Stands in for the rows outside the window, so the scrollbar is the length of
1810
- the FILE rather than of whatever is currently rendered. Spans every column,
1811
- including the blame gutter's. */
1812
- .sideSpacer { grid-column: 1 / -1; }
1813
- /* The same, for the unified view's flex column. `flex: none` so it is exactly
1814
- the height it claims rather than one stretched by its siblings. */
1815
- .diffSpacer { flex: none; }
1816
- .sideCodeSame { color: var(--gs-fg-muted); }
1817
- .sideCodeAdd { background: var(--gs-add-line); }
1818
- .sideCodeDel { background: var(--gs-del-line); }
1819
- /* The block affordance: a changed row's cells carry the block tint above, and
1820
- the hovered block's every cell — code and gutter — takes the hot outline,
1821
- so the reader sees the unit the buttons act on rather than the one cell the
1822
- pointer happens to be over. `.sideCellBlock` also positions the action bar,
1823
- which a cell of the block's first row carries. */
1824
- .sideCellBlock { position: relative; }
1825
- .sideBlockHot { outline: 1px solid var(--gs-accent); outline-offset: -1px; }
1826
-
1827
- /* The hovered block's actions, floating on the row above the block. It lives
1828
- INSIDE the block's first code cell, so it travels with the column and the
1829
- scroll, and it only ever covers context — never the change it acts on. The
1830
- surface and shadow are what keep it readable over code. */
1831
- .blockBar {
1832
- position: absolute; top: -21px; left: 0; z-index: 4;
1833
- display: flex; gap: 4px;
1834
- padding: 2px;
1835
- border: 1px solid var(--gs-border);
1836
- border-radius: var(--gs-r-control);
1837
- background: var(--gs-surface-2, var(--gs-bg));
1838
- box-shadow: 0 4px 14px var(--gs-shadow);
1839
- white-space: nowrap;
1840
- }
1841
- .blockBtn {
1842
- padding: 0 8px;
1843
- border: 1px solid var(--gs-border); border-radius: var(--gs-r-pill);
1844
- background: var(--gs-bg); color: var(--gs-fg-dim);
1845
- font-family: inherit; font-size: var(--gs-t-meta); line-height: 16px;
1846
- cursor: pointer;
1847
- }
1848
- .blockBtn:hover:not(:disabled) { color: var(--gs-fg); border-color: var(--gs-accent); }
1849
- .blockBtn:disabled { opacity: 0.45; cursor: default; }
1850
- /* The roll-back button stays red at rest, not only on hover: it is the one
1851
- action in the bar that cannot be undone, and arriving under the pointer is
1852
- not what should make that visible. */
1853
- .blockBtnDanger { color: var(--gs-del); border-color: var(--gs-del-bg, var(--gs-border)); }
1854
- .blockBtnDanger:hover:not(:disabled) { color: var(--gs-del); border-color: var(--gs-del); }
1855
-
1856
- /* The editable right column. The working-tree column is readable but visibly
1857
- a text surface before editing is armed; the tab row carries the arm/save
1858
- cluster at its right end. */
1859
- .sideArmable { cursor: text; }
1860
-
1861
- /* An editable buffer says so — the one thing a rendered code pane cannot say
1862
- by itself.
1863
-
1864
- Nothing about monospaced text on a background looks like a field. In the
1865
- Files tab the editor is live the moment the file opens, so without a mark
1866
- the reader finds out whether they may type by typing — and finds out they
1867
- may not the same way, on a pane that silently swallows it. The rule runs
1868
- the full height of the editor's leading edge: dim while nothing is focused
1869
- (present, not loud — it sits beside code someone came to read), full accent
1870
- once the caret is actually inside, which doubles as "this pane has the
1871
- keyboard".
1872
-
1873
- Driven by an attribute the component sets from the same boolean that
1874
- configures `EditorState.readOnly`, so the rule cannot appear on a pane that
1875
- would refuse the keystroke. `pointer-events: none` because the bar overlays
1876
- the gutter's first two pixels, and a click there must still reach the
1877
- view. */
1878
- .cmHost { position: relative; }
1879
- .cmHost[data-editable]::before {
1880
- content: '';
1881
- position: absolute;
1882
- top: 0; bottom: 0; left: 0;
1883
- width: 2px;
1884
- border-radius: 1px;
1885
- background: var(--gs-accent);
1886
- opacity: 0.32;
1887
- transition: opacity 140ms ease;
1888
- pointer-events: none;
1889
- }
1890
- .cmHost[data-editable]:focus-within::before { opacity: 1; }
1891
- /* The change nav takes the free space, so it and `.sideActions` after it sit
1892
- together at the right end. Only the FIRST auto margin absorbs anything —
1893
- the one on `.sideActions` then has nothing left to take, which is what
1894
- keeps the two groups adjacent rather than pushed to opposite ends. */
1895
- .sideNav { margin-left: auto; display: flex; gap: 4px; align-items: center; padding: 2px 0 5px; }
1896
- .sideNav .blockBtn { padding-inline: 6px; line-height: 0; }
1897
- .sideNavCount { color: var(--gs-fg-faint); font-size: var(--gs-t-meta); white-space: nowrap; }
1898
- .sideActions { margin-left: auto; display: flex; gap: 4px; align-items: flex-start; padding: 2px 0 5px; }
1899
- /* Save gains the accent only once there is something to save: the button's
1900
- whole enabled state is the dirty flag, and colour says it sooner than the
1901
- pointer does. */
1902
- .sideSaveReady { color: var(--gs-accent); border-color: var(--gs-accent); }
1903
-
1904
- /* The reload-or-overwrite banner (§4) and the failed-save banner: a warning
1905
- strip under the tabs carrying its own actions, so the decision it asks for
1906
- is one click away rather than a sentence away. */
1907
- .sideBanner {
1908
- flex: none;
1909
- display: flex; flex-direction: column; gap: 6px;
1910
- padding: 8px 16px;
1911
- border-bottom: 1px solid var(--gs-border-soft);
1912
- background: var(--gs-del-bg, transparent);
1913
- color: var(--gs-del);
1914
- font-size: var(--gs-t-dense);
1915
- }
1916
- .sideBannerTitle { font-weight: 600; }
1917
- .sideBannerActs { display: flex; gap: 4px; }
1918
-
1919
- /* The editor grid item that positions the textarea. It spans exactly the
1920
- buffer's rows of the fourth column; the textarea fills it absolutely. */
1921
- .sideEditCell { position: relative; }
1922
-
1923
-
1924
- /* No syntax-tint classes here: Shiki resolves a real TextMate theme and emits a
1925
- hex per token, so the panel writes colour inline. A four-class approximation
1926
- painted identifiers, types and punctuation all one colour, which is what made
1927
- the diff look unhighlighted next to GitHub's. */
1928
-
1929
- .empty { padding: 32px 24px; color: var(--gs-fg-faint); font-size: var(--gs-t-ui); }
1930
-
1931
- /* ============================ write operations ============================ */
1932
-
1933
- /* Sync bar: fetch / pull / push, with the divergence they act on. A chrome bar
1934
- like the others, so it takes the drawer gutter and not a gutter of its own. */
1935
- .syncBar {
1936
- display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
1937
- flex: none;
1938
- padding: 8px var(--gs-gutter);
1939
- border-bottom: 1px solid var(--gs-border-soft);
1940
- background: var(--gs-surface-2);
1941
- font-size: var(--gs-t-dense);
1942
- color: var(--gs-fg-dim);
1943
- }
1944
- .syncUpstream {
1945
- max-width: 260px;
1946
- overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
1947
- color: var(--gs-fg-muted);
1948
- font-variant-numeric: tabular-nums;
1949
- }
1950
- .syncLevel { color: var(--gs-fg-faint); }
1951
- .syncSpacer { flex: 1 1 auto; }
1952
- .btn:disabled, .treeIcon:disabled, .miniBtn:disabled, .refButton:disabled, .funnelButton:disabled {
1953
- opacity: 0.45;
1954
- cursor: default;
1955
- }
1956
- /* A control disabled ONLY by an operation too young to report keeps its full
1957
- strength. Every tick stages through git, and fading the whole sync row for
1958
- the ~150ms that takes was a blink, not feedback — nobody can act inside it.
1959
- The click is still refused from the first frame; see `op-feedback.ts`. */
1960
- .btn[data-quiet]:disabled, .refButton[data-quiet]:disabled { opacity: 1; }
1961
-
1962
- /* Outcome of the last operation. Pre-wrap because git's own message rides along
1963
- under the explanation, and it arrives with its newlines. */
1964
- .opBanner {
1965
- flex: none;
1966
- padding: 8px var(--gs-gutter);
1967
- border-bottom: 1px solid var(--gs-border-soft);
1968
- font-size: var(--gs-t-dense); line-height: 18px;
1969
- white-space: pre-wrap; overflow-wrap: anywhere;
1970
- user-select: text;
1971
- }
1972
- .opBannerOk { background: var(--gs-add-bg); color: var(--gs-add); }
1973
- .opBannerBad { background: var(--gs-del-bg); color: var(--gs-del); }
1974
-
1975
- /* A file row and its stage toggle. The row keeps its own indent padding, so the
1976
- toggle trails it rather than displacing the tree's left edge. */
1977
- .fileLi { display: flex; align-items: stretch; }
1978
- .fileLi .file { flex: 1 1 auto; min-width: 0; }
1979
- /* Roll back, in the same 22px slot the tick occupies on the other side — the
1980
- row already has two gutters, and a third would eat the name.
1981
- It holds its width at all times and only fades in on hover or keyboard
1982
- focus: revealing it by `display` would re-flow the name column under the
1983
- pointer, and a destructive control that shifts the row as it appears is one
1984
- the pointer arrives at by accident. */
1985
- .fileDiscard {
1986
- flex: none;
1987
- display: flex; align-items: center; justify-content: center;
1988
- width: 22px;
1989
- padding: 0;
1990
- border: 0;
1991
- background: transparent;
1992
- color: var(--gs-fg-faint);
1993
- opacity: 0;
1994
- cursor: pointer;
1995
- transition: opacity 120ms ease, color 120ms ease;
1996
- }
1997
- .fileLi:hover .fileDiscard, .fileDiscard:focus-visible { opacity: 1; }
1998
- /* Red on hover, and only on hover. Standing red would make every row of a
1999
- working tree look like a warning; arriving under the pointer is where the
2000
- colour has something to say. */
2001
- .fileDiscard:hover { color: var(--gs-del); }
2002
- .fileDiscard:focus-visible { outline: 2px solid var(--gs-accent); outline-offset: -2px; border-radius: var(--gs-r-control); }
2003
- /* Directory rows need the same two-slot row as files, but their subtree list
2004
- has to stay a child of the <li> — hence a wrapper around just the row. */
2005
- .treeRow { display: flex; align-items: stretch; }
2006
- .treeRow .treeDir { flex: 1 1 auto; min-width: 0; }
2007
-
2008
- /* ---- the commit tick ----
2009
- *
2010
- * The drawer presents a commit the way IDEA does: a set of ticks, not a staging
2011
- * area. A tick is `git add` the moment it is clicked, so the tree and
2012
- * `git status` can never disagree.
2013
- *
2014
- * The column does NOT indent with the tree. Depth already costs room in a pane
2015
- * that drags to 170px, and a straight column down the left edge is what makes
2016
- * "what am I about to commit" readable in one glance — which is why the ticks
2017
- * exist. Its width is TREE_CHECK_W; the row indents start after it. */
2018
- .checkBox {
2019
- flex: none;
2020
- display: flex; align-items: center; justify-content: center;
2021
- width: 22px;
2022
- padding: 0;
2023
- border: 0;
2024
- background: transparent;
2025
- cursor: pointer;
2026
- }
2027
- .checkMark {
2028
- display: flex; align-items: center; justify-content: center;
2029
- box-sizing: border-box;
2030
- width: 14px; height: 14px;
2031
- border: 1px solid var(--gs-fg-faint); border-radius: 3px;
2032
- background: var(--gs-bg);
2033
- color: transparent;
2034
- font-size: 10px; font-weight: 700; line-height: 1;
2035
- transition: background 120ms ease, border-color 120ms ease;
2036
- }
2037
- .checkBox:hover .checkMark { border-color: var(--gs-accent); }
2038
- .checkBox:focus-visible { outline: none; }
2039
- .checkBox:focus-visible .checkMark { outline: 2px solid var(--gs-accent); outline-offset: 1px; }
2040
- /* Ticked and half-ticked share a filled box: both mean "some of this is going
2041
- into the commit", and the glyph inside says which. */
2042
- .checkMarkOn, .checkMarkPartial {
2043
- border-color: var(--gs-accent);
2044
- background: var(--gs-accent);
2045
- color: var(--gs-on-accent);
2046
- }
2047
-
2048
- /* Commit box: pinned under the tree, so the message stays reachable however far
2049
- the file list scrolls. */
2050
- .commitBox {
2051
- flex: none;
2052
- display: flex; flex-direction: column; gap: 6px;
2053
- padding: 8px var(--gs-gutter-pane) 10px;
2054
- border-top: 1px solid var(--gs-border);
2055
- background: var(--gs-surface-2);
2056
- }
2057
- .commitMessage {
2058
- width: 100%; box-sizing: border-box;
2059
- min-height: 46px; max-height: 160px;
2060
- padding: 6px 8px;
2061
- border: 1px solid var(--gs-border); border-radius: var(--gs-r-control);
2062
- background: var(--gs-bg); color: var(--gs-fg);
2063
- font-family: inherit; font-size: var(--gs-t-dense); line-height: 18px;
2064
- resize: vertical;
2065
- outline: none;
2066
- }
2067
- .commitMessage:focus { border-color: var(--gs-accent-border); }
2068
- .commitMessage::placeholder { color: var(--gs-fg-faint); }
2069
- .commitRow { display: flex; align-items: center; gap: 8px; }
2070
- .commitAmend {
2071
- display: inline-flex; align-items: center; gap: 4px;
2072
- color: var(--gs-fg-dim); font-size: var(--gs-t-meta);
2073
- cursor: pointer;
2074
- }
2075
- .commitAmend input { accent-color: var(--gs-accent); cursor: pointer; }
2076
- .commitStaged { margin-left: auto; color: var(--gs-fg-faint); font-size: var(--gs-t-meta); }
2077
- /* The commit box's one standing instruction: tick to stage, then commit. A
2078
- constant quiet line at the top — the blocker banner that appeared only when
2079
- the index was empty read as an error, and arrived after the confusion it
2080
- explains. */
2081
- .commitLead { margin: 0; color: var(--gs-fg-faint); font-size: var(--gs-t-meta); line-height: 15px; }
2082
-
2083
- /* Commit is the one PRIMARY action in the drawer, and the only control that
2084
- gets a solid fill. As a quiet outlined button it sat at the same weight as
2085
- Expand all and disappeared into the row — the button that writes history
2086
- should not look like the button that folds a directory. */
2087
- .commitBtn {
2088
- flex: none;
2089
- box-sizing: border-box;
2090
- display: inline-flex; align-items: center; justify-content: center;
2091
- min-width: 92px;
2092
- height: var(--gs-h-control);
2093
- padding: 0 16px;
2094
- border: 1px solid transparent; border-radius: var(--gs-r-control);
2095
- background: var(--gs-accent); color: var(--gs-on-accent);
2096
- font-family: inherit; font-size: var(--gs-t-ui); font-weight: 600; line-height: 1;
2097
- white-space: nowrap;
2098
- cursor: pointer;
2099
- transition: filter 120ms ease, opacity 120ms ease;
2100
- }
2101
- .commitBtn:hover:not(:disabled) { filter: brightness(1.1); }
2102
- .commitBtn:focus-visible { outline: 2px solid var(--gs-accent); outline-offset: 2px; }
2103
- /* Disabled keeps the shape and loses the weight: the button stays findable, so
2104
- "nothing is staged yet" reads as a state rather than as a missing feature. */
2105
- .commitBtn:disabled {
2106
- background: var(--gs-raise); color: var(--gs-fg-faint);
2107
- border-color: var(--gs-border);
2108
- cursor: default;
2109
- }
2110
-
2111
- /* ============================ one button vocabulary ============================
2112
- *
2113
- * Every clickable control in the drawer is one of two shapes, and which one it
2114
- * gets is decided by WHERE IT LIVES, never by what it does: drawer-level chrome
2115
- * (header actions, ref chips, menu triggers) is a PILL, and anything inside a
2116
- * panel or pane is a ROUNDED RECT. They differ in shape and nothing else — same
2117
- * height, same type size, same hover, same focus ring, same transition.
2118
- *
2119
- * Grouping the selectors is what enforces that. Each class used to spell out
2120
- * its own padding, radius and font-size, and the same "small secondary button"
2121
- * had drifted to four heights and three radii across one screen.
2122
- */
2123
- .btn, .miniBtn, .treeIcon, .commitCopy, .scopeBtn, .refButton, .funnelButton, .funnelPreset, .layoutButton {
2124
- display: inline-flex; align-items: center; justify-content: center; gap: 6px;
2125
- box-sizing: border-box;
2126
- flex: none;
2127
- height: var(--gs-h-control);
2128
- padding: var(--gs-pad-control);
2129
- border: 1px solid var(--gs-border);
2130
- border-radius: var(--gs-r-control);
2131
- background: transparent;
2132
- color: var(--gs-fg-dim);
2133
- font-family: inherit;
2134
- font-size: var(--gs-t-ui);
2135
- line-height: 1;
2136
- white-space: nowrap;
2137
- cursor: pointer;
2138
- transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
2139
- }
2140
- .btn:hover, .miniBtn:hover, .treeIcon:hover, .commitCopy:hover,
2141
- .scopeBtn:hover, .refButton:hover, .funnelButton:hover, .funnelPreset:hover, .layoutButton:hover {
2142
- background: var(--gs-raise);
2143
- color: var(--gs-fg);
2144
- border-color: var(--gs-fg-fainter);
2145
- }
2146
- .btn:focus-visible, .miniBtn:focus-visible, .treeIcon:focus-visible,
2147
- .commitCopy:focus-visible, .scopeBtn:focus-visible, .refButton:focus-visible,
2148
- .funnelPreset:focus-visible, .layoutButton:focus-visible {
2149
- outline: 2px solid var(--gs-accent);
2150
- outline-offset: 1px;
2151
- }
2152
-
2153
- /* Pills are for drawer-level chrome: the header's actions, the ref chips, and
2154
- anything that opens a menu — dsh spells its own dropdown triggers
2155
- ("Code mode ⌄", "glm-5.3 ⌄") as pills, so a trigger that is not one reads as
2156
- foreign. Controls that live INSIDE a panel or pane stay 8px rects. */
2157
- .btn, .refButton { border-radius: var(--gs-r-pill); }
2158
-
2159
- /* Pane tools give their vertical space back to the diff; the date presets sit
2160
- inside a 320px popover, so they take the compact height too. */
2161
- .treeIcon, .commitCopy, .funnelPreset, .layoutButton {
2162
- height: var(--gs-h-compact);
2163
- padding: var(--gs-pad-compact);
2164
- font-size: var(--gs-t-dense);
2165
- }
2166
- /* Expand / collapse all are the tree's own chevron at its two angles: the
2167
- button shows the state it puts every row into. Icon-only because at the 170px
2168
- the pane drags down to, two more text buttons cannot share a row with the
2169
- count — and these two are the pair a tree header always carries. */
2170
- .treeIcon { width: var(--gs-h-compact); padding: 0; }
2171
- /* The arrangement switch: two square icon buttons at the end of the commit
2172
- list's head, side by side with no gap so they read as one control. Icon-only
2173
- for the same reason the tree's are — the head already carries a title, a
2174
- funnel and a search box, and at the 190px the pane drags down to there is no
2175
- room for two more words. */
2176
- /* Pushed to the far end of History's toolbar row, away from the ref picker:
2177
- one changes WHAT is listed, the other only where it is drawn. */
2178
- .layoutSwitch { display: inline-flex; flex: none; gap: 2px; margin-left: auto; }
2179
- .layoutButton { width: var(--gs-h-compact); padding: 0; }
2180
- /* Qualified, like `.treeIconOn`: the shared vocabulary sets `color` at the
2181
- same specificity a few rules above. */
2182
- .layoutButton.layoutButtonOn { color: var(--gs-accent); border-color: var(--gs-accent); }
2183
- .layoutGlyph { display: block; }
2184
- /* Qualified, like `.funnelButtonActive`: this sits inside the shared button
2185
- vocabulary, which sets `color` at the same specificity a few lines above. */
2186
- .treeIcon.treeIconOn { color: var(--gs-accent); border-color: var(--gs-accent); }
2187
- .treeIconGlyph { display: block; font-size: var(--gs-t-meta); line-height: 1; }
2188
- .treeIconDown { transform: rotate(90deg); }
2189
-
2190
- /* The scope toggle splits its row in two equal halves, so it opts out of the
2191
- vocabulary's `flex: none`. Declared here rather than beside `.scopeRow`
2192
- because it has to win against the grouped rule above by source order. */
2193
- .scopeBtn { flex: 1 1 0; }
2194
-
2195
- /* The single "this one is selected" idiom. Three different ones — accent chip,
2196
- bordered panel, left accent bar — used to mean the same thing in one drawer. */
2197
- .scopeBtnActive, .miniBtnPrimary, .segmentActive, .refRowActive, .paletteRowActive {
2198
- border-color: var(--gs-accent-border);
2199
- background: var(--gs-accent-bg);
2200
- color: var(--gs-accent);
2201
- font-weight: 600;
2202
- }
2203
-
2204
- /* Icon-only chrome: square, so four of them read as a control cluster rather
2205
- than four pills of drifting width. Height, border, radius and hover still
2206
- come from the vocabulary — only the box changes shape. */
2207
- .btnIcon { width: var(--gs-h-control); padding: 0; }
2208
- .btnIcon svg { display: block; }
2209
- /* Close is the one destructive control in the row, and every window on the
2210
- machine says so with red on hover. Quiet until then: a permanently red X in
2211
- a header would read as an error state. */
2212
- .btnClose:hover:not(:disabled) {
2213
- border-color: var(--gs-del); background: var(--gs-del-bg); color: var(--gs-del);
2214
- }
2215
-
2216
- /* Sync actions that have something to report.
2217
- *
2218
- * These three variants set COLOUR ONLY — height, padding, radius and type size
2219
- * still come from the vocabulary above, so a loud Push is the same object as a
2220
- * quiet one and not a second button that happens to look similar.
2221
- *
2222
- * Fetch deliberately has no variant. It writes nothing, so it has no state to
2223
- * announce, and leaving one of the three permanently quiet is what makes the
2224
- * other two register as signal rather than decoration. */
2225
- .btnBehind {
2226
- border-color: var(--gs-warn); background: var(--gs-warn-bg); color: var(--gs-warn);
2227
- font-weight: 600;
2228
- }
2229
- .btnBehind:hover:not(:disabled) { background: var(--gs-warn-bg); color: var(--gs-warn); border-color: var(--gs-warn); }
2230
- .btnAhead {
2231
- border-color: var(--gs-add); background: var(--gs-add-bg); color: var(--gs-add);
2232
- font-weight: 600;
2233
- }
2234
- .btnAhead:hover:not(:disabled) { background: var(--gs-add-bg); color: var(--gs-add); border-color: var(--gs-add); }
2235
- /* Publishing a branch that exists nowhere else is the one moment this bar has a
2236
- primary action, so it gets the solid fill Commit uses — and only then. */
2237
- .btnPrimary {
2238
- border-color: var(--gs-accent); background: var(--gs-accent); color: var(--gs-on-accent, #fff);
2239
- font-weight: 600;
2240
- }
2241
- .btnPrimary:hover:not(:disabled) { background: var(--gs-accent); color: var(--gs-on-accent, #fff); border-color: var(--gs-accent); }
2242
-
2243
- /* The count riding inside Pull / Push. Reads as part of the label rather than a
2244
- badge stuck on it: same colour, tabular, just set apart by a hairline. */
2245
- .btnCount {
2246
- margin-left: 1px; padding-left: 6px;
2247
- border-left: 1px solid currentColor;
2248
- font-variant-numeric: tabular-nums;
2249
- opacity: 0.85;
2250
- }
2251
-
2252
- /* Pull and its strategy are one control. The picker is Pull's argument, not a
2253
- third action, so the two are welded: shared border, one outer radius. */
2254
- .pullGroup { display: inline-flex; flex: none; align-items: center; }
2255
- .pullGroup > .refPicker > .refButton {
2256
- border-top-right-radius: 0; border-bottom-right-radius: 0;
2257
- border-right-color: transparent;
2258
- }
2259
- .pullGroup > .btn {
2260
- margin-left: -1px;
2261
- border-top-left-radius: 0; border-bottom-left-radius: 0;
2262
- }
2263
-
2264
- /* Scrollbars inside the drawer.
2265
- Listing the scrolling panes by name is what let the file tree keep the
2266
- platform's own wide light scrollbar after that pane was renamed, so this
2267
- matches every descendant instead: nothing to keep in step as panes change.
2268
- `scrollbar-*` inherit, which covers Firefox from the same declaration. */
2269
- .drawer { scrollbar-width: thin; scrollbar-color: var(--gs-border) transparent; }
2270
- .drawer ::-webkit-scrollbar { width: 10px; height: 10px; }
2271
- .drawer ::-webkit-scrollbar-track, .drawer ::-webkit-scrollbar-corner { background: transparent; }
2272
- .drawer ::-webkit-scrollbar-thumb {
2273
- /* Painted inside a transparent border so the thumb reads as a floating pill
2274
- rather than a bar welded to the pane edge. */
2275
- background: var(--gs-border);
2276
- background-clip: padding-box;
2277
- border: 3px solid transparent;
2278
- border-radius: 999px;
2279
- }
2280
- .drawer ::-webkit-scrollbar-thumb:hover { background: var(--gs-fg-faint); background-clip: padding-box; }
2281
-
2282
- /* Overflow counter for a commit carrying more refs than the row can show. */
2283
- .commitRefMore {
2284
- flex: none;
2285
- padding: 0 5px;
2286
- border-radius: var(--gs-r-pill);
2287
- background: var(--gs-neutral-bg);
2288
- color: var(--gs-fg-dim);
2289
- font-size: var(--gs-t-meta); line-height: 15px;
2290
- cursor: default;
2291
- }
2292
-
2293
- /* ---- the roll-back confirmation ----
2294
- *
2295
- * Scoped to the drawer rather than the viewport: `.drawer` is the positioned
2296
- * ancestor, so the scrim dims the thing the question is about and leaves the
2297
- * session behind it alone. It is also the only modal surface in this plugin —
2298
- * everything else the drawer does can be done again.
4
+ * The build inlines these source modules before Lightning CSS scopes class
5
+ * names. Runtime behavior therefore stays exactly one class map and one
6
+ * <style> element; the split is for ownership and review, not extra requests.
2299
7
  */
2300
- .confirmScrim {
2301
- /* `position`, `inset` and the stacking rank live with the drawer's other
2302
- child ranks — see `.drawer > .confirmScrim`. */
2303
- display: flex; align-items: center; justify-content: center;
2304
- padding: 24px;
2305
- background: var(--gs-backdrop);
2306
- animation: gsFade 120ms ease-out;
2307
- }
2308
- .confirmBox {
2309
- display: flex; flex-direction: column; gap: 10px;
2310
- box-sizing: border-box;
2311
- width: min(440px, 100%);
2312
- padding: 18px;
2313
- border: 1px solid var(--gs-border); border-radius: var(--gs-r-drawer);
2314
- background: var(--gs-bg);
2315
- box-shadow: 0 18px 48px var(--gs-shadow);
2316
- }
2317
- .confirmTitle { color: var(--gs-fg); font-size: var(--gs-t-ui); font-weight: 600; }
2318
- /* The body carries a path, which is the one string here that can be long and
2319
- has no sensible break points of its own. */
2320
- .confirmBody {
2321
- color: var(--gs-fg-dim);
2322
- font-size: var(--gs-t-dense); line-height: 1.55;
2323
- overflow-wrap: anywhere;
2324
- }
2325
- /* Cancel first in the DOM so it takes focus and the Tab order starts on the
2326
- harmless answer; `row-reverse` puts the destructive one where the eye ends. */
2327
- .confirmActions { display: flex; flex-direction: row-reverse; gap: 8px; margin-top: 2px; }
2328
- /* Qualified rather than a bare modifier: written this way it out-specifies
2329
- `.btn` wherever either rule sits, which is the failure the drawer already
2330
- shipped once. */
2331
- .btn.btnDanger {
2332
- border-color: var(--gs-del); background: var(--gs-del); color: var(--gs-on-accent, #fff);
2333
- font-weight: 600;
2334
- }
2335
- .btn.btnDanger:hover:not(:disabled) { border-color: var(--gs-del); background: var(--gs-del); color: var(--gs-on-accent, #fff); }
2336
-
2337
- /* Files tab: the repository browser's tree, and the pane that opens a file.
2338
- Modifier rules stay BELOW the base rule they narrow — a modifier declared
2339
- first loses the cascade, which the drawer-chrome test checks for. */
2340
- .fbTree {
2341
- flex: none;
2342
- min-width: var(--gs-min-tree);
2343
- display: flex;
2344
- flex-direction: column;
2345
- gap: 4px;
2346
- padding: 6px;
2347
- overflow: auto;
2348
- background: var(--gs-surface-2);
2349
- }
2350
- .fbSearch {
2351
- flex: none;
2352
- width: 100%;
2353
- box-sizing: border-box;
2354
- padding: 4px 6px;
2355
- font: inherit;
2356
- font-size: var(--gs-t-meta);
2357
- color: var(--gs-fg);
2358
- background: var(--gs-surface);
2359
- border: 1px solid var(--gs-border);
2360
- border-radius: 4px;
2361
- }
2362
- .fbSearch:focus { outline: none; border-color: var(--gs-accent); }
2363
- .fbNote { flex: none; font-size: var(--gs-t-meta); color: var(--gs-fg-dim); padding: 2px 4px; }
2364
- .fbList { list-style: none; margin: 0; padding: 0; }
2365
- .fbRow {
2366
- display: flex;
2367
- align-items: center;
2368
- gap: 4px;
2369
- width: 100%;
2370
- padding: 2px 6px 2px 4px;
2371
- font: inherit;
2372
- font-size: var(--gs-t-dense);
2373
- color: var(--gs-fg-dim);
2374
- text-align: left;
2375
- background: none;
2376
- border: 0;
2377
- border-radius: 3px;
2378
- cursor: pointer;
2379
- white-space: nowrap;
2380
- overflow: hidden;
2381
- text-overflow: ellipsis;
2382
- }
2383
- .fbRow:hover { background: var(--gs-hover); }
2384
- .fbRowActive { background: var(--gs-selected); color: var(--gs-fg); }
2385
- .fbDirName { overflow: hidden; text-overflow: ellipsis; }
2386
- .fbFileName { overflow: hidden; text-overflow: ellipsis; }
2387
- .fbMain {
2388
- flex: 1 1 0;
2389
- min-width: var(--gs-min-diff);
2390
- display: flex;
2391
- flex-direction: column;
2392
- min-height: 0;
2393
- background: var(--gs-surface);
2394
- }
2395
- .fbHeader {
2396
- flex: none;
2397
- display: flex;
2398
- align-items: flex-start;
2399
- gap: 8px;
2400
- padding: 4px 8px 0;
2401
- border-bottom: 1px solid var(--gs-border);
2402
- }
2403
- .fbPath {
2404
- font-size: var(--gs-t-meta);
2405
- color: var(--gs-fg-dim);
2406
- overflow: hidden;
2407
- text-overflow: ellipsis;
2408
- white-space: nowrap;
2409
- padding: 4px 0;
2410
- }
2411
- /* The editor scrolls inside this, so the header and the notices above stay
2412
- put — the same reason the side pane's toolbar is pinned. */
2413
- .fbBody { flex: 1 1 0; min-height: 0; overflow: auto; padding: 4px 8px; }
2414
-
2415
- /* The commit behind the picked blame line. A strip rather than a floating
2416
- popup: it never covers the code it is about, and it survives a scroll. */
2417
- .fbCommit {
2418
- flex: none;
2419
- display: flex;
2420
- align-items: baseline;
2421
- gap: 8px;
2422
- padding: 4px 8px;
2423
- font-size: var(--gs-t-meta);
2424
- color: var(--gs-fg-dim);
2425
- background: var(--gs-surface-2);
2426
- border-bottom: 1px solid var(--gs-border);
2427
- }
2428
- .fbCommitLine { flex: none; color: var(--gs-fg-faint); font-variant-numeric: tabular-nums; }
2429
- .fbCommitWho { flex: none; color: var(--gs-fg); font-weight: 600; }
2430
- .fbCommitWhen { flex: none; font-variant-numeric: tabular-nums; }
2431
- .fbCommitHash { flex: none; font-family: inherit; color: var(--gs-fg-faint); }
2432
- .fbCommitWhat {
2433
- flex: 1 1 auto;
2434
- min-width: 0;
2435
- overflow: hidden;
2436
- text-overflow: ellipsis;
2437
- white-space: nowrap;
2438
- color: var(--gs-fg);
2439
- }
2440
- .fbCommitHint { color: var(--gs-fg-faint); }
2441
- .fbMore {
2442
- display: block;
2443
- padding: 2px 6px 4px 4px;
2444
- font-size: var(--gs-t-meta);
2445
- color: var(--gs-fg-faint);
2446
- font-style: italic;
2447
- }
2448
-
2449
- /* ============================ image preview ============================= */
2450
-
2451
- /* A picture fills the pane and its facts sit under it, because the facts are
2452
- the reason this is a viewer rather than an image tag: a screenshot in a
2453
- repository is usually opened to check a size or a crop. */
2454
- .imgPane {
2455
- display: flex;
2456
- flex-direction: column;
2457
- min-height: 0;
2458
- height: 100%;
2459
- gap: 8px;
2460
- }
2461
-
2462
- /* The checkerboard is not decoration. A transparent PNG on a flat background
2463
- is indistinguishable from an opaque one of that colour, and "is this
2464
- transparent" is the question the file was opened to answer. Two overlaid
2465
- gradients rather than an asset: no request, and it takes the theme. */
2466
- .imgStage {
2467
- flex: 1 1 auto;
2468
- min-height: 0;
2469
- display: flex;
2470
- align-items: center;
2471
- justify-content: center;
2472
- overflow: auto;
2473
- padding: 12px;
2474
- border-radius: var(--gs-r-control);
2475
- background-color: var(--gs-surface-2);
2476
- background-image:
2477
- linear-gradient(45deg, var(--gs-border-soft) 25%, transparent 25%, transparent 75%, var(--gs-border-soft) 75%),
2478
- linear-gradient(45deg, var(--gs-border-soft) 25%, transparent 25%, transparent 75%, var(--gs-border-soft) 75%);
2479
- background-size: 16px 16px;
2480
- background-position: 0 0, 8px 8px;
2481
- }
2482
-
2483
- /* Fit is the default: an 8000px render opened at full size shows its top-left
2484
- corner, which looks like a broken image rather than a big one. */
2485
- .imgShot {
2486
- max-width: 100%;
2487
- max-height: 100%;
2488
- object-fit: contain;
2489
- /* Nearest-neighbour would be right for icons and wrong for photographs, and
2490
- the pane cannot tell them apart; smooth is the safe half of that trade. */
2491
- image-rendering: auto;
2492
- }
2493
-
2494
- /* Actual size: the scaling comes off and the stage scrolls instead. Written
2495
- as a descendant of the stage's own state so the picture keeps ONE class,
2496
- and the two views cannot disagree about which is in force. */
2497
- .imgStage[data-actual] {
2498
- align-items: flex-start;
2499
- justify-content: flex-start;
2500
- }
2501
- .imgStage[data-actual] .imgShot {
2502
- max-width: none;
2503
- max-height: none;
2504
- object-fit: none;
2505
- }
2506
-
2507
- .imgCaption {
2508
- flex: none;
2509
- display: flex;
2510
- align-items: center;
2511
- gap: 8px;
2512
- font-size: var(--gs-t-meta);
2513
- color: var(--gs-fg-dim);
2514
- font-variant-numeric: tabular-nums;
2515
- }
8
+ @import './styles/environment.css';
9
+ @import './styles/themes.css';
10
+ @import './styles/shell.css';
11
+ @import './styles/history-filters.css';
12
+ @import './styles/history.css';
13
+ @import './styles/changes.css';
14
+ @import './styles/operations.css';
15
+ @import './styles/controls.css';
16
+ @import './styles/files.css';
17
+ @import './styles/image.css';