Sidebar becomes a mobile drawer below 768px
The sidebar was flex:0 0 15rem inside .app__body unconditionally, so on a narrow viewport it and the main content just fought over space. Below 768px it now leaves the flex flow entirely (position: fixed, translated off-canvas by default) and slides in as a drawer instead, leaving .app__main the full width. - App.vue: a ☰ button in the header (hidden above the breakpoint) opens it; a backdrop tap, the drawer's own close button, or any navigation (route.fullPath watcher) closes it. State lives in App.vue since the toggle button is in the header, not the sidebar. - AppSidebar.vue: a close button (✕), only visible in the drawer, emitting 'close'. - Desktop (>=768px) layout and behaviour is untouched -- the drawer CSS and the toggle button both live behind the same media query. Drag-and-drop is untouched, as asked -- this only affects layout, and the sidebar's inbox draggable works the same as before either way. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -58,6 +58,15 @@ it also works as a project switcher from anywhere. `<RouterView
|
|||||||
:key="route.path">` remounts the view on every path change so switching
|
:key="route.path">` remounts the view on every path change so switching
|
||||||
projects always does a fresh load.
|
projects always does a fresh load.
|
||||||
|
|
||||||
|
Below `768px` (`style.css`'s one layout breakpoint) the sidebar becomes an
|
||||||
|
off-canvas drawer instead of sitting beside the page: `position: fixed`,
|
||||||
|
translated out of view by default, slid in via a `.sidebar--open` class.
|
||||||
|
`App.vue` owns the `drawerOpen` state -- a `☰` button in the header (hidden
|
||||||
|
above the breakpoint) opens it; a backdrop tap, the drawer's own `✕` (which
|
||||||
|
`AppSidebar` emits `close` for), or any navigation (a `route.fullPath`
|
||||||
|
watcher) closes it. Above the breakpoint `drawerOpen` just goes unused, since
|
||||||
|
nothing renders the button that would set it.
|
||||||
|
|
||||||
`/` redirects to `/dashboard` (`DashboardView.vue`): a full-width grid linking
|
`/` redirects to `/dashboard` (`DashboardView.vue`): a full-width grid linking
|
||||||
to each project (title + card count), with a **"Create a project" tile**
|
to each project (title + card count), with a **"Create a project" tile**
|
||||||
styled to match sitting last in the same grid (creating one stays on the
|
styled to match sitting last in the same grid (creating one stays on the
|
||||||
|
|||||||
+28
-3
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { RouterLink, RouterView, useRoute, useRouter } from 'vue-router'
|
import { RouterLink, RouterView, useRoute, useRouter } from 'vue-router'
|
||||||
import AppSidebar from './components/AppSidebar.vue'
|
import AppSidebar from './components/AppSidebar.vue'
|
||||||
import PasskeyNotice from './components/PasskeyNotice.vue'
|
import PasskeyNotice from './components/PasskeyNotice.vue'
|
||||||
@@ -19,6 +19,14 @@ const route = useRoute()
|
|||||||
// profile) and stays mounted as you navigate between them.
|
// profile) and stays mounted as you navigate between them.
|
||||||
const showSidebar = computed(() => auth.isAuthenticated && route.meta.requiresAuth === true)
|
const showSidebar = computed(() => auth.isAuthenticated && route.meta.requiresAuth === true)
|
||||||
|
|
||||||
|
// On narrow viewports the sidebar becomes an off-canvas drawer (see
|
||||||
|
// .sidebar's mobile rules in style.css), opened via the header's "menu"
|
||||||
|
// button; on wide viewports drawerOpen is never set, so it's inert (the
|
||||||
|
// button that sets it is itself hidden there). Close it on every navigation
|
||||||
|
// so it doesn't stay open over the page it was just used to get to.
|
||||||
|
const drawerOpen = ref(false)
|
||||||
|
watch(() => route.fullPath, () => (drawerOpen.value = false))
|
||||||
|
|
||||||
async function onLogout() {
|
async function onLogout() {
|
||||||
auth.logout()
|
auth.logout()
|
||||||
projects.reset()
|
projects.reset()
|
||||||
@@ -31,7 +39,20 @@ async function onLogout() {
|
|||||||
<template>
|
<template>
|
||||||
<div class="app">
|
<div class="app">
|
||||||
<header class="app__bar">
|
<header class="app__bar">
|
||||||
<span class="app__brand">Projects</span>
|
<div class="app__bar-left">
|
||||||
|
<button
|
||||||
|
v-if="showSidebar"
|
||||||
|
type="button"
|
||||||
|
class="app__menu-toggle"
|
||||||
|
aria-label="Open menu"
|
||||||
|
aria-haspopup="true"
|
||||||
|
:aria-expanded="drawerOpen"
|
||||||
|
@click="drawerOpen = true"
|
||||||
|
>
|
||||||
|
☰
|
||||||
|
</button>
|
||||||
|
<span class="app__brand">Projects</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="auth.isAuthenticated" class="app__account">
|
<div v-if="auth.isAuthenticated" class="app__account">
|
||||||
<RouterLink to="/profile" class="app__email">{{ auth.user?.email }}</RouterLink>
|
<RouterLink to="/profile" class="app__email">{{ auth.user?.email }}</RouterLink>
|
||||||
@@ -42,7 +63,11 @@ async function onLogout() {
|
|||||||
<PasskeyNotice />
|
<PasskeyNotice />
|
||||||
|
|
||||||
<div class="app__body">
|
<div class="app__body">
|
||||||
<AppSidebar v-if="showSidebar" />
|
<!-- Mobile only (see style.css): dims the page and closes the drawer
|
||||||
|
on tap, while the sidebar is a fixed off-canvas panel over it. -->
|
||||||
|
<div v-if="showSidebar && drawerOpen" class="app__drawer-backdrop" @click="drawerOpen = false" />
|
||||||
|
|
||||||
|
<AppSidebar v-if="showSidebar" :class="{ 'sidebar--open': drawerOpen }" @close="drawerOpen = false" />
|
||||||
|
|
||||||
<main class="app__main" :class="{ 'app__main--wide': route.meta.wide }">
|
<main class="app__main" :class="{ 'app__main--wide': route.meta.wide }">
|
||||||
<!-- Key by path so navigating between projects via the sidebar remounts
|
<!-- Key by path so navigating between projects via the sidebar remounts
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ const inbox = useInboxStore()
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
|
// Only meaningful on mobile, where the sidebar is a drawer (see style.css) --
|
||||||
|
// its own close button emits this to tell App.vue to close it (App.vue also
|
||||||
|
// closes it directly on a backdrop tap or any navigation).
|
||||||
|
const emit = defineEmits<{ close: [] }>()
|
||||||
|
|
||||||
const loadError = ref<string | null>(null)
|
const loadError = ref<string | null>(null)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -96,6 +101,8 @@ async function onInboxChange(change: ColumnChange) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
|
<button type="button" class="sidebar__close" aria-label="Close menu" @click="emit('close')">✕</button>
|
||||||
|
|
||||||
<nav class="sidebar__nav">
|
<nav class="sidebar__nav">
|
||||||
<RouterLink :to="{ name: 'dashboard' }" class="sidebar__link" active-class="sidebar__link--active">
|
<RouterLink :to="{ name: 'dashboard' }" class="sidebar__link" active-class="sidebar__link--active">
|
||||||
<svg class="sidebar__icon" viewBox="0 0 16 16" aria-hidden="true">
|
<svg class="sidebar__icon" viewBox="0 0 16 16" aria-hidden="true">
|
||||||
|
|||||||
@@ -62,10 +62,36 @@ body {
|
|||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app__bar-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.app__brand {
|
.app__brand {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Opens the sidebar drawer on mobile (see the sidebar's own mobile rules
|
||||||
|
below) -- hidden here, shown only under that same breakpoint. */
|
||||||
|
.app__menu-toggle {
|
||||||
|
display: none;
|
||||||
|
flex: none;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.35rem 0.55rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app__menu-toggle:hover {
|
||||||
|
border-color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
.app__account {
|
.app__account {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -118,6 +144,70 @@ body {
|
|||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Only shown once .sidebar becomes a drawer, under the mobile breakpoint
|
||||||
|
below -- hidden here. */
|
||||||
|
.sidebar__close {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 0.75rem;
|
||||||
|
right: 0.75rem;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: var(--muted);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 0.3rem 0.5rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar__close:hover {
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- mobile: sidebar becomes an off-canvas drawer ---------------------- */
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.app__menu-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app__body {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 200;
|
||||||
|
width: 85vw;
|
||||||
|
max-width: 20rem;
|
||||||
|
height: 100vh;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
padding-top: 2.75rem;
|
||||||
|
transform: translateX(-100%);
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar--open {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar__close {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app__drawer-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 150;
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar__nav {
|
.sidebar__nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
Reference in New Issue
Block a user