diff --git a/packages/panel/src/diff-view/index.ts b/packages/panel/src/diff-view/index.ts
index 89836f7a38..aca7c44303 100644
--- a/packages/panel/src/diff-view/index.ts
+++ b/packages/panel/src/diff-view/index.ts
@@ -173,6 +173,13 @@ const LOOK_CSS = `
[data-gutter] {
scrollbar-width: thin;
}
+ /* Note that a line's code scrolls sideways alone, so it keeps no room for a
+ scroll bar up and down, which a system that draws its bars in the
+ layout would otherwise leave as a strip at the end of every line, its
+ rows and its gaps stopping short of the file's header. */
+ [data-code] {
+ scrollbar-gutter: auto;
+ }
/* A file drawn as its notes alone, laid out as blocks: its one empty line, the old side, the numbers, and the hatching beside a side's missing lines are hidden, so its notes run across its whole width, as tall as Pierre measured them. */
:host([${LEAD_ATTRIBUTE}]) pre,
:host([${LEAD_ATTRIBUTE}]) [data-code],
@@ -210,19 +217,50 @@ const FILL_CSS = `
}
`;
-/** How tall a list's file header stands, in pixels, which Pierre reserves for it before it is measured: the view's `.diff-list-head` (`styles/tasks.css`). */
-const LIST_HEADER_HEIGHT = 34;
+/** How tall a list's file header stands, in pixels, which Pierre reserves for it before it is measured: the view's `.diff-list-head` (`styles/tasks.css`), its row and its rule. */
+const LIST_HEADER_HEIGHT = 37;
+
+/** How tall a band between two hunks of a list's file stands, in pixels, which Pierre lays the list out by (`LIST_CSS`). */
+const LIST_GAP_HEIGHT = 24;
+
+/** The room between two files of a list, and above the first, in pixels: the panel's ground between their cards. */
+const LIST_FILE_GAP = 12;
/** The room below a list's last file, in pixels, so its last lines can be scrolled up off the foot of the pane. */
const LIST_FOOT_ROOM = 48;
+/**
+ * A file in a list: a card, its edge and its corners the panel's, holding
+ * its header and its lines, each gap between hunks a quiet band of its own
+ * height (`LIST_GAP_HEIGHT`). Note that the card's corners are clipped at
+ * the card's own edge, so its header, stuck at the top of the list while
+ * its lines scroll by, stands square there, the rounded top scrolled away.
+ */
+const LIST_CSS = `
+@layer unsafe {
+ :host {
+ box-sizing: border-box;
+ overflow: clip;
+ border: 1px solid var(--luke-color-border);
+ border-radius: var(--luke-control-radius);
+ --diffs-bg-separator-override: var(--luke-color-fill-hover);
+ }
+ [data-separator="line-info-basic"] {
+ height: ${LIST_GAP_HEIGHT}px;
+ color: var(--luke-color-text-subtle);
+ font-family: var(--luke-font-sans);
+ font-size: var(--luke-type-caption-size);
+ }
+}
+`;
+
let sheets: { look: CSSStyleSheet; fill: CSSStyleSheet } | undefined;
-/** The look's sheets, made once and adopted by every diff's shadow root: the look every diff in a list wears, and the same with the fill one diff alone wears. */
+/** The look's sheets, made once and adopted by every diff's shadow root: the look with the card every file of a list wears, and the look with the fill one diff alone wears. */
function lookSheets(): { look: CSSStyleSheet; fill: CSSStyleSheet } {
if (sheets !== undefined) return sheets;
const look = new CSSStyleSheet();
- look.replaceSync(LOOK_CSS);
+ look.replaceSync(`${LOOK_CSS}${LIST_CSS}`);
const fill = new CSSStyleSheet();
fill.replaceSync(`${LOOK_CSS}${FILL_CSS}`);
sheets = { look, fill };
@@ -476,8 +514,9 @@ function listOptions(
itemMetrics: {
lineHeight: Number(drawn.fontSize) * Number(LINE_HEIGHT_RATIO[drawn.lineHeight]),
diffHeaderHeight: LIST_HEADER_HEIGHT,
+ hunkSeparatorHeight: LIST_GAP_HEIGHT,
},
- layout: { paddingTop: 0, paddingBottom: LIST_FOOT_ROOM, gap: 0 },
+ layout: { paddingTop: LIST_FILE_GAP, paddingBottom: LIST_FOOT_ROOM, gap: LIST_FILE_GAP },
renderCustomHeader: (
_drawn: FileDiffMetadata | FileContents,
context: { item: { id: string } },
diff --git a/packages/panel/src/styles/tasks.css b/packages/panel/src/styles/tasks.css
index ae1d4e522c..9ad5acee14 100644
--- a/packages/panel/src/styles/tasks.css
+++ b/packages/panel/src/styles/tasks.css
@@ -1497,12 +1497,19 @@
user-select: text;
}
-/* The list fills the File tab's place, its scroll area the whole of it. */
+/* The list fills the File tab's place, its scroll area the whole of it, its
+ files cards on the panel's ground, inset from its edges by the room between
+ two of them (diff-view/index.ts's LIST_FILE_GAP). */
.diff-list {
display: flex;
min-height: 0;
flex: 1;
flex-direction: column;
+ background: var(--luke-color-ground);
+}
+
+.diff-list > .luke-scroll-area > :first-child {
+ padding-inline: var(--luke-space-12);
}
/* The sheet of Files changed's keys (files-changed-keys.tsx): its groups
@@ -1520,21 +1527,20 @@
bundle reserves for it (diff-view/index.ts's LIST_HEADER_HEIGHT). The file
stage-cli's keys stand on is ringed as a focused control is. */
.diff-list-head {
- border-top: 1px solid var(--luke-color-border);
- background: var(--code-ground);
+ background: var(--luke-color-surface);
color: var(--luke-color-text);
font-family: var(--luke-font-sans);
+ cursor: default;
}
.diff-list-head-row {
display: flex;
box-sizing: border-box;
- height: 33px;
+ height: 36px;
min-width: 0;
align-items: center;
gap: var(--luke-space-8);
- padding: 0 var(--luke-space-12) 0 var(--luke-space-6);
- background: var(--luke-color-surface);
+ padding: 0 var(--luke-space-12) 0 var(--luke-space-4);
font-size: var(--luke-type-caption-size);
white-space: nowrap;
}
@@ -1557,14 +1563,43 @@
transform: none;
}
+/* A file's path: its folder quiet and cut first where the row runs short, so
+ its name, in the full ink, stands whole as long as it can. */
.diff-list-path {
+ display: flex;
min-width: 0;
flex: 0 1 auto;
- overflow: hidden;
font-family: var(--luke-font-mono);
+}
+
+.diff-list-folder {
+ min-width: 0;
+ overflow: hidden;
+ color: var(--luke-color-text-muted);
+ text-overflow: ellipsis;
+}
+
+.diff-list-name {
+ flex: none;
+ max-width: 100%;
+ overflow: hidden;
+ font-weight: var(--luke-weight-medium);
text-overflow: ellipsis;
}
+/* A file's letter in its header stands on a tint of its own colour, the
+ letter itself what says what became of it. */
+.diff-list-head .file-view-status {
+ display: inline-flex;
+ min-width: var(--luke-space-16);
+ height: var(--luke-space-16);
+ align-items: center;
+ justify-content: center;
+ border-radius: var(--luke-radius-6);
+ background: color-mix(in srgb, currentcolor 14%, transparent);
+ font-size: var(--luke-type-caption-size);
+}
+
/* A file's Viewed box, GitHub's own (viewed-files.ts), at the end of its
header's row. */
.diff-list-viewed {
@@ -1695,6 +1730,12 @@
flex: 1;
}
+/* Beside Files changed's cards, the search stands where the first card does,
+ the room between files above it, so the tree and the list share a top. */
+.file-tab:has(> .diff-list) + .file-tree-pane .file-tree-search {
+ padding-top: var(--luke-space-12);
+}
+
.file-tree-notice {
display: flex;
flex-direction: column;
@@ -2460,7 +2501,7 @@
so the window's own type and card draw it, inset from the code's edges
as GitHub's are; one the diff draws no row for stands in a list above it. */
.diff-note {
- padding: var(--luke-space-6) var(--luke-space-12);
+ padding: var(--luke-space-8) var(--luke-space-12);
font-family: var(--luke-font-sans);
white-space: normal;
}
diff --git a/packages/panel/src/tasks/agent-changes-tab.tsx b/packages/panel/src/tasks/agent-changes-tab.tsx
index 27002106ef..4c550347d1 100644
--- a/packages/panel/src/tasks/agent-changes-tab.tsx
+++ b/packages/panel/src/tasks/agent-changes-tab.tsx
@@ -61,6 +61,7 @@ import {
FOLD_CHORD,
LIST_WORDS,
useChangesList,
+ VIEWED_CHORD,
} from "./changes-list.js";
import {
blobShaOf,
@@ -732,11 +733,16 @@ function ViewedBox({
onToggle: () => void;
}): React.JSX.Element {
return (
-
-
- {LIST_WORDS.VIEWED}
-
-
+
+
+
+ {LIST_WORDS.VIEWED}
+
+
+
);
}
diff --git a/packages/panel/src/tasks/changes-list.tsx b/packages/panel/src/tasks/changes-list.tsx
index 7d778f50c2..2b60d19c73 100644
--- a/packages/panel/src/tasks/changes-list.tsx
+++ b/packages/panel/src/tasks/changes-list.tsx
@@ -6,7 +6,7 @@ import {
filesChangedKeyFor,
shortcutAria,
} from "@luke/settings/shortcuts";
-import { IconButton } from "@luke/ui";
+import { BUTTON_SIZE, IconButton } from "@luke/ui";
import { ChevronRightIcon } from "lucide-react";
import {
type ReactNode,
@@ -66,6 +66,9 @@ export const FOLD_CHORD = {
ALL: FILES_CHANGED_SHORTCUTS[FILES_CHANGED_KEY.TOGGLE_ALL_FILES].chord,
} as const;
+/** The key that marks the file stood on viewed, or unmarks it, as GitHub's own V does. */
+export const VIEWED_CHORD = FILES_CHANGED_SHORTCUTS[FILES_CHANGED_KEY.MARK_VIEWED].chord;
+
/** What the list says where a filter keeps no file. */
export const LIST_WORDS = {
NONE_KEPT: "No changed file's path holds that.",
@@ -76,6 +79,8 @@ export const LIST_WORDS = {
SHOW_SOURCE: "Show source",
SHOW_IMAGE: "Show image",
VIEWED: "Viewed",
+ MARK_VIEWED: "Mark as viewed",
+ UNMARK_VIEWED: "Mark as not viewed",
/** How far through the files the member is, as GitHub counts it. */
viewedOf: (viewed: number, files: number) => `${viewed} / ${files} viewed`,
} as const;
@@ -348,6 +353,23 @@ export function useChangesList(
};
}
+/** A path as a header names it: its folder, quiet, then its name. */
+function PathParts({ path }: { path: string }): React.JSX.Element {
+ const cut = path.lastIndexOf("/") + 1;
+ return (
+ <>
+ {cut === 0 ? null : (
+
+ {path.slice(0, cut)}
+
+ )}
+
+ {path.slice(cut)}
+
+ >
+ );
+}
+
/** A pointer arriving over a control, as the window says it. */
function pointerOver(): PointerEvent {
return new PointerEvent("pointerover", { bubbles: true, pointerType: "mouse" });
@@ -403,6 +425,7 @@ export function FileHead({
ref={foldRef}
icon={}
label={label}
+ size={BUTTON_SIZE.COMPACT}
aria-expanded={!collapsed}
aria-keyshortcuts={shortcutAria(FOLD_CHORD.ONE)}
onClick={onToggle}
@@ -452,11 +475,11 @@ export function FileHead({
{near ? {letter} : letter}
{file.previousPath === null ? null : (
- <>
- {file.previousPath} →{" "}
- >
+
+ {file.previousPath} →{"\u00a0"}
+
)}
- {file.path}
+
{controls}