@young1lin/dsh-ui-gitworkbench 0.1.5 → 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.
Files changed (49) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/CHANGELOG_EN.md +43 -0
  3. package/lib/apply-blocks.js +159 -0
  4. package/lib/atomic-json.js +23 -5
  5. package/lib/blame.js +83 -0
  6. package/lib/client.js +34960 -11826
  7. package/lib/git-ops.js +25 -0
  8. package/lib/image-sniff.js +197 -0
  9. package/lib/index.js +401 -7
  10. package/lib/patch-model.js +223 -0
  11. package/lib/side-guard.js +55 -0
  12. package/lib/write-checked.js +164 -0
  13. package/package.json +7 -1
  14. package/src/apply-blocks.ts +215 -0
  15. package/src/atomic-json.ts +29 -5
  16. package/src/blame.ts +94 -0
  17. package/src/client/CodeEditor.tsx +317 -0
  18. package/src/client/FileBrowser.tsx +657 -0
  19. package/src/client/GitWorkbenchPanel.module.css +491 -12
  20. package/src/client/GitWorkbenchPanel.tsx +1655 -190
  21. package/src/client/ImageView.tsx +120 -0
  22. package/src/client/blame-gutter.ts +108 -0
  23. package/src/client/blame-view.ts +104 -0
  24. package/src/client/cm-diff.ts +108 -0
  25. package/src/client/cm-tokens.ts +79 -0
  26. package/src/client/diff-nav.ts +198 -0
  27. package/src/client/file-icon.ts +190 -0
  28. package/src/client/file-rows.ts +184 -0
  29. package/src/client/files-place.ts +178 -0
  30. package/src/client/glyphs.tsx +86 -0
  31. package/src/client/highlight.ts +25 -0
  32. package/src/client/history-layout.ts +52 -0
  33. package/src/client/idle-value.ts +53 -0
  34. package/src/client/image-view.ts +106 -0
  35. package/src/client/indent.ts +74 -0
  36. package/src/client/index.ts +59 -0
  37. package/src/client/locales.ts +179 -4
  38. package/src/client/pane-size.ts +71 -0
  39. package/src/client/side-edit.ts +244 -0
  40. package/src/client/side-rows.ts +258 -0
  41. package/src/client/stable-list.ts +31 -0
  42. package/src/client/use-change-nav.ts +83 -0
  43. package/src/client/worktree-view.ts +11 -1
  44. package/src/git-ops.ts +36 -1
  45. package/src/image-sniff.ts +204 -0
  46. package/src/index.ts +447 -7
  47. package/src/patch-model.ts +267 -0
  48. package/src/side-guard.ts +58 -0
  49. package/src/write-checked.ts +223 -0
@@ -501,6 +501,16 @@
501
501
  }
502
502
  .paneDivider:hover::after { opacity: 0.6; }
503
503
  .paneDividerActive::after { opacity: 0.9; height: 48px; }
504
+ /* The same handle turned through a right angle. Both modifier rules below beat
505
+ the shared ones on specificity, so their order relative to them does not
506
+ matter — the stylesheet's usual "same value loses" trap does not apply. */
507
+ .paneDividerY {
508
+ width: auto; height: 7px;
509
+ cursor: row-resize;
510
+ }
511
+ .paneDividerY::before { top: 3px; bottom: auto; left: 0; right: 0; width: auto; height: 1px; }
512
+ .paneDividerY::after { width: 28px; height: 3px; transition: opacity 140ms ease, width 140ms ease; }
513
+ .paneDividerY.paneDividerActive::after { width: 48px; height: 3px; }
504
514
 
