From 4206161def0a8875c2a05be3b921836d023c6e1d Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Wed, 3 Jun 2026 12:04:22 -0400 Subject: [PATCH] =?UTF-8?q?Mobile=20bottom=20toolbar:=20explorer,=20search?= =?UTF-8?q?,=20palette,=20tab=20switcher=20=E2=80=94=20Obsidian-style?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/index.html | 32 +++++ frontend/js/app.js | 2 + frontend/js/mobile-toolbar.js | 162 +++++++++++++++++++++++++ frontend/style.css | 222 ++++++++++++++++++++++++++++++++++ 4 files changed, 418 insertions(+) create mode 100644 frontend/js/mobile-toolbar.js diff --git a/frontend/index.html b/frontend/index.html index 4146b10..9eb844f 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1572,5 +1572,37 @@ curl -X POST https://votre-serveur.com/webhook \ + + + + + +
+
+
+ Fichiers ouverts + +
+
+
+
diff --git a/frontend/js/app.js b/frontend/js/app.js index 0f45232..2d4bbca 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -14,6 +14,7 @@ import * as Sync from './sync.js'; import { initGraphView } from './graph.js'; import * as Legacy from './legacy.js'; import { initCommandPalette } from './palette.js'; +import { initMobileToolbar } from './mobile-toolbar.js'; // ========================================================================= // Initialization — mirrors the original app.js init() ordering @@ -46,6 +47,7 @@ async function init() { UI.ContextMenuManager.init(); UI.TabManager.init(); initCommandPalette(); + initMobileToolbar(); const authOk = await Auth.AuthManager.initAuth(); diff --git a/frontend/js/mobile-toolbar.js b/frontend/js/mobile-toolbar.js new file mode 100644 index 0000000..a6fd345 --- /dev/null +++ b/frontend/js/mobile-toolbar.js @@ -0,0 +1,162 @@ +/* ObsiGate — Mobile Bottom Toolbar (Obsidian-style) + Phases 1-5: fixed bar, vault home, file palette, commands, tab switcher */ +import { state } from './state.js'; +import { TabManager } from './ui.js'; + +// ── Open file palette ── +function openPalette() { + import('./palette.js').then(m => { m.open(''); }); +} + +// ── Open command palette ── +function openCommands() { + import('./palette.js').then(m => { m.open('> '); }); +} + +// ── Tab switcher ── +function openTabSwitcher() { + const sheet = document.getElementById('mt-tab-switcher'); + const overlay = document.getElementById('mt-overlay'); + if (!sheet || !overlay) return; + + renderTabList(); + sheet.classList.add('open'); + overlay.classList.add('visible'); +} + +function closeTabSwitcher() { + const sheet = document.getElementById('mt-tab-switcher'); + const overlay = document.getElementById('mt-overlay'); + if (sheet) sheet.classList.remove('open'); + if (overlay) overlay.classList.remove('visible'); +} + +function renderTabList() { + const list = document.getElementById('mt-tab-list'); + if (!list) return; + + const tabs = TabManager._tabs || []; + const activeId = TabManager._activeTabId; + + if (tabs.length === 0) { + list.innerHTML = '
Aucun fichier ouvert
'; + return; + } + + list.innerHTML = tabs.map(t => { + const isActive = t.id === activeId; + const icon = t.path.endsWith('.md') ? '📄' : '📝'; + return `
+ ${icon} + ${t.title || t.path.split('/').pop()} + +
`; + }).join(''); + + // Click handler for switching tabs + list.querySelectorAll('.mt-tab-item').forEach(el => { + el.addEventListener('click', (e) => { + if (e.target.closest('.mt-tab-item-close')) return; + const tabId = el.dataset.tabId; + if (tabId) { + TabManager.activate(tabId); + closeTabSwitcher(); + } + }); + }); + + // Click handler for closing tabs + list.querySelectorAll('.mt-tab-item-close').forEach(btn => { + btn.addEventListener('click', (e) => { + e.stopPropagation(); + const tabId = btn.dataset.close; + if (tabId) { + TabManager.close(tabId); + setTimeout(() => renderTabList(), 100); + } + }); + }); +} + +// ── Update tab count badge ── +function updateTabCount() { + const badge = document.getElementById('mt-tab-count'); + if (!badge) return; + const count = (TabManager._tabs || []).length; + if (count > 0) { + badge.style.display = 'flex'; + badge.textContent = count; + } else { + badge.style.display = 'none'; + } +} + +// ── Vault home / sidebar toggle ── +function toggleExplorer() { + // On mobile: toggle sidebar + const sidebar = document.getElementById('sidebar'); + if (sidebar) { + if (sidebar.classList.contains('mobile-open')) { + sidebar.classList.remove('mobile-open'); + } else { + sidebar.classList.add('mobile-open'); + } + } + // Also focus the vault if none selected + if (!state.currentVault && state.allVaults && state.allVaults.length > 0) { + import('./sidebar.js').then(m => { + if (m.setSelectedVaultContext) m.setSelectedVaultContext(state.allVaults[0].name); + }); + } +} + +// ── Focus search ── +function focusSearch() { + const searchInput = document.querySelector('.search-input'); + if (searchInput) { + searchInput.focus(); + searchInput.select(); + } +} + +// ── Init ── +export function initMobileToolbar() { + // Only relevant on mobile, but attach handlers anyway + const explorerBtn = document.getElementById('mt-explorer'); + const searchBtn = document.getElementById('mt-search'); + const paletteBtn = document.getElementById('mt-palette'); + const tabsBtn = document.getElementById('mt-tabs'); + const sheetClose = document.getElementById('mt-sheet-close'); + const overlay = document.getElementById('mt-overlay'); + + if (explorerBtn) explorerBtn.addEventListener('click', toggleExplorer); + if (searchBtn) searchBtn.addEventListener('click', focusSearch); + if (paletteBtn) paletteBtn.addEventListener('click', openPalette); + if (tabsBtn) tabsBtn.addEventListener('click', openTabSwitcher); + if (sheetClose) sheetClose.addEventListener('click', closeTabSwitcher); + if (overlay) overlay.addEventListener('click', closeTabSwitcher); + + // Long-press palette for commands + if (paletteBtn) { + let pressTimer; + paletteBtn.addEventListener('touchstart', () => { + pressTimer = setTimeout(() => { + openCommands(); + }, 500); + }); + paletteBtn.addEventListener('touchend', () => clearTimeout(pressTimer)); + paletteBtn.addEventListener('touchmove', () => clearTimeout(pressTimer)); + // Click = file palette, right-click / context menu = commands + paletteBtn.addEventListener('contextmenu', (e) => { + e.preventDefault(); + openCommands(); + }); + } + + // Update tab count periodically + setInterval(updateTabCount, 2000); + updateTabCount(); + + // Also expose tab count update for external triggers + window._mtUpdateTabs = updateTabCount; +} diff --git a/frontend/style.css b/frontend/style.css index 3134606..ff0374c 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -6547,3 +6547,225 @@ body.popup-mode .content-area { font-size: 0.88rem; } } + +/* ══════════════════════════════════════════ + Mobile Bottom Toolbar (Obsidian-style) + ══════════════════════════════════════════ */ +.mobile-toolbar { + display: none; +} + +@media (max-width: 768px) { + .mobile-toolbar { + display: flex; + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: 56px; + background: var(--bg-secondary); + border-top: 1px solid var(--border); + z-index: 900; + align-items: center; + justify-content: space-around; + padding: 0 4px; + padding-bottom: env(safe-area-inset-bottom, 0); + -webkit-backdrop-filter: blur(12px); + backdrop-filter: blur(12px); + } + + /* Push content up to avoid toolbar overlap */ + .main-layout { + padding-bottom: 56px; + padding-bottom: calc(56px + env(safe-area-inset-bottom, 0)); + } + .editor-modal.active ~ .main-layout { + padding-bottom: 0; + } + + .mt-btn { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 2px; + flex: 1; + height: 48px; + padding: 2px 4px; + border: none; + border-radius: 8px; + background: transparent; + color: var(--text-muted); + cursor: pointer; + transition: color 0.15s, background 0.15s; + position: relative; + -webkit-tap-highlight-color: transparent; + } + .mt-btn:active { + background: rgba(255,255,255,0.06); + } + .mt-btn svg { + flex-shrink: 0; + } + .mt-label { + font-size: 10px; + font-weight: 500; + line-height: 1; + } + .mt-btn-primary { + color: var(--accent); + } + .mt-btn-primary .mt-label { + color: var(--accent); + } + + /* Tab count badge */ + .mt-badge { + position: absolute; + top: 2px; + right: 50%; + transform: translateX(18px); + background: var(--accent); + color: #fff; + font-size: 9px; + font-weight: 700; + min-width: 16px; + height: 16px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + padding: 0 4px; + line-height: 1; + } + + /* ── Tab Switcher Bottom Sheet ── */ + .mt-overlay { + display: none; + position: fixed; + inset: 0; + background: rgba(0,0,0,0.5); + z-index: 950; + opacity: 0; + transition: opacity 0.25s; + } + .mt-overlay.visible { + display: block; + opacity: 1; + } + + .mt-tab-switcher { + position: fixed; + bottom: 0; + left: 0; + right: 0; + max-height: 60vh; + background: var(--bg-secondary); + border-radius: 16px 16px 0 0; + z-index: 960; + transform: translateY(100%); + transition: transform 0.3s cubic-bezier(0.32, 0.72, 0, 1); + display: flex; + flex-direction: column; + box-shadow: 0 -4px 24px rgba(0,0,0,0.4); + padding-bottom: env(safe-area-inset-bottom, 0); + } + .mt-tab-switcher.open { + transform: translateY(0); + } + + .mt-sheet-handle { + width: 36px; + height: 4px; + background: var(--border); + border-radius: 2px; + margin: 10px auto 6px; + flex-shrink: 0; + } + + .mt-sheet-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 16px 12px; + flex-shrink: 0; + } + .mt-sheet-title { + font-family: "JetBrains Mono", monospace; + font-size: 0.8rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-muted); + } + .mt-sheet-close { + width: 28px; height: 28px; + padding: 0; + border: 1px solid var(--border); + border-radius: 6px; + background: transparent; + color: var(--text-secondary); + cursor: pointer; + font-size: 12px; + display: flex; + align-items: center; + justify-content: center; + } + .mt-sheet-close:hover { color: var(--accent); border-color: var(--accent); } + + .mt-sheet-list { + flex: 1; + overflow-y: auto; + padding: 0 12px 12px; + -webkit-overflow-scrolling: touch; + } + + .mt-tab-item { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 12px; + border-radius: 8px; + cursor: pointer; + color: var(--text-primary); + font-size: 0.85rem; + transition: background 0.1s; + margin-bottom: 2px; + } + .mt-tab-item:active { + background: rgba(255,255,255,0.06); + } + .mt-tab-item.active { + background: color-mix(in srgb, var(--accent) 12%, transparent); + color: var(--accent); + font-weight: 600; + } + .mt-tab-item-icon { + flex-shrink: 0; + opacity: 0.6; + } + .mt-tab-item-name { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .mt-tab-item-close { + width: 24px; height: 24px; + padding: 0; + border: none; + border-radius: 4px; + background: transparent; + color: var(--text-muted); + cursor: pointer; + font-size: 14px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + } + .mt-tab-item-close:hover { + color: var(--danger, #ef4444); + background: rgba(239,68,68,0.1); + } +}