Skip to content

Commit d5dab1a

Browse files
authored
Merge pull request #564 from Chris0Jeky/feature/543-mobile-responsive-css
MOB-03: Mobile-responsive CSS for core flows
2 parents e462ba5 + 524a81e commit d5dab1a

7 files changed

Lines changed: 462 additions & 2 deletions

File tree

frontend/taskdeck-web/src/components/common/CaptureModal.vue

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,4 +224,60 @@ onUnmounted(() => {
224224
background: var(--td-color-error-light);
225225
color: var(--td-color-error);
226226
}
227+
228+
/* ─── Mobile: full-screen capture ─── */
229+
@media (max-width: 640px) {
230+
.td-overlay {
231+
padding: 0;
232+
align-items: stretch;
233+
}
234+
235+
.td-capture-modal {
236+
width: 100%;
237+
height: 100%;
238+
max-height: 100vh;
239+
max-height: 100dvh; /* iOS dynamic viewport — avoids browser chrome overlap */
240+
border-radius: 0;
241+
padding: var(--td-space-4);
242+
display: flex;
243+
flex-direction: column;
244+
}
245+
246+
.td-capture-modal__header h2 {
247+
font-size: var(--td-font-lg);
248+
}
249+
250+
.td-capture-modal__close {
251+
min-width: 44px;
252+
min-height: 44px;
253+
display: flex;
254+
align-items: center;
255+
justify-content: center;
256+
font-size: var(--td-font-lg);
257+
}
258+
259+
.td-capture-modal__input {
260+
flex: 1;
261+
font-size: 16px; /* Prevents iOS zoom on focus */
262+
min-height: 200px;
263+
resize: none;
264+
}
265+
266+
.td-capture-modal__actions {
267+
flex-direction: column-reverse;
268+
gap: var(--td-space-3);
269+
}
270+
271+
.td-capture-modal__actions .td-btn {
272+
width: 100%;
273+
min-height: 48px;
274+
font-size: var(--td-font-base);
275+
justify-content: center;
276+
}
277+
278+
.td-capture-modal__hint {
279+
font-size: var(--td-font-sm);
280+
line-height: 1.5;
281+
}
282+
}
227283
</style>

frontend/taskdeck-web/src/components/shell/AppShell.vue

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,17 @@ onUnmounted(() => {
162162
/>
163163

164164
<div class="td-main-container">
165+
<div class="td-mobile-topbar">
166+
<button
167+
class="td-mobile-topbar__hamburger"
168+
aria-label="Open navigation menu"
169+
@click="sidebarRef?.toggleMobileMenu()"
170+
>
171+
<span class="material-symbols-outlined">menu</span>
172+
</button>
173+
<span class="td-mobile-topbar__title">Taskdeck</span>
174+
</div>
175+
165176
<ShellTopbar @open-command-palette="openCommandPalette" />
166177

167178
<main class="td-content" role="main">
@@ -211,4 +222,50 @@ onUnmounted(() => {
211222
padding: var(--td-space-8);
212223
background: var(--td-surface-base);
213224
}
225+
226+
/* ─── Mobile top bar (hamburger) ─── */
227+
.td-mobile-topbar {
228+
display: none;
229+
}
230+
231+
@media (max-width: 640px) {
232+
.td-mobile-topbar {
233+
display: flex;
234+
align-items: center;
235+
gap: var(--td-space-3);
236+
padding: var(--td-space-3) var(--td-space-4);
237+
background: var(--td-surface-container);
238+
border-bottom: 1px solid var(--td-border-ghost);
239+
}
240+
241+
.td-mobile-topbar__hamburger {
242+
display: flex;
243+
align-items: center;
244+
justify-content: center;
245+
width: 44px;
246+
height: 44px;
247+
border: none;
248+
background: transparent;
249+
color: var(--td-text-primary);
250+
cursor: pointer;
251+
border-radius: var(--td-radius-md);
252+
}
253+
254+
.td-mobile-topbar__hamburger:hover {
255+
background: var(--td-surface-container-high);
256+
}
257+
258+
.td-mobile-topbar__title {
259+
font-family: 'Manrope', system-ui, sans-serif;
260+
font-size: var(--td-font-lg);
261+
font-weight: 800;
262+
letter-spacing: -0.04em;
263+
color: var(--td-text-primary);
264+
}
265+
266+
.td-content {
267+
padding: var(--td-space-4);
268+
overflow-x: hidden; /* mobile safeguard — prevents horizontal scroll from wide content */
269+
}
270+
}
214271
</style>

frontend/taskdeck-web/src/components/shell/ShellSidebar.vue

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script setup lang="ts">
2-
import { computed, ref } from 'vue'
2+
import { computed, onUnmounted, ref, watch } from 'vue'
33
import { useRoute } from 'vue-router'
4+
import { registerEscapeHandler } from '../../composables/useEscapeStack'
45
import { useFeatureFlagStore } from '../../store/featureFlagStore'
56
import { useWorkspaceStore } from '../../store/workspaceStore'
67
import type { FeatureFlags } from '../../types/feature-flags'
@@ -33,6 +34,35 @@ const featureFlags = useFeatureFlagStore()
3334
const workspace = useWorkspaceStore()
3435
3536
const sidebarCollapsed = ref(false)
37+
const mobileOpen = ref(false)
38+
39+
function closeMobileMenu() {
40+
mobileOpen.value = false
41+
}
42+
43+
function toggleMobileMenu() {
44+
mobileOpen.value = !mobileOpen.value
45+
}
46+
47+
// Lock body scroll and register Escape handler while mobile menu is open
48+
watch(mobileOpen, (isOpen, _, onCleanup) => {
49+
if (!isOpen) return
50+
51+
document.body.style.overflow = 'hidden'
52+
const unregisterEscape = registerEscapeHandler(closeMobileMenu)
53+
54+
onCleanup(() => {
55+
document.body.style.overflow = ''
56+
unregisterEscape()
57+
})
58+
})
59+
60+
onUnmounted(() => {
61+
// Safety: restore scroll if component unmounts while open
62+
if (mobileOpen.value) {
63+
document.body.style.overflow = ''
64+
}
65+
})
3666
3767
const navCatalog: NavItem[] = [
3868
{
@@ -221,13 +251,22 @@ function toggleSidebar() {
221251
*/
222252
defineExpose({
223253
availableNavItems,
254+
mobileOpen,
255+
toggleMobileMenu,
256+
closeMobileMenu,
224257
})
225258
</script>
226259

227260
<template>
261+
<div
262+
v-if="mobileOpen"
263+
class="td-sidebar-overlay"
264+
aria-hidden="true"
265+
@click="closeMobileMenu"
266+
/>
228267
<aside
229268
class="td-sidebar"
230-
:class="{ 'td-sidebar--collapsed': sidebarCollapsed }"
269+
:class="{ 'td-sidebar--collapsed': sidebarCollapsed, 'td-sidebar--mobile-open': mobileOpen }"
231270
role="navigation"
232271
aria-label="Main navigation"
233272
>
@@ -253,6 +292,7 @@ defineExpose({
253292
class="td-nav-item"
254293
:class="{ 'td-nav-item--active': isActiveRoute(item.path) }"
255294
:aria-current="isActiveRoute(item.path) ? 'page' : undefined"
295+
@click="closeMobileMenu"
256296
>
257297
<span class="td-nav-item__icon">{{ item.icon }}</span>
258298
<span v-if="!sidebarCollapsed" class="td-nav-item__label">{{ item.label }}</span>
@@ -272,6 +312,7 @@ defineExpose({
272312
class="td-nav-item td-nav-item--secondary"
273313
:class="{ 'td-nav-item--active': isActiveRoute(item.path) }"
274314
:aria-current="isActiveRoute(item.path) ? 'page' : undefined"
315+
@click="closeMobileMenu"
275316
>
276317
<span class="td-nav-item__icon">{{ item.icon }}</span>
277318
<span v-if="!sidebarCollapsed" class="td-nav-item__label">{{ item.label }}</span>
@@ -490,4 +531,38 @@ defineExpose({
490531
flex-direction: column;
491532
gap: 1px;
492533
}
534+
535+
/* ─── Mobile overlay ─── */
536+
.td-sidebar-overlay {
537+
display: none;
538+
}
539+
540+
/* ─── Mobile: sidebar off-canvas ─── */
541+
@media (max-width: 640px) {
542+
.td-sidebar {
543+
position: fixed;
544+
top: 0;
545+
left: 0;
546+
bottom: 0;
547+
transform: translateX(-100%);
548+
transition: transform 0.25s ease;
549+
z-index: 50;
550+
}
551+
552+
.td-sidebar--mobile-open {
553+
transform: translateX(0);
554+
}
555+
556+
.td-sidebar-overlay {
557+
display: block;
558+
position: fixed;
559+
inset: 0;
560+
background: rgba(0, 0, 0, 0.5);
561+
z-index: 45;
562+
}
563+
564+
.td-nav-item {
565+
min-height: 44px;
566+
}
567+
}
493568
</style>

frontend/taskdeck-web/src/components/shell/ShellTopbar.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,4 +257,10 @@ function handleWorkspaceModeChange(event: Event) {
257257
justify-content: space-between;
258258
}
259259
}
260+
261+
@media (max-width: 640px) {
262+
.td-topbar {
263+
display: none;
264+
}
265+
}
260266
</style>

frontend/taskdeck-web/src/views/HomeView.vue

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,4 +723,66 @@ onActivated(refreshHomeSummary)
723723
grid-template-columns: 1fr;
724724
}
725725
}
726+
727+
@media (max-width: 640px) {
728+
.td-home {
729+
gap: var(--td-space-4);
730+
}
731+
732+
.td-home__hero {
733+
padding: var(--td-space-4);
734+
gap: var(--td-space-4);
735+
border-radius: var(--td-radius-lg);
736+
}
737+
738+
.td-home__hero-actions {
739+
flex-direction: column;
740+
width: 100%;
741+
}
742+
743+
.td-home__hero-actions .td-btn {
744+
width: 100%;
745+
min-height: 44px;
746+
justify-content: center;
747+
}
748+
749+
.td-home__onboarding-steps {
750+
grid-template-columns: 1fr;
751+
}
752+
753+
.td-home__onboarding-actions {
754+
flex-direction: column;
755+
}
756+
757+
.td-home__onboarding-actions .td-btn {
758+
width: 100%;
759+
min-height: 44px;
760+
justify-content: center;
761+
}
762+
763+
.td-home-step {
764+
min-height: 44px;
765+
}
766+
767+
.td-home__grid {
768+
gap: var(--td-space-3);
769+
}
770+
771+
/* stats inherit 1fr from 768px breakpoint — single column on mobile */
772+
773+
.td-home-action {
774+
min-height: 44px;
775+
padding: var(--td-space-3) var(--td-space-4);
776+
}
777+
778+
.td-home-board {
779+
min-height: 44px;
780+
padding: var(--td-space-3) var(--td-space-4);
781+
}
782+
783+
.td-home__subtitle {
784+
font-size: var(--td-font-sm);
785+
line-height: 1.5;
786+
}
787+
}
726788
</style>

0 commit comments

Comments
 (0)