@young1lin/dsh-ui-gitworkbench 0.1.12 → 0.1.14
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/CHANGELOG.md +21 -0
- package/CHANGELOG_EN.md +21 -0
- package/README.md +76 -0
- package/README_EN.md +1 -1
- package/lib/client.js +741 -309
- package/package.json +1 -1
- package/src/client/CodeEditor.tsx +129 -3
- package/src/client/DiffViews.tsx +41 -12
- package/src/client/cm-search-theme.ts +250 -0
- package/src/client/cr-mark.ts +39 -0
- package/src/client/locales.ts +4 -4
- package/src/client/search-count.ts +125 -0
- package/src/client/styles/changes.css +19 -10
- package/src/client/styles/controls.css +19 -8
- package/src/client/styles/files.css +6 -4
- package/src/client/styles/history-filters.css +25 -35
- package/src/client/styles/history.css +1 -1
- package/src/client/styles/shell.css +6 -5
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How many matches the find panel found, and which one you are on.
|
|
3
|
+
*
|
|
4
|
+
* The panel ships without a count, which leaves the one question a reader
|
|
5
|
+
* actually has unanswered: a query that highlights nothing on screen might
|
|
6
|
+
* have no matches at all, or two hundred of them below the fold, and the panel
|
|
7
|
+
* looks identical either way.
|
|
8
|
+
*
|
|
9
|
+
* Counting is proportional to the DOCUMENT, and this drawer does not put work
|
|
10
|
+
* proportional to the document on the keystroke path. Two things keep that
|
|
11
|
+
* true, and both live here rather than in the wiring:
|
|
12
|
+
*
|
|
13
|
+
* - the walk stops at {@link MATCH_CAP}. A cap that turns a feature off would
|
|
14
|
+
* be no fix, so this one bounds the WORK and keeps the feature: past the cap
|
|
15
|
+
* the total reads `5000+`, which answers "is my query too broad" as well as
|
|
16
|
+
* an exact number would. It bounds memory the same way — a single-letter
|
|
17
|
+
* search over 20,000 lines of real TypeScript finds 71,515 matches, and an
|
|
18
|
+
* array of those is half a megabyte kept alive for a number nobody reads.
|
|
19
|
+
* - the offsets are KEPT, so moving between matches is a binary search rather
|
|
20
|
+
* than a second walk. Measured on this repo's own client sources: a full
|
|
21
|
+
* count costs 3-5ms at 300 lines, 9-11ms at 2,000, 12ms at 4,000, and 60ms
|
|
22
|
+
* at 20,000 (`SIDE_LINE_CAP`, the ceiling the pane will load). Recounting on
|
|
23
|
+
* every Enter would put that 60ms on the navigation path; recounting when
|
|
24
|
+
* the typing stops puts it nowhere the reader can feel it.
|
|
25
|
+
*
|
|
26
|
+
* The index belongs to one query over one document, and the caller throws it
|
|
27
|
+
* away when either changes — see `SearchCount` in `CodeEditor.tsx`.
|
|
28
|
+
*
|
|
29
|
+
* @module @young1lin/dsh-ui-gitworkbench/client/search-count
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* How many match positions are kept.
|
|
34
|
+
*
|
|
35
|
+
* 5,000 is past any count a reader distinguishes from "lots" and well inside
|
|
36
|
+
* what the pane can hold: at 8 bytes an offset it is 40KB, against the half a
|
|
37
|
+
* megabyte an uncapped single-letter search over the ceiling would take.
|
|
38
|
+
*/
|
|
39
|
+
export const MATCH_CAP = 5000
|
|
40
|
+
|
|
41
|
+
/** Where every match starts, and whether the walk stopped early. */
|
|
42
|
+
export interface MatchIndex {
|
|
43
|
+
/** Ascending start offsets, at most {@link MATCH_CAP} of them. */
|
|
44
|
+
readonly offsets: readonly number[]
|
|
45
|
+
/** There were more matches than were kept; the total reads `N+`. */
|
|
46
|
+
readonly capped: boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** No query, or a query with nothing to find. */
|
|
50
|
+
export const EMPTY_INDEX: MatchIndex = { offsets: [], capped: false }
|
|
51
|
+
|
|
52
|
+
/** One match start, however it is handed over. */
|
|
53
|
+
interface Match { readonly from: number }
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Anything that yields matches in order.
|
|
57
|
+
*
|
|
58
|
+
* Both halves of the union are here for a reason: `SearchQuery.getCursor` is
|
|
59
|
+
* DECLARED as an iterator and is also iterable at runtime, while a test wants
|
|
60
|
+
* to hand over a plain array. Accepting either keeps the rule readable without
|
|
61
|
+
* a document, a view or a DOM behind it.
|
|
62
|
+
*/
|
|
63
|
+
export type MatchWalk = Iterable<Match> | Iterator<Match>
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Walk matches into an index, stopping at `cap`.
|
|
67
|
+
*
|
|
68
|
+
* The walk is driven by hand rather than with `for…of`, because the union
|
|
69
|
+
* above may arrive already unwrapped. A cursor reuses ONE object across
|
|
70
|
+
* iterations, which is safe here only because nothing but the number is kept.
|
|
71
|
+
*/
|
|
72
|
+
export function indexMatches(matches: MatchWalk, cap: number = MATCH_CAP): MatchIndex {
|
|
73
|
+
const step: Iterator<Match> = Symbol.iterator in matches
|
|
74
|
+
? matches[Symbol.iterator]()
|
|
75
|
+
: matches
|
|
76
|
+
const offsets: number[] = []
|
|
77
|
+
for (;;) {
|
|
78
|
+
if (offsets.length >= cap) {
|
|
79
|
+
// Ask once more: `capped` has to mean "there are more", not "there might
|
|
80
|
+
// have been", or a total that lands exactly on the cap reads as `5000+`.
|
|
81
|
+
return { offsets, capped: step.next().done !== true }
|
|
82
|
+
}
|
|
83
|
+
const next = step.next()
|
|
84
|
+
if (next.done === true) return { offsets, capped: false }
|
|
85
|
+
offsets.push(next.value.from)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Which match the position `from` is on or before, 1-based; 0 when there are
|
|
91
|
+
* none.
|
|
92
|
+
*
|
|
93
|
+
* The first match at or after the caret, because that is the one the panel
|
|
94
|
+
* will land on — which makes the answer right in both of the states the reader
|
|
95
|
+
* sees it in. Straight after typing the caret has not moved and the count
|
|
96
|
+
* should already read `1/128`, not `0/128`; after Enter the selection sits
|
|
97
|
+
* exactly on a match and the lower bound IS that match. Past the last one the
|
|
98
|
+
* panel wraps, so the answer wraps with it.
|
|
99
|
+
*/
|
|
100
|
+
export function ordinalAt(index: MatchIndex, from: number): number {
|
|
101
|
+
const { offsets } = index
|
|
102
|
+
if (offsets.length === 0) return 0
|
|
103
|
+
let low = 0
|
|
104
|
+
let high = offsets.length
|
|
105
|
+
while (low < high) {
|
|
106
|
+
const mid = (low + high) >> 1
|
|
107
|
+
if (offsets[mid]! < from) low = mid + 1
|
|
108
|
+
else high = mid
|
|
109
|
+
}
|
|
110
|
+
return low === offsets.length ? 1 : low + 1
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The label: `3/128`, `3/5000+` when the walk stopped early, `0/0` for a query
|
|
115
|
+
* that finds nothing.
|
|
116
|
+
*
|
|
117
|
+
* No words, in a panel whose own labels are the library's English and are not
|
|
118
|
+
* translated either — a pair of numbers says it in every language the drawer
|
|
119
|
+
* ships (see `locales.ts` for the strings that do need both).
|
|
120
|
+
*/
|
|
121
|
+
export function formatCount(index: MatchIndex, ordinal: number): string {
|
|
122
|
+
const total = index.offsets.length
|
|
123
|
+
if (total === 0) return '0/0'
|
|
124
|
+
return `${ordinal}/${total}${index.capped ? '+' : ''}`
|
|
125
|
+
}
|
|
@@ -107,6 +107,7 @@
|
|
|
107
107
|
box-sizing: border-box; width: 100%;
|
|
108
108
|
padding: 4px 12px 4px 8px;
|
|
109
109
|
border: 0;
|
|
110
|
+
border-radius: var(--gs-r-control);
|
|
110
111
|
background: transparent;
|
|
111
112
|
color: var(--gs-fg-dim);
|
|
112
113
|
font-family: inherit;
|
|
@@ -114,7 +115,7 @@
|
|
|
114
115
|
text-align: left;
|
|
115
116
|
cursor: pointer;
|
|
116
117
|
}
|
|
117
|
-
.treeDir:hover { background: var(--gs-
|
|
118
|
+
.treeDir:hover { background: var(--gs-raise); color: var(--gs-fg-muted); }
|
|
118
119
|
.treeDirActive { color: var(--gs-fg-muted); }
|
|
119
120
|
.chevron {
|
|
120
121
|
flex: none;
|
|
@@ -132,7 +133,7 @@
|
|
|
132
133
|
.treeDirCount {
|
|
133
134
|
flex: none;
|
|
134
135
|
padding: 0 6px;
|
|
135
|
-
border-radius:
|
|
136
|
+
border-radius: var(--gs-r-pill);
|
|
136
137
|
background: var(--gs-neutral-bg);
|
|
137
138
|
color: var(--gs-fg-dim);
|
|
138
139
|
font-size: var(--gs-t-meta); font-weight: 600; line-height: 16px;
|
|
@@ -144,6 +145,7 @@
|
|
|
144
145
|
padding: 5px 14px;
|
|
145
146
|
border: 0;
|
|
146
147
|
border-left: 2px solid transparent;
|
|
148
|
+
border-radius: var(--gs-r-control);
|
|
147
149
|
background: transparent;
|
|
148
150
|
color: var(--gs-fg-muted);
|
|
149
151
|
font-family: inherit;
|
|
@@ -151,18 +153,14 @@
|
|
|
151
153
|
text-align: left;
|
|
152
154
|
cursor: pointer;
|
|
153
155
|
}
|
|
154
|
-
.file:hover { background: var(--gs-
|
|
155
|
-
.fileActive
|
|
156
|
-
background: var(--gs-panel);
|
|
157
|
-
border-left-color: var(--gs-accent);
|
|
158
|
-
color: var(--gs-fg);
|
|
159
|
-
}
|
|
156
|
+
.file:hover { background: var(--gs-raise); }
|
|
157
|
+
/* .fileActive: see the shared selected-state rule in controls.css. */
|
|
160
158
|
|
|
161
159
|
.fileStatus {
|
|
162
160
|
flex: none;
|
|
163
161
|
display: inline-flex; align-items: center; justify-content: center;
|
|
164
162
|
width: 18px; height: 18px;
|
|
165
|
-
border-radius:
|
|
163
|
+
border-radius: var(--gs-r-control);
|
|
166
164
|
font-weight: 700; font-size: var(--gs-t-meta); line-height: 1;
|
|
167
165
|
}
|
|
168
166
|
.stAdded, .stUntracked { color: var(--gs-add); background: var(--gs-add-bg); }
|
|
@@ -174,7 +172,7 @@
|
|
|
174
172
|
.fileBinary {
|
|
175
173
|
flex: none;
|
|
176
174
|
padding: 0 5px;
|
|
177
|
-
border-radius:
|
|
175
|
+
border-radius: var(--gs-r-control);
|
|
178
176
|
background: var(--gs-neutral-bg);
|
|
179
177
|
color: var(--gs-fg-dim);
|
|
180
178
|
font-size: 9px; font-weight: 700; letter-spacing: 0.06em;
|
|
@@ -281,6 +279,14 @@
|
|
|
281
279
|
|
|
282
280
|
.wordAdd { background: var(--gs-add-word); border-radius: 2px; }
|
|
283
281
|
.wordDel { background: var(--gs-del-word); border-radius: 2px; }
|
|
282
|
+
/* A carriage return, drawn. The diff's text carries the CR byte through
|
|
283
|
+
verbatim and a browser paints nothing for it, so a line whose only change
|
|
284
|
+
is its ending rendered as two identical-looking cells; the glyph (U+240D,
|
|
285
|
+
the picture of the control character — `cr-mark.ts`) makes the ending
|
|
286
|
+
visible wherever the diff paints one. Dim and slightly small so a whole
|
|
287
|
+
CRLF file reads as code with quiet markers, not as a wall of symbols, and
|
|
288
|
+
unselectable so copying a line copies code. */
|
|
289
|
+
.crMark { color: var(--gs-fg-faint); font-size: 0.9em; user-select: none; }
|
|
284
290
|
|
|
285
291
|
/* Side-by-side diff pane. One grid holds EVERY row's four cells — a per-row
|
|
286
292
|
grid would let the column boundary drift with each row's content, and a
|
|
@@ -459,6 +465,9 @@
|
|
|
459
465
|
opacity: 0.32;
|
|
460
466
|
transition: opacity 140ms ease;
|
|
461
467
|
pointer-events: none;
|
|
468
|
+
/* Above the gutter, which CodeMirror stacks at 200. The bar overlays its
|
|
469
|
+
first two pixels, so at the default z-index an opaque gutter buries it. */
|
|
470
|
+
z-index: 201;
|
|
462
471
|
}
|
|
463
472
|
.cmHost[data-editable]:focus-within::before { opacity: 1; }
|
|
464
473
|
/* The change nav takes the free space, so it and `.sideActions` after it sit
|
|
@@ -82,9 +82,20 @@
|
|
|
82
82
|
because it has to win against the grouped rule above by source order. */
|
|
83
83
|
.scopeBtn { flex: 1 1 0; }
|
|
84
84
|
|
|
85
|
-
/* The single "this one is selected" idiom
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
/* The single "this one is selected" idiom: accent text, in an accent tint,
|
|
86
|
+
inside an accent border. Everything that can be the chosen one is here — a
|
|
87
|
+
tree leaf, a commit, a ref, a palette, a segment, a funnel tab, a date
|
|
88
|
+
bound, a preset — because the drawer had reached three spellings of one
|
|
89
|
+
meaning (this chip, a filled panel, a left accent bar) and showed two of
|
|
90
|
+
them at once whenever the Changes tree sat beside the history.
|
|
91
|
+
|
|
92
|
+
A modifier applied as `${css.base} ${css.mod}` carries the SAME specificity
|
|
93
|
+
as its base, so source order decides. Every base named here is declared
|
|
94
|
+
above this rule; `.fbRow.fbRowActive` is the one exception and is written
|
|
95
|
+
qualified, because `files.css` is imported after this file. */
|
|
96
|
+
.scopeBtnActive, .miniBtnPrimary, .segmentActive, .refRowActive, .paletteRowActive,
|
|
97
|
+
.fileActive, .commitActive, .funnelTabActive, .funnelBoundBtnActive, .funnelPresetActive,
|
|
98
|
+
.fbRow.fbRowActive {
|
|
88
99
|
border-color: var(--gs-accent-border);
|
|
89
100
|
background: var(--gs-accent-bg);
|
|
90
101
|
color: var(--gs-accent);
|
|
@@ -125,10 +136,10 @@
|
|
|
125
136
|
/* Publishing a branch that exists nowhere else is the one moment this bar has a
|
|
126
137
|
primary action, so it gets the solid fill Commit uses — and only then. */
|
|
127
138
|
.btnPrimary {
|
|
128
|
-
border-color: var(--gs-accent); background: var(--gs-accent); color: var(--gs-on-accent
|
|
139
|
+
border-color: var(--gs-accent); background: var(--gs-accent); color: var(--gs-on-accent);
|
|
129
140
|
font-weight: 600;
|
|
130
141
|
}
|
|
131
|
-
.btnPrimary:hover:not(:disabled) { background: var(--gs-accent); color: var(--gs-on-accent
|
|
142
|
+
.btnPrimary:hover:not(:disabled) { background: var(--gs-accent); color: var(--gs-on-accent); border-color: var(--gs-accent); }
|
|
132
143
|
|
|
133
144
|
/* The count riding inside Pull / Push. Reads as part of the label rather than a
|
|
134
145
|
badge stuck on it: same colour, tabular, just set apart by a hairline. */
|
|
@@ -165,7 +176,7 @@
|
|
|
165
176
|
background: var(--gs-border);
|
|
166
177
|
background-clip: padding-box;
|
|
167
178
|
border: 3px solid transparent;
|
|
168
|
-
border-radius:
|
|
179
|
+
border-radius: var(--gs-r-pill);
|
|
169
180
|
}
|
|
170
181
|
.drawer ::-webkit-scrollbar-thumb:hover { background: var(--gs-fg-faint); background-clip: padding-box; }
|
|
171
182
|
|
|
@@ -219,7 +230,7 @@
|
|
|
219
230
|
`.btn` wherever either rule sits, which is the failure the drawer already
|
|
220
231
|
shipped once. */
|
|
221
232
|
.btn.btnDanger {
|
|
222
|
-
border-color: var(--gs-del); background: var(--gs-del); color: var(--gs-on-accent
|
|
233
|
+
border-color: var(--gs-del); background: var(--gs-del); color: var(--gs-on-accent);
|
|
223
234
|
font-weight: 600;
|
|
224
235
|
}
|
|
225
|
-
.btn.btnDanger:hover:not(:disabled) { border-color: var(--gs-del); background: var(--gs-del); color: var(--gs-on-accent
|
|
236
|
+
.btn.btnDanger:hover:not(:disabled) { border-color: var(--gs-del); background: var(--gs-del); color: var(--gs-on-accent); }
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
color: var(--gs-fg);
|
|
22
22
|
background: var(--gs-surface);
|
|
23
23
|
border: 1px solid var(--gs-border);
|
|
24
|
-
border-radius:
|
|
24
|
+
border-radius: var(--gs-r-control);
|
|
25
25
|
}
|
|
26
26
|
.fbSearch:focus { outline: none; border-color: var(--gs-accent); }
|
|
27
27
|
.fbNote { flex: none; font-size: var(--gs-t-meta); color: var(--gs-fg-dim); padding: 2px 4px; }
|
|
@@ -38,14 +38,16 @@
|
|
|
38
38
|
text-align: left;
|
|
39
39
|
background: none;
|
|
40
40
|
border: 0;
|
|
41
|
-
border-radius:
|
|
41
|
+
border-radius: var(--gs-r-control);
|
|
42
42
|
cursor: pointer;
|
|
43
43
|
white-space: nowrap;
|
|
44
44
|
overflow: hidden;
|
|
45
45
|
text-overflow: ellipsis;
|
|
46
46
|
}
|
|
47
|
-
.fbRow:hover { background: var(--gs-
|
|
48
|
-
.fbRowActive
|
|
47
|
+
.fbRow:hover { background: var(--gs-raise); }
|
|
48
|
+
/* .fbRowActive: see the shared selected-state rule in controls.css. It is
|
|
49
|
+
written there qualified, because this file is imported after that one and a
|
|
50
|
+
bare modifier declared above its base loses the cascade. */
|
|
49
51
|
.fbDirName { overflow: hidden; text-overflow: ellipsis; }
|
|
50
52
|
.fbFileName { overflow: hidden; text-overflow: ellipsis; }
|
|
51
53
|
.fbMain {
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
font-family: inherit; font-size: var(--gs-t-dense); line-height: 20px;
|
|
48
48
|
cursor: pointer;
|
|
49
49
|
}
|
|
50
|
-
.filterChipRemove:hover { color: var(--gs-
|
|
50
|
+
.filterChipRemove:hover { color: var(--gs-del); }
|
|
51
51
|
.filterClear {
|
|
52
52
|
border: none; background: transparent;
|
|
53
53
|
padding: 2px 4px;
|
|
@@ -91,13 +91,17 @@
|
|
|
91
91
|
display: flex; gap: 2px;
|
|
92
92
|
margin: 8px 8px 0;
|
|
93
93
|
padding: 2px;
|
|
94
|
-
border: 1px solid var(--gs-border-soft); border-radius: var(--gs-r-
|
|
94
|
+
border: 1px solid var(--gs-border-soft); border-radius: var(--gs-r-control);
|
|
95
95
|
background: var(--gs-bg);
|
|
96
96
|
}
|
|
97
97
|
.funnelTab {
|
|
98
98
|
flex: 1; min-width: 0;
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
box-sizing: border-box;
|
|
100
|
+
height: var(--gs-h-compact); padding: 0 8px;
|
|
101
|
+
/* Transparent rather than none: the shared selected rule paints its
|
|
102
|
+
members through `border-color`, and a border that is not there takes
|
|
103
|
+
no colour. Inside `border-box`, so the strip does not grow by 2px. */
|
|
104
|
+
border: 1px solid transparent; border-radius: calc(var(--gs-r-control) - 3px);
|
|
101
105
|
background: transparent;
|
|
102
106
|
color: var(--gs-fg-dim);
|
|
103
107
|
font-family: inherit; font-size: var(--gs-t-dense); line-height: 1;
|
|
@@ -106,11 +110,7 @@
|
|
|
106
110
|
transition: background 120ms ease, color 120ms ease;
|
|
107
111
|
}
|
|
108
112
|
.funnelTab:hover { color: var(--gs-fg); }
|
|
109
|
-
.
|
|
110
|
-
color: var(--gs-fg);
|
|
111
|
-
background: var(--gs-raise);
|
|
112
|
-
box-shadow: inset 0 0 0 1px var(--gs-border);
|
|
113
|
-
}
|
|
113
|
+
/* .funnelTabActive: see the shared selected-state rule in controls.css. */
|
|
114
114
|
.funnelTab:focus-visible { outline: 2px solid var(--gs-accent); outline-offset: 1px; }
|
|
115
115
|
/* A tab's own criteria count, so the two sections nobody is looking at still
|
|
116
116
|
say they hold something. */
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
.funnelCaption {
|
|
127
127
|
flex: none;
|
|
128
128
|
color: var(--gs-fg-faint);
|
|
129
|
-
font-size:
|
|
129
|
+
font-size: var(--gs-t-meta); line-height: 14px;
|
|
130
130
|
letter-spacing: 0.04em; text-transform: uppercase;
|
|
131
131
|
}
|
|
132
132
|
|
|
@@ -143,8 +143,9 @@
|
|
|
143
143
|
}
|
|
144
144
|
.funnelBoundBtn {
|
|
145
145
|
flex: 1; min-width: 0;
|
|
146
|
-
|
|
147
|
-
|
|
146
|
+
box-sizing: border-box;
|
|
147
|
+
height: var(--gs-h-compact); padding: 0 6px;
|
|
148
|
+
border: 1px solid transparent; border-radius: calc(var(--gs-r-control) - 3px);
|
|
148
149
|
background: transparent;
|
|
149
150
|
color: var(--gs-fg-dim);
|
|
150
151
|
font-family: inherit; font-size: var(--gs-t-dense); line-height: 1;
|
|
@@ -152,11 +153,7 @@
|
|
|
152
153
|
transition: background 120ms ease, color 120ms ease;
|
|
153
154
|
}
|
|
154
155
|
.funnelBoundBtn:hover { color: var(--gs-fg); }
|
|
155
|
-
.
|
|
156
|
-
color: var(--gs-accent);
|
|
157
|
-
background: var(--gs-accent-bg);
|
|
158
|
-
box-shadow: inset 0 0 0 1px var(--gs-accent-border);
|
|
159
|
-
}
|
|
156
|
+
/* .funnelBoundBtnActive: see the shared selected-state rule in controls.css. */
|
|
160
157
|
|
|
161
158
|
/* The calendar — modern-grid look in the drawer's own tokens. */
|
|
162
159
|
.cal { flex: none; display: flex; flex-direction: column; gap: 4px; }
|
|
@@ -168,7 +165,7 @@
|
|
|
168
165
|
}
|
|
169
166
|
.calNav {
|
|
170
167
|
flex: none;
|
|
171
|
-
width:
|
|
168
|
+
width: var(--gs-h-compact); height: var(--gs-h-compact); padding: 0;
|
|
172
169
|
border: none; border-radius: var(--gs-r-control);
|
|
173
170
|
background: transparent;
|
|
174
171
|
color: var(--gs-fg-dim);
|
|
@@ -184,10 +181,10 @@
|
|
|
184
181
|
.calWeek span {
|
|
185
182
|
text-align: center;
|
|
186
183
|
color: var(--gs-fg-faint);
|
|
187
|
-
font-size:
|
|
184
|
+
font-size: var(--gs-t-meta); line-height: 14px;
|
|
188
185
|
}
|
|
189
186
|
.calGrid button {
|
|
190
|
-
height:
|
|
187
|
+
height: var(--gs-h-compact);
|
|
191
188
|
border: none; border-radius: var(--gs-r-control);
|
|
192
189
|
background: transparent;
|
|
193
190
|
color: var(--gs-fg-muted);
|
|
@@ -215,7 +212,7 @@
|
|
|
215
212
|
padding: 3px 6px;
|
|
216
213
|
border: 1px solid var(--gs-border-soft); border-radius: var(--gs-r-control);
|
|
217
214
|
background: var(--gs-bg);
|
|
218
|
-
font-size:
|
|
215
|
+
font-size: var(--gs-t-meta); line-height: 16px;
|
|
219
216
|
}
|
|
220
217
|
.funnelBoundKey { flex: none; color: var(--gs-fg-faint); }
|
|
221
218
|
.funnelBoundVal {
|
|
@@ -233,12 +230,12 @@
|
|
|
233
230
|
font-family: inherit; font-size: var(--gs-t-dense); line-height: 1;
|
|
234
231
|
cursor: pointer;
|
|
235
232
|
}
|
|
236
|
-
.funnelBoundClear:hover { color: var(--gs-
|
|
233
|
+
.funnelBoundClear:hover { color: var(--gs-del); }
|
|
237
234
|
|
|
238
235
|
.funnelSearch {
|
|
239
236
|
flex: none;
|
|
240
237
|
box-sizing: border-box;
|
|
241
|
-
height:
|
|
238
|
+
height: var(--gs-h-compact); padding: 0 8px;
|
|
242
239
|
border: 1px solid var(--gs-border-soft); border-radius: var(--gs-r-control);
|
|
243
240
|
background: var(--gs-bg); color: var(--gs-fg);
|
|
244
241
|
font-family: inherit; font-size: var(--gs-t-dense);
|
|
@@ -257,7 +254,7 @@
|
|
|
257
254
|
.funnelList::-webkit-scrollbar-track { background: transparent; }
|
|
258
255
|
.funnelList::-webkit-scrollbar-thumb {
|
|
259
256
|
background: var(--gs-fg-fainter);
|
|
260
|
-
border: 3px solid transparent; border-radius:
|
|
257
|
+
border: 3px solid transparent; border-radius: var(--gs-r-pill);
|
|
261
258
|
background-clip: content-box;
|
|
262
259
|
}
|
|
263
260
|
/* Every row is the same four slots — chevron, tick, glyph, name — then the
|
|
@@ -339,7 +336,7 @@
|
|
|
339
336
|
.funnelCount {
|
|
340
337
|
flex: none;
|
|
341
338
|
color: var(--gs-fg-faint);
|
|
342
|
-
font-size:
|
|
339
|
+
font-size: var(--gs-t-meta);
|
|
343
340
|
font-variant-numeric: tabular-nums;
|
|
344
341
|
}
|
|
345
342
|
.funnelMore { color: var(--gs-fg-faint); padding: 4px 6px; }
|
|
@@ -360,14 +357,7 @@
|
|
|
360
357
|
the drawer. It is in both selector lists now; this only sizes them to share
|
|
361
358
|
one row. */
|
|
362
359
|
.funnelPreset { flex: 1 1 0; min-width: 0; padding: 0 6px; }
|
|
363
|
-
/*
|
|
364
|
-
several hundred lines BELOW this one, so a bare modifier would lose its
|
|
365
|
-
background to that rule's `background: transparent`. */
|
|
366
|
-
.funnelPreset.funnelPresetActive {
|
|
367
|
-
color: var(--gs-accent);
|
|
368
|
-
border-color: var(--gs-accent);
|
|
369
|
-
background: var(--gs-accent-bg);
|
|
370
|
-
}
|
|
360
|
+
/* .funnelPresetActive: see the shared selected-state rule in controls.css. */
|
|
371
361
|
|
|
372
362
|
/* The footer says what the panel is currently asking git for. Without it the
|
|
373
363
|
only feedback lived on the chip row BEHIND the panel, so a reader had to
|
|
@@ -378,7 +368,7 @@
|
|
|
378
368
|
padding: 6px 10px;
|
|
379
369
|
border-top: 1px solid var(--gs-border-soft);
|
|
380
370
|
background: var(--gs-bg);
|
|
381
|
-
font-size:
|
|
371
|
+
font-size: var(--gs-t-meta); line-height: 16px;
|
|
382
372
|
}
|
|
383
373
|
.funnelFootCount { color: var(--gs-fg-faint); font-variant-numeric: tabular-nums; }
|
|
384
374
|
.funnelFootCountOn { color: var(--gs-fg-muted); }
|
|
@@ -388,7 +378,7 @@
|
|
|
388
378
|
border: none; border-radius: var(--gs-r-control);
|
|
389
379
|
background: transparent;
|
|
390
380
|
color: var(--gs-fg-dim);
|
|
391
|
-
font-family: inherit; font-size:
|
|
381
|
+
font-family: inherit; font-size: var(--gs-t-meta); line-height: 16px;
|
|
392
382
|
cursor: pointer;
|
|
393
383
|
}
|
|
394
384
|
.funnelFootClear:hover:not(:disabled) { color: var(--gs-fg); background: var(--gs-raise); }
|
|
@@ -208,7 +208,7 @@
|
|
|
208
208
|
min-width: 0;
|
|
209
209
|
}
|
|
210
210
|
.commit:hover { background: var(--gs-raise); }
|
|
211
|
-
.commitActive
|
|
211
|
+
/* .commitActive: see the shared selected-state rule in controls.css. */
|
|
212
212
|
/* Pushed to the right end of the row, and never squeezed: the subject is what
|
|
213
213
|
gives way when the pane narrows, because it is the part with an ellipsis to
|
|
214
214
|
give. */
|
|
@@ -41,8 +41,9 @@
|
|
|
41
41
|
|
|
42
42
|
/* Radii. Four values, each with one job; nothing else may invent a fifth. */
|
|
43
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
|
-
|
|
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 */
|
|
46
47
|
--gs-r-drawer: 14px; /* the drawer and its companion card */
|
|
47
48
|
|
|
48
49
|
/* Type — dsh's --dsw-font-* ladder, which tops out at 11px for meta text. */
|
|
@@ -215,7 +216,7 @@
|
|
|
215
216
|
display: inline-flex; align-items: center; gap: 6px;
|
|
216
217
|
padding: 2px 10px;
|
|
217
218
|
border: 1px solid var(--gs-accent-border);
|
|
218
|
-
border-radius:
|
|
219
|
+
border-radius: var(--gs-r-pill);
|
|
219
220
|
background: var(--gs-accent-bg);
|
|
220
221
|
color: var(--gs-accent);
|
|
221
222
|
font-weight: 600;
|
|
@@ -271,7 +272,7 @@
|
|
|
271
272
|
.headerView {
|
|
272
273
|
flex: none;
|
|
273
274
|
padding: 1px 8px;
|
|
274
|
-
border-radius:
|
|
275
|
+
border-radius: var(--gs-r-pill);
|
|
275
276
|
background: var(--gs-neutral-bg);
|
|
276
277
|
color: var(--gs-fg-muted);
|
|
277
278
|
font-size: var(--gs-t-meta);
|
|
@@ -279,7 +280,7 @@
|
|
|
279
280
|
}
|
|
280
281
|
.headerDetached {
|
|
281
282
|
padding: 1px 8px;
|
|
282
|
-
border-radius:
|
|
283
|
+
border-radius: var(--gs-r-pill);
|
|
283
284
|
background: var(--gs-warn-bg);
|
|
284
285
|
color: var(--gs-warn);
|
|
285
286
|
font-size: var(--gs-t-meta);
|