feat: v0.12.0 Settings window, per-session Remote Control, delete conversations
- Settings (⋯ → Settings…, palette): theme, terminal text size, interface size, notifications, Remote Control & privacy. The ⋯ menu shrinks. - Remote Control per session: registry choice > Settings default > DEVDESK_REMOTE_CONTROL. Switching an idle session restarts its Claude on the same conversation; busy sessions change at the next start. rc_active comes from the running process; RC tag on sessions. - --delete-convo: remove a conversation's local transcript and subagent files; refused while in use. Delete… in Search past conversations. Closes #61 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WHRLCCdXYm41Qx5kNhehaP
This commit is contained in:
+1
-10
@@ -53,16 +53,7 @@
|
||||
<div class="dd-more">
|
||||
<button type="button" class="lt-btn lt-btn-ghost lt-btn-sm dd-more-toggle" id="more-toggle" aria-haspopup="menu" aria-expanded="false" title="Settings" aria-label="Settings">⋯</button>
|
||||
<div class="dd-menu dd-more-menu" id="more-menu" role="menu" hidden>
|
||||
<div class="dd-menu-row">
|
||||
<span>Text size</span>
|
||||
<span class="dd-menu-inline">
|
||||
<button type="button" class="dd-icon-btn" id="font-down" aria-label="Smaller text">A−</button>
|
||||
<span id="font-size">14</span>
|
||||
<button type="button" class="dd-icon-btn" id="font-up" aria-label="Larger text">A+</button>
|
||||
</span>
|
||||
</div>
|
||||
<button type="button" class="dd-menu-item" id="theme-menu" role="menuitem">Light mode</button>
|
||||
<button type="button" class="dd-menu-item" id="notify-toggle" role="menuitem">Desktop notifications</button>
|
||||
<button type="button" class="dd-menu-item" id="settings-menu" role="menuitem">Settings…</button>
|
||||
<div class="dd-menu-row dd-usage-row" id="usage-row" hidden><span>Plan usage</span><span class="dd-usage" id="usage-menu"></span></div>
|
||||
<button type="button" class="dd-menu-item" id="history-menu" role="menuitem">Search past conversations</button>
|
||||
<button type="button" class="dd-menu-item" id="palette-menu" role="menuitem">Command palette</button>
|
||||
|
||||
+23
-3
@@ -6,7 +6,7 @@ import { runCommand, sessionAction } from './connection.js';
|
||||
import { parseListOutput, slugify } from './protocol.js';
|
||||
import { el } from './dom.js';
|
||||
import { ago } from './sessions.js';
|
||||
import { openDialog, toast } from './ui.js';
|
||||
import { confirmDialog, openDialog, toast } from './ui.js';
|
||||
|
||||
const ROLE = { you: 'You', claude: 'Claude', command: 'Ran', output: 'Output' };
|
||||
|
||||
@@ -82,9 +82,28 @@ export function openHistory(hooks) {
|
||||
el('button', {
|
||||
type: 'button', class: 'lt-btn lt-btn-ghost lt-btn-sm',
|
||||
onclick: () => navigator.clipboard?.writeText(`claude --resume ${r.id}`).then(() => toast.success('Copied the resume command')),
|
||||
}, 'Copy command'));
|
||||
}, 'Copy command'),
|
||||
live ? null : el('button', {
|
||||
type: 'button', class: 'lt-btn lt-btn-ghost lt-btn-sm dd-danger-text', title: 'Delete this conversation from this machine for good',
|
||||
onclick: async () => {
|
||||
const ok = await confirmDialog({
|
||||
title: 'Delete this conversation?',
|
||||
text: `“${r.title.slice(0, 80)}” (${day(r.started)}) is removed from this machine for good: its transcript and subagent files in ~/.claude/projects. It can't be resumed or found again. If its session had Remote Control on, a copy is also at claude.ai/code: delete it there too.`,
|
||||
confirmLabel: 'Delete for good',
|
||||
danger: true,
|
||||
});
|
||||
if (!ok) return;
|
||||
try {
|
||||
await sessionAction('delete-convo', r.id);
|
||||
item.remove();
|
||||
toast.success('Conversation deleted from this machine', 8000, [{ label: 'claude.ai/code', run: () => window.open('https://claude.ai/code', '_blank', 'noopener') }]);
|
||||
} catch (error) {
|
||||
toast.error(error.message);
|
||||
}
|
||||
},
|
||||
}, 'Delete…'));
|
||||
fill();
|
||||
return el('div', { class: 'dd-history-item' },
|
||||
const item = el('div', { class: 'dd-history-item' },
|
||||
el('div', { class: 'dd-history-title' }, r.title),
|
||||
el('div', { class: 'dd-history-meta' },
|
||||
[r.cwd, day(r.started), `updated ${ago(r.updated)}${ago(r.updated) === 'now' ? '' : ' ago'}`, `${r.hits} match${r.hits === 1 ? '' : 'es'}`, live ? `open in ${live}` : '']
|
||||
@@ -92,6 +111,7 @@ export function openHistory(hooks) {
|
||||
...r.matches.map((m) => el('div', { class: `dd-history-snippet is-${m.role}` },
|
||||
el('span', { class: 'dd-history-role' }, ROLE[m.role] ?? m.role), el('span', {}, ...highlight(m.snippet, data.query)))),
|
||||
actions);
|
||||
return item;
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
+39
-30
@@ -1,6 +1,6 @@
|
||||
/* global __DEVDESK_VERSION__ */
|
||||
import { THEMES, createTerminal, loadFonts } from './term.js';
|
||||
import { getTheme, initTheme, onThemeChange, toggleTheme } from './theme.js';
|
||||
import { getTheme, initTheme, onThemeChange, setTheme, toggleTheme } from './theme.js';
|
||||
import {
|
||||
TtydConnection, fetchAlerts, fetchRepo, fetchToken, investigateAlert, lastReply, replyTo, sessionAction, uploadFile,
|
||||
AuthExpiredError, UnauthorizedError,
|
||||
@@ -16,6 +16,7 @@ import { Inbox } from './inbox.js';
|
||||
import { FilesPane } from './files.js';
|
||||
import { registerPathLinks } from './links.js';
|
||||
import { openHistory } from './history.js';
|
||||
import { applyUiScale, openSettings } from './settings.js';
|
||||
import { el } from './dom.js';
|
||||
import * as notify from './notify.js';
|
||||
|
||||
@@ -55,7 +56,6 @@ const ui = {
|
||||
overlayActions: $('overlay-actions'),
|
||||
moreToggle: $('more-toggle'),
|
||||
moreMenu: $('more-menu'),
|
||||
notifyButton: $('notify-toggle'),
|
||||
sidebarButton: $('sidebar-toggle'),
|
||||
backdrop: $('backdrop'),
|
||||
};
|
||||
@@ -610,10 +610,41 @@ function setFontSize(size) {
|
||||
term.options.fontSize = clamped;
|
||||
store.set('fontSize', clamped);
|
||||
document.documentElement.style.setProperty('--dd-editor-font', `${clamped - 1}px`);
|
||||
$('font-size').textContent = String(clamped);
|
||||
fit.fit();
|
||||
}
|
||||
|
||||
// Settings (devdesk#61): appearance, notifications, Remote Control per session.
|
||||
function showSettings() {
|
||||
closeMenus();
|
||||
openSettings({
|
||||
getTheme,
|
||||
setTheme,
|
||||
fontSize: () => term.options.fontSize,
|
||||
setFontSize,
|
||||
uiScale: () => Number(store.get('uiScale', 1)) || 1,
|
||||
setUiScale: (scale) => {
|
||||
store.set('uiScale', scale);
|
||||
applyUiScale(scale);
|
||||
requestAnimationFrame(() => fit.fit());
|
||||
},
|
||||
notifications: () => {
|
||||
const current = notify.notificationsState();
|
||||
return {
|
||||
label: {
|
||||
unsupported: 'Not supported', denied: 'Blocked by the browser', default: 'Turn on',
|
||||
granted: notify.isMuted() ? 'Muted: unmute' : 'On: mute',
|
||||
}[current],
|
||||
disabled: current === 'unsupported' || current === 'denied',
|
||||
run: toggleNotifications,
|
||||
};
|
||||
},
|
||||
sessions: () => sessionsPanel?.data,
|
||||
current: () => state.session,
|
||||
refresh: () => sessionsPanel.reload(),
|
||||
openHistory: showHistory,
|
||||
});
|
||||
}
|
||||
|
||||
async function toggleNotifications() {
|
||||
const current = notify.notificationsState();
|
||||
if (current === 'default') {
|
||||
@@ -850,12 +881,13 @@ function buildCommands() {
|
||||
add({ id: 'font-up', group: 'View', icon: 'A+', label: 'Larger text', keywords: 'font zoom', run: () => setFontSize(term.options.fontSize + 1) });
|
||||
add({ id: 'font-down', group: 'View', icon: 'A−', label: 'Smaller text', keywords: 'font zoom', run: () => setFontSize(term.options.fontSize - 1) });
|
||||
add({ id: 'font-reset', group: 'View', icon: 'A', label: 'Reset text size', keywords: 'font zoom', run: () => setFontSize(14) });
|
||||
add({ id: 'settings', group: 'View', icon: '⚙', label: 'Settings…', keywords: 'preferences text size font theme remote control privacy notifications', run: showSettings });
|
||||
add({ id: 'fullscreen', group: 'View', icon: '⛶', label: document.fullscreenElement ? 'Exit fullscreen' : 'Fullscreen', run: toggleFullscreen });
|
||||
if (notifyState !== 'unsupported') {
|
||||
add({
|
||||
id: 'notify', group: 'View', icon: '🔔', keywords: 'notifications alerts bell',
|
||||
label: { default: 'Enable desktop notifications', denied: 'Desktop notifications (blocked)', granted: notify.isMuted() ? 'Unmute desktop notifications' : 'Mute desktop notifications' }[notifyState],
|
||||
run: () => toggleNotifications().then(renderNotifyButton),
|
||||
run: () => toggleNotifications(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -968,17 +1000,6 @@ async function toggleFullscreen() {
|
||||
navigator.keyboard?.lock?.().catch(() => {});
|
||||
}
|
||||
|
||||
function renderNotifyButton() {
|
||||
const current = notify.notificationsState();
|
||||
const labels = {
|
||||
unsupported: 'Desktop notifications: unsupported',
|
||||
denied: 'Desktop notifications: blocked by the browser',
|
||||
default: 'Enable desktop notifications',
|
||||
granted: notify.isMuted() ? 'Desktop notifications: muted (unmute)' : 'Desktop notifications: on (mute)',
|
||||
};
|
||||
ui.notifyButton.textContent = labels[current];
|
||||
ui.notifyButton.disabled = current === 'unsupported' || current === 'denied';
|
||||
}
|
||||
|
||||
function setSidebar(open) {
|
||||
if (open && files?.isOpen) files.close();
|
||||
@@ -1028,12 +1049,7 @@ function wireKeybar() {
|
||||
}
|
||||
|
||||
function wireChrome() {
|
||||
$('font-down').addEventListener('click', () => setFontSize(term.options.fontSize - 1));
|
||||
$('font-up').addEventListener('click', () => setFontSize(term.options.fontSize + 1));
|
||||
ui.notifyButton.addEventListener('click', async () => {
|
||||
await toggleNotifications();
|
||||
renderNotifyButton();
|
||||
});
|
||||
$('settings-menu').addEventListener('click', showSettings);
|
||||
$('fullscreen-toggle').addEventListener('click', () => {
|
||||
closeMenus();
|
||||
toggleFullscreen();
|
||||
@@ -1095,14 +1111,7 @@ function wireChrome() {
|
||||
else if (!dialogOpen()) palette.open();
|
||||
}
|
||||
}, true);
|
||||
wireMenu(ui.moreToggle, ui.moreMenu, () => {
|
||||
renderNotifyButton();
|
||||
$('theme-menu').textContent = getTheme() === 'light' ? 'Dark mode' : 'Light mode';
|
||||
});
|
||||
$('theme-menu').addEventListener('click', () => {
|
||||
closeMenus();
|
||||
toggleTheme();
|
||||
});
|
||||
wireMenu(ui.moreToggle, ui.moreMenu, () => {});
|
||||
inbox = new Inbox({
|
||||
button: $('inbox-toggle'),
|
||||
badge: $('inbox-badge'),
|
||||
@@ -1233,7 +1242,7 @@ async function boot() {
|
||||
initTheme();
|
||||
$('version').textContent = `devdesk ${__DEVDESK_VERSION__}`;
|
||||
const fontSize = Math.min(24, Math.max(10, Number(store.get('fontSize', 14)) || 14));
|
||||
$('font-size').textContent = String(fontSize);
|
||||
applyUiScale(Number(store.get('uiScale', 1)) || 1);
|
||||
await loadFonts(fontSize);
|
||||
|
||||
({ term, fit } = createTerminal($('terminal'), {
|
||||
|
||||
@@ -321,6 +321,8 @@ export class SessionsPanel {
|
||||
el('button', { class: 'dd-session-open', type: 'button', title, onclick: () => this.hooks.onOpen(s.name) },
|
||||
el('span', { class: `lt-dot ${status.dot}` }),
|
||||
el('span', { class: 'dd-session-name' }, s.name,
|
||||
// Remote Control on: synced to claude.ai and the Claude app (devdesk#61).
|
||||
s.rc_active ? el('span', { class: 'dd-tag dd-rc', title: 'Remote Control is on: this session syncs to claude.ai and the Claude app. Change it in Settings.' }, 'RC') : null,
|
||||
s.worktree ? el('span', { class: 'dd-tag dd-wt', title: `Own worktree on ${s.worktree.branch}\n${s.worktree.path}` }, `⑂ ${s.worktree.branch.replace(/^devdesk\//, '')}`) : null,
|
||||
s.tags?.length ? el('span', { class: 'dd-tags', title: 'Hosts this session worked on' }, ...s.tags.slice(0, 3).map((t) => el('span', { class: 'dd-tag' }, t))) : null,
|
||||
// The pull request of this branch (devdesk#25)
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
// Settings (devdesk#61): the things you set once and leave, in one window
|
||||
// instead of the ⋯ menu. Appearance (theme, terminal text size, interface size)
|
||||
// and notifications are per browser; Remote Control is per session, with a
|
||||
// default for new sessions, kept on the server.
|
||||
|
||||
import { el } from './dom.js';
|
||||
import { openDialog, toast } from './ui.js';
|
||||
import { sessionAction } from './connection.js';
|
||||
|
||||
const SCALES = [['Small', 0.9], ['Normal', 1], ['Large', 1.12], ['Larger', 1.25]];
|
||||
|
||||
// Interface size: the page is sized in rem off the root font size (14px).
|
||||
export function applyUiScale(scale) {
|
||||
document.documentElement.style.fontSize = scale === 1 ? '' : `${Math.round(14 * scale * 10) / 10}px`;
|
||||
}
|
||||
|
||||
function segmented(label, options, current, onPick) {
|
||||
const buttons = options.map(([text, value]) => el('button', {
|
||||
type: 'button', class: 'dd-seg-btn', 'aria-pressed': String(value === current),
|
||||
onclick: (event) => {
|
||||
for (const b of buttons) b.setAttribute('aria-pressed', 'false');
|
||||
event.currentTarget.setAttribute('aria-pressed', 'true');
|
||||
onPick(value);
|
||||
},
|
||||
}, text));
|
||||
return el('div', { class: 'dd-setting' }, el('span', { class: 'dd-setting-label' }, label), el('span', { class: 'dd-seg', role: 'group', 'aria-label': label }, ...buttons));
|
||||
}
|
||||
|
||||
function toggle(label, checked, onChange, hint) {
|
||||
const input = el('input', { type: 'checkbox', role: 'switch', checked: checked ? true : null });
|
||||
input.addEventListener('change', () => onChange(input.checked, input));
|
||||
return el('label', { class: 'dd-setting dd-switch-row' },
|
||||
el('span', { class: 'dd-setting-label' }, label, hint ? el('span', { class: 'dd-setting-hint' }, hint) : null),
|
||||
el('span', { class: 'dd-switch' }, input, el('span', { class: 'dd-switch-track', 'aria-hidden': 'true' })));
|
||||
}
|
||||
|
||||
// hooks: getTheme(), setTheme(t), fontSize(), setFontSize(n), uiScale(), setUiScale(s),
|
||||
// notifications() -> { label, disabled, run }, sessions() -> list data, current(),
|
||||
// refresh(), openHistory()
|
||||
export async function openSettings(hooks) {
|
||||
const data = hooks.sessions();
|
||||
const current = hooks.current();
|
||||
const size = el('span', { class: 'dd-setting-value' }, String(hooks.fontSize()));
|
||||
const setSize = (n) => {
|
||||
hooks.setFontSize(n);
|
||||
size.textContent = String(hooks.fontSize());
|
||||
};
|
||||
const notif = hooks.notifications();
|
||||
const notifyButton = el('button', { type: 'button', class: 'lt-btn lt-btn-sm', disabled: notif.disabled ? true : null }, notif.label);
|
||||
notifyButton.addEventListener('click', async () => {
|
||||
await hooks.notifications().run();
|
||||
const next = hooks.notifications();
|
||||
notifyButton.textContent = next.label;
|
||||
notifyButton.disabled = next.disabled;
|
||||
});
|
||||
|
||||
// Remote Control, per session.
|
||||
const running = (data?.sessions ?? []).filter((s) => s.running).sort((a, b) => (a.name === current ? -1 : b.name === current ? 1 : a.name.localeCompare(b.name)));
|
||||
const rcRows = running.map((s) => toggle(s.name, s.remote_control, async (on, input) => {
|
||||
input.disabled = true;
|
||||
try {
|
||||
const result = await sessionAction('remote-control', s.name, on ? 'on' : 'off');
|
||||
const what = `Remote Control ${on ? 'on' : 'off'}`;
|
||||
if (result?.applied === 'now') toast.success(`${s.name}: restarting Claude with ${what} (same conversation)`, 6000);
|
||||
else if (result?.applied === 'next-start') toast.info(`${s.name} is busy: ${what} when Claude next starts`, 7000);
|
||||
else toast.success(`${s.name}: ${what}`);
|
||||
hooks.refresh();
|
||||
} catch (error) {
|
||||
input.checked = !on;
|
||||
toast.error(`${s.name}: ${error.message}`);
|
||||
} finally {
|
||||
input.disabled = false;
|
||||
}
|
||||
}, s.rc_active ? 'syncing to claude.ai now' : s.remote_control ? 'on at the next Claude start' : ''));
|
||||
const defaultRow = toggle('New sessions', Boolean(data?.remote_control_default), async (on, input) => {
|
||||
input.disabled = true;
|
||||
try {
|
||||
await sessionAction('settings', 'remote-control-default', on ? 'on' : 'off');
|
||||
toast.success(`New sessions: Remote Control ${on ? 'on' : 'off'}`);
|
||||
hooks.refresh();
|
||||
} catch (error) {
|
||||
input.checked = !on;
|
||||
toast.error(error.message);
|
||||
} finally {
|
||||
input.disabled = false;
|
||||
}
|
||||
}, 'sessions you create from now on');
|
||||
|
||||
const link = (href, text) => el('a', { href, target: '_blank', rel: 'noopener' }, text);
|
||||
const body = el('div', { class: 'dd-settings' },
|
||||
el('section', { class: 'dd-settings-group' },
|
||||
el('h3', {}, 'Appearance'),
|
||||
segmented('Theme', [['Dark', 'dark'], ['Light', 'light']], hooks.getTheme(), (t) => hooks.setTheme(t)),
|
||||
el('div', { class: 'dd-setting' },
|
||||
el('span', { class: 'dd-setting-label' }, 'Terminal text size'),
|
||||
el('span', { class: 'dd-stepper' },
|
||||
el('button', { type: 'button', class: 'dd-icon-btn', 'aria-label': 'Smaller terminal text', onclick: () => setSize(hooks.fontSize() - 1) }, 'A−'),
|
||||
size,
|
||||
el('button', { type: 'button', class: 'dd-icon-btn', 'aria-label': 'Larger terminal text', onclick: () => setSize(hooks.fontSize() + 1) }, 'A+'),
|
||||
el('button', { type: 'button', class: 'lt-btn lt-btn-ghost lt-btn-sm', onclick: () => setSize(14) }, 'Reset'))),
|
||||
segmented('Interface size', SCALES, hooks.uiScale(), (s) => hooks.setUiScale(s)),
|
||||
el('p', { class: 'dd-setting-note' }, 'Appearance is remembered in this browser.')),
|
||||
el('section', { class: 'dd-settings-group' },
|
||||
el('h3', {}, 'Notifications'),
|
||||
el('div', { class: 'dd-setting' }, el('span', { class: 'dd-setting-label' }, 'Desktop notifications', el('span', { class: 'dd-setting-hint' }, 'when another session needs you')), notifyButton)),
|
||||
el('section', { class: 'dd-settings-group' },
|
||||
el('h3', {}, 'Remote Control & privacy'),
|
||||
el('p', { class: 'dd-setting-note' },
|
||||
'With Remote Control on, a session also appears in the Claude app and at claude.ai/code, and Anthropic stores its transcript to keep your devices in sync (30 days, or 5 years if "Help improve Claude" is on in ',
|
||||
link('https://claude.ai/settings/data-privacy-controls', 'your privacy settings'), '). Turning it off stops syncing from then on; what was already synced stays until you delete it at ',
|
||||
link('https://claude.ai/code', 'claude.ai/code'), '. Switching a running session restarts its Claude on the same conversation.'),
|
||||
defaultRow,
|
||||
running.length ? el('div', { class: 'dd-setting-sub' }, 'Running sessions') : null,
|
||||
...rcRows),
|
||||
el('section', { class: 'dd-settings-group' },
|
||||
el('h3', {}, 'Conversations'),
|
||||
el('p', { class: 'dd-setting-note' }, 'Every conversation is also kept on this machine in ~/.claude/projects (plaintext, 30 days). Delete one for good from Search past conversations.'),
|
||||
el('div', { class: 'dd-setting' }, el('span', { class: 'dd-setting-label' }, 'Find or delete a conversation'),
|
||||
el('button', { type: 'button', class: 'lt-btn lt-btn-sm', 'data-close': 'history' }, 'Search past conversations…'))));
|
||||
const choice = await openDialog({
|
||||
title: 'Settings',
|
||||
className: 'dd-settings-dialog',
|
||||
body,
|
||||
onOpen: () => {
|
||||
body.querySelector('[data-close="history"]').addEventListener('click', () => {
|
||||
body.closest('.lt-modal-overlay, .lt-modal')?.querySelector('[aria-label="Close"], .lt-modal-close')?.click();
|
||||
setTimeout(() => hooks.openHistory(), 50);
|
||||
});
|
||||
return body.querySelector('button, input');
|
||||
},
|
||||
actions: [{ label: 'Done', primary: true }],
|
||||
});
|
||||
return choice;
|
||||
}
|
||||
@@ -1144,3 +1144,32 @@ body.dd-touch .dd-footer-usage { display: none; }
|
||||
.dd-search-ln { flex: none; min-width: 2.5em; color: var(--text-muted); text-align: right; }
|
||||
.dd-search-text { overflow: hidden; text-overflow: ellipsis; }
|
||||
.dd-search-text mark { background: var(--accent-amber-dim); color: var(--accent-amber); outline: 1px solid color-mix(in srgb, var(--accent-amber) 50%, transparent); }
|
||||
|
||||
/* Settings (devdesk#61) */
|
||||
.dd-settings-dialog .lt-modal { width: min(620px, calc(100vw - 24px)); max-height: calc(100vh - 40px); }
|
||||
.dd-settings-dialog .dd-dialog-body { max-height: calc(100vh - 170px); overflow-y: auto; }
|
||||
.dd-settings { display: flex; flex-direction: column; gap: 18px; }
|
||||
.dd-settings-group h3 { margin: 0 0 8px; color: var(--accent-orange); font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.12em; text-transform: uppercase; }
|
||||
.dd-setting { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 34px; padding: 4px 0; border-bottom: 1px solid var(--border-dim); }
|
||||
.dd-setting-label { display: flex; flex-direction: column; color: var(--text-primary); font-size: 0.8rem; }
|
||||
.dd-setting-hint { color: var(--text-muted); font-size: 0.68rem; }
|
||||
.dd-setting-value { min-width: 2ch; text-align: center; font-family: var(--font-mono); }
|
||||
.dd-setting-note { margin: 4px 0 8px; color: var(--text-secondary); font-size: 0.72rem; line-height: 1.5; }
|
||||
.dd-setting-note a { color: var(--accent-cyan); }
|
||||
.dd-setting-sub { margin-top: 10px; color: var(--text-muted); font-family: var(--font-mono); font-size: 0.64rem; letter-spacing: 0.08em; text-transform: uppercase; }
|
||||
.dd-stepper { display: inline-flex; align-items: center; gap: 6px; }
|
||||
.dd-seg { display: inline-flex; border: 1px solid var(--border-color); }
|
||||
.dd-seg-btn { padding: 4px 10px; border: 0; border-right: 1px solid var(--border-color); background: none; color: var(--text-secondary); font-family: var(--font-mono); font-size: 0.72rem; cursor: pointer; }
|
||||
.dd-seg-btn:last-child { border-right: 0; }
|
||||
.dd-seg-btn[aria-pressed="true"] { background: var(--accent-cyan-dim); color: var(--accent-cyan); }
|
||||
.dd-switch-row { cursor: pointer; }
|
||||
.dd-switch { position: relative; flex: none; width: 36px; height: 20px; }
|
||||
.dd-switch input { position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; }
|
||||
.dd-switch-track { position: absolute; inset: 0; border: 1px solid var(--border-color); border-radius: 10px; background: var(--bg-tertiary); transition: background 0.15s; }
|
||||
.dd-switch-track::after { content: ''; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; border-radius: 50%; background: var(--text-muted); transition: transform 0.15s, background 0.15s; }
|
||||
.dd-switch input:checked + .dd-switch-track { background: var(--accent-cyan-dim); border-color: var(--accent-cyan); }
|
||||
.dd-switch input:checked + .dd-switch-track::after { transform: translateX(16px); background: var(--accent-cyan); }
|
||||
.dd-switch input:focus-visible + .dd-switch-track { outline: 1px dashed var(--accent-cyan); outline-offset: 2px; }
|
||||
.dd-switch input:disabled + .dd-switch-track { opacity: 0.5; }
|
||||
.dd-rc { color: var(--accent-purple); border-color: color-mix(in srgb, var(--accent-purple) 50%, transparent); }
|
||||
.dd-danger-text { color: var(--accent-red) !important; }
|
||||
|
||||
Reference in New Issue
Block a user