@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,505 @@
|
|
|
1
|
+
/* File tree column. A peer of the commit pane and the diff, never their parent:
|
|
2
|
+
each of the three scrolls on its own and keeps its full height. */
|
|
3
|
+
.treeCol {
|
|
4
|
+
flex: 0 1 auto; box-sizing: border-box;
|
|
5
|
+
display: flex; flex-direction: column; min-height: 0;
|
|
6
|
+
width: 28%; min-width: var(--gs-min-tree); max-width: 400px;
|
|
7
|
+
background: var(--gs-surface);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/* file tree */
|
|
11
|
+
.treeWrap {
|
|
12
|
+
flex: 1; box-sizing: border-box;
|
|
13
|
+
display: flex; flex-direction: column;
|
|
14
|
+
min-width: 0; min-height: 0;
|
|
15
|
+
}
|
|
16
|
+
/* Two items, not five: the count and one group of buttons. The pane drags down
|
|
17
|
+
to 170px, so the row does have to wrap — but it wraps between the count and
|
|
18
|
+
the group, never inside it. Wrapping the buttons individually is what left a
|
|
19
|
+
lone "Collapse all" stranded on a second row under a right-aligned
|
|
20
|
+
"Expand all". `space-between` then aligns whichever way the row broke. */
|
|
21
|
+
.treeTools {
|
|
22
|
+
display: flex; align-items: center; flex-wrap: wrap; gap: 6px;
|
|
23
|
+
justify-content: space-between;
|
|
24
|
+
flex: none;
|
|
25
|
+
min-height: 37px; box-sizing: border-box;
|
|
26
|
+
padding: 6px var(--gs-gutter-pane);
|
|
27
|
+
border-bottom: 1px solid var(--gs-border-soft);
|
|
28
|
+
}
|
|
29
|
+
/* Wraps only when the group alone cannot fit the pane — five controls do not
|
|
30
|
+
go into 170px by any arrangement, and a clipped button is worse than a
|
|
31
|
+
second row. Above ~250px this never fires. */
|
|
32
|
+
.treeActions { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }
|
|
33
|
+
/* The root tick sits in the same column the rows read down, so it is pulled out
|
|
34
|
+
of the toolbar's gutter to the pane edge rather than aligned to the label. */
|
|
35
|
+
.treeLead { display: flex; align-items: center; gap: 4px; min-width: 0; }
|
|
36
|
+
.treeLabel {
|
|
37
|
+
flex: none;
|
|
38
|
+
white-space: nowrap;
|
|
39
|
+
color: var(--gs-fg-dim); font-size: var(--gs-t-dense);
|
|
40
|
+
}
|
|
41
|
+
/* The file filter, revealed under the tools rather than squeezed into them:
|
|
42
|
+
the pane drags down to 170px, where the tick, the count and two icon buttons
|
|
43
|
+
already own the row. It costs a row only while it is being used. */
|
|
44
|
+
.treeFilter {
|
|
45
|
+
position: relative;
|
|
46
|
+
flex: none;
|
|
47
|
+
display: flex; align-items: center;
|
|
48
|
+
padding: 6px var(--gs-gutter-pane);
|
|
49
|
+
border-bottom: 1px solid var(--gs-border-soft);
|
|
50
|
+
}
|
|
51
|
+
.treeFilterInput {
|
|
52
|
+
flex: 1 1 auto; min-width: 0;
|
|
53
|
+
height: var(--gs-h-compact); box-sizing: border-box;
|
|
54
|
+
padding: 0 24px 0 8px;
|
|
55
|
+
border: 1px solid var(--gs-border-soft); border-radius: var(--gs-r-control);
|
|
56
|
+
background: var(--gs-bg); color: var(--gs-fg);
|
|
57
|
+
font-family: inherit; font-size: var(--gs-t-dense);
|
|
58
|
+
outline: none;
|
|
59
|
+
}
|
|
60
|
+
.treeFilterInput:focus { border-color: var(--gs-accent); }
|
|
61
|
+
.treeFilterInput::placeholder { color: var(--gs-fg-faint); }
|
|
62
|
+
/* Inside the field's right padding, so clearing does not move the field or
|
|
63
|
+
change its width as the button comes and goes. */
|
|
64
|
+
.treeFilterClear {
|
|
65
|
+
position: absolute; right: calc(var(--gs-gutter-pane) + 6px);
|
|
66
|
+
display: flex; align-items: center; justify-content: center;
|
|
67
|
+
width: 16px; height: 16px;
|
|
68
|
+
padding: 0;
|
|
69
|
+
border: 0; border-radius: var(--gs-r-pill);
|
|
70
|
+
background: transparent; color: var(--gs-fg-faint);
|
|
71
|
+
font-size: var(--gs-t-ui); line-height: 1;
|
|
72
|
+
cursor: pointer;
|
|
73
|
+
}
|
|
74
|
+
.treeFilterClear:hover { color: var(--gs-fg); }
|
|
75
|
+
.tree {
|
|
76
|
+
flex: 1; min-height: 0;
|
|
77
|
+
margin: 0; padding: 6px 0 16px;
|
|
78
|
+
overflow: auto; list-style: none;
|
|
79
|
+
}
|
|
80
|
+
/* The list's stand-in while the view's file list is in flight: centred and
|
|
81
|
+
quiet, never a claim about how many changes exist. */
|
|
82
|
+
.treeEmpty {
|
|
83
|
+
flex: 1; min-height: 0;
|
|
84
|
+
display: flex; align-items: center; justify-content: center;
|
|
85
|
+
color: var(--gs-fg-faint); font-size: var(--gs-t-ui);
|
|
86
|
+
}
|
|
87
|
+
/* Nested lists are NOT .tree: only the root list scrolls, otherwise every level
|
|
88
|
+
would grow its own scrollbar. */
|
|
89
|
+
.treeSub {
|
|
90
|
+
position: relative;
|
|
91
|
+
margin: 0; padding: 0;
|
|
92
|
+
list-style: none;
|
|
93
|
+
}
|
|
94
|
+
/* One hairline per nesting level at the parent's chevron column, so a deep tree
|
|
95
|
+
reads as a tree rather than a ragged left edge. The offset comes from the row
|
|
96
|
+
that owns the level (see TREE_RAIL_OFFSET). */
|
|
97
|
+
.treeSub::before {
|
|
98
|
+
content: '';
|
|
99
|
+
position: absolute;
|
|
100
|
+
top: 0; bottom: 0; left: var(--gs-rail, 0);
|
|
101
|
+
width: 1px;
|
|
102
|
+
background: var(--gs-border-soft);
|
|
103
|
+
}
|
|
104
|
+
.treeDirLi { position: relative; }
|
|
105
|
+
.treeDir {
|
|
106
|
+
display: flex; align-items: center; gap: 6px;
|
|
107
|
+
box-sizing: border-box; width: 100%;
|
|
108
|
+
padding: 4px 12px 4px 8px;
|
|
109
|
+
border: 0;
|
|
110
|
+
border-radius: var(--gs-r-control);
|
|
111
|
+
background: transparent;
|
|
112
|
+
color: var(--gs-fg-dim);
|
|
113
|
+
font-family: inherit;
|
|
114
|
+
font-size: var(--gs-t-dense); font-weight: 600;
|
|
115
|
+
text-align: left;
|
|
116
|
+
cursor: pointer;
|
|
117
|
+
}
|
|
118
|
+
.treeDir:hover { background: var(--gs-raise); color: var(--gs-fg-muted); }
|
|
119
|
+
.treeDirActive { color: var(--gs-fg-muted); }
|
|
120
|
+
.chevron {
|
|
121
|
+
flex: none;
|
|
122
|
+
display: inline-block;
|
|
123
|
+
width: 10px;
|
|
124
|
+
color: var(--gs-fg-fainter);
|
|
125
|
+
font-size: var(--gs-t-meta);
|
|
126
|
+
transition: transform 120ms ease;
|
|
127
|
+
}
|
|
128
|
+
.chevronOpen { transform: rotate(90deg); }
|
|
129
|
+
.treeDirName {
|
|
130
|
+
flex: 1; min-width: 0;
|
|
131
|
+
overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
|
|
132
|
+
}
|
|
133
|
+
.treeDirCount {
|
|
134
|
+
flex: none;
|
|
135
|
+
padding: 0 6px;
|
|
136
|
+
border-radius: var(--gs-r-pill);
|
|
137
|
+
background: var(--gs-neutral-bg);
|
|
138
|
+
color: var(--gs-fg-dim);
|
|
139
|
+
font-size: var(--gs-t-meta); font-weight: 600; line-height: 16px;
|
|
140
|
+
}
|
|
141
|
+
.treeDirCounts { flex: none; font-size: var(--gs-t-meta); font-variant-numeric: tabular-nums; }
|
|
142
|
+
.file {
|
|
143
|
+
display: flex; align-items: center; gap: 8px;
|
|
144
|
+
box-sizing: border-box; width: 100%;
|
|
145
|
+
padding: 5px 14px;
|
|
146
|
+
border: 0;
|
|
147
|
+
border-left: 2px solid transparent;
|
|
148
|
+
border-radius: var(--gs-r-control);
|
|
149
|
+
background: transparent;
|
|
150
|
+
color: var(--gs-fg-muted);
|
|
151
|
+
font-family: inherit;
|
|
152
|
+
font-size: var(--gs-t-dense);
|
|
153
|
+
text-align: left;
|
|
154
|
+
cursor: pointer;
|
|
155
|
+
}
|
|
156
|
+
.file:hover { background: var(--gs-raise); }
|
|
157
|
+
/* .fileActive: see the shared selected-state rule in controls.css. */
|
|
158
|
+
|
|
159
|
+
.fileStatus {
|
|
160
|
+
flex: none;
|
|
161
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
162
|
+
width: 18px; height: 18px;
|
|
163
|
+
border-radius: var(--gs-r-control);
|
|
164
|
+
font-weight: 700; font-size: var(--gs-t-meta); line-height: 1;
|
|
165
|
+
}
|
|
166
|
+
.stAdded, .stUntracked { color: var(--gs-add); background: var(--gs-add-bg); }
|
|
167
|
+
.stDeleted { color: var(--gs-del); background: var(--gs-del-bg); }
|
|
168
|
+
.stModified { color: var(--gs-warn); background: var(--gs-warn-bg); }
|
|
169
|
+
.stRenamed { color: var(--gs-info); background: var(--gs-info-bg); }
|
|
170
|
+
|
|
171
|
+
.filePath { flex: 1; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
172
|
+
.fileBinary {
|
|
173
|
+
flex: none;
|
|
174
|
+
padding: 0 5px;
|
|
175
|
+
border-radius: var(--gs-r-control);
|
|
176
|
+
background: var(--gs-neutral-bg);
|
|
177
|
+
color: var(--gs-fg-dim);
|
|
178
|
+
font-size: 9px; font-weight: 700; letter-spacing: 0.06em;
|
|
179
|
+
}
|
|
180
|
+
.fileCounts { flex: none; font-size: var(--gs-t-meta); font-variant-numeric: tabular-nums; }
|
|
181
|
+
.fileCountAdd { color: var(--gs-add); }
|
|
182
|
+
.fileCountDel { color: var(--gs-del); }
|
|
183
|
+
|
|
184
|
+
/* diff pane */
|
|
185
|
+
/* A column, so a header can sit above the scrolled diff instead of inside it.
|
|
186
|
+
`overflow: auto` stays as the outer guard — the rename line and the
|
|
187
|
+
side-by-side pane are also children here, and one of them growing must
|
|
188
|
+
scroll rather than be clipped. */
|
|
189
|
+
.diffPane {
|
|
190
|
+
flex: 1 1 0; min-width: var(--gs-min-diff);
|
|
191
|
+
display: flex; flex-direction: column;
|
|
192
|
+
overflow: auto; background: var(--gs-surface);
|
|
193
|
+
}
|
|
194
|
+
/* The unified view: its own header and its own scroller, filling whatever the
|
|
195
|
+
pane has left after a rename line. */
|
|
196
|
+
.diffWrap { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; width: 100%; }
|
|
197
|
+
.diffScroll { flex: 1 1 0; min-height: 0; overflow: auto; outline: none; }
|
|
198
|
+
/* The walk's controls. Right-aligned and quiet: they belong to the diff, and
|
|
199
|
+
the diff is what the reader came for. Same shape as `.sideNav` in the
|
|
200
|
+
side-by-side pane, because it is the same control. */
|
|
201
|
+
.diffNav {
|
|
202
|
+
flex: none;
|
|
203
|
+
display: flex; justify-content: flex-end; align-items: center; gap: 4px;
|
|
204
|
+
padding: 3px 12px 3px 0;
|
|
205
|
+
border-bottom: 1px solid var(--gs-border-soft);
|
|
206
|
+
}
|
|
207
|
+
.diffNav .blockBtn { padding-inline: 6px; line-height: 0; }
|
|
208
|
+
/* The pre is as wide as its longest line (`max-content`) and at least the pane
|
|
209
|
+
(`min-width: 100%`). Each row then stretches to that width, so add/delete
|
|
210
|
+
tints survive horizontal scroll instead of stopping at the viewport edge. */
|
|
211
|
+
.diffPre {
|
|
212
|
+
display: flex; flex-direction: column;
|
|
213
|
+
margin: 0; padding: 8px 0 16px;
|
|
214
|
+
min-width: 100%; width: max-content;
|
|
215
|
+
/* GitHub's blob stack — Cascadia ligatures and a 16px leading were why this
|
|
216
|
+
read as a terminal dump instead of a pull-request diff. */
|
|
217
|
+
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
|
|
218
|
+
font-size: var(--gs-t-dense); line-height: 20px; font-weight: 400;
|
|
219
|
+
font-variant-ligatures: none;
|
|
220
|
+
font-feature-settings: 'liga' 0, 'calt' 0;
|
|
221
|
+
tab-size: 4;
|
|
222
|
+
}
|
|
223
|
+
.renameLine {
|
|
224
|
+
flex: none;
|
|
225
|
+
padding: 8px 16px;
|
|
226
|
+
border-bottom: 1px solid var(--gs-border-soft);
|
|
227
|
+
color: var(--gs-info);
|
|
228
|
+
font-size: var(--gs-t-dense);
|
|
229
|
+
}
|
|
230
|
+
/* The old path is literal content, not prose: give it the body colour so it
|
|
231
|
+
reads louder than the sentence around it. */
|
|
232
|
+
.renameLine code { color: var(--gs-fg); }
|
|
233
|
+
|
|
234
|
+
/* Each line: [old#][new#][sign][code]. Stretch, don't baseline — empty line-number
|
|
235
|
+
cells plus nested syntax spans used to inflate the row into prose leading. */
|
|
236
|
+
.line {
|
|
237
|
+
display: flex; align-items: stretch;
|
|
238
|
+
width: max-content; min-width: 100%;
|
|
239
|
+
min-height: 20px; line-height: 20px;
|
|
240
|
+
white-space: nowrap;
|
|
241
|
+
}
|
|
242
|
+
.lnOld, .lnNew {
|
|
243
|
+
flex: none;
|
|
244
|
+
box-sizing: border-box;
|
|
245
|
+
width: 3.2em;
|
|
246
|
+
padding: 0 8px;
|
|
247
|
+
text-align: right;
|
|
248
|
+
color: var(--gs-fg-fainter);
|
|
249
|
+
user-select: none;
|
|
250
|
+
font-variant-numeric: tabular-nums;
|
|
251
|
+
}
|
|
252
|
+
.line:hover .lnOld, .line:hover .lnNew { color: var(--gs-fg-dim); }
|
|
253
|
+
/* No rule down the right edge: GitHub carries the +/- as the first glyph of the
|
|
254
|
+
code cell and draws nothing between the columns. The rule that used to be
|
|
255
|
+
here cut every tinted row in half, which is most of why a full-file addition
|
|
256
|
+
read as two stacked green bands rather than one block of new code. */
|
|
257
|
+
.gutter {
|
|
258
|
+
flex: none;
|
|
259
|
+
width: 1.4em;
|
|
260
|
+
text-align: center;
|
|
261
|
+
user-select: none;
|
|
262
|
+
}
|
|
263
|
+
.code { flex: 1 0 auto; white-space: pre; padding: 0 16px 0 10px; }
|
|
264
|
+
|
|
265
|
+
.lineAdd { background: var(--gs-add-line); }
|
|
266
|
+
.lineDel { background: var(--gs-del-line); }
|
|
267
|
+
.lineAdd .lnOld, .lineAdd .lnNew, .lineAdd .gutter { background: var(--gs-add-num, var(--gs-add-line)); }
|
|
268
|
+
.lineDel .lnOld, .lineDel .lnNew, .lineDel .gutter { background: var(--gs-del-num, var(--gs-del-line)); }
|
|
269
|
+
.lineContext { color: var(--gs-fg-muted); }
|
|
270
|
+
/* GitHub's hunk header is quiet: a faint accent wash on the row, muted grey
|
|
271
|
+
text, and the saturated blue confined to the number cells. It used to run the
|
|
272
|
+
accent colour across the whole line, which made every hunk boundary shout
|
|
273
|
+
louder than the code it introduces. */
|
|
274
|
+
.lineHunk { background: var(--gs-hunk); }
|
|
275
|
+
.lineHunk .code { color: var(--gs-fg-dim); }
|
|
276
|
+
.lineHunk .lnOld, .lineHunk .lnNew, .lineHunk .gutter { background: var(--gs-hunk-num, var(--gs-hunk)); }
|
|
277
|
+
.signAdd { color: var(--gs-add); }
|
|
278
|
+
.signDel { color: var(--gs-del); }
|
|
279
|
+
|
|
280
|
+
.wordAdd { background: var(--gs-add-word); border-radius: 2px; }
|
|
281
|
+
.wordDel { background: var(--gs-del-word); border-radius: 2px; }
|
|
282
|
+
|
|
283
|
+
/* Side-by-side diff pane. One grid holds EVERY row's four cells — a per-row
|
|
284
|
+
grid would let the column boundary drift with each row's content, and a
|
|
285
|
+
fixed split would clip long lines. Equal 1fr code tracks keep the two
|
|
286
|
+
columns identically wide however wide the widest line is, and the whole pane
|
|
287
|
+
(this view's single shared vertical scroll) carries the horizontal one. */
|
|
288
|
+
/* The pane is a HEADER plus a scroller, not one wide column: the tab row
|
|
289
|
+
carries Save/Revert, and a row sized to the widest line of code pushes them
|
|
290
|
+
off the right edge of a long file — reachable only by scrolling sideways,
|
|
291
|
+
which is not where anyone looks for a toolbar. Only `.sideScroll` scrolls;
|
|
292
|
+
the tabs and the notices above it stay put on both axes. */
|
|
293
|
+
.sidePane { display: flex; flex-direction: column; width: 100%; min-width: 0; height: 100%; }
|
|
294
|
+
/* Vertical scrolling belongs to the pane; horizontal belongs to each column,
|
|
295
|
+
which is what lets the divider mean something. A single scroller for both
|
|
296
|
+
axes would tie the two sides' horizontal position together and size the
|
|
297
|
+
whole grid to the widest line in the file. */
|
|
298
|
+
.sideScroll { flex: 1 1 0; min-height: 0; overflow-y: auto; overflow-x: hidden; }
|
|
299
|
+
.sideCols { display: flex; align-items: stretch; min-height: 100%; }
|
|
300
|
+
/* `min-width: 0` is what lets a flex column be narrower than its content —
|
|
301
|
+
without it the column refuses to shrink and the divider stops moving. */
|
|
302
|
+
.sideCol { flex: 0 0 auto; min-width: 0; overflow-x: auto; }
|
|
303
|
+
.sideColRight { flex: 1 1 0; }
|
|
304
|
+
.sideColGrid {
|
|
305
|
+
display: grid;
|
|
306
|
+
grid-template-columns: 3.2em 1fr;
|
|
307
|
+
align-content: start;
|
|
308
|
+
padding: 8px 0 16px;
|
|
309
|
+
min-width: 100%; width: max-content;
|
|
310
|
+
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
|
|
311
|
+
font-size: var(--gs-t-dense); line-height: 20px; font-weight: 400;
|
|
312
|
+
font-variant-ligatures: none;
|
|
313
|
+
font-feature-settings: 'liga' 0, 'calt' 0;
|
|
314
|
+
tab-size: 4;
|
|
315
|
+
}
|
|
316
|
+
/* The blame gutter, on the working-tree column only. Fixed width so the code
|
|
317
|
+
beside it does not shift line to line, and clipped rather than wrapped: a
|
|
318
|
+
gutter row that grew to two lines would break the alignment the whole
|
|
319
|
+
side-by-side view rests on. */
|
|
320
|
+
.sideColGridBlame { grid-template-columns: 13em 3.2em 1fr; }
|
|
321
|
+
.blameCell {
|
|
322
|
+
box-sizing: border-box;
|
|
323
|
+
padding: 0 8px 0 10px;
|
|
324
|
+
border-right: 1px solid var(--gs-border-soft);
|
|
325
|
+
color: var(--gs-fg-faint);
|
|
326
|
+
font-size: var(--gs-t-meta);
|
|
327
|
+
white-space: pre; overflow: hidden; text-overflow: ellipsis;
|
|
328
|
+
user-select: none;
|
|
329
|
+
}
|
|
330
|
+
.sideTabs {
|
|
331
|
+
z-index: 5;
|
|
332
|
+
display: flex; column-gap: 16px; row-gap: 0; align-items: flex-start;
|
|
333
|
+
flex: none; flex-wrap: wrap;
|
|
334
|
+
padding: 6px 16px 0;
|
|
335
|
+
border-bottom: 1px solid var(--gs-border-soft);
|
|
336
|
+
background: var(--gs-surface-2);
|
|
337
|
+
}
|
|
338
|
+
.sideTab {
|
|
339
|
+
position: relative;
|
|
340
|
+
padding: 2px 0 5px;
|
|
341
|
+
border: none;
|
|
342
|
+
background: transparent;
|
|
343
|
+
font-family: inherit;
|
|
344
|
+
font-size: var(--gs-t-meta); line-height: 16px; font-weight: 500;
|
|
345
|
+
color: var(--gs-fg-dim);
|
|
346
|
+
cursor: pointer;
|
|
347
|
+
}
|
|
348
|
+
.sideTab::after {
|
|
349
|
+
content: '';
|
|
350
|
+
position: absolute; right: 0; bottom: 1px; left: 0;
|
|
351
|
+
height: 2px; border-radius: 2px;
|
|
352
|
+
background: transparent;
|
|
353
|
+
}
|
|
354
|
+
.sideTabActive { color: var(--gs-accent); }
|
|
355
|
+
.sideTabActive::after { background: var(--gs-accent); }
|
|
356
|
+
.sideNotice {
|
|
357
|
+
flex: none;
|
|
358
|
+
padding: 8px 16px;
|
|
359
|
+
border-bottom: 1px solid var(--gs-border-soft);
|
|
360
|
+
color: var(--gs-info);
|
|
361
|
+
font-size: var(--gs-t-dense);
|
|
362
|
+
}
|
|
363
|
+
.sideNum {
|
|
364
|
+
box-sizing: border-box;
|
|
365
|
+
padding: 0 8px;
|
|
366
|
+
text-align: right;
|
|
367
|
+
color: var(--gs-fg-fainter);
|
|
368
|
+
user-select: none;
|
|
369
|
+
font-variant-numeric: tabular-nums;
|
|
370
|
+
}
|
|
371
|
+
.sideNumAdd { background: var(--gs-add-num, var(--gs-add-line)); }
|
|
372
|
+
.sideNumDel { background: var(--gs-del-num, var(--gs-del-line)); }
|
|
373
|
+
.sideCode { white-space: pre; padding: 0 16px 0 10px; min-height: 20px; }
|
|
374
|
+
/* Stands in for the rows outside the window, so the scrollbar is the length of
|
|
375
|
+
the FILE rather than of whatever is currently rendered. Spans every column,
|
|
376
|
+
including the blame gutter's. */
|
|
377
|
+
.sideSpacer { grid-column: 1 / -1; }
|
|
378
|
+
/* The same, for the unified view's flex column. `flex: none` so it is exactly
|
|
379
|
+
the height it claims rather than one stretched by its siblings. */
|
|
380
|
+
.diffSpacer { flex: none; }
|
|
381
|
+
.sideCodeSame { color: var(--gs-fg-muted); }
|
|
382
|
+
.sideCodeAdd { background: var(--gs-add-line); }
|
|
383
|
+
.sideCodeDel { background: var(--gs-del-line); }
|
|
384
|
+
/* The block affordance outlines one WHOLE changed region, not every row. The
|
|
385
|
+
number cell owns the outer left edge, the code cell the right, and only the
|
|
386
|
+
first/last present cells add horizontal edges. One-sided changes paint no
|
|
387
|
+
cage in the empty opposite pane. `.sideCellBlock` positions the action bar. */
|
|
388
|
+
.sideCellBlock { position: relative; }
|
|
389
|
+
.sideNum.sideBlockHot { box-shadow: inset 1px 0 var(--gs-accent); }
|
|
390
|
+
.sideCode.sideBlockHot { box-shadow: inset -1px 0 var(--gs-accent); }
|
|
391
|
+
.sideNum.sideBlockHot.sideBlockHotFirst { box-shadow: inset 1px 0 var(--gs-accent), inset 0 1px var(--gs-accent); }
|
|
392
|
+
.sideCode.sideBlockHot.sideBlockHotFirst { box-shadow: inset -1px 0 var(--gs-accent), inset 0 1px var(--gs-accent); }
|
|
393
|
+
.sideNum.sideBlockHot.sideBlockHotLast { box-shadow: inset 1px 0 var(--gs-accent), inset 0 -1px var(--gs-accent); }
|
|
394
|
+
.sideCode.sideBlockHot.sideBlockHotLast { box-shadow: inset -1px 0 var(--gs-accent), inset 0 -1px var(--gs-accent); }
|
|
395
|
+
.sideNum.sideBlockHot.sideBlockHotFirst.sideBlockHotLast { box-shadow: inset 1px 0 var(--gs-accent), inset 0 1px var(--gs-accent), inset 0 -1px var(--gs-accent); }
|
|
396
|
+
.sideCode.sideBlockHot.sideBlockHotFirst.sideBlockHotLast { box-shadow: inset -1px 0 var(--gs-accent), inset 0 1px var(--gs-accent), inset 0 -1px var(--gs-accent); }
|
|
397
|
+
|
|
398
|
+
/* The hovered block's actions, floating on the row above the block. It lives
|
|
399
|
+
INSIDE the block's first code cell, so it travels with the column and the
|
|
400
|
+
scroll, and it only ever covers context — never the change it acts on. The
|
|
401
|
+
surface and shadow are what keep it readable over code. */
|
|
402
|
+
.blockBar {
|
|
403
|
+
position: absolute; left: 0; z-index: 4;
|
|
404
|
+
display: flex; gap: 4px;
|
|
405
|
+
padding: 2px;
|
|
406
|
+
border: 1px solid var(--gs-border);
|
|
407
|
+
border-radius: var(--gs-r-control);
|
|
408
|
+
background: var(--gs-surface-2, var(--gs-bg));
|
|
409
|
+
box-shadow: 0 4px 14px var(--gs-shadow);
|
|
410
|
+
white-space: nowrap;
|
|
411
|
+
}
|
|
412
|
+
.blockBtn {
|
|
413
|
+
padding: 0 8px;
|
|
414
|
+
border: 1px solid var(--gs-border); border-radius: var(--gs-r-pill);
|
|
415
|
+
background: var(--gs-bg); color: var(--gs-fg-dim);
|
|
416
|
+
font-family: inherit; font-size: var(--gs-t-meta); line-height: 16px;
|
|
417
|
+
cursor: pointer;
|
|
418
|
+
}
|
|
419
|
+
.blockBtn:hover:not(:disabled) { color: var(--gs-fg); border-color: var(--gs-accent); }
|
|
420
|
+
.blockBtn:disabled { opacity: 0.45; cursor: default; }
|
|
421
|
+
/* The roll-back button stays red at rest, not only on hover: it is the one
|
|
422
|
+
action in the bar that cannot be undone, and arriving under the pointer is
|
|
423
|
+
not what should make that visible. */
|
|
424
|
+
.blockBtnDanger { color: var(--gs-del); border-color: var(--gs-del-bg, var(--gs-border)); }
|
|
425
|
+
.blockBtnDanger:hover:not(:disabled) { color: var(--gs-del); border-color: var(--gs-del); }
|
|
426
|
+
|
|
427
|
+
/* The editable right column. The working-tree column is readable but visibly
|
|
428
|
+
a text surface before editing is armed; the tab row carries the arm/save
|
|
429
|
+
cluster at its right end. */
|
|
430
|
+
.sideArmable { cursor: text; }
|
|
431
|
+
|
|
432
|
+
/* An editable buffer says so — the one thing a rendered code pane cannot say
|
|
433
|
+
by itself.
|
|
434
|
+
|
|
435
|
+
Nothing about monospaced text on a background looks like a field. In the
|
|
436
|
+
Files tab the editor is live the moment the file opens, so without a mark
|
|
437
|
+
the reader finds out whether they may type by typing — and finds out they
|
|
438
|
+
may not the same way, on a pane that silently swallows it. The rule runs
|
|
439
|
+
the full height of the editor's leading edge: dim while nothing is focused
|
|
440
|
+
(present, not loud — it sits beside code someone came to read), full accent
|
|
441
|
+
once the caret is actually inside, which doubles as "this pane has the
|
|
442
|
+
keyboard".
|
|
443
|
+
|
|
444
|
+
Driven by an attribute the component sets from the same boolean that
|
|
445
|
+
configures `EditorState.readOnly`, so the rule cannot appear on a pane that
|
|
446
|
+
would refuse the keystroke. `pointer-events: none` because the bar overlays
|
|
447
|
+
the gutter's first two pixels, and a click there must still reach the
|
|
448
|
+
view. */
|
|
449
|
+
.cmHost { position: relative; }
|
|
450
|
+
.cmHost[data-editable]::before {
|
|
451
|
+
content: '';
|
|
452
|
+
position: absolute;
|
|
453
|
+
top: 0; bottom: 0; left: 0;
|
|
454
|
+
width: 2px;
|
|
455
|
+
border-radius: 1px;
|
|
456
|
+
background: var(--gs-accent);
|
|
457
|
+
opacity: 0.32;
|
|
458
|
+
transition: opacity 140ms ease;
|
|
459
|
+
pointer-events: none;
|
|
460
|
+
/* Above the gutter, which CodeMirror stacks at 200. The bar overlays its
|
|
461
|
+
first two pixels, so at the default z-index an opaque gutter buries it. */
|
|
462
|
+
z-index: 201;
|
|
463
|
+
}
|
|
464
|
+
.cmHost[data-editable]:focus-within::before { opacity: 1; }
|
|
465
|
+
/* The change nav takes the free space, so it and `.sideActions` after it sit
|
|
466
|
+
together at the right end. Only the FIRST auto margin absorbs anything —
|
|
467
|
+
the one on `.sideActions` then has nothing left to take, which is what
|
|
468
|
+
keeps the two groups adjacent rather than pushed to opposite ends. */
|
|
469
|
+
.sideNav { margin-left: auto; display: flex; gap: 4px; align-items: center; padding: 2px 0 5px; }
|
|
470
|
+
.sideNav .blockBtn { padding-inline: 6px; line-height: 0; }
|
|
471
|
+
.sideNavCount { color: var(--gs-fg-faint); font-size: var(--gs-t-meta); white-space: nowrap; }
|
|
472
|
+
.sideCurrentBlockActions { margin-left: 4px; display: flex; flex-wrap: wrap; gap: 4px; align-items: flex-start; padding: 2px 0 5px; }
|
|
473
|
+
.sideActions { margin-left: auto; display: flex; gap: 4px; align-items: flex-start; padding: 2px 0 5px; }
|
|
474
|
+
.sideActionsAdjacent { margin-left: 0; }
|
|
475
|
+
/* Save gains the accent only once there is something to save: the button's
|
|
476
|
+
whole enabled state is the dirty flag, and colour says it sooner than the
|
|
477
|
+
pointer does. */
|
|
478
|
+
.sideSaveReady { color: var(--gs-accent); border-color: var(--gs-accent); }
|
|
479
|
+
|
|
480
|
+
/* The reload-or-overwrite banner (§4) and the failed-save banner: a warning
|
|
481
|
+
strip under the tabs carrying its own actions, so the decision it asks for
|
|
482
|
+
is one click away rather than a sentence away. */
|
|
483
|
+
.sideBanner {
|
|
484
|
+
flex: none;
|
|
485
|
+
display: flex; flex-direction: column; gap: 6px;
|
|
486
|
+
padding: 8px 16px;
|
|
487
|
+
border-bottom: 1px solid var(--gs-border-soft);
|
|
488
|
+
background: var(--gs-del-bg, transparent);
|
|
489
|
+
color: var(--gs-del);
|
|
490
|
+
font-size: var(--gs-t-dense);
|
|
491
|
+
}
|
|
492
|
+
.sideBannerTitle { font-weight: 600; }
|
|
493
|
+
.sideBannerActs { display: flex; gap: 4px; }
|
|
494
|
+
|
|
495
|
+
/* The editor grid item that positions the textarea. It spans exactly the
|
|
496
|
+
buffer's rows of the fourth column; the textarea fills it absolutely. */
|
|
497
|
+
.sideEditCell { position: relative; }
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
/* No syntax-tint classes here: Shiki resolves a real TextMate theme and emits a
|
|
501
|
+
hex per token, so the panel writes colour inline. A four-class approximation
|
|
502
|
+
painted identifiers, types and punctuation all one colour, which is what made
|
|
503
|
+
the diff look unhighlighted next to GitHub's. */
|
|
504
|
+
|
|
505
|
+
.empty { padding: 32px 24px; color: var(--gs-fg-faint); font-size: var(--gs-t-ui); }
|