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">
|
<script lang="ts">
|
||||||
import Dialog from './ui/Dialog.svelte'
|
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 Select from './ui/Select.svelte'
|
||||||
import Switch from './ui/switch/Switch.svelte'
|
import Switch from './ui/switch/Switch.svelte'
|
||||||
import Button from './ui/Button.svelte'
|
import Button from './ui/Button.svelte'
|
||||||
|
|
@ -93,8 +96,7 @@
|
||||||
const currentPicture = $derived(pictureData.find((p) => p.filename === picture))
|
const currentPicture = $derived(pictureData.find((p) => p.filename === picture))
|
||||||
|
|
||||||
// Handle form submission
|
// Handle form submission
|
||||||
async function handleSave(e: Event) {
|
async function handleSave() {
|
||||||
e.preventDefault()
|
|
||||||
error = null
|
error = null
|
||||||
saving = true
|
saving = true
|
||||||
|
|
||||||
|
|
@ -160,101 +162,98 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Dialog
|
<Dialog bind:open {...onOpenChange ? { onOpenChange } : {}}>
|
||||||
bind:open
|
|
||||||
{...onOpenChange ? { onOpenChange } : {}}
|
|
||||||
title="@{username}"
|
|
||||||
description="Account Settings"
|
|
||||||
>
|
|
||||||
{#snippet children()}
|
{#snippet children()}
|
||||||
<form onsubmit={handleSave} class="settings-form">
|
<ModalHeader title="Account settings" description="@{username}" />
|
||||||
{#if error}
|
|
||||||
<div class="error-message">{error}</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="form-fields">
|
<ModalBody>
|
||||||
<!-- Picture Selection with Preview -->
|
<div class="settings-form">
|
||||||
<div class="picture-section">
|
{#if error}
|
||||||
<div class="current-avatar">
|
<div class="error-message">{error}</div>
|
||||||
<img
|
{/if}
|
||||||
src={`/profile/${picture}.png`}
|
|
||||||
srcset={`/profile/${picture}.png 1x, /profile/${picture}@2x.png 2x`}
|
<div class="form-fields">
|
||||||
alt={currentPicture?.name[locale] || ''}
|
<!-- Picture Selection with Preview -->
|
||||||
class="avatar-preview element-{element}"
|
<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>
|
</div>
|
||||||
|
|
||||||
|
<!-- Element Selection -->
|
||||||
<Select
|
<Select
|
||||||
bind:value={picture}
|
bind:value={element}
|
||||||
options={pictureOptions}
|
options={elementOptions}
|
||||||
label="Avatar"
|
label="Element"
|
||||||
placeholder="Select an avatar"
|
placeholder="Select an element"
|
||||||
fullWidth
|
fullWidth
|
||||||
contained
|
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>
|
</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>
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
<div class="form-actions">
|
<ModalFooter>
|
||||||
|
{#snippet children()}
|
||||||
<Button variant="ghost" onclick={handleClose} disabled={saving}>Cancel</Button>
|
<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'}
|
{saving ? 'Saving...' : 'Save Changes'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
{/snippet}
|
||||||
</form>
|
</ModalFooter>
|
||||||
{/snippet}
|
|
||||||
|
|
||||||
{#snippet footer()}
|
|
||||||
<!-- Empty footer, actions are in the form -->
|
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
|
@ -263,7 +262,6 @@
|
||||||
@use '$src/themes/colors' as colors;
|
@use '$src/themes/colors' as colors;
|
||||||
@use '$src/themes/typography' as typography;
|
@use '$src/themes/typography' as typography;
|
||||||
@use '$src/themes/layout' as layout;
|
@use '$src/themes/layout' as layout;
|
||||||
@use '$src/themes/effects' as effects;
|
|
||||||
|
|
||||||
.settings-form {
|
.settings-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -331,7 +329,7 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: spacing.$unit-2x;
|
padding: spacing.$unit-2x;
|
||||||
background-color: var(--input-bg);
|
background-color: var(--input-bound-bg);
|
||||||
border-radius: layout.$card-corner;
|
border-radius: layout.$card-corner;
|
||||||
|
|
||||||
span {
|
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) {
|
:global(fieldset) {
|
||||||
border: none;
|
border: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,8 @@
|
||||||
</span>
|
</span>
|
||||||
{:else if iconOnly && icon}
|
{:else if iconOnly && icon}
|
||||||
<Icon name={icon} size={iconSizes[size]} />
|
<Icon name={icon} size={iconSizes[size]} />
|
||||||
|
{:else if iconOnly && children}
|
||||||
|
{@render children()}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if rightAccessory}
|
{#if rightAccessory}
|
||||||
|
|
@ -364,24 +366,24 @@
|
||||||
gap: 0;
|
gap: 0;
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
padding: calc($unit * 1.5); // Default square padding
|
padding: calc($unit * 1.5); // Default square padding
|
||||||
|
}
|
||||||
|
|
||||||
&.small {
|
:global([data-button-root].iconOnly.small) {
|
||||||
padding: $unit !important; // Override size padding
|
padding: $unit !important; // Override size padding
|
||||||
width: calc($unit * 3.5);
|
width: 30px;
|
||||||
height: calc($unit * 3.5);
|
height: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.medium {
|
:global([data-button-root].iconOnly.medium) {
|
||||||
padding: calc($unit * 1.5) !important; // Override size padding
|
padding: calc($unit * 1.5) !important; // Override size padding
|
||||||
width: calc($unit * 5.5);
|
width: calc($unit * 5.5);
|
||||||
height: calc($unit * 5.5);
|
height: calc($unit * 5.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.large {
|
:global([data-button-root].iconOnly.large) {
|
||||||
padding: $unit-2x !important; // Override size padding
|
padding: $unit-2x !important; // Override size padding
|
||||||
width: calc($unit * 6.5);
|
width: calc($unit * 6.5);
|
||||||
height: calc($unit * 6.5);
|
height: calc($unit * 6.5);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save button special states
|
// Save button special states
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,21 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Dialog as DialogBase } from 'bits-ui'
|
import { Dialog as DialogBase } from 'bits-ui'
|
||||||
import type { Snippet } from 'svelte'
|
import type { Snippet } from 'svelte'
|
||||||
import Icon from '$lib/components/Icon.svelte'
|
|
||||||
|
|
||||||
type DialogSize = 'default' | 'large'
|
type DialogSize = 'default' | 'large'
|
||||||
|
|
||||||
interface DialogProps {
|
interface DialogProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
onOpenChange?: (open: boolean) => void
|
onOpenChange?: (open: boolean) => void
|
||||||
title?: string
|
|
||||||
description?: string
|
|
||||||
size?: DialogSize
|
size?: DialogSize
|
||||||
children: Snippet
|
children: Snippet
|
||||||
footer?: Snippet
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
open = $bindable(false),
|
open = $bindable(false),
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
title,
|
|
||||||
description,
|
|
||||||
size = 'default',
|
size = 'default',
|
||||||
children,
|
children
|
||||||
footer
|
|
||||||
}: DialogProps = $props()
|
}: DialogProps = $props()
|
||||||
|
|
||||||
const sizeClass = $derived(size === 'large' ? 'dialog-content-large' : '')
|
const sizeClass = $derived(size === 'large' ? 'dialog-content-large' : '')
|
||||||
|
|
@ -37,26 +30,11 @@
|
||||||
<DialogBase.Portal>
|
<DialogBase.Portal>
|
||||||
<DialogBase.Overlay class="dialog-overlay" />
|
<DialogBase.Overlay class="dialog-overlay" />
|
||||||
<DialogBase.Content class="dialog-content {sizeClass}">
|
<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">
|
<DialogBase.Close class="dialog-close">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</DialogBase.Close>
|
</DialogBase.Close>
|
||||||
|
|
||||||
<div class="dialog-body">
|
{@render children()}
|
||||||
{@render children()}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if footer}
|
|
||||||
<div class="dialog-footer">
|
|
||||||
{@render footer()}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</DialogBase.Content>
|
</DialogBase.Content>
|
||||||
</DialogBase.Portal>
|
</DialogBase.Portal>
|
||||||
</DialogBase.Root>
|
</DialogBase.Root>
|
||||||
|
|
@ -80,32 +58,18 @@
|
||||||
left: 50%;
|
left: 50%;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
background: var(--app-bg, white);
|
background: white;
|
||||||
border-radius: $card-corner;
|
border-radius: $card-corner;
|
||||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
||||||
max-width: 90vw;
|
max-width: 90vw;
|
||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
width: 500px;
|
width: 540px;
|
||||||
z-index: 101;
|
z-index: 101;
|
||||||
animation: slide-up $duration-standard ease-out;
|
animation: slide-up $duration-standard ease-out;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
: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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dialog-close) {
|
:global(.dialog-close) {
|
||||||
|
|
@ -123,6 +87,7 @@
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: $item-corner-small;
|
border-radius: $item-corner-small;
|
||||||
|
z-index: 1;
|
||||||
@include smooth-transition($duration-standard, all);
|
@include smooth-transition($duration-standard, all);
|
||||||
|
|
||||||
&:hover {
|
&: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.
|
// Large dialog variant for collection modals, etc.
|
||||||
:global(.dialog-content-large) {
|
:global(.dialog-content-large) {
|
||||||
width: 90vw;
|
width: 90vw;
|
||||||
|
|
@ -157,16 +108,6 @@
|
||||||
max-height: 85vh;
|
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 {
|
@keyframes fade-in {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue