Pools: löschbar mit Zuordnungs-Auflösung, Neuanmelden über Keychain, Key-Status
- delete_pool löst Projektzuordnungen (Terminal-Config bleibt), Dialog zeigt betroffene Projekte - oauth_login: nur bei ungenutztem Pool, löscht vorher den suffixierten Keychain-Eintrag; Warn-Dialog mit keychain_status; oauth_refresh raus - PoolInfo.hasCredentials: apikey-Dateicheck; Button Key ändern/eintragen, einheitliche Button-Breite - Starten deaktiviert ohne Pool bzw. ohne Key (Tooltip unterscheidet) - Icon-Pfad im Terminal-Dialog gekürzt mit Hover-Popover
This commit is contained in:
+88
-28
@@ -264,6 +264,8 @@ struct PoolInfo {
|
||||
#[serde(rename = "credentialType")]
|
||||
credential_type: String,
|
||||
projects: Vec<String>,
|
||||
#[serde(rename = "hasCredentials")]
|
||||
has_credentials: bool,
|
||||
}
|
||||
|
||||
fn list_pools_in(paths: &Paths) -> Result<Vec<PoolInfo>, String> {
|
||||
@@ -278,10 +280,19 @@ fn list_pools_in(paths: &Paths) -> Result<Vec<PoolInfo>, String> {
|
||||
let raw = fs::read_to_string(&cfg_path).map_err(|e| e.to_string())?;
|
||||
let pool: Pool =
|
||||
serde_json::from_str(&raw).map_err(|e| format!("{}: {e}", cfg_path.display()))?;
|
||||
// oauth: Credentials liegen im Keychain, dessen Prüfung wäre ein
|
||||
// security-Aufruf pro Pool im 3-s-Polling — deshalb hier immer true.
|
||||
let has_credentials = match pool.credential_type.as_str() {
|
||||
"apikey" => fs::read_to_string(entry.path().join(APIKEY_FILE))
|
||||
.map(|s| !s.trim().is_empty())
|
||||
.unwrap_or(false),
|
||||
_ => true,
|
||||
};
|
||||
pools.push(PoolInfo {
|
||||
projects: projects_using_pool(paths, &pool.name)?,
|
||||
name: pool.name,
|
||||
credential_type: pool.credential_type,
|
||||
has_credentials,
|
||||
});
|
||||
}
|
||||
pools.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
@@ -346,14 +357,15 @@ fn create_oauth_pool_in(
|
||||
}
|
||||
|
||||
/// Löscht einen Pool samt Ordner (inkl. Credentials); nur wenn kein Projekt ihn nutzt.
|
||||
/// Löscht den Pool; Projekte, die ihn zugeordnet hatten, verlieren die
|
||||
/// Zuordnung (Terminal-Einstellungen bleiben erhalten).
|
||||
fn delete_pool_in(paths: &Paths, name: &str) -> Result<(), String> {
|
||||
let dir = paths.pool_dir(name);
|
||||
if !dir.join(POOL_FILE).is_file() {
|
||||
return Err(format!("Pool nicht gefunden: {name}"));
|
||||
}
|
||||
let users = projects_using_pool(paths, name)?;
|
||||
if !users.is_empty() {
|
||||
return Err(format!("Pool ist Projekten zugeordnet: {}", users.join(", ")));
|
||||
for project in projects_using_pool(paths, name)? {
|
||||
unassign_pool_in(paths, &project)?;
|
||||
}
|
||||
fs::remove_dir_all(&dir).map_err(|e| e.to_string())
|
||||
}
|
||||
@@ -441,29 +453,59 @@ fn set_apikey(pool: String, key: String) -> Result<(), String> {
|
||||
set_apikey_in(&Paths::real(), &pool, &key)
|
||||
}
|
||||
|
||||
/// Meldet einen bestehenden oauth-Pool neu an.
|
||||
#[tauri::command]
|
||||
fn oauth_login(pool: String) -> Result<(), String> {
|
||||
run_oauth_flow(&oauth_credential_path(&Paths::real(), &pool)?)
|
||||
/// Keychain-Service-Name des Pools: claude legt pro CLAUDE_CONFIG_DIR einen
|
||||
/// suffixierten Eintrag an, Suffix = erste 8 Hex-Zeichen von SHA-256 über
|
||||
/// den Pool-Pfad.
|
||||
fn keychain_service(paths: &Paths, pool: &str) -> String {
|
||||
let hash = Sha256::digest(paths.pool_dir(pool).to_string_lossy().as_bytes());
|
||||
let hex: String = hash.iter().map(|b| format!("{b:02x}")).collect();
|
||||
format!("Claude Code-credentials-{}", &hex[..8])
|
||||
}
|
||||
|
||||
/// Erneuert das Access-Token über das gespeicherte Refresh-Token.
|
||||
#[tauri::command]
|
||||
fn oauth_refresh(pool: String) -> Result<(), String> {
|
||||
let cred_path = oauth_credential_path(&Paths::real(), &pool)?;
|
||||
let raw = fs::read_to_string(&cred_path)
|
||||
.map_err(|e| format!("{}: {e}", cred_path.display()))?;
|
||||
let v: serde_json::Value = serde_json::from_str(&raw).map_err(|e| e.to_string())?;
|
||||
let refresh = v["claudeAiOauth"]["refreshToken"]
|
||||
.as_str()
|
||||
.ok_or("kein refreshToken in Credential-Datei")?;
|
||||
fn keychain_entry_exists(service: &str) -> Result<bool, String> {
|
||||
let out = Command::new("security")
|
||||
.args(["find-generic-password", "-s", service])
|
||||
.output()
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(out.status.success())
|
||||
}
|
||||
|
||||
let body = serde_json::json!({
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": refresh,
|
||||
"client_id": CLIENT_ID,
|
||||
});
|
||||
exchange_and_store(body, &cred_path)
|
||||
#[tauri::command]
|
||||
fn keychain_status(pool: String) -> Result<bool, String> {
|
||||
keychain_entry_exists(&keychain_service(&Paths::real(), &pool))
|
||||
}
|
||||
|
||||
/// Meldet einen oauth-Pool neu an: nur bei ungenutztem Pool. Löscht vorher
|
||||
/// den suffixierten Keychain-Eintrag, damit der neue Login beim nächsten
|
||||
/// Session-Start auch greift (claude importiert die frische
|
||||
/// .credentials.json und legt den Eintrag neu an).
|
||||
#[tauri::command]
|
||||
fn oauth_login(pool: String) -> Result<(), String> {
|
||||
let paths = Paths::real();
|
||||
let cred_path = oauth_credential_path(&paths, &pool)?;
|
||||
|
||||
let running: Vec<String> = projects_using_pool(&paths, &pool)?
|
||||
.into_iter()
|
||||
.filter(|p| is_running(p))
|
||||
.collect();
|
||||
if !running.is_empty() {
|
||||
return Err(format!(
|
||||
"Neuanmeldung nur bei ungenutztem Pool möglich — läuft: {}",
|
||||
running.join(", ")
|
||||
));
|
||||
}
|
||||
|
||||
let service = keychain_service(&paths, &pool);
|
||||
if keychain_entry_exists(&service)? {
|
||||
let out = Command::new("security")
|
||||
.args(["delete-generic-password", "-s", &service])
|
||||
.output()
|
||||
.map_err(|e| e.to_string())?;
|
||||
if !out.status.success() {
|
||||
return Err(String::from_utf8_lossy(&out.stderr).trim().to_string());
|
||||
}
|
||||
}
|
||||
run_oauth_flow(&cred_path)
|
||||
}
|
||||
|
||||
fn open_project(project: &str) -> Result<(), String> {
|
||||
@@ -841,7 +883,7 @@ fn main_builder() -> tauri::Builder<tauri::Wry> {
|
||||
stop_project,
|
||||
restart_project,
|
||||
oauth_login,
|
||||
oauth_refresh,
|
||||
keychain_status,
|
||||
set_apikey,
|
||||
terminal::open_terminal
|
||||
])
|
||||
@@ -1037,14 +1079,32 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pool_loeschen_mit_zuordnung_scheitert() {
|
||||
fn pool_loeschen_loest_zuordnungen() {
|
||||
let p = tmp_paths();
|
||||
create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
|
||||
create_project_in(&p, "proj").unwrap();
|
||||
assign_pool_in(&p, "proj", "kunde").unwrap();
|
||||
let err = delete_pool_in(&p, "kunde").unwrap_err();
|
||||
assert!(err.contains("proj"));
|
||||
assert!(p.pool_dir("kunde").exists());
|
||||
delete_pool_in(&p, "kunde").unwrap();
|
||||
assert!(!p.pool_dir("kunde").exists());
|
||||
assert!(!p.project_config("proj").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pool_loeschen_erhaelt_terminal_config() {
|
||||
let p = tmp_paths();
|
||||
create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
|
||||
create_project_in(&p, "proj").unwrap();
|
||||
assign_pool_in(&p, "proj", "kunde").unwrap();
|
||||
set_terminal_config_in(
|
||||
&p,
|
||||
"proj",
|
||||
TerminalConfig { theme: Some("dracula".into()), icon: None, title: None },
|
||||
)
|
||||
.unwrap();
|
||||
delete_pool_in(&p, "kunde").unwrap();
|
||||
let cfg = read_project_config_in(&p, "proj").unwrap();
|
||||
assert_eq!(cfg.pool, None);
|
||||
assert_eq!(cfg.terminal.theme.as_deref(), Some("dracula"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+84
-26
@@ -9,6 +9,7 @@ interface Pool {
|
||||
name: string;
|
||||
credentialType: string;
|
||||
projects: string[];
|
||||
hasCredentials: boolean;
|
||||
}
|
||||
|
||||
const pools = ref<Pool[]>([]);
|
||||
@@ -79,6 +80,10 @@ async function submit() {
|
||||
|
||||
const deleteName = ref<string | null>(null);
|
||||
|
||||
const deleteProjects = computed(
|
||||
() => pools.value.find((p) => p.name === deleteName.value)?.projects ?? [],
|
||||
);
|
||||
|
||||
function askDelete(pool: string) {
|
||||
error.value = "";
|
||||
deleteName.value = pool;
|
||||
@@ -100,26 +105,42 @@ async function confirmDelete() {
|
||||
}
|
||||
}
|
||||
|
||||
async function relogin(pool: string) {
|
||||
interface Project {
|
||||
name: string;
|
||||
pool: string | null;
|
||||
running: boolean;
|
||||
}
|
||||
|
||||
const reloginPool = ref<string | null>(null);
|
||||
const reloginHasEntry = ref(false);
|
||||
const reloginRunning = ref<string[]>([]);
|
||||
|
||||
async function askRelogin(pool: string) {
|
||||
error.value = "";
|
||||
try {
|
||||
reloginHasEntry.value = await invoke<boolean>("keychain_status", { pool });
|
||||
const projects = await invoke<Project[]>("list_projects");
|
||||
reloginRunning.value = projects
|
||||
.filter((p) => p.pool === pool && p.running)
|
||||
.map((p) => p.name);
|
||||
reloginPool.value = pool;
|
||||
} catch (e) {
|
||||
error.value = String(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmRelogin() {
|
||||
const pool = reloginPool.value;
|
||||
if (!pool) return;
|
||||
busy.value = true;
|
||||
error.value = "";
|
||||
try {
|
||||
await invoke("oauth_login", { pool });
|
||||
reloginPool.value = null;
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
error.value = String(e);
|
||||
} finally {
|
||||
busy.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function renew(pool: string) {
|
||||
busy.value = true;
|
||||
error.value = "";
|
||||
try {
|
||||
await invoke("oauth_refresh", { pool });
|
||||
} catch (e) {
|
||||
error.value = String(e);
|
||||
reloginPool.value = null;
|
||||
} finally {
|
||||
busy.value = false;
|
||||
}
|
||||
@@ -164,7 +185,7 @@ onMounted(refresh);
|
||||
<span class="badge" :class="p.credentialType">{{ p.credentialType }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="p.projects.length" class="assigned">
|
||||
<span v-if="p.projects.length" class="assigned hover-pop">
|
||||
{{ $t("pools.assigned", p.projects.length) }}
|
||||
<span class="pop">
|
||||
<span v-for="name in p.projects" :key="name" class="pop-item">
|
||||
@@ -176,24 +197,16 @@ onMounted(refresh);
|
||||
<td class="cell-actions">
|
||||
<span class="row-actions">
|
||||
<template v-if="p.credentialType === 'oauth'">
|
||||
<button :disabled="busy" @click="relogin(p.name)">
|
||||
<button class="w-action" :disabled="busy" @click="askRelogin(p.name)">
|
||||
{{ $t("pools.relogin") }}
|
||||
</button>
|
||||
<button :disabled="busy" @click="renew(p.name)">
|
||||
{{ $t("pools.renewToken") }}
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button :disabled="busy" @click="openEditKey(p.name)">
|
||||
{{ $t("pools.changeKey") }}
|
||||
<button class="w-action" :disabled="busy" @click="openEditKey(p.name)">
|
||||
{{ p.hasCredentials ? $t("pools.changeKey") : $t("pools.insertKey") }}
|
||||
</button>
|
||||
</template>
|
||||
<button
|
||||
v-if="!p.projects.length"
|
||||
class="danger"
|
||||
:disabled="busy"
|
||||
@click="askDelete(p.name)"
|
||||
>
|
||||
<button class="danger" :disabled="busy" @click="askDelete(p.name)">
|
||||
{{ $t("pools.delete") }}
|
||||
</button>
|
||||
</span>
|
||||
@@ -241,10 +254,55 @@ onMounted(refresh);
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="reloginPool"
|
||||
class="overlay"
|
||||
@click.self="busy ? undefined : (reloginPool = null)"
|
||||
>
|
||||
<div class="dialog">
|
||||
<h3>{{ $t("pools.reloginTitle", { name: reloginPool }) }}</h3>
|
||||
<template v-if="reloginRunning.length">
|
||||
<p class="hint">{{ $t("pools.reloginBlocked") }}</p>
|
||||
<ul class="affected">
|
||||
<li v-for="name in reloginRunning" :key="name">{{ name }}</li>
|
||||
</ul>
|
||||
<div class="actions">
|
||||
<button type="button" @click="reloginPool = null">
|
||||
{{ $t("pools.cancel") }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p class="hint">
|
||||
{{
|
||||
reloginHasEntry
|
||||
? $t("pools.reloginWarning", { name: reloginPool })
|
||||
: $t("pools.reloginNoEntry")
|
||||
}}
|
||||
</p>
|
||||
<p v-if="busy" class="hint busy">{{ $t("pools.oauthWaiting") }}</p>
|
||||
<div class="actions">
|
||||
<button type="button" :disabled="busy" @click="reloginPool = null">
|
||||
{{ $t("pools.cancel") }}
|
||||
</button>
|
||||
<button class="primary" :disabled="busy" @click="confirmRelogin">
|
||||
{{ $t("pools.startLogin") }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="deleteName" class="overlay" @click.self="deleteName = null">
|
||||
<div class="dialog">
|
||||
<h3>{{ $t("pools.deletePool") }}</h3>
|
||||
<p class="hint">{{ $t("pools.deleteWarning", { name: deleteName }) }}</p>
|
||||
<template v-if="deleteProjects.length">
|
||||
<p class="hint">{{ $t("pools.deleteUnassigns") }}</p>
|
||||
<ul class="affected">
|
||||
<li v-for="name in deleteProjects" :key="name">{{ name }}</li>
|
||||
</ul>
|
||||
</template>
|
||||
<div class="actions">
|
||||
<button type="button" :disabled="busy" @click="deleteName = null">
|
||||
{{ $t("pools.cancel") }}
|
||||
|
||||
@@ -13,8 +13,19 @@ interface Project {
|
||||
|
||||
interface Pool {
|
||||
name: string;
|
||||
credentialFile: string;
|
||||
credentialType: string;
|
||||
hasCredentials: boolean;
|
||||
}
|
||||
|
||||
function poolReady(p: Project): boolean {
|
||||
if (!p.pool) return false;
|
||||
const pool = pools.value.find((x) => x.name === p.pool);
|
||||
return pool !== undefined && pool.hasCredentials;
|
||||
}
|
||||
|
||||
function startBlockedReason(p: Project): string | undefined {
|
||||
if (poolReady(p)) return undefined;
|
||||
return p.pool ? "projects.needsKey" : "projects.needsPool";
|
||||
}
|
||||
|
||||
const projects = ref<Project[]>([]);
|
||||
@@ -192,7 +203,13 @@ onUnmounted(() => window.clearInterval(timer));
|
||||
<button v-if="p.running" class="stop" @click="stop(p)">
|
||||
{{ $t("projects.stop") }}
|
||||
</button>
|
||||
<button v-else class="start" @click="openTerminal(p)">
|
||||
<button
|
||||
v-else
|
||||
class="start"
|
||||
:disabled="!poolReady(p)"
|
||||
:title="startBlockedReason(p) && $t(startBlockedReason(p)!)"
|
||||
@click="openTerminal(p)"
|
||||
>
|
||||
{{ $t("projects.start") }}
|
||||
</button>
|
||||
</span>
|
||||
@@ -223,9 +240,11 @@ onUnmounted(() => window.clearInterval(timer));
|
||||
<button v-if="settings.icon" @click="settings.icon = null">
|
||||
{{ $t("projects.remove") }}
|
||||
</button>
|
||||
<span class="icon-path">
|
||||
{{ settings.icon ?? $t("projects.defaultIcon") }}
|
||||
<span v-if="settings.icon" class="icon-path hover-pop">
|
||||
{{ settings.icon.split("/").pop() }}
|
||||
<span class="pop pop-path">{{ settings.icon }}</span>
|
||||
</span>
|
||||
<span v-else class="icon-path">{{ $t("projects.defaultIcon") }}</span>
|
||||
</span>
|
||||
</label>
|
||||
<p class="hint">{{ $t("projects.appliesNextStart") }}</p>
|
||||
|
||||
+22
-2
@@ -32,6 +32,8 @@ const de = {
|
||||
cancel: "Abbrechen",
|
||||
save: "Speichern",
|
||||
noPoolAssigned: "kein Pool",
|
||||
needsPool: "Kein Pool zugeordnet",
|
||||
needsKey: "Pool hat keinen API-Key",
|
||||
},
|
||||
pools: {
|
||||
pool: "Pool",
|
||||
@@ -40,8 +42,8 @@ const de = {
|
||||
newOauth: "+ oAuth",
|
||||
newApikey: "+ apiKey",
|
||||
relogin: "Neu anmelden",
|
||||
renewToken: "Token erneuern",
|
||||
changeKey: "Key ändern",
|
||||
insertKey: "Key eintragen",
|
||||
delete: "Löschen",
|
||||
assigned: "kein Projekt | 1 Projekt | {count} Projekte",
|
||||
empty: "Noch keine Pools angelegt.",
|
||||
@@ -59,6 +61,14 @@ const de = {
|
||||
deletePool: "Pool löschen",
|
||||
deleteWarning:
|
||||
"Pool {name} und seine Credential-Datei werden gelöscht. Das lässt sich nicht rückgängig machen.",
|
||||
deleteUnassigns: "Diese Projekte verlieren ihre Pool-Zuordnung:",
|
||||
reloginTitle: "Neu anmelden – {name}",
|
||||
reloginWarning:
|
||||
"Der im Schlüsselbund gespeicherte Zugriffstoken von {name} wird gelöscht. Danach öffnet sich der Browser zur Neuanmeldung.",
|
||||
reloginNoEntry:
|
||||
"Kein Schlüsselbund-Eintrag vorhanden — es erfolgt nur die Neuanmeldung im Browser.",
|
||||
reloginBlocked:
|
||||
"Neuanmeldung ist nur bei ungenutztem Pool möglich. Diese Projekte laufen noch:",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -94,6 +104,8 @@ const en: typeof de = {
|
||||
cancel: "Cancel",
|
||||
save: "Save",
|
||||
noPoolAssigned: "no pool",
|
||||
needsPool: "No pool assigned",
|
||||
needsKey: "Pool has no API key",
|
||||
},
|
||||
pools: {
|
||||
pool: "Pool",
|
||||
@@ -102,8 +114,8 @@ const en: typeof de = {
|
||||
newOauth: "+ oAuth",
|
||||
newApikey: "+ apiKey",
|
||||
relogin: "Sign in again",
|
||||
renewToken: "Renew token",
|
||||
changeKey: "Change key",
|
||||
insertKey: "Insert key",
|
||||
delete: "Delete",
|
||||
assigned: "no projects | 1 project | {count} projects",
|
||||
empty: "No pools yet.",
|
||||
@@ -121,6 +133,14 @@ const en: typeof de = {
|
||||
deletePool: "Delete pool",
|
||||
deleteWarning:
|
||||
"Pool {name} and its credential file will be deleted. This cannot be undone.",
|
||||
deleteUnassigns: "These projects lose their pool assignment:",
|
||||
reloginTitle: "Sign in again – {name}",
|
||||
reloginWarning:
|
||||
"The access token for {name} stored in the keychain will be deleted. The browser then opens for a new sign-in.",
|
||||
reloginNoEntry:
|
||||
"No keychain entry present — only the new sign-in happens.",
|
||||
reloginBlocked:
|
||||
"Signing in again requires an unused pool. These projects are still running:",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
+41
-10
@@ -204,6 +204,10 @@ button.danger:hover:not(:disabled) {
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.row-actions .w-action {
|
||||
width: 9.5rem;
|
||||
}
|
||||
|
||||
button.gear {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -238,7 +242,7 @@ col.col-actions {
|
||||
width: 15rem;
|
||||
}
|
||||
col.col-actions-wide {
|
||||
width: 19rem;
|
||||
width: 16rem;
|
||||
}
|
||||
col.col-type {
|
||||
width: 7rem;
|
||||
@@ -330,23 +334,20 @@ td select {
|
||||
color: var(--peach);
|
||||
}
|
||||
|
||||
/* Zugeordnete Projekte: Anzahl in der Zeile, Liste im Hover. */
|
||||
.assigned {
|
||||
/* Hover-Popover: kompakter Text in der Zeile, Details im Hover. */
|
||||
.hover-pop {
|
||||
position: relative;
|
||||
color: var(--subtext);
|
||||
font-size: 0.8rem;
|
||||
cursor: default;
|
||||
border-bottom: 1px dotted var(--surface2);
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
.assigned .pop {
|
||||
.hover-pop .pop {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
left: 0;
|
||||
z-index: 20;
|
||||
min-width: 13rem;
|
||||
max-width: 26rem;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
background: var(--crust);
|
||||
@@ -356,10 +357,26 @@ td select {
|
||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
|
||||
.assigned:hover .pop {
|
||||
.hover-pop:hover .pop {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.pop-path {
|
||||
font-family: var(--mono);
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
color: var(--text);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* Zugeordnete Projekte: Anzahl in der Zeile, Liste im Hover. */
|
||||
.assigned {
|
||||
color: var(--subtext);
|
||||
font-size: 0.8rem;
|
||||
border-bottom: 1px dotted var(--surface2);
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
.pop-item {
|
||||
font-family: var(--mono);
|
||||
font-size: 0.8rem;
|
||||
@@ -442,7 +459,10 @@ td select {
|
||||
font-family: var(--mono);
|
||||
color: var(--overlay);
|
||||
font-size: 0.75rem;
|
||||
word-break: break-all;
|
||||
max-width: 11rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dialog .pools {
|
||||
@@ -470,6 +490,17 @@ td select {
|
||||
color: var(--overlay);
|
||||
}
|
||||
|
||||
.dialog .affected {
|
||||
margin: 0;
|
||||
padding-left: 1.1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
font-family: var(--mono);
|
||||
font-size: 0.8rem;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.dialog .actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
Reference in New Issue
Block a user