feat: improve mobile responsiveness

- Add mobile navigation drawer with hamburger menu
- Sidebar slides in as overlay on mobile (<768px)
- Close button inside sidebar for mobile
- Ensure touch targets are at least 44px on mobile
- Make modals full-screen on mobile
- Editor toolbar scrolls horizontally on mobile
- Improve spacing and typography for small screens
- Keep dark theme consistent across breakpoints
- Projects page cards stack vertically on mobile
- Document cards full-width on mobile
This commit is contained in:
Hiro
2026-03-28 14:00:05 +00:00
parent c090cd3a71
commit 8f7ad3f673
5 changed files with 468 additions and 76 deletions

View File

@@ -1239,10 +1239,90 @@ ul, ol {
to { transform: rotate(360deg); } to { transform: rotate(360deg); }
} }
/* === Mobile Navigation Drawer === */
.mobile-nav-btn {
display: none;
width: 44px;
height: 44px;
align-items: center;
justify-content: center;
border-radius: var(--radius-md);
color: var(--color-text-secondary);
font-size: 1.25rem;
transition: var(--transition-fast);
flex-shrink: 0;
}
.mobile-nav-btn:hover {
background: var(--color-hover);
color: var(--color-text);
}
.sidebar-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: calc(var(--z-sidebar) - 1);
opacity: 0;
transition: opacity 0.2s ease;
}
.sidebar-overlay.active {
opacity: 1;
}
.sidebar-close-btn {
display: none;
position: absolute;
top: var(--space-3);
right: var(--space-3);
width: 44px;
height: 44px;
align-items: center;
justify-content: center;
border-radius: var(--radius-md);
color: var(--color-text-secondary);
font-size: 1.25rem;
z-index: 1;
}
.sidebar-close-btn:hover {
background: var(--color-hover);
color: var(--color-text);
}
/* === Responsive === */ /* === Responsive === */
@media (max-width: 1024px) { @media (max-width: 1024px) {
.sidebar { .sidebar {
display: none; position: fixed;
left: 0;
top: 0;
bottom: 0;
z-index: var(--z-sidebar);
transform: translateX(-100%);
transition: transform 0.25s ease;
box-shadow: var(--shadow-lg);
}
.sidebar.mobile-open {
transform: translateX(0);
}
.sidebar-scroll {
padding-top: 56px;
}
.sidebar-close-btn {
display: flex;
}
.mobile-nav-btn {
display: flex;
}
.sidebar-overlay {
display: block;
} }
.doc-sidebar { .doc-sidebar {
@@ -1276,22 +1356,300 @@ ul, ol {
} }
.content-body { .content-body {
padding: var(--space-4); padding: var(--space-3);
} }
.doc-grid { .doc-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: var(--space-3);
}
.doc-card {
padding: var(--space-4);
min-height: 44px;
}
.doc-card-actions {
opacity: 1;
position: static;
margin-top: var(--space-2);
display: flex;
gap: var(--space-2);
} }
.modal { .modal {
min-width: auto; min-width: auto;
margin: var(--space-4); width: 100%;
height: 100%;
max-width: 100%;
margin: 0;
border-radius: 0;
display: flex;
flex-direction: column;
}
.modal-backdrop {
padding: 0;
align-items: stretch;
}
.modal-header {
padding: var(--space-4);
flex-shrink: 0;
}
.modal-body {
flex: 1;
overflow-y: auto;
padding: var(--space-4);
}
.modal-footer {
flex-shrink: 0;
padding: var(--space-4);
gap: var(--space-3);
} }
.login-card { .login-card {
margin: var(--space-4); margin: var(--space-4);
padding: var(--space-6); padding: var(--space-6);
} }
/* === Projects Page Mobile === */
.projects-page {
padding: var(--space-4);
}
.projects-header {
margin-bottom: var(--space-6);
}
.projects-header h1 {
font-size: 1.5rem;
}
.projects-grid {
grid-template-columns: 1fr;
gap: var(--space-3);
}
.project-card {
padding: var(--space-4);
}
.project-card-header {
flex-direction: column;
gap: var(--space-3);
}
.project-icon {
font-size: 1.5rem;
}
.project-name {
font-size: 1rem;
}
/* === Header Mobile === */
.app-header {
padding: var(--space-2) var(--space-3);
gap: var(--space-2);
}
.app-header .logo {
font-size: 1rem;
flex: 1;
min-width: 0;
}
.header-actions .btn {
padding: var(--space-2) var(--space-3);
font-size: 0.8125rem;
}
/* === Content Header Mobile === */
.content-header {
padding: var(--space-3) var(--space-4);
flex-wrap: wrap;
gap: var(--space-3);
}
.content-header-left {
flex-direction: column;
gap: var(--space-1);
align-items: flex-start;
}
.content-header-left h1 {
font-size: 1.125rem;
}
.content-header-right {
width: 100%;
flex-wrap: wrap;
}
.search-box-inline {
flex: 1;
min-width: 0;
}
.search-box-inline input {
width: 100%;
}
/* === Editor Mobile === */
.editor-container {
max-width: 100%;
}
.editor-header {
padding: var(--space-3) var(--space-4);
}
.editor-form {
padding: var(--space-4);
}
.form-row {
grid-template-columns: 1fr;
gap: var(--space-3);
}
.editor-toolbar {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
gap: var(--space-1);
padding: var(--space-2);
flex-wrap: nowrap;
justify-content: flex-start;
}
.toolbar-btn {
width: 44px;
height: 44px;
flex-shrink: 0;
}
.toolbar-separator {
flex-shrink: 0;
}
.toolbar-tabs {
margin-left: auto;
flex-shrink: 0;
}
.tab-btn {
padding: var(--space-2) var(--space-3);
min-height: 44px;
display: flex;
align-items: center;
}
.form-group textarea {
min-height: 200px;
font-size: 16px; /* Prevents zoom on iOS */
}
.form-group input,
.form-group select {
min-height: 44px;
font-size: 16px; /* Prevents zoom on iOS */
}
/* === Doc Card Mobile === */
.doc-card-header {
flex-wrap: wrap;
}
.doc-title {
font-size: 0.9375rem;
}
.doc-tags {
margin-top: var(--space-2);
}
.doc-meta {
flex-wrap: wrap;
gap: var(--space-2);
}
/* === Touch Targets === */
.btn {
min-height: 44px;
min-width: 44px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.btn-icon-only {
width: 44px;
height: 44px;
}
.tree-item {
min-height: 44px;
padding: var(--space-2) var(--space-3);
}
.tree-item .tree-action {
width: 32px;
height: 32px;
opacity: 1;
}
.tag-item {
min-height: 44px;
padding: var(--space-2) var(--space-3);
}
.quick-link {
min-height: 44px;
padding: var(--space-2) var(--space-3);
}
.doc-card {
cursor: pointer;
}
/* === Empty State Mobile === */
.empty-state {
padding: var(--space-8);
}
/* === Breadcrumbs Mobile === */
.breadcrumb-nav {
margin-left: 0;
flex-wrap: wrap;
font-size: 0.8125rem;
}
/* === Toast Mobile === */
#toast-container {
bottom: var(--space-4);
right: var(--space-4);
left: var(--space-4);
}
.toast {
min-width: auto;
width: 100%;
}
}
/* Ensure minimum touch target size */
@media (max-width: 768px) {
a, button, [role="button"] {
min-height: 44px;
min-width: 44px;
}
input, select, textarea {
font-size: 16px; /* Prevents iOS zoom */
}
} }
/* === Projects Page === */ /* === Projects Page === */

