@pactor-app/ui 0.1.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.
@@ -0,0 +1,536 @@
1
+ import {
2
+ Dialog,
3
+ DialogClose,
4
+ DialogContent,
5
+ DialogDescription,
6
+ DialogFooter,
7
+ DialogHeader,
8
+ DialogTitle
9
+ } from "./chunk-QDTVOJ5P.js";
10
+ import {
11
+ Button
12
+ } from "./chunk-QWZ23QLS.js";
13
+ import {
14
+ cn
15
+ } from "./chunk-RQHJBTEU.js";
16
+
17
+ // src/interaction/manager.ts
18
+ var OVERLAY_CANCEL_PAYLOAD = { success: false };
19
+ var OverlayManager = class {
20
+ constructor() {
21
+ this.pending = [];
22
+ this.listeners = /* @__PURE__ */ new Set();
23
+ this.counter = 0;
24
+ /** 缓存的堆栈快照:变化时才更换引用,供 useSyncExternalStore 使用 */
25
+ this.snapshot = Object.freeze([]);
26
+ }
27
+ /**
28
+ * 打开 overlay:入栈、通知,返回 Promise——close 时以 payload
29
+ * resolve;取消关闭(无 payload)resolve `{ success: false }`。
30
+ * `options.parentScope` 记录打开时刻的父作用域浅快照。
31
+ */
32
+ open(config, options = {}) {
33
+ if (!config || config.type !== "modal" && config.type !== "drawer") {
34
+ return Promise.reject(
35
+ new Error(
36
+ '[pactor-interaction] overlay.open \u9700\u8981 type \u4E3A "modal" \u6216 "drawer" \u7684\u914D\u7F6E'
37
+ )
38
+ );
39
+ }
40
+ this.counter += 1;
41
+ const entry = { ...config, id: `overlay-${this.counter}` };
42
+ return new Promise((resolve) => {
43
+ this.pending.push({
44
+ entry,
45
+ parentScope: options.parentScope ? { ...options.parentScope } : void 0,
46
+ resolve
47
+ });
48
+ this.commit();
49
+ });
50
+ }
51
+ /**
52
+ * 关闭 overlay 并以 payload resolve 对应的 open Promise:
53
+ * - `close(id, payload)`:关闭指定条目,原样 resolve payload
54
+ * (payload 缺省为取消语义 `{ success: false }`)
55
+ * - `close(payload)`:关闭栈顶,原样 resolve payload
56
+ * - `close()`:关闭栈顶,取消语义
57
+ * 未命中任何条目时为受控 no-op,不抛错。
58
+ */
59
+ close(idOrPayload, payload) {
60
+ let index;
61
+ let resolvedPayload;
62
+ if (typeof idOrPayload === "string") {
63
+ index = this.pending.findIndex((p) => p.entry.id === idOrPayload);
64
+ if (index === -1) {
65
+ return;
66
+ }
67
+ resolvedPayload = payload;
68
+ } else {
69
+ index = this.pending.length - 1;
70
+ if (index === -1) {
71
+ return;
72
+ }
73
+ resolvedPayload = idOrPayload === void 0 ? payload : idOrPayload;
74
+ }
75
+ const [target] = this.pending.splice(index, 1);
76
+ this.commit();
77
+ target?.resolve(
78
+ resolvedPayload === void 0 ? OVERLAY_CANCEL_PAYLOAD : resolvedPayload
79
+ );
80
+ }
81
+ /** 当前堆栈快照(打开顺序,栈顶在末尾);引用在堆栈不变时保持稳定 */
82
+ stack() {
83
+ return this.snapshot;
84
+ }
85
+ /** 打开时记录的父作用域浅快照(只读约定);未知 id 返回 undefined */
86
+ parentScopeOf(id) {
87
+ return this.pending.find((p) => p.entry.id === id)?.parentScope;
88
+ }
89
+ /** 订阅堆栈变化(open / close 时通知),返回退订函数 */
90
+ subscribe(listener) {
91
+ this.listeners.add(listener);
92
+ return () => {
93
+ this.listeners.delete(listener);
94
+ };
95
+ }
96
+ /** 重建快照并通知订阅者 */
97
+ commit() {
98
+ this.snapshot = Object.freeze(this.pending.map((p) => p.entry));
99
+ for (const listener of [...this.listeners]) {
100
+ listener(this.snapshot);
101
+ }
102
+ }
103
+ };
104
+
105
+ // src/interaction/actions.ts
106
+ import { resolveDeep } from "@pactor-app/runtime";
107
+ function createUiBridge() {
108
+ return {};
109
+ }
110
+ function registerInteractionActions(actions, manager, ui) {
111
+ const register = (name, handler) => {
112
+ if (!actions.has(name)) {
113
+ actions.register(name, handler);
114
+ }
115
+ };
116
+ register("overlay.open", async (action, ctx) => {
117
+ const overlay = action.params?.["overlay"];
118
+ if (!isRecord(overlay)) {
119
+ return failure("[pactor-interaction] overlay.open \u7F3A\u5C11 overlay \u914D\u7F6E\u53C2\u6570");
120
+ }
121
+ const { content, ...rest } = overlay;
122
+ const resolved = resolveDeep(rest, ctx.scope);
123
+ const config = { ...resolved, content };
124
+ const data = await manager.open(config, {
125
+ parentScope: { ...ctx.scope }
126
+ });
127
+ return { ok: true, data };
128
+ });
129
+ register("overlay.close", (action) => {
130
+ const id = action.params?.["id"];
131
+ const payload = action.params?.["payload"];
132
+ if (typeof id === "string" && id) {
133
+ manager.close(id, payload);
134
+ } else if (payload !== void 0) {
135
+ manager.close(payload);
136
+ } else {
137
+ manager.close();
138
+ }
139
+ return { ok: true };
140
+ });
141
+ register("confirm", async (action) => {
142
+ const modal = ui.modal;
143
+ if (!modal) {
144
+ return failure(
145
+ "[pactor-interaction] confirm \u52A8\u4F5C\u9700\u8981 <InteractionProvider> \u6CE8\u5165 modal \u5B9E\u4F8B"
146
+ );
147
+ }
148
+ const params = action.params ?? {};
149
+ const data = await new Promise((resolve) => {
150
+ modal.confirm({
151
+ title: params["title"],
152
+ content: params["content"],
153
+ onOk: () => resolve(true),
154
+ onCancel: () => resolve(false)
155
+ });
156
+ });
157
+ return { ok: true, data };
158
+ });
159
+ register("message", (action) => {
160
+ const message = ui.message;
161
+ if (!message) {
162
+ return failure(
163
+ "[pactor-interaction] message \u52A8\u4F5C\u9700\u8981 <InteractionProvider> \u6CE8\u5165 message \u5B9E\u4F8B"
164
+ );
165
+ }
166
+ const params = action.params ?? {};
167
+ const type = params["type"];
168
+ const text = params["text"];
169
+ if (type === "success" || type === "error" || type === "warning") {
170
+ message[type](text);
171
+ } else {
172
+ message.info(text);
173
+ }
174
+ return { ok: true };
175
+ });
176
+ }
177
+ function failure(message) {
178
+ return { ok: false, error: new Error(message) };
179
+ }
180
+ function isRecord(value) {
181
+ return typeof value === "object" && value !== null && !Array.isArray(value);
182
+ }
183
+
184
+ // src/interaction/provider.tsx
185
+ import { useEffect, useMemo, useState } from "react";
186
+ import { createPortal } from "react-dom";
187
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
188
+ var TOAST_DURATION = 3e3;
189
+ var toastTypeClass = {
190
+ success: "border-l-4 border-l-success",
191
+ error: "border-l-4 border-l-destructive",
192
+ info: "border-l-4 border-l-info",
193
+ warning: "border-l-4 border-l-warning"
194
+ };
195
+ function ShadcnInteractionProvider({
196
+ bridge,
197
+ children
198
+ }) {
199
+ const [confirmState, setConfirmState] = useState(null);
200
+ const [toasts, setToasts] = useState([]);
201
+ const { modal, message } = useMemo(() => {
202
+ const pushToast = (type, content) => {
203
+ const id = Date.now() + Math.random();
204
+ setToasts((prev) => [...prev, { id, type, content }]);
205
+ setTimeout(() => {
206
+ setToasts((prev) => prev.filter((toast) => toast.id !== id));
207
+ }, TOAST_DURATION);
208
+ };
209
+ const modalImpl = {
210
+ confirm(config) {
211
+ setConfirmState({ ...config });
212
+ }
213
+ };
214
+ const messageImpl = {
215
+ success: (content) => pushToast("success", content),
216
+ error: (content) => pushToast("error", content),
217
+ info: (content) => pushToast("info", content),
218
+ warning: (content) => pushToast("warning", content)
219
+ };
220
+ return { modal: modalImpl, message: messageImpl };
221
+ }, []);
222
+ useEffect(() => {
223
+ bridge.modal = modal;
224
+ bridge.message = message;
225
+ return () => {
226
+ bridge.modal = void 0;
227
+ bridge.message = void 0;
228
+ };
229
+ }, [bridge, modal, message]);
230
+ const handleOk = () => {
231
+ const state = confirmState;
232
+ setConfirmState(null);
233
+ state?.onOk?.();
234
+ };
235
+ const handleCancel = () => {
236
+ const state = confirmState;
237
+ setConfirmState(null);
238
+ state?.onCancel?.();
239
+ };
240
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
241
+ children,
242
+ typeof document !== "undefined" && createPortal(
243
+ /* @__PURE__ */ jsxs(Fragment, { children: [
244
+ /* @__PURE__ */ jsx(
245
+ Dialog,
246
+ {
247
+ open: confirmState !== null,
248
+ onOpenChange: (open) => {
249
+ if (!open) {
250
+ handleCancel();
251
+ }
252
+ },
253
+ children: /* @__PURE__ */ jsxs(DialogContent, { "data-slot": "confirm-dialog", className: "sm:max-w-sm", children: [
254
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [
255
+ /* @__PURE__ */ jsx(DialogTitle, { children: confirmState?.title ?? "" }),
256
+ confirmState?.content !== void 0 && /* @__PURE__ */ jsx(
257
+ DialogDescription,
258
+ {
259
+ render: /* @__PURE__ */ jsx("div", { children: confirmState.content })
260
+ }
261
+ )
262
+ ] }),
263
+ /* @__PURE__ */ jsxs(DialogFooter, { children: [
264
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", onClick: handleCancel, children: "\u53D6\u6D88" }),
265
+ /* @__PURE__ */ jsx(Button, { type: "button", onClick: handleOk, children: "\u786E\u5B9A" })
266
+ ] })
267
+ ] })
268
+ }
269
+ ),
270
+ toasts.length > 0 && /* @__PURE__ */ jsx(
271
+ "div",
272
+ {
273
+ "data-slot": "toast-container",
274
+ className: "fixed top-4 right-4 z-[100] flex w-80 flex-col gap-2",
275
+ children: toasts.map((toast) => /* @__PURE__ */ jsx(
276
+ "div",
277
+ {
278
+ "data-slot": "toast",
279
+ "data-type": toast.type,
280
+ role: "status",
281
+ className: cn(
282
+ "rounded-md border border-border bg-popover px-4 py-3 text-sm text-popover-foreground shadow-md",
283
+ toastTypeClass[toast.type]
284
+ ),
285
+ children: toast.content
286
+ },
287
+ toast.id
288
+ ))
289
+ }
290
+ )
291
+ ] }),
292
+ document.body
293
+ )
294
+ ] });
295
+ }
296
+
297
+ // src/interaction/host.tsx
298
+ import { XIcon } from "lucide-react";
299
+ import {
300
+ createPageRuntime,
301
+ PageRuntimeView,
302
+ useRenderer
303
+ } from "@pactor-app/runtime";
304
+ import { useEffect as useEffect2, useState as useState2, useSyncExternalStore } from "react";
305
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
306
+ function toCssWidth(width) {
307
+ if (width === void 0) {
308
+ return void 0;
309
+ }
310
+ return typeof width === "number" ? `${width}px` : width;
311
+ }
312
+ function ShadcnOverlayHost({ manager, resolvePage }) {
313
+ const renderer = useRenderer();
314
+ const stack = useSyncExternalStore(
315
+ (onChange) => manager.subscribe(onChange),
316
+ () => manager.stack()
317
+ );
318
+ useSyncExternalStore(
319
+ (onChange) => {
320
+ const offState = renderer.context.state.subscribe(onChange);
321
+ const offData = renderer.context.data.subscribe(onChange);
322
+ return () => {
323
+ offState();
324
+ offData();
325
+ };
326
+ },
327
+ () => `${renderer.context.state.getVersion()}:${renderer.context.data.getVersion()}`
328
+ );
329
+ return /* @__PURE__ */ jsx2(Fragment2, { children: stack.map((entry) => /* @__PURE__ */ jsx2(
330
+ OverlayEntryView,
331
+ {
332
+ entry,
333
+ manager,
334
+ renderer,
335
+ resolvePage
336
+ },
337
+ entry.id
338
+ )) });
339
+ }
340
+ function OverlayEntryView({
341
+ entry,
342
+ manager,
343
+ renderer,
344
+ resolvePage
345
+ }) {
346
+ const handleCancel = () => manager.close(entry.id);
347
+ let body = null;
348
+ if (entry.content) {
349
+ body = renderer.renderChildren([entry.content], {
350
+ params: entry.params ?? {},
351
+ parent: manager.parentScopeOf(entry.id)
352
+ });
353
+ } else if (entry.page) {
354
+ body = /* @__PURE__ */ jsx2(
355
+ PageOverlayBody,
356
+ {
357
+ entry,
358
+ renderer,
359
+ resolvePage
360
+ }
361
+ );
362
+ }
363
+ if (entry.type === "drawer") {
364
+ return /* @__PURE__ */ jsx2(ShadcnDrawerView, { entry, onCancel: handleCancel, children: body });
365
+ }
366
+ return /* @__PURE__ */ jsx2(
367
+ Dialog,
368
+ {
369
+ open: true,
370
+ onOpenChange: (open) => {
371
+ if (!open) {
372
+ handleCancel();
373
+ }
374
+ },
375
+ children: /* @__PURE__ */ jsxs2(
376
+ DialogContent,
377
+ {
378
+ "data-slot": "overlay-modal",
379
+ className: cn(entry.width !== void 0 && "sm:max-w-none"),
380
+ style: entry.width === void 0 ? void 0 : { width: toCssWidth(entry.width) },
381
+ showCloseButton: false,
382
+ children: [
383
+ /* @__PURE__ */ jsxs2(DialogHeader, { children: [
384
+ /* @__PURE__ */ jsx2(DialogTitle, { children: entry.title ?? "" }),
385
+ /* @__PURE__ */ jsx2(
386
+ DialogClose,
387
+ {
388
+ render: /* @__PURE__ */ jsx2(
389
+ "button",
390
+ {
391
+ type: "button",
392
+ "data-slot": "overlay-modal-close",
393
+ "aria-label": "\u5173\u95ED",
394
+ className: "absolute top-4 right-4 cursor-pointer rounded-xs opacity-70 transition-opacity hover:opacity-100 focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none",
395
+ children: /* @__PURE__ */ jsx2(XIcon, { className: "size-4" })
396
+ }
397
+ )
398
+ }
399
+ )
400
+ ] }),
401
+ /* @__PURE__ */ jsx2("div", { "data-slot": "overlay-modal-body", children: body })
402
+ ]
403
+ }
404
+ )
405
+ }
406
+ );
407
+ }
408
+ function ShadcnDrawerView({
409
+ entry,
410
+ onCancel,
411
+ children
412
+ }) {
413
+ useEffect2(() => {
414
+ const handleKeyDown = (event) => {
415
+ if (event.key === "Escape") {
416
+ onCancel();
417
+ }
418
+ };
419
+ document.addEventListener("keydown", handleKeyDown);
420
+ return () => {
421
+ document.removeEventListener("keydown", handleKeyDown);
422
+ };
423
+ }, [onCancel]);
424
+ const panelStyle = entry.width === void 0 ? void 0 : { width: toCssWidth(entry.width) };
425
+ return /* @__PURE__ */ jsxs2("div", { "data-slot": "drawer-root", children: [
426
+ /* @__PURE__ */ jsx2(
427
+ "div",
428
+ {
429
+ "data-slot": "drawer-mask",
430
+ "data-testid": "drawer-mask",
431
+ className: "fixed inset-0 z-50 bg-black/50",
432
+ onClick: onCancel
433
+ }
434
+ ),
435
+ /* @__PURE__ */ jsxs2(
436
+ "div",
437
+ {
438
+ "data-slot": "drawer-panel",
439
+ role: "dialog",
440
+ "aria-modal": "true",
441
+ "aria-label": entry.title,
442
+ style: panelStyle,
443
+ className: "fixed inset-y-0 right-0 z-50 flex w-96 max-w-full flex-col border-l border-border bg-background shadow-lg",
444
+ children: [
445
+ /* @__PURE__ */ jsxs2(
446
+ "div",
447
+ {
448
+ "data-slot": "drawer-header",
449
+ className: "flex items-center justify-between border-b border-border px-6 py-4",
450
+ children: [
451
+ /* @__PURE__ */ jsx2("span", { className: "text-lg font-semibold", children: entry.title ?? "" }),
452
+ /* @__PURE__ */ jsx2(
453
+ "button",
454
+ {
455
+ type: "button",
456
+ "data-slot": "drawer-close",
457
+ "aria-label": "\u5173\u95ED",
458
+ className: "cursor-pointer rounded-xs opacity-70 transition-opacity hover:opacity-100 focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none",
459
+ onClick: onCancel,
460
+ children: /* @__PURE__ */ jsx2(XIcon, { className: "size-4" })
461
+ }
462
+ )
463
+ ]
464
+ }
465
+ ),
466
+ /* @__PURE__ */ jsx2("div", { "data-slot": "drawer-body", className: "flex-1 overflow-auto p-6", children })
467
+ ]
468
+ }
469
+ )
470
+ ] });
471
+ }
472
+ function PageOverlayBody({
473
+ entry,
474
+ renderer,
475
+ resolvePage
476
+ }) {
477
+ const [child, setChild] = useState2(null);
478
+ const [error, setError] = useState2(null);
479
+ useEffect2(() => {
480
+ if (!resolvePage) {
481
+ setError(
482
+ new Error(
483
+ `[@pactor-app/ui] page overlay\uFF08pageId: "${entry.page?.pageId}"\uFF09\u9700\u8981 ShadcnOverlayHost \u6CE8\u5165 resolvePage`
484
+ )
485
+ );
486
+ return;
487
+ }
488
+ let cancelled = false;
489
+ let created = null;
490
+ void (async () => {
491
+ try {
492
+ const pageDsl = await resolvePage(entry.page?.pageId ?? "");
493
+ if (cancelled) {
494
+ return;
495
+ }
496
+ const host = renderer.context;
497
+ created = createPageRuntime(pageDsl, {
498
+ app: host.app,
499
+ user: host.user,
500
+ components: host.components,
501
+ services: host.services,
502
+ actions: host.actions,
503
+ permission: host.permission,
504
+ capabilities: host.capabilities,
505
+ http: host.http,
506
+ route: { params: entry.params ?? {} }
507
+ });
508
+ setChild(created);
509
+ } catch (cause) {
510
+ if (!cancelled) {
511
+ setError(cause instanceof Error ? cause : new Error(String(cause)));
512
+ }
513
+ }
514
+ })();
515
+ return () => {
516
+ cancelled = true;
517
+ void created?.destroy();
518
+ };
519
+ }, [entry.id]);
520
+ if (error) {
521
+ return /* @__PURE__ */ jsx2("div", { role: "alert", children: error.message });
522
+ }
523
+ if (!child) {
524
+ return null;
525
+ }
526
+ return /* @__PURE__ */ jsx2(PageRuntimeView, { runtime: child });
527
+ }
528
+
529
+ export {
530
+ OVERLAY_CANCEL_PAYLOAD,
531
+ OverlayManager,
532
+ createUiBridge,
533
+ registerInteractionActions,
534
+ ShadcnInteractionProvider,
535
+ ShadcnOverlayHost
536
+ };