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:
commit
aa5e262f3a
880 changed files with 111042 additions and 0 deletions
24
apps/web/src/components/mobile/source-chips.tsx
Normal file
24
apps/web/src/components/mobile/source-chips.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
"use client";
|
||||
|
||||
import { useRef, type TouchEvent } from "react";
|
||||
|
||||
export interface SourceChip { id: string; label: string; dot?: string; count?: number; }
|
||||
|
||||
export function SourceChips({ sources, value, onChange }: { sources: SourceChip[]; value: string; onChange: (value: string) => void }): React.ReactElement {
|
||||
const start = useRef<{ x: number; y: number } | null>(null);
|
||||
function end(event: TouchEvent<HTMLDivElement>): void {
|
||||
if (start.current === null) return;
|
||||
const dx = (event.changedTouches[0]?.clientX ?? start.current.x) - start.current.x;
|
||||
const dy = (event.changedTouches[0]?.clientY ?? start.current.y) - start.current.y;
|
||||
start.current = null;
|
||||
if (Math.abs(dx) < 50 || Math.abs(dx) < Math.abs(dy) * 1.5) return;
|
||||
const index = sources.findIndex((source) => source.id === value);
|
||||
const next = sources[Math.max(0, Math.min(sources.length - 1, index + (dx < 0 ? 1 : -1)))];
|
||||
if (next) onChange(next.id);
|
||||
}
|
||||
return <div className="flex min-w-0 gap-2 overflow-x-auto px-4 py-2 scrollbar-thin" role="tablist" aria-label="Feed sources" onTouchStart={(event) => { start.current = { x: event.touches[0]?.clientX ?? 0, y: event.touches[0]?.clientY ?? 0 }; }} onTouchEnd={end}>
|
||||
{sources.map((source) => <button key={source.id} type="button" role="tab" aria-selected={value === source.id} onClick={() => onChange(source.id)} className={`flex min-h-11 shrink-0 items-center gap-2 rounded-full border px-4 text-xs font-semibold ${value === source.id ? "border-accent bg-accent-dim text-accent" : "border-border text-text-dim"}`}>
|
||||
{source.dot && <span className="h-2 w-2 rounded-full bg-accent" aria-hidden="true" />}{source.label}{source.count !== undefined && <span className="text-text-faint">{source.count}</span>}
|
||||
</button>)}
|
||||
</div>;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue