Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions lib/src/generator/builders/page_layout_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ class PageLayoutBuilder {
return '''
<header class="header">
<div class="header-inner">
<button class="mobile-menu-toggle" id="mobile-menu-toggle" aria-label="Toggle menu">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="3" y1="6" x2="21" y2="6"/>
<line x1="3" y1="12" x2="21" y2="12"/>
<line x1="3" y1="18" x2="21" y2="18"/>
</svg>
</button>
<a href="${_prefixPath('/')}" class="logo">
$logoHtml
${config.header.showName ? '<span class="logo-text">${config.name}</span>' : ''}
Expand Down Expand Up @@ -207,6 +214,12 @@ class PageLayoutBuilder {

return '''
<aside class="sidebar">
<button class="sidebar-close" id="sidebar-close" aria-label="Close menu">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
$groups
</aside>
''';
Expand Down
25 changes: 25 additions & 0 deletions lib/src/generator/builders/page_scripts_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,31 @@ class PageScriptsBuilder {
activeSidebarLink.scrollIntoView({ block: 'center', behavior: 'instant' });
}

const menuToggle = document.getElementById('mobile-menu-toggle');
const mobileSidebar = document.querySelector('.sidebar');
const mobileOverlay = document.getElementById('mobile-overlay');
if (menuToggle && mobileSidebar && mobileOverlay) {
const closeMenu = () => {
mobileSidebar.classList.remove('open');
mobileOverlay.classList.remove('active');
document.body.style.overflow = '';
};
menuToggle.addEventListener('click', () => {
const isOpen = mobileSidebar.classList.toggle('open');
mobileOverlay.classList.toggle('active');
document.body.style.overflow = isOpen ? 'hidden' : '';
});
mobileOverlay.addEventListener('click', closeMenu);
const sidebarClose = document.getElementById('sidebar-close');
if (sidebarClose) sidebarClose.addEventListener('click', closeMenu);
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && mobileSidebar.classList.contains('open')) closeMenu();
});
mobileSidebar.querySelectorAll('.sidebar-link').forEach(link => {
link.addEventListener('click', closeMenu);
});
}

document.querySelectorAll('.code-group').forEach(group => {
const buttons = group.querySelectorAll('.tab-button');
const panels = group.querySelectorAll('.tab-panel');
Expand Down
63 changes: 62 additions & 1 deletion lib/src/generator/builders/page_styles_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2246,6 +2246,44 @@ ${_buildResponsiveStyles()}
.dark .hljs-name, .dark .hljs-selector-id, .dark .hljs-selector-class { color: #7ee787; }''';

String _buildResponsiveStyles() => '''
.mobile-menu-toggle {
display: none;
background: none;
border: none;
color: var(--color-text);
cursor: pointer;
padding: 0.25rem;
align-items: center;
justify-content: center;
}

.sidebar-close {
display: none;
background: none;
border: none;
color: var(--color-text-secondary);
cursor: pointer;
padding: 0.5rem;
margin: 0 0.5rem 0.5rem;
align-self: flex-end;
}

.sidebar-close:hover {
color: var(--color-text);
}

.mobile-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 199;
}

.mobile-overlay.active {
display: block;
}

@media (max-width: 1280px) {
.main-container {
grid-template-columns: 260px 1fr;
Expand All @@ -2256,11 +2294,34 @@ ${_buildResponsiveStyles()}
}

@media (max-width: 768px) {
.mobile-menu-toggle {
display: flex;
}
.sidebar-close {
display: flex;
}
.nav {
display: none;
}
.main-container {
grid-template-columns: 1fr;
}
.sidebar {
display: none;
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: 280px;
height: 100vh;
z-index: 200;
transform: translateX(-100%);
transition: transform 0.3s ease;
background: var(--color-bg);
border-right: 1px solid var(--color-border);
padding-top: 1rem;
}
.sidebar.open {
transform: translateX(0);
}
.content {
padding: 1.5rem;
Expand Down
1 change: 1 addition & 0 deletions lib/src/generator/page_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class PageBuilder {
<body>
<div class="layout">
${_layoutBuilder.buildHeader()}
<div class="mobile-overlay" id="mobile-overlay"></div>
<div class="main-container">
${_layoutBuilder.buildSidebar(sidebar, page.path)}
<main class="content">
Expand Down
40 changes: 40 additions & 0 deletions test/generator/builders/page_layout_builder_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,46 @@ void main() {
});
});

group('mobile menu', () {
test('header includes mobile menu toggle button', () {
final result = builder.buildHeader();

expect(result, contains('mobile-menu-toggle'));
expect(result, contains('id="mobile-menu-toggle"'));
expect(result, contains('aria-label="Toggle menu"'));
});

test('mobile menu toggle has hamburger icon', () {
final result = builder.buildHeader();

expect(result, contains('<line x1="3" y1="6"'));
expect(result, contains('<line x1="3" y1="12"'));
expect(result, contains('<line x1="3" y1="18"'));
});

test('hamburger is before logo in header', () {
final result = builder.buildHeader();

final hamburgerIndex = result.indexOf('mobile-menu-toggle');
final logoIndex = result.indexOf('class="logo"');
expect(hamburgerIndex, lessThan(logoIndex));
});

test('sidebar contains close button', () {
const sidebar = [
SidebarGroup(group: 'Guide', pages: [
SidebarPage(slug: 'intro'),
]),
];

final result = builder.buildSidebar(sidebar, '/other');

expect(result, contains('sidebar-close'));
expect(result, contains('id="sidebar-close"'));
expect(result, contains('aria-label="Close menu"'));
});
});

group('header with disabled features', () {
test('hides search when disabled', () {
const config = StardustConfig(
Expand Down
2 changes: 2 additions & 0 deletions test/generator/page_builder_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ void main() {
final html = builder.build(page, sidebar: []);

expect(html, contains('class="layout"'));
expect(html, contains('class="mobile-overlay"'));
expect(html, contains('id="mobile-overlay"'));
expect(html, contains('class="main-container"'));
expect(html, contains('<main'));
});
Expand Down