feat(frontend): show the RD mark beside the storefront wordmark
SonarQube Analysis / sonarqube (pull_request) Failing after 15m22s
Tests / lint (pull_request) Successful in 2m8s
Tests / backend-unit (pull_request) Successful in 44s
Tests / frontend-e2e (pull_request) Failing after 10m12s

Puts the monogram to the left of "Redefined Designs" in the site header.

Inline SVG rather than an <img src="/favicon.svg"> for two reasons. The tile is drawn in currentColor so it follows the antd text token and inverts with the app's own theme switch — the favicon file inverts on prefers-color-scheme, which tracks the operating system and would disagree with the app whenever someone toggles the theme themselves. And the letters are knocked out with a mask rather than painted in the background colour, so the mark sits correctly on any surface instead of carrying a backdrop that only matches this header.

The mask id is generated per instance, since two marks on one page would otherwise share an id and the second would reference the first. It is stripped to alphanumerics because React's generated ids contain colons.

Marked aria-hidden: the wordmark beside it already says the name, so announcing it again would be noise.

Verified by screenshotting the header in both themes rather than by reasoning about the colours.
This commit is contained in:
2026-08-20 10:55:47 -05:00
parent 0587c18307
commit 7b89023d5e
3 changed files with 88 additions and 3 deletions
+10 -3
View File
@@ -4,6 +4,7 @@ import { ShoppingCartOutlined, FilterOutlined } from '@ant-design/icons';
import { Link, useLocation, useSearchParams } from 'react-router-dom';
import { Item, FilterOptions, fetchItems, fetchFilterOptions } from './api';
import ItemCard from './components/ItemCard';
import BrandMark from './components/BrandMark';
import FilterDrawer from './components/FilterDrawer';
import ActiveFilterChips from './components/ActiveFilterChips';
import {
@@ -118,9 +119,15 @@ export default function App() {
borderBottom: `1px solid ${token.colorBorderSecondary}`
}}
>
<Title level={3} className="site-header-title" style={{ color: token.colorText }}>
Redefined Designs
</Title>
{/* Wrapped so the mark and the wordmark travel together — the title
keeps its own ellipsis behaviour when the header gets narrow, and the
mark is never what gets truncated. */}
<div className="site-header-brand" style={{ color: token.colorText }}>
<BrandMark size={28} />
<Title level={3} className="site-header-title" style={{ color: token.colorText }}>
Redefined Designs
</Title>
</div>
<div className="site-header-actions">
<Switch checked={mode === 'dark'} onChange={toggle} checkedChildren="Dark" unCheckedChildren="Light" />
<Link to="/cart">
+65
View File
@@ -0,0 +1,65 @@
import { useId } from 'react';
interface Props {
size?: number;
className?: string;
}
/**
* The RD monogram, as an inline SVG rather than an `<img src="/favicon.svg">`.
*
* Two reasons it has to be inline. The tile is drawn in `currentColor`, so it
* follows the antd text token and inverts with the app's own theme switch — the
* favicon file inverts on `prefers-color-scheme`, which tracks the operating
* system and would disagree with the app whenever someone toggles the theme
* themselves. And the letters are knocked out with a mask rather than painted in
* the background colour, so the mark sits correctly on any surface instead of
* carrying a hardcoded backdrop that only matches the header.
*
* Decorative: the header already says "Redefined Designs" beside it, so it is
* hidden from assistive technology rather than read out twice.
*/
export default function BrandMark({ size = 28, className }: Props) {
// Unique per instance — two marks on one page would otherwise share a mask id
// and the second would reference the first. The id is stripped to alphanumerics
// because React's generated ids contain colons.
const maskId = `rd-knockout-${useId().replace(/[^a-zA-Z0-9]/g, '')}`;
return (
<svg
className={className}
width={size}
height={size}
viewBox="0 0 64 64"
aria-hidden="true"
focusable="false"
>
<mask id={maskId}>
{/* White shows the tile, black punches the letters through it. */}
<rect x="2" y="2" width="60" height="60" rx="15" fill="#fff" />
<g
fill="none"
stroke="#000"
strokeWidth="6.5"
strokeLinecap="butt"
strokeLinejoin="miter"
>
<path d="M15 18 V46" />
<path d="M15 18 H20 A7 7 0 0 1 20 32 H15" />
<path d="M20 32 L27 46" />
<path d="M37 18 V46" />
<path d="M37 18 H38 A14 14 0 0 1 38 46 H37" />
</g>
</mask>
<rect
x="2"
y="2"
width="60"
height="60"
rx="15"
fill="currentColor"
mask={`url(#${maskId})`}
/>
</svg>
);
}
+13
View File
@@ -45,6 +45,19 @@ body { margin: 0; }
line-height: normal;
}
.site-header-brand {
display: flex;
align-items: center;
gap: 10px;
flex: 1 1 auto;
min-width: 0;
}
/* The mark holds its size while the wordmark absorbs the squeeze. */
.site-header-brand svg {
flex: none;
}
.site-header-title {
margin: 0 !important;
font-size: clamp(16px, 5vw, 24px);