@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,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); }