Skip to content

Commit

Permalink
feat(ui): update toast styling and support dark theme
Browse files Browse the repository at this point in the history
Signed-off-by: Jordan Shatford <[email protected]>
  • Loading branch information
jordanshatford committed Sep 25, 2023
1 parent 459922b commit ffc7b47
Show file tree
Hide file tree
Showing 10 changed files with 144 additions and 188 deletions.
12 changes: 5 additions & 7 deletions apps/web/src/lib/stores/downloads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,9 @@ function createDownloadsStore() {
updateDownload(info.id, info);
try {
const result = await DownloadsService.postDownloads(info);
toast.info(`Added '${video.title}' to downloads.`);
updateDownload(result.id, result);
} catch (err) {
toast.error(`Failed to add '${video.title}' to downloads.`);
toast.error('Error', `Failed to add '${video.title}' to downloads.`);
console.error('Failed to add video to download ', err);
updateDownload(info.id, { status: { state: DownloadState.ERROR } });
}
Expand All @@ -66,10 +65,9 @@ function createDownloadsStore() {
updateDownload(info.id, info);
try {
const result = await DownloadsService.putDownloads(info);
toast.info(`Restarted download of '${info.title}'.`);
updateDownload(result.id, result);
} catch (err) {
toast.error(`Failed to restart '${info.title}' download.`);
toast.error('Error', `Failed to restart '${info.title}' download.`);
console.error('Failed to restart download ', err);
updateDownload(info.id, { status: { state: DownloadState.ERROR } });
}
Expand All @@ -80,13 +78,13 @@ function createDownloadsStore() {

try {
await DownloadsService.deleteDownload(id);
toast.success('Download removed successfully.');
toast.success('Deleted', 'Download removed successfully.');
update((state) => {
delete state[id];
return state;
});
} catch (err) {
toast.error('Failed to remove download.');
toast.error('Error', 'Failed to remove download.');
console.error('Failed to remove download ', err);
}
}
Expand All @@ -101,7 +99,7 @@ function createDownloadsStore() {
saveAs(blob, filename);
updateDownload(id, { awaitingFileBlob: false });
} catch (err) {
toast.error('Failed to get file for download.');
toast.error('Error', 'Failed to get file for download.');
console.error('Failed to get file for download ', err);
updateDownload(id, { awaitingFileBlob: false, status: { state: DownloadState.ERROR } });
}
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/lib/stores/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ function createSearchStore() {
let results: Video[] = [];
try {
results = await SearchService.getSearch(term);
toast.success(`Found ${results.length} search results.`);
toast.success('Success', `Found ${results.length} search results.`);
} catch (err) {
toast.error('Failed to get search results.');
toast.error('Error', 'Failed to get search results.');
console.error('Failed to search for videos ', err);
}
update((state) => {
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/lib/stores/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ function createSessionStore() {
const session = await SessionService.getSession();
set(session.id);
} catch (err) {
toast.error('Failed to get session. Reattempting shortly.');
toast.error('Error', 'Failed to get session. Re-attempting shortly.');
console.error('Connection failed, could not connect to internal server. ', err);
_reAttempt();
}
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/routes/settings/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,19 @@
<div class="mb-2 md:flex">
<div class="mt-4 w-full">
<Select
on:change={() => toast.success('Settings saved successfully.')}
on:change={() => toast.success('Updated', 'Quality settings updated successfully.')}
title="Quality:"
options={toSelectOptions(DownloadQuality)}
bind:value={$settings.quality}
/>
<Select
on:change={() => toast.success('Settings saved successfully.')}
on:change={() => toast.success('Updated', 'Embedding settings updated successfully.')}
title="Embed metadata:"
options={toSelectOptions({ YES: true, NO: false })}
bind:value={$settings.embed_metadata}
/>
<Select
on:change={() => toast.success('Settings saved successfully.')}
on:change={() => toast.success('Updated', 'Format settings updated successfully.')}
title="Format:"
options={toSelectOptions(AudioFormat)}
bind:value={$settings.format}
Expand Down
179 changes: 89 additions & 90 deletions packages/ui/src/lib/components/toast/Toast.svelte
Original file line number Diff line number Diff line change
@@ -1,108 +1,107 @@
<script lang="ts" context="module">
import {
ExclamationCircleIcon,
CheckCircleIcon,
ExclamationTriangleIcon,
LoaderIcon,
InformationCircleIcon,
type IconSource
} from '../../icons';
export const ICONS_MAP: { readonly [T in ToastType]: IconSource } = {
error: ExclamationCircleIcon,
warning: ExclamationTriangleIcon,
info: InformationCircleIcon,
success: CheckCircleIcon,
promise: LoaderIcon
};
import { tv, type VariantProps } from 'tailwind-variants';
const toastClasses = tv({
slots: {
outerDivClass: 'pointer-events-auto relative flex space-y-5',
outerContentDivClass:
'border-secondary-50 relative mx-auto min-w-[300px] max-w-[400px] rounded-xl border bg-white p-3 text-sm shadow-lg dark:border-zinc-700 dark:bg-zinc-900',
contentDivClass: 'flex space-x-4',
closeIconClass:
'absolute right-2 top-1 ml-auto h-8 w-8 text-zinc-500 hover:text-zinc-900 dark:text-zinc-100 dark:hover:text-zinc-500',
iconDivClass: 'flex h-10 w-10 items-center justify-center rounded-full',
iconClass: 'h-6 w-6',
textDivClass: 'flex-1',
titleClass: 'pr-6 font-medium text-zinc-900 dark:text-zinc-100',
descriptionClass: 'mt-1 text-zinc-500 dark:text-zinc-300'
},
variants: {
variant: {
error: {
iconDivClass: 'bg-red-100 text-red-700 dark:bg-red-700 dark:text-red-100'
},
warning: {
iconDivClass: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-700 dark:text-yellow-100'
},
info: {
iconDivClass: 'bg-blue-100 text-blue-700 dark:bg-blue-700 dark:text-blue-100'
},
success: {
iconDivClass: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-700 dark:text-emerald-100'
},
promise: {
iconDivClass: 'bg-blue-100 text-blue-700 dark:bg-blue-700 dark:text-blue-100'
}
}
},
defaultVariants: {
variant: 'info'
},
compoundSlots: [
{
slots: ['iconClass'],
variant: 'promise',
class: 'animate-spin'
}
]
});
export type ToastVariants = VariantProps<typeof toastClasses>;
</script>

<script lang="ts">
import type { ToastType } from './types';
import { toast as _toast, position } from './stores';
import type { ToastComponent } from './types';
import { XMarkIcon, Icon } from '../../icons';
import { toIcon } from '../../utilities';
import IconButton from '../IconButton.svelte';
export let toast: ToastComponent;
$: icon = toIcon(toast.type);
const {
outerDivClass,
outerContentDivClass,
contentDivClass,
closeIconClass,
iconDivClass,
iconClass,
textDivClass,
titleClass,
descriptionClass
} = toastClasses({
variant: toast.type
});
</script>

<div
id="yd-toast-{toast.id}"
class="yd-toast {toast.type}"
data-position={$position}
aria-live="polite"
role="status"
class={outerDivClass()}
>
<div class="yd-toast-bar" />
<div class="yd-toast-icon {toast.type}">
<Icon src={ICONS_MAP[toast.type]} class="h-5 w-5" />
</div>
<div class="yd-toast-message">
{toast.message}
<div class={outerContentDivClass()}>
{#if toast.closable && toast.type !== 'promise'}
<IconButton
src={XMarkIcon}
class={closeIconClass()}
on:click={() => _toast.remove(toast.id)}
/>
{/if}
<div class={contentDivClass()}>
{#if icon}
<div class={iconDivClass({ variant: toast.type })}>
<Icon src={icon} theme="solid" class={iconClass({ variant: toast.type })} />
</div>
{/if}
<div class={textDivClass()}>
<h4 class={titleClass()}>{toast.title}</h4>
<div class={descriptionClass()}>
{toast.description}
</div>
</div>
</div>
</div>
{#if toast.closable && toast.type !== 'promise'}
<button type="button" class="yd-toast-dismiss" on:click={() => _toast.remove(toast.id)}>
<Icon src={XMarkIcon} class="h-5 w-5" />
</button>
{/if}
</div>

<style>
.yd-toast {
background: var(--yd-toast-bg, #333);
color: var(--yd-toast-text, #fff);
padding: var(--yd-toast-padding, 12px 15px 12px 18px);
border-radius: var(--yd-toast-radius, 4px);
box-shadow: var(--yd-toast-shadow, 0 2px 7px rgba(0, 0, 0, 0.25));
font-size: var(--yd-toast-font-size, 14px);
position: relative;
overflow: hidden;
pointer-events: all;
display: flex;
gap: var(--yd-toast-dismiss-gap, 8px);
max-width: var(--yd-toast-max-width, unset);
}
.yd-toast-bar {
position: absolute;
top: 0;
left: 0;
width: var(--yd-toast-bar-width, 3px);
height: 100%;
background: var(--yd-toast-colour);
}
.yd-toast-icon {
min-width: var(--yd-toast-icon-size, 21px);
min-height: var(--yd-toast-icon-size, 21px);
max-width: var(--yd-toast-icon-size, 21px);
max-height: var(--yd-toast-icon-size, 21px);
color: var(--yd-toast-colour);
}
.yd-toast-icon.promise {
animation: promiseSpin 1s linear infinite;
}
.yd-toast-dismiss {
min-width: var(--yd-toast-icon-size, 21px);
min-height: var(--yd-toast-icon-size, 21px);
max-width: var(--yd-toast-icon-size, 21px);
max-height: var(--yd-toast-icon-size, 21px);
padding: var(--yd-toast-icon-padding, 2px);
}
.yd-toast.info {
--yd-toast-colour: var(--yd-toast-info-colour, #38bdf8);
}
.yd-toast.success {
--yd-toast-colour: var(--yd-toast-success-colour, #4ade80);
}
.yd-toast.warning {
--yd-toast-colour: var(--yd-toast-warning-colour, #fb923c);
}
.yd-toast.error {
--yd-toast-colour: var(--yd-toast-error-colour, #ef4444);
}
@keyframes promiseSpin {
from {
rotate: 0deg;
}
to {
rotate: 360deg;
}
}
</style>
59 changes: 5 additions & 54 deletions packages/ui/src/lib/components/toast/Toasts.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,13 @@
const ANIMATION = objectMerge(DEFAULT_ANIMATION, animation);
</script>

<div class="yd-toast-container" data-position={position}>
<div
class="pointer-events-none fixed top-0 z-[999] flex h-full w-full flex-col gap-y-1 overflow-hidden p-[16px] data-[position*=center]:left-2/4 data-[position*=left]:left-0 data-[position*=right]:right-0 data-[position*=center]:-translate-x-2/4 data-[position*=left]:items-start data-[position*=right]:items-end data-[position*=center]:items-center data-[position*=bottom]:justify-end"
data-position={position}
>
{#each $toasts as toast (toast.id)}
<div
class="yd-toast-wrapper"
class="data-[position=bottom-center]:origin-bottom data-[position=bottom-left]:origin-bottom-left data-[position=bottom-right]:origin-bottom-right data-[position=top-center]:origin-top data-[position=top-left]:origin-top-left data-[position=top-right]:origin-top-right"
data-position={position}
in:scale={{
start: ANIMATION.start,
Expand All @@ -37,55 +40,3 @@
</div>
{/each}
</div>

<style>
.yd-toast-container {
position: fixed;
padding: var(--yd-toast-offset, 16px);
top: 0;
height: 100%;
width: 100%;
pointer-events: none;
z-index: 999;
display: flex;
flex-direction: column;
gap: var(--yd-toast-gap, 16px);
overflow: hidden;
}
.yd-toast-container[data-position*='center'] {
align-items: center;
}
.yd-toast-container[data-position*='bottom'] {
justify-content: flex-end;
}
.yd-toast-container[data-position*='center'] {
left: 50%;
transform: translateX(-50%);
}
.yd-toast-container[data-position*='-left'] {
left: 0;
align-items: flex-start;
}
.yd-toast-container[data-position*='-right'] {
right: 0;
align-items: flex-end;
}
.yd-toast-wrapper[data-position='bottom-center'] {
transform-origin: bottom center;
}
.yd-toast-wrapper[data-position='bottom-left'] {
transform-origin: bottom left;
}
.yd-toast-wrapper[data-position='bottom-right'] {
transform-origin: bottom right;
}
.yd-toast-wrapper[data-position='top-center'] {
transform-origin: top center;
}
.yd-toast-wrapper[data-position='top-left'] {
transform-origin: top left;
}
.yd-toast-wrapper[data-position='top-right'] {
transform-origin: top right;
}
</style>
2 changes: 1 addition & 1 deletion packages/ui/src/lib/components/toast/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { default as Toast } from './Toast.svelte';
export { default as Toast, type ToastVariants } from './Toast.svelte';
export { default as Toasts } from './Toasts.svelte';
export { toast } from './stores';
Loading

1 comment on commit ffc7b47

@vercel
Copy link

@vercel vercel bot commented on ffc7b47 Sep 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.