diff --git a/CHANGELOG.md b/CHANGELOG.md index c7abea9..11a74cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,36 @@ # Changelog - FlowDeck +## v7.59.0 (2026-10-07) — Mobile : drawer réglable, side-nav settings, aide refondue + +### Fixed + +- **Scroll souris du drawer sidebar mobile** — `display:block` du breakpoint + ≤768px cassait le `flex:1` de `.sidebar-scroll` (hauteur = contenu, jamais + de zone scrollable). Remis en `display:flex;flex-direction:column`. +- **Rate limiter du routeur de test** — fenêtre remise à zéro à chaque login + : suite e2e via un seul login `beforeAll` + cookies `storageState`. + +### Changed + +- **Settings mobile : le menu devient une side-navigation gauche** ouverte + par un hamburger (`.settings-menu-btn`) avec backdrop, au lieu des onglets + horizontaux ; fermeture auto au choix de section. Squelette panneau + (overlay/panel/nav) extrait de `settings.html` vers + `/static/css/settings.css` partagé avec l'aide. +- **Page `/help` refondue au complet** — ancien monologue HTML inline de la + route remplacé par `app/templates/help.html` : même panneau 2 colonnes que + settings, nav latérale en 13 sections couvrant toute l'app (démarrer, + éditeur, bases & vues, tâches/dépendances, workspaces/forges, collaboration, + bibliothèque/recherche, agent IA & automations, API v2/webhooks/clipper, + PWA offline, comptes & SSO, raccourcis, tips) ; `static/js/help.js` + (`Alpine.data helpInit`). + +### Tests + +- Gate e2e `mobile_regression.spec.js` : 6/6 (drawer + wheel scroll + + side-nav settings + aide mobile/desktop) · `test_app.py` + + `test_pwa_offline.py` : 230/230. + ## v7.58.0 (2026-10-06) — Add plugins : catalogue on/off à effet réel (phase 8/8) ### Added diff --git a/VERSION b/VERSION index 70e2b22..5ce8bfd 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -7.58.0 +7.59.0 diff --git a/app/main.py b/app/main.py index 6dde682..a0f5413 100644 --- a/app/main.py +++ b/app/main.py @@ -186,7 +186,7 @@ async def lifespan(_app: FastAPI): app = FastAPI( title="FlowDeck", - version="7.58.0", + version="7.59.0", docs_url="/docs", redoc_url="/redoc", lifespan=lifespan, diff --git a/app/routers/dashboard/pages_html.py b/app/routers/dashboard/pages_html.py index f0b9526..3215b2d 100644 --- a/app/routers/dashboard/pages_html.py +++ b/app/routers/dashboard/pages_html.py @@ -251,209 +251,10 @@ def accounts_page(request: Request): @router.get("/help", response_class=HTMLResponse) def help_page(request: Request): - """Comprehensive help & documentation page.""" + """Help & documentation page — settings-style panel with side navigation.""" from app.templating import ENV - env = ENV sidebar = _sidebar_data(request, []) - # Render via a block-based template so content_html lands in {% block content %} - block_tpl = env.from_string( - '{% extends "base.html" %}{% block content %}{{ content_html|safe }}{% endblock %}' - ) - return HTMLResponse(block_tpl.render( - **sidebar, - request=request, - page_title="Help", - title_prefix="Help", - page_icon="❓", - content_html=""" -
-
-

❓ FlowDeck Help

-

Everything you need to know about your Notion-style workspace with Gitea & GitHub integration.

-
- -
-
-

🚀Getting Started

-

FlowDeck is your private, self-hosted workspace. Create pages, organize projects, and integrate with your Git forge.

-
    -
  • Create a workspace from the Workspaces page
  • -
  • Click 📄 New Page in the sidebar to start writing
  • -
  • Use Ctrl+N anywhere to create a page
  • -
-
- -
-

📝Pages & Editor

-

Notion-style block editor with slash commands, markdown shortcuts, and rich formatting.

-
    -
  • Type / for the slash command menu
  • -
  • Drag & drop pages in the sidebar to reorganize
  • -
  • Right-click for context menu (duplicate, rename, delete)
  • -
-
- -
-

{{ fd_icon("folder",16) }}Workspaces

-

Organize your work into separate workspaces. Each has its own pages and files.

-
    -
  • Local Files stored on your server
  • -
  • Gitea Connect to browse & edit repos
  • -
  • GitHub Connect via Settings → Integrations
  • -
-
- -
-

🦎Gitea Integration

-

Connect your Gitea account to access repositories directly from FlowDeck.

-
    -
  • Go to Settings → Integrations to connect
  • -
  • Browse repo file trees in the sidebar
  • -
  • Create & edit files with commit messages
  • -
  • Sync labels as tags
  • -
-
- -
-

🌐Sharing & Publishing

-

Share pages with collaborators or publish them to the web.

-
    -
  • Click Share in the page editor top-right
  • -
  • Share with specific users or get a public link
  • -
  • Publish to make a page visible at /p/your-slug
  • -
-
- -
-

📚Library, Trash & Tasks

-

Find all your content in one place with powerful filtering.

-
    -
  • Library — Tabs for Recents, Favorites, Shared, Published
  • -
  • Trash — Soft-deleted pages (30-day retention)
  • -
  • My Tasks — Aggregated tasks from all collections
  • -
-
- -
-

📶Offline & PWA

-

Install FlowDeck as an app and keep working without a connection.

-
    -
  • Install — browser menu → Install app / Add to Home Screen
  • -
  • Edits made offline are queued locally and synced automatically
  • -
  • A ⟳ marker shows pages with pending changes
  • -
-
-
- -
-

⌨️ Keyboard Shortcuts

-
Ctrl+N
Create new page
-
Ctrl+K
Quick find / command palette
-
Ctrl+S
Save current page
-
F2
Rename selected item
-
Delete
Move selected item to trash
-
Escape
Close modal / cancel editing
-
Enter
Open selected page
-
Ctrl+O
New AI chat (in footer)
-
- -
-

🔐 Authentication

-

-FlowDeck supports three authentication methods:
-Local Email + password — create an account on the login page.
-Gitea OAuth Login with your Gitea account. Your repos appear as workspaces.
-GitHub OAuth Login or link your GitHub account in Settings → Integrations.

-Tip: You can connect Gitea/GitHub to an existing local account — your identity stays as your local user. -

-

-SSO Enterprise SSO (v6.7.0) — sign in with your organization account.
-For administrators: open Settings → Admin → SSO / Enterprise and pick a provider:
-• SAML 2.0 — paste the IdP Entity ID, SSO URL and signing certificate, then give the IdP this -/auth/saml/metadata link (it contains the SP Entity ID, ACS URL and certificate).
-• OpenID Connect — paste the Issuer URL, Client ID and Client Secret (PKCE is used, scopes default to openid profile email).
-• Provisioning — accounts are created automatically on first login, groups from the IdP map to workspace roles, -and SSO only disables local login (admins keep their local door). Every attempt is audited in -Settings → Admin → SSO / Enterprise (login history). -

-
- -
-

📶 Offline mode (PWA)

-

-FlowDeck is a Progressive Web App: pages you visited stay available offline and your -edits are saved locally, then synchronised when the connection returns.

-Install: open your browser menu and choose Install app (Chrome/Edge) or -Add to Home Screen (Safari/iOS). FlowDeck then opens in its own window.
-Offline editing: while offline, the editor stores changes in the browser -(IndexedDB) and shows an offline banner with the number of pending changes. A -⟳ icon appears next to pages that have unsynced edits.
-Reconnection: the queue is replayed automatically (and via Background Sync). -A spinner badge appears while syncing, followed by a confirmation toast.
-Conflicts: if a page changed on the server, the latest edit wins and a notice is -shown. If a page was deleted server-side, your offline copy is recreated as an orphan -page. If a page with the same title already exists, the offline copy is renamed -“Title (copie offline)”. -

-
- -
-

🔌 API publique v2

-

-FlowDeck exposes a full REST API under /api/v2 for third-party integrations.
-Auth: create a token in Settings → API tokens, then send it as -Authorization: Bearer <token>. Tokens carry scopes -read, write or admin (a higher scope implies the lower ones).
-Features: CRUD on collections, pages, properties, views, comments, notifications, -favorites, tags, sharing, sprints and templates; pagination (?limit=&offset= + -X-Total-Count), filters (filter[prop]=value), sorting, full-text search -(/api/v2/search), idempotency (Idempotency-Key) and RFC 7807 error bodies.
-Reference: interactive OpenAPI docs at /docs -(also /redoc, docs/openapi-v2.json). -

-
- -
-

💡 Tips

-

-• Toggle the sidebar with the « button in the top-left corner.
-• Switch between workspaces using the dropdown menu in the sidebar header.
-• The Private section appears when a remote workspace is active — files here stay local.
-• Hover over any sidebar item to see action buttons (favorite, share, delete).
-• Use Ctrl+Click or Shift+Click to multi-select items in the sidebar. -

-
- -
""" - )) - - + return ENV.get_template("help.html").render(**sidebar, request=request) @router.get("/accounts/settings", response_class=HTMLResponse) diff --git a/app/templates/base.html b/app/templates/base.html index 30235c7..d8dcc87 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -13,6 +13,7 @@ + +
+ + +
+
Basics
+
🚀 Getting Started
+
📝 Pages & Editor
+
🗄️ Databases & Views
+
✅ Tasks & Dependencies
+
Workspace
+
📁 Workspaces & Forges
+
👥 Collaboration
+
📚 Library, Trash & Search
+
Features
+
🤖 AI Agent & Automations
+
🔌 Integrations & API
+
📶 Offline & PWA
+
🔐 Accounts & SSO
+
Reference
+
⌨️ Keyboard Shortcuts
+
💡 Tips
+
+ + +
+ +
+

Getting Started

+

FlowDeck is your private, self-hosted Notion-style workspace — pages, databases, and Git-forge (Gitea / GitHub) integration in one app.

+

First steps

+ +

The sidebar

+ +
+ +
+

Pages & Editor

+

Notion-style block editor: every line is a block you can drag, convert, and nest.

+

Blocks

+ +

Page features

+ +
+ +
+

Databases & Views

+

Collections are databases with a JSON schema, independent of Gitea. 21 property types: title, text, number, select, multi-select, status, date, person, checkbox, url, email, phone, files, unique_id, relation, rollup, formula, and auto-properties.

+

Views

+ +

Relations, rollups, formulas

+ +
+ +
+

Tasks & Dependencies

+ +
+ +
+

Workspaces & Forges

+

Organize work into separate workspaces, each with its own page tree, collections and members.

+ +
+ +
+

Collaboration

+ +
+ +
+

Library, Trash & Search

+ +
+ +
+

AI Agent & Automations

+ +
+ +
+

Integrations & API

+ +
+ +
+

Offline mode (PWA)

+

FlowDeck is a Progressive Web App: pages you visited stay available offline and your edits are saved locally, then synchronised when the connection returns.

+ +
+ +
+

Accounts & SSO

+

+ Local Email + password — create an account on the login page.

+ Gitea OAuth / GitHub OAuth — sign in with your forge; your repos become workspaces. You can link a forge to an existing local account — your identity stays local.

+ SSO Enterprise SSO — in Settings → Admin → SSO / Enterprise: +

+ +

Sessions and API tokens are managed in Settings → Sessions / API tokens.

+
+ +
+

Keyboard Shortcuts

+
Ctrl+K
Command palette / quick find
+
Ctrl+N
Create new page
+
Ctrl+S
Save current page
+
Ctrl+O
New AI chat (footer)
+
/
Slash command menu (in editor)
+
F2
Rename selected item
+
Delete
Move selected item to trash
+
Enter
Open selected page
+
Tab / Shift+Tab
Indent / outdent block
+
Escape
Close modal / cancel editing
+
+ +
+

Tips

+ +
+ +
+ + + + +{% endblock %} diff --git a/app/templates/settings.html b/app/templates/settings.html index 6703830..62d9149 100644 --- a/app/templates/settings.html +++ b/app/templates/settings.html @@ -11,16 +11,6 @@ {% block content %} -
-
+
+
+ - -
+ +
+
Account
{{ fd_icon("user",14) }} My account
{{ fd_icon("bell",14) }} Notifications
diff --git a/e2e/mobile_regression.spec.js b/e2e/mobile_regression.spec.js index c4dd20d..6b62abd 100644 --- a/e2e/mobile_regression.spec.js +++ b/e2e/mobile_regression.spec.js @@ -1,10 +1,15 @@ -// Mobile regression: hamburger opens the sidebar drawer, settings page fits viewport. +// Mobile regression: hamburger drawer, wheel scroll, settings side-nav, help page. const { test, expect } = require('@playwright/test'); const FD_BASE = process.env.FD_BASE_URL || 'http://localhost:8080'; const USER = process.env.FD_USER || 'e2e@flowdeck.test'; const PASS = process.env.FD_PASS || 'e2e-pass-123'; -async function login(page) { +// One login for the whole file (app rate limiter: 60 req/min per IP) — reused +// as storageState by every test. +let STATE = null; +test.beforeAll(async ({ browser }) => { + const ctx = await browser.newContext(); + const page = await ctx.newPage(); await page.goto(`${FD_BASE}/auth/login?provider=local`, { waitUntil: 'domcontentloaded' }); await page.fill('#email', USER); await page.fill('#password', PASS); @@ -19,6 +24,12 @@ async function login(page) { await page.click('.btn-primary'); await page.waitForURL('**/workspaces', { timeout: 10000 }); } + STATE = await ctx.storageState(); + await ctx.close(); +}); +async function login(page) { + // Session reused from the single beforeAll login (rate limiter: 60 req/min/IP) + await page.context().addCookies(STATE.cookies); } test.describe('mobile', () => { @@ -49,6 +60,19 @@ test.describe('mobile', () => { await expect(sidebar).not.toHaveClass(/mobile-open/, { timeout: 5000 }); }); + test('mobile sidebar drawer scrolls with mouse wheel', async ({ page }) => { + await login(page); + await page.goto(`${FD_BASE}/library`, { waitUntil: 'domcontentloaded' }); + await page.locator('.hamburger-btn').click(); + await page.waitForSelector('#sidebar.mobile-open'); + const sc = page.locator('.sidebar-scroll'); + await expect.poll(async () => sc.evaluate((el) => el.scrollHeight > el.clientHeight), { timeout: 3000 }).toBe(true); + const box = await sc.boundingBox(); + await page.mouse.move(box.x + box.width / 2, box.y + Math.min(box.height / 2, 400)); + await page.mouse.wheel(0, 300); + await expect.poll(async () => sc.evaluate((el) => el.scrollTop), { timeout: 3000 }).toBeGreaterThan(0); + }); + test('settings page fits mobile viewport', async ({ page }) => { await login(page); await page.goto(`${FD_BASE}/settings`, { waitUntil: 'domcontentloaded' }); @@ -59,16 +83,81 @@ test.describe('mobile', () => { 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); }); + + test('settings mobile nav is a hamburger side drawer', async ({ page }) => { + await login(page); + await page.goto(`${FD_BASE}/settings`, { waitUntil: 'domcontentloaded' }); + const burger = page.locator('.settings-menu-btn'); + await expect(burger).toBeVisible(); + const nav = page.locator('.settings-nav'); + const before = await nav.boundingBox(); + expect(before.x + before.width).toBeLessThanOrEqual(1); + + await burger.click(); + await expect(nav).toHaveClass(/nav-open/); + + // Nav item switches section AND closes the drawer + await page.locator('.settings-nav-item', { hasText: 'Tags' }).click(); + await expect(nav).not.toHaveClass(/nav-open/); + await expect(page.locator('h2', { hasText: 'Tags' }).first()).toBeVisible(); + + // Re-open and dismiss via backdrop + await burger.click(); + await expect(nav).toHaveClass(/nav-open/); + await page.locator('.settings-nav-backdrop').click({ position: { x: 375, y: 500 } }); + await expect(nav).not.toHaveClass(/nav-open/); + }); + + test('help page uses settings-style side navigation', async ({ page }) => { + const errs = []; + page.on('pageerror', (e) => errs.push(e.message)); + await login(page); + await page.goto(`${FD_BASE}/help`, { waitUntil: 'domcontentloaded' }); + + // Same panel skeleton as settings + await expect(page.locator('.settings-panel')).toBeVisible(); + await expect(page.locator('.settings-menu-btn')).toBeVisible(); + + // Mobile: nav drawer behind hamburger + const nav = page.locator('.settings-nav'); + await expect(nav).not.toHaveClass(/nav-open/); + await page.locator('.settings-menu-btn').click(); + await expect(nav).toHaveClass(/nav-open/); + await page.locator('.settings-nav-item', { hasText: 'Databases' }).click(); + await expect(nav).not.toHaveClass(/nav-open/); + await expect(page.locator('h2', { hasText: 'Databases & Views' })).toBeVisible(); + + // App-wide coverage: all sections exist in the nav + const sections = ['Getting Started', 'Pages & Editor', 'Databases & Views', 'Tasks & Dependencies', + 'Workspaces & Forges', 'Collaboration', 'Library, Trash & Search', 'AI Agent & Automations', + 'Integrations & API', 'Offline & PWA', 'Accounts & SSO', 'Keyboard Shortcuts', 'Tips']; + const labels = (await nav.locator('.settings-nav-item').allInnerTexts()).join(' '); + for (const s of sections) expect(labels, s).toContain(s); + + expect(errs).toEqual([]); + }); +}); + +test.describe('desktop', () => { + test.use({ viewport: { width: 1440, height: 900 } }); + + test('settings and help keep the 2-column layout on desktop', async ({ page }) => { + await login(page); + await page.goto(`${FD_BASE}/settings`, { waitUntil: 'domcontentloaded' }); + await expect(page.locator('.settings-menu-btn')).toBeHidden(); + const nav = page.locator('.settings-nav'); + await expect(nav).toBeVisible(); + const navBox = await nav.boundingBox(); + expect(navBox.height).toBeGreaterThan(navBox.width); + + await page.goto(`${FD_BASE}/help`, { waitUntil: 'domcontentloaded' }); + await expect(page.locator('.settings-menu-btn')).toBeHidden(); + await page.locator('.settings-nav-item', { hasText: 'SSO' }).click(); + await expect(page.locator('h2', { hasText: 'Accounts & SSO' })).toBeVisible(); + await expect(page.locator('.settings-content')).toContainText('IndexedDB'); + }); }); diff --git a/e2e/probe_scroll.spec.js b/e2e/probe_scroll.spec.js new file mode 100644 index 0000000..ae93d04 --- /dev/null +++ b/e2e/probe_scroll.spec.js @@ -0,0 +1,37 @@ +const { test } = require('@playwright/test'); +const FD_BASE = process.env.FD_BASE_URL || 'http://localhost:8080'; + +test.use({ viewport: { width: 390, height: 844 } }); +test('diagnose mobile sidebar wheel scroll', async ({ page }) => { + await page.goto(`${FD_BASE}/auth/login?provider=local`, { waitUntil: 'domcontentloaded' }); + await page.fill('#email', process.env.FD_USER || 'e2e@flowdeck.test'); + await page.fill('#password', process.env.FD_PASS || 'e2e-pass-123'); + await page.click('.btn-primary'); + await page.waitForURL('**/workspaces', { timeout: 10000 }).catch(() => {}); + await page.goto(`${FD_BASE}/library`, { waitUntil: 'domcontentloaded' }); + await page.waitForFunction(() => document.querySelector('.hamburger-btn')); + await page.locator('.hamburger-btn').click(); + await page.waitForSelector('#sidebar.mobile-open'); + + const info = await page.evaluate(() => { + const sb = document.getElementById('sidebar'); + const sc = document.querySelector('.sidebar-scroll'); + const cs = getComputedStyle(sb); + const ccs = sc ? getComputedStyle(sc) : null; + return { + sidebar: { display: cs.display, height: cs.height, overflow: cs.overflow, sh: sb.scrollHeight, ch: sb.clientHeight }, + scroll: sc ? { top: sc.scrollTop, sh: sc.scrollHeight, ch: sc.clientHeight, oy: ccs.overflowY, flex: ccs.flex, rect: JSON.stringify(sc.getBoundingClientRect()) } : null, + wheelTarget: (() => { const el = document.elementFromPoint(150, 400); return el ? el.className : 'none'; })(), + }; + }); + console.log('DIAG:', JSON.stringify(info, null, 1)); + + await page.mouse.move(150, 400); + await page.mouse.wheel(0, 300); + await page.waitForTimeout(300); + const after = await page.evaluate(() => { + const sc = document.querySelector('.sidebar-scroll'); + return { scTop: sc.scrollTop, scSH: sc.scrollHeight, scCH: sc.clientHeight, bodyTop: document.scrollingElement.scrollTop }; + }); + console.log('AFTER WHEEL:', JSON.stringify(after)); +}); diff --git a/static/css/app.css b/static/css/app.css index e02ae14..83f9ab7 100644 --- a/static/css/app.css +++ b/static/css/app.css @@ -3424,7 +3424,7 @@ button.breadcrumb-current { max-width: 260px; } /* ── Sidebar mobile ── */ .sidebar { - display: block; + display: flex; flex-direction: column; /* block cassait flex:1 de .sidebar-scroll → drawer impossible à scroller */ position: fixed !important; top: 0; left: 0; bottom: 0; width: 85vw; max-width: 320px; diff --git a/static/css/settings.css b/static/css/settings.css new file mode 100644 index 0000000..705a091 --- /dev/null +++ b/static/css/settings.css @@ -0,0 +1,34 @@ +/* ═══ Panel skeleton shared by /settings and /help (Notion-style modal) ═══ */ +.settings-overlay{position:fixed;inset:0;z-index:500;display:flex;background:rgba(0,0,0,.4);backdrop-filter:blur(2px);} +.settings-panel{display:flex;width:1050px;max-width:98vw;height:85vh;margin:auto;background:var(--bg-primary);border:1px solid var(--border-strong);border-radius:14px;box-shadow:var(--shadow-modal);overflow:hidden;} +.settings-nav{width:200px;min-width:200px;background:var(--bg-sidebar);border-right:1px solid var(--border);padding:8px 0;overflow-y:auto;} +.settings-nav-header{padding:12px 16px;font-size:13px;font-weight:600;color:var(--text-dim);text-transform:uppercase;letter-spacing:.5px;} +.settings-nav-item{display:flex;align-items:center;gap:8px;padding:6px 16px;font-size:13px;color:var(--text-secondary);cursor:pointer;transition:background 100ms;border-radius:0;} +.settings-nav-item:hover{background:var(--bg-hover);} +.settings-nav-item.active{background:rgba(35,131,226,.12);color:var(--text);font-weight:500;} +.settings-content{flex:1;overflow-y:auto;padding:24px 32px;} +.settings-content h2{font-size:18px;font-weight:600;margin-bottom:4px;} +.settings-content .section-desc{font-size:12px;color:var(--text-dim);margin-bottom:24px;} +.settings-close{position:absolute;top:12px;right:12px;width:32px;height:32px;background:none;border:none;color:var(--text-dim);font-size:20px;cursor:pointer;border-radius:6px;display:flex;align-items:center;justify-content:center;text-decoration:none;} +.settings-close:hover{background:var(--bg-hover);color:var(--text);} + +/* Mobile hamburger for the panel nav — hidden on desktop */ +.settings-menu-btn{display:none;position:absolute;top:8px;left:8px;z-index:20;width:40px;height:40px;border:none;border-radius:8px;background:var(--bg-tertiary);color:var(--text);align-items:center;justify-content:center;cursor:pointer;} +.settings-nav-backdrop{display:none;} + +/* ── Mobile: nav becomes a left side-navigation drawer (hamburger opens it) ── */ +@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-menu-btn{display:flex;} + .settings-nav{position:fixed;top:0;left:0;bottom:0;width:260px;max-width:80vw;min-width:0;z-index:600;transform:translateX(-100%);transition:transform .2s cubic-bezier(0.4,0,0.2,1);border-right:1px solid var(--border);box-shadow:4px 0 24px rgba(0,0,0,.4);padding-top:8px;} + .settings-nav.nav-open{transform:translateX(0);} + .settings-nav-item{padding:10px 16px;} + .settings-nav-backdrop{display:block;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:599;} + .settings-content{padding:56px 16px 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;} +} diff --git a/static/js/help.js b/static/js/help.js new file mode 100644 index 0000000..7450c28 --- /dev/null +++ b/static/js/help.js @@ -0,0 +1,18 @@ +if (!window.__fdHelpScriptLoaded) { +window.__fdHelpScriptLoaded = true; +// Alpine.data register (CSP build ne résout pas les globales window) +if (window.Alpine) { Alpine.data('helpInit', helpInit); fdRefreshXData('helpInit'); } else document.addEventListener('alpine:init', function () { Alpine.data('helpInit', helpInit); }); +function helpInit() { + return { + section: 'start', + navOpen: false, + closeNavOnMobile() { this.navOpen = false; }, + go(sec) { + this.section = sec; + this.navOpen = false; + var c = document.querySelector('.settings-content'); + if (c) c.scrollTop = 0; + }, + }; +} +} diff --git a/static/js/settings.js b/static/js/settings.js index cf4a050..7bea536 100644 --- a/static/js/settings.js +++ b/static/js/settings.js @@ -18,6 +18,9 @@ function settingsInit() { fmtAuditDate(e) { return new Date(e.at).toLocaleString(); }, activeSection: 'account', + // Mobile side-navigation drawer for the settings panel (hamburger in topbar) + navOpen: false, + closeNavOnMobile() { this.navOpen = false; }, allTags: [], newTagName: '', newTagColor: '#787774',