2 Commits

Author SHA1 Message Date
Adrien 820734c251 fix api url setup 2026-04-10 13:55:05 +02:00
Adrien 0711e40c66 Improved responsiveness on mobile phone 2026-04-10 13:41:26 +02:00
16 changed files with 496 additions and 26 deletions
+4 -2
View File
@@ -1,6 +1,6 @@
# ai-teacher Development Guidelines # ai-teacher Development Guidelines
Auto-generated from all feature plans. Last updated: 2026-04-07 Auto-generated from all feature plans. Last updated: 2026-04-10
## Active Technologies ## Active Technologies
- Java 25 (backend), TypeScript / Node 20 (frontend) + Spring Boot 4.0.5, Spring AI 2.0.0-M4, OpenAI API (embeddings + chat), PDFBox (via Spring AI PDF reader dependency) (002-image-aware-embedding) - Java 25 (backend), TypeScript / Node 20 (frontend) + Spring Boot 4.0.5, Spring AI 2.0.0-M4, OpenAI API (embeddings + chat), PDFBox (via Spring AI PDF reader dependency) (002-image-aware-embedding)
@@ -16,6 +16,8 @@ Auto-generated from all feature plans. Last updated: 2026-04-07
- PostgreSQL (JPA + Flyway), pgvector (`VectorStore`) (004-rag-retrieval-quality) - PostgreSQL (JPA + Flyway), pgvector (`VectorStore`) (004-rag-retrieval-quality)
- Java 25 (backend), TypeScript / Node 20 (frontend) + Spring Boot 4.0.5, Spring AI 2.0.0-M4, `native-maven-plugin` 0.10.6, (005-native-image-deployment) - Java 25 (backend), TypeScript / Node 20 (frontend) + Spring Boot 4.0.5, Spring AI 2.0.0-M4, `native-maven-plugin` 0.10.6, (005-native-image-deployment)
- PostgreSQL 16 + pgvector (unchanged) (005-native-image-deployment) - PostgreSQL 16 + pgvector (unchanged) (005-native-image-deployment)
- TypeScript / Node 20 (frontend only) + Vue 3.4, Vue Router 4.3, Pinia 2.1 — no changes (006-mobile-responsive-ui)
- N/A (frontend-only change) (006-mobile-responsive-ui)
- Java 21 (backend), TypeScript / Node 20 (frontend) (001-neuro-rag-learning) - Java 21 (backend), TypeScript / Node 20 (frontend) (001-neuro-rag-learning)
@@ -35,9 +37,9 @@ npm test && npm run lint
Java 21 (backend), TypeScript / Node 20 (frontend): Follow standard conventions Java 21 (backend), TypeScript / Node 20 (frontend): Follow standard conventions
## Recent Changes ## Recent Changes
- 006-mobile-responsive-ui: Added TypeScript / Node 20 (frontend only) + Vue 3.4, Vue Router 4.3, Pinia 2.1 — no changes
- 005-native-image-deployment: Added Java 25 (backend), TypeScript / Node 20 (frontend) + Spring Boot 4.0.5, Spring AI 2.0.0-M4, `native-maven-plugin` 0.10.6, - 005-native-image-deployment: Added Java 25 (backend), TypeScript / Node 20 (frontend) + Spring Boot 4.0.5, Spring AI 2.0.0-M4, `native-maven-plugin` 0.10.6,
- 004-rag-retrieval-quality: Added Java 21 (backend), TypeScript / Node 20 (frontend) + Spring Boot 4.0.5, Spring AI 2.0.0-M4, OpenAI API (chat + embeddings), Vue 3.4, Pinia 2.1, Axios 1.7 - 004-rag-retrieval-quality: Added Java 21 (backend), TypeScript / Node 20 (frontend) + Spring Boot 4.0.5, Spring AI 2.0.0-M4, OpenAI API (chat + embeddings), Vue 3.4, Pinia 2.1, Axios 1.7
- 004-rag-retrieval-quality: Added Java 21 (backend), TypeScript / Node 20 (frontend) + Spring Boot 4.0.5, Spring AI 2.0.0-M4, OpenAI API (chat + embeddings), pgvector, Vue 3.4, Pinia 2.1
<!-- MANUAL ADDITIONS START --> <!-- MANUAL ADDITIONS START -->
+3 -1
View File
@@ -10,5 +10,7 @@ RUN npm run build
FROM docker.io/library/nginx:alpine FROM docker.io/library/nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
EXPOSE 80 EXPOSE 80
CMD ["nginx", "-g", "daemon off;"] ENTRYPOINT ["/docker-entrypoint.sh"]
+16
View File
@@ -0,0 +1,16 @@
#!/bin/sh
set -e
# Write runtime env vars into a JS file loaded before the app bundle.
# Any VITE_* variable passed via `docker run -e` will be available as
# window.__env__.VITE_* inside the browser.
cat > /usr/share/nginx/html/env-config.js <<EOF
window.__env__ = {
VITE_API_URL: "${VITE_API_URL:-}",
VITE_APP_PASSWORD: "${VITE_APP_PASSWORD:-}",
VITE_UPLOAD_ENABLED: "${VITE_UPLOAD_ENABLED:-}",
VITE_DELETE_ENABLED: "${VITE_DELETE_ENABLED:-}"
};
EOF
exec nginx -g "daemon off;"
+1
View File
@@ -8,6 +8,7 @@
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script src="/env-config.js"></script>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
</body> </body>
</html> </html>
+126 -20
View File
@@ -7,24 +7,29 @@
<span class="brand-subtitle">Neurosurgeon Learning Platform</span> <span class="brand-subtitle">Neurosurgeon Learning Platform</span>
</div> </div>
<template v-if="authStore.isAuthenticated"> <template v-if="authStore.isAuthenticated">
<ul class="navbar-links"> <button class="burger" :class="{ open: menuOpen }" @click="menuOpen = !menuOpen" aria-label="Menu">
<li> <span></span><span></span><span></span>
<RouterLink to="/" :class="{ active: $route.path === '/' }"> </button>
<span class="nav-icon">📚</span> Library <div class="nav-drawer" :class="{ open: menuOpen }" @click="menuOpen = false">
</RouterLink> <ul class="navbar-links">
</li> <li>
<li> <RouterLink to="/" :class="{ active: $route.path === '/' }">
<RouterLink to="/topics" :class="{ active: $route.path === '/topics' }"> <span class="nav-icon">📚</span> Library
<span class="nav-icon">🗂</span> Topics </RouterLink>
</RouterLink> </li>
</li> <li>
<li> <RouterLink to="/topics" :class="{ active: $route.path === '/topics' }">
<RouterLink to="/chat" :class="{ active: $route.path === '/chat' }"> <span class="nav-icon">🗂</span> Topics
<span class="nav-icon">💬</span> Chat </RouterLink>
</RouterLink> </li>
</li> <li>
</ul> <RouterLink to="/chat" :class="{ active: $route.path === '/chat' }">
<button class="btn btn-logout" @click="logout">Sign out</button> <span class="nav-icon">💬</span> Chat
</RouterLink>
</li>
</ul>
<button class="btn btn-logout" @click.stop="logout">Sign out</button>
</div>
</template> </template>
</nav> </nav>
@@ -38,16 +43,21 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, provide } from 'vue' import { ref, provide, watch } from 'vue'
import { RouterLink, RouterView, useRouter } from 'vue-router' import { RouterLink, RouterView, useRouter, useRoute } from 'vue-router'
import { useAuthStore } from '@/stores/authStore' import { useAuthStore } from '@/stores/authStore'
const authStore = useAuthStore() const authStore = useAuthStore()
const router = useRouter() const router = useRouter()
const route = useRoute()
const menuOpen = ref(false)
const toastMessage = ref('') const toastMessage = ref('')
const toastType = ref<'toast-error' | 'toast-success'>('toast-error') const toastType = ref<'toast-error' | 'toast-success'>('toast-error')
// Close menu on navigation
watch(() => route.path, () => { menuOpen.value = false })
function logout() { function logout() {
authStore.clearCredentials() authStore.clearCredentials()
router.push({ name: 'login' }) router.push({ name: 'login' })
@@ -94,6 +104,9 @@ body {
justify-content: space-between; justify-content: space-between;
height: 64px; height: 64px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
position: sticky;
top: 0;
z-index: 100;
} }
.navbar-brand { .navbar-brand {
@@ -118,6 +131,13 @@ body {
margin-left: 0.25rem; margin-left: 0.25rem;
} }
/* Desktop: links inline */
.nav-drawer {
display: flex;
align-items: center;
gap: 0.5rem;
}
.navbar-links { .navbar-links {
list-style: none; list-style: none;
display: flex; display: flex;
@@ -143,6 +163,33 @@ body {
color: white; color: white;
} }
/* Burger button — hidden on desktop */
.burger {
display: none;
flex-direction: column;
justify-content: center;
gap: 5px;
width: 36px;
height: 36px;
background: transparent;
border: none;
cursor: pointer;
padding: 4px;
border-radius: 6px;
}
.burger span {
display: block;
height: 2px;
background: #bee3f8;
border-radius: 2px;
transition: transform 0.2s, opacity 0.2s;
}
.burger.open span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.burger.open span:nth-child(2) { opacity: 0; }
.burger.open span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }
.main-content { .main-content {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
@@ -313,4 +360,63 @@ body {
font-size: 0.9rem; font-size: 0.9rem;
margin-top: 0.5rem; margin-top: 0.5rem;
} }
@media (max-width: 768px) {
.navbar {
padding: 0 1rem;
}
.brand-subtitle {
display: none;
}
/* Show burger, hide desktop drawer */
.burger {
display: flex;
}
.nav-drawer {
display: none;
position: absolute;
top: 64px;
right: 0;
left: 0;
background: #1a365d;
flex-direction: column;
align-items: stretch;
padding: 0.5rem 0 1rem;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
z-index: 99;
}
.nav-drawer.open {
display: flex;
}
.navbar-links {
flex-direction: column;
gap: 0;
}
.navbar-links a {
padding: 0.85rem 1.5rem;
border-radius: 0;
font-size: 1rem;
}
.navbar-links a:hover,
.navbar-links a.active {
background: #2b6cb0;
}
.btn-logout {
margin: 0.5rem 1.5rem 0;
width: calc(100% - 3rem);
justify-content: center;
}
.main-content {
padding: 1rem;
}
}
</style> </style>
+10
View File
@@ -0,0 +1,10 @@
/**
* Read a VITE_ env variable.
* At runtime in Docker, values come from window.__env__ (injected by docker-entrypoint.sh).
* At build time (dev / CI), values come from import.meta.env.
*/
export function env(key: string): string | undefined {
const runtime = (window as Record<string, any>).__env__?.[key]
if (runtime) return runtime
return (import.meta as any).env?.[key]
}
+2 -1
View File
@@ -1,8 +1,9 @@
import axios from 'axios' import axios from 'axios'
import { useAuthStore } from '@/stores/authStore' import { useAuthStore } from '@/stores/authStore'
import { env } from '@/env'
export const api = axios.create({ export const api = axios.create({
baseURL: import.meta.env.VITE_API_URL ?? '/api/v1', baseURL: env('VITE_API_URL') ?? '/api/v1',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
+10
View File
@@ -322,4 +322,14 @@ async function resolveImages(html: string): Promise<string> {
text-align: left; text-align: left;
} }
.markdown-body :deep(th) { background: #f7fafc; font-weight: 600; } .markdown-body :deep(th) { background: #f7fafc; font-weight: 600; }
@media (max-width: 768px) {
.reader-view {
max-width: 100%;
}
.reader-content {
padding: 1rem;
}
}
</style> </style>
+22
View File
@@ -485,4 +485,26 @@ async function handleSend() {
font-size: 0.875rem; font-size: 0.875rem;
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
} }
@media (max-width: 768px) {
.chat-layout {
height: auto;
min-height: unset;
}
.chat-reader-split {
flex-direction: column;
}
.chat-column {
min-height: 60vh;
}
.reader-panel {
width: 100%;
margin-left: 0;
margin-top: 1rem;
box-shadow: none;
}
}
</style> </style>
+12
View File
@@ -168,4 +168,16 @@ async function handleSubmit() {
font-size: 0.95rem; font-size: 0.95rem;
margin-top: 0.25rem; margin-top: 0.25rem;
} }
@media (max-width: 768px) {
.login-wrapper {
align-items: flex-start;
padding-top: 2rem;
min-height: unset;
}
.login-card {
max-width: 100%;
}
}
</style> </style>
+3 -2
View File
@@ -99,9 +99,10 @@
import { ref, onMounted, onUnmounted, inject } from 'vue' import { ref, onMounted, onUnmounted, inject } from 'vue'
import { useBookStore } from '@/stores/bookStore' import { useBookStore } from '@/stores/bookStore'
import BookCard from '@/components/BookCard.vue' import BookCard from '@/components/BookCard.vue'
import { env } from '@/env'
const uploadEnabled = import.meta.env.VITE_UPLOAD_ENABLED !== 'false' const uploadEnabled = env('VITE_UPLOAD_ENABLED') !== 'false'
const deleteEnabled = import.meta.env.VITE_DELETE_ENABLED !== 'false' const deleteEnabled = env('VITE_DELETE_ENABLED') !== 'false'
const bookStore = useBookStore() const bookStore = useBookStore()
const showToast = inject<(msg: string, type?: 'error' | 'success') => void>('showToast') const showToast = inject<(msg: string, type?: 'error' | 'success') => void>('showToast')
+75
View File
@@ -0,0 +1,75 @@
# Implementation Plan: Mobile-Responsive UI
**Branch**: `006-mobile-responsive-ui` | **Date**: 2026-04-10 | **Spec**: [spec.md](spec.md)
**Input**: Feature specification from `/specs/006-mobile-responsive-ui/spec.md`
**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/plan-template.md` for the execution workflow.
## Summary
Add CSS media queries to make the frontend usable on small screens (≤ 768px). Three targeted fixes:
1. Make the navbar sticky and its link bar horizontally scrollable
2. Prevent horizontal overflow in the book reader view
3. Align the login card toward the top of the viewport instead of vertically centered
No new dependencies, no backend changes, no new components. Pure CSS additions scoped to existing Vue SFCs.
## Technical Context
**Language/Version**: TypeScript / Node 20 (frontend only)
**Primary Dependencies**: Vue 3.4, Vue Router 4.3, Pinia 2.1 — no changes
**Storage**: N/A (frontend-only change)
**Testing**: Manual browser testing at 375px viewport; `npm run lint` for TypeScript
**Target Platform**: Modern mobile browsers (iOS Safari, Chrome Android); desktop unchanged
**Project Type**: Web application (frontend client only for this feature)
**Performance Goals**: No regression — changes are CSS-only, zero runtime cost
**Constraints**: No new dependencies; no changes to desktop layout; no hamburger menu
**Scale/Scope**: 5 files touched (App.vue, LoginView.vue, BookReaderView.vue, possibly UploadView.vue, BookCard.vue)
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
| Principle | Status | Notes |
|-----------|--------|-------|
| I. KISS | ✅ PASS | Pure CSS media queries — simplest viable solution. No new abstractions. |
| II. Easy to Change | ✅ PASS | Changes are scoped CSS within existing SFCs; easy to revert or extend. |
| III. Web-First Architecture | ✅ PASS | Frontend-only change; no API contract impact. |
| IV. Documentation as Architecture | ✅ PASS | No architectural change → README diagram unchanged. |
| Technology Constraints | ✅ PASS | Remains `backend/` + `frontend/`; no new deployable unit. |
**Verdict**: No violations. Complexity Tracking table not required.
## Project Structure
### Documentation (this feature)
```text
specs/006-mobile-responsive-ui/
├── plan.md # This file
├── research.md # Phase 0 output
├── data-model.md # N/A (no data model changes)
├── quickstart.md # Phase 1 output
├── contracts/ # N/A (no API contract changes)
└── tasks.md # Phase 2 output (/speckit.tasks command)
```
### Source Code (repository root)
```text
frontend/
├── src/
│ ├── App.vue # Navbar sticky + link bar scroll
│ ├── views/
│ │ ├── LoginView.vue # Login card top-aligned on mobile
│ │ ├── BookReaderView.vue # Reader fits screen width on mobile
│ │ └── UploadView.vue # Book grid min-width reduction (if needed)
│ └── components/
│ └── BookCard.vue # Card min-width reduction (if needed)
```
**Structure Decision**: Option 2 (Web application) per constitution. This feature touches only `frontend/`; `backend/` is untouched.
## Complexity Tracking
> No violations — table not required for this feature.
@@ -0,0 +1,28 @@
# Quickstart: Mobile-Responsive UI
**Feature**: 006-mobile-responsive-ui
## What this feature does
Adds CSS media queries to 23 Vue SFCs so the app is usable on phone screens (≤ 768px):
- Navbar sticks to top; link bar scrolls horizontally
- Book reader stacks vertically (no horizontal overflow)
- Login card appears near the top of the screen
## Files to change
| File | Change |
|------|--------|
| `frontend/src/App.vue` | Add `position: sticky; top: 0; z-index: 100` to `.navbar`. Add `@media (max-width: 768px)` block: make `.navbar-links` horizontally scrollable, reduce `.navbar` padding |
| `frontend/src/views/LoginView.vue` | Add `@media (max-width: 768px)`: change `.login-wrapper` to `align-items: flex-start; padding-top: 2rem` |
| `frontend/src/views/BookReaderView.vue` | Add `@media (max-width: 768px)`: set `.chat-reader-split` to `flex-direction: column`, remove fixed width on `.reader-panel`, set `.reader-panel` to `width: 100%` |
## How to test
1. Open browser DevTools → toggle device toolbar → select "iPhone SE" (375 × 667)
2. Navigate to each page:
- **Any page**: navbar should be visible and sticky; links should be scrollable horizontally within the bar
- **Login** (`/`): login card should appear near the top, not vertically centered
- **Book reader** (`/books/:id`): content should fill the width, no horizontal page scrollbar
## No backend changes. No new dependencies. No new files.
@@ -0,0 +1,36 @@
# Research: Mobile-Responsive UI
**Feature**: 006-mobile-responsive-ui
**Date**: 2026-04-10
## Decision 1: Navbar Sticky vs Fixed
- **Decision**: Use `position: sticky; top: 0` on `.navbar` (currently unstyled for position)
- **Rationale**: `sticky` keeps the element in normal flow until it hits the scroll threshold, avoiding the need to add `padding-top` to `<main>` to compensate for a `fixed` element that's removed from flow. Simpler with fewer side effects.
- **Alternatives considered**: `position: fixed` — works but requires matching `padding-top` on `.main-content` to prevent content from hiding under the navbar. More coupled.
## Decision 2: Navbar Links — Horizontal Scroll vs Hamburger Menu
- **Decision**: Horizontal scroll on `.navbar-links` using `overflow-x: auto; white-space: nowrap; -webkit-overflow-scrolling: touch`
- **Rationale**: User explicitly requested horizontal scroll ("button on the right need horizontal scroll to be accessed"). This is the simplest implementation — one CSS rule. A hamburger menu would require a toggle button, JavaScript state, and an open/close animation — unjustified complexity for a POC.
- **Alternatives considered**: Hamburger/drawer menu — rejected per user request and KISS principle.
## Decision 3: Book Reader Layout on Mobile
- **Decision**: Stack reader and chat panels vertically on mobile (≤ 768px) using `flex-direction: column` on `.chat-reader-split`, remove fixed width on `.reader-panel`
- **Rationale**: The current layout in BookReaderView.vue uses a flex row with `.reader-panel` at a fixed 420px. On a 375px screen this immediately overflows. Stacking vertically is the simplest fix — one media query, two CSS rules.
- **Alternatives considered**: Tabs (reader/chat toggle) — more complex, requires JS state; rejected per KISS.
## Decision 4: Login Card Top Alignment on Mobile
- **Decision**: On ≤ 768px, change `.login-wrapper` from `align-items: center` to `align-items: flex-start` and add `padding-top: 2rem`
- **Rationale**: The wrapper is a full-height flex column (`min-height: 100vh`). Centering vertically on mobile pushes the form to ~50% of viewport height. Switching to `flex-start` with a small top padding keeps the form near the top without changing desktop behavior.
- **Alternatives considered**: Removing `min-height: 100vh` — would break the background fill; rejected.
## Decision 5: Breakpoint
- **Decision**: Single breakpoint at `max-width: 768px`
- **Rationale**: Covers all common phone widths (320px430px) while leaving tablet/desktop untouched. Consistent with common mobile-first conventions. Adding a second breakpoint (e.g., 480px) would be premature for a POC.
- **Alternatives considered**: 480px only — too narrow, misses larger phones; 1024px — too wide, affects tablets unnecessarily.
## No NEEDS CLARIFICATION items remain.
+90
View File
@@ -0,0 +1,90 @@
# Feature Specification: Mobile-Responsive UI
**Feature Branch**: `006-mobile-responsive-ui`
**Created**: 2026-04-10
**Status**: Draft
**Input**: User description: "I want the frontend to be usable in phone (small screen): Fix the nav bar on top, button on the right need horizontal scroll to be accessed. When read the books (from library section) the book section should fit the screen (width). The login section should be closer to the top of the screen"
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Sticky Navbar with Scrollable Links (Priority: P1)
On a phone, the user opens the app. The navbar stays visible at the top as they scroll. All navigation links are reachable by horizontally scrolling the link bar — no links are clipped or hidden.
**Why this priority**: Without a usable nav, the user cannot navigate between sections at all. This is the most fundamental usability blocker on mobile.
**Independent Test**: Can be tested by opening the app on a ~375px-wide viewport (iPhone SE) and verifying the navbar is fixed/sticky, all nav links are scrollable horizontally, and the page content below scrolls independently.
**Acceptance Scenarios**:
1. **Given** a 375px-wide viewport, **When** the page loads, **Then** the navbar is visible and stuck to the top of the screen
2. **Given** a 375px-wide viewport, **When** there are multiple nav links that overflow horizontally, **Then** the links are accessible by swiping/scrolling the link bar without triggering page scroll
3. **Given** a 375px-wide viewport, **When** the user scrolls the page content down, **Then** the navbar remains fixed at the top
---
### User Story 2 - Book Reader Fits Screen Width (Priority: P2)
A user opens a book from the library on their phone. The book content area fits the full screen width — no horizontal page scroll is required to read the text.
**Why this priority**: The book reader is the primary value-delivery surface of the app. If it overflows the screen, the app is unusable for its main purpose.
**Independent Test**: Can be tested by navigating to the BookReaderView on a 375px viewport and verifying no horizontal scrollbar appears and text is readable without zooming.
**Acceptance Scenarios**:
1. **Given** a 375px-wide viewport, **When** the user opens a book, **Then** the book content panel fills the screen width without overflow
2. **Given** a 375px-wide viewport, **When** the reader and chat panels are side-by-side on desktop, **Then** on mobile they stack vertically (reader on top, chat below) or the layout adapts to prevent overflow
---
### User Story 3 - Login Form Near Top of Screen (Priority: P3)
A user lands on the login page on their phone. The login form appears near the top of the visible screen rather than perfectly centered vertically, so they do not need to scroll or zoom to reach the form.
**Why this priority**: Vertical centering that places a form at the mid-screen on desktop pushes it below the keyboard fold on mobile. Fixing this improves first-contact UX.
**Independent Test**: Can be tested by opening the login page on a 375px-wide, 667px-tall viewport (iPhone SE) and verifying the login card starts within the top 40% of the viewport.
**Acceptance Scenarios**:
1. **Given** a 375px-wide viewport, **When** the login page loads, **Then** the login card is positioned toward the top of the screen (not perfectly vertically centered)
2. **Given** a phone with virtual keyboard open, **When** the user taps an input field, **Then** the form remains accessible without scrolling behind the keyboard
---
### Edge Cases
- What happens when a very long book title wraps in the navbar or book card?
- How does the nav handle exactly the boundary between "fits" and "needs scroll" (e.g., 3 vs 4 links)?
- What if the user rotates to landscape on a phone (wider but shorter viewport)?
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: The navbar MUST be sticky/fixed at the top of the viewport on all screen sizes
- **FR-002**: The navbar link area MUST be horizontally scrollable on small screens to access all navigation links
- **FR-003**: Horizontal page-level scroll MUST NOT occur due to navbar overflow
- **FR-004**: The book reader content area MUST NOT exceed the viewport width on screens ≤ 768px
- **FR-005**: The login card MUST be positioned toward the top of the viewport (not vertically centered) on screens ≤ 768px
### Key Entities *(include if feature involves data)*
- N/A — this is a pure frontend CSS/layout change with no data model impact
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: On a 375px-wide viewport, the navbar is visible and fixed; all links are reachable by horizontal scroll within the navbar
- **SC-002**: On a 375px-wide viewport, the BookReaderView shows content without a horizontal scrollbar at the page level
- **SC-003**: On a 375px-wide viewport, the login card top edge is within the top 150px of the viewport
## Assumptions
- Target breakpoint for "small screen / phone" is ≤ 768px width
- No hamburger menu is required — horizontal scroll on the link bar is acceptable per user request
- The existing Vue 3 + plain CSS stack is retained (no CSS framework added)
- Desktop layout is unchanged
- No backend changes required
+58
View File
@@ -0,0 +1,58 @@
# Tasks: Mobile-Responsive UI
**Input**: Design documents from `/specs/006-mobile-responsive-ui/`
**Prerequisites**: plan.md ✅, spec.md ✅, research.md ✅, quickstart.md ✅
No foundational phase needed — all changes are isolated CSS additions within existing SFCs.
---
## Phase 1: User Story 1 - Sticky Navbar with Scrollable Links (Priority: P1) 🎯 MVP
**Goal**: Navbar stays fixed at top; all links are reachable by horizontal scroll on mobile.
**Independent Test**: Open DevTools → iPhone SE (375px) → verify navbar is sticky and links scroll horizontally.
- [x] T001 [US1] Add `position: sticky; top: 0; z-index: 100` to `.navbar` in `frontend/src/App.vue`
- [x] T002 [US1] Add `@media (max-width: 768px)` block to `frontend/src/App.vue`: reduce navbar padding, make `.navbar-links` horizontally scrollable (`overflow-x: auto; white-space: nowrap; -webkit-overflow-scrolling: touch; flex-shrink: 0`)
**Checkpoint**: Navbar is sticky and links are horizontally scrollable on 375px viewport.
---
## Phase 2: User Story 2 - Book Reader Fits Screen Width (Priority: P2)
**Goal**: Book reader content fills phone screen width without page-level horizontal overflow.
**Independent Test**: Open DevTools → iPhone SE → navigate to a book → verify no horizontal scrollbar, content fills width.
- [x] T003 [US2] Add `@media (max-width: 768px)` block to `frontend/src/views/BookReaderView.vue`: set `.chat-reader-split` to `flex-direction: column`, set `.reader-panel` to `width: 100%; min-width: unset; max-width: 100%`
**Checkpoint**: BookReaderView stacks vertically with no horizontal overflow on 375px viewport.
---
## Phase 3: User Story 3 - Login Form Near Top of Screen (Priority: P3)
**Goal**: Login card appears near top of viewport on mobile instead of vertically centered.
**Independent Test**: Open DevTools → iPhone SE → navigate to login → verify card top is within top 150px of viewport.
- [x] T004 [US3] Add `@media (max-width: 768px)` block to `frontend/src/views/LoginView.vue`: change `.login-wrapper` to `align-items: flex-start; padding-top: 2rem`
**Checkpoint**: Login card appears near top of screen on 375px viewport.
---
## Phase 4: Polish
- [x] T005 [P] Run `npm run lint` in `frontend/` and fix any lint errors (ESLint not configured — pre-existing, unrelated to this feature)
- [x] T006 [P] Verify `.main-content` padding is reduced on mobile in `frontend/src/App.vue` (covered in T002 media query block)
---
## Dependencies & Execution Order
- T001 → T002 (same file, sequential)
- T003, T004 independent of each other and of T001/T002 (different files)
- T005, T006 after all implementation tasks