feat: ui theming & improved clickOutside handler (for modals)

This commit is contained in:
ae 2025-05-06 12:59:29 +03:00
parent a950da9412
commit b3b897be85
Signed by: ae
GPG Key ID: 995EFD5C1B532B3E
5 changed files with 293 additions and 264 deletions

View File

@ -13,7 +13,7 @@
:root { :root {
--light-background: #f5f5f5; --light-background: #f5f5f5;
--light-foreground: #e0e0e0; --light-foreground: #e0e0e0;
--light-accent: #303052; --light-accent: #2e2e88;
--light-text: rgb(34, 40, 49); --light-text: rgb(34, 40, 49);
--light-highlight-text: #c28e4a; --light-highlight-text: #c28e4a;
--light-error-text: #4b0000; --light-error-text: #4b0000;
@ -910,6 +910,14 @@
@apply fixed inset-0 z-40 flex items-center justify-center backdrop-blur-xs; @apply fixed inset-0 z-40 flex items-center justify-center backdrop-blur-xs;
} }
.modal-exit-button {
@apply flex h-8 w-8 items-center justify-center rounded-lg border border-[var(--light-accent)] bg-transparent p-0 text-[var(--light-text)] hover:bg-[var(--light-foreground)]/80;
}
.dark .modal-exit-button {
@apply hover:bg-[var(--dark-foreground)]/80;
}
.modal-content { .modal-content {
@apply mx-4 max-h-[90vh] w-full max-w-[90vw] overflow-y-auto rounded-lg border-2 border-[var(--light-accent)]/10 bg-[var(--light-background)] shadow-lg; @apply mx-4 max-h-[90vh] w-full max-w-[90vw] overflow-y-auto rounded-lg border-2 border-[var(--light-accent)]/10 bg-[var(--light-background)] shadow-lg;
} }

View File

@ -4,7 +4,7 @@
import { onMount } from "svelte" import { onMount } from "svelte"
import type { User } from "$lib/logic/model" import type { User } from "$lib/logic/model"
import Delete from "$lib/icons/sidebar/Delete.svelte" import Delete from "$lib/icons/sidebar/Delete.svelte"
import { formatDateShort } from "$lib/util/contentVisual" import { clickOutside, formatDateShort } from "$lib/util/contentVisual"
// props // props
export let onClose: () => void export let onClose: () => void
@ -46,14 +46,6 @@
} }
} }
const handleClickOutside = (event: MouseEvent) => {
// close the modal if user clicks outside of it
const target = event.target as HTMLElement
if (target.classList.contains("modal-backdrop")) {
onClose()
}
}
const handleModalContentKeydown = (_event: KeyboardEvent) => { const handleModalContentKeydown = (_event: KeyboardEvent) => {
// accessibility compliance handler, actual handling is done by the global keydown handler // accessibility compliance handler, actual handling is done by the global keydown handler
} }
@ -71,13 +63,13 @@
<div <div
class="modal-backdrop" class="modal-backdrop"
on:click={handleClickOutside}
on:keydown={handleKeydown} on:keydown={handleKeydown}
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
tabindex="-1" tabindex="-1"
> >
<!-- svelte-ignore a11y_no_noninteractive_element_interactions --> <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<div use:clickOutside={() => onClose()}>
<div <div
bind:this={modalContent} bind:this={modalContent}
class="modal-content max-w-xl" class="modal-content max-w-xl"
@ -89,7 +81,7 @@
<!-- Header (title + close button) --> <!-- Header (title + close button) -->
<div class="modal-section flex items-center justify-between" role="heading" aria-level="1"> <div class="modal-section flex items-center justify-between" role="heading" aria-level="1">
<h2 class="text-xl font-bold">Administration</h2> <h2 class="text-xl font-bold">Administration</h2>
<button on:click={onClose} class="sidebar-button" aria-label="Close settings"> <button on:click={onClose} class="modal-exit-button" aria-label="Close settings">
<Close /> <Close />
</button> </button>
</div> </div>
@ -139,4 +131,5 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</div> </div>

View File

