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