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
33 changes: 33 additions & 0 deletions public/announcement-bar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const bars = document.querySelectorAll("[data-announcement-bar]");

for (const bar of bars) {
const storageKey = bar.dataset.storageKey;
let dismissed = false;

if (storageKey) {
try {
dismissed = localStorage.getItem(storageKey) === "dismissed";
} catch {
// Storage can be unavailable; dismissal still works for this page view.
}
}

if (dismissed) continue;

bar.hidden = false;
const dismissButton = bar.querySelector("[data-announcement-dismiss]");
dismissButton?.addEventListener(
"click",
() => {
bar.hidden = true;
if (!storageKey) return;

try {
localStorage.setItem(storageKey, "dismissed");
} catch {
// The bar remains dismissed for this page view when storage is blocked.
}
},
{ once: true },
);
}
205 changes: 205 additions & 0 deletions src/components/AnnouncementBar.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
---
interface Props {
storageKey: string;
message: string;
href: string;
linkLabel: string;
tone?: "ember" | "rust" | "mint" | "copper" | "electric" | "gold" | "cyan" | "amber" | "lime";
}

const {
storageKey,
message,
href,
linkLabel,
tone = "ember",
} = Astro.props;
const external = /^https?:\/\//.test(href);
---

<aside
class:list={["announcement-bar", `announcement-bar--${tone}`]}
data-announcement-bar
data-storage-key={storageKey}
aria-label="Announcement"
hidden
>
<div class="announcement-bar__inner">
<p>
<span class="announcement-bar__label">Now building</span>
<span>{message}</span>
<a href={href} target={external ? "_blank" : undefined} rel={external ? "noopener noreferrer" : undefined}>
{linkLabel} <span aria-hidden="true">↗</span>
</a>
</p>
<button type="button" data-announcement-dismiss aria-label="Dismiss announcement">
<svg viewBox="0 0 20 20" aria-hidden="true">
<path d="m5 5 10 10M15 5 5 15"></path>
</svg>
</button>
</div>
</aside>

<script is:inline type="module" src="/announcement-bar.js"></script>

<style>
.announcement-bar {
--announcement-bg: #0b1020;
--announcement-border: #f6ad5538;
--announcement-accent: #f6ad55;
position: relative;
z-index: 400;
border-bottom: 1px solid var(--announcement-border);
background: var(--announcement-bg);
color: #f8fafc;
font: 600 0.875rem/1.45 Inter, ui-sans-serif, system-ui, sans-serif;
}

.announcement-bar[hidden] {
display: none;
}

.announcement-bar--rust {
--announcement-bg: #211411;
--announcement-border: #fb92704a;
--announcement-accent: #ffad92;
}

.announcement-bar--mint {
--announcement-bg: #071c1a;
--announcement-border: #5eead44a;
--announcement-accent: #7cebd8;
}

.announcement-bar--copper {
--announcement-bg: #17110d;
--announcement-border: #f59e5b4a;
--announcement-accent: #f5a66b;
}

.announcement-bar--electric {
--announcement-bg: #080b16;
--announcement-border: #7dd3fc4a;
--announcement-accent: #8fd3f4;
}

.announcement-bar--gold {
--announcement-bg: #15140d;
--announcement-border: #d9bd674f;
--announcement-accent: #e8ce78;
}

.announcement-bar--cyan {
--announcement-bg: #071324;
--announcement-border: #22d3ee4a;
--announcement-accent: #67e8f9;
}

.announcement-bar--amber {
--announcement-bg: #1c1208;
--announcement-border: #fb923c4a;
--announcement-accent: #fdba74;
}

.announcement-bar--lime {
--announcement-bg: #102019;
--announcement-border: #86efac4a;
--announcement-accent: #86efac;
}

.announcement-bar__inner {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 0.75rem;
width: min(1180px, calc(100% - 2rem));
min-height: 2.75rem;
margin: 0 auto;
padding: 0.35rem 0;
}

p {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 0.3rem 0.65rem;
margin: 0;
text-align: center;
text-wrap: balance;
}

.announcement-bar__label {
border: 1px solid color-mix(in srgb, var(--announcement-accent) 58%, transparent);
border-radius: 999px;
padding: 0.12rem 0.48rem;
color: var(--announcement-accent);
font: 800 0.68rem/1.25 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
letter-spacing: 0.08em;
text-transform: uppercase;
}

a {
color: var(--announcement-accent);
font-weight: 800;
text-decoration-thickness: 1px;
text-underline-offset: 0.2em;
}

a:hover {
text-decoration-thickness: 2px;
}

button {
display: inline-grid;
width: 2rem;
height: 2rem;
place-items: center;
border: 0;
border-radius: 999px;
background: transparent;
color: #cbd5e1;
cursor: pointer;
}

button:hover {
background: #ffffff14;
color: #ffffff;
}

button:focus-visible {
outline: 2px solid var(--announcement-accent);
outline-offset: 2px;
}

svg {
width: 1.125rem;
height: 1.125rem;
fill: none;
stroke: currentColor;
stroke-linecap: round;
stroke-width: 1.75;
}

@media (max-width: 640px) {
.announcement-bar__inner {
width: min(100% - 1.25rem, 1180px);
align-items: start;
padding-block: 0.55rem;
}

p {
justify-content: flex-start;
text-align: left;
}
}

@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
scroll-behavior: auto !important;
transition-duration: 0.01ms !important;
}
}
</style>
9 changes: 9 additions & 0 deletions src/layouts/Layout.astro
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
---
import AnnouncementBar from '../components/AnnouncementBar.astro';

interface Props {
title: string;
description: string;
Expand Down Expand Up @@ -47,6 +49,13 @@ const csp = [
</head>
<body>
<a class="skip-link" href="#content">Skip to content</a>
<AnnouncementBar
storageKey="site-announcement:declarative-migrations:v1"
message="Happy 2026, y’all!"
href="https://github.com/declarative-migrations"
linkLabel="See what we’re building"
tone="rust"
/>
<header class="site-nav">
<div class="nav-inner">
<a class="nav-brand" href="/" aria-label="Declarative Migrations home"><span>⇄</span><strong>Declarative Migrations</strong></a>
Expand Down
Loading