View File

@@ -25,70 +25,36 @@ export function renderSidebar({ libraries, tags, selectedLibrary, selectedTag, o
.join(''); .join('');
}; };
// Store callbacks in a way that's safe and doesn't rely on inline script execution
const callbacks = {
onSelectLibrary,
onSelectTag,
onHome
};
return ` return `
<aside class="sidebar"> <div class="sidebar-scroll">
<div class="sidebar-scroll"> <div class="sidebar-section">
<div class="sidebar-section"> <h3>📚 Libraries</h3>
<h3>📚 Libraries</h3> <div class="library-tree">
<div class="library-tree"> <div class="tree-item ${!selectedLibrary ? 'active' : ''}" data-action="home">
<div class="tree-item ${!selectedLibrary ? 'active' : ''}" data-action="home"> <span class="icon">🏠</span>
<span class="icon">🏠</span> <span class="label">All Documents</span>
<span class="label">All Documents</span>
</div>
${buildLibraryTree(libraries)}
</div> </div>
</div> ${buildLibraryTree(libraries)}
<div class="sidebar-section">
<h3>🏷️ Tags</h3>
<div class="tag-list">
${tags.map(tag => `
<div class="tag-item ${selectedTag === tag.name ? 'active' : ''}" data-action="tag" data-tag="${escapeHtml(tag.name)}">
<span>#${escapeHtml(tag.name)}</span>
<span class="tag-count">${tag.count}</span>
</div>
`).join('')}
</div>
</div>
<div class="quick-links">
<a class="quick-link" data-action="home">📋 All Documents</a>
<a class="quick-link" href="#" onclick="window.app.navigate('projects'); return false;">📂 Projects</a>
<a class="quick-link" href="#" onclick="window.showNewDocModal(); return false;">📄 New Document</a>
<a class="quick-link" href="#" onclick="window.showNewLibraryModal(); return false;">📁 New Library</a>
</div> </div>
</div> </div>
</aside> <div class="sidebar-section">
<script> <h3>🏷️ Tags</h3>
(function() { <div class="tag-list">
var callbacks = window.__sidebarCallbacks; ${tags.map(tag => `
document.querySelectorAll('[data-action="home"]').forEach(function(el) { <div class="tag-item ${selectedTag === tag.name ? 'active' : ''}" data-action="tag" data-tag="${escapeHtml(tag.name)}">
el.addEventListener('click', function(e) { <span>#${escapeHtml(tag.name)}</span>
e.stopPropagation(); <span class="tag-count">${tag.count}</span>
if (callbacks && callbacks.onHome) callbacks.onHome(); </div>
}); `).join('')}
}); </div>
document.querySelectorAll('[data-action="library"]').forEach(function(el) { </div>
el.addEventListener('click', function(e) { <div class="quick-links">
e.stopPropagation(); <a class="quick-link" data-action="home">📋 All Documents</a>
var id = this.getAttribute('data-library-id'); <a class="quick-link" href="#" onclick="window.app.navigate('projects'); return false;">📂 Projects</a>
if (callbacks && callbacks.onSelectLibrary) callbacks.onSelectLibrary(id); <a class="quick-link" href="#" onclick="window.showNewDocModal(); return false;">📄 New Document</a>
}); <a class="quick-link" href="#" onclick="window.showNewLibraryModal(); return false;">📁 New Library</a>
}); </div>
document.querySelectorAll('[data-action="tag"]').forEach(function(el) { </div>
el.addEventListener('click', function(e) {
e.stopPropagation();
var tag = this.getAttribute('data-tag');
if (callbacks && callbacks.onSelectTag) callbacks.onSelectTag(tag);
});
});
})();
</script>
`; `;
} }