@ -3,7 +3,7 @@
import Close from "$lib/icons/Close.svelte" import Close from "$lib/icons/Close.svelte"
import { isPasswordValid } from "$lib/util/authValidation" import { isPasswordValid } from "$lib/util/authValidation"
import { onMount } from "svelte" import { onMount } from "svelte"
import { formatDateLong } from "$lib/util/contentVisual" import { clickOutside, formatDateLong } from "$lib/util/contentVisual"
// props // props
export let onClose: () => void export let onClose: () => void
@ -103,14 +103,6 @@
} }
} }
const handleClickOutside = (event: MouseEvent) => {
// close the modal if user clicks outside of it
const target = event.target as HTMLElement
if (target.classList.contains("modal-backdrop")) {
onClose()
}
}
const handleModalContentKeydown = (_event: KeyboardEvent) => { const handleModalContentKeydown = (_event: KeyboardEvent) => {
// accessibility compliance handler, actual handling is done by the global keydown handler // accessibility compliance handler, actual handling is done by the global keydown handler
} }
@ -126,13 +118,13 @@
<div <div
class="modal-backdrop" class="modal-backdrop"
on:click={handleClickOutside}
on:keydown={handleKeydown} on:keydown={handleKeydown}
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
tabindex="-1" tabindex="-1"
> >
<!-- svelte-ignore a11y_no_noninteractive_element_interactions --> <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<div use:clickOutside={() => onClose()}>
<div <div
bind:this={modalContent} bind:this={modalContent}
class="modal-content max-w-xl" class="modal-content max-w-xl"
@ -144,7 +136,7 @@
<!-- Header (title + close button) --> <!-- Header (title + close button) -->
<div class="modal-section flex items-center justify-between" role="heading" aria-level="1"> <div class="modal-section flex items-center justify-between" role="heading" aria-level="1">
<h2 class="text-xl font-bold">Settings</h2> <h2 class="text-xl font-bold">Settings</h2>
<button on:click={onClose} class="sidebar-button" aria-label="Close settings"> <button on:click={onClose} class="modal-exit-button" aria-label="Close settings">
<Close /> <Close />
</button> </button>
</div> </div>
@ -246,8 +238,8 @@
<div class="space-y-4"> <div class="space-y-4">
<p class="text-sm"> <p class="text-sm">
Deleting your account will permanently remove all your notes and account information. This Deleting your account will permanently remove all your notes and account information.
action cannot be undone. This action cannot be undone.
</p> </p>
<!-- Confirm Password --> <!-- Confirm Password -->
@ -283,4 +275,5 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</div> </div>

View File

@ -12,7 +12,12 @@
import AdminWrench from "$lib/icons/sidebar/AdminShield.svelte" import AdminWrench from "$lib/icons/sidebar/AdminShield.svelte"
import Exit from "$lib/icons/sidebar/Exit.svelte" import Exit from "$lib/icons/sidebar/Exit.svelte"
import ThemeToggle from "./ThemeToggle.svelte" import ThemeToggle from "./ThemeToggle.svelte"
import { formatDateLong, formatDateShort, parseExpirationPrefix } from "$lib/util/contentVisual" import {
clickOutside,
formatDateLong,
formatDateShort,
parseExpirationPrefix
} from "$lib/util/contentVisual"
import type { FullNote, NoteMetadata } from "$lib/logic/model" import type { FullNote, NoteMetadata } from "$lib/logic/model"
// props // props
@ -210,6 +215,13 @@
<!-- User section (single button) with dropdown --> <!-- User section (single button) with dropdown -->
<div class="relative flex flex-col px-2 py-3"> <div class="relative flex flex-col px-2 py-3">
<div
use:clickOutside={() => {
if (isUserMenuOpen) {
isUserMenuOpen = false
}
}}
>
<button <button
on:click={toggleUserMenu} on:click={toggleUserMenu}
class="sidebar-action-button flex-wrap justify-between px-4 py-4" class="sidebar-action-button flex-wrap justify-between px-4 py-4"
@ -260,5 +272,6 @@
</div> </div>
{/if} {/if}
</div> </div>
</div>
</aside> </aside>
</div> </div>

View File

@ -50,3 +50,25 @@ export const formatTitleWithHighlight = (title: string): string => {
return `<span class="note-title-exp-highlight">${expirationPrefix}</span> ${cleanTitle}` return `<span class="note-title-exp-highlight">${expirationPrefix}</span> ${cleanTitle}`
} }
export const clickOutside = (
node: HTMLElement,
callbackFunction: () => void
): {
destroy: () => void
} => {
const handleClick = (event: MouseEvent): void => {
if (node && !node.contains(event.target as Node) && !event.defaultPrevented) {
callbackFunction()
}
}
// event listener with capture phase to ensure it runs before other click handlers
document.addEventListener("click", handleClick, true)
return {
destroy: () => {
document.removeEventListener("click", handleClick, true)
}
}
}