@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.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +28 -0
- package/CHANGELOG_EN.md +28 -0
- package/README.md +100 -50
- package/README_EN.md +2 -1
- package/lib/client.js +6902 -6256
- package/package.json +1 -1
- package/src/client/ChangesFileTree.tsx +550 -0
- package/src/client/ChromeGlyph.tsx +27 -0
- package/src/client/CodeEditor.tsx +129 -3
- package/src/client/CommitHistory.tsx +1001 -0
- package/src/client/DiffViews.tsx +1070 -0
- package/src/client/GitWorkbenchPanel.module.css +15 -2513
- package/src/client/GitWorkbenchPanel.tsx +37 -3959
- package/src/client/PaneDivider.tsx +74 -0
- package/src/client/WorkbenchControls.tsx +1047 -0
- package/src/client/WorktreeGlyph.tsx +24 -0
- package/src/client/cm-search-theme.ts +250 -0
- package/src/client/diff-nav.ts +59 -0
- package/src/client/git-workbench-types.ts +252 -0
- package/src/client/locales.ts +14 -8
- package/src/client/row-window.ts +23 -0
- package/src/client/search-count.ts +125 -0
- package/src/client/side-rows.ts +66 -0
- package/src/client/styles/changes.css +505 -0
- package/src/client/styles/controls.css +236 -0
- package/src/client/styles/environment.css +89 -0
- package/src/client/styles/files.css +113 -0
- package/src/client/styles/history-filters.css +400 -0
- package/src/client/styles/history.css +276 -0
- package/src/client/styles/image.css +67 -0
- package/src/client/styles/operations.css +179 -0
- package/src/client/styles/shell.css +431 -0
- package/src/client/styles/themes.css +224 -0
- package/src/client/use-change-nav.ts +6 -5
- 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
|
+
}
|