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={