diff --git a/public/flag-tibet.svg b/public/flag-tibet.svg
new file mode 100644
index 00000000..0bc13ea6
--- /dev/null
+++ b/public/flag-tibet.svg
@@ -0,0 +1,236 @@
+
+
\ No newline at end of file
diff --git a/src/components/CountryFlag.tsx b/src/components/CountryFlag.tsx
new file mode 100644
index 00000000..36a1f6d3
--- /dev/null
+++ b/src/components/CountryFlag.tsx
@@ -0,0 +1,75 @@
+import { cn } from '@/lib/utils';
+
+interface CountryFlagProps {
+ /**
+ * ISO 3166-1 alpha-2 country code (e.g. `US`, `BR`) or ISO 3166-2
+ * subdivision code (e.g. `CN-XZ`, `GB-SCT`). Case-insensitive.
+ */
+ code: string;
+ /** The flag emoji to render when no custom asset is available. */
+ emoji: string;
+ /** Accessible label / `alt` for the flag. */
+ label: string;
+ /** Optional extra classes applied to the rendering element. */
+ className?: string;
+}
+
+/**
+ * Map of ISO codes that render as a bundled `` asset instead of an
+ * emoji. Unicode does not define flag emoji for everything that has a
+ * recognised flag — Tibet (the Snow Lion flag) being the headline case —
+ * so we ship the canonical SVG and swap it in when we can.
+ *
+ * Note we treat `CN-XZ` as country-level Tibet here, mirroring the
+ * editorial choice the older Agora codebase made: the ISO 3166-2 code is
+ * used as the identifier, but the display reads as a country.
+ */
+const CUSTOM_FLAG_ASSETS: Record = {
+ 'CN-XZ': '/flag-tibet.svg',
+};
+
+/**
+ * Render a flag for a country or subdivision. For codes with a bundled
+ * SVG (currently Tibet) we emit an `` that visually matches the
+ * surrounding emoji line-height; for everything else we drop the emoji
+ * straight into a `` so it inherits font color and selection
+ * behaviour like the rest of the text run.
+ *
+ * Callers control sizing via Tailwind classes — pass `text-3xl` to size
+ * the emoji and the SVG will scale to match (`h-[1em] w-auto`).
+ */
+export function CountryFlag({ code, emoji, label, className }: CountryFlagProps) {
+ const upper = code.toUpperCase();
+ const customAsset = CUSTOM_FLAG_ASSETS[upper];
+
+ if (customAsset) {
+ return (
+ // The wrapper span carries the font-size class so the inner image
+ // can size itself in `em` units and stay in lockstep with the
+ // emoji glyphs on neighbouring chips.
+
+
+
+ );
+ }
+
+ return (
+
+ {emoji}
+
+ );
+}
diff --git a/src/components/discovery/CountryPulseStrip.tsx b/src/components/discovery/CountryPulseStrip.tsx
index be7dca39..7a4f6396 100644
--- a/src/components/discovery/CountryPulseStrip.tsx
+++ b/src/components/discovery/CountryPulseStrip.tsx
@@ -2,8 +2,9 @@ import { useMemo } from 'react';
import { Link } from 'react-router-dom';
import { useGlobalActivity } from '@/hooks/useGlobalActivity';
-import { getCountryInfo } from '@/lib/countries';
+import { getCountryInfo, subdivisionFlag } from '@/lib/countries';
import { Skeleton } from '@/components/ui/skeleton';
+import { CountryFlag } from '@/components/CountryFlag';
import { cn } from '@/lib/utils';
interface CountryPulseStripProps {
@@ -13,30 +14,61 @@ interface CountryPulseStripProps {
}
interface CountryEntry {
- /** ISO 3166-1 alpha-2 code. */
+ /** Full ISO 3166 identifier — either `XX` (country) or `XX-YY` (subdivision). */
code: string;
- /** Display name (from `getCountryInfo`). */
+ /**
+ * Display name. For subdivisions this is the subdivision name
+ * (e.g. "California"); for countries it's the country name
+ * (e.g. "United States").
+ */
name: string;
- /** Flag emoji. */
+ /**
+ * Best emoji flag for this entry. Subdivisions with an RGI tag
+ * sequence (currently England, Scotland, Wales) get their actual
+ * subnational flag; everything else falls back to the parent country
+ * flag and a subdivision-code badge.
+ */
flag: string;
+ /**
+ * Subdivision token (e.g. `CA`, `TX`, `BY`) shown as a small badge
+ * overlay when the entry is a state/province *and* no native
+ * subdivision flag exists. `undefined` for country-level entries and
+ * for subdivisions that already have their own flag emoji.
+ */
+ subdivisionToken?: string;
/** Activity count from kind 30385 stats. */
count: number;
}
/**
- * Horizontal strip of country flag chips, ordered by trailing-window
- * activity from the trusted kind 30385 stats snapshots. Hovering a chip
- * lifts its flag and brightens its warm gradient sleeve; clicking opens
- * the country's NIP-73 external-identifier feed at `/i/iso3166:XX`.
+ * Horizontal strip of country (and subdivision) flag chips, ordered by
+ * trailing-window activity from the trusted kind 30385 stats snapshots.
+ * Hovering a chip lifts its flag and brightens its warm gradient
+ * sleeve; clicking opens the entity's NIP-73 external-identifier feed
+ * at `/i/iso3166:XX` (or `/i/iso3166:XX-YY` for subdivisions).
*
* Sits below the hero on the Discover page as a low-friction entry into
- * country-scoped browsing — the "this is where the world is showing up
+ * geo-scoped browsing — the "this is where the world is showing up
* today" rail.
*
* Renders a soft-pulse skeleton while activity data is loading so the
* page never collapses to zero height between the hero and the campaign
* shelf.
*/
+/**
+ * ISO 3166-2 codes we display as country-level entries rather than
+ * subdivisions. The strip shows the subdivision's *own* name (no parent
+ * country fallback) and suppresses the small ISO-suffix badge — these
+ * are entities with their own widely-recognised flag and identity.
+ *
+ * Currently just Tibet: ISO 3166-2 lists `CN-XZ` as "Tibet Autonomous
+ * Region" under China, but the editorial position here is to surface it
+ * as a country in its own right with the Snow Lion flag.
+ */
+const COUNTRY_LEVEL_SUBDIVISIONS: Record = {
+ 'CN-XZ': 'Tibet',
+};
+
export function CountryPulseStrip({ limit = 24, className }: CountryPulseStripProps) {
const { data: activityByCountry, isLoading } = useGlobalActivity();
@@ -46,15 +78,59 @@ export function CountryPulseStrip({ limit = 24, className }: CountryPulseStripPr
for (const [code, count] of activityByCountry) {
const info = getCountryInfo(code);
if (!info) continue;
- out.push({ code, name: info.name, flag: info.flag, count });
+ const upperCode = code.toUpperCase();
+ const countryLevelOverride = COUNTRY_LEVEL_SUBDIVISIONS[upperCode];
+ const isSubdivision = !!info.subdivision;
+
+ // Country-level override (Tibet etc.) wins: use the editorial
+ // name, drop the subdivision-token badge, and let CountryFlag pick
+ // up the bundled SVG asset on render.
+ if (countryLevelOverride) {
+ out.push({
+ code,
+ name: countryLevelOverride,
+ flag: info.flag,
+ subdivisionToken: undefined,
+ count,
+ });
+ continue;
+ }
+
+ // Prefer subdivision-level naming when available — "California" reads
+ // far more usefully than yet another "United States" tile.
+ const name = (isSubdivision ? info.subdivisionName : info.name) ?? info.name;
+ // Use the real subdivision flag (England/Scotland/Wales tag
+ // sequences) when one exists; otherwise fall back to the parent
+ // country flag and surface the ISO 3166-2 suffix as a badge.
+ const nativeSubFlag = isSubdivision && info.subdivision
+ ? subdivisionFlag(info.subdivision)
+ : null;
+ const flag = nativeSubFlag ?? info.flag;
+ const subdivisionToken = isSubdivision && !nativeSubFlag
+ ? info.subdivision?.split('-')[1]
+ : undefined;
+ out.push({
+ code,
+ name,
+ flag,
+ subdivisionToken,
+ count,
+ });
}
out.sort((a, b) => b.count - a.count);
return out.slice(0, limit);
}, [activityByCountry, limit]);
+ // Vertical padding (`py-2`) on the scroll track gives the chips room
+ // to lift on hover (`-translate-y-0.5` plus the slightly larger glyph)
+ // without `overflow-x-auto` cropping the top edge — `overflow-x-auto`
+ // implicitly clips on the Y axis too, and `overflow-y-visible` is
+ // unreliable across browsers, so we pad instead of fight the clip.
+ const scrollClass = 'flex gap-3 overflow-x-auto scrollbar-none px-4 py-2';
+
if (isLoading && entries.length === 0) {
return (
-
{entries.map((entry) => (
))}
@@ -74,6 +150,10 @@ export function CountryPulseStrip({ limit = 24, className }: CountryPulseStripPr
}
function CountryChip({ entry }: { entry: CountryEntry }) {
+ const ariaLabel = entry.subdivisionToken
+ ? `${entry.name} (${entry.code}): ${entry.count.toLocaleString()} recent comments`
+ : `${entry.name}: ${entry.count.toLocaleString()} recent comments`;
+
return (
-
- {entry.flag}
+ {/* Flag + optional subdivision token. The token is a tiny pill
+ anchored bottom-right of the flag glyph — no emoji font has
+ reliable subnational flags, so we surface the ISO 3166-2
+ suffix (e.g. "CA", "TX", "BY") as a typographic badge.
+ `CountryFlag` swaps in a bundled SVG for codes that have a
+ recognised flag but no Unicode emoji (currently Tibet). */}
+
+
+ {entry.subdivisionToken && (
+
+ {entry.subdivisionToken}
+
+ )}
{entry.name}
diff --git a/src/components/discovery/DiscoverHero.tsx b/src/components/discovery/DiscoverHero.tsx
index 1cc14b03..db90f157 100644
--- a/src/components/discovery/DiscoverHero.tsx
+++ b/src/components/discovery/DiscoverHero.tsx
@@ -245,22 +245,33 @@ export function DiscoverHero({ className }: DiscoverHeroProps) {
+ {/* Readability scrim. Sits above the globe + atmosphere but below
+ the foreground content so the headline / paragraph stay legible
+ regardless of which hue the palette is currently cycling
+ through. Top-down so the eye-line lands on the darkest pixels;
+ we taper to transparent before the ticker pill so the CTAs and
+ stat row underneath keep their warm wash. */}
+
+
{/* Foreground content — headline above the sphere, ticker + CTAs
below it. Uses the same `max-w-5xl` container as the rest of
the Discover page so the hero never sprawls wider than the
shelves beneath it. */}
-
+
Discover
-
+
The world,
gathering.
-
+
Campaigns, communities, and conversations from every corner of the
- globe — backed by Bitcoin, broadcast on Nostr, owned by no one.
+ globe. Backed by Bitcoin, broadcast on Nostr, owned by no one.
diff --git a/src/lib/countries.ts b/src/lib/countries.ts
index 79d98bf5..98997b61 100644
--- a/src/lib/countries.ts
+++ b/src/lib/countries.ts
@@ -391,10 +391,12 @@ export function getGeoDisplayName(code: string, _lang?: string): string {
}
/**
- * Return the flag emoji for an ISO 3166-1 country code or 3166-2 subdivision
- * code. Subdivisions resolve to their parent country's flag (no per-region
- * flag exists in Unicode for arbitrary subdivisions). Unknown codes return
- * an empty string.
+ * Convert a 2-letter ISO 3166-1 alpha-2 country code (or a subdivision
+ * code) to its regional indicator emoji sequence representing the country
+ * flag. Returns the parent country flag for subdivisions — for actual
+ * subnational flags use {@link subdivisionFlag}.
+ *
+ * Unknown codes return an empty string.
*/
export function countryCodeToFlag(code: string): string {
const upper = code.toUpperCase();
@@ -407,6 +409,37 @@ export function countryCodeToFlag(code: string): string {
.join('');
}
+/**
+ * Returns the Unicode emoji flag for ISO 3166-2 subdivisions that have an
+ * RGI ("Recommended for General Interchange") tag sequence — the only
+ * subnational flags broadly supported across Apple, Google, Microsoft,
+ * and major mobile fonts.
+ *
+ * As of Unicode 15.1 that set is exactly three: the constituent countries
+ * of the United Kingdom (England, Scotland, Wales). All other ISO 3166-2
+ * codes return `null`, and callers should fall back to the parent country
+ * flag plus a typographic subdivision-code badge.
+ *
+ * Construction: a black flag (`U+1F3F4`) followed by ASCII tag characters
+ * for each lowercase letter of the `cc-sss` identifier, terminated by a
+ * cancel tag (`U+E007F`).
+ */
+const RGI_SUBDIVISION_FLAGS = new Set(['GB-ENG', 'GB-SCT', 'GB-WLS']);
+
+export function subdivisionFlag(code: string): string | null {
+ const upper = code.toUpperCase();
+ if (!RGI_SUBDIVISION_FLAGS.has(upper)) return null;
+
+ // Tag sequence:
+ const [country, region] = upper.toLowerCase().split('-');
+ const codePoints: number[] = [0x1f3f4];
+ // Each ASCII letter / digit is tagged via U+E0000 + codepoint.
+ for (const ch of country) codePoints.push(0xe0000 + ch.charCodeAt(0));
+ for (const ch of region) codePoints.push(0xe0000 + ch.charCodeAt(0));
+ codePoints.push(0xe007f); // Cancel tag.
+ return String.fromCodePoint(...codePoints);
+}
+
// ── Map coordinates ──────────────────────────────────────────────────────────
//
// Approximate centre points used by the world map (`/world`). Values are