feat: add <Banner /> component

This commit is contained in:
madkarmaa 2025-05-07 23:07:55 +02:00
parent bee1c8ccfe
commit f3c3660159
No known key found for this signature in database
GPG Key ID: BF5E2EF8F188606D
4 changed files with 205 additions and 17 deletions

View File

@ -2,27 +2,44 @@
:root { :root {
--font-family: 'Manrope', sans-serif; --font-family: 'Manrope', sans-serif;
--text-one: hsl(206, 100%, 94%); --text-one: hsl(206, 100%, 94%);
--surface-one: hsl(206, 100%, 94%); --surface-one: hsl(206, 100%, 94%);
--primary: hsl(206, 100%, 81%); --primary: hsl(206, 100%, 81%);
--secondary: hsl(208, 75%, 82%); --secondary: hsl(208, 75%, 82%);
--tertiary: hsla(205, 91%, 69%, 0.15); --tertiary: hsla(205, 91%, 69%, 0.15);
--background-one: hsl(252, 10%, 11%); --background-one: hsl(252, 10%, 11%);
--surface-two: hsl(252, 10%, 11%); --surface-two: hsl(252, 10%, 11%);
--surface-three: hsl(210, 14%, 17%); --surface-three: hsl(210, 14%, 17%);
--surface-four: hsl(212, 19%, 19%); --surface-four: hsl(212, 19%, 19%);
--text-two: hsl(212, 19%, 19%); --text-two: hsl(212, 19%, 19%);
--border: hsl(221, 17%, 26%); --border: hsl(221, 17%, 26%);
--surface-five: hsl(221, 17%, 26%); --surface-five: hsl(221, 17%, 26%);
--text-three: hsl(226, 48%, 18%); --text-three: hsl(226, 48%, 18%);
--text-four: hsl(208, 30%, 75%); --text-four: hsl(208, 30%, 75%);
--surface-six: hsl(208, 30%, 75%); --surface-six: hsl(208, 30%, 75%);
--surface-seven: hsl(230, 9%, 13%);
--surface-eight: hsl(240, 9%, 13.5%);
--surface-nine: hsl(230, 9.5%, 17.5%);
--red-one: hsl(333, 84%, 62%);
--bezier-one: cubic-bezier(0.25, 0.46, 0.45, 0.94); --surface-seven: hsl(230, 9%, 13%);
--surface-eight: hsl(240, 9%, 13.5%);
--surface-nine: hsl(230, 9.5%, 17.5%);
--red-one: hsl(333, 84%, 62%);
--red-two: hsl(357, 74%, 60%);
--yellow-one: hsl(59, 100%, 72%);
--bezier-one: ease-out;
--drop-shadow-one: 0px 4px 5px 0px rgba(0, 0, 0, 0.14), 0px 1px 10px 0px rgba(0, 0, 0, 0.12), --drop-shadow-one: 0px 4px 5px 0px rgba(0, 0, 0, 0.14), 0px 1px 10px 0px rgba(0, 0, 0, 0.12),
0px 2px 4px -1px rgba(0, 0, 0, 0.2); 0px 2px 4px -1px rgba(0, 0, 0, 0.2);
} }

View File

