Files
site-builder/craft/src/state/MobileChromeContext.tsx
T
shadowdao 2a8a26687b fix(builder): mobile-A2 hardening -- 16px inputs, shared sheet/modal chrome, z-scale + portal
- Force font-size:16px !important on Styles-sheet/topbar/Sitesmith inputs
  inside the mobile media query so inline 12px/14px styles stop triggering
  iOS zoom-on-focus.
- Lift sheet-open + Templates/Head Code modal-open state out of private
  useState into a shared MobileChromeContext (EditorShell), so Phase B can
  open/close sheets from outside MobilePanelBar.
- Add an explicit z-index layer scale, portal TemplateModal to
  document.body (was trapped under the tab bar inside .topbar's stacking
  context), align Sitesmith to the same --z-modal layer, and make opening
  a sheet close any open modal. Also fix modal backdrops swallowing tab
  bar taps (mirrors the sheet backdrop's existing tab-bar cutout).
- Drop BottomSheet's incorrect aria-modal; mobile-aware AssetsPanel empty
  state copy.
- Tests: useIsMobile (matchMedia mock incl. legacy fallback + cleanup),
  MobileChromeContext invariants (one sheet open, sheet closes modals),
  MobilePanelBar wiring.

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

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>;
};