@deftui/addons 0.1.6 → 0.2.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.
@@ -1,17 +1,24 @@
1
- "use client";
2
1
  import { Component, Suspense, memo, useLayoutEffect } from "react";
3
- import { jsx } from "react/jsx-runtime";
4
- // src/modal-manager/host.tsx
5
- function ModalRenderer({ view, onPresent }) {
6
- useLayoutEffect(() => onPresent(view.id, view.component), [
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ // src/modal-registry/provider.tsx
4
+ function ModalRenderer({ view, onPresent, onError }) {
5
+ useLayoutEffect(() => onPresent(view.id, view.definition), [
7
6
  onPresent,
8
7
  view.id,
9
- view.component
8
+ view.definition
10
9
  ]);
11
- const Content = view.component;
12
- return /* @__PURE__ */ jsx(Content, {
13
- ...view.props,
14
- modal: view.modal
10
+ const outlet = view.children.length > 0 ? view.children.map((child) => /* @__PURE__ */ jsx(ModalEntry, {
11
+ view: child,
12
+ onPresent,
13
+ onError
14
+ }, child.id)) : null;
15
+ const { root: Root, component: Content } = view.definition;
16
+ return /* @__PURE__ */ jsxs(Root, {
17
+ ...view.rootProps,
18
+ children: [/* @__PURE__ */ jsx(Content, {
19
+ ...view.props,
20
+ modal: view.modal
21
+ }), outlet]
15
22
  });
16
23
  }
17
24
  var ModalErrorBoundary = class extends Component {
@@ -26,7 +33,7 @@ var ModalErrorBoundary = class extends Component {
26
33
  return this.state.failed ? null : this.props.children;
27
34
  }
28
35
  };
29
- /** Unchanged instances do not rerender when another instance opens or closes. */
36
+ /** Unchanged calls keep their component instance when a sibling opens or closes. */
30
37
  const ModalEntry = memo(function ModalEntry({ view, onPresent, onError }) {
31
38
  return /* @__PURE__ */ jsx(ModalErrorBoundary, {
32
39
  onError: (error) => onError(view.id, error),
@@ -34,7 +41,8 @@ const ModalEntry = memo(function ModalEntry({ view, onPresent, onError }) {
34
41
  fallback: null,
35
42
  children: /* @__PURE__ */ jsx(ModalRenderer, {
36
43
  view,
37
- onPresent
44
+ onPresent,
45
+ onError
38
46
  })
39
47
  })
40
48
  });
@@ -0,0 +1,16 @@
1
+ import { ModalRegistry, ModalRegistryOptions } from "./types.mjs";
2
+ /**
3
+ * Create one registry for the application and define modal modules locally.
4
+ * Definitions are callable components, so modal modules remain Fast Refresh-compatible.
5
+ *
6
+ * @example
7
+ * export const registry = createModalRegistry({ hot: import.meta.hot, key: "modals" });
8
+ *
9
+ * export const EditDialog = registry.define(Dialog, EditDialogContent);
10
+ * function EditDialogContent({ id }: Modal<{ id: string }>) {
11
+ * return <Dialog.Content>{id}</Dialog.Content>;
12
+ * }
13
+ *
14
+ * const handle = EditDialog.show({ id: "123" });
15
+ */
16
+ export declare function createModalRegistry(options?: ModalRegistryOptions): ModalRegistry;
@@ -0,0 +1,263 @@
1
+ import { ModalEntry } from "./provider.mjs";
2
+ import { useEffect, useSyncExternalStore } from "react";
3
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
4
+ // src/modal-registry/registry.tsx
5
+ const EMPTY = [];
6
+ /**
7
+ * Create one registry for the application and define modal modules locally.
8
+ * Definitions are callable components, so modal modules remain Fast Refresh-compatible.
9
+ *
10
+ * @example
11
+ * export const registry = createModalRegistry({ hot: import.meta.hot, key: "modals" });
12
+ *
13
+ * export const EditDialog = registry.define(Dialog, EditDialogContent);
14
+ * function EditDialogContent({ id }: Modal<{ id: string }>) {
15
+ * return <Dialog.Content>{id}</Dialog.Content>;
16
+ * }
17
+ *
18
+ * const handle = EditDialog.show({ id: "123" });
19
+ */
20
+ function createModalRegistry(options) {
21
+ const cache = options?.hot ? options.hot.data.__deftuiModalRegistries ??= /* @__PURE__ */ new Map() : void 0;
22
+ const cached = options && cache?.get(options.key);
23
+ if (cached) return cached;
24
+ let nextId = 0;
25
+ let snapshot = EMPTY;
26
+ let providerAttached = false;
27
+ const hotDefinitions = options?.hot ? options.hot.data.__deftuiModalDefinitions ??= /* @__PURE__ */ new Map() : void 0;
28
+ const roots = /* @__PURE__ */ new Set();
29
+ const instances = /* @__PURE__ */ new Map();
30
+ const callIds = /* @__PURE__ */ new WeakMap();
31
+ const listeners = /* @__PURE__ */ new Set();
32
+ function subscribe(listener) {
33
+ listeners.add(listener);
34
+ return () => {
35
+ listeners.delete(listener);
36
+ };
37
+ }
38
+ function getSnapshot() {
39
+ return snapshot;
40
+ }
41
+ function getServerSnapshot() {
42
+ return EMPTY;
43
+ }
44
+ function createRootProps(instance) {
45
+ return {
46
+ open: instance.phase !== "closing",
47
+ onOpenChange: (open) => {
48
+ if (!open) instance.call.dismiss();
49
+ },
50
+ onOpenChangeComplete: (open) => {
51
+ if (!open && instance.phase === "closing") removeInstance(instance);
52
+ }
53
+ };
54
+ }
55
+ function publish() {
56
+ function build(instance) {
57
+ const children = [...instance.children].map(build);
58
+ const open = instance.phase !== "closing";
59
+ const existing = instance.view;
60
+ if (existing && existing.definition === instance.definition && existing.props === instance.props && existing.rootProps.open === open && sameViews(existing.children, children)) return existing;
61
+ return instance.view = {
62
+ id: instance.id,
63
+ definition: instance.definition,
64
+ props: instance.props,
65
+ modal: instance.modal,
66
+ rootProps: createRootProps(instance),
67
+ children
68
+ };
69
+ }
70
+ const next = [...roots].map(build);
71
+ if (sameViews(snapshot, next)) return;
72
+ snapshot = next;
73
+ listeners.forEach((listener) => listener());
74
+ }
75
+ function sameViews(left, right) {
76
+ return left.length === right.length && left.every((view, index) => view === right[index]);
77
+ }
78
+ function removeInstance(instance) {
79
+ if (!instances.delete(instance.id)) return;
80
+ roots.delete(instance);
81
+ callIds.delete(instance.call);
82
+ if (instance.parentId !== null) {
83
+ const parent = instances.get(instance.parentId);
84
+ parent?.children.delete(instance);
85
+ if (parent) closeWhenReady(parent);
86
+ }
87
+ publish();
88
+ }
89
+ function settleTree(instance, result) {
90
+ if (instance.phase === "active") {
91
+ instance.phase = "waiting";
92
+ instance.resolveResult(result);
93
+ }
94
+ for (const child of instance.children) settleTree(child, { status: "dismissed" });
95
+ }
96
+ function closeWhenReady(instance) {
97
+ if (instance.phase !== "waiting" || instance.children.size > 0) return;
98
+ if (!instance.presented) {
99
+ removeInstance(instance);
100
+ return;
101
+ }
102
+ instance.phase = "closing";
103
+ }
104
+ function requestCloseTree(instance) {
105
+ if (instance.phase === "closing") return;
106
+ for (const child of instance.children) requestCloseTree(child);
107
+ closeWhenReady(instance);
108
+ }
109
+ function finish(instance, result) {
110
+ if (instance.phase !== "active") return;
111
+ settleTree(instance, result);
112
+ requestCloseTree(instance);
113
+ publish();
114
+ }
115
+ function onPresent(id, definition) {
116
+ const instance = instances.get(id);
117
+ if (!instance || instance.definition !== definition) return;
118
+ instance.presented = true;
119
+ }
120
+ function discardInstance(instance, rejection) {
121
+ for (const child of instance.children) discardInstance(child);
122
+ instance.children.clear();
123
+ instances.delete(instance.id);
124
+ roots.delete(instance);
125
+ callIds.delete(instance.call);
126
+ if (instance.parentId !== null) instances.get(instance.parentId)?.children.delete(instance);
127
+ if (instance.phase !== "active") return;
128
+ instance.phase = "waiting";
129
+ if (rejection) instance.rejectResult(rejection.error);
130
+ else instance.resolveResult({ status: "dismissed" });
131
+ }
132
+ function onError(id, error) {
133
+ const instance = instances.get(id);
134
+ if (!instance) return;
135
+ const { parentId } = instance;
136
+ discardInstance(instance, { error });
137
+ const parent = parentId === null ? void 0 : instances.get(parentId);
138
+ if (parent) closeWhenReady(parent);
139
+ publish();
140
+ }
141
+ function clearInstances() {
142
+ const remaining = [...instances.values()];
143
+ instances.clear();
144
+ roots.clear();
145
+ publish();
146
+ for (const instance of remaining) {
147
+ instance.children.clear();
148
+ if (instance.phase !== "active") continue;
149
+ instance.phase = "waiting";
150
+ instance.resolveResult({ status: "dismissed" });
151
+ }
152
+ }
153
+ function attachProvider() {
154
+ if (providerAttached) throw new Error("A modal registry can only have one mounted Provider.");
155
+ providerAttached = true;
156
+ return () => {
157
+ providerAttached = false;
158
+ queueMicrotask(() => {
159
+ if (!providerAttached) clearInstances();
160
+ });
161
+ };
162
+ }
163
+ function Provider({ children }) {
164
+ const views = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
165
+ useEffect(attachProvider, []);
166
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [children, views.map((view) => /* @__PURE__ */ jsx(ModalEntry, {
167
+ view,
168
+ onPresent,
169
+ onError
170
+ }, view.id))] });
171
+ }
172
+ function define(root, component) {
173
+ function ModalRoot() {
174
+ return null;
175
+ }
176
+ const definitionRecord = {
177
+ root,
178
+ component
179
+ };
180
+ const definition = Object.assign(ModalRoot, { show: ((props, showOptions) => open(definitionRecord, props, showOptions)) });
181
+ const hotName = options?.hot ? component.displayName ?? component.name : void 0;
182
+ if (hotDefinitions && hotName) {
183
+ const hotKey = `${options?.key}:${hotName}`;
184
+ const previous = hotDefinitions.get(hotKey);
185
+ if (previous) for (const instance of instances.values()) {
186
+ if (instance.definition !== previous || instance.phase !== "active") continue;
187
+ instance.definition = definitionRecord;
188
+ instance.presented = false;
189
+ }
190
+ hotDefinitions.set(hotKey, definitionRecord);
191
+ publish();
192
+ }
193
+ return definition;
194
+ }
195
+ function open(definition, props = {}, options) {
196
+ let parentId = null;
197
+ if (options?.parent) {
198
+ const id = callIds.get(options.parent);
199
+ if (id === void 0) throw new Error("The parent modal call belongs to another registry or is no longer active.");
200
+ const parent = instances.get(id);
201
+ if (!parent || parent.phase !== "active") throw new Error("A child call cannot be opened from a settled parent.");
202
+ parentId = id;
203
+ }
204
+ const id = nextId++;
205
+ let resolveResult;
206
+ let rejectResult;
207
+ const result = new Promise((resolve, reject) => {
208
+ resolveResult = resolve;
209
+ rejectResult = reject;
210
+ });
211
+ let instance;
212
+ const call = Object.assign(result, {
213
+ type: "modal-call",
214
+ update: (patch) => {
215
+ if (instance.phase !== "active") return;
216
+ instance.props = {
217
+ ...instance.props,
218
+ ...patch
219
+ };
220
+ publish();
221
+ },
222
+ resolve: (value) => finish(instance, {
223
+ status: "resolved",
224
+ value
225
+ }),
226
+ dismiss: () => finish(instance, { status: "dismissed" })
227
+ });
228
+ instance = {
229
+ id,
230
+ definition,
231
+ parentId,
232
+ children: /* @__PURE__ */ new Set(),
233
+ props,
234
+ phase: "active",
235
+ presented: false,
236
+ resolveResult,
237
+ rejectResult,
238
+ call,
239
+ modal: {
240
+ call,
241
+ resolve: call.resolve,
242
+ dismiss: call.dismiss
243
+ }
244
+ };
245
+ instances.set(id, instance);
246
+ if (parentId === null) roots.add(instance);
247
+ else instances.get(parentId)?.children.add(instance);
248
+ callIds.set(call, id);
249
+ publish();
250
+ return call;
251
+ }
252
+ function dismissAll() {
253
+ for (const instance of [...roots].reverse()) finish(instance, { status: "dismissed" });
254
+ }
255
+ const registry = {
256
+ Provider,
257
+ define,
258
+ dismissAll
259
+ };
260
+ if (cache && options) cache.set(options.key, registry);
261
+ return registry;
262
+ }
263
+ export { createModalRegistry };
@@ -0,0 +1,75 @@
1
+ import { ComponentType, FunctionComponent, ReactNode } from "react";
2
+ /** Props the registry injects into a Base UI Dialog, AlertDialog, Drawer, or Sheet root. */
3
+ export interface ModalRootProps {
4
+ open: boolean;
5
+ onOpenChange: (open: boolean, details?: unknown) => void;
6
+ onOpenChangeComplete: (open: boolean) => void;
7
+ }
8
+ /** A Base UI root component controlled by the registry. */
9
+ export type ModalRootComponent = ComponentType<ModalRootProps & {
10
+ children?: ReactNode;
11
+ }>;
12
+ /** Per-call controls exposed to the modal component. */
13
+ export interface ModalController<Props extends object = {}, Output = void> {
14
+ /** The current call, suitable for `show(..., { parent: modal.call })`. */
15
+ readonly call: ModalCall<Props, Output>;
16
+ /** Resolve the result and start closing. The first result or dismissal wins. */
17
+ resolve: (value: Output) => void;
18
+ /** Dismiss normally, without rejecting the returned promise. */
19
+ dismiss: () => void;
20
+ }
21
+ /** Props for a modal component. */
22
+ export type Modal<Props extends object = {}, Output = void> = Props & {
23
+ modal: ModalController<Props, Output>;
24
+ };
25
+ export type ModalResult<Output> = {
26
+ status: "resolved";
27
+ value: Output;
28
+ } | {
29
+ status: "dismissed";
30
+ };
31
+ /** A call handle usable as another call's `parent`. */
32
+ export type ModalHandle = {
33
+ readonly type: "modal-call";
34
+ };
35
+ export interface ModalCall<Props, Output> extends Promise<ModalResult<Output>>, ModalHandle {
36
+ /** Shallow-merge new props into this call while it is still active. */
37
+ update: (patch: Partial<Props>) => void;
38
+ /** Resolve this call and start closing. */
39
+ resolve: (value: Output) => void;
40
+ /** Dismiss this call and its child calls. */
41
+ dismiss: () => void;
42
+ }
43
+ /** A callable definition doubles as a Fast Refresh-compatible component handle. */
44
+ export type ModalDefinition<Props extends object = {}, Output = void> = FunctionComponent<{}> & {
45
+ /** Open this definition through its owning registry. */
46
+ readonly show: ModalShow<Props, Output>;
47
+ };
48
+ export interface ModalShowOptions {
49
+ /** Render this call inside the parent call's root. */
50
+ parent?: ModalHandle;
51
+ }
52
+ type ModalArguments<Props> = {} extends Props ? [props?: Props] : [props: Props];
53
+ interface ModalShow<Props, Output> {
54
+ (...args: ModalArguments<Props>): ModalCall<Props, Output>;
55
+ (props: Props, options: ModalShowOptions): ModalCall<Props, Output>;
56
+ }
57
+ export interface ModalRegistry {
58
+ /** Mount once around the application. Unmounting dismisses pending calls. */
59
+ Provider: ComponentType<{
60
+ children?: ReactNode;
61
+ }>;
62
+ /** Register a modal component. The registry controls the root and injects child calls through its children. */
63
+ define<Props extends object, Output = void>(root: ModalRootComponent, component: ComponentType<Modal<Props, Output>>): ModalDefinition<Props, Output>;
64
+ /** Dismiss every active call, waiting for children to exit before their parents. */
65
+ dismissAll: () => void;
66
+ }
67
+ export interface ModalRegistryOptions {
68
+ /** The registration module's `import.meta.hot`. */
69
+ hot?: {
70
+ data: Record<string, unknown>;
71
+ };
72
+ /** Stable across updates and unique among registries in the same module. */
73
+ key: string;
74
+ }
75
+ export {};
package/dist/modal.d.mts CHANGED
@@ -1,3 +1,3 @@
1
- import { ModalController, ModalInput, ModalManager, ModalManagerOptions, ModalProps, ModalRegistry, ModalResult, ModalRootProps, ModalValue } from "./modal-manager/types.mjs";
2
- import { createModalManager } from "./modal-manager/manager.mjs";
3
- export { type ModalController, type ModalInput, type ModalManager, type ModalManagerOptions, type ModalProps, type ModalRegistry, type ModalResult, type ModalRootProps, type ModalValue, createModalManager };
1
+ import { Modal, ModalCall, ModalController, ModalDefinition, ModalHandle, ModalRegistry, ModalRegistryOptions, ModalResult, ModalRootComponent, ModalRootProps, ModalShowOptions } from "./modal-registry/types.mjs";
2
+ import { createModalRegistry } from "./modal-registry/registry.mjs";
3
+ export { type Modal, type ModalCall, type ModalController, type ModalDefinition, type ModalHandle, type ModalRegistry, type ModalRegistryOptions, type ModalResult, type ModalRootComponent, type ModalRootProps, type ModalShowOptions, createModalRegistry };
package/dist/modal.mjs CHANGED
@@ -1 +1 @@
1
- export { createModalManager } from "./modal-manager/manager.mjs";
1
+ export { createModalRegistry } from "./modal-registry/registry.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deftui/addons",
3
- "version": "0.1.6",
3
+ "version": "0.2.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/KiriRainCat/deftui.git",
@@ -40,7 +40,7 @@
40
40
  "fix": "vp check --fix"
41
41
  },
42
42
  "devDependencies": {
43
- "@deftui/react": "^0.1.6",
43
+ "@deftui/react": "^0.2.0",
44
44
  "@tanstack/react-router": "^1.170.46",
45
45
  "@types/react": "^19.3.0",
46
46
  "@types/react-dom": "^19.3.0",
@@ -49,7 +49,7 @@
49
49
  "vite-plus": "catalog:"
50
50
  },
51
51
  "peerDependencies": {
52
- "@deftui/react": "^0.1.6",
52
+ "@deftui/react": "^0.2.0",
53
53
  "@tanstack/react-router": "^1.170.0",
54
54
  "react": "^19.0.0",
55
55
  "react-dom": "^19.0.0",
@@ -1,15 +0,0 @@
1
- import { ModalManager, ModalManagerOptions, ModalProps, ModalRegistry } from "./types.mjs";
2
- import { ComponentType } from "react";
3
- /**
4
- * Create once outside rendering; accepts regular, memo, and lazy components.
5
- *
6
- * Call `open` on the client, outside rendering. The Host renders nothing during SSR.
7
- *
8
- * Pass the caller's `hot` and a stable `key` to retain instances and promises during HMR.
9
- * Reload the page when the manager implementation changes.
10
- *
11
- * @example
12
- * export const modals = createModalManager({ edit: EditModal }, { hot: import.meta.hot, key: "modals" });
13
- * if (import.meta.hot) import.meta.hot.accept();
14
- */
15
- export declare function createModalManager<Registry extends ModalRegistry>(registry: Registry & { [Key in keyof Registry]: Registry[Key] extends ComponentType<infer Props> ? (Props extends ModalProps<never> ? unknown : never) : never; }, options?: ModalManagerOptions): ModalManager<Registry>;
@@ -1,177 +0,0 @@
1
- "use client";
2
- import { ModalEntry } from "./host.mjs";
3
- import { useEffect, useSyncExternalStore } from "react";
4
- import { jsx } from "react/jsx-runtime";
5
- // src/modal-manager/manager.tsx
6
- const EMPTY = [];
7
- /**
8
- * Create once outside rendering; accepts regular, memo, and lazy components.
9
- *
10
- * Call `open` on the client, outside rendering. The Host renders nothing during SSR.
11
- *
12
- * Pass the caller's `hot` and a stable `key` to retain instances and promises during HMR.
13
- * Reload the page when the manager implementation changes.
14
- *
15
- * @example
16
- * export const modals = createModalManager({ edit: EditModal }, { hot: import.meta.hot, key: "modals" });
17
- * if (import.meta.hot) import.meta.hot.accept();
18
- */
19
- function createModalManager(registry, options) {
20
- const cache = options?.hot ? options.hot.data.__deftuiModalManagers ??= /* @__PURE__ */ new Map() : void 0;
21
- const cached = options && cache?.get(options.key);
22
- if (cached) {
23
- cached.setRegistry(registry);
24
- return cached;
25
- }
26
- let definitions = registry;
27
- let snapshot = EMPTY;
28
- let nextId = 0;
29
- let hostAttached = false;
30
- const instances = /* @__PURE__ */ new Map();
31
- const listeners = /* @__PURE__ */ new Set();
32
- function publish() {
33
- snapshot = Array.from(instances.values(), (instance) => instance.view);
34
- listeners.forEach((listener) => listener());
35
- }
36
- function subscribe(listener) {
37
- listeners.add(listener);
38
- return () => {
39
- listeners.delete(listener);
40
- };
41
- }
42
- function getSnapshot() {
43
- return snapshot;
44
- }
45
- function getServerSnapshot() {
46
- return EMPTY;
47
- }
48
- function remove(id) {
49
- if (instances.delete(id)) publish();
50
- }
51
- function finish(instance, result) {
52
- if (instance.settled) return;
53
- instance.settled = true;
54
- if (instance.presented) {
55
- instance.view = {
56
- ...instance.view,
57
- modal: {
58
- ...instance.view.modal,
59
- rootProps: {
60
- ...instance.view.modal.rootProps,
61
- open: false
62
- }
63
- }
64
- };
65
- publish();
66
- } else remove(instance.view.id);
67
- instance.resolve(result);
68
- }
69
- function onPresent(id, component) {
70
- const instance = instances.get(id);
71
- if (instance?.view.component === component) instance.presented = true;
72
- }
73
- function onError(id, error) {
74
- const instance = instances.get(id);
75
- if (!instance) return;
76
- remove(id);
77
- if (!instance.settled) {
78
- instance.settled = true;
79
- instance.reject(error);
80
- }
81
- }
82
- function attachHost() {
83
- if (hostAttached) throw new Error("A modal manager can only have one mounted Host.");
84
- hostAttached = true;
85
- return () => {
86
- hostAttached = false;
87
- queueMicrotask(() => {
88
- if (hostAttached) return;
89
- const remaining = Array.from(instances.values());
90
- instances.clear();
91
- publish();
92
- for (const instance of remaining) {
93
- if (instance.settled) continue;
94
- instance.settled = true;
95
- instance.resolve({ status: "dismissed" });
96
- }
97
- });
98
- };
99
- }
100
- function Host() {
101
- const views = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
102
- useEffect(attachHost, []);
103
- return views.map((view) => /* @__PURE__ */ jsx(ModalEntry, {
104
- view,
105
- onPresent,
106
- onError
107
- }, view.id));
108
- }
109
- function open(key, props = {}) {
110
- const definition = definitions[key];
111
- if (!definition) throw new Error(`Unknown modal: ${key}`);
112
- return new Promise((resolve, reject) => {
113
- const id = nextId++;
114
- const instance = {
115
- key,
116
- presented: false,
117
- settled: false,
118
- resolve,
119
- reject,
120
- view: {
121
- id,
122
- component: definition,
123
- props,
124
- modal: {
125
- resolve: (value) => finish(instance, {
126
- status: "resolved",
127
- value
128
- }),
129
- dismiss: () => finish(instance, { status: "dismissed" }),
130
- rootProps: {
131
- open: true,
132
- onOpenChange: (isOpen) => {
133
- if (!isOpen) finish(instance, { status: "dismissed" });
134
- },
135
- onOpenChangeComplete: (isOpen) => {
136
- if (!isOpen && instance.settled) remove(id);
137
- }
138
- }
139
- }
140
- }
141
- };
142
- instances.set(id, instance);
143
- publish();
144
- });
145
- }
146
- function dismissAll() {
147
- for (const instance of instances.values()) finish(instance, { status: "dismissed" });
148
- }
149
- function setRegistry(nextRegistry) {
150
- definitions = nextRegistry;
151
- for (const instance of instances.values()) {
152
- if (instance.settled) continue;
153
- const definition = definitions[instance.key];
154
- if (!definition) {
155
- finish(instance, { status: "dismissed" });
156
- continue;
157
- }
158
- const component = definition;
159
- if (component === instance.view.component) continue;
160
- instance.presented = false;
161
- instance.view = {
162
- ...instance.view,
163
- component
164
- };
165
- }
166
- publish();
167
- }
168
- const manager = {
169
- Host,
170
- open,
171
- dismissAll,
172
- setRegistry
173
- };
174
- if (options) cache?.set(options.key, manager);
175
- return manager;
176
- }
177
- export { createModalManager };
@@ -1,55 +0,0 @@
1
- import { ComponentType } from "react";
2
- /** Spread onto a controlled Dialog, AlertDialog, or Sheet root. */
3
- export interface ModalRootProps {
4
- open: boolean;
5
- onOpenChange: (open: boolean) => void;
6
- onOpenChangeComplete: (open: boolean) => void;
7
- }
8
- export interface ModalController<Value> {
9
- /** Spread all three props onto the Root; forward callbacks when overriding them. */
10
- rootProps: ModalRootProps;
11
- /** Resolve the result and start closing. The first resolution or dismissal wins. */
12
- resolve: (value: Value) => void;
13
- /** Dismiss normally, without rejecting the returned promise. */
14
- dismiss: () => void;
15
- }
16
- /** Intersect with the business component's props. The manager supplies `modal`. */
17
- export interface ModalProps<Value = void> {
18
- modal: ModalController<Value>;
19
- }
20
- export type ModalRegistry = Record<string, ComponentType<never>>;
21
- type ModalComponentProps<T extends ComponentType<never>> = T extends ComponentType<infer Props> ? Props : never;
22
- type ModalArguments<Props> = {} extends Props ? [props?: Props] : [props: Props];
23
- export type ModalInput<T extends ComponentType<never>> = Omit<ModalComponentProps<T>, "modal">;
24
- export type ModalValue<T extends ComponentType<never>> = ModalComponentProps<T> extends ModalProps<infer Value> ? Value : never;
25
- export type ModalResult<Value> = {
26
- status: "resolved";
27
- value: Value;
28
- } | {
29
- status: "dismissed";
30
- };
31
- export interface ModalManagerOptions {
32
- /** The registration module's `import.meta.hot`. No HMR callbacks are registered. */
33
- hot?: {
34
- data: Record<string, unknown>;
35
- };
36
- /** Stable across updates and unique among managers in the same module. */
37
- key: string;
38
- }
39
- export interface ModalManager<Registry extends ModalRegistry> {
40
- /** Mount once inside the required contexts. Unmounting dismisses pending calls. */
41
- Host: ComponentType;
42
- /**
43
- * Each call creates an independent instance.
44
- *
45
- * Resolves when closing starts; unmounts after exit.
46
- *
47
- * Loading/rendering failures reject; normal dismissal resolves with `status: "dismissed"`.
48
- */
49
- open: <Key extends Extract<keyof Registry, string>>(key: Key, ...args: ModalArguments<ModalInput<Registry[Key]>>) => Promise<ModalResult<ModalValue<Registry[Key]>>>;
50
- /** Dismiss all current instances, preserving their exit animations. */
51
- dismissAll: () => void;
52
- /** Replace definitions, retaining promises. Removed entries dismiss; new components may reset local state. */
53
- setRegistry: (registry: Registry) => void;
54
- }
55
- export {};