feat(caldav): add Settings UI and Event Modal CalDAV target selection
- Add CalDAV accounts card to Settings page with: * List of configured accounts showing URL and last sync * Expandable calendar list with enable/disable checkboxes * Sync Now, Refresh Calendars, and Delete actions per account * Add Account modal with name, URL, username, password fields - Add CalDAV target selector to event modal: * Dropdown showing local and all enabled CalDAV calendars * Grouped by account using optgroups * Pre-selects current target when editing events * Includes target_caldav_account_id and target_caldav_calendar_url in save - Add CalDAV component styles to settings.css: * Account cards with header, meta, and action sections * Expandable calendar details with checkboxes and color dots * Empty state for no accounts - Add missing i18n keys for calendar enable/disable, refresh, delete confirm - Load CalDAV targets async when modal opens - Admin-only access to account management Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1010,7 +1010,12 @@
|
|||||||
"caldavRefreshCalendars": "Kalender aktualisieren",
|
"caldavRefreshCalendars": "Kalender aktualisieren",
|
||||||
"caldavSyncSuccess": "CalDAV-Synchronisation erfolgreich",
|
"caldavSyncSuccess": "CalDAV-Synchronisation erfolgreich",
|
||||||
"caldavSyncFailed": "CalDAV-Synchronisation fehlgeschlagen",
|
"caldavSyncFailed": "CalDAV-Synchronisation fehlgeschlagen",
|
||||||
"caldavConnectionFailed": "Verbindung zum CalDAV-Server fehlgeschlagen"
|
"caldavConnectionFailed": "Verbindung zum CalDAV-Server fehlgeschlagen",
|
||||||
|
"calendarEnabled": "Kalender aktiviert",
|
||||||
|
"calendarDisabled": "Kalender deaktiviert",
|
||||||
|
"calendarsRefreshed": "Kalender aktualisiert",
|
||||||
|
"deleteAccountConfirm": "CalDAV-Konto wirklich löschen? Alle synchronisierten Kalender werden entfernt.",
|
||||||
|
"lastSync": "Zuletzt synchronisiert"
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"tagline": "Familienplanung. Sicher. Datenschutzfreundlich. Open Source.",
|
"tagline": "Familienplanung. Sicher. Datenschutzfreundlich. Open Source.",
|
||||||
|
|||||||
@@ -1253,6 +1253,59 @@ function bindTimeInputs(root) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------
|
||||||
|
// CalDAV Target Helpers
|
||||||
|
// --------------------------------------------------------
|
||||||
|
|
||||||
|
async function loadCalDAVTargets(selectElement, currentEvent = null) {
|
||||||
|
if (!selectElement) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const accountsRes = await api.get('/calendar/caldav/accounts');
|
||||||
|
const accounts = accountsRes.data || [];
|
||||||
|
|
||||||
|
// Keep only the "local" option
|
||||||
|
selectElement.replaceChildren();
|
||||||
|
const localOption = document.createElement('option');
|
||||||
|
localOption.value = '';
|
||||||
|
localOption.textContent = t('calendar.caldavTargetLocal');
|
||||||
|
selectElement.appendChild(localOption);
|
||||||
|
|
||||||
|
// Load calendars for each account and build options
|
||||||
|
for (const account of accounts) {
|
||||||
|
try {
|
||||||
|
const calendarsRes = await api.get(`/calendar/caldav/accounts/${account.id}/calendars`);
|
||||||
|
const calendars = calendarsRes.data || [];
|
||||||
|
const enabledCalendars = calendars.filter((cal) => cal.enabled);
|
||||||
|
|
||||||
|
if (enabledCalendars.length === 0) continue;
|
||||||
|
|
||||||
|
const optgroup = document.createElement('optgroup');
|
||||||
|
optgroup.label = account.name;
|
||||||
|
|
||||||
|
for (const calendar of enabledCalendars) {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = `${account.id}|${calendar.url}`;
|
||||||
|
option.textContent = calendar.display_name || calendar.url;
|
||||||
|
optgroup.appendChild(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
selectElement.appendChild(optgroup);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`Failed to load calendars for account ${account.id}:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-select current event's target if editing
|
||||||
|
if (currentEvent?.target_caldav_account_id && currentEvent?.target_caldav_calendar_url) {
|
||||||
|
const targetValue = `${currentEvent.target_caldav_account_id}|${currentEvent.target_caldav_calendar_url}`;
|
||||||
|
selectElement.value = targetValue;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Failed to load CalDAV targets:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
// Event-Modal (Erstellen / Bearbeiten)
|
// Event-Modal (Erstellen / Bearbeiten)
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
@@ -1465,6 +1518,12 @@ function openEventModal({ mode, event = null, date = null, reminder = null }) {
|
|||||||
if (reminderCustom) reminderCustom.hidden = reminderOffset.value !== 'custom';
|
if (reminderCustom) reminderCustom.hidden = reminderOffset.value !== 'custom';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Load CalDAV targets
|
||||||
|
const caldavTargetSelect = panel.querySelector('#event-caldav-target');
|
||||||
|
if (caldavTargetSelect) {
|
||||||
|
loadCalDAVTargets(caldavTargetSelect, event);
|
||||||
|
}
|
||||||
|
|
||||||
panel.querySelector('#modal-cancel').addEventListener('click', closeModal);
|
panel.querySelector('#modal-cancel').addEventListener('click', closeModal);
|
||||||
|
|
||||||
panel.querySelector('#modal-delete')?.addEventListener('click', async () => {
|
panel.querySelector('#modal-delete')?.addEventListener('click', async () => {
|
||||||
@@ -1611,6 +1670,14 @@ function buildEventModalContent({ mode, event, date, reminder = null }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="event-caldav-target">${t('calendar.caldavTargetLabel')}</label>
|
||||||
|
<select class="form-input" id="event-caldav-target">
|
||||||
|
<option value="">${t('calendar.caldavTargetLocal')}</option>
|
||||||
|
</select>
|
||||||
|
<small class="form-hint">${t('calendar.caldavTargetHint')}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="modal-description">${t('calendar.descriptionLabel')}</label>
|
<label class="form-label" for="modal-description">${t('calendar.descriptionLabel')}</label>
|
||||||
<textarea class="form-input" id="modal-description" rows="2"
|
<textarea class="form-input" id="modal-description" rows="2"
|
||||||
@@ -1725,6 +1792,19 @@ async function saveEvent(overlay, mode, eventId, existingReminder = null, attach
|
|||||||
attachmentPayload.data = await readFileAsDataUrl(attachmentFile);
|
attachmentPayload.data = await readFileAsDataUrl(attachmentFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract CalDAV target
|
||||||
|
const caldavTargetValue = overlay.querySelector('#event-caldav-target')?.value || '';
|
||||||
|
let target_caldav_account_id = null;
|
||||||
|
let target_caldav_calendar_url = null;
|
||||||
|
|
||||||
|
if (caldavTargetValue) {
|
||||||
|
const [accountId, calendarUrl] = caldavTargetValue.split('|');
|
||||||
|
if (accountId && calendarUrl) {
|
||||||
|
target_caldav_account_id = parseInt(accountId, 10);
|
||||||
|
target_caldav_calendar_url = calendarUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
title, description, start_datetime, end_datetime,
|
title, description, start_datetime, end_datetime,
|
||||||
all_day: allday ? 1 : 0,
|
all_day: allday ? 1 : 0,
|
||||||
@@ -1734,6 +1814,8 @@ async function saveEvent(overlay, mode, eventId, existingReminder = null, attach
|
|||||||
attachment_mime: attachmentPayload.mime,
|
attachment_mime: attachmentPayload.mime,
|
||||||
attachment_size: attachmentPayload.size,
|
attachment_size: attachmentPayload.size,
|
||||||
attachment_data: attachmentPayload.data,
|
attachment_data: attachmentPayload.data,
|
||||||
|
target_caldav_account_id,
|
||||||
|
target_caldav_calendar_url,
|
||||||
};
|
};
|
||||||
|
|
||||||
let savedEventId = eventId;
|
let savedEventId = eventId;
|
||||||
|
|||||||
@@ -529,6 +529,23 @@ export async function render(container, { user }) {
|
|||||||
` : `<span class="form-hint">${t('settings.appleOnlyAdmin')}</span>`}
|
` : `<span class="form-hint">${t('settings.appleOnlyAdmin')}</span>`}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- CalDAV Kalender -->
|
||||||
|
<div class="settings-card">
|
||||||
|
<h2>${t('settings.caldavTitle')}</h2>
|
||||||
|
<p class="settings-card-description">${t('settings.caldavDescription')}</p>
|
||||||
|
|
||||||
|
<div id="caldav-accounts-list"></div>
|
||||||
|
<div id="caldav-empty-state" class="caldav-empty-state" style="display: none;">
|
||||||
|
<p>${t('settings.caldavEmptyState')}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${user?.role === 'admin' ? `
|
||||||
|
<button class="btn btn--primary" id="caldav-add-account-btn">
|
||||||
|
${t('settings.caldavAddAccount')}
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ICS-Abonnements -->
|
<!-- ICS-Abonnements -->
|
||||||
<div class="settings-card" id="ics-card">
|
<div class="settings-card" id="ics-card">
|
||||||
<div class="settings-sync-header">
|
<div class="settings-sync-header">
|
||||||
@@ -1167,6 +1184,220 @@ function bindEvents(container, user, users, categories, icsSubscriptions, apiTok
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CalDAV-Konten laden
|
||||||
|
async function loadCalDAVAccounts(container) {
|
||||||
|
const listEl = container.querySelector('#caldav-accounts-list');
|
||||||
|
const emptyEl = container.querySelector('#caldav-empty-state');
|
||||||
|
if (!listEl || !emptyEl) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const accountsRes = await api.get('/calendar/caldav/accounts');
|
||||||
|
const accounts = accountsRes.data || [];
|
||||||
|
|
||||||
|
if (accounts.length === 0) {
|
||||||
|
listEl.replaceChildren();
|
||||||
|
emptyEl.style.display = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emptyEl.style.display = 'none';
|
||||||
|
listEl.replaceChildren();
|
||||||
|
|
||||||
|
for (const account of accounts) {
|
||||||
|
const calendarsRes = await api.get(`/calendar/caldav/accounts/${account.id}/calendars`);
|
||||||
|
const calendars = calendarsRes.data || [];
|
||||||
|
|
||||||
|
const accountCard = document.createElement('div');
|
||||||
|
accountCard.className = 'caldav-account-item';
|
||||||
|
accountCard.insertAdjacentHTML('beforeend', `
|
||||||
|
<div class="caldav-account-header">
|
||||||
|
<h4>${esc(account.name)}</h4>
|
||||||
|
<div class="caldav-account-meta">
|
||||||
|
<span>${esc(account.caldav_url)}</span>
|
||||||
|
${account.last_sync ? `<span>${t('settings.lastSync')}: ${formatDateTime(account.last_sync)}</span>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<details class="caldav-calendars-details">
|
||||||
|
<summary class="caldav-calendars-summary">
|
||||||
|
${t('settings.caldavCalendarsToggle')} (${calendars.length})
|
||||||
|
</summary>
|
||||||
|
<div class="caldav-calendars-list">
|
||||||
|
${calendars.map((cal) => `
|
||||||
|
<label class="caldav-calendar-item">
|
||||||
|
<input type="checkbox" class="caldav-calendar-checkbox"
|
||||||
|
data-account-id="${account.id}"
|
||||||
|
data-calendar-url="${esc(cal.url)}"
|
||||||
|
${cal.enabled ? 'checked' : ''}>
|
||||||
|
<span class="caldav-calendar-color" style="background-color: ${esc(cal.color || '#007AFF')}"></span>
|
||||||
|
<span class="caldav-calendar-name">${esc(cal.display_name || cal.url)}</span>
|
||||||
|
</label>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
<div class="caldav-account-actions">
|
||||||
|
<button class="btn btn--secondary btn--sm" data-caldav-sync="${account.id}">${t('settings.syncNow')}</button>
|
||||||
|
<button class="btn btn--secondary btn--sm" data-caldav-refresh="${account.id}">${t('settings.caldavRefreshCalendars')}</button>
|
||||||
|
${user?.role === 'admin' ? `<button class="btn btn--danger-outline btn--sm" data-caldav-delete="${account.id}">${t('common.delete')}</button>` : ''}
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
listEl.appendChild(accountCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.lucide) lucide.createIcons({ el: listEl });
|
||||||
|
|
||||||
|
// Bind calendar checkbox events
|
||||||
|
listEl.querySelectorAll('.caldav-calendar-checkbox').forEach((checkbox) => {
|
||||||
|
checkbox.addEventListener('change', async () => {
|
||||||
|
const accountId = parseInt(checkbox.dataset.accountId, 10);
|
||||||
|
const calendarUrl = checkbox.dataset.calendarUrl;
|
||||||
|
const enabled = checkbox.checked;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.patch(`/calendar/caldav/accounts/${accountId}/calendars`, {
|
||||||
|
calendarUrl,
|
||||||
|
enabled,
|
||||||
|
});
|
||||||
|
window.oikos?.showToast(
|
||||||
|
enabled ? t('settings.calendarEnabled') : t('settings.calendarDisabled'),
|
||||||
|
'success'
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
window.oikos?.showToast(err.message, 'danger');
|
||||||
|
checkbox.checked = !enabled; // Revert on error
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bind sync buttons
|
||||||
|
listEl.querySelectorAll('[data-caldav-sync]').forEach((btn) => {
|
||||||
|
btn.addEventListener('click', async () => {
|
||||||
|
btn.disabled = true;
|
||||||
|
const originalText = btn.textContent;
|
||||||
|
btn.textContent = t('settings.synchronizing');
|
||||||
|
try {
|
||||||
|
await api.post('/calendar/caldav/sync');
|
||||||
|
window.oikos?.showToast(t('settings.caldavSyncSuccess'), 'success');
|
||||||
|
await loadCalDAVAccounts(container);
|
||||||
|
} catch (err) {
|
||||||
|
window.oikos?.showToast(err.message || t('settings.caldavSyncFailed'), 'danger');
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = originalText;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bind refresh buttons
|
||||||
|
listEl.querySelectorAll('[data-caldav-refresh]').forEach((btn) => {
|
||||||
|
btn.addEventListener('click', async () => {
|
||||||
|
const accountId = parseInt(btn.dataset.caldavRefresh, 10);
|
||||||
|
btn.disabled = true;
|
||||||
|
const originalText = btn.textContent;
|
||||||
|
btn.textContent = t('settings.loading');
|
||||||
|
try {
|
||||||
|
await api.get(`/calendar/caldav/accounts/${accountId}/calendars?refresh=true`);
|
||||||
|
await loadCalDAVAccounts(container);
|
||||||
|
window.oikos?.showToast(t('settings.calendarsRefreshed'), 'success');
|
||||||
|
} catch (err) {
|
||||||
|
window.oikos?.showToast(err.message, 'danger');
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = originalText;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bind delete buttons
|
||||||
|
listEl.querySelectorAll('[data-caldav-delete]').forEach((btn) => {
|
||||||
|
btn.addEventListener('click', async () => {
|
||||||
|
const accountId = parseInt(btn.dataset.caldavDelete, 10);
|
||||||
|
if (!await confirmModal(t('settings.deleteAccountConfirm'), { danger: true })) return;
|
||||||
|
try {
|
||||||
|
await api.delete(`/calendar/caldav/accounts/${accountId}`);
|
||||||
|
window.oikos?.showToast(t('settings.caldavAccountDeleted'), 'success');
|
||||||
|
await loadCalDAVAccounts(container);
|
||||||
|
} catch (err) {
|
||||||
|
window.oikos?.showToast(err.message, 'danger');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load CalDAV accounts:', err);
|
||||||
|
window.oikos?.showToast(t('settings.caldavConnectionFailed'), 'danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load CalDAV accounts on page load
|
||||||
|
if (user?.role === 'admin') {
|
||||||
|
loadCalDAVAccounts(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalDAV add account button
|
||||||
|
const caldavAddBtn = container.querySelector('#caldav-add-account-btn');
|
||||||
|
if (caldavAddBtn) {
|
||||||
|
caldavAddBtn.addEventListener('click', () => {
|
||||||
|
openModal({
|
||||||
|
title: t('settings.caldavAddAccount'),
|
||||||
|
size: 'sm',
|
||||||
|
content: `
|
||||||
|
<form id="caldav-add-form" novalidate autocomplete="off">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="caldav-name">${t('settings.caldavNameLabel')}<span class="required-marker" aria-hidden="true"> *</span></label>
|
||||||
|
<input class="form-input" type="text" id="caldav-name" required
|
||||||
|
placeholder="${t('settings.caldavNamePlaceholder')}" maxlength="100" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="caldav-url">${t('settings.caldavUrlLabel')}<span class="required-marker" aria-hidden="true"> *</span></label>
|
||||||
|
<input class="form-input" type="url" id="caldav-url" required
|
||||||
|
placeholder="${t('settings.caldavUrlPlaceholder')}" />
|
||||||
|
<small class="form-hint">${t('settings.caldavUrlHint')}</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="caldav-username">${t('settings.caldavUsernameLabel')}<span class="required-marker" aria-hidden="true"> *</span></label>
|
||||||
|
<input class="form-input" type="text" id="caldav-username" required autocomplete="username" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="caldav-password">${t('settings.caldavPasswordLabel')}<span class="required-marker" aria-hidden="true"> *</span></label>
|
||||||
|
<input class="form-input" type="password" id="caldav-password" required autocomplete="current-password" />
|
||||||
|
<small class="form-hint">${t('settings.caldavPasswordHint')}</small>
|
||||||
|
</div>
|
||||||
|
<div id="caldav-add-error" class="form-error" hidden></div>
|
||||||
|
</form>
|
||||||
|
`,
|
||||||
|
onSave: async (panel) => {
|
||||||
|
const form = panel.querySelector('#caldav-add-form');
|
||||||
|
const errorEl = panel.querySelector('#caldav-add-error');
|
||||||
|
errorEl.hidden = true;
|
||||||
|
|
||||||
|
const name = panel.querySelector('#caldav-name').value.trim();
|
||||||
|
const caldavUrl = panel.querySelector('#caldav-url').value.trim();
|
||||||
|
const username = panel.querySelector('#caldav-username').value.trim();
|
||||||
|
const password = panel.querySelector('#caldav-password').value;
|
||||||
|
|
||||||
|
if (!name || !caldavUrl || !username || !password) {
|
||||||
|
showError(errorEl, t('common.requiredFields'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.post('/calendar/caldav/accounts', {
|
||||||
|
name,
|
||||||
|
caldavUrl,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
closeModal({ force: true });
|
||||||
|
window.oikos?.showToast(t('settings.caldavAccountAdded'), 'success');
|
||||||
|
await loadCalDAVAccounts(container);
|
||||||
|
} catch (err) {
|
||||||
|
showError(errorEl, err.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Mitglied hinzufügen (Admin)
|
// Mitglied hinzufügen (Admin)
|
||||||
const addMemberBtn = container.querySelector('#add-member-btn');
|
const addMemberBtn = container.querySelector('#add-member-btn');
|
||||||
if (addMemberBtn) {
|
if (addMemberBtn) {
|
||||||
|
|||||||
@@ -772,3 +772,122 @@
|
|||||||
background: var(--color-surface-2);
|
background: var(--color-surface-2);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --------------------------------------------------------
|
||||||
|
CalDAV Components
|
||||||
|
-------------------------------------------------------- */
|
||||||
|
|
||||||
|
.settings-card-description {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.caldav-empty-state {
|
||||||
|
padding: var(--space-6) var(--space-4);
|
||||||
|
text-align: center;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.caldav-account-item {
|
||||||
|
padding: var(--space-4);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: var(--color-surface-2);
|
||||||
|
margin-bottom: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.caldav-account-header h4 {
|
||||||
|
font-size: var(--text-base);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
margin: 0 0 var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.caldav-account-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-1);
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.caldav-calendars-details {
|
||||||
|
margin: var(--space-3) 0;
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
padding-top: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.caldav-calendars-summary {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
padding: var(--space-2);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
transition: background-color var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.caldav-calendars-summary:hover {
|
||||||
|
background-color: var(--color-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.caldav-calendars-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-2);
|
||||||
|
margin-top: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.caldav-calendar-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color var(--transition-fast);
|
||||||
|
min-height: var(--target-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.caldav-calendar-item:hover {
|
||||||
|
background-color: var(--color-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.caldav-calendar-checkbox {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
accent-color: var(--color-accent);
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.caldav-calendar-color {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.caldav-calendar-name {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.caldav-account-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-2);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: var(--space-3);
|
||||||
|
padding-top: var(--space-3);
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user