Skip to content

Commit 648a911

Browse files
committed
Fix mobile menus and replace glyph icons with SVG
1 parent 0219ad8 commit 648a911

20 files changed

Lines changed: 165 additions & 40 deletions

AGENTS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ The learn articles are a friendly guide, not a reference manual. Match the voice
2222
- Section headings are `#` (H1), subsections `##`. Unusual, but consistent across the directory.
2323
- Show code early and keep examples focused on one idea at a time.
2424
- Tables are fine — and the one place terse language is welcome — for attribute and parameter reference.
25-
- End with `Continue to the [next topic ▸](/learn/{{ page.next_topic }})` when `next_topic` is set.
25+
- Symbols like ``, ``, and `` aren't in the body font, so the OS substitutes one and iOS renders some as color emoji. Draw arrows and icons in CSS or inline SVG instead of typing the character.
26+
- End with `Continue to the [next topic](/learn/{{ page.next_topic }}){:.next-topic}` when `next_topic` is set.
2627

2728
When in doubt, read a neighboring article and trust your ear. If it sounds like a person talking, you're there.

_css/app.css

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,34 @@ video.sprite-video {
362362
transform: scaleX(1);
363363
}
364364

365+
/* The mobile menu overlays the page rather than pushing it down, so it gets a
366+
shadow to sit above the content instead of reading as part of it. */
367+
#mobile-menu {
368+
box-shadow: 0 12px 30px rgba(0,0,0,.10);
369+
}
370+
371+
/* Mobile nav links: a stacked list, so the current page is marked the way the
372+
learn sidebar does it rather than with the desktop underline. */
373+
.mobile-nav-link {
374+
display: block;
375+
padding: 0.5rem 0.75rem;
376+
border-radius: 0.75rem;
377+
color: var(--text-secondary);
378+
text-decoration: none;
379+
font-size: 0.875rem;
380+
font-weight: 600;
381+
transition: color 0.2s, background-color 0.2s;
382+
}
383+
.mobile-nav-link:hover {
384+
color: var(--ruby-red);
385+
background: var(--ruby-glow);
386+
}
387+
.mobile-nav-link.current {
388+
color: var(--ruby-red);
389+
background: var(--ruby-glow);
390+
font-weight: 700;
391+
}
392+
365393
/* --- Sidebar (learn pages) ----------------------------------------------- */
366394
.sidebar-link {
367395
display: block;
@@ -391,6 +419,27 @@ video.sprite-video {
391419
-webkit-user-select: none;
392420
user-select: none;
393421
}
422+
/* The "next topic" arrow at the foot of each learn page. Drawn with borders for
423+
the same reason as the Contents caret below: Outfit carries no Geometric
424+
Shapes glyphs, so a typed ▸ falls back to whatever font the OS picks. */
425+
.next-topic::after {
426+
content: '';
427+
display: inline-block;
428+
vertical-align: middle;
429+
margin-left: 0.35em;
430+
margin-bottom: 0.08em;
431+
border-top: 0.28em solid transparent;
432+
border-bottom: 0.28em solid transparent;
433+
border-left: 0.32em solid currentColor;
434+
}
435+
436+
/* It's reachable by keyboard now, so focus has to be visible when it lands. */
437+
/* Inset, because the button now runs to the card's edge and an outset ring
438+
would draw outside it. The outline follows the button's own border-radius. */
439+
#learn-contents-menu:focus-visible {
440+
outline: 2px solid var(--ruby-red);
441+
outline-offset: -3px;
442+
}
394443
#learn-contents-menu-links {
395444
display: block;
396445
}
@@ -732,6 +781,9 @@ video.sprite-video {
732781
margin-top: 1.25rem;
733782
}
734783
.try-step-btn {
784+
display: inline-flex;
785+
align-items: center;
786+
gap: 0.4rem;
735787
padding: 0.4rem 1.1rem;
736788
border-radius: 9999px;
737789
font-size: 0.875rem;
@@ -863,8 +915,35 @@ video.sprite-video {
863915
padding: 0.75rem;
864916
background: var(--surface-card);
865917
}
918+
/* The whole top strip of the card toggles the menu, not just the word itself.
919+
The negative margin bleeds the button out over the card's padding and the
920+
matching padding puts the label back exactly where it sat, so the target
921+
grows from the height of the text to a comfortable ~42px without anything
922+
moving. */
923+
.learn-sidebar #learn-contents-menu {
924+
display: block;
925+
width: calc(100% + 1.5rem);
926+
margin: -0.75rem;
927+
padding: 0.75rem 1rem;
928+
border-radius: 0.85rem;
929+
}
930+
931+
/* Drawn with borders rather than set as ▾/▴: Outfit has no Geometric Shapes
932+
glyphs, so those fall back per-character and the two arrows end up coming
933+
from different fonts at different weights. One shape rotated can't mismatch. */
866934
.learn-sidebar #learn-contents-menu::after {
867-
content: ' \25BE';
935+
content: '';
936+
display: inline-block;
937+
vertical-align: middle;
938+
margin-left: 0.4em;
939+
margin-bottom: 0.1em;
940+
border-left: 0.3em solid transparent;
941+
border-right: 0.3em solid transparent;
942+
border-top: 0.35em solid currentColor;
943+
transition: transform 0.2s cubic-bezier(.4,.2,.2,1);
944+
}
945+
.learn-sidebar #learn-contents-menu[aria-expanded="true"]::after {
946+
transform: rotate(180deg);
868947
}
869948
#learn-contents-menu-links {
870949
display: none;

