// AddonLayoutContext — broadcast the active addon entry's layout selection
// (`shell` vs `immersive`) up to the host so it can hide/show its chrome
// (Sidebar, Topbar, breadcrumbs) when an immersive addon is mounted.
//
// Why a context rather than a prop on the host shell:
//
// 1. The host shell is rendered ABOVE the addon route in the tree, but the
// decision about what layout the addon wants comes from the addon itself
// (manifest.frontend.layout) which the AddonLoader knows about at mount
// time. A bottom-up signal via context inverts the dependency cleanly.
//
// 2. Addon entries can swap layouts at runtime (think a kiosk-mode toggle
// inside a POS). A context value reactively updates the host without
// asking each route to wire props.
//
// 3. When the user navigates AWAY from an immersive addon, the AddonLoader
// unmounts, its layout context updater fires `setLayout("shell")` from
// a cleanup effect, and the chrome restores automatically.
//
// Host integration (starter-core, ops, …):
//
// function AppShell({ children }) {
// const layout = useAddonLayout()
// const chrome = layout !== "immersive"
// return (
//
// {chrome && }
// {chrome && }{children}
//
// )
// }
//
// The context defaults to `"shell"`, so apps that never mount an
// `` keep the legacy behaviour.
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from 'react'
import type { AddonLayout } from '@asteby/metacore-sdk'
export type { AddonLayout }
interface AddonLayoutState {
/** Active layout. `"shell"` (default) or `"immersive"`. */
layout: AddonLayout
/**
* Imperative setter for the host or an addon-loader to mutate the active
* layout. Exposed for advanced use; most callers should use
* `useDeclareAddonLayout(layout)` from a route component, which scopes the
* change to the route's mount lifetime.
*/
setLayout: (layout: AddonLayout) => void
}
const defaultState: AddonLayoutState = {
layout: 'shell',
setLayout: () => {
/* noop — provider missing; consumers degrade to legacy "shell" */
},
}
const AddonLayoutContext = createContext(defaultState)
export interface AddonLayoutProviderProps {
/** Initial layout — usually `"shell"`. */
initial?: AddonLayout
children: React.ReactNode
}
/**
* Wrap the host app once, above the router outlet. The provider keeps the
* currently-active layout in state; addon-loader and `useDeclareAddonLayout`
* mutate it from below.
*/
export function AddonLayoutProvider({
initial = 'shell',
children,
}: AddonLayoutProviderProps) {
const [layout, setLayout] = useState(initial)
const value = useMemo(
() => ({ layout, setLayout }),
[layout],
)
return (
{children}
)
}
/**
* Read the currently-active layout. The host shell calls this and decides
* whether to render its chrome. Returns `"shell"` when no provider is
* mounted, so apps that have not adopted immersive addons keep working.
*/
export function useAddonLayout(): AddonLayout {
return useContext(AddonLayoutContext).layout
}
/**
* Imperative API — the value returned mirrors `useAddonLayout()` but also
* exposes the setter for hosts that need to flip the layout outside of a
* route lifecycle (e.g. a hotkey forcing kiosk mode). Most addon entries do
* NOT need this; prefer `useDeclareAddonLayout`.
*/
export function useAddonLayoutControl(): AddonLayoutState {
return useContext(AddonLayoutContext)
}
/**
* Declare the layout from the addon side. Mounts the value, restores
* `"shell"` on unmount. Skip when `layout` is undefined so route components
* can pass `manifest.frontend?.layout` directly without branching.
*
* function PosEntry({ manifest }: { manifest: Manifest }) {
* useDeclareAddonLayout(manifest.frontend?.layout)
* return
* }
*/
export function useDeclareAddonLayout(layout: AddonLayout | undefined): void {
const { setLayout } = useAddonLayoutControl()
// useCallback so the effect only re-runs on a real layout change, not on
// every render of the consumer that happens to forward an inline literal.
const apply = useCallback(setLayout, [setLayout])
useEffect(() => {
if (!layout || layout === 'shell') return
apply(layout)
return () => {
apply('shell')
}
}, [layout, apply])
}