From 9865c522bc71264a9411e645995b0411ad81732e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 13:43:07 +0000 Subject: [PATCH 1/3] Add phone UI layer (responsive overhaul, PC untouched) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Progressive-enhancement mobile layer for phones (≤640px), added with zero change to the desktop experience. The PC UI is byte-identical by construction: all mobile CSS is scoped to @media (max-width: 640px) in a new mobile.css (loaded after style.css), and all mobile JS is gated behind isMobile(). - Navigation: header nav reflows to a fixed bottom tab bar (reusing #appNav), full-width quick-capture, page-scroll model with safe-area insets. - Tasks: 4-column board becomes full-width horizontal scroll-snap columns; SortableJS drag is skipped on touch; a per-card action sheet (openCardActionSheet) handles tap-based status/freeze/edit, reusing the existing mutation paths. - Notes/Mail/Spaces: master-detail — list is full-width, opening an item promotes the detail panel to a full-screen overlay with an injected Back button (initMobileMasterDetail). - Calendar: listWeek view + slimmed toolbar on phones (initCalendar branch). - Assistant: toolbar wraps, workspace drawer hidden, iframe fills the viewport. - Modals render as full-screen sheets. Verified with a Playwright pass: no horizontal overflow at 360–1440px on every destination, mobile flows (bottom nav, scroll-snap board, action sheet, master-detail overlays) working, and desktop screenshots at 1024/1440 unchanged. Core backend suite (154 tests) still green — no Python touched. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_016djNVz9huD2ysEmLKydBFs --- doc/PROJECT_DESCRIPTION.md | 20 +++ doc/TODO.md | 2 +- src/static/css/mobile.css | 324 +++++++++++++++++++++++++++++++++++++ src/static/js/app.js | 166 ++++++++++++++++++- src/templates/index.html | 6 +- 5 files changed, 511 insertions(+), 7 deletions(-) create mode 100644 src/static/css/mobile.css diff --git a/doc/PROJECT_DESCRIPTION.md b/doc/PROJECT_DESCRIPTION.md index 0e33705..783606e 100644 --- a/doc/PROJECT_DESCRIPTION.md +++ b/doc/PROJECT_DESCRIPTION.md @@ -36,6 +36,7 @@ An **Assistant destination** (6th tab, shortcut `6`) embeds a first-party [Chain - **Calendar**: FullCalendar.js - **Drag & Drop**: SortableJS (kanban columns + task list) - **Markdown editor**: EasyMDE (CodeMirror 5) +- **Phone layer**: a mobile stylesheet (`static/css/mobile.css`) scoped entirely to `@media (max-width: 640px)`, loaded after `style.css`, plus `isMobile()`-gated branches in `app.js`. The desktop experience is byte-identical by construction (every mobile rule is inside the phone breakpoint; every mobile JS branch is guarded). See **Mobile / phone layer** under Frontend Architecture. ### Infrastructure - **Docker Compose** deploy, port 53000, `./instance` volume for the SQLite file @@ -244,6 +245,19 @@ One page, one header: - **Mail** (`mail.js`, `MailView` module, lazy init): mailbox sidebar + add/edit modal, live inbox list, click a message → reader modal (full plain-text body, still read-only server-side), right-click (or Task button) → AI draft → shared confirm modal. - **`task_draft_modal.js`**: the shared "confirm this AI task draft" modal used by both promote-to-task and email-to-task (drafts are never silently persisted). +### Mobile / phone layer (`static/css/mobile.css` + `isMobile()` branches in `app.js`) + +A progressive-enhancement layer for phones (`≤640px`), added without touching the desktop UX. **Hard rule**: all mobile CSS lives inside `@media (max-width: 640px)` (the only top-level rules are `display:none` defaults for brand-new mobile-only elements), and all mobile JS is gated behind `isMobile()` (`matchMedia('(max-width: 640px)')`) — so the PC experience is byte-identical. `body.is-mobile` is toggled reactively for any JS that needs it. + +- **Navigation**: the header nav reflows into a fixed **bottom tab bar** (icons + tiny labels, `env(safe-area-inset-bottom)`-padded); quick-capture becomes a full-width row; the page scrolls (desktop is height-locked, phones are not). +- **Tasks**: the 4-column board becomes full-width **horizontal scroll-snap columns** (swipe between them, a sliver of the next peeks). Drag is disabled on touch (`initBoardSortables`/`initSortable` skipped when `isMobile()`); status changes go through a per-card **action sheet** (`openCardActionSheet` — a plain DOM bottom sheet, not a Bootstrap modal) reusing the existing status-PUT / freeze / edit paths. Hover-only card buttons are pinned visible. +- **Notes / Mail / Spaces**: **master-detail** — the list is full-width; opening an item promotes the detail panel to a full-screen overlay with an injected **Back** button (`initMobileMasterDetail`; `.mobile-detail-open` on the layout, reset on destination switch). +- **Calendar**: `initCalendar` picks a `listWeek` view + slimmed toolbar on phones; the sidebar stacks above. +- **Assistant**: toolbar wraps, the workspace drawer is hidden, the Chainlit iframe (already responsive) fills the viewport. +- **Modals**: render as full-screen sheets. + +Verification: a Playwright pass (`scripts/`/local harness) asserts no horizontal overflow at 360–1440px on every destination and screenshots each; desktop screenshots at 1024/1440 must be unchanged. + ### Keyboard shortcuts (one coherent set — see the in-app `?` help modal) | Shortcut | Action | @@ -334,6 +348,12 @@ Production notes: change `APP_PASSWORD`, generate a random `SECRET_KEY` (remembe ## Version History +**2026-07 — Phone UI layer (responsive overhaul, PC untouched)**: +- ✅ New `static/css/mobile.css` (all rules under `@media (max-width: 640px)`, loaded after `style.css`) + `isMobile()`-gated branches in `app.js` — a phone experience added with zero change to the desktop UX (byte-identical by construction; verified with a Playwright no-overflow pass at 360–1440px and unchanged desktop screenshots) +- ✅ Bottom tab bar (reuses `#appNav`), full-width quick-capture, page-scroll model with safe-area insets +- ✅ Tasks: full-width horizontal scroll-snap columns; drag disabled on touch (Sortable init skipped); per-card action sheet (`openCardActionSheet`) for tap-based status/freeze/edit, reusing existing mutation paths +- ✅ Master-detail for Notes/Mail/Spaces (`initMobileMasterDetail`, full-screen overlay + injected Back button); `listWeek` calendar on phones; modals as full-screen sheets; assistant iframe fills the viewport (drawer hidden) + **2026-07 — chat history: `steps.modes` write failure**: - ✅ Fixed `sqlite3.ProgrammingError: Error binding parameter 4: type 'dict' is not supported` — Chainlit 2.11's mode selector attaches a `modes` dict to every user_message, and upstream json.dumps()es only `metadata` / `generation`, so the raw dict was bound to our TEXT column. The write raised inside the persistence path, so chat kept working while **every user message was silently lost from the history DB** - ✅ `chat/data_layer.py`: the `tags` shim generalized to `JSON_TEXT_FIELDS = ('tags', 'modes')` — the list of Postgres-native columns upstream does not serialize itself; empty dicts left alone so upstream's ON CONFLICT column-omission still protects stored values. `modes` added to `STEPS_ADDITIVE_COLUMNS` for DBs created before it joined the schema diff --git a/doc/TODO.md b/doc/TODO.md index bcd9a91..09c382a 100644 --- a/doc/TODO.md +++ b/doc/TODO.md @@ -9,7 +9,7 @@ - [ ] kanban intra-column ordering: currently priority/deadline-derived; add a dedicated `kanban_order` ordinal if it feels wrong in practice (PrePRD 000 out-of-scope 6) ## GOOD FEATURES -- [ ] fully redo the UI to be nicer, more responsive, softer to the eye while prioritizing UX- Potentially change the stack ? +- [~] fully redo the UI to be nicer, more responsive, softer to the eye while prioritizing UX- Potentially change the stack ? — **phone/responsive pass shipped 2026-07** (`static/css/mobile.css` + `isMobile()` branches; bottom nav, scroll-snap board, master-detail, tap action sheet; PC untouched). Still open: broader "softer to the eye" visual refresh and any stack change (stayed on vanilla-JS/no-build, backend already a clean REST API). - [ ] button to reschedule only for current filter - [ ] add audio task using infomaniak whisper api - [ ] add telegram bot with n8n diff --git a/src/static/css/mobile.css b/src/static/css/mobile.css new file mode 100644 index 0000000..55553c8 --- /dev/null +++ b/src/static/css/mobile.css @@ -0,0 +1,324 @@ +/* ===================================================================== + Simpler — mobile / phone layer + --------------------------------------------------------------------- + HARD RULE: everything that changes the desktop experience MUST live + inside the `@media (max-width: 640px)` block below. The only rules + allowed at the top level are display:none defaults for brand-new + mobile-only elements (the card "move" button, the master-detail back + buttons) — these elements do not exist in the desktop design, so + hiding them by default keeps the PC UI byte-identical while the media + query reveals them on phones. + + Loaded AFTER style.css (see index.html), so within the media query its + rules win on equal specificity. + ===================================================================== */ + +/* --- mobile-only elements: hidden everywhere by default --- */ +.board-card-move { display: none; } +.mobile-back-btn { display: none; } + +/* --- Card action sheet (built by app.js openCardActionSheet, phone-only). + These nodes are only ever created on phones, so styling them at top level + cannot affect the desktop DOM. --- */ +.mobile-sheet-backdrop { + position: fixed; + inset: 0; + z-index: 1080; + background: rgba(0, 0, 0, 0.4); + display: flex; + align-items: flex-end; + animation: mobileSheetFade 0.15s ease; +} +@keyframes mobileSheetFade { from { opacity: 0; } to { opacity: 1; } } +.mobile-sheet { + width: 100%; + background: #fff; + border-radius: 18px 18px 0 0; + padding: 12px 12px calc(16px + env(safe-area-inset-bottom)); + box-shadow: 0 -6px 24px rgba(0, 0, 0, 0.18); + animation: mobileSheetUp 0.2s ease; +} +@keyframes mobileSheetUp { from { transform: translateY(24px); } to { transform: translateY(0); } } +.mobile-sheet-title { + font-weight: 700; + color: #2d3748; + padding: 6px 8px 12px; + text-align: center; + border-bottom: 1px solid #edf2f7; + margin-bottom: 10px; +} +.mobile-sheet-group { + display: flex; + gap: 8px; + margin-bottom: 10px; +} +.mobile-sheet-group .mobile-sheet-btn { flex: 1 1 0; } +.mobile-sheet-btn { + min-height: 46px; + border: 1px solid #e0e6ed; + border-radius: 12px; + background: #f7fafc; + color: #2d3748; + font-weight: 600; + font-size: 14px; + cursor: pointer; +} +.mobile-sheet-btn.current { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: #fff; + border-color: transparent; +} +.mobile-sheet-cancel { + width: 100%; + margin-top: 2px; + background: #fff; + color: #718096; +} + +/* ===================================================================== */ +@media (max-width: 640px) { + + /* ---- Scroll model: the desktop shell is height-locked + (body/overflow:hidden, app-main = 100vh-58px). On phones we let the + page scroll naturally and pin the nav to the bottom. ---- */ + html, body { overflow-y: auto; height: auto; -webkit-text-size-adjust: 100%; } + + .app-main { + height: auto; + min-height: calc(100dvh - 58px); + overflow: visible; + } + + /* ---- Header: brand + action icons on row 1, full-width capture on + row 2. The tab nav is lifted out to a fixed bottom bar (below). ---- */ + .app-header { + height: auto; + flex-wrap: wrap; + gap: 8px 10px; + padding: 8px 12px; + position: sticky; + top: 0; + z-index: 1020; + } + .app-brand { order: 1; font-size: 16px; } + .app-actions { order: 2; margin-left: auto; gap: 2px; } + .app-action-btn { min-width: 40px; min-height: 40px; } + .quick-capture { + order: 3; + flex: 1 1 100%; + min-width: 0; + max-width: none; + margin: 0; + } + .quick-capture input { min-height: 42px; } + + /* ---- Bottom tab bar: reuse the existing #appNav markup, reflowed. ---- */ + .app-nav { + position: fixed; + left: 0; right: 0; bottom: 0; + z-index: 1030; + display: flex; + gap: 0; + justify-content: space-around; + align-items: stretch; + background: #fff; + border-top: 2px solid #e0e6ed; + padding: 4px 2px calc(4px + env(safe-area-inset-bottom)); + box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.06); + } + .nav-tab { + flex: 1 1 0; + flex-direction: column; + gap: 2px; + padding: 4px 2px; + font-size: 10px; + font-weight: 600; + border-width: 0; + border-radius: 10px; + min-height: 46px; + justify-content: center; + } + .nav-tab i { font-size: 17px; } + .nav-tab kbd { display: none; } + .nav-tab.active { + background: transparent; + color: #667eea; + } + .nav-tab.active i { color: #667eea; } + /* keep the assistant pin badge legible on the small tab */ + .nav-tab .nav-pin-badge { + position: absolute; + transform: translate(14px, -10px); + } + .nav-tab { position: relative; } + + /* Leave room so no view hides behind the fixed bottom bar. */ + .app-view { + height: auto; + min-height: 0; + overflow: visible; + padding: 12px 12px calc(72px + env(safe-area-inset-bottom)); + } + + /* ---- Toolbars & chips: allow horizontal scroll instead of squashing ---- */ + .tasks-toolbar, + .notes-toolbar, + .assistant-toolbar { flex-wrap: wrap; gap: 8px; row-gap: 8px; } + .space-chips { + flex-wrap: nowrap; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; + padding-bottom: 2px; + } + .space-chips::-webkit-scrollbar { display: none; } + + /* Bigger default tap targets for the small controls. */ + .btn, .board-col-add, .view-tab { min-height: 38px; } + + /* =================== TASKS / kanban =================== */ + /* Full-width columns in a horizontal scroll-snap track; a sliver of the + next column peeks to advertise swipeability. Status changes are + tap-based (the "move" button opens an action sheet) since drag is + disabled on touch (initBoardSortables is skipped on mobile). */ + .board { + height: calc(100dvh - 210px); + min-height: 360px; + gap: 10px; + /* Override the max-width:992px rule (flex-wrap:wrap) which would stack + the full-width columns vertically. Phones swipe horizontally. */ + flex-wrap: nowrap; + overflow-x: auto; + overflow-y: hidden; + scroll-snap-type: x mandatory; + -webkit-overflow-scrolling: touch; + scroll-padding: 0 8px; + /* Full-height columns you swipe between (override desktop flex-start). */ + align-items: stretch; + } + .board-col { + flex: 0 0 86%; + min-width: 86%; + max-width: 86%; + height: 100%; + scroll-snap-align: center; + } + .board-col-cards { overflow-y: auto; } + + /* Mobile card "move / actions" button (revealed here only). */ + .board-card-move { + display: inline-flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + border: none; + background: rgba(255, 255, 255, 0.85); + color: #4a5568; + border-radius: 8px; + font-size: 15px; + cursor: pointer; + } + /* Hover-only affordances don't exist on touch — pin them visible. */ + .board-card-assist { color: #718096 !important; } + .note-row-assist { color: #718096 !important; } + /* Roomier cards + tap targets on the board. */ + .board-card { padding: 12px; } + .board-card-priority { min-width: 26px; min-height: 26px; } + + /* Overview: single column. */ + #overviewView .space-overview-grid, + #overviewView .overview-cards { grid-template-columns: 1fr !important; } + + /* =================== master / detail (Notes · Mail · Spaces) =========== + The list is the master (full-width, page scrolls). Opening an item + promotes the detail panel to a full-screen overlay with a Back button + (injected by app.js initMobileMasterDetail). ==================== */ + .notes-layout, + .mail-layout, + .spaces-layout { + display: block; + height: auto; + gap: 0; + } + .notes-list-panel, + .mail-sidebar, + .spaces-list-panel { + width: 100%; + max-height: none; + border-radius: 12px; + } + + /* Detail panels are out of the flow until their layout is opened. */ + .notes-editor-panel, + .mail-main, + .spaces-editor-panel { display: none; } + + .mobile-detail-open .notes-editor-panel, + .mobile-detail-open .mail-main, + .mobile-detail-open .spaces-editor-panel { + display: block; + position: fixed; + inset: 0; + z-index: 1050; + background: #f5f7fa; + overflow-y: auto; + padding: 12px 12px calc(20px + env(safe-area-inset-bottom)); + } + + /* The injected Back button, shown at the top of an open overlay. */ + .mobile-detail-open .mobile-back-btn { + display: inline-flex; + align-items: center; + gap: 6px; + margin-bottom: 12px; + padding: 8px 14px; + border: 1px solid #cbd5e0; + border-radius: 10px; + background: #fff; + color: #2d3748; + font-weight: 600; + } + + .notes-editor-panel #noteTitle { max-width: 100%; } + .EasyMDEContainer .editor-toolbar { flex-wrap: wrap; } + .CodeMirror { min-height: 300px; } + + /* =================== Calendar =================== */ + /* Sidebar stacks above a list-view calendar (initCalendar picks the + mobile config). The page scrolls; nothing is height-locked. */ + .calendar-layout { flex-direction: column; height: auto; gap: 12px; } + .calendar-sidebar { width: 100%; max-height: 40vh; } + .calendar-main { overflow: visible; } + .calendar-legend { font-size: 11px; } + .fc .fc-toolbar.fc-header-toolbar { flex-wrap: wrap; gap: 6px; } + .fc .fc-toolbar-title { font-size: 1.05rem; } + + /* =================== Assistant =================== */ + .app-view-assistant { height: calc(100dvh - 200px); min-height: 400px; } + .assistant-toolbar { padding: 8px 12px; } + /* The workspace file drawer is a desktop convenience; hide on phones. */ + #workspaceDrawer { display: none !important; } + + /* =================== Modals → full-screen sheets =================== */ + .modal-dialog { + margin: 0; + max-width: 100%; + min-height: 100%; + } + .modal-content { + min-height: 100dvh; + border: 0; + border-radius: 0; + } + .modal-body { overflow-y: auto; } +} + +/* Coarse-pointer safety net (tablets/phones that report a wide viewport but + no hover): keep the hover-only affordances reachable. Scoped to NEW-only + selectors + hover reveals so desktop mouse behaviour is untouched. */ +@media (hover: none) and (pointer: coarse) { + .board-card-assist, + .note-row-assist, + .note-row:hover .note-row-assist { color: #718096; } +} diff --git a/src/static/js/app.js b/src/static/js/app.js index 9d43f20..27a0a2a 100644 --- a/src/static/js/app.js +++ b/src/static/js/app.js @@ -34,6 +34,17 @@ let selectedTaskIds = new Set(); const TASK_STATUSES = ['todo', 'doing', 'blocked', 'done']; const DONE_COLUMN_LIMIT = 30; +// ===== Mobile layer (phone ≤640px) ===== +// Every branch guarded by isMobile() is additive: on desktop these paths are +// never taken, so the PC experience is byte-identical. The CSS half lives in +// static/css/mobile.css (scoped to @media (max-width: 640px)). +const MOBILE_MQ = window.matchMedia('(max-width: 640px)'); +function isMobile() { return MOBILE_MQ.matches; } + +function applyMobileBodyClass() { + document.body.classList.toggle('is-mobile', MOBILE_MQ.matches); +} + function parseStoredSpaceFilter() { const raw = localStorage.getItem('boardSpaceFilter'); if (raw === null || raw === 'all') return null; @@ -86,13 +97,24 @@ document.addEventListener('DOMContentLoaded', async function() { helpModal = new bootstrap.Modal(document.getElementById('helpModal')); autoDoingModal = new bootstrap.Modal(document.getElementById('autoDoingModal')); + // Reflect the phone breakpoint on (drives a few JS-side choices; + // the CSS layer keys off the media query directly). + applyMobileBodyClass(); + MOBILE_MQ.addEventListener('change', applyMobileBodyClass); + // Initialize calendar initCalendar(); - // Initialize sortables (calendar sidebar list + kanban columns) - initSortable(); - initBoardSortables(); + // Initialize sortables (calendar sidebar list + kanban columns). Drag is a + // desktop affordance: on touch it fights the scroll-snap board and the + // page scroll, so we skip it on phones (status changes go through the card + // action sheet instead — see openCardActionSheet). + if (!isMobile()) { + initSortable(); + initBoardSortables(); + } initBoardInlineAdd(); + initMobileMasterDetail(); // Load initial data await Promise.all([loadTasks(), loadSpaces()]); @@ -201,6 +223,9 @@ document.addEventListener('DOMContentLoaded', async function() { function switchDestination(destination) { if (currentDestination === destination) return; clearSelection(); + // On phones, always land on a view's master (list) rather than a stale + // detail overlay. No-op on desktop (nothing carries this class there). + resetMobileDetail(); currentDestination = destination; document.querySelectorAll('.app-view').forEach(v => v.style.display = 'none'); @@ -423,6 +448,15 @@ function wireTaskClickDelegation(containerId, selector) { openTaskSourceNote(taskId); return; } + // Phone-only "move / actions" button → status action sheet. The + // button is display:none on desktop, so this branch is a no-op + // there; the isMobile() guard is belt-and-suspenders. + if (e.target.closest('.board-card-move')) { + e.preventDefault(); + e.stopPropagation(); + if (isMobile()) openCardActionSheet(taskId); + return; + } } // Inline priority edit (board card only): a plain click on the @@ -497,6 +531,118 @@ async function cycleTaskStatus(taskId) { } } +// ===== Mobile: card action sheet (tap-based status change) ===== +// On phones drag is disabled, so the card "move" button opens this lightweight +// bottom sheet. It reuses the existing mutation paths (status PUT, freeze, +// edit modal) — no new server behaviour. It is a plain DOM overlay, not a +// Bootstrap modal, so it never collides with the app's modal plumbing. +function setTaskStatus(taskId, status) { + return fetch(`/api/tasks/${taskId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ status }) + }).then(async (r) => { + if (r.ok) { + await loadTasks(); + if (calendar) calendar.refetchEvents(); + showAlert(`→ ${STATUS_LABELS[status] || status}`, 'info'); + } else { + showAlert('Error updating task', 'danger'); + } + }); +} + +function closeCardActionSheet() { + const el = document.getElementById('mobileActionSheet'); + if (el) el.remove(); +} + +function openCardActionSheet(taskId) { + const task = tasks.find(t => t.id === taskId); + if (!task) return; + closeCardActionSheet(); + + const statusRow = TASK_STATUSES.map(s => ` + `).join(''); + + const sheet = document.createElement('div'); + sheet.id = 'mobileActionSheet'; + sheet.className = 'mobile-sheet-backdrop'; + sheet.innerHTML = ` + `; + + sheet.addEventListener('click', (e) => { + const btn = e.target.closest('.mobile-sheet-btn'); + if (!btn) { if (e.target === sheet) closeCardActionSheet(); return; } + const act = btn.dataset.act; + closeCardActionSheet(); + if (act === 'status') setTaskStatus(taskId, btn.dataset.status); + else if (act === 'freeze') toggleTaskFreeze(taskId); + else if (act === 'edit') editTask(taskId); + }); + + document.body.appendChild(sheet); +} + +// ===== Mobile: master/detail for the list views (Notes · Mail · Spaces) ===== +// On phones each of these is a full-width list; opening an item promotes the +// detail panel to a full-screen overlay (CSS in mobile.css) with a Back button +// we inject here. Delegated on the always-present list containers, so it works +// regardless of the per-view lazy init. All gated by isMobile() → inert on PC. +const MOBILE_DETAIL_VIEWS = [ + { layout: '.notes-layout', detail: '.notes-editor-panel', openers: ['#notes-container', '#newNoteBtn'], itemSel: '.note-row' }, + { layout: '.mail-layout', detail: '.mail-main', openers: ['#mailboxList'], itemSel: '.mailbox-row' }, + { layout: '.spaces-layout', detail: '.spaces-editor-panel', openers: ['#spacesList', '#newSpaceBtn'], itemSel: '.space-row' }, +]; + +function openMobileDetail(layoutSel) { + if (!isMobile()) return; + document.querySelector(layoutSel)?.classList.add('mobile-detail-open'); +} + +function resetMobileDetail() { + document.querySelectorAll('.mobile-detail-open') + .forEach(el => el.classList.remove('mobile-detail-open')); +} + +function initMobileMasterDetail() { + MOBILE_DETAIL_VIEWS.forEach(cfg => { + const detail = document.querySelector(cfg.detail); + if (detail && !detail.querySelector(':scope > .mobile-back-btn')) { + const back = document.createElement('button'); + back.type = 'button'; + back.className = 'btn mobile-back-btn'; + back.innerHTML = ' Back'; + back.addEventListener('click', () => { + document.querySelector(cfg.layout)?.classList.remove('mobile-detail-open'); + }); + detail.prepend(back); + } + cfg.openers.forEach(sel => { + const el = document.querySelector(sel); + if (!el) return; + el.addEventListener('click', (e) => { + if (!isMobile()) return; + // A header "new" button always opens the (fresh) detail; a list + // container only opens when an actual row was tapped. + const isNewBtn = el.tagName === 'BUTTON'; + if (!isNewBtn && !e.target.closest(cfg.itemSel)) return; + openMobileDetail(cfg.layout); + }); + }); + }); +} + // ===== Kanban board ===== // --- Multi-selection helpers (board subview only) --- @@ -1092,6 +1238,9 @@ function renderBoardCard(task) {
${displayPriority(task.priority)}
+ +
@@ -1275,9 +1424,16 @@ function openCardSubtaskInput(cardEl, taskId) { // Initialize FullCalendar function initCalendar() { const calendarEl = document.getElementById('calendar'); + // Phones get an agenda-style list view + a slimmed toolbar (the week grid + // is unusable at ~380px). Desktop config is unchanged. + const mobileCal = isMobile(); calendar = new FullCalendar.Calendar(calendarEl, { - initialView: 'timeGridWeek', - headerToolbar: { + initialView: mobileCal ? 'listWeek' : 'timeGridWeek', + headerToolbar: mobileCal ? { + left: 'prev,next', + center: 'title', + right: 'today' + } : { left: 'prev,next today', center: 'title', right: 'timeGridWeek,timeGridDay,dayGridMonth' diff --git a/src/templates/index.html b/src/templates/index.html index ed2b8aa..c88e4bf 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -2,13 +2,17 @@ - + + Simpler: ADHD-Friendly Workspace + + From c5c7728a1e0f3978772e1da59c162f9fbd42eb66 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Jul 2026 09:20:33 +0000 Subject: [PATCH 2/3] Phone: replace space filter chips with a single-select dropdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With ~10 spaces the chip row ate most of a phone screen. On phones only, each .space-chips container (kanban board, Notes, Assistant) now shows a companion is display:none outside the phone breakpoint. Verified by pixel-diffing the desktop at 1440/1024/800 across all five destinations with 10 spaces, against the original pre-mobile-layer code — 0 differing pixels — plus an interactive check that chip plain/Ctrl/Alt clicks still produce single/multi/excluded filters. Core backend suite still green (154 tests). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_016djNVz9huD2ysEmLKydBFs --- doc/PROJECT_DESCRIPTION.md | 1 + src/static/css/mobile.css | 20 +++++++---- src/static/js/app.js | 68 ++++++++++++++++++++++++++++++++++++++ src/static/js/notes.js | 19 +++++++++++ 4 files changed, 101 insertions(+), 7 deletions(-) diff --git a/doc/PROJECT_DESCRIPTION.md b/doc/PROJECT_DESCRIPTION.md index 783606e..96cae3d 100644 --- a/doc/PROJECT_DESCRIPTION.md +++ b/doc/PROJECT_DESCRIPTION.md @@ -250,6 +250,7 @@ One page, one header: A progressive-enhancement layer for phones (`≤640px`), added without touching the desktop UX. **Hard rule**: all mobile CSS lives inside `@media (max-width: 640px)` (the only top-level rules are `display:none` defaults for brand-new mobile-only elements), and all mobile JS is gated behind `isMobile()` (`matchMedia('(max-width: 640px)')`) — so the PC experience is byte-identical. `body.is-mobile` is toggled reactively for any JS that needs it. - **Navigation**: the header nav reflows into a fixed **bottom tab bar** (icons + tiny labels, `env(safe-area-inset-bottom)`-padded); quick-capture becomes a full-width row; the page scrolls (desktop is height-locked, phones are not). +- **Space filter**: a chip row for ~10 spaces would eat most of a phone screen, so every `.space-chips` container (board, Notes, Assistant) gets a companion ` for each .space-chips row (app.js syncMobileSpaceSelect). + Desktop keeps the chips and never shows this. */ +.space-select-mobile { display: none; } /* --- Card action sheet (built by app.js openCardActionSheet, phone-only). These nodes are only ever created on phones, so styling them at top level @@ -164,14 +167,17 @@ .tasks-toolbar, .notes-toolbar, .assistant-toolbar { flex-wrap: wrap; gap: 8px; row-gap: 8px; } - .space-chips { - flex-wrap: nowrap; - overflow-x: auto; - -webkit-overflow-scrolling: touch; - scrollbar-width: none; - padding-bottom: 2px; + /* Space filter: a chip row for ~10 spaces would eat most of the screen, so + phones get the companion dropdown instead (single-select — see + syncMobileSpaceSelect). The chips stay in the DOM, just hidden. */ + .space-chips { display: none; } + .space-select-mobile { + display: block; + flex: 1 1 auto; + min-width: 0; + max-width: 16rem; + min-height: 38px; } - .space-chips::-webkit-scrollbar { display: none; } /* Bigger default tap targets for the small controls. */ .btn, .board-col-add, .view-tab { min-height: 38px; } diff --git a/src/static/js/app.js b/src/static/js/app.js index 27a0a2a..76cc759 100644 --- a/src/static/js/app.js +++ b/src/static/js/app.js @@ -45,6 +45,47 @@ function applyMobileBodyClass() { document.body.classList.toggle('is-mobile', MOBILE_MQ.matches); } +// ===== Mobile: space filter as a dropdown ===== +// A chip row for ~10 spaces eats most of a phone screen, so every +// .space-chips container gets a companion