);
}
/** Build a clip-path polygon for a pie slice with slight overlap to prevent gaps. */
function pieSliceClipPath(index: number, total: number): string {
const angle = 360 / total;
const overlap = 0.5;
const startAngle = index * angle - 90 - overlap;
const sweepAngle = angle + overlap * 2;
const scale = 1.5; // extend past edges so slices cover the full rect
const steps = 12;
const points: string[] = ['50% 50%'];
for (let i = 0; i <= steps; i++) {
const a = startAngle + (sweepAngle * i) / steps;
const rad = (a * Math.PI) / 180;
const x = 50 + 50 * scale * Math.cos(rad);
const y = 50 + 50 * scale * Math.sin(rad);
points.push(`${x}% ${y}%`);
}
return `polygon(${points.join(', ')})`;
}
function StarLayout({ colors }: { colors: string[] }) {
return (
{/* Background fill to cover the center seam where all slices meet */}
{colors.map((color, i) => (
))}
);
}
function CheckerboardLayout({ colors }: { colors: string[] }) {
const n = colors.length;
const height = 180;
// Target ~6 rows so squares are large and legible
const rows = n * Math.max(2, 4 - n);
const cellSize = height / rows;
// Enough columns to cover any container width (overflow is clipped)
const cols = Math.ceil(600 / cellSize);
const cells: { color: string; key: string }[] = [];
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const colorIndex = (row + col) % n;
cells.push({ color: colors[colorIndex], key: `${row}-${col}` });
}
}
return (
{cells.map(({ color, key }) => (
))}
);
}
function DiagonalStripesLayout({ colors }: { colors: string[] }) {
const n = colors.length;
const H = 180;
// Use a canvas wider than the visible area by H so that parallelogram stripes
// at a 45° diagonal fill the rectangle exactly. The viewBox is (W+H) × H but
// we clip it back to W × H via the wrapper div + overflow-hidden.
// Each stripe is a true parallelogram with vertical width W/n and a horizontal
// shift of H from top to bottom, giving a consistent 45° diagonal boundary.
const W = 400;
const totalW = W + H; // canvas wide enough so last stripe reaches bottom-right
const stripeW = totalW / n;
return (
);
}
// ─── Main component ──────────────────────────────────────
const LAYOUT_MAP: Record> = {
horizontal: HorizontalLayout,
vertical: VerticalLayout,
grid: GridLayout,
star: StarLayout,
checkerboard: CheckerboardLayout,
diagonalStripes: DiagonalStripesLayout,
};
/** Standalone blinking eye button — REMOVED.
*
* This component used to let any user apply an arbitrary kind-3367 event's
* palette as their active theme via `applyCustomTheme(paletteToTheme(...))`.
* Agora no longer supports user-customizable themes, so the apply path has
* been removed entirely. Color Moments still render as palette art via
* `ColorMomentContent` below.
*/
export function ColorMomentContent({ event }: { event: NostrEvent }) {
const colors = useMemo(() => getColors(event.tags), [event.tags]);
const layout = (getTag(event.tags, 'layout') ?? 'horizontal') as Layout;
const name = getTag(event.tags, 'name');
const rawContent = event.content.trim();
// Only treat content as an emoji if it's a single grapheme cluster (emoji or short symbol).
// Long text should not be overlaid on the palette.
const isSingleEmoji = rawContent.length > 0 && [...rawContent].length <= 2 && /\p{Emoji}/u.test(rawContent);
const emoji = isSingleEmoji ? rawContent : undefined;
const LayoutComponent = LAYOUT_MAP[layout] ?? HorizontalLayout;
// Determine whether overlay text should be light or dark
const avgLum = useMemo(() => {
if (colors.length === 0) return 0.5;
return colors.reduce((sum, c) => sum + luminance(c), 0) / colors.length;
}, [colors]);
const overlayTextClass = avgLum > 0.5 ? 'text-black/80' : 'text-white/90';
if (colors.length === 0) return null;
return (
{/* Name above the palette */}
{name && (
{name}
)}
{/* Color palette with emoji overlay */}
{/* Emoji centered over the palette */}
{emoji && (