Skip to content

Commit 69469c6

Browse files
author
Tuola-waj
authored
feat(landing-page): add HTML Anything page and responsive header (#2452)
1 parent 0ee363f commit 69469c6

4 files changed

Lines changed: 1111 additions & 10 deletions

File tree

apps/landing-page/app/_components/header-enhancer.astro

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,35 @@
4141
);
4242
}
4343

44+
// Hamburger menu toggle. Active only at narrow viewports (CSS hides
45+
// the toggle button at ≥1100px). Click toggles `.is-open` on the
46+
// header; outside-click, Escape, and clicking any link inside the
47+
// menu close it again. Keeps `aria-expanded` in sync.
48+
const toggle = document.querySelector('[data-nav-toggle]');
49+
const primaryNav = document.querySelector('[data-nav-primary]');
50+
if (toggle && primaryNav && nav) {
51+
const setOpen = (open) => {
52+
nav.classList.toggle('is-open', open);
53+
toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
54+
};
55+
toggle.addEventListener('click', (ev) => {
56+
ev.stopPropagation();
57+
setOpen(!nav.classList.contains('is-open'));
58+
});
59+
// Close when clicking any actual nav link (but not the Product
60+
// parent trigger — its href='/' is a real page link, so we let it
61+
// navigate which closes the menu naturally).
62+
primaryNav.querySelectorAll('a').forEach((link) => {
63+
link.addEventListener('click', () => setOpen(false));
64+
});
65+
document.addEventListener('click', (ev) => {
66+
if (!nav.contains(ev.target)) setOpen(false);
67+
});
68+
document.addEventListener('keydown', (ev) => {
69+
if (ev.key === 'Escape') setOpen(false);
70+
});
71+
}
72+
4473
const starSlots = document.querySelectorAll('[data-github-stars]');
4574
if (starSlots.length > 0) {
4675
fetch(REPO_API, {

apps/landing-page/app/_components/header.tsx

Lines changed: 82 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,15 @@ const ext = {
2020

2121
export interface HeaderProps {
2222
/** Nav highlight target. `'home'` is the default for `/`. */
23-
active?: 'home' | 'skills' | 'systems' | 'templates' | 'craft' | 'tutorials' | 'blog';
23+
active?:
24+
| 'home'
25+
| 'product'
26+
| 'html-anything'
27+
| 'skills'
28+
| 'systems'
29+
| 'templates'
30+
| 'craft'
31+
| 'blog';
2432
/**
2533
* Live counts from the Markdown catalogs. Required so we can never
2634
* silently render stale fallback numbers when a caller forgets to
@@ -62,8 +70,80 @@ export function Header({
6270
<b>Studio Nº 01</b>Berlin / Open / Earth
6371
</span>
6472
</a>
65-
<nav>
73+
{/*
74+
Mobile / tablet hamburger. Hidden by CSS at ≥1100px (the desktop
75+
breakpoint where the full nav fits). At narrower widths it toggles
76+
`.is-open` on the parent <header> via a small handler in
77+
`header-enhancer.astro` — when open, the `<nav>` element below
78+
drops down underneath the header bar as a vertical list.
79+
*/}
80+
<button
81+
type='button'
82+
className='nav-toggle'
83+
aria-label='Toggle navigation menu'
84+
aria-controls='primary-nav'
85+
aria-expanded='false'
86+
data-nav-toggle
87+
>
88+
<span className='nav-toggle-icon' aria-hidden='true' />
89+
</button>
90+
<nav id='primary-nav' data-nav-primary>
6691
<ul className='nav-links'>
92+
<li className='has-dropdown'>
93+
{/*
94+
Product menu — top-level group exposing the Open Design family.
95+
CSS-only dropdown via :hover / :focus-within (no JS), so this
96+
still renders correctly under static export with no React
97+
runtime on the client. The trigger is a focusable <a> rather
98+
than a button so it remains a keyboard tab stop, with
99+
aria-haspopup signaling the submenu to assistive tech.
100+
*/}
101+
<a
102+
href='/'
103+
className={
104+
active === 'product' ||
105+
active === 'home' ||
106+
active === 'html-anything'
107+
? 'is-active'
108+
: undefined
109+
}
110+
aria-haspopup='true'
111+
aria-expanded='false'
112+
>
113+
Product
114+
<span className='dropdown-caret' aria-hidden='true'></span>
115+
</a>
116+
<ul className='nav-dropdown' role='menu'>
117+
<li role='none'>
118+
<a
119+
role='menuitem'
120+
href='/'
121+
className={
122+
active === 'home' || active === 'product'
123+
? 'is-active'
124+
: undefined
125+
}
126+
>
127+
<span className='dropdown-name'>Open Design</span>
128+
<span className='dropdown-blurb'>
129+
The agentic design surface — skills, systems, templates.
130+
</span>
131+
</a>
132+
</li>
133+
<li role='none'>
134+
<a
135+
role='menuitem'
136+
href='/html-anything/'
137+
className={linkClass('html-anything')}
138+
>
139+
<span className='dropdown-name'>HTML Anything</span>
140+
<span className='dropdown-blurb'>
141+
Markdown / data → ship-ready HTML, by your local agent.
142+
</span>
143+
</a>
144+
</li>
145+
</ul>
146+
</li>
67147
<li>
68148
<a href='/skills/' className={linkClass('skills')}>
69149
Skills<span className='num'>{counts.skills}</span>
@@ -84,11 +164,6 @@ export function Header({
84164
Craft<span className='num'>{counts.craft}</span>
85165
</a>
86166
</li>
87-
<li>
88-
<a href='/tutorials/' className={linkClass('tutorials')}>
89-
Tutorials
90-
</a>
91-
</li>
92167
<li>
93168
<a href='/blog/' className={linkClass('blog')}>
94169
Blog

apps/landing-page/app/globals.css

Lines changed: 220 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,140 @@ body::before {
283283
right: -16px;
284284
letter-spacing: 0.04em;
285285
}
286+
287+
/*
288+
* Product dropdown — CSS-only, no JS. The submenu is hidden by default and
289+
* revealed on `:hover` of the parent `li.has-dropdown`, or on `:focus-within`
290+
* for keyboard navigation. This keeps the static export self-contained.
291+
*/
292+
.nav-links li.has-dropdown {
293+
position: relative;
294+
}
295+
.nav-links .dropdown-caret {
296+
display: inline-block;
297+
margin-left: 4px;
298+
font-size: 10px;
299+
color: var(--ink-faint);
300+
transition: transform 0.18s ease;
301+
}
302+
.nav-links li.has-dropdown:hover .dropdown-caret,
303+
.nav-links li.has-dropdown:focus-within .dropdown-caret {
304+
transform: translateY(1px);
305+
color: var(--coral);
306+
}
307+
.nav-dropdown {
308+
position: absolute;
309+
top: 100%;
310+
left: -16px;
311+
margin: 0;
312+
padding: 10px;
313+
list-style: none;
314+
min-width: 320px;
315+
background: var(--paper);
316+
border: 1px solid rgba(26, 26, 26, 0.12);
317+
border-radius: 8px;
318+
box-shadow: 0 12px 36px rgba(26, 26, 26, 0.08);
319+
opacity: 0;
320+
visibility: hidden;
321+
transform: translateY(-4px);
322+
transition: opacity 0.18s ease, transform 0.18s ease, visibility 0.18s;
323+
z-index: 50;
324+
}
325+
.nav-links li.has-dropdown:hover .nav-dropdown,
326+
.nav-links li.has-dropdown:focus-within .nav-dropdown {
327+
opacity: 1;
328+
visibility: visible;
329+
transform: translateY(0);
330+
}
331+
/* invisible bridge so cursor can travel from trigger to menu without
332+
* crossing a gap that closes the menu mid-traverse */
333+
.nav-dropdown::before {
334+
content: '';
335+
position: absolute;
336+
top: -10px;
337+
left: 0;
338+
right: 0;
339+
height: 10px;
340+
}
341+
.nav-dropdown a {
342+
display: block;
343+
padding: 10px 12px;
344+
border-radius: 6px;
345+
color: var(--ink);
346+
text-decoration: none;
347+
transition: background 0.12s ease;
348+
}
349+
.nav-dropdown a:hover {
350+
background: rgba(255, 122, 89, 0.08);
351+
color: var(--ink);
352+
}
353+
.nav-dropdown a.is-active {
354+
background: rgba(26, 26, 26, 0.04);
355+
}
356+
.nav-dropdown .dropdown-name {
357+
display: block;
358+
font-family: var(--sans);
359+
font-size: 14px;
360+
font-weight: 600;
361+
margin-bottom: 2px;
362+
}
363+
.nav-dropdown .dropdown-blurb {
364+
display: block;
365+
font-size: 12px;
366+
color: var(--ink-faint);
367+
line-height: 1.4;
368+
}
369+
370+
/*
371+
* Hamburger toggle. Hidden at desktop widths (≥1080px). At narrower
372+
* viewports it replaces the inline nav-links and toggles a panel
373+
* underneath the header bar via a small handler in `header-enhancer`.
374+
*/
375+
.nav-toggle {
376+
display: none;
377+
align-items: center;
378+
justify-content: center;
379+
width: 38px;
380+
height: 38px;
381+
margin-left: auto;
382+
padding: 0;
383+
background: transparent;
384+
border: 1px solid rgba(26, 26, 26, 0.18);
385+
border-radius: 8px;
386+
cursor: pointer;
387+
color: var(--ink);
388+
transition: background 0.18s ease, border-color 0.18s ease;
389+
}
390+
.nav-toggle:hover {
391+
background: rgba(26, 26, 26, 0.04);
392+
border-color: rgba(26, 26, 26, 0.32);
393+
}
394+
.nav-toggle-icon,
395+
.nav-toggle-icon::before,
396+
.nav-toggle-icon::after {
397+
display: block;
398+
width: 18px;
399+
height: 2px;
400+
background: currentColor;
401+
border-radius: 2px;
402+
transition: transform 0.18s ease, opacity 0.18s ease;
403+
}
404+
.nav-toggle-icon {
405+
position: relative;
406+
}
407+
.nav-toggle-icon::before,
408+
.nav-toggle-icon::after {
409+
content: '';
410+
position: absolute;
411+
left: 0;
412+
}
413+
.nav-toggle-icon::before { top: -6px; }
414+
.nav-toggle-icon::after { top: 6px; }
415+
/* When open, morph into an "X" */
416+
.nav.is-open .nav-toggle-icon { background: transparent; }
417+
.nav.is-open .nav-toggle-icon::before { top: 0; transform: rotate(45deg); }
418+
.nav.is-open .nav-toggle-icon::after { top: 0; transform: rotate(-45deg); }
419+
286420
.nav-side {
287421
display: inline-flex;
288422
align-items: center;
@@ -1936,14 +2070,94 @@ footer {
19362070
@media (max-width: 1200px) {
19372071
.topbar-inner .mid { display: none; }
19382072
}
1939-
/* nav: between 1080 and 1180 the brand tail + 5 nav links + 2 CTAs + dot
2073+
/* nav: between 1080 and 1180 the brand tail + nav links + 2 CTAs + dot
19402074
* crowd the row. Drop the brand sub-meta first, then tighten link spacing,
19412075
* so the Star CTA never has to compress. */
19422076
@media (max-width: 1180px) {
19432077
.nav-inner { gap: 18px; }
19442078
.brand-meta { display: none; }
19452079
.nav-links { gap: 28px; }
19462080
}
2081+
2082+
/* nav: at ≤1080px there's no clean way to fit Product · Skills · Systems ·
2083+
* Templates · Craft · Blog · Contact + Download + Star + dot in one row,
2084+
* so swap to a hamburger panel. Star CTA stays visible in the bar (it's
2085+
* the social-proof CTA); Download moves into the panel. The Product
2086+
* submenu flattens from a hover-popup to an always-expanded inline list,
2087+
* since hover doesn't translate to touch.
2088+
*/
2089+
@media (max-width: 1080px) {
2090+
.nav-toggle { display: inline-flex; }
2091+
.brand { white-space: nowrap; }
2092+
/* Hide Download from the bar (Star stays). */
2093+
.nav-side .nav-cta.ghost { display: none; }
2094+
/* Collapse the nav <ul> into a panel that drops below the header bar.
2095+
* The header is `position: sticky`, so absolute-positioning the panel
2096+
* relative to the header element keeps it pinned correctly. */
2097+
.nav { position: sticky; }
2098+
.nav > .nav-inner > nav[data-nav-primary] {
2099+
position: absolute;
2100+
top: 100%;
2101+
left: 0;
2102+
right: 0;
2103+
margin: 0;
2104+
padding: 0;
2105+
pointer-events: none;
2106+
opacity: 0;
2107+
visibility: hidden;
2108+
transform: translateY(-6px);
2109+
transition: opacity 0.2s ease, transform 0.2s ease, visibility 0.2s;
2110+
background: var(--paper);
2111+
border-top: 1px solid rgba(26, 26, 26, 0.08);
2112+
box-shadow: 0 12px 32px rgba(26, 26, 26, 0.06);
2113+
}
2114+
.nav.is-open > .nav-inner > nav[data-nav-primary] {
2115+
pointer-events: auto;
2116+
opacity: 1;
2117+
visibility: visible;
2118+
transform: translateY(0);
2119+
}
2120+
.nav-links {
2121+
flex-direction: column;
2122+
align-items: stretch;
2123+
gap: 0;
2124+
padding: 12px 24px 20px 24px;
2125+
}
2126+
.nav-links li {
2127+
border-bottom: 1px solid rgba(26, 26, 26, 0.06);
2128+
}
2129+
.nav-links li:last-child { border-bottom: none; }
2130+
.nav-links a {
2131+
display: block;
2132+
padding: 14px 0;
2133+
font-size: 16px;
2134+
}
2135+
.nav-links a .num {
2136+
position: static;
2137+
margin-left: 6px;
2138+
font-size: 11px;
2139+
}
2140+
/* Product dropdown flattens — always show OD + HA as nested static items. */
2141+
.nav-links li.has-dropdown { position: static; }
2142+
.nav-links .dropdown-caret { display: none; }
2143+
.nav-dropdown {
2144+
position: static;
2145+
opacity: 1;
2146+
visibility: visible;
2147+
transform: none;
2148+
margin: 0 0 4px 0;
2149+
padding: 0 0 4px 16px;
2150+
border: none;
2151+
border-left: 2px solid rgba(26, 26, 26, 0.08);
2152+
border-radius: 0;
2153+
box-shadow: none;
2154+
background: transparent;
2155+
min-width: 0;
2156+
}
2157+
.nav-dropdown::before { display: none; }
2158+
.nav-dropdown a { padding: 8px 8px; border-radius: 4px; }
2159+
.nav-dropdown .dropdown-blurb { display: none; }
2160+
}
19472161
@media (max-width: 1080px) {
19482162
.container { padding: 0 32px; }
19492163
.hero h1 { font-size: clamp(36px, 4.6vw, 54px); }
@@ -2024,9 +2238,12 @@ footer {
20242238
.foot-grid { grid-template-columns: 1fr 1fr; gap: 32px; }
20252239
.foot-bottom { flex-direction: column; align-items: flex-start; gap: 12px; }
20262240
.foot-bottom .right { flex-wrap: wrap; gap: 12px 20px; }
2027-
/* nav */
2241+
/* nav — at ≤880px tighten padding; nav-links stay reachable through
2242+
* the hamburger panel introduced at ≤1080px. Brand meta and Download
2243+
* stay hidden; Star CTA still pings in the bar. */
20282244
.nav { padding: 16px 0; }
2029-
.nav-links, .brand-meta, .nav-cta { display: none; }
2245+
.brand-meta { display: none; }
2246+
.nav-side .nav-cta.ghost { display: none; }
20302247
}
20312248
@media (max-width: 640px) {
20322249
.topbar-inner { gap: 14px; }

0 commit comments

Comments
 (0)