import type { Validator } from "./validators"; /** * Logical identity for a layer (`find` / `upsert` / `gcTime`). * * Must be JSON-safe: `string` | `boolean` | `null` | finite `number`, or * plain objects / arrays of those. Compared via sorted `JSON.stringify` — * two keys with the same values in a different object-property order are equal. * Invalid keys throw `LayerKeyError` from any path that hashes the key * (`hashKey`, `open`, `find`, `cancelQueued`, `createLayer`). */ export type LayerKey = ReadonlyArray; export type LayerPhase = | "pending" | "queued" | "active" | "dismissed" | "error"; /** Animation state, independent of resolution phase. */ export type LayerTransition = "entering" | "settled" | "exiting"; export type LayerActionStatus = "idle" | "running"; /** Instance-scoped predicate — `true` allows dismissal. */ export type BlockerFn = () => boolean | Promise; /** Stack-scoped predicate — `true` allows dismissal. */ export type StackBlockerFn = (layer: LayerState) => boolean | Promise; /** * How {@link LayerStack#dismissAll} treats blockers. All modes still * **resolve** `open()` with the dismiss response — unlike {@link LayerStack#cancelAll}, * which rejects with {@link LayerCancelledError}. * * - `skipBlocked` — soft-dismiss each layer; leave blocked ones * - `stopAtBlocked` — soft-dismiss until the first veto, then stop * - `force` — bypass blockers; still completes with the response */ export type DismissAllMode = "skipBlocked" | "stopAtBlocked" | "force"; export interface DismissOptions { force?: boolean; } export interface DismissAllOptions { mode?: DismissAllMode; } /** Immutable layer snapshot consumed by selectors. */ export interface LayerState< P = unknown, R = void, E = DefaultLayerError, D = unknown, > { id: string; key: LayerKey; payload: P; data?: D; response?: R; error?: E; phase: LayerPhase; transition: LayerTransition; /** `true` while a user-intent dismiss is consulting blockers. */ dismissing: boolean; actionStatus: LayerActionStatus; ended: boolean; index: number; stackSize: number; } /** Imperative controls available to a rendered layer. */ export interface LayerCallContext { /** Resolve and dismiss — response optional iff `undefined extends R` ({@link EndArgs}). */ end: (...args: EndArgs) => Promise; /** Alias of {@link LayerCallContext.end}. */ dismiss: (...args: EndArgs) => Promise; addBlocker: (fn: BlockerFn) => () => void; update: (patch: Partial

) => void; setRunning: (running: boolean) => void; /** Finishes the current transition immediately. */ settle: () => void; ended: boolean; index: number; stackSize: number; root: RootProps; readonly stackId: string; readonly layerId: string; } export interface LayerComponentProps< P = unknown, R = void, E = DefaultLayerError, D = unknown, RootProps = unknown, > { call: LayerCallContext; payload: P; data?: D; error?: E; phase: LayerPhase; transition: LayerTransition; dismissing: boolean; actionStatus: LayerActionStatus; } /** Framework adapters narrow this to their component type. */ export type LayerComponent = unknown; export interface LayerOptions< P = unknown, R = void, E = DefaultLayerError, D = unknown, RootProps = unknown, > { /** @default "default" */ stack?: string; /** Stable identity; same key + `upsert` → update existing instance. */ key: LayerKey; component?: LayerComponent; /** Enter duration in milliseconds; `call.settle()` finishes it early. @default 0 */ enteringDelay?: number; /** Exit duration in milliseconds; `call.settle()` finishes it early. @default 0 */ exitingDelay?: number; /** When true, reusing an active key updates its payload instead of stacking. */ upsert?: boolean; /** Loads data before activation; dismissal aborts the signal. */ loadFn?: (ctx: { payload: P; signal: AbortSignal }) => Promise | D; /** Validates payload before opening; the parsed output becomes the payload. */ validate?: Validator

; /** Props passed to every layer in the stack via `call.root`. */ rootProps?: RootProps; /** * Phantom fields preserve payload/response/error inference without runtime values. * `@internal` is required for `stripInternal` to hide them from published declarations. */ /** @internal */ readonly _payload?: P; /** @internal */ readonly _response?: R; /** @internal */ readonly _error?: E; } /** * Makes `payload` optional only when `P` admits `undefined`. * Optional object properties alone do not make the payload omittable. */ export type PayloadArg

