Skip to content

Commit

Permalink
feat: modal component
Browse files Browse the repository at this point in the history
  • Loading branch information
jrasm91 committed Jan 8, 2025
1 parent 5557982 commit 6c89aba
Show file tree
Hide file tree
Showing 31 changed files with 383 additions and 310 deletions.
2 changes: 1 addition & 1 deletion src/docs/components/ComponentExampleCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
</HStack>
</div>
</CardHeader>
<CardBody class={viewMode === 'code' ? 'p-0 pt-4' : ''}>
<CardBody class={viewMode === 'code' ? 'p-0' : ''}>
{#if viewMode === 'preview'}
<Component />
{:else}
Expand Down
14 changes: 14 additions & 0 deletions src/docs/components/ComponentNoteCard.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script lang="ts">
import { Alert } from '@immich/ui';
import type { Snippet } from 'svelte';
type Props = {
children: Snippet;
};
const { children }: Props = $props();
</script>

<Alert color="info" class="mt-4" title="Note">
{@render children()}
</Alert>
2 changes: 1 addition & 1 deletion src/docs/components/DecorativeBlock.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import type { Color, Size } from '$lib/types.js';
import type { Color, Size } from '@immich/ui';
import { cleanClass } from '$lib/utils.js';
import { tv } from 'tailwind-variants';
Expand Down
2 changes: 2 additions & 0 deletions src/docs/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
mdiPartyPopper,
mdiToggleSwitch,
mdiViewSequential,
mdiWindowMaximize,
} from '@mdi/js';
import type { Component } from 'svelte';

Expand All @@ -49,6 +50,7 @@ export const componentGroups = [
{ name: 'AppShell', icon: mdiApplicationOutline },
{ name: 'Card', icon: mdiCardOutline },
{ name: 'Navbar', icon: mdiMenu },
{ name: 'Modal', icon: mdiWindowMaximize },
{ name: 'Scrollable', icon: mdiPanVertical },
{ name: 'Stack', icon: mdiViewSequential },
],
Expand Down
16 changes: 5 additions & 11 deletions src/lib/common/use-child.svelte.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,17 @@
import { ChildKey } from '$lib/constants.js';
import type { ChildData } from '$lib/types.js';
import { withPrefix } from '$lib/utils.js';
import { setContext, type Snippet } from 'svelte';
import { setContext } from 'svelte';
import { SvelteMap } from 'svelte/reactivity';

