feat: Unified portaled sign-in overlay

Updated the sign-in overlay to be unified across the site, improving UX
This commit is contained in:
2026-04-06 14:48:03 +09:30
parent 3c613f4938
commit 94d54f65bf
8 changed files with 174 additions and 140 deletions
@@ -1,29 +1,32 @@
<script lang="ts"> <script lang="ts">
import { fade } from "svelte/transition"; import { fade } from "svelte/transition";
import { animate } from "motion"; import { animate } from "motion";
import { closeExtensionPopup } from "@/seqta/utils/Closers/closeExtensionPopup"; import { onMount } from "svelte";
import { cloudAuth } from "@/seqta/utils/CloudAuth";
import CloudLoginForm from "@/interface/components/store/CloudLoginForm.svelte";
let { onClose } = $props<{ onClose: () => void }>(); let { onClose } = $props<{ onClose: () => void }>();
let modalElement: HTMLElement; let modalElement: HTMLElement;
$effect(() => { onMount(() => {
if (modalElement) { return cloudAuth.subscribe((s) => {
animate(modalElement, { scale: [0.9, 1], opacity: [0, 1] }, { type: "spring", stiffness: 300, damping: 25 }); if (s.isLoggedIn) onClose();
} });
}); });
function handleSignIn() { $effect(() => {
onClose(); if (modalElement) {
if (document.getElementById("ExtensionPopup")) { animate(
closeExtensionPopup(); modalElement,
} else { { scale: [0.9, 1], opacity: [0, 1] },
window.close(); { type: "spring", stiffness: 300, damping: 25 },
} );
} }
});
</script> </script>
<div <div
class="flex fixed inset-0 z-[10000] justify-center items-center bg-black/50" class="flex fixed inset-0 z-[99999] justify-center items-center bg-black/50"
onclick={(e) => { onclick={(e) => {
if (e.target === e.currentTarget) onClose(); if (e.target === e.currentTarget) onClose();
}} }}
@@ -37,7 +40,7 @@
<!-- svelte-ignore a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
<div <div
bind:this={modalElement} bind:this={modalElement}
class="p-4 mx-4 w-full max-w-md bg-white rounded-2xl shadow-2xl dark:bg-zinc-800 dark:text-white" class="p-4 mx-4 w-full max-w-md max-h-[90vh] overflow-y-auto bg-white rounded-2xl shadow-2xl dark:bg-zinc-800 dark:text-white"
onclick={(e) => e.stopPropagation()} onclick={(e) => e.stopPropagation()}
onkeydown={(e) => e.stopPropagation()} onkeydown={(e) => e.stopPropagation()}
> >
@@ -45,32 +48,19 @@
Sign in to favorite themes Sign in to favorite themes
</h2> </h2>
<p class="mb-6 text-zinc-600 dark:text-zinc-400"> <p class="mb-4 text-sm text-zinc-600 dark:text-zinc-400">
Sign in in the Theme Store to save favorites across devices, or create an account to get started. Sign in to the Theme Store to save favorites across devices, or create an account to get started.
</p> </p>
<div class="flex flex-wrap gap-2 justify-end"> <CloudLoginForm compact onSuccess={onClose} />
<div class="flex justify-end mt-4">
<button <button
type="button" type="button"
onclick={onClose} onclick={onClose}
class="px-4 py-2 text-sm font-medium rounded-lg bg-zinc-200 dark:bg-zinc-700 text-zinc-700 dark:text-zinc-200 hover:bg-zinc-300 dark:hover:bg-zinc-600 transition-colors duration-200" class="px-4 py-2 text-sm font-medium rounded-lg bg-zinc-200 dark:bg-zinc-700 text-zinc-700 dark:text-zinc-200 hover:bg-zinc-300 dark:hover:bg-zinc-600 transition-colors duration-200"
> >
OK Close
</button>
<a
href="https://accounts.betterseqta.org/register"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg border border-zinc-200 dark:border-zinc-600 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-all duration-200"
>
Create account
</a>
<button
type="button"
onclick={handleSignIn}
class="px-4 py-2 text-sm font-medium rounded-lg bg-zinc-800 dark:bg-zinc-200 text-white dark:text-zinc-900 hover:bg-zinc-700 dark:hover:bg-zinc-300 transition-colors duration-200"
>
Sign in
</button> </button>
</div> </div>
</div> </div>
@@ -1,11 +1,8 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
import { cloudAuth } from "@/seqta/utils/CloudAuth"; import { cloudAuth } from "@/seqta/utils/CloudAuth";
import CloudLoginForm from "./CloudLoginForm.svelte";
let username = $state("");
let password = $state("");
let loading = $state(false);
let error = $state<string | null>(null);
let cloudState = $state(cloudAuth.state); let cloudState = $state(cloudAuth.state);
let open = $state(false); let open = $state(false);
let dropdownEl: HTMLElement; let dropdownEl: HTMLElement;
@@ -35,27 +32,6 @@
} }
}); });
async function handleLogin() {
if (loading) return;
error = null;
if (!username.trim() || !password) {
error = "Please enter username and password";
return;
}
loading = true;
try {
const result = await cloudAuth.login(username.trim(), password);
if (result.success) {
password = "";
open = false;
} else {
error = result.error ?? "Login failed";
}
} finally {
loading = false;
}
}
async function handleLogout() { async function handleLogout() {
await cloudAuth.logout(); await cloudAuth.logout();
open = false; open = false;
@@ -142,58 +118,11 @@
</button> </button>
</div> </div>
{:else} {:else}
<p class="mb-4 text-base text-zinc-600 dark:text-zinc-400"> <CloudLoginForm
Sign in to favorite themes. Your favorites sync across devices when logged in. onSuccess={() => {
</p> open = false;
<form
class="flex flex-col gap-3"
autocomplete="off"
onsubmit={(e) => {
e.preventDefault();
handleLogin();
}} }}
>
<input
type="text"
name="betterseqta-cloud-username"
autocomplete="off"
placeholder="Email or username"
bind:value={username}
disabled={loading}
readonly
onfocus={(e) => e.currentTarget.removeAttribute('readonly')}
class="w-full px-4 py-3 text-base rounded-lg bg-zinc-100 dark:bg-zinc-800 dark:text-white border border-zinc-200 dark:border-zinc-600 focus:outline-none focus:ring-2 focus:ring-accent-ring focus:border-transparent transition-colors duration-200"
/> />
<input
type="password"
name="betterseqta-cloud-password"
autocomplete="new-password"
placeholder="Password"
bind:value={password}
disabled={loading}
readonly
onfocus={(e) => e.currentTarget.removeAttribute('readonly')}
class="w-full px-4 py-3 text-base rounded-lg bg-zinc-100 dark:bg-zinc-800 dark:text-white border border-zinc-200 dark:border-zinc-600 focus:outline-none focus:ring-2 focus:ring-accent-ring focus:border-transparent transition-colors duration-200"
/>
{#if error}
<p class="text-base text-red-600 dark:text-red-400">{error}</p>
{/if}
<button
type="submit"
disabled={loading}
class="w-full px-4 py-3 text-base font-medium rounded-lg bg-zinc-800 dark:bg-zinc-200 text-white dark:text-zinc-900 hover:bg-zinc-700 dark:hover:bg-zinc-300 disabled:opacity-50 transition-colors duration-200"
>
{loading ? "Signing in..." : "Sign in"}
</button>
<a
href="https://accounts.betterseqta.org/register"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center justify-center gap-2 px-4 py-3 text-base font-medium rounded-lg border border-zinc-200 dark:border-zinc-600 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-all duration-200"
>
Create account
</a>
</form>
{/if} {/if}
</div> </div>
</div> </div>
@@ -0,0 +1,111 @@
<script lang="ts">
import { cloudAuth } from "@/seqta/utils/CloudAuth";
let {
introText,
onSuccess,
compact = false,
} = $props<{
introText?: string;
onSuccess?: () => void;
/** Smaller padding/text for overlays (e.g. SignInToFavoriteModal) */
compact?: boolean;
}>();
let username = $state("");
let password = $state("");
let loading = $state(false);
let error = $state<string | null>(null);
const inputClass = $derived(
compact
? "w-full px-4 py-2 text-sm rounded-lg bg-zinc-100 dark:bg-zinc-800 dark:text-white border border-zinc-200 dark:border-zinc-600 focus:outline-none focus:ring-2 focus:ring-accent-ring focus:border-transparent transition-colors duration-200"
: "w-full px-4 py-3 text-base rounded-lg bg-zinc-100 dark:bg-zinc-800 dark:text-white border border-zinc-200 dark:border-zinc-600 focus:outline-none focus:ring-2 focus:ring-accent-ring focus:border-transparent transition-colors duration-200",
);
const btnClass = $derived(
compact
? "w-full px-4 py-2 text-sm font-medium rounded-lg bg-zinc-800 dark:bg-zinc-200 text-white dark:text-zinc-900 hover:bg-zinc-700 dark:hover:bg-zinc-300 disabled:opacity-50 transition-colors duration-200"
: "w-full px-4 py-3 text-base font-medium rounded-lg bg-zinc-800 dark:bg-zinc-200 text-white dark:text-zinc-900 hover:bg-zinc-700 dark:hover:bg-zinc-300 disabled:opacity-50 transition-colors duration-200",
);
const linkClass = $derived(
compact
? "inline-flex items-center justify-center gap-2 px-4 py-2 text-sm font-medium rounded-lg border border-zinc-200 dark:border-zinc-600 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-all duration-200"
: "inline-flex items-center justify-center gap-2 px-4 py-3 text-base font-medium rounded-lg border border-zinc-200 dark:border-zinc-600 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-all duration-200",
);
async function handleLogin() {
if (loading) return;
error = null;
if (!username.trim() || !password) {
error = "Please enter username and password";
return;
}
loading = true;
try {
const result = await cloudAuth.login(username.trim(), password);
if (result.success) {
password = "";
onSuccess?.();
} else {
error = result.error ?? "Login failed";
}
} finally {
loading = false;
}
}
</script>
{#if introText}
<p
class="mb-4 text-zinc-600 dark:text-zinc-400 {compact ? 'text-sm' : 'text-base'}"
>
{introText}
</p>
{/if}
<form
class="flex flex-col gap-3"
autocomplete="off"
onsubmit={(e) => {
e.preventDefault();
handleLogin();
}}
>
<input
type="text"
name="betterseqta-cloud-username"
autocomplete="off"
placeholder="Email or username"
bind:value={username}
disabled={loading}
readonly
onfocus={(e) => e.currentTarget.removeAttribute("readonly")}
class={inputClass}
/>
<input
type="password"
name="betterseqta-cloud-password"
autocomplete="new-password"
placeholder="Password"
bind:value={password}
disabled={loading}
readonly
onfocus={(e) => e.currentTarget.removeAttribute("readonly")}
class={inputClass}
/>
{#if error}
<p class="text-red-600 dark:text-red-400 {compact ? 'text-sm' : 'text-base'}">{error}</p>
{/if}
<button type="submit" disabled={loading} class={btnClass}>
{loading ? "Signing in..." : "Sign in"}
</button>
<a
href="https://accounts.betterseqta.org/register"
target="_blank"
rel="noopener noreferrer"
class={linkClass}
>
Create account
</a>
</form>
+14 -11
View File
@@ -2,16 +2,14 @@
import type { Theme } from '@/interface/types/Theme' import type { Theme } from '@/interface/types/Theme'
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import SignInToFavoriteModal from '@/interface/components/SignInToFavoriteModal.svelte'; let { theme, onClick, toggleFavorite, isLoggedIn, onRequestSignIn } = $props<{
let { theme, onClick, toggleFavorite, isLoggedIn } = $props<{
theme: Theme; theme: Theme;
onClick: () => void; onClick: () => void;
toggleFavorite: (theme: Theme) => void; toggleFavorite: (theme: Theme) => void;
isLoggedIn: boolean; isLoggedIn: boolean;
onRequestSignIn?: () => void;
}>(); }>();
let menuOpen = $state(false); let menuOpen = $state(false);
let showSignInModal = $state(false);
let menuRef: HTMLDivElement; let menuRef: HTMLDivElement;
onMount(() => { onMount(() => {
@@ -34,14 +32,23 @@
if (isLoggedIn) { if (isLoggedIn) {
toggleFavorite(theme); toggleFavorite(theme);
} else { } else {
showSignInModal = true; onRequestSignIn?.();
} }
menuOpen = false; menuOpen = false;
} }
</script> </script>
<div class="w-full cursor-pointer" role="button" tabindex="-1" onkeydown={onClick} onclick={handleCardClick}> <div
<div class="bg-gray-50 w-full transition-all hover:scale-105 duration-500 relative group flex flex-col hover:shadow-2xl dark:hover:shadow-white/[0.1] dark:hover:shadow-white/[0.8] dark:bg-zinc-800 dark:border-white/[0.1] h-auto rounded-xl overflow-clip border" transition:fade> class="relative z-0 hover:z-20 w-full cursor-pointer"
role="button"
tabindex="-1"
onkeydown={onClick}
onclick={handleCardClick}
>
<div
class="bg-gray-50 w-full transition-all duration-500 ease-out relative group flex flex-col rounded-xl overflow-clip border hover:scale-105 hover:shadow-2xl dark:hover:shadow-white/[0.1] dark:hover:shadow-white/[0.8] dark:bg-zinc-800 dark:border-white/[0.1] h-auto"
transition:fade
>
<!-- Menu dropdown --> <!-- Menu dropdown -->
<div class="absolute top-2 right-2 z-20" data-theme-menu bind:this={menuRef}> <div class="absolute top-2 right-2 z-20" data-theme-menu bind:this={menuRef}>
<button <button
@@ -104,7 +111,3 @@
</div> </div>
</div> </div>
</div> </div>
{#if showSignInModal}
<SignInToFavoriteModal onClose={() => (showSignInModal = false)} />
{/if}
@@ -2,12 +2,13 @@
import type { Theme } from '@/interface/types/Theme' import type { Theme } from '@/interface/types/Theme'
import ThemeCard from './ThemeCard.svelte'; import ThemeCard from './ThemeCard.svelte';
let { themes, searchTerm, setDisplayTheme, toggleFavorite, isLoggedIn } = $props<{ let { themes, searchTerm, setDisplayTheme, toggleFavorite, isLoggedIn, onRequestSignIn } = $props<{
themes: Theme[]; themes: Theme[];
searchTerm: string; searchTerm: string;
setDisplayTheme: (theme: Theme) => void; setDisplayTheme: (theme: Theme) => void;
toggleFavorite: (theme: Theme) => void; toggleFavorite: (theme: Theme) => void;
isLoggedIn: boolean; isLoggedIn: boolean;
onRequestSignIn?: () => void;
}>(); }>();
let filteredThemes = $derived(themes.filter((theme: Theme) => let filteredThemes = $derived(themes.filter((theme: Theme) =>
@@ -23,12 +24,13 @@
onClick={() => setDisplayTheme(theme)} onClick={() => setDisplayTheme(theme)}
{toggleFavorite} {toggleFavorite}
{isLoggedIn} {isLoggedIn}
{onRequestSignIn}
/> />
{/each} {/each}
{#if filteredThemes.length !== 0} {#if filteredThemes.length !== 0}
<a href="https://betterseqta.gitbook.io/betterseqta-docs" class='w-full cursor-pointer'> <a href="https://betterseqta.gitbook.io/betterseqta-docs" class="block relative z-0 hover:z-20 w-full cursor-pointer">
<div class="bg-zinc-50 h-48 w-full transition-all hover:scale-105 duration-500 relative justify-center items-center group group/card flex flex-col hover:shadow-2xl dark:hover:shadow-white/[0.1] hover:shadow-white/[0.8] dark:bg-zinc-800 dark:border-white/[0.1] rounded-xl overflow-clip border"> <div class="bg-zinc-50 h-48 w-full transition-all duration-500 ease-out relative overflow-clip rounded-xl border group group/card flex flex-col justify-center items-center hover:scale-105 hover:shadow-2xl dark:hover:shadow-white/[0.1] hover:shadow-white/[0.8] dark:bg-zinc-800 dark:border-white/[0.1]">
<div class="text-2xl font-IconFamily">{'\uecb3'}</div> <div class="text-2xl font-IconFamily">{'\uecb3'}</div>
<div class="text-xl font-bold text-center transition-all duration-500 dark:text-white"> <div class="text-xl font-bold text-center transition-all duration-500 dark:text-white">
Got a Theme Idea? Got a Theme Idea?
@@ -2,9 +2,7 @@
import type { Theme } from '@/interface/types/Theme' import type { Theme } from '@/interface/types/Theme'
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import { animate } from 'motion'; import { animate } from 'motion';
import SignInToFavoriteModal from '@/interface/components/SignInToFavoriteModal.svelte'; let { theme, currentThemes, setDisplayTheme, onInstall, onRemove, allThemes, displayTheme, toggleFavorite, isLoggedIn, onRequestSignIn } = $props<{
let { theme, currentThemes, setDisplayTheme, onInstall, onRemove, allThemes, displayTheme, toggleFavorite, isLoggedIn } = $props<{
theme: Theme | null; theme: Theme | null;
currentThemes: string[]; currentThemes: string[];
setDisplayTheme: (theme: Theme | null) => void; setDisplayTheme: (theme: Theme | null) => void;
@@ -14,16 +12,16 @@
displayTheme: Theme | null; displayTheme: Theme | null;
toggleFavorite?: (theme: Theme) => void; toggleFavorite?: (theme: Theme) => void;
isLoggedIn?: boolean; isLoggedIn?: boolean;
onRequestSignIn?: () => void;
}>(); }>();
let installing = $state(false); let installing = $state(false);
let showSignInModal = $state(false);
let modalElement: HTMLElement; let modalElement: HTMLElement;
function handleFavoriteClick() { function handleFavoriteClick() {
if (isLoggedIn && toggleFavorite && theme) { if (isLoggedIn && toggleFavorite && theme) {
toggleFavorite(theme); toggleFavorite(theme);
} else { } else {
showSignInModal = true; onRequestSignIn?.();
} }
} }
@@ -159,8 +157,8 @@
</h3> </h3>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2"> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
{#each getRelatedThemes() as relatedTheme (relatedTheme.id)} {#each getRelatedThemes() as relatedTheme (relatedTheme.id)}
<button onclick={() => { hideModal(relatedTheme) }} class="w-full cursor-pointer"> <button onclick={() => { hideModal(relatedTheme) }} class="relative z-0 hover:z-20 w-full cursor-pointer">
<div class="bg-gray-50 w-full transition-all hover:scale-105 duration-500 relative group group/card flex flex-col hover:shadow-2xl dark:hover:shadow-white/[0.1] hover:shadow-white/[0.8] dark:bg-zinc-800 dark:border-white/[0.1] h-auto rounded-xl overflow-clip border"> <div class="bg-gray-50 w-full transition-all duration-500 ease-out relative group group/card flex flex-col hover:scale-105 hover:shadow-2xl dark:hover:shadow-white/[0.1] hover:shadow-white/[0.8] dark:bg-zinc-800 dark:border-white/[0.1] h-auto rounded-xl overflow-clip border">
<div class="absolute bottom-1 left-3 z-10 mb-1 text-xl font-bold text-white transition-all duration-500 group-hover:-translate-y-0.5"> <div class="absolute bottom-1 left-3 z-10 mb-1 text-xl font-bold text-white transition-all duration-500 group-hover:-translate-y-0.5">
{relatedTheme.name} {relatedTheme.name}
</div> </div>
@@ -180,7 +178,3 @@
{/if} {/if}
</div> </div>
</div> </div>
{#if showSignInModal}
<SignInToFavoriteModal onClose={() => (showSignInModal = false)} />
{/if}
+8
View File
@@ -16,6 +16,7 @@
import { loadBackground } from '@/seqta/ui/ImageBackgrounds' import { loadBackground } from '@/seqta/ui/ImageBackgrounds'
import Backgrounds from '../components/store/Backgrounds.svelte' import Backgrounds from '../components/store/Backgrounds.svelte'
import { cloudAuth } from '@/seqta/utils/CloudAuth' import { cloudAuth } from '@/seqta/utils/CloudAuth'
import SignInToFavoriteModal from '../components/SignInToFavoriteModal.svelte'
const themeManager = ThemeManager.getInstance(); const themeManager = ThemeManager.getInstance();
let cloudLoggedIn = $state(cloudAuth.state.isLoggedIn); let cloudLoggedIn = $state(cloudAuth.state.isLoggedIn);
@@ -34,6 +35,7 @@
let error = $state<string | null>(null); let error = $state<string | null>(null);
let selectedBackground = $state<string | null>(null); let selectedBackground = $state<string | null>(null);
let showSignInOverlay = $state(false);
const fetchCurrentThemes = async () => { const fetchCurrentThemes = async () => {
const themes = await themeManager.getAvailableThemes(); const themes = await themeManager.getAvailableThemes();
@@ -169,6 +171,7 @@
{setDisplayTheme} {setDisplayTheme}
{toggleFavorite} {toggleFavorite}
isLoggedIn={cloudLoggedIn} isLoggedIn={cloudLoggedIn}
onRequestSignIn={() => (showSignInOverlay = true)}
/> />
{#if displayTheme} {#if displayTheme}
@@ -180,6 +183,7 @@
{setDisplayTheme} {setDisplayTheme}
{toggleFavorite} {toggleFavorite}
isLoggedIn={cloudLoggedIn} isLoggedIn={cloudLoggedIn}
onRequestSignIn={() => (showSignInOverlay = true)}
onInstall={async () => { onInstall={async () => {
if (displayTheme) { if (displayTheme) {
await themeManager.downloadTheme(displayTheme); await themeManager.downloadTheme(displayTheme);
@@ -204,4 +208,8 @@
{/if} {/if}
</div> </div>
</div> </div>
{#if showSignInOverlay}
<SignInToFavoriteModal onClose={() => (showSignInOverlay = false)} />
{/if}
</div> </div>
+2 -5
View File
@@ -18,12 +18,9 @@ export class SettingsResizer {
if (!iframePopup) return; if (!iframePopup) return;
const viewportHeight = window.innerHeight; const viewportHeight = window.innerHeight;
const idealHeight = viewportHeight - 80 - 15; // -80px for the top of the popup const rawIdeal = viewportHeight - 80 - 15; // room below top chrome
const idealHeight = Math.min(Math.max(rawIdeal, 280), 600);
if (idealHeight > 600) {
iframePopup.style.height = "600px";
} else {
iframePopup.style.height = `${idealHeight}px`; iframePopup.style.height = `${idealHeight}px`;
} }
} }
}