@young1lin/dsh-ui-gitworkbench 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +28 -0
  3. package/CHANGELOG_EN.md +28 -0
  4. package/README.md +100 -50
  5. package/README_EN.md +2 -1
  6. package/lib/client.js +6902 -6256
  7. package/package.json +1 -1
  8. package/src/client/ChangesFileTree.tsx +550 -0
  9. package/src/client/ChromeGlyph.tsx +27 -0
  10. package/src/client/CodeEditor.tsx +129 -3
  11. package/src/client/CommitHistory.tsx +1001 -0
  12. package/src/client/DiffViews.tsx +1070 -0
  13. package/src/client/GitWorkbenchPanel.module.css +15 -2513
  14. package/src/client/GitWorkbenchPanel.tsx +37 -3959
  15. package/src/client/PaneDivider.tsx +74 -0
  16. package/src/client/WorkbenchControls.tsx +1047 -0
  17. package/src/client/WorktreeGlyph.tsx +24 -0
  18. package/src/client/cm-search-theme.ts +250 -0
  19. package/src/client/diff-nav.ts +59 -0
  20. package/src/client/git-workbench-types.ts +252 -0
  21. package/src/client/locales.ts +14 -8
  22. package/src/client/row-window.ts +23 -0
  23. package/src/client/search-count.ts +125 -0
  24. package/src/client/side-rows.ts +66 -0
  25. package/src/client/styles/changes.css +505 -0
  26. package/src/client/styles/controls.css +236 -0
  27. package/src/client/styles/environment.css +89 -0
  28. package/src/client/styles/files.css +113 -0
  29. package/src/client/styles/history-filters.css +400 -0
  30. package/src/client/styles/history.css +276 -0
  31. package/src/client/styles/image.css +67 -0
  32. package/src/client/styles/operations.css +179 -0
  33. package/src/client/styles/shell.css +431 -0
  34. package/src/client/styles/themes.css +224 -0
  35. package/src/client/use-change-nav.ts +6 -5
  36. package/src/client/use-row-window.ts +55 -0