export const withChildrenSnippets = (key: ChildKey) => {
const map = $state(new SvelteMap<ChildKey, Snippet>());
const map = new SvelteMap<ChildKey, () => ChildData>();

setContext(withPrefix(key), {
register: async (child: ChildKey, snippet: Snippet) => {
if (map.has(child)) {
console.warn(`Snippet with key ${child} already exists in the context`);
return;
}

map.set(child, snippet);
},
register: (child: ChildKey, data: () => ChildData) => map.set(child, data),
});

return {
getChildren: (key: ChildKey) => map.get(key),
getChildren: (key: ChildKey) => map.get(key)?.(),
};
};
12 changes: 6 additions & 6 deletions src/lib/components/Alert/Alert.svelte
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<script lang="ts">
import Card from '$lib/components/Card/Card.svelte';
import CardHeader from '$lib/components/Card/CardHeader.svelte';
import CardBody from '$lib/components/Card/CardBody.svelte';
import CloseButton from '$lib/components/CloseButton/CloseButton.svelte';
import Icon from '$lib/components/Icon/Icon.svelte';
import Text from '$lib/components/Text/Text.svelte';
import CloseButton from '$lib/components/CloseButton/CloseButton.svelte';
import type { Color, Size } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import {
Expand Down Expand Up @@ -78,9 +78,9 @@
</script>

{#if open}
<Card {color} variant="subtle" class={cleanClass(className)}>
<CardHeader>
<div class="flex justify-between">
<Card {color} class={cleanClass(className)}>
<CardBody>
<div class="flex items-center justify-between">
<div class={cleanClass('flex gap-2')}>
{#if icon}
<div>
Expand All @@ -103,6 +103,6 @@
</div>
{/if}
</div>
</CardHeader>
</CardBody>
</Card>
{/if}
4 changes: 2 additions & 2 deletions src/lib/components/AppShell/AppShell.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@
<div class={cleanClass('flex h-screen flex-col overflow-hidden', className)}>
{#if header}
<header class="border-b">
{@render header?.()}
{@render header?.snippet()}
</header>
{/if}
<div class="flex w-full grow overflow-y-auto">
{#if sidebar}
{@render sidebar()}
{@render sidebar?.snippet()}
{/if}
<Scrollable class="grow">
{@render children?.()}
Expand Down
191 changes: 83 additions & 108 deletions src/lib/components/Card/Card.svelte
Original file line number Diff line number Diff line change
@@ -1,48 +1,52 @@
<script lang="ts">
import { withChildrenSnippets } from '$lib/common/use-child.svelte.js';
import IconButton from '$lib/components/IconButton/IconButton.svelte';
import Scrollable from '$lib/components/Scrollable/Scrollable.svelte';
import { ChildKey } from '$lib/constants.js';
import type { Color, Shape } from '$lib/types.js';
import type { Color } from '$lib/types.js';
import { cleanClass } from '$lib/utils.js';
import { mdiChevronDown } from '@mdi/js';
import { type Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
import { twMerge } from 'tailwind-merge';
import { tv } from 'tailwind-variants';
type Props = HTMLAttributes<HTMLDivElement> & {
color?: Color;
shape?: Shape;
variant?: 'filled' | 'outline' | 'subtle';
shape?: 'round' | 'rectangle';
expanded?: boolean;
expandable?: boolean;
children: Snippet;
};
let {
color = 'secondary',
color,
class: className,
shape = 'round',
expanded = $bindable(true),
expandable = false,
variant,
children,
...restProps
}: Props = $props();
const cardStyles = tv({
base: 'flex flex-col rounded-2xl bg-light text-dark shadow-sm w-full overflow-hidden',
const containerStyles = tv({
base: 'w-full overflow-hidden bg-light text-dark shadow-sm',
variants: {
defaultStyle: {
shape: {
rectangle: '',
round: 'rounded-2xl',
},
border: {
true: 'border',
false: '',
default: '',
},
outlineColor: {
primary: 'border border-primary',
secondary: 'border',
success: 'border border-success',
danger: 'border border-danger',
warning: 'border border-warning',
info: 'border border-info',
},
subtleColor: {
},
});
const cardStyles = tv({
base: 'flex flex-col w-full h-full',
variants: {
color: {
primary: 'bg-primary/25 dark:bg-primary/25',
secondary: 'bg-dark/5 dark:bg-dark/25 text-dark',
success: 'bg-success/15 dark:bg-success/30',
Expand All @@ -54,132 +58,103 @@
});
const headerContainerStyles = tv({
base: 'p-4',
variants: {
bottomPadding: {
padding: {
true: '',
false: 'pb-0',
},
filledColor: {
primary: 'bg-primary text-light rounded-t-xl',
secondary: 'bg-dark text-light rounded-t-xl',
success: 'bg-success text-light rounded-t-xl',
danger: 'bg-danger text-light rounded-t-xl',
warning: 'bg-warning text-black rounded-t-xl',
info: 'bg-info text-light rounded-t-xl',
},
outlineColor: {
primary: 'text-primary',
secondary: 'text-dark',
success: 'text-success',
danger: 'text-danger',
warning: 'text-warning',
info: 'text-info',
},
},
});
const iconStyles = tv({
variants: {
filledColor: {
primary: 'text-light',
secondary: 'text-light',
success: 'text-light',
danger: 'text-light',
warning: 'text-dark',
info: 'text-light',
},
outlineColor: {
primary: 'text-primary',
secondary: 'text-dark',
success: 'text-success',
danger: 'text-danger',
warning: 'text-warning',
info: 'text-info',
border: {
true: 'border-b',
false: '',
},
},
});
let expanded = $state(!expandable);
const onToggle = () => {
expanded = !expanded;
};
const { getChildren: getChildSnippet } = withChildrenSnippets(ChildKey.Card);
const headerChildren = $derived(getChildSnippet(ChildKey.CardHeader));
const bodyChildren = $derived(getChildSnippet(ChildKey.CardBody));
const footerChildren = $derived(getChildSnippet(ChildKey.CardFooter));
const headerChild = $derived(getChildSnippet(ChildKey.CardHeader));
const bodyChild = $derived(getChildSnippet(ChildKey.CardBody));
const footerChild = $derived(getChildSnippet(ChildKey.CardFooter));
const headerBorder = $derived(!color);
const headerPadding = $derived(headerBorder || !expanded);
const headerClasses = 'flex flex-col space-y-1.5';
const headerContainerClasses = $derived(
cleanClass(
headerContainerStyles({
bottomPadding: variant === 'filled' || !bodyChildren,
outlineColor: variant === 'outline' ? color : undefined,
filledColor: variant === 'filled' ? color : undefined,
}),
twMerge(
cleanClass(
headerContainerStyles({
padding: headerPadding,
border: headerBorder,
}),
headerChild?.class,
),
),
);
</script>

{#snippet header()}
{#if expandable}
<button type="button" onclick={onToggle} class="w-full">
<div class={cleanClass(headerContainerClasses, 'flex items-center justify-between px-4')}>
<div class={cleanClass(headerClasses, 'py-4')}>
{@render headerChildren?.()}
</div>
<div>
<IconButton
class={iconStyles({
filledColor: variant === 'filled' ? color : undefined,
outlineColor: variant === 'outline' ? color : undefined,
}) as Color}
icon={mdiChevronDown}
flopped={expanded}
variant="ghost"
shape="round"
size="large"
/>
</div>
<button
type="button"
onclick={onToggle}
class={cleanClass('flex w-full items-center justify-between px-4', headerContainerClasses)}
>
<div class="flex flex-col space-y-1.5">
{@render headerChild?.snippet()}
</div>
<div>
<IconButton
color="secondary"
icon={mdiChevronDown}
flopped={expanded}
variant="ghost"
shape="round"
size="large"
/>
</div>
</button>
{:else}
<div class={cleanClass(headerClasses, headerContainerClasses, 'p-4')}>
{@render headerChildren?.()}
<div class={cleanClass('flex flex-col space-y-1.5', headerContainerClasses)}>
{@render headerChild?.snippet()}
</div>
{/if}
{/snippet}

{#snippet body()}
{@render bodyChildren?.()}
{/snippet}

{#snippet footer()}
{@render footerChildren?.()}
{/snippet}

<div
class={cleanClass(
cardStyles({
defaultStyle: variant === undefined,
outlineColor: variant === 'outline' || variant === 'filled' ? color : undefined,
subtleColor: variant === 'subtle' ? color : undefined,
containerStyles({
shape,
border: !color,
}),
className,
)}
{...restProps}
>
{#if headerChildren}
{@render header()}
{/if}
<div class={cleanClass(cardStyles({ color }))}>
{#if headerChild}
{@render header()}
{/if}

{#if bodyChildren && expanded}
{@render body()}
{/if}
{#if bodyChild && expanded}
<Scrollable class={twMerge(cleanClass('p-4', bodyChild?.class))}>
{@render bodyChild?.snippet()}
</Scrollable>
{/if}

{#if footerChildren}
{@render footer()}
{/if}
{#if footerChild}
<div
class={twMerge(
cleanClass('flex items-center border-t border-t-subtle p-4', footerChild.class),
)}
>
{@render footerChild.snippet()}
</div>
{/if}

{@render children()}
{@render children()}
</div>
</div>
Loading

0 comments on commit 6c89aba

Please sign in to comment.