@tooee/overlays 0.1.20 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
- export { OverlayContext, OverlayControllerContext, OverlayStateContext, useOverlay, useOverlayState, useCurrentOverlay, useHasOverlay, } from "./overlay-context.js";
1
+ export { OverlayContext, OverlayControllerContext, OverlayStateContext, useOverlay, useOverlayState, useCurrentOverlay, useHasOverlay, useHasModalOverlay, } from "./overlay-context.js";
2
2
  export type { OverlayContextValue, OverlayId, OverlayCloseReason, OverlayOpenOptions, OverlayRole, OverlayRenderArgs, OverlayRenderer, OverlayHandle, OverlayController, OverlayState, } from "./overlay-context.js";
3
+ export { createOverlayStore, selectHasOverlay, selectIsOpen, selectStack, selectStackIds, selectTop, } from "./overlay-store.js";
4
+ export type { OverlayClosedEmit, OverlayRecord, OverlayStore, OverlayStoreContext, } from "./overlay-store.js";
3
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,wBAAwB,EACxB,mBAAmB,EACnB,UAAU,EACV,eAAe,EACf,iBAAiB,EACjB,aAAa,GACd,MAAM,sBAAsB,CAAA;AAC7B,YAAY,EACV,mBAAmB,EACnB,SAAS,EACT,kBAAkB,EAClB,kBAAkB,EAClB,WAAW,EACX,iBAAiB,EACjB,eAAe,EACf,aAAa,EACb,iBAAiB,EACjB,YAAY,GACb,MAAM,sBAAsB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,wBAAwB,EACxB,mBAAmB,EACnB,UAAU,EACV,eAAe,EACf,iBAAiB,EACjB,aAAa,EACb,kBAAkB,GACnB,MAAM,sBAAsB,CAAA;AAC7B,YAAY,EACV,mBAAmB,EACnB,SAAS,EACT,kBAAkB,EAClB,kBAAkB,EAClB,WAAW,EACX,iBAAiB,EACjB,eAAe,EACf,aAAa,EACb,iBAAiB,EACjB,YAAY,GACb,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,YAAY,EACZ,WAAW,EACX,cAAc,EACd,SAAS,GACV,MAAM,oBAAoB,CAAA;AAC3B,YAAY,EACV,iBAAiB,EACjB,aAAa,EACb,YAAY,EACZ,mBAAmB,GACpB,MAAM,oBAAoB,CAAA"}
package/dist/index.js CHANGED
@@ -1,2 +1,3 @@
1
- export { OverlayContext, OverlayControllerContext, OverlayStateContext, useOverlay, useOverlayState, useCurrentOverlay, useHasOverlay, } from "./overlay-context.js";
1
+ export { OverlayContext, OverlayControllerContext, OverlayStateContext, useOverlay, useOverlayState, useCurrentOverlay, useHasOverlay, useHasModalOverlay, } from "./overlay-context.js";
2
+ export { createOverlayStore, selectHasOverlay, selectIsOpen, selectStack, selectStackIds, selectTop, } from "./overlay-store.js";
2
3
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,wBAAwB,EACxB,mBAAmB,EACnB,UAAU,EACV,eAAe,EACf,iBAAiB,EACjB,aAAa,GACd,MAAM,sBAAsB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,wBAAwB,EACxB,mBAAmB,EACnB,UAAU,EACV,eAAe,EACf,iBAAiB,EACjB,aAAa,EACb,kBAAkB,GACnB,MAAM,sBAAsB,CAAA;AAa7B,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,YAAY,EACZ,WAAW,EACX,cAAc,EACd,SAAS,GACV,MAAM,oBAAoB,CAAA"}
@@ -52,6 +52,13 @@ export interface OverlayController {
52
52
  export interface OverlayState {
53
53
  current: ReactNode | null;
54
54
  hasOverlay: boolean;
55
+ /**
56
+ * True when any overlay other than a passive owned surface is open. Passive
57
+ * surfaces (e.g. the which-key hint) render for visuals only and never own
58
+ * input, so they don't count. Use this for guards that should stand down
59
+ * while a modal overlay is up but keep working under passive hints.
60
+ */
61
+ hasModalOverlay: boolean;
55
62
  stack: OverlayId[];
56
63
  }
57
64
  export interface OverlayContextValue {
@@ -66,5 +73,6 @@ export declare const OverlayContext: import("react").Context<OverlayContextValue
66
73
  export declare function useOverlay(): OverlayController;
67
74
  export declare function useOverlayState(): OverlayState;
68
75
  export declare function useHasOverlay(): boolean;
76
+ export declare function useHasModalOverlay(): boolean;
69
77
  export declare function useCurrentOverlay(): ReactNode | null;
70
78
  //# sourceMappingURL=overlay-context.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"overlay-context.d.ts","sourceRoot":"","sources":["../src/overlay-context.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"overlay-context.d.ts","sourceRoot":"","sources":["../src/overlay-context.tsx"],"names":[],"mappings":"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"}
@@ -12,6 +12,7 @@ const defaultController = {
12
12
  const defaultState = {
13
13
  current: null,
14
14
  hasOverlay: false,
15
+ hasModalOverlay: false,
15
16
  stack: [],
16
17
  };
17
18
  export const OverlayControllerContext = createContext(defaultController);
@@ -33,6 +34,9 @@ export function useOverlayState() {
33
34
  export function useHasOverlay() {
34
35
  return useContext(OverlayStateContext).hasOverlay;
35
36
  }
37
+ export function useHasModalOverlay() {
38
+ return useContext(OverlayStateContext).hasModalOverlay;
39
+ }
36
40
  export function useCurrentOverlay() {
37
41
  return useContext(OverlayStateContext).current;
38
42
  }
@@ -1 +1 @@
1
- {"version":3,"file":"overlay-context.js","sourceRoot":"","sources":["../src/overlay-context.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,OAAO,CAAA;AAkFjD,WAAW;AAEX,MAAM,iBAAiB,GAAsB;IAC3C,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,CAAC;IACzD,MAAM,EAAE,GAAG,EAAE,GAAE,CAAC;IAChB,IAAI,EAAE,GAAG,EAAE,GAAE,CAAC;IACd,IAAI,EAAE,GAAG,EAAE,GAAE,CAAC;IACd,QAAQ,EAAE,GAAG,EAAE,GAAE,CAAC;IAClB,MAAM,EAAE,GAAG,EAAE,CAAC,KAAK;IACnB,KAAK,EAAE,IAAI;CACZ,CAAA;AAED,MAAM,YAAY,GAAiB;IACjC,OAAO,EAAE,IAAI;IACb,UAAU,EAAE,KAAK;IACjB,KAAK,EAAE,EAAE;CACV,CAAA;AAED,MAAM,CAAC,MAAM,wBAAwB,GAAG,aAAa,CAAoB,iBAAiB,CAAC,CAAA;AAC3F,MAAM,CAAC,MAAM,mBAAmB,GAAG,aAAa,CAAe,YAAY,CAAC,CAAA;AAE5E,mFAAmF;AACnF,MAAM,CAAC,MAAM,cAAc,GAAG,aAAa,CAAsB;IAC/D,IAAI,EAAE,GAAG,EAAE,GAAE,CAAC;IACd,IAAI,EAAE,GAAG,EAAE,GAAE,CAAC;IACd,OAAO,EAAE,IAAI;IACb,UAAU,EAAE,KAAK;CAClB,CAAC,CAAA;AAEF,QAAQ;AAER,MAAM,UAAU,UAAU;IACxB,OAAO,UAAU,CAAC,wBAAwB,CAAC,CAAA;AAC7C,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,OAAO,UAAU,CAAC,mBAAmB,CAAC,CAAA;AACxC,CAAC;AAED,MAAM,UAAU,aAAa;IAC3B,OAAO,UAAU,CAAC,mBAAmB,CAAC,CAAC,UAAU,CAAA;AACnD,CAAC;AAED,MAAM,UAAU,iBAAiB;IAC/B,OAAO,UAAU,CAAC,mBAAmB,CAAC,CAAC,OAAO,CAAA;AAChD,CAAC"}
1
+ {"version":3,"file":"overlay-context.js","sourceRoot":"","sources":["../src/overlay-context.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,OAAO,CAAA;AAyFjD,WAAW;AAEX,MAAM,iBAAiB,GAAsB;IAC3C,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,CAAC;IACzD,MAAM,EAAE,GAAG,EAAE,GAAE,CAAC;IAChB,IAAI,EAAE,GAAG,EAAE,GAAE,CAAC;IACd,IAAI,EAAE,GAAG,EAAE,GAAE,CAAC;IACd,QAAQ,EAAE,GAAG,EAAE,GAAE,CAAC;IAClB,MAAM,EAAE,GAAG,EAAE,CAAC,KAAK;IACnB,KAAK,EAAE,IAAI;CACZ,CAAA;AAED,MAAM,YAAY,GAAiB;IACjC,OAAO,EAAE,IAAI;IACb,UAAU,EAAE,KAAK;IACjB,eAAe,EAAE,KAAK;IACtB,KAAK,EAAE,EAAE;CACV,CAAA;AAED,MAAM,CAAC,MAAM,wBAAwB,GAAG,aAAa,CAAoB,iBAAiB,CAAC,CAAA;AAC3F,MAAM,CAAC,MAAM,mBAAmB,GAAG,aAAa,CAAe,YAAY,CAAC,CAAA;AAE5E,mFAAmF;AACnF,MAAM,CAAC,MAAM,cAAc,GAAG,aAAa,CAAsB;IAC/D,IAAI,EAAE,GAAG,EAAE,GAAE,CAAC;IACd,IAAI,EAAE,GAAG,EAAE,GAAE,CAAC;IACd,OAAO,EAAE,IAAI;IACb,UAAU,EAAE,KAAK;CAClB,CAAC,CAAA;AAEF,QAAQ;AAER,MAAM,UAAU,UAAU;IACxB,OAAO,UAAU,CAAC,wBAAwB,CAAC,CAAA;AAC7C,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,OAAO,UAAU,CAAC,mBAAmB,CAAC,CAAA;AACxC,CAAC;AAED,MAAM,UAAU,aAAa;IAC3B,OAAO,UAAU,CAAC,mBAAmB,CAAC,CAAC,UAAU,CAAA;AACnD,CAAC;AAED,MAAM,UAAU,kBAAkB;IAChC,OAAO,UAAU,CAAC,mBAAmB,CAAC,CAAC,eAAe,CAAA;AACxD,CAAC;AAED,MAAM,UAAU,iBAAiB;IAC/B,OAAO,UAAU,CAAC,mBAAmB,CAAC,CAAC,OAAO,CAAA;AAChD,CAAC"}
@@ -0,0 +1,72 @@
1
+ import type { OverlayCloseReason, OverlayId, OverlayOpenOptions, OverlayRenderer } from "./overlay-context.js";
2
+ /**
3
+ * An overlay stack entry as tracked by the store. `prevMode` is the host mode
4
+ * captured at open time (mode itself stays outside this package — see the
5
+ * shell's OverlayProvider, which owns all mode side effects).
6
+ */
7
+ export interface OverlayRecord<TPayload = unknown> {
8
+ id: OverlayId;
9
+ render: OverlayRenderer<TPayload>;
10
+ payload: TPayload;
11
+ options: OverlayOpenOptions;
12
+ prevMode: string;
13
+ }
14
+ export interface OverlayStoreContext {
15
+ stack: readonly OverlayRecord[];
16
+ }
17
+ /**
18
+ * Emitted when a record leaves the stack (close, closeTop, or same-id
19
+ * replacement). Lifecycle side effects (onClose callbacks, mode restoration,
20
+ * sequence-reset bridges) are performed by the adapter subscribed via
21
+ * `store.on("closed", ...)`; transitions stay pure.
22
+ */
23
+ export interface OverlayClosedEmit {
24
+ record: OverlayRecord;
25
+ reason: OverlayCloseReason;
26
+ /**
27
+ * Legacy mode restoration decision, computed in the transition (it needs
28
+ * stack state): `record.prevMode` when the record is non-ownCommands,
29
+ * `restoreMode !== false`, AND it was the topmost non-ownCommands entry
30
+ * (closing a buried legacy overlay must not clobber the mode set by the one
31
+ * above it — R-04); otherwise null. Same-id replacement displacement never
32
+ * restores (the successor takes over).
33
+ */
34
+ restoreModeTo: string | null;
35
+ }
36
+ /**
37
+ * Event payload map, passed explicitly as a `createStore` generic
38
+ * (@xstate/store v4 removed the `emits` config; event/emitted types come from
39
+ * generics or `schemas`). Both maps here are deliberately CLOSED — no index
40
+ * signatures — so unknown `store.trigger.*` / `store.send` / `store.on` /
41
+ * `enqueue.emit.*` event names are compile errors (pinned by
42
+ * `overlay-store.typecheck.ts`). The v4 `schemas.emitted` alternative was
43
+ * evaluated and rejected: it gives the same strictness while adding runtime
44
+ * schema objects this store does not need.
45
+ */
46
+ export type OverlayStoreEvents = {
47
+ opened: {
48
+ record: OverlayRecord;
49
+ };
50
+ updated: {
51
+ id: OverlayId;
52
+ next: unknown | ((prev: unknown) => unknown);
53
+ };
54
+ closed: {
55
+ id: OverlayId;
56
+ reason: OverlayCloseReason;
57
+ };
58
+ closedTop: {
59
+ reason: OverlayCloseReason;
60
+ };
61
+ };
62
+ export declare function createOverlayStore(): import("@xstate/store").Store<OverlayStoreContext, OverlayStoreEvents, OverlayClosedEmit & {
63
+ type: "closed";
64
+ }>;
65
+ export type OverlayStore = ReturnType<typeof createOverlayStore>;
66
+ export declare function selectStack(ctx: OverlayStoreContext): readonly OverlayRecord[];
67
+ export declare function selectTop(ctx: OverlayStoreContext): OverlayRecord | null;
68
+ export declare function selectHasOverlay(ctx: OverlayStoreContext): boolean;
69
+ export declare function selectIsOpen(ctx: OverlayStoreContext, id: OverlayId): boolean;
70
+ /** Fresh array; prefer selectStack identity + memo in render paths. */
71
+ export declare function selectStackIds(ctx: OverlayStoreContext): readonly OverlayId[];
72
+ //# sourceMappingURL=overlay-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"overlay-store.d.ts","sourceRoot":"","sources":["../src/overlay-store.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,kBAAkB,EAClB,SAAS,EACT,kBAAkB,EAClB,eAAe,EAChB,MAAM,sBAAsB,CAAA;AAE7B;;;;GAIG;AACH,MAAM,WAAW,aAAa,CAAC,QAAQ,GAAG,OAAO;IAC/C,EAAE,EAAE,SAAS,CAAA;IACb,MAAM,EAAE,eAAe,CAAC,QAAQ,CAAC,CAAA;IACjC,OAAO,EAAE,QAAQ,CAAA;IACjB,OAAO,EAAE,kBAAkB,CAAA;IAC3B,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,SAAS,aAAa,EAAE,CAAA;CAChC;AAED;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,aAAa,CAAA;IACrB,MAAM,EAAE,kBAAkB,CAAA;IAC1B;;;;;;;OAOG;IACH,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;CAC7B;AAgBD;;;;;;;;;GASG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE;QAAE,MAAM,EAAE,aAAa,CAAA;KAAE,CAAA;IACjC,OAAO,EAAE;QAAE,EAAE,EAAE,SAAS,CAAC;QAAC,IAAI,EAAE,OAAO,GAAG,CAAC,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC,CAAA;KAAE,CAAA;IACxE,MAAM,EAAE;QAAE,EAAE,EAAE,SAAS,CAAC;QAAC,MAAM,EAAE,kBAAkB,CAAA;KAAE,CAAA;IACrD,SAAS,EAAE;QAAE,MAAM,EAAE,kBAAkB,CAAA;KAAE,CAAA;CAC1C,CAAA;AAOD,wBAAgB,kBAAkB;;GAmDjC;AAED,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAA;AAIhE,wBAAgB,WAAW,CAAC,GAAG,EAAE,mBAAmB,GAAG,SAAS,aAAa,EAAE,CAE9E;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,mBAAmB,GAAG,aAAa,GAAG,IAAI,CAExE;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,mBAAmB,GAAG,OAAO,CAElE;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,mBAAmB,EAAE,EAAE,EAAE,SAAS,GAAG,OAAO,CAE7E;AAED,uEAAuE;AACvE,wBAAgB,cAAc,CAAC,GAAG,EAAE,mBAAmB,GAAG,SAAS,SAAS,EAAE,CAE7E"}
@@ -0,0 +1,84 @@
1
+ import { createStore } from "@xstate/store";
2
+ /**
3
+ * Owned command surfaces keep their mode local; the host mode was never
4
+ * mutated on open, so there is nothing to restore. For legacy entries, only
5
+ * the topmost non-ownCommands overlay owns the host mode.
6
+ */
7
+ function restoreModeDecision(stack, record) {
8
+ if (record.options.ownCommands || record.options.restoreMode === false)
9
+ return null;
10
+ const topLegacy = stack.findLast((entry) => !entry.options.ownCommands);
11
+ return topLegacy === record ? record.prevMode : null;
12
+ }
13
+ export function createOverlayStore() {
14
+ return createStore({
15
+ context: { stack: [] },
16
+ on: {
17
+ opened: (ctx, event, enqueue) => {
18
+ // Replacing an existing same-id entry closes it: consumers release
19
+ // open-state (e.g. a picker's isOpen) via the contract's "replaced"
20
+ // reason instead of the entry being silently filtered away.
21
+ const displaced = ctx.stack.find((entry) => entry.id === event.record.id);
22
+ if (displaced) {
23
+ enqueue.emit.closed({ record: displaced, reason: "replaced", restoreModeTo: null });
24
+ }
25
+ const filtered = displaced
26
+ ? ctx.stack.filter((entry) => entry.id !== event.record.id)
27
+ : ctx.stack;
28
+ return { stack: [...filtered, event.record] };
29
+ },
30
+ updated: (ctx, event) => {
31
+ const idx = ctx.stack.findIndex((entry) => entry.id === event.id);
32
+ if (idx === -1)
33
+ return ctx;
34
+ const record = ctx.stack[idx];
35
+ const payload = typeof event.next === "function"
36
+ ? event.next(record.payload)
37
+ : event.next;
38
+ const stack = [...ctx.stack];
39
+ stack[idx] = { ...record, payload };
40
+ return { stack };
41
+ },
42
+ closed: (ctx, event, enqueue) => {
43
+ const record = ctx.stack.find((entry) => entry.id === event.id);
44
+ if (!record)
45
+ return ctx;
46
+ enqueue.emit.closed({
47
+ record,
48
+ reason: event.reason,
49
+ restoreModeTo: restoreModeDecision(ctx.stack, record),
50
+ });
51
+ return { stack: ctx.stack.filter((entry) => entry !== record) };
52
+ },
53
+ closedTop: (ctx, event, enqueue) => {
54
+ if (ctx.stack.length === 0)
55
+ return ctx;
56
+ const record = ctx.stack[ctx.stack.length - 1];
57
+ enqueue.emit.closed({
58
+ record,
59
+ reason: event.reason,
60
+ restoreModeTo: restoreModeDecision(ctx.stack, record),
61
+ });
62
+ return { stack: ctx.stack.slice(0, -1) };
63
+ },
64
+ },
65
+ });
66
+ }
67
+ // --- Selectors ---------------------------------------------------------------
68
+ export function selectStack(ctx) {
69
+ return ctx.stack;
70
+ }
71
+ export function selectTop(ctx) {
72
+ return ctx.stack.length > 0 ? ctx.stack[ctx.stack.length - 1] : null;
73
+ }
74
+ export function selectHasOverlay(ctx) {
75
+ return ctx.stack.length > 0;
76
+ }
77
+ export function selectIsOpen(ctx, id) {
78
+ return ctx.stack.some((entry) => entry.id === id);
79
+ }
80
+ /** Fresh array; prefer selectStack identity + memo in render paths. */
81
+ export function selectStackIds(ctx) {
82
+ return ctx.stack.map((entry) => entry.id);
83
+ }
84
+ //# sourceMappingURL=overlay-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"overlay-store.js","sourceRoot":"","sources":["../src/overlay-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare function overlayStoreTypeChecks(): void;
2
+ //# sourceMappingURL=overlay-store.typecheck.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"overlay-store.typecheck.d.ts","sourceRoot":"","sources":["../src/overlay-store.typecheck.ts"],"names":[],"mappings":"AAoBA,wBAAgB,sBAAsB,IAAI,IAAI,CA4B7C"}
@@ -0,0 +1,29 @@
1
+ import { createOverlayStore } from "./overlay-store.js";
2
+ // Never called; exists only to be typechecked.
3
+ export function overlayStoreTypeChecks() {
4
+ const store = createOverlayStore();
5
+ // --- Valid usage must compile -------------------------------------------
6
+ store.trigger.opened({ record });
7
+ store.trigger.updated({ id: "x", next: null });
8
+ store.trigger.closed({ id: "x", reason });
9
+ store.trigger.closedTop({ reason });
10
+ store.send({ type: "closedTop", reason });
11
+ store.on("closed", (emitted) => {
12
+ const _reason = emitted.reason;
13
+ const _restoreModeTo = emitted.restoreModeTo;
14
+ const _record = emitted.record;
15
+ });
16
+ // --- Unknown event names must NOT compile --------------------------------
17
+ // @ts-expect-error unknown trigger event name
18
+ store.trigger.bogusEvent({});
19
+ // @ts-expect-error unknown send event type
20
+ store.send({ type: "bogusEvent" });
21
+ // @ts-expect-error unknown emitted event name
22
+ store.on("bogusEmit", () => { });
23
+ // --- Wrong payloads must NOT compile --------------------------------------
24
+ // @ts-expect-error reason must be an OverlayCloseReason
25
+ store.trigger.closedTop({ reason: 42 });
26
+ // @ts-expect-error record is required
27
+ store.trigger.opened({});
28
+ }
29
+ //# sourceMappingURL=overlay-store.typecheck.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"overlay-store.typecheck.js","sourceRoot":"","sources":["../src/overlay-store.typecheck.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,kBAAkB,EAAsB,MAAM,oBAAoB,CAAA;AAK3E,+CAA+C;AAC/C,MAAM,UAAU,sBAAsB;IACpC,MAAM,KAAK,GAAG,kBAAkB,EAAE,CAAA;IAElC,2EAA2E;IAC3E,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC,CAAA;IAChC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAA;IAC9C,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,CAAA;IACzC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC,CAAA;IACnC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,CAAC,CAAA;IACzC,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,OAAO,GAAuB,OAAO,CAAC,MAAM,CAAA;QAClD,MAAM,cAAc,GAAkB,OAAO,CAAC,aAAa,CAAA;QAC3D,MAAM,OAAO,GAAkB,OAAO,CAAC,MAAM,CAAA;IAC/C,CAAC,CAAC,CAAA;IAEF,4EAA4E;IAC5E,8CAA8C;IAC9C,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC,CAAA;IAC5B,2CAA2C;IAC3C,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,CAAA;IAClC,8CAA8C;IAC9C,KAAK,CAAC,EAAE,CAAC,WAAW,EAAE,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;IAE/B,6EAA6E;IAC7E,wDAAwD;IACxD,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAA;IACvC,sCAAsC;IACtC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;AAC1B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tooee/overlays",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
4
4
  "description": "Overlay stack management for Tooee",
5
5
  "keywords": [
6
6
  "cli",
@@ -33,6 +33,9 @@
33
33
  "scripts": {
34
34
  "typecheck": "tsc --noEmit"
35
35
  },
36
+ "dependencies": {
37
+ "@xstate/store": "^4"
38
+ },
36
39
  "devDependencies": {
37
40
  "@types/bun": "^1.3.10",
38
41
  "@types/react": "^19.2.14",
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@ export {
6
6
  useOverlayState,
7
7
  useCurrentOverlay,
8
8
  useHasOverlay,
9
+ useHasModalOverlay,
9
10
  } from "./overlay-context.js"
10
11
  export type {
11
12
  OverlayContextValue,
@@ -19,3 +20,17 @@ export type {
19
20
  OverlayController,
20
21
  OverlayState,
21
22
  } from "./overlay-context.js"
23
+ export {
24
+ createOverlayStore,
25
+ selectHasOverlay,
26
+ selectIsOpen,
27
+ selectStack,
28
+ selectStackIds,
29
+ selectTop,
30
+ } from "./overlay-store.js"
31
+ export type {
32
+ OverlayClosedEmit,
33
+ OverlayRecord,
34
+ OverlayStore,
35
+ OverlayStoreContext,
36
+ } from "./overlay-store.js"
@@ -69,6 +69,13 @@ export interface OverlayController {
69
69
  export interface OverlayState {
70
70
  current: ReactNode | null
71
71
  hasOverlay: boolean
72
+ /**
73
+ * True when any overlay other than a passive owned surface is open. Passive
74
+ * surfaces (e.g. the which-key hint) render for visuals only and never own
75
+ * input, so they don't count. Use this for guards that should stand down
76
+ * while a modal overlay is up but keep working under passive hints.
77
+ */
78
+ hasModalOverlay: boolean
72
79
  stack: OverlayId[]
73
80
  }
74
81
 
@@ -95,6 +102,7 @@ const defaultController: OverlayController = {
95
102
  const defaultState: OverlayState = {
96
103
  current: null,
97
104
  hasOverlay: false,
105
+ hasModalOverlay: false,
98
106
  stack: [],
99
107
  }
100
108
 
@@ -123,6 +131,10 @@ export function useHasOverlay(): boolean {
123
131
  return useContext(OverlayStateContext).hasOverlay
124
132
  }
125
133
 
134
+ export function useHasModalOverlay(): boolean {
135
+ return useContext(OverlayStateContext).hasModalOverlay
136
+ }
137
+
126
138
  export function useCurrentOverlay(): ReactNode | null {
127
139
  return useContext(OverlayStateContext).current
128
140
  }
@@ -0,0 +1,158 @@
1
+ import { createStore } from "@xstate/store"
2
+ import type {
3
+ OverlayCloseReason,
4
+ OverlayId,
5
+ OverlayOpenOptions,
6
+ OverlayRenderer,
7
+ } from "./overlay-context.js"
8
+
9
+ /**
10
+ * An overlay stack entry as tracked by the store. `prevMode` is the host mode
11
+ * captured at open time (mode itself stays outside this package — see the
12
+ * shell's OverlayProvider, which owns all mode side effects).
13
+ */
14
+ export interface OverlayRecord<TPayload = unknown> {
15
+ id: OverlayId
16
+ render: OverlayRenderer<TPayload>
17
+ payload: TPayload
18
+ options: OverlayOpenOptions
19
+ prevMode: string
20
+ }
21
+
22
+ export interface OverlayStoreContext {
23
+ stack: readonly OverlayRecord[]
24
+ }
25
+
26
+ /**
27
+ * Emitted when a record leaves the stack (close, closeTop, or same-id
28
+ * replacement). Lifecycle side effects (onClose callbacks, mode restoration,
29
+ * sequence-reset bridges) are performed by the adapter subscribed via
30
+ * `store.on("closed", ...)`; transitions stay pure.
31
+ */
32
+ export interface OverlayClosedEmit {
33
+ record: OverlayRecord
34
+ reason: OverlayCloseReason
35
+ /**
36
+ * Legacy mode restoration decision, computed in the transition (it needs
37
+ * stack state): `record.prevMode` when the record is non-ownCommands,
38
+ * `restoreMode !== false`, AND it was the topmost non-ownCommands entry
39
+ * (closing a buried legacy overlay must not clobber the mode set by the one
40
+ * above it — R-04); otherwise null. Same-id replacement displacement never
41
+ * restores (the successor takes over).
42
+ */
43
+ restoreModeTo: string | null
44
+ }
45
+
46
+ /**
47
+ * Owned command surfaces keep their mode local; the host mode was never
48
+ * mutated on open, so there is nothing to restore. For legacy entries, only
49
+ * the topmost non-ownCommands overlay owns the host mode.
50
+ */
51
+ function restoreModeDecision(
52
+ stack: readonly OverlayRecord[],
53
+ record: OverlayRecord,
54
+ ): string | null {
55
+ if (record.options.ownCommands || record.options.restoreMode === false) return null
56
+ const topLegacy = stack.findLast((entry) => !entry.options.ownCommands)
57
+ return topLegacy === record ? record.prevMode : null
58
+ }
59
+
60
+ /**
61
+ * Event payload map, passed explicitly as a `createStore` generic
62
+ * (@xstate/store v4 removed the `emits` config; event/emitted types come from
63
+ * generics or `schemas`). Both maps here are deliberately CLOSED — no index
64
+ * signatures — so unknown `store.trigger.*` / `store.send` / `store.on` /
65
+ * `enqueue.emit.*` event names are compile errors (pinned by
66
+ * `overlay-store.typecheck.ts`). The v4 `schemas.emitted` alternative was
67
+ * evaluated and rejected: it gives the same strictness while adding runtime
68
+ * schema objects this store does not need.
69
+ */
70
+ export type OverlayStoreEvents = {
71
+ opened: { record: OverlayRecord }
72
+ updated: { id: OverlayId; next: unknown | ((prev: unknown) => unknown) }
73
+ closed: { id: OverlayId; reason: OverlayCloseReason }
74
+ closedTop: { reason: OverlayCloseReason }
75
+ }
76
+
77
+ /** Emitted payload map (see OverlayStoreEvents docs — deliberately closed). */
78
+ type OverlayStoreEmitted = {
79
+ closed: OverlayClosedEmit
80
+ }
81
+
82
+ export function createOverlayStore() {
83
+ return createStore<OverlayStoreContext, OverlayStoreEvents, OverlayStoreEmitted>({
84
+ context: { stack: [] },
85
+ on: {
86
+ opened: (ctx, event, enqueue) => {
87
+ // Replacing an existing same-id entry closes it: consumers release
88
+ // open-state (e.g. a picker's isOpen) via the contract's "replaced"
89
+ // reason instead of the entry being silently filtered away.
90
+ const displaced = ctx.stack.find((entry) => entry.id === event.record.id)
91
+ if (displaced) {
92
+ enqueue.emit.closed({ record: displaced, reason: "replaced", restoreModeTo: null })
93
+ }
94
+ const filtered = displaced
95
+ ? ctx.stack.filter((entry) => entry.id !== event.record.id)
96
+ : ctx.stack
97
+ return { stack: [...filtered, event.record] }
98
+ },
99
+ updated: (ctx, event) => {
100
+ const idx = ctx.stack.findIndex((entry) => entry.id === event.id)
101
+ if (idx === -1) return ctx
102
+ const record = ctx.stack[idx]!
103
+ const payload =
104
+ typeof event.next === "function"
105
+ ? (event.next as (prev: unknown) => unknown)(record.payload)
106
+ : event.next
107
+ const stack = [...ctx.stack]
108
+ stack[idx] = { ...record, payload }
109
+ return { stack }
110
+ },
111
+ closed: (ctx, event, enqueue) => {
112
+ const record = ctx.stack.find((entry) => entry.id === event.id)
113
+ if (!record) return ctx
114
+ enqueue.emit.closed({
115
+ record,
116
+ reason: event.reason,
117
+ restoreModeTo: restoreModeDecision(ctx.stack, record),
118
+ })
119
+ return { stack: ctx.stack.filter((entry) => entry !== record) }
120
+ },
121
+ closedTop: (ctx, event, enqueue) => {
122
+ if (ctx.stack.length === 0) return ctx
123
+ const record = ctx.stack[ctx.stack.length - 1]!
124
+ enqueue.emit.closed({
125
+ record,
126
+ reason: event.reason,
127
+ restoreModeTo: restoreModeDecision(ctx.stack, record),
128
+ })
129
+ return { stack: ctx.stack.slice(0, -1) }
130
+ },
131
+ },
132
+ })
133
+ }
134
+
135
+ export type OverlayStore = ReturnType<typeof createOverlayStore>
136
+
137
+ // --- Selectors ---------------------------------------------------------------
138
+
139
+ export function selectStack(ctx: OverlayStoreContext): readonly OverlayRecord[] {
140
+ return ctx.stack
141
+ }
142
+
143
+ export function selectTop(ctx: OverlayStoreContext): OverlayRecord | null {
144
+ return ctx.stack.length > 0 ? ctx.stack[ctx.stack.length - 1]! : null
145
+ }
146
+
147
+ export function selectHasOverlay(ctx: OverlayStoreContext): boolean {
148
+ return ctx.stack.length > 0
149
+ }
150
+
151
+ export function selectIsOpen(ctx: OverlayStoreContext, id: OverlayId): boolean {
152
+ return ctx.stack.some((entry) => entry.id === id)
153
+ }
154
+
155
+ /** Fresh array; prefer selectStack identity + memo in render paths. */
156
+ export function selectStackIds(ctx: OverlayStoreContext): readonly OverlayId[] {
157
+ return ctx.stack.map((entry) => entry.id)
158
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Compile-time regression tests for the overlay store's event typing. This
3
+ * module has no runtime behaviour and is never imported; it exists so that
4
+ * `tsc -b` (which only checks `src/`) fails if the store's typing is ever
5
+ * loosened.
6
+ *
7
+ * The store's event/emitted payload maps (`OverlayStoreEvents`, the emitted
8
+ * map inside `overlay-store.ts`) are deliberately CLOSED — no index
9
+ * signatures. That makes unknown `store.trigger.*`, `store.send`,
10
+ * `store.on(...)`, and `enqueue.emit.*` event names compile errors. If one of
11
+ * the `@ts-expect-error` lines below starts compiling, the maps have been
12
+ * widened (e.g. an index signature was added); fix the maps, not this file.
13
+ */
14
+ import type { OverlayCloseReason } from "./overlay-context.js"
15
+ import { createOverlayStore, type OverlayRecord } from "./overlay-store.js"
16
+
17
+ declare const record: OverlayRecord
18
+ declare const reason: OverlayCloseReason
19
+
20
+ // Never called; exists only to be typechecked.
21
+ export function overlayStoreTypeChecks(): void {
22
+ const store = createOverlayStore()
23
+
24
+ // --- Valid usage must compile -------------------------------------------
25
+ store.trigger.opened({ record })
26
+ store.trigger.updated({ id: "x", next: null })
27
+ store.trigger.closed({ id: "x", reason })
28
+ store.trigger.closedTop({ reason })
29
+ store.send({ type: "closedTop", reason })
30
+ store.on("closed", (emitted) => {
31
+ const _reason: OverlayCloseReason = emitted.reason
32
+ const _restoreModeTo: string | null = emitted.restoreModeTo
33
+ const _record: OverlayRecord = emitted.record
34
+ })
35
+
36
+ // --- Unknown event names must NOT compile --------------------------------
37
+ // @ts-expect-error unknown trigger event name
38
+ store.trigger.bogusEvent({})
39
+ // @ts-expect-error unknown send event type
40
+ store.send({ type: "bogusEvent" })
41
+ // @ts-expect-error unknown emitted event name
42
+ store.on("bogusEmit", () => {})
43
+
44
+ // --- Wrong payloads must NOT compile --------------------------------------
45
+ // @ts-expect-error reason must be an OverlayCloseReason
46
+ store.trigger.closedTop({ reason: 42 })
47
+ // @ts-expect-error record is required
48
+ store.trigger.opened({})
49
+ }