@ -9,6 +9,7 @@
type: ButtonType; type: ButtonType;
icon?: typeof import('~icons/mdi').default; icon?: typeof import('~icons/mdi').default;
iconColor?: string; iconColor?: string;
color?: string;
label?: string; label?: string;
children?: Snippet; children?: Snippet;
}; };
@ -40,6 +41,7 @@
label = '', label = '',
onclick = () => {}, onclick = () => {},
iconColor = 'currentColor', iconColor = 'currentColor',
color = 'currentColor',
target = '_self' target = '_self'
}: Props = $props(); }: Props = $props();
@ -47,6 +49,14 @@
const navBarButtonSelected = type === 'navbar' && href && new URL(href, page.url.href).pathname === page.url.pathname; const navBarButtonSelected = type === 'navbar' && href && new URL(href, page.url.href).pathname === page.url.pathname;
</script> </script>
<!-- reusable snippet to remove duplicate code -->
{#snippet content()}
{#if Icon}
<Icon color={iconColor} />
{/if}
<span class="content" style="color: {color};">{@render children?.()}</span>
{/snippet}
{#if href} {#if href}
<a <a
{href} {href}
@ -54,21 +64,11 @@
class={`button-${type}${navBarButtonSelected ? ' selected' : ''}`} class={`button-${type}${navBarButtonSelected ? ' selected' : ''}`}
aria-label={label} aria-label={label}
> >
{#if Icon} {@render content()}
<Icon color={iconColor} />
{/if}
{#if type === 'navbar'}
<span>{@render children?.()}</span>
{:else}
{@render children?.()}
{/if}
</a> </a>
{:else} {:else}
<button {onclick} class={`button-${type}`} aria-label={label}> <button {onclick} class={`button-${type}`} aria-label={label}>
{#if Icon} {@render content()}
<Icon color={iconColor} />
{/if}
{@render children?.()}
</button> </button>
{/if} {/if}
@ -114,6 +114,11 @@
letter-spacing: 0.01rem; letter-spacing: 0.01rem;
} }
.button-text:hover .content {
text-decoration: underline;
text-decoration-color: currentColor;
}
button:not(.button-navbar):hover, button:not(.button-navbar):hover,
a:not(.button-navbar):hover { a:not(.button-navbar):hover {
filter: brightness(85%); filter: brightness(85%);

View File

@ -0,0 +1,148 @@
<script lang="ts">
import Info from '~icons/mdi/information-outline';
import Warning from '~icons/mdi/alert-outline';
import Caution from '~icons/mdi/alert-circle-outline';
import Close from '~icons/mdi/close';
import { slide } from 'svelte/transition';
import type { Snippet } from 'svelte';
import Button from '$components/atoms/Button.svelte';
type Props = {
children: Snippet;
level: 'info' | 'warning' | 'caution';
permanent?: boolean;
onDismiss?: () => void;
};
let { children, level = 'info', permanent = false, onDismiss }: Props = $props();
const icons = { info: Info, warning: Warning, caution: Caution };
const Icon = icons[level];
const dismissButtonColor = level === 'info' ? 'var(--text-one)' : '#000';
let closed = $state(!permanent);
if (!permanent)
$effect(() => {
setTimeout(() => {
closed = false;
}, 1); // trigger the in transition
});
const dismissBanner = () => {
closed = true;
onDismiss?.();
};
</script>
{#if permanent}
<div class="banner-container permanent">
<div class="banner {level}">
<div class="banner-text">
<Icon width={24} height={24} />
{@render children()}
</div>
</div>
</div>
{:else if !closed}
<div class="banner-container" transition:slide={{ duration: 300, axis: 'y' }}>
<div class="banner {level}">
<div class="banner-text">
<Icon width={32} height={32} />
{@render children()}
</div>
<Button
type="text"
icon={Close}
onclick={dismissBanner}
iconColor={dismissButtonColor}
color={dismissButtonColor}
>
Dismiss
</Button>
</div>
</div>
{/if}
<style>
.banner-container,
.banner-container *,
.banner-container :global(*) {
transition: none;
}
.banner-text :global(a) {
color: inherit;
text-decoration: none;
font-weight: 700;
}
.banner-text :global(a:hover) {
text-decoration: underline;
}
.banner-container {
display: flex;
justify-content: center;
width: 100%;
/* optional: to prevent content spill during transition */
}
.permanent {
font-size: 0.87rem;
}
.banner {
margin: 0;
padding: 1.5rem 1.7rem;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1.3rem;
margin: 0.7rem 1rem;
border-radius: 1rem;
}
.permanent > .banner {
padding: 0.5rem 0.7rem;
margin: 0;
border-radius: 0;
width: 100%;
}
.banner-text {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
gap: 0.55rem;
word-wrap: break-word;
}
.banner.info {
background-color: var(--surface-four);
color: var(--text-one);
}
.banner.warning {
background-color: var(--yellow-one);
color: #000;
}
.banner.caution {
background-color: var(--red-two);
color: #000;
}
@media screen and (max-width: 767px) {
.banner {
flex-direction: column;
padding: 1.1rem 1.3rem;
}
.banner > :global(button) {
align-self: flex-end;
}
}
</style>

View File

@ -0,0 +1,18 @@
<script lang="ts">
import Banner from '$components/organisms/Banner.svelte';
import Button from '$components/atoms/Button.svelte';
import api from '$api';
const statusUrl = 'https://status.revanced.app/'; // TODO: replace with env variable
</script>
{#await api.general.ping() then apiUp}
{#if !apiUp}
<Banner level="caution">
The API is currently unresponsive and some services may not work correctly.
{#if statusUrl}
Check the <Button type="text" href={statusUrl}>status page</Button> for updates.
{/if}
</Banner>
{/if}
{/await}