mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-08-28 17:21:05 +00:00
834 lines
24 KiB
Svelte
834 lines
24 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from "svelte";
|
|
import { fade, fly } from "svelte/transition";
|
|
import browser from "webextension-polyfill";
|
|
import {
|
|
GOOGLE_CALENDAR_SYNC_WEEKS_MAX,
|
|
GOOGLE_CALENDAR_SYNC_WEEKS_MIN,
|
|
} from "@/config/googleCalendar";
|
|
import { maybeRunDueWeeklySync } from "@/seqta/utils/googleCalendar/calendarSyncListener";
|
|
import { deleteSyncedEventsFromGoogleCalendar } from "@/seqta/utils/googleCalendar/syncEngine";
|
|
import {
|
|
formatSyncResultMessage,
|
|
runGoogleCalendarSync,
|
|
} from "@/seqta/utils/googleCalendar/syncRunner";
|
|
import type {
|
|
GoogleCalendarStatus,
|
|
GoogleCalendarSyncProgress,
|
|
GoogleCalendarSyncResult,
|
|
} from "@/seqta/utils/googleCalendar/types";
|
|
import CalendarDeleteEventsModal from "./CalendarDeleteEventsModal.svelte";
|
|
import CalendarDisconnectModal from "./CalendarDisconnectModal.svelte";
|
|
import CalendarSyncProgress from "./CalendarSyncProgress.svelte";
|
|
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
|
import { syncCalendarSyncTheme } from "./calendarSyncTheme";
|
|
|
|
type BusyPhase = "connect" | "sync" | "delete" | "disconnect" | null;
|
|
|
|
let status = $state<GoogleCalendarStatus>({ configured: true, connected: false });
|
|
let busy = $state<BusyPhase>(null);
|
|
let menuOpen = $state(false);
|
|
let showDisconnect = $state(false);
|
|
let showDeleteEvents = $state(false);
|
|
let toast = $state<{ message: string; error: boolean } | null>(null);
|
|
let syncProgress = $state<GoogleCalendarSyncProgress | null>(null);
|
|
let syncWeeksAhead = $state(12);
|
|
let autoSyncWeekly = $state(true);
|
|
|
|
let rootEl = $state<HTMLDivElement | null>(null);
|
|
let triggerEl = $state<HTMLButtonElement | null>(null);
|
|
let menuEl = $state<HTMLDivElement | null>(null);
|
|
let menuStyle = $state("");
|
|
let toastTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
const isBusy = $derived(busy !== null);
|
|
const accent = "var(--bsplus-cal-accent, var(--better-main, #3b82f6))";
|
|
|
|
function showToastMessage(message: string, isError = false) {
|
|
toast = { message, error: isError };
|
|
if (toastTimer) clearTimeout(toastTimer);
|
|
toastTimer = setTimeout(() => {
|
|
toast = null;
|
|
}, 4500);
|
|
}
|
|
|
|
async function refreshStatus() {
|
|
status = (await browser.runtime.sendMessage({
|
|
type: "googleCalendarStatus",
|
|
})) as GoogleCalendarStatus;
|
|
syncWeeksAhead = status.syncWeeksAhead ?? 12;
|
|
autoSyncWeekly = status.autoSyncWeekly !== false;
|
|
}
|
|
|
|
async function getAccessTokenFromBackground(): Promise<string> {
|
|
const res = (await browser.runtime.sendMessage({
|
|
type: "googleCalendarGetAccessToken",
|
|
})) as { success?: boolean; accessToken?: string; error?: string };
|
|
if (!res?.success || !res.accessToken) {
|
|
throw new Error(res?.error ?? "Could not get Google Calendar access token.");
|
|
}
|
|
return res.accessToken;
|
|
}
|
|
|
|
function handleSyncProgress(progress: GoogleCalendarSyncProgress) {
|
|
syncProgress = progress;
|
|
}
|
|
|
|
async function saveSyncSettings(patch: {
|
|
syncWeeksAhead?: number;
|
|
autoSyncWeekly?: boolean;
|
|
}) {
|
|
const result = (await browser.runtime.sendMessage({
|
|
type: "googleCalendarUpdateSyncSettings",
|
|
...patch,
|
|
})) as GoogleCalendarStatus & { success?: boolean };
|
|
if (result.syncWeeksAhead != null) syncWeeksAhead = result.syncWeeksAhead;
|
|
if (result.autoSyncWeekly != null) autoSyncWeekly = result.autoSyncWeekly;
|
|
status = { ...status, ...result };
|
|
}
|
|
|
|
async function performSync(mode: "full" | "incremental" = "full"): Promise<boolean> {
|
|
const result = await runGoogleCalendarSync({
|
|
mode,
|
|
onProgress: handleSyncProgress,
|
|
});
|
|
|
|
syncProgress = null;
|
|
|
|
if (!result.success) {
|
|
showToastMessage(result.error ?? "Calendar sync failed.", true);
|
|
return false;
|
|
}
|
|
|
|
status = {
|
|
...status,
|
|
connected: true,
|
|
lastSyncAt: result.lastSyncAt ?? status.lastSyncAt,
|
|
};
|
|
showToastMessage(formatSyncResultMessage(result));
|
|
return true;
|
|
}
|
|
|
|
async function connectGoogle() {
|
|
if (!status.configured || isBusy) return;
|
|
menuOpen = true;
|
|
busy = "connect";
|
|
try {
|
|
const result = (await browser.runtime.sendMessage({
|
|
type: "googleCalendarConnect",
|
|
})) as GoogleCalendarSyncResult;
|
|
if (!result.success) {
|
|
showToastMessage(result.error ?? "Could not connect to Google Calendar.", true);
|
|
return;
|
|
}
|
|
status = { ...status, connected: true };
|
|
busy = "sync";
|
|
await performSync();
|
|
} catch (err) {
|
|
showToastMessage(err instanceof Error ? err.message : "Could not connect.", true);
|
|
} finally {
|
|
syncProgress = null;
|
|
busy = null;
|
|
}
|
|
}
|
|
|
|
async function syncTimetable() {
|
|
if (!status.configured || isBusy) return;
|
|
if (!status.connected) {
|
|
await connectGoogle();
|
|
return;
|
|
}
|
|
|
|
busy = "sync";
|
|
try {
|
|
await performSync();
|
|
} catch (err) {
|
|
showToastMessage(err instanceof Error ? err.message : "Calendar sync failed.", true);
|
|
} finally {
|
|
syncProgress = null;
|
|
busy = null;
|
|
}
|
|
}
|
|
|
|
async function confirmDeleteEvents() {
|
|
if (isBusy) return;
|
|
busy = "delete";
|
|
syncProgress = {
|
|
phase: "preparing",
|
|
current: 0,
|
|
total: 1,
|
|
message: "Preparing removal…",
|
|
};
|
|
try {
|
|
const result = await deleteSyncedEventsFromGoogleCalendar(
|
|
location.origin,
|
|
getAccessTokenFromBackground,
|
|
{ onProgress: handleSyncProgress },
|
|
);
|
|
|
|
if (!result.success) {
|
|
showToastMessage(result.error ?? "Could not remove calendar events.", true);
|
|
return;
|
|
}
|
|
|
|
const removed = result.deleted ?? 0;
|
|
showDeleteEvents = false;
|
|
menuOpen = false;
|
|
if (removed === 0) {
|
|
showToastMessage("No synced events to remove.");
|
|
} else {
|
|
showToastMessage(`Removed ${removed} event${removed === 1 ? "" : "s"} from Google Calendar.`);
|
|
}
|
|
} catch (err) {
|
|
showToastMessage(err instanceof Error ? err.message : "Remove failed.", true);
|
|
} finally {
|
|
syncProgress = null;
|
|
busy = null;
|
|
}
|
|
}
|
|
|
|
async function onWeeksAheadChange(event: Event) {
|
|
const value = Number((event.currentTarget as HTMLInputElement).value);
|
|
if (!Number.isFinite(value)) return;
|
|
await saveSyncSettings({ syncWeeksAhead: value });
|
|
}
|
|
|
|
async function onAutoSyncToggle(event: Event) {
|
|
const checked = (event.currentTarget as HTMLInputElement).checked;
|
|
autoSyncWeekly = checked;
|
|
await saveSyncSettings({ autoSyncWeekly: checked });
|
|
}
|
|
|
|
async function confirmDisconnect() {
|
|
if (isBusy) return;
|
|
busy = "disconnect";
|
|
try {
|
|
const result = (await browser.runtime.sendMessage({
|
|
type: "googleCalendarDisconnect",
|
|
})) as { success?: boolean };
|
|
if (!result?.success) {
|
|
showToastMessage("Could not disconnect Google Calendar.", true);
|
|
return;
|
|
}
|
|
status = { ...status, connected: false, lastSyncAt: undefined };
|
|
showDisconnect = false;
|
|
menuOpen = false;
|
|
showToastMessage("Disconnected from Google Calendar.");
|
|
} catch (err) {
|
|
showToastMessage(err instanceof Error ? err.message : "Disconnect failed.", true);
|
|
} finally {
|
|
busy = null;
|
|
}
|
|
}
|
|
|
|
function toggleMenu() {
|
|
if (isBusy) return;
|
|
menuOpen = !menuOpen;
|
|
if (menuOpen) {
|
|
queueMicrotask(() => syncMenuTheme());
|
|
}
|
|
}
|
|
|
|
function formatLastSync(ts?: number): string | null {
|
|
if (!ts) return null;
|
|
const diff = Date.now() - ts;
|
|
if (diff < 60_000) return "Synced just now";
|
|
if (diff < 3_600_000) return `Synced ${Math.floor(diff / 60_000)}m ago`;
|
|
if (diff < 86_400_000) return `Synced ${Math.floor(diff / 3_600_000)}h ago`;
|
|
return `Synced ${new Date(ts).toLocaleDateString()}`;
|
|
}
|
|
|
|
function updateMenuPosition() {
|
|
if (!triggerEl) return;
|
|
const rect = triggerEl.getBoundingClientRect();
|
|
menuStyle = `top:${rect.bottom + 8}px;right:${window.innerWidth - rect.right}px;`;
|
|
syncMenuTheme();
|
|
}
|
|
|
|
function syncMenuTheme() {
|
|
if (!menuEl) return;
|
|
syncCalendarSyncTheme(menuEl);
|
|
}
|
|
|
|
function syncMountedTheme() {
|
|
const themeHost = rootEl?.closest(".bsplus-calendar-sync-mount") as HTMLElement | null;
|
|
if (themeHost) syncCalendarSyncTheme(themeHost);
|
|
if (menuOpen) syncMenuTheme();
|
|
}
|
|
|
|
function portalMenu(node: HTMLElement) {
|
|
document.body.appendChild(node);
|
|
return {
|
|
destroy() {
|
|
node.remove();
|
|
},
|
|
};
|
|
}
|
|
|
|
$effect(() => {
|
|
const host = rootEl?.closest(".timetable-calendar-controls");
|
|
host?.classList.toggle("bsplus-cal-menu-open", menuOpen);
|
|
return () => host?.classList.remove("bsplus-cal-menu-open");
|
|
});
|
|
|
|
$effect(() => {
|
|
if (!menuOpen || !menuEl) return;
|
|
syncMenuTheme();
|
|
updateMenuPosition();
|
|
const onLayout = () => updateMenuPosition();
|
|
window.addEventListener("resize", onLayout);
|
|
window.addEventListener("scroll", onLayout, true);
|
|
return () => {
|
|
window.removeEventListener("resize", onLayout);
|
|
window.removeEventListener("scroll", onLayout, true);
|
|
};
|
|
});
|
|
|
|
onMount(() => {
|
|
void refreshStatus().then(() => {
|
|
void maybeRunDueWeeklySync((message, isError) => {
|
|
showToastMessage(message, isError);
|
|
void refreshStatus();
|
|
});
|
|
});
|
|
|
|
const themeKeys = [
|
|
"selectedColor",
|
|
"DarkMode",
|
|
"adaptiveThemeColour",
|
|
"adaptiveThemeGradient",
|
|
"selectedTheme",
|
|
] as const;
|
|
const onThemeChange = () => syncMountedTheme();
|
|
for (const key of themeKeys) {
|
|
settingsState.register(key, onThemeChange);
|
|
}
|
|
|
|
const themeObserver = new MutationObserver(onThemeChange);
|
|
themeObserver.observe(document.documentElement, {
|
|
attributes: true,
|
|
attributeFilter: ["style", "class"],
|
|
});
|
|
|
|
const onDocPointer = (event: PointerEvent) => {
|
|
if (!menuOpen) return;
|
|
const target = event.target as Node;
|
|
if (rootEl?.contains(target)) return;
|
|
if (menuEl?.contains(target)) return;
|
|
menuOpen = false;
|
|
};
|
|
|
|
document.addEventListener("pointerdown", onDocPointer);
|
|
return () => {
|
|
document.removeEventListener("pointerdown", onDocPointer);
|
|
for (const key of themeKeys) {
|
|
settingsState.unregister(key, onThemeChange);
|
|
}
|
|
themeObserver.disconnect();
|
|
if (toastTimer) clearTimeout(toastTimer);
|
|
};
|
|
});
|
|
</script>
|
|
|
|
<div class="bsplus-cal-sync" bind:this={rootEl}>
|
|
<button
|
|
type="button"
|
|
class="uiButton bsplus-cal-trigger iconFamily"
|
|
bind:this={triggerEl}
|
|
class:bsplus-cal-trigger--open={menuOpen}
|
|
class:bsplus-cal-trigger--connected={status.connected}
|
|
class:bsplus-cal-trigger--busy={isBusy}
|
|
aria-haspopup="menu"
|
|
aria-expanded={menuOpen}
|
|
aria-busy={isBusy}
|
|
aria-label={status.connected ? "Google Calendar sync options" : "Connect Google Calendar"}
|
|
onclick={() => {
|
|
if (!isBusy) toggleMenu();
|
|
}}
|
|
>
|
|
<span class="bsplus-cal-trigger-icon" aria-hidden="true"></span>
|
|
<span class="bsplus-cal-trigger-text">Calendar</span>
|
|
{#if status.connected}
|
|
<span class="bsplus-cal-status-dot" aria-hidden="true"></span>
|
|
{/if}
|
|
</button>
|
|
|
|
{#if menuOpen}
|
|
<div
|
|
class="bsplus-cal-menu"
|
|
role="menu"
|
|
bind:this={menuEl}
|
|
style={menuStyle}
|
|
use:portalMenu
|
|
transition:fly={{ y: -6, duration: 160 }}
|
|
>
|
|
<div class="bsplus-cal-menu-header">
|
|
<span class="bsplus-cal-menu-title">Calendar sync</span>
|
|
<span class="bsplus-cal-menu-sub">Connect providers to sync your timetable</span>
|
|
</div>
|
|
|
|
<div class="bsplus-cal-provider" role="none">
|
|
<div class="bsplus-cal-provider-row">
|
|
<span class="bsplus-cal-provider-icon" aria-hidden="true">
|
|
<svg viewBox="0 0 24 24">
|
|
<path
|
|
fill="#4285F4"
|
|
d="M22 12c0-.96-.08-1.88-.24-2.76H12v5.22h5.68c-.24 1.28-.96 2.44-2.04 3.18v2.64h3.3c1.92-1.76 3.06-4.36 3.06-7.28z"
|
|
/>
|
|
<path
|
|
fill="#34A853"
|
|
d="M12 22c2.76 0 5.08-.92 6.78-2.5l-3.3-2.64c-.92.62-2.1.98-3.48.98-2.68 0-4.96-1.8-5.78-4.22H2.18v2.72A10 10 0 0 0 12 22z"
|
|
/>
|
|
<path
|
|
fill="#FBBC05"
|
|
d="M6.22 13.62A5.98 5.98 0 0 1 5.82 12c0-.56.1-1.1.28-1.62V7.66H2.18A10 10 0 0 0 2 12c0 1.62.38 3.16 1.06 4.52l3.16-2.9z"
|
|
/>
|
|
<path
|
|
fill="#EA4335"
|
|
d="M12 5.38c1.5 0 2.84.52 3.9 1.54l2.92-2.92C17.08 2.34 14.76 1.2 12 1.2 7.54 1.2 3.72 3.94 2.18 7.66l4.04 3.14c.82-2.42 3.1-4.22 5.78-4.22z"
|
|
/>
|
|
</svg>
|
|
</span>
|
|
<div class="bsplus-cal-provider-copy">
|
|
<span class="bsplus-cal-provider-name">
|
|
<span class="bsplus-google-word bsplus-google-word--sm">
|
|
<span class="bsplus-google-g">G</span><span class="bsplus-google-o1">o</span><span class="bsplus-google-o2">o</span><span class="bsplus-google-g2">g</span><span class="bsplus-google-l">l</span><span class="bsplus-google-e">e</span>
|
|
</span>
|
|
<span> Calendar</span>
|
|
</span>
|
|
<span class="bsplus-cal-provider-status">
|
|
{#if !status.configured}
|
|
Not available in this build
|
|
{:else if status.connected}
|
|
Connected{formatLastSync(status.lastSyncAt) ? ` · ${formatLastSync(status.lastSyncAt)}` : ""}
|
|
{:else}
|
|
Not connected
|
|
{/if}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{#if status.connected}
|
|
<div class="bsplus-cal-settings" role="group" aria-label="Sync settings">
|
|
<label class="bsplus-cal-setting">
|
|
<span class="bsplus-cal-setting-label">Weeks ahead</span>
|
|
<input
|
|
type="number"
|
|
class="bsplus-cal-setting-input"
|
|
min={GOOGLE_CALENDAR_SYNC_WEEKS_MIN}
|
|
max={GOOGLE_CALENDAR_SYNC_WEEKS_MAX}
|
|
value={syncWeeksAhead}
|
|
disabled={isBusy}
|
|
onchange={(e) => void onWeeksAheadChange(e)}
|
|
/>
|
|
</label>
|
|
<label class="bsplus-cal-setting bsplus-cal-setting--toggle">
|
|
<span class="bsplus-cal-setting-label">Auto-sync weekly</span>
|
|
<input
|
|
type="checkbox"
|
|
class="bsplus-cal-setting-checkbox"
|
|
checked={autoSyncWeekly}
|
|
disabled={isBusy}
|
|
onchange={(e) => void onAutoSyncToggle(e)}
|
|
/>
|
|
</label>
|
|
<p class="bsplus-cal-setting-hint">
|
|
Keeps a rolling {syncWeeksAhead}-week window. Each week adds the next week and removes the oldest.
|
|
</p>
|
|
</div>
|
|
{/if}
|
|
|
|
<CalendarSyncProgress progress={syncProgress} />
|
|
|
|
<div class="bsplus-cal-provider-actions">
|
|
{#if !status.connected}
|
|
<button
|
|
type="button"
|
|
class="bsplus-cal-action bsplus-cal-action--primary"
|
|
style:--bsplus-cal-accent={accent}
|
|
role="menuitem"
|
|
disabled={!status.configured || isBusy}
|
|
onclick={() => void connectGoogle()}
|
|
>
|
|
{busy === "connect" ? "Connecting…" : "Connect"}
|
|
</button>
|
|
{:else}
|
|
<button
|
|
type="button"
|
|
class="bsplus-cal-action bsplus-cal-action--primary"
|
|
style:--bsplus-cal-accent={accent}
|
|
role="menuitem"
|
|
disabled={isBusy}
|
|
onclick={() => void syncTimetable()}
|
|
>
|
|
{busy === "sync" ? "Syncing…" : "Sync now"}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="bsplus-cal-action bsplus-cal-action--ghost"
|
|
role="menuitem"
|
|
disabled={isBusy}
|
|
onclick={() => {
|
|
showDeleteEvents = true;
|
|
}}
|
|
>
|
|
{busy === "delete" ? "Removing…" : "Remove from calendar"}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="bsplus-cal-action bsplus-cal-action--ghost"
|
|
role="menuitem"
|
|
disabled={isBusy}
|
|
onclick={() => {
|
|
showDisconnect = true;
|
|
}}
|
|
>
|
|
Disconnect
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bsplus-cal-coming-soon" role="none">
|
|
<span class="bsplus-cal-coming-soon-label">More providers</span>
|
|
<span class="bsplus-cal-coming-soon-hint">Outlook, Apple Calendar — coming soon</span>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<CalendarDeleteEventsModal
|
|
open={showDeleteEvents}
|
|
busy={busy === "delete"}
|
|
onCancel={() => {
|
|
if (busy !== "delete") showDeleteEvents = false;
|
|
}}
|
|
onConfirm={confirmDeleteEvents}
|
|
/>
|
|
|
|
<CalendarDisconnectModal
|
|
open={showDisconnect}
|
|
busy={busy === "disconnect"}
|
|
onCancel={() => {
|
|
if (busy !== "disconnect") showDisconnect = false;
|
|
}}
|
|
onConfirm={confirmDisconnect}
|
|
/>
|
|
|
|
{#if toast}
|
|
<div
|
|
class="bsplus-cal-toast"
|
|
class:bsplus-cal-toast--error={toast.error}
|
|
role="status"
|
|
transition:fade={{ duration: 150 }}
|
|
>
|
|
{toast.message}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<style>
|
|
.bsplus-cal-sync {
|
|
position: relative;
|
|
display: inline-flex;
|
|
font-family: Rubik, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
color: var(--bsplus-cal-text, var(--text-primary, #111));
|
|
}
|
|
|
|
.bsplus-cal-trigger {
|
|
position: relative;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 6px;
|
|
min-width: auto;
|
|
height: auto;
|
|
padding: 0 10px;
|
|
margin-left: 4px;
|
|
border-radius: 16px !important;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.bsplus-cal-trigger-icon {
|
|
font-size: 16px;
|
|
line-height: 1;
|
|
opacity: 0.9;
|
|
}
|
|
|
|
.bsplus-cal-trigger-text {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
line-height: 1;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.bsplus-google-word {
|
|
display: inline-flex;
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
letter-spacing: -0.02em;
|
|
}
|
|
|
|
.bsplus-google-word--sm {
|
|
font-size: 13px;
|
|
}
|
|
|
|
.bsplus-google-g,
|
|
.bsplus-google-g2 {
|
|
color: #4285f4;
|
|
}
|
|
|
|
.bsplus-google-o1,
|
|
.bsplus-google-e {
|
|
color: #ea4335;
|
|
}
|
|
|
|
.bsplus-google-o2 {
|
|
color: #fbbc05;
|
|
}
|
|
|
|
.bsplus-google-l {
|
|
color: #34a853;
|
|
}
|
|
|
|
.bsplus-cal-trigger:hover:not(.bsplus-cal-trigger--busy) {
|
|
transform: scale(1.03);
|
|
}
|
|
|
|
.bsplus-cal-trigger:active:not(.bsplus-cal-trigger--busy) {
|
|
transform: scale(0.97);
|
|
}
|
|
|
|
.bsplus-cal-trigger--open {
|
|
background: color-mix(in srgb, var(--bsplus-cal-accent, var(--better-main, #3b82f6)) 14%, transparent) !important;
|
|
}
|
|
|
|
.bsplus-cal-trigger--connected .bsplus-cal-status-dot {
|
|
display: block;
|
|
}
|
|
|
|
.bsplus-cal-trigger--busy {
|
|
opacity: 0.85;
|
|
cursor: wait;
|
|
}
|
|
|
|
.bsplus-cal-status-dot {
|
|
position: absolute;
|
|
top: 4px;
|
|
right: 4px;
|
|
width: 7px;
|
|
height: 7px;
|
|
border-radius: 999px;
|
|
background: #22c55e;
|
|
box-shadow: 0 0 0 2px var(--bsplus-cal-surface, #fff);
|
|
display: none;
|
|
}
|
|
|
|
.bsplus-cal-settings {
|
|
margin: 8px 0 10px;
|
|
padding: 10px;
|
|
border-radius: 10px;
|
|
border: 1px solid var(--bsplus-cal-border, color-mix(in srgb, var(--bsplus-cal-text) 12%, transparent));
|
|
background: color-mix(in srgb, var(--bsplus-cal-surface, #fff) 92%, transparent);
|
|
display: grid;
|
|
gap: 8px;
|
|
}
|
|
|
|
.bsplus-cal-setting {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 10px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.bsplus-cal-setting-label {
|
|
font-weight: 600;
|
|
color: var(--bsplus-cal-text, var(--text-primary, #111));
|
|
}
|
|
|
|
.bsplus-cal-setting-input {
|
|
width: 64px;
|
|
padding: 6px 8px;
|
|
border-radius: 10px;
|
|
border: 1px solid var(--bsplus-cal-border, color-mix(in srgb, var(--bsplus-cal-text) 18%, transparent));
|
|
background: var(--bsplus-cal-surface, #fff);
|
|
color: var(--bsplus-cal-text, var(--text-primary, #111));
|
|
font-size: 12px;
|
|
text-align: center;
|
|
}
|
|
|
|
.bsplus-cal-setting-checkbox {
|
|
width: 16px;
|
|
height: 16px;
|
|
accent-color: var(--bsplus-cal-accent, var(--better-main, #3b82f6));
|
|
}
|
|
|
|
.bsplus-cal-setting-hint {
|
|
margin: 0;
|
|
font-size: 10px;
|
|
line-height: 1.4;
|
|
color: color-mix(in srgb, var(--bsplus-cal-text, #111) 58%, transparent);
|
|
}
|
|
|
|
.bsplus-cal-menu {
|
|
position: fixed;
|
|
z-index: 2147483647;
|
|
width: min(320px, calc(100vw - 24px));
|
|
padding: 10px;
|
|
border-radius: 14px;
|
|
background: var(--bsplus-cal-surface, #fff);
|
|
color: var(--bsplus-cal-text, #18181b);
|
|
border: 1px solid var(--bsplus-cal-border, color-mix(in srgb, var(--bsplus-cal-text) 12%, transparent));
|
|
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.22);
|
|
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
}
|
|
|
|
.bsplus-cal-menu.dark {
|
|
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.45);
|
|
}
|
|
|
|
.bsplus-cal-menu-header {
|
|
padding: 6px 8px 10px;
|
|
border-bottom: 1px solid var(--bsplus-cal-border, color-mix(in srgb, var(--bsplus-cal-text) 10%, transparent));
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.bsplus-cal-menu-title {
|
|
display: block;
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.bsplus-cal-menu-sub {
|
|
display: block;
|
|
margin-top: 2px;
|
|
font-size: 11px;
|
|
color: color-mix(in srgb, var(--bsplus-cal-text, #111) 62%, transparent);
|
|
}
|
|
|
|
.bsplus-cal-provider {
|
|
padding: 8px;
|
|
border-radius: 10px;
|
|
background: var(--bsplus-cal-surface-muted, color-mix(in srgb, var(--bsplus-cal-text) 4%, transparent));
|
|
}
|
|
|
|
.bsplus-cal-provider-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.bsplus-cal-provider-icon {
|
|
display: flex;
|
|
flex: 0 0 auto;
|
|
width: 28px;
|
|
height: 28px;
|
|
}
|
|
|
|
.bsplus-cal-provider-icon svg {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
.bsplus-cal-provider-copy {
|
|
min-width: 0;
|
|
}
|
|
|
|
.bsplus-cal-provider-name {
|
|
display: block;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.bsplus-cal-provider-status {
|
|
display: block;
|
|
margin-top: 1px;
|
|
font-size: 11px;
|
|
color: color-mix(in srgb, var(--bsplus-cal-text, #111) 62%, transparent);
|
|
}
|
|
|
|
.bsplus-cal-provider-actions {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 6px;
|
|
}
|
|
|
|
.bsplus-cal-action {
|
|
flex: 1 1 auto;
|
|
min-width: 0;
|
|
padding: 7px 10px;
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.bsplus-cal-action:disabled {
|
|
opacity: 0.55;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.bsplus-cal-action--primary {
|
|
background: var(--bsplus-cal-accent, var(--better-main, #3b82f6));
|
|
color: #fff;
|
|
}
|
|
|
|
.bsplus-cal-action--primary:hover:not(:disabled) {
|
|
filter: brightness(1.06);
|
|
transform: scale(1.02);
|
|
}
|
|
|
|
.bsplus-cal-action--ghost {
|
|
background: var(--bsplus-cal-surface-muted, color-mix(in srgb, var(--bsplus-cal-text) 8%, transparent));
|
|
color: var(--bsplus-cal-text, #18181b);
|
|
}
|
|
|
|
.bsplus-cal-action--ghost:hover:not(:disabled) {
|
|
background: color-mix(in srgb, var(--bsplus-cal-text) 14%, var(--bsplus-cal-surface));
|
|
}
|
|
|
|
.bsplus-cal-coming-soon {
|
|
margin-top: 8px;
|
|
padding: 8px 10px;
|
|
border-radius: 8px;
|
|
border: 1px dashed var(--bsplus-cal-border, color-mix(in srgb, var(--bsplus-cal-text) 18%, transparent));
|
|
}
|
|
|
|
.bsplus-cal-coming-soon-label {
|
|
display: block;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
color: color-mix(in srgb, var(--bsplus-cal-text, #111) 70%, transparent);
|
|
}
|
|
|
|
.bsplus-cal-coming-soon-hint {
|
|
display: block;
|
|
margin-top: 2px;
|
|
font-size: 10px;
|
|
color: color-mix(in srgb, var(--bsplus-cal-text, #111) 50%, transparent);
|
|
}
|
|
|
|
.bsplus-cal-toast {
|
|
position: fixed;
|
|
right: 16px;
|
|
bottom: 16px;
|
|
z-index: 100000;
|
|
max-width: min(360px, calc(100vw - 32px));
|
|
padding: 12px 14px;
|
|
border-radius: 12px;
|
|
background: rgba(20, 20, 20, 0.92);
|
|
color: #fff;
|
|
font-size: 13px;
|
|
line-height: 1.4;
|
|
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.25);
|
|
pointer-events: none;
|
|
}
|
|
|
|
.bsplus-cal-toast--error {
|
|
background: rgba(120, 24, 24, 0.95);
|
|
}
|
|
</style>
|