Tactical Media Broadcast Hub — initial release

Self-hosted multi-platform broadcast newsroom for activist collectives:
publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style
column wall with hostile-content-safe rendering, live control room with Auto
Director, bridged chat governance with polls and quorum, distributed sandboxed
transcode workers, encrypted content-addressed archive, monthly analytics.

MIT licensed. See README.md and docs/.
This commit is contained in:
glass beadian 2026-10-10 11:56:42 +00:00
commit aa5e262f3a
880 changed files with 111042 additions and 0 deletions

View file

@ -0,0 +1,28 @@
import type { ReactElement } from "react";
import { CircleA } from "./logo";
/**
* Branded empty state: the circle-A outline sitting low-contrast behind the
* message, the way a screen-printed mark sits behind text on a black tee.
*/
export function EmptyState({
title,
hint,
size = 64,
className = "",
}: {
title: string;
hint?: string;
size?: number;
className?: string;
}): ReactElement {
return (
<div className={`flex flex-col items-center justify-center gap-3 py-8 text-center ${className}`}>
<CircleA size={size} className="text-accent/30" />
<p className="text-sm font-medium text-text-dim">{title}</p>
{hint !== undefined && (
<p className="max-w-sm text-2xs leading-relaxed text-text-faint">{hint}</p>
)}
</div>
);
}

View file

@ -0,0 +1,90 @@
import type { ReactElement } from "react";
interface LogoProps {
size?: number;
withText?: boolean;
}
export function Logo({ size = 32, withText = false }: LogoProps): ReactElement {
return (
<span className="flex items-center gap-2">
<svg width={size} height={size} viewBox="0 0 1200 1200" aria-label="Tactical Media Broadcast Hub">
<g fill="none" stroke="var(--color-accent)" strokeWidth="72" strokeLinecap="round" strokeLinejoin="round">
<circle cx="600" cy="610" r="330" />
<path d="M 365 915 L 600 250 L 835 915" strokeWidth="92" />
<path d="M 250 610 L 950 610" strokeWidth="82" />
</g>
<g fill="var(--color-accent)">
<polygon points="600.0,145.0 660.0,270.0 540.0,270.0" />
<polygon points="125.0,610.0 250.0,550.0 250.0,670.0" />
<polygon points="1075.0,610.0 950.0,670.0 950.0,550.0" />
<polygon points="285.0,1050.0 331.0,919.2 415.8,1004.0" />
<polygon points="915.0,1050.0 784.2,1004.0 869.0,919.2" />
</g>
</svg>
{withText && (
<span className="text-sm font-bold tracking-tight" style={{ color: "var(--color-accent)" }}>
Tactical Media Broadcast Hub
</span>
)}
</span>
);
}
export function Spinner({ size = 24 }: { size?: number }): ReactElement {
return (
<svg
width={size}
height={size}
viewBox="0 0 1200 1200"
className="animate-spin"
style={{ animationDuration: "1.2s" }}
aria-label="Loading"
>
<path d="M 600 280 A 330 330 0 0 1 930 610" fill="none" stroke="var(--color-accent)" strokeWidth="90" strokeLinecap="round" />
<path d="M 930 610 A 330 330 0 0 1 600 940" fill="none" stroke="var(--color-accent)" strokeWidth="90" strokeLinecap="round" opacity="0.2" />
<path d="M 600 940 A 330 330 0 0 1 270 610" fill="none" stroke="var(--color-accent)" strokeWidth="90" strokeLinecap="round" opacity="0.2" />
<path d="M 270 610 A 330 330 0 0 1 600 280" fill="none" stroke="var(--color-accent)" strokeWidth="90" strokeLinecap="round" opacity="0.2" />
</svg>
);
}
export function EmptyState({ message, size = 120 }: { message: string; size?: number }): ReactElement {
return (
<div className="flex flex-col items-center justify-center gap-4 py-12">
<svg width={size} height={size} viewBox="0 0 1200 1200" opacity="0.25">
<g fill="none" stroke="var(--color-accent)" strokeWidth="80" strokeLinecap="round" strokeLinejoin="round">
<circle cx="600" cy="610" r="330" />
<path d="M 365 915 L 600 250 L 835 915" strokeWidth="100" />
<path d="M 250 610 L 950 610" strokeWidth="80" />
</g>
</svg>
<p className="text-xs text-text-faint">{message}</p>
</div>
);
}
export function PlatformIcon({ platform, size = 20 }: { platform: string; size?: number }): ReactElement {
return (
<img
src={`/brand/icon-${platform}.svg`}
alt={platform}
width={size}
height={size}
className="rounded-full"
loading="lazy"
/>
);
}
export function NavIcon({ name, size = 18 }: { name: string; size?: number }): ReactElement {
return (
<img
src={`/brand/nav-${name}.svg`}
alt={name}
width={size}
height={size}
loading="lazy"
/>
);
}

View file