505
515
  .header {
506
516
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
@@ -1268,7 +1278,11 @@
1268
1278
  the compact control it hosts, so a pane with tools and a pane without one
1269
1279
  still line up across the divider. */
1270
1280
  .paneHead {
1271
- display: flex; align-items: center; gap: 8px;
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;
1272
1286
  flex: none;
1273
1287
  min-height: 37px; box-sizing: border-box;
1274
1288
  padding: 6px var(--gs-gutter-pane);
@@ -1310,12 +1324,17 @@
1310
1324
  .commitLine {
1311
1325
  display: flex; align-items: stretch;
1312
1326
  flex: none;
1313
- height: 48px;
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);
1314
1333
  }
1315
1334
  .graphCell { flex: none; display: block; }
1316
1335
  /* An inset row, like dsh's own session rows: 12px radius, filled when active. */
1317
1336
  .commit {
1318
- display: flex; flex-direction: column; justify-content: center; gap: 2px;
1337
+ display: flex; align-items: center; gap: 10px;
1319
1338
  flex: 1 1 auto; min-width: 0; box-sizing: border-box;
1320
1339
  height: 100%;
1321
1340
  margin: 1px 0;
@@ -1330,16 +1349,40 @@
1330
1349
  overflow: hidden;
1331
1350
  transition: background 120ms ease, border-color 120ms ease;
1332
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
+ }
1333
1366
  .commit:hover { background: var(--gs-raise); }
1334
1367
  .commitActive { border-color: var(--gs-accent-border); background: var(--gs-accent-bg); }
1335
- .commitTop { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
1336
- .commitHash { color: var(--gs-info); }
1368
+ /* Pushed to the right end of the row, and never squeezed: the subject is what
1369
+ gives way when the pane narrows, because it is the part with an ellipsis to
1370
+ give. */
1371
+ .commitMeta { flex: none; display: flex; align-items: baseline; gap: 8px; }
1372
+ .commitHash { flex: none; color: var(--gs-info); }
1337
1373
  .commitSubjectRow {
1338
1374
  display: flex; align-items: baseline; gap: 4px;
1339
- min-width: 0;
1375
+ flex: 1 1 auto; min-width: 0;
1340
1376
  }
1377
+ /* Shrinks, never grows. Growing was invisible while the list was a 26%%
1378
+ column — the subject filled the row either way — and became a defect the
1379
+ moment the list spanned the drawer: a subject that takes ALL the free space
1380
+ pushes the has-a-body marker a thousand pixels right, where it reads as an
1381
+ unexplained ellipsis next to the author rather than as "there is more to
1382
+ this message". The row still pushes author and date to the right end;
1383
+ .commitSubjectRow is what absorbs the slack. */
1341
1384
  .commitSubject {
1342
- flex: 1; min-width: 0;
1385
+ flex: 0 1 auto; min-width: 0;
1343
1386
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
1344
1387
  color: var(--gs-fg-muted);
1345
1388
  }
@@ -1366,6 +1409,27 @@
1366
1409
  }
1367
1410
 
1368
1411
  .body { display: flex; flex: 1; min-height: 0; z-index: 1; }
1412
+ /* History stacks: the commit list over the tree-and-diff row. `.bodyRow` is
1413
+ rendered on every tab, so the unstacked tabs are a row inside a row — which
1414
+ costs nothing and keeps one JSX shape instead of two. */
1415
+ .body[data-stacked] { flex-direction: column; }
1416
+ .bodyRow { display: flex; flex: 1 1 0; min-width: 0; min-height: 0; }
1417
+ /* Stacked, the list is sized by HEIGHT and spans the drawer; the width rules
1418
+ it carries for the column layout would otherwise pin it to 26%. */
1419
+ .body[data-stacked] .commitsPane {
1420
+ width: auto; max-width: none; min-width: 0;
1421
+ height: 34%; flex: none;
1422
+ /* The drag's clamp, restated where a RESIZE can also reach it. An inline
1423
+ `height` in pixels survives the window getting shorter, and then the list
1424
+ is taller than the body: the lower half goes to zero and the handle ends
1425
+ up below the bottom edge with nothing to grab. Percentages re-resolve on
1426
+ every layout, which is exactly what the stored pixel height cannot do.
1427
+ Both numbers come from the panel's own constants, so there is still one
1428
+ home for them. `max()` keeps the range from inverting in a drawer too
1429
+ short to hold both floors — the list then simply takes its minimum. */
1430
+ min-height: var(--gs-min-commits-tall);
1431
+ max-height: max(var(--gs-min-commits-tall), calc(100% - var(--gs-min-stacked-lower)));
1432
+ }
1369
1433
 
1370
1434
  /* File tree column. A peer of the commit pane and the diff, never their parent:
1371
1435
  each of the three scrolls on its own and keeps its full height. */
@@ -1553,7 +1617,29 @@
1553
1617
  .fileCountDel { color: var(--gs-del); }
1554
1618
 
1555
1619
  /* diff pane */
1556
- .diffPane { flex: 1 1 0; min-width: var(--gs-min-diff); overflow: auto; background: var(--gs-surface); }
1620
+ /* A column, so a header can sit above the scrolled diff instead of inside it.
1621
+ `overflow: auto` stays as the outer guard — the rename line and the
1622
+ side-by-side pane are also children here, and one of them growing must
1623
+ scroll rather than be clipped. */
1624
+ .diffPane {
1625
+ flex: 1 1 0; min-width: var(--gs-min-diff);
1626
+ display: flex; flex-direction: column;
1627
+ overflow: auto; background: var(--gs-surface);
1628
+ }
1629
+ /* The unified view: its own header and its own scroller, filling whatever the
1630
+ pane has left after a rename line. */
1631
+ .diffWrap { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; width: 100%; }
1632
+ .diffScroll { flex: 1 1 0; min-height: 0; overflow: auto; outline: none; }
1633
+ /* The walk's controls. Right-aligned and quiet: they belong to the diff, and
1634
+ the diff is what the reader came for. Same shape as `.sideNav` in the
1635
+ side-by-side pane, because it is the same control. */
1636
+ .diffNav {
1637
+ flex: none;
1638
+ display: flex; justify-content: flex-end; align-items: center; gap: 4px;
1639
+ padding: 3px 12px 3px 0;
1640
+ border-bottom: 1px solid var(--gs-border-soft);
1641
+ }
1642
+ .diffNav .blockBtn { padding-inline: 6px; line-height: 0; }
1557
1643
  /* The pre is as wide as its longest line (`max-content`) and at least the pane
1558
1644
  (`min-width: 100%`). Each row then stretches to that width, so add/delete
1559
1645
  tints survive horizontal scroll instead of stopping at the viewport edge. */
@@ -1570,6 +1656,7 @@
1570
1656
  tab-size: 4;
1571
1657
  }