_layouts/learn.html

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ <h2 class="text-xs font-bold uppercase tracking-widest text-[var(--text-muted)]
2727

2828
<!-- Mobile sidebar -->
2929
<div class="md:hidden learn-sidebar mb-6">
30-
<h2 id="learn-contents-menu" class="text-sm font-bold text-[var(--text-secondary)] mt-0 mb-0 px-1">Contents</h2>
30+
<h2 class="text-sm font-bold text-[var(--text-secondary)] mt-0 mb-0">
31+
<button id="learn-contents-menu" type="button" class="text-left" aria-controls="learn-contents-menu-links" aria-expanded="false">Contents</button>
32+
</h2>
3133
<div id="learn-contents-menu-links" class="flex flex-col gap-0.5 mt-2">
3234
<a class="sidebar-link {% if page.id == '/learn/get-started' %}current{% endif %}" href="/learn/get-started">Get started</a>
3335
<a class="sidebar-link {% if page.id == '/learn/window' %}current{% endif %}" href="/learn/window">Window</a>
@@ -70,11 +72,21 @@ <h1 class="title text-3xl sm:text-4xl font-bold mt-0 mb-2 border-none pb-0">{{ p
7072
var contentsMenu = document.getElementById('learn-contents-menu');
7173
var contentsLinks = document.getElementById('learn-contents-menu-links');
7274
if (contentsMenu && contentsLinks) {
73-
contentsMenu.onclick = function() {
74-
contentsLinks.classList.toggle('is-expanded');
75+
// aria-expanded is the single source of truth for the open state: the caret
76+
// in the header is drawn off it, so there's no second flag to keep in sync.
77+
var setContents = function(open) {
78+
contentsLinks.classList.toggle('is-expanded', open);
79+
contentsMenu.setAttribute('aria-expanded', open ? 'true' : 'false');
7580
};
81+
// A real <button>, so Enter and Space come for free.
82+
contentsMenu.addEventListener('click', function() {
83+
setContents(!contentsLinks.classList.contains('is-expanded'));
84+
});
85+
// Only collapse when we've grown to the desktop sidebar. Mobile browsers fire
86+
// `resize` while scrolling (the URL bar hides and shows), and that shouldn't
87+
// close a menu the reader is in the middle of scrolling through.
7688
window.addEventListener('resize', function() {
77-
contentsLinks.classList.remove('is-expanded');
89+
if (window.innerWidth >= 768) setContents(false);
7890
});
7991
}
8092
</script>

_layouts/main.html

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@
3232
</head>
3333

3434
<body class="min-h-screen relative">
35+
<!-- Which nav section we're in, worked out once so the desktop and mobile
36+
menus can't drift apart -->
37+
{% if page.url == '/try' or page.url == '/try/' %}{% assign nav_section = 'try' %}
38+
{% elsif page.url contains '/examples' %}{% assign nav_section = 'examples' %}
39+
{% elsif page.url contains '/learn/' %}{% assign nav_section = 'learn' %}
40+
{% elsif page.url == '/community/' %}{% assign nav_section = 'community' %}
41+
{% endif %}
42+
3543
<!-- Navigation -->
3644
<nav class="sticky top-0 z-50 border-b-2 border-[var(--surface-border)] bg-[var(--surface-bg)]/90 backdrop-blur-xl">
3745
<div class="max-w-6xl mx-auto px-4 sm:px-6 flex items-center justify-between h-16">
@@ -47,31 +55,31 @@
4755

4856
<!-- Desktop links -->
4957
<div class="hidden sm:flex items-center gap-8">
50-
<a href="/try" class="nav-link {% if page.url == '/try/' %}active{% endif %}">Try</a>
51-
<a href="/examples" class="nav-link {% if page.url contains '/examples' %}active{% endif %}">Examples</a>
52-
<a href="/learn/get-started" class="nav-link {% if page.url contains '/learn/' %}active{% endif %}">Learn</a>
53-
<a href="/community" class="nav-link {% if page.url == '/community/' %}active{% endif %}">Community</a>
58+
<a href="/try" class="nav-link {% if nav_section == 'try' %}active{% endif %}" {% if nav_section == 'try' %}aria-current="page"{% endif %}>Try</a>
59+
<a href="/examples" class="nav-link {% if nav_section == 'examples' %}active{% endif %}" {% if nav_section == 'examples' %}aria-current="page"{% endif %}>Examples</a>
60+
<a href="/learn/get-started" class="nav-link {% if nav_section == 'learn' %}active{% endif %}" {% if nav_section == 'learn' %}aria-current="page"{% endif %}>Learn</a>
61+
<a href="/community" class="nav-link {% if nav_section == 'community' %}active{% endif %}" {% if nav_section == 'community' %}aria-current="page"{% endif %}>Community</a>
5462
<a href="https://github.com/ruby2d/ruby2d" class="text-[var(--text-muted)] hover:text-[var(--ruby-red)] transition-colors" aria-label="GitHub">
5563
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>
5664
</a>
5765
</div>
5866

5967
<!-- Mobile hamburger -->
60-
<button id="mobile-menu-btn" class="sm:hidden text-[var(--text-secondary)] hover:text-[var(--ruby-red)] p-1 transition-colors cursor-pointer" aria-label="Menu">
68+
<button id="mobile-menu-btn" class="sm:hidden text-[var(--text-secondary)] hover:text-[var(--ruby-red)] p-1 transition-colors cursor-pointer" aria-label="Menu" aria-controls="mobile-menu" aria-expanded="false">
6169
<svg class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
6270
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16"/>
6371
</svg>
6472
</button>
6573
</div>
6674

6775
<!-- Mobile menu -->
68-
<div id="mobile-menu" class="sm:hidden hidden border-t-2 border-[var(--surface-border)] bg-[var(--surface-card)]">
76+
<div id="mobile-menu" class="sm:hidden hidden absolute top-full inset-x-0 border-t-2 border-[var(--surface-border)] bg-[var(--surface-card)]">
6977
<div class="px-4 py-3 flex flex-col gap-1">
70-
<a href="/try" class="block px-3 py-2 rounded-xl text-[var(--text-secondary)] hover:text-[var(--ruby-red)] hover:bg-[var(--ruby-glow)] font-semibold text-sm transition-colors">Try</a>
71-
<a href="/examples" class="block px-3 py-2 rounded-xl text-[var(--text-secondary)] hover:text-[var(--ruby-red)] hover:bg-[var(--ruby-glow)] font-semibold text-sm transition-colors">Examples</a>
72-
<a href="/learn/get-started" class="block px-3 py-2 rounded-xl text-[var(--text-secondary)] hover:text-[var(--ruby-red)] hover:bg-[var(--ruby-glow)] font-semibold text-sm transition-colors">Learn</a>
73-
<a href="/community" class="block px-3 py-2 rounded-xl text-[var(--text-secondary)] hover:text-[var(--ruby-red)] hover:bg-[var(--ruby-glow)] font-semibold text-sm transition-colors">Community</a>
74-
<a href="https://github.com/ruby2d/ruby2d" class="block px-3 py-2 rounded-xl text-[var(--text-secondary)] hover:text-[var(--ruby-red)] hover:bg-[var(--ruby-glow)] font-semibold text-sm transition-colors">GitHub</a>
78+
<a href="/try" class="mobile-nav-link {% if nav_section == 'try' %}current{% endif %}" {% if nav_section == 'try' %}aria-current="page"{% endif %}>Try</a>
79+
<a href="/examples" class="mobile-nav-link {% if nav_section == 'examples' %}current{% endif %}" {% if nav_section == 'examples' %}aria-current="page"{% endif %}>Examples</a>
80+
<a href="/learn/get-started" class="mobile-nav-link {% if nav_section == 'learn' %}current{% endif %}" {% if nav_section == 'learn' %}aria-current="page"{% endif %}>Learn</a>
81+
<a href="/community" class="mobile-nav-link {% if nav_section == 'community' %}current{% endif %}" {% if nav_section == 'community' %}aria-current="page"{% endif %}>Community</a>
82+
<a href="https://github.com/ruby2d/ruby2d" class="mobile-nav-link">GitHub</a>
7583
</div>
7684
</div>
7785
</nav>
@@ -109,11 +117,36 @@
109117
var menuBtn = document.getElementById('mobile-menu-btn');
110118
var mobileMenu = document.getElementById('mobile-menu');
111119
if (menuBtn && mobileMenu) {
120+
var isOpen = function() { return !mobileMenu.classList.contains('hidden'); };
121+
var setMenu = function(open) {
122+
mobileMenu.classList.toggle('hidden', !open);
123+
menuBtn.setAttribute('aria-expanded', open ? 'true' : 'false');
124+
};
125+
112126
menuBtn.addEventListener('click', function() {
113-
mobileMenu.classList.toggle('hidden');
127+
setMenu(!isOpen());
128+
});
129+
130+
// The menu floats over the page, so it needs a way out that isn't the
131+
// hamburger itself: tap anywhere else, or press Escape.
132+
document.addEventListener('click', function(e) {
133+
if (!isOpen()) return;
134+
if (menuBtn.contains(e.target) || mobileMenu.contains(e.target)) return;
135+
setMenu(false);
114136
});
137+
138+
// Capture phase, because the Try page's wasm runtime binds its own
139+
// keydown handler to `window` and consumes plain keys like Escape.
140+
document.addEventListener('keydown', function(e) {
141+
if (e.key !== 'Escape' || !isOpen()) return;
142+
setMenu(false);
143+
menuBtn.focus();
144+
}, true);
145+
146+
// Only on a real width change — mobile browsers fire `resize` while
147+
// scrolling, as the URL bar hides and shows.
115148
window.addEventListener('resize', function() {
116-
if (window.innerWidth >= 640) mobileMenu.classList.add('hidden');
149+
if (window.innerWidth >= 640) setMenu(false);
117150
});
118151
}
119152
</script>

_pages/examples.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ <h2 class="font-fredoka font-semibold text-2xl text-[var(--text-primary)] mb-1">
5050
<!-- Player -->
5151
<div id="playerView" class="hidden example-player max-w-[840px] mx-auto">
5252

53-
<a href="#" id="backLink" class="inline-flex items-center gap-1.5 font-semibold text-sm text-[var(--text-muted)] hover:text-[var(--ruby-red)] transition-colors no-underline">All examples</a>
53+
<a href="#" id="backLink" class="inline-flex items-center gap-1.5 font-semibold text-sm text-[var(--text-muted)] hover:text-[var(--ruby-red)] transition-colors no-underline"><svg class="w-3 h-3 shrink-0" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7.5 2.5L4 6l3.5 3.5"/></svg>All examples</a>
5454

5555
<div class="flex items-center gap-4 mt-5 mb-6">
5656
<span id="playerIcon" class="example-icon"></span>
@@ -84,8 +84,8 @@ <h1 id="playerTitle" class="text-2xl sm:text-3xl font-bold text-[var(--text-prim
8484
disabled
8585
onclick="runCode()"
8686
title="Run (⌘Enter / Ctrl+Enter)"
87-
class="px-5 py-2 rounded-full font-semibold text-sm text-white bg-[var(--ruby-red)] shadow-[0_2px_8px_rgba(255,71,87,0.35)] hover:opacity-90 hover:-translate-y-0.5 active:translate-y-0 transition-all cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed disabled:translate-y-0"
88-
>Run &ensp;▶</button>
87+
class="inline-flex items-center gap-2 px-5 py-2 rounded-full font-semibold text-sm text-white bg-[var(--ruby-red)] shadow-[0_2px_8px_rgba(255,71,87,0.35)] hover:opacity-90 hover:-translate-y-0.5 active:translate-y-0 transition-all cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed disabled:translate-y-0"
88+
>Run<svg class="w-2.5 h-2.5 shrink-0" viewBox="0 0 12 12" fill="currentColor" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round" aria-hidden="true"><path d="M3.2 2.4L9.4 6L3.2 9.6Z"/></svg></button>
8989
<button
9090
id="consoleBtn"
9191
onclick="toggleConsole()"

_pages/learn/2d-basics.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,4 +168,4 @@ end
168168
show
169169
```
170170

171-
Continue to the [next topic](/learn/{{ page.next_topic }})
171+
Continue to the [next topic](/learn/{{ page.next_topic }}){:.next-topic}

_pages/learn/audio.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,4 @@ on :key_down do
5959
end
6060
```
6161

62-
Continue to the [next topic](/learn/{{ page.next_topic }})
62+
Continue to the [next topic](/learn/{{ page.next_topic }}){:.next-topic}

_pages/learn/building.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ On the web, the render loop is driven by the browser's `requestAnimationFrame`,
107107
A couple of browser specifics worth knowing:
108108

109109
- **Chrome** and **Firefox** run at the full refresh rate by default.
110-
- **Safari** caps `requestAnimationFrame` at 60Hz unless you turn off the **"Prefer Page Rendering Updates near 60fps"** flag (Safari Develop Feature Flags). With it off, Safari matches the display refresh rate too.
110+
- **Safari** caps `requestAnimationFrame` at 60Hz unless you turn off the **"Prefer Page Rendering Updates near 60fps"** flag (Safari Develop Feature Flags). With it off, Safari matches the display refresh rate too.
111111

112112
# Setting up other platforms
113113

_pages/learn/buttons.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,4 @@ btn.remove # remove from the window
142142
btn.add # add back to the window
143143
```
144144

145-
Continue to the [next topic](/learn/{{ page.next_topic }})
145+
Continue to the [next topic](/learn/{{ page.next_topic }}){:.next-topic}

_pages/learn/canvas.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,4 +253,4 @@ end
253253

254254
Any of `x:`, `y:`, `width:`, `height:`, `rotate:`, `tint:`, and `opacity:` can be passed as a per-frame override.
255255

256-
Continue to the [next topic](/learn/{{ page.next_topic }})
256+
Continue to the [next topic](/learn/{{ page.next_topic }}){:.next-topic}

0 commit comments

Comments
 (0)