@ilokesto/overlay 1.0.0
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/README.ko.md +143 -0
- package/README.md +143 -0
- package/dist/contracts/adapter.d.ts +10 -0
- package/dist/contracts/adapter.js +1 -0
- package/dist/contracts/overlay.d.ts +37 -0
- package/dist/contracts/overlay.js +1 -0
- package/dist/core/OverlayHost.d.ts +1 -0
- package/dist/core/OverlayHost.js +29 -0
- package/dist/core/OverlayProvider.d.ts +9 -0
- package/dist/core/OverlayProvider.js +17 -0
- package/dist/core/createOverlayStore.d.ts +2 -0
- package/dist/core/createOverlayStore.js +102 -0
- package/dist/core/useOverlay.d.ts +9 -0
- package/dist/core/useOverlay.js +23 -0
- package/dist/core/useOverlayItems.d.ts +2 -0
- package/dist/core/useOverlayItems.js +6 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +5 -0
- package/package.json +47 -0
package/README.ko.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# @ilokesto/overlay
|
|
2
|
+
|
|
3
|
+
[English](./README.md) | **한국어**
|
|
4
|
+
|
|
5
|
+
`@ilokesto/store` 위에 얹는 작은 React overlay runtime입니다.
|
|
6
|
+
|
|
7
|
+
이 패키지는 provider-scoped overlay core, built-in host, item lifecycle 관리, adapter 주입 구조를 제공합니다. modal이나 toast 의미론은 의도적으로 코어에 넣지 않아서, 상위 패키지가 같은 runtime 위에서 자신만의 동작을 구현할 수 있게 되어 있습니다.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- 전역 싱글턴이 아닌 provider-scoped overlay runtime
|
|
12
|
+
- adapter registry를 통해 overlay item을 렌더링하는 built-in host
|
|
13
|
+
- 같은 store lifecycle 위에서 동기/비동기 overlay 열기 지원
|
|
14
|
+
- runtime core와 공용 contract의 명확한 분리
|
|
15
|
+
- overlay를 열고 닫고 제거하고 관찰하는 작은 공개 API
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pnpm add @ilokesto/overlay react
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
또는
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install @ilokesto/overlay react
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Basic Usage
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
import { OverlayProvider, useOverlay, type OverlayAdapterMap } from '@ilokesto/overlay';
|
|
33
|
+
|
|
34
|
+
const adapters: OverlayAdapterMap = {
|
|
35
|
+
modal: ({ isOpen, close, title }) => {
|
|
36
|
+
if (!isOpen) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div role="dialog" aria-modal="true">
|
|
42
|
+
<h1>{String(title)}</h1>
|
|
43
|
+
<button onClick={() => close(true)}>Confirm</button>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function ExampleButton() {
|
|
50
|
+
const { display } = useOverlay();
|
|
51
|
+
|
|
52
|
+
const handleClick = async () => {
|
|
53
|
+
const result = await display<boolean>({
|
|
54
|
+
type: 'modal',
|
|
55
|
+
props: { title: 'Delete this item?' },
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
console.log(result);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return <button onClick={handleClick}>Open</button>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function App() {
|
|
65
|
+
return (
|
|
66
|
+
<OverlayProvider adapters={adapters}>
|
|
67
|
+
<ExampleButton />
|
|
68
|
+
</OverlayProvider>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Source Layout
|
|
74
|
+
|
|
75
|
+
```text
|
|
76
|
+
src/
|
|
77
|
+
core/
|
|
78
|
+
createOverlayStore.ts
|
|
79
|
+
OverlayProvider.tsx
|
|
80
|
+
OverlayHost.tsx
|
|
81
|
+
useOverlay.ts
|
|
82
|
+
useOverlayItems.ts
|
|
83
|
+
contracts/
|
|
84
|
+
adapter.ts
|
|
85
|
+
overlay.ts
|
|
86
|
+
index.ts
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Responsibilities
|
|
90
|
+
|
|
91
|
+
### `src/core`
|
|
92
|
+
|
|
93
|
+
- `createOverlayStore.ts` → provider 단위 overlay store를 만들고 `open`, `close`, `remove`, `clear` 수명주기를 관리합니다
|
|
94
|
+
- `OverlayProvider.tsx` → store를 만들거나 주입받아 context로 노출하고 built-in `OverlayHost`를 마운트합니다
|
|
95
|
+
- `OverlayHost.tsx` → 현재 overlay item 목록을 읽고 각 item을 `adapters[item.type]`에 위임해 렌더링합니다
|
|
96
|
+
- `useOverlay.ts` → overlay를 열고 닫고 제거하는 명령형 API를 제공합니다
|
|
97
|
+
- `useOverlayItems.ts` → `useSyncExternalStore`로 현재 overlay item 목록을 구독합니다
|
|
98
|
+
|
|
99
|
+
### `src/contracts`
|
|
100
|
+
|
|
101
|
+
- `adapter.ts` → `OverlayRenderProps`, `OverlayAdapterComponent`, `OverlayAdapterMap` 같은 adapter 렌더링 계약을 정의합니다
|
|
102
|
+
- `overlay.ts` → `OverlayItem`, `OverlayStoreApi`, `DisplayOptions`, `OverlayProviderProps` 같은 overlay runtime 계약을 정의합니다
|
|
103
|
+
|
|
104
|
+
### `src/index.ts`
|
|
105
|
+
|
|
106
|
+
- `core/*`의 runtime API를 다시 export합니다
|
|
107
|
+
- `contracts/*`의 공용 타입을 다시 export합니다
|
|
108
|
+
|
|
109
|
+
## Dependency Direction
|
|
110
|
+
|
|
111
|
+
- `core/*` 는 `contracts/*` 에 의존합니다
|
|
112
|
+
- `contracts/overlay.ts` 는 `contracts/adapter.ts` 에 의존합니다
|
|
113
|
+
- `contracts/adapter.ts` 는 runtime 코드에 의존하지 않습니다
|
|
114
|
+
- modal이나 toast 같은 adapter 패키지는 `@ilokesto/overlay` 에 의존해야 합니다
|
|
115
|
+
- `@ilokesto/overlay` 는 modal/toast 구현을 직접 import하면 안 됩니다
|
|
116
|
+
|
|
117
|
+
한 줄로 말하면, 코어는 lifecycle과 hosting을 담당하고 adapter 패키지는 의미론과 표현을 담당합니다.
|
|
118
|
+
|
|
119
|
+
## Adapter Packages
|
|
120
|
+
|
|
121
|
+
이 패키지는 의도적으로 generic하게 설계되어 있습니다.
|
|
122
|
+
|
|
123
|
+
- modal 패키지는 overlay runtime 위에 modal adapter를 주입해서 사용할 수 있습니다
|
|
124
|
+
- toast 패키지도 같은 runtime 위에 toast adapter를 주입해서 사용할 수 있습니다
|
|
125
|
+
- focus trap, scroll lock, backdrop 동작, deduplication, timer, animation 같은 정책은 overlay core가 아니라 adapter 레이어에 있어야 합니다
|
|
126
|
+
|
|
127
|
+
## Exports
|
|
128
|
+
|
|
129
|
+
- `@ilokesto/overlay` → `createOverlayStore`, `OverlayProvider`, `OverlayHost`, `useOverlay`, `useOverlayItems`
|
|
130
|
+
- `@ilokesto/overlay` 타입 → `src/contracts/adapter.ts`, `src/contracts/overlay.ts`, `UseOverlayReturn`에서 다시 export된 타입
|
|
131
|
+
|
|
132
|
+
## Development
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
pnpm install
|
|
136
|
+
pnpm run build
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
빌드 결과물은 `dist` 디렉터리에 생성됩니다.
|
|
140
|
+
|
|
141
|
+
## License
|
|
142
|
+
|
|
143
|
+
MIT
|
package/README.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# @ilokesto/overlay
|
|
2
|
+
|
|
3
|
+
**English** | [한국어](./README.ko.md)
|
|
4
|
+
|
|
5
|
+
A small React overlay runtime built on top of `@ilokesto/store`.
|
|
6
|
+
|
|
7
|
+
This package provides a provider-scoped overlay core with a built-in host, item lifecycle management, and adapter injection. It is intentionally headless about modal or toast semantics so higher-level packages can build on top of the same runtime without leaking behavior into the core.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- Provider-scoped overlay runtime instead of a global singleton
|
|
12
|
+
- Built-in host that renders overlay items through an adapter registry
|
|
13
|
+
- Sync and async overlay opening through the same store lifecycle
|
|
14
|
+
- Clean separation between runtime core and shared contracts
|
|
15
|
+
- A small public API for opening, closing, removing, and observing overlays
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pnpm add @ilokesto/overlay react
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
or
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install @ilokesto/overlay react
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Basic Usage
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
import { OverlayProvider, useOverlay, type OverlayAdapterMap } from '@ilokesto/overlay';
|
|
33
|
+
|
|
34
|
+
const adapters: OverlayAdapterMap = {
|
|
35
|
+
modal: ({ isOpen, close, title }) => {
|
|
36
|
+
if (!isOpen) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div role="dialog" aria-modal="true">
|
|
42
|
+
<h1>{String(title)}</h1>
|
|
43
|
+
<button onClick={() => close(true)}>Confirm</button>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function ExampleButton() {
|
|
50
|
+
const { display } = useOverlay();
|
|
51
|
+
|
|
52
|
+
const handleClick = async () => {
|
|
53
|
+
const result = await display<boolean>({
|
|
54
|
+
type: 'modal',
|
|
55
|
+
props: { title: 'Delete this item?' },
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
console.log(result);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return <button onClick={handleClick}>Open</button>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function App() {
|
|
65
|
+
return (
|
|
66
|
+
<OverlayProvider adapters={adapters}>
|
|
67
|
+
<ExampleButton />
|
|
68
|
+
</OverlayProvider>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Source Layout
|
|
74
|
+
|
|
75
|
+
```text
|
|
76
|
+
src/
|
|
77
|
+
core/
|
|
78
|
+
createOverlayStore.ts
|
|
79
|
+
OverlayProvider.tsx
|
|
80
|
+
OverlayHost.tsx
|
|
81
|
+
useOverlay.ts
|
|
82
|
+
useOverlayItems.ts
|
|
83
|
+
contracts/
|
|
84
|
+
adapter.ts
|
|
85
|
+
overlay.ts
|
|
86
|
+
index.ts
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Responsibilities
|
|
90
|
+
|
|
91
|
+
### `src/core`
|
|
92
|
+
|
|
93
|
+
- `createOverlayStore.ts` → creates the provider-scoped overlay store and manages `open`, `close`, `remove`, and `clear`
|
|
94
|
+
- `OverlayProvider.tsx` → creates or receives a store, exposes it through context, and mounts the built-in `OverlayHost`
|
|
95
|
+
- `OverlayHost.tsx` → reads the current overlay items and dispatches each item to `adapters[item.type]`
|
|
96
|
+
- `useOverlay.ts` → exposes the command API for opening and dismissing overlays
|
|
97
|
+
- `useOverlayItems.ts` → subscribes to the current overlay item list with `useSyncExternalStore`
|
|
98
|
+
|
|
99
|
+
### `src/contracts`
|
|
100
|
+
|
|
101
|
+
- `adapter.ts` → adapter-facing rendering contracts such as `OverlayRenderProps`, `OverlayAdapterComponent`, and `OverlayAdapterMap`
|
|
102
|
+
- `overlay.ts` → overlay runtime contracts such as `OverlayItem`, `OverlayStoreApi`, `DisplayOptions`, and `OverlayProviderProps`
|
|
103
|
+
|
|
104
|
+
### `src/index.ts`
|
|
105
|
+
|
|
106
|
+
- Re-exports the runtime APIs from `core/*`
|
|
107
|
+
- Re-exports shared contract types from `contracts/*`
|
|
108
|
+
|
|
109
|
+
## Dependency Direction
|
|
110
|
+
|
|
111
|
+
- `core/*` depends on `contracts/*`
|
|
112
|
+
- `contracts/overlay.ts` depends on `contracts/adapter.ts`
|
|
113
|
+
- `contracts/adapter.ts` does not depend on runtime code
|
|
114
|
+
- Adapter packages such as modal or toast should depend on `@ilokesto/overlay`
|
|
115
|
+
- `@ilokesto/overlay` should not import modal or toast implementations directly
|
|
116
|
+
|
|
117
|
+
In short, the core owns lifecycle and hosting, while adapter packages own semantics and presentation.
|
|
118
|
+
|
|
119
|
+
## Adapter Packages
|
|
120
|
+
|
|
121
|
+
This package is intentionally generic.
|
|
122
|
+
|
|
123
|
+
- A modal package can use the overlay runtime and inject modal adapters
|
|
124
|
+
- A toast package can use the same runtime and inject toast adapters
|
|
125
|
+
- Policies such as focus trapping, scroll lock, backdrop behavior, deduplication, timers, and animations belong in the adapter layer, not in the overlay core
|
|
126
|
+
|
|
127
|
+
## Exports
|
|
128
|
+
|
|
129
|
+
- `@ilokesto/overlay` → `createOverlayStore`, `OverlayProvider`, `OverlayHost`, `useOverlay`, `useOverlayItems`
|
|
130
|
+
- `@ilokesto/overlay` types → contracts from `src/contracts/adapter.ts`, `src/contracts/overlay.ts`, and `UseOverlayReturn`
|
|
131
|
+
|
|
132
|
+
## Development
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
pnpm install
|
|
136
|
+
pnpm run build
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Build outputs are generated in the `dist` directory.
|
|
140
|
+
|
|
141
|
+
## License
|
|
142
|
+
|
|
143
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ComponentType } from "react";
|
|
2
|
+
export interface OverlayRenderProps<TResult = unknown> {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly isOpen: boolean;
|
|
5
|
+
readonly status: "open" | "closing";
|
|
6
|
+
readonly close: (result?: TResult) => void;
|
|
7
|
+
readonly remove: () => void;
|
|
8
|
+
}
|
|
9
|
+
export type OverlayAdapterComponent<TResult = unknown> = ComponentType<OverlayRenderProps<TResult> & Record<string, unknown>>;
|
|
10
|
+
export type OverlayAdapterMap = Readonly<Record<string, OverlayAdapterComponent>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { OverlayAdapterMap } from "./adapter";
|
|
3
|
+
export type OverlayId = string;
|
|
4
|
+
export type OverlayStatus = "open" | "closing";
|
|
5
|
+
export interface OverlayItem {
|
|
6
|
+
readonly id: OverlayId;
|
|
7
|
+
readonly type: string;
|
|
8
|
+
readonly props: Readonly<Record<string, unknown>>;
|
|
9
|
+
readonly status: OverlayStatus;
|
|
10
|
+
readonly createdAt: number;
|
|
11
|
+
}
|
|
12
|
+
export interface OverlayState {
|
|
13
|
+
readonly items: ReadonlyArray<OverlayItem>;
|
|
14
|
+
}
|
|
15
|
+
export interface DisplayOptions {
|
|
16
|
+
readonly id?: OverlayId;
|
|
17
|
+
readonly type: string;
|
|
18
|
+
readonly props?: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
export interface OverlayRequest<TResult = unknown> {
|
|
21
|
+
readonly id: OverlayId;
|
|
22
|
+
readonly promise: Promise<TResult | undefined>;
|
|
23
|
+
}
|
|
24
|
+
export interface OverlayStoreApi {
|
|
25
|
+
open: <TResult = unknown>(options: DisplayOptions) => OverlayRequest<TResult>;
|
|
26
|
+
close: (id: OverlayId, result?: unknown) => void;
|
|
27
|
+
remove: (id?: OverlayId) => void;
|
|
28
|
+
clear: () => void;
|
|
29
|
+
subscribe: (listener: () => void) => () => void;
|
|
30
|
+
getSnapshot: () => ReadonlyArray<OverlayItem>;
|
|
31
|
+
getInitialSnapshot: () => ReadonlyArray<OverlayItem>;
|
|
32
|
+
}
|
|
33
|
+
export interface OverlayProviderProps {
|
|
34
|
+
readonly adapters: OverlayAdapterMap;
|
|
35
|
+
readonly children: ReactNode;
|
|
36
|
+
readonly store?: OverlayStoreApi;
|
|
37
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function OverlayHost(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback } from "react";
|
|
3
|
+
import { useOverlayContext } from "./OverlayProvider";
|
|
4
|
+
import { useOverlayItems } from "./useOverlayItems";
|
|
5
|
+
function OverlayItemRenderer({ item }) {
|
|
6
|
+
const { store, adapters } = useOverlayContext();
|
|
7
|
+
const Adapter = adapters[item.type];
|
|
8
|
+
const close = useCallback((result) => {
|
|
9
|
+
store.close(item.id, result);
|
|
10
|
+
}, [item.id, store]);
|
|
11
|
+
const remove = useCallback(() => {
|
|
12
|
+
store.remove(item.id);
|
|
13
|
+
}, [item.id, store]);
|
|
14
|
+
if (!Adapter) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const renderProps = {
|
|
18
|
+
id: item.id,
|
|
19
|
+
isOpen: item.status === "open",
|
|
20
|
+
status: item.status,
|
|
21
|
+
close,
|
|
22
|
+
remove,
|
|
23
|
+
};
|
|
24
|
+
return _jsx(Adapter, { ...renderProps, ...item.props });
|
|
25
|
+
}
|
|
26
|
+
export function OverlayHost() {
|
|
27
|
+
const items = useOverlayItems();
|
|
28
|
+
return (_jsx(_Fragment, { children: items.map((item) => (_jsx(OverlayItemRenderer, { item: item }, item.id))) }));
|
|
29
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { OverlayAdapterMap } from "../contracts/adapter";
|
|
2
|
+
import type { OverlayProviderProps, OverlayStoreApi } from "../contracts/overlay";
|
|
3
|
+
interface OverlayContextValue {
|
|
4
|
+
readonly store: OverlayStoreApi;
|
|
5
|
+
readonly adapters: OverlayAdapterMap;
|
|
6
|
+
}
|
|
7
|
+
export declare function useOverlayContext(): OverlayContextValue;
|
|
8
|
+
export declare function OverlayProvider({ adapters, children, store: storeProp, }: OverlayProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useMemo } from "react";
|
|
3
|
+
import { createOverlayStore } from "./createOverlayStore";
|
|
4
|
+
import { OverlayHost } from "./OverlayHost";
|
|
5
|
+
const OverlayContext = createContext(null);
|
|
6
|
+
export function useOverlayContext() {
|
|
7
|
+
const context = useContext(OverlayContext);
|
|
8
|
+
if (context === null) {
|
|
9
|
+
throw new Error("useOverlayContext must be used within an <OverlayProvider>.");
|
|
10
|
+
}
|
|
11
|
+
return context;
|
|
12
|
+
}
|
|
13
|
+
export function OverlayProvider({ adapters, children, store: storeProp, }) {
|
|
14
|
+
const store = useMemo(() => storeProp ?? createOverlayStore(), [storeProp]);
|
|
15
|
+
const value = useMemo(() => ({ store, adapters }), [store, adapters]);
|
|
16
|
+
return (_jsxs(OverlayContext.Provider, { value: value, children: [children, _jsx(OverlayHost, {})] }));
|
|
17
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { Store } from "@ilokesto/store";
|
|
2
|
+
function createOverlayItem(options, id) {
|
|
3
|
+
return {
|
|
4
|
+
id,
|
|
5
|
+
type: options.type,
|
|
6
|
+
props: options.props ?? {},
|
|
7
|
+
status: "open",
|
|
8
|
+
createdAt: Date.now(),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function createOverlayStore() {
|
|
12
|
+
const store = new Store({ items: [] });
|
|
13
|
+
const pendingResolvers = new Map();
|
|
14
|
+
let counter = 0;
|
|
15
|
+
function nextId() {
|
|
16
|
+
counter += 1;
|
|
17
|
+
return `overlay-${counter}-${Date.now()}`;
|
|
18
|
+
}
|
|
19
|
+
function settle(id, value) {
|
|
20
|
+
const resolver = pendingResolvers.get(id);
|
|
21
|
+
if (!resolver) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
pendingResolvers.delete(id);
|
|
25
|
+
resolver(value);
|
|
26
|
+
}
|
|
27
|
+
function open(options) {
|
|
28
|
+
const id = options.id ?? nextId();
|
|
29
|
+
const item = createOverlayItem(options, id);
|
|
30
|
+
const promise = new Promise((resolve) => {
|
|
31
|
+
pendingResolvers.set(id, resolve);
|
|
32
|
+
});
|
|
33
|
+
store.setState((prev) => ({
|
|
34
|
+
...prev,
|
|
35
|
+
items: [...prev.items, item],
|
|
36
|
+
}));
|
|
37
|
+
return { id, promise };
|
|
38
|
+
}
|
|
39
|
+
function close(id, result) {
|
|
40
|
+
let shouldResolve = false;
|
|
41
|
+
store.setState((prev) => ({
|
|
42
|
+
...prev,
|
|
43
|
+
items: prev.items.map((item) => {
|
|
44
|
+
if (item.id !== id || item.status === "closing") {
|
|
45
|
+
return item;
|
|
46
|
+
}
|
|
47
|
+
shouldResolve = true;
|
|
48
|
+
return {
|
|
49
|
+
...item,
|
|
50
|
+
status: "closing",
|
|
51
|
+
};
|
|
52
|
+
}),
|
|
53
|
+
}));
|
|
54
|
+
if (shouldResolve) {
|
|
55
|
+
settle(id, result);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function remove(id) {
|
|
59
|
+
const targetId = id ?? store.getState().items.at(-1)?.id;
|
|
60
|
+
if (!targetId) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const targetItem = store.getState().items.find((item) => item.id === targetId);
|
|
64
|
+
if (!targetItem) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (targetItem.status !== "closing") {
|
|
68
|
+
settle(targetId, undefined);
|
|
69
|
+
}
|
|
70
|
+
store.setState((prev) => ({
|
|
71
|
+
...prev,
|
|
72
|
+
items: prev.items.filter((item) => item.id !== targetId),
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
function clear() {
|
|
76
|
+
for (const item of store.getState().items) {
|
|
77
|
+
settle(item.id, undefined);
|
|
78
|
+
}
|
|
79
|
+
store.setState((prev) => ({
|
|
80
|
+
...prev,
|
|
81
|
+
items: [],
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
function subscribe(listener) {
|
|
85
|
+
return store.subscribe(listener);
|
|
86
|
+
}
|
|
87
|
+
function getSnapshot() {
|
|
88
|
+
return store.getState().items;
|
|
89
|
+
}
|
|
90
|
+
function getInitialSnapshot() {
|
|
91
|
+
return store.getInitialState().items;
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
open,
|
|
95
|
+
close,
|
|
96
|
+
remove,
|
|
97
|
+
clear,
|
|
98
|
+
subscribe,
|
|
99
|
+
getSnapshot,
|
|
100
|
+
getInitialSnapshot,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DisplayOptions, OverlayId } from "../contracts/overlay";
|
|
2
|
+
export interface UseOverlayReturn {
|
|
3
|
+
readonly display: <TResult = unknown>(options: DisplayOptions) => Promise<TResult | undefined>;
|
|
4
|
+
readonly open: (options: DisplayOptions) => OverlayId;
|
|
5
|
+
readonly close: (id: OverlayId, result?: unknown) => void;
|
|
6
|
+
readonly remove: (id?: OverlayId) => void;
|
|
7
|
+
readonly clear: () => void;
|
|
8
|
+
}
|
|
9
|
+
export declare function useOverlay(): UseOverlayReturn;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import { useOverlayContext } from "./OverlayProvider";
|
|
3
|
+
export function useOverlay() {
|
|
4
|
+
const { store } = useOverlayContext();
|
|
5
|
+
const display = useCallback((options) => {
|
|
6
|
+
const { promise } = store.open(options);
|
|
7
|
+
return promise;
|
|
8
|
+
}, [store]);
|
|
9
|
+
const open = useCallback((options) => {
|
|
10
|
+
const { id } = store.open(options);
|
|
11
|
+
return id;
|
|
12
|
+
}, [store]);
|
|
13
|
+
const close = useCallback((id, result) => {
|
|
14
|
+
store.close(id, result);
|
|
15
|
+
}, [store]);
|
|
16
|
+
const remove = useCallback((id) => {
|
|
17
|
+
store.remove(id);
|
|
18
|
+
}, [store]);
|
|
19
|
+
const clear = useCallback(() => {
|
|
20
|
+
store.clear();
|
|
21
|
+
}, [store]);
|
|
22
|
+
return { display, open, close, remove, clear };
|
|
23
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { useSyncExternalStore } from "react";
|
|
2
|
+
import { useOverlayContext } from "./OverlayProvider";
|
|
3
|
+
export function useOverlayItems() {
|
|
4
|
+
const { store } = useOverlayContext();
|
|
5
|
+
return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getInitialSnapshot);
|
|
6
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { createOverlayStore } from "./core/createOverlayStore";
|
|
2
|
+
export { OverlayProvider } from "./core/OverlayProvider";
|
|
3
|
+
export { OverlayHost } from "./core/OverlayHost";
|
|
4
|
+
export { useOverlay } from "./core/useOverlay";
|
|
5
|
+
export { useOverlayItems } from "./core/useOverlayItems";
|
|
6
|
+
export type { OverlayAdapterComponent, OverlayAdapterMap, OverlayRenderProps, } from "./contracts/adapter";
|
|
7
|
+
export type { DisplayOptions, OverlayId, OverlayItem, OverlayProviderProps, OverlayRequest, OverlayState, OverlayStatus, OverlayStoreApi, } from "./contracts/overlay";
|
|
8
|
+
export type { UseOverlayReturn } from "./core/useOverlay";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createOverlayStore } from "./core/createOverlayStore";
|
|
2
|
+
export { OverlayProvider } from "./core/OverlayProvider";
|
|
3
|
+
export { OverlayHost } from "./core/OverlayHost";
|
|
4
|
+
export { useOverlay } from "./core/useOverlay";
|
|
5
|
+
export { useOverlayItems } from "./core/useOverlayItems";
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ilokesto/overlay",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"scripts": {
|
|
5
|
+
"build": "rm -rf dist && tsc"
|
|
6
|
+
},
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/ilokesto/overlay"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://ilokesto.ayden94.com/en/overlay",
|
|
12
|
+
"sideEffects": false,
|
|
13
|
+
"types": "dist/index.d.ts",
|
|
14
|
+
"module": "dist/index.js",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"import": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"keywords": [
|
|
25
|
+
"overlay",
|
|
26
|
+
"react",
|
|
27
|
+
"modal",
|
|
28
|
+
"provider",
|
|
29
|
+
"hooks"
|
|
30
|
+
],
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"author": "Jeong Jinho <wpfekdml@me.com>",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@ilokesto/store": "1.1.1"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"react": "18.2.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^22.19.1",
|
|
44
|
+
"@types/react": "^19.1.6",
|
|
45
|
+
"typescript": "^6.0.2"
|
|
46
|
+
}
|
|
47
|
+
}
|