105 lines
4.5 KiB
TypeScript
105 lines
4.5 KiB
TypeScript
|
|
import React, { createContext, useCallback, useContext, useMemo, useState } from 'react';
|
||
|
|
import { useSitesmithModal } from './SitesmithContext';
|
||
|
|
|
||
|
|
/** Mobile bottom-sheet tabs (Phase A: Blocks/Pages/Layers/Assets/Styles).
|
||
|
|
* Exported so Phase B can open a specific sheet from outside
|
||
|
|
* `MobilePanelBar` -- e.g. the selection toolbar's "Edit styles" action
|
||
|
|
* opening 'styles', or tap-to-add closing 'blocks' after inserting a
|
||
|
|
* block on the canvas. */
|
||
|
|
export type MobileSheetTab = 'blocks' | 'pages' | 'layers' | 'assets' | 'styles';
|
||
|
|
|
||
|
|
interface MobileChromeContextValue {
|
||
|
|
/** Which bottom sheet (if any) is open. Only one at a time -- this is a
|
||
|
|
* single value by construction, not a set, so "one sheet open" is an
|
||
|
|
* invariant of the type, not something callers have to maintain. */
|
||
|
|
activeSheet: MobileSheetTab | null;
|
||
|
|
/** Opens `tab`'s sheet (replacing whatever sheet, if any, was open).
|
||
|
|
* Per item 3 (no accidental overlay stacking), this also closes any
|
||
|
|
* open full-screen modal -- Templates, Head Code, Sitesmith -- so a
|
||
|
|
* sheet is never shown stacked underneath one. Toggle-to-close is the
|
||
|
|
* CALLER's job (see MobilePanelBar's tab click handler): this function
|
||
|
|
* always opens, so Phase B can call it unconditionally. */
|
||
|
|
openSheet: (tab: MobileSheetTab) => void;
|
||
|
|
/** Closes the open sheet, if any. No-op otherwise. */
|
||
|
|
closeSheet: () => void;
|
||
|
|
|
||
|
|
/** Mobile topbar "..." overflow menu open state -- lifted here too
|
||
|
|
* ("+ the overflow-menu open state if convenient" per the brief) so
|
||
|
|
* Phase B can close it alongside sheets/modals from one place. */
|
||
|
|
overflowOpen: boolean;
|
||
|
|
setOverflowOpen: (open: boolean) => void;
|
||
|
|
|
||
|
|
/** Templates / Head Code modal open state, lifted out of TopBar so
|
||
|
|
* `openSheet` can close them directly (see above). Behavior is
|
||
|
|
* otherwise identical to the private useState these replaced. */
|
||
|
|
templateModalOpen: boolean;
|
||
|
|
setTemplateModalOpen: (open: boolean) => void;
|
||
|
|
|
||
|
|
headCodeModalOpen: boolean;
|
||
|
|
setHeadCodeModalOpen: (open: boolean) => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
const noop = () => {};
|
||
|
|
|
||
|
|
/** Default value mirrors the other state contexts in this codebase
|
||
|
|
* (EditorConfigContext, PageContext, SiteDesignContext): a harmless no-op
|
||
|
|
* stub rather than throwing, so a consumer rendered outside the provider
|
||
|
|
* (e.g. in a narrower test) degrades quietly instead of crashing. */
|
||
|
|
const MobileChromeContext = createContext<MobileChromeContextValue>({
|
||
|
|
activeSheet: null,
|
||
|
|
openSheet: noop,
|
||
|
|
closeSheet: noop,
|
||
|
|
overflowOpen: false,
|
||
|
|
setOverflowOpen: noop,
|
||
|
|
templateModalOpen: false,
|
||
|
|
setTemplateModalOpen: noop,
|
||
|
|
headCodeModalOpen: false,
|
||
|
|
setHeadCodeModalOpen: noop,
|
||
|
|
});
|
||
|
|
|
||
|
|
export const useMobileChrome = () => useContext(MobileChromeContext);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Shared mobile editor-chrome state (Phase A hardening / Phase B prep).
|
||
|
|
* Previously `activeTab`/sheet-open state lived as a private `useState`
|
||
|
|
* inside `MobilePanelBar`, which meant only a tap on the tab bar itself
|
||
|
|
* could open or close a sheet. Phase B needs to drive this from elsewhere
|
||
|
|
* (a canvas selection toolbar, tap-to-add, etc.), so it's lifted into this
|
||
|
|
* context, provided once in `EditorShell.tsx` around the whole mobile+
|
||
|
|
* desktop tree (harmless on desktop -- nothing there consumes it except
|
||
|
|
* `TopBar`'s Templates/Head Code modal state, which behaves identically to
|
||
|
|
* the private `useState` it replaced).
|
||
|
|
*/
|
||
|
|
export const MobileChromeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||
|
|
const [activeSheet, setActiveSheet] = useState<MobileSheetTab | null>(null);
|
||
|
|
const [overflowOpen, setOverflowOpen] = useState(false);
|
||
|
|
const [templateModalOpen, setTemplateModalOpen] = useState(false);
|
||
|
|
const [headCodeModalOpen, setHeadCodeModalOpen] = useState(false);
|
||
|
|
const { close: closeSitesmith } = useSitesmithModal();
|
||
|
|
|
||
|
|
const openSheet = useCallback((tab: MobileSheetTab) => {
|
||
|
|
setActiveSheet(tab);
|
||
|
|
// Item 3: a sheet and a full-screen modal must never stack -- opening
|
||
|
|
// a sheet always wins over whatever modal was open.
|
||
|
|
setTemplateModalOpen(false);
|
||
|
|
setHeadCodeModalOpen(false);
|
||
|
|
closeSitesmith();
|
||
|
|
}, [closeSitesmith]);
|
||
|
|
|
||
|
|
const closeSheet = useCallback(() => setActiveSheet(null), []);
|
||
|
|
|
||
|
|
const value = useMemo<MobileChromeContextValue>(() => ({
|
||
|
|
activeSheet,
|
||
|
|
openSheet,
|
||
|
|
closeSheet,
|
||
|
|
overflowOpen,
|
||
|
|
setOverflowOpen,
|
||
|
|
templateModalOpen,
|
||
|
|
setTemplateModalOpen,
|
||
|
|
headCodeModalOpen,
|
||
|
|
setHeadCodeModalOpen,
|
||
|
|
}), [activeSheet, openSheet, closeSheet, overflowOpen, templateModalOpen, headCodeModalOpen]);
|
||
|
|
|
||
|
|
return <MobileChromeContext.Provider value={value}>{children}</MobileChromeContext.Provider>;
|
||
|
|
};
|