refactor modal components to use header/body/footer pattern
UserSettingsModal now uses ModalHeader, ModalBody, ModalFooter. Dialog simplified. Button tweaks.
This commit is contained in:
parent
eaea344db4
commit
4418157ca0
3 changed files with 109 additions and 176 deletions
|
|
@ -2,6 +2,9 @@
|
|||
|
||||
<script lang="ts">
|
||||
import Dialog from './ui/Dialog.svelte'
|
||||
import ModalHeader from './ui/ModalHeader.svelte'
|
||||
import ModalBody from './ui/ModalBody.svelte'
|
||||
import ModalFooter from './ui/ModalFooter.svelte'
|
||||
import Select from './ui/Select.svelte'
|
||||
import Switch from './ui/switch/Switch.svelte'
|
||||
import Button from './ui/Button.svelte'
|
||||
|
|
@ -10,7 +13,7 @@
|
|||
import type { UserCookie } from '$lib/types/UserCookie'
|
||||
import { setUserCookie } from '$lib/auth/cookies'
|
||||
import { invalidateAll } from '$app/navigation'
|
||||
|
||||
|
||||
interface Props {
|
||||
open: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
|
|
@ -93,8 +96,7 @@
|
|||
const currentPicture = $derived(pictureData.find((p) => p.filename === picture))
|
||||
|
||||
// Handle form submission
|
||||
async function handleSave(e: Event) {
|
||||
e.preventDefault()
|
||||
async function handleSave() {
|
||||
error = null
|
||||
saving = true
|
||||
|
||||
|
|
@ -160,101 +162,98 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<Dialog
|
||||
bind:open
|
||||
{...onOpenChange ? { onOpenChange } : {}}
|
||||
title="@{username}"
|
||||
description="Account Settings"
|
||||
>
|
||||
<Dialog bind:open {...onOpenChange ? { onOpenChange } : {}}>
|
||||
{#snippet children()}
|
||||
<form onsubmit={handleSave} class="settings-form">
|
||||
{#if error}
|
||||
<div class="error-message">{error}</div>
|
||||
{/if}
|
||||
<ModalHeader title="Account settings" description="@{username}" />
|
||||
|
||||
<div class="form-fields">
|
||||
<!-- Picture Selection with Preview -->
|
||||
<div class="picture-section">
|
||||
<div class="current-avatar">
|
||||
<img
|
||||
src={`/profile/${picture}.png`}
|
||||
srcset={`/profile/${picture}.png 1x, /profile/${picture}@2x.png 2x`}
|
||||
alt={currentPicture?.name[locale] || ''}
|
||||
class="avatar-preview element-{element}"
|
||||
<ModalBody>
|
||||
<div class="settings-form">
|
||||
{#if error}
|
||||
<div class="error-message">{error}</div>
|
||||
{/if}
|
||||
|
||||
<div class="form-fields">
|
||||
<!-- Picture Selection with Preview -->
|
||||
<div class="picture-section">
|
||||
<div class="current-avatar">
|
||||
<img
|
||||
src={`/profile/${picture}.png`}
|
||||
srcset={`/profile/${picture}.png 1x, /profile/${picture}@2x.png 2x`}
|
||||
alt={currentPicture?.name[locale] || ''}
|
||||
class="avatar-preview element-{element}"
|
||||
/>
|
||||
</div>
|
||||
<Select
|
||||
bind:value={picture}
|
||||
options={pictureOptions}
|
||||
label="Avatar"
|
||||
placeholder="Select an avatar"
|
||||
fullWidth
|
||||
contained
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Element Selection -->
|
||||
<Select
|
||||
bind:value={picture}
|
||||
options={pictureOptions}
|
||||
label="Avatar"
|
||||
placeholder="Select an avatar"
|
||||
bind:value={element}
|
||||
options={elementOptions}
|
||||
label="Element"
|
||||
placeholder="Select an element"
|
||||
fullWidth
|
||||
contained
|
||||
/>
|
||||
|
||||
<!-- Gender Selection -->
|
||||
<Select
|
||||
bind:value={gender}
|
||||
options={genderOptions}
|
||||
label="Gender"
|
||||
placeholder="Select gender"
|
||||
fullWidth
|
||||
contained
|
||||
/>
|
||||
|
||||
<!-- Language Selection -->
|
||||
<Select
|
||||
bind:value={language}
|
||||
options={languageOptions}
|
||||
label="Language"
|
||||
placeholder="Select language"
|
||||
fullWidth
|
||||
contained
|
||||
/>
|
||||
|
||||
<!-- Theme Selection -->
|
||||
<Select
|
||||
bind:value={theme}
|
||||
options={themeOptions}
|
||||
label="Theme"
|
||||
placeholder="Select theme"
|
||||
fullWidth
|
||||
contained
|
||||
/>
|
||||
|
||||
<!-- Admin Mode (only for admins) -->
|
||||
{#if role === 9}
|
||||
<div class="switch-field">
|
||||
<label for="bahamut-mode">
|
||||
<span>Admin Mode</span>
|
||||
<Switch bind:checked={bahamut} name="bahamut-mode" />
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Element Selection -->
|
||||
<Select
|
||||
bind:value={element}
|
||||
options={elementOptions}
|
||||
label="Element"
|
||||
placeholder="Select an element"
|
||||
fullWidth
|
||||
contained
|
||||
/>
|
||||
|
||||
<!-- Gender Selection -->
|
||||
<Select
|
||||
bind:value={gender}
|
||||
options={genderOptions}
|
||||
label="Gender"
|
||||
placeholder="Select gender"
|
||||
fullWidth
|
||||
contained
|
||||
/>
|
||||
|
||||
<!-- Language Selection -->
|
||||
<Select
|
||||
bind:value={language}
|
||||
options={languageOptions}
|
||||
label="Language"
|
||||
placeholder="Select language"
|
||||
fullWidth
|
||||
contained
|
||||
/>
|
||||
|
||||
<!-- Theme Selection -->
|
||||
<Select
|
||||
bind:value={theme}
|
||||
options={themeOptions}
|
||||
label="Theme"
|
||||
placeholder="Select theme"
|
||||
fullWidth
|
||||
contained
|
||||
/>
|
||||
|
||||
<!-- Admin Mode (only for admins) -->
|
||||
{#if role === 9}
|
||||
<div class="switch-field">
|
||||
<label for="bahamut-mode">
|
||||
<span>Admin Mode</span>
|
||||
<Switch bind:checked={bahamut} name="bahamut-mode" />
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
<div class="form-actions">
|
||||
<ModalFooter>
|
||||
{#snippet children()}
|
||||
<Button variant="ghost" onclick={handleClose} disabled={saving}>Cancel</Button>
|
||||
<Button type="submit" variant="primary" disabled={saving}>
|
||||
<Button onclick={handleSave} variant="primary" disabled={saving}>
|
||||
{saving ? 'Saving...' : 'Save Changes'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
{/snippet}
|
||||
|
||||
{#snippet footer()}
|
||||
<!-- Empty footer, actions are in the form -->
|
||||
{/snippet}
|
||||
</ModalFooter>
|
||||
{/snippet}
|
||||
</Dialog>
|
||||
|
||||
|
|
@ -263,7 +262,6 @@
|
|||
@use '$src/themes/colors' as colors;
|
||||
@use '$src/themes/typography' as typography;
|
||||
@use '$src/themes/layout' as layout;
|
||||
@use '$src/themes/effects' as effects;
|
||||
|
||||
.settings-form {
|
||||
display: flex;
|
||||
|
|
@ -331,7 +329,7 @@
|
|||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: spacing.$unit-2x;
|
||||
background-color: var(--input-bg);
|
||||
background-color: var(--input-bound-bg);
|
||||
border-radius: layout.$card-corner;
|
||||
|
||||
span {
|
||||
|
|
@ -341,14 +339,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: spacing.$unit-2x;
|
||||
justify-content: flex-end;
|
||||
padding-top: spacing.$unit-2x;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
:global(fieldset) {
|
||||
border: none;
|
||||
padding: 0;
|
||||
|
|
|
|||
|
|
@ -139,6 +139,8 @@
|
|||
</span>
|
||||
{:else if iconOnly && icon}
|
||||
<Icon name={icon} size={iconSizes[size]} />
|
||||
{:else if iconOnly && children}
|
||||
{@render children()}
|
||||
{/if}
|
||||
|
||||
{#if rightAccessory}
|
||||
|
|
@ -364,24 +366,24 @@
|
|||
gap: 0;
|
||||
aspect-ratio: 1;
|
||||
padding: calc($unit * 1.5); // Default square padding
|
||||
}
|
||||
|
||||
&.small {
|
||||
padding: $unit !important; // Override size padding
|
||||
width: calc($unit * 3.5);
|
||||
height: calc($unit * 3.5);
|
||||
}
|
||||
:global([data-button-root].iconOnly.small) {
|
||||
padding: $unit !important; // Override size padding
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
&.medium {
|
||||
padding: calc($unit * 1.5) !important; // Override size padding
|
||||
width: calc($unit * 5.5);
|
||||
height: calc($unit * 5.5);
|
||||
}
|
||||
:global([data-button-root].iconOnly.medium) {
|
||||
padding: calc($unit * 1.5) !important; // Override size padding
|
||||
width: calc($unit * 5.5);
|
||||
height: calc($unit * 5.5);
|
||||
}
|
||||
|
||||
&.large {
|
||||
padding: $unit-2x !important; // Override size padding
|
||||
width: calc($unit * 6.5);
|
||||
height: calc($unit * 6.5);
|
||||
}
|
||||
:global([data-button-root].iconOnly.large) {
|
||||
padding: $unit-2x !important; // Override size padding
|
||||
width: calc($unit * 6.5);
|
||||
height: calc($unit * 6.5);
|
||||
}
|
||||
|
||||
// Save button special states
|
||||
|
|
|
|||
|
|
@ -1,28 +1,21 @@
|
|||
<script lang="ts">
|
||||
import { Dialog as DialogBase } from 'bits-ui'
|
||||
import type { Snippet } from 'svelte'
|
||||
import Icon from '$lib/components/Icon.svelte'
|
||||
|
||||
type DialogSize = 'default' | 'large'
|
||||
|
||||
interface DialogProps {
|
||||
open: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
title?: string
|
||||
description?: string
|
||||
size?: DialogSize
|
||||
children: Snippet
|
||||
footer?: Snippet
|
||||
}
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
onOpenChange,
|
||||
title,
|
||||
description,
|
||||
size = 'default',
|
||||
children,
|
||||
footer
|
||||
children
|
||||
}: DialogProps = $props()
|
||||
|
||||
const sizeClass = $derived(size === 'large' ? 'dialog-content-large' : '')
|
||||
|
|
@ -37,26 +30,11 @@
|
|||
<DialogBase.Portal>
|
||||
<DialogBase.Overlay class="dialog-overlay" />
|
||||
<DialogBase.Content class="dialog-content {sizeClass}">
|
||||
{#if title}
|
||||
<DialogBase.Title class="dialog-title">{title}</DialogBase.Title>
|
||||
{/if}
|
||||
{#if description}
|
||||
<DialogBase.Description class="dialog-description">{description}</DialogBase.Description>
|
||||
{/if}
|
||||
|
||||
<DialogBase.Close class="dialog-close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</DialogBase.Close>
|
||||
|
||||
<div class="dialog-body">
|
||||
{@render children()}
|
||||
</div>
|
||||
|
||||
{#if footer}
|
||||
<div class="dialog-footer">
|
||||
{@render footer()}
|
||||
</div>
|
||||
{/if}
|
||||
{@render children()}
|
||||
</DialogBase.Content>
|
||||
</DialogBase.Portal>
|
||||
</DialogBase.Root>
|
||||
|
|
@ -80,32 +58,18 @@
|
|||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: var(--app-bg, white);
|
||||
background: white;
|
||||
border-radius: $card-corner;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
width: 500px;
|
||||
width: 540px;
|
||||
z-index: 101;
|
||||
animation: slide-up $duration-standard ease-out;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:global(.dialog-title) {
|
||||
font-size: $font-large;
|
||||
font-weight: $bold;
|
||||
margin: 0;
|
||||
padding: $unit-2x;
|
||||
padding-bottom: 0;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
:global(.dialog-description) {
|
||||
font-size: $font-regular;
|
||||
color: var(--text-secondary);
|
||||
padding: 0 $unit-2x;
|
||||
margin: $unit 0;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:global(.dialog-close) {
|
||||
|
|
@ -123,6 +87,7 @@
|
|||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
border-radius: $item-corner-small;
|
||||
z-index: 1;
|
||||
@include smooth-transition($duration-standard, all);
|
||||
|
||||
&:hover {
|
||||
|
|
@ -135,20 +100,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
:global(.dialog-body) {
|
||||
padding: $unit-2x;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
:global(.dialog-footer) {
|
||||
padding: $unit-2x;
|
||||
padding-top: 0;
|
||||
display: flex;
|
||||
gap: $unit;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
// Large dialog variant for collection modals, etc.
|
||||
:global(.dialog-content-large) {
|
||||
width: 90vw;
|
||||
|
|
@ -157,16 +108,6 @@
|
|||
max-height: 85vh;
|
||||
}
|
||||
|
||||
:global(.dialog-content-large .dialog-body) {
|
||||
padding: $unit-3x;
|
||||
}
|
||||
|
||||
:global(.dialog-content-large .dialog-footer) {
|
||||
padding: $unit-3x;
|
||||
padding-top: $unit-2x;
|
||||
border-top: 1px solid var(--border-color, rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
|
|
@ -186,4 +127,4 @@
|
|||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in a new issue