View File

@@ -77,29 +77,33 @@ export async function renderDashboard(app) {
appEl.innerHTML = ` appEl.innerHTML = `
<header class="app-header"> <header class="app-header">
<button class="mobile-nav-btn" onclick="toggleMobileSidebar()" title="Menu">☰</button>
<div class="logo">📝 SimpleNote</div> <div class="logo">📝 SimpleNote</div>
<div class="search-box"> <div class="search-box">
<span class="icon">🔍</span> <span class="icon">🔍</span>
<input type="text" id="search-input" placeholder="Search documents..." value="${searchQuery}"> <input type="text" id="search-input" placeholder="Search documents..." value="${searchQuery}">
</div> </div>
<div class="header-actions"> <div class="header-actions">
<button class="btn btn-primary" onclick="window.showNewDocModal()">+ New Document</button> <button class="btn btn-primary" onclick="window.showNewDocModal()">+ New</button>
<button class="btn btn-ghost" onclick="window.showNewLibraryModal()">📁 New Library</button>
</div> </div>
</header> </header>
<div class="sidebar-overlay" onclick="closeMobileSidebar()"></div>
<div class="app-layout"> <div class="app-layout">
${renderSidebar({ <aside class="sidebar" id="sidebar">
libraries, <button class="sidebar-close-btn" onclick="closeMobileSidebar()">✕</button>
tags, ${renderSidebar({
selectedLibrary, libraries,
selectedTag, tags,
...sidebarCallbacks selectedLibrary,
})} selectedTag,
...sidebarCallbacks
})}
</aside>
<main class="main-content"> <main class="main-content">
<div class="content-header"> <div class="content-header">
<h1>${selectedLibrary ? getLibraryName(libraries, selectedLibrary) : selectedTag ? `#${selectedTag}` : 'All Documents'}</h1> <h1>${selectedLibrary ? getLibraryName(libraries, selectedLibrary) : selectedTag ? `#${selectedTag}` : 'All Documents'}</h1>
<div class="header-actions"> <div class="header-actions">
<button class="btn btn-primary" onclick="window.showNewDocModal()">+ New Document</button> <button class="btn btn-primary" onclick="window.showNewDocModal()">+ New</button>
</div> </div>
</div> </div>
<div class="content-body"> <div class="content-body">
@@ -120,12 +124,54 @@ export async function renderDashboard(app) {
</div> </div>
`; `;
// Mobile sidebar functions
window.toggleMobileSidebar = function() {
const sidebar = document.getElementById('sidebar');
const overlay = document.querySelector('.sidebar-overlay');
if (sidebar) {
sidebar.classList.toggle('mobile-open');
if (overlay) overlay.classList.toggle('active');
}
};
window.closeMobileSidebar = function() {
const sidebar = document.getElementById('sidebar');
const overlay = document.querySelector('.sidebar-overlay');
if (sidebar) {
sidebar.classList.remove('mobile-open');
if (overlay) overlay.classList.remove('active');
}
};
// Event listeners // Event listeners
const searchInput = document.getElementById('search-input'); const searchInput = document.getElementById('search-input');
searchInput.oninput = (e) => { searchInput.oninput = (e) => {
searchQuery = e.target.value; searchQuery = e.target.value;
render(); render();
}; };
// Sidebar item listeners
document.querySelectorAll('[data-action="home"]').forEach(el => {
el.addEventListener('click', (e) => {
e.stopPropagation();
sidebarCallbacks.onHome();
closeMobileSidebar();
});
});
document.querySelectorAll('[data-action="library"]').forEach(el => {
el.addEventListener('click', (e) => {
e.stopPropagation();
sidebarCallbacks.onSelectLibrary(el.getAttribute('data-library-id'));
closeMobileSidebar();
});
});
document.querySelectorAll('[data-action="tag"]').forEach(el => {
el.addEventListener('click', (e) => {
e.stopPropagation();
sidebarCallbacks.onSelectTag(el.getAttribute('data-tag'));
closeMobileSidebar();
});
});
} }
render(); render();

