mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
fix: make plugin sidebar rows as tall as folder rows on phones
This commit is contained in:
@@ -68,6 +68,17 @@
|
||||
--density-header-py: 12px;
|
||||
--density-card-p: 16px;
|
||||
--density-sidebar-py: 4px;
|
||||
|
||||
/* Minimum height of a tappable list row below the lg breakpoint, where
|
||||
* folder rows use max-lg:min-h-[44px]. Plugin frames get it as a variable
|
||||
* because their own viewport is the frame, not the page. */
|
||||
--touch-row-min-h: 0px;
|
||||
}
|
||||
|
||||
@media (width < 64rem) {
|
||||
:root {
|
||||
--touch-row-min-h: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { afterEach, describe, it, expect, vi } from 'vitest';
|
||||
import type { InstalledPlugin } from '../plugin-types';
|
||||
import type { ThemeSnapshot } from '../plugin-sandbox/host-theme';
|
||||
|
||||
// Folder rows are 44px tall below the lg breakpoint. A plugin frame's own
|
||||
// viewport is the frame, so the page passes that height in as a variable,
|
||||
// and has to pass it again when the page crosses the breakpoint, which no
|
||||
// DOM mutation reports.
|
||||
|
||||
const page = vi.hoisted(() => ({ theme: { dark: false, fontFamily: 'sans-serif', vars: { '--touch-row-min-h': '0px' } } as ThemeSnapshot }));
|
||||
|
||||
vi.mock('../plugin-sandbox/host-api', () => ({ dispatchApiCall: vi.fn(async () => 'ok') }));
|
||||
vi.mock('../plugin-sandbox/host-theme', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('../plugin-sandbox/host-theme')>()),
|
||||
snapshotHostTheme: () => page.theme,
|
||||
}));
|
||||
|
||||
import { createSlotInstance } from '../plugin-sandbox/host-bridge';
|
||||
|
||||
function plugin(): InstalledPlugin {
|
||||
return {
|
||||
id: 'p1', name: 'P', version: '1.0.0', author: '', description: '', type: 'hook',
|
||||
permissions: [], entrypoint: 'index.js', enabled: true, status: 'running', settings: {},
|
||||
};
|
||||
}
|
||||
|
||||
const instances: Array<{ destroy(): void }> = [];
|
||||
afterEach(() => {
|
||||
for (const i of instances.splice(0)) i.destroy();
|
||||
});
|
||||
|
||||
describe('touch row height in plugin frames', () => {
|
||||
it('is among the tokens the page mirrors into frames', async () => {
|
||||
const { snapshotHostTheme } = await vi.importActual<typeof import('../plugin-sandbox/host-theme')>('../plugin-sandbox/host-theme');
|
||||
document.documentElement.style.setProperty('--touch-row-min-h', '44px');
|
||||
try {
|
||||
expect(snapshotHostTheme().vars['--touch-row-min-h']).toBe('44px');
|
||||
} finally {
|
||||
document.documentElement.style.removeProperty('--touch-row-min-h');
|
||||
}
|
||||
});
|
||||
|
||||
it('reaches open frames when the page is resized across the breakpoint', async () => {
|
||||
const inst = createSlotInstance({
|
||||
plugin: plugin(), slot: 'sidebar-widget', code: '', locale: 'en', tier: 'untrusted', extraProps: {},
|
||||
hostContainer: document.body, onResize: () => {},
|
||||
});
|
||||
instances.push(inst);
|
||||
const setTheme = vi.spyOn(inst, 'setTheme');
|
||||
|
||||
page.theme = { ...page.theme, vars: { '--touch-row-min-h': '44px' } };
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
|
||||
expect(setTheme).toHaveBeenCalledWith(expect.objectContaining({ vars: { '--touch-row-min-h': '44px' } }));
|
||||
});
|
||||
});
|
||||
@@ -125,6 +125,15 @@ describe('plugin UI kit components', () => {
|
||||
expect(container.querySelector('dd')).toHaveTextContent('2 MB');
|
||||
});
|
||||
|
||||
it('makes sidebar rows as tall as folder rows on touch screens', () => {
|
||||
// The page sets --touch-row-min-h to 44px below lg, where folder rows
|
||||
// are max-lg:min-h-[44px]; the frame can't see the page's width itself.
|
||||
expect(source('components/layout/sidebar.tsx')).toContain('max-lg:min-h-[44px]');
|
||||
expect(source('app/globals.css')).toMatch(/@media \(width < 64rem\) \{\s*:root \{\s*--touch-row-min-h: 44px;/);
|
||||
render(<kit.SidebarRow label="Anna" onClick={() => {}} />);
|
||||
expect(screen.getByRole('button', { name: 'Anna' }).style.minHeight).toBe('var(--touch-row-min-h, 0px)');
|
||||
});
|
||||
|
||||
it('draws a checkbox the user can toggle by its label', () => {
|
||||
const onChange = vi.fn();
|
||||
render(<kit.Checkbox checked={false} onChange={onChange} label="Send as link" hint="For files over 10 MB" />);
|
||||
|
||||
@@ -129,6 +129,13 @@ function ensureThemeWatcher(): void {
|
||||
});
|
||||
themeWatcher.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] });
|
||||
themeWatcher.observe(document.head, { childList: true, subtree: true, characterData: true });
|
||||
// Breakpoint-dependent tokens (the touch row height) change with the
|
||||
// page width, which no mutation reports.
|
||||
window.addEventListener('resize', () => {
|
||||
if (scheduled) return;
|
||||
scheduled = true;
|
||||
requestAnimationFrame(() => { scheduled = false; pushThemeToFrames(); });
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Sandbox instance ────────────────────────────────────────
|
||||
|
||||
@@ -74,6 +74,7 @@ const THEME_TOKENS: readonly string[] = [
|
||||
'--density-header-py',
|
||||
'--density-card-p',
|
||||
'--density-sidebar-py',
|
||||
'--touch-row-min-h',
|
||||
];
|
||||
|
||||
// Mirrors the body font stack in app/globals.css. Used when no document is
|
||||
|
||||
@@ -787,7 +787,9 @@ export function SidebarRow({ icon, label, detail, onClick, title, ariaLabel }: {
|
||||
title={title}
|
||||
aria-label={ariaLabel}
|
||||
className={cn('bk w-full flex items-center gap-2 ps-[28px] pe-3 text-sm text-foreground text-start border-s-2 border-transparent transition-colors duration-150', onClick && 'hover:bg-muted/50 focus-visible:outline-none focus-visible:bg-muted/50')}
|
||||
style={{ paddingBlock: 'var(--density-sidebar-py)' }}
|
||||
// Folder rows are 44px tall on phones (max-lg); the frame's own
|
||||
// viewport can't tell, so the page passes the height in.
|
||||
style={{ paddingBlock: 'var(--density-sidebar-py)', minHeight: 'var(--touch-row-min-h, 0px)' }}
|
||||
>
|
||||
{icon && <span className="flex items-center justify-center flex-shrink-0 w-4 h-4 text-muted-foreground"><Icon name={icon} /></span>}
|
||||
<span className="flex-1 min-w-0 truncate">{label}</span>
|
||||
|
||||
Reference in New Issue
Block a user