Initial commit: Claude Live Dashboard
Always-on-Top Electron-Widget für die Claude-Plan-Auslastung unter Windows. Liest Transkripte lokal, fragt optional den /usage-Endpunkt ab, bietet einen Einstellungsdialog und lässt sich als NSIS-Installer oder portable Fassung bauen.
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Einstellungsdialog. Liest und schreibt dieselbe config.json, die auch von
|
||||
* Hand bearbeitet werden kann — unbekannte Felder und die `_`-Kommentare
|
||||
* bleiben dabei erhalten.
|
||||
*/
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
||||
const el = {
|
||||
path: $('config-path'),
|
||||
close: $('btn-close'),
|
||||
save: $('btn-save'),
|
||||
revert: $('btn-revert'),
|
||||
openFile: $('btn-open-file'),
|
||||
feedback: $('feedback'),
|
||||
|
||||
opacity: $('opacity'),
|
||||
opacityOut: $('opacity-out'),
|
||||
clickThrough: $('clickThrough'),
|
||||
autostart: $('autostart'),
|
||||
|
||||
oauthEnabled: $('oauthEnabled'),
|
||||
oauthEndpoint: $('oauthEndpoint'),
|
||||
|
||||
limitWeek: $('limitWeek'),
|
||||
limitBlock: $('limitBlock'),
|
||||
|
||||
wInput: $('wInput'),
|
||||
wOutput: $('wOutput'),
|
||||
wCacheCreate: $('wCacheCreate'),
|
||||
wCacheRead: $('wCacheRead'),
|
||||
mOpus: $('mOpus'),
|
||||
mSonnet: $('mSonnet'),
|
||||
mHaiku: $('mHaiku'),
|
||||
|
||||
weekday: $('weekday'),
|
||||
resetHour: $('resetHour'),
|
||||
resetMinute: $('resetMinute'),
|
||||
};
|
||||
|
||||
/** Voreinstellungen, die auch der Main-Prozess verwendet. */
|
||||
const DEFAULTS = {
|
||||
opacity: 1,
|
||||
tokenWeights: { input: 1, output: 5, cacheCreate: 1.25, cacheRead: 0.1 },
|
||||
modelWeights: { opus: 5, sonnet: 1, haiku: 0.3 },
|
||||
};
|
||||
|
||||
let feedbackTimer = null;
|
||||
|
||||
function say(text, kind = 'ok') {
|
||||
el.feedback.hidden = false;
|
||||
el.feedback.textContent = text;
|
||||
el.feedback.dataset.kind = kind;
|
||||
clearTimeout(feedbackTimer);
|
||||
feedbackTimer = setTimeout(() => {
|
||||
el.feedback.hidden = true;
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
/** Zahl aus einem Feld; leer ergibt null. */
|
||||
function num(input) {
|
||||
const raw = input.value.trim();
|
||||
if (raw === '') return null;
|
||||
const value = Number(raw.replace(',', '.'));
|
||||
return Number.isFinite(value) ? value : NaN;
|
||||
}
|
||||
|
||||
function setNum(input, value, fallback = '') {
|
||||
input.value = value == null ? fallback : String(value);
|
||||
}
|
||||
|
||||
/** Füllt die Oberfläche aus der Konfiguration. */
|
||||
function fill(config, meta) {
|
||||
el.path.textContent = meta.configPath;
|
||||
|
||||
const opacity = Math.round((config.opacity ?? DEFAULTS.opacity) * 100);
|
||||
el.opacity.value = String(opacity);
|
||||
el.opacityOut.textContent = `${opacity} %`;
|
||||
el.clickThrough.checked = Boolean(config.clickThrough);
|
||||
el.autostart.checked = Boolean(meta.autostart);
|
||||
|
||||
const oauth = config.oauth || {};
|
||||
el.oauthEnabled.checked = oauth.enabled !== false;
|
||||
el.oauthEndpoint.value = oauth.endpoint || '';
|
||||
|
||||
const limits = config.limits || {};
|
||||
setNum(el.limitWeek, limits.week);
|
||||
setNum(el.limitBlock, limits.block);
|
||||
|
||||
const tw = { ...DEFAULTS.tokenWeights, ...(config.tokenWeights || {}) };
|
||||
setNum(el.wInput, tw.input);
|
||||
setNum(el.wOutput, tw.output);
|
||||
setNum(el.wCacheCreate, tw.cacheCreate);
|
||||
setNum(el.wCacheRead, tw.cacheRead);
|
||||
|
||||
const mw = { ...DEFAULTS.modelWeights, ...(config.modelWeights || {}) };
|
||||
setNum(el.mOpus, mw.opus);
|
||||
setNum(el.mSonnet, mw.sonnet);
|
||||
setNum(el.mHaiku, mw.haiku);
|
||||
|
||||
el.weekday.value = config.weekResetWeekday == null ? '' : String(config.weekResetWeekday);
|
||||
setNum(el.resetHour, config.weekResetHour, '0');
|
||||
setNum(el.resetMinute, config.weekResetMinute, '0');
|
||||
|
||||
updateWeekFields();
|
||||
}
|
||||
|
||||
/** Reset-Zeit ist ohne gewählten Wochentag bedeutungslos. */
|
||||
function updateWeekFields() {
|
||||
const off = el.weekday.value === '';
|
||||
el.resetHour.disabled = off;
|
||||
el.resetMinute.disabled = off;
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut das Änderungsobjekt. Nur gesetzte Werte werden übernommen — so bleiben
|
||||
* Felder, die der Dialog nicht kennt, in der Datei unangetastet.
|
||||
* @returns {{patch: Object, errors: string[]}}
|
||||
*/
|
||||
function collect() {
|
||||
const errors = [];
|
||||
const patch = {};
|
||||
|
||||
const check = (input, label, { min = 0, max = Infinity, required = false } = {}) => {
|
||||
const value = num(input);
|
||||
const invalid =
|
||||
Number.isNaN(value) || (value != null && (value < min || value > max)) || (required && value == null);
|
||||
input.setAttribute('aria-invalid', invalid ? 'true' : 'false');
|
||||
if (invalid) errors.push(label);
|
||||
return invalid ? undefined : value;
|
||||
};
|
||||
|
||||
patch.opacity = Number(el.opacity.value) / 100;
|
||||
patch.clickThrough = el.clickThrough.checked;
|
||||
|
||||
patch.oauth = { enabled: el.oauthEnabled.checked };
|
||||
const endpoint = el.oauthEndpoint.value.trim();
|
||||
if (endpoint) {
|
||||
if (!/^https:\/\/[^\s]+$/.test(endpoint)) errors.push('Endpunkt');
|
||||
else patch.oauth.endpoint = endpoint;
|
||||
} else {
|
||||
patch.oauth.endpoint = '';
|
||||
}
|
||||
|
||||
const week = check(el.limitWeek, 'Bezugsgröße Woche');
|
||||
const block = check(el.limitBlock, 'Bezugsgröße 5-Stunden-Fenster');
|
||||
patch.limits = {};
|
||||
if (week) patch.limits.week = week;
|
||||
if (block) patch.limits.block = block;
|
||||
|
||||
patch.tokenWeights = {
|
||||
input: check(el.wInput, 'Gewicht Eingabe', { required: true }),
|
||||
output: check(el.wOutput, 'Gewicht Ausgabe', { required: true }),
|
||||
cacheCreate: check(el.wCacheCreate, 'Gewicht Cache anlegen', { required: true }),
|
||||
cacheRead: check(el.wCacheRead, 'Gewicht Cache lesen', { required: true }),
|
||||
};
|
||||
patch.modelWeights = {
|
||||
opus: check(el.mOpus, 'Gewicht Opus', { required: true }),
|
||||
sonnet: check(el.mSonnet, 'Gewicht Sonnet', { required: true }),
|
||||
haiku: check(el.mHaiku, 'Gewicht Haiku', { required: true }),
|
||||
};
|
||||
|
||||
if (el.weekday.value === '') {
|
||||
patch.weekResetWeekday = null;
|
||||
patch.weekResetHour = null;
|
||||
patch.weekResetMinute = null;
|
||||
} else {
|
||||
patch.weekResetWeekday = Number(el.weekday.value);
|
||||
patch.weekResetHour = check(el.resetHour, 'Reset-Stunde', { max: 23, required: true });
|
||||
patch.weekResetMinute = check(el.resetMinute, 'Reset-Minute', { max: 59, required: true });
|
||||
}
|
||||
|
||||
return { patch, errors };
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const { config, meta } = await window.settings.load();
|
||||
fill(config, meta);
|
||||
}
|
||||
|
||||
el.opacity.addEventListener('input', () => {
|
||||
el.opacityOut.textContent = `${el.opacity.value} %`;
|
||||
});
|
||||
|
||||
el.weekday.addEventListener('change', updateWeekFields);
|
||||
|
||||
// Autostart ist keine Einstellung der Datei, sondern ein Systemzustand —
|
||||
// deshalb sofort wirksam statt beim Speichern.
|
||||
el.autostart.addEventListener('change', async () => {
|
||||
await window.settings.setAutostart(el.autostart.checked);
|
||||
say(el.autostart.checked ? 'Autostart aktiviert.' : 'Autostart deaktiviert.');
|
||||
});
|
||||
|
||||
el.save.addEventListener('click', async () => {
|
||||
const { patch, errors } = collect();
|
||||
if (errors.length) {
|
||||
say(`Bitte prüfen: ${errors.join(', ')}`, 'error');
|
||||
return;
|
||||
}
|
||||
el.save.disabled = true;
|
||||
const result = await window.settings.save(patch);
|
||||
el.save.disabled = false;
|
||||
say(result.ok ? 'Gespeichert und übernommen.' : `Fehler: ${result.error}`, result.ok ? 'ok' : 'error');
|
||||
});
|
||||
|
||||
el.revert.addEventListener('click', async () => {
|
||||
await load();
|
||||
say('Änderungen verworfen.');
|
||||
});
|
||||
|
||||
el.openFile.addEventListener('click', () => window.settings.openFile());
|
||||
el.close.addEventListener('click', () => window.settings.close());
|
||||
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') window.settings.close();
|
||||
if (event.key === 's' && (event.ctrlKey || event.metaKey)) {
|
||||
event.preventDefault();
|
||||
el.save.click();
|
||||
}
|
||||
});
|
||||
|
||||
load();
|
||||
@@ -0,0 +1,157 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'self'; script-src 'self';" />
|
||||
<title>Einstellungen</title>
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<header class="titlebar">
|
||||
<span class="title">Einstellungen</span>
|
||||
<span class="spacer"></span>
|
||||
<button class="icon-btn" id="btn-close" title="Schließen" aria-label="Schließen">✕</button>
|
||||
</header>
|
||||
|
||||
<main class="content">
|
||||
<p class="path" id="config-path">—</p>
|
||||
|
||||
<section class="group">
|
||||
<h2>Anzeige</h2>
|
||||
|
||||
<div class="row">
|
||||
<label for="opacity">Deckkraft</label>
|
||||
<div class="control">
|
||||
<input type="range" id="opacity" min="30" max="100" step="5" />
|
||||
<output id="opacity-out">100 %</output>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label for="clickThrough">Klicks durchlassen</label>
|
||||
<div class="control">
|
||||
<input type="checkbox" id="clickThrough" />
|
||||
<span class="hint">Das Widget fängt keine Mausklicks mehr ab.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label for="autostart">Mit Windows starten</label>
|
||||
<div class="control">
|
||||
<input type="checkbox" id="autostart" />
|
||||
<span class="hint">Wird sofort übernommen, unabhängig vom Speichern.</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="group">
|
||||
<h2>Datenquelle</h2>
|
||||
|
||||
<div class="row">
|
||||
<label for="oauthEnabled">Serverabfrage</label>
|
||||
<div class="control">
|
||||
<input type="checkbox" id="oauthEnabled" />
|
||||
<span class="hint">
|
||||
Liefert exakte Prozentwerte und Reset-Zeiten. Abgeschaltet rechnet
|
||||
das Dashboard nur lokal — dann sind alle Werte Näherungen.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label for="oauthEndpoint">Endpunkt</label>
|
||||
<div class="control">
|
||||
<input type="text" id="oauthEndpoint" placeholder="Standard verwenden" spellcheck="false" />
|
||||
<span class="hint">Nur ausfüllen, um den Standard zu überschreiben.</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="group">
|
||||
<h2>Bezugsgrößen <span class="tag">Rückfallebene</span></h2>
|
||||
<p class="note">
|
||||
Werden nur benutzt, wenn die Serverabfrage ausfällt. Ermitteln mit
|
||||
<code>npm run calibrate</code>. Leer lassen für Auto-Kalibrierung.
|
||||
</p>
|
||||
|
||||
<div class="row">
|
||||
<label for="limitWeek">Woche</label>
|
||||
<div class="control">
|
||||
<input type="number" id="limitWeek" min="0" step="1000000" placeholder="automatisch" />
|
||||
<span class="hint">gewichtete Tokens für 100 %</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label for="limitBlock">5-Stunden-Fenster</label>
|
||||
<div class="control">
|
||||
<input type="number" id="limitBlock" min="0" step="1000000" placeholder="automatisch" />
|
||||
<span class="hint">gewichtete Tokens für 100 %</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="group">
|
||||
<h2>Gewichtung</h2>
|
||||
<p class="note">
|
||||
Wie stark Token-Arten und Modelle ins Kontingent zählen. Beeinflusst
|
||||
die lokale Rechnung und die Hochrechnung „reicht noch …“, nicht die
|
||||
Serverwerte.
|
||||
</p>
|
||||
|
||||
<div class="grid" id="weights">
|
||||
<label for="wInput">Eingabe</label><input type="number" id="wInput" min="0" step="0.05" />
|
||||
<label for="wOutput">Ausgabe</label><input type="number" id="wOutput" min="0" step="0.05" />
|
||||
<label for="wCacheCreate">Cache anlegen</label><input type="number" id="wCacheCreate" min="0" step="0.05" />
|
||||
<label for="wCacheRead">Cache lesen</label><input type="number" id="wCacheRead" min="0" step="0.05" />
|
||||
<label for="mOpus">Opus</label><input type="number" id="mOpus" min="0" step="0.1" />
|
||||
<label for="mSonnet">Sonnet</label><input type="number" id="mSonnet" min="0" step="0.1" />
|
||||
<label for="mHaiku">Haiku</label><input type="number" id="mHaiku" min="0" step="0.1" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="group">
|
||||
<h2>Wochenfenster <span class="tag">Rückfallebene</span></h2>
|
||||
<p class="note">
|
||||
Bei aktiver Serverabfrage wird der echte Reset-Zeitpunkt verwendet und
|
||||
diese Angaben bleiben ungenutzt.
|
||||
</p>
|
||||
|
||||
<div class="row">
|
||||
<label for="weekday">Reset-Tag</label>
|
||||
<div class="control">
|
||||
<select id="weekday">
|
||||
<option value="">rollierende 7 Tage</option>
|
||||
<option value="0">Sonntag</option>
|
||||
<option value="1">Montag</option>
|
||||
<option value="2">Dienstag</option>
|
||||
<option value="3">Mittwoch</option>
|
||||
<option value="4">Donnerstag</option>
|
||||
<option value="5">Freitag</option>
|
||||
<option value="6">Samstag</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label for="resetHour">Uhrzeit</label>
|
||||
<div class="control inline">
|
||||
<input type="number" id="resetHour" min="0" max="23" step="1" />
|
||||
<span class="colon">:</span>
|
||||
<input type="number" id="resetMinute" min="0" max="59" step="1" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="actions">
|
||||
<button class="btn ghost" id="btn-open-file">Datei öffnen</button>
|
||||
<span class="spacer"></span>
|
||||
<span class="feedback" id="feedback" hidden></span>
|
||||
<button class="btn ghost" id="btn-revert">Verwerfen</button>
|
||||
<button class="btn primary" id="btn-save">Speichern</button>
|
||||
</footer>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,315 @@
|
||||
/* Einstellungsfenster — gleiche Farbwelt wie das Widget, aber auf Lesbarkeit
|
||||
in einem größeren Fenster ausgelegt. */
|
||||
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
|
||||
--surface: #1e1e22;
|
||||
--surface-raised: rgba(255, 255, 255, 0.05);
|
||||
--surface-input: rgba(255, 255, 255, 0.06);
|
||||
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #c3c2b7;
|
||||
--text-muted: #898781;
|
||||
--hairline: rgba(255, 255, 255, 0.1);
|
||||
|
||||
--accent: #3987e5;
|
||||
--good: #0ca30c;
|
||||
--warning: #fab219;
|
||||
|
||||
--font: system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
background: var(--surface);
|
||||
color: var(--text-primary);
|
||||
font-family: var(--font);
|
||||
font-size: 13px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* ── Titelleiste ─────────────────────────────────────────────────────── */
|
||||
|
||||
.titlebar {
|
||||
-webkit-app-region: drag;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 9px 10px 9px 14px;
|
||||
border-bottom: 1px solid var(--hairline);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
-webkit-app-region: no-drag;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 22px;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon-btn:hover {
|
||||
background: var(--surface-raised);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* ── Inhalt ──────────────────────────────────────────────────────────── */
|
||||
|
||||
.content {
|
||||
flex: 1 1 auto;
|
||||
overflow-y: auto;
|
||||
padding: 14px 16px 18px;
|
||||
}
|
||||
|
||||
.content::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.content::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.path {
|
||||
margin: 0 0 16px;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.group + .group {
|
||||
margin-top: 20px;
|
||||
padding-top: 18px;
|
||||
border-top: 1px solid var(--hairline);
|
||||
}
|
||||
|
||||
.group h2 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 0 0 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.tag {
|
||||
font-size: 9.5px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.03em;
|
||||
text-transform: none;
|
||||
color: var(--warning);
|
||||
border: 1px solid currentColor;
|
||||
border-radius: 3px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.note {
|
||||
margin: 0 0 12px;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.note code {
|
||||
font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
|
||||
font-size: 10.5px;
|
||||
background: var(--surface-input);
|
||||
border-radius: 3px;
|
||||
padding: 1px 4px;
|
||||
}
|
||||
|
||||
/* ── Zeilen ──────────────────────────────────────────────────────────── */
|
||||
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 148px 1fr;
|
||||
gap: 10px;
|
||||
align-items: start;
|
||||
padding: 7px 0;
|
||||
}
|
||||
|
||||
.row > label {
|
||||
padding-top: 5px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.control {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.control.inline {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.control .hint {
|
||||
font-size: 10.5px;
|
||||
line-height: 1.45;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.colon {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 148px 1fr;
|
||||
gap: 8px 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.grid label {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* ── Eingabefelder ───────────────────────────────────────────────────── */
|
||||
|
||||
input[type="text"],
|
||||
input[type="number"],
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: 5px;
|
||||
background: var(--surface-input);
|
||||
color: var(--text-primary);
|
||||
font-family: inherit;
|
||||
font-size: 12.5px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
max-width: 170px;
|
||||
}
|
||||
|
||||
.control.inline input[type="number"] {
|
||||
max-width: 72px;
|
||||
}
|
||||
|
||||
input:focus-visible,
|
||||
select:focus-visible,
|
||||
button:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
margin: 4px 0 0;
|
||||
accent-color: var(--accent);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="range"] {
|
||||
width: 190px;
|
||||
accent-color: var(--accent);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.control:has(> input[type="range"]) {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
output {
|
||||
font-size: 12px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--text-secondary);
|
||||
min-width: 42px;
|
||||
}
|
||||
|
||||
/* Ungültige Eingabe deutlich, aber nicht allein über die Farbe */
|
||||
input[aria-invalid="true"] {
|
||||
border-color: var(--warning);
|
||||
background: rgba(250, 178, 25, 0.08);
|
||||
}
|
||||
|
||||
/* ── Fußzeile ────────────────────────────────────────────────────────── */
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 11px 16px;
|
||||
border-top: 1px solid var(--hairline);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 6px 14px;
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: 5px;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
font-family: inherit;
|
||||
font-size: 12.5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: var(--surface-raised);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn.primary {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn.primary:hover {
|
||||
filter: brightness(1.12);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.feedback {
|
||||
font-size: 11.5px;
|
||||
color: var(--good);
|
||||
}
|
||||
|
||||
.feedback[data-kind="error"] {
|
||||
color: var(--warning);
|
||||
}
|
||||
Reference in New Issue
Block a user