mirror of
https://github.com/Xevion/xevion.dev.git
synced 2026-01-31 04:26:43 -06:00
feat: implement portal action for modal rendering and add SvelteKit page state
- Add portal action to render modals at document.body, escaping stacking context - Switch Discord modal from bindable prop to SvelteKit page state management - Add cursor-pointer utility to interactive elements for better UX
This commit is contained in:
Vendored
+20
@@ -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 {};
|
||||||
@@ -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
|
||||||
|
* <div use:portal class="fixed inset-0 z-50">
|
||||||
|
* <!-- Modal content renders at document.body -->
|
||||||
|
* </div>
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```svelte
|
||||||
|
* <!-- Portal to a specific container -->
|
||||||
|
* <div use:portal={"#modal-container"}>
|
||||||
|
* ...
|
||||||
|
* </div>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -2,19 +2,20 @@
|
|||||||
import { fade, scale } from "svelte/transition";
|
import { fade, scale } from "svelte/transition";
|
||||||
import IconCopy from "~icons/material-symbols/content-copy-rounded";
|
import IconCopy from "~icons/material-symbols/content-copy-rounded";
|
||||||
import IconCheck from "~icons/material-symbols/check-rounded";
|
import IconCheck from "~icons/material-symbols/check-rounded";
|
||||||
|
import { portal } from "$lib/actions/portal";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
|
||||||
username: string;
|
username: string;
|
||||||
avatarUrl?: string;
|
avatarUrl?: string;
|
||||||
bannerUrl?: string;
|
bannerUrl?: string;
|
||||||
|
onclose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
open = $bindable(false),
|
|
||||||
username,
|
username,
|
||||||
avatarUrl = "https://cdn.discordapp.com/avatars/184118083143598081/798e497f55abdcadbd8440e5eed551a0.png?size=4096",
|
avatarUrl = "https://cdn.discordapp.com/avatars/184118083143598081/798e497f55abdcadbd8440e5eed551a0.png?size=4096",
|
||||||
bannerUrl = "https://cdn.discordapp.com/banners/184118083143598081/174425460b67261a124d873b016e038f.png?size=4096",
|
bannerUrl = "https://cdn.discordapp.com/banners/184118083143598081/174425460b67261a124d873b016e038f.png?size=4096",
|
||||||
|
onclose,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let copySuccess = $state(false);
|
let copySuccess = $state(false);
|
||||||
@@ -23,12 +24,12 @@
|
|||||||
|
|
||||||
function handleBackdropClick(e: MouseEvent) {
|
function handleBackdropClick(e: MouseEvent) {
|
||||||
if (e.target === e.currentTarget) {
|
if (e.target === e.currentTarget) {
|
||||||
open = false;
|
onclose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClose() {
|
function handleClose() {
|
||||||
open = false;
|
onclose();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyUsername() {
|
async function copyUsername() {
|
||||||
@@ -44,124 +45,121 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if open}
|
<div
|
||||||
|
use:portal
|
||||||
|
class="fixed inset-0 z-[60] flex items-start justify-center bg-black/30 backdrop-blur-[3px] p-4 pt-[15vh]"
|
||||||
|
onclick={handleBackdropClick}
|
||||||
|
onkeydown={(e) => e.key === "Escape" && handleClose()}
|
||||||
|
role="presentation"
|
||||||
|
tabindex="-1"
|
||||||
|
transition:fade={{ duration: 200 }}
|
||||||
|
>
|
||||||
|
<!-- SCALE: Adjust the scale() value to resize entire modal proportionally -->
|
||||||
<div
|
<div
|
||||||
class="fixed inset-0 z-50 flex items-start justify-center bg-black/30 backdrop-blur-[2px] p-4 pt-[15vh]"
|
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"
|
||||||
onclick={handleBackdropClick}
|
role="dialog"
|
||||||
onkeydown={(e) => e.key === "Escape" && handleClose()}
|
aria-modal="true"
|
||||||
role="presentation"
|
aria-labelledby="discord-profile-title"
|
||||||
tabindex="-1"
|
transition:scale={{ duration: 200, start: 0.95 }}
|
||||||
transition:fade={{ duration: 200 }}
|
|
||||||
>
|
>
|
||||||
<!-- SCALE: Adjust the scale() value to resize entire modal proportionally -->
|
<!-- Banner -->
|
||||||
<div
|
{#if bannerUrl && !bannerFailed}
|
||||||
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"
|
<img
|
||||||
role="dialog"
|
src={bannerUrl}
|
||||||
aria-modal="true"
|
alt=""
|
||||||
aria-labelledby="discord-profile-title"
|
class="h-28 w-full object-cover"
|
||||||
transition:scale={{ duration: 200, start: 0.95 }}
|
onerror={() => (bannerFailed = true)}
|
||||||
>
|
/>
|
||||||
<!-- Banner -->
|
{:else}
|
||||||
{#if bannerUrl && !bannerFailed}
|
<div
|
||||||
<img
|
class="h-28 bg-linear-to-br from-zinc-300 to-zinc-400 dark:from-zinc-700 dark:to-zinc-800"
|
||||||
src={bannerUrl}
|
></div>
|
||||||
alt=""
|
{/if}
|
||||||
class="h-28 w-full object-cover"
|
|
||||||
onerror={() => (bannerFailed = true)}
|
<!-- Content area -->
|
||||||
/>
|
<div class="px-5 pb-5">
|
||||||
{:else}
|
<!-- Avatar with stroke effect -->
|
||||||
|
<div class="relative -mt-14 mb-3 w-fit">
|
||||||
|
<!-- Stroke ring (larger circle behind avatar) -->
|
||||||
|
<!-- SIZE: avatar (96px) + stroke (4px * 2) = 104px -->
|
||||||
|
<!-- POSITION: -m-1 centers the stroke ring behind the avatar -->
|
||||||
<div
|
<div
|
||||||
class="h-28 bg-linear-to-br from-zinc-300 to-zinc-400 dark:from-zinc-700 dark:to-zinc-800"
|
class="absolute inset-0 -m-1 size-[104px] rounded-full bg-zinc-100 dark:bg-zinc-900"
|
||||||
></div>
|
></div>
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Content area -->
|
<!-- Avatar circle -->
|
||||||
<div class="px-5 pb-5">
|
<!-- SIZE: size-24 = 96px -->
|
||||||
<!-- Avatar with stroke effect -->
|
{#if avatarUrl && !avatarFailed}
|
||||||
<div class="relative -mt-14 mb-3 w-fit">
|
<img
|
||||||
<!-- Stroke ring (larger circle behind avatar) -->
|
src={avatarUrl}
|
||||||
<!-- SIZE: avatar (96px) + stroke (4px * 2) = 104px -->
|
alt="Profile avatar"
|
||||||
<!-- POSITION: -m-1 centers the stroke ring behind the avatar -->
|
class="relative size-24 rounded-full object-cover"
|
||||||
|
onerror={() => (avatarFailed = true)}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 -m-1 size-[104px] rounded-full bg-zinc-100 dark:bg-zinc-900"
|
class="relative size-24 rounded-full bg-linear-to-br from-zinc-400 to-zinc-500 dark:from-zinc-500 dark:to-zinc-600"
|
||||||
></div>
|
></div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Avatar circle -->
|
<!-- Online indicator -->
|
||||||
<!-- SIZE: size-24 = 96px -->
|
<!-- POSITION: bottom/right values place center on avatar circumference -->
|
||||||
{#if avatarUrl && !avatarFailed}
|
<!-- For 96px avatar at 315° (bottom-right): ~4px from edge -->
|
||||||
<img
|
|
||||||
src={avatarUrl}
|
|
||||||
alt="Profile avatar"
|
|
||||||
class="relative size-24 rounded-full object-cover"
|
|
||||||
onerror={() => (avatarFailed = true)}
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<div
|
|
||||||
class="relative size-24 rounded-full bg-linear-to-br from-zinc-400 to-zinc-500 dark:from-zinc-500 dark:to-zinc-600"
|
|
||||||
></div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Online indicator -->
|
|
||||||
<!-- POSITION: bottom/right values place center on avatar circumference -->
|
|
||||||
<!-- For 96px avatar at 315° (bottom-right): ~4px from edge -->
|
|
||||||
<div
|
|
||||||
class="absolute bottom-0.5 right-0.5 size-5 rounded-full bg-green-500 border-[3px] border-zinc-100 dark:border-zinc-900"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Profile info -->
|
|
||||||
<!-- SPACING: mb-4 controls gap before About Me section -->
|
|
||||||
<div class="mb-4">
|
|
||||||
<h2
|
|
||||||
id="discord-profile-title"
|
|
||||||
class="text-xl font-bold text-zinc-900 dark:text-zinc-100"
|
|
||||||
>
|
|
||||||
Xevion
|
|
||||||
</h2>
|
|
||||||
<!-- USERNAME ROW: gap-1.5 controls spacing between elements -->
|
|
||||||
<div class="flex items-center gap-1.5 text-sm">
|
|
||||||
<span
|
|
||||||
class="font-mono text-xs px-1.5 py-0.5 rounded border border-zinc-300 dark:border-zinc-700 bg-zinc-200/50 dark:bg-zinc-800/50 text-zinc-600 dark:text-zinc-400"
|
|
||||||
>{username}</span
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
onclick={copyUsername}
|
|
||||||
class="p-0.5 rounded hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-colors"
|
|
||||||
title={copySuccess ? "Copied!" : "Copy username"}
|
|
||||||
>
|
|
||||||
{#if copySuccess}
|
|
||||||
<IconCheck
|
|
||||||
class="size-3.5 text-green-600 dark:text-green-500"
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<IconCopy class="size-3.5 text-zinc-400 dark:text-zinc-500" />
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
<span class="text-zinc-400 dark:text-zinc-500">·</span>
|
|
||||||
<span class="text-zinc-500 dark:text-zinc-400">any/they</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- About Me section -->
|
|
||||||
<div
|
<div
|
||||||
class="p-3 rounded-lg bg-zinc-200/50 dark:bg-zinc-800/50 border border-zinc-200 dark:border-zinc-700"
|
class="absolute bottom-0.5 right-0.5 size-5 rounded-full bg-green-500 border-[3px] border-zinc-100 dark:border-zinc-900"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Profile info -->
|
||||||
|
<!-- SPACING: mb-4 controls gap before About Me section -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<h2
|
||||||
|
id="discord-profile-title"
|
||||||
|
class="text-xl font-bold text-zinc-900 dark:text-zinc-100"
|
||||||
>
|
>
|
||||||
<h3
|
Xevion
|
||||||
class="text-xs font-semibold uppercase text-zinc-500 dark:text-zinc-500 mb-1"
|
</h2>
|
||||||
|
<!-- USERNAME ROW: gap-1.5 controls spacing between elements -->
|
||||||
|
<div class="flex items-center gap-1.5 text-sm">
|
||||||
|
<span
|
||||||
|
class="font-mono text-xs px-1.5 py-0.5 rounded border border-zinc-300 dark:border-zinc-700 bg-zinc-200/50 dark:bg-zinc-800/50 text-zinc-600 dark:text-zinc-400"
|
||||||
|
>{username}</span
|
||||||
>
|
>
|
||||||
About Me
|
<button
|
||||||
</h3>
|
onclick={copyUsername}
|
||||||
<p class="text-sm text-zinc-700 dark:text-zinc-300">
|
class="p-0.5 rounded hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-colors"
|
||||||
Live with dignity.<br />
|
title={copySuccess ? "Copied!" : "Copy username"}
|
||||||
<a
|
>
|
||||||
href="https://xevion.dev"
|
{#if copySuccess}
|
||||||
class="text-blue-600 dark:text-blue-400 hover:underline"
|
<IconCheck class="size-3.5 text-green-600 dark:text-green-500" />
|
||||||
target="_blank"
|
{:else}
|
||||||
rel="noopener noreferrer">https://xevion.dev</a
|
<IconCopy class="size-3.5 text-zinc-400 dark:text-zinc-500" />
|
||||||
>
|
{/if}
|
||||||
</p>
|
</button>
|
||||||
|
<span class="text-zinc-400 dark:text-zinc-500">·</span>
|
||||||
|
<span class="text-zinc-500 dark:text-zinc-400">any/they</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- About Me section -->
|
||||||
|
<div
|
||||||
|
class="p-3 rounded-lg bg-zinc-200/50 dark:bg-zinc-800/50 border border-zinc-200 dark:border-zinc-700"
|
||||||
|
>
|
||||||
|
<h3
|
||||||
|
class="text-xs font-semibold uppercase text-zinc-500 dark:text-zinc-500 mb-1"
|
||||||
|
>
|
||||||
|
About Me
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-zinc-700 dark:text-zinc-300">
|
||||||
|
Live with dignity.<br />
|
||||||
|
<a
|
||||||
|
href="https://xevion.dev"
|
||||||
|
class="text-blue-600 dark:text-blue-400 hover:underline"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer">https://xevion.dev</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</div>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
aria-label={themeStore.isDark
|
aria-label={themeStore.isDark
|
||||||
? "Switch to light mode"
|
? "Switch to light mode"
|
||||||
: "Switch to dark 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"
|
||||||
>
|
>
|
||||||
<div class="absolute inset-0 flex items-center justify-center">
|
<div class="absolute inset-0 flex items-center justify-center">
|
||||||
<IconSun
|
<IconSun
|
||||||
|
|||||||
+16
-11
@@ -1,4 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { pushState } from "$app/navigation";
|
||||||
|
import { page } from "$app/state";
|
||||||
import ProjectCard from "$lib/components/ProjectCard.svelte";
|
import ProjectCard from "$lib/components/ProjectCard.svelte";
|
||||||
import DiscordProfileModal from "$lib/components/DiscordProfileModal.svelte";
|
import DiscordProfileModal from "$lib/components/DiscordProfileModal.svelte";
|
||||||
import type { PageData } from "./$types";
|
import type { PageData } from "./$types";
|
||||||
@@ -26,8 +28,9 @@
|
|||||||
socialLinksWithIcons.filter((link: { visible: boolean }) => link.visible),
|
socialLinksWithIcons.filter((link: { visible: boolean }) => link.visible),
|
||||||
);
|
);
|
||||||
|
|
||||||
let discordModalOpen = $state(false);
|
function openDiscordModal(username: string) {
|
||||||
let discordUsername = $state("");
|
pushState("", { discordModal: { open: true, username } });
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main class="page-main overflow-x-hidden font-schibsted">
|
<main class="page-main overflow-x-hidden font-schibsted">
|
||||||
@@ -61,7 +64,7 @@
|
|||||||
<!-- Simple link platforms -->
|
<!-- Simple link platforms -->
|
||||||
<a
|
<a
|
||||||
href={link.value}
|
href={link.value}
|
||||||
class="flex items-center gap-x-1.5 px-1.5 py-1 rounded-sm bg-zinc-100 dark:bg-zinc-900 shadow-sm hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 dark:focus-visible:ring-zinc-500"
|
class="flex items-center gap-x-1.5 px-1.5 py-1 rounded-sm bg-zinc-100 dark:bg-zinc-900 shadow-sm hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 dark:focus-visible:ring-zinc-500 cursor-pointer"
|
||||||
>
|
>
|
||||||
<span class="size-4 text-zinc-600 dark:text-zinc-300">
|
<span class="size-4 text-zinc-600 dark:text-zinc-300">
|
||||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||||
@@ -76,11 +79,8 @@
|
|||||||
<!-- Discord - button that opens profile modal -->
|
<!-- Discord - button that opens profile modal -->
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex items-center gap-x-1.5 px-1.5 py-1 rounded-sm bg-zinc-100 dark:bg-zinc-900 shadow-sm hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 dark:focus-visible:ring-zinc-500"
|
class="flex items-center gap-x-1.5 px-1.5 py-1 rounded-sm bg-zinc-100 dark:bg-zinc-900 shadow-sm hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 dark:focus-visible:ring-zinc-500 cursor-pointer"
|
||||||
onclick={() => {
|
onclick={() => openDiscordModal(link.value)}
|
||||||
discordUsername = link.value;
|
|
||||||
discordModalOpen = true;
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<span class="size-4 text-zinc-600 dark:text-zinc-300">
|
<span class="size-4 text-zinc-600 dark:text-zinc-300">
|
||||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||||
@@ -95,7 +95,7 @@
|
|||||||
<!-- Email - mailto link -->
|
<!-- Email - mailto link -->
|
||||||
<a
|
<a
|
||||||
href="mailto:{link.value}"
|
href="mailto:{link.value}"
|
||||||
class="flex items-center gap-x-1.5 px-1.5 py-1 rounded-sm bg-zinc-100 dark:bg-zinc-900 shadow-sm hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 dark:focus-visible:ring-zinc-500"
|
class="flex items-center gap-x-1.5 px-1.5 py-1 rounded-sm bg-zinc-100 dark:bg-zinc-900 shadow-sm hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 dark:focus-visible:ring-zinc-500 cursor-pointer"
|
||||||
>
|
>
|
||||||
<span class="size-4.5 text-zinc-600 dark:text-zinc-300">
|
<span class="size-4.5 text-zinc-600 dark:text-zinc-300">
|
||||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||||
@@ -111,7 +111,7 @@
|
|||||||
<!-- PGP Key - links to dedicated page -->
|
<!-- PGP Key - links to dedicated page -->
|
||||||
<a
|
<a
|
||||||
href="/pgp"
|
href="/pgp"
|
||||||
class="flex items-center gap-x-1.5 px-1.5 py-1 rounded-sm bg-zinc-100 dark:bg-zinc-900 shadow-sm hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 dark:focus-visible:ring-zinc-500"
|
class="flex items-center gap-x-1.5 px-1.5 py-1 rounded-sm bg-zinc-100 dark:bg-zinc-900 shadow-sm hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 dark:focus-visible:ring-zinc-500 cursor-pointer"
|
||||||
>
|
>
|
||||||
<MaterialSymbolsVpnKey
|
<MaterialSymbolsVpnKey
|
||||||
class="size-4.5 text-zinc-600 dark:text-zinc-300"
|
class="size-4.5 text-zinc-600 dark:text-zinc-300"
|
||||||
@@ -135,4 +135,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<DiscordProfileModal bind:open={discordModalOpen} username={discordUsername} />
|
{#if page.state.discordModal?.open}
|
||||||
|
<DiscordProfileModal
|
||||||
|
username={page.state.discordModal.username}
|
||||||
|
onclose={() => history.back()}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|||||||
@@ -128,7 +128,7 @@
|
|||||||
<div class="flex flex-col sm:flex-row gap-2 sm:gap-3">
|
<div class="flex flex-col sm:flex-row gap-2 sm:gap-3">
|
||||||
<button
|
<button
|
||||||
onclick={copyToClipboard}
|
onclick={copyToClipboard}
|
||||||
class="flex items-center justify-center gap-2 px-3 sm:px-4 py-2 sm:py-2.5 rounded-sm bg-zinc-900 dark:bg-zinc-100 text-white dark:text-zinc-900 hover:bg-zinc-800 dark:hover:bg-zinc-200 transition-colors shadow-sm"
|
class="flex items-center justify-center gap-2 px-3 sm:px-4 py-2 sm:py-2.5 rounded-sm bg-zinc-900 dark:bg-zinc-100 text-white dark:text-zinc-900 hover:bg-zinc-800 dark:hover:bg-zinc-200 transition-colors shadow-sm cursor-pointer"
|
||||||
>
|
>
|
||||||
<IconCopy class="size-4 sm:size-5" />
|
<IconCopy class="size-4 sm:size-5" />
|
||||||
<span class="text-sm sm:text-base font-medium"
|
<span class="text-sm sm:text-base font-medium"
|
||||||
@@ -137,7 +137,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onclick={downloadKey}
|
onclick={downloadKey}
|
||||||
class="flex items-center justify-center gap-2 px-3 sm:px-4 py-2 sm:py-2.5 rounded-sm bg-zinc-100 dark:bg-zinc-800 text-zinc-800 dark:text-zinc-100 hover:bg-zinc-200 dark:hover:bg-zinc-700 transition-colors"
|
class="flex items-center justify-center gap-2 px-3 sm:px-4 py-2 sm:py-2.5 rounded-sm bg-zinc-100 dark:bg-zinc-800 text-zinc-800 dark:text-zinc-100 hover:bg-zinc-200 dark:hover:bg-zinc-700 transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
<IconDownload class="size-4 sm:size-5" />
|
<IconDownload class="size-4 sm:size-5" />
|
||||||
<span class="text-sm sm:text-base font-medium">Download</span>
|
<span class="text-sm sm:text-base font-medium">Download</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user