T-HUB/apps/web/src/components/brand/logo.tsx
hacklab cfabbf20ef style: light theme and hand-inked logo matching acampada26s.net
The previous pass kept the dark base and the red icons, which did not
read as the acampada. Now:

- Light theme: white page, black ink and borders, black buttons with a
  hard neon offset shadow, neon for focus and the active nav item;
  h1/h2 in Anton with the stepped neon shadow of the site headings.
- Nav, empty-state, spinner and pattern assets in black; platform icons
  keep their dark disc with a neon stroke.
- New mark: three dome tents wired from their doors into a switch with
  an antenna, drawn in the inked style of the acampada logo (curved
  poles, ball ends, hatching) over a neon offset copy. LogoMark renders
  it inline; visible product name is T-HUB.
- docs/brand: t-hub.svg and a lockup with the Anton wordmark converted
  to outlines so it renders without the font.
- Public submission page moved to the light palette.

Checked with headless Chromium screenshots (offline sample data) on
desktop and mobile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 14:40:02 +02:00

137 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { ReactElement } from "react";
/**
* The T-HUB mark, inline: three dome tents in a circle, each with a cable
* out of its door into a switch with an antenna. Drawn in the hand-inked
* style of the acampada26s.net mark, ink in `currentColor` over a neon
* offset copy. Source: docs/brand/t-hub.svg (same 400×400 geometry).
*/
const TENTS: ReadonlyArray<readonly [number, number]> = [[105, 150], [295, 150], [200, 340]];
function Tent({ x, y }: { x: number; y: number }): ReactElement {
return (
<g transform={`translate(${x},${y})`}>
<path fill="none" d="M-58 0 Q-56 -66 -1 -72 Q55 -66 58 0" strokeWidth="7" />
<path fill="none" d="M-64 1 Q0 -3 64 1" strokeWidth="6" />
<path fill="none" d="M-44 0 Q-30 -58 -1 -72" strokeWidth="4.5" />
<path fill="none" d="M44 0 Q30 -58 -1 -72" strokeWidth="4.5" />
<path d="M-15 0 Q-14 -30 0 -33 Q14 -30 15 0 Z" strokeWidth="4" />
<path fill="none" d="M-50 -14 L-41 -25 M-50 -4 L-37 -19 M-43 -1 L-33 -13 M-52 -27 L-45 -36" strokeWidth="2.6" />
<path fill="none" d="M28 -50 L37 -42 M24 -40 L36 -30 M22 -29 L34 -19" strokeWidth="2.6" />
<circle cx="-64" cy="1" r="5.5" stroke="none" />
<circle cx="64" cy="1" r="5.5" stroke="none" />
<circle cx="-1" cy="-73" r="5.5" stroke="none" />
</g>
);
}
function Mark({ paper }: { paper: string }): ReactElement {
return (
<>
<path fill="none" d="M105 150 Q112 206 168 214" strokeWidth="5" />
<path fill="none" d="M295 150 Q288 206 232 214" strokeWidth="5" />
<path fill="none" d="M200 318 Q197 280 200 236" strokeWidth="5" />
<path fill="none" d="M200 200 Q198 160 200 118" strokeWidth="6" />
<path fill="none" d="M184 98 Q200 82 216 98" strokeWidth="5" />
<path fill="none" d="M171 88 Q200 58 229 88" strokeWidth="5" />
<circle cx="200" cy="112" r="7" stroke="none" />
<path d="M168 201 L233 200 L234 235 L167 236 Z" strokeWidth="6" fill={paper} />
<path d="M179 212 h9 v11 h-9 z M196 212 h9 v11 h-9 z M213 212 h9 v11 h-9 z" stroke="none" />
{TENTS.map(([x, y]) => <Tent key={`${x}-${y}`} x={x} y={y} />)}
</>
);
}
export function LogoMark({
size = 32,
className = "text-text",
title = "T-HUB",
}: {
size?: number;
className?: string;
title?: string;
}): ReactElement {
return (
<svg width={size} height={size} viewBox="0 0 400 400" role="img" aria-label={title} className={className}>
<g strokeLinecap="round" strokeLinejoin="round">
<g transform="translate(7,7)" stroke="#00e5ff" fill="#00e5ff"><Mark paper="#fff" /></g>
<g stroke="currentColor" fill="currentColor"><Mark paper="#fff" /></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={`uppercase tracking-wide text-text ${stacked ? "text-4xl" : ""}`}
style={{
fontFamily: "var(--font-title)",
textShadow: "0.04em 0.04em 0 var(--color-neon)",
...(stacked ? {} : { fontSize: `${Math.max(16, Math.round(size * 0.6))}px` }),
}}
>
T-HUB
</span>
</span>
);
}