feat: keep modal hierarchy

This commit is contained in:
madkarmaa
2025-11-19 10:44:47 +01:00
parent 8bd5153b67
commit 6d430b4487
6 changed files with 122 additions and 21 deletions

View File

@@ -0,0 +1,28 @@
<script lang="ts">
import { modalsStack } from '$stores/modals.svelte';
import { fade } from 'svelte/transition';
let hasModals = $derived(modalsStack.getStack().length > 0);
</script>
{#if hasModals}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
class="modal-background"
transition:fade={{ duration: 300 }}
onclick={() => modalsStack.closeTop()}
></div>
{/if}
<style>
.modal-background {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.3);
z-index: 9998;
}
</style>

View File

@@ -1,15 +1,35 @@
<script lang="ts">
import type { WithChildren } from '$types';
import type { Snippet } from 'svelte';
import type { WithChildren } from '$types';
import { modalsStack } from '$stores/modals.svelte';
import { fade } from 'svelte/transition';
type Props = {
id: string;
open?: boolean;
buttons?: Snippet;
} & WithChildren;
let { buttons, children }: Props = $props();
let { id, buttons, children, open = $bindable(true) }: Props = $props();
let isTopModal = $derived(modalsStack.isTopModal(id));
$effect(() => {
if (open)
modalsStack.push(id, () => {
open = false;
});
else modalsStack.pop(id);
});
</script>
<div class="background">
<div class="modal rounded">
{#if isTopModal}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<dialog
class="modal rounded"
transition:fade={{ duration: 300 }}
onclick={(e) => e.stopPropagation()}
>
<div class="content">
{@render children()}
</div>
@@ -18,24 +38,15 @@
{@render buttons()}
</div>
{/if}
</div>
</div>
</dialog>
{/if}
<style>
.background {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: 2rem;
max-width: 90vw;
max-height: 90vh;
@@ -43,6 +54,7 @@
display: flex;
flex-direction: column;
gap: 1.5rem;
z-index: 9999;
}
.content {

View File

@@ -3,6 +3,7 @@
import logo from '$assets/logo.svg';
import Notifications from 'virtual:icons/material-symbols/notifications-outline';
import Settings from 'virtual:icons/material-symbols/settings-outline';
import Modal from '$components/molecules/Modal.svelte';
const navItems = [
{ label: 'Home', href: '/' },
@@ -11,6 +12,9 @@
{ label: 'Contributors', href: '/contributors' },
{ label: 'Donate', href: '/donate' }
] as const satisfies { label: string; href: string }[];
let settingsOpen = $state(false);
let loginOpen = $state(false);
</script>
<nav>
@@ -32,10 +36,25 @@
class="rounded nav-button unselectable"
class:active={page.url.pathname === '/announcements'}><Notifications /></a
>
<button class="rounded nav-button unselectable" type="button"><Settings /></button>
<button
class="rounded nav-button unselectable"
type="button"
onclick={() => (settingsOpen = true)}><Settings /></button
>
</div>
</nav>
<Modal id="settings" bind:open={settingsOpen}>
<h2>Settings</h2>
{#snippet buttons()}
<button type="button" onclick={() => (loginOpen = true)}>Login</button>
{/snippet}
</Modal>
<Modal id="login" bind:open={loginOpen}>
<h2>Login</h2>
</Modal>
<style>
nav {
display: flex;
@@ -46,7 +65,7 @@
position: sticky;
top: 0;
left: 0;
z-index: 9998;
z-index: 9997;
}
.nav-group {

View File

@@ -0,0 +1,37 @@
import { SvelteMap } from 'svelte/reactivity';
class ModalStack {
private stack = $state<string[]>([]);
private modals = new SvelteMap<string, () => void>();
push(id: string, closeCallback: () => void) {
if (!this.modals.has(id)) {
this.stack.push(id);
this.modals.set(id, closeCallback);
}
}
pop(id: string) {
const index = this.stack.indexOf(id);
if (index !== -1) {
this.stack.splice(index, 1);
this.modals.delete(id);
}
}
closeTop() {
if (this.stack.length < 0) return;
const topId = this.stack[this.stack.length - 1];
this.modals.get(topId)?.();
}
isTopModal(id: string): boolean {
return this.stack.length > 0 && this.stack[this.stack.length - 1] === id;
}
getStack(): readonly string[] {
return this.stack;
}
}
export const modalsStack = new ModalStack();

View File

@@ -2,6 +2,7 @@
import '../app.css';
import favicon from '$assets/favicon.ico';
import NavBar from '$components/molecules/NavBar.svelte';
import ModalBackground from '$components/atoms/ModalBackground.svelte';
import type { WithChildren } from '$types';
let { children }: WithChildren = $props();
@@ -12,4 +13,7 @@
</svelte:head>
<NavBar />
<ModalBackground />
{@render children()}

View File

@@ -12,6 +12,7 @@ const config = {
$components: 'src/lib/components',
$assets: 'src/lib/assets',
$types: 'src/lib/types.ts',
$stores: 'src/lib/stores',
$lib: 'src/lib'
}
}