1572
1658
  .renameLine {
1659
+ flex: none;
1573
1660
  padding: 8px 16px;
1574
1661
  border-bottom: 1px solid var(--gs-border-soft);
1575
1662
  color: var(--gs-info);
@@ -1628,6 +1715,205 @@
1628
1715
  .wordAdd { background: var(--gs-add-word); border-radius: 2px; }
1629
1716
  .wordDel { background: var(--gs-del-word); border-radius: 2px; }
1630
1717
 
1718
+ /* Side-by-side diff pane. One grid holds EVERY row's four cells — a per-row
1719
+ grid would let the column boundary drift with each row's content, and a
1720
+ fixed split would clip long lines. Equal 1fr code tracks keep the two
1721
+ columns identically wide however wide the widest line is, and the whole pane
1722
+ (this view's single shared vertical scroll) carries the horizontal one. */
1723
+ /* The pane is a HEADER plus a scroller, not one wide column: the tab row
1724
+ carries Save/Revert, and a row sized to the widest line of code pushes them
1725
+ off the right edge of a long file — reachable only by scrolling sideways,
1726
+ which is not where anyone looks for a toolbar. Only `.sideScroll` scrolls;
1727
+ the tabs and the notices above it stay put on both axes. */
1728
+ .sidePane { display: flex; flex-direction: column; width: 100%; min-width: 0; height: 100%; }
1729
+ /* Vertical scrolling belongs to the pane; horizontal belongs to each column,
1730
+ which is what lets the divider mean something. A single scroller for both
1731
+ axes would tie the two sides' horizontal position together and size the
1732
+ whole grid to the widest line in the file. */
1733
+ .sideScroll { flex: 1 1 0; min-height: 0; overflow-y: auto; overflow-x: hidden; }
1734
+ .sideCols { display: flex; align-items: stretch; min-height: 100%; }
1735
+ /* `min-width: 0` is what lets a flex column be narrower than its content —
1736
+ without it the column refuses to shrink and the divider stops moving. */
1737
+ .sideCol { flex: 0 0 auto; min-width: 0; overflow-x: auto; }
1738
+ .sideColRight { flex: 1 1 0; }
1739
+ .sideColGrid {
1740
+ display: grid;
1741
+ grid-template-columns: 3.2em 1fr;
1742
+ align-content: start;
1743
+ padding: 8px 0 16px;
1744
+ min-width: 100%; width: max-content;
1745
+ font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
1746
+ font-size: var(--gs-t-dense); line-height: 20px; font-weight: 400;
1747
+ font-variant-ligatures: none;
1748
+ font-feature-settings: 'liga' 0, 'calt' 0;
1749
+ tab-size: 4;
1750
+ }
1751
+ /* The blame gutter, on the working-tree column only. Fixed width so the code
1752
+ beside it does not shift line to line, and clipped rather than wrapped: a
1753
+ gutter row that grew to two lines would break the alignment the whole
1754
+ side-by-side view rests on. */
1755
+ .sideColGridBlame { grid-template-columns: 13em 3.2em 1fr; }
1756
+ .blameCell {
1757
+ box-sizing: border-box;
1758
+ padding: 0 8px 0 10px;
1759
+ border-right: 1px solid var(--gs-border-soft);
1760
+ color: var(--gs-fg-faint);
1761
+ font-size: var(--gs-t-meta);
1762
+ white-space: pre; overflow: hidden; text-overflow: ellipsis;
1763
+ user-select: none;
1764
+ }
1765
+ .sideTabs {
1766
+ z-index: 5;
1767
+ display: flex; gap: 16px;
1768
+ flex: none;
1769
+ padding: 6px 16px 0;
1770
+ border-bottom: 1px solid var(--gs-border-soft);
1771
+ background: var(--gs-surface-2);
1772
+ }
1773
+ .sideTab {
1774
+ position: relative;
1775
+ padding: 2px 0 5px;
1776
+ border: none;
1777
+ background: transparent;
1778
+ font-family: inherit;
1779
+ font-size: var(--gs-t-meta); line-height: 16px; font-weight: 500;
1780
+ color: var(--gs-fg-dim);
1781
+ cursor: pointer;
1782
+ }
1783
+ .sideTab::after {
1784
+ content: '';
1785
+ position: absolute; right: 0; bottom: 1px; left: 0;
1786
+ height: 2px; border-radius: 2px;
1787
+ background: transparent;
1788
+ }
1789
+ .sideTabActive { color: var(--gs-accent); }
1790
+ .sideTabActive::after { background: var(--gs-accent); }
1791
+ .sideNotice {
1792
+ flex: none;
1793
+ padding: 8px 16px;
1794
+ border-bottom: 1px solid var(--gs-border-soft);
1795
+ color: var(--gs-info);
1796
+ font-size: var(--gs-t-dense);
1797
+ }
1798
+ .sideNum {
1799
+ box-sizing: border-box;
1800
+ padding: 0 8px;
1801
+ text-align: right;
1802
+ color: var(--gs-fg-fainter);
1803
+ user-select: none;
1804
+ font-variant-numeric: tabular-nums;
1805
+ }
1806
+ .sideNumAdd { background: var(--gs-add-num, var(--gs-add-line)); }
1807
+ .sideNumDel { background: var(--gs-del-num, var(--gs-del-line)); }
1808
+ .sideCode { white-space: pre; padding: 0 16px 0 10px; min-height: 20px; }
1809
+ .sideCodeSame { color: var(--gs-fg-muted); }
1810
+ .sideCodeAdd { background: var(--gs-add-line); }
1811
+ .sideCodeDel { background: var(--gs-del-line); }
1812
+ /* The block affordance: a changed row's cells carry the block tint above, and
1813
+ the hovered block's every cell — code and gutter — takes the hot outline,
1814
+ so the reader sees the unit the buttons act on rather than the one cell the
1815
+ pointer happens to be over. `.sideCellBlock` also positions the action bar,
1816
+ which a cell of the block's first row carries. */
1817
+ .sideCellBlock { position: relative; }
1818
+ .sideBlockHot { outline: 1px solid var(--gs-accent); outline-offset: -1px; }
1819
+
1820
+ /* The hovered block's actions, floating on the row above the block. It lives
1821
+ INSIDE the block's first code cell, so it travels with the column and the
1822
+ scroll, and it only ever covers context — never the change it acts on. The
1823
+ surface and shadow are what keep it readable over code. */
1824
+ .blockBar {
1825
+ position: absolute; top: -21px; left: 0; z-index: 4;
1826
+ display: flex; gap: 4px;
1827
+ padding: 2px;
1828
+ border: 1px solid var(--gs-border);
1829
+ border-radius: var(--gs-r-control);
1830
+ background: var(--gs-surface-2, var(--gs-bg));
1831
+ box-shadow: 0 4px 14px var(--gs-shadow);
1832
+ white-space: nowrap;
1833
+ }
1834
+ .blockBtn {
1835
+ padding: 0 8px;
1836
+ border: 1px solid var(--gs-border); border-radius: var(--gs-r-pill);
1837
+ background: var(--gs-bg); color: var(--gs-fg-dim);
1838
+ font-family: inherit; font-size: var(--gs-t-meta); line-height: 16px;
1839
+ cursor: pointer;
1840
+ }
1841
+ .blockBtn:hover:not(:disabled) { color: var(--gs-fg); border-color: var(--gs-accent); }
1842
+ .blockBtn:disabled { opacity: 0.45; cursor: default; }
1843
+ /* The roll-back button stays red at rest, not only on hover: it is the one
1844
+ action in the bar that cannot be undone, and arriving under the pointer is
1845
+ not what should make that visible. */
1846
+ .blockBtnDanger { color: var(--gs-del); border-color: var(--gs-del-bg, var(--gs-border)); }
1847
+ .blockBtnDanger:hover:not(:disabled) { color: var(--gs-del); border-color: var(--gs-del); }
1848
+
1849
+ /* The editable right column. The working-tree column is readable but visibly
1850
+ a text surface before editing is armed; the tab row carries the arm/save
1851
+ cluster at its right end. */
1852
+ .sideArmable { cursor: text; }
1853
+
1854
+ /* An editable buffer says so — the one thing a rendered code pane cannot say
1855
+ by itself.
1856
+
1857
+ Nothing about monospaced text on a background looks like a field. In the
1858
+ Files tab the editor is live the moment the file opens, so without a mark
1859
+ the reader finds out whether they may type by typing — and finds out they
1860
+ may not the same way, on a pane that silently swallows it. The rule runs
1861
+ the full height of the editor's leading edge: dim while nothing is focused
1862
+ (present, not loud — it sits beside code someone came to read), full accent
1863
+ once the caret is actually inside, which doubles as "this pane has the
1864
+ keyboard".
1865
+
1866
+ Driven by an attribute the component sets from the same boolean that
1867
+ configures `EditorState.readOnly`, so the rule cannot appear on a pane that
1868
+ would refuse the keystroke. `pointer-events: none` because the bar overlays
1869
+ the gutter's first two pixels, and a click there must still reach the
1870
+ view. */
1871
+ .cmHost { position: relative; }
1872
+ .cmHost[data-editable]::before {
1873
+ content: '';
1874
+ position: absolute;
1875
+ top: 0; bottom: 0; left: 0;
1876
+ width: 2px;
1877
+ border-radius: 1px;
1878
+ background: var(--gs-accent);
1879
+ opacity: 0.32;
1880
+ transition: opacity 140ms ease;
1881
+ pointer-events: none;
1882
+ }
1883
+ .cmHost[data-editable]:focus-within::before { opacity: 1; }
1884
+ /* The change nav takes the free space, so it and `.sideActions` after it sit
1885
+ together at the right end. Only the FIRST auto margin absorbs anything —
1886
+ the one on `.sideActions` then has nothing left to take, which is what
1887
+ keeps the two groups adjacent rather than pushed to opposite ends. */
1888
+ .sideNav { margin-left: auto; display: flex; gap: 4px; align-items: center; padding: 2px 0 5px; }
1889
+ .sideNav .blockBtn { padding-inline: 6px; line-height: 0; }
1890
+ .sideNavCount { color: var(--gs-fg-faint); font-size: var(--gs-t-meta); white-space: nowrap; }
1891
+ .sideActions { margin-left: auto; display: flex; gap: 4px; align-items: flex-start; padding: 2px 0 5px; }
1892
+ /* Save gains the accent only once there is something to save: the button's
1893
+ whole enabled state is the dirty flag, and colour says it sooner than the
1894
+ pointer does. */
1895
+ .sideSaveReady { color: var(--gs-accent); border-color: var(--gs-accent); }
1896
+
1897
+ /* The reload-or-overwrite banner (§4) and the failed-save banner: a warning
1898
+ strip under the tabs carrying its own actions, so the decision it asks for
1899
+ is one click away rather than a sentence away. */
1900
+ .sideBanner {
1901
+ flex: none;
1902
+ display: flex; flex-direction: column; gap: 6px;
1903
+ padding: 8px 16px;
1904
+ border-bottom: 1px solid var(--gs-border-soft);
1905
+ background: var(--gs-del-bg, transparent);
1906
+ color: var(--gs-del);
1907
+ font-size: var(--gs-t-dense);
1908
+ }
1909
+ .sideBannerTitle { font-weight: 600; }
1910
+ .sideBannerActs { display: flex; gap: 4px; }
1911
+
1912
+ /* The editor grid item that positions the textarea. It spans exactly the
1913
+ buffer's rows of the fourth column; the textarea fills it absolutely. */
1914
+ .sideEditCell { position: relative; }
1915
+
1916
+
1631
1917
  /* No syntax-tint classes here: Shiki resolves a real TextMate theme and emits a
1632
1918
  hex per token, so the panel writes colour inline. A four-class approximation
1633
1919
  painted identifiers, types and punctuation all one colour, which is what made
@@ -1827,7 +2113,7 @@
1827
2113
  * its own padding, radius and font-size, and the same "small secondary button"
1828
2114
  * had drifted to four heights and three radii across one screen.
1829
2115
  */
1830
- .btn, .miniBtn, .treeIcon, .commitCopy, .scopeBtn, .refButton, .funnelButton, .funnelPreset {
2116
+ .btn, .miniBtn, .treeIcon, .commitCopy, .scopeBtn, .refButton, .funnelButton, .funnelPreset, .layoutButton {
1831
2117
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
1832
2118
  box-sizing: border-box;
1833
2119
  flex: none;
@@ -1845,14 +2131,14 @@
1845
2131
  transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
1846
2132
  }
1847
2133
  .btn:hover, .miniBtn:hover, .treeIcon:hover, .commitCopy:hover,
1848
- .scopeBtn:hover, .refButton:hover, .funnelButton:hover, .funnelPreset:hover {
2134
+ .scopeBtn:hover, .refButton:hover, .funnelButton:hover, .funnelPreset:hover, .layoutButton:hover {
1849
2135
  background: var(--gs-raise);
1850
2136
  color: var(--gs-fg);
1851
2137
  border-color: var(--gs-fg-fainter);
1852
2138
  }
1853
2139
  .btn:focus-visible, .miniBtn:focus-visible, .treeIcon:focus-visible,
1854
2140
  .commitCopy:focus-visible, .scopeBtn:focus-visible, .refButton:focus-visible,
1855
- .funnelPreset:focus-visible {
2141
+ .funnelPreset:focus-visible, .layoutButton:focus-visible {
1856
2142
  outline: 2px solid var(--gs-accent);
1857
2143
  outline-offset: 1px;
1858
2144
  }
@@ -1865,7 +2151,7 @@
1865
2151
 
1866
2152
  /* Pane tools give their vertical space back to the diff; the date presets sit
1867
2153
  inside a 320px popover, so they take the compact height too. */
1868
- .treeIcon, .commitCopy, .funnelPreset {
2154
+ .treeIcon, .commitCopy, .funnelPreset, .layoutButton {
1869
2155
  height: var(--gs-h-compact);
1870
2156
  padding: var(--gs-pad-compact);
1871
2157
  font-size: var(--gs-t-dense);
@@ -1875,6 +2161,19 @@
1875
2161
  the pane drags down to, two more text buttons cannot share a row with the
1876
2162
  count — and these two are the pair a tree header always carries. */
1877
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; }
1878
2177
  /* Qualified, like `.funnelButtonActive`: this sits inside the shared button
1879
2178
  vocabulary, which sets `color` at the same specificity a few lines above. */
1880
2179
  .treeIcon.treeIconOn { color: var(--gs-accent); border-color: var(--gs-accent); }
@@ -2027,3 +2326,183 @@
2027
2326
  font-weight: 600;
2028
2327
  }
2029
2328
  .btn.btnDanger:hover:not(:disabled) { border-color: var(--gs-del); background: var(--gs-del); color: var(--gs-on-accent, #fff); }
2329
+
2330
+ /* Files tab: the repository browser's tree, and the pane that opens a file.
2331
+ Modifier rules stay BELOW the base rule they narrow — a modifier declared
2332
+ first loses the cascade, which the drawer-chrome test checks for. */
2333
+ .fbTree {
2334
+ flex: none;
2335
+ min-width: var(--gs-min-tree);
2336
+ display: flex;
2337
+ flex-direction: column;
2338
+ gap: 4px;
2339
+ padding: 6px;
2340
+ overflow: auto;
2341
+ background: var(--gs-surface-2);
2342
+ }
2343
+ .fbSearch {
2344
+ flex: none;
2345
+ width: 100%;
2346
+ box-sizing: border-box;
2347
+ padding: 4px 6px;
2348
+ font: inherit;
2349
+ font-size: var(--gs-t-meta);
2350
+ color: var(--gs-fg);
2351
+ background: var(--gs-surface);
2352
+ border: 1px solid var(--gs-border);
2353
+ border-radius: 4px;
2354
+ }
2355
+ .fbSearch:focus { outline: none; border-color: var(--gs-accent); }
2356
+ .fbNote { flex: none; font-size: var(--gs-t-meta); color: var(--gs-fg-dim); padding: 2px 4px; }
2357
+ .fbList { list-style: none; margin: 0; padding: 0; }
2358
+ .fbRow {
2359
+ display: flex;
2360
+ align-items: center;
2361
+ gap: 4px;
2362
+ width: 100%;
2363
+ padding: 2px 6px 2px 4px;
2364
+ font: inherit;
2365
+ font-size: var(--gs-t-dense);
2366
+ color: var(--gs-fg-dim);
2367
+ text-align: left;
2368
+ background: none;
2369
+ border: 0;
2370
+ border-radius: 3px;
2371
+ cursor: pointer;
2372
+ white-space: nowrap;
2373
+ overflow: hidden;
2374
+ text-overflow: ellipsis;
2375
+ }
2376
+ .fbRow:hover { background: var(--gs-hover); }
2377
+ .fbRowActive { background: var(--gs-selected); color: var(--gs-fg); }
2378
+ .fbDirName { overflow: hidden; text-overflow: ellipsis; }
2379
+ .fbFileName { overflow: hidden; text-overflow: ellipsis; }
2380
+ .fbMain {
2381
+ flex: 1 1 0;
2382
+ min-width: var(--gs-min-diff);
2383
+ display: flex;
2384
+ flex-direction: column;
2385
+ min-height: 0;
2386
+ background: var(--gs-surface);
2387
+ }
2388
+ .fbHeader {
2389
+ flex: none;
2390
+ display: flex;
2391
+ align-items: flex-start;
2392
+ gap: 8px;
2393
+ padding: 4px 8px 0;
2394
+ border-bottom: 1px solid var(--gs-border);
2395
+ }
2396
+ .fbPath {
2397
+ font-size: var(--gs-t-meta);
2398
+ color: var(--gs-fg-dim);
2399
+ overflow: hidden;
2400
+ text-overflow: ellipsis;
2401
+ white-space: nowrap;
2402
+ padding: 4px 0;
2403
+ }
2404
+ /* The editor scrolls inside this, so the header and the notices above stay
2405
+ put — the same reason the side pane's toolbar is pinned. */
2406
+ .fbBody { flex: 1 1 0; min-height: 0; overflow: auto; padding: 4px 8px; }
2407
+
2408
+ /* The commit behind the picked blame line. A strip rather than a floating
2409
+ popup: it never covers the code it is about, and it survives a scroll. */
2410
+ .fbCommit {
2411
+ flex: none;
2412
+ display: flex;
2413
+ align-items: baseline;
2414
+ gap: 8px;
2415
+ padding: 4px 8px;
2416
+ font-size: var(--gs-t-meta);
2417
+ color: var(--gs-fg-dim);
2418
+ background: var(--gs-surface-2);
2419
+ border-bottom: 1px solid var(--gs-border);
2420
+ }
2421
+ .fbCommitLine { flex: none; color: var(--gs-fg-faint); font-variant-numeric: tabular-nums; }
2422
+ .fbCommitWho { flex: none; color: var(--gs-fg); font-weight: 600; }
2423
+ .fbCommitWhen { flex: none; font-variant-numeric: tabular-nums; }
2424
+ .fbCommitHash { flex: none; font-family: inherit; color: var(--gs-fg-faint); }
2425
+ .fbCommitWhat {
2426
+ flex: 1 1 auto;
2427
+ min-width: 0;
2428
+ overflow: hidden;
2429
+ text-overflow: ellipsis;
2430
+ white-space: nowrap;
2431
+ color: var(--gs-fg);
2432
+ }
2433
+ .fbCommitHint { color: var(--gs-fg-faint); }
2434
+ .fbMore {
2435
+ display: block;
2436
+ padding: 2px 6px 4px 4px;
2437
+ font-size: var(--gs-t-meta);
2438
+ color: var(--gs-fg-faint);
2439
+ font-style: italic;
2440
+ }
2441
+
2442
+ /* ============================ image preview ============================= */
2443
+
2444
+ /* A picture fills the pane and its facts sit under it, because the facts are
2445
+ the reason this is a viewer rather than an image tag: a screenshot in a
2446
+ repository is usually opened to check a size or a crop. */
2447
+ .imgPane {
2448
+ display: flex;
2449
+ flex-direction: column;
2450
+ min-height: 0;
2451
+ height: 100%;
2452
+ gap: 8px;
2453
+ }
2454
+
2455
+ /* The checkerboard is not decoration. A transparent PNG on a flat background
2456
+ is indistinguishable from an opaque one of that colour, and "is this
2457
+ transparent" is the question the file was opened to answer. Two overlaid
2458
+ gradients rather than an asset: no request, and it takes the theme. */
2459
+ .imgStage {
2460
+ flex: 1 1 auto;
2461
+ min-height: 0;
2462
+ display: flex;
2463
+ align-items: center;
2464
+ justify-content: center;
2465
+ overflow: auto;
2466
+ padding: 12px;
2467
+ border-radius: var(--gs-r-control);
2468
+ background-color: var(--gs-surface-2);
2469
+ background-image:
2470
+ linear-gradient(45deg, var(--gs-border-soft) 25%, transparent 25%, transparent 75%, var(--gs-border-soft) 75%),
2471
+ linear-gradient(45deg, var(--gs-border-soft) 25%, transparent 25%, transparent 75%, var(--gs-border-soft) 75%);
2472
+ background-size: 16px 16px;
2473
+ background-position: 0 0, 8px 8px;
2474
+ }
2475
+
2476
+ /* Fit is the default: an 8000px render opened at full size shows its top-left
2477
+ corner, which looks like a broken image rather than a big one. */
2478
+ .imgShot {
2479
+ max-width: 100%;
2480
+ max-height: 100%;
2481
+ object-fit: contain;
2482
+ /* Nearest-neighbour would be right for icons and wrong for photographs, and
2483
+ the pane cannot tell them apart; smooth is the safe half of that trade. */
2484
+ image-rendering: auto;
2485
+ }
2486
+
2487
+ /* Actual size: the scaling comes off and the stage scrolls instead. Written
2488
+ as a descendant of the stage's own state so the picture keeps ONE class,
2489
+ and the two views cannot disagree about which is in force. */
2490
+ .imgStage[data-actual] {
2491
+ align-items: flex-start;
2492
+ justify-content: flex-start;
2493
+ }
2494
+ .imgStage[data-actual] .imgShot {
2495
+ max-width: none;
2496
+ max-height: none;
2497
+ object-fit: none;
2498
+ }
2499
+
2500
+ .imgCaption {
2501
+ flex: none;
2502
+ display: flex;
2503
+ align-items: center;
2504
+ gap: 8px;
2505
+ font-size: var(--gs-t-meta);
2506
+ color: var(--gs-fg-dim);
2507
+ font-variant-numeric: tabular-nums;
2508
+ }