Add embedded PTY terminal, keyboard shortcuts, and assignee/label integrations #53

Merged
Christoph merged 9 commits from UI-UX into main 2026-09-22 19:13:07 +00:00
4 changed files with 51 additions and 7 deletions
Showing only changes of commit 62e59c6e95 - Show all commits
+5
View File
@@ -6579,6 +6579,11 @@ input:focus, textarea:focus, select:focus { box-shadow: 0 0 0 3px color-mix(in s
linear-gradient(180deg, color-mix(in srgb, var(--color-success) 2.5%, transparent), transparent 120px),
var(--color-surface-solid);
}
/* The lane holding the keyboard focus is the one Ctrl/Cmd + A selects in. */
.status-lane:focus { outline: none; }
.status-lane:focus-within {
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 34%, transparent);
}
.status-flow-divider {
position: relative;
display: grid;
+4 -2
View File
@@ -249,7 +249,8 @@
summary: "Die Hilfe ist überall erreichbar. Dialoge lassen sich konsistent schließen und Suchfelder direkt fokussieren.",
commands: [
{ command: "Ctrl + /", description: "Diese Hilfe öffnen" },
{ command: "Escape", description: "Aktuelles Overlay oder Dialogfenster schließen" },
{ command: "Ctrl + A", description: "Alle Dateien in der aktiven Statusliste („Ungestaged“ oder „Gestaged“) auswählen" },
{ command: "Escape", description: "Aktuelles Overlay oder Dialogfenster schließen in der Statusliste die aktuelle Auswahl aufheben" },
{ command: "Tab / Shift + Tab", description: "Zwischen Bedienelementen wechseln" },
{ command: "Enter / Leertaste", description: "Fokussierte Aktion ausführen" },
],
@@ -459,7 +460,8 @@
summary: "Help is available everywhere. Dialogs close consistently and search fields receive focus automatically.",
commands: [
{ command: "Ctrl + /", description: "Open this help center" },
{ command: "Escape", description: "Close the current overlay or dialog" },
{ command: "Ctrl + A", description: "Select every file in the focused status list (Unstaged or Staged)" },
{ command: "Escape", description: "Close the current overlay or dialog in the status list, clear the current selection" },
{ command: "Tab / Shift + Tab", description: "Move between controls" },
{ command: "Enter / Space", description: "Activate the focused control" },
],
+41 -5
View File
@@ -323,8 +323,44 @@
onStopTracking(targets, target.kind);
}
function focusStatusLane(event: PointerEvent) {
const target = event.target;
if (target instanceof Element && !target.closest("input, textarea, select, a, [contenteditable]")) {
(event.currentTarget as HTMLElement).focus({ preventScroll: true });
}
}
function clearStatusSelection(): boolean {
if (selectedStatusPaths.size === 0 && !selectionAnchorKey) return false;
selectedStatusPaths = new Set();
selectionAnchorKey = "";
return true;
}
function handleStatusWindowKeydown(event: KeyboardEvent) {
if (event.key === "Escape") closeStatusContextMenu();
// Escape backs out one step at a time: first the context menu, then the
// current multi-selection. No preventDefault, so an open dialog still
// closes on the same keypress.
if (event.key === "Escape") {
if (statusContextTarget) {
closeStatusContextMenu();
return;
}
const escapeTarget = event.target;
if (escapeTarget instanceof Element && escapeTarget.closest("input, textarea, select, [contenteditable]")) return;
clearStatusSelection();
return;
}
if (event.defaultPrevented || isBusy || !hasRepository || !(event.ctrlKey || event.metaKey) || event.altKey || event.shiftKey || event.key.toLowerCase() !== "a") return;
const target = event.target;
if (!(target instanceof Element) || target.closest("input, textarea, select, [contenteditable]")) return;
const lane = target.closest(".status-lane");
if (!lane) return;
const files = lane.classList.contains("unstaged-lane") ? unstagedFiles : stagedFiles;
event.preventDefault();
selectedStatusPaths = new Set(files.map(fileKey));
selectionAnchorKey = files.length ? fileKey(files[0]) : "";
closeStatusContextMenu();
}
function isStatusSelected(file: GitFileStatus): boolean {
@@ -459,8 +495,8 @@
<div class="blank-state">{t("status.noChanges")}</div>
{:else}
<div class="status-lanes">
<section class="status-lane unstaged-lane" aria-label={t("status.laneUnstaged")}>
<header class="status-lane-head">
<section class="status-lane unstaged-lane" tabindex="-1" onpointerdown={focusStatusLane} aria-label={t("status.laneUnstaged")}>
<header class="status-lane-head" title={t("status.selectAllHint")}>
<div class="status-lane-title">
<div class="status-lane-copy"><strong>{t("status.unstagedTitle")}</strong><small>{t("status.workingTree")}</small></div>
<span class="status-lane-count">{unstagedCount}</span>
@@ -516,8 +552,8 @@
<span><ArrowRight size={12} /></span>
</div>
<section class="status-lane staged-lane" aria-label={t("status.laneStaged")}>
<header class="status-lane-head">
<section class="status-lane staged-lane" tabindex="-1" onpointerdown={focusStatusLane} aria-label={t("status.laneStaged")}>
<header class="status-lane-head" title={t("status.selectAllHint")}>
<div class="status-lane-title">
<div class="status-lane-copy"><strong>{t("status.stagedTitle")}</strong><small>{t("status.nextCommit")}</small></div>
<span class="status-lane-count">{stagedCount}</span>
+1
View File
@@ -206,6 +206,7 @@ export const messages = {
"status.stagedTitle": { en: "Staged", de: "Gestaged" },
"status.workingTree": { en: "Working tree", de: "Arbeitsverzeichnis" },
"status.nextCommit": { en: "Next commit", de: "Nächster Commit" },
"status.selectAllHint": { en: "Ctrl + A selects every file in this list, Esc clears the selection", de: "Strg + A wählt alle Dateien in dieser Liste aus, Esc hebt die Auswahl auf" },
"status.stageSelected": { en: "Stage {count} selected files", de: "{count} ausgewählte Dateien stagen" },
"status.unstageSelected": { en: "Unstage {count} selected files", de: "{count} ausgewählte Dateien entstagen" },
"status.stageAllHint": { en: "Stage all unstaged files", de: "Alle ungestagten Dateien stagen" },