@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,67 @@
|
|
|
1
|
+
/* ============================ image preview ============================= */
|
|
2
|
+
|
|
3
|
+
/* A picture fills the pane and its facts sit under it, because the facts are
|
|
4
|
+
the reason this is a viewer rather than an image tag: a screenshot in a
|
|
5
|
+
repository is usually opened to check a size or a crop. */
|
|
6
|
+
.imgPane {
|
|
7
|
+
display: flex;
|
|
8
|
+
flex-direction: column;
|
|
9
|
+
min-height: 0;
|
|
10
|
+
height: 100%;
|
|
11
|
+
gap: 8px;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* The checkerboard is not decoration. A transparent PNG on a flat background
|
|
15
|
+
is indistinguishable from an opaque one of that colour, and "is this
|
|
16
|
+
transparent" is the question the file was opened to answer. Two overlaid
|
|
17
|
+
gradients rather than an asset: no request, and it takes the theme. */
|
|
18
|
+
.imgStage {
|
|
19
|
+
flex: 1 1 auto;
|
|
20
|
+
min-height: 0;
|
|
21
|
+
display: flex;
|
|
22
|
+
align-items: center;
|
|
23
|
+
justify-content: center;
|
|
24
|
+
overflow: auto;
|
|
25
|
+
padding: 12px;
|
|
26
|
+
border-radius: var(--gs-r-control);
|
|
27
|
+
background-color: var(--gs-surface-2);
|
|
28
|
+
background-image:
|
|
29
|
+
linear-gradient(45deg, var(--gs-border-soft) 25%, transparent 25%, transparent 75%, var(--gs-border-soft) 75%),
|
|
30
|
+
linear-gradient(45deg, var(--gs-border-soft) 25%, transparent 25%, transparent 75%, var(--gs-border-soft) 75%);
|
|
31
|
+
background-size: 16px 16px;
|
|
32
|
+
background-position: 0 0, 8px 8px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* Fit is the default: an 8000px render opened at full size shows its top-left
|
|
36
|
+
corner, which looks like a broken image rather than a big one. */
|
|
37
|
+
.imgShot {
|
|
38
|
+
max-width: 100%;
|
|
39
|
+
max-height: 100%;
|
|
40
|
+
object-fit: contain;
|
|
41
|
+
/* Nearest-neighbour would be right for icons and wrong for photographs, and
|
|
42
|
+
the pane cannot tell them apart; smooth is the safe half of that trade. */
|
|
43
|
+
image-rendering: auto;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* Actual size: the scaling comes off and the stage scrolls instead. Written
|
|
47
|
+
as a descendant of the stage's own state so the picture keeps ONE class,
|
|
48
|
+
and the two views cannot disagree about which is in force. */
|
|
49
|
+
.imgStage[data-actual] {
|
|
50
|
+
align-items: flex-start;
|
|
51
|
+
justify-content: flex-start;
|
|
52
|
+
}
|
|
53
|
+
.imgStage[data-actual] .imgShot {
|
|
54
|
+
max-width: none;
|
|
55
|
+
max-height: none;
|
|
56
|
+
object-fit: none;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.imgCaption {
|
|
60
|
+
flex: none;
|
|
61
|
+
display: flex;
|
|
62
|
+
align-items: center;
|
|
63
|
+
gap: 8px;
|
|
64
|
+
font-size: var(--gs-t-meta);
|
|
65
|
+
color: var(--gs-fg-dim);
|
|
66
|
+
font-variant-numeric: tabular-nums;
|
|
67
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/* ============================ write operations ============================ */
|
|
2
|
+
|
|
3
|
+
/* Sync bar: fetch / pull / push, with the divergence they act on. A chrome bar
|
|
4
|
+
like the others, so it takes the drawer gutter and not a gutter of its own. */
|
|
5
|
+
.syncBar {
|
|
6
|
+
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
|
|
7
|
+
flex: none;
|
|
8
|
+
padding: 8px var(--gs-gutter);
|
|
9
|
+
border-bottom: 1px solid var(--gs-border-soft);
|
|
10
|
+
background: var(--gs-surface-2);
|
|
11
|
+
font-size: var(--gs-t-dense);
|
|
12
|
+
color: var(--gs-fg-dim);
|
|
13
|
+
}
|
|
14
|
+
.syncUpstream {
|
|
15
|
+
max-width: 260px;
|
|
16
|
+
overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
|
|
17
|
+
color: var(--gs-fg-muted);
|
|
18
|
+
font-variant-numeric: tabular-nums;
|
|
19
|
+
}
|
|
20
|
+
.syncLevel { color: var(--gs-fg-faint); }
|
|
21
|
+
.syncSpacer { flex: 1 1 auto; }
|
|
22
|
+
.btn:disabled, .treeIcon:disabled, .miniBtn:disabled, .refButton:disabled, .funnelButton:disabled {
|
|
23
|
+
opacity: 0.45;
|
|
24
|
+
cursor: default;
|
|
25
|
+
}
|
|
26
|
+
/* A control disabled ONLY by an operation too young to report keeps its full
|
|
27
|
+
strength. Every tick stages through git, and fading the whole sync row for
|
|
28
|
+
the ~150ms that takes was a blink, not feedback — nobody can act inside it.
|
|
29
|
+
The click is still refused from the first frame; see `op-feedback.ts`. */
|
|
30
|
+
.btn[data-quiet]:disabled, .refButton[data-quiet]:disabled { opacity: 1; }
|
|
31
|
+
|
|
32
|
+
/* Outcome of the last operation. Pre-wrap because git's own message rides along
|
|
33
|
+
under the explanation, and it arrives with its newlines. */
|
|
34
|
+
.opBanner {
|
|
35
|
+
flex: none;
|
|
36
|
+
padding: 8px var(--gs-gutter);
|
|
37
|
+
border-bottom: 1px solid var(--gs-border-soft);
|
|
38
|
+
font-size: var(--gs-t-dense); line-height: 18px;
|
|
39
|
+
white-space: pre-wrap; overflow-wrap: anywhere;
|
|
40
|
+
user-select: text;
|
|
41
|
+
}
|
|
42
|
+
.opBannerOk { background: var(--gs-add-bg); color: var(--gs-add); }
|
|
43
|
+
.opBannerBad { background: var(--gs-del-bg); color: var(--gs-del); }
|
|
44
|
+
|
|
45
|
+
/* A file row and its stage toggle. The row keeps its own indent padding, so the
|
|
46
|
+
toggle trails it rather than displacing the tree's left edge. */
|
|
47
|
+
.fileLi { display: flex; align-items: stretch; }
|
|
48
|
+
.fileLi .file { flex: 1 1 auto; min-width: 0; }
|
|
49
|
+
/* Roll back, in the same 22px slot the tick occupies on the other side — the
|
|
50
|
+
row already has two gutters, and a third would eat the name.
|
|
51
|
+
It holds its width at all times and only fades in on hover or keyboard
|
|
52
|
+
focus: revealing it by `display` would re-flow the name column under the
|
|
53
|
+
pointer, and a destructive control that shifts the row as it appears is one
|
|
54
|
+
the pointer arrives at by accident. */
|
|
55
|
+
.fileDiscard {
|
|
56
|
+
flex: none;
|
|
57
|
+
display: flex; align-items: center; justify-content: center;
|
|
58
|
+
width: 22px;
|
|
59
|
+
padding: 0;
|
|
60
|
+
border: 0;
|
|
61
|
+
background: transparent;
|
|
62
|
+
color: var(--gs-fg-faint);
|
|
63
|
+
opacity: 0;
|
|
64
|
+
cursor: pointer;
|
|
65
|
+
transition: opacity 120ms ease, color 120ms ease;
|
|
66
|
+
}
|
|
67
|
+
.fileLi:hover .fileDiscard, .fileDiscard:focus-visible { opacity: 1; }
|
|
68
|
+
/* Red on hover, and only on hover. Standing red would make every row of a
|
|
69
|
+
working tree look like a warning; arriving under the pointer is where the
|
|
70
|
+
colour has something to say. */
|
|
71
|
+
.fileDiscard:hover { color: var(--gs-del); }
|
|
72
|
+
.fileDiscard:focus-visible { outline: 2px solid var(--gs-accent); outline-offset: -2px; border-radius: var(--gs-r-control); }
|
|
73
|
+
/* Directory rows need the same two-slot row as files, but their subtree list
|
|
74
|
+
has to stay a child of the <li> — hence a wrapper around just the row. */
|
|
75
|
+
.treeRow { display: flex; align-items: stretch; }
|
|
76
|
+
.treeRow .treeDir { flex: 1 1 auto; min-width: 0; }
|
|
77
|
+
|
|
78
|
+
/* ---- the commit tick ----
|
|
79
|
+
*
|
|
80
|
+
* The drawer presents a commit the way IDEA does: a set of ticks, not a staging
|
|
81
|
+
* area. A tick is `git add` the moment it is clicked, so the tree and
|
|
82
|
+
* `git status` can never disagree.
|
|
83
|
+
*
|
|
84
|
+
* The column does NOT indent with the tree. Depth already costs room in a pane
|
|
85
|
+
* that drags to 170px, and a straight column down the left edge is what makes
|
|
86
|
+
* "what am I about to commit" readable in one glance — which is why the ticks
|
|
87
|
+
* exist. Its width is TREE_CHECK_W; the row indents start after it. */
|
|
88
|
+
.checkBox {
|
|
89
|
+
flex: none;
|
|
90
|
+
display: flex; align-items: center; justify-content: center;
|
|
91
|
+
width: 22px;
|
|
92
|
+
padding: 0;
|
|
93
|
+
border: 0;
|
|
94
|
+
background: transparent;
|
|
95
|
+
cursor: pointer;
|
|
96
|
+
}
|
|
97
|
+
.checkMark {
|
|
98
|
+
display: flex; align-items: center; justify-content: center;
|
|
99
|
+
box-sizing: border-box;
|
|
100
|
+
width: 14px; height: 14px;
|
|
101
|
+
border: 1px solid var(--gs-fg-faint); border-radius: 3px;
|
|
102
|
+
background: var(--gs-bg);
|
|
103
|
+
color: transparent;
|
|
104
|
+
font-size: 10px; font-weight: 700; line-height: 1;
|
|
105
|
+
transition: background 120ms ease, border-color 120ms ease;
|
|
106
|
+
}
|
|
107
|
+
.checkBox:hover .checkMark { border-color: var(--gs-accent); }
|
|
108
|
+
.checkBox:focus-visible { outline: none; }
|
|
109
|
+
.checkBox:focus-visible .checkMark { outline: 2px solid var(--gs-accent); outline-offset: 1px; }
|
|
110
|
+
/* Ticked and half-ticked share a filled box: both mean "some of this is going
|
|
111
|
+
into the commit", and the glyph inside says which. */
|
|
112
|
+
.checkMarkOn, .checkMarkPartial {
|
|
113
|
+
border-color: var(--gs-accent);
|
|
114
|
+
background: var(--gs-accent);
|
|
115
|
+
color: var(--gs-on-accent);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* Commit box: pinned under the tree, so the message stays reachable however far
|
|
119
|
+
the file list scrolls. */
|
|
120
|
+
.commitBox {
|
|
121
|
+
flex: none;
|
|
122
|
+
display: flex; flex-direction: column; gap: 6px;
|
|
123
|
+
padding: 8px var(--gs-gutter-pane) 10px;
|
|
124
|
+
border-top: 1px solid var(--gs-border);
|
|
125
|
+
background: var(--gs-surface-2);
|
|
126
|
+
}
|
|
127
|
+
.commitMessage {
|
|
128
|
+
width: 100%; box-sizing: border-box;
|
|
129
|
+
min-height: 46px; max-height: 160px;
|
|
130
|
+
padding: 6px 8px;
|
|
131
|
+
border: 1px solid var(--gs-border); border-radius: var(--gs-r-control);
|
|
132
|
+
background: var(--gs-bg); color: var(--gs-fg);
|
|
133
|
+
font-family: inherit; font-size: var(--gs-t-dense); line-height: 18px;
|
|
134
|
+
resize: vertical;
|
|
135
|
+
outline: none;
|
|
136
|
+
}
|
|
137
|
+
.commitMessage:focus { border-color: var(--gs-accent-border); }
|
|
138
|
+
.commitMessage::placeholder { color: var(--gs-fg-faint); }
|
|
139
|
+
.commitRow { display: flex; align-items: center; gap: 8px; }
|
|
140
|
+
.commitAmend {
|
|
141
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
142
|
+
color: var(--gs-fg-dim); font-size: var(--gs-t-meta);
|
|
143
|
+
cursor: pointer;
|
|
144
|
+
}
|
|
145
|
+
.commitAmend input { accent-color: var(--gs-accent); cursor: pointer; }
|
|
146
|
+
.commitStaged { margin-left: auto; color: var(--gs-fg-faint); font-size: var(--gs-t-meta); }
|
|
147
|
+
/* The commit box's one standing instruction: tick to stage, then commit. A
|
|
148
|
+
constant quiet line at the top — the blocker banner that appeared only when
|
|
149
|
+
the index was empty read as an error, and arrived after the confusion it
|
|
150
|
+
explains. */
|
|
151
|
+
.commitLead { margin: 0; color: var(--gs-fg-faint); font-size: var(--gs-t-meta); line-height: 15px; }
|
|
152
|
+
|
|
153
|
+
/* Commit is the one PRIMARY action in the drawer, and the only control that
|
|
154
|
+
gets a solid fill. As a quiet outlined button it sat at the same weight as
|
|
155
|
+
Expand all and disappeared into the row — the button that writes history
|
|
156
|
+
should not look like the button that folds a directory. */
|
|
157
|
+
.commitBtn {
|
|
158
|
+
flex: none;
|
|
159
|
+
box-sizing: border-box;
|
|
160
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
161
|
+
min-width: 92px;
|
|
162
|
+
height: var(--gs-h-control);
|
|
163
|
+
padding: 0 16px;
|
|
164
|
+
border: 1px solid transparent; border-radius: var(--gs-r-control);
|
|
165
|
+
background: var(--gs-accent); color: var(--gs-on-accent);
|
|
166
|
+
font-family: inherit; font-size: var(--gs-t-ui); font-weight: 600; line-height: 1;
|
|
167
|
+
white-space: nowrap;
|
|
168
|
+
cursor: pointer;
|
|
169
|
+
transition: filter 120ms ease, opacity 120ms ease;
|
|
170
|
+
}
|
|
171
|
+
.commitBtn:hover:not(:disabled) { filter: brightness(1.1); }
|
|
172
|
+
.commitBtn:focus-visible { outline: 2px solid var(--gs-accent); outline-offset: 2px; }
|
|
173
|
+
/* Disabled keeps the shape and loses the weight: the button stays findable, so
|
|
174
|
+
"nothing is staged yet" reads as a state rather than as a missing feature. */
|
|
175
|
+
.commitBtn:disabled {
|
|
176
|
+
background: var(--gs-raise); color: var(--gs-fg-faint);
|
|
177
|
+
border-color: var(--gs-border);
|
|
178
|
+
cursor: default;
|
|
179
|
+
}
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
/* ============================ drawer shell ============================ */
|
|
2
|
+
|
|
3
|
+
/* ---- geometry and type scale ----
|
|
4
|
+
*
|
|
5
|
+
* The drawer is a plugin surface inside dsh, so it borrows dsh's own control
|
|
6
|
+
* vocabulary rather than inventing a second one. The numbers below are measured
|
|
7
|
+
* from the running app, not guessed: dsh settles on a 28px control height, a
|
|
8
|
+
* 12px radius for its rows and panes, a full pill for its dropdown triggers,
|
|
9
|
+
* and the `--dsw-font-*` ladder for type.
|
|
10
|
+
*
|
|
11
|
+
* Every control in this stylesheet resolves its height, radius, padding and
|
|
12
|
+
* size through these. That is the whole mechanism keeping the drawer coherent —
|
|
13
|
+
* before it, eight classes each spelled out their own padding and radius, and
|
|
14
|
+
* the same "small secondary button" rendered at four different heights.
|
|
15
|
+
*/
|
|
16
|
+
.overlay {
|
|
17
|
+
--gs-inset: 14px;
|
|
18
|
+
|
|
19
|
+
/* Control geometry — dsh's own. */
|
|
20
|
+
--gs-h-control: 28px; /* chrome: header actions, chips, ref pickers */
|
|
21
|
+
--gs-h-compact: 24px; /* in-pane tools, where vertical space belongs to the diff */
|
|
22
|
+
--gs-pad-control: 0 12px;
|
|
23
|
+
--gs-pad-compact: 0 10px;
|
|
24
|
+
|
|
25
|
+
/* Readable against a solid --gs-accent fill. White for every palette whose
|
|
26
|
+
accent is a mid-to-dark hue; the two palettes with a light accent override
|
|
27
|
+
it, because white on #00f0ff is not a colour anyone can read. */
|
|
28
|
+
--gs-on-accent: #ffffff;
|
|
29
|
+
|
|
30
|
+
/* Commit-graph lanes. Deliberately NOT per-palette: a lane's colour carries no
|
|
31
|
+
meaning beyond "not the same as the lane beside it", so it has nothing to
|
|
32
|
+
follow the theme for. What it does need is to stay legible on every ground
|
|
33
|
+
the drawer can paint, from #0d1117 to Solarized's cream — these are mid
|
|
34
|
+
hues chosen to hold on both ends rather than tuned to either. */
|
|
35
|
+
--gs-graph-0: #58a6ff;
|
|
36
|
+
--gs-graph-1: #3fb950;
|
|
37
|
+
--gs-graph-2: #d29922;
|
|
38
|
+
--gs-graph-3: #bc8cff;
|
|
39
|
+
--gs-graph-4: #f778ba;
|
|
40
|
+
--gs-graph-5: #39c5cf;
|
|
41
|
+
|
|
42
|
+
/* Radii. Four values, each with one job; nothing else may invent a fifth. */
|
|
43
|
+
--gs-r-pill: 999px; /* drawer chrome: header actions, ref chips, triggers */
|
|
44
|
+
--gs-r-control: 8px; /* controls inside a panel or pane; inputs; badges;
|
|
45
|
+
and the dense list rows a tree or a picker is made of */
|
|
46
|
+
--gs-r-surface: 12px; /* the tall card rows, popovers and panes they sit in */
|
|
47
|
+
--gs-r-drawer: 14px; /* the drawer and its companion card */
|
|
48
|
+
|
|
49
|
+
/* Type — dsh's --dsw-font-* ladder, which tops out at 11px for meta text. */
|
|
50
|
+
--gs-t-meta: 11px; /* --dsw-font-xxxs-11: counters, captions */
|
|
51
|
+
--gs-t-dense: 12px; /* --dsw-font-xxs-12: dense list rows */
|
|
52
|
+
--gs-t-ui: 13px; /* --dsw-font-xs-13: controls, tabs, body */
|
|
53
|
+
|
|
54
|
+
/* Two gutters, one per container. Chrome bars that span the drawer — header,
|
|
55
|
+
tabs, worktree chips, path line, compare bar — share `--gs-gutter`; they
|
|
56
|
+
used to sit at 20/20/14/20/20 and the drawer's left edge read as ragged.
|
|
57
|
+
Inside a pane the gutter is tighter, because a pane is already inset by its
|
|
58
|
+
divider and its rows carry indentation of their own. */
|
|
59
|
+
--gs-gutter: 16px;
|
|
60
|
+
--gs-gutter-pane: 12px;
|
|
61
|
+
|
|
62
|
+
position: fixed; inset: 0; z-index: 1000;
|
|
63
|
+
display: flex; justify-content: flex-end; align-items: stretch; gap: 12px;
|
|
64
|
+
padding: var(--gs-inset);
|
|
65
|
+
box-sizing: border-box;
|
|
66
|
+
background: var(--gs-backdrop);
|
|
67
|
+
animation: gsFade 160ms ease-out;
|
|
68
|
+
}
|
|
69
|
+
.drawer {
|
|
70
|
+
position: relative;
|
|
71
|
+
display: flex; flex-direction: column; box-sizing: border-box;
|
|
72
|
+
width: min(1600px, 94vw); height: 100%;
|
|
73
|
+
color: var(--gs-fg);
|
|
74
|
+
background: var(--gs-bg);
|
|
75
|
+
border: 1px solid var(--gs-border);
|
|
76
|
+
border-radius: var(--gs-r-drawer);
|
|
77
|
+
/* Rounded corners only hold if the panes cannot paint past them. This also
|
|
78
|
+
clips the scaled-up background layer below. */
|
|
79
|
+
overflow: hidden;
|
|
80
|
+
box-shadow: 0 24px 64px var(--gs-shadow);
|
|
81
|
+
font-family: var(--dsw-font-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif);
|
|
82
|
+
animation: gsSlide 180ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
83
|
+
/* Surfaces the panes paint with. Identical to the palette's own colours until
|
|
84
|
+
a background image is set, at which point they thin out to let it show. Every
|
|
85
|
+
large filled area uses these; small chips and hovers keep the solid tokens,
|
|
86
|
+
since a translucent 20px pill only reads as muddy. */
|
|
87
|
+
--gs-surface: var(--gs-bg);
|
|
88
|
+
--gs-surface-2: var(--gs-panel);
|
|
89
|
+
}
|
|
90
|
+
/* `--gs-bg-image`, `--gs-bg-blur` and `--gs-veil` are set inline by the panel
|
|
91
|
+
from the winning style scope; the attribute is what switches the surfaces. */
|
|
92
|
+
.drawer[data-gs-bg] {
|
|
93
|
+
--gs-surface: color-mix(in srgb, var(--gs-bg) var(--gs-veil), transparent);
|
|
94
|
+
--gs-surface-2: color-mix(in srgb, var(--gs-panel) var(--gs-veil), transparent);
|
|
95
|
+
}
|
|
96
|
+
/* Background image layer. Scaled past the edges because a blur samples beyond
|
|
97
|
+
its own box and would otherwise fade to transparent at the border. */
|
|
98
|
+
.drawer[data-gs-bg]::before {
|
|
99
|
+
content: '';
|
|
100
|
+
position: absolute; inset: 0; z-index: 0;
|
|
101
|
+
background-image: var(--gs-bg-image);
|
|
102
|
+
background-size: cover; background-position: center;
|
|
103
|
+
filter: blur(var(--gs-bg-blur));
|
|
104
|
+
transform: scale(1.12);
|
|
105
|
+
pointer-events: none;
|
|
106
|
+
}
|
|
107
|
+
/* Everything real sits above that layer. The drag handle is excluded because it
|
|
108
|
+
is absolutely positioned and carries its own stacking order; matching it here
|
|
109
|
+
would leave the two rules deciding by source order.
|
|
110
|
+
Chrome that hosts a popover must sit ABOVE `.body`: later siblings with the
|
|
111
|
+
same z-index paint on top, which buried Theme and Branch menus under the
|
|
112
|
+
panes. A menu that renders but cannot be clicked looks like a dead control.
|
|
113
|
+
Popovers also drop DOWNWARD, so a bar's list can reach past every bar below
|
|
114
|
+
it — which means the ranks must strictly DECREASE down the DOM, or a lower
|
|
115
|
+
bar at the same z-index absorbs the rows (the worktree picker's list was
|
|
116
|
+
unclickable behind the sync bar until they were separated).
|
|
117
|
+
STRICTLY decreasing, including between the header and the tabs. Those two
|
|
118
|
+
shared rank 23 for as long as the header's only menu was Appearance, which
|
|
119
|
+
portals to the rail outside the drawer and so never dropped through anything.
|
|
120
|
+
The worktree picker does drop, and the tab bar — a later sibling at the same
|
|
121
|
+
rank — swallowed its filter box on the first try. */
|
|
122
|
+
.drawer > *:not(.resizer) { position: relative; z-index: 1; }
|
|
123
|
+
.drawer > .header { z-index: 24; }
|
|
124
|
+
.drawer > .tabs { z-index: 23; }
|
|
125
|
+
.drawer > .compareBar { z-index: 22; }
|
|
126
|
+
.drawer > .syncBar { z-index: 20; }
|
|
127
|
+
/* The roll-back confirmation, and the one deliberate exception to the
|
|
128
|
+
strictly-decreasing rule above: it is the LAST child and outranks every
|
|
129
|
+
other, because it is not a popover dropping through its siblings but a
|
|
130
|
+
modal that has to cover them. It also has to be declared here — the
|
|
131
|
+
`> *:not(.resizer)` rule out-specifies a bare `.confirmScrim`, which is
|
|
132
|
+
what pinned it to the bottom of the drawer as an in-flow flex item. */
|
|
133
|
+
.drawer > .confirmScrim { position: absolute; inset: 0; z-index: 40; }
|
|
134
|
+
/* Maximized drops the inset and the rounding: at full bleed they would only
|
|
135
|
+
waste edge pixels the diff wants. */
|
|
136
|
+
.overlayMax { padding: 0; gap: 0; }
|
|
137
|
+
.overlayMax .drawer { flex: 1 1 auto; width: auto; min-width: 0; border-radius: 0; border-width: 0 0 0 1px; }
|
|
138
|
+
@keyframes gsFade { from { opacity: 0; } }
|
|
139
|
+
@keyframes gsSlide { from { transform: translateX(24px); opacity: 0; } }
|
|
140
|
+
|
|
141
|
+
/* Drag handle on the leading edge: a 10px hit area that is easy to grab, showing
|
|
142
|
+
a short centred grip in the card's own border colour. A full-height accent bar
|
|
143
|
+
was read as a second scrollbar and pulled the eye off the diff. */
|
|
144
|
+
.resizer {
|
|
145
|
+
position: absolute; top: 0; bottom: 0; left: 0; z-index: 30;
|
|
146
|
+
display: flex; align-items: center; justify-content: center;
|
|
147
|
+
width: 10px;
|
|
148
|
+
cursor: col-resize;
|
|
149
|
+
touch-action: none;
|
|
150
|
+
}
|
|
151
|
+
.resizer::after {
|
|
152
|
+
content: '';
|
|
153
|
+
width: 3px; height: 32px;
|
|
154
|
+
border-radius: 999px;
|
|
155
|
+
background: var(--gs-fg-fainter);
|
|
156
|
+
opacity: 0;
|
|
157
|
+
transition: opacity 140ms ease, height 140ms ease;
|
|
158
|
+
}
|
|
159
|
+
.resizer:hover::after { opacity: 0.6; }
|
|
160
|
+
.resizerActive::after { opacity: 0.9; height: 56px; }
|
|
161
|
+
|
|
162
|
+
/* Divider between two panes. It carries the 1px rule the panes used to draw as
|
|
163
|
+
their own border, so the visible line and the drag target are one element and
|
|
164
|
+
cannot drift apart. */
|
|
165
|
+
.paneDivider {
|
|
166
|
+
position: relative; z-index: 5;
|
|
167
|
+
flex: none;
|
|
168
|
+
display: flex; align-items: center; justify-content: center;
|
|
169
|
+
width: 7px;
|
|
170
|
+
cursor: col-resize;
|
|
171
|
+
touch-action: none;
|
|
172
|
+
}
|
|
173
|
+
.paneDivider::before {
|
|
174
|
+
content: '';
|
|
175
|
+
position: absolute; top: 0; bottom: 0; left: 3px;
|
|
176
|
+
width: 1px;
|
|
177
|
+
background: var(--gs-border-soft);
|
|
178
|
+
}
|
|
179
|
+
.paneDivider::after {
|
|
180
|
+
content: '';
|
|
181
|
+
position: relative;
|
|
182
|
+
width: 3px; height: 28px;
|
|
183
|
+
border-radius: 999px;
|
|
184
|
+
background: var(--gs-fg-fainter);
|
|
185
|
+
opacity: 0;
|
|
186
|
+
transition: opacity 140ms ease, height 140ms ease;
|
|
187
|
+
}
|
|
188
|
+
.paneDivider:hover::after { opacity: 0.6; }
|
|
189
|
+
.paneDividerActive::after { opacity: 0.9; height: 48px; }
|
|
190
|
+
/* The same handle turned through a right angle. Both modifier rules below beat
|
|
191
|
+
the shared ones on specificity, so their order relative to them does not
|
|
192
|
+
matter — the stylesheet's usual "same value loses" trap does not apply. */
|
|
193
|
+
.paneDividerY {
|
|
194
|
+
width: auto; height: 7px;
|
|
195
|
+
cursor: row-resize;
|
|
196
|
+
}
|
|
197
|
+
.paneDividerY::before { top: 3px; bottom: auto; left: 0; right: 0; width: auto; height: 1px; }
|
|
198
|
+
.paneDividerY::after { width: 28px; height: 3px; transition: opacity 140ms ease, width 140ms ease; }
|
|
199
|
+
.paneDividerY.paneDividerActive::after { width: 48px; height: 3px; }
|
|
200
|
+
|
|
201
|
+
.header {
|
|
202
|
+
display: flex; align-items: center; justify-content: space-between; gap: 12px;
|
|
203
|
+
flex: none;
|
|
204
|
+
padding: 10px var(--gs-gutter);
|
|
205
|
+
border-bottom: 1px solid var(--gs-border);
|
|
206
|
+
background: var(--gs-surface-2);
|
|
207
|
+
font-family: var(--dsw-font-ui, system-ui, sans-serif);
|
|
208
|
+
font-size: var(--gs-t-ui);
|
|
209
|
+
}
|
|
210
|
+
.headerLeft { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; min-width: 0; }
|
|
211
|
+
/* The subject of the drawer: which worktree. Worn by the picker's trigger when
|
|
212
|
+
there is a choice (`.headerPicker` below) and by a plain span when there is
|
|
213
|
+
not, so a one-worktree repository still states its identity in the same
|
|
214
|
+
place, at the same weight. */
|
|
215
|
+
.headerBranch {
|
|
216
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
217
|
+
padding: 2px 10px;
|
|
218
|
+
border: 1px solid var(--gs-accent-border);
|
|
219
|
+
border-radius: var(--gs-r-pill);
|
|
220
|
+
background: var(--gs-accent-bg);
|
|
221
|
+
color: var(--gs-accent);
|
|
222
|
+
font-weight: 600;
|
|
223
|
+
}
|
|
224
|
+
/* Colour only — the geometry stays the button vocabulary's, so the trigger is
|
|
225
|
+
the same pill as every other menu in the drawer and only its role differs.
|
|
226
|
+
Qualified with `.refButton` deliberately: the shared vocabulary is declared
|
|
227
|
+
at the FOOT of this file, so a bare `.headerPicker` up here loses every
|
|
228
|
+
property they both set to source order, and the chip came out grey. */
|
|
229
|
+
.refButton.headerPicker {
|
|
230
|
+
/* The drawer's subject, in a row with 1400px to spare — the 260px the small
|
|
231
|
+
pickers share clipped `feature/nested/deep/scheduler` at a point where the
|
|
232
|
+
name needed 205. Still capped, so one absurd branch cannot push the totals
|
|
233
|
+
off the row. */
|
|
234
|
+
max-width: 420px;
|
|
235
|
+
border-color: var(--gs-accent-border);
|
|
236
|
+
background: var(--gs-accent-bg);
|
|
237
|
+
color: var(--gs-accent);
|
|
238
|
+
font-weight: 600;
|
|
239
|
+
}
|
|
240
|
+
.refButton.headerPicker:hover { background: var(--gs-accent-bg); color: var(--gs-accent); border-color: var(--gs-accent); }
|
|
241
|
+
|
|
242
|
+
/* Slash-separated names — paths and branches alike — that give up their HEAD
|
|
243
|
+
when the row runs short. Plain `text-overflow: ellipsis` eats the leaf, and
|
|
244
|
+
the leaf is the half that answers "which one": `…/worktrees/fixture-07` vs
|
|
245
|
+
`…/worktrees/fixture-14`, `feature/deep/parser` vs `feature/deep/lexer`.
|
|
246
|
+
|
|
247
|
+
The shrink is WEIGHTED rather than pinned. `flex: none` on the tail read
|
|
248
|
+
correctly for a path, but a name with no head at all — a bare
|
|
249
|
+
`some-very-long-branch-name` — would then refuse to shrink and simply
|
|
250
|
+
overflow its row. At 999 the head absorbs effectively all of it, and the
|
|
251
|
+
tail only starts giving ground once the head has nothing left. */
|
|
252
|
+
.elide { display: inline-flex; align-items: baseline; flex: 0 1 auto; min-width: 0; }
|
|
253
|
+
.elideHead {
|
|
254
|
+
flex: 0 999 auto; min-width: 0;
|
|
255
|
+
overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
|
|
256
|
+
}
|
|
257
|
+
.elideTail {
|
|
258
|
+
flex: 0 1 auto; min-width: 0;
|
|
259
|
+
overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
|
|
260
|
+
}
|
|
261
|
+
/* The header's path: quiet, with the leaf a shade brighter than its directory,
|
|
262
|
+
and free to take whatever the totals beside it leave. */
|
|
263
|
+
.headerPathMain { color: var(--gs-fg-faint); font-size: var(--gs-t-meta); }
|
|
264
|
+
.headerPathMain .elideTail { color: var(--gs-fg-dim); }
|
|
265
|
+
/* Either end of the compare pill, so a long branch elides there too rather
|
|
266
|
+
than stretching the pill across the header. */
|
|
267
|
+
.headerViewRef { max-width: 160px; }
|
|
268
|
+
|
|
269
|
+
/* What the view is about when it is NOT the working tree: a commit hash, or a
|
|
270
|
+
comparison's two ends. Quieter than the worktree chip — the worktree is where
|
|
271
|
+
you are, this is only what you are looking at inside it. */
|
|
272
|
+
.headerView {
|
|
273
|
+
flex: none;
|
|
274
|
+
padding: 1px 8px;
|
|
275
|
+
border-radius: var(--gs-r-pill);
|
|
276
|
+
background: var(--gs-neutral-bg);
|
|
277
|
+
color: var(--gs-fg-muted);
|
|
278
|
+
font-size: var(--gs-t-meta);
|
|
279
|
+
font-variant-numeric: tabular-nums;
|
|
280
|
+
}
|
|
281
|
+
.headerDetached {
|
|
282
|
+
padding: 1px 8px;
|
|
283
|
+
border-radius: var(--gs-r-pill);
|
|
284
|
+
background: var(--gs-warn-bg);
|
|
285
|
+
color: var(--gs-warn);
|
|
286
|
+
font-size: var(--gs-t-meta);
|
|
287
|
+
letter-spacing: 0.02em;
|
|
288
|
+
}
|
|
289
|
+
.headerTotals { font-variant-numeric: tabular-nums; }
|
|
290
|
+
.headerTotalsAdd { color: var(--gs-add); font-weight: 600; }
|
|
291
|
+
.headerTotalsDel { color: var(--gs-del); font-weight: 600; }
|
|
292
|
+
.headerTotalsDim { color: var(--gs-fg-dim); font-size: var(--gs-t-dense); }
|
|
293
|
+
/* The gap is the rule, not each button's margin. `.btn` used to carry its own
|
|
294
|
+
`margin-left`, which meant the spacing lived in the button and vanished the
|
|
295
|
+
moment the button's geometry moved into the shared vocabulary. */
|
|
296
|
+
.headerRight { display: flex; flex: none; gap: 8px; position: relative; }
|
|
297
|
+
|
|
298
|
+
/* Theme button in the header. The panel is a companion card in the overlay,
|
|
299
|
+
to the LEFT of the drawer — not a column stolen from the diff, and not a
|
|
300
|
+
popover painted over the code. */
|
|
301
|
+
.theme { position: relative; display: inline-flex; }
|
|
302
|
+
/* Settings hangs from its own gear like every other menu in the drawer, right
|
|
303
|
+
edge aligned to the button because the button is the last thing in the row
|
|
304
|
+
and a left-aligned card would run off the drawer. Wider than a ref list: it
|
|
305
|
+
holds a palette grid, an image preview and a CSS editor, none of which fit a
|
|
306
|
+
menu-width column. */
|
|
307
|
+
.refPop.settingsPop {
|
|
308
|
+
top: calc(100% + 6px); right: 0; left: auto;
|
|
309
|
+
width: 320px; max-width: calc(100vw - 32px);
|
|
310
|
+
max-height: min(560px, calc(100vh - 120px));
|
|
311
|
+
}
|
|
312
|
+
.themeRail {
|
|
313
|
+
flex: 1; min-height: 0;
|
|
314
|
+
display: flex; flex-direction: column; gap: 14px;
|
|
315
|
+
padding: 12px 14px 16px;
|
|
316
|
+
overflow-y: auto;
|
|
317
|
+
font-family: var(--dsw-font-ui, system-ui, sans-serif);
|
|
318
|
+
}
|
|
319
|
+
.themeGroup { display: flex; flex-direction: column; gap: 6px; }
|
|
320
|
+
.themeLabel {
|
|
321
|
+
color: var(--gs-fg-faint);
|
|
322
|
+
font-size: var(--gs-t-meta); font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase;
|
|
323
|
+
}
|
|
324
|
+
.themeRowSplit { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
325
|
+
|
|
326
|
+
/* Mode picker. Each option is a painted swatch over its label, and the swatch
|
|
327
|
+
colours are FIXED rather than palette-derived: "light" and "dark" name the
|
|
328
|
+
colours themselves, so a dark-palette button rendering the light option in
|
|
329
|
+
dark grey would be telling the user the opposite of what it does. */
|
|
330
|
+
/* The mode picker is the one control that is taller than the scale: it stacks a
|
|
331
|
+
painted swatch over its label, because "light" and "dark" are better shown
|
|
332
|
+
than named. Everything except the height comes from the vocabulary. */
|
|
333
|
+
.segmented { display: flex; gap: 6px; }
|
|
334
|
+
.segment {
|
|
335
|
+
flex: 1 1 0;
|
|
336
|
+
display: flex; flex-direction: column; gap: 4px; align-items: stretch;
|
|
337
|
+
box-sizing: border-box;
|
|
338
|
+
padding: 4px;
|
|
339
|
+
border: 1px solid var(--gs-border); border-radius: var(--gs-r-control);
|
|
340
|
+
background: transparent; color: var(--gs-fg-dim);
|
|
341
|
+
font-family: inherit; font-size: var(--gs-t-meta);
|
|
342
|
+
cursor: pointer;
|
|
343
|
+
transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
|
|
344
|
+
}
|
|
345
|
+
.segment:hover { background: var(--gs-raise); border-color: var(--gs-fg-fainter); color: var(--gs-fg); }
|
|
346
|
+
.segment:focus-visible { outline: 2px solid var(--gs-accent); outline-offset: 1px; }
|
|
347
|
+
.segmentChip {
|
|
348
|
+
height: 20px;
|
|
349
|
+
border: 1px solid rgba(128, 128, 128, 0.45);
|
|
350
|
+
border-radius: calc(var(--gs-r-control) - 4px);
|
|
351
|
+
}
|
|
352
|
+
.chipLight { background: #ffffff; }
|
|
353
|
+
.chipDark { background: #0b0b0d; }
|
|
354
|
+
/* System takes both halves, which is how every OS appearance control shows it. */
|
|
355
|
+
.chipSystem { background: linear-gradient(105deg, #ffffff 0 50%, #0b0b0d 50% 100%); }
|
|
356
|
+
|
|
357
|
+
.paletteRow {
|
|
358
|
+
display: flex; align-items: center; gap: 8px;
|
|
359
|
+
width: 100%; box-sizing: border-box;
|
|
360
|
+
padding: 5px 8px;
|
|
361
|
+
border: 0; border-radius: var(--gs-r-control);
|
|
362
|
+
background: transparent; color: var(--gs-fg-muted);
|
|
363
|
+
font-family: inherit; font-size: var(--gs-t-dense);
|
|
364
|
+
text-align: left; cursor: pointer;
|
|
365
|
+
}
|
|
366
|
+
.paletteRow:hover { background: var(--gs-raise); color: var(--gs-fg); }
|
|
367
|
+
/* .paletteRowActive: see the shared selected-state rule at the foot of the file. */
|
|
368
|
+
/* Three-stop swatch: ground, accent, and the add colour the diff leans on. */
|
|
369
|
+
.swatch {
|
|
370
|
+
flex: none;
|
|
371
|
+
display: inline-flex; overflow: hidden;
|
|
372
|
+
width: 26px; height: 12px;
|
|
373
|
+
border: 1px solid var(--gs-border); border-radius: 3px;
|
|
374
|
+
}
|
|
375
|
+
.swatch span { flex: 1; }
|
|
376
|
+
|
|
377
|
+
/* Scope switch: which of the two stores an edit lands in. */
|
|
378
|
+
.scopeRow { display: flex; gap: 6px; }
|
|
379
|
+
.scopeHint { color: var(--gs-fg-faint); font-size: var(--gs-t-meta); overflow-wrap: anywhere; }
|
|
380
|
+
|
|
381
|
+
.bgPreview {
|
|
382
|
+
height: 64px;
|
|
383
|
+
border: 1px solid var(--gs-border); border-radius: var(--gs-r-control);
|
|
384
|
+
background-color: var(--gs-bg);
|
|
385
|
+
background-size: cover; background-position: center;
|
|
386
|
+
}
|
|
387
|
+
.bgEmpty {
|
|
388
|
+
display: flex; align-items: center; justify-content: center;
|
|
389
|
+
color: var(--gs-fg-faint); font-size: var(--gs-t-meta);
|
|
390
|
+
}
|
|
391
|
+
.sliderRow { display: flex; align-items: center; gap: 8px; font-size: var(--gs-t-meta); color: var(--gs-fg-dim); }
|
|
392
|
+
.sliderRow input { flex: 1; min-width: 0; accent-color: var(--gs-accent); }
|
|
393
|
+
.sliderValue { flex: none; width: 38px; text-align: right; font-variant-numeric: tabular-nums; }
|
|
394
|
+
|
|
395
|
+
.cssArea {
|
|
396
|
+
width: 100%; box-sizing: border-box;
|
|
397
|
+
min-height: 96px; max-height: 220px;
|
|
398
|
+
padding: 7px 8px;
|
|
399
|
+
border: 1px solid var(--gs-border); border-radius: var(--gs-r-control);
|
|
400
|
+
background: var(--gs-bg); color: var(--gs-fg);
|
|
401
|
+
font-family: var(--dsw-font-mono, ui-monospace, Consolas, monospace); font-size: var(--gs-t-meta);
|
|
402
|
+
resize: vertical;
|
|
403
|
+
outline: none;
|
|
404
|
+
}
|
|
405
|
+
.cssArea:focus { border-color: var(--gs-accent-border); }
|
|
406
|
+
.themeNote { color: var(--gs-warn); font-size: var(--gs-t-meta); }
|
|
407
|
+
.themeDirty { color: var(--gs-accent); font-size: var(--gs-t-meta); }
|
|
408
|
+
|
|
409
|
+
/* Worktree rows give their whole width to the branch; `.refRowName` already
|
|
410
|
+
flexes and ellipsises, so nothing else here competes for it. The path is on
|
|
411
|
+
the row's `title`. */
|
|
412
|
+
/* Marks the session's OWN worktree, independent of which one is being viewed. */
|
|
413
|
+
.wtCurrent { color: var(--gs-add); font-size: 8px; line-height: 1; }
|
|
414
|
+
|
|
415
|
+
.commitCopy { margin-left: auto; }
|
|
416
|
+
|
|
417
|
+
.commitPop {
|
|
418
|
+
position: fixed; z-index: 40;
|
|
419
|
+
box-sizing: border-box;
|
|
420
|
+
width: min(380px, calc(100vw - 24px));
|
|
421
|
+
display: flex; flex-direction: column; gap: 6px;
|
|
422
|
+
padding: 10px 12px 12px;
|
|
423
|
+
overflow: auto;
|
|
424
|
+
border: 1px solid var(--gs-border); border-radius: var(--gs-r-surface);
|
|
425
|
+
background: var(--gs-panel); color: var(--gs-fg);
|
|
426
|
+
box-shadow: 0 16px 40px var(--gs-shadow);
|
|
427
|
+
font-family: var(--dsw-font-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif);
|
|
428
|
+
font-size: var(--gs-t-dense);
|
|
429
|
+
}
|
|
430
|
+
.commitPopTop { display: flex; align-items: center; gap: 8px; }
|
|
431
|
+
.commitPopTop .commitWhen { margin-right: auto; }
|