Move pull-request discovery and caching into a dedicated background loader component. The repository dashboard no longer performs the network/keychain work itself and instead receives a shared badge state that is refreshed and persisted independently. The app wires the loader into top-level state and binds badge data into the dashboard view. - Add a background loader to fetch, group and cache open PR counts. - Simplify dashboard to accept and render pull-request badge state. - Wire loader into app-level state and bind badges to the dashboard.
338 lines
30 KiB
Svelte
338 lines
30 KiB
Svelte
<script lang="ts">
|
||
import { ArrowDown, ArrowUp, Check, ChevronDown, ChevronRight, Circle, Download, FolderGit2, GitBranch, GitPullRequest, LayoutGrid, List, Plus, Search, Star, TriangleAlert, Trash2, X } from "@lucide/svelte";
|
||
import type { PullRequestBadgeState } from "../pullRequestBadges";
|
||
import SelectMenu from "./SelectMenu.svelte";
|
||
|
||
interface DashboardRepository {
|
||
path: string;
|
||
name: string;
|
||
branch: string | null;
|
||
ahead: number;
|
||
behind: number;
|
||
changed: number;
|
||
lastOpened: number;
|
||
known: boolean;
|
||
favorite: boolean;
|
||
isOpen: boolean;
|
||
recent: boolean;
|
||
}
|
||
|
||
interface Workspace { id: string; name: string; }
|
||
export let repos: DashboardRepository[] = [];
|
||
export let language: "de" | "en" = "en";
|
||
export let isBusy = false;
|
||
export let onOpen: (path: string) => unknown;
|
||
export let onClose: (path: string) => unknown;
|
||
export let onAdd: () => unknown;
|
||
export let onClone: () => unknown;
|
||
export let onInit: () => unknown;
|
||
export let onFavorite: (path: string) => unknown;
|
||
export let onRemoveRecent: (path: string) => unknown;
|
||
export let onOpenPullRequests: (repository: string, sourceId: string) => unknown;
|
||
|
||
const STORAGE_KEY = "gitty.dashboard.v1";
|
||
let query = "";
|
||
let viewMode: "cards" | "list" = "list";
|
||
let selectedWorkspace = "";
|
||
let collapsedCategories = new Set<string>();
|
||
let workspaces: Workspace[] = [];
|
||
let assignments: Record<string, string> = {};
|
||
let workspaceDialog: HTMLDialogElement;
|
||
let workspaceInput: HTMLInputElement;
|
||
let workspaceName = "";
|
||
let workspaceError = "";
|
||
let workspaceSelection = new Set<string>();
|
||
export let pullRequestBadges: Record<string, PullRequestBadgeState> = {};
|
||
|
||
$: de = language === "de";
|
||
$: normalizedQuery = query.trim().toLocaleLowerCase();
|
||
$: filteredRepos = repos.filter((repo) => {
|
||
const matchesSearch = !normalizedQuery || `${repo.name} ${repo.path} ${repo.branch ?? ""}`.toLocaleLowerCase().includes(normalizedQuery);
|
||
return matchesSearch && (!selectedWorkspace || assignments[repo.path] === selectedWorkspace);
|
||
});
|
||
$: openRepos = filteredRepos.filter((repo) => repo.isOpen);
|
||
$: favoriteRepos = filteredRepos.filter((repo) => repo.favorite);
|
||
$: recentRepos = filteredRepos.filter((repo) => repo.recent && !repo.isOpen);
|
||
$: categories = [
|
||
{ id: "open", label: "Open", repos: openRepos },
|
||
{ id: "favorites", label: de ? "Favoriten" : "Favorites", repos: favoriteRepos },
|
||
{ id: "recent", label: "Recent", repos: recentRepos },
|
||
];
|
||
$: changedCount = repos.filter((repo) => repo.known && repo.changed > 0).length;
|
||
$: workspaceOptions = [
|
||
{ value: "", label: de ? "Alle Workspaces" : "All workspaces" },
|
||
...workspaces.map((workspace) => ({ value: workspace.id, label: workspace.name, group: "Workspaces" })),
|
||
];
|
||
loadPreferences();
|
||
|
||
function loadPreferences() {
|
||
try {
|
||
const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "{}") as { workspaces?: unknown; assignments?: unknown; viewMode?: unknown };
|
||
viewMode = saved.viewMode === "cards" ? "cards" : "list";
|
||
if (Array.isArray(saved.workspaces)) {
|
||
workspaces = saved.workspaces.filter((item): item is Workspace => Boolean(
|
||
item && typeof item === "object"
|
||
&& "id" in item && typeof item.id === "string" && item.id.startsWith("workspace-")
|
||
&& "name" in item && typeof item.name === "string" && item.name.trim(),
|
||
));
|
||
}
|
||
if (saved.assignments && typeof saved.assignments === "object" && !Array.isArray(saved.assignments)) {
|
||
assignments = Object.fromEntries(Object.entries(saved.assignments).filter(([, workspaceId]) =>
|
||
typeof workspaceId === "string" && workspaces.some((workspace) => workspace.id === workspaceId)));
|
||
}
|
||
} catch {
|
||
workspaces = [];
|
||
assignments = {};
|
||
}
|
||
}
|
||
|
||
function savePreferences() {
|
||
try { localStorage.setItem(STORAGE_KEY, JSON.stringify({ workspaces, assignments, viewMode })); }
|
||
catch { /* Preferences remain optional when storage is unavailable. */ }
|
||
}
|
||
|
||
function setViewMode(mode: "cards" | "list") {
|
||
viewMode = mode;
|
||
savePreferences();
|
||
}
|
||
|
||
function openWorkspaceDialog() {
|
||
workspaceName = "";
|
||
workspaceError = "";
|
||
workspaceSelection = new Set();
|
||
workspaceDialog.showModal();
|
||
requestAnimationFrame(() => workspaceInput.focus());
|
||
}
|
||
|
||
function toggleWorkspaceRepo(path: string) {
|
||
const next = new Set(workspaceSelection);
|
||
if (next.has(path)) next.delete(path);
|
||
else next.add(path);
|
||
workspaceSelection = next;
|
||
}
|
||
|
||
function createWorkspace(event: SubmitEvent) {
|
||
event.preventDefault();
|
||
const name = workspaceName.trim();
|
||
if (!name) {
|
||
workspaceError = de ? "Bitte einen Namen eingeben." : "Please enter a name.";
|
||
return;
|
||
}
|
||
if (workspaces.some((workspace) => workspace.name.toLocaleLowerCase() === name.toLocaleLowerCase())) {
|
||
workspaceError = de ? "Dieser Workspace existiert bereits." : "This workspace already exists.";
|
||
return;
|
||
}
|
||
const id = `workspace-${crypto.randomUUID()}`;
|
||
workspaces = [...workspaces, { id, name }];
|
||
assignments = { ...assignments, ...Object.fromEntries([...workspaceSelection].map((path) => [path, id])) };
|
||
selectedWorkspace = id;
|
||
savePreferences();
|
||
workspaceDialog.close();
|
||
}
|
||
|
||
function deleteSelectedWorkspace() {
|
||
if (!selectedWorkspace) return;
|
||
const deletedWorkspace = selectedWorkspace;
|
||
workspaces = workspaces.filter((workspace) => workspace.id !== deletedWorkspace);
|
||
assignments = Object.fromEntries(Object.entries(assignments).filter(([, workspaceId]) => workspaceId !== deletedWorkspace));
|
||
selectedWorkspace = "";
|
||
savePreferences();
|
||
}
|
||
|
||
function openCard(path: string) { if (!isBusy) onOpen(path); }
|
||
function closeCard(event: MouseEvent, path: string) { event.stopPropagation(); if (!isBusy) onClose(path); }
|
||
function favoriteCard(event: MouseEvent, path: string) { event.stopPropagation(); if (!isBusy) onFavorite(path); }
|
||
function removeRecentCard(event: MouseEvent, path: string) { event.stopPropagation(); if (!isBusy) onRemoveRecent(path); }
|
||
function openPullRequests(event: MouseEvent, repo: DashboardRepository) {
|
||
event.stopPropagation();
|
||
const badge = pullRequestBadges[repo.path];
|
||
if (!isBusy && badge?.status === "ready") onOpenPullRequests(badge.repository || repo.name, badge.sourceId);
|
||
}
|
||
|
||
function pullRequestTitle(badge: PullRequestBadgeState | undefined): string {
|
||
if (!badge || badge.status === "loading") return de ? "Offene Pull Requests werden geladen" : "Loading open pull requests";
|
||
if (badge.status === "error" || badge.status === "unavailable") return badge.message;
|
||
const title = de ? `${badge.count} offene Pull Requests` : `${badge.count} open pull requests`;
|
||
return badge.message ? `${title} (${de ? "Letzter bekannter Stand" : "Last known status"}: ${badge.message})` : title;
|
||
}
|
||
function toggleCategory(id: string) {
|
||
const next = new Set(collapsedCategories);
|
||
if (next.has(id)) next.delete(id);
|
||
else next.add(id);
|
||
collapsedCategories = next;
|
||
}
|
||
</script>
|
||
|
||
<section class="repo-dashboard" aria-label={de ? "Repository-Verwaltung" : "Repository management"}>
|
||
<header class="dashboard-header">
|
||
<h1>Repository Management</h1>
|
||
<div class="header-actions">
|
||
<button type="button" onclick={onClone} disabled={isBusy}><Download size={14} />{de ? "Klonen" : "Clone"}</button>
|
||
<button type="button" onclick={onInit} disabled={isBusy}><FolderGit2 size={14} />{de ? "Initialisieren" : "Initialize"}</button>
|
||
<button class="primary" type="button" onclick={onAdd} disabled={isBusy}><Plus size={14} />Repository</button>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="dashboard-toolbar">
|
||
<label class="dashboard-search">
|
||
<Search size={14} />
|
||
<input bind:value={query} autocomplete="off" spellcheck="false" placeholder={de ? "Repositories suchen …" : "Search repositories …"} aria-label={de ? "Repositories suchen" : "Search repositories"} />
|
||
</label>
|
||
<div class="view-switch" role="group" aria-label={de ? "Repository-Ansicht" : "Repository view"}>
|
||
<button type="button" class:active={viewMode === "list"} aria-pressed={viewMode === "list"} aria-label={de ? "Listenansicht" : "List view"} title={de ? "Listenansicht" : "List view"} onclick={() => setViewMode("list")}><List size={16} /></button>
|
||
<button type="button" class:active={viewMode === "cards"} aria-pressed={viewMode === "cards"} aria-label={de ? "Kachelansicht" : "Card view"} title={de ? "Kachelansicht" : "Card view"} onclick={() => setViewMode("cards")}><LayoutGrid size={15} /></button>
|
||
</div>
|
||
<div class="workspace-tools">
|
||
<SelectMenu
|
||
class="workspace-select"
|
||
value={selectedWorkspace}
|
||
options={workspaceOptions}
|
||
ariaLabel={de ? "Workspace auswählen" : "Select workspace"}
|
||
onChange={(value) => { selectedWorkspace = value; }}
|
||
/>
|
||
{#if selectedWorkspace}
|
||
<button class="workspace-delete" type="button" onclick={deleteSelectedWorkspace} aria-label={de ? "Ausgewählten Workspace löschen" : "Delete selected workspace"} title={de ? "Workspace löschen" : "Delete workspace"}><Trash2 size={15} strokeWidth={2} /></button>
|
||
{/if}
|
||
<button type="button" onclick={openWorkspaceDialog}><Plus size={14} />Workspace</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="dashboard-summary">{repos.length} Repositories <span>·</span> {changedCount} {de ? "mit Änderungen" : "with changes"}</div>
|
||
|
||
<div class="dashboard-content" class:table-view={viewMode === "list"}>
|
||
{#each categories as category (category.id)}
|
||
<section class="dashboard-section">
|
||
<header class="section-header">
|
||
<button class="section-toggle" type="button" aria-expanded={!collapsedCategories.has(category.id)} onclick={() => toggleCategory(category.id)}>
|
||
{#if collapsedCategories.has(category.id)}<ChevronRight size={14} />{:else}<ChevronDown size={14} />{/if}
|
||
<strong>{category.label}</strong><span>{category.repos.length}</span><i></i>
|
||
</button>
|
||
</header>
|
||
{#if !collapsedCategories.has(category.id)}
|
||
{#if category.repos.length > 0}
|
||
<div class="repo-grid" class:list-view={viewMode === "list"} class:tiles-view={viewMode === "cards"}>
|
||
{#each category.repos as repo (repo.path)}
|
||
{@const pullRequests = pullRequestBadges[repo.path]}
|
||
<article class="repo-card" class:open={repo.isOpen} class:favorite-category={category.id === "favorites"}>
|
||
<button class="card-main" type="button" onclick={() => openCard(repo.path)} disabled={isBusy}>
|
||
<span class="card-title"><FolderGit2 size={15} /><strong title={repo.path}>{repo.name}</strong></span>
|
||
<span class="card-branch" title={repo.branch ?? undefined}><GitBranch size={13} />{repo.branch ?? (de ? "Branch unbekannt" : "Unknown branch")}</span>
|
||
<span class="card-status">
|
||
{#if !repo.known}<span class="muted"><Circle class="filled" size={9} />{de ? "Status nicht geladen" : "Status not loaded"}</span>
|
||
{:else if repo.changed > 0}<span class="changed"><Circle class="filled" size={9} />{repo.changed} {de ? (repo.changed === 1 ? "Änderung" : "Änderungen") : (repo.changed === 1 ? "change" : "changes")}</span>
|
||
{:else}<span class="clean"><Circle class="filled" size={9} />{de ? "Sauber" : "Clean"}</span>{/if}
|
||
</span>
|
||
<span class="list-sync">{#if repo.known}<span class:ahead={repo.ahead > 0} title={de ? `${repo.ahead} Commits voraus` : `${repo.ahead} commits ahead`}><ArrowUp size={14} />{repo.ahead}</span><span class:behind={repo.behind > 0} title={de ? `${repo.behind} Commits zurück` : `${repo.behind} commits behind`}><ArrowDown size={14} />{repo.behind}</span>{:else}<span class="muted">—</span>{#if viewMode === "cards"}<span class="muted">—</span>{/if}{/if}</span>
|
||
</button>
|
||
<div class="card-actions">
|
||
<button class:has-open-prs={pullRequests?.status === "ready" && pullRequests.count > 0} class:pr-error={pullRequests?.status === "error"} class="pr-badge" type="button" onclick={(event) => openPullRequests(event, repo)} disabled={isBusy || !pullRequests || pullRequests.status !== "ready"} aria-label={`${repo.name}: ${pullRequestTitle(pullRequests)}`} title={pullRequestTitle(pullRequests)}>
|
||
{#if pullRequests?.status === "error"}<TriangleAlert size={14} />{:else}<GitPullRequest size={14} /><span>{pullRequests?.status === "ready" ? pullRequests.count : "–"}</span>{/if}
|
||
</button>
|
||
<button class:active={repo.favorite} type="button" onclick={(event) => favoriteCard(event, repo.path)} disabled={isBusy} aria-label={`${repo.name}: ${de ? "Favorit umschalten" : "Toggle favorite"}`} aria-pressed={repo.favorite}><Star size={15} /></button>
|
||
{#if repo.isOpen && category.id === "open"}<button type="button" onclick={(event) => closeCard(event, repo.path)} disabled={isBusy} aria-label={`${repo.name}: ${de ? "Schließen" : "Close"}`} title={de ? "Repository schließen" : "Close repository"}><X size={15} /></button>{/if}
|
||
{#if category.id === "recent"}<button type="button" onclick={(event) => removeRecentCard(event, repo.path)} disabled={isBusy} aria-label={`${repo.name}: ${de ? "Aus Recent entfernen" : "Remove from recent"}`} title={de ? "Aus Recent entfernen" : "Remove from recent"}><X size={15} /></button>{/if}
|
||
</div>
|
||
</article>
|
||
{/each}
|
||
</div>
|
||
{:else}
|
||
<p class="category-empty">{de ? "Keine Repositories in dieser Kategorie." : "No repositories in this category."}</p>
|
||
{/if}
|
||
{/if}
|
||
</section>
|
||
{/each}
|
||
</div>
|
||
<footer class="dashboard-footer">{repos.length} {de ? "lokale Repositories" : "local repositories"}</footer>
|
||
</section>
|
||
|
||
<dialog class="workspace-dialog" bind:this={workspaceDialog} aria-labelledby="workspace-title">
|
||
<form onsubmit={createWorkspace}>
|
||
<header><h2 id="workspace-title">{de ? "Workspace anlegen" : "Create workspace"}</h2><button type="button" onclick={() => workspaceDialog.close()} aria-label={de ? "Dialog schließen" : "Close dialog"}><X size={14} /></button></header>
|
||
<label for="workspace-name">Name</label>
|
||
<input id="workspace-name" bind:this={workspaceInput} bind:value={workspaceName} maxlength="64" autocomplete="off" aria-invalid={Boolean(workspaceError)} />
|
||
{#if workspaceError}<p class="dialog-error" role="alert">{workspaceError}</p>{/if}
|
||
{#if repos.length > 0}
|
||
<fieldset><legend>{de ? "Repositories zuordnen" : "Assign repositories"}</legend><div class="workspace-repos">
|
||
{#each repos as repo (repo.path)}<label><input type="checkbox" checked={workspaceSelection.has(repo.path)} onchange={() => toggleWorkspaceRepo(repo.path)} /><span>{repo.name}</span><small>{repo.path}</small></label>{/each}
|
||
</div></fieldset>
|
||
{/if}
|
||
<footer><button type="button" onclick={() => workspaceDialog.close()}>{de ? "Abbrechen" : "Cancel"}</button><button class="primary" type="submit"><Check size={14} />{de ? "Anlegen" : "Create"}</button></footer>
|
||
</form>
|
||
</dialog>
|
||
|
||
<style>
|
||
.repo-dashboard{display:flex;flex:1;min-height:0;flex-direction:column;overflow:hidden;color:var(--color-ink);background:var(--app-bg);font-size:12px}button,input{font:inherit}button{color:inherit;cursor:pointer}button:disabled{cursor:default;opacity:.5}
|
||
.dashboard-header{display:flex;min-height:58px;align-items:center;justify-content:space-between;gap:16px;padding:8px 20px;border-bottom:1px solid var(--color-border-subtle)}.dashboard-header h1{margin:0;font-size:18px;line-height:1.2;font-weight:700}.header-actions,.workspace-tools{display:flex;align-items:center;gap:8px}.header-actions button,.workspace-tools button{display:inline-flex;min-height:30px;align-items:center;gap:6px;padding:0 10px;border:1px solid var(--color-border);background:var(--app-button-bg)}button.primary{border-color:var(--color-primary);background:var(--color-primary);color:#fff}
|
||
.dashboard-toolbar{display:flex;min-height:48px;align-items:center;gap:8px;padding:8px 20px;border-bottom:1px solid var(--color-border-subtle)}.dashboard-search{display:flex;width:300px;height:30px;align-items:center;gap:8px;padding:0 9px;border:1px solid var(--color-border-input);border-radius:var(--ui-radius-sm);color:var(--color-ink-faint);background:var(--app-input-bg)}.dashboard-search:focus-within{border-color:var(--color-accent);box-shadow:0 0 0 2px color-mix(in srgb,var(--color-primary) 14%,transparent)}.dashboard-search :global(svg){flex:0 0 auto}.dashboard-search input{width:100%;min-width:0;height:100%;padding:0;border:0;border-radius:0;outline:0;color:var(--color-ink);background:transparent;box-shadow:none}.dashboard-search input:focus,.dashboard-search input:focus-visible{border:0;outline:0;box-shadow:none}.workspace-tools{margin-left:auto}.workspace-tools :global(.workspace-select){width:240px}.workspace-tools :global(.workspace-select .select-menu-trigger){height:30px;min-height:30px;padding:0 9px;border-color:var(--color-border-input);background:var(--app-input-bg);font-size:12px;font-weight:500}.workspace-tools .workspace-delete{width:30px;min-width:30px;padding:0;border-color:color-mix(in srgb,#df626b 55%,var(--color-border));color:#df747b;background:color-mix(in srgb,#c92f3a 8%,var(--app-button-bg))}.workspace-tools .workspace-delete:hover:not(:disabled){border-color:#df626b;color:#fff;background:#c93b45;box-shadow:0 0 0 2px color-mix(in srgb,#c92f3a 18%,transparent)}
|
||
.dashboard-summary{padding:8px 20px;color:var(--color-ink-muted)}.dashboard-summary span{padding:0 6px;color:var(--color-ink-faint)}.dashboard-content{flex:1;min-height:0;overflow:auto;padding:0 20px 24px}.dashboard-section+.dashboard-section{margin-top:20px}.section-header{min-height:31px;margin-bottom:6px}.section-toggle{display:flex;width:100%;min-height:31px;align-items:center;justify-content:flex-start;gap:7px;padding:0;border:0;background:transparent;color:var(--color-ink);text-align:left}.section-toggle:hover:not(:disabled){border:0;background:transparent;color:var(--color-ink)}.section-toggle :global(svg){flex:0 0 auto;color:var(--color-ink-muted)}.section-toggle strong{font-size:13px;line-height:1.2;font-weight:750}.section-toggle span{color:var(--color-ink-faint);font-weight:400}.section-toggle i{height:1px;flex:1;background:var(--color-border-subtle)}.category-empty{margin:0;padding:9px 12px;border:1px solid var(--color-border-subtle);color:var(--color-ink-faint);background:var(--color-surface)}
|
||
.repo-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.repo-card{position:relative;min-width:0;height:138px;border:1px solid var(--color-border);background:var(--color-surface)}.repo-card:hover,.repo-card:focus-within{border-color:var(--color-border-input);background:var(--color-surface-hover)}.card-main{display:flex;width:100%;height:100%;flex-direction:column;align-items:flex-start;gap:7px;padding:12px 104px 11px 14px;border:0;background:transparent;text-align:left}.card-main:focus-visible{outline:2px solid var(--color-accent);outline-offset:-2px}.card-title{display:flex;max-width:100%;align-items:center;gap:8px}.card-title strong{overflow:hidden;font-size:14px;font-weight:700;text-overflow:ellipsis;white-space:nowrap}.card-branch,.card-status,.card-status>span{display:flex;align-items:center;gap:6px}.card-branch{max-width:100%;overflow:hidden;color:var(--color-ink-muted);text-overflow:ellipsis;white-space:nowrap}.card-status{flex-wrap:wrap;gap:8px 18px;margin-top:auto}.card-status :global(.filled){fill:currentColor}.changed{color:#eeb94e}.behind{color:var(--color-sync-behind)}.clean{color:#68c878}.ahead{color:var(--color-sync-ahead)}.muted{color:var(--color-ink-muted)}
|
||
.card-actions{position:absolute;top:8px;right:8px;z-index:2;display:flex;align-items:center;gap:2px}.card-actions button{display:grid;width:26px;height:26px;place-items:center;padding:0;border:0;color:var(--color-ink-muted);background:transparent}.card-actions button:hover:not(:disabled){color:var(--color-ink);background:var(--color-surface-dim)}.card-actions button.active{color:var(--color-accent)}.card-actions button.active :global(svg){fill:color-mix(in srgb,var(--color-accent) 22%,transparent)}.card-actions .pr-badge{display:flex;width:auto;min-width:31px;grid-template-columns:none;align-items:center;justify-content:center;gap:4px;padding:0 5px;color:var(--color-ink-faint);font-size:11px}.card-actions .pr-badge:disabled{opacity:.72}.card-actions .pr-badge.has-open-prs{color:var(--color-accent)}.card-actions .pr-badge.pr-error{color:#e0a35b}.card-actions .pr-badge:hover:not(:disabled){color:var(--color-accent);background:color-mix(in srgb,var(--color-accent) 9%,transparent)}
|
||
.dashboard-footer{display:flex;min-height:30px;align-items:center;padding:0 20px;border-top:1px solid var(--color-border-subtle);color:var(--color-ink-muted)}
|
||
.workspace-dialog{width:min(420px,calc(100vw - 32px));margin:auto;padding:0;border:1px solid var(--color-border);color:var(--color-ink);background:var(--app-dialog-bg);box-shadow:var(--app-dialog-shadow);font-size:12px}.workspace-dialog::backdrop{background:var(--app-dialog-backdrop)}.workspace-dialog form>header,.workspace-dialog form>footer{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;background:var(--app-dialog-chrome)}.workspace-dialog form>header{border-bottom:1px solid var(--color-border)}.workspace-dialog form>footer{justify-content:flex-end;gap:8px;border-top:1px solid var(--color-border)}.workspace-dialog h2{margin:0;font-size:13px}.workspace-dialog header button{width:26px;height:26px;padding:0;border:0;background:transparent}.workspace-dialog form>label,.workspace-dialog form>input,.workspace-dialog fieldset,.dialog-error{margin-right:12px;margin-left:12px}.workspace-dialog form>label{display:block;margin-top:12px;margin-bottom:5px;color:var(--color-ink-muted)}.workspace-dialog form>input{width:calc(100% - 24px);height:30px;padding:0 8px;border:1px solid var(--color-border-input);color:var(--color-ink);background:var(--app-input-bg)}.dialog-error{margin-top:7px;color:#ed9292}.workspace-dialog fieldset{margin-top:14px;margin-bottom:14px;padding:0;border:0}.workspace-dialog legend{margin-bottom:6px;color:var(--color-ink-muted)}.workspace-repos{max-height:190px;overflow:auto;border:1px solid var(--color-border-subtle)}.workspace-repos label{display:grid;grid-template-columns:auto minmax(100px,auto) 1fr;align-items:center;gap:8px;min-height:32px;padding:4px 8px;border-bottom:1px solid var(--color-border-subtle)}.workspace-repos label:last-child{border-bottom:0}.workspace-repos small{overflow:hidden;color:var(--color-ink-faint);text-overflow:ellipsis;white-space:nowrap}.workspace-dialog footer button{display:inline-flex;min-height:28px;align-items:center;gap:6px;padding:0 10px;border:1px solid var(--color-border);background:var(--app-button-bg)}
|
||
@media(max-width:1000px){.dashboard-toolbar{flex-wrap:wrap;gap:8px}.dashboard-search{flex:1 1 260px}.workspace-tools{margin-left:0}.repo-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||
@media(max-width:680px){.dashboard-header{min-height:auto;align-items:flex-start;flex-direction:column}.header-actions{flex-wrap:wrap}.workspace-tools{width:100%}.workspace-tools :global(.workspace-select){min-width:0;flex:1}.repo-grid{grid-template-columns:1fr}.repo-card{height:132px}}
|
||
.view-switch{display:flex;flex:0 0 auto;height:30px;border:1px solid var(--color-border-input);background:var(--app-input-bg)}
|
||
.view-switch button{display:grid;width:32px;min-height:0;height:28px;place-items:center;padding:0;border:0;border-radius:0;color:var(--color-ink-muted);background:transparent}
|
||
.view-switch button+button{border-left:1px solid var(--color-border-subtle)}
|
||
.view-switch button.active{color:var(--color-accent);background:color-mix(in srgb,var(--color-accent) 12%,var(--app-input-bg));box-shadow:inset 0 -2px var(--color-accent)}
|
||
.view-switch button:hover{color:var(--color-ink);background:var(--color-surface-hover)}
|
||
.view-switch button:focus-visible{outline:2px solid var(--color-accent);outline-offset:2px}
|
||
/* Approved list design: shared columns across every category. */
|
||
.table-view{--repo-list-columns:minmax(180px,1.8fr) minmax(140px,1fr) minmax(150px,1fr) minmax(110px,.75fr)}
|
||
.table-view .repo-card{display:grid;grid-template-columns:minmax(0,1fr) minmax(170px,22%)}
|
||
.list-view .card-main{display:grid;grid-template-columns:var(--repo-list-columns);gap:16px;padding:0 18px;align-items:center}
|
||
.repo-grid.list-view{display:block;border:0}
|
||
.table-view .repo-card{height:auto;min-height:48px;border:0;border-bottom:1px solid var(--color-border-subtle);background:transparent}
|
||
.table-view .repo-card:hover,.table-view .repo-card:focus-within{background:color-mix(in srgb,var(--color-accent) 5%,var(--color-surface))}
|
||
.table-view .repo-card:focus-within{box-shadow:inset 2px 0 var(--color-accent)}
|
||
.list-view .card-main{height:auto;min-height:48px;min-width:0;justify-content:stretch;align-content:center;text-align:left}
|
||
.list-view .card-title{min-width:0;gap:9px}.list-view .card-title strong{font-size:12px;font-weight:500}.list-view .card-title :global(svg){flex:none}
|
||
.list-view .card-branch{min-width:0;font-size:12px}.list-view .card-branch :global(svg){flex:none}
|
||
.list-view .card-status{margin:0;gap:5px;font-size:12px;flex-wrap:nowrap;white-space:nowrap}.list-view .card-status .muted{font-style:italic}
|
||
.list-sync{display:flex;align-items:center;gap:18px;color:var(--color-ink-faint);font-size:12px}.list-sync>span{display:flex;align-items:center;gap:5px}
|
||
.list-view .card-actions{position:static;display:grid;grid-template-columns:minmax(60px,1fr) 40px 40px;gap:0;align-items:center;margin:0}
|
||
.list-view .card-actions .pr-badge{justify-self:start;justify-content:flex-start;padding:0;gap:8px;color:var(--color-accent)}
|
||
.list-view .pr-badge span{min-width:18px;padding:1px 4px;border:1px solid var(--color-border);border-radius:2px;color:var(--color-ink-muted);font-size:11px;text-align:center}
|
||
.list-view .card-actions button{height:28px;width:28px}.list-view .card-actions button :global(svg){width:16px;height:16px}
|
||
.list-view .card-actions button.active :global(svg){fill:var(--color-accent)}
|
||
.table-view .category-empty{border:0;border-bottom:1px solid var(--color-border-subtle);background:transparent}
|
||
.list-view .card-actions .pr-badge{width:auto;min-width:60px}.list-view .card-actions .pr-badge :global(svg){flex:0 0 16px}
|
||
/* Approved tile layout: repository identity above a shared status footer. */
|
||
.tiles-view{gap:12px}
|
||
.tiles-view .repo-card{height:150px;border-radius:3px;background:color-mix(in srgb,var(--color-surface) 35%,var(--app-bg))}
|
||
.tiles-view .repo-card:focus-within{box-shadow:inset 3px 0 var(--color-accent)}
|
||
.tiles-view .card-main{display:grid;grid-template-columns:minmax(0,1fr) 104px 42px;grid-template-rows:24px 24px 1fr 30px;gap:0 10px;padding:16px 16px 10px;align-items:center;align-content:stretch}
|
||
.tiles-view .card-main:after{position:absolute;left:16px;right:16px;bottom:51px;height:1px;background:var(--color-border);content:"";pointer-events:none}
|
||
.tiles-view .card-title{grid-column:1/-1;grid-row:1;min-width:0;padding:0 60px 0 48px;max-width:100%;display:block}
|
||
.tiles-view .card-title strong{display:block;font-size:13px;font-weight:650;line-height:22px}
|
||
.tiles-view .card-title>:global(svg){position:absolute;top:21px;left:18px;width:32px;height:32px;color:var(--color-ink-muted);stroke-width:1.4}
|
||
.tiles-view .card-branch{grid-column:1/-1;grid-row:2;padding-left:48px;min-width:0;display:block;overflow:hidden;text-overflow:ellipsis;font-size:11px;line-height:22px}
|
||
.tiles-view .card-branch>:global(svg){display:inline-block;margin-right:6px;vertical-align:-2px}
|
||
.tiles-view .card-status{grid-column:1;grid-row:4;min-width:0;flex-wrap:nowrap;margin:0;font-size:11px;gap:0;white-space:nowrap}
|
||
.tiles-view .card-status>span{min-width:0;overflow:hidden;text-overflow:ellipsis}
|
||
.tiles-view .list-sync{grid-column:2;grid-row:4;gap:16px;font-size:11px}
|
||
.tiles-view .card-actions{position:static;display:contents}
|
||
.tiles-view .card-actions button{position:absolute;top:14px;right:14px;width:26px;height:26px}
|
||
.tiles-view .card-actions button[aria-pressed]{right:44px}
|
||
.tiles-view .card-actions .pr-badge{top:auto;bottom:12px;right:14px;width:42px;min-width:0;height:26px;gap:6px;padding:0;font-size:12px;color:var(--color-ink-muted)}
|
||
.tiles-view .card-actions .pr-badge.has-open-prs{color:var(--color-accent)}
|
||
.tiles-view .card-actions .pr-badge.pr-error{color:#e0a35b}
|
||
.tiles-view .card-actions .pr-badge>:global(svg){width:16px;height:16px;flex:none}
|
||
.tiles-view .card-actions button.active>:global(svg){fill:var(--color-accent)}
|
||
.table-view{--repo-list-columns:minmax(180px,1.8fr) minmax(140px,1fr) minmax(150px,1fr) minmax(110px,.75fr)}
|
||
.table-view .dashboard-section{min-width:900px}
|
||
.list-view .card-title strong,.list-view .card-branch,.list-view .card-status{font-size:13px}
|
||
.list-view .card-actions .pr-badge.pr-error{color:#e0a35b}
|
||
.list-view .card-actions .pr-badge:disabled{color:var(--color-ink-faint)}
|
||
@media(min-width:1001px) and (max-width:1150px){.tiles-view{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||
@media(max-width:420px){.tiles-view .card-main{grid-template-columns:minmax(0,1fr) 82px 34px;gap:0 6px;padding-right:12px;padding-left:12px}.tiles-view .list-sync{gap:8px}.tiles-view .card-title{padding-left:42px}.tiles-view .card-branch{padding-left:42px}.tiles-view .card-title>:global(svg){left:14px;width:28px;height:28px}.tiles-view .card-status{font-size:10px}}
|
||
.tiles-view .card-title strong{font-size:14px}
|
||
.tiles-view .card-branch,.tiles-view .card-status,.tiles-view .list-sync{font-size:12px}
|
||
.tiles-view .card-status .changed,.tiles-view .card-status .clean{color:var(--color-ink-muted)}
|
||
.tiles-view .card-status .changed>:global(svg){color:#eeb94e;flex:none}.tiles-view .card-status .clean>:global(svg){color:#68c878;flex:none}
|
||
.tiles-view .favorite-category .card-actions button[aria-pressed]{right:14px}
|
||
.tiles-view .favorite-category .card-title{padding-right:30px}
|
||
.list-view .card-status .changed,.list-view .card-status .clean{color:var(--color-ink-muted)}
|
||
.list-view .card-status .changed>:global(svg){color:#eeb94e}.list-view .card-status .clean>:global(svg){color:#68c878}
|
||
</style>
|