Add embedded PTY terminal, keyboard shortcuts, and assignee/label integrations #53
@@ -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),
|
linear-gradient(180deg, color-mix(in srgb, var(--color-success) 2.5%, transparent), transparent 120px),
|
||||||
var(--color-surface-solid);
|
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 {
|
.status-flow-divider {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
@@ -249,7 +249,8 @@
|
|||||||
summary: "Die Hilfe ist überall erreichbar. Dialoge lassen sich konsistent schließen und Suchfelder direkt fokussieren.",
|
summary: "Die Hilfe ist überall erreichbar. Dialoge lassen sich konsistent schließen und Suchfelder direkt fokussieren.",
|
||||||
commands: [
|
commands: [
|
||||||
{ command: "Ctrl + /", description: "Diese Hilfe öffnen" },
|
{ 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: "Tab / Shift + Tab", description: "Zwischen Bedienelementen wechseln" },
|
||||||
{ command: "Enter / Leertaste", description: "Fokussierte Aktion ausführen" },
|
{ 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.",
|
summary: "Help is available everywhere. Dialogs close consistently and search fields receive focus automatically.",
|
||||||
commands: [
|
commands: [
|
||||||
{ command: "Ctrl + /", description: "Open this help center" },
|
{ 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: "Tab / Shift + Tab", description: "Move between controls" },
|
||||||
{ command: "Enter / Space", description: "Activate the focused control" },
|
{ command: "Enter / Space", description: "Activate the focused control" },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -323,8 +323,44 @@
|
|||||||
onStopTracking(targets, target.kind);
|
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) {
|
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 {
|
function isStatusSelected(file: GitFileStatus): boolean {
|
||||||
@@ -459,8 +495,8 @@
|
|||||||
<div class="blank-state">{t("status.noChanges")}</div>
|
<div class="blank-state">{t("status.noChanges")}</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="status-lanes">
|
<div class="status-lanes">
|
||||||
<section class="status-lane unstaged-lane" aria-label={t("status.laneUnstaged")}>
|
<section class="status-lane unstaged-lane" tabindex="-1" onpointerdown={focusStatusLane} aria-label={t("status.laneUnstaged")}>
|
||||||
<header class="status-lane-head">
|
<header class="status-lane-head" title={t("status.selectAllHint")}>
|
||||||
<div class="status-lane-title">
|
<div class="status-lane-title">
|
||||||
<div class="status-lane-copy"><strong>{t("status.unstagedTitle")}</strong><small>{t("status.workingTree")}</small></div>
|
<div class="status-lane-copy"><strong>{t("status.unstagedTitle")}</strong><small>{t("status.workingTree")}</small></div>
|
||||||
<span class="status-lane-count">{unstagedCount}</span>
|
<span class="status-lane-count">{unstagedCount}</span>
|
||||||
@@ -516,8 +552,8 @@
|
|||||||
<span><ArrowRight size={12} /></span>
|
<span><ArrowRight size={12} /></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="status-lane staged-lane" aria-label={t("status.laneStaged")}>
|
<section class="status-lane staged-lane" tabindex="-1" onpointerdown={focusStatusLane} aria-label={t("status.laneStaged")}>
|
||||||
<header class="status-lane-head">
|
<header class="status-lane-head" title={t("status.selectAllHint")}>
|
||||||
<div class="status-lane-title">
|
<div class="status-lane-title">
|
||||||
<div class="status-lane-copy"><strong>{t("status.stagedTitle")}</strong><small>{t("status.nextCommit")}</small></div>
|
<div class="status-lane-copy"><strong>{t("status.stagedTitle")}</strong><small>{t("status.nextCommit")}</small></div>
|
||||||
<span class="status-lane-count">{stagedCount}</span>
|
<span class="status-lane-count">{stagedCount}</span>
|
||||||
|
|||||||
@@ -206,6 +206,7 @@ export const messages = {
|
|||||||
"status.stagedTitle": { en: "Staged", de: "Gestaged" },
|
"status.stagedTitle": { en: "Staged", de: "Gestaged" },
|
||||||
"status.workingTree": { en: "Working tree", de: "Arbeitsverzeichnis" },
|
"status.workingTree": { en: "Working tree", de: "Arbeitsverzeichnis" },
|
||||||
"status.nextCommit": { en: "Next commit", de: "Nächster Commit" },
|
"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.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.unstageSelected": { en: "Unstage {count} selected files", de: "{count} ausgewählte Dateien entstagen" },
|
||||||
"status.stageAllHint": { en: "Stage all unstaged files", de: "Alle ungestagten Dateien stagen" },
|
"status.stageAllHint": { en: "Stage all unstaged files", de: "Alle ungestagten Dateien stagen" },
|
||||||
|
|||||||
Reference in New Issue
Block a user