Skip to content

Commit

Permalink
feat: modal component
Browse files Browse the repository at this point in the history
  • Loading branch information
paring-chan committed Jan 4, 2025
1 parent 2209fac commit 5e719ac
Show file tree
Hide file tree
Showing 14 changed files with 338 additions and 38 deletions.
3 changes: 3 additions & 0 deletions src/lib/components/Button.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use?: ActionArray
variant?: ButtonStyle
size?: ButtonSize
transparent?: boolean
leftIcon?: string
rightIcon?: string
Expand All @@ -31,6 +32,7 @@
children,
iconOnly = false,
link,
transparent = false,
...rest
}: Props = $props()
</script>
Expand All @@ -41,6 +43,7 @@
use:melt={$meltElement}
class="button button-style-{variant} button-size-{size}"
class:icon-only={iconOnly}
class:transparent
{...rest}
>
{#if leftIcon}
Expand Down
66 changes: 34 additions & 32 deletions src/lib/components/form/FormField.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
noLabel?: boolean
helpText?: TranslationKey
horizontal?: boolean
modal?: boolean
children?: Snippet
hints?: Snippet
}
Expand All @@ -24,13 +25,14 @@
helpText,
required = false,
error,
modal = false,
hints
}: Props = $props()
</script>

{#snippet hintsFallback()}
{#if error}
<FormHintArea>
<FormHintArea {modal}>
<FormHint type="error">
<Translation key={error} />
</FormHint>
Expand Down Expand Up @@ -71,28 +73,28 @@
</div>

<style lang="scss">
@use '../../stylesheets/system/colors' as *;
@use '../../stylesheets/system/colors' as *;

.label {
font-size: 16px;
font-weight: 500;
line-height: 140%;
}
.label {
font-size: 16px;
font-weight: 500;
line-height: 140%;
}

.required-sign {
color: rgba($red, 1);
font-size: 12px;
font-weight: 500;
margin-left: 4px;
}
.required-sign {
color: rgba($red, 1);
font-size: 12px;
font-weight: 500;
margin-left: 4px;
}

.form-field {
display: flex;
flex-direction: column;
}
.form-field {
display: flex;
flex-direction: column;
}

.horizontal {
.form-field-content {
.horizontal {
.form-field-content {
flex-direction: row;
min-height: 38px;
gap: 16px;
Expand All @@ -109,19 +111,19 @@
width: 0;
flex-grow: 1;
}
}
}
}
}

.form-field-content {
display: flex;
flex-direction: column;
gap: 2px;
}
.form-field-content {
display: flex;
flex-direction: column;
gap: 2px;
}

.form-help-text {
font-size: 12px;
color: rgba(255, 255, 255, 0.6);
line-height: 12px;
margin-top: 4px;
}
.form-help-text {
font-size: 12px;
color: rgba(255, 255, 255, 0.6);
line-height: 12px;
margin-top: 4px;
}
</style>
11 changes: 9 additions & 2 deletions src/lib/components/form/FormHint.svelte
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
<script lang="ts">
export let type: 'hint' | 'error'
import type { Snippet } from 'svelte'
interface Props {
type: 'hint' | 'error'
children?: Snippet
}
const { type, children }: Props = $props()
</script>

<div class="hint-container {type}">
<div class="dot"></div>
<slot />
{@render children?.()}
</div>

<style lang="scss">
Expand Down
19 changes: 17 additions & 2 deletions src/lib/components/form/FormHintArea.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
<div class="form-hint-area" {...$$restProps}>
<slot />
<script lang="ts">
import type { Snippet } from 'svelte'
interface Props {
modal?: boolean
children?: Snippet
}
const { modal, children }: Props = $props()
</script>

<div class="form-hint-area" class:modal>
{@render children?.()}
</div>

<style lang="scss">
Expand All @@ -12,5 +23,9 @@
border-bottom-right-radius: 6px;
grid-template-columns: repeat(var(--hint-columns, 1), 1fr);
&.modal {
background-color: rgba(255, 255, 255, 0.05);
}
}
</style>
55 changes: 55 additions & 0 deletions src/lib/components/modal/Modal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<script lang="ts">
import {
createDialog,
createSync,
melt,
type AnyMeltElement,
type CreateDialogProps
} from '@melt-ui/svelte'
import { ModalContext, type ModalContextData } from '$lib/components/modal/context'
import { setContext, type Snippet } from 'svelte'
import ModalOverlay from './ModalOverlay.svelte'
import ModalPopup from './ModalPopup.svelte'
interface Props {
trigger?: Snippet<[{ trigger: ModalContextData['elements']['trigger'] }]>
children?: Snippet<[{ close: () => void }]>
options?: Omit<CreateDialogProps, 'forceVisible'>
open?: boolean
}
let { trigger, options = {}, children, open = $bindable(false) }: Props = $props()
const modal = createDialog({
forceVisible: true,
...options
})
setContext(ModalContext, modal)
const {
elements: { trigger: triggerEl, portalled, overlay }
} = modal
const sync = createSync(modal.states)
$effect(() => {
sync.open(open, (v) => (open = v))
})
</script>

