brand: WebSensor pulse mark — SVG logo, multi-size favicon.ico, apple-touch and PWA icons (incl. maskable), redesigned OpenGraph/Twitter share image with live counters, mark in nav and event share images
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
13 changed files +114 −24
added
apps/web/public/icons/icon-192.png
+0 −0
Binary file not shown.
added
apps/web/public/icons/icon-512-maskable.png
+0 −0
Binary file not shown.
added
apps/web/public/icons/icon-512.png
+0 −0
Binary file not shown.
added
apps/web/public/logo.svg
+13 −0
@@ -0,0 +1,13 @@ | ||
| 1 | +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64"> | |
| 2 | + <title>WebSensor</title> | |
| 3 | + <rect width="64" height="64" rx="14" fill="#0a0e15"/> | |
| 4 | + <rect x="0.5" y="0.5" width="63" height="63" rx="13.5" fill="none" stroke="#22d3a5" stroke-opacity="0.18"/> | |
| 5 | + <!-- baseline of the signal --> | |
| 6 | + <path d="M8 46H56" stroke="#2d394e" stroke-width="2" stroke-linecap="round"/> | |
| 7 | + <!-- the pulse: a W-shaped waveform, grey until the detected change, signal green after --> | |
| 8 | + <path d="M9 24L19 46L31 30L43 46" fill="none" stroke="#c9d1e0" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/> | |
| 9 | + <path d="M43 46L54 22" fill="none" stroke="#22d3a5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/> | |
| 10 | + <!-- sensor reading --> | |
| 11 | + <circle cx="54" cy="22" r="7" fill="#22d3a5" fill-opacity="0.22"/> | |
| 12 | + <circle cx="54" cy="22" r="4" fill="#22d3a5"/> | |
| 13 | +</svg> | |
added
apps/web/src/app/apple-icon.png
+0 −0
Binary file not shown.
modified
apps/web/src/app/event/[slug]/opengraph-image.tsx
+2 −1
@@ -1,4 +1,5 @@ | ||
| 1 | 1 | import { ImageResponse } from "next/og"; |
| 2 | +import { Mark } from "@/components/brand"; | |
| 2 | 3 | import { api } from "@/lib/api"; |
| 3 | 4 | import { fmtScore, utcDateTime } from "@/lib/format"; |
| 4 | 5 | |
@@ -47,7 +48,7 @@ export default async function OpenGraphImage({ params }: { params: Promise<{ slu | ||
| 47 | 48 | <div style={{ width: "100%", height: "100%", display: "flex", flexDirection: "column", justifyContent: "space-between", padding: 56, background: BG, color: FG, fontFamily: "Inter, system-ui, sans-serif" }}> |
| 48 | 49 | <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}> |
| 49 | 50 | <div style={{ display: "flex", alignItems: "center", gap: 14, fontSize: 30, fontWeight: 700 }}> |
| 50 | − <div style={{ width: 16, height: 16, borderRadius: 16, background: SIGNAL, boxShadow: "0 0 0 8px rgba(34,211,165,0.22)" }} /> | |
| 51 | + <Mark size={44} /> | |
| 51 | 52 | <span> |
| 52 | 53 | Web<span style={{ color: SIGNAL }}>Sensor</span> |
| 53 | 54 | </span> |
added
apps/web/src/app/favicon.ico
+0 −0
Binary file not shown.
modified
apps/web/src/app/icon.svg
+13 −1
@@ -1 +1,13 @@ | ||
| 1 | −<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="12" fill="#0b0f17"/><circle cx="32" cy="32" r="8" fill="#22d3a5"/><circle cx="32" cy="32" r="16" fill="none" stroke="#22d3a5" stroke-opacity="0.55" stroke-width="3"/><circle cx="32" cy="32" r="25" fill="none" stroke="#22d3a5" stroke-opacity="0.25" stroke-width="3"/></svg> | |
| 1 | +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64"> | |
| 2 | + <title>WebSensor</title> | |
| 3 | + <rect width="64" height="64" rx="14" fill="#0a0e15"/> | |
| 4 | + <rect x="0.5" y="0.5" width="63" height="63" rx="13.5" fill="none" stroke="#22d3a5" stroke-opacity="0.18"/> | |
| 5 | + <!-- baseline of the signal --> | |
| 6 | + <path d="M8 46H56" stroke="#2d394e" stroke-width="2" stroke-linecap="round"/> | |
| 7 | + <!-- the pulse: a W-shaped waveform, grey until the detected change, signal green after --> | |
| 8 | + <path d="M9 24L19 46L31 30L43 46" fill="none" stroke="#c9d1e0" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/> | |
| 9 | + <path d="M43 46L54 22" fill="none" stroke="#22d3a5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/> | |
| 10 | + <!-- sensor reading --> | |
| 11 | + <circle cx="54" cy="22" r="7" fill="#22d3a5" fill-opacity="0.22"/> | |
| 12 | + <circle cx="54" cy="22" r="4" fill="#22d3a5"/> | |
| 13 | +</svg> | |
modified
apps/web/src/app/manifest.ts
+10 −5
@@ -2,13 +2,18 @@ import type { MetadataRoute } from "next"; | ||
| 2 | 2 | |
| 3 | 3 | export default function manifest(): MetadataRoute.Manifest { |
| 4 | 4 | return { |
| 5 | − name: "WebSensor", | |
| 5 | + name: "WebSensor — Real-time Internet change intelligence", | |
| 6 | 6 | short_name: "WebSensor", |
| 7 | − description: "Detect What Changed. Know Why It Matters.", | |
| 7 | + description: "The Web is changing. We are watching. Detect what changed, know why it matters.", | |
| 8 | 8 | start_url: "/", |
| 9 | 9 | display: "standalone", |
| 10 | − background_color: "#0b0f17", | |
| 11 | − theme_color: "#0b0f17", | |
| 12 | − icons: [{ src: "/icon.svg", sizes: "any", type: "image/svg+xml" }], | |
| 10 | + background_color: "#0a0e15", | |
| 11 | + theme_color: "#0a0e15", | |
| 12 | + icons: [ | |
| 13 | + { src: "/icon.svg", sizes: "any", type: "image/svg+xml", purpose: "any" }, | |
| 14 | + { src: "/icons/icon-192.png", sizes: "192x192", type: "image/png", purpose: "any" }, | |
| 15 | + { src: "/icons/icon-512.png", sizes: "512x512", type: "image/png", purpose: "any" }, | |
| 16 | + { src: "/icons/icon-512-maskable.png", sizes: "512x512", type: "image/png", purpose: "maskable" }, | |
| 17 | + ], | |
| 13 | 18 | }; |
| 14 | 19 | } |
modified
apps/web/src/app/opengraph-image.tsx
+31 −14
@@ -1,30 +1,47 @@ | ||
| 1 | 1 | import { ImageResponse } from "next/og"; |
| 2 | +import { BRAND, Mark, PulseBackdrop } from "@/components/brand"; | |
| 3 | +import { api, type Stats } from "@/lib/api"; | |
| 4 | +import { fmtInt } from "@/lib/format"; | |
| 2 | 5 | |
| 3 | 6 | export const runtime = "nodejs"; |
| 4 | −export const alt = "WebSensor — The Web, Live."; | |
| 7 | +export const alt = "WebSensor — The Web is changing. We are watching."; | |
| 5 | 8 | export const size = { width: 1200, height: 630 }; |
| 6 | 9 | export const contentType = "image/png"; |
| 7 | 10 | |
| 8 | −export default function OpenGraphImage() { | |
| 11 | +/** Site-wide share image (spec §82–83). Live counters come from the API; nothing is fabricated. */ | |
| 12 | +export default async function OpenGraphImage() { | |
| 13 | + const s: Stats = await api.stats().catch(() => ({}) as Stats); | |
| 14 | + const facts = [s.sources ? `${fmtInt(s.sources)} sources` : null, s.sensors ? `${fmtInt(s.sensors)} sensors` : null, s.events_24h ? `${fmtInt(s.events_24h)} events / 24 h` : null].filter(Boolean) as string[]; | |
| 9 | 15 | return new ImageResponse( |
| 10 | 16 | ( |
| 11 | − <div style={{ width: "100%", height: "100%", display: "flex", flexDirection: "column", justifyContent: "space-between", padding: 64, background: "#0b0f17", color: "#e5e7eb", fontFamily: "Inter, system-ui, sans-serif" }}> | |
| 12 | − <div style={{ display: "flex", alignItems: "center", gap: 16, fontSize: 36, fontWeight: 700 }}> | |
| 13 | − <div style={{ width: 20, height: 20, borderRadius: 20, background: "#22d3a5", boxShadow: "0 0 0 10px rgba(34,211,165,0.25)" }} /> | |
| 14 | − <span> | |
| 15 | − Web<span style={{ color: "#22d3a5" }}>Sensor</span> | |
| 16 | − </span> | |
| 17 | + <div style={{ width: "100%", height: "100%", display: "flex", flexDirection: "column", justifyContent: "space-between", padding: 64, background: `linear-gradient(135deg, ${BRAND.bg} 0%, #0f1622 100%)`, color: "#e6e8ee", fontFamily: "Inter, system-ui, sans-serif", position: "relative" }}> | |
| 18 | + <PulseBackdrop width={1200} height={630} /> | |
| 19 | + <div style={{ display: "flex", alignItems: "center", gap: 20 }}> | |
| 20 | + <Mark size={72} /> | |
| 21 | + <div style={{ display: "flex", flexDirection: "column" }}> | |
| 22 | + <div style={{ display: "flex", fontSize: 44, fontWeight: 700, letterSpacing: -1 }}> | |
| 23 | + Web<span style={{ color: BRAND.signal }}>Sensor</span> | |
| 24 | + </div> | |
| 25 | + <div style={{ display: "flex", fontSize: 20, color: BRAND.subtle, fontFamily: "monospace", letterSpacing: 3 }}>REAL-TIME INTERNET CHANGE INTELLIGENCE</div> | |
| 26 | + </div> | |
| 17 | 27 | </div> |
| 18 | − <div style={{ display: "flex", flexDirection: "column", gap: 18 }}> | |
| 19 | − <div style={{ display: "flex", flexDirection: "column", fontSize: 76, fontWeight: 700, letterSpacing: -2, lineHeight: 1.05 }}> | |
| 28 | + <div style={{ display: "flex", flexDirection: "column", gap: 20, maxWidth: 900 }}> | |
| 29 | + <div style={{ display: "flex", flexDirection: "column", fontSize: 80, fontWeight: 700, letterSpacing: -3, lineHeight: 1.02 }}> | |
| 20 | 30 | <span>The Web is changing.</span> |
| 21 | − <span>We are watching.</span> | |
| 31 | + <span style={{ color: BRAND.muted }}>We are watching.</span> | |
| 22 | 32 | </div> |
| 23 | − <div style={{ fontSize: 30, color: "#a3adc2" }}>Detect What Changed. Know Why It Matters.</div> | |
| 33 | + <div style={{ display: "flex", fontSize: 28, color: BRAND.muted, lineHeight: 1.35 }}>What changed, where, when, whether it was silent, how important it is — with immutable evidence.</div> | |
| 24 | 34 | </div> |
| 25 | − <div style={{ display: "flex", justifyContent: "space-between", fontSize: 24, color: "#6b7591", fontFamily: "monospace" }}> | |
| 35 | + <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", fontSize: 24, color: BRAND.subtle, fontFamily: "monospace" }}> | |
| 36 | + <div style={{ display: "flex", gap: 28 }}> | |
| 37 | + {facts.map((f) => ( | |
| 38 | + <span key={f} style={{ display: "flex", alignItems: "center", gap: 10 }}> | |
| 39 | + <span style={{ width: 8, height: 8, borderRadius: 8, background: BRAND.signal }} /> | |
| 40 | + {f} | |
| 41 | + </span> | |
| 42 | + ))} | |
| 43 | + </div> | |
| 26 | 44 | <span>www.websensor.io</span> |
| 27 | − <span>LIVE · UTC</span> | |
| 28 | 45 | </div> |
| 29 | 46 | </div> |
| 30 | 47 | ), |
added
apps/web/src/app/twitter-image.tsx
+1 −0
@@ -0,0 +1 @@ | ||
| 1 | +export { default, alt, size, contentType, runtime } from "./opengraph-image"; | |
added
apps/web/src/components/brand.tsx
+42 −0
@@ -0,0 +1,42 @@ | ||
| 1 | +import type { CSSProperties } from "react"; | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * WebSensor brand mark: a dark rounded tile with a W-shaped pulse — grey until the detected change, | |
| 5 | + * signal green after it, ending in the sensor reading. The same geometry is used for the favicon | |
| 6 | + * (app/icon.svg), the nav wordmark, the OpenGraph images and the PWA icons (public/icons/*). | |
| 7 | + */ | |
| 8 | +export const BRAND = { | |
| 9 | + bg: "#0a0e15", | |
| 10 | + fg: "#c9d1e0", | |
| 11 | + signal: "#22d3a5", | |
| 12 | + line: "#2d394e", | |
| 13 | + muted: "#a3adc2", | |
| 14 | + subtle: "#6b7591", | |
| 15 | +} as const; | |
| 16 | + | |
| 17 | +export function Mark({ size = 20, className = "", style, title = "WebSensor" }: { size?: number; className?: string; style?: CSSProperties; title?: string }) { | |
| 18 | + return ( | |
| 19 | + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width={size} height={size} className={className} style={style} role="img" aria-label={title}> | |
| 20 | + <rect width="64" height="64" rx="14" fill={BRAND.bg} /> | |
| 21 | + <rect x="0.5" y="0.5" width="63" height="63" rx="13.5" fill="none" stroke={BRAND.signal} strokeOpacity="0.18" /> | |
| 22 | + <path d="M8 46H56" stroke={BRAND.line} strokeWidth="2" strokeLinecap="round" /> | |
| 23 | + <path d="M9 24L19 46L31 30L43 46" fill="none" stroke={BRAND.fg} strokeWidth="5" strokeLinecap="round" strokeLinejoin="round" /> | |
| 24 | + <path d="M43 46L54 22" fill="none" stroke={BRAND.signal} strokeWidth="5" strokeLinecap="round" strokeLinejoin="round" /> | |
| 25 | + <circle cx="54" cy="22" r="7" fill={BRAND.signal} fillOpacity="0.22" /> | |
| 26 | + <circle cx="54" cy="22" r="4" fill={BRAND.signal} /> | |
| 27 | + </svg> | |
| 28 | + ); | |
| 29 | +} | |
| 30 | + | |
| 31 | +/** Large decorative pulse used as the OpenGraph backdrop (no tile, stroke only). */ | |
| 32 | +export function PulseBackdrop({ width, height, opacity = 0.16 }: { width: number; height: number; opacity?: number }) { | |
| 33 | + const y0 = height * 0.72; | |
| 34 | + const d = `M-20 ${y0} H${width * 0.28} L${width * 0.36} ${y0 - height * 0.42} L${width * 0.47} ${y0 + height * 0.06} L${width * 0.56} ${y0 - height * 0.18} L${width * 0.63} ${y0} H${width * 0.74} L${width * 0.8} ${y0 - height * 0.62} L${width * 0.86} ${y0} H${width + 20}`; | |
| 35 | + return ( | |
| 36 | + <svg xmlns="http://www.w3.org/2000/svg" width={width} height={height} viewBox={`0 0 ${width} ${height}`} style={{ position: "absolute", top: 0, left: 0 }}> | |
| 37 | + <path d={d} fill="none" stroke={BRAND.signal} strokeWidth={6} strokeLinecap="round" strokeLinejoin="round" strokeOpacity={opacity} /> | |
| 38 | + <circle cx={width * 0.8} cy={y0 - height * 0.62} r={22} fill={BRAND.signal} fillOpacity={opacity * 0.6} /> | |
| 39 | + <circle cx={width * 0.8} cy={y0 - height * 0.62} r={11} fill={BRAND.signal} fillOpacity={opacity * 1.6} /> | |
| 40 | + </svg> | |
| 41 | + ); | |
| 42 | +} | |
modified
apps/web/src/components/nav.tsx
+2 −3
@@ -4,6 +4,7 @@ import Link from "next/link"; | ||
| 4 | 4 | import { usePathname } from "next/navigation"; |
| 5 | 5 | import { Activity, Bell, Bookmark, Compass, Eye, Globe2, LayoutGrid, Radar, Search, Siren, X, Zap } from "lucide-react"; |
| 6 | 6 | import { useState } from "react"; |
| 7 | +import { Mark } from "./brand"; | |
| 7 | 8 | import { DensityToggle } from "./prefs"; |
| 8 | 9 | import { ThemeToggle } from "./theme"; |
| 9 | 10 | import { Kbd } from "./ui"; |
@@ -24,9 +25,7 @@ const NAV = [ | ||
| 24 | 25 | export function Wordmark() { |
| 25 | 26 | return ( |
| 26 | 27 | <Link href="/" className="flex items-center gap-2 font-semibold tracking-tight" aria-label="WebSensor home"> |
| 27 | − <span className="relative inline-flex size-2.5 items-center justify-center"> | |
| 28 | − <span className="absolute inset-0 rounded-full bg-signal animate-pulse-dot" /> | |
| 29 | − </span> | |
| 28 | + <Mark size={22} className="shrink-0 rounded-[5px]" /> | |
| 30 | 29 | <span className="text-[15px]"> |
| 31 | 30 | Web<span className="text-signal">Sensor</span> |
| 32 | 31 | </span> |
| 33 | 32 | |