diff --git a/src/lib/components/Sidebar.svelte b/src/lib/components/Sidebar.svelte index 16a259e..86ba958 100644 --- a/src/lib/components/Sidebar.svelte +++ b/src/lib/components/Sidebar.svelte @@ -610,6 +610,14 @@ contextMenu = { x, y, notebook: nb }; } + function openNotebookMenu(e: Event, nb: NotebookEntry) { + e.preventDefault(); + e.stopPropagation(); + const rect = (e.currentTarget as HTMLElement).getBoundingClientRect(); + const { x, y } = clampMenu(rect.right - 200, rect.bottom + 4, 200, 280); + contextMenu = { x, y, notebook: nb }; + } + function closeContextMenu() { contextMenu = null; } @@ -892,8 +900,11 @@ {#if contextMenu} + {#if isMobile} + + {/if} -
e.stopPropagation()}> +
e.stopPropagation()}> + {/if} -
e.stopPropagation()}> +
e.stopPropagation()}> + {#if isMobile} + + {/if} +
{/if} {#if hasChildren && !isCollapsed} {#each nb.children as child (child.path)} @@ -1306,6 +1332,10 @@ padding: 0 2px; } + .notebook-row { + position: relative; + } + .notebook-item { display: flex; align-items: center; @@ -1413,6 +1443,28 @@ cursor: grabbing; } + .notebook-actions-btn { + display: flex; + align-items: center; + justify-content: center; + position: absolute; + right: 4px; + top: 50%; + transform: translateY(-50%); + width: 44px; + height: 44px; + padding: 0; + border: none; + border-radius: 8px; + background: none; + color: var(--text-tertiary); + } + + .notebook-actions-btn:active { + background: var(--bg-hover); + color: var(--text-primary); + } + .tag-list { padding: 0 4px; } @@ -1481,6 +1533,15 @@ min-width: 140px; } + .context-menu-backdrop { + position: fixed; + inset: 0; + z-index: 999; + padding: 0; + border: none; + background: rgba(0, 0, 0, 0.18); + } + .context-menu button { display: flex; align-items: center; @@ -1635,7 +1696,7 @@ } .sidebar.mobile .notebook-item { - padding: 12px 12px; + padding: 2px 56px 2px 12px; min-height: 48px; gap: 10px; font-size: 15px; @@ -1676,34 +1737,40 @@ display: none; } - .sidebar.mobile .context-menu { - min-width: 200px; + .context-menu.mobile { + left: calc(12px + env(safe-area-inset-left, 0px)) !important; + right: calc(12px + env(safe-area-inset-right, 0px)); + top: auto !important; + bottom: calc(12px + env(safe-area-inset-bottom, 0px)); + min-width: 0; + max-height: calc(100dvh - 24px - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px)); + overflow-y: auto; border-radius: 12px; padding: 6px; } - .sidebar.mobile .context-menu button { + .context-menu.mobile button { padding: 12px 16px; font-size: 15px; min-height: 44px; border-radius: 8px; } - .sidebar.mobile .delete-confirm { + .delete-confirm.mobile { max-width: calc(100vw - 40px); padding: 24px; } - .sidebar.mobile .delete-confirm h4 { + .delete-confirm.mobile h4 { font-size: 16px; } - .sidebar.mobile .delete-confirm p { + .delete-confirm.mobile p { font-size: 14px; } - .sidebar.mobile .delete-confirm-cancel, - .sidebar.mobile .delete-confirm-btn { + .delete-confirm.mobile .delete-confirm-cancel, + .delete-confirm.mobile .delete-confirm-btn { padding: 10px 20px; font-size: 14px; min-height: 44px; diff --git a/tests/sidebar-mobile-actions.test.mjs b/tests/sidebar-mobile-actions.test.mjs new file mode 100644 index 0000000..342839e --- /dev/null +++ b/tests/sidebar-mobile-actions.test.mjs @@ -0,0 +1,31 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import test from 'node:test'; + +const sidebar = await readFile( + new URL('../src/lib/components/Sidebar.svelte', import.meta.url), + 'utf8' +); + +test('mobile notebook rows expose an accessible actions button', () => { + assert.match(sidebar, /\{#if isMobile\}[\s\S]{0,500}class="notebook-actions-btn"/); + assert.match(sidebar, /aria-label=\{`Actions for \$\{nb\.name\}`\}/); + assert.match(sidebar, /onclick=\{\(e\) => openNotebookMenu\(e, nb\)\}/); +}); + +test('the notebook context menu receives its mobile styling outside the sidebar', () => { + assert.match(sidebar, /class="context-menu" class:mobile=\{isMobile\}/); + assert.match(sidebar, /\.context-menu\.mobile\s*\{/); +}); + +test('mobile action sheets stay inside every safe area and prevent tap-through', () => { + assert.match(sidebar, /class="context-menu-backdrop"/); + assert.match(sidebar, /safe-area-inset-left/); + assert.match(sidebar, /safe-area-inset-right/); + assert.match(sidebar, /safe-area-inset-top/); + assert.match(sidebar, /safe-area-inset-bottom/); +}); + +test('the actions button remains inside the notebook manual-sort drop target', () => { + assert.match(sidebar, /
/); +});