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>
111 lines
3.8 KiB
TypeScript
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>
|
|
);
|
|
};
|