mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-08-28 17:21:05 +00:00
feat: some nice animations for settings areas missing it & fix update video
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { fade } from "svelte/transition";
|
import { fade, scale } from "svelte/transition";
|
||||||
import { onMount } from "svelte";
|
import { onMount, tick } from "svelte";
|
||||||
import Switch from "./Switch.svelte";
|
import Switch from "./Switch.svelte";
|
||||||
import {
|
import {
|
||||||
FEEDBACK_CATEGORIES,
|
FEEDBACK_CATEGORIES,
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
type FeedbackStatusItem,
|
type FeedbackStatusItem,
|
||||||
} from "@/seqta/utils/feedback/client";
|
} from "@/seqta/utils/feedback/client";
|
||||||
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||||
|
import Spinner from "./Spinner.svelte";
|
||||||
|
|
||||||
let { onClose, initialFeedbackId = null } = $props<{
|
let { onClose, initialFeedbackId = null } = $props<{
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -65,6 +66,63 @@
|
|||||||
const btnPrimary = $derived(
|
const btnPrimary = $derived(
|
||||||
`${btn} ${isDark ? "bg-zinc-200 text-zinc-900" : "bg-zinc-800 text-white"}`,
|
`${btn} ${isDark ? "bg-zinc-200 text-zinc-900" : "bg-zinc-800 text-white"}`,
|
||||||
);
|
);
|
||||||
|
const panelKey = $derived(
|
||||||
|
tab === "send"
|
||||||
|
? successId
|
||||||
|
? "send-success"
|
||||||
|
: "send-form"
|
||||||
|
: selectedItem
|
||||||
|
? `status-${selectedItem.id}`
|
||||||
|
: "status-list",
|
||||||
|
);
|
||||||
|
|
||||||
|
let panelBodyEl = $state<HTMLDivElement | null>(null);
|
||||||
|
let panelHeightPx = $state<number | null>(null);
|
||||||
|
|
||||||
|
const panelHeightEase = "cubic-bezier(0.22, 1, 0.36, 1)";
|
||||||
|
const panelHeightMotion = $derived(
|
||||||
|
$settingsState.animations && panelHeightPx !== null
|
||||||
|
? `height 280ms ${panelHeightEase}`
|
||||||
|
: "none",
|
||||||
|
);
|
||||||
|
|
||||||
|
function measurePanelHeight() {
|
||||||
|
if (!panelBodyEl) return;
|
||||||
|
panelHeightPx = panelBodyEl.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function panelMeasure(node: HTMLDivElement) {
|
||||||
|
panelBodyEl = node;
|
||||||
|
|
||||||
|
const update = () => {
|
||||||
|
panelHeightPx = node.scrollHeight;
|
||||||
|
};
|
||||||
|
|
||||||
|
update();
|
||||||
|
const ro = new ResizeObserver(() => update());
|
||||||
|
ro.observe(node);
|
||||||
|
|
||||||
|
return {
|
||||||
|
destroy() {
|
||||||
|
ro.disconnect();
|
||||||
|
if (panelBodyEl === node) panelBodyEl = null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
panelKey;
|
||||||
|
includeContact;
|
||||||
|
errorMessage;
|
||||||
|
statusLoading;
|
||||||
|
statusItems.length;
|
||||||
|
selectedItem;
|
||||||
|
void tick().then(() => measurePanelHeight());
|
||||||
|
});
|
||||||
|
|
||||||
|
function isStatusDetailKey(key: string): boolean {
|
||||||
|
return key.startsWith("status-") && key !== "status-list";
|
||||||
|
}
|
||||||
|
|
||||||
function errText(e: unknown): string {
|
function errText(e: unknown): string {
|
||||||
if (e instanceof FeedbackApiError) return e.message;
|
if (e instanceof FeedbackApiError) return e.message;
|
||||||
@@ -112,6 +170,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
void tick().then(() => measurePanelHeight());
|
||||||
if (typeof initialFeedbackId === "string" && initialFeedbackId) {
|
if (typeof initialFeedbackId === "string" && initialFeedbackId) {
|
||||||
void openStatusItem(initialFeedbackId);
|
void openStatusItem(initialFeedbackId);
|
||||||
}
|
}
|
||||||
@@ -166,11 +225,11 @@
|
|||||||
}}
|
}}
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
transition:fade={{ duration: 150 }}
|
transition:fade={{ duration: $settingsState.animations ? 200 : 0 }}
|
||||||
>
|
>
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<div
|
<div
|
||||||
class="p-5 mx-4 w-full max-w-lg max-h-[90vh] overflow-y-auto rounded-2xl shadow-2xl border text-[18px] {isDark
|
class="feedback-dialog p-5 mx-4 w-full max-w-lg max-h-[90vh] overflow-y-auto rounded-2xl shadow-2xl border text-[18px] {isDark
|
||||||
? 'bg-zinc-800 text-white border-zinc-700'
|
? 'bg-zinc-800 text-white border-zinc-700'
|
||||||
: 'bg-white text-zinc-900 border-zinc-200'}"
|
: 'bg-white text-zinc-900 border-zinc-200'}"
|
||||||
onclick={(e) => e.stopPropagation()}
|
onclick={(e) => e.stopPropagation()}
|
||||||
@@ -179,6 +238,12 @@
|
|||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="feedback-modal-title"
|
aria-labelledby="feedback-modal-title"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
|
in:scale={{
|
||||||
|
duration: $settingsState.animations ? 280 : 0,
|
||||||
|
start: 0.92,
|
||||||
|
opacity: 0,
|
||||||
|
easing: (t) => 1 - Math.pow(1 - t, 3),
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex gap-1 p-1 mb-4 rounded-full {isDark ? 'bg-zinc-900' : 'bg-zinc-100'}"
|
class="flex gap-1 p-1 mb-4 rounded-full {isDark ? 'bg-zinc-900' : 'bg-zinc-100'}"
|
||||||
@@ -193,8 +258,13 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if tab === "send"}
|
<div
|
||||||
{#if successId}
|
class="feedback-panel"
|
||||||
|
style:height={panelHeightPx === null ? "auto" : `${panelHeightPx}px`}
|
||||||
|
style:transition={panelHeightMotion}
|
||||||
|
>
|
||||||
|
<div class="feedback-panel__body" use:panelMeasure>
|
||||||
|
{#if panelKey === "send-success" && successId}
|
||||||
<h2 id="feedback-modal-title" class="mb-3 text-xl font-bold">Thanks for the feedback</h2>
|
<h2 id="feedback-modal-title" class="mb-3 text-xl font-bold">Thanks for the feedback</h2>
|
||||||
<p class="mb-2 text-zinc-600 dark:text-zinc-300">Reference ID:</p>
|
<p class="mb-2 text-zinc-600 dark:text-zinc-300">Reference ID:</p>
|
||||||
<p class="mb-4 px-3 py-2 font-mono text-base rounded-lg {isDark ? 'bg-zinc-900' : 'bg-zinc-100'} break-all">{successId}</p>
|
<p class="mb-4 px-3 py-2 font-mono text-base rounded-lg {isDark ? 'bg-zinc-900' : 'bg-zinc-100'} break-all">{successId}</p>
|
||||||
@@ -202,7 +272,7 @@
|
|||||||
<button type="button" class={btnMuted} onclick={() => successId && openStatusItem(successId)}>Check status</button>
|
<button type="button" class={btnMuted} onclick={() => successId && openStatusItem(successId)}>Check status</button>
|
||||||
<button type="button" class={btnPrimary} onclick={onClose}>Done</button>
|
<button type="button" class={btnPrimary} onclick={onClose}>Done</button>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else if panelKey === "send-form"}
|
||||||
<h2 id="feedback-modal-title" class="mb-1 text-xl font-bold">Send feedback</h2>
|
<h2 id="feedback-modal-title" class="mb-1 text-xl font-bold">Send feedback</h2>
|
||||||
<p class="mb-4 text-zinc-600 dark:text-zinc-400">Anonymous by default. Contact and school details are optional.</p>
|
<p class="mb-4 text-zinc-600 dark:text-zinc-400">Anonymous by default. Contact and school details are optional.</p>
|
||||||
|
|
||||||
@@ -274,8 +344,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{:else if isStatusDetailKey(panelKey) && selectedItem}
|
||||||
{:else if selectedItem}
|
|
||||||
<div class="flex items-start justify-between gap-3 mb-3">
|
<div class="flex items-start justify-between gap-3 mb-3">
|
||||||
<h2 id="feedback-modal-title" class="text-xl font-bold">Feedback status</h2>
|
<h2 id="feedback-modal-title" class="text-xl font-bold">Feedback status</h2>
|
||||||
<button type="button" class="{btnMuted} !text-base !px-3 !py-1.5" onclick={() => { selectedItem = null; void loadStatusList(); }}>Back</button>
|
<button type="button" class="{btnMuted} !text-base !px-3 !py-1.5" onclick={() => { selectedItem = null; void loadStatusList(); }}>Back</button>
|
||||||
@@ -292,7 +361,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{#if statusError}<p class="mb-3 text-red-600 dark:text-red-400" role="alert">{statusError}</p>{/if}
|
{#if statusError}<p class="mb-3 text-red-600 dark:text-red-400" role="alert">{statusError}</p>{/if}
|
||||||
<div class="flex gap-3 justify-end">
|
<div class="flex gap-3 justify-end">
|
||||||
<button type="button" class={btnMuted} onclick={() => openStatusItem(selectedItem.id)} disabled={statusLoading}>
|
<button type="button" class={btnMuted} onclick={() => openStatusItem(selectedItem!.id)} disabled={statusLoading}>
|
||||||
{statusLoading ? "Refreshing…" : "Refresh"}
|
{statusLoading ? "Refreshing…" : "Refresh"}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class={btnPrimary} onclick={onClose}>Close</button>
|
<button type="button" class={btnPrimary} onclick={onClose}>Close</button>
|
||||||
@@ -306,7 +375,15 @@
|
|||||||
</div>
|
</div>
|
||||||
{#if statusError}<p class="mb-3 text-red-600 dark:text-red-400" role="alert">{statusError}</p>{/if}
|
{#if statusError}<p class="mb-3 text-red-600 dark:text-red-400" role="alert">{statusError}</p>{/if}
|
||||||
{#if statusLoading && !statusItems.length}
|
{#if statusLoading && !statusItems.length}
|
||||||
<p class="text-zinc-500">Loading…</p>
|
<div class="feedback-loading" aria-busy="true" aria-live="polite">
|
||||||
|
<Spinner size="md" />
|
||||||
|
<p class="text-zinc-500">Checking your feedback…</p>
|
||||||
|
<div class="feedback-loading__skeleton" aria-hidden="true">
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{:else if !statusItems.length}
|
{:else if !statusItems.length}
|
||||||
<p class="mb-4 text-zinc-500">No feedback yet.</p>
|
<p class="mb-4 text-zinc-500">No feedback yet.</p>
|
||||||
<button type="button" class={btnPrimary} onclick={() => selectTab("send")}>Send feedback</button>
|
<button type="button" class={btnPrimary} onclick={() => selectTab("send")}>Send feedback</button>
|
||||||
@@ -335,9 +412,70 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.feedback-dialog {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-panel {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-panel__body {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-loading {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
min-height: 14rem;
|
||||||
|
padding: 1rem 0 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-loading__skeleton {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-loading__skeleton span {
|
||||||
|
display: block;
|
||||||
|
height: 3.5rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background: color-mix(in srgb, currentColor 8%, transparent);
|
||||||
|
animation: feedback-skeleton-pulse 1.2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-loading__skeleton span:nth-child(2) {
|
||||||
|
animation-delay: 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-loading__skeleton span:nth-child(3) {
|
||||||
|
animation-delay: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes feedback-skeleton-pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 0.45;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
:global(.feedback-field),
|
:global(.feedback-field),
|
||||||
:global(.feedback-field option) {
|
:global(.feedback-field option) {
|
||||||
-webkit-text-fill-color: currentColor !important;
|
-webkit-text-fill-color: currentColor !important;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { cloudAuth } from "@/seqta/utils/CloudAuth";
|
import { cloudAuth } from "@/seqta/utils/CloudAuth";
|
||||||
import CloudPfpAvatar from "@/interface/components/CloudPfpAvatar.svelte";
|
import CloudPfpAvatar from "@/interface/components/CloudPfpAvatar.svelte";
|
||||||
|
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||||
|
|
||||||
let { alwaysShowUserName = false, onClick = undefined } = $props<{
|
let { alwaysShowUserName = false, onClick = undefined } = $props<{
|
||||||
alwaysShowUserName?: boolean;
|
alwaysShowUserName?: boolean;
|
||||||
@@ -19,7 +20,7 @@
|
|||||||
return unsubscribe;
|
return unsubscribe;
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleClickOutside(e: MouseEvent) {
|
function handleClickOutside(e: PointerEvent) {
|
||||||
if (dropdownEl && !dropdownEl.contains(e.target as Node)) {
|
if (dropdownEl && !dropdownEl.contains(e.target as Node)) {
|
||||||
open = false;
|
open = false;
|
||||||
}
|
}
|
||||||
@@ -28,11 +29,11 @@
|
|||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
document.addEventListener("click", handleClickOutside);
|
document.addEventListener("pointerdown", handleClickOutside);
|
||||||
}, 0);
|
}, 0);
|
||||||
return () => {
|
return () => {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
document.removeEventListener("click", handleClickOutside);
|
document.removeEventListener("pointerdown", handleClickOutside);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -47,7 +48,8 @@
|
|||||||
open = false;
|
open = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleButtonClick() {
|
function handleButtonClick(event: MouseEvent) {
|
||||||
|
event.stopPropagation();
|
||||||
if (onClick) {
|
if (onClick) {
|
||||||
onClick();
|
onClick();
|
||||||
} else {
|
} else {
|
||||||
@@ -65,7 +67,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="relative flex items-center" bind:this={dropdownEl}>
|
<div class="relative z-20 flex shrink-0 items-center" bind:this={dropdownEl}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={handleButtonClick}
|
onclick={handleButtonClick}
|
||||||
@@ -100,7 +102,8 @@
|
|||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<div
|
<div
|
||||||
class="absolute right-0 top-full mt-2 w-80 rounded-xl border border-zinc-200 dark:border-zinc-600 bg-white dark:bg-zinc-800 shadow-xl z-[100] overflow-hidden"
|
class="cloud-dropdown absolute right-0 top-full mt-2 w-80 rounded-xl border border-zinc-200 dark:border-zinc-600 bg-white dark:bg-zinc-800 shadow-xl z-[100] overflow-hidden"
|
||||||
|
class:cloud-dropdown--motion={$settingsState.animations}
|
||||||
onclick={(e) => e.stopPropagation()}
|
onclick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<div class="p-4 border-b border-zinc-200 dark:border-zinc-600">
|
<div class="p-4 border-b border-zinc-200 dark:border-zinc-600">
|
||||||
@@ -160,3 +163,21 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.cloud-dropdown--motion {
|
||||||
|
transform-origin: top right;
|
||||||
|
animation: cloud-dropdown-in 0.22s cubic-bezier(0.22, 1, 0.36, 1) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes cloud-dropdown-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.94) translateY(-6px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import MotionDiv from "../MotionDiv.svelte";
|
import MotionDiv from "../MotionDiv.svelte";
|
||||||
import PlainCloseButton from "../PlainCloseButton.svelte";
|
import PlainCloseButton from "../PlainCloseButton.svelte";
|
||||||
import CloudHeader from "./CloudHeader.svelte";
|
import CloudHeader from "./CloudHeader.svelte";
|
||||||
|
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||||
|
|
||||||
type SettingsPage = "settings" | "themes" | "backgrounds";
|
type SettingsPage = "settings" | "themes" | "backgrounds";
|
||||||
|
|
||||||
@@ -33,10 +34,12 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
const activeIndex = $derived(tabs.findIndex((tab) => tab.id === activePage));
|
const activeIndex = $derived(tabs.findIndex((tab) => tab.id === activePage));
|
||||||
|
const motion = $derived($settingsState.animations ? "var(--bs-settings-ease)" : "0s");
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header
|
<header
|
||||||
class="flex shrink-0 items-center gap-4 border-b border-zinc-200/60 px-5 py-4 dark:border-zinc-700/50"
|
class="flex shrink-0 items-center gap-4 border-b border-zinc-200/60 px-5 py-4 dark:border-zinc-700/50"
|
||||||
|
style="--bs-settings-ease: 0.28s cubic-bezier(0.22, 1, 0.36, 1)"
|
||||||
>
|
>
|
||||||
<button type="button" class="hidden shrink-0 lg:block" onclick={onLogoClick}>
|
<button type="button" class="hidden shrink-0 lg:block" onclick={onLogoClick}>
|
||||||
<img
|
<img
|
||||||
@@ -52,7 +55,7 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="h-12 min-w-[14rem] flex-1 rounded-full bg-zinc-100/80 p-1 dark:bg-zinc-900/50"
|
class="tab-track h-12 min-w-0 flex-1 rounded-full bg-zinc-100/80 p-1 dark:bg-zinc-900/50"
|
||||||
role="tablist"
|
role="tablist"
|
||||||
aria-label="Settings pages"
|
aria-label="Settings pages"
|
||||||
>
|
>
|
||||||
@@ -68,7 +71,7 @@
|
|||||||
role="tab"
|
role="tab"
|
||||||
aria-selected={activePage === tab.id}
|
aria-selected={activePage === tab.id}
|
||||||
onclick={() => setActivePage(tab.id)}
|
onclick={() => setActivePage(tab.id)}
|
||||||
class="relative z-10 h-10 flex-1 rounded-full px-4 text-base transition-colors duration-200
|
class="relative z-10 h-10 flex-1 whitespace-nowrap rounded-full px-4 text-base transition-colors duration-200
|
||||||
{activePage === tab.id
|
{activePage === tab.id
|
||||||
? 'font-semibold text-zinc-900 dark:text-white'
|
? 'font-semibold text-zinc-900 dark:text-white'
|
||||||
: 'text-zinc-500 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white'}"
|
: 'text-zinc-500 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white'}"
|
||||||
@@ -80,8 +83,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex shrink-0 items-center gap-2">
|
<div class="flex shrink-0 items-center gap-2">
|
||||||
{#if showStoreTools}
|
<div
|
||||||
<label class="relative hidden w-40 md:block xl:w-56">
|
class="store-search-wrap hidden md:block"
|
||||||
|
class:store-search-wrap--open={showStoreTools}
|
||||||
|
style="transition: max-width {motion}, opacity {motion}, transform {motion}"
|
||||||
|
aria-hidden={!showStoreTools}
|
||||||
|
>
|
||||||
|
<label class="relative block w-40 md:w-48 xl:w-56">
|
||||||
<span class="sr-only">Search {activePage}</span>
|
<span class="sr-only">Search {activePage}</span>
|
||||||
<svg
|
<svg
|
||||||
class="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400"
|
class="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400"
|
||||||
@@ -96,11 +104,14 @@
|
|||||||
type="search"
|
type="search"
|
||||||
placeholder={activePage === "themes" ? "Search themes" : "Search backgrounds"}
|
placeholder={activePage === "themes" ? "Search themes" : "Search backgrounds"}
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
|
tabindex={showStoreTools ? undefined : -1}
|
||||||
oninput={(event) => setSearchTerm(event.currentTarget.value)}
|
oninput={(event) => setSearchTerm(event.currentTarget.value)}
|
||||||
class="store-search h-12 w-full rounded-full bg-zinc-100/80 pl-9 pr-4 text-sm text-zinc-900 transition-colors duration-150 placeholder:text-zinc-400 focus:bg-zinc-200/70 dark:bg-zinc-900/50 dark:text-white dark:focus:bg-zinc-700"
|
class="store-search h-12 w-full rounded-full bg-zinc-100/80 pl-9 pr-4 text-sm text-zinc-900 transition-colors duration-150 placeholder:text-zinc-400 focus:bg-zinc-200/70 dark:bg-zinc-900/50 dark:text-white dark:focus:bg-zinc-700"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if showStoreTools}
|
||||||
<CloudHeader />
|
<CloudHeader />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -109,6 +120,27 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.store-search-wrap {
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(8px);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-search-wrap--open {
|
||||||
|
max-width: 10rem;
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
.store-search-wrap--open {
|
||||||
|
max-width: 14rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.store-search,
|
.store-search,
|
||||||
.store-search:focus,
|
.store-search:focus,
|
||||||
.store-search:focus-visible {
|
.store-search:focus-visible {
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
let compactActiveTab = $state(0);
|
let compactActiveTab = $state(0);
|
||||||
let activePage = $state<PageId>("settings");
|
let activePage = $state<PageId>("settings");
|
||||||
let activeSection = $state("general");
|
let activeSection = $state("general");
|
||||||
let activeThemeView = $state<ThemeView>("theme-settings");
|
let activeThemeView = $state<ThemeView>("theme-store");
|
||||||
let activeBackgroundView = $state<BackgroundView>("background-settings");
|
let activeBackgroundView = $state<BackgroundView>("background-settings");
|
||||||
let selectedBackgroundCategory = $state("All");
|
let selectedBackgroundCategory = $state("All");
|
||||||
let backgroundCategories = $state<string[]>([]);
|
let backgroundCategories = $state<string[]>([]);
|
||||||
@@ -93,9 +93,9 @@
|
|||||||
{
|
{
|
||||||
label: "Themes",
|
label: "Themes",
|
||||||
items: [
|
items: [
|
||||||
|
{ id: "theme-store", label: "Store" },
|
||||||
{ id: "theme-settings", label: "Theme settings" },
|
{ id: "theme-settings", label: "Theme settings" },
|
||||||
{ id: "create-theme", label: "Create theme" },
|
{ id: "create-theme", label: "Create theme" },
|
||||||
{ id: "theme-store", label: "Store" },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -356,7 +356,10 @@
|
|||||||
searchTerm={storeSearchTerm}
|
searchTerm={storeSearchTerm}
|
||||||
setSearchTerm={(term) => (storeSearchTerm = term)}
|
setSearchTerm={(term) => (storeSearchTerm = term)}
|
||||||
{activePage}
|
{activePage}
|
||||||
setActivePage={(page) => (activePage = page)}
|
setActivePage={(page) => {
|
||||||
|
activePage = page;
|
||||||
|
if (page === "themes") activeThemeView = "theme-store";
|
||||||
|
}}
|
||||||
showStoreTools={isStoreView}
|
showStoreTools={isStoreView}
|
||||||
onLogoClick={handleDevModeToggle}
|
onLogoClick={handleDevModeToggle}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
|
|||||||
@@ -66,6 +66,7 @@
|
|||||||
let rootEl = $state<HTMLDivElement | null>(null);
|
let rootEl = $state<HTMLDivElement | null>(null);
|
||||||
let triggerEl = $state<HTMLButtonElement | null>(null);
|
let triggerEl = $state<HTMLButtonElement | null>(null);
|
||||||
let menuEl = $state<HTMLDivElement | null>(null);
|
let menuEl = $state<HTMLDivElement | null>(null);
|
||||||
|
let modalEl = $state<HTMLDivElement | null>(null);
|
||||||
let menuStyle = $state("");
|
let menuStyle = $state("");
|
||||||
let toastTimer: ReturnType<typeof setTimeout> | null = null;
|
let toastTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
@@ -325,12 +326,17 @@
|
|||||||
const themeHost = rootEl?.closest(".bsplus-calendar-sync-mount");
|
const themeHost = rootEl?.closest(".bsplus-calendar-sync-mount");
|
||||||
if (themeHost instanceof HTMLElement) syncCalendarSyncTheme(themeHost);
|
if (themeHost instanceof HTMLElement) syncCalendarSyncTheme(themeHost);
|
||||||
if (menuEl) syncCalendarSyncTheme(menuEl);
|
if (menuEl) syncCalendarSyncTheme(menuEl);
|
||||||
|
if (modalEl) syncCalendarSyncTheme(modalEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (menuOpen && menuEl) syncHostTheme();
|
if (menuOpen && menuEl) syncHostTheme();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (modalOpen && modalEl) syncHostTheme();
|
||||||
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!menuOpen || !triggerEl) return;
|
if (!menuOpen || !triggerEl) return;
|
||||||
updateMenuPosition();
|
updateMenuPosition();
|
||||||
@@ -550,6 +556,7 @@
|
|||||||
{#if modalOpen}
|
{#if modalOpen}
|
||||||
<div
|
<div
|
||||||
class="bsplus-cal-modal-backdrop"
|
class="bsplus-cal-modal-backdrop"
|
||||||
|
bind:this={modalEl}
|
||||||
use:portalToBody
|
use:portalToBody
|
||||||
onclick={(e) => {
|
onclick={(e) => {
|
||||||
if (e.target === e.currentTarget && !modalBusy) closeModal();
|
if (e.target === e.currentTarget && !modalBusy) closeModal();
|
||||||
@@ -963,6 +970,10 @@
|
|||||||
backdrop-filter: blur(4px);
|
backdrop-filter: blur(4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bsplus-cal-modal-backdrop.dark .bsplus-cal-modal {
|
||||||
|
box-shadow: 0 24px 48px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
.bsplus-cal-modal {
|
.bsplus-cal-modal {
|
||||||
width: min(100%, 400px);
|
width: min(100%, 400px);
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
|||||||
Binary file not shown.
+114
-35
@@ -1,5 +1,6 @@
|
|||||||
import browser from "webextension-polyfill";
|
import browser from "webextension-polyfill";
|
||||||
import stringToHTML from "@/seqta/utils/stringToHTML";
|
import stringToHTML from "@/seqta/utils/stringToHTML";
|
||||||
|
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||||
import loadingSpinner from "./loading-spinner.html?raw";
|
import loadingSpinner from "./loading-spinner.html?raw";
|
||||||
import loadingOverlay from "./loading-overlay.html?raw";
|
import loadingOverlay from "./loading-overlay.html?raw";
|
||||||
import { startLoadingCanvas } from "./loadingCanvas";
|
import { startLoadingCanvas } from "./loadingCanvas";
|
||||||
@@ -9,32 +10,46 @@ import {
|
|||||||
} from "./loadingVariants";
|
} from "./loadingVariants";
|
||||||
|
|
||||||
let stopCanvas: (() => void) | null = null;
|
let stopCanvas: (() => void) | null = null;
|
||||||
|
let unbindThemeGuard: (() => void) | null = null;
|
||||||
|
let activeLoading: {
|
||||||
|
host: HTMLElement;
|
||||||
|
shadow: ShadowRoot;
|
||||||
|
variant: LoadingVariant;
|
||||||
|
} | null = null;
|
||||||
|
|
||||||
|
const THEME_GUARD_KEYS = [
|
||||||
|
"selectedTheme",
|
||||||
|
"selectedColor",
|
||||||
|
"DarkMode",
|
||||||
|
"adaptiveThemeColour",
|
||||||
|
"selectedFont",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const LOADING_PALETTE = {
|
||||||
|
line: "rgba(255, 255, 255, 0.08)",
|
||||||
|
lineAccent: "rgba(255, 255, 255, 0.15)",
|
||||||
|
lineBlue: "rgba(96, 165, 250, 0.42)",
|
||||||
|
grid: "rgba(255, 255, 255, 0.028)",
|
||||||
|
text: "#f4f4f5",
|
||||||
|
} as const;
|
||||||
|
|
||||||
const loadingStyles = /* css */ `
|
const loadingStyles = /* css */ `
|
||||||
.bkloading {
|
:host {
|
||||||
--bk-line-color: rgba(255, 255, 255, 0.08);
|
--bk-line-color: ${LOADING_PALETTE.line};
|
||||||
--bk-line-accent: rgba(255, 255, 255, 0.15);
|
--bk-line-accent: ${LOADING_PALETTE.lineAccent};
|
||||||
--bk-line-blue: rgba(96, 165, 250, 0.42);
|
--bk-line-blue: ${LOADING_PALETTE.lineBlue};
|
||||||
--bk-grid-color: rgba(255, 255, 255, 0.028);
|
--bk-grid-color: ${LOADING_PALETTE.grid};
|
||||||
--bk-spin-outer: 1s;
|
--bk-spin-outer: 1s;
|
||||||
--bk-spin-inner: 3s;
|
--bk-spin-inner: 3s;
|
||||||
--bk-spin-small: 3s;
|
--bk-spin-small: 3s;
|
||||||
--bk-stage-name: bkloading-stage-in;
|
--bk-stage-name: bkloading-stage-in;
|
||||||
--bk-stage-duration: 0.85s;
|
--bk-stage-duration: 0.85s;
|
||||||
position: absolute;
|
color: ${LOADING_PALETTE.text};
|
||||||
inset: 0;
|
|
||||||
z-index: 1000000;
|
|
||||||
overflow: hidden;
|
|
||||||
contain: strict;
|
|
||||||
color: #f4f4f5;
|
|
||||||
background:
|
|
||||||
radial-gradient(ellipse 70% 45% at 50% 95%, rgba(37, 99, 235, 0.12), transparent 62%),
|
|
||||||
linear-gradient(180deg, #010101 0%, #040404 50%, #080808 100%);
|
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transition: opacity 0.85s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: opacity 0.85s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bkloading.closeLoading {
|
:host(.closeLoading) {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
@@ -184,24 +199,80 @@ function overlayWithSpinner(): string {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyVariantTheme(root: HTMLElement, variant: LoadingVariant) {
|
function applyHostShell(host: HTMLElement) {
|
||||||
const { theme } = variant;
|
const shell = [
|
||||||
root.dataset.variant = variant.id;
|
["position", "fixed"],
|
||||||
root.style.background = theme.background;
|
["inset", "0"],
|
||||||
root.style.setProperty("--bk-spin-outer", theme.spinOuter);
|
["z-index", "1000000"],
|
||||||
root.style.setProperty("--bk-spin-inner", theme.spinInner);
|
["display", "block"],
|
||||||
root.style.setProperty("--bk-spin-small", theme.spinSmall);
|
["overflow", "hidden"],
|
||||||
root.style.setProperty("--bk-stage-name", theme.stageName);
|
["contain", "strict"],
|
||||||
root.style.setProperty("--bk-stage-duration", theme.stageDuration);
|
["visibility", "visible"],
|
||||||
|
["pointer-events", "auto"],
|
||||||
|
["color", LOADING_PALETTE.text],
|
||||||
|
["--bk-line-color", LOADING_PALETTE.line],
|
||||||
|
["--bk-line-accent", LOADING_PALETTE.lineAccent],
|
||||||
|
["--bk-line-blue", LOADING_PALETTE.lineBlue],
|
||||||
|
["--bk-grid-color", LOADING_PALETTE.grid],
|
||||||
|
] as const;
|
||||||
|
|
||||||
const vignette = root.querySelector(".bkloading__vignette") as HTMLElement | null;
|
for (const [prop, value] of shell) {
|
||||||
|
host.style.setProperty(prop, value, "important");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyVariantTheme(
|
||||||
|
host: HTMLElement,
|
||||||
|
shadow: ShadowRoot,
|
||||||
|
variant: LoadingVariant,
|
||||||
|
) {
|
||||||
|
const { theme } = variant;
|
||||||
|
host.dataset.variant = variant.id;
|
||||||
|
applyHostShell(host);
|
||||||
|
host.style.setProperty("background", theme.background, "important");
|
||||||
|
host.style.setProperty("--bk-spin-outer", theme.spinOuter, "important");
|
||||||
|
host.style.setProperty("--bk-spin-inner", theme.spinInner, "important");
|
||||||
|
host.style.setProperty("--bk-spin-small", theme.spinSmall, "important");
|
||||||
|
host.style.setProperty("--bk-stage-name", theme.stageName, "important");
|
||||||
|
host.style.setProperty("--bk-stage-duration", theme.stageDuration, "important");
|
||||||
|
|
||||||
|
const vignette = shadow.querySelector(".bkloading__vignette") as HTMLElement | null;
|
||||||
if (vignette) vignette.style.opacity = String(theme.vignetteOpacity);
|
if (vignette) vignette.style.opacity = String(theme.vignetteOpacity);
|
||||||
}
|
}
|
||||||
|
|
||||||
function startCanvasForRoot(root: HTMLElement, variant: LoadingVariant) {
|
function refreshActiveLoadingTheme() {
|
||||||
|
if (!activeLoading || activeLoading.host.classList.contains("closeLoading")) return;
|
||||||
|
applyVariantTheme(activeLoading.host, activeLoading.shadow, activeLoading.variant);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindLoadingThemeGuard() {
|
||||||
|
const onThemeChange = () => refreshActiveLoadingTheme();
|
||||||
|
for (const key of THEME_GUARD_KEYS) {
|
||||||
|
settingsState.register(key, onThemeChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver(onThemeChange);
|
||||||
|
observer.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ["style", "class", "data-city-state"],
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
for (const key of THEME_GUARD_KEYS) {
|
||||||
|
settingsState.unregister(key, onThemeChange);
|
||||||
|
}
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function startCanvasForRoot(
|
||||||
|
host: HTMLElement,
|
||||||
|
shadow: ShadowRoot,
|
||||||
|
variant: LoadingVariant,
|
||||||
|
) {
|
||||||
stopCanvas?.();
|
stopCanvas?.();
|
||||||
const canvas = root.querySelector(".bkloading__canvas") as HTMLCanvasElement | null;
|
const canvas = shadow.querySelector(".bkloading__canvas") as HTMLCanvasElement | null;
|
||||||
stopCanvas = canvas ? startLoadingCanvas(canvas, root, variant) : null;
|
stopCanvas = canvas ? startLoadingCanvas(canvas, host, variant) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AppendLoadingSymbol(givenID: string, position: string) {
|
export function AppendLoadingSymbol(givenID: string, position: string) {
|
||||||
@@ -216,23 +287,31 @@ export function AppendLoadingSymbol(givenID: string, position: string) {
|
|||||||
export function stopLoadingAnimation() {
|
export function stopLoadingAnimation() {
|
||||||
stopCanvas?.();
|
stopCanvas?.();
|
||||||
stopCanvas = null;
|
stopCanvas = null;
|
||||||
|
unbindThemeGuard?.();
|
||||||
|
unbindThemeGuard = null;
|
||||||
|
activeLoading = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function loading() {
|
export default function loading() {
|
||||||
stopLoadingAnimation();
|
stopLoadingAnimation();
|
||||||
|
|
||||||
const variant = pickLoadingVariant();
|
const variant = pickLoadingVariant();
|
||||||
|
const host = document.createElement("div");
|
||||||
|
host.id = "loading";
|
||||||
|
host.className = "bkloading";
|
||||||
|
|
||||||
const loadinghtml = stringToHTML(/* html */ `
|
const shadow = host.attachShadow({ mode: "open" });
|
||||||
<div class="bkloading" id="loading" data-variant="${variant.id}">
|
shadow.innerHTML = `
|
||||||
<style>${loadingStyles}</style>
|
<style>${loadingStyles}</style>
|
||||||
${overlayWithSpinner()}
|
${overlayWithSpinner()}
|
||||||
<div class="bkloading__version">v${browser.runtime.getManifest().version}</div>
|
<div class="bkloading__version">v${browser.runtime.getManifest().version}</div>
|
||||||
</div>`);
|
`;
|
||||||
|
|
||||||
const root = loadinghtml.firstChild as HTMLElement;
|
document.documentElement.append(host);
|
||||||
document.documentElement.append(root);
|
|
||||||
|
|
||||||
applyVariantTheme(root, variant);
|
applyVariantTheme(host, shadow, variant);
|
||||||
startCanvasForRoot(root, variant);
|
startCanvasForRoot(host, shadow, variant);
|
||||||
|
|
||||||
|
activeLoading = { host, shadow, variant };
|
||||||
|
unbindThemeGuard = bindLoadingThemeGuard();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,8 +78,8 @@ function cycle(
|
|||||||
return { t: fadeT, fade: true, ambient: smooth((len - tick) / revealMs) };
|
return { t: fadeT, fade: true, ambient: smooth((len - tick) / revealMs) };
|
||||||
}
|
}
|
||||||
|
|
||||||
function cssVar(root: HTMLElement, name: string, fallback: string): string {
|
function cssVar(_root: HTMLElement, _name: string, fallback: string): string {
|
||||||
return getComputedStyle(root).getPropertyValue(name).trim() || fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawGrid(
|
function drawGrid(
|
||||||
|
|||||||
Reference in New Issue
Block a user