@deftui/addons 0.1.7 → 0.2.1
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/form.d.mts +6 -0
- package/dist/form.mjs +7 -1
- package/dist/{modal-manager/host.mjs → modal-registry/provider.mjs} +20 -12
- package/dist/modal-registry/registry.d.mts +16 -0
- package/dist/modal-registry/registry.mjs +263 -0
- package/dist/modal-registry/types.d.mts +75 -0
- package/dist/modal.d.mts +3 -3
- package/dist/modal.mjs +1 -1
- package/dist/react-hook-form/widgets/combobox.d.mts +9 -0
- package/dist/react-hook-form/widgets/combobox.mjs +21 -0
- package/dist/react-hook-form/widgets/select.d.mts +8 -0
- package/dist/react-hook-form/widgets/select.mjs +20 -0
- package/dist/react-hook-form/widgets/types.d.mts +5 -0
- package/package.json +3 -3
- package/dist/modal-manager/manager.d.mts +0 -15
- package/dist/modal-manager/manager.mjs +0 -177
- package/dist/modal-manager/types.d.mts +0 -55
package/dist/form.d.mts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { RHFField } from "./react-hook-form/field.mjs";
|
|
2
2
|
import { RHFForm } from "./react-hook-form/form.mjs";
|
|
3
|
+
import { RHFCombobox } from "./react-hook-form/widgets/combobox.mjs";
|
|
4
|
+
import { RHFSelect } from "./react-hook-form/widgets/select.mjs";
|
|
3
5
|
export declare const RHF: {
|
|
4
6
|
/** A Base UI form with a default grid layout; submission stays controlled by the caller. */
|
|
5
7
|
Form: typeof RHFForm;
|
|
6
8
|
/** Wraps `useController` with label, description, error, and control-prop forwarding. */
|
|
7
9
|
Field: typeof RHFField;
|
|
10
|
+
/** One-liner component for Select, used under {@link RHF.Field}. */
|
|
11
|
+
Select: typeof RHFSelect;
|
|
12
|
+
/** One-liner component for Combobox, used under {@link RHF.Field}. */
|
|
13
|
+
Combobox: typeof RHFCombobox;
|
|
8
14
|
};
|
package/dist/form.mjs
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { RHFField } from "./react-hook-form/field.mjs";
|
|
2
2
|
import { RHFForm } from "./react-hook-form/form.mjs";
|
|
3
|
+
import { RHFCombobox } from "./react-hook-form/widgets/combobox.mjs";
|
|
4
|
+
import { RHFSelect } from "./react-hook-form/widgets/select.mjs";
|
|
3
5
|
// src/react-hook-form/index.ts
|
|
4
6
|
const RHF = {
|
|
5
7
|
/** A Base UI form with a default grid layout; submission stays controlled by the caller. */
|
|
6
8
|
Form: RHFForm,
|
|
7
9
|
/** Wraps `useController` with label, description, error, and control-prop forwarding. */
|
|
8
|
-
Field: RHFField
|
|
10
|
+
Field: RHFField,
|
|
11
|
+
/** One-liner component for Select, used under {@link RHF.Field}. */
|
|
12
|
+
Select: RHFSelect,
|
|
13
|
+
/** One-liner component for Combobox, used under {@link RHF.Field}. */
|
|
14
|
+
Combobox: RHFCombobox
|
|
9
15
|
};
|
|
10
16
|
export { RHF };
|
|
@@ -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-
|
|
5
|
-
function ModalRenderer({ view, onPresent }) {
|
|
6
|
-
useLayoutEffect(() => onPresent(view.id, view.
|
|
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.
|
|
8
|
+
view.definition
|
|
10
9
|
]);
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
export { type
|
|
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 {
|
|
1
|
+
export { createModalRegistry } from "./modal-registry/registry.mjs";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Options } from "./types.mjs";
|
|
2
|
+
import { Combobox } from "@deftui/react";
|
|
3
|
+
import { ComponentProps } from "react";
|
|
4
|
+
type Props<V extends string> = {
|
|
5
|
+
options: Options<V>;
|
|
6
|
+
emptyLabel?: string;
|
|
7
|
+
} & Omit<ComponentProps<typeof Combobox>, "items" | "children"> & Pick<ComponentProps<typeof Combobox.Input>, "size" | "placeholder" | "showClear" | "className">;
|
|
8
|
+
export declare function RHFCombobox<V extends string>(props: Props<V>): import("react").JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Combobox } from "@deftui/react";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
// src/react-hook-form/widgets/combobox.tsx
|
|
4
|
+
function RHFCombobox(props) {
|
|
5
|
+
const { options, size, placeholder, showClear, className, emptyLabel, ...rootProps } = props;
|
|
6
|
+
return /* @__PURE__ */ jsxs(Combobox, {
|
|
7
|
+
...rootProps,
|
|
8
|
+
items: options,
|
|
9
|
+
children: [/* @__PURE__ */ jsx(Combobox.Input, {
|
|
10
|
+
size,
|
|
11
|
+
placeholder,
|
|
12
|
+
showClear,
|
|
13
|
+
className
|
|
14
|
+
}), /* @__PURE__ */ jsxs(Combobox.Content, { children: [/* @__PURE__ */ jsx(Combobox.Empty, { children: emptyLabel }), /* @__PURE__ */ jsx(Combobox.List, { children: options.map((option) => /* @__PURE__ */ jsx(Combobox.Item, {
|
|
15
|
+
value: option.value,
|
|
16
|
+
disabled: option.disabled,
|
|
17
|
+
children: option.label
|
|
18
|
+
}, option.value)) })] })]
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
export { RHFCombobox };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Options } from "./types.mjs";
|
|
2
|
+
import { Select } from "@deftui/react";
|
|
3
|
+
import { ComponentProps } from "react";
|
|
4
|
+
type Props<V extends string> = {
|
|
5
|
+
options: Options<V>;
|
|
6
|
+
} & Omit<ComponentProps<typeof Select>, "items" | "children"> & Pick<ComponentProps<typeof Select.Trigger>, "size" | "className"> & Pick<ComponentProps<typeof Select.Value>, "placeholder" | "className">;
|
|
7
|
+
export declare function RHFSelect<V extends string>(props: Props<V>): import("react").JSX.Element;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Select } from "@deftui/react";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
// src/react-hook-form/widgets/select.tsx
|
|
4
|
+
function RHFSelect(props) {
|
|
5
|
+
const { options, size, placeholder, className, ...rootProps } = props;
|
|
6
|
+
return /* @__PURE__ */ jsxs(Select, {
|
|
7
|
+
...rootProps,
|
|
8
|
+
items: options,
|
|
9
|
+
children: [/* @__PURE__ */ jsx(Select.Trigger, {
|
|
10
|
+
size,
|
|
11
|
+
className,
|
|
12
|
+
children: /* @__PURE__ */ jsx(Select.Value, { placeholder })
|
|
13
|
+
}), /* @__PURE__ */ jsx(Select.Content, { children: options.map((option) => /* @__PURE__ */ jsx(Select.Item, {
|
|
14
|
+
value: option.value,
|
|
15
|
+
disabled: option.disabled,
|
|
16
|
+
children: option.label
|
|
17
|
+
}, option.value)) })]
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export { RHFSelect };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deftui/addons",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
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
|
|
43
|
+
"@deftui/react": "^0.2.1",
|
|
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
|
|
52
|
+
"@deftui/react": "^0.2.1",
|
|
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 {};
|