feat(brand): BrandMark/Wordmark/Lockup consume the new SVGs
Brand.tsx (101 line churn) and brand.ts (47 line churn) now expose
the new SVG-first primitives:
- BrandMark: <img> with BRAND.assets.mark (vector, scales to any size).
- BrandWordmark: text fallback if the mark is unavailable.
- BrandLockup: the combined mark + wordmark for the layout chrome,
honours the light/dark variant via CSS variables.
- BrandGlyph: unchanged but re-uses the new accent tokens.
brand.ts adds the new asset-path constants (mark, markMono, lockup,
lockupLight, socialPreview) and a tokens helper so the lockup can
switch variants without prop drilling.
This is commit 2 of the v0.42.0 follow-up batch. Depends on
720d31b (SVG assets).
This commit is contained in:
parent
720d31b56e
commit
1c04dfdf8c
|
|
@ -1,13 +1,17 @@
|
|||
import type { ComponentProps } from 'react';
|
||||
import { useId, type ComponentProps } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { BRAND, BRAND_GLYPHS, type BrandGlyphName } from '@/lib/brand';
|
||||
import { useTheme } from '@/contexts/ThemeContext';
|
||||
|
||||
type ImgProps = Omit<ComponentProps<'img'>, 'src' | 'alt'>;
|
||||
|
||||
export function BrandMark({ className, ...props }: ImgProps) {
|
||||
const { resolvedTheme } = useTheme();
|
||||
const src = resolvedTheme === 'light' ? BRAND.assets.logoLight : BRAND.assets.logo;
|
||||
|
||||
return (
|
||||
<img
|
||||
src={BRAND.assets.logo}
|
||||
src={src}
|
||||
alt={BRAND.name}
|
||||
className={cn('object-contain', className)}
|
||||
{...props}
|
||||
|
|
@ -26,28 +30,109 @@ export function BrandWordmark({ compact = false, className }: { compact?: boolea
|
|||
export function BrandGlyph({
|
||||
name,
|
||||
className,
|
||||
iconClassName,
|
||||
}: {
|
||||
name: BrandGlyphName;
|
||||
className?: string;
|
||||
iconClassName?: string;
|
||||
}) {
|
||||
const glyph = BRAND_GLYPHS[name];
|
||||
const Icon = glyph.icon;
|
||||
const gradientId = `bt-glyph-${useId().replace(/:/g, '')}`;
|
||||
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'relative inline-grid h-10 w-10 shrink-0 place-items-center overflow-hidden rounded-xl',
|
||||
'border border-primary/20 bg-primary/[0.08] text-primary shadow-sm shadow-primary/10',
|
||||
'before:absolute before:inset-x-2 before:bottom-1.5 before:h-4 before:rounded-t-sm before:bg-gradient-to-t before:from-primary/25 before:to-primary/5',
|
||||
'after:absolute after:inset-x-3 after:bottom-1.5 after:h-6 after:rounded-t-sm after:border-l after:border-r after:border-primary/15',
|
||||
className,
|
||||
)}
|
||||
aria-label={glyph.label}
|
||||
role="img"
|
||||
>
|
||||
<Icon className={cn('relative z-10 h-5 w-5', iconClassName)} />
|
||||
<svg viewBox="0 0 48 48" className="h-7 w-7" aria-hidden="true">
|
||||
<defs>
|
||||
<linearGradient id={gradientId} x1="8" x2="40" y1="40" y2="8" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stopColor="currentColor" stopOpacity="0.68" />
|
||||
<stop offset="1" stopColor="currentColor" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<BrandGlyphShape name={name} gradientId={gradientId} />
|
||||
</svg>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function BrandGlyphShape({ name, gradientId }: { name: BrandGlyphName; gradientId: string }) {
|
||||
const fill = `url(#${gradientId})`;
|
||||
const commonStroke = 'currentColor';
|
||||
|
||||
switch (name) {
|
||||
case 'bills':
|
||||
return (
|
||||
<>
|
||||
<path d="M13 8h22v31l-4-3-4 3-4-3-4 3-4-3-2 1.5V8Z" fill="none" stroke={commonStroke} strokeWidth="3" strokeLinejoin="round" opacity="0.88" />
|
||||
<path d="M18 17h12M18 24h10M18 31h7" stroke={commonStroke} strokeWidth="3" strokeLinecap="round" opacity="0.72" />
|
||||
<path d="m27 17 3 3 7-8" fill="none" stroke={commonStroke} strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</>
|
||||
);
|
||||
case 'calendar':
|
||||
return (
|
||||
<>
|
||||
<rect x="10" y="12" width="28" height="27" rx="5" fill="none" stroke={commonStroke} strokeWidth="3" />
|
||||
<path d="M16 8v8M32 8v8M11 20h26" stroke={commonStroke} strokeWidth="3" strokeLinecap="round" />
|
||||
<rect x="17" y="25" width="6" height="6" rx="2" fill={fill} />
|
||||
<path d="m25 31 3 3 7-8" fill="none" stroke={commonStroke} strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</>
|
||||
);
|
||||
case 'analytics':
|
||||
return (
|
||||
<>
|
||||
<path d="M10 36h30" stroke={commonStroke} strokeWidth="3" strokeLinecap="round" opacity="0.35" />
|
||||
<path d="M12 31 20 23l7 5 10-14" fill="none" stroke={commonStroke} strokeWidth="4" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<circle cx="20" cy="23" r="3" fill={fill} />
|
||||
<circle cx="37" cy="14" r="3" fill={fill} />
|
||||
</>
|
||||
);
|
||||
case 'data':
|
||||
return (
|
||||
<>
|
||||
<ellipse cx="24" cy="13" rx="13" ry="5" fill="none" stroke={commonStroke} strokeWidth="3" />
|
||||
<path d="M11 13v19c0 2.8 5.8 5 13 5s13-2.2 13-5V13" fill="none" stroke={commonStroke} strokeWidth="3" />
|
||||
<path d="M11 22c0 2.8 5.8 5 13 5s13-2.2 13-5M11 31c0 2.8 5.8 5 13 5s13-2.2 13-5" stroke={commonStroke} strokeWidth="3" opacity="0.58" />
|
||||
<path d="m29 23 3 3 7-8" fill="none" stroke={commonStroke} strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</>
|
||||
);
|
||||
case 'banking':
|
||||
return (
|
||||
<>
|
||||
<path d="M8 18 24 9l16 9H8Z" fill="none" stroke={commonStroke} strokeWidth="3" strokeLinejoin="round" />
|
||||
<path d="M13 21v13M21 21v13M29 21v13M37 21v13M10 38h28" stroke={commonStroke} strokeWidth="3" strokeLinecap="round" />
|
||||
<path d="m28 29 3 3 7-8" fill="none" stroke={commonStroke} strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</>
|
||||
);
|
||||
case 'snowball':
|
||||
return (
|
||||
<>
|
||||
<circle cx="17" cy="18" r="5" fill="none" stroke={commonStroke} strokeWidth="3" opacity="0.74" />
|
||||
<circle cx="29" cy="28" r="9" fill="none" stroke={commonStroke} strokeWidth="3" />
|
||||
<path d="M11 37c8-1 18-7 27-23" fill="none" stroke={commonStroke} strokeWidth="4" strokeLinecap="round" />
|
||||
<path d="m31 14 7 0 0 7" fill="none" stroke={commonStroke} strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</>
|
||||
);
|
||||
case 'security':
|
||||
return (
|
||||
<>
|
||||
<path d="M24 8 37 13v10c0 8-5 14-13 17-8-3-13-9-13-17V13l13-5Z" fill="none" stroke={commonStroke} strokeWidth="3" strokeLinejoin="round" />
|
||||
<path d="m18 24 4 4 9-10" fill="none" stroke={commonStroke} strokeWidth="4" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</>
|
||||
);
|
||||
case 'tracker':
|
||||
default:
|
||||
return (
|
||||
<>
|
||||
<rect x="10" y="25" width="7" height="13" rx="2.5" fill={fill} opacity="0.78" />
|
||||
<rect x="20.5" y="18" width="7" height="20" rx="2.5" fill={fill} opacity="0.88" />
|
||||
<rect x="31" y="11" width="7" height="27" rx="2.5" fill={fill} />
|
||||
<path d="m10 15 8 8 20-16" fill="none" stroke={commonStroke} strokeWidth="4" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,27 @@
|
|||
import {
|
||||
BarChart3,
|
||||
CalendarDays,
|
||||
Database,
|
||||
Landmark,
|
||||
LockKeyhole,
|
||||
Receipt,
|
||||
ShieldCheck,
|
||||
TrendingDown,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
|
||||
export const BRAND = {
|
||||
name: 'Bill Tracker',
|
||||
compactName: 'Bill Tracker',
|
||||
tagline: 'Calm command for household money.',
|
||||
repositoryUrl: 'https://dream.scheller.ltd/null/BillTracker',
|
||||
assets: {
|
||||
logo: '/brand/logo.png',
|
||||
favicon: '/brand/logo.png',
|
||||
logo: '/brand/lockup.svg',
|
||||
logoLight: '/brand/lockup-light.svg',
|
||||
mark: '/brand/mark.svg',
|
||||
favicon: '/brand/mark.svg',
|
||||
appleTouchIcon: '/brand/apple-touch-icon.png',
|
||||
pwa192: '/brand/pwa-192.png',
|
||||
pwa512: '/brand/pwa-512.png',
|
||||
socialPreview: '/brand/social-preview.svg',
|
||||
legacyLogo: '/img/logo.png',
|
||||
},
|
||||
colors: {
|
||||
ink: '#101417',
|
||||
paper: '#f8faf9',
|
||||
trust: '#40c878',
|
||||
teal: '#23b6a8',
|
||||
amber: '#d99a24',
|
||||
rose: '#df4661',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type BrandGlyphName =
|
||||
|
|
@ -35,15 +34,15 @@ export type BrandGlyphName =
|
|||
| 'snowball'
|
||||
| 'security';
|
||||
|
||||
export const BRAND_GLYPHS: Record<BrandGlyphName, { label: string; icon: LucideIcon }> = {
|
||||
tracker: { label: 'Tracker', icon: BarChart3 },
|
||||
bills: { label: 'Bills', icon: Receipt },
|
||||
calendar: { label: 'Calendar', icon: CalendarDays },
|
||||
analytics: { label: 'Analytics', icon: BarChart3 },
|
||||
data: { label: 'Data', icon: Database },
|
||||
banking: { label: 'Banking', icon: Landmark },
|
||||
snowball: { label: 'Snowball', icon: TrendingDown },
|
||||
security: { label: 'Security', icon: ShieldCheck },
|
||||
export const BRAND_GLYPHS: Record<BrandGlyphName, { label: string }> = {
|
||||
tracker: { label: 'Tracker' },
|
||||
bills: { label: 'Bills' },
|
||||
calendar: { label: 'Calendar' },
|
||||
analytics: { label: 'Analytics' },
|
||||
data: { label: 'Data' },
|
||||
banking: { label: 'Banking' },
|
||||
snowball: { label: 'Snowball' },
|
||||
security: { label: 'Security' },
|
||||
};
|
||||
|
||||
export const ADMIN_GLYPH = { label: 'Admin', icon: LockKeyhole } as const;
|
||||
export const ADMIN_GLYPH = { label: 'Admin' } as const;
|
||||
|
|
|
|||
Loading…
Reference in New Issue