@ -0,0 +1,62 @@
import type { ReactElement } from "react";
import { LogoMark } from "./logo";
/**
* Branded loading state: the mark upright inside a spinning ring.
*
* The mark itself is radially asymmetric (the A's apex sits high in its
* bounding box), so rotating the mark would wobble instead of spin. The ring
* carries the rotation; the A stays plumb. `animation` rather than
* Tailwind's animate-spin so globals.css's prefers-reduced-motion override
* (0.01ms) still applies via the transition/animation shorthand.
*/
export function Loading({
size = 28,
label = "Loading…",
className = "",
}: {
size?: number;
label?: string;
className?: string;
}): ReactElement {
return (
<span
role="status"
aria-label={label}
className={`relative inline-flex items-center justify-center ${className}`}
style={{ width: size, height: size }}
>
<svg
width={size}
height={size}
viewBox="0 0 100 100"
aria-hidden="true"
className="absolute inset-0 text-accent"
style={{ animation: "anp-spin 0.9s linear infinite" }}
>
{/* Three-quarter arc: the gap becomes the motion cue. */}
<circle
cx="50"
cy="50"
r="44"
fill="none"
stroke="currentColor"
strokeWidth="6"
strokeLinecap="round"
strokeDasharray="207 276"
/>
</svg>
<LogoMark size={Math.round(size * 0.5)} className="text-accent" title="" />
</span>
);
}
/** Full-panel variant for screens that are still fetching their first page. */
export function LoadingPanel({ label }: { label?: string }): ReactElement {
return (
<div className="flex h-full flex-col items-center justify-center gap-3">
<Loading size={32} label={label ?? "Loading…"} />
{label !== undefined && <span className="text-2xs text-text-faint">{label}</span>}
</div>
);
}

View file

@ -0,0 +1,122 @@
import type { ReactElement } from "react";
/**
* The Tactical Media Broadcast Hub mark, inline.
*
* Source of truth: `public/brand/tactical-media-broadcast-hub.svg` — the arrows-from-circle
* mark, tilted −17.5° counterclockwise so the top arrow points upper-left
* (nothing static; everything points outward). The strokes use `currentColor`
* so the mark always rides the workspace accent, which in the warm palette
* lands within a hair of the brand red (#FF1610 ≈ oklch(0.63 0.24 29) vs the
* token's oklch(0.62 0.22 30)). Geometry is copied 1:1 from the asset on its
* 1700×1700 grid, minus the black plate and the screen-print distress paths
* (those only read on pure black at full size).
*/
export function LogoMark({
size = 32,
className = "text-accent",
title = "Tactical Media Broadcast Hub",
}: {
size?: number;
className?: string;
title?: string;
}): ReactElement {
return (
<svg
width={size}
height={size}
viewBox="0 0 1700 1700"
role="img"
aria-label={title}
className={className}
>
<g transform="rotate(-17.5 850 850)">
<g fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round">
<circle cx="850" cy="850" r="350" strokeWidth="74" />
<path d="M 850 405 L 464.77 1269.52" strokeWidth="88" />
<path d="M 850 405 L 1235.23 1269.52" strokeWidth="88" />
<path d="M 850 405 L 850 350" strokeWidth="88" />
<path d="M 345 850 L 1355 850" strokeWidth="88" />
</g>
<g fill="currentColor">
<polygon points="850,240 925,385 775,385" />
<polygon points="235,850 380,775 380,925" />
<polygon points="1465,850 1320,925 1320,775" />
<polygon points="420,1370 410.51,1207.03 547.52,1268.08" />
<polygon points="1280,1370 1152.48,1268.08 1289.49,1207.03" />
</g>
</g>
</svg>
);
}
/**
* The quiet version of the mark: just the circle-A outline, no arrowheads.
* Used wherever the brand needs to whisper — empty states, placeholders.
*/
export function CircleA({
size = 64,
className = "text-accent",
}: {
size?: number;
className?: string;
}): ReactElement {
return (
<svg
width={size}
height={size}
viewBox="0 0 1700 1700"
aria-hidden="true"
className={className}
>
<g transform="rotate(-17.5 850 850)">
<g fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round">
<circle cx="850" cy="850" r="350" strokeWidth="74" />
<path d="M 850 405 L 464.77 1269.52" strokeWidth="88" />
<path d="M 850 405 L 1235.23 1269.52" strokeWidth="88" />
<path d="M 850 405 L 850 350" strokeWidth="88" />
<path d="M 345 850 L 1355 850" strokeWidth="88" />
</g>
</g>
</svg>
);
}
/**
* A single arrowhead lifted from the mark — the brand's bullet point.
* Points the same way the right-hand barb of the circle-A does.
*/
export function ArrowGlyph({
size = 12,
className = "text-accent",
}: {
size?: number;
className?: string;
}): ReactElement {
return (
<svg width={size} height={size} viewBox="0 0 120 120" aria-hidden="true" className={className}>
<polygon points="120,60 30,14 30,106" fill="currentColor" />
</svg>
);
}
/**
* Mark + wordmark. Inline below ~96px (sidebar, palette); stacked above it
* (onboarding hero) so the wordmark never gets dwarfed by the mark.
*/
export function Logo({ size = 32, withText = true }: { size?: number; withText?: boolean }): ReactElement {
if (!withText) return <LogoMark size={size} />;
const stacked = size >= 96;
return (
<span className={`inline-flex ${stacked ? "flex-col items-center gap-3" : "items-center gap-2.5"}`}>
<LogoMark size={size} />
<span
className={`font-semibold tracking-tight text-text ${stacked ? "text-2xl" : ""}`}
style={stacked ? undefined : { fontSize: `${Math.max(13, Math.round(size * 0.45))}px` }}
>
Tactical Media Broadcast Hub
</span>
</span>
);
}