commit 35f88e34e939a041d77ae4d4f5bb86fb1d96959f
parent d61befb94644de5118e0e2645ebf8742949a6db0
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Fri, 25 Sep 2026 21:30:59 -0400
common: BrandMark and Wordmark components (brand S1)
BrandMark draws the inline mark from lib/brand.ts's MARK list — the same
shapes markSvg writes into the icon files — aria-hidden and not focusable,
with colours in style so a slot can be a var() expression. Wordmark splits
through splitWordmark: the lead at 720 in the foreground, the suffix at 380
muted, Archivo at font-stretch 118%, two adjacent inline spans so the link's
name is the title exactly.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
2 files changed, 113 insertions(+), 0 deletions(-)
diff --git a/common/components/BrandMark.tsx b/common/components/BrandMark.tsx
@@ -0,0 +1,60 @@
+import type { CSSProperties } from "react";
+import { MARK, MARK_VIEWBOX, type MarkTone } from "../lib/brand";
+
+// A palette whose slots are any CSS colour — a hex, or a `var()` expression
+// such as the export header's `var(--brand-mark, var(--brand))`, which is why
+// this is not lib/brand.ts's hex-only IconPalette.
+export type BrandMarkPalette = Readonly<Record<MarkTone, string>>;
+
+// The Found-line mark, inline — drawn from the same MARK shape list as
+// markSvg (the icon files), so the two cannot drift. Decorative: the text
+// beside it names the link. Colours go through `style`, not a `fill="var()"`
+// attribute, because a presentation attribute does not resolve custom
+// properties everywhere a style declaration does. Size it with `className`
+// (e.g. `size-7`) or `size`.
+export function BrandMark({
+ palette,
+ size,
+ className,
+ style,
+}: {
+ palette: BrandMarkPalette;
+ size?: number;
+ className?: string;
+ style?: CSSProperties;
+}) {
+ return (
+ <svg
+ viewBox={`0 0 ${MARK_VIEWBOX} ${MARK_VIEWBOX}`}
+ width={size}
+ height={size}
+ aria-hidden="true"
+ focusable="false"
+ className={className}
+ style={style}
+ data-brand-mark=""
+ >
+ {MARK.map((s) =>
+ s.kind === "rect" ? (
+ <rect
+ key={s.part}
+ x={s.x || undefined}
+ y={s.y || undefined}
+ width={s.width}
+ height={s.height}
+ rx={s.rx}
+ data-tone={s.tone}
+ style={{ fill: palette[s.tone] }}
+ />
+ ) : (
+ <polygon
+ key={s.part}
+ points={s.points.map(([x, y]) => `${x},${y}`).join(" ")}
+ data-tone={s.tone}
+ style={{ fill: palette[s.tone] }}
+ />
+ ),
+ )}
+ </svg>
+ );
+}
diff --git a/common/components/Wordmark.tsx b/common/components/Wordmark.tsx
@@ -0,0 +1,53 @@
+import { splitWordmark } from "../lib/brand";
+
+// The two-weight wordmark: Archivo at `font-stretch: 118%`, the lead (the
+// SUBJECT's name — "Jer", "Rekieta", "Archi") heavy in the foreground, the
+// suffix light and muted. The split is configured (site.json `wordmarkLead`,
+// PROJECT_WORDMARK_LEAD), never guessed; with no usable lead the whole title
+// is the lead.
+//
+// The two spans are adjacent with NO whitespace between them and stay inline,
+// so the enclosing link's accessible name is the title exactly ("Jeralyzer",
+// not "Jer alyzer"). Do not make this element `flex`: that would blockify the
+// spans and the browser would put a space between them in the name.
+//
+// Size, leading and truncation come from `className`. The face is Archivo
+// wherever it is loaded: `--font-grotesk` names it until the themes slice makes
+// Archivo the display face everywhere, after which the fallback is the same
+// face and the first term can go.
+export function Wordmark({
+ title,
+ lead,
+ className,
+}: {
+ title: string;
+ lead?: string;
+ className?: string;
+}) {
+ const parts = splitWordmark(title, lead);
+ return (
+ <span
+ className={className}
+ data-wordmark=""
+ style={{
+ fontFamily: "var(--font-grotesk, var(--font-display))",
+ fontStretch: "118%",
+ }}
+ >
+ <span
+ data-wordmark-lead=""
+ style={{ fontWeight: 720, color: "var(--foreground)" }}
+ >
+ {parts.lead}
+ </span>
+ {parts.suffix ? (
+ <span
+ data-wordmark-suffix=""
+ style={{ fontWeight: 380, color: "var(--muted-foreground)" }}
+ >
+ {parts.suffix}
+ </span>
+ ) : null}
+ </span>
+ );
+}