feat(vip): ✨ Add VIP quote page component and route to /quotes/vip in index.html
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
dba94b6dad
commit
87faa3367e
2 changed files with 130 additions and 3 deletions
|
|
@ -53,6 +53,85 @@
|
|||
.vip-personal-note { font-size: 24px; max-width: 100%; }
|
||||
}
|
||||
|
||||
/* vip-text-well: prose has tighter measure than the wide container */
|
||||
.vip-section-body { max-width: 720px; margin: 0 auto; }
|
||||
.vip-section-hero .vip-doc-intro { max-width: 640px; }
|
||||
/* but the option-card grid and the table get the full container width */
|
||||
.vip-section-body .vip-pricing,
|
||||
.vip-section-body .vip-table-wrap { max-width: none; }
|
||||
/* Salutation spacing */
|
||||
.vip-doc-intro .vip-p:first-child { margin-bottom: 6px; }
|
||||
|
||||
/* vip-section-nav */
|
||||
.vip-section-nav {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
right: 28px;
|
||||
transform: translateY(-50%);
|
||||
z-index: 10;
|
||||
padding: 14px 0;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.vip-section-nav-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
.vip-section-nav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
text-decoration: none;
|
||||
color: rgba(212,175,55,0.55);
|
||||
font-family: 'Menlo', 'Monaco', monospace;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
transition: color 220ms ease, transform 220ms ease;
|
||||
}
|
||||
.vip-section-nav-link::before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 18px;
|
||||
height: 1px;
|
||||
background: currentColor;
|
||||
opacity: 0.6;
|
||||
transition: width 280ms ease;
|
||||
}
|
||||
.vip-section-nav-item.is-active .vip-section-nav-link {
|
||||
color: #D4AF37;
|
||||
}
|
||||
.vip-section-nav-item.is-active .vip-section-nav-link::before {
|
||||
width: 40px;
|
||||
opacity: 1;
|
||||
}
|
||||
.vip-section-nav-link:hover { color: #D4AF37; }
|
||||
.vip-section-nav-index {
|
||||
flex: 0 0 auto;
|
||||
opacity: 0.7;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.vip-section-nav-label {
|
||||
max-width: 140px;
|
||||
opacity: 0;
|
||||
transform: translateX(-4px);
|
||||
transition: opacity 220ms ease, transform 220ms ease;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.vip-section-nav:hover .vip-section-nav-label,
|
||||
.vip-section-nav-item.is-active .vip-section-nav-label {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
@media (max-width: 980px) {
|
||||
.vip-section-nav { display: none; }
|
||||
}
|
||||
|
||||
/* vip-parallax: section bands, ghost numerals, scroll reveal */
|
||||
.vip-doc {
|
||||
display: flex;
|
||||
|
|
@ -69,7 +148,7 @@
|
|||
}
|
||||
.vip-section-inner {
|
||||
width: 100%;
|
||||
max-width: 760px;
|
||||
max-width: 920px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
|
|
|||
|
|
@ -371,7 +371,8 @@ export function QuotePage(): ReactElement {
|
|||
const { intro, sections } = parseDocSections(doc.body);
|
||||
return (
|
||||
<div className="vip-doc">
|
||||
<section className="vip-section vip-section-hero">
|
||||
<SectionNav items={sections.map((s, i) => ({ id: `section-${i + 1}`, label: s.heading }))} />
|
||||
<section className="vip-section vip-section-hero" id="section-hero">
|
||||
<div className="vip-section-inner">
|
||||
<div className="vip-stamp vip-stamp-hero">
|
||||
<span>Quote №{state.quote.version}</span>
|
||||
|
|
@ -389,6 +390,7 @@ export function QuotePage(): ReactElement {
|
|||
{sections.map((s, i) => (
|
||||
<section
|
||||
key={i}
|
||||
id={`section-${i + 1}`}
|
||||
className={`vip-section vip-section-band vip-section-${i % 2 === 0 ? 'b' : 'a'}`}
|
||||
data-revealable
|
||||
>
|
||||
|
|
@ -435,6 +437,52 @@ interface MdBlock {
|
|||
rows?: InlineToken[][][];
|
||||
}
|
||||
|
||||
interface SectionNavItem { id: string; label: string }
|
||||
|
||||
function SectionNav({ items }: { items: SectionNavItem[] }): ReactElement {
|
||||
const [active, setActive] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof IntersectionObserver === 'undefined') return;
|
||||
const nodes = items
|
||||
.map((it) => document.getElementById(it.id))
|
||||
.filter((n): n is HTMLElement => n !== null);
|
||||
if (nodes.length === 0) return;
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const visible = entries
|
||||
.filter((e) => e.isIntersecting)
|
||||
.sort((a, b) => b.intersectionRatio - a.intersectionRatio);
|
||||
if (visible.length > 0) setActive(visible[0]!.target.id);
|
||||
},
|
||||
{ rootMargin: '-30% 0px -55% 0px', threshold: [0, 0.25, 0.5, 0.75, 1] },
|
||||
);
|
||||
for (const n of nodes) observer.observe(n);
|
||||
return () => observer.disconnect();
|
||||
}, [items]);
|
||||
|
||||
const handleJump = (e: React.MouseEvent<HTMLAnchorElement>, id: string): void => {
|
||||
e.preventDefault();
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="vip-section-nav" aria-label="Sections">
|
||||
<ol className="vip-section-nav-list">
|
||||
{items.map((it, i) => (
|
||||
<li key={it.id} className={`vip-section-nav-item${active === it.id ? ' is-active' : ''}`}>
|
||||
<a href={`#${it.id}`} onClick={(e) => handleJump(e, it.id)} className="vip-section-nav-link">
|
||||
<span className="vip-section-nav-index">{String(i + 1).padStart(2, '0')}</span>
|
||||
<span className="vip-section-nav-label">{it.label}</span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
interface QuoteDocParts {
|
||||
title: string;
|
||||
subtitle: string | null;
|
||||
|
|
@ -780,7 +828,7 @@ const rootStyle: React.CSSProperties = {
|
|||
justifyContent: 'center',
|
||||
padding: '64px 24px 96px',
|
||||
position: 'relative',
|
||||
fontFamily: "'system-ui', -apple-system, sans-serif",
|
||||
fontFamily: "'Inter', system-ui, -apple-system, sans-serif",
|
||||
color: G.cream,
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue