fix(mobile): hamburger ouvrait sidebarCollapsed au lieu du drawer + layout settings responsive (v7.58.1)
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:
@@ -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"/>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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()">
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user