diff --git a/web/src/app.d.ts b/web/src/app.d.ts new file mode 100644 index 0000000..0a084a4 --- /dev/null +++ b/web/src/app.d.ts @@ -0,0 +1,20 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts + +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + + interface PageState { + discordModal?: { + open: boolean; + username: string; + }; + } + + // interface Platform {} + } +} + +export {}; diff --git a/web/src/lib/actions/portal.ts b/web/src/lib/actions/portal.ts new file mode 100644 index 0000000..e65dfff --- /dev/null +++ b/web/src/lib/actions/portal.ts @@ -0,0 +1,40 @@ +/** + * Svelte action that moves an element to a target container (default: document.body). + * This allows elements to escape their parent's stacking context, which is essential + * for modals that need to appear above all other content regardless of z-index. + * + * @example + * ```svelte + *
+ * + *
+ * ``` + * + * @example + * ```svelte + * + *
+ * ... + *
+ * ``` + */ +export function portal( + node: HTMLElement, + target: HTMLElement | string = document.body, +): { destroy: () => void } | void { + const targetEl = + typeof target === "string" ? document.querySelector(target) : target; + + if (!targetEl) { + console.warn(`Portal target "${target}" not found`); + return; + } + + targetEl.appendChild(node); + + return { + destroy() { + node.remove(); + }, + }; +} diff --git a/web/src/lib/components/DiscordProfileModal.svelte b/web/src/lib/components/DiscordProfileModal.svelte index 049f5dc..ab56f0f 100644 --- a/web/src/lib/components/DiscordProfileModal.svelte +++ b/web/src/lib/components/DiscordProfileModal.svelte @@ -2,19 +2,20 @@ import { fade, scale } from "svelte/transition"; import IconCopy from "~icons/material-symbols/content-copy-rounded"; import IconCheck from "~icons/material-symbols/check-rounded"; + import { portal } from "$lib/actions/portal"; interface Props { - open: boolean; username: string; avatarUrl?: string; bannerUrl?: string; + onclose: () => void; } let { - open = $bindable(false), username, avatarUrl = "https://cdn.discordapp.com/avatars/184118083143598081/798e497f55abdcadbd8440e5eed551a0.png?size=4096", bannerUrl = "https://cdn.discordapp.com/banners/184118083143598081/174425460b67261a124d873b016e038f.png?size=4096", + onclose, }: Props = $props(); let copySuccess = $state(false); @@ -23,12 +24,12 @@ function handleBackdropClick(e: MouseEvent) { if (e.target === e.currentTarget) { - open = false; + onclose(); } } function handleClose() { - open = false; + onclose(); } async function copyUsername() { @@ -44,124 +45,121 @@ } -{#if open} +
e.key === "Escape" && handleClose()} + role="presentation" + tabindex="-1" + transition:fade={{ duration: 200 }} +> +
e.key === "Escape" && handleClose()} - role="presentation" - tabindex="-1" - transition:fade={{ duration: 200 }} + class="relative w-full max-w-md rounded-xl bg-zinc-100 dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700 shadow-lg overflow-hidden scale-110 origin-top" + role="dialog" + aria-modal="true" + aria-labelledby="discord-profile-title" + transition:scale={{ duration: 200, start: 0.95 }} > - - -{/if} +
diff --git a/web/src/lib/components/ThemeToggle.svelte b/web/src/lib/components/ThemeToggle.svelte index f38b130..7f28c3c 100644 --- a/web/src/lib/components/ThemeToggle.svelte +++ b/web/src/lib/components/ThemeToggle.svelte @@ -10,7 +10,7 @@ aria-label={themeStore.isDark ? "Switch to light mode" : "Switch to dark mode"} - class="relative size-9 rounded-md border border-zinc-300 dark:border-zinc-700 bg-zinc-100 dark:bg-zinc-900/50 hover:bg-zinc-200 dark:hover:bg-zinc-800/70 transition-all duration-200" + class="relative size-9 rounded-md border border-zinc-300 dark:border-zinc-700 bg-zinc-100 dark:bg-zinc-900/50 hover:bg-zinc-200 dark:hover:bg-zinc-800/70 transition-all duration-200 cursor-pointer" >
+ import { pushState } from "$app/navigation"; + import { page } from "$app/state"; import ProjectCard from "$lib/components/ProjectCard.svelte"; import DiscordProfileModal from "$lib/components/DiscordProfileModal.svelte"; import type { PageData } from "./$types"; @@ -26,8 +28,9 @@ socialLinksWithIcons.filter((link: { visible: boolean }) => link.visible), ); - let discordModalOpen = $state(false); - let discordUsername = $state(""); + function openDiscordModal(username: string) { + pushState("", { discordModal: { open: true, username } }); + }
@@ -61,7 +64,7 @@ @@ -76,11 +79,8 @@
- +{#if page.state.discordModal?.open} + history.back()} + /> +{/if} diff --git a/web/src/routes/pgp/+page.svelte b/web/src/routes/pgp/+page.svelte index 2ec49f3..3f1a85c 100644 --- a/web/src/routes/pgp/+page.svelte +++ b/web/src/routes/pgp/+page.svelte @@ -128,7 +128,7 @@