diff --git a/web/src/app/community/layout.tsx b/web/src/app/community/layout.tsx index 6c31a54..ffd5690 100644 --- a/web/src/app/community/layout.tsx +++ b/web/src/app/community/layout.tsx @@ -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"; @@ -66,6 +67,10 @@ export default async function CommunityLayout({ children }: { children: React.Re return (
+ {/* 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. */} +
diff --git a/web/src/components/community/Community.module.css b/web/src/components/community/Community.module.css index 386acf0..7671194 100644 --- a/web/src/components/community/Community.module.css +++ b/web/src/components/community/Community.module.css @@ -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 diff --git a/web/src/components/community/FirmwareNotice.tsx b/web/src/components/community/FirmwareNotice.tsx new file mode 100644 index 0000000..26c809d --- /dev/null +++ b/web/src/components/community/FirmwareNotice.tsx @@ -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 ( +
+ Firmware + + {LATEST} is out — patterns install from a link, and a set now ships with + the board. + + + Update over Wi-Fi → + + +
+ ); +}