|
| 1 | +/** |
| 2 | + * GhostCode Dynamics — Brand Logo System |
| 3 | + * --------------------------------------------------------------- |
| 4 | + * This file is the single source of truth for the brand mark across |
| 5 | + * the navbar, footer, mobile menu, favicon, OG images, etc. |
| 6 | + * |
| 7 | + * REPLACING THE PLACEHOLDER WITH THE FINAL PNG (≤ 2 min): |
| 8 | + * 1. Drop the final files into `src/assets/brand/`: |
| 9 | + * - logo-mark.png (square icon, ≥ 512×512) |
| 10 | + * - logo-mark-light.png (optional, for light backgrounds) |
| 11 | + * - logo-full.png (optional full lockup; if absent we |
| 12 | + * keep the SVG monogram + typeset wordmark) |
| 13 | + * 2. Flip `USE_PNG_MARK = true` below. |
| 14 | + * 3. (Optional) Replace `public/favicon.svg` with the new icon. |
| 15 | + * |
| 16 | + * Nothing else in the codebase needs to change — every surface |
| 17 | + * imports <Logo />, <LogoCompact />, or <LogoMark /> from this file. |
| 18 | + * --------------------------------------------------------------- |
| 19 | + */ |
| 20 | + |
1 | 21 | import { cn } from "@/lib/utils"; |
2 | 22 |
|
3 | | -interface LogoProps { |
4 | | - className?: string; |
5 | | - iconOnly?: boolean; |
| 23 | +// Toggle this to `true` once the founder ships the final PNG assets. |
| 24 | +const USE_PNG_MARK = false; |
| 25 | + |
| 26 | +// When USE_PNG_MARK is true these are imported lazily by <LogoMark />. |
| 27 | +// Keeping them as string paths means the build never fails if the |
| 28 | +// PNGs are not yet present in the repo. |
| 29 | +const PNG_MARK_DARK = "/brand/logo-mark.png"; |
| 30 | +const PNG_MARK_LIGHT = "/brand/logo-mark-light.png"; |
| 31 | + |
| 32 | +// --------------------------------------------------------------- |
| 33 | +// Variant: <LogoMark /> — icon only (favicon, app icon, avatar) |
| 34 | +// --------------------------------------------------------------- |
| 35 | + |
| 36 | +interface LogoMarkProps { |
6 | 37 | size?: number; |
| 38 | + className?: string; |
| 39 | + /** Force a specific contrast mode. Defaults to currentColor (auto). */ |
| 40 | + tone?: "auto" | "dark" | "light"; |
7 | 41 | } |
8 | 42 |
|
9 | 43 | /** |
10 | | - * GhostCode Dynamics brand mark. |
11 | | - * The "G" of the wordmark is represented by the ghost-in-circle icon, |
12 | | - * so the visible wordmark reads "hostCode / DYNAMICS". |
| 44 | + * Ghost-in-monogram mark. Pure SVG so it is sharp on every density, |
| 45 | + * inherits theme color via `currentColor`, and ships zero bytes of |
| 46 | + * raster data until the real PNG is wired in. |
13 | 47 | */ |
14 | | -export function Logo({ className, iconOnly = false, size = 32 }: LogoProps) { |
15 | | - return ( |
16 | | - <div |
17 | | - className={cn("flex items-center gap-2.5", className)} |
18 | | - aria-label="GhostCode Dynamics" |
19 | | - > |
20 | | - <GhostIcon size={size} /> |
21 | | - {!iconOnly && ( |
22 | | - <span className="flex flex-col leading-none font-display"> |
23 | | - <span className="text-[1.05rem] font-semibold tracking-tight text-foreground"> |
24 | | - hostCode |
25 | | - </span> |
26 | | - <span className="text-[0.6rem] font-medium tracking-[0.32em] text-muted-foreground mt-0.5"> |
27 | | - DYNAMICS |
28 | | - </span> |
29 | | - </span> |
30 | | - )} |
31 | | - </div> |
32 | | - ); |
33 | | -} |
| 48 | +export function LogoMark({ size = 32, className, tone = "auto" }: LogoMarkProps) { |
| 49 | + if (USE_PNG_MARK) { |
| 50 | + const src = tone === "light" ? PNG_MARK_LIGHT : PNG_MARK_DARK; |
| 51 | + return ( |
| 52 | + <img |
| 53 | + src={src} |
| 54 | + width={size} |
| 55 | + height={size} |
| 56 | + alt="GhostCode Dynamics" |
| 57 | + className={cn("shrink-0 select-none", className)} |
| 58 | + draggable={false} |
| 59 | + /> |
| 60 | + ); |
| 61 | + } |
| 62 | + |
| 63 | + const fg = |
| 64 | + tone === "dark" |
| 65 | + ? "#0a0a0f" |
| 66 | + : tone === "light" |
| 67 | + ? "#ffffff" |
| 68 | + : "currentColor"; |
34 | 69 |
|
35 | | -/** |
36 | | - * Ghost-in-G mark. Uses currentColor so it inverts cleanly on light/dark |
37 | | - * backgrounds without altering the original black / white / gray palette. |
38 | | - */ |
39 | | -export function GhostIcon({ |
40 | | - size = 32, |
41 | | - className, |
42 | | -}: { |
43 | | - size?: number; |
44 | | - className?: string; |
45 | | -}) { |
46 | 70 | return ( |
47 | 71 | <svg |
48 | 72 | width={size} |
49 | 73 | height={size} |
50 | 74 | viewBox="0 0 64 64" |
51 | 75 | fill="none" |
52 | 76 | xmlns="http://www.w3.org/2000/svg" |
53 | | - className={cn("shrink-0 text-foreground", className)} |
| 77 | + className={cn("shrink-0", className)} |
54 | 78 | role="img" |
55 | | - aria-label="GhostCode Dynamics logo" |
| 79 | + aria-label="GhostCode Dynamics" |
| 80 | + shapeRendering="geometricPrecision" |
56 | 81 | > |
57 | | - {/* Solid disc — uses currentColor so it adapts to theme */} |
58 | | - <circle cx="32" cy="32" r="30" fill="currentColor" /> |
| 82 | + {/* Rounded squircle backdrop — the monogram "G" */} |
| 83 | + <rect x="1" y="1" width="62" height="62" rx="16" fill={fg} /> |
59 | 84 |
|
60 | | - {/* G opening on the right side (notch carved out of the disc) */} |
| 85 | + {/* Ghost silhouette cut out of the squircle */} |
61 | 86 | <path |
62 | | - d="M44 28h-9v8h5v4a10 10 0 1 1-3-15" |
63 | | - stroke="var(--background)" |
64 | | - strokeWidth="4" |
65 | | - strokeLinecap="round" |
66 | | - strokeLinejoin="round" |
67 | | - fill="none" |
68 | | - opacity="0" |
69 | | - /> |
70 | | - |
71 | | - {/* Ghost silhouette in the center, in the background color so it |
72 | | - appears as a cut-out on both light and dark themes */} |
73 | | - <path |
74 | | - d="M32 16c-7 0-12 5-12 12v18c0 1.4 1.6 2.2 2.7 1.3l2.2-1.7c.6-.5 1.5-.5 2.1 0l2.2 1.7c.6.5 1.5.5 2.1 0l2.2-1.7c.6-.5 1.5-.5 2.1 0l2.2 1.7c1.1.9 2.7.1 2.7-1.3V28c0-7-5-12-12-12Z" |
| 87 | + d="M32 16c-7 0-12 5-12 12v18.2c0 1.4 1.6 2.2 2.7 1.3l2.2-1.7c.6-.5 1.5-.5 2.1 0l2.2 1.7c.6.5 1.5.5 2.1 0l2.2-1.7c.6-.5 1.5-.5 2.1 0l2.2 1.7c1.1.9 2.7.1 2.7-1.3V28c0-7-5-12-12-12Z" |
75 | 88 | fill="var(--background)" |
76 | 89 | /> |
77 | 90 |
|
78 | | - {/* Soft gray tail/shadow on the ghost for the original 3-tone look */} |
| 91 | + {/* Eyes — restate foreground for a crisp 3-tone read */} |
| 92 | + <circle cx="28.4" cy="29.5" r="1.9" fill={fg} /> |
| 93 | + <circle cx="35.6" cy="29.5" r="1.9" fill={fg} /> |
| 94 | + |
| 95 | + {/* Subtle "C / D" tick: opening on the right side of the G */} |
79 | 96 | <path |
80 | | - d="M38 38c2.5 1.5 4.5 4 5 7-1 .5-2.5.4-3.6-.4L37 43.5V38Z" |
81 | | - fill="currentColor" |
82 | | - opacity="0.35" |
| 97 | + d="M46 30v6" |
| 98 | + stroke="var(--background)" |
| 99 | + strokeWidth="3" |
| 100 | + strokeLinecap="round" |
83 | 101 | /> |
84 | | - |
85 | | - {/* Eyes */} |
86 | | - <circle cx="28.5" cy="29" r="1.8" fill="currentColor" /> |
87 | | - <circle cx="35.5" cy="29" r="1.8" fill="currentColor" /> |
88 | 102 | </svg> |
89 | 103 | ); |
90 | 104 | } |
| 105 | + |
| 106 | +// --------------------------------------------------------------- |
| 107 | +// Variant: <Logo /> — full lockup (icon + "GhostCode Dynamics") |
| 108 | +// --------------------------------------------------------------- |
| 109 | + |
| 110 | +interface LogoProps { |
| 111 | + className?: string; |
| 112 | + size?: number; |
| 113 | + /** Hide the wordmark, keep only the mark. */ |
| 114 | + iconOnly?: boolean; |
| 115 | +} |
| 116 | + |
| 117 | +export function Logo({ className, size = 32, iconOnly = false }: LogoProps) { |
| 118 | + return ( |
| 119 | + <span |
| 120 | + className={cn( |
| 121 | + "inline-flex items-center gap-2.5 transition-transform duration-300 ease-out group-hover:scale-[1.02]", |
| 122 | + className, |
| 123 | + )} |
| 124 | + aria-label="GhostCode Dynamics" |
| 125 | + > |
| 126 | + <LogoMark size={size} /> |
| 127 | + {!iconOnly && <Wordmark />} |
| 128 | + </span> |
| 129 | + ); |
| 130 | +} |
| 131 | + |
| 132 | +// --------------------------------------------------------------- |
| 133 | +// Variant: <LogoCompact /> — icon + "GCD" (tight navs, mobile) |
| 134 | +// --------------------------------------------------------------- |
| 135 | + |
| 136 | +export function LogoCompact({ className, size = 28 }: LogoProps) { |
| 137 | + return ( |
| 138 | + <span |
| 139 | + className={cn( |
| 140 | + "inline-flex items-center gap-2 transition-transform duration-300 ease-out group-hover:scale-[1.02]", |
| 141 | + className, |
| 142 | + )} |
| 143 | + aria-label="GhostCode Dynamics" |
| 144 | + > |
| 145 | + <LogoMark size={size} /> |
| 146 | + <span className="font-display text-base font-semibold tracking-[0.18em] text-foreground"> |
| 147 | + GCD |
| 148 | + </span> |
| 149 | + </span> |
| 150 | + ); |
| 151 | +} |
| 152 | + |
| 153 | +// --------------------------------------------------------------- |
| 154 | +// Wordmark — geometric, Linear / Vercel / Stripe inspired |
| 155 | +// --------------------------------------------------------------- |
| 156 | + |
| 157 | +function Wordmark() { |
| 158 | + return ( |
| 159 | + <span className="font-display text-[1.02rem] leading-none tracking-tight text-foreground"> |
| 160 | + <span className="font-semibold">Ghost</span> |
| 161 | + <span className="font-medium text-foreground/85">Code</span> |
| 162 | + <span className="mx-1 text-muted-foreground/60">·</span> |
| 163 | + <span className="font-medium tracking-[0.02em] text-muted-foreground"> |
| 164 | + Dynamics |
| 165 | + </span> |
| 166 | + </span> |
| 167 | + ); |
| 168 | +} |
| 169 | + |
| 170 | +// Re-export the icon under its previous name to keep older imports working. |
| 171 | +export { LogoMark as GhostIcon }; |
0 commit comments