feat(frontend): show the RD mark beside the storefront wordmark #75
@@ -4,6 +4,7 @@ import { ShoppingCartOutlined, FilterOutlined } from '@ant-design/icons';
|
|||||||
import { Link, useLocation, useSearchParams } from 'react-router-dom';
|
import { Link, useLocation, useSearchParams } from 'react-router-dom';
|
||||||
import { Item, FilterOptions, fetchItems, fetchFilterOptions } from './api';
|
import { Item, FilterOptions, fetchItems, fetchFilterOptions } from './api';
|
||||||
import ItemCard from './components/ItemCard';
|
import ItemCard from './components/ItemCard';
|
||||||
|
import BrandMark from './components/BrandMark';
|
||||||
import FilterDrawer from './components/FilterDrawer';
|
import FilterDrawer from './components/FilterDrawer';
|
||||||
import ActiveFilterChips from './components/ActiveFilterChips';
|
import ActiveFilterChips from './components/ActiveFilterChips';
|
||||||
import {
|
import {
|
||||||
@@ -118,9 +119,15 @@ export default function App() {
|
|||||||
borderBottom: `1px solid ${token.colorBorderSecondary}`
|
borderBottom: `1px solid ${token.colorBorderSecondary}`
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{/* 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 }}>
|
<Title level={3} className="site-header-title" style={{ color: token.colorText }}>
|
||||||
Redefined Designs
|
Redefined Designs
|
||||||
</Title>
|
</Title>
|
||||||
|
</div>
|
||||||
<div className="site-header-actions">
|
<div className="site-header-actions">
|
||||||
<Switch checked={mode === 'dark'} onChange={toggle} checkedChildren="Dark" unCheckedChildren="Light" />
|
<Switch checked={mode === 'dark'} onChange={toggle} checkedChildren="Dark" unCheckedChildren="Light" />
|
||||||
<Link to="/cart">
|
<Link to="/cart">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -45,6 +45,19 @@ body { margin: 0; }
|
|||||||
line-height: normal;
|
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 {
|
.site-header-title {
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
font-size: clamp(16px, 5vw, 24px);
|
font-size: clamp(16px, 5vw, 24px);
|
||||||
|
|||||||
Reference in New Issue
Block a user