@young1lin/dsh-ui-gitworkbench 0.1.4 → 0.1.6

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 (51) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/CHANGELOG_EN.md +42 -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 +34811 -11818
  7. package/lib/fs-remove.js +73 -0
  8. package/lib/git-ops.js +25 -0
  9. package/lib/image-sniff.js +197 -0
  10. package/lib/index.js +404 -32
  11. package/lib/patch-model.js +223 -0
  12. package/lib/side-guard.js +55 -0
  13. package/lib/write-checked.js +164 -0
  14. package/package.json +7 -1
  15. package/src/apply-blocks.ts +215 -0
  16. package/src/atomic-json.ts +29 -5
  17. package/src/blame.ts +94 -0
  18. package/src/client/CodeEditor.tsx +317 -0
  19. package/src/client/FileBrowser.tsx +657 -0
  20. package/src/client/GitWorkbenchPanel.module.css +453 -7
  21. package/src/client/GitWorkbenchPanel.tsx +1465 -166
  22. package/src/client/ImageView.tsx +120 -0
  23. package/src/client/blame-gutter.ts +108 -0
  24. package/src/client/blame-view.ts +104 -0
  25. package/src/client/cm-diff.ts +108 -0
  26. package/src/client/cm-tokens.ts +79 -0
  27. package/src/client/diff-nav.ts +198 -0
  28. package/src/client/discard-flow.ts +82 -0
  29. package/src/client/file-icon.ts +190 -0
  30. package/src/client/file-rows.ts +184 -0
  31. package/src/client/files-place.ts +178 -0
  32. package/src/client/glyphs.tsx +86 -0
  33. package/src/client/highlight.ts +25 -0
  34. package/src/client/idle-value.ts +53 -0
  35. package/src/client/image-view.ts +106 -0
  36. package/src/client/indent.ts +74 -0
  37. package/src/client/index.ts +76 -9
  38. package/src/client/locales.ts +171 -4
  39. package/src/client/pane-size.ts +71 -0
  40. package/src/client/side-edit.ts +244 -0
  41. package/src/client/side-rows.ts +258 -0
  42. package/src/client/stable-list.ts +31 -0
  43. package/src/client/use-change-nav.ts +83 -0
  44. package/src/client/worktree-view.ts +11 -1
  45. package/src/fs-remove.ts +76 -0
  46. package/src/git-ops.ts +36 -1
  47. package/src/image-sniff.ts +204 -0
  48. package/src/index.ts +450 -32
  49. package/src/patch-model.ts +267 -0
  50. package/src/side-guard.ts +58 -0
  51. 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;
@@ -1310,12 +1320,15 @@
1310
1320
  .commitLine {
1311
1321
  display: flex; align-items: stretch;
1312
1322
  flex: none;
1313
- height: 48px;
1323
+ /* Must equal GRAPH_ROW_H in GitWorkbenchPanel.tsx, or the lanes drawn per
1324
+ row stop meeting across the seam between rows. Guarded by
1325
+ tests/commit-row-height.test.ts. */
1326
+ height: 28px;
1314
1327
  }
1315
1328
  .graphCell { flex: none; display: block; }
1316
1329
  /* An inset row, like dsh's own session rows: 12px radius, filled when active. */
1317
1330
  .commit {
1318
- display: flex; flex-direction: column; justify-content: center; gap: 2px;
1331
+ display: flex; align-items: center; gap: 10px;
1319
1332
  flex: 1 1 auto; min-width: 0; box-sizing: border-box;
1320
1333
  height: 100%;
1321
1334
  margin: 1px 0;
@@ -1332,14 +1345,24 @@
1332
1345
  }
1333
1346
  .commit:hover { background: var(--gs-raise); }
1334
1347
  .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); }
1348
+ /* Pushed to the right end of the row, and never squeezed: the subject is what
1349
+ gives way when the pane narrows, because it is the part with an ellipsis to
1350
+ give. */
1351
+ .commitMeta { flex: none; display: flex; align-items: baseline; gap: 8px; }
1352
+ .commitHash { flex: none; color: var(--gs-info); }
1337
1353
  .commitSubjectRow {
1338
1354
  display: flex; align-items: baseline; gap: 4px;
1339
- min-width: 0;
1355
+ flex: 1 1 auto; min-width: 0;
1340
1356
  }
