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
5 changes: 5 additions & 0 deletions web/src/app/community/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import AuthStatus from "@/components/community/AuthStatus";
import CommunityNav from "@/components/community/CommunityNav";
import DeckDock from "@/components/community/DeckDock";
import DeckPanel from "@/components/community/DeckPanel";
import FirmwareNotice from "@/components/community/FirmwareNotice";
import MobileTabBar from "@/components/community/MobileTabBar";
import styles from "@/components/community/Community.module.css";

Expand Down Expand Up @@ -66,6 +67,10 @@ export default async function CommunityLayout({ children }: { children: React.Re
return (
<main className={styles.page}>
<div className={styles.inner}>
{/* Above the header on purpose. A board owner needs to see this once;
putting it below the nav would let it sit under the fold on the
page they actually came for. */}
<FirmwareNotice />
<header className={styles.pageHeader}>
<div className={styles.brandBlock}>
<Link href="/" className={styles.brand}>
Expand Down
73 changes: 73 additions & 0 deletions web/src/components/community/Community.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -2873,6 +2873,79 @@
pointer-events: none;
}

/* ── Firmware notice ──────────────────────────────────────────────────────
A thin strip above the header. Deliberately quiet: it is news, not an
error, and the person reading it came here to look at patterns. It uses
the LED accent only on the tag and the link, so it registers without
turning the top of every page into a warning. */
.fwNotice {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
padding: 9px 12px;
margin-bottom: 14px;
border: 1px solid rgba(255, 92, 46, 0.28);
background: rgba(255, 92, 46, 0.06);
font-size: 12.5px;
}

.fwNoticeTag {
font-family: var(--pf-mono, monospace);
font-size: 10px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--pfc-led);
flex: none;
}

.fwNoticeText {
color: var(--pfc-ink-muted);
min-width: 0;
}

.fwNoticeText strong {
color: var(--pfc-ink);
font-weight: 600;
}

.fwNoticeLink {
color: var(--pfc-led);
text-decoration: none;
white-space: nowrap;
flex: none;
}

.fwNoticeLink:hover {
text-decoration: underline;
}

.fwNoticeClose {
appearance: none;
border: 0;
background: none;
color: var(--pfc-ink-faint);
font-size: 13px;
line-height: 1;
padding: 4px;
cursor: pointer;
margin-left: auto;
flex: none;
}

.fwNoticeClose:hover {
color: var(--pfc-ink);
}

@media (max-width: 640px) {
/* The close button keeps its place; the link drops below the text rather
than squeezing it to two words a line. */
.fwNoticeText {
flex: 1 1 100%;
order: 3;
}
}

/* ── Share panel ──────────────────────────────────────────────────────────
The link is shown, not just copied: a button that says "Copied" about an
address you never saw asks for trust it has not earned, and the field is
Expand Down
81 changes: 81 additions & 0 deletions web/src/components/community/FirmwareNotice.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"use client";

import Link from "next/link";
import { useSyncExternalStore } from "react";
import manifest from "../../../public/flash/manifest.json";
import styles from "./Community.module.css";

// "There is a new firmware" — said here, because there is nowhere else to say
// it.
//
// A board checks for updates from its own console, and that check only exists
// in v3.4.0 and later. Everyone on an earlier build sees nothing: the banner
// telling you to update is in the update you have not installed. Nor can this
// site read their version to find out — patternflow.work is https and a board
// is http, and a browser will not let an https page fetch http at all. The
// direction only runs the other way, which is why every device flow works by
// navigating TO the board rather than reaching into it.
//
// So the only place left to mention it is a page they already visit, which is
// this one: the community is where somebody who owns a Patternflow goes.
//
// Dismissal is per version, not forever. Dismiss this and it stays gone until
// there is a different release to mention — it cannot know whether you updated
// (see above), so the honest fallback is to ask once per version and drop it.

const LATEST = manifest.version;
const KEY = "pf-firmware-notice";

/** Read once per render from localStorage, with a server snapshot of "not
* dismissed" so the markup matches and the banner never flashes in late. */
function useDismissed(): boolean {
return useSyncExternalStore(
(onChange) => {
window.addEventListener("storage", onChange);
return () => window.removeEventListener("storage", onChange);
},
() => {
try {
return window.localStorage.getItem(KEY) === LATEST;
} catch {
return false; /* private mode */
}
},
() => true, // server: assume dismissed, so nothing appears before hydration
);
}

export default function FirmwareNotice() {
const dismissed = useDismissed();
if (dismissed) return null;

const dismiss = () => {
try {
window.localStorage.setItem(KEY, LATEST);
} catch {
/* private mode — it will ask again, which is the lesser failure */
}
window.dispatchEvent(new Event("storage"));
};

return (
<div className={styles.fwNotice}>
<span className={styles.fwNoticeTag}>Firmware</span>
<span className={styles.fwNoticeText}>
<strong>{LATEST}</strong> is out — patterns install from a link, and a set now ships with
the board.
</span>
<Link href="/update" className={styles.fwNoticeLink}>
Update over Wi-Fi →
</Link>
<button
type="button"
className={styles.fwNoticeClose}
onClick={dismiss}
aria-label="Dismiss until the next release"
>
</button>
</div>
);
}
Loading