Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/lib/components/header/header.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@

.search-bar {
position: fixed;
top: 0.5rem;
top: calc(0.5rem + var(--incident-banner-offset, 0px));
left: 50%;
transform: translateX(-50%);
width: 100%;
Expand Down
169 changes: 169 additions & 0 deletions src/lib/components/incident-banner/incident-banner.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
<script lang="ts">
import { onMount } from 'svelte';
import { z } from 'zod';
import ExclamationCircle from '$lib/components/icons/ExclamationCircle.svelte';
import CrossSmall from '$lib/components/icons/CrossSmall.svelte';
import dismissablesStore from '$lib/stores/dismissables/dismissables.store';

const STATUS_URL = 'https://status.drips.network/status.json';

const incidentSchema = z.object({
id: z.string().nullable().optional(),
title: z.string(),
severity: z.string().optional(),
status: z.string().optional(),
startedAt: z.string().optional(),
});

const statusSchema = z.object({
incidents: z
.object({
active: z.array(incidentSchema).optional(),
})
.optional(),
});

type Incident = z.infer<typeof incidentSchema>;

let active = $state<Incident[]>([]);

// A stable per-incident key so dismissing one incident doesn't dismiss future
// ones. Falls back to title + start time when the incident has no id.
function dismissId(incident: Incident): string {
return `incident-banner-${incident.id ?? `${incident.title}-${incident.startedAt ?? ''}`}`;
}

// Show a single banner at a time so its height stays predictable (sub-app
// headers offset themselves by a fixed --incident-banner-height).
let current = $derived(
active.filter((i) => !$dismissablesStore.includes(dismissId(i)))[0] ?? null,
);

// Toggle a root class so headers/content can offset below the fixed banner.
$effect(() => {
document.documentElement.classList.toggle('incident-banner-visible', Boolean(current));
return () => document.documentElement.classList.remove('incident-banner-visible');
});

function dismiss(incident: Incident) {
dismissablesStore.dismiss(dismissId(incident));
}

onMount(async () => {
try {
const res = await fetch(STATUS_URL, { cache: 'no-store' });
if (!res.ok) return;
const parsed = statusSchema.safeParse(await res.json());
if (!parsed.success) return;
active = parsed.data.incidents?.active ?? [];
} catch {
// Status page unreachable — fail silent; a banner is best-effort.
}
});
</script>

{#if current}
{@const incident = current}
<div class="incident-banner typo-text-small" role="alert">
<div class="content">
<div class="message">
<ExclamationCircle style="height: 1rem; width: 1rem; fill: currentColor; flex-shrink: 0;" />
<span class="title">{incident.title}</span>
</div>
<a
class="status-link"
href="https://status.drips.network"
target="_blank"
rel="noopener noreferrer"
>
View status
</a>
</div>
<button class="dismiss" aria-label="Dismiss" onclick={() => dismiss(incident)}>
<CrossSmall style="height: 1rem; width: 1rem; fill: currentColor;" />
</button>
</div>
{/if}

<style>
.incident-banner {
position: fixed;
top: 0;
left: 0;
right: 0;
height: var(--incident-banner-height);
z-index: 100;
background-color: var(--color-negative-level-1);
color: var(--color-negative-level-6);
padding: 0 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
overflow: hidden;
view-transition-name: incident-banner;
}

/* Fills the space left of the dismiss button and centres its children. */
.content {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
}

/* Icon + title; the title is the only part allowed to shrink/ellipsise. */
.message {
display: flex;
align-items: center;
gap: 0.5rem;
min-width: 0;
max-width: 100%;
}

.title {
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

/* Never shrinks or wraps, so "View status" is never cut off. */
.status-link {
flex-shrink: 0;
white-space: nowrap;
color: inherit;
font-weight: bold;
text-decoration: underline;
}

.status-link:hover {
opacity: 0.8;
}

.dismiss {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 0.25rem;
border: none;
background: none;
color: inherit;
cursor: pointer;
border-radius: 0.25rem;
}

.dismiss:hover {
background-color: var(--color-negative-level-2);
}

/* Two lines on mobile: message on top, "View status" below. */
@media (max-width: 768px) {
.content {
flex-direction: column;
gap: 0.125rem;
}
}
</style>
2 changes: 1 addition & 1 deletion src/lib/components/lp-header/lp-header.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@
box-shadow 0.3s,
border-radius 0.3s;
overflow: hidden;
top: 1rem;
top: calc(1rem + var(--incident-banner-offset, 0px));
view-transition-name: header;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,17 @@
sortBy = { key: preset.key, direction: preset.direction };
}

const STICKY_TOP_PX = 6.5 * 16; // 6.5rem in px

let wrapperEl = $state<HTMLDivElement>();
let isSticky = $state(false);

function checkSticky() {
if (!wrapperEl) return;
// Read the resolved sticky `top` (CSS: calc(6.5rem + --incident-banner-offset))
// rather than hardcoding 6.5rem, so the threshold stays correct when the
// global incident banner pushes everything down.
const stickyTopPx = parseFloat(getComputedStyle(wrapperEl).top) || 0;
const rect = wrapperEl.getBoundingClientRect();
isSticky = rect.top <= STICKY_TOP_PX + 1;
isSticky = rect.top <= stickyTopPx + 1;
}

$effect(() => {
Expand Down Expand Up @@ -123,7 +125,7 @@
<style>
.sticky-wrapper {
position: sticky;
top: 6.5rem;
top: calc(6.5rem + var(--incident-banner-offset, 0px));
z-index: 1;
}

Expand Down
4 changes: 2 additions & 2 deletions src/lib/components/wave/issues-page/issues-page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -493,9 +493,9 @@
flex: 1;
display: flex;
flex-direction: column;
height: calc(100dvh - 5.5rem);
height: calc(100dvh - 5.5rem - var(--incident-banner-offset, 0px));
position: sticky;
top: 4.5rem;
top: calc(4.5rem + var(--incident-banner-offset, 0px));
}

.issue-list-configuration {
Expand Down
6 changes: 3 additions & 3 deletions src/lib/components/wave/issues-page/single-issue-page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -850,9 +850,9 @@

.sidebar-wrapper {
position: sticky;
top: 6.5rem;
top: calc(6.5rem + var(--incident-banner-offset, 0px));
align-self: start;
max-height: calc(100dvh - 5.5rem);
max-height: calc(100dvh - 5.5rem - var(--incident-banner-offset, 0px));
--fade-size: 1.5rem;
mask-image: linear-gradient(
to bottom,
Expand Down Expand Up @@ -883,7 +883,7 @@

.sidebar {
overflow-y: auto;
max-height: calc(100dvh - 7.5rem);
max-height: calc(100dvh - 7.5rem - var(--incident-banner-offset, 0px));
display: flex;
flex-direction: column;
gap: 1rem;
Expand Down
4 changes: 2 additions & 2 deletions src/routes/(pages)/(lp)/blog/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
width: 100vw;
margin: 0 auto;
padding: 1rem;
padding-top: 6rem;
padding-top: calc(6rem + var(--incident-banner-offset, 0px));
display: flex;
flex-direction: column;
align-items: center;
Expand All @@ -34,7 +34,7 @@

@media (max-width: 577px) {
.wrapper {
padding-top: 5rem;
padding-top: calc(5rem + var(--incident-banner-offset, 0px));
}
}
</style>
2 changes: 1 addition & 1 deletion src/routes/(pages)/(lp)/legal/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
width: 100vw;
margin: 0 auto;
padding: 1rem;
padding-top: 6rem;
padding-top: calc(6rem + var(--incident-banner-offset, 0px));
display: flex;
flex-direction: column;
align-items: center;
Expand Down
2 changes: 1 addition & 1 deletion src/routes/(pages)/(lp)/solutions/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<style>
.wrapper {
background-color: var(--color-foreground-level-1);
padding-top: 6rem;
padding-top: calc(6rem + var(--incident-banner-offset, 0px));
padding-bottom: 2rem;
width: 100vw;
min-height: 100svh;
Expand Down
2 changes: 2 additions & 0 deletions src/routes/(pages)/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import '$lib/stores/theme/theme.store';
import animationsStore from '$lib/stores/animations/animations.store';
import IncidentBanner from '$lib/components/incident-banner/incident-banner.svelte';

import { onMount } from 'svelte';
import { get } from 'svelte/store';
Expand Down Expand Up @@ -49,6 +50,7 @@
</script>

<div class="main" data-uifont="inter">
<IncidentBanner />
<main class="page">
{@render children?.()}
</main>
Expand Down
12 changes: 7 additions & 5 deletions src/routes/(pages)/app/(app)/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -165,25 +165,26 @@

.header {
position: fixed;
top: 0;
top: var(--incident-banner-offset, 0px);
left: 0;
width: 100%;
z-index: 1;
}

.has-read-only-banner .sidenav {
padding-top: 8rem;
padding-top: calc(8rem + var(--incident-banner-offset, 0px));
}

.has-read-only-banner .page {
padding-top: 8rem;
padding-top: calc(8rem + var(--incident-banner-offset, 0px));
}

.page {
position: relative;
min-height: 100vh;
width: 100vw;
padding: 6rem 2.5rem 1rem 2.5rem;
padding-top: calc(6rem + var(--incident-banner-offset, 0px));
margin: 0 auto 0 16rem;
}

Expand Down Expand Up @@ -214,7 +215,7 @@
height: 100%;
width: 16rem;
padding: 1rem;
padding-top: 6rem;
padding-top: calc(6rem + var(--incident-banner-offset, 0px));
z-index: 2;
}

Expand Down Expand Up @@ -259,12 +260,13 @@
.page,
.sidenav-forced-collapsed .page {
padding: 6rem 1rem 6rem 1rem;
padding-top: calc(6rem + var(--incident-banner-offset, 0px));
margin-left: 0;
}

.has-read-only-banner .page,
.has-read-only-banner.sidenav-forced-collapsed .page {
padding-top: 8rem;
padding-top: calc(8rem + var(--incident-banner-offset, 0px));
}

.bottom-nav {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,10 @@
}

.posts-grid.compact {
grid-template-columns: repeat(auto-fill, minmax(24rem, 1fr));
/* min(24rem, 100%) so a column never forces a track wider than its
container — otherwise narrow viewports overflow horizontally (the
max-width: 767px rule below can't override this due to specificity). */
grid-template-columns: repeat(auto-fill, minmax(min(24rem, 100%), 1fr));
}

@media (max-width: 767px) {
Expand Down
Loading
Loading