Files
site-builder/craft/src/panels/topbar/TopBarOverflowMenu.tsx
T
shadowdaoandClaude Opus 4.8 979331b12d feat(builder): mobile-responsive editor chrome (Phase A)
Makes the Craft.js editor usable on phones (≤768px) without touching desktop
layout/behavior: a useIsMobile() hook gates a bottom tab bar + sheets (hosting
the existing Blocks/Pages/Layers/Assets/Styles panels unchanged) in place of
the side panels, a collapsed TopBar with a "..." overflow menu, 44px touch
targets, 16px inputs, dvh/safe-area-aware sizing, and small copy/overflow
fixes (empty-canvas hint, Templates modal tabs + close button).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 06:54:15 -07:00

111 lines
3.8 KiB
TypeScript

import React, { useEffect, useRef } from 'react';
import { DeviceMode } from '../../types';
export interface TopBarOverflowMenuProps {
open: boolean;
onClose: () => void;
device: DeviceMode;
onDeviceChange: (device: DeviceMode) => void;
showGuides: boolean;
onToggleGuides: () => void;
onOpenTemplates: () => void;
onOpenHeadCode: () => void;
onPreview: () => void;
/** Rendered `<SitesmithButton onClick={...} />` -- passed in rather than
* re-implemented here so the mobile menu reuses the exact same
* lock/cap-aware button the desktop topbar shows. */
sitesmithNode: React.ReactNode;
}
/**
* Mobile topbar "⋯" overflow menu (Phase A). At ≤768px, TopBar collapses to
* just the back arrow, Undo/Redo, Save/Publish, and this menu -- everything
* else that's inline on desktop (Templates, Sitesmith, Preview, Head Code,
* the guides toggle, and the device-width switcher) moves in here so the
* collapsed bar never overlaps or pushes controls off-screen.
*/
export const TopBarOverflowMenu: React.FC<TopBarOverflowMenuProps> = ({
open,
onClose,
device,
onDeviceChange,
showGuides,
onToggleGuides,
onOpenTemplates,
onOpenHeadCode,
onPreview,
sitesmithNode,
}) => {
const menuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!open) return;
const handleClick = (e: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) onClose();
};
const handleEsc = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
document.addEventListener('mousedown', handleClick);
document.addEventListener('keydown', handleEsc);
return () => {
document.removeEventListener('mousedown', handleClick);
document.removeEventListener('keydown', handleEsc);
};
}, [open, onClose]);
if (!open) return null;
const runAndClose = (fn: () => void) => () => {
fn();
onClose();
};
return (
<div ref={menuRef} className="topbar-overflow-menu" role="menu">
<div className="topbar-overflow-section">
<span className="topbar-overflow-label">Preview width</span>
<div className="device-switcher">
{(['desktop', 'tablet', 'mobile'] as DeviceMode[]).map((d) => (
<button
key={d}
type="button"
className={`device-btn ${device === d ? 'active' : ''}`}
onClick={() => onDeviceChange(d)}
title={d.charAt(0).toUpperCase() + d.slice(1)}
aria-label={d.charAt(0).toUpperCase() + d.slice(1)}
aria-pressed={device === d}
>
<i className={`fa ${d === 'desktop' ? 'fa-desktop' : d === 'tablet' ? 'fa-tablet' : 'fa-mobile'}`} />
</button>
))}
</div>
</div>
<button type="button" className="topbar-overflow-item" role="menuitem" onClick={runAndClose(onOpenTemplates)}>
<i className="fa fa-th-large" aria-hidden="true" /> Templates
</button>
<button type="button" className="topbar-overflow-item" role="menuitem" onClick={runAndClose(onPreview)}>
<i className="fa fa-eye" aria-hidden="true" /> Preview
</button>
<button type="button" className="topbar-overflow-item" role="menuitem" onClick={runAndClose(onOpenHeadCode)}>
<i className="fa fa-code" aria-hidden="true" /> Head Code
</button>
<button
type="button"
className={`topbar-overflow-item${showGuides ? ' active' : ''}`}
role="menuitem"
onClick={runAndClose(onToggleGuides)}
aria-pressed={showGuides}
>
<i className={`fa ${showGuides ? 'fa-th' : 'fa-th-large'}`} aria-hidden="true" />
{' '}{showGuides ? 'Hide Guides' : 'Show Guides'}
</button>
<div className="topbar-overflow-sitesmith" onClick={onClose}>
{sitesmithNode}
</div>
</div>
);
};