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>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user