= undefined extends P ? { payload?: P } : { payload: P }; /** Rest-tuple factory used by {@link EndArgs} and stack/handle variants. */ export type ResponseArgTuple = undefined extends R ? [response?: R, opts?: Opts] : [response: R, opts?: Opts]; /** * Rest-args for `call.end` / `call.dismiss` / `LayerStack.dismiss`. * Response is optional only when `R` admits `undefined` — twin of {@link PayloadArg}. * When response is optional, pass opts as the second arg (`end(undefined, { force: true })`); * a lone object is treated as the response (not as opts). */ export type EndArgs = ResponseArgTuple; /** `LayerStack.dismissAll` rest-args (same gate as {@link EndArgs}). */ export type DismissAllArgs = ResponseArgTuple; /** `cancelQueued` rest-args (same gate as {@link EndArgs}). */ export type CancelQueuedArgs = ResponseArgTuple; /** `LayerHandle.dismiss` rest-args (same gate as {@link EndArgs}; opts may include `id`). */ export type HandleDismissArgs = ResponseArgTuple< R, DismissOptions & { id?: string } >; export type OpenLayerOptions< P = unknown, R = void, E = DefaultLayerError, D = unknown, RootProps = unknown, > = LayerOptions & PayloadArg

; /** Rejects keys that do not exist on `T`. */ export type OmitKeyof = Omit; /** Serial policy when a mounted layer's `loadFn` rejects. */ export type SerialOnLoadError = "block" | "advance"; export interface StackOptions { /** * Serial scope queues unmounted opens until the occupying layer leaves * (`pending` / `active` / `error` for `onLoadError: "block"`). * @default { strategy: "parallel" } */ scope?: { strategy: "serial" | "parallel"; /** * Serial only. `block` — keep `phase: "error"` until dismiss. * `advance` — remove the failed layer and drain the next queued open. * @default "block" */ onLoadError?: SerialOnLoadError; }; /** Retains loaded data for same-key restoration. @default 0 */ gcTime?: number; /** @default "skipBlocked" */ dismissAllMode?: DismissAllMode; } export type StackDefaults = Record; export interface LayerClientOptions { defaultStackOptions?: StackDefaults; } /** * Coarse mutation label for devtools / {@link LayerClient#subscribeNotify}. * `dismissAll` = bulk completion; `cancelAll` = teardown that rejects `open()`. */ export type StackNotifyAction = | "register" | "open" | "queue" | "update" | "setRunning" | "settle" | "dismiss" | "dismissVetoed" | "dismissAll" | "cancelAll" | "cancelQueued" | "phase" | "remove"; /** JSON-safe layer projection on {@link StackNotifyEvent}. */ export interface LayerNotifyView { id: string; /** Display string from {@link keySignature}. */ key: string; phase: LayerPhase; transition: LayerTransition; actionStatus: LayerActionStatus; dismissing: boolean; ended: boolean; index: number; stackSize: number; payload?: unknown; /** `true` when `payload` could not be JSON-cloned and was omitted. */ payloadTruncated?: boolean; } /** Emitted when a stack snapshot changes after a labeled mutation. */ export interface StackNotifyEvent { stackId: string; seq: number; ts: number; action: StackNotifyAction; active: LayerNotifyView[]; queued: LayerNotifyView[]; } /** * Module-augmentation point for app-wide type defaults. * Augment `defaultError` to set the library-wide error type once. * * @example * declare module "@stainless-code/layers" { * interface Register { defaultError: AppError } * } */ export interface Register {} /** App-wide error type configured through {@link Register}. */ export type DefaultLayerError = Register extends { defaultError: infer E } ? E : Error;