Add a new git::review_cleanup module that implements a CleanupPlan with prepare() and finish() routines to safely remove/clean tracking and local branches after a PR/MR is merged. The cleanup logic validates branch names, ensures a clean worktree, checks remotes/URLs, verifies commits/ancestry, protects against concurrent worktrees or divergent local/remote commits, and performs authenticated fetch/push and ref updates. Unit tests for the cleanup behavior are included. Wire provider-side cleanup into integrations: - add an integrations/cleanup module to read provider PR payloads and derive cleanup inputs - run cleanup::prepare(...) before performing a merge when an optional cleanup_path is provided - after a successful provider merge, run cleanup::finish(...); any failure is reported as MERGE_ACCEPTED_CLEANUP_FAILED Also: - export the new git review_cleanup module (src-tauri/src/git.rs) - accept an optional cleanup_path parameter in run_integration_review_action - remove the previous REVIEW_REQUEST_TIMEOUT wrapper around the spawned blocking task (the integration action is no longer wrapped with the 35s timeout)
774 lines
93 KiB
Svelte
774 lines
93 KiB
Svelte
<script lang="ts">
|
||
import { onMount } from "svelte";
|
||
import type { AiSettings } from "../types";
|
||
import CreateReviewDialog from "./CreateReviewDialog.svelte";
|
||
import ConfirmDialog, { type ConfirmRequest } from "./ConfirmDialog.svelte";
|
||
import { t } from "../i18n.svelte";
|
||
import CommentEditor from "./CommentEditor.svelte";
|
||
import SelectMenu from "./SelectMenu.svelte";
|
||
import { cubicOut } from "svelte/easing";
|
||
import { fly } from "svelte/transition";
|
||
import {
|
||
AlertTriangle, Check, ChevronDown, ChevronRight,
|
||
CircleCheck, Clock3, CircleDotDashed, ExternalLink, GitBranch, GitMerge, GitPullRequest, Inbox,
|
||
LoaderCircle, PanelRightOpen, RefreshCw, RotateCcw, Search, Settings2, X, XCircle,
|
||
} from "@lucide/svelte";
|
||
import { addIntegrationReviewComment, getIntegrationReviewMergeOptions, getIntegrationReviewDetails, listIntegrationReviewRequests, openInBrowser, runIntegrationReviewAction } from "../git";
|
||
import { configuredIntegrationSources, integrationCredentialKey, providerLabel } from "../integrations";
|
||
import type { AppLanguage, GitIntegrationSettings, IntegrationMergeMethod, IntegrationMergeOptions, IntegrationReviewAction, IntegrationReviewRequest, IntegrationReviewState, StoredCredential } from "../types";
|
||
|
||
type LocalResolutionPhase = "idle" | "preparing" | "conflicts" | "ready-to-continue" | "ready-to-push" | "complete" | "error";
|
||
|
||
interface Props {
|
||
aiSettings: AiSettings;
|
||
language: AppLanguage;
|
||
localRepositoryPath?: string;
|
||
integrations: GitIntegrationSettings;
|
||
initialQuery?: string;
|
||
initialSourceId?: string;
|
||
localResolutionRequestId?: string;
|
||
localResolutionPhase?: LocalResolutionPhase;
|
||
localResolutionMessage?: string;
|
||
loadCredential: (key: string) => Promise<StoredCredential | null>;
|
||
onOpenSettings: () => void;
|
||
onCleanupStateChange?: (busy: boolean, path: string) => void | Promise<void>;
|
||
onStartLocalResolution?: (request: IntegrationReviewRequest, sourceId: string) => void | Promise<void>;
|
||
onOpenLocalResolver?: () => void | Promise<void>;
|
||
onContinueLocalResolution?: () => void | Promise<void>;
|
||
onAbortLocalResolution?: () => void | Promise<void>;
|
||
onPushLocalResolution?: () => void | Promise<void>;
|
||
}
|
||
|
||
let { aiSettings, localRepositoryPath = "", language = "en", integrations, initialQuery = "", initialSourceId = "", localResolutionRequestId = "", localResolutionPhase = "idle", localResolutionMessage = "", loadCredential, onOpenSettings = () => {}, onCleanupStateChange = () => {}, onStartLocalResolution = () => {}, onOpenLocalResolver = () => {}, onContinueLocalResolution = () => {}, onAbortLocalResolution = () => {}, onPushLocalResolution = () => {} }: Props = $props();
|
||
let createOpen = $state(false);
|
||
let requests = $state<IntegrationReviewRequest[]>([]);
|
||
let loading = $state(false);
|
||
let errors = $state<Array<{ source: string; message: string }>>([]);
|
||
let query = $state("");
|
||
let stateFilter = $state<IntegrationReviewState>("open");
|
||
let repositoryFilter = $state("");
|
||
let selectedSourceId = $state("");
|
||
let selectedId = $state("");
|
||
let detailOpen = $state(false);
|
||
let detailPanel = $state<HTMLElement>();
|
||
let actionMenuId = $state("");
|
||
let actionBusyId = $state("");
|
||
let actionNotice = $state("");
|
||
let detailLoadingId = $state("");
|
||
let commentDraft = $state("");
|
||
let commentPosting = $state(false);
|
||
let collapsedRepositories = $state(new Set<string>());
|
||
let loadGeneration = 0;
|
||
let loadedStates = $state(new Set<"open" | "merged" | "closed">());
|
||
let completedResolutionRefreshKey = "";
|
||
|
||
const SOURCE_TIMEOUT_MS = 46_000;
|
||
const de = $derived(language === "de");
|
||
const sources = $derived(configuredIntegrationSources(integrations));
|
||
const activeSource = $derived(sources.find((source) => source.id === selectedSourceId) ?? null);
|
||
const normalizedQuery = $derived(query.trim().toLocaleLowerCase());
|
||
const filtered = $derived(requests.filter((request) => {
|
||
const haystack = `${request.title} ${request.repositoryName} ${request.author} ${request.number} ${request.sourceBranch} ${request.targetBranch}`.toLocaleLowerCase();
|
||
return request.state === stateFilter
|
||
&& (!repositoryFilter || request.repositoryName === repositoryFilter)
|
||
&& (!normalizedQuery || haystack.includes(normalizedQuery));
|
||
}));
|
||
|
||
/**
|
||
* Repositories of the requests loaded for the current state tab, grouped by
|
||
* owner and carrying the number of requests as the right-hand meta text.
|
||
*/
|
||
const repositoryOptions = $derived.by(() => {
|
||
const counts = new Map<string, number>();
|
||
for (const request of requests) {
|
||
if (request.state !== stateFilter || !request.repositoryName) continue;
|
||
counts.set(request.repositoryName, (counts.get(request.repositoryName) ?? 0) + 1);
|
||
}
|
||
|
||
const entries = [...counts.entries()]
|
||
.sort(([left], [right]) => left.localeCompare(right, undefined, { sensitivity: "base" }))
|
||
.map(([name, count]) => {
|
||
const separator = name.lastIndexOf("/");
|
||
return {
|
||
value: name,
|
||
label: separator > 0 ? name.slice(separator + 1) : name,
|
||
group: separator > 0 ? name.slice(0, separator) : (activeSource?.label ?? ""),
|
||
meta: String(count),
|
||
};
|
||
});
|
||
|
||
entries.sort((left, right) => left.group.localeCompare(right.group, undefined, { sensitivity: "base" })
|
||
|| left.label.localeCompare(right.label, undefined, { sensitivity: "base" }));
|
||
|
||
return [
|
||
{ value: "", label: de ? "Alle Repositories" : "All repositories", meta: String(counts.size) },
|
||
...entries,
|
||
];
|
||
});
|
||
|
||
$effect(() => {
|
||
// Drop the filter as soon as the chosen repository is no longer in the list.
|
||
if (repositoryFilter && !repositoryOptions.some((option) => option.value === repositoryFilter)) {
|
||
repositoryFilter = "";
|
||
}
|
||
});
|
||
const groupedRequests = $derived.by(() => {
|
||
const groups = new Map<string, IntegrationReviewRequest[]>();
|
||
for (const request of filtered) {
|
||
const key = request.repositoryName || providerLabel(request.provider);
|
||
groups.set(key, [...(groups.get(key) ?? []), request]);
|
||
}
|
||
return [...groups.entries()].map(([name, items]) => ({ name, items }));
|
||
});
|
||
const selected = $derived(requests.find((request) => request.id === selectedId) ?? null);
|
||
const backendRestartRequired = $derived(errors.some((error) => /command.*not found|unknown command|not registered/i.test(error.message)));
|
||
|
||
onMount(() => {
|
||
const closeActionMenu = () => { actionMenuId = ""; };
|
||
const closeDetailsOutside = (event: PointerEvent) => {
|
||
if (detailOpen && detailPanel && event.target instanceof Node && !detailPanel.contains(event.target)) {
|
||
detailOpen = false;
|
||
}
|
||
};
|
||
window.addEventListener("click", closeActionMenu);
|
||
window.addEventListener("pointerdown", closeDetailsOutside, true);
|
||
query = initialQuery;
|
||
const source = sources.find((candidate) => candidate.id === initialSourceId) ?? sources[0];
|
||
if (source) {
|
||
selectedSourceId = source.id;
|
||
void loadRequests("open", source.id);
|
||
}
|
||
return () => {
|
||
window.removeEventListener("click", closeActionMenu);
|
||
window.removeEventListener("pointerdown", closeDetailsOutside, true);
|
||
};
|
||
});
|
||
|
||
$effect(() => {
|
||
const key = localResolutionPhase === "complete" && localResolutionRequestId && selectedSourceId
|
||
? `${selectedSourceId}:${localResolutionRequestId}`
|
||
: "";
|
||
if (!key || key === completedResolutionRefreshKey) return;
|
||
completedResolutionRefreshKey = key;
|
||
void refreshCompletedResolution(key, localResolutionRequestId, selectedSourceId);
|
||
});
|
||
|
||
function withTimeout<T>(promise: Promise<T>, source: string, timeoutMs = SOURCE_TIMEOUT_MS): Promise<T> {
|
||
return new Promise<T>((resolve, reject) => {
|
||
const seconds = Math.round(timeoutMs / 1_000);
|
||
const timer = window.setTimeout(() => reject(new Error(de ? `${source} hat nach ${seconds} Sekunden nicht geantwortet.` : `${source} did not respond within ${seconds} seconds.`)), timeoutMs);
|
||
promise.then((value) => { window.clearTimeout(timer); resolve(value); }, (error) => { window.clearTimeout(timer); reject(error); });
|
||
});
|
||
}
|
||
|
||
async function loadRequests(state: "open" | "merged" | "closed" = stateFilter === "draft" ? "open" : stateFilter, sourceId = selectedSourceId) {
|
||
const source = sources.find((candidate) => candidate.id === sourceId);
|
||
if (!source) return;
|
||
const generation = ++loadGeneration;
|
||
loading = true;
|
||
errors = [];
|
||
requests = requests.filter((request) => state === "open" ? request.state !== "open" && request.state !== "draft" : request.state !== state);
|
||
selectedId = "";
|
||
detailOpen = false;
|
||
actionMenuId = "";
|
||
actionNotice = "";
|
||
try {
|
||
const credential = await withTimeout(loadCredential(integrationCredentialKey(source.provider, source.accountId)), `${source.label} keychain`, 15_000);
|
||
if (!credential?.password) throw new Error(de ? "Kein Token für diese Integration gespeichert." : "No token is stored for this integration.");
|
||
const loaded = await withTimeout(listIntegrationReviewRequests(source.provider, source.baseUrl, credential.username, credential.password, state), source.label);
|
||
if (generation !== loadGeneration) return;
|
||
requests = [...requests, ...loaded].sort((left, right) => Date.parse(right.updatedAt || right.createdAt) - Date.parse(left.updatedAt || left.createdAt));
|
||
loadedStates = new Set([...loadedStates, state]);
|
||
const missingBranches = loaded.filter((request) => !request.sourceBranch || !request.targetBranch);
|
||
if (missingBranches.length) void enrichMissingBranches(missingBranches, source, credential, generation);
|
||
} catch (error) {
|
||
if (generation !== loadGeneration) return;
|
||
errors = [{ source: source.label, message: error instanceof Error ? error.message : String(error) }];
|
||
} finally {
|
||
if (generation === loadGeneration) loading = false;
|
||
}
|
||
}
|
||
|
||
async function enrichMissingBranches(items: IntegrationReviewRequest[], source: (typeof sources)[number], credential: StoredCredential, generation: number) {
|
||
const queue = [...items];
|
||
const worker = async () => {
|
||
while (queue.length && generation === loadGeneration) {
|
||
const request = queue.shift();
|
||
if (!request) return;
|
||
try {
|
||
const details = await withTimeout(getIntegrationReviewDetails(source.provider, source.baseUrl, credential.username, credential.password, request), source.label, 20_000);
|
||
if (generation !== loadGeneration) return;
|
||
requests = requests.map((item) => item.id === details.id ? details : item);
|
||
} catch {
|
||
// Keep the request visible; selecting it retries the detail request and surfaces the error.
|
||
}
|
||
}
|
||
};
|
||
await Promise.all(Array.from({ length: Math.min(4, queue.length) }, worker));
|
||
}
|
||
|
||
function selectSource(sourceId: string) {
|
||
if (sourceId === selectedSourceId) return;
|
||
selectedSourceId = sourceId;
|
||
stateFilter = "open";
|
||
requests = [];
|
||
errors = [];
|
||
selectedId = "";
|
||
detailOpen = false;
|
||
actionMenuId = "";
|
||
collapsedRepositories = new Set();
|
||
loadedStates = new Set();
|
||
void loadRequests("open", sourceId);
|
||
}
|
||
|
||
function selectState(state: IntegrationReviewState) {
|
||
stateFilter = state;
|
||
selectedId = "";
|
||
detailOpen = false;
|
||
const apiState = state === "draft" ? "open" : state;
|
||
if (!loadedStates.has(apiState)) void loadRequests(apiState);
|
||
}
|
||
|
||
function count(state: IntegrationReviewState): number {
|
||
return requests.filter((request) => request.state === state).length;
|
||
}
|
||
|
||
function countLabel(state: IntegrationReviewState): string {
|
||
return loadedStates.has(state === "draft" ? "open" : state) ? String(count(state)) : "";
|
||
}
|
||
|
||
function stateLabel(state: IntegrationReviewState): string {
|
||
if (state === "draft") return "Draft";
|
||
if (state === "merged") return de ? "Zusammengeführt" : "Merged";
|
||
if (state === "closed") return de ? "Geschlossen" : "Closed";
|
||
return "Open";
|
||
}
|
||
|
||
function formatDate(value: string): string {
|
||
if (!value) return "–";
|
||
const date = new Date(value);
|
||
if (Number.isNaN(date.getTime())) return "–";
|
||
return new Intl.DateTimeFormat(de ? "de-DE" : "en-US", { dateStyle: "medium", timeStyle: "short" }).format(date);
|
||
}
|
||
|
||
function formatRelativeDate(value: string): string {
|
||
if (!value) return "–";
|
||
const timestamp = Date.parse(value);
|
||
if (Number.isNaN(timestamp)) return "–";
|
||
const minutes = Math.max(0, Math.round((Date.now() - timestamp) / 60_000));
|
||
if (minutes < 1) return de ? "gerade eben" : "just now";
|
||
if (minutes < 60) return de ? `vor ${minutes} Min.` : `${minutes}m ago`;
|
||
const hours = Math.round(minutes / 60);
|
||
if (hours < 24) return de ? `vor ${hours} Std.` : `${hours}h ago`;
|
||
const days = Math.round(hours / 24);
|
||
return de ? `vor ${days} T.` : `${days}d ago`;
|
||
}
|
||
|
||
function initials(name: string): string {
|
||
const parts = name.trim().split(/\s+/).filter(Boolean);
|
||
if (!parts.length) return "?";
|
||
return (parts.length > 1 ? parts[0][0] + parts[parts.length - 1][0] : parts[0].slice(0, 2)).toLocaleUpperCase();
|
||
}
|
||
|
||
function actionLabel(provider: IntegrationReviewRequest["provider"]): string {
|
||
return provider === "azure-devops" ? "Open in Azure" : `Open in ${providerLabel(provider)}`;
|
||
}
|
||
|
||
function requestTypeLabel(provider: IntegrationReviewRequest["provider"]): string {
|
||
return provider === "gitlab" || provider === "gitlab-self-hosted" ? "Merge Request" : "Pull Request";
|
||
}
|
||
|
||
function toggleRepository(name: string) {
|
||
const next = new Set(collapsedRepositories);
|
||
if (next.has(name)) next.delete(name);
|
||
else next.add(name);
|
||
collapsedRepositories = next;
|
||
}
|
||
|
||
function selectRequest(request: IntegrationReviewRequest, showDetail = false) {
|
||
if (selectedId !== request.id) commentDraft = "";
|
||
selectedId = request.id;
|
||
if (showDetail) detailOpen = true;
|
||
if (request.changedFiles === null && detailLoadingId !== request.id) void loadDetails(request);
|
||
}
|
||
|
||
function openDetail(request: IntegrationReviewRequest) {
|
||
selectRequest(request, true);
|
||
}
|
||
|
||
async function loadDetails(request: IntegrationReviewRequest) {
|
||
const source = activeSource;
|
||
if (!source) return;
|
||
detailLoadingId = request.id;
|
||
try {
|
||
const credential = await withTimeout(loadCredential(integrationCredentialKey(source.provider, source.accountId)), `${source.label} keychain`, 15_000);
|
||
if (!credential?.password) return;
|
||
const details = await withTimeout(getIntegrationReviewDetails(source.provider, source.baseUrl, credential.username, credential.password, request), source.label);
|
||
requests = requests.map((item) => item.id === details.id ? details : item);
|
||
} catch (error) {
|
||
errors = [{ source: source.label, message: error instanceof Error ? error.message : String(error) }];
|
||
} finally { detailLoadingId = ""; }
|
||
}
|
||
|
||
async function refreshCompletedResolution(key: string, requestId: string, sourceId: string) {
|
||
const source = sources.find((candidate) => candidate.id === sourceId);
|
||
const initialRequest = requests.find((candidate) => candidate.id === requestId);
|
||
if (!source || !initialRequest) return;
|
||
let request: IntegrationReviewRequest = initialRequest;
|
||
actionNotice = de ? "Branch wurde gepusht. Der Merge-Status wird geprüft …" : "Branch pushed. Checking the merge status …";
|
||
try {
|
||
const credential = await withTimeout(loadCredential(integrationCredentialKey(source.provider, source.accountId)), `${source.label} keychain`, 15_000);
|
||
if (!credential?.password) return;
|
||
for (const delay of [700, 1_500, 3_000, 5_000]) {
|
||
await new Promise((resolve) => window.setTimeout(resolve, delay));
|
||
if (completedResolutionRefreshKey !== key || selectedSourceId !== sourceId) return;
|
||
const details: IntegrationReviewRequest = await withTimeout(getIntegrationReviewDetails(source.provider, source.baseUrl, credential.username, credential.password, request), source.label, 20_000);
|
||
requests = requests.map((candidate) => candidate.id === details.id ? details : candidate);
|
||
request = details;
|
||
if (details.mergeStatus === "mergeable") {
|
||
actionNotice = de ? "Der Konflikt ist gelöst. Der Request kann jetzt zusammengeführt werden." : "The conflict is resolved. The request can now be merged.";
|
||
return;
|
||
}
|
||
if (details.mergeStatus !== "conflicts" && details.mergeStatus !== "checking") return;
|
||
}
|
||
actionNotice = de ? "Der Anbieter prüft den neuen Branch noch. Aktualisiere den Status in einigen Sekunden erneut." : "The provider is still checking the updated branch. Refresh the status again in a few seconds.";
|
||
} catch (error) {
|
||
errors = [{ source: source.label, message: error instanceof Error ? error.message : String(error) }];
|
||
}
|
||
}
|
||
|
||
function hasConflicts(request: IntegrationReviewRequest): boolean { return request.mergeStatus === "conflicts"; }
|
||
|
||
function isLocalResolutionActive(request: IntegrationReviewRequest): boolean {
|
||
return request.id === localResolutionRequestId && localResolutionPhase !== "idle";
|
||
}
|
||
|
||
function isWaitingForResolvedStatus(request: IntegrationReviewRequest): boolean {
|
||
return isLocalResolutionActive(request) && localResolutionPhase === "complete" && hasConflicts(request);
|
||
}
|
||
|
||
async function startLocalResolution(request: IntegrationReviewRequest) {
|
||
const source = activeSource;
|
||
if (!source || !request.sourceBranch || !request.targetBranch) return;
|
||
actionMenuId = "";
|
||
selectedId = request.id;
|
||
detailOpen = true;
|
||
await onStartLocalResolution(request, source.id);
|
||
}
|
||
|
||
async function runPrimaryAction(request: IntegrationReviewRequest) {
|
||
if (isWaitingForResolvedStatus(request)) return;
|
||
if (hasConflicts(request)) await startLocalResolution(request);
|
||
else if (request.state === "open" || request.state === "draft") await performReviewAction(request, "merge");
|
||
else if (request.state === "closed") await performReviewAction(request, "reopen");
|
||
else await openRequest(request);
|
||
}
|
||
|
||
function primaryActionLabel(request: IntegrationReviewRequest): string {
|
||
if (isWaitingForResolvedStatus(request)) return de ? "Status wird geprüft" : "Checking status";
|
||
if (hasConflicts(request)) return de ? "Konflikt lösen" : "Resolve conflict";
|
||
if (request.state === "open" || request.state === "draft") return de ? "Zusammenführen" : "Merge";
|
||
if (request.state === "closed") return de ? "Wieder öffnen" : "Reopen";
|
||
return actionLabel(request.provider);
|
||
}
|
||
|
||
async function openRequest(request: IntegrationReviewRequest | null = selected) {
|
||
if (!request?.webUrl) return;
|
||
try { await openInBrowser(request.webUrl); }
|
||
catch (error) { errors = [{ source: providerLabel(request.provider), message: error instanceof Error ? error.message : String(error) }]; }
|
||
}
|
||
|
||
function reviewActionLabel(action: IntegrationReviewAction): string {
|
||
if (action === "merge") return de ? "Zusammenführen" : "Merge";
|
||
if (action === "approve") return de ? "Freigeben" : "Approve";
|
||
if (action === "close") return de ? "Request schließen" : "Close request";
|
||
return de ? "Request wieder öffnen" : "Reopen request";
|
||
}
|
||
|
||
function mergeMethodLabel(method: IntegrationMergeMethod, request: IntegrationReviewRequest): string {
|
||
if (request.provider.startsWith("gitlab")) {
|
||
if (method === "merge") return de ? "Ohne Squash zusammenführen" : "Merge without squashing";
|
||
if (method === "squash") return de ? "Mit Squash zusammenführen" : "Squash and merge";
|
||
}
|
||
switch (method) {
|
||
case "merge": return de ? "Merge-Commit erstellen" : "Create merge commit";
|
||
case "rebase": return de ? "Rebase, dann Fast-forward" : "Rebase, then fast-forward";
|
||
case "rebase-merge": return de ? "Rebase, dann Merge-Commit erstellen" : "Rebase, then create merge commit";
|
||
case "squash": return de ? "Squash-Commit erstellen" : "Create squash commit";
|
||
case "fast-forward-only": return de ? "Nur Fast-forward" : "Fast-forward only";
|
||
default: return de ? "Projektstandard verwenden" : "Use project default";
|
||
}
|
||
}
|
||
|
||
let reviewConfirmRequest = $state<ConfirmRequest | null>(null);
|
||
let reviewConfirmResolve: ((result: { confirmed: boolean; method?: IntegrationMergeMethod; deleteBranch?: boolean }) => void) | null = null;
|
||
|
||
function askReviewConfirmation(request: IntegrationReviewRequest, action: IntegrationReviewAction, mergeOptions?: IntegrationMergeOptions): Promise<{ confirmed: boolean; method?: IntegrationMergeMethod; deleteBranch?: boolean }> {
|
||
const values = { number: request.number };
|
||
reviewConfirmRequest = action === "merge"
|
||
? { title: t("confirm.review.mergeTitle", values), message: t("confirm.review.mergeMessage"), items: [request.title], confirmLabel: t("confirm.review.mergeAction"), danger: false,
|
||
checkbox: { label: de ? "Quellbranch remote und lokal löschen" : "Delete source branch remotely and locally", note: de ? `Nach dem Merge zu „${request.targetBranch}“ wechseln und „${request.sourceBranch}“ löschen. Ein passendes lokales Repository ohne ungesicherte Änderungen muss geöffnet sein.` : `After merging, switch to “${request.targetBranch}” and delete “${request.sourceBranch}”. Open the matching local repository with a clean working tree first.`, defaultChecked: false },
|
||
select: mergeOptions ? { label: de ? "Merge-Methode" : "Merge method", value: mergeOptions.defaultMethod, options: mergeOptions.methods.map(method => ({ value: method, label: mergeMethodLabel(method, request) })) } : undefined }
|
||
: action === "close"
|
||
? { title: t("confirm.review.closeTitle", values), message: t("confirm.review.closeMessage"), items: [request.title], confirmLabel: t("confirm.review.closeAction") }
|
||
: { title: t("confirm.review.reopenTitle", values), message: t("confirm.review.reopenMessage"), items: [request.title], confirmLabel: t("confirm.review.reopenAction"), danger: false };
|
||
|
||
return new Promise<{ confirmed: boolean; method?: IntegrationMergeMethod; deleteBranch?: boolean }>((resolve) => {
|
||
reviewConfirmResolve = resolve;
|
||
});
|
||
}
|
||
|
||
function answerReviewConfirmation(confirmed: boolean, value?: string, checked = false) {
|
||
const method = reviewConfirmRequest?.select?.options.find(option => option.value === value)?.value as IntegrationMergeMethod | undefined;
|
||
const resolve = reviewConfirmResolve;
|
||
reviewConfirmRequest = null;
|
||
reviewConfirmResolve = null;
|
||
resolve?.({ confirmed, method, deleteBranch: checked });
|
||
}
|
||
|
||
async function performReviewAction(request: IntegrationReviewRequest, action: IntegrationReviewAction) {
|
||
const source = activeSource;
|
||
if (!source || actionBusyId) return;
|
||
actionMenuId = "";
|
||
actionNotice = "";
|
||
actionBusyId = request.id;
|
||
errors = [];
|
||
let cleanupPath: string | undefined;
|
||
try {
|
||
const credential = await withTimeout(loadCredential(integrationCredentialKey(source.provider, source.accountId)), `${source.label} keychain`, 15_000);
|
||
if (!credential?.password) throw new Error(de ? "Kein Token für diese Integration gespeichert." : "No token is stored for this integration.");
|
||
let mergeMethod: IntegrationMergeMethod | undefined;
|
||
if (action !== "approve") {
|
||
const options = action === "merge" ? await withTimeout(getIntegrationReviewMergeOptions(source.provider, source.baseUrl, credential.password, request), source.label) : undefined;
|
||
if (options && options.methods.length === 0) throw new Error(de ? "Für dieses Repository ist keine unterstützte Merge-Methode freigegeben." : "No supported merge method is enabled for this repository.");
|
||
const result = await askReviewConfirmation(request, action, options);
|
||
if (!result.confirmed) return;
|
||
mergeMethod = result.method;
|
||
if (action === "merge" && result.deleteBranch) {
|
||
if (!localRepositoryPath) throw new Error(de ? "Öffne zuerst das passende lokale Repository, um den Quellbranch remote und lokal zu löschen." : "Open the matching local repository before deleting the source branch remotely and locally.");
|
||
cleanupPath = localRepositoryPath;
|
||
await onCleanupStateChange(true, cleanupPath);
|
||
}
|
||
}
|
||
let cleanupError = "";
|
||
try {
|
||
// Cleanup can include fetch, checkout and push; do not time out a still-running mutation.
|
||
await runIntegrationReviewAction(source.provider, source.baseUrl, credential.username, credential.password, request, action, mergeMethod, cleanupPath);
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
if (!message.startsWith("MERGE_ACCEPTED_CLEANUP_FAILED:")) throw error;
|
||
cleanupError = (de ? "Merge vom Anbieter angenommen, Branch-Aufräumen nicht abgeschlossen: " : "Merge accepted by the provider, branch cleanup incomplete: ") + message.replace("MERGE_ACCEPTED_CLEANUP_FAILED:", "").trim();
|
||
}
|
||
requests = [];
|
||
loadedStates = new Set();
|
||
await loadRequests(stateFilter === "draft" ? "open" : stateFilter);
|
||
if (cleanupError) errors = [...errors, { source: source.label, message: cleanupError }];
|
||
else actionNotice = cleanupPath
|
||
? (de ? `PR zusammengeführt. Zu „${request.targetBranch}“ gewechselt und „${request.sourceBranch}“ remote und lokal gelöscht.` : `PR merged. Switched to “${request.targetBranch}” and deleted “${request.sourceBranch}” remotely and locally.`)
|
||
: (de ? `${reviewActionLabel(action)} erfolgreich.` : `${reviewActionLabel(action)} succeeded.`);
|
||
} catch (error) {
|
||
errors = [{ source: source.label, message: error instanceof Error ? error.message : String(error) }];
|
||
} finally {
|
||
if (cleanupPath) {
|
||
try { await onCleanupStateChange(false, cleanupPath); }
|
||
catch (error) { errors = [...errors, { source: source.label, message: String(error) }]; }
|
||
}
|
||
actionBusyId = "";
|
||
}
|
||
}
|
||
|
||
async function submitComment() {
|
||
const request = selected;
|
||
const source = activeSource;
|
||
const body = commentDraft.trim();
|
||
if (!request || !source || !body || commentPosting) return;
|
||
commentPosting = true;
|
||
errors = [];
|
||
try {
|
||
const credential = await withTimeout(loadCredential(integrationCredentialKey(source.provider, source.accountId)), `${source.label} keychain`, 15_000);
|
||
if (!credential?.password) throw new Error(de ? "Kein Token für diese Integration gespeichert." : "No token is stored for this integration.");
|
||
await withTimeout(addIntegrationReviewComment(source.provider, source.baseUrl, credential.username, credential.password, request, body), source.label);
|
||
commentDraft = "";
|
||
await loadDetails(request);
|
||
} catch (error) {
|
||
errors = [{ source: source.label, message: error instanceof Error ? error.message : String(error) }];
|
||
} finally { commentPosting = false; }
|
||
}
|
||
</script>
|
||
|
||
{#if createOpen && activeSource}
|
||
<CreateReviewDialog {aiSettings} {localRepositoryPath} source={activeSource} {de} {loadCredential} onClose={() => createOpen = false} onCreated={(request) => {
|
||
++loadGeneration;
|
||
loading = false;
|
||
requests = [request, ...requests.filter(item => item.id !== request.id)];
|
||
stateFilter = request.state;
|
||
query = "";
|
||
collapsedRepositories = new Set();
|
||
errors = [];
|
||
createOpen = false;
|
||
selectRequest(request, true);
|
||
}}/>
|
||
{/if}
|
||
|
||
<section class:has-inspector={detailOpen && !!selected} class="review-center" aria-label="Review Center">
|
||
<header class="review-header page-header">
|
||
<div class="review-heading page-heading"><GitPullRequest size={17} /><h1>Review Center</h1></div>
|
||
{#if activeSource}<button class="create-review" type="button" disabled={loading} onclick={() => { detailOpen = false; createOpen = true; }}><GitPullRequest size={14}/>{activeSource.provider.startsWith("gitlab") ? (de ? "MR erstellen" : "Create MR") : (de ? "PR erstellen" : "Create PR")}</button>{/if}
|
||
</header>
|
||
|
||
{#if sources.length === 0}
|
||
<div class="empty-state"><div class="empty-icon"><GitPullRequest size={23} /></div><h2>{de ? "Keine Integration eingerichtet" : "No integration configured"}</h2><p>{de ? "Verbinde GitHub, GitLab, Gitea oder Azure DevOps, um Pull- und Merge-Requests hier zu sehen." : "Connect GitHub, GitLab, Gitea, or Azure DevOps to see pull and merge requests here."}</p><button class="primary" type="button" onclick={onOpenSettings}><Settings2 size={14} />{de ? "Integrationen öffnen" : "Open integrations"}</button></div>
|
||
{:else}
|
||
<div class="review-toolbar">
|
||
<nav class="state-tabs" aria-label={de ? "Request-Status" : "Request status"}>
|
||
{#each ["open", "draft", "merged", "closed"] as state}
|
||
<button class:active={stateFilter === state} type="button" onclick={() => selectState(state as IntegrationReviewState)}>{state === "draft" ? (de ? "Entwürfe" : "Drafts") : stateLabel(state as IntegrationReviewState)}{#if countLabel(state as IntegrationReviewState)}<span>{countLabel(state as IntegrationReviewState)}</span>{/if}</button>
|
||
{/each}
|
||
</nav>
|
||
<label class="review-search"><Search size={14} /><input bind:value={query} placeholder={de ? "Requests durchsuchen …" : "Search requests …"} /></label>
|
||
<div class="repository-picker">
|
||
<SelectMenu
|
||
value={repositoryFilter}
|
||
options={repositoryOptions}
|
||
placeholder={de ? "Alle Repositories" : "All repositories"}
|
||
searchable={repositoryOptions.length > 8}
|
||
searchPlaceholder={de ? "Repository suchen …" : "Search repository …"}
|
||
emptyText={de ? "Kein passendes Repository" : "No matching repository"}
|
||
ariaLabel={de ? "Repository filtern" : "Filter repository"}
|
||
onChange={(value) => { repositoryFilter = value; selectedId = ""; }}
|
||
>
|
||
{#snippet optionIcon()}<GitBranch size={14} aria-hidden="true" />{/snippet}
|
||
</SelectMenu>
|
||
</div>
|
||
<div class="integration-picker">
|
||
<SelectMenu value={selectedSourceId} options={sources.map(source => ({ value: source.id, label: source.label }))} ariaLabel={de ? "Integration auswählen" : "Select integration"} onChange={selectSource} />
|
||
</div>
|
||
<div class="group-actions">
|
||
<button class="icon-button" title={de ? "Aktualisieren" : "Refresh"} aria-label={de ? "Aktualisieren" : "Refresh"} type="button" onclick={() => loadRequests()} disabled={loading}><RefreshCw class={loading ? "spin" : ""} size={14} /></button>
|
||
</div>
|
||
</div>
|
||
|
||
{#if actionNotice}<div class="action-notice"><Check size={13} />{actionNotice}</div>{/if}
|
||
|
||
{#if errors.length > 0}
|
||
<details class="source-warning" open>
|
||
<summary><AlertTriangle size={14} /><span>{backendRestartRequired ? (de ? "Gitty muss neu gestartet werden, damit das Review-Backend geladen wird." : "Restart Gitty to load the Review Center backend.") : (de ? "Die ausgewählte Integration konnte nicht geladen werden." : "The selected integration could not be loaded.")}</span></summary>
|
||
<ul>{#each errors as error}<li><strong>{error.source}:</strong> {error.message}</li>{/each}</ul>
|
||
</details>
|
||
{/if}
|
||
|
||
<div class:inspector-open={detailOpen && selected} class="review-layout">
|
||
<main class="table-pane">
|
||
<div class="table-head">
|
||
<span>Status</span><span>Request</span><span>{de ? "Autor" : "Author"}</span><span>{de ? "Prüfer" : "Reviewers"}</span><span>{de ? "Repository / Branch" : "Repository / Branch"}</span><span>{de ? "Aktion" : "Action"}</span>
|
||
</div>
|
||
{#if loading && requests.length === 0}
|
||
<div class="loading-state"><LoaderCircle class="spin" size={17} />{de ? "Requests werden geladen …" : "Loading requests …"}</div>
|
||
{:else if groupedRequests.length === 0}
|
||
<div class="list-empty"><Inbox size={20} /><strong>{de ? "Keine passenden Requests" : "No matching requests"}</strong><span>{de ? "Passe Status oder Suche an." : "Adjust the status or search."}</span></div>
|
||
{:else}
|
||
<div class="request-groups">
|
||
{#each groupedRequests as group (group.name)}
|
||
<section class="request-group">
|
||
<button class="group-header" type="button" onclick={() => toggleRepository(group.name)}>
|
||
{#if collapsedRepositories.has(group.name)}<ChevronRight size={14} />{:else}<ChevronDown size={14} />{/if}
|
||
<GitBranch size={13} /><strong>{group.name}</strong><span>{group.items.length}</span>
|
||
</button>
|
||
{#if !collapsedRepositories.has(group.name)}
|
||
{#each group.items as request (request.id)}
|
||
<div class:selected={selectedId === request.id} class="request-row" role="button" tabindex="0" onclick={() => selectRequest(request)} onkeydown={(event) => { if (event.key === "Enter") selectRequest(request); }}>
|
||
<span class="request-status" class:open={request.state === "open"} class:draft={request.state === "draft"} class:merged={request.state === "merged"} class:closed={request.state === "closed"}>
|
||
{#if request.state === "merged"}<GitMerge size={13} />{:else if request.state === "closed"}<XCircle size={13} />{:else if request.state === "draft"}<CircleDotDashed size={13} />{:else}<GitPullRequest size={13} />{/if}
|
||
<span>{stateLabel(request.state)}<small title={formatDate(request.updatedAt || request.createdAt)}>{formatRelativeDate(request.updatedAt || request.createdAt)}</small></span>
|
||
</span>
|
||
<span class="request-title"><span><code>#{request.number}</code><strong title={request.title}>{request.title}</strong></span><small class="change-stats"><b>+{request.additions ?? "–"}</b><i>/</i><em>−{request.deletions ?? "–"}</em>{#if request.changedFiles !== null}<span title={de ? "Geänderte Dateien" : "Changed files"}>{request.changedFiles} {request.changedFiles === 1 ? (de ? "Datei geändert" : "file changed") : (de ? "Dateien geändert" : "files changed")}</span>{:else if detailLoadingId === request.id}<span>{de ? "Lädt …" : "Loading …"}</span>{/if}</small></span>
|
||
<span class="request-author" title={request.author || (de ? "Unbekannt" : "Unknown")} aria-label={request.author || (de ? "Unbekannt" : "Unknown")}><i aria-hidden="true">{initials(request.author)}</i></span>
|
||
<span class="collaborators">{#if request.collaborators.length}{#each request.collaborators.slice(0, 3) as collaborator}<i title={collaborator}>{initials(collaborator)}</i>{/each}{:else}<span>–</span>{/if}</span>
|
||
<span class="repo-branch"><strong>{request.repositoryName}</strong>{#if request.sourceBranch && request.targetBranch}<span class="branch-route"><GitBranch size={11} /><code title={request.sourceBranch}>{request.sourceBranch}</code><b>→</b><code title={request.targetBranch}>{request.targetBranch}</code></span>{:else}<span class="branch-loading"><LoaderCircle class="spin" size={11} />{de ? "Branches werden geladen …" : "Loading branches …"}</span>{/if}</span>
|
||
<span class="row-actions">
|
||
<span class:merge-action={request.state === "open" || request.state === "draft"} class:conflict-action={hasConflicts(request)} class="provider-action">
|
||
<button class="provider-button" class:merge-primary={request.state === "open" || request.state === "draft"} class:conflict={hasConflicts(request)} disabled={!!actionBusyId || isWaitingForResolvedStatus(request) || (hasConflicts(request) && (!request.sourceBranch || !request.targetBranch || (isLocalResolutionActive(request) && localResolutionPhase === "preparing")))} type="button" onclick={(event) => { event.stopPropagation(); void runPrimaryAction(request); }}>{#if actionBusyId === request.id || isWaitingForResolvedStatus(request) || (isLocalResolutionActive(request) && localResolutionPhase === "preparing")}<LoaderCircle class="spin" size={12} />{:else if request.state === "open" || request.state === "draft"}<GitMerge size={12} />{:else}<ExternalLink size={12} />{/if}{primaryActionLabel(request)}</button>
|
||
<button class="action-toggle" class:active={actionMenuId === request.id} type="button" aria-label={de ? "Weitere Aktionen" : "More actions"} onclick={(event) => { event.stopPropagation(); actionMenuId = actionMenuId === request.id ? "" : request.id; }}><ChevronDown size={13} /></button>
|
||
{#if actionMenuId === request.id}
|
||
<span class="action-menu" role="menu" tabindex="-1">
|
||
{#if request.state === "open" || request.state === "draft"}
|
||
{#if isWaitingForResolvedStatus(request)}<button class="menu-merge" type="button" role="menuitem" disabled><LoaderCircle class="spin" size={13} />{de ? "Merge-Status wird geprüft" : "Checking merge status"}</button>{:else if hasConflicts(request)}<button class="menu-conflict" type="button" role="menuitem" disabled={!request.sourceBranch || !request.targetBranch || (isLocalResolutionActive(request) && localResolutionPhase === "preparing")} onclick={() => void startLocalResolution(request)}><GitMerge size={13} />{de ? "Konflikt lokal lösen" : "Resolve conflict locally"}</button>{:else}<button class="menu-merge" type="button" role="menuitem" disabled={actionBusyId === request.id} onclick={() => void performReviewAction(request, "merge")}><GitMerge size={13} />{reviewActionLabel("merge")}</button>{/if}
|
||
<button class="menu-approve" type="button" role="menuitem" disabled={actionBusyId === request.id} onclick={() => void performReviewAction(request, "approve")}><CircleCheck size={14} />{reviewActionLabel("approve")}</button>
|
||
<button class="menu-provider" type="button" role="menuitem" disabled={!request.webUrl} onclick={() => void openRequest(request)}><ExternalLink size={13} />{actionLabel(request.provider)}</button>
|
||
<button class="danger" type="button" role="menuitem" disabled={actionBusyId === request.id} onclick={() => void performReviewAction(request, "close")}><XCircle size={13} />{reviewActionLabel("close")}</button>
|
||
{:else if request.state === "closed"}
|
||
<button type="button" role="menuitem" disabled={actionBusyId === request.id} onclick={() => void performReviewAction(request, "reopen")}><RotateCcw size={13} />{reviewActionLabel("reopen")}</button>
|
||
{/if}
|
||
</span>
|
||
{/if}
|
||
</span>
|
||
<button class="panel-button" class:active={detailOpen && selectedId === request.id} title={de ? "Detailpanel öffnen" : "Open detail panel"} aria-label={de ? "Detailpanel öffnen" : "Open detail panel"} type="button" onclick={(event) => { event.stopPropagation(); openDetail(request); }}><PanelRightOpen size={14} /></button>
|
||
</span>
|
||
</div>
|
||
{/each}
|
||
{/if}
|
||
</section>
|
||
{/each}
|
||
</div>
|
||
{/if}
|
||
</main>
|
||
|
||
{#if detailOpen && selected}
|
||
<aside bind:this={detailPanel} class="detail-panel" transition:fly={{ x: 140, duration: 210, easing: cubicOut }}>
|
||
<header class="detail-header unified-dialog-header">
|
||
<div class="detail-provider unified-dialog-heading"><span class="unified-dialog-icon" aria-hidden="true"><GitPullRequest size={17} /></span><div class="unified-dialog-text"><strong>{providerLabel(selected.provider)} {requestTypeLabel(selected.provider)}</strong></div></div>
|
||
<div class="detail-header-actions"><button class="icon-button" type="button" aria-label={de ? "Im Anbieter öffnen" : "Open in provider"} onclick={() => void openRequest()}><ExternalLink size={16} /></button><button data-dialog-close class="icon-button" type="button" aria-label={de ? "Detailansicht schließen" : "Close details"} onclick={() => { detailOpen = false; }}><X size={17} /></button></div>
|
||
</header>
|
||
<div class="detail-content">
|
||
<main class="detail-main">
|
||
<section class="detail-title"><div class="detail-title-line"><span>#{selected.number}</span><h2>{selected.title}</h2></div><div class="detail-summary"><span class="state-badge" class:open={selected.state === "open"} class:draft={selected.state === "draft"} class:merged={selected.state === "merged"} class:closed={selected.state === "closed"}>{stateLabel(selected.state)}</span><span class="avatar">{initials(selected.author)}</span><strong>{selected.author || (de ? "Unbekannt" : "Unknown")}</strong><span class="summary-separator">·</span><span class="detail-branch-route"><GitBranch size={13} />{#if selected.sourceBranch && selected.targetBranch}<code>{selected.sourceBranch}</code><span>→</span><code>{selected.targetBranch}</code>{:else}<span>{de ? "Branches werden geladen …" : "Loading branches …"}</span>{/if}</span></div></section>
|
||
<div class:conflict={hasConflicts(selected) && !isWaitingForResolvedStatus(selected)} class="merge-summary">{#if detailLoadingId === selected.id || isWaitingForResolvedStatus(selected)}<LoaderCircle class="spin" size={14} /><span>{de ? "Der Anbieter prüft den gelösten Branch …" : "The provider is checking the resolved branch …"}</span>{:else if hasConflicts(selected)}<AlertTriangle size={14} /><strong>{de ? "Dieser Request hat Merge-Konflikte." : "This request has merge conflicts."}</strong>{:else}<CircleCheck size={19} /><span>{de ? "Kann ohne erkannte Konflikte zusammengeführt werden." : "Can be merged without detected conflicts."}</span>{/if}<span class="merge-spacer"></span><small class="detail-change-stats"><b>+{selected.additions ?? "–"}</b><em>−{selected.deletions ?? "–"}</em>{#if selected.changedFiles !== null}<span>{selected.changedFiles} {selected.changedFiles === 1 ? (de ? "Datei geändert" : "file changed") : (de ? "Dateien geändert" : "files changed")}</span>{/if}</small></div>
|
||
{#if hasConflicts(selected) || isLocalResolutionActive(selected)}
|
||
<div class:error={localResolutionPhase === "error"} class:complete={localResolutionPhase === "complete"} class="local-resolution">
|
||
<div>{#if localResolutionPhase === "preparing"}<LoaderCircle class="spin" size={14} />{:else if localResolutionPhase === "complete"}<Check size={14} />{:else}<GitMerge size={14} />{/if}<span>{isLocalResolutionActive(selected) && localResolutionMessage ? localResolutionMessage : (de ? "Löse den Konflikt mit dem lokalen Repository und Gittys Konflikt-Editor." : "Resolve this conflict with the local repository and Gitty's conflict editor.")}</span></div>
|
||
<footer>
|
||
{#if !isLocalResolutionActive(selected) || localResolutionPhase === "idle" || localResolutionPhase === "error"}<button type="button" onclick={() => void startLocalResolution(selected)} disabled={!selected.sourceBranch || !selected.targetBranch}>{de ? "Konflikt lokal lösen" : "Resolve locally"}</button>{/if}
|
||
{#if isLocalResolutionActive(selected) && localResolutionPhase === "conflicts"}<button type="button" onclick={() => void onOpenLocalResolver()}>{de ? "Konflikt-Editor öffnen" : "Open conflict editor"}</button><button class="secondary" type="button" onclick={() => void onAbortLocalResolution()}>{de ? "Abbrechen" : "Abort"}</button>{/if}
|
||
{#if isLocalResolutionActive(selected) && localResolutionPhase === "ready-to-continue"}<button type="button" onclick={() => void onContinueLocalResolution()}>{de ? "Merge abschließen" : "Complete merge"}</button><button class="secondary" type="button" onclick={() => void onAbortLocalResolution()}>{de ? "Abbrechen" : "Abort"}</button>{/if}
|
||
{#if isLocalResolutionActive(selected) && localResolutionPhase === "ready-to-push"}<button type="button" onclick={() => void onPushLocalResolution()}>{de ? "Gelösten Branch pushen" : "Push resolved branch"}</button>{/if}
|
||
</footer>
|
||
</div>
|
||
{/if}
|
||
<section class="description"><h3>{de ? "Beschreibung" : "Description"}</h3><p class:muted={!selected.description}>{selected.description || (de ? "Keine Beschreibung vorhanden." : "No description provided.")}</p></section>
|
||
<section class="comments-section">
|
||
<header><h3>{de ? "Kommentare" : "Comments"}</h3><span>{selected.comments?.length ?? 0}</span></header>
|
||
{#if detailLoadingId === selected.id && !(selected.comments?.length)}<div class="comments-loading"><LoaderCircle class="spin" size={13} />{de ? "Kommentare werden geladen …" : "Loading comments …"}</div>{:else if selected.comments?.length}<div class="comment-list">{#each selected.comments as comment (comment.id)}<article><div class="comment-card"><header><span class="avatar">{initials(comment.author)}</span><strong>{comment.author || (de ? "Unbekannt" : "Unknown")}</strong><time>{formatRelativeDate(comment.createdAt)}</time>{#if comment.author === selected.author}<span class="owner-badge" title={de ? "Autor dieses Requests" : "Author of this request"}>{de ? "Autor" : "Author"}</span>{/if}</header><p>{comment.body}</p></div></article>{/each}</div>{:else}<p class="no-comments">{de ? "Noch keine Kommentare." : "No comments yet."}</p>{/if}
|
||
<div class="review-comment-editor"><CommentEditor bind:value={commentDraft} language={de ? "de" : "en"} busy={commentPosting} onSend={submitComment} /></div>
|
||
</section>
|
||
</main>
|
||
<aside class="detail-sidebar">
|
||
<div class="detail-actions">{#if selected.state === "open" || selected.state === "draft"}{#if isWaitingForResolvedStatus(selected)}<button class="detail-action" type="button" disabled><LoaderCircle class="spin" size={14} />{de ? "Status wird geprüft" : "Checking status"}</button>{:else if hasConflicts(selected)}<button class="detail-action danger-action" type="button" disabled={isLocalResolutionActive(selected) && localResolutionPhase === "preparing"} onclick={() => void startLocalResolution(selected)}><GitMerge size={14} />{de ? "Konflikt lösen" : "Resolve conflict"}</button>{:else}<button class="detail-action primary-action" type="button" disabled={!!actionBusyId} onclick={() => void performReviewAction(selected, "merge")}><GitMerge size={14} />{reviewActionLabel("merge")}</button>{/if}<button class="detail-action" type="button" disabled={!!actionBusyId} onclick={() => void performReviewAction(selected, "approve")}><Check size={14} />{reviewActionLabel("approve")}</button><button class="detail-action danger-action" type="button" disabled={!!actionBusyId} onclick={() => void performReviewAction(selected, "close")}><XCircle size={14} />{reviewActionLabel("close")}</button>{:else if selected.state === "closed"}<button class="detail-action primary-action" type="button" disabled={!!actionBusyId} onclick={() => void performReviewAction(selected, "reopen")}><RotateCcw size={14} />{reviewActionLabel("reopen")}</button>{/if}<button class="detail-action" type="button" onclick={() => void openRequest()} disabled={!selected.webUrl}><ExternalLink size={14} />{actionLabel(selected.provider)}</button></div>
|
||
<section class="people-section"><header><h3>{de ? "Teilnehmer" : "Participants"}</h3></header><div class="people compact"><span class="avatar">{initials(selected.author)}</span><strong>{selected.author || (de ? "Unbekannt" : "Unknown")}</strong></div></section>
|
||
<section class="detail-meta"><h3>Repository</h3><strong><GitBranch size={16} />{selected.repositoryName}</strong></section>
|
||
<section class="detail-meta"><h3>{de ? "Aktualisiert" : "Updated"}</h3><span><Clock3 size={16} />{formatDate(selected.updatedAt || selected.createdAt)}</span></section>
|
||
</aside>
|
||
</div>
|
||
</aside>
|
||
{/if}
|
||
</div>
|
||
{/if}
|
||
</section>
|
||
|
||
<style>
|
||
.create-review{margin-left:auto;display:flex;align-items:center;gap:7px;padding:7px 11px;border:1px solid var(--color-border-subtle);border-radius:7px;background:var(--color-accent-solid);color:var(--color-on-accent);cursor:pointer}.create-review:disabled{opacity:.5;cursor:default}
|
||
.review-center{display:flex;min-height:0;flex:1;flex-direction:column;overflow:hidden;color:var(--color-ink);background:var(--app-bg);font-size:12px}.review-center button,.review-center input{font:inherit}.review-header{display:flex;min-height:48px;align-items:center;padding:0 18px;border-bottom:1px solid var(--color-border-subtle);background:var(--color-surface)}.review-heading{display:flex;align-items:center;gap:9px}.review-heading>:global(svg){color:var(--color-accent)}.review-heading h1{margin:0;font-size:16px;font-weight:650}
|
||
.state-tabs{display:flex;min-height:42px;align-items:stretch;gap:18px;padding:0 18px;border-bottom:1px solid var(--color-border-subtle);background:var(--color-surface)}.state-tabs button{position:relative;display:flex;align-items:center;gap:7px;padding:0 5px;border:0;color:var(--color-ink-muted);background:transparent;font-size:11px}.state-tabs button:hover{color:var(--color-ink)}.state-tabs button.active{color:var(--color-accent)}.state-tabs button.active:after{position:absolute;right:0;bottom:0;left:0;height:2px;background:var(--color-accent);content:""}.state-tabs span,.group-header>span{display:grid;min-width:18px;height:18px;place-items:center;padding:0 5px;border-radius:9px;color:var(--color-ink-muted);background:var(--color-surface-raised);font-size:9.5px}
|
||
.review-toolbar{display:flex;min-height:48px;align-items:center;gap:10px;padding:7px 18px;border-bottom:1px solid var(--color-border-subtle)}.group-actions{display:flex;align-items:center;gap:2px}.group-actions button,.icon-button{display:inline-flex;height:30px;align-items:center;gap:5px;padding:0 7px;border:1px solid transparent;color:var(--color-ink-muted);background:transparent}.group-actions button:hover,.icon-button:hover{border-color:var(--color-border);color:var(--color-ink);background:var(--color-surface-hover)}.group-actions .icon-button{width:30px;justify-content:center;padding:0;border-color:var(--color-border-subtle);margin-left:3px}.review-search{display:flex;min-width:180px;height:30px;align-items:center;gap:7px;flex:1;padding:0 9px;border:1px solid var(--color-border-input);color:var(--color-ink-faint);background:var(--app-input-bg)}.review-search:focus-within{border-color:var(--color-accent)}.review-search input{width:100%;min-width:0;height:100%;padding:0;border:0;outline:0;color:var(--color-ink);background:transparent}
|
||
.source-warning{padding:7px 18px;border-bottom:1px solid color-mix(in srgb,var(--color-warning) 28%,var(--color-border));color:var(--color-warning);background:color-mix(in srgb,var(--color-warning) 6%,var(--app-bg))}.source-warning summary{display:flex;align-items:center;gap:7px;cursor:pointer}.source-warning ul{display:grid;gap:4px;margin:7px 0 2px;padding-left:22px;color:var(--color-ink-muted);font-size:10.5px}.source-warning li strong{color:var(--color-ink)}.action-notice{display:flex;min-height:30px;align-items:center;gap:7px;padding:0 18px;border-bottom:1px solid color-mix(in srgb,var(--color-success) 28%,var(--color-border));color:var(--color-success);background:color-mix(in srgb,var(--color-success) 6%,var(--app-bg));font-size:10.5px}
|
||
.review-layout{display:grid;grid-template-columns:minmax(0,1fr);flex:1;min-height:0}.review-layout.inspector-open{grid-template-columns:minmax(560px,1fr) minmax(360px,38%)}.table-pane{min-width:0;min-height:0;overflow:auto}.table-head,.request-row{display:grid;grid-template-columns:95px minmax(280px,1.5fr) 135px 145px minmax(210px,1fr) 190px;align-items:center}.table-head{position:sticky;z-index:2;top:0;min-height:30px;padding:0 12px;border-bottom:1px solid var(--color-border);color:var(--color-ink-faint);background:var(--color-surface-raised);font-size:9px;font-weight:700;letter-spacing:.035em;text-transform:uppercase}.request-groups{min-width:1075px}.request-group{border-bottom:1px solid var(--color-border-subtle)}.group-header{display:flex;width:100%;height:34px;align-items:center;justify-content:flex-start;gap:7px;padding:0 12px;border:0;border-bottom:1px solid var(--color-border-subtle);color:var(--color-ink);background:var(--color-surface);text-align:left}.group-header:hover{background:var(--color-surface-hover)}.group-header>:global(svg){color:var(--color-ink-faint)}.group-header strong{font-size:11px}.group-header>span{margin-left:2px}.request-row{position:relative;min-height:52px;padding:0 12px;border-bottom:1px solid var(--color-border-subtle);outline:1px solid transparent;outline-offset:-1px;color:var(--color-ink-muted);background:transparent;cursor:default}.request-row:hover{background:var(--color-surface-hover)}.request-row.selected{z-index:1;outline-color:var(--color-accent);background:var(--color-surface-raised)}.request-status{display:flex;align-items:center;gap:6px;font-size:10px}.request-status>span{display:grid;gap:2px}.request-status small{color:var(--color-ink-faint);font-size:9px}.request-title{display:grid;min-width:0;gap:5px;padding-right:14px}.request-title>span{display:flex;min-width:0;gap:8px}.request-title code{color:var(--color-accent);font-size:10px}.request-title strong{overflow:hidden;color:var(--color-ink);font-size:11px;font-weight:560;text-overflow:ellipsis;white-space:nowrap}.change-stats{display:flex;align-items:center;gap:6px;font-size:9.5px}.change-stats b{color:var(--color-success)}.change-stats em{color:var(--color-danger);font-style:normal}.change-stats i{color:var(--color-ink-faint);font-style:normal}.change-stats span{color:var(--color-ink-faint)}.request-author{display:flex;min-width:0;align-items:center;gap:7px;padding-right:10px}.request-author i,.avatar,.collaborators i{display:grid;width:23px;height:23px;flex:0 0 auto;place-items:center;border:1px solid color-mix(in srgb,var(--color-accent) 35%,var(--color-border));border-radius:50%;color:var(--color-ink);background:color-mix(in srgb,var(--color-accent) 45%,var(--color-surface-raised));font-size:9px;font-style:normal;font-weight:750}.collaborators{display:flex;padding-left:3px}.collaborators i+ i{margin-left:-5px}.repo-branch{display:grid;min-width:0;gap:4px}.repo-branch>strong{overflow:hidden;color:var(--color-ink-muted);font-size:10px;text-overflow:ellipsis;white-space:nowrap}.branch-route{display:flex;min-width:0;align-items:center;gap:5px;padding-right:12px}.branch-route code{max-width:42%;overflow:hidden;color:var(--color-ink-faint);font-size:9px;text-overflow:ellipsis;white-space:nowrap}.branch-route b{color:var(--color-ink-faint);font-weight:400}.row-actions{display:flex;align-items:center;justify-content:flex-end;gap:5px}.provider-action{position:relative;display:flex}.provider-button,.action-toggle,.detail-action{display:inline-flex;height:28px;align-items:center;justify-content:center;gap:6px;border:1px solid var(--color-accent);color:var(--color-accent);background:color-mix(in srgb,var(--color-accent) 5%,transparent)}.provider-button{min-width:120px;padding:0 8px;border-right:0}.provider-button.merge-primary{color:var(--color-success);border-color:var(--color-success);background:color-mix(in srgb,var(--color-success) 15%,var(--color-surface))}.provider-button.conflict{color:var(--color-danger);border-color:var(--color-danger)}.action-toggle{width:27px;padding:0}.provider-button:hover,.action-toggle:hover,.action-toggle.active,.detail-action:hover{color:var(--color-ink);background:color-mix(in srgb,var(--color-accent) 22%,var(--color-surface))}.action-menu{position:absolute;z-index:8;top:31px;right:0;display:grid;width:178px;padding:4px;border:1px solid var(--color-border);box-shadow:var(--app-menu-shadow);background:var(--color-surface-raised)}.action-menu button{display:flex;height:29px;align-items:center;gap:8px;padding:0 8px;border:0;color:var(--color-ink-muted);background:transparent;text-align:left}.action-menu button:hover{color:var(--color-ink);background:var(--color-surface-hover)}.action-menu button.danger{color:var(--color-danger)}.panel-button{display:grid;width:28px;height:28px;place-items:center;border:1px solid var(--color-border);color:var(--color-ink-muted);background:var(--app-button-bg)}.panel-button:hover,.panel-button.active{border-color:var(--color-accent);color:var(--color-accent)}
|
||
.detail-panel{min-width:0;min-height:0;overflow:auto;border-left:1px solid var(--color-border);background:var(--color-surface)}.detail-header{display:flex;min-height:78px;align-items:flex-start;justify-content:space-between;gap:14px;padding:15px 16px;border-bottom:1px solid var(--color-border)}.detail-header>div{min-width:0}.detail-content{display:grid;gap:0;padding:0 16px}.detail-summary{display:flex;min-height:48px;align-items:center;gap:8px;border-bottom:1px solid var(--color-border-subtle)}.state-badge{padding:3px 7px;border:1px solid currentColor;font-size:9px;font-weight:700}.detail-summary .avatar{margin-left:4px}.detail-summary strong{font-size:11px}.description{padding:15px 0;border-bottom:1px solid var(--color-border-subtle)}.description h3{margin:0 0 9px;font-size:11px}.description p{margin:0;white-space:pre-wrap;color:var(--color-ink-muted);font-size:11px;line-height:1.55}.description p.muted{color:var(--color-ink-faint)}.detail-actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px;margin-top:16px}.detail-action{width:100%;height:32px;padding:0 8px}.detail-action.primary-action{color:var(--color-success);border-color:var(--color-success)}.detail-action.danger-action{color:var(--color-danger);border-color:color-mix(in srgb,var(--color-danger) 75%,var(--color-border))}.open{color:var(--color-success)}.draft{color:var(--color-ink-muted)}.merged{color:#b785e8}.closed{color:var(--color-danger)}
|
||
.loading-state,.list-empty,.empty-state{display:flex;min-height:180px;align-items:center;justify-content:center;gap:8px;color:var(--color-ink-muted)}.list-empty,.empty-state{flex-direction:column}.list-empty strong{color:var(--color-ink);font-size:12px}.list-empty span{font-size:10px}.empty-state{flex:1;text-align:center}.empty-state h2{margin:4px 0 0;font-size:14px}.empty-state p{max-width:430px;margin:0 0 8px;line-height:1.5}.empty-icon{display:grid;width:42px;height:42px;place-items:center;border:1px solid var(--color-border);color:var(--color-accent);background:var(--color-surface)}.primary{display:inline-flex;min-height:30px;align-items:center;gap:6px;padding:0 10px;border:1px solid var(--color-accent-solid);color:var(--color-on-accent);background:var(--color-accent-solid)}
|
||
@media(max-width:1100px){.review-layout.inspector-open{grid-template-columns:minmax(500px,1fr) 360px}.table-head,.request-row{grid-template-columns:78px minmax(230px,1.5fr) minmax(180px,1fr) 90px 170px}.table-head>span:nth-child(3),.request-author{display:none}.request-groups{min-width:760px}}
|
||
@media(max-width:760px){.review-toolbar{flex-wrap:wrap}.group-actions{order:2}.review-search{order:1;flex-basis:calc(100% - 200px)}.review-layout.inspector-open{display:block}.detail-panel{position:absolute;inset:90px 0 0 15%;z-index:5;box-shadow:var(--app-drawer-shadow)}.state-tabs{gap:8px}.group-actions button{font-size:0}.group-actions button>:global(svg){margin:0}.table-head,.request-row{grid-template-columns:72px minmax(220px,1fr) 90px 160px}.table-head>span:nth-child(3),.table-head>span:nth-child(4),.request-author,.branch-route{display:none}.request-groups{min-width:580px}}
|
||
.merge-summary{display:flex;min-height:45px;align-items:center;gap:7px;border-bottom:1px solid var(--color-border-subtle);color:var(--color-success)}.merge-summary.conflict{color:var(--color-danger)}.merge-summary>span,.merge-summary>strong{font-size:10.5px}
|
||
@media(max-width:1100px){.table-head,.request-row{grid-template-columns:78px minmax(270px,1fr) minmax(190px,1fr) 190px}.table-head>span:nth-child(3),.table-head>span:nth-child(4),.request-author,.collaborators{display:none}.request-groups{min-width:780px}}
|
||
@media(max-width:760px){.table-head,.request-row{grid-template-columns:72px minmax(260px,1fr) 190px}.table-head>span:nth-child(5),.repo-branch{display:none}.request-groups{min-width:600px}.merge-summary{align-items:flex-start;flex-wrap:wrap;padding:10px 0}}
|
||
.branch-loading{display:flex;align-items:center;gap:5px;color:var(--color-ink-faint);font-size:9px}
|
||
.provider-action{overflow:visible;border-radius:2px;box-shadow:var(--app-edge-shadow)}.provider-button{border-radius:2px 0 0 2px;font-weight:600}.action-toggle{border-radius:0 2px 2px 0;background:color-mix(in srgb,var(--color-accent) 14%,var(--color-surface))}.panel-button{margin-left:2px;border-radius:2px}.review-layout.inspector-open{grid-template-columns:minmax(580px,1fr) minmax(420px,44%)}.detail-header{background:linear-gradient(180deg,color-mix(in srgb,var(--color-surface-raised) 72%,var(--color-surface)),var(--color-surface))}
|
||
.comments-section{padding:15px 0;border-bottom:1px solid var(--color-border-subtle)}.comments-section>header{display:flex;align-items:center;gap:7px;margin-bottom:10px}.comments-section h3{margin:0;font-size:11px}.comments-section>header>span{display:grid;min-width:18px;height:18px;place-items:center;border-radius:9px;color:var(--color-ink-faint);background:var(--color-surface-raised);font-size:9px}.comments-loading,.no-comments{margin:8px 0;color:var(--color-ink-faint);font-size:10px}.comments-loading{display:flex;align-items:center;gap:6px}.comment-list{display:grid;gap:10px;margin-bottom:12px}.comment-list article{display:flex;align-items:flex-start;gap:9px}.comment-list article>div{min-width:0;flex:1;padding:9px 10px;border:1px solid var(--color-border-subtle);border-radius:2px;background:var(--app-bg)}.comment-list article header{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:6px}.comment-list article strong{font-size:10.5px}.comment-list article time{color:var(--color-ink-faint);font-size:9px}.comment-list article p{margin:0;white-space:pre-wrap;color:var(--color-ink-muted);font-size:10.5px;line-height:1.5}
|
||
@media(max-width:1100px){.review-layout.inspector-open{grid-template-columns:minmax(480px,1fr) 380px}}
|
||
@media(max-width:760px){.review-layout.inspector-open{display:block}.detail-panel{position:absolute;inset:90px 0 0 12%;z-index:5;box-shadow:var(--app-drawer-shadow)}}
|
||
.comment-list{max-height:230px;overflow-y:auto;overscroll-behavior:contain;padding-right:5px;scrollbar-gutter:stable}.comment-list::-webkit-scrollbar{width:7px}.comment-list::-webkit-scrollbar-thumb{border:2px solid transparent;border-radius:4px;background:var(--color-border-input);background-clip:padding-box}.comment-list::-webkit-scrollbar-track{background:transparent}
|
||
.detail-panel{display:flex;flex-direction:column;overflow:hidden}.detail-header{flex:0 0 auto}.detail-content{display:flex;min-height:0;flex:1;flex-direction:column;overflow:hidden}.detail-summary,.merge-summary,.description,.detail-actions{flex:0 0 auto}.description p{display:-webkit-box;overflow:hidden;-webkit-box-orient:vertical;-webkit-line-clamp:3;line-clamp:3}.comments-section{display:flex;min-height:0;flex:1;flex-direction:column;overflow:hidden}.comments-section>header{flex:0 0 auto}.comment-list{min-height:0;max-height:none;flex:1;overflow-y:auto}.detail-actions{margin-bottom:12px}
|
||
.review-center{position:relative}.review-layout.inspector-open{grid-template-columns:minmax(0,1fr)}.detail-panel{position:absolute;z-index:20;inset:0;display:flex;width:100%;height:100%;flex-direction:column;overflow:hidden;border:0;background:var(--app-bg)}.detail-header{display:flex;min-height:48px;align-items:center;padding:0 20px;border-bottom:1px solid var(--color-border);background:var(--color-surface-raised)}.detail-provider{display:flex;align-items:center;gap:9px;color:var(--color-ink-muted)}.detail-provider>:global(svg){color:var(--color-accent)}.detail-provider strong{font-size:13px;font-weight:600}.detail-header-actions{display:flex;align-items:center;gap:3px;margin-left:auto}.detail-content{display:grid;min-height:0;flex:1;grid-template-columns:minmax(0,1fr) 300px;gap:28px;overflow:hidden;padding:0 28px}.detail-main{display:flex;min-width:0;min-height:0;flex-direction:column;overflow:hidden}.detail-title{flex:0 0 auto;padding:18px 0 0;border-bottom:1px solid var(--color-border-subtle)}.detail-title h2{margin:7px 0 10px;font-size:20px;font-weight:520;line-height:1.25}.detail-title .detail-summary{min-height:34px;border:0}.detail-title .detail-summary>span:last-child{color:var(--color-ink-muted);font-size:10.5px}.detail-main>.description{padding:13px 0}.detail-main>.comments-section{display:flex;min-height:0;flex:1;flex-direction:column;overflow:hidden;padding:13px 0 18px}.detail-main .merge-summary{flex:0 0 auto;margin-bottom:11px;padding:0 12px;border:1px solid var(--color-border);background:var(--color-surface)}.detail-main .comment-list{min-height:0;flex:1}.detail-sidebar{display:flex;min-height:0;flex-direction:column;gap:0;overflow:hidden;padding:18px 0;border-left:1px solid var(--color-border-subtle);padding-left:22px;background:var(--color-surface)}.detail-sidebar .detail-actions{display:grid;grid-template-columns:1fr;gap:6px;margin:0 0 8px}.detail-sidebar section{padding:17px 0;border-bottom:1px solid var(--color-border-subtle)}.detail-sidebar section header{display:flex;align-items:center;justify-content:space-between;gap:12px}.detail-sidebar section h3{margin:0;color:var(--color-ink-muted);font-size:11px;font-weight:560}.people{display:grid;grid-template-columns:24px minmax(0,1fr);align-items:center;gap:8px;margin-top:12px}.people.compact{grid-template-columns:24px}
|
||
@media(max-width:900px){.detail-content{grid-template-columns:minmax(0,1fr) 270px;gap:18px;padding:0 18px}.detail-sidebar{padding-left:16px}.detail-title h2{font-size:17px}}
|
||
@media(max-width:680px){.detail-content{display:block;overflow:hidden;padding:0 15px}.detail-main{height:100%}.detail-sidebar{display:none}.detail-panel{inset:0}}
|
||
.comment-list{align-content:start;grid-auto-rows:max-content}
|
||
.detail-panel{inset:0 0 0 auto;width:50%;min-width:760px;max-width:100%;height:100%;border-left:1px solid var(--color-border);box-shadow:var(--app-drawer-shadow)}.review-layout.inspector-open{grid-template-columns:minmax(0,1fr)}
|
||
.provider-action.merge-action .action-toggle{border-color:var(--color-success);color:var(--color-success);background:color-mix(in srgb,var(--color-success) 15%,var(--color-surface))}.provider-action.merge-action .action-toggle:hover,.provider-action.merge-action .action-toggle.active{color:var(--color-on-status);background:var(--color-success)}.provider-action.conflict-action .action-toggle{border-color:var(--color-danger);color:var(--color-danger);background:color-mix(in srgb,var(--color-danger) 12%,var(--color-surface))}.panel-button{border-color:color-mix(in srgb,var(--color-accent) 45%,var(--color-border));color:var(--color-accent);background:color-mix(in srgb,var(--color-accent) 6%,var(--color-surface))}.panel-button:hover,.panel-button.active{border-color:var(--color-accent);color:var(--color-ink);background:color-mix(in srgb,var(--color-accent) 22%,var(--color-surface))}
|
||
.local-resolution{display:grid;flex:0 0 auto;gap:9px;margin:0 0 11px;padding:10px 12px;border:1px solid color-mix(in srgb,var(--color-danger) 52%,var(--color-border));background:color-mix(in srgb,var(--color-danger) 6%,var(--color-surface))}.local-resolution>div{display:flex;align-items:flex-start;gap:8px;color:var(--color-ink-muted);font-size:10.5px;line-height:1.45}.local-resolution>div>:global(svg){flex:0 0 auto;margin-top:1px;color:var(--color-danger)}.local-resolution footer{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:6px}.local-resolution button{display:inline-flex;min-height:28px;align-items:center;padding:0 9px;border:1px solid var(--color-danger);color:var(--color-danger);background:color-mix(in srgb,var(--color-danger) 12%,var(--color-surface))}.local-resolution button:hover:not(:disabled){color:var(--color-on-status);background:var(--color-danger)}.local-resolution button.secondary{border-color:var(--color-border-input);color:var(--color-ink-muted);background:var(--app-button-bg)}.local-resolution.error{border-color:var(--color-warning);background:color-mix(in srgb,var(--color-warning) 7%,var(--color-surface))}.local-resolution.error>div>:global(svg){color:var(--color-warning)}.local-resolution.complete{border-color:var(--color-success);background:color-mix(in srgb,var(--color-success) 7%,var(--color-surface))}.local-resolution.complete>div>:global(svg){color:var(--color-success)}
|
||
@media(max-width:760px){.detail-panel{inset:0;width:100%;min-width:0;max-width:none}}
|
||
|
||
/* Review Center B — compact hierarchy, overlay inspector, semantic actions. */
|
||
.review-center{font-size:11px}.review-header{min-height:44px}.review-heading h1{font-size:14px}
|
||
.review-toolbar{display:grid;min-height:50px;grid-template-columns:auto minmax(200px,1fr) 170px 190px auto;align-items:center;gap:10px;padding:0 14px;background:color-mix(in srgb,var(--color-surface) 35%,var(--app-bg))}
|
||
.state-tabs{height:100%;min-height:0;gap:3px;padding:0;border:0;background:transparent}.state-tabs button{min-width:58px;justify-content:center;padding:0 9px;font-size:10.5px}.state-tabs button.active{color:var(--color-ink)}.state-tabs button.active:after{right:7px;left:7px}.state-tabs span,.group-header>span{min-width:17px;height:17px;padding:0 4px;border:1px solid var(--color-border-subtle);border-radius:1px;font-size:9px}
|
||
.review-search{min-width:0;height:30px}.review-search:focus-within{box-shadow:inset 2px 0 0 var(--color-accent)}.group-actions{gap:4px}.group-actions .icon-button{width:30px;margin:0;padding:0;border-color:var(--color-border-subtle)}
|
||
.table-head,.request-row{grid-template-columns:92px minmax(285px,1.65fr) 132px 126px minmax(220px,1fr) 190px}.table-head{min-height:34px;padding:0 16px;border-top:1px solid var(--color-border-subtle);border-bottom-color:var(--color-border);color:color-mix(in srgb,var(--color-ink-faint) 82%,transparent);background:color-mix(in srgb,var(--color-surface-raised) 80%,var(--app-bg));font-size:8.5px;letter-spacing:.07em}.request-groups{min-width:1080px}.request-group{border-bottom:0}.group-header{height:39px;gap:8px;padding:0 16px;border-bottom-color:var(--color-border);color:var(--color-ink);background:color-mix(in srgb,var(--color-surface) 78%,var(--app-bg))}.group-header:hover{background:color-mix(in srgb,var(--color-surface-hover) 82%,var(--app-bg))}.group-header>:global(svg){color:var(--color-accent)}.group-header strong{font-size:11.5px;font-weight:680;letter-spacing:.005em}.group-header>span{margin-left:4px;color:var(--color-ink-muted);background:color-mix(in srgb,var(--color-surface-raised) 88%,var(--app-bg))}
|
||
.request-row{min-height:64px;padding:0 16px;border-bottom-color:color-mix(in srgb,var(--color-border-subtle) 76%,transparent);background:color-mix(in srgb,var(--app-bg) 96%,var(--color-surface))}.request-row:before{position:absolute;top:0;bottom:0;left:0;width:3px;background:transparent;content:""}.request-row:hover{background:color-mix(in srgb,var(--color-surface-hover) 72%,var(--app-bg))}.request-row.selected{outline-color:color-mix(in srgb,var(--color-accent) 74%,var(--color-border));background:color-mix(in srgb,var(--color-accent) 5%,var(--color-surface-raised));box-shadow:inset 0 1px color-mix(in srgb,var(--color-accent) 15%,transparent),inset 0 -1px color-mix(in srgb,var(--color-accent) 15%,transparent)}.request-row.selected:before{background:var(--color-accent);box-shadow:2px 0 10px color-mix(in srgb,var(--color-accent) 24%,transparent)}.request-status{gap:8px}.request-status>:global(svg){width:15px;height:15px}.request-status>span{gap:4px;font-weight:600}.request-status small{font-weight:400}.request-title{gap:7px}.request-title>span{align-items:center}.request-title code{font-size:10.5px;font-weight:650}.request-title strong{font-size:11.5px;font-weight:680}.change-stats{gap:7px;font-size:9.5px}.request-author i,.avatar,.collaborators i{border-radius:1px}.repo-branch{gap:6px}.repo-branch>strong{color:var(--color-ink);font-size:10.5px;font-weight:650}.branch-route code{padding:2px 4px;color:var(--color-ink-muted);background:color-mix(in srgb,var(--color-accent) 6%,transparent)}
|
||
.provider-action{box-shadow:none}.provider-button,.action-toggle{height:32px}.provider-button{min-width:124px;padding:0 11px;font-weight:650}.provider-button.merge-primary{color:var(--color-success);border-color:var(--color-success);background:color-mix(in srgb,var(--color-success) 9%,var(--app-bg))}.provider-button.merge-primary:hover{color:var(--color-on-status);background:var(--color-success)}.provider-action.merge-action .action-toggle{background:color-mix(in srgb,var(--color-success) 20%,var(--app-bg))}.provider-button.conflict{color:var(--color-warning);border-color:var(--color-warning);background:color-mix(in srgb,var(--color-warning) 9%,var(--app-bg))}.provider-action.conflict-action .action-toggle{color:var(--color-warning);border-color:var(--color-warning);background:color-mix(in srgb,var(--color-warning) 17%,var(--app-bg))}.panel-button{width:32px;height:32px;margin-left:3px;background:color-mix(in srgb,var(--color-accent) 5%,var(--app-bg))}
|
||
.action-menu{top:36px;width:190px;padding:6px;border-color:color-mix(in srgb,var(--color-border) 86%,var(--color-accent));box-shadow:var(--app-float-shadow);background:color-mix(in srgb,var(--color-surface-raised) 92%,var(--app-bg))}.action-menu:before{position:absolute;top:-5px;right:10px;width:8px;height:8px;transform:rotate(45deg);border-top:1px solid var(--color-border);border-left:1px solid var(--color-border);background:inherit;content:""}.action-menu button{position:relative;height:34px;gap:10px;padding:0 10px;border-left:2px solid transparent;color:var(--color-ink-muted);font-size:10.5px}.action-menu button:hover:not(:disabled){color:var(--color-ink);border-left-color:var(--color-accent);background:color-mix(in srgb,var(--color-accent) 8%,var(--color-surface-hover))}.action-menu button>:global(svg){flex:0 0 auto}.action-menu .menu-merge>:global(svg){color:var(--color-success)}.action-menu .menu-approve>:global(svg),.action-menu .menu-provider>:global(svg){color:var(--color-accent)}.action-menu button.danger{margin-top:4px;border-top:1px solid var(--color-border-subtle);color:var(--color-danger)}.action-menu button.danger:hover{border-left-color:var(--color-danger);background:color-mix(in srgb,var(--color-danger) 8%,var(--color-surface-hover))}.action-menu .menu-conflict{color:var(--color-warning)}.action-menu button:disabled{opacity:.48}
|
||
.detail-panel{width:50%;min-width:720px;max-width:960px;box-shadow:var(--app-drawer-shadow)}.detail-header{min-height:47px;padding:0 16px}.detail-provider strong{font-size:12px}.detail-content{grid-template-columns:minmax(0,1fr) 250px;gap:20px;padding:0 16px}.detail-title{padding:15px 0 10px}.detail-title h2{margin:5px 0 8px;font-size:18px;font-weight:620}.detail-summary{min-height:27px;border:0}.detail-summary .avatar{margin-left:3px}
|
||
.detail-main>.merge-summary{min-height:43px;margin:11px 0 0;padding:0 10px;border:1px solid color-mix(in srgb,var(--color-success) 48%,var(--color-border));background:color-mix(in srgb,var(--color-success) 6%,var(--color-surface))}.detail-main>.merge-summary.conflict{border-color:color-mix(in srgb,var(--color-warning) 55%,var(--color-border));color:var(--color-warning);background:color-mix(in srgb,var(--color-warning) 7%,var(--color-surface))}.merge-spacer{flex:1}.detail-change-stats{display:flex;align-items:center;gap:7px;white-space:nowrap;font-size:9px}.detail-change-stats b{color:var(--color-success)}.detail-change-stats em{color:var(--color-danger);font-style:normal}.detail-change-stats span{color:var(--color-ink-faint)}
|
||
.detail-main>.description{padding:12px 0}.detail-main>.comments-section{padding:12px 0 14px}.comment-list{gap:8px}.comment-list article{gap:8px}.comment-list article>div{padding:8px 9px;border-radius:0;background:color-mix(in srgb,var(--color-surface) 48%,var(--app-bg))}
|
||
.detail-sidebar{padding:15px 0 15px 16px;background:color-mix(in srgb,var(--color-surface) 58%,var(--app-bg))}.detail-sidebar .detail-actions{gap:6px;margin-bottom:9px}.detail-action{height:31px}.detail-sidebar section{padding:14px 0}.people{grid-template-columns:23px minmax(0,1fr);gap:8px;margin-top:10px}.people strong,.detail-meta strong,.detail-meta span{overflow:hidden;color:var(--color-ink-muted);font-size:10px;font-weight:500;text-overflow:ellipsis}.detail-meta{display:grid;gap:8px}.detail-actions>.danger-action:first-child{color:var(--color-warning);border-color:var(--color-warning);background:color-mix(in srgb,var(--color-warning) 8%,var(--color-surface))}
|
||
.local-resolution{margin:8px 0 0;border-color:color-mix(in srgb,var(--color-warning) 55%,var(--color-border));background:color-mix(in srgb,var(--color-warning) 7%,var(--color-surface))}.local-resolution>div>:global(svg){color:var(--color-warning)}.local-resolution button{color:var(--color-warning);border-color:var(--color-warning);background:color-mix(in srgb,var(--color-warning) 10%,var(--color-surface))}
|
||
@media(max-width:1280px){.review-toolbar{grid-template-columns:auto minmax(160px,1fr) 150px 155px auto}.state-tabs button{min-width:52px;padding:0 6px}.table-head,.request-row{grid-template-columns:80px minmax(250px,1.5fr) 110px minmax(190px,1fr) 180px}.table-head>span:nth-child(4),.collaborators{display:none}.table-head,.request-groups{min-width:980px}.detail-panel{width:58%;min-width:680px}}
|
||
@media(max-width:900px){.review-toolbar{grid-template-columns:minmax(120px,1fr) 150px 150px auto;grid-template-rows:38px 38px;padding:0 10px}.state-tabs{grid-column:1/-1;grid-row:1}.review-search{grid-column:1;grid-row:2}.group-actions{grid-column:3;grid-row:2}.detail-panel{width:76%;min-width:620px}.detail-content{grid-template-columns:minmax(0,1fr) 220px;gap:14px}.table-head,.request-row{grid-template-columns:76px minmax(240px,1.5fr) minmax(180px,1fr) 180px}.table-head>span:nth-child(3),.request-author{display:none}.request-groups{min-width:760px}}
|
||
@media(max-width:680px){.state-tabs button{min-width:0;flex:1}.group-actions .icon-button:nth-child(-n+2){display:none}.detail-panel{width:100%;min-width:0;max-width:none}.detail-content{display:block;padding:0 13px}.detail-main{height:100%}.detail-sidebar{display:none}}
|
||
|
||
/* Accepted Review Center concept — faithful final layout. */
|
||
.review-header{min-height:54px;padding:0 24px;background:color-mix(in srgb,var(--app-bg) 78%,var(--color-surface))}.review-heading{gap:10px}.review-heading h1{font-size:14px;font-weight:700}
|
||
.review-toolbar{min-height:55px;grid-template-columns:350px minmax(200px,1fr) 155px 160px 34px;gap:10px;padding:0 16px;border-bottom-color:var(--color-border);background:color-mix(in srgb,var(--app-bg) 88%,var(--color-surface))}.state-tabs{gap:8px}.state-tabs button{min-width:72px;justify-content:flex-start;padding:0 8px;color:var(--color-ink-muted);font-size:11px}.state-tabs button.active{color:var(--color-ink)}.state-tabs button.active:after{right:0;left:0;height:2px}.state-tabs span{min-width:17px;height:17px;margin-left:auto;border:0;background:var(--color-surface-raised)}.review-search,.group-actions .icon-button{height:34px;background:color-mix(in srgb,var(--app-input-bg) 92%,var(--app-bg))}.review-search{padding:0 11px}.group-actions .icon-button{width:34px}
|
||
.table-head,.request-row{grid-template-columns:90px minmax(255px,1.65fr) 105px 120px minmax(180px,1fr) 150px}.table-head{min-height:37px;padding:0 16px;background:color-mix(in srgb,var(--color-surface-raised) 88%,var(--app-bg));font-size:8.5px}.table-head,.request-groups{min-width:980px}.group-header{height:45px;padding:0 17px;background:color-mix(in srgb,var(--app-bg) 72%,var(--color-surface))}.group-header strong{font-size:11.5px}.request-row{min-height:78px;padding:0 16px}.request-row.selected{background:linear-gradient(90deg,color-mix(in srgb,var(--color-accent) 7%,var(--color-surface-raised)),color-mix(in srgb,var(--color-surface-raised) 72%,var(--app-bg)))}.request-title{gap:8px}.request-title strong{font-size:12px}.request-status{font-size:10.5px}.request-author i,.avatar,.collaborators i{width:28px;height:28px;background:color-mix(in srgb,var(--color-accent) 58%,var(--app-bg))}.provider-button,.action-toggle,.panel-button{height:34px}.provider-button{min-width:102px}.action-toggle{width:34px}.panel-button{width:34px}.action-menu{top:38px;width:154px}.action-menu button{height:36px}
|
||
.detail-panel{width:44%;min-width:680px;max-width:none;background:color-mix(in srgb,var(--app-bg) 94%,var(--color-surface))}.detail-header{min-height:54px;padding:0 24px;background:color-mix(in srgb,var(--app-bg) 78%,var(--color-surface))}.detail-provider strong{font-size:13px}.detail-content{grid-template-columns:minmax(0,1fr) 225px;gap:18px;padding:0 0 0 24px}.detail-main{padding-right:0}.detail-title{padding:18px 0 16px}.detail-title-line{display:flex;min-width:0;align-items:baseline;gap:10px}.detail-title-line>span{flex:0 0 auto;color:var(--color-accent);font-size:17px;font-weight:700}.detail-title h2{min-width:0;margin:0;overflow:hidden;font-size:19px;font-weight:700;text-overflow:ellipsis;white-space:nowrap}.detail-title .detail-summary{min-height:33px;gap:7px}.detail-summary .avatar{width:23px;height:23px;margin-left:5px}.detail-summary strong{font-size:10.5px}.summary-separator{color:var(--color-ink-faint)}.detail-branch-route{display:flex;min-width:0;align-items:center;gap:6px;color:var(--color-ink-muted)}.detail-branch-route>:global(svg){color:var(--color-ink-muted)}.detail-branch-route code{max-width:95px;overflow:hidden;color:var(--color-ink-muted);font-size:10.5px;text-overflow:ellipsis;white-space:nowrap}.detail-branch-route>span{color:var(--color-ink-faint)}.state-badge{padding:0;border:0;font-size:10.5px}
|
||
.detail-main>.merge-summary{min-height:55px;margin:0 0 4px;padding:0 12px;border-color:color-mix(in srgb,var(--color-success) 65%,var(--color-border));background:color-mix(in srgb,var(--color-success) 5%,var(--app-bg))}.detail-main>.description{padding:14px 0 18px}.description h3,.comments-section h3{font-size:11.5px}.description p{font-size:10.5px}.detail-main>.comments-section{padding:13px 0 0}.comments-section>header{min-height:28px;margin:0 0 8px}.comments-section>header>span{border-radius:1px}.comment-list{gap:10px;padding-right:0}.comment-list article{display:block}.comment-card{padding:0!important;border:1px solid var(--color-border)!important;background:color-mix(in srgb,var(--color-surface) 35%,var(--app-bg))!important}.comment-card header{min-height:42px;margin:0!important;padding:0 10px;border-bottom:0}.comment-card header .avatar{width:25px;height:25px;margin-right:2px}.comment-card header strong{font-size:10.5px}.comment-card header time{margin-left:auto}.owner-badge{padding:3px 6px;border:1px solid var(--color-border);color:var(--color-ink-faint);font-size:8.5px}.comment-card p{padding:0 44px 13px!important;color:var(--color-ink)!important}
|
||
.detail-sidebar{padding:28px 17px 16px;border-left-color:var(--color-border);background:color-mix(in srgb,var(--color-surface) 46%,var(--app-bg))}.detail-sidebar .detail-actions{gap:10px;margin:0 0 12px}.detail-action{height:39px;font-size:11px}.detail-sidebar section{padding:17px 0}.detail-sidebar section h3{font-size:11.5px}.people{margin-top:12px}.detail-meta{gap:11px}
|
||
@media(max-width:1280px){.review-toolbar{grid-template-columns:310px minmax(160px,1fr) 140px 145px 34px}.state-tabs{gap:3px}.state-tabs button{min-width:64px}.table-head,.request-row{grid-template-columns:80px minmax(250px,1.5fr) 105px minmax(190px,1fr) 150px}.request-groups{min-width:840px}.detail-panel{width:50%;min-width:650px}}
|
||
@media(max-width:900px){.review-toolbar{grid-template-columns:minmax(120px,1fr) 145px 145px 34px;grid-template-rows:40px 40px}.detail-panel{width:72%;min-width:600px}.detail-content{grid-template-columns:minmax(0,1fr) 205px;gap:14px;padding-left:16px}.table-head,.request-row{grid-template-columns:76px minmax(240px,1.5fr) minmax(180px,1fr) 150px}.request-groups{min-width:720px}}
|
||
@media(max-width:680px){.detail-panel{width:100%;min-width:0}.detail-content{display:block;padding:0 14px}.detail-sidebar{display:none}.detail-title h2{font-size:16px}.detail-title-line>span{font-size:14px}}
|
||
|
||
/* Match the approved reference at its 1672px desktop width. */
|
||
.review-center{--review-divider:color-mix(in srgb,var(--color-border) 82%,transparent);font-size:13px;background:color-mix(in srgb,var(--app-bg) 96%,var(--color-surface))}
|
||
.review-center.has-inspector{--inspector-width:44%}
|
||
.review-header,.detail-header{box-sizing:border-box;height:54px;min-height:54px;background:color-mix(in srgb,var(--app-bg) 94%,var(--color-surface))}
|
||
.review-heading h1,.detail-provider strong{font-size:14px}.detail-provider>:global(svg){display:none}.detail-header{gap:4px}.detail-header-actions{display:contents}.detail-header-actions button:first-child{margin-right:auto}.detail-provider{flex:none}
|
||
.review-toolbar{height:60px;min-height:60px}.review-search input{font-size:13px}.review-center .state-tabs button{font-size:13px}.state-tabs span{font-size:11px}.table-pane{padding:0 7px;box-sizing:border-box;border-top:8px solid var(--app-bg)}.table-head{padding:0 16px;height:37px}.table-head,.request-row{grid-template-columns:100px minmax(210px,1fr) 106px 120px 165px 230px}.table-head>span{font-size:10px}.table-head,.request-groups{min-width:980px}.group-header{height:44px;padding:0 12px}.group-header strong{font-size:13px}.request-row{padding:0 10px;min-height:78px}.request-row.selected{outline-color:color-mix(in srgb,var(--color-accent) 50%,var(--color-border));background:color-mix(in srgb,var(--app-bg) 93%,var(--color-accent));box-shadow:none}.request-row.selected:before{width:2px;box-shadow:none}.request-status{font-size:13px;font-weight:400}.request-status>span{font-weight:400}.request-status small{font-size:11px}.request-title strong{font-size:13px;font-weight:650}.request-title code{font-size:12px}.change-stats{font-size:12px}.request-author{font-size:12px}.repo-branch>strong{font-size:12px;font-weight:500}.branch-route code{padding:0;background:none;font-size:11px}.branch-route{gap:6px}
|
||
.review-center .provider-button{min-width:130px;flex-shrink:0;padding:0 10px;font-size:13px;font-weight:500;white-space:nowrap;line-height:1.2}.provider-button>:global(svg){flex-shrink:0}.provider-action,.action-toggle,.panel-button{flex-shrink:0}.provider-button.merge-primary>:global(svg){display:none}.provider-action .action-toggle{width:32px;background:transparent}.row-actions{gap:7px}.panel-button{margin:0;width:32px}.action-menu{width:154px;padding:4px;background:color-mix(in srgb,var(--app-bg) 90%,var(--color-surface-raised));border:1px solid var(--color-border);box-shadow:var(--app-menu-shadow)}.action-menu button{font-size:13px;gap:10px;height:37px;padding:0 10px}.action-menu button.danger{margin:0;border-top:0;color:var(--color-ink)}.action-menu button.danger>:global(svg){color:var(--color-danger)}.action-menu .menu-merge>:global(svg),.action-menu .menu-approve>:global(svg),.action-menu .menu-provider>:global(svg){color:var(--color-ink-muted)}
|
||
.detail-panel{min-width:0;width:var(--inspector-width,44%);box-shadow:var(--app-edge-shadow)}.detail-content{grid-template-columns:minmax(0,1fr) 225px;gap:0;padding:0}.detail-main{padding:0 18px 0 24px}.detail-sidebar{padding:28px 17px 16px;background:transparent;border-left:1px solid var(--review-divider)}.detail-title{padding:17px 0 9px;border:0}.detail-title h2{font-size:21px;font-weight:650}.detail-title-line>span{font-size:20px;color:var(--color-ink-muted)}.detail-title .detail-summary{min-height:34px;gap:7px}.detail-summary strong{font-size:12px;font-weight:400}.state-badge{font-size:13px;font-weight:400}.detail-branch-route code{font-size:13px}.detail-main>.merge-summary{min-height:54px;margin:0;padding:0 11px;gap:10px}.merge-summary>span,.merge-summary>strong{font-size:12px}.detail-change-stats{font-size:12px;gap:9px}.merge-summary>:global(svg){flex:none}.detail-main>.description{padding:20px 0 22px}.description h3{font-size:14px;font-weight:600;margin-bottom:10px}.description p{font-size:13px}.detail-main>.comments-section{padding:12px 0 0;overflow:visible;border:0}.comments-section>header{margin-bottom:10px}.comments-section h3{font-size:14px;font-weight:600}.comments-section>header>span{font-size:10px}.comment-list{gap:10px;margin:0;scrollbar-gutter:auto}.comment-list article .comment-card{min-height:78px;border-color:var(--review-divider)!important;background:transparent!important}.comment-list article header{justify-content:flex-start;gap:5px;padding:0 11px}.comment-card header .avatar{margin-right:5px;width:28px;height:28px}.comment-list article strong{font-size:12px;font-weight:500}.comment-list article time{font-size:11px;margin:0}.owner-badge{margin-left:auto;font-size:10px}.comment-list article p{font-size:12px;padding:2px 14px 15px 56px!important}.detail-sidebar .detail-actions{gap:15px;margin-bottom:27px}.review-center .detail-action{font-size:13px;position:relative}.detail-action>:global(svg){position:absolute;left:12px;width:18px;height:18px}.detail-action.primary-action{background:color-mix(in srgb,var(--color-success) 4%,transparent)}.detail-sidebar section{padding:16px 0;border-top:1px solid var(--review-divider);border-bottom:0}.detail-sidebar section h3{font-size:14px;font-weight:600}.people.compact{display:flex;gap:10px}.people strong,.detail-meta strong,.detail-meta span{font-size:12px}.detail-meta strong,.detail-meta span{display:flex;align-items:center;gap:10px}.detail-meta :global(svg){flex:none;color:var(--color-ink-muted)}
|
||
@media(max-width:1450px){.review-center.has-inspector{--inspector-width:52%}.detail-content{grid-template-columns:minmax(0,1fr) 190px}.detail-main{padding:0 14px}.detail-title h2{font-size:17px}.detail-title-line>span{font-size:17px}.detail-summary{flex-wrap:wrap}.detail-main>.merge-summary{flex-wrap:wrap;gap:7px;padding:9px}.detail-change-stats{font-size:10px}.detail-main>.merge-summary>span{font-size:11px}.detail-sidebar{padding:24px 12px}}
|
||
@media(max-width:900px){.review-center.has-inspector{--inspector-width:100%}.detail-content{display:grid;grid-template-columns:minmax(0,1fr) 200px}.detail-sidebar{display:flex}.detail-main{height:auto}.detail-panel{inset:0 0 0 auto;min-width:0}.group-actions .icon-button{display:flex!important}}
|
||
@media(max-width:520px){.detail-content{display:flex;flex-direction:column}.detail-main{flex:1;min-height:0}.detail-sidebar{flex:none;padding:8px 14px;border-top:1px solid var(--review-divider);border-left:0}.detail-sidebar section{display:none}.detail-sidebar .detail-actions{grid-template-columns:1fr 1fr;gap:6px;margin:0}.detail-action{height:32px}.detail-header{padding:0 14px}.detail-main>.description{padding:10px 0}.detail-title h2{white-space:normal}}
|
||
|
||
|
||
/* The inspector overlays an unchanged background; only viewport size reflows it. */
|
||
.review-toolbar{box-sizing:border-box;width:100%;height:56px;min-height:56px;grid-template-columns:316px minmax(140px,1fr) 150px 145px 30px;grid-template-rows:1fr;gap:10px;padding:0 14px 0 8px}
|
||
.review-toolbar .state-tabs{grid-column:1;grid-row:1;gap:8px;align-items:stretch}
|
||
.review-center .state-tabs button{min-width:0;flex:1;gap:6px;padding:0 6px;justify-content:center;font-size:12px;white-space:nowrap}
|
||
.state-tabs span{min-width:14px;height:16px;margin-left:0;padding:0 3px;font-size:10px}
|
||
.state-tabs button.active span{background:transparent}
|
||
.review-search{grid-column:2;grid-row:1;height:32px;order:0}
|
||
.review-search input{font-size:12px}
|
||
.repository-picker{position:relative;grid-column:3;grid-row:1;height:32px;min-width:0}
|
||
.integration-picker{position:relative;grid-column:4;grid-row:1;height:32px;min-width:0}
|
||
|
||
|
||
.review-toolbar .group-actions{grid-column:5;grid-row:1;order:0}
|
||
.review-toolbar .group-actions .icon-button{width:30px;height:32px}
|
||
@media(max-width:720px){.review-toolbar{grid-template-columns:minmax(110px,1fr) 130px 130px 30px;grid-template-rows:36px 38px;height:80px;min-height:80px;gap:0 10px}.review-toolbar .state-tabs{grid-column:1/-1;grid-row:1;max-width:316px}.review-search{grid-column:1;grid-row:2}.repository-picker{grid-column:2;grid-row:2}.integration-picker{grid-column:3;grid-row:2}.review-toolbar .group-actions{grid-column:4;grid-row:2}}
|
||
|
||
.integration-picker :global(.select-menu),
|
||
.repository-picker :global(.select-menu){width:100%;height:32px}
|
||
.integration-picker :global(.select-menu-trigger),
|
||
.repository-picker :global(.select-menu-trigger){height:32px;min-height:32px;font-size:12px}
|
||
.repository-picker :global(.select-menu-popup){min-width:290px;max-height:420px;padding:4px}
|
||
.repository-picker :global(.select-menu-option){min-height:34px;font-weight:650}
|
||
.repository-picker :global(.select-menu-group){margin:6px 3px 2px;padding:7px 6px 5px}
|
||
/* Compact request actions, matching the approved menu reference. */
|
||
.review-center .row-actions .provider-button{min-width:54px;height:27px;min-height:27px;padding:0 8px;font-size:11px;font-weight:500}
|
||
.review-center .row-actions .action-toggle{width:25px;height:27px;min-height:27px;background:transparent}
|
||
.review-center .row-actions .action-toggle.active{color:var(--color-success);background:color-mix(in srgb,var(--color-success) 8%,var(--app-bg))}
|
||
.review-center .row-actions .panel-button{width:27px;height:27px;min-height:27px}
|
||
.review-center .action-menu{top:36px;right:-2px;box-sizing:border-box;width:max-content;min-width:122px;padding:3px 0;border:1px solid var(--color-border);border-radius:0;background:color-mix(in srgb,var(--app-bg) 96%,var(--color-surface-raised));box-shadow:var(--app-menu-shadow)}
|
||
.review-center .action-menu:before{right:calc(50% - 4px)}
|
||
.review-center .action-menu button{box-sizing:border-box;display:flex;width:100%;height:30px;min-height:30px;justify-content:flex-start;align-items:center;gap:9px;padding:0 11px;margin:0;border:0;border-radius:0;font-size:11px;font-weight:400;line-height:1.2;text-align:left;white-space:nowrap;color:var(--color-ink)}
|
||
.review-center .action-menu button>:global(svg){width:14px;height:14px;flex:0 0 14px;color:var(--color-ink-muted)}
|
||
.review-center .action-menu button.danger>:global(svg){color:var(--color-danger)}
|
||
.review-center .action-menu button.menu-conflict{color:var(--color-warning)}
|
||
.review-center .action-menu button:hover:not(:disabled){background:var(--color-surface-hover)}
|
||
.review-comment-editor{flex:0 0 auto;min-width:0;padding:14px 0 18px;border-top:1px solid var(--color-border-subtle)}
|
||
</style>
|
||
|
||
{#if reviewConfirmRequest}
|
||
<ConfirmDialog
|
||
request={reviewConfirmRequest}
|
||
onConfirm={({ value, checked }) => answerReviewConfirmation(true, value, checked)}
|
||
onCancel={() => answerReviewConfirmation(false)}
|
||
/>
|
||
{/if}
|