@@ -0,0 +1,400 @@
1
+ /* Author / committer / exact date — the precise answers the row's summary
2
+ ("3 weeks ago") deliberately defers to the hover card. */
3
+ .commitPopMeta {
4
+ display: flex; flex-direction: column; gap: 2px;
5
+ color: var(--gs-fg-muted);
6
+ font-size: var(--gs-t-dense); line-height: 16px;
7
+ }
8
+ /* The filter box in the history pane head — it flexes to fill the head beside
9
+ the title (the funnel button is flex:none), so a wide pane gets a wide box
10
+ and a narrow pane still leaves room to type. */
11
+ .commitFilter {
12
+ flex: 1 1 auto; min-width: 120px;
13
+ height: var(--gs-h-control); box-sizing: border-box;
14
+ padding: 0 10px;
15
+ border: 1px solid var(--gs-border-soft); border-radius: var(--gs-r-control);
16
+ background: var(--gs-bg); color: var(--gs-fg);
17
+ font-family: inherit; font-size: var(--gs-t-dense);
18
+ outline: none;
19
+ }
20
+ .commitFilter:focus { border-color: var(--gs-accent); }
21
+ .commitFilter::placeholder { color: var(--gs-fg-faint); }
22
+ /* Active criteria under the head: one removable chip each, grammar tokens as
23
+ their own labels — the chip IS the query it would take to retype it. */
24
+ .filterChips {
25
+ display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
26
+ flex: none;
27
+ padding: 6px 10px;
28
+ border-bottom: 1px solid var(--gs-border-soft);
29
+ }
30
+ .filterChip {
31
+ display: inline-flex; align-items: center; gap: 2px;
32
+ max-width: 220px;
33
+ border: 1px solid var(--gs-border-soft); border-radius: var(--gs-r-control);
34
+ background: var(--gs-surface-2);
35
+ font-size: var(--gs-t-dense); line-height: 18px;
36
+ }
37
+ .filterChipLabel {
38
+ padding: 2px 4px 2px 8px;
39
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
40
+ color: var(--gs-fg-muted);
41
+ }
42
+ .filterChipRemove {
43
+ flex: none;
44
+ padding: 0 6px;
45
+ border: none; border-left: 1px solid var(--gs-border-soft);
46
+ background: transparent; color: var(--gs-fg-dim);
47
+ font-family: inherit; font-size: var(--gs-t-dense); line-height: 20px;
48
+ cursor: pointer;
49
+ }
50
+ .filterChipRemove:hover { color: var(--gs-del); }
51
+ .filterClear {
52
+ border: none; background: transparent;
53
+ padding: 2px 4px;
54
+ color: var(--gs-fg-faint);
55
+ font-family: inherit; font-size: var(--gs-t-dense); line-height: 18px;
56
+ cursor: pointer;
57
+ }
58
+ .filterClear:hover { color: var(--gs-fg); }
59
+ /* The funnel dropdown — RefPicker's positioning pattern (relative anchor,
60
+ absolute panel, dismiss-on-outside). Grows leftward: it sits at the pane's
61
+ right edge. */
62
+ .funnel { position: relative; display: inline-flex; }
63
+ /* The button itself carries no geometry of its own — it joined the shared
64
+ control vocabulary above (`.btn, .refButton, …`) so it reads as a sibling of
65
+ every other button in the drawer, not a foreign element. */
66
+ .funnelButton.funnelButtonActive { color: var(--gs-accent); border-color: var(--gs-accent); }
67
+
68
+ /* The panel is a flex COLUMN with exactly ONE scrolling child. It used to
69
+ scroll itself AND cap its list at 240px: two nested scroll containers in a
70
+ 331px box that had ~700px of anchor space to spend, so a long roster got a
71
+ fat inner scrollbar instead of the height already available. The tab strip
72
+ and the footer are `flex: none`; the list takes what is left. */
73
+ .funnelPop {
74
+ position: fixed; z-index: 40;
75
+ display: flex; flex-direction: column;
76
+ width: 320px; max-width: calc(100vw - 24px);
77
+ box-sizing: border-box;
78
+ overflow: hidden;
79
+ border: 1px solid var(--gs-border); border-radius: var(--gs-r-surface);
80
+ background: var(--gs-panel);
81
+ box-shadow: 0 16px 40px var(--gs-shadow);
82
+ font-size: var(--gs-t-dense);
83
+ }
84
+ /* One section at a time — the panel's height is a property of its design,
85
+ not of how many authors the repository happens to have. The strip is a
86
+ segmented control (inset track, raised active chip) rather than one
87
+ outlined pill beside two bare words, which read as a button and two labels
88
+ instead of one three-way choice. */
89
+ .funnelTabs {
90
+ flex: none;
91
+ display: flex; gap: 2px;
92
+ margin: 8px 8px 0;
93
+ padding: 2px;
94
+ border: 1px solid var(--gs-border-soft); border-radius: var(--gs-r-control);
95
+ background: var(--gs-bg);
96
+ }
97
+ .funnelTab {
98
+ flex: 1; min-width: 0;
99
+ box-sizing: border-box;
100
+ height: var(--gs-h-compact); padding: 0 8px;
101
+ /* Transparent rather than none: the shared selected rule paints its
102
+ members through `border-color`, and a border that is not there takes
103
+ no colour. Inside `border-box`, so the strip does not grow by 2px. */
104
+ border: 1px solid transparent; border-radius: calc(var(--gs-r-control) - 3px);
105
+ background: transparent;
106
+ color: var(--gs-fg-dim);
107
+ font-family: inherit; font-size: var(--gs-t-dense); line-height: 1;
108
+ white-space: nowrap;
109
+ cursor: pointer;
110
+ transition: background 120ms ease, color 120ms ease;
111
+ }
112
+ .funnelTab:hover { color: var(--gs-fg); }
113
+ /* .funnelTabActive: see the shared selected-state rule in controls.css. */
114
+ .funnelTab:focus-visible { outline: 2px solid var(--gs-accent); outline-offset: 1px; }
115
+ /* A tab's own criteria count, so the two sections nobody is looking at still
116
+ say they hold something. */
117
+ .funnelTabCount { margin-left: 4px; color: var(--gs-accent); font-variant-numeric: tabular-nums; }
118
+
119
+ /* The body of the active section. Everything inside is `flex: none` except
120
+ the list, which is the one thing allowed to scroll. */
121
+ .funnelPane {
122
+ flex: 1 1 auto; min-height: 0;
123
+ display: flex; flex-direction: column; gap: 6px;
124
+ padding: 8px;
125
+ }
126
+ .funnelCaption {
127
+ flex: none;
128
+ color: var(--gs-fg-faint);
129
+ font-size: var(--gs-t-meta); line-height: 14px;
130
+ letter-spacing: 0.04em; text-transform: uppercase;
131
+ }
132
+
133
+ /* Which bound a picked day lands in. Deliberately NOT the tab strip's shape:
134
+ a rect track with an accent-tinted chip under a caption, because two
135
+ identical pill rows stacked six pixels apart said nothing about meaning two
136
+ different things. */
137
+ .funnelBounds {
138
+ flex: none;
139
+ display: flex; gap: 2px;
140
+ padding: 2px;
141
+ border: 1px solid var(--gs-border-soft); border-radius: var(--gs-r-control);
142
+ background: var(--gs-bg);
143
+ }
144
+ .funnelBoundBtn {
145
+ flex: 1; min-width: 0;
146
+ box-sizing: border-box;
147
+ height: var(--gs-h-compact); padding: 0 6px;
148
+ border: 1px solid transparent; border-radius: calc(var(--gs-r-control) - 3px);
149
+ background: transparent;
150
+ color: var(--gs-fg-dim);
151
+ font-family: inherit; font-size: var(--gs-t-dense); line-height: 1;
152
+ cursor: pointer;
153
+ transition: background 120ms ease, color 120ms ease;
154
+ }
155
+ .funnelBoundBtn:hover { color: var(--gs-fg); }
156
+ /* .funnelBoundBtnActive: see the shared selected-state rule in controls.css. */
157
+
158
+ /* The calendar — modern-grid look in the drawer's own tokens. */
159
+ .cal { flex: none; display: flex; flex-direction: column; gap: 4px; }
160
+ .calHead { display: flex; align-items: center; gap: 4px; }
161
+ .calTitle {
162
+ flex: 1; text-align: center;
163
+ color: var(--gs-fg);
164
+ font-size: var(--gs-t-dense); line-height: 20px;
165
+ }
166
+ .calNav {
167
+ flex: none;
168
+ width: var(--gs-h-compact); height: var(--gs-h-compact); padding: 0;
169
+ border: none; border-radius: var(--gs-r-control);
170
+ background: transparent;
171
+ color: var(--gs-fg-dim);
172
+ font-family: inherit; font-size: var(--gs-t-ui);
173
+ cursor: pointer;
174
+ }
175
+ .calNav:hover { color: var(--gs-fg); background: var(--gs-raise); }
176
+ .calWeek, .calGrid {
177
+ display: grid;
178
+ grid-template-columns: repeat(7, 1fr);
179
+ gap: 2px;
180
+ }
181
+ .calWeek span {
182
+ text-align: center;
183
+ color: var(--gs-fg-faint);
184
+ font-size: var(--gs-t-meta); line-height: 14px;
185
+ }
186
+ .calGrid button {
187
+ height: var(--gs-h-compact);
188
+ border: none; border-radius: var(--gs-r-control);
189
+ background: transparent;
190
+ color: var(--gs-fg-muted);
191
+ font-family: inherit; font-size: var(--gs-t-dense);
192
+ font-variant-numeric: tabular-nums;
193
+ cursor: pointer;
194
+ }
195
+ .calGrid button:hover { background: var(--gs-raise); color: var(--gs-fg); }
196
+ .calGrid button.calOut { color: var(--gs-fg-fainter); }
197
+ .calGrid button.calToday { color: var(--gs-accent); font-weight: 600; }
198
+ /* Days strictly between the two bounds: the span the filter actually means,
199
+ which two lone marked endpoints never showed. */
200
+ .calGrid button.calIn { background: var(--gs-accent-bg); color: var(--gs-fg); }
201
+ .calGrid button.calMark {
202
+ background: var(--gs-accent);
203
+ color: var(--gs-on-accent);
204
+ font-weight: 600;
205
+ }
206
+
207
+ /* The two bound readouts, side by side under the grid. */
208
+ .funnelBoundRows { flex: none; display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
209
+ .funnelBoundRow {
210
+ display: flex; align-items: center; gap: 4px; min-width: 0;
211
+ box-sizing: border-box;
212
+ padding: 3px 6px;
213
+ border: 1px solid var(--gs-border-soft); border-radius: var(--gs-r-control);
214
+ background: var(--gs-bg);
215
+ font-size: var(--gs-t-meta); line-height: 16px;
216
+ }
217
+ .funnelBoundKey { flex: none; color: var(--gs-fg-faint); }
218
+ .funnelBoundVal {
219
+ flex: 1; min-width: 0;
220
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
221
+ color: var(--gs-fg-faint);
222
+ font-variant-numeric: tabular-nums;
223
+ }
224
+ .funnelBoundValSet { color: var(--gs-fg); }
225
+ .funnelBoundClear {
226
+ flex: none;
227
+ padding: 0 2px;
228
+ border: none; background: transparent;
229
+ color: var(--gs-fg-dim);
230
+ font-family: inherit; font-size: var(--gs-t-dense); line-height: 1;
231
+ cursor: pointer;
232
+ }
233
+ .funnelBoundClear:hover { color: var(--gs-del); }
234
+
235
+ .funnelSearch {
236
+ flex: none;
237
+ box-sizing: border-box;
238
+ height: var(--gs-h-compact); padding: 0 8px;
239
+ border: 1px solid var(--gs-border-soft); border-radius: var(--gs-r-control);
240
+ background: var(--gs-bg); color: var(--gs-fg);
241
+ font-family: inherit; font-size: var(--gs-t-dense);
242
+ outline: none;
243
+ }
244
+ .funnelSearch::placeholder { color: var(--gs-fg-faint); }
245
+ .funnelSearch:focus { border-color: var(--gs-accent); box-shadow: 0 0 0 2px var(--gs-accent-bg); }
246
+ .funnelList {
247
+ flex: 1 1 auto; min-height: 0;
248
+ overflow-y: auto; overscroll-behavior: contain;
249
+ display: flex; flex-direction: column; gap: 1px;
250
+ margin: 0 -4px; padding: 0 4px;
251
+ scrollbar-width: thin; scrollbar-color: var(--gs-fg-fainter) transparent;
252
+ }
253
+ .funnelList::-webkit-scrollbar { width: 10px; }
254
+ .funnelList::-webkit-scrollbar-track { background: transparent; }
255
+ .funnelList::-webkit-scrollbar-thumb {
256
+ background: var(--gs-fg-fainter);
257
+ border: 3px solid transparent; border-radius: var(--gs-r-pill);
258
+ background-clip: content-box;
259
+ }
260
+ /* Every row is the same four slots — chevron, tick, glyph, name — then the
261
+ count. Directories used to skip the glyph that files carried, so a folder
262
+ name and the file names under it started at different x and the list had a
263
+ ragged left edge. */
264
+ .funnelRow {
265
+ display: flex; align-items: center; gap: 6px;
266
+ box-sizing: border-box;
267
+ min-height: 24px; padding: 0 4px;
268
+ border-radius: var(--gs-r-control);
269
+ cursor: pointer;
270
+ }
271
+ .funnelRow:hover { background: var(--gs-raise); }
272
+ .funnelRow:has(input[type='checkbox']:checked) .funnelName,
273
+ .funnelRow:has(input[type='checkbox']:indeterminate) .funnelName { color: var(--gs-fg); }
274
+ /* The tick IS the input — no wrapper span, no visually-hidden twin: the live
275
+ probes select `[class*="funnelRow"] input[type=checkbox]` and the directory
276
+ row's DIRECT child, so the markup has to stay flat. `appearance: none`
277
+ hands us the box; the mark is a rotated border, so nothing ships as an
278
+ asset and every state recolors with the theme. */
279
+ .funnelRow input[type='checkbox'] {
280
+ appearance: none; -webkit-appearance: none;
281
+ flex: none; position: relative;
282
+ box-sizing: border-box;
283
+ width: 14px; height: 14px; margin: 0;
284
+ border: 1px solid var(--gs-fg-fainter); border-radius: 3px;
285
+ background: transparent;
286
+ cursor: pointer;
287
+ transition: background 120ms ease, border-color 120ms ease;
288
+ }
289
+ .funnelRow input[type='checkbox']:hover { border-color: var(--gs-fg-dim); }
290
+ .funnelRow input[type='checkbox']:checked,
291
+ .funnelRow input[type='checkbox']:indeterminate {
292
+ background: var(--gs-accent); border-color: var(--gs-accent);
293
+ }
294
+ .funnelRow input[type='checkbox']:focus-visible { outline: 2px solid var(--gs-accent); outline-offset: 1px; }
295
+ .funnelRow input[type='checkbox']:checked::after {
296
+ content: ''; position: absolute;
297
+ left: 4px; top: 1px; width: 3px; height: 7px;
298
+ border: solid var(--gs-on-accent); border-width: 0 1.5px 1.5px 0;
299
+ transform: rotate(42deg);
300
+ }
301
+ /* Partial — a folder some of whose subtree is ticked. */
302
+ .funnelRow input[type='checkbox']:indeterminate::after {
303
+ content: ''; position: absolute;
304
+ left: 2px; top: 5px; width: 8px; height: 2px;
305
+ border-radius: 1px;
306
+ background: var(--gs-on-accent);
307
+ }
308
+ /* Tree chrome in the drawer's own visual language: a directory folds with the
309
+ chevron the file tree uses, and both glyphs are inline SVG in the shape
310
+ IntelliJ's New UI icon set uses — a 16px grid, 1px strokes, no fill,
311
+ rounded joins, outlines rather than the filled silhouettes the old UI
312
+ shipped. Drawn in `PathDirGlyph` / `PathFileGlyph` rather than imported:
313
+ the bundle purity gate forbids an icon package, and the picker needs
314
+ exactly these two. Indentation is the row's own padding ALONE; the nested
315
+ rail this used to draw was `--gs-border-soft` over `--gs-panel` (the same
316
+ value as `--gs-raise` in the GitHub palettes, so it vanished under hover)
317
+ and its margin double-counted every level. */
318
+ .pathNode { display: flex; flex-direction: column; }
319
+ .pathChildren { display: flex; flex-direction: column; }
320
+ /* One shared slot so a folder and a file start their names at the same x —
321
+ New UI draws both on the same 16px grid, and the column only lines up if
322
+ the box does too. `--gs-fg-dim` is the neutral the tree icons sit at there:
323
+ present, but never louder than the filename beside them. */
324
+ .pathFileGlyph, .pathDirGlyph {
325
+ flex: none;
326
+ width: 16px; height: 16px;
327
+ color: var(--gs-fg-dim);
328
+ }
329
+ .funnelRow:hover .pathFileGlyph,
330
+ .funnelRow:hover .pathDirGlyph { color: var(--gs-fg-muted); }
331
+ .funnelName {
332
+ flex: 1; min-width: 0;
333
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
334
+ color: var(--gs-fg-muted);
335
+ }
336
+ .funnelCount {
337
+ flex: none;
338
+ color: var(--gs-fg-faint);
339
+ font-size: var(--gs-t-meta);
340
+ font-variant-numeric: tabular-nums;
341
+ }
342
+ .funnelMore { color: var(--gs-fg-faint); padding: 4px 6px; }
343
+ .funnelChevron {
344
+ flex: none;
345
+ width: 14px; padding: 0;
346
+ border: none; background: transparent;
347
+ color: var(--gs-fg-faint);
348
+ font-family: inherit; font-size: 9px; line-height: 16px;
349
+ cursor: pointer;
350
+ }
351
+ .funnelChevron:hover:not(:disabled) { color: var(--gs-fg); }
352
+ .funnelChevron:disabled { cursor: default; }
353
+ .funnelPresets { flex: none; display: flex; gap: 4px; }
354
+ /* `.funnelPreset` shipped with `flex: none` and nothing else, so it never
355
+ joined the shared button vocabulary and the three date presets rendered as
356
+ the browser's default grey buttons — the one patch of unthemed chrome in
357
+ the drawer. It is in both selector lists now; this only sizes them to share
358
+ one row. */
359
+ .funnelPreset { flex: 1 1 0; min-width: 0; padding: 0 6px; }
360
+ /* .funnelPresetActive: see the shared selected-state rule in controls.css. */
361
+
362
+ /* The footer says what the panel is currently asking git for. Without it the
363
+ only feedback lived on the chip row BEHIND the panel, so a reader had to
364
+ close the thing to find out whether their ticks took. */
365
+ .funnelFoot {
366
+ flex: none;
367
+ display: flex; align-items: center; justify-content: space-between; gap: 8px;
368
+ padding: 6px 10px;
369
+ border-top: 1px solid var(--gs-border-soft);
370
+ background: var(--gs-bg);
371
+ font-size: var(--gs-t-meta); line-height: 16px;
372
+ }
373
+ .funnelFootCount { color: var(--gs-fg-faint); font-variant-numeric: tabular-nums; }
374
+ .funnelFootCountOn { color: var(--gs-fg-muted); }
375
+ .funnelFootClear {
376
+ flex: none;
377
+ padding: 2px 6px;
378
+ border: none; border-radius: var(--gs-r-control);
379
+ background: transparent;
380
+ color: var(--gs-fg-dim);
381
+ font-family: inherit; font-size: var(--gs-t-meta); line-height: 16px;
382
+ cursor: pointer;
383
+ }
384
+ .funnelFootClear:hover:not(:disabled) { color: var(--gs-fg); background: var(--gs-raise); }
385
+ .funnelFootClear:disabled { color: var(--gs-fg-fainter); cursor: default; }
386
+
387
+ .commitPopSubject {
388
+ color: var(--gs-fg);
389
+ font-size: var(--gs-t-ui); font-weight: 600; line-height: 18px;
390
+ white-space: pre-wrap; overflow-wrap: anywhere;
391
+ user-select: text;
392
+ }
393
+ .commitPopBody {
394
+ margin: 0;
395
+ color: var(--gs-fg-muted);
396
+ font-family: inherit;
397
+ font-size: var(--gs-t-dense); line-height: 18px;
398
+ white-space: pre-wrap; overflow-wrap: anywhere;
399
+ user-select: text;
400
+ }
@@ -0,0 +1,276 @@
1
+ /* Drawer tabs — mirrors the app's own tab bar: 13/16 text at weight 500, a
2
+ tertiary label, the selected one riding the accent over a 2px bar. */
3
+ .tabs {
4
+ display: flex; gap: 24px;
5
+ flex: none;
6
+ padding: 0 var(--gs-gutter);
7
+ border-bottom: 1px solid var(--gs-border-soft);
8
+ background: var(--gs-surface-2);
9
+ font-family: inherit;
10
+ }
11
+ .tab {
12
+ position: relative;
13
+ padding: 8px 0 9px;
14
+ border: none;
15
+ background: transparent;
16
+ font-family: inherit;
17
+ font-size: var(--gs-t-ui); line-height: 16px; font-weight: 500;
18
+ color: var(--gs-fg-dim);
19
+ cursor: pointer;
20
+ }
21
+ .tab::after {
22
+ content: '';
23
+ position: absolute; right: 0; bottom: 1px; left: 0;
24
+ height: 2px; border-radius: 2px;
25
+ background: transparent;
26
+ }
27
+ .tabActive { color: var(--gs-accent); }
28
+ .tabActive::after { background: var(--gs-accent); }
29
+
30
+ /* Compare tab: the base/head ref pickers. */
31
+ .compareBar {
32
+ display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
33
+ flex: none;
34
+ padding: 8px var(--gs-gutter);
35
+ border-bottom: 1px solid var(--gs-border-soft);
36
+ background: var(--gs-surface-2);
37
+ color: var(--gs-fg-dim);
38
+ font-family: var(--dsw-font-ui, system-ui, sans-serif);
39
+ font-size: var(--gs-t-dense);
40
+ }
41
+ .compareArrow { color: var(--gs-fg-faint); }
42
+
43
+ /* Ref picker: a button that opens a filter box over a scrolling list. Sized for
44
+ a repository with hundreds of branches, where a chip row or a plain select
45
+ stops being usable. */
46
+ .refPicker { position: relative; display: inline-flex; align-items: center; gap: 6px; }
47
+ .refLabel { flex: none; color: var(--gs-fg-faint); }
48
+ .refButton { max-width: 260px; justify-content: space-between; }
49
+ .refValue { min-width: 0; overflow: hidden; }
50
+ .refCaret { flex: none; color: var(--gs-fg-faint); font-size: 9px; }
51
+ .refPop {
52
+ position: absolute; top: calc(100% + 4px); left: 0; z-index: 10;
53
+ display: flex; flex-direction: column;
54
+ width: 300px; max-width: 80vw;
55
+ border: 1px solid var(--gs-border); border-radius: var(--gs-r-control);
56
+ background: var(--gs-panel);
57
+ box-shadow: 0 12px 32px var(--gs-shadow);
58
+ overflow: hidden;
59
+ }
60
+ /* A short fixed menu rather than a branch list: no search box, no footer, and
61
+ sized to its own labels instead of the 300px a few hundred refs need. Anchored
62
+ right because the pickers that use it sit in a bar's right-hand cluster, where
63
+ a left-anchored popover would open off the drawer. */
64
+ .menuPop {
65
+ width: auto; min-width: 100%;
66
+ left: auto; right: 0;
67
+ padding: 4px;
68
+ }
69
+ .refSearch {
70
+ flex: none;
71
+ padding: 7px 10px;
72
+ border: 0; border-bottom: 1px solid var(--gs-border-soft);
73
+ background: var(--gs-bg); color: var(--gs-fg);
74
+ font-family: inherit; font-size: var(--gs-t-dense);
75
+ outline: none;
76
+ }
77
+ .refSearch::placeholder { color: var(--gs-fg-faint); }
78
+ /* Bounded so the popover never runs off the drawer, whatever the branch count. */
79
+ .refList { flex: 1; max-height: 280px; overflow-y: auto; padding: 4px; }
80
+ .refGroup {
81
+ padding: 6px 8px 3px;
82
+ color: var(--gs-fg-faint);
83
+ font-size: var(--gs-t-meta); font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase;
84
+ }
85
+ .refRow {
86
+ display: flex; align-items: center; gap: 6px;
87
+ box-sizing: border-box; width: 100%;
88
+ padding: 5px 8px;
89
+ border: 0; border-radius: var(--gs-r-control);
90
+ background: transparent; color: var(--gs-fg-muted);
91
+ font-family: inherit; font-size: var(--gs-t-dense);
92
+ text-align: left; cursor: pointer;
93
+ }
94
+ .refRow:hover { background: var(--gs-raise); color: var(--gs-fg); }
95
+ /* .refRowActive: see the shared selected-state rule at the foot of the file. */
96
+ /* Keeps a branch without a worktree aligned with one that has the glyph. */
97
+ .refRowSpacer { flex: none; width: 12px; }
98
+ .refRowName { flex: 1 1 auto; min-width: 0; overflow: hidden; }
99
+ .refEmpty { padding: 14px 10px; color: var(--gs-fg-faint); font-size: var(--gs-t-dense); }
100
+ .refFoot {
101
+ flex: none;
102
+ padding: 5px 10px;
103
+ border-top: 1px solid var(--gs-border-soft);
104
+ color: var(--gs-fg-faint); font-size: var(--gs-t-meta);
105
+ }
106
+
107
+ /* Commit log — its own full-height pane beside the file tree. Rows are stacked
108
+ so each subject gets the pane's full width instead of a 340px ellipsis. */
109
+ .commitsPane {
110
+ /* Shrinkable, with the floor as a min-width rather than a JS clamp alone: a
111
+ width dragged in one tab must not squeeze the diff to nothing when a third
112
+ pane appears in another. The floors come from the panel's own constants, so
113
+ the clamp and the stylesheet cannot drift apart. */
114
+ flex: 0 1 auto; box-sizing: border-box;
115
+ display: flex; flex-direction: column; min-height: 0;
116
+ /* Capped, so every pixel past the cap goes to the diff — the pane whose
117
+ content cannot wrap. */
118
+ width: 26%; min-width: var(--gs-min-commits); max-width: 340px;
119
+ background: var(--gs-surface);
120
+ }
121
+ /* Shared header bar for the commit and tree panes. Its own height comes from
122
+ the compact control it hosts, so a pane with tools and a pane without one
123
+ still line up across the divider. */
124
+ .paneHead {
125
+ /* Wraps rather than overflows. Dragged narrow the pane cannot hold a title,
126
+ a funnel and a search box on one line, and what it used to do was clip the
127
+ search box at the pane's edge — a control half off the screen, with no
128
+ sign that the rest of it existed. */
129
+ display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
130
+ flex: none;
131
+ min-height: 37px; box-sizing: border-box;
132
+ padding: 6px var(--gs-gutter-pane);
133
+ border-bottom: 1px solid var(--gs-border-soft);
134
+ color: var(--gs-fg-dim);
135
+ font-family: var(--dsw-font-ui, system-ui, sans-serif);
136
+ font-size: var(--gs-t-dense);
137
+ }
138
+ .paneTitle { font-weight: 600; }
139
+ /* Zero-height probe below the last row; scrolling it into view fetches the next
140
+ page. It carries no pixels of its own so it cannot disturb the list. */
141
+ .commitsSentinel { flex: none; height: 1px; }
142
+ .commitsFoot {
143
+ flex: none;
144
+ padding: 8px;
145
+ color: var(--gs-fg-faint);
146
+ font-family: var(--dsw-font-ui, system-ui, sans-serif);
147
+ font-size: var(--gs-t-meta); text-align: center;
148
+ /* Reserved whether or not there is text, so arriving at the end of the list
149
+ does not shift the rows above it. */
150
+ min-height: 16px;
151
+ }
152
+
153
+ /* `flex: 1` with `min-height: 0` gives this scroller a DEFINITE height from the
154
+ pane. A percentage max-height cannot: its parent is sized by content, so the
155
+ percentage has nothing to resolve against and the list grows without bound —
156
+ which is what pushed the file tree off screen and the footer out of reach. */
157
+ /* No gap between lines and no left padding: the graph column runs down the left
158
+ edge and a lane has to reach the next row to stay one line. The rows' own
159
+ breathing room is inside `.commit`, which keeps its inset and rounding. */
160
+ .commits {
161
+ display: flex; flex-direction: column;
162
+ flex: 1; min-height: 0;
163
+ overflow-y: auto;
164
+ padding: 4px 6px 4px 0;
165
+ }
166
+ /* One list line: the graph slice, then the row. Fixed height, because the SVG
167
+ is drawn at exactly this height and the lanes only join if they agree. */
168
+ .commitLine {
169
+ display: flex; align-items: stretch;
170
+ flex: none;
171
+ /* Published by the panel from `COMMIT_ROW_H`, which the lane graph also draws
172
+ each row's segment at — one source, because a row and its segment that
173
+ disagree leave the lanes short of the seam between rows. The two
174
+ arrangements want different rows, which is why this is a property and not
175
+ a number. Guarded by tests/commit-row-height.test.ts. */
176
+ height: var(--gs-commit-row);
177
+ }
178
+ .graphCell { flex: none; display: block; }
179
+ /* An inset row, like dsh's own session rows: 12px radius, filled when active. */
180
+ .commit {
181
+ display: flex; align-items: center; gap: 10px;
182
+ flex: 1 1 auto; min-width: 0; box-sizing: border-box;
183
+ height: 100%;
184
+ margin: 1px 0;
185
+ padding: 4px 8px;
186
+ border: 1px solid transparent;
187
+ border-radius: var(--gs-r-surface);
188
+ background: transparent;
189
+ font-family: inherit;
190
+ text-align: left;
191
+ cursor: pointer;
192
+ font-size: var(--gs-t-dense);
193
+ overflow: hidden;
194
+ transition: background 120ms ease, border-color 120ms ease;
195
+ }
196
+ /* Beside the diff the row turns into two lines: a pane of about 340px has no
197
+ width for the hash, the author, the date AND the subject on one, and the
198
+ subject is the part that would have given way. Above the diff it stays one
199
+ line — see the row's two shapes in `CommitRow`. */
200
+ .commitsPane[data-layout="columns"] .commit {
201
+ flex-direction: column; align-items: stretch; justify-content: center; gap: 2px;
202
+ }
203
+ /* The subject line must not absorb the row's spare height, or the two lines
204
+ stop sitting together in the middle of the row. */
205
+ .commitsPane[data-layout="columns"] .commitSubjectRow { flex: none; }
206
+ .commitTop {
207
+ display: flex; align-items: center; justify-content: space-between; gap: 8px;
208
+ min-width: 0;
209
+ }
210
+ .commit:hover { background: var(--gs-raise); }
211
+ /* .commitActive: see the shared selected-state rule in controls.css. */
212
+ /* Pushed to the right end of the row, and never squeezed: the subject is what
213
+ gives way when the pane narrows, because it is the part with an ellipsis to
214
+ give. */
215
+ .commitMeta { flex: none; display: flex; align-items: baseline; gap: 8px; }
216
+ .commitHash { flex: none; color: var(--gs-info); }
217
+ .commitSubjectRow {
218
+ display: flex; align-items: baseline; gap: 4px;
219
+ flex: 1 1 auto; min-width: 0;
220
+ }
221
+ /* Shrinks, never grows. Growing was invisible while the list was a 26%%
222
+ column — the subject filled the row either way — and became a defect the
223
+ moment the list spanned the drawer: a subject that takes ALL the free space
224
+ pushes the has-a-body marker a thousand pixels right, where it reads as an
225
+ unexplained ellipsis next to the author rather than as "there is more to
226
+ this message". The row still pushes author and date to the right end;
227
+ .commitSubjectRow is what absorbs the slack. */
228
+ .commitSubject {
229
+ flex: 0 1 auto; min-width: 0;
230
+ overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
231
+ color: var(--gs-fg-muted);
232
+ }
233
+ .commitHasBody { flex: none; color: var(--gs-fg-faint); letter-spacing: 0.04em; }
234
+ /* Branch and tag names pointing at this commit. Capped and ellipsised: a release
235
+ commit can carry a dozen refs, and none of them is worth the subject's room. */
236
+ .commitRef {
237
+ flex: none;
238
+ max-width: 96px;
239
+ overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
240
+ padding: 0 5px;
241
+ border: 1px solid var(--gs-accent-border); border-radius: var(--gs-r-pill);
242
+ background: var(--gs-accent-bg);
243
+ color: var(--gs-accent);
244
+ font-size: var(--gs-t-meta); line-height: 15px;
245
+ }
246
+ .commitWhen { flex: none; color: var(--gs-fg-faint); }
247
+ /* Who wrote it — sits between hash and relative time, capped so a long name
248
+ cannot evict the time from the row. */
249
+ .commitAuthor {
250
+ flex: none; max-width: 120px;
251
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
252
+ color: var(--gs-fg-dim);
253
+ }
254
+
255
+ .body { display: flex; flex: 1; min-height: 0; z-index: 1; }
256
+ /* History stacks: the commit list over the tree-and-diff row. `.bodyRow` is
257
+ rendered on every tab, so the unstacked tabs are a row inside a row — which
258
+ costs nothing and keeps one JSX shape instead of two. */
259
+ .body[data-stacked] { flex-direction: column; }
260
+ .bodyRow { display: flex; flex: 1 1 0; min-width: 0; min-height: 0; }
261
+ /* Stacked, the list is sized by HEIGHT and spans the drawer; the width rules
262
+ it carries for the column layout would otherwise pin it to 26%. */
263
+ .body[data-stacked] .commitsPane {
264
+ width: auto; max-width: none; min-width: 0;
265
+ height: 34%; flex: none;
266
+ /* The drag's clamp, restated where a RESIZE can also reach it. An inline
267
+ `height` in pixels survives the window getting shorter, and then the list
268
+ is taller than the body: the lower half goes to zero and the handle ends
269
+ up below the bottom edge with nothing to grab. Percentages re-resolve on
270
+ every layout, which is exactly what the stored pixel height cannot do.
271
+ Both numbers come from the panel's own constants, so there is still one
272
+ home for them. `max()` keeps the range from inverting in a drawer too
273
+ short to hold both floors — the list then simply takes its minimum. */
274
+ min-height: var(--gs-min-commits-tall);
275
+ max-height: max(var(--gs-min-commits-tall), calc(100% - var(--gs-min-stacked-lower)));
276
+ }