fix(mobile): hamburger ouvrait sidebarCollapsed au lieu du drawer + layout settings responsive (v7.58.1)
FlowDeck CI / lint (push) Successful in 2m15s
FlowDeck CI / test (push) Failing after 21m14s
FlowDeck CI / docker (push) Skipped

Le handler du hamburger était une expression Alpine avec un effet de bord
dans un ternaire (sidebarCollapsed ? toggleSidebar() : (mobileSidebarOpen = true, ...)).
Évaluée sans eval dans le build CSP d'Alpine, elle ne produisait aucun effet
visible sur mobile: le bouton ne faisait rien (prouvé via Alpine.evaluate@390px,
open=false). Remplacée par la méthode fdHamburgerClick() (matchMedia 768px:
drawer mobile, sinon toggle desktop).

Settings: panel 1050px + nav verticale 200px inutilisable sur 390px →
@media 768px: plein écran (dvh), nav en onglets horizontaux scrollables,
rows en colonne, prov-grid 1 colonne, close 44px.

+ gate e2e mobile_regression.spec.js (drawer open/close + viewport settings)
This commit is contained in:
2026-10-07 14:27:33 -04:00
parent 21b96afa12
commit 992200a90a
4 changed files with 101 additions and 1 deletions
+1 -1
View File
@@ -37,7 +37,7 @@
{% if not hide_hamburger %} {% if not hide_hamburger %}
<button class="hamburger-btn" <button class="hamburger-btn"
:class="{ 'hamburger-desktop-show': sidebarCollapsed }" :class="{ 'hamburger-desktop-show': sidebarCollapsed }"
@click="{{ hamburger_click|default('sidebarCollapsed ? toggleSidebar() : (mobileSidebarOpen = true, sidebarCollapsed = false)') }}" @click="{{ hamburger_click|default('fdHamburgerClick()') }}"
title="Toggle sidebar"> title="Toggle sidebar">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="3" y1="6" x2="21" y2="6"/> <line x1="3" y1="6" x2="21" y2="6"/>
+11
View File
@@ -1944,6 +1944,17 @@
this.sidebarCollapsed = !this.sidebarCollapsed; this.sidebarCollapsed = !this.sidebarCollapsed;
}, },
// Hamburger: mobile opens the drawer (assignment expressions are illegal
// in the Alpine CSP build, hence this method), desktop toggles collapse.
fdHamburgerClick() {
if (window.matchMedia('(max-width: 768px)').matches) {
this.mobileSidebarOpen = true;
this.sidebarCollapsed = false;
} else {
this.toggleSidebar();
}
},
closeSidebar() { closeSidebar() {
if (this.sidebarPeek) { this.sidebarPeek = false; return; } if (this.sidebarPeek) { this.sidebarPeek = false; return; }
this.sidebarCollapsed = true; this.sidebarCollapsed = true;
+15
View File
@@ -125,6 +125,21 @@
.llm-detail .prov-field-row{margin-top:12px;} .llm-detail .prov-field-row{margin-top:12px;}
.llm-detail-empty{display:flex;align-items:center;justify-content:center;height:260px;font-size:13px;color:var(--text-dim);text-align:center;padding:0 20px;} .llm-detail-empty{display:flex;align-items:center;justify-content:center;height:260px;font-size:13px;color:var(--text-dim);text-align:center;padding:0 20px;}
@media (max-width:760px){.llm-layout{grid-template-columns:1fr;}.llm-list{max-height:220px;}} @media (max-width:760px){.llm-layout{grid-template-columns:1fr;}.llm-list{max-height:220px;}}
/* ── Mobile: full-screen panel + horizontal nav tabs ── */
@media (max-width:768px){
.settings-overlay{background:var(--bg-primary);backdrop-filter:none;}
.settings-panel{flex-direction:column;width:100vw;max-width:100vw;height:100dvh;max-height:100dvh;margin:0;border:none;border-radius:0;box-shadow:none;}
.settings-nav{width:auto;min-width:0;flex-direction:row;flex-shrink:0;display:flex;align-items:center;overflow-x:auto;border-right:none;border-bottom:1px solid var(--border);padding:6px 8px;gap:4px;overscroll-behavior-x:contain;}
.settings-nav-header{display:none;}
.settings-nav-item{white-space:nowrap;padding:8px 12px;border-radius:8px;flex-shrink:0;}
.settings-content{padding:16px;}
.settings-close{top:8px;right:8px;width:44px;height:44px;background:var(--bg-tertiary);}
.setting-row{flex-wrap:wrap;}
.setting-control{margin-left:0;width:100%;}
.prov-grid{grid-template-columns:1fr;}
.avatar-section{gap:10px;}
}
</style> </style>
<div class="settings-overlay" x-data="settingsInit()" @keydown.escape="historyBack()"> <div class="settings-overlay" x-data="settingsInit()" @keydown.escape="historyBack()">
+74
View File
@@ -0,0 +1,74 @@
// Mobile regression: hamburger opens the sidebar drawer, settings page fits viewport.
const { test, expect } = require('@playwright/test');
const FD_BASE = process.env.FD_BASE_URL || 'http://localhost:8080';
const USER = process.env.FD_USER || '[email protected]';
const PASS = process.env.FD_PASS || 'e2e-pass-123';
async function login(page) {
await page.goto(`${FD_BASE}/auth/login?provider=local`, { waitUntil: 'domcontentloaded' });
await page.fill('#email', USER);
await page.fill('#password', PASS);
await page.click('.btn-primary');
const ok = await page.waitForURL('**/workspaces', { timeout: 10000 }).then(() => true).catch(() => false);
if (!ok) {
const resp = await page.request.post(`${FD_BASE}/auth/register`, { data: { email: USER, password: PASS, name: 'E2E Mobile' } });
if (!resp.ok() && resp.status() !== 409) throw new Error(`register ${resp.status()}`);
await page.goto(`${FD_BASE}/auth/login?provider=local`, { waitUntil: 'domcontentloaded' });
await page.fill('#email', USER);
await page.fill('#password', PASS);
await page.click('.btn-primary');
await page.waitForURL('**/workspaces', { timeout: 10000 });
}
}
test.describe('mobile', () => {
test.use({ viewport: { width: 390, height: 844 } });
test('hamburger opens mobile sidebar drawer', async ({ page }) => {
const errs = [];
page.on('pageerror', (e) => errs.push(e.message));
await login(page);
await page.goto(`${FD_BASE}/library`, { waitUntil: 'domcontentloaded' });
const burger = page.locator('.hamburger-btn');
await expect(burger).toBeVisible();
// Sidebar starts off-canvas
const sidebar = page.locator('#sidebar');
const boxBefore = await sidebar.boundingBox();
expect(boxBefore.x).toBeLessThan(0);
await burger.click();
await expect(sidebar).toHaveClass(/mobile-open/, { timeout: 5000 });
const boxAfter = await sidebar.boundingBox();
expect(boxAfter.x).toBeGreaterThanOrEqual(0);
expect(errs).toEqual([]);
// Overlay tap closes it
await page.locator('.sidebar-overlay').click({ position: { x: 350, y: 400 } });
await expect(sidebar).not.toHaveClass(/mobile-open/, { timeout: 5000 });
});
test('settings page fits mobile viewport', async ({ page }) => {
await login(page);
await page.goto(`${FD_BASE}/settings`, { waitUntil: 'domcontentloaded' });
const panel = page.locator('.settings-panel');
await expect(panel).toBeVisible();
const box = await panel.boundingBox();
expect(box.width).toBeLessThanOrEqual(390);
expect(box.height).toBeLessThanOrEqual(844);
// Nav becomes horizontal tabs: nav wider than tall
const navBox = await page.locator('.settings-nav').boundingBox();
expect(navBox.width).toBeGreaterThan(navBox.height);
// Tapping a nav item still switches sections
await page.locator('.settings-nav-item', { hasText: 'Tags' }).click();
await expect(page.locator('h2', { hasText: 'Tags' }).first()).toBeVisible();
// No horizontal page overflow
const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);
expect(overflow).toBeLessThanOrEqual(0);
});
});