@young1lin/dsh-ui-gitworkbench 0.1.6 → 0.1.7
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 +10 -0
- package/CHANGELOG_EN.md +10 -0
- package/lib/client.js +565 -355
- package/package.json +1 -1
- package/src/client/GitWorkbenchPanel.module.css +42 -9
- package/src/client/GitWorkbenchPanel.tsx +250 -78
- package/src/client/history-layout.ts +52 -0
- package/src/client/locales.ts +8 -0
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.7",
|
|
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",
|
|
@@ -1278,7 +1278,11 @@
|
|
|
1278
1278
|
the compact control it hosts, so a pane with tools and a pane without one
|
|
1279
1279
|
still line up across the divider. */
|
|
1280
1280
|
.paneHead {
|
|
1281
|
-
|
|
1281
|
+
/* Wraps rather than overflows. Dragged narrow the pane cannot hold a title,
|
|
1282
|
+
a funnel and a search box on one line, and what it used to do was clip the
|
|
1283
|
+
search box at the pane's edge — a control half off the screen, with no
|
|
1284
|
+
sign that the rest of it existed. */
|
|
1285
|
+
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
|
|
1282
1286
|
flex: none;
|
|
1283
1287
|
min-height: 37px; box-sizing: border-box;
|
|
1284
1288
|
padding: 6px var(--gs-gutter-pane);
|
|
@@ -1320,10 +1324,12 @@
|
|
|
1320
1324
|
.commitLine {
|
|
1321
1325
|
display: flex; align-items: stretch;
|
|
1322
1326
|
flex: none;
|
|
1323
|
-
/*
|
|
1324
|
-
row
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
+
/* Published by the panel from `COMMIT_ROW_H`, which the lane graph also draws
|
|
1328
|
+
each row's segment at — one source, because a row and its segment that
|
|
1329
|
+
disagree leave the lanes short of the seam between rows. The two
|
|
1330
|
+
arrangements want different rows, which is why this is a property and not
|
|
1331
|
+
a number. Guarded by tests/commit-row-height.test.ts. */
|
|
1332
|
+
height: var(--gs-commit-row);
|
|
1327
1333
|
}
|
|
1328
1334
|
.graphCell { flex: none; display: block; }
|
|
1329
1335
|
/* An inset row, like dsh's own session rows: 12px radius, filled when active. */
|
|
@@ -1343,6 +1349,20 @@
|
|
|
1343
1349
|
overflow: hidden;
|
|
1344
1350
|
transition: background 120ms ease, border-color 120ms ease;
|
|
1345
1351
|
}
|
|
1352
|
+
/* Beside the diff the row turns into two lines: a pane of about 340px has no
|
|
1353
|
+
width for the hash, the author, the date AND the subject on one, and the
|
|
1354
|
+
subject is the part that would have given way. Above the diff it stays one
|
|
1355
|
+
line — see the row's two shapes in `CommitRow`. */
|
|
1356
|
+
.commitsPane[data-layout="columns"] .commit {
|
|
1357
|
+
flex-direction: column; align-items: stretch; justify-content: center; gap: 2px;
|
|
1358
|
+
}
|
|
1359
|
+
/* The subject line must not absorb the row's spare height, or the two lines
|
|
1360
|
+
stop sitting together in the middle of the row. */
|
|
1361
|
+
.commitsPane[data-layout="columns"] .commitSubjectRow { flex: none; }
|
|
1362
|
+
.commitTop {
|
|
1363
|
+
display: flex; align-items: center; justify-content: space-between; gap: 8px;
|
|
1364
|
+
min-width: 0;
|
|
1365
|
+
}
|
|
1346
1366
|
.commit:hover { background: var(--gs-raise); }
|
|
1347
1367
|
.commitActive { border-color: var(--gs-accent-border); background: var(--gs-accent-bg); }
|
|
1348
1368
|
/* Pushed to the right end of the row, and never squeezed: the subject is what
|
|
@@ -2093,7 +2113,7 @@
|
|
|
2093
2113
|
* its own padding, radius and font-size, and the same "small secondary button"
|
|
2094
2114
|
* had drifted to four heights and three radii across one screen.
|
|
2095
2115
|
*/
|
|
2096
|
-
.btn, .miniBtn, .treeIcon, .commitCopy, .scopeBtn, .refButton, .funnelButton, .funnelPreset {
|
|
2116
|
+
.btn, .miniBtn, .treeIcon, .commitCopy, .scopeBtn, .refButton, .funnelButton, .funnelPreset, .layoutButton {
|
|
2097
2117
|
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
|
|
2098
2118
|
box-sizing: border-box;
|
|
2099
2119
|
flex: none;
|
|
@@ -2111,14 +2131,14 @@
|
|
|
2111
2131
|
transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
|
|
2112
2132
|
}
|
|
2113
2133
|
.btn:hover, .miniBtn:hover, .treeIcon:hover, .commitCopy:hover,
|
|
2114
|
-
.scopeBtn:hover, .refButton:hover, .funnelButton:hover, .funnelPreset:hover {
|
|
2134
|
+
.scopeBtn:hover, .refButton:hover, .funnelButton:hover, .funnelPreset:hover, .layoutButton:hover {
|
|
2115
2135
|
background: var(--gs-raise);
|
|
2116
2136
|
color: var(--gs-fg);
|
|
2117
2137
|
border-color: var(--gs-fg-fainter);
|
|
2118
2138
|
}
|
|
2119
2139
|
.btn:focus-visible, .miniBtn:focus-visible, .treeIcon:focus-visible,
|
|
2120
2140
|
.commitCopy:focus-visible, .scopeBtn:focus-visible, .refButton:focus-visible,
|
|
2121
|
-
.funnelPreset:focus-visible {
|
|
2141
|
+
.funnelPreset:focus-visible, .layoutButton:focus-visible {
|
|
2122
2142
|
outline: 2px solid var(--gs-accent);
|
|
2123
2143
|
outline-offset: 1px;
|
|
2124
2144
|
}
|
|
@@ -2131,7 +2151,7 @@
|
|
|
2131
2151
|
|
|
2132
2152
|
/* Pane tools give their vertical space back to the diff; the date presets sit
|
|
2133
2153
|
inside a 320px popover, so they take the compact height too. */
|
|
2134
|
-
.treeIcon, .commitCopy, .funnelPreset {
|
|
2154
|
+
.treeIcon, .commitCopy, .funnelPreset, .layoutButton {
|
|
2135
2155
|
height: var(--gs-h-compact);
|
|
2136
2156
|
padding: var(--gs-pad-compact);
|
|
2137
2157
|
font-size: var(--gs-t-dense);
|
|
@@ -2141,6 +2161,19 @@
|
|
|
2141
2161
|
the pane drags down to, two more text buttons cannot share a row with the
|
|
2142
2162
|
count — and these two are the pair a tree header always carries. */
|
|
2143
2163
|
.treeIcon { width: var(--gs-h-compact); padding: 0; }
|
|
2164
|
+
/* The arrangement switch: two square icon buttons at the end of the commit
|
|
2165
|
+
list's head, side by side with no gap so they read as one control. Icon-only
|
|
2166
|
+
for the same reason the tree's are — the head already carries a title, a
|
|
2167
|
+
funnel and a search box, and at the 190px the pane drags down to there is no
|
|
2168
|
+
room for two more words. */
|
|
2169
|
+
/* Pushed to the far end of History's toolbar row, away from the ref picker:
|
|
2170
|
+
one changes WHAT is listed, the other only where it is drawn. */
|
|
2171
|
+
.layoutSwitch { display: inline-flex; flex: none; gap: 2px; margin-left: auto; }
|
|
2172
|
+
.layoutButton { width: var(--gs-h-compact); padding: 0; }
|
|
2173
|
+
/* Qualified, like `.treeIconOn`: the shared vocabulary sets `color` at the
|
|
2174
|
+
same specificity a few rules above. */
|
|
2175
|
+
.layoutButton.layoutButtonOn { color: var(--gs-accent); border-color: var(--gs-accent); }
|
|
2176
|
+
.layoutGlyph { display: block; }
|
|
2144
2177
|
/* Qualified, like `.funnelButtonActive`: this sits inside the shared button
|
|
2145
2178
|
vocabulary, which sets `color` at the same specificity a few lines above. */
|
|
2146
2179
|
.treeIcon.treeIconOn { color: var(--gs-accent); border-color: var(--gs-accent); }
|
|
@@ -60,6 +60,7 @@ import { parsePatch } from '../patch-model.ts'
|
|
|
60
60
|
import { alignRows, blockCount, blockIsWholeFile, blockLines, blockTally, sideBodyState, type SideCell, type SideRow } from './side-rows.ts'
|
|
61
61
|
import { countBlocks, unifiedBlocks } from './diff-nav.ts'
|
|
62
62
|
import { clampPane, neighbourWidth } from './pane-size.ts'
|
|
63
|
+
import { COMMIT_ROW_H, DEFAULT_HISTORY_LAYOUT, isHistoryLayout, type HistoryLayout } from './history-layout.ts'
|
|
63
64
|
import { useChangeNav } from './use-change-nav.ts'
|
|
64
65
|
import {
|
|
65
66
|
applySaveOk, applySides, armEdit, armRefusal, DISARMED, editableSides, gateLeave, isDirty,
|
|
@@ -423,6 +424,10 @@ const STORE_PANES = 'dsh-ui-gitworkbench:panes'
|
|
|
423
424
|
* belongs here beside the layout rather than in the host's per-project store:
|
|
424
425
|
* two people on one repository should not share each other's place. */
|
|
425
426
|
const STORE_FILES = 'dsh-ui-gitworkbench:files'
|
|
427
|
+
/** Which way the History tab arranges its panes. Its own key rather than a
|
|
428
|
+
* field of the appearance object: that one is about colour, and this choice
|
|
429
|
+
* has to survive a build that adds a palette. */
|
|
430
|
+
const STORE_HISTORY_LAYOUT = 'dsh-ui-gitworkbench:history-layout'
|
|
426
431
|
|
|
427
432
|
/** Dragged pane sizes in px; null on any of them keeps that pane's CSS default. */
|
|
428
433
|
interface PaneWidths {
|
|
@@ -743,6 +748,16 @@ export function GitWorkbenchPanel({ sessionId, useSessions, t, fetchStats, fetch
|
|
|
743
748
|
const [panes, setPanes] = useState<PaneWidths>(
|
|
744
749
|
() => readStored(STORE_PANES, isPaneWidths, DEFAULT_PANES),
|
|
745
750
|
)
|
|
751
|
+
/**
|
|
752
|
+
* The History tab's arrangement.
|
|
753
|
+
*
|
|
754
|
+
* Panel-level, beside the palette rather than inside the tab: the choice has
|
|
755
|
+
* to hold across tab switches and reopens, and the drawer's own card is where
|
|
756
|
+
* the row height it implies is published from.
|
|
757
|
+
*/
|
|
758
|
+
const [historyLayout, setHistoryLayout] = useState<HistoryLayout>(
|
|
759
|
+
() => readStored(STORE_HISTORY_LAYOUT, isHistoryLayout, DEFAULT_HISTORY_LAYOUT),
|
|
760
|
+
)
|
|
746
761
|
/** Per-project and global styling; both scopes, unresolved. */
|
|
747
762
|
const [style, setStyle] = useState<StyleSettings>(EMPTY_SETTINGS)
|
|
748
763
|
/** Whether dsh's resolved palette is currently dark. */
|
|
@@ -1435,6 +1450,18 @@ export function GitWorkbenchPanel({ sessionId, useSessions, t, fetchStats, fetch
|
|
|
1435
1450
|
if (persist) writeStored(STORE_WIDTH, clamped)
|
|
1436
1451
|
}
|
|
1437
1452
|
|
|
1453
|
+
/**
|
|
1454
|
+
* Pick the History tab's arrangement.
|
|
1455
|
+
*
|
|
1456
|
+
* Written through immediately: unlike a drag this is one click, so there is
|
|
1457
|
+
* no intermediate frame to withhold a synchronous storage write for.
|
|
1458
|
+
* @param next - the arrangement to switch to.
|
|
1459
|
+
*/
|
|
1460
|
+
const applyHistoryLayout = (next: HistoryLayout): void => {
|
|
1461
|
+
setHistoryLayout(next)
|
|
1462
|
+
writeStored(STORE_HISTORY_LAYOUT, next)
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1438
1465
|
/** Tab switch. No direction refetches the working tree: `viewKey` already
|
|
1439
1466
|
* separates the tabs' per-file diff caches, so bumping `gen` here only cost a
|
|
1440
1467
|
* redundant round trip. */
|
|
@@ -1534,6 +1561,8 @@ export function GitWorkbenchPanel({ sessionId, useSessions, t, fetchStats, fetch
|
|
|
1534
1561
|
panes={panes}
|
|
1535
1562
|
onPane={applyPane}
|
|
1536
1563
|
onCommitsTall={applyCommitsTall}
|
|
1564
|
+
historyLayout={historyLayout}
|
|
1565
|
+
onHistoryLayout={applyHistoryLayout}
|
|
1537
1566
|
onClose={() => setOpen(false)}
|
|
1538
1567
|
onRefresh={refresh}
|
|
1539
1568
|
commitDraft={commitDraft}
|
|
@@ -1747,6 +1776,9 @@ interface DrawerProps {
|
|
|
1747
1776
|
onPane: (which: PaneWidthKey, next: number, measured: { drawer: number; commits: number; tree: number }, persist: boolean) => void
|
|
1748
1777
|
/** Drag the History tab's horizontal split: the commit list's height in px. */
|
|
1749
1778
|
onCommitsTall: (next: number, bodyHeight: number, persist: boolean) => void
|
|
1779
|
+
/** Which way the History tab arranges its panes. */
|
|
1780
|
+
historyLayout: HistoryLayout
|
|
1781
|
+
onHistoryLayout: (next: HistoryLayout) => void
|
|
1750
1782
|
onClose: () => void
|
|
1751
1783
|
onRefresh: () => void
|
|
1752
1784
|
/** Commit draft, lifted so a tab switch cannot discard it. */
|
|
@@ -1793,7 +1825,7 @@ interface DrawerProps {
|
|
|
1793
1825
|
onCollapsedChange: (next: Set<string>) => void
|
|
1794
1826
|
}
|
|
1795
1827
|
|
|
1796
|
-
function Drawer({ stats, shown, tab, onSwitchTab, commits, commitHash, onSelectCommit, hasMoreCommits, loadingMore, onLoadMoreCommits, historyRef, onHistoryRef, historyQuery, onHistoryQuery, historyError, fetchAuthors, fetchRepoTree, branches, worktreeBranches, branchesTruncated, baseRef, headRef, onBaseRef, onHeadRef, comparable, t, binding, worktrees, sessionPath, statsPath, onSwitchSource, segments, selected, onSelect, maximized, onToggleMaximized, theme, mode, family, onMode, onFamily, style, background, onStyle, width, onWidth, panes, onPane, onCommitsTall, onClose, onRefresh, commitDraft, onCommitDraft, commitAmend, onCommitAmend, sync, treeLoading, historyLoading, busy, opResult, runOp, fetchDiscardPlan, onOpError, pendingTicks, onTick, fetchFileDiff, fetchFileSides, writeChecked, fetchBlame, fetchFileImage, viewKey, gen, collapsed, onCollapsedChange, filesPlaces, onFilesPlace, filesTrees, onFilesTree }: DrawerProps): ReactNode {
|
|
1828
|
+
function Drawer({ stats, shown, tab, onSwitchTab, commits, commitHash, onSelectCommit, hasMoreCommits, loadingMore, onLoadMoreCommits, historyRef, onHistoryRef, historyQuery, onHistoryQuery, historyError, fetchAuthors, fetchRepoTree, branches, worktreeBranches, branchesTruncated, baseRef, headRef, onBaseRef, onHeadRef, comparable, t, binding, worktrees, sessionPath, statsPath, onSwitchSource, segments, selected, onSelect, maximized, onToggleMaximized, theme, mode, family, onMode, onFamily, style, background, onStyle, width, onWidth, panes, onPane, onCommitsTall, historyLayout, onHistoryLayout, onClose, onRefresh, commitDraft, onCommitDraft, commitAmend, onCommitAmend, sync, treeLoading, historyLoading, busy, opResult, runOp, fetchDiscardPlan, onOpError, pendingTicks, onTick, fetchFileDiff, fetchFileSides, writeChecked, fetchBlame, fetchFileImage, viewKey, gen, collapsed, onCollapsedChange, filesPlaces, onFilesPlace, filesTrees, onFilesTree }: DrawerProps): ReactNode {
|
|
1797
1829
|
// Empty stand-in while a commit's change set loads, so every hook below keeps a
|
|
1798
1830
|
// stable shape and the panes simply render nothing.
|
|
1799
1831
|
const body = shown ?? EMPTY_STATS
|
|
@@ -2066,6 +2098,15 @@ function Drawer({ stats, shown, tab, onSwitchTab, commits, commitHash, onSelectC
|
|
|
2066
2098
|
onCommitsTall(clientY - box.top, box.height, done)
|
|
2067
2099
|
}
|
|
2068
2100
|
|
|
2101
|
+
/**
|
|
2102
|
+
* Whether the History tab is showing its stacked arrangement.
|
|
2103
|
+
*
|
|
2104
|
+
* Three things follow from it and they must agree: the body's direction, how
|
|
2105
|
+
* the commit list is sized, and which way its divider slides. Read from one
|
|
2106
|
+
* name so a fourth reader cannot be added out of step.
|
|
2107
|
+
*/
|
|
2108
|
+
const stackedHistory = tab === 'history' && historyLayout === 'stacked'
|
|
2109
|
+
|
|
2069
2110
|
// Width and the background's three tunables are inline because both are live
|
|
2070
2111
|
// user values; the stylesheet only says what reads them. The pane floors are
|
|
2071
2112
|
// inline for a different reason: they belong to the drag clamp above, and
|
|
@@ -2079,6 +2120,11 @@ function Drawer({ stats, shown, tab, onSwitchTab, commits, commitHash, onSelectC
|
|
|
2079
2120
|
'--gs-min-diff': `${MIN_DIFF_WIDTH}px`,
|
|
2080
2121
|
'--gs-min-commits-tall': `${MIN_COMMITS_HEIGHT}px`,
|
|
2081
2122
|
'--gs-min-stacked-lower': `${MIN_STACKED_LOWER}px`,
|
|
2123
|
+
// The commit row's height, which the lane graph also draws itself at.
|
|
2124
|
+
// Published rather than written into the stylesheet twice: the two
|
|
2125
|
+
// arrangements want different rows, and lanes only meet across the seam
|
|
2126
|
+
// between rows while both numbers come from `COMMIT_ROW_H`.
|
|
2127
|
+
'--gs-commit-row': `${COMMIT_ROW_H[historyLayout]}px`,
|
|
2082
2128
|
} as CSSProperties,
|
|
2083
2129
|
...maximized || width === null ? {} : { width: `${width}px` },
|
|
2084
2130
|
...background === null ? {} : {
|
|
@@ -2230,14 +2276,46 @@ function Drawer({ stats, shown, tab, onSwitchTab, commits, commitHash, onSelectC
|
|
|
2230
2276
|
onHeadRef={onHeadRef}
|
|
2231
2277
|
/>
|
|
2232
2278
|
) : null}
|
|
2233
|
-
{
|
|
2279
|
+
{/* History's own toolbar: which ref is listed, and how the panes are
|
|
2280
|
+
arranged. The switch lives HERE rather than in the commit list's
|
|
2281
|
+
head because that head is about 340px wide in the column layout and
|
|
2282
|
+
already holds a title, a funnel and a search box — adding a fourth
|
|
2283
|
+
control pushed it off the pane, and the narrower the reader dragged
|
|
2284
|
+
the list the sooner it went. This row spans the drawer whichever
|
|
2285
|
+
arrangement is in force, so the control cannot be squeezed out of
|
|
2286
|
+
reach by the thing it controls.
|
|
2287
|
+
|
|
2288
|
+
The bar renders for the whole tab, not only when there are branches
|
|
2289
|
+
to pick between: a repository with an unborn HEAD still has an
|
|
2290
|
+
arrangement, and a control that comes and goes is worse than one
|
|
2291
|
+
beside an empty space. */}
|
|
2292
|
+
{tab === 'history' ? (
|
|
2234
2293
|
<div className={css.compareBar}>
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2294
|
+
{branches.length > 0 ? (
|
|
2295
|
+
<RefPicker
|
|
2296
|
+
t={t} label={t('historyRefLabel')} value={historyRef}
|
|
2297
|
+
branches={branches} worktreeBranches={worktreeBranches} truncated={branchesTruncated}
|
|
2298
|
+
onPick={onHistoryRef}
|
|
2299
|
+
allLabel={t('allBranches')}
|
|
2300
|
+
/>
|
|
2301
|
+
) : null}
|
|
2302
|
+
{/* Two pressed-state buttons rather than one that toggles, so the
|
|
2303
|
+
arrangement in force is readable without knowing which way a
|
|
2304
|
+
toggle points. */}
|
|
2305
|
+
<div className={css.layoutSwitch} role="group" aria-label={t('historyLayout')}>
|
|
2306
|
+
<LayoutButton
|
|
2307
|
+
glyph={<ColumnsGlyph />}
|
|
2308
|
+
label={t('layoutColumns')}
|
|
2309
|
+
on={historyLayout === 'columns'}
|
|
2310
|
+
onPick={() => onHistoryLayout('columns')}
|
|
2311
|
+
/>
|
|
2312
|
+
<LayoutButton
|
|
2313
|
+
glyph={<StackedGlyph />}
|
|
2314
|
+
label={t('layoutStacked')}
|
|
2315
|
+
on={historyLayout === 'stacked'}
|
|
2316
|
+
onPick={() => onHistoryLayout('stacked')}
|
|
2317
|
+
/>
|
|
2318
|
+
</div>
|
|
2241
2319
|
</div>
|
|
2242
2320
|
) : null}
|
|
2243
2321
|
{/* Write operations act on the working tree, so they belong to the tab
|
|
@@ -2252,19 +2330,19 @@ function Drawer({ stats, shown, tab, onSwitchTab, commits, commitHash, onSelectC
|
|
|
2252
2330
|
role="status"
|
|
2253
2331
|
>{opMessage(t, opResult.op, opResult.result)}</div>
|
|
2254
2332
|
) : null}
|
|
2255
|
-
{/* History
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
<div ref={bodyRef} className={css.body} data-stacked={tab === 'history' ? '' : undefined}>
|
|
2333
|
+
{/* History arranges itself two ways and the reader picks; see
|
|
2334
|
+
`history-layout.ts` for what each is good at. Stacked, the commit
|
|
2335
|
+
list spans the top and the tree and diff sit below it, so a subject
|
|
2336
|
+
is never cut; in columns the list is a pane beside them, so the log
|
|
2337
|
+
is as tall as the drawer. Everything else on the tab is identical,
|
|
2338
|
+
which is why one flag decides all three differences here. */}
|
|
2339
|
+
<div ref={bodyRef} className={css.body} data-stacked={stackedHistory ? '' : undefined}>
|
|
2263
2340
|
{tab === 'history' ? (
|
|
2264
2341
|
<>
|
|
2265
2342
|
<CommitList
|
|
2266
2343
|
paneRef={commitsRef}
|
|
2267
|
-
style={paneTall(panes.commitsTall ?? null)}
|
|
2344
|
+
style={stackedHistory ? paneTall(panes.commitsTall ?? null) : paneStyle(panes.commits)}
|
|
2345
|
+
layout={historyLayout}
|
|
2268
2346
|
t={t}
|
|
2269
2347
|
loading={historyLoading}
|
|
2270
2348
|
commits={commits}
|
|
@@ -2281,7 +2359,12 @@ function Drawer({ stats, shown, tab, onSwitchTab, commits, commitHash, onSelectC
|
|
|
2281
2359
|
fetchAuthors={fetchAuthors}
|
|
2282
2360
|
fetchRepoTree={fetchRepoTree}
|
|
2283
2361
|
/>
|
|
2284
|
-
|
|
2362
|
+
{/* Each arrangement drags its own stored size — a width and a
|
|
2363
|
+
height are separate fields, so switching back finds the pane
|
|
2364
|
+
where it was left rather than reset. */}
|
|
2365
|
+
{stackedHistory
|
|
2366
|
+
? <PaneDivider axis="y" label={t('resizeCommits')} onDrag={commitsTallDrag} />
|
|
2367
|
+
: <PaneDivider label={t('resizeCommits')} onDrag={paneDrag('commits', commitsRef)} />}
|
|
2285
2368
|
</>
|
|
2286
2369
|
) : null}
|
|
2287
2370
|
<div className={css.bodyRow}>
|
|
@@ -3518,17 +3601,6 @@ function CopyCommitButton({ t, text }: { t: Translate; text: string }): ReactNod
|
|
|
3518
3601
|
*/
|
|
3519
3602
|
/* ---------- commit graph ---------- */
|
|
3520
3603
|
|
|
3521
|
-
/**
|
|
3522
|
-
* Row height the graph and the list agree on. The lines only join up if every
|
|
3523
|
-
* row is exactly as tall as the segment drawn for it, so this number and
|
|
3524
|
-
* `.commitLine`'s `height` are one fact with two homes —
|
|
3525
|
-
* `tests/commit-row-height.test.ts` holds them together.
|
|
3526
|
-
*
|
|
3527
|
-
* 28 since the History tab stacked: the list spans the drawer's full width, a
|
|
3528
|
-
* commit fits on one line, and 48px of row would spend the vertical space the
|
|
3529
|
-
* stacked layout has least of on empty padding.
|
|
3530
|
-
*/
|
|
3531
|
-
const GRAPH_ROW_H = 28
|
|
3532
3604
|
/** Horizontal distance between lanes. */
|
|
3533
3605
|
const GRAPH_LANE_W = 14
|
|
3534
3606
|
/** Ref chips shown inline before the subject; the rest collapse into a "+N". */
|
|
@@ -3542,23 +3614,28 @@ const laneX = (lane: number): number => lane * GRAPH_LANE_W + GRAPH_LANE_W / 2
|
|
|
3542
3614
|
/**
|
|
3543
3615
|
* One row's slice of the commit graph.
|
|
3544
3616
|
*
|
|
3545
|
-
* Drawn as an SVG
|
|
3546
|
-
*
|
|
3547
|
-
*
|
|
3548
|
-
*
|
|
3549
|
-
*
|
|
3617
|
+
* Drawn as an SVG exactly as tall as the row, so consecutive rows butt together
|
|
3618
|
+
* and a lane reads as one unbroken line down the list. The dot sits at the
|
|
3619
|
+
* vertical centre; edges leave the top edge, the dot, or the bottom edge, and a
|
|
3620
|
+
* cubic with its control points at the quarter heights gives the S-curve every
|
|
3621
|
+
* git client draws for a branch or a merge.
|
|
3622
|
+
*
|
|
3623
|
+
* The height is passed in rather than read from a constant here: the two
|
|
3624
|
+
* History arrangements want differently shaped rows, and the segment and the
|
|
3625
|
+
* row it belongs to must come from the same entry of `COMMIT_ROW_H` or the
|
|
3626
|
+
* lanes stop meeting across the seam between rows.
|
|
3550
3627
|
*/
|
|
3551
|
-
function GraphCell({ row, width, active }: { row: GraphRow; width: number; active: boolean }): ReactNode {
|
|
3628
|
+
function GraphCell({ row, width, active, rowH }: { row: GraphRow; width: number; active: boolean; rowH: number }): ReactNode {
|
|
3552
3629
|
const lanes = Math.min(width, GRAPH_MAX_LANES)
|
|
3553
3630
|
const w = lanes * GRAPH_LANE_W
|
|
3554
|
-
const mid =
|
|
3631
|
+
const mid = rowH / 2
|
|
3555
3632
|
const visible = (lane: number): boolean => lane < GRAPH_MAX_LANES
|
|
3556
3633
|
const stroke = (lane: number): string => `var(--gs-graph-${lane % 6})`
|
|
3557
3634
|
|
|
3558
3635
|
const paths: ReactNode[] = []
|
|
3559
3636
|
for (const lane of row.through) {
|
|
3560
3637
|
if (!visible(lane)) continue
|
|
3561
|
-
paths.push(<path key={`t${lane}`} d={`M ${laneX(lane)} 0 V ${
|
|
3638
|
+
paths.push(<path key={`t${lane}`} d={`M ${laneX(lane)} 0 V ${rowH}`} stroke={stroke(lane)} />)
|
|
3562
3639
|
}
|
|
3563
3640
|
for (const lane of row.into) {
|
|
3564
3641
|
if (!visible(lane) || !visible(row.lane)) continue
|
|
@@ -3575,11 +3652,11 @@ function GraphCell({ row, width, active }: { row: GraphRow; width: number; activ
|
|
|
3575
3652
|
for (const lane of row.outOf) {
|
|
3576
3653
|
if (!visible(lane) || !visible(row.lane)) continue
|
|
3577
3654
|
paths.push(lane === row.lane
|
|
3578
|
-
? <path key={`o${lane}`} d={`M ${laneX(lane)} ${mid} V ${
|
|
3655
|
+
? <path key={`o${lane}`} d={`M ${laneX(lane)} ${mid} V ${rowH}`} stroke={stroke(lane)} />
|
|
3579
3656
|
: (
|
|
3580
3657
|
<path
|
|
3581
3658
|
key={`o${lane}`}
|
|
3582
|
-
d={`M ${laneX(row.lane)} ${mid} C ${laneX(row.lane)} ${mid + mid / 2}, ${laneX(lane)} ${mid + mid / 2}, ${laneX(lane)} ${
|
|
3659
|
+
d={`M ${laneX(row.lane)} ${mid} C ${laneX(row.lane)} ${mid + mid / 2}, ${laneX(lane)} ${mid + mid / 2}, ${laneX(lane)} ${rowH}`}
|
|
3583
3660
|
stroke={stroke(lane)}
|
|
3584
3661
|
/>
|
|
3585
3662
|
))
|
|
@@ -3589,8 +3666,8 @@ function GraphCell({ row, width, active }: { row: GraphRow; width: number; activ
|
|
|
3589
3666
|
<svg
|
|
3590
3667
|
className={css.graphCell}
|
|
3591
3668
|
width={w}
|
|
3592
|
-
height={
|
|
3593
|
-
viewBox={`0 0 ${w} ${
|
|
3669
|
+
height={rowH}
|
|
3670
|
+
viewBox={`0 0 ${w} ${rowH}`}
|
|
3594
3671
|
aria-hidden="true"
|
|
3595
3672
|
focusable="false"
|
|
3596
3673
|
>
|
|
@@ -3611,7 +3688,7 @@ function GraphCell({ row, width, active }: { row: GraphRow; width: number; activ
|
|
|
3611
3688
|
)
|
|
3612
3689
|
}
|
|
3613
3690
|
|
|
3614
|
-
function CommitRow({ t, commit, active, onSelect, graphRow, graphWidth }: {
|
|
3691
|
+
function CommitRow({ t, commit, active, onSelect, graphRow, graphWidth, layout }: {
|
|
3615
3692
|
t: Translate
|
|
3616
3693
|
commit: GitCommit
|
|
3617
3694
|
active: boolean
|
|
@@ -3619,6 +3696,8 @@ function CommitRow({ t, commit, active, onSelect, graphRow, graphWidth }: {
|
|
|
3619
3696
|
/** This commit's lane geometry; absent while the graph is still empty. */
|
|
3620
3697
|
graphRow?: GraphRow
|
|
3621
3698
|
graphWidth: number
|
|
3699
|
+
/** Which arrangement the list is in, which decides the row's shape. */
|
|
3700
|
+
layout: HistoryLayout
|
|
3622
3701
|
}): ReactNode {
|
|
3623
3702
|
const rowRef = useRef<HTMLButtonElement>(null)
|
|
3624
3703
|
const [open, setOpen] = useState(false)
|
|
@@ -3661,6 +3740,25 @@ function CommitRow({ t, commit, active, onSelect, graphRow, graphWidth }: {
|
|
|
3661
3740
|
|
|
3662
3741
|
const refs = commit.refs ?? []
|
|
3663
3742
|
|
|
3743
|
+
/* The part both shapes share, and the only part either is really for. */
|
|
3744
|
+
const subjectRow = (
|
|
3745
|
+
<span className={css.commitSubjectRow}>
|
|
3746
|
+
{/* Capped at two. A release commit can carry six refs, and the
|
|
3747
|
+
subject is what the row is actually for — the rest are counted
|
|
3748
|
+
and named in the title rather than crowding it out. */}
|
|
3749
|
+
{refs.slice(0, COMMIT_REF_CHIPS).map(ref => (
|
|
3750
|
+
<span key={ref} className={css.commitRef} title={ref}>{ref}</span>
|
|
3751
|
+
))}
|
|
3752
|
+
{refs.length > COMMIT_REF_CHIPS ? (
|
|
3753
|
+
<span className={css.commitRefMore} title={refs.slice(COMMIT_REF_CHIPS).join('\n')}>
|
|
3754
|
+
+{refs.length - COMMIT_REF_CHIPS}
|
|
3755
|
+
</span>
|
|
3756
|
+
) : null}
|
|
3757
|
+
<span className={css.commitSubject}>{commit.subject}</span>
|
|
3758
|
+
{body.length > 0 ? <span className={css.commitHasBody} aria-hidden="true">···</span> : null}
|
|
3759
|
+
</span>
|
|
3760
|
+
)
|
|
3761
|
+
|
|
3664
3762
|
return (
|
|
3665
3763
|
<>
|
|
3666
3764
|
{/* The graph is a SIBLING of the row button, spanning the line's full
|
|
@@ -3669,7 +3767,7 @@ function CommitRow({ t, commit, active, onSelect, graphRow, graphWidth }: {
|
|
|
3669
3767
|
inset and its rounded corners. */}
|
|
3670
3768
|
<div className={css.commitLine}>
|
|
3671
3769
|
{graphRow !== undefined
|
|
3672
|
-
? <GraphCell row={graphRow} width={graphWidth} active={active} />
|
|
3770
|
+
? <GraphCell row={graphRow} width={graphWidth} active={active} rowH={COMMIT_ROW_H[layout]} />
|
|
3673
3771
|
: null}
|
|
3674
3772
|
<button
|
|
3675
3773
|
ref={rowRef}
|
|
@@ -3681,31 +3779,33 @@ function CommitRow({ t, commit, active, onSelect, graphRow, graphWidth }: {
|
|
|
3681
3779
|
onMouseEnter={show}
|
|
3682
3780
|
onMouseLeave={hide}
|
|
3683
3781
|
>
|
|
3684
|
-
{
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
))}
|
|
3697
|
-
{refs.length > COMMIT_REF_CHIPS ? (
|
|
3698
|
-
<span className={css.commitRefMore} title={refs.slice(COMMIT_REF_CHIPS).join('\n')}>
|
|
3699
|
-
+{refs.length - COMMIT_REF_CHIPS}
|
|
3782
|
+
{layout === 'stacked' ? (
|
|
3783
|
+
<>
|
|
3784
|
+
{/* One line, in git log --oneline's order: a fixed-width hash,
|
|
3785
|
+
then the subject, then who and when pushed to the right. The
|
|
3786
|
+
hash being fixed width is what aligns every subject into a
|
|
3787
|
+
column the eye can run down — leading with the author's name
|
|
3788
|
+
instead would start each subject at a different place. */}
|
|
3789
|
+
<code className={css.commitHash}>{commit.hash}</code>
|
|
3790
|
+
{subjectRow}
|
|
3791
|
+
<span className={css.commitMeta}>
|
|
3792
|
+
{authorName.length > 0 ? <span className={css.commitAuthor}>{authorName}</span> : null}
|
|
3793
|
+
<span className={css.commitWhen}>{commit.when}</span>
|
|
3700
3794
|
</span>
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3795
|
+
</>
|
|
3796
|
+
) : (
|
|
3797
|
+
<>
|
|
3798
|
+
{/* Two lines, because a pane beside the diff has no width to
|
|
3799
|
+
spare: everything but the subject goes above it, and the
|
|
3800
|
+
subject then gets the column to itself. */}
|
|
3801
|
+
<span className={css.commitTop}>
|
|
3802
|
+
<code className={css.commitHash}>{commit.hash}</code>
|
|
3803
|
+
{authorName.length > 0 ? <span className={css.commitAuthor}>{authorName}</span> : null}
|
|
3804
|
+
<span className={css.commitWhen}>{commit.when}</span>
|
|
3805
|
+
</span>
|
|
3806
|
+
{subjectRow}
|
|
3807
|
+
</>
|
|
3808
|
+
)}
|
|
3709
3809
|
</button>
|
|
3710
3810
|
</div>
|
|
3711
3811
|
{open && box !== null && host !== null ? createPortal(
|
|
@@ -3893,15 +3993,81 @@ function PathTreeRows({ dirs, depth, expanded, stateOf, onToggleOpen, onTogglePa
|
|
|
3893
3993
|
}
|
|
3894
3994
|
|
|
3895
3995
|
/**
|
|
3896
|
-
* The
|
|
3996
|
+
* The two arrangements, drawn in the same 16px/1px idiom as the drawer's other
|
|
3997
|
+
* glyphs: a pane and its neighbour, either side by side or one over the other.
|
|
3998
|
+
* The filled half is the list, so the picture says which pane moves.
|
|
3999
|
+
*/
|
|
4000
|
+
function ColumnsGlyph(): ReactNode {
|
|
4001
|
+
return (
|
|
4002
|
+
<svg
|
|
4003
|
+
className={css.layoutGlyph}
|
|
4004
|
+
width="16" height="16" viewBox="0 0 16 16"
|
|
4005
|
+
fill="none" stroke="currentColor" strokeWidth="1"
|
|
4006
|
+
strokeLinejoin="round" aria-hidden="true"
|
|
4007
|
+
>
|
|
4008
|
+
<rect x="1.5" y="2.5" width="13" height="11" rx="1.5" />
|
|
4009
|
+
<rect x="1.5" y="2.5" width="5" height="11" rx="1.5" fill="currentColor" stroke="none" opacity="0.55" />
|
|
4010
|
+
<path d="M6.5 2.5 V13.5" />
|
|
4011
|
+
</svg>
|
|
4012
|
+
)
|
|
4013
|
+
}
|
|
4014
|
+
|
|
4015
|
+
function StackedGlyph(): ReactNode {
|
|
4016
|
+
return (
|
|
4017
|
+
<svg
|
|
4018
|
+
className={css.layoutGlyph}
|
|
4019
|
+
width="16" height="16" viewBox="0 0 16 16"
|
|
4020
|
+
fill="none" stroke="currentColor" strokeWidth="1"
|
|
4021
|
+
strokeLinejoin="round" aria-hidden="true"
|
|
4022
|
+
>
|
|
4023
|
+
<rect x="1.5" y="2.5" width="13" height="11" rx="1.5" />
|
|
4024
|
+
<rect x="1.5" y="2.5" width="13" height="4" rx="1.5" fill="currentColor" stroke="none" opacity="0.55" />
|
|
4025
|
+
<path d="M1.5 6.5 H14.5" />
|
|
4026
|
+
</svg>
|
|
4027
|
+
)
|
|
4028
|
+
}
|
|
4029
|
+
|
|
4030
|
+
/**
|
|
4031
|
+
* One end of the arrangement switch.
|
|
4032
|
+
*
|
|
4033
|
+
* `aria-pressed` rather than a radio group: these are two states of one view
|
|
4034
|
+
* control, not a value being submitted, and a screen reader then reads the
|
|
4035
|
+
* arrangement in force without the group needing a name per option.
|
|
4036
|
+
* @param glyph - the arrangement, drawn.
|
|
4037
|
+
* @param label - accessible name, also the tooltip.
|
|
4038
|
+
* @param on - whether this arrangement is the one in force.
|
|
4039
|
+
* @param onPick - switch to it.
|
|
4040
|
+
*/
|
|
4041
|
+
function LayoutButton({ glyph, label, on, onPick }: {
|
|
4042
|
+
glyph: ReactNode
|
|
4043
|
+
label: string
|
|
4044
|
+
on: boolean
|
|
4045
|
+
onPick: () => void
|
|
4046
|
+
}): ReactNode {
|
|
4047
|
+
return (
|
|
4048
|
+
<button
|
|
4049
|
+
type="button"
|
|
4050
|
+
className={on ? `${css.layoutButton} ${css.layoutButtonOn}` : css.layoutButton}
|
|
4051
|
+
aria-pressed={on}
|
|
4052
|
+
aria-label={label}
|
|
4053
|
+
title={label}
|
|
4054
|
+
onClick={onPick}
|
|
4055
|
+
>{glyph}</button>
|
|
4056
|
+
)
|
|
4057
|
+
}
|
|
4058
|
+
|
|
4059
|
+
/**
|
|
4060
|
+
* The commit log as its own pane, in whichever arrangement the reader picked.
|
|
3897
4061
|
*
|
|
3898
|
-
*
|
|
3899
|
-
*
|
|
3900
|
-
*
|
|
3901
|
-
*
|
|
3902
|
-
*
|
|
3903
|
-
*
|
|
3904
|
-
*
|
|
4062
|
+
* Beside the file tree it is a peer pane the way GitHub Desktop, the JetBrains
|
|
4063
|
+
* git log and GitKraken all draw it — list and selected commit's files side by
|
|
4064
|
+
* side, each with its own scrollbar. Across the top it is IDEA's git log
|
|
4065
|
+
* instead, which is the arrangement that stops a long subject being cut; see
|
|
4066
|
+
* `history-layout.ts` for what each costs. The switch that picks between them
|
|
4067
|
+
* is in the toolbar row above, not in this pane's head — the head is the first
|
|
4068
|
+
* thing to run out of room when the pane is dragged narrow, which is exactly
|
|
4069
|
+
* when a reader reaches for the switch. Either way there is nothing to
|
|
4070
|
+
* collapse and nothing to discover.
|
|
3905
4071
|
*
|
|
3906
4072
|
* Pages load by scrolling. A button at the end of a growing list is the worst
|
|
3907
4073
|
* of both worlds — it retreats every time it is used, and it asks the reader to
|
|
@@ -3910,13 +4076,18 @@ function PathTreeRows({ dirs, depth, expanded, stateOf, onToggleOpen, onTogglePa
|
|
|
3910
4076
|
* what GitHub and GitLens do. The observer is rebuilt whenever the list grows,
|
|
3911
4077
|
* so a page too short to fill the pane immediately triggers the next one.
|
|
3912
4078
|
*/
|
|
3913
|
-
function CommitList({ paneRef, style, t, loading, commits, active, onSelect, hasMore, loadingMore, onLoadMore, query, onQueryChange, error, statsPath, refName, fetchAuthors, fetchRepoTree }: {
|
|
4079
|
+
function CommitList({ paneRef, style, layout, t, loading, commits, active, onSelect, hasMore, loadingMore, onLoadMore, query, onQueryChange, error, statsPath, refName, fetchAuthors, fetchRepoTree }: {
|
|
3914
4080
|
/** The pane element, which the divider beside it measures from. Not named
|
|
3915
4081
|
* `ref`: React reserves that on a function component, so it would be stripped
|
|
3916
4082
|
* from props and never reach this element. */
|
|
3917
4083
|
paneRef: Ref<HTMLDivElement>
|
|
3918
|
-
/** Dragged
|
|
4084
|
+
/** Dragged size, when the divider has been used: a width beside the diff, a
|
|
4085
|
+
* height above it. */
|
|
3919
4086
|
style: CSSProperties | undefined
|
|
4087
|
+
/** The arrangement in force, which decides the row's shape as well as the
|
|
4088
|
+
* pane's. The control that CHANGES it is not in here — see the toolbar row
|
|
4089
|
+
* above the panes. */
|
|
4090
|
+
layout: HistoryLayout
|
|
3920
4091
|
t: Translate
|
|
3921
4092
|
/** First page in flight — the pane says "loading", not "no history", which
|
|
3922
4093
|
* would be a claim about the repository the data has not made. */
|
|
@@ -4107,7 +4278,7 @@ function CommitList({ paneRef, style, t, loading, commits, active, onSelect, has
|
|
|
4107
4278
|
}, [hasMore, loadingMore, commits.length, onLoadMore])
|
|
4108
4279
|
|
|
4109
4280
|
return (
|
|
4110
|
-
<div ref={paneRef} className={css.commitsPane} style={style} data-gs-part="commits">
|
|
4281
|
+
<div ref={paneRef} className={css.commitsPane} style={style} data-layout={layout} data-gs-part="commits">
|
|
4111
4282
|
{/* No count: the only number available is how many pages have been loaded,
|
|
4112
4283
|
which is not how many commits exist. A number that cannot be right is
|
|
4113
4284
|
worse than none. */}
|
|
@@ -4369,6 +4540,7 @@ function CommitList({ paneRef, style, t, loading, commits, active, onSelect, has
|
|
|
4369
4540
|
onSelect={onSelect}
|
|
4370
4541
|
graphRow={graph.rows[index]}
|
|
4371
4542
|
graphWidth={graph.width}
|
|
4543
|
+
layout={layout}
|
|
4372
4544
|
/>
|
|
4373
4545
|
))}
|
|
4374
4546
|
<div ref={sentinelRef} className={css.commitsSentinel} />
|