View File

@@ -74,6 +74,7 @@ export async function renderProjectView(app) {
appEl.innerHTML = ` appEl.innerHTML = `
<header class="app-header"> <header class="app-header">
<button class="mobile-nav-btn" onclick="toggleMobileSidebar()" title="Menu">☰</button>
<div class="logo">📝 SimpleNote</div> <div class="logo">📝 SimpleNote</div>
<div class="breadcrumb-nav"> <div class="breadcrumb-nav">
<span class="breadcrumb-link" onclick="window.app.navigate('projects')">Projects</span> <span class="breadcrumb-link" onclick="window.app.navigate('projects')">Projects</span>
@@ -85,8 +86,10 @@ export async function renderProjectView(app) {
<button class="btn btn-ghost" onclick="window.confirmDeleteProject('${project.id}')" title="Delete Project">🗑️</button> <button class="btn btn-ghost" onclick="window.confirmDeleteProject('${project.id}')" title="Delete Project">🗑️</button>
</div> </div>
</header> </header>
<div class="sidebar-overlay" onclick="closeMobileSidebar()"></div>
<div class="app-layout"> <div class="app-layout">
<aside class="sidebar project-sidebar"> <aside class="sidebar project-sidebar" id="sidebar">
<button class="sidebar-close-btn" onclick="closeMobileSidebar()">✕</button>
<div class="sidebar-scroll"> <div class="sidebar-scroll">
<div class="sidebar-section"> <div class="sidebar-section">
<div class="section-header"> <div class="section-header">
@@ -130,7 +133,7 @@ export async function renderProjectView(app) {
<span class="icon">🔍</span> <span class="icon">🔍</span>
<input type="text" id="search-input" placeholder="Search documents..." value="${escapeHtml(searchQuery)}"> <input type="text" id="search-input" placeholder="Search documents..." value="${escapeHtml(searchQuery)}">
</div> </div>
<button class="btn btn-primary" onclick="window.showNewDocModal('${projectId}', '${selectedFolderId || ''}')">+ New Document</button> <button class="btn btn-primary" onclick="window.showNewDocModal('${projectId}', '${selectedFolderId || ''}')">+ New</button>
</div> </div>
</div> </div>
<div class="content-body"> <div class="content-body">
@@ -155,6 +158,25 @@ export async function renderProjectView(app) {
attachEventListeners(); attachEventListeners();
} }
// Mobile sidebar functions
window.toggleMobileSidebar = function() {
const sidebar = document.getElementById('sidebar');
const overlay = document.querySelector('.sidebar-overlay');
if (sidebar) {
sidebar.classList.toggle('mobile-open');
if (overlay) overlay.classList.toggle('active');
}
};
window.closeMobileSidebar = function() {
const sidebar = document.getElementById('sidebar');
const overlay = document.querySelector('.sidebar-overlay');
if (sidebar) {
sidebar.classList.remove('mobile-open');
if (overlay) overlay.classList.remove('active');
}
};
function attachEventListeners() { function attachEventListeners() {
// Search // Search
const searchInput = document.getElementById('search-input'); const searchInput = document.getElementById('search-input');

View File

@@ -19,7 +19,7 @@ export async function renderProjects(app) {
<header class="app-header"> <header class="app-header">
<div class="logo">📝 SimpleNote</div> <div class="logo">📝 SimpleNote</div>
<div class="header-actions"> <div class="header-actions">
<button class="btn btn-primary" onclick="window.showNewProjectModal()">+ New Project</button> <button class="btn btn-primary" onclick="window.showNewProjectModal()">+ New</button>
</div> </div>
</header> </header>
<div class="projects-page"> <div class="projects-page">