Files
simple-react-app-kit/packages/overlays/src/scope.ts
T
2026-09-28 23:13:44 +08:00

72 lines
1.9 KiB
TypeScript

let nextScopeId = 0
let nextLayerOrder = 0
export interface CreateOverlayScopeOptions {
name?: string
}
export interface OverlayLayer {
dismiss: () => void
dismissible: boolean
id: string
order: number
}
/** UI-agnostic storage shared by independently imported overlay features. */
export class OverlayScope {
readonly id: string
readonly name?: string
private readonly channels = new Map<symbol, unknown>()
private readonly layers = new Map<string, OverlayLayer>()
constructor({ name }: CreateOverlayScopeOptions = {}) {
this.id = `overlay-scope-${nextScopeId++}`
this.name = name
}
/** @internal Used by atomic overlay feature modules. */
getChannel<Channel>(key: symbol, create: () => Channel): Channel {
const existing = this.channels.get(key)
if (existing) return existing as Channel
const channel = create()
this.channels.set(key, channel)
return channel
}
/** @internal Registers a visible layer for navigation interception. */
registerLayer(layer: Omit<OverlayLayer, "order">) {
const registered = { ...layer, order: nextLayerOrder++ }
this.layers.set(registered.id, registered)
return registered.order
}
/** @internal Removes a layer after it has settled. */
unregisterLayer(id: string) {
this.layers.delete(id)
}
getTopmostLayer() {
let topmost: OverlayLayer | undefined
for (const layer of this.layers.values()) {
if (!topmost || layer.order > topmost.order) topmost = layer
}
return topmost
}
/** Cancels navigation while a layer is open and dismisses it when allowed. */
interceptNavigation() {
const layer = this.getTopmostLayer()
if (!layer) return false
if (layer.dismissible) layer.dismiss()
return true
}
}
export function createOverlayScope(options?: CreateOverlayScopeOptions) {
return new OverlayScope(options)
}
export const globalOverlayScope = createOverlayScope({ name: "global" })