1357
+ /* Shrinks, never grows. Growing was invisible while the list was a 26%%
1358
+ column — the subject filled the row either way — and became a defect the
1359
+ moment the list spanned the drawer: a subject that takes ALL the free space
1360
+ pushes the has-a-body marker a thousand pixels right, where it reads as an
1361
+ unexplained ellipsis next to the author rather than as "there is more to
1362
+ this message". The row still pushes author and date to the right end;
1363
+ .commitSubjectRow is what absorbs the slack. */
1341
1364
  .commitSubject {
1342
- flex: 1; min-width: 0;
1365
+ flex: 0 1 auto; min-width: 0;
1343
1366
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
1344
1367
  color: var(--gs-fg-muted);
1345
1368
  }
@@ -1366,6 +1389,27 @@
1366
1389
  }
1367
1390
 
1368
1391
  .body { display: flex; flex: 1; min-height: 0; z-index: 1; }
1392
+ /* History stacks: the commit list over the tree-and-diff row. `.bodyRow` is
1393
+ rendered on every tab, so the unstacked tabs are a row inside a row — which
1394
+ costs nothing and keeps one JSX shape instead of two. */
1395
+ .body[data-stacked] { flex-direction: column; }
1396
+ .bodyRow { display: flex; flex: 1 1 0; min-width: 0; min-height: 0; }
1397
+ /* Stacked, the list is sized by HEIGHT and spans the drawer; the width rules
1398
+ it carries for the column layout would otherwise pin it to 26%. */
1399
+ .body[data-stacked] .commitsPane {
1400
+ width: auto; max-width: none; min-width: 0;
1401
+ height: 34%; flex: none;
1402
+ /* The drag's clamp, restated where a RESIZE can also reach it. An inline
1403
+ `height` in pixels survives the window getting shorter, and then the list
1404
+ is taller than the body: the lower half goes to zero and the handle ends
1405
+ up below the bottom edge with nothing to grab. Percentages re-resolve on
1406
+ every layout, which is exactly what the stored pixel height cannot do.
1407
+ Both numbers come from the panel's own constants, so there is still one
1408
+ home for them. `max()` keeps the range from inverting in a drawer too
1409
+ short to hold both floors — the list then simply takes its minimum. */
1410
+ min-height: var(--gs-min-commits-tall);
1411
+ max-height: max(var(--gs-min-commits-tall), calc(100% - var(--gs-min-stacked-lower)));
1412
+ }
1369
1413
 
1370
1414
  /* File tree column. A peer of the commit pane and the diff, never their parent:
1371
1415
  each of the three scrolls on its own and keeps its full height. */
@@ -1553,7 +1597,29 @@
1553
1597
  .fileCountDel { color: var(--gs-del); }
1554
1598
 
1555
1599
  /* diff pane */
1556
- .diffPane { flex: 1 1 0; min-width: var(--gs-min-diff); overflow: auto; background: var(--gs-surface); }
1600
+ /* A column, so a header can sit above the scrolled diff instead of inside it.
1601
+ `overflow: auto` stays as the outer guard — the rename line and the
1602
+ side-by-side pane are also children here, and one of them growing must
1603
+ scroll rather than be clipped. */
1604
+ .diffPane {
1605
+ flex: 1 1 0; min-width: var(--gs-min-diff);
1606
+ display: flex; flex-direction: column;
1607
+ overflow: auto; background: var(--gs-surface);
1608
+ }
1609
+ /* The unified view: its own header and its own scroller, filling whatever the
1610
+ pane has left after a rename line. */
1611
+ .diffWrap { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; width: 100%; }
1612
+ .diffScroll { flex: 1 1 0; min-height: 0; overflow: auto; outline: none; }
1613
+ /* The walk's controls. Right-aligned and quiet: they belong to the diff, and
1614
+ the diff is what the reader came for. Same shape as `.sideNav` in the
1615
+ side-by-side pane, because it is the same control. */
1616
+ .diffNav {
1617
+ flex: none;
1618
+ display: flex; justify-content: flex-end; align-items: center; gap: 4px;
1619
+ padding: 3px 12px 3px 0;
1620
+ border-bottom: 1px solid var(--gs-border-soft);
1621
+ }
1622
+ .diffNav .blockBtn { padding-inline: 6px; line-height: 0; }
1557
1623
  /* The pre is as wide as its longest line (`max-content`) and at least the pane
1558
1624
  (`min-width: 100%`). Each row then stretches to that width, so add/delete
1559
1625
  tints survive horizontal scroll instead of stopping at the viewport edge. */