{@render trigger?.({ trigger: triggerEl })}

{#if open}
<div use:melt={$portalled}>
<ModalOverlay>
<ModalPopup>
{@render children?.({
close: () => {
open = false
}
})}
</ModalPopup>
</ModalOverlay>
</div>
{/if}
27 changes: 27 additions & 0 deletions src/lib/components/modal/ModalActions.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<script lang="ts">
import type { Snippet } from 'svelte'
interface Props {
left?: Snippet
right?: Snippet
}
const { left, right }: Props = $props()
</script>

<div class="actions">
{@render left?.()}
<div class="spacer"></div>
{@render right?.()}
</div>

<style lang="scss">
.actions {
display: flex;
gap: 12px;
}
.spacer {
flex-grow: 1;
}
</style>
45 changes: 45 additions & 0 deletions src/lib/components/modal/ModalOverlay.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<script lang="ts">
import { melt } from '@melt-ui/svelte'
import { ModalContext, type ModalContextData } from '$lib/components/modal/context'
import { getContext, type Snippet } from 'svelte'
import { fade, fly } from 'svelte/transition'
interface Props {
children?: Snippet
}
let { children }: Props = $props()
const {
elements: { overlay }
} = getContext<ModalContextData>(ModalContext)
</script>

<div class="modal-overlay">
<div class="backdrop" use:melt={$overlay} transition:fade={{ duration: 150 }}></div>
<div class="content" transition:fly={{ duration: 400, y: 48, opacity: 0 }}>
{@render children?.()}
</div>
</div>

<style lang="scss">
.modal-overlay {
position: fixed;
z-index: 100;
inset: 0;
}
.content {
position: fixed;
inset: 0;
display: flex;
justify-content: center;
pointer-events: none;
}
.backdrop {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.3);
}
</style>
47 changes: 47 additions & 0 deletions src/lib/components/modal/ModalPopup.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<script lang="ts">
import { getContext, type Snippet } from 'svelte'
import { ModalContext, type ModalContextData } from './context'
import { melt } from '@melt-ui/svelte'
interface Props {
children?: Snippet
}
const { children }: Props = $props()
const {
elements: { content }
} = getContext<ModalContextData>(ModalContext)
</script>

<div class="modal-popup" use:melt={$content}>
{@render children?.()}
</div>

<style lang="scss">
@use '../../stylesheets/system/colors' as *;
@use '../../stylesheets/system/breakpoints' as *;
.modal-popup {
background-color: $darkblue;
padding: 32px;
display: flex;
flex-direction: column;
gap: 24px;
pointer-events: all;
width: 100%;
max-height: calc(100% - 64px);
overflow-y: auto;
align-self: flex-end;
border-radius: 12px 12px 0 0;
@include breakpoint('sm') {
border-radius: 12px;
max-width: 564px;
margin: 18px;
align-self: center;
}
}
</style>
28 changes: 28 additions & 0 deletions src/lib/components/modal/ModalTitle.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<script lang="ts">
import { getContext, type Snippet } from 'svelte'
import { ModalContext, type ModalContextData } from './context'
import { melt } from '@melt-ui/svelte'
interface Props {
children?: Snippet
}
const { children }: Props = $props()
const {
elements: { title }
} = getContext<ModalContextData>(ModalContext)
</script>

<div use:melt={$title} class="title">
{@render children?.()}
</div>

<style lang="scss">
.title {
font-size: 28px;
line-height: 120%;
font-weight: 500;
text-align: center;
}
</style>
4 changes: 4 additions & 0 deletions src/lib/components/modal/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { createDialog } from '@melt-ui/svelte'

export const ModalContext = Symbol('modal context')
export type ModalContextData = ReturnType<typeof createDialog>
7 changes: 7 additions & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,10 @@ export { default as SearchBar } from './components/search/SearchBar.svelte'

export { default as LoadingSpinner } from './components/LoadingSpinner.svelte'
export { default as Pagination } from './components/Pagination.svelte'

// Modal
export { default as Modal } from './components/modal/Modal.svelte'
export { default as ModalOverlay } from './components/modal/ModalOverlay.svelte'
export { default as ModalPopup } from './components/modal/ModalPopup.svelte'
export { default as ModalTitle } from './components/modal/ModalTitle.svelte'
export * from './components/modal/context.js'
Loading

0 comments on commit 5e719ac

Please sign in to comment.