@young1lin/dsh-ui-gitworkbench 0.1.3 → 0.1.4
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 +39 -0
- package/CHANGELOG_EN.md +39 -0
- package/README.md +36 -6
- package/README_EN.md +3 -2
- package/lib/client.js +2058 -305
- package/lib/discard-ops.js +158 -0
- package/lib/git-log.js +10 -6
- package/lib/git-ops.js +30 -9
- package/lib/index.js +215 -7
- package/lib/log-filter.js +71 -0
- package/lib/shortlog.js +40 -0
- package/package.json +1 -1
- package/src/client/GitWorkbenchPanel.module.css +523 -6
- package/src/client/GitWorkbenchPanel.tsx +1024 -26
- package/src/client/active-file.ts +83 -0
- package/src/client/calendar.ts +99 -0
- package/src/client/commit-filter.ts +49 -0
- package/src/client/dir-tree.ts +112 -0
- package/src/client/file-filter.ts +66 -0
- package/src/client/index.ts +44 -5
- package/src/client/locales.ts +94 -0
- package/src/client/log-filter-query.ts +189 -0
- package/src/client/path-select.ts +131 -0
- package/src/discard-ops.ts +197 -0
- package/src/git-log.ts +24 -6
- package/src/git-ops.ts +39 -7
- package/src/index.ts +215 -6
- package/src/log-filter.ts +87 -0
- package/src/shortlog.ts +46 -0
package/lib/shortlog.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Author roster from `git shortlog -sne`, for the filter popup's user picker.
|
|
3
|
+
*
|
|
4
|
+
* The REF the roster walks is the caller's choice (the host passes the same
|
|
5
|
+
* ref the history list walks, or `--all` in All-branches mode); shortlog
|
|
6
|
+
* already did the aggregation, and this module only parses its fixed shape
|
|
7
|
+
* (`<count> <name> <<email>>`), sorts by activity and applies the cap — with
|
|
8
|
+
* the cap VISIBLE, because a filter popup that silently lost the long tail of
|
|
9
|
+
* occasional committers would be quietly wrong about who exists.
|
|
10
|
+
*
|
|
11
|
+
* @module @young1lin/dsh-ui-gitworkbench/shortlog
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Parse shortlog output into busiest-first authors, capped.
|
|
15
|
+
* @param stdout - `git shortlog -sne <ref>` output (the ref is the caller's choice).
|
|
16
|
+
* @param limit - how many authors to keep; the busier half survives.
|
|
17
|
+
* @returns the roster and whether it was cut short.
|
|
18
|
+
*/
|
|
19
|
+
export function parseShortlog(stdout, limit) {
|
|
20
|
+
const authors = [];
|
|
21
|
+
for (const line of stdout.split('\n')) {
|
|
22
|
+
const match = /^\s*(\d+)\s+(.+)$/.exec(line);
|
|
23
|
+
if (match === null)
|
|
24
|
+
continue;
|
|
25
|
+
const count = Number(match[1]);
|
|
26
|
+
const who = match[2];
|
|
27
|
+
// The email is the LAST <...> on the line; a name may legally contain
|
|
28
|
+
// anything else.
|
|
29
|
+
const emailMatch = /<([^>]*)>\s*$/.exec(who);
|
|
30
|
+
if (emailMatch === null)
|
|
31
|
+
continue;
|
|
32
|
+
const name = who.slice(0, emailMatch.index).trim();
|
|
33
|
+
if (name.length === 0)
|
|
34
|
+
continue;
|
|
35
|
+
authors.push({ name, email: emailMatch[1], count });
|
|
36
|
+
}
|
|
37
|
+
authors.sort((a, b) => b.count - a.count);
|
|
38
|
+
const truncated = authors.length > limit;
|
|
39
|
+
return { authors: truncated ? authors.slice(0, limit) : authors, truncated };
|
|
40
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@young1lin/dsh-ui-gitworkbench",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Out-of-tree dsh web UI plugin: a session-header git workbench chip opening a drawer with the file tree, per-file diff, history, compare, staging, commit, and sync (fetch/pull/push).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -438,6 +438,13 @@
|
|
|
438
438
|
.drawer > .tabs { z-index: 23; }
|
|
439
439
|
.drawer > .compareBar { z-index: 22; }
|
|
440
440
|
.drawer > .syncBar { z-index: 20; }
|
|
441
|
+
/* The roll-back confirmation, and the one deliberate exception to the
|
|
442
|
+
strictly-decreasing rule above: it is the LAST child and outranks every
|
|
443
|
+
other, because it is not a popover dropping through its siblings but a
|
|
444
|
+
modal that has to cover them. It also has to be declared here — the
|
|
445
|
+
`> *:not(.resizer)` rule out-specifies a bare `.confirmScrim`, which is
|
|
446
|
+
what pinned it to the bottom of the drawer as an in-flow flex item. */
|
|
447
|
+
.drawer > .confirmScrim { position: absolute; inset: 0; z-index: 40; }
|
|
441
448
|
/* Maximized drops the inset and the rounding: at full bleed they would only
|
|
442
449
|
waste edge pixels the diff wants. */
|
|
443
450
|
.overlayMax { padding: 0; gap: 0; }
|
|
@@ -726,6 +733,402 @@
|
|
|
726
733
|
}
|
|
727
734
|
.commitPopTop { display: flex; align-items: center; gap: 8px; }
|
|
728
735
|
.commitPopTop .commitWhen { margin-right: auto; }
|
|
736
|
+
/* Author / committer / exact date — the precise answers the row's summary
|
|
737
|
+
("3 weeks ago") deliberately defers to the hover card. */
|
|
738
|
+
.commitPopMeta {
|
|
739
|
+
display: flex; flex-direction: column; gap: 2px;
|
|
740
|
+
color: var(--gs-fg-muted);
|
|
741
|
+
font-size: var(--gs-t-dense); line-height: 16px;
|
|
742
|
+
}
|
|
743
|
+
/* The filter box in the history pane head — it flexes to fill the head beside
|
|
744
|
+
the title (the funnel button is flex:none), so a wide pane gets a wide box
|
|
745
|
+
and a narrow pane still leaves room to type. */
|
|
746
|
+
.commitFilter {
|
|
747
|
+
flex: 1 1 auto; min-width: 120px;
|
|
748
|
+
height: var(--gs-h-control); box-sizing: border-box;
|
|
749
|
+
padding: 0 10px;
|
|
750
|
+
border: 1px solid var(--gs-border-soft); border-radius: var(--gs-r-control);
|
|
751
|
+
background: var(--gs-bg); color: var(--gs-fg);
|
|
752
|
+
font-family: inherit; font-size: var(--gs-t-dense);
|
|
753
|
+
outline: none;
|
|
754
|
+
}
|
|
755
|
+
.commitFilter:focus { border-color: var(--gs-accent); }
|
|
756
|
+
.commitFilter::placeholder { color: var(--gs-fg-faint); }
|
|
757
|
+
/* Active criteria under the head: one removable chip each, grammar tokens as
|
|
758
|
+
their own labels — the chip IS the query it would take to retype it. */
|
|
759
|
+
.filterChips {
|
|
760
|
+
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
761
|
+
flex: none;
|
|
762
|
+
padding: 6px 10px;
|
|
763
|
+
border-bottom: 1px solid var(--gs-border-soft);
|
|
764
|
+
}
|
|
765
|
+
.filterChip {
|
|
766
|
+
display: inline-flex; align-items: center; gap: 2px;
|
|
767
|
+
max-width: 220px;
|
|
768
|
+
border: 1px solid var(--gs-border-soft); border-radius: var(--gs-r-control);
|
|
769
|
+
background: var(--gs-surface-2);
|
|
770
|
+
font-size: var(--gs-t-dense); line-height: 18px;
|
|
771
|
+
}
|
|
772
|
+
.filterChipLabel {
|
|
773
|
+
padding: 2px 4px 2px 8px;
|
|
774
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
775
|
+
color: var(--gs-fg-muted);
|
|
776
|
+
}
|
|
777
|
+
.filterChipRemove {
|
|
778
|
+
flex: none;
|
|
779
|
+
padding: 0 6px;
|
|
780
|
+
border: none; border-left: 1px solid var(--gs-border-soft);
|
|
781
|
+
background: transparent; color: var(--gs-fg-dim);
|
|
782
|
+
font-family: inherit; font-size: var(--gs-t-dense); line-height: 20px;
|
|
783
|
+
cursor: pointer;
|
|
784
|
+
}
|
|
785
|
+
.filterChipRemove:hover { color: var(--gs-danger, var(--gs-fg)); }
|
|
786
|
+
.filterClear {
|
|
787
|
+
border: none; background: transparent;
|
|
788
|
+
padding: 2px 4px;
|
|
789
|
+
color: var(--gs-fg-faint);
|
|
790
|
+
font-family: inherit; font-size: var(--gs-t-dense); line-height: 18px;
|
|
791
|
+
cursor: pointer;
|
|
792
|
+
}
|
|
793
|
+
.filterClear:hover { color: var(--gs-fg); }
|
|
794
|
+
/* The funnel dropdown — RefPicker's positioning pattern (relative anchor,
|
|
795
|
+
absolute panel, dismiss-on-outside). Grows leftward: it sits at the pane's
|
|
796
|
+
right edge. */
|
|
797
|
+
.funnel { position: relative; display: inline-flex; }
|
|
798
|
+
/* The button itself carries no geometry of its own — it joined the shared
|
|
799
|
+
control vocabulary above (`.btn, .refButton, …`) so it reads as a sibling of
|
|
800
|
+
every other button in the drawer, not a foreign element. */
|
|
801
|
+
.funnelButton.funnelButtonActive { color: var(--gs-accent); border-color: var(--gs-accent); }
|
|
802
|
+
|
|
803
|
+
/* The panel is a flex COLUMN with exactly ONE scrolling child. It used to
|
|
804
|
+
scroll itself AND cap its list at 240px: two nested scroll containers in a
|
|
805
|
+
331px box that had ~700px of anchor space to spend, so a long roster got a
|
|
806
|
+
fat inner scrollbar instead of the height already available. The tab strip
|
|
807
|
+
and the footer are `flex: none`; the list takes what is left. */
|
|
808
|
+
.funnelPop {
|
|
809
|
+
position: fixed; z-index: 40;
|
|
810
|
+
display: flex; flex-direction: column;
|
|
811
|
+
width: 320px; max-width: calc(100vw - 24px);
|
|
812
|
+
box-sizing: border-box;
|
|
813
|
+
overflow: hidden;
|
|
814
|
+
border: 1px solid var(--gs-border); border-radius: var(--gs-r-surface);
|
|
815
|
+
background: var(--gs-panel);
|
|
816
|
+
box-shadow: 0 16px 40px var(--gs-shadow);
|
|
817
|
+
font-size: var(--gs-t-dense);
|
|
818
|
+
}
|
|
819
|
+
/* One section at a time — the panel's height is a property of its design,
|
|
820
|
+
not of how many authors the repository happens to have. The strip is a
|
|
821
|
+
segmented control (inset track, raised active chip) rather than one
|
|
822
|
+
outlined pill beside two bare words, which read as a button and two labels
|
|
823
|
+
instead of one three-way choice. */
|
|
824
|
+
.funnelTabs {
|
|
825
|
+
flex: none;
|
|
826
|
+
display: flex; gap: 2px;
|
|
827
|
+
margin: 8px 8px 0;
|
|
828
|
+
padding: 2px;
|
|
829
|
+
border: 1px solid var(--gs-border-soft); border-radius: var(--gs-r-pill);
|
|
830
|
+
background: var(--gs-bg);
|
|
831
|
+
}
|
|
832
|
+
.funnelTab {
|
|
833
|
+
flex: 1; min-width: 0;
|
|
834
|
+
height: 22px; padding: 0 8px;
|
|
835
|
+
border: none; border-radius: var(--gs-r-pill);
|
|
836
|
+
background: transparent;
|
|
837
|
+
color: var(--gs-fg-dim);
|
|
838
|
+
font-family: inherit; font-size: var(--gs-t-dense); line-height: 1;
|
|
839
|
+
white-space: nowrap;
|
|
840
|
+
cursor: pointer;
|
|
841
|
+
transition: background 120ms ease, color 120ms ease;
|
|
842
|
+
}
|
|
843
|
+
.funnelTab:hover { color: var(--gs-fg); }
|
|
844
|
+
.funnelTab.funnelTabActive {
|
|
845
|
+
color: var(--gs-fg);
|
|
846
|
+
background: var(--gs-raise);
|
|
847
|
+
box-shadow: inset 0 0 0 1px var(--gs-border);
|
|
848
|
+
}
|
|
849
|
+
.funnelTab:focus-visible { outline: 2px solid var(--gs-accent); outline-offset: 1px; }
|
|
850
|
+
/* A tab's own criteria count, so the two sections nobody is looking at still
|
|
851
|
+
say they hold something. */
|
|
852
|
+
.funnelTabCount { margin-left: 4px; color: var(--gs-accent); font-variant-numeric: tabular-nums; }
|
|
853
|
+
|
|
854
|
+
/* The body of the active section. Everything inside is `flex: none` except
|
|
855
|
+
the list, which is the one thing allowed to scroll. */
|
|
856
|
+
.funnelPane {
|
|
857
|
+
flex: 1 1 auto; min-height: 0;
|
|
858
|
+
display: flex; flex-direction: column; gap: 6px;
|
|
859
|
+
padding: 8px;
|
|
860
|
+
}
|
|
861
|
+
.funnelCaption {
|
|
862
|
+
flex: none;
|
|
863
|
+
color: var(--gs-fg-faint);
|
|
864
|
+
font-size: 11px; line-height: 14px;
|
|
865
|
+
letter-spacing: 0.04em; text-transform: uppercase;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/* Which bound a picked day lands in. Deliberately NOT the tab strip's shape:
|
|
869
|
+
a rect track with an accent-tinted chip under a caption, because two
|
|
870
|
+
identical pill rows stacked six pixels apart said nothing about meaning two
|
|
871
|
+
different things. */
|
|
872
|
+
.funnelBounds {
|
|
873
|
+
flex: none;
|
|
874
|
+
display: flex; gap: 2px;
|
|
875
|
+
padding: 2px;
|
|
876
|
+
border: 1px solid var(--gs-border-soft); border-radius: var(--gs-r-control);
|
|
877
|
+
background: var(--gs-bg);
|
|
878
|
+
}
|
|
879
|
+
.funnelBoundBtn {
|
|
880
|
+
flex: 1; min-width: 0;
|
|
881
|
+
height: 20px; padding: 0 6px;
|
|
882
|
+
border: none; border-radius: calc(var(--gs-r-control) - 3px);
|
|
883
|
+
background: transparent;
|
|
884
|
+
color: var(--gs-fg-dim);
|
|
885
|
+
font-family: inherit; font-size: var(--gs-t-dense); line-height: 1;
|
|
886
|
+
cursor: pointer;
|
|
887
|
+
transition: background 120ms ease, color 120ms ease;
|
|
888
|
+
}
|
|
889
|
+
.funnelBoundBtn:hover { color: var(--gs-fg); }
|
|
890
|
+
.funnelBoundBtn.funnelBoundBtnActive {
|
|
891
|
+
color: var(--gs-accent);
|
|
892
|
+
background: var(--gs-accent-bg);
|
|
893
|
+
box-shadow: inset 0 0 0 1px var(--gs-accent-border);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/* The calendar — modern-grid look in the drawer's own tokens. */
|
|
897
|
+
.cal { flex: none; display: flex; flex-direction: column; gap: 4px; }
|
|
898
|
+
.calHead { display: flex; align-items: center; gap: 4px; }
|
|
899
|
+
.calTitle {
|
|
900
|
+
flex: 1; text-align: center;
|
|
901
|
+
color: var(--gs-fg);
|
|
902
|
+
font-size: var(--gs-t-dense); line-height: 20px;
|
|
903
|
+
}
|
|
904
|
+
.calNav {
|
|
905
|
+
flex: none;
|
|
906
|
+
width: 22px; height: 20px; padding: 0;
|
|
907
|
+
border: none; border-radius: var(--gs-r-control);
|
|
908
|
+
background: transparent;
|
|
909
|
+
color: var(--gs-fg-dim);
|
|
910
|
+
font-family: inherit; font-size: var(--gs-t-ui);
|
|
911
|
+
cursor: pointer;
|
|
912
|
+
}
|
|
913
|
+
.calNav:hover { color: var(--gs-fg); background: var(--gs-raise); }
|
|
914
|
+
.calWeek, .calGrid {
|
|
915
|
+
display: grid;
|
|
916
|
+
grid-template-columns: repeat(7, 1fr);
|
|
917
|
+
gap: 2px;
|
|
918
|
+
}
|
|
919
|
+
.calWeek span {
|
|
920
|
+
text-align: center;
|
|
921
|
+
color: var(--gs-fg-faint);
|
|
922
|
+
font-size: 11px; line-height: 14px;
|
|
923
|
+
}
|
|
924
|
+
.calGrid button {
|
|
925
|
+
height: 26px;
|
|
926
|
+
border: none; border-radius: var(--gs-r-control);
|
|
927
|
+
background: transparent;
|
|
928
|
+
color: var(--gs-fg-muted);
|
|
929
|
+
font-family: inherit; font-size: var(--gs-t-dense);
|
|
930
|
+
font-variant-numeric: tabular-nums;
|
|
931
|
+
cursor: pointer;
|
|
932
|
+
}
|
|
933
|
+
.calGrid button:hover { background: var(--gs-raise); color: var(--gs-fg); }
|
|
934
|
+
.calGrid button.calOut { color: var(--gs-fg-fainter); }
|
|
935
|
+
.calGrid button.calToday { color: var(--gs-accent); font-weight: 600; }
|
|
936
|
+
/* Days strictly between the two bounds: the span the filter actually means,
|
|
937
|
+
which two lone marked endpoints never showed. */
|
|
938
|
+
.calGrid button.calIn { background: var(--gs-accent-bg); color: var(--gs-fg); }
|
|
939
|
+
.calGrid button.calMark {
|
|
940
|
+
background: var(--gs-accent);
|
|
941
|
+
color: var(--gs-on-accent);
|
|
942
|
+
font-weight: 600;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/* The two bound readouts, side by side under the grid. */
|
|
946
|
+
.funnelBoundRows { flex: none; display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
|
|
947
|
+
.funnelBoundRow {
|
|
948
|
+
display: flex; align-items: center; gap: 4px; min-width: 0;
|
|
949
|
+
box-sizing: border-box;
|
|
950
|
+
padding: 3px 6px;
|
|
951
|
+
border: 1px solid var(--gs-border-soft); border-radius: var(--gs-r-control);
|
|
952
|
+
background: var(--gs-bg);
|
|
953
|
+
font-size: 11px; line-height: 16px;
|
|
954
|
+
}
|
|
955
|
+
.funnelBoundKey { flex: none; color: var(--gs-fg-faint); }
|
|
956
|
+
.funnelBoundVal {
|
|
957
|
+
flex: 1; min-width: 0;
|
|
958
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
959
|
+
color: var(--gs-fg-faint);
|
|
960
|
+
font-variant-numeric: tabular-nums;
|
|
961
|
+
}
|
|
962
|
+
.funnelBoundValSet { color: var(--gs-fg); }
|
|
963
|
+
.funnelBoundClear {
|
|
964
|
+
flex: none;
|
|
965
|
+
padding: 0 2px;
|
|
966
|
+
border: none; background: transparent;
|
|
967
|
+
color: var(--gs-fg-dim);
|
|
968
|
+
font-family: inherit; font-size: var(--gs-t-dense); line-height: 1;
|
|
969
|
+
cursor: pointer;
|
|
970
|
+
}
|
|
971
|
+
.funnelBoundClear:hover { color: var(--gs-danger, var(--gs-fg)); }
|
|
972
|
+
|
|
973
|
+
.funnelSearch {
|
|
974
|
+
flex: none;
|
|
975
|
+
box-sizing: border-box;
|
|
976
|
+
height: 26px; padding: 0 8px;
|
|
977
|
+
border: 1px solid var(--gs-border-soft); border-radius: var(--gs-r-control);
|
|
978
|
+
background: var(--gs-bg); color: var(--gs-fg);
|
|
979
|
+
font-family: inherit; font-size: var(--gs-t-dense);
|
|
980
|
+
outline: none;
|
|
981
|
+
}
|
|
982
|
+
.funnelSearch::placeholder { color: var(--gs-fg-faint); }
|
|
983
|
+
.funnelSearch:focus { border-color: var(--gs-accent); box-shadow: 0 0 0 2px var(--gs-accent-bg); }
|
|
984
|
+
.funnelList {
|
|
985
|
+
flex: 1 1 auto; min-height: 0;
|
|
986
|
+
overflow-y: auto; overscroll-behavior: contain;
|
|
987
|
+
display: flex; flex-direction: column; gap: 1px;
|
|
988
|
+
margin: 0 -4px; padding: 0 4px;
|
|
989
|
+
scrollbar-width: thin; scrollbar-color: var(--gs-fg-fainter) transparent;
|
|
990
|
+
}
|
|
991
|
+
.funnelList::-webkit-scrollbar { width: 10px; }
|
|
992
|
+
.funnelList::-webkit-scrollbar-track { background: transparent; }
|
|
993
|
+
.funnelList::-webkit-scrollbar-thumb {
|
|
994
|
+
background: var(--gs-fg-fainter);
|
|
995
|
+
border: 3px solid transparent; border-radius: 5px;
|
|
996
|
+
background-clip: content-box;
|
|
997
|
+
}
|
|
998
|
+
/* Every row is the same four slots — chevron, tick, glyph, name — then the
|
|
999
|
+
count. Directories used to skip the glyph that files carried, so a folder
|
|
1000
|
+
name and the file names under it started at different x and the list had a
|
|
1001
|
+
ragged left edge. */
|
|
1002
|
+
.funnelRow {
|
|
1003
|
+
display: flex; align-items: center; gap: 6px;
|
|
1004
|
+
box-sizing: border-box;
|
|
1005
|
+
min-height: 24px; padding: 0 4px;
|
|
1006
|
+
border-radius: var(--gs-r-control);
|
|
1007
|
+
cursor: pointer;
|
|
1008
|
+
}
|
|
1009
|
+
.funnelRow:hover { background: var(--gs-raise); }
|
|
1010
|
+
.funnelRow:has(input[type='checkbox']:checked) .funnelName,
|
|
1011
|
+
.funnelRow:has(input[type='checkbox']:indeterminate) .funnelName { color: var(--gs-fg); }
|
|
1012
|
+
/* The tick IS the input — no wrapper span, no visually-hidden twin: the live
|
|
1013
|
+
probes select `[class*="funnelRow"] input[type=checkbox]` and the directory
|
|
1014
|
+
row's DIRECT child, so the markup has to stay flat. `appearance: none`
|
|
1015
|
+
hands us the box; the mark is a rotated border, so nothing ships as an
|
|
1016
|
+
asset and every state recolors with the theme. */
|
|
1017
|
+
.funnelRow input[type='checkbox'] {
|
|
1018
|
+
appearance: none; -webkit-appearance: none;
|
|
1019
|
+
flex: none; position: relative;
|
|
1020
|
+
box-sizing: border-box;
|
|
1021
|
+
width: 14px; height: 14px; margin: 0;
|
|
1022
|
+
border: 1px solid var(--gs-fg-fainter); border-radius: 3px;
|
|
1023
|
+
background: transparent;
|
|
1024
|
+
cursor: pointer;
|
|
1025
|
+
transition: background 120ms ease, border-color 120ms ease;
|
|
1026
|
+
}
|
|
1027
|
+
.funnelRow input[type='checkbox']:hover { border-color: var(--gs-fg-dim); }
|
|
1028
|
+
.funnelRow input[type='checkbox']:checked,
|
|
1029
|
+
.funnelRow input[type='checkbox']:indeterminate {
|
|
1030
|
+
background: var(--gs-accent); border-color: var(--gs-accent);
|
|
1031
|
+
}
|
|
1032
|
+
.funnelRow input[type='checkbox']:focus-visible { outline: 2px solid var(--gs-accent); outline-offset: 1px; }
|
|
1033
|
+
.funnelRow input[type='checkbox']:checked::after {
|
|
1034
|
+
content: ''; position: absolute;
|
|
1035
|
+
left: 4px; top: 1px; width: 3px; height: 7px;
|
|
1036
|
+
border: solid var(--gs-on-accent); border-width: 0 1.5px 1.5px 0;
|
|
1037
|
+
transform: rotate(42deg);
|
|
1038
|
+
}
|
|
1039
|
+
/* Partial — a folder some of whose subtree is ticked. */
|
|
1040
|
+
.funnelRow input[type='checkbox']:indeterminate::after {
|
|
1041
|
+
content: ''; position: absolute;
|
|
1042
|
+
left: 2px; top: 5px; width: 8px; height: 2px;
|
|
1043
|
+
border-radius: 1px;
|
|
1044
|
+
background: var(--gs-on-accent);
|
|
1045
|
+
}
|
|
1046
|
+
/* Tree chrome in the drawer's own visual language: a directory folds with the
|
|
1047
|
+
chevron the file tree uses, and both glyphs are inline SVG in the shape
|
|
1048
|
+
IntelliJ's New UI icon set uses — a 16px grid, 1px strokes, no fill,
|
|
1049
|
+
rounded joins, outlines rather than the filled silhouettes the old UI
|
|
1050
|
+
shipped. Drawn in `PathDirGlyph` / `PathFileGlyph` rather than imported:
|
|
1051
|
+
the bundle purity gate forbids an icon package, and the picker needs
|
|
1052
|
+
exactly these two. Indentation is the row's own padding ALONE; the nested
|
|
1053
|
+
rail this used to draw was `--gs-border-soft` over `--gs-panel` (the same
|
|
1054
|
+
value as `--gs-raise` in the GitHub palettes, so it vanished under hover)
|
|
1055
|
+
and its margin double-counted every level. */
|
|
1056
|
+
.pathNode { display: flex; flex-direction: column; }
|
|
1057
|
+
.pathChildren { display: flex; flex-direction: column; }
|
|
1058
|
+
/* One shared slot so a folder and a file start their names at the same x —
|
|
1059
|
+
New UI draws both on the same 16px grid, and the column only lines up if
|
|
1060
|
+
the box does too. `--gs-fg-dim` is the neutral the tree icons sit at there:
|
|
1061
|
+
present, but never louder than the filename beside them. */
|
|
1062
|
+
.pathFileGlyph, .pathDirGlyph {
|
|
1063
|
+
flex: none;
|
|
1064
|
+
width: 16px; height: 16px;
|
|
1065
|
+
color: var(--gs-fg-dim);
|
|
1066
|
+
}
|
|
1067
|
+
.funnelRow:hover .pathFileGlyph,
|
|
1068
|
+
.funnelRow:hover .pathDirGlyph { color: var(--gs-fg-muted); }
|
|
1069
|
+
.funnelName {
|
|
1070
|
+
flex: 1; min-width: 0;
|
|
1071
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
1072
|
+
color: var(--gs-fg-muted);
|
|
1073
|
+
}
|
|
1074
|
+
.funnelCount {
|
|
1075
|
+
flex: none;
|
|
1076
|
+
color: var(--gs-fg-faint);
|
|
1077
|
+
font-size: 11px;
|
|
1078
|
+
font-variant-numeric: tabular-nums;
|
|
1079
|
+
}
|
|
1080
|
+
.funnelMore { color: var(--gs-fg-faint); padding: 4px 6px; }
|
|
1081
|
+
.funnelChevron {
|
|
1082
|
+
flex: none;
|
|
1083
|
+
width: 14px; padding: 0;
|
|
1084
|
+
border: none; background: transparent;
|
|
1085
|
+
color: var(--gs-fg-faint);
|
|
1086
|
+
font-family: inherit; font-size: 9px; line-height: 16px;
|
|
1087
|
+
cursor: pointer;
|
|
1088
|
+
}
|
|
1089
|
+
.funnelChevron:hover:not(:disabled) { color: var(--gs-fg); }
|
|
1090
|
+
.funnelChevron:disabled { cursor: default; }
|
|
1091
|
+
.funnelPresets { flex: none; display: flex; gap: 4px; }
|
|
1092
|
+
/* `.funnelPreset` shipped with `flex: none` and nothing else, so it never
|
|
1093
|
+
joined the shared button vocabulary and the three date presets rendered as
|
|
1094
|
+
the browser's default grey buttons — the one patch of unthemed chrome in
|
|
1095
|
+
the drawer. It is in both selector lists now; this only sizes them to share
|
|
1096
|
+
one row. */
|
|
1097
|
+
.funnelPreset { flex: 1 1 0; min-width: 0; padding: 0 6px; }
|
|
1098
|
+
/* Qualified, like `.funnelButtonActive`: the shared vocabulary is declared
|
|
1099
|
+
several hundred lines BELOW this one, so a bare modifier would lose its
|
|
1100
|
+
background to that rule's `background: transparent`. */
|
|
1101
|
+
.funnelPreset.funnelPresetActive {
|
|
1102
|
+
color: var(--gs-accent);
|
|
1103
|
+
border-color: var(--gs-accent);
|
|
1104
|
+
background: var(--gs-accent-bg);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/* The footer says what the panel is currently asking git for. Without it the
|
|
1108
|
+
only feedback lived on the chip row BEHIND the panel, so a reader had to
|
|
1109
|
+
close the thing to find out whether their ticks took. */
|
|
1110
|
+
.funnelFoot {
|
|
1111
|
+
flex: none;
|
|
1112
|
+
display: flex; align-items: center; justify-content: space-between; gap: 8px;
|
|
1113
|
+
padding: 6px 10px;
|
|
1114
|
+
border-top: 1px solid var(--gs-border-soft);
|
|
1115
|
+
background: var(--gs-bg);
|
|
1116
|
+
font-size: 11px; line-height: 16px;
|
|
1117
|
+
}
|
|
1118
|
+
.funnelFootCount { color: var(--gs-fg-faint); font-variant-numeric: tabular-nums; }
|
|
1119
|
+
.funnelFootCountOn { color: var(--gs-fg-muted); }
|
|
1120
|
+
.funnelFootClear {
|
|
1121
|
+
flex: none;
|
|
1122
|
+
padding: 2px 6px;
|
|
1123
|
+
border: none; border-radius: var(--gs-r-control);
|
|
1124
|
+
background: transparent;
|
|
1125
|
+
color: var(--gs-fg-dim);
|
|
1126
|
+
font-family: inherit; font-size: 11px; line-height: 16px;
|
|
1127
|
+
cursor: pointer;
|
|
1128
|
+
}
|
|
1129
|
+
.funnelFootClear:hover:not(:disabled) { color: var(--gs-fg); background: var(--gs-raise); }
|
|
1130
|
+
.funnelFootClear:disabled { color: var(--gs-fg-fainter); cursor: default; }
|
|
1131
|
+
|
|
729
1132
|
.commitPopSubject {
|
|
730
1133
|
color: var(--gs-fg);
|
|
731
1134
|
font-size: var(--gs-t-ui); font-weight: 600; line-height: 18px;
|
|
@@ -954,6 +1357,13 @@
|
|
|
954
1357
|
font-size: var(--gs-t-meta); line-height: 15px;
|
|
955
1358
|
}
|
|
956
1359
|
.commitWhen { flex: none; color: var(--gs-fg-faint); }
|
|
1360
|
+
/* Who wrote it — sits between hash and relative time, capped so a long name
|
|
1361
|
+
cannot evict the time from the row. */
|
|
1362
|
+
.commitAuthor {
|
|
1363
|
+
flex: none; max-width: 120px;
|
|
1364
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
1365
|
+
color: var(--gs-fg-dim);
|
|
1366
|
+
}
|
|
957
1367
|
|
|
958
1368
|
.body { display: flex; flex: 1; min-height: 0; z-index: 1; }
|
|
959
1369
|
|
|
@@ -997,6 +1407,40 @@
|
|
|
997
1407
|
white-space: nowrap;
|
|
998
1408
|
color: var(--gs-fg-dim); font-size: var(--gs-t-dense);
|
|
999
1409
|
}
|
|
1410
|
+
/* The file filter, revealed under the tools rather than squeezed into them:
|
|
1411
|
+
the pane drags down to 170px, where the tick, the count and two icon buttons
|
|
1412
|
+
already own the row. It costs a row only while it is being used. */
|
|
1413
|
+
.treeFilter {
|
|
1414
|
+
position: relative;
|
|
1415
|
+
flex: none;
|
|
1416
|
+
display: flex; align-items: center;
|
|
1417
|
+
padding: 6px var(--gs-gutter-pane);
|
|
1418
|
+
border-bottom: 1px solid var(--gs-border-soft);
|
|
1419
|
+
}
|
|
1420
|
+
.treeFilterInput {
|
|
1421
|
+
flex: 1 1 auto; min-width: 0;
|
|
1422
|
+
height: var(--gs-h-compact); box-sizing: border-box;
|
|
1423
|
+
padding: 0 24px 0 8px;
|
|
1424
|
+
border: 1px solid var(--gs-border-soft); border-radius: var(--gs-r-control);
|
|
1425
|
+
background: var(--gs-bg); color: var(--gs-fg);
|
|
1426
|
+
font-family: inherit; font-size: var(--gs-t-dense);
|
|
1427
|
+
outline: none;
|
|
1428
|
+
}
|
|
1429
|
+
.treeFilterInput:focus { border-color: var(--gs-accent); }
|
|
1430
|
+
.treeFilterInput::placeholder { color: var(--gs-fg-faint); }
|
|
1431
|
+
/* Inside the field's right padding, so clearing does not move the field or
|
|
1432
|
+
change its width as the button comes and goes. */
|
|
1433
|
+
.treeFilterClear {
|
|
1434
|
+
position: absolute; right: calc(var(--gs-gutter-pane) + 6px);
|
|
1435
|
+
display: flex; align-items: center; justify-content: center;
|
|
1436
|
+
width: 16px; height: 16px;
|
|
1437
|
+
padding: 0;
|
|
1438
|
+
border: 0; border-radius: var(--gs-r-pill);
|
|
1439
|
+
background: transparent; color: var(--gs-fg-faint);
|
|
1440
|
+
font-size: var(--gs-t-ui); line-height: 1;
|
|
1441
|
+
cursor: pointer;
|
|
1442
|
+
}
|
|
1443
|
+
.treeFilterClear:hover { color: var(--gs-fg); }
|
|
1000
1444
|
.tree {
|
|
1001
1445
|
flex: 1; min-height: 0;
|
|
1002
1446
|
margin: 0; padding: 6px 0 16px;
|
|
@@ -1212,7 +1656,7 @@
|
|
|
1212
1656
|
}
|
|
1213
1657
|
.syncLevel { color: var(--gs-fg-faint); }
|
|
1214
1658
|
.syncSpacer { flex: 1 1 auto; }
|
|
1215
|
-
.btn:disabled, .treeIcon:disabled, .miniBtn:disabled, .refButton:disabled {
|
|
1659
|
+
.btn:disabled, .treeIcon:disabled, .miniBtn:disabled, .refButton:disabled, .funnelButton:disabled {
|
|
1216
1660
|
opacity: 0.45;
|
|
1217
1661
|
cursor: default;
|
|
1218
1662
|
}
|
|
@@ -1239,6 +1683,30 @@
|
|
|
1239
1683
|
toggle trails it rather than displacing the tree's left edge. */
|
|
1240
1684
|
.fileLi { display: flex; align-items: stretch; }
|
|
1241
1685
|
.fileLi .file { flex: 1 1 auto; min-width: 0; }
|
|
1686
|
+
/* Roll back, in the same 22px slot the tick occupies on the other side — the
|
|
1687
|
+
row already has two gutters, and a third would eat the name.
|
|
1688
|
+
It holds its width at all times and only fades in on hover or keyboard
|
|
1689
|
+
focus: revealing it by `display` would re-flow the name column under the
|
|
1690
|
+
pointer, and a destructive control that shifts the row as it appears is one
|
|
1691
|
+
the pointer arrives at by accident. */
|
|
1692
|
+
.fileDiscard {
|
|
1693
|
+
flex: none;
|
|
1694
|
+
display: flex; align-items: center; justify-content: center;
|
|
1695
|
+
width: 22px;
|
|
1696
|
+
padding: 0;
|
|
1697
|
+
border: 0;
|
|
1698
|
+
background: transparent;
|
|
1699
|
+
color: var(--gs-fg-faint);
|
|
1700
|
+
opacity: 0;
|
|
1701
|
+
cursor: pointer;
|
|
1702
|
+
transition: opacity 120ms ease, color 120ms ease;
|
|
1703
|
+
}
|
|
1704
|
+
.fileLi:hover .fileDiscard, .fileDiscard:focus-visible { opacity: 1; }
|
|
1705
|
+
/* Red on hover, and only on hover. Standing red would make every row of a
|
|
1706
|
+
working tree look like a warning; arriving under the pointer is where the
|
|
1707
|
+
colour has something to say. */
|
|
1708
|
+
.fileDiscard:hover { color: var(--gs-del); }
|
|
1709
|
+
.fileDiscard:focus-visible { outline: 2px solid var(--gs-accent); outline-offset: -2px; border-radius: var(--gs-r-control); }
|
|
1242
1710
|
/* Directory rows need the same two-slot row as files, but their subtree list
|
|
1243
1711
|
has to stay a child of the <li> — hence a wrapper around just the row. */
|
|
1244
1712
|
.treeRow { display: flex; align-items: stretch; }
|
|
@@ -1359,7 +1827,7 @@
|
|
|
1359
1827
|
* its own padding, radius and font-size, and the same "small secondary button"
|
|
1360
1828
|
* had drifted to four heights and three radii across one screen.
|
|
1361
1829
|
*/
|
|
1362
|
-
.btn, .miniBtn, .treeIcon, .commitCopy, .scopeBtn, .refButton {
|
|
1830
|
+
.btn, .miniBtn, .treeIcon, .commitCopy, .scopeBtn, .refButton, .funnelButton, .funnelPreset {
|
|
1363
1831
|
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
|
|
1364
1832
|
box-sizing: border-box;
|
|
1365
1833
|
flex: none;
|
|
@@ -1377,13 +1845,14 @@
|
|
|
1377
1845
|
transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
|
|
1378
1846
|
}
|
|
1379
1847
|
.btn:hover, .miniBtn:hover, .treeIcon:hover, .commitCopy:hover,
|
|
1380
|
-
.scopeBtn:hover, .refButton:hover {
|
|
1848
|
+
.scopeBtn:hover, .refButton:hover, .funnelButton:hover, .funnelPreset:hover {
|
|
1381
1849
|
background: var(--gs-raise);
|
|
1382
1850
|
color: var(--gs-fg);
|
|
1383
1851
|
border-color: var(--gs-fg-fainter);
|
|
1384
1852
|
}
|
|
1385
1853
|
.btn:focus-visible, .miniBtn:focus-visible, .treeIcon:focus-visible,
|
|
1386
|
-
.commitCopy:focus-visible, .scopeBtn:focus-visible, .refButton:focus-visible
|
|
1854
|
+
.commitCopy:focus-visible, .scopeBtn:focus-visible, .refButton:focus-visible,
|
|
1855
|
+
.funnelPreset:focus-visible {
|
|
1387
1856
|
outline: 2px solid var(--gs-accent);
|
|
1388
1857
|
outline-offset: 1px;
|
|
1389
1858
|
}
|
|
@@ -1394,8 +1863,9 @@
|
|
|
1394
1863
|
foreign. Controls that live INSIDE a panel or pane stay 8px rects. */
|
|
1395
1864
|
.btn, .refButton { border-radius: var(--gs-r-pill); }
|
|
1396
1865
|
|
|
1397
|
-
/* Pane tools give their vertical space back to the diff
|
|
1398
|
-
|
|
1866
|
+
/* Pane tools give their vertical space back to the diff; the date presets sit
|
|
1867
|
+
inside a 320px popover, so they take the compact height too. */
|
|
1868
|
+
.treeIcon, .commitCopy, .funnelPreset {
|
|
1399
1869
|
height: var(--gs-h-compact);
|
|
1400
1870
|
padding: var(--gs-pad-compact);
|
|
1401
1871
|
font-size: var(--gs-t-dense);
|
|
@@ -1405,6 +1875,9 @@
|
|
|
1405
1875
|
the pane drags down to, two more text buttons cannot share a row with the
|
|
1406
1876
|
count — and these two are the pair a tree header always carries. */
|
|
1407
1877
|
.treeIcon { width: var(--gs-h-compact); padding: 0; }
|
|
1878
|
+
/* Qualified, like `.funnelButtonActive`: this sits inside the shared button
|
|
1879
|
+
vocabulary, which sets `color` at the same specificity a few lines above. */
|
|
1880
|
+
.treeIcon.treeIconOn { color: var(--gs-accent); border-color: var(--gs-accent); }
|
|
1408
1881
|
.treeIconGlyph { display: block; font-size: var(--gs-t-meta); line-height: 1; }
|
|
1409
1882
|
.treeIconDown { transform: rotate(90deg); }
|
|
1410
1883
|
|
|
@@ -1510,3 +1983,47 @@
|
|
|
1510
1983
|
font-size: var(--gs-t-meta); line-height: 15px;
|
|
1511
1984
|
cursor: default;
|
|
1512
1985
|
}
|
|
1986
|
+
|
|
1987
|
+
/* ---- the roll-back confirmation ----
|
|
1988
|
+
*
|
|
1989
|
+
* Scoped to the drawer rather than the viewport: `.drawer` is the positioned
|
|
1990
|
+
* ancestor, so the scrim dims the thing the question is about and leaves the
|
|
1991
|
+
* session behind it alone. It is also the only modal surface in this plugin —
|
|
1992
|
+
* everything else the drawer does can be done again.
|
|
1993
|
+
*/
|
|
1994
|
+
.confirmScrim {
|
|
1995
|
+
/* `position`, `inset` and the stacking rank live with the drawer's other
|
|
1996
|
+
child ranks — see `.drawer > .confirmScrim`. */
|
|
1997
|
+
display: flex; align-items: center; justify-content: center;
|
|
1998
|
+
padding: 24px;
|
|
1999
|
+
background: var(--gs-backdrop);
|
|
2000
|
+
animation: gsFade 120ms ease-out;
|
|
2001
|
+
}
|
|
2002
|
+
.confirmBox {
|
|
2003
|
+
display: flex; flex-direction: column; gap: 10px;
|
|
2004
|
+
box-sizing: border-box;
|
|
2005
|
+
width: min(440px, 100%);
|
|
2006
|
+
padding: 18px;
|
|
2007
|
+
border: 1px solid var(--gs-border); border-radius: var(--gs-r-drawer);
|
|
2008
|
+
background: var(--gs-bg);
|
|
2009
|
+
box-shadow: 0 18px 48px var(--gs-shadow);
|
|
2010
|
+
}
|
|
2011
|
+
.confirmTitle { color: var(--gs-fg); font-size: var(--gs-t-ui); font-weight: 600; }
|
|
2012
|
+
/* The body carries a path, which is the one string here that can be long and
|
|
2013
|
+
has no sensible break points of its own. */
|
|
2014
|
+
.confirmBody {
|
|
2015
|
+
color: var(--gs-fg-dim);
|
|
2016
|
+
font-size: var(--gs-t-dense); line-height: 1.55;
|
|
2017
|
+
overflow-wrap: anywhere;
|
|
2018
|
+
}
|
|
2019
|
+
/* Cancel first in the DOM so it takes focus and the Tab order starts on the
|
|
2020
|
+
harmless answer; `row-reverse` puts the destructive one where the eye ends. */
|
|
2021
|
+
.confirmActions { display: flex; flex-direction: row-reverse; gap: 8px; margin-top: 2px; }
|
|
2022
|
+
/* Qualified rather than a bare modifier: written this way it out-specifies
|
|
2023
|
+
`.btn` wherever either rule sits, which is the failure the drawer already
|
|
2024
|
+
shipped once. */
|
|
2025
|
+
.btn.btnDanger {
|
|
2026
|
+
border-color: var(--gs-del); background: var(--gs-del); color: var(--gs-on-accent, #fff);
|
|
2027
|
+
font-weight: 600;
|
|
2028
|
+
}
|
|
2029
|
+
.btn.btnDanger:hover:not(:disabled) { border-color: var(--gs-del); background: var(--gs-del); color: var(--gs-on-accent, #fff); }
|