@@ -1570,6 +1636,7 @@
1570
1636
  tab-size: 4;
1571
1637
  }
1572
1638
  .renameLine {
1639
+ flex: none;
1573
1640
  padding: 8px 16px;
1574
1641
  border-bottom: 1px solid var(--gs-border-soft);
1575
1642
  color: var(--gs-info);
@@ -1628,6 +1695,205 @@
1628
1695
  .wordAdd { background: var(--gs-add-word); border-radius: 2px; }
1629
1696
  .wordDel { background: var(--gs-del-word); border-radius: 2px; }
1630
1697
 
1698
+ /* Side-by-side diff pane. One grid holds EVERY row's four cells — a per-row
1699
+ grid would let the column boundary drift with each row's content, and a
1700
+ fixed split would clip long lines. Equal 1fr code tracks keep the two
1701
+ columns identically wide however wide the widest line is, and the whole pane
1702
+ (this view's single shared vertical scroll) carries the horizontal one. */
1703
+ /* The pane is a HEADER plus a scroller, not one wide column: the tab row
1704
+ carries Save/Revert, and a row sized to the widest line of code pushes them
1705
+ off the right edge of a long file — reachable only by scrolling sideways,
1706
+ which is not where anyone looks for a toolbar. Only `.sideScroll` scrolls;
1707
+ the tabs and the notices above it stay put on both axes. */
1708
+ .sidePane { display: flex; flex-direction: column; width: 100%; min-width: 0; height: 100%; }
1709
+ /* Vertical scrolling belongs to the pane; horizontal belongs to each column,
1710
+ which is what lets the divider mean something. A single scroller for both
1711
+ axes would tie the two sides' horizontal position together and size the
1712
+ whole grid to the widest line in the file. */
1713
+ .sideScroll { flex: 1 1 0; min-height: 0; overflow-y: auto; overflow-x: hidden; }
1714
+ .sideCols { display: flex; align-items: stretch; min-height: 100%; }
1715
+ /* `min-width: 0` is what lets a flex column be narrower than its content —
1716
+ without it the column refuses to shrink and the divider stops moving. */
1717
+ .sideCol { flex: 0 0 auto; min-width: 0; overflow-x: auto; }
1718
+ .sideColRight { flex: 1 1 0; }
1719
+ .sideColGrid {
1720
+ display: grid;
1721
+ grid-template-columns: 3.2em 1fr;
1722
+ align-content: start;
1723
+ padding: 8px 0 16px;
1724
+ min-width: 100%; width: max-content;
1725
+ font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
1726
+ font-size: var(--gs-t-dense); line-height: 20px; font-weight: 400;
1727
+ font-variant-ligatures: none;
1728
+ font-feature-settings: 'liga' 0, 'calt' 0;
1729
+ tab-size: 4;
1730
+ }
1731
+ /* The blame gutter, on the working-tree column only. Fixed width so the code
1732
+ beside it does not shift line to line, and clipped rather than wrapped: a
1733
+ gutter row that grew to two lines would break the alignment the whole
1734
+ side-by-side view rests on. */
1735
+ .sideColGridBlame { grid-template-columns: 13em 3.2em 1fr; }
1736
+ .blameCell {
1737
+ box-sizing: border-box;
1738
+ padding: 0 8px 0 10px;
1739
+ border-right: 1px solid var(--gs-border-soft);
1740
+ color: var(--gs-fg-faint);
1741
+ font-size: var(--gs-t-meta);
1742
+ white-space: pre; overflow: hidden; text-overflow: ellipsis;
1743
+ user-select: none;
1744
+ }
1745
+ .sideTabs {
1746
+ z-index: 5;
1747
+ display: flex; gap: 16px;
1748
+ flex: none;
1749
+ padding: 6px 16px 0;
1750
+ border-bottom: 1px solid var(--gs-border-soft);
1751
+ background: var(--gs-surface-2);
1752
+ }
1753
+ .sideTab {
1754
+ position: relative;
1755
+ padding: 2px 0 5px;
1756
+ border: none;
1757
+ background: transparent;
1758
+ font-family: inherit;
1759
+ font-size: var(--gs-t-meta); line-height: 16px; font-weight: 500;
1760
+ color: var(--gs-fg-dim);
1761
+ cursor: pointer;
1762
+ }
1763
+ .sideTab::after {
1764
+ content: '';
1765
+ position: absolute; right: 0; bottom: 1px; left: 0;
1766
+ height: 2px; border-radius: 2px;
1767
+ background: transparent;
1768
+ }
1769
+ .sideTabActive { color: var(--gs-accent); }
1770
+ .sideTabActive::after { background: var(--gs-accent); }
1771
+ .sideNotice {
1772
+ flex: none;
1773
+ padding: 8px 16px;
1774
+ border-bottom: 1px solid var(--gs-border-soft);
1775
+ color: var(--gs-info);
1776
+ font-size: var(--gs-t-dense);
1777
+ }
1778
+ .sideNum {
1779
+ box-sizing: border-box;
1780
+ padding: 0 8px;
1781
+ text-align: right;
1782
+ color: var(--gs-fg-fainter);
1783
+ user-select: none;
1784
+ font-variant-numeric: tabular-nums;
1785
+ }
1786
+ .sideNumAdd { background: var(--gs-add-num, var(--gs-add-line)); }
1787
+ .sideNumDel { background: var(--gs-del-num, var(--gs-del-line)); }
1788
+ .sideCode { white-space: pre; padding: 0 16px 0 10px; min-height: 20px; }
1789
+ .sideCodeSame { color: var(--gs-fg-muted); }
1790
+ .sideCodeAdd { background: var(--gs-add-line); }
1791
+ .sideCodeDel { background: var(--gs-del-line); }
1792
+ /* The block affordance: a changed row's cells carry the block tint above, and
1793
+ the hovered block's every cell — code and gutter — takes the hot outline,
1794
+ so the reader sees the unit the buttons act on rather than the one cell the
1795
+ pointer happens to be over. `.sideCellBlock` also positions the action bar,
1796
+ which a cell of the block's first row carries. */
1797
+ .sideCellBlock { position: relative; }
1798
+ .sideBlockHot { outline: 1px solid var(--gs-accent); outline-offset: -1px; }
1799
+
1800
+ /* The hovered block's actions, floating on the row above the block. It lives
1801
+ INSIDE the block's first code cell, so it travels with the column and the
1802
+ scroll, and it only ever covers context — never the change it acts on. The
1803
+ surface and shadow are what keep it readable over code. */
1804
+ .blockBar {
1805
+ position: absolute; top: -21px; left: 0; z-index: 4;
1806
+ display: flex; gap: 4px;
1807
+ padding: 2px;
1808
+ border: 1px solid var(--gs-border);
1809
+ border-radius: var(--gs-r-control);
1810
+ background: var(--gs-surface-2, var(--gs-bg));
1811
+ box-shadow: 0 4px 14px var(--gs-shadow);
1812
+ white-space: nowrap;
1813
+ }
1814
+ .blockBtn {
1815
+ padding: 0 8px;
1816
+ border: 1px solid var(--gs-border); border-radius: var(--gs-r-pill);
1817
+ background: var(--gs-bg); color: var(--gs-fg-dim);
1818
+ font-family: inherit; font-size: var(--gs-t-meta); line-height: 16px;
1819
+ cursor: pointer;
1820
+ }
1821
+ .blockBtn:hover:not(:disabled) { color: var(--gs-fg); border-color: var(--gs-accent); }
1822
+ .blockBtn:disabled { opacity: 0.45; cursor: default; }
1823
+ /* The roll-back button stays red at rest, not only on hover: it is the one
1824
+ action in the bar that cannot be undone, and arriving under the pointer is
1825
+ not what should make that visible. */
1826
+ .blockBtnDanger { color: var(--gs-del); border-color: var(--gs-del-bg, var(--gs-border)); }
1827
+ .blockBtnDanger:hover:not(:disabled) { color: var(--gs-del); border-color: var(--gs-del); }
1828
+
1829
+ /* The editable right column. The working-tree column is readable but visibly
1830
+ a text surface before editing is armed; the tab row carries the arm/save
1831
+ cluster at its right end. */
1832
+ .sideArmable { cursor: text; }
1833
+
1834
+ /* An editable buffer says so — the one thing a rendered code pane cannot say
1835
+ by itself.
1836
+
1837
+ Nothing about monospaced text on a background looks like a field. In the
1838
+ Files tab the editor is live the moment the file opens, so without a mark
1839
+ the reader finds out whether they may type by typing — and finds out they
1840
+ may not the same way, on a pane that silently swallows it. The rule runs
1841
+ the full height of the editor's leading edge: dim while nothing is focused
1842
+ (present, not loud — it sits beside code someone came to read), full accent
1843
+ once the caret is actually inside, which doubles as "this pane has the
1844
+ keyboard".
1845
+
1846
+ Driven by an attribute the component sets from the same boolean that
1847
+ configures `EditorState.readOnly`, so the rule cannot appear on a pane that
1848
+ would refuse the keystroke. `pointer-events: none` because the bar overlays
1849
+ the gutter's first two pixels, and a click there must still reach the
1850
+ view. */
1851
+ .cmHost { position: relative; }
1852
+ .cmHost[data-editable]::before {
1853
+ content: '';
1854
+ position: absolute;
1855
+ top: 0; bottom: 0; left: 0;
1856
+ width: 2px;
1857
+ border-radius: 1px;
1858
+ background: var(--gs-accent);
1859
+ opacity: 0.32;
1860
+ transition: opacity 140ms ease;
1861
+ pointer-events: none;
1862
+ }
1863
+ .cmHost[data-editable]:focus-within::before { opacity: 1; }
1864
+ /* The change nav takes the free space, so it and `.sideActions` after it sit
1865
+ together at the right end. Only the FIRST auto margin absorbs anything —
1866
+ the one on `.sideActions` then has nothing left to take, which is what
1867
+ keeps the two groups adjacent rather than pushed to opposite ends. */
1868
+ .sideNav { margin-left: auto; display: flex; gap: 4px; align-items: center; padding: 2px 0 5px; }
1869
+ .sideNav .blockBtn { padding-inline: 6px; line-height: 0; }
1870
+ .sideNavCount { color: var(--gs-fg-faint); font-size: var(--gs-t-meta); white-space: nowrap; }
1871
+ .sideActions { margin-left: auto; display: flex; gap: 4px; align-items: flex-start; padding: 2px 0 5px; }
1872
+ /* Save gains the accent only once there is something to save: the button's
1873
+ whole enabled state is the dirty flag, and colour says it sooner than the
1874
+ pointer does. */
1875
+ .sideSaveReady { color: var(--gs-accent); border-color: var(--gs-accent); }
1876
+
1877
+ /* The reload-or-overwrite banner (§4) and the failed-save banner: a warning
1878
+ strip under the tabs carrying its own actions, so the decision it asks for
1879
+ is one click away rather than a sentence away. */
1880
+ .sideBanner {
1881
+ flex: none;
1882
+ display: flex; flex-direction: column; gap: 6px;
1883
+ padding: 8px 16px;
1884
+ border-bottom: 1px solid var(--gs-border-soft);
1885
+ background: var(--gs-del-bg, transparent);
1886
+ color: var(--gs-del);
1887
+ font-size: var(--gs-t-dense);
1888
+ }
1889
+ .sideBannerTitle { font-weight: 600; }
1890
+ .sideBannerActs { display: flex; gap: 4px; }
1891
+
1892
+ /* The editor grid item that positions the textarea. It spans exactly the
1893
+ buffer's rows of the fourth column; the textarea fills it absolutely. */
1894
+ .sideEditCell { position: relative; }
1895
+
1896
+
1631
1897
  /* No syntax-tint classes here: Shiki resolves a real TextMate theme and emits a
1632
1898
  hex per token, so the panel writes colour inline. A four-class approximation
1633
1899
  painted identifiers, types and punctuation all one colour, which is what made
@@ -2027,3 +2293,183 @@
2027
2293
  font-weight: 600;
2028
2294
  }
2029
2295
  .btn.btnDanger:hover:not(:disabled) { border-color: var(--gs-del); background: var(--gs-del); color: var(--gs-on-accent, #fff); }
2296
+
2297
+ /* Files tab: the repository browser's tree, and the pane that opens a file.
2298
+ Modifier rules stay BELOW the base rule they narrow — a modifier declared
2299
+ first loses the cascade, which the drawer-chrome test checks for. */
2300
+ .fbTree {
2301
+ flex: none;
2302
+ min-width: var(--gs-min-tree);
2303
+ display: flex;
2304
+ flex-direction: column;
2305
+ gap: 4px;
2306
+ padding: 6px;
2307
+ overflow: auto;
2308
+ background: var(--gs-surface-2);
2309
+ }
2310
+ .fbSearch {
2311
+ flex: none;
2312
+ width: 100%;
2313
+ box-sizing: border-box;
2314
+ padding: 4px 6px;
2315
+ font: inherit;
2316
+ font-size: var(--gs-t-meta);
2317
+ color: var(--gs-fg);
2318
+ background: var(--gs-surface);
2319
+ border: 1px solid var(--gs-border);
2320
+ border-radius: 4px;
2321
+ }
2322
+ .fbSearch:focus { outline: none; border-color: var(--gs-accent); }
2323
+ .fbNote { flex: none; font-size: var(--gs-t-meta); color: var(--gs-fg-dim); padding: 2px 4px; }
2324
+ .fbList { list-style: none; margin: 0; padding: 0; }
2325
+ .fbRow {
2326
+ display: flex;
2327
+ align-items: center;
2328
+ gap: 4px;
2329
+ width: 100%;
2330
+ padding: 2px 6px 2px 4px;
2331
+ font: inherit;
2332
+ font-size: var(--gs-t-dense);
2333
+ color: var(--gs-fg-dim);
2334
+ text-align: left;
2335
+ background: none;
2336
+ border: 0;
2337
+ border-radius: 3px;
2338
+ cursor: pointer;
2339
+ white-space: nowrap;
2340
+ overflow: hidden;
2341
+ text-overflow: ellipsis;
2342
+ }
2343
+ .fbRow:hover { background: var(--gs-hover); }
2344
+ .fbRowActive { background: var(--gs-selected); color: var(--gs-fg); }
2345
+ .fbDirName { overflow: hidden; text-overflow: ellipsis; }
2346
+ .fbFileName { overflow: hidden; text-overflow: ellipsis; }
2347
+ .fbMain {
2348
+ flex: 1 1 0;
2349
+ min-width: var(--gs-min-diff);
2350
+ display: flex;
2351
+ flex-direction: column;
2352
+ min-height: 0;
2353
+ background: var(--gs-surface);
2354
+ }
2355
+ .fbHeader {
2356
+ flex: none;
2357
+ display: flex;
2358
+ align-items: flex-start;
2359
+ gap: 8px;
2360
+ padding: 4px 8px 0;
2361
+ border-bottom: 1px solid var(--gs-border);
2362
+ }
2363
+ .fbPath {
2364
+ font-size: var(--gs-t-meta);
2365
+ color: var(--gs-fg-dim);
2366
+ overflow: hidden;
2367
+ text-overflow: ellipsis;
2368
+ white-space: nowrap;
2369
+ padding: 4px 0;
2370
+ }
2371
+ /* The editor scrolls inside this, so the header and the notices above stay
2372
+ put — the same reason the side pane's toolbar is pinned. */
2373
+ .fbBody { flex: 1 1 0; min-height: 0; overflow: auto; padding: 4px 8px; }
2374
+
2375
+ /* The commit behind the picked blame line. A strip rather than a floating
2376
+ popup: it never covers the code it is about, and it survives a scroll. */
2377
+ .fbCommit {
2378
+ flex: none;
2379
+ display: flex;
2380
+ align-items: baseline;
2381
+ gap: 8px;
2382
+ padding: 4px 8px;
2383
+ font-size: var(--gs-t-meta);
2384
+ color: var(--gs-fg-dim);
2385
+ background: var(--gs-surface-2);
2386
+ border-bottom: 1px solid var(--gs-border);
2387
+ }
2388
+ .fbCommitLine { flex: none; color: var(--gs-fg-faint); font-variant-numeric: tabular-nums; }
2389
+ .fbCommitWho { flex: none; color: var(--gs-fg); font-weight: 600; }
2390
+ .fbCommitWhen { flex: none; font-variant-numeric: tabular-nums; }
2391
+ .fbCommitHash { flex: none; font-family: inherit; color: var(--gs-fg-faint); }
2392
+ .fbCommitWhat {
2393
+ flex: 1 1 auto;
2394
+ min-width: 0;
2395
+ overflow: hidden;
2396
+ text-overflow: ellipsis;
2397
+ white-space: nowrap;
2398
+ color: var(--gs-fg);
2399
+ }
2400
+ .fbCommitHint { color: var(--gs-fg-faint); }
2401
+ .fbMore {
2402
+ display: block;
2403
+ padding: 2px 6px 4px 4px;
2404
+ font-size: var(--gs-t-meta);
2405
+ color: var(--gs-fg-faint);
2406
+ font-style: italic;
2407
+ }
2408
+
2409
+ /* ============================ image preview ============================= */
2410
+
2411
+ /* A picture fills the pane and its facts sit under it, because the facts are
2412
+ the reason this is a viewer rather than an image tag: a screenshot in a
2413
+ repository is usually opened to check a size or a crop. */
2414
+ .imgPane {
2415
+ display: flex;
2416
+ flex-direction: column;
2417
+ min-height: 0;
2418
+ height: 100%;
2419
+ gap: 8px;
2420
+ }
2421
+
2422
+ /* The checkerboard is not decoration. A transparent PNG on a flat background
2423
+ is indistinguishable from an opaque one of that colour, and "is this
2424
+ transparent" is the question the file was opened to answer. Two overlaid
2425
+ gradients rather than an asset: no request, and it takes the theme. */
2426
+ .imgStage {
2427
+ flex: 1 1 auto;
2428
+ min-height: 0;
2429
+ display: flex;
2430
+ align-items: center;
2431
+ justify-content: center;
2432
+ overflow: auto;
2433
+ padding: 12px;
2434
+ border-radius: var(--gs-r-control);
2435
+ background-color: var(--gs-surface-2);
2436
+ background-image:
2437
+ linear-gradient(45deg, var(--gs-border-soft) 25%, transparent 25%, transparent 75%, var(--gs-border-soft) 75%),
2438
+ linear-gradient(45deg, var(--gs-border-soft) 25%, transparent 25%, transparent 75%, var(--gs-border-soft) 75%);
2439
+ background-size: 16px 16px;
2440
+ background-position: 0 0, 8px 8px;
2441
+ }
2442
+
2443
+ /* Fit is the default: an 8000px render opened at full size shows its top-left
2444
+ corner, which looks like a broken image rather than a big one. */
2445
+ .imgShot {
2446
+ max-width: 100%;
2447
+ max-height: 100%;
2448
+ object-fit: contain;
2449
+ /* Nearest-neighbour would be right for icons and wrong for photographs, and
2450
+ the pane cannot tell them apart; smooth is the safe half of that trade. */
2451
+ image-rendering: auto;
2452
+ }
2453
+
2454
+ /* Actual size: the scaling comes off and the stage scrolls instead. Written
2455
+ as a descendant of the stage's own state so the picture keeps ONE class,
2456
+ and the two views cannot disagree about which is in force. */
2457
+ .imgStage[data-actual] {
2458
+ align-items: flex-start;
2459
+ justify-content: flex-start;
2460
+ }
2461
+ .imgStage[data-actual] .imgShot {
2462
+ max-width: none;
2463
+ max-height: none;
2464
+ object-fit: none;
2465
+ }
2466
+
2467
+ .imgCaption {
2468
+ flex: none;
2469
+ display: flex;
2470
+ align-items: center;
2471
+ gap: 8px;
2472
+ font-size: var(--gs-t-meta);
2473
+ color: var(--gs-fg-dim);
2474
+ font-variant-numeric: tabular-nums;
2475
+ }