@stigmer/react 3.12.2 → 3.12.3
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/access/ManageAccessDialog.d.ts.map +1 -1
- package/access/ManageAccessDialog.js +3 -19
- package/access/ManageAccessDialog.js.map +1 -1
- package/agent-instance/CreateAgentInstanceDialog.d.ts.map +1 -1
- package/agent-instance/CreateAgentInstanceDialog.js +7 -20
- package/agent-instance/CreateAgentInstanceDialog.js.map +1 -1
- package/attachment/AttachmentImageLightbox.d.ts.map +1 -1
- package/attachment/AttachmentImageLightbox.js +8 -23
- package/attachment/AttachmentImageLightbox.js.map +1 -1
- package/channel/ChannelConversationsDialog.d.ts.map +1 -1
- package/channel/ChannelConversationsDialog.js +3 -19
- package/channel/ChannelConversationsDialog.js.map +1 -1
- package/channel/ChannelCredentialsDialog.d.ts.map +1 -1
- package/channel/ChannelCredentialsDialog.js +3 -19
- package/channel/ChannelCredentialsDialog.js.map +1 -1
- package/channel/ChannelTemplatesDialog.d.ts.map +1 -1
- package/channel/ChannelTemplatesDialog.js +4 -20
- package/channel/ChannelTemplatesDialog.js.map +1 -1
- package/channel/ConnectSlackDialog.d.ts.map +1 -1
- package/channel/ConnectSlackDialog.js +3 -21
- package/channel/ConnectSlackDialog.js.map +1 -1
- package/channel/ConnectWhatsAppDialog.d.ts.map +1 -1
- package/channel/ConnectWhatsAppDialog.js +3 -21
- package/channel/ConnectWhatsAppDialog.js.map +1 -1
- package/conversation/ConversationAttentionBanner.d.ts +23 -6
- package/conversation/ConversationAttentionBanner.d.ts.map +1 -1
- package/conversation/ConversationAttentionBanner.js +25 -10
- package/conversation/ConversationAttentionBanner.js.map +1 -1
- package/conversation/ConversationListPane.d.ts.map +1 -1
- package/conversation/ConversationListPane.js +25 -6
- package/conversation/ConversationListPane.js.map +1 -1
- package/conversation/ConversationsWorkbench.d.ts.map +1 -1
- package/conversation/ConversationsWorkbench.js +1 -1
- package/conversation/ConversationsWorkbench.js.map +1 -1
- package/execution/ApprovalCard.d.ts.map +1 -1
- package/execution/ApprovalCard.js +13 -3
- package/execution/ApprovalCard.js.map +1 -1
- package/execution/ArtifactPreviewModal.d.ts.map +1 -1
- package/execution/ArtifactPreviewModal.js +11 -17
- package/execution/ArtifactPreviewModal.js.map +1 -1
- package/execution/ToolCallItem.d.ts +3 -1
- package/execution/ToolCallItem.d.ts.map +1 -1
- package/execution/ToolCallItem.js +19 -7
- package/execution/ToolCallItem.js.map +1 -1
- package/execution/index.d.ts +1 -1
- package/execution/index.d.ts.map +1 -1
- package/execution/index.js +1 -1
- package/execution/index.js.map +1 -1
- package/execution/tool-categories.d.ts +51 -0
- package/execution/tool-categories.d.ts.map +1 -1
- package/execution/tool-categories.js +103 -1
- package/execution/tool-categories.js.map +1 -1
- package/execution/tool-presenter.d.ts +15 -0
- package/execution/tool-presenter.d.ts.map +1 -1
- package/execution/tool-presenter.js +7 -2
- package/execution/tool-presenter.js.map +1 -1
- package/index.d.ts +1 -1
- package/index.d.ts.map +1 -1
- package/index.js +1 -1
- package/index.js.map +1 -1
- package/internal/DialogShell.d.ts +77 -0
- package/internal/DialogShell.d.ts.map +1 -0
- package/internal/DialogShell.js +77 -0
- package/internal/DialogShell.js.map +1 -0
- package/manifest/ApplyManifestDialog.d.ts.map +1 -1
- package/manifest/ApplyManifestDialog.js +7 -17
- package/manifest/ApplyManifestDialog.js.map +1 -1
- package/manifest/EditResourceYamlDialog.d.ts.map +1 -1
- package/manifest/EditResourceYamlDialog.js +3 -19
- package/manifest/EditResourceYamlDialog.js.map +1 -1
- package/mcp-server/McpServerConnectDialog.d.ts.map +1 -1
- package/mcp-server/McpServerConnectDialog.js +8 -20
- package/mcp-server/McpServerConnectDialog.js.map +1 -1
- package/mcp-server/McpServerDetailView.d.ts.map +1 -1
- package/mcp-server/McpServerDetailView.js +4 -15
- package/mcp-server/McpServerDetailView.js.map +1 -1
- package/package.json +4 -4
- package/resource-detail/ConfirmDialog.d.ts +2 -3
- package/resource-detail/ConfirmDialog.d.ts.map +1 -1
- package/resource-detail/ConfirmDialog.js +8 -20
- package/resource-detail/ConfirmDialog.js.map +1 -1
- package/sharing/ShareAgentDialog.d.ts.map +1 -1
- package/sharing/ShareAgentDialog.js +2 -20
- package/sharing/ShareAgentDialog.js.map +1 -1
- package/skill/SkillDiffDialog.d.ts.map +1 -1
- package/skill/SkillDiffDialog.js +5 -21
- package/skill/SkillDiffDialog.js.map +1 -1
- package/src/access/ManageAccessDialog.tsx +7 -27
- package/src/agent-instance/CreateAgentInstanceDialog.tsx +14 -27
- package/src/attachment/AttachmentImageLightbox.tsx +13 -36
- package/src/channel/ChannelConversationsDialog.tsx +8 -27
- package/src/channel/ChannelCredentialsDialog.tsx +8 -28
- package/src/channel/ChannelTemplatesDialog.tsx +10 -29
- package/src/channel/ConnectSlackDialog.tsx +8 -30
- package/src/channel/ConnectWhatsAppDialog.tsx +8 -30
- package/src/conversation/ConversationAttentionBanner.tsx +64 -19
- package/src/conversation/ConversationListPane.tsx +42 -17
- package/src/conversation/ConversationsWorkbench.tsx +1 -0
- package/src/conversation/__tests__/ConversationAttentionBanner.test.tsx +76 -2
- package/src/conversation/__tests__/ConversationListPane.test.tsx +15 -4
- package/src/execution/ApprovalCard.tsx +23 -2
- package/src/execution/ArtifactPreviewModal.tsx +15 -27
- package/src/execution/ToolCallItem.tsx +19 -8
- package/src/execution/__tests__/intent-title.test.tsx +221 -0
- package/src/execution/index.ts +3 -0
- package/src/execution/tool-categories.ts +123 -1
- package/src/execution/tool-presenter.ts +22 -1
- package/src/index.ts +3 -0
- package/src/internal/DialogShell.tsx +169 -0
- package/src/internal/__tests__/DialogShell.test.tsx +223 -0
- package/src/manifest/ApplyManifestDialog.tsx +8 -26
- package/src/manifest/EditResourceYamlDialog.tsx +4 -31
- package/src/mcp-server/McpServerConnectDialog.tsx +15 -29
- package/src/mcp-server/McpServerDetailView.tsx +12 -24
- package/src/resource-detail/ConfirmDialog.tsx +9 -30
- package/src/sharing/ShareAgentDialog.tsx +7 -29
- package/src/skill/SkillDiffDialog.tsx +13 -36
- package/src/workflow/ViewYamlDialog.tsx +18 -37
- package/src/workflow/WorkflowArchitectDialog.tsx +11 -43
- package/src/workflow/WorkflowExplainDialog.tsx +11 -37
- package/src/workflow/WorkflowGraphFullscreenDialog.tsx +14 -26
- package/src/workflow/WorkflowRunDialog.tsx +21 -48
- package/src/workflow/execution-comparison/ExecutionComparisonPicker.tsx +14 -31
- package/src/workflow/instance/CreateWorkflowInstanceDialog.tsx +14 -27
- package/src/workflow/templates/WorkflowTemplatePreview.tsx +18 -28
- package/styles.css +1 -1
- package/workflow/ViewYamlDialog.d.ts.map +1 -1
- package/workflow/ViewYamlDialog.js +10 -29
- package/workflow/ViewYamlDialog.js.map +1 -1
- package/workflow/WorkflowArchitectDialog.d.ts.map +1 -1
- package/workflow/WorkflowArchitectDialog.js +5 -24
- package/workflow/WorkflowArchitectDialog.js.map +1 -1
- package/workflow/WorkflowExplainDialog.d.ts.map +1 -1
- package/workflow/WorkflowExplainDialog.js +5 -21
- package/workflow/WorkflowExplainDialog.js.map +1 -1
- package/workflow/WorkflowGraphFullscreenDialog.d.ts.map +1 -1
- package/workflow/WorkflowGraphFullscreenDialog.js +11 -17
- package/workflow/WorkflowGraphFullscreenDialog.js.map +1 -1
- package/workflow/WorkflowRunDialog.d.ts.map +1 -1
- package/workflow/WorkflowRunDialog.js +13 -29
- package/workflow/WorkflowRunDialog.js.map +1 -1
- package/workflow/execution-comparison/ExecutionComparisonPicker.d.ts.map +1 -1
- package/workflow/execution-comparison/ExecutionComparisonPicker.js +8 -26
- package/workflow/execution-comparison/ExecutionComparisonPicker.js.map +1 -1
- package/workflow/instance/CreateWorkflowInstanceDialog.d.ts.map +1 -1
- package/workflow/instance/CreateWorkflowInstanceDialog.js +7 -20
- package/workflow/instance/CreateWorkflowInstanceDialog.js.map +1 -1
- package/workflow/templates/WorkflowTemplatePreview.d.ts +1 -1
- package/workflow/templates/WorkflowTemplatePreview.d.ts.map +1 -1
- package/workflow/templates/WorkflowTemplatePreview.js +10 -22
- package/workflow/templates/WorkflowTemplatePreview.js.map +1 -1
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
4
|
+
import type { MouseEvent, ReactNode, SyntheticEvent } from "react";
|
|
5
|
+
import { cn } from "@stigmer/theme";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Width presets for {@link DialogShell}, mapping to Tailwind `max-w-*`
|
|
9
|
+
* utilities. Free-form sizes (e.g. a lightbox's `85vw`) ride `className`,
|
|
10
|
+
* which wins over the preset via `cn()`'s tailwind-merge.
|
|
11
|
+
*/
|
|
12
|
+
export type DialogShellWidth = "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl";
|
|
13
|
+
|
|
14
|
+
const WIDTH_CLASS: Record<DialogShellWidth, string> = {
|
|
15
|
+
sm: "stg:max-w-sm",
|
|
16
|
+
md: "stg:max-w-md",
|
|
17
|
+
lg: "stg:max-w-lg",
|
|
18
|
+
xl: "stg:max-w-xl",
|
|
19
|
+
"2xl": "stg:max-w-2xl",
|
|
20
|
+
"3xl": "stg:max-w-3xl",
|
|
21
|
+
"4xl": "stg:max-w-4xl",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Props for {@link DialogShell}. */
|
|
25
|
+
export interface DialogShellProps {
|
|
26
|
+
/** Whether the dialog is open (controlled — the SDK dialog convention). */
|
|
27
|
+
readonly open: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Called with `false` when the dialog should close: Escape / the platform
|
|
30
|
+
* cancel gesture, or a native close the host did not drive (e.g. a
|
|
31
|
+
* `method="dialog"` form submit). The shell never closes itself — state
|
|
32
|
+
* stays authoritative, so a host that ignores the callback keeps the
|
|
33
|
+
* dialog open (matching the pre-extraction per-dialog behavior).
|
|
34
|
+
*/
|
|
35
|
+
readonly onOpenChange: (open: boolean) => void;
|
|
36
|
+
/** Max-width preset (default `"md"`). See {@link DialogShellWidth}. */
|
|
37
|
+
readonly width?: DialogShellWidth;
|
|
38
|
+
/**
|
|
39
|
+
* Modal (default) renders in the top layer via `showModal()` — focus trap,
|
|
40
|
+
* Escape, and the token backdrop. `modal={false}` renders the dialog
|
|
41
|
+
* in-flow (`open` attribute, relative positioning, no backdrop, no focus
|
|
42
|
+
* trap) — the channel-dialog embedding mode.
|
|
43
|
+
*/
|
|
44
|
+
readonly modal?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Light dismiss: a click on the backdrop (the dialog element itself —
|
|
47
|
+
* clicks inside the content land on descendants) reports close intent.
|
|
48
|
+
* The house rule from the pre-extraction census: read-only viewers
|
|
49
|
+
* (diff/YAML/explain/lightbox/pickers) dismiss on backdrop, form dialogs
|
|
50
|
+
* do NOT — a stray click must never discard a half-filled form. Default
|
|
51
|
+
* `false`.
|
|
52
|
+
*/
|
|
53
|
+
readonly dismissOnBackdrop?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Merged after the shell's own chrome, so deliberate outliers (a
|
|
56
|
+
* lightbox's viewport sizing, a fullscreen graph's `h-[90vh]`, a
|
|
57
|
+
* `bg-background` surface) override the defaults per class.
|
|
58
|
+
*/
|
|
59
|
+
readonly className?: string;
|
|
60
|
+
/** Dialog content. Lazy-mounting bodies (`{open && …}`) stay caller-owned. */
|
|
61
|
+
readonly children: ReactNode;
|
|
62
|
+
/** Accessible name, when no visible heading labels the dialog. */
|
|
63
|
+
readonly "aria-label"?: string;
|
|
64
|
+
/** Id of the visible heading that labels the dialog. */
|
|
65
|
+
readonly "aria-labelledby"?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The one dialog shell (stigmer#653). Owns everything every modal dialog in
|
|
70
|
+
* the SDK used to re-transcribe by hand: the `<dialog>` element and its
|
|
71
|
+
* `showModal()`/`close()` lifecycle, cancel/Escape wiring, the token
|
|
72
|
+
* backdrop (`--stgm-backdrop`), the open animation, and the base
|
|
73
|
+
* positioning/chrome — parameterized by {@link DialogShellWidth}.
|
|
74
|
+
*
|
|
75
|
+
* Converged chrome: `rounded-xl` + `shadow-xl` on the `popover` token
|
|
76
|
+
* surface (the token family for top-layer content). Before the extraction
|
|
77
|
+
* the 24 hand-rolled shells had drifted across `rounded-lg/xl`,
|
|
78
|
+
* `shadow-lg/xl/2xl`, three background tokens, two hardcoded backdrops that
|
|
79
|
+
* escaped the #652 fence, and animation classes present on some dialogs and
|
|
80
|
+
* missing from others.
|
|
81
|
+
*
|
|
82
|
+
* Internal on purpose — not exported from the package until a host needs it.
|
|
83
|
+
* The `stigmer/no-handrolled-dialog` lint rule fences new `<dialog>`
|
|
84
|
+
* elements onto this shell.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```tsx
|
|
88
|
+
* <DialogShell open={open} onOpenChange={setOpen} width="lg" aria-labelledby={titleId}>
|
|
89
|
+
* {open && <MyDialogBody titleId={titleId} onClose={() => setOpen(false)} />}
|
|
90
|
+
* </DialogShell>
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
export function DialogShell({
|
|
94
|
+
open,
|
|
95
|
+
onOpenChange,
|
|
96
|
+
width = "md",
|
|
97
|
+
modal = true,
|
|
98
|
+
dismissOnBackdrop = false,
|
|
99
|
+
className,
|
|
100
|
+
children,
|
|
101
|
+
"aria-label": ariaLabel,
|
|
102
|
+
"aria-labelledby": ariaLabelledby,
|
|
103
|
+
}: DialogShellProps) {
|
|
104
|
+
const dialogRef = useRef<HTMLDialogElement>(null);
|
|
105
|
+
|
|
106
|
+
// Sync the native top-layer state to the controlled prop. Runs on mount
|
|
107
|
+
// too, so a shell mounted with `open` already true shows immediately.
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
const dialog = dialogRef.current;
|
|
110
|
+
if (!dialog || !modal) return;
|
|
111
|
+
if (open && !dialog.open) {
|
|
112
|
+
dialog.showModal();
|
|
113
|
+
} else if (!open && dialog.open) {
|
|
114
|
+
dialog.close();
|
|
115
|
+
}
|
|
116
|
+
}, [open, modal]);
|
|
117
|
+
|
|
118
|
+
// Escape / platform cancel: keep the native dialog open (preventDefault)
|
|
119
|
+
// and report the intent — the controlled `open` prop then drives the
|
|
120
|
+
// actual close through the effect above.
|
|
121
|
+
const handleCancel = useCallback(
|
|
122
|
+
(e: SyntheticEvent<HTMLDialogElement>) => {
|
|
123
|
+
e.preventDefault();
|
|
124
|
+
onOpenChange(false);
|
|
125
|
+
},
|
|
126
|
+
[onOpenChange],
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// A native close the effect did not perform (e.g. a `method="dialog"`
|
|
130
|
+
// form submit) must not leave the controlled state stranded open.
|
|
131
|
+
const handleClose = useCallback(() => {
|
|
132
|
+
if (open) onOpenChange(false);
|
|
133
|
+
}, [open, onOpenChange]);
|
|
134
|
+
|
|
135
|
+
// Backdrop clicks hit the <dialog> element itself; clicks anywhere in the
|
|
136
|
+
// content land on a descendant, so the target check is exact.
|
|
137
|
+
const handleClick = useCallback(
|
|
138
|
+
(e: MouseEvent<HTMLDialogElement>) => {
|
|
139
|
+
if (e.target === e.currentTarget) onOpenChange(false);
|
|
140
|
+
},
|
|
141
|
+
[onOpenChange],
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<dialog
|
|
146
|
+
ref={dialogRef}
|
|
147
|
+
open={modal ? undefined : open || undefined}
|
|
148
|
+
onCancel={modal ? handleCancel : undefined}
|
|
149
|
+
onClose={modal ? handleClose : undefined}
|
|
150
|
+
onClick={modal && dismissOnBackdrop ? handleClick : undefined}
|
|
151
|
+
aria-label={ariaLabel}
|
|
152
|
+
aria-labelledby={ariaLabelledby}
|
|
153
|
+
className={cn(
|
|
154
|
+
"stg:w-full stg:rounded-xl stg:border stg:border-border stg:bg-popover stg:p-0 stg:text-popover-foreground stg:shadow-xl",
|
|
155
|
+
modal
|
|
156
|
+
? cn(
|
|
157
|
+
"stg:fixed stg:inset-0 stg:z-50 stg:m-auto",
|
|
158
|
+
"stg:backdrop:bg-backdrop",
|
|
159
|
+
"stg:open:animate-in stg:open:fade-in-0 stg:open:zoom-in-95",
|
|
160
|
+
)
|
|
161
|
+
: "stg:relative",
|
|
162
|
+
WIDTH_CLASS[width],
|
|
163
|
+
className,
|
|
164
|
+
)}
|
|
165
|
+
>
|
|
166
|
+
{children}
|
|
167
|
+
</dialog>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DialogShell (stigmer#653): the one modal shell — showModal lifecycle,
|
|
3
|
+
* cancel/Escape wiring with state authority, native-close sync, the token
|
|
4
|
+
* backdrop + converged chrome, and the non-modal in-flow mode.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, vi, beforeAll, afterEach } from "vitest";
|
|
8
|
+
import { render, cleanup, fireEvent } from "@testing-library/react";
|
|
9
|
+
import { DialogShell } from "../DialogShell";
|
|
10
|
+
|
|
11
|
+
// happy-dom does not implement the native dialog show/close methods.
|
|
12
|
+
beforeAll(() => {
|
|
13
|
+
HTMLDialogElement.prototype.showModal = function showModal() {
|
|
14
|
+
this.open = true;
|
|
15
|
+
};
|
|
16
|
+
HTMLDialogElement.prototype.close = function close() {
|
|
17
|
+
this.open = false;
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(cleanup);
|
|
22
|
+
|
|
23
|
+
function dialogOf(container: HTMLElement): HTMLDialogElement {
|
|
24
|
+
return container.querySelector("dialog")!;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("DialogShell lifecycle", () => {
|
|
28
|
+
it("opens via showModal when the controlled prop flips true", () => {
|
|
29
|
+
const { container, rerender } = render(
|
|
30
|
+
<DialogShell open={false} onOpenChange={() => {}}>
|
|
31
|
+
<p>body</p>
|
|
32
|
+
</DialogShell>,
|
|
33
|
+
);
|
|
34
|
+
expect(dialogOf(container).open).toBe(false);
|
|
35
|
+
|
|
36
|
+
rerender(
|
|
37
|
+
<DialogShell open onOpenChange={() => {}}>
|
|
38
|
+
<p>body</p>
|
|
39
|
+
</DialogShell>,
|
|
40
|
+
);
|
|
41
|
+
expect(dialogOf(container).open).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("shows immediately when mounted already open", () => {
|
|
45
|
+
const { container } = render(
|
|
46
|
+
<DialogShell open onOpenChange={() => {}}>
|
|
47
|
+
<p>body</p>
|
|
48
|
+
</DialogShell>,
|
|
49
|
+
);
|
|
50
|
+
expect(dialogOf(container).open).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("closes the native dialog when the controlled prop flips false", () => {
|
|
54
|
+
const { container, rerender } = render(
|
|
55
|
+
<DialogShell open onOpenChange={() => {}}>
|
|
56
|
+
<p>body</p>
|
|
57
|
+
</DialogShell>,
|
|
58
|
+
);
|
|
59
|
+
rerender(
|
|
60
|
+
<DialogShell open={false} onOpenChange={() => {}}>
|
|
61
|
+
<p>body</p>
|
|
62
|
+
</DialogShell>,
|
|
63
|
+
);
|
|
64
|
+
expect(dialogOf(container).open).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("Escape/cancel reports intent but never closes on its own — state stays authoritative", () => {
|
|
68
|
+
const onOpenChange = vi.fn();
|
|
69
|
+
const { container } = render(
|
|
70
|
+
<DialogShell open onOpenChange={onOpenChange}>
|
|
71
|
+
<p>body</p>
|
|
72
|
+
</DialogShell>,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
fireEvent(dialogOf(container), new Event("cancel", { cancelable: true }));
|
|
76
|
+
|
|
77
|
+
expect(onOpenChange).toHaveBeenCalledExactlyOnceWith(false);
|
|
78
|
+
// The shell prevented the native close; the dialog stays open until the
|
|
79
|
+
// host flips the prop.
|
|
80
|
+
expect(dialogOf(container).open).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("syncs the controlled state when something else closes the dialog natively", () => {
|
|
84
|
+
const onOpenChange = vi.fn();
|
|
85
|
+
const { container } = render(
|
|
86
|
+
<DialogShell open onOpenChange={onOpenChange}>
|
|
87
|
+
<p>body</p>
|
|
88
|
+
</DialogShell>,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// e.g. a method="dialog" form submit closes without going through props.
|
|
92
|
+
const dialog = dialogOf(container);
|
|
93
|
+
dialog.close();
|
|
94
|
+
fireEvent(dialog, new Event("close"));
|
|
95
|
+
|
|
96
|
+
expect(onOpenChange).toHaveBeenCalledExactlyOnceWith(false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("does not re-report a close the host itself drove", () => {
|
|
100
|
+
const onOpenChange = vi.fn();
|
|
101
|
+
const { container, rerender } = render(
|
|
102
|
+
<DialogShell open onOpenChange={onOpenChange}>
|
|
103
|
+
<p>body</p>
|
|
104
|
+
</DialogShell>,
|
|
105
|
+
);
|
|
106
|
+
rerender(
|
|
107
|
+
<DialogShell open={false} onOpenChange={onOpenChange}>
|
|
108
|
+
<p>body</p>
|
|
109
|
+
</DialogShell>,
|
|
110
|
+
);
|
|
111
|
+
// The effect's own dialog.close() fires a native close event in real
|
|
112
|
+
// browsers — replay it against the now-false prop.
|
|
113
|
+
fireEvent(dialogOf(container), new Event("close"));
|
|
114
|
+
expect(onOpenChange).not.toHaveBeenCalled();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("DialogShell chrome", () => {
|
|
119
|
+
it("carries the token backdrop, animation, positioning, and width preset", () => {
|
|
120
|
+
const { container } = render(
|
|
121
|
+
<DialogShell open onOpenChange={() => {}} width="3xl">
|
|
122
|
+
<p>body</p>
|
|
123
|
+
</DialogShell>,
|
|
124
|
+
);
|
|
125
|
+
const cls = dialogOf(container).className;
|
|
126
|
+
|
|
127
|
+
expect(cls).toContain("stg:backdrop:bg-backdrop");
|
|
128
|
+
expect(cls).toContain("stg:open:animate-in");
|
|
129
|
+
expect(cls).toContain("stg:fixed");
|
|
130
|
+
expect(cls).toContain("stg:m-auto");
|
|
131
|
+
expect(cls).toContain("stg:max-w-3xl");
|
|
132
|
+
expect(cls).toContain("stg:bg-popover");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("className overrides the shell's own chrome per class (tailwind-merge)", () => {
|
|
136
|
+
const { container } = render(
|
|
137
|
+
<DialogShell
|
|
138
|
+
open
|
|
139
|
+
onOpenChange={() => {}}
|
|
140
|
+
className="stg:bg-background stg:max-w-[85vw]"
|
|
141
|
+
>
|
|
142
|
+
<p>body</p>
|
|
143
|
+
</DialogShell>,
|
|
144
|
+
);
|
|
145
|
+
const cls = dialogOf(container).className;
|
|
146
|
+
|
|
147
|
+
expect(cls).toContain("stg:bg-background");
|
|
148
|
+
expect(cls).not.toContain("stg:bg-popover");
|
|
149
|
+
expect(cls).toContain("stg:max-w-[85vw]");
|
|
150
|
+
expect(cls).not.toContain("stg:max-w-md");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("labels the dialog for assistive tech", () => {
|
|
154
|
+
const { container } = render(
|
|
155
|
+
<DialogShell open onOpenChange={() => {}} aria-label="Confirm delete">
|
|
156
|
+
<p>body</p>
|
|
157
|
+
</DialogShell>,
|
|
158
|
+
);
|
|
159
|
+
expect(dialogOf(container).getAttribute("aria-label")).toBe("Confirm delete");
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("DialogShell backdrop dismiss", () => {
|
|
164
|
+
it("dismissOnBackdrop: a click on the dialog element itself reports close intent", () => {
|
|
165
|
+
const onOpenChange = vi.fn();
|
|
166
|
+
const { container } = render(
|
|
167
|
+
<DialogShell open onOpenChange={onOpenChange} dismissOnBackdrop>
|
|
168
|
+
<p>body</p>
|
|
169
|
+
</DialogShell>,
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
fireEvent.click(dialogOf(container));
|
|
173
|
+
expect(onOpenChange).toHaveBeenCalledExactlyOnceWith(false);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("dismissOnBackdrop: clicks on content descendants never dismiss", () => {
|
|
177
|
+
const onOpenChange = vi.fn();
|
|
178
|
+
const { container, getByText } = render(
|
|
179
|
+
<DialogShell open onOpenChange={onOpenChange} dismissOnBackdrop>
|
|
180
|
+
<p>body</p>
|
|
181
|
+
</DialogShell>,
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
fireEvent.click(getByText("body"));
|
|
185
|
+
expect(onOpenChange).not.toHaveBeenCalled();
|
|
186
|
+
expect(dialogOf(container).open).toBe(true);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("default: backdrop clicks are inert (form dialogs)", () => {
|
|
190
|
+
const onOpenChange = vi.fn();
|
|
191
|
+
const { container } = render(
|
|
192
|
+
<DialogShell open onOpenChange={onOpenChange}>
|
|
193
|
+
<p>body</p>
|
|
194
|
+
</DialogShell>,
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
fireEvent.click(dialogOf(container));
|
|
198
|
+
expect(onOpenChange).not.toHaveBeenCalled();
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe("DialogShell non-modal mode", () => {
|
|
203
|
+
it("renders in-flow: open attribute, no backdrop, no fixed positioning", () => {
|
|
204
|
+
const { container, rerender } = render(
|
|
205
|
+
<DialogShell open modal={false} onOpenChange={() => {}}>
|
|
206
|
+
<p>body</p>
|
|
207
|
+
</DialogShell>,
|
|
208
|
+
);
|
|
209
|
+
const dialog = dialogOf(container);
|
|
210
|
+
|
|
211
|
+
expect(dialog.open).toBe(true);
|
|
212
|
+
expect(dialog.className).toContain("stg:relative");
|
|
213
|
+
expect(dialog.className).not.toContain("stg:backdrop:bg-backdrop");
|
|
214
|
+
expect(dialog.className).not.toContain("stg:fixed");
|
|
215
|
+
|
|
216
|
+
rerender(
|
|
217
|
+
<DialogShell open={false} modal={false} onOpenChange={() => {}}>
|
|
218
|
+
<p>body</p>
|
|
219
|
+
</DialogShell>,
|
|
220
|
+
);
|
|
221
|
+
expect(dialogOf(container).open).toBe(false);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { lazy, Suspense, useCallback, useEffect, useRef } from "react";
|
|
4
4
|
import { cn } from "@stigmer/theme";
|
|
5
|
+
import { DialogShell } from "../internal/DialogShell.js";
|
|
5
6
|
import { toast } from "../feedback/toast.js";
|
|
6
7
|
import { RedactedSecretsNotice } from "./RedactedSecretsNotice.js";
|
|
7
8
|
import {
|
|
@@ -71,22 +72,10 @@ export function ApplyManifestDialog({
|
|
|
71
72
|
org,
|
|
72
73
|
onApplied,
|
|
73
74
|
}: ApplyManifestDialogProps) {
|
|
74
|
-
const dialogRef = useRef<HTMLDialogElement>(null);
|
|
75
75
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
76
76
|
const manifest = useApplyManifest(org);
|
|
77
77
|
const { reset } = manifest;
|
|
78
78
|
|
|
79
|
-
// Sync dialog open state with the native <dialog> element.
|
|
80
|
-
useEffect(() => {
|
|
81
|
-
const dialog = dialogRef.current;
|
|
82
|
-
if (!dialog) return;
|
|
83
|
-
if (open && !dialog.open) {
|
|
84
|
-
dialog.showModal();
|
|
85
|
-
} else if (!open && dialog.open) {
|
|
86
|
-
dialog.close();
|
|
87
|
-
}
|
|
88
|
-
}, [open]);
|
|
89
|
-
|
|
90
79
|
// Start each session clean.
|
|
91
80
|
useEffect(() => {
|
|
92
81
|
if (open) {
|
|
@@ -109,10 +98,11 @@ export function ApplyManifestDialog({
|
|
|
109
98
|
if (applied.length > 0) onApplied?.(applied);
|
|
110
99
|
}, [onOpenChange, manifest.entries, onApplied]);
|
|
111
100
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
101
|
+
// Escape/cancel routes through the same dismiss-after-partial path as the
|
|
102
|
+
// Close button, via the shell's single close-intent callback.
|
|
103
|
+
const handleShellOpenChange = useCallback(
|
|
104
|
+
(nextOpen: boolean) => {
|
|
105
|
+
if (!nextOpen) closeAndMaybeRefresh();
|
|
116
106
|
},
|
|
117
107
|
[closeAndMaybeRefresh],
|
|
118
108
|
);
|
|
@@ -151,15 +141,7 @@ export function ApplyManifestDialog({
|
|
|
151
141
|
!manifest.isApplying;
|
|
152
142
|
|
|
153
143
|
return (
|
|
154
|
-
<
|
|
155
|
-
ref={dialogRef}
|
|
156
|
-
onCancel={handleCancel}
|
|
157
|
-
className={cn(
|
|
158
|
-
"stg:fixed stg:inset-0 stg:z-50 stg:m-auto stg:w-full stg:max-w-3xl stg:rounded-lg stg:border stg:border-border stg:bg-popover stg:p-0 stg:text-popover-foreground stg:shadow-lg",
|
|
159
|
-
"stg:backdrop:bg-backdrop",
|
|
160
|
-
"stg:open:animate-in stg:open:fade-in-0 stg:open:zoom-in-95",
|
|
161
|
-
)}
|
|
162
|
-
>
|
|
144
|
+
<DialogShell open={open} onOpenChange={handleShellOpenChange} width="3xl" aria-label="Apply YAML">
|
|
163
145
|
<div className="stg:flex stg:flex-col stg:gap-4 stg:p-6">
|
|
164
146
|
{/* Header */}
|
|
165
147
|
<div className="stg:flex stg:flex-col stg:gap-1">
|
|
@@ -252,7 +234,7 @@ export function ApplyManifestDialog({
|
|
|
252
234
|
</div>
|
|
253
235
|
</div>
|
|
254
236
|
</div>
|
|
255
|
-
</
|
|
237
|
+
</DialogShell>
|
|
256
238
|
);
|
|
257
239
|
}
|
|
258
240
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { lazy, Suspense, useCallback, useEffect
|
|
3
|
+
import { lazy, Suspense, useCallback, useEffect } from "react";
|
|
4
4
|
import type { Message } from "@bufbuild/protobuf";
|
|
5
5
|
import { cn } from "@stigmer/theme";
|
|
6
|
+
import { DialogShell } from "../internal/DialogShell.js";
|
|
6
7
|
import { getUserMessage, type AppliedManifest } from "@stigmer/sdk";
|
|
7
8
|
import { toast } from "../feedback/toast.js";
|
|
8
9
|
import { RedactedSecretsNotice } from "./RedactedSecretsNotice.js";
|
|
@@ -67,34 +68,14 @@ export function EditResourceYamlDialog({
|
|
|
67
68
|
resource,
|
|
68
69
|
onApplied,
|
|
69
70
|
}: EditResourceYamlDialogProps) {
|
|
70
|
-
const dialogRef = useRef<HTMLDialogElement>(null);
|
|
71
71
|
const edit = useEditResourceYaml({ resource });
|
|
72
72
|
const { reset } = edit;
|
|
73
73
|
|
|
74
|
-
// Sync dialog open state with the native <dialog> element.
|
|
75
|
-
useEffect(() => {
|
|
76
|
-
const dialog = dialogRef.current;
|
|
77
|
-
if (!dialog) return;
|
|
78
|
-
if (open && !dialog.open) {
|
|
79
|
-
dialog.showModal();
|
|
80
|
-
} else if (!open && dialog.open) {
|
|
81
|
-
dialog.close();
|
|
82
|
-
}
|
|
83
|
-
}, [open]);
|
|
84
|
-
|
|
85
74
|
// Discard stale edits from a previous session when the dialog opens.
|
|
86
75
|
useEffect(() => {
|
|
87
76
|
if (open) reset();
|
|
88
77
|
}, [open, reset]);
|
|
89
78
|
|
|
90
|
-
const handleCancel = useCallback(
|
|
91
|
-
(e: React.SyntheticEvent) => {
|
|
92
|
-
e.preventDefault();
|
|
93
|
-
onOpenChange(false);
|
|
94
|
-
},
|
|
95
|
-
[onOpenChange],
|
|
96
|
-
);
|
|
97
|
-
|
|
98
79
|
const handleApply = useCallback(async () => {
|
|
99
80
|
try {
|
|
100
81
|
const result = await edit.apply();
|
|
@@ -111,15 +92,7 @@ export function EditResourceYamlDialog({
|
|
|
111
92
|
edit.validation.status === "valid" && edit.isDirty && !edit.isApplying;
|
|
112
93
|
|
|
113
94
|
return (
|
|
114
|
-
<
|
|
115
|
-
ref={dialogRef}
|
|
116
|
-
onCancel={handleCancel}
|
|
117
|
-
className={cn(
|
|
118
|
-
"stg:fixed stg:inset-0 stg:z-50 stg:m-auto stg:w-full stg:max-w-3xl stg:rounded-lg stg:border stg:border-border stg:bg-popover stg:p-0 stg:text-popover-foreground stg:shadow-lg",
|
|
119
|
-
"stg:backdrop:bg-backdrop",
|
|
120
|
-
"stg:open:animate-in stg:open:fade-in-0 stg:open:zoom-in-95",
|
|
121
|
-
)}
|
|
122
|
-
>
|
|
95
|
+
<DialogShell open={open} onOpenChange={onOpenChange} width="3xl" aria-label="Edit YAML">
|
|
123
96
|
<div className="stg:flex stg:flex-col stg:gap-4 stg:p-6">
|
|
124
97
|
{/* Header */}
|
|
125
98
|
<div className="stg:flex stg:flex-col stg:gap-1">
|
|
@@ -233,7 +206,7 @@ export function EditResourceYamlDialog({
|
|
|
233
206
|
</div>
|
|
234
207
|
</div>
|
|
235
208
|
</div>
|
|
236
|
-
</
|
|
209
|
+
</DialogShell>
|
|
237
210
|
);
|
|
238
211
|
}
|
|
239
212
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useCallback, useEffect,
|
|
3
|
+
import { useCallback, useEffect, useState } from "react";
|
|
4
4
|
import { cn } from "@stigmer/theme";
|
|
5
|
+
import { DialogShell } from "../internal/DialogShell.js";
|
|
5
6
|
import type { EnvVarInput } from "@stigmer/sdk";
|
|
6
7
|
import { useMcpServer } from "./useMcpServer.js";
|
|
7
8
|
import { useMcpServerCredentials } from "./useMcpServerCredentials.js";
|
|
@@ -88,29 +89,11 @@ export function McpServerConnectDialog({
|
|
|
88
89
|
onOpenDetails,
|
|
89
90
|
className,
|
|
90
91
|
}: McpServerConnectDialogProps) {
|
|
91
|
-
const dialogRef = useRef<HTMLDialogElement>(null);
|
|
92
92
|
const resolvedOrg = activeOrg || org;
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (open && !dialog.open) {
|
|
99
|
-
dialog.showModal();
|
|
100
|
-
} else if (!open && dialog.open) {
|
|
101
|
-
dialog.close();
|
|
102
|
-
}
|
|
103
|
-
}, [open]);
|
|
104
|
-
|
|
105
|
-
const handleDialogClose = useCallback(() => {
|
|
106
|
-
onClose();
|
|
107
|
-
}, [onClose]);
|
|
108
|
-
|
|
109
|
-
const handleBackdropClick = useCallback(
|
|
110
|
-
(e: React.MouseEvent<HTMLDialogElement>) => {
|
|
111
|
-
if (e.target === dialogRef.current) {
|
|
112
|
-
onClose();
|
|
113
|
-
}
|
|
94
|
+
const handleOpenChange = useCallback(
|
|
95
|
+
(nextOpen: boolean) => {
|
|
96
|
+
if (!nextOpen) onClose();
|
|
114
97
|
},
|
|
115
98
|
[onClose],
|
|
116
99
|
);
|
|
@@ -118,15 +101,18 @@ export function McpServerConnectDialog({
|
|
|
118
101
|
if (!open) return null;
|
|
119
102
|
|
|
120
103
|
return (
|
|
121
|
-
<
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
104
|
+
<DialogShell
|
|
105
|
+
open
|
|
106
|
+
onOpenChange={handleOpenChange}
|
|
107
|
+
width="md"
|
|
108
|
+
dismissOnBackdrop
|
|
109
|
+
// Card surface + visible overflow are deliberate: the connect form
|
|
110
|
+
// hosts popovers that extend past the dialog bounds.
|
|
125
111
|
className={cn(
|
|
126
|
-
"stg:
|
|
127
|
-
"stg:backdrop:bg-backdrop",
|
|
112
|
+
"stg:max-h-[85vh] stg:overflow-visible stg:bg-card stg:text-foreground",
|
|
128
113
|
className,
|
|
129
114
|
)}
|
|
115
|
+
aria-label={`Connect ${slug}`}
|
|
130
116
|
>
|
|
131
117
|
<div
|
|
132
118
|
className="stg:flex stg:max-h-[85vh] stg:flex-col stg:overflow-y-auto stg:p-6"
|
|
@@ -141,7 +127,7 @@ export function McpServerConnectDialog({
|
|
|
141
127
|
onOpenDetails={onOpenDetails}
|
|
142
128
|
/>
|
|
143
129
|
</div>
|
|
144
|
-
</
|
|
130
|
+
</DialogShell>
|
|
145
131
|
);
|
|
146
132
|
}
|
|
147
133
|
|
|
@@ -36,6 +36,7 @@ import { OAuthAppForm } from "./OAuthAppForm.js";
|
|
|
36
36
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
37
37
|
import { ErrorMessage } from "../error/ErrorMessage.js";
|
|
38
38
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../internal/tooltip.js";
|
|
39
|
+
import { DialogShell } from "../internal/DialogShell.js";
|
|
39
40
|
import { EnvVarForm } from "../environment/EnvVarForm.js";
|
|
40
41
|
import type { EnvVarFormVariable } from "../environment/EnvVarForm.js";
|
|
41
42
|
import { VisibilityBadge } from "../library/VisibilitySelector.js";
|
|
@@ -282,7 +283,6 @@ export function McpServerDetailView({
|
|
|
282
283
|
const [showCredentialForm, setShowCredentialForm] = useState(defaultShowCredentialForm);
|
|
283
284
|
const [showByoaForm, setShowByoaForm] = useState(false);
|
|
284
285
|
const [capabilityTab, setCapabilityTab] = useState<CapabilityTab>(defaultCapabilityTab);
|
|
285
|
-
const byoaDialogRef = useRef<HTMLDialogElement>(null);
|
|
286
286
|
|
|
287
287
|
const onResourceLoadRef = useRef(onResourceLoad);
|
|
288
288
|
onResourceLoadRef.current = onResourceLoad;
|
|
@@ -388,20 +388,9 @@ export function McpServerDetailView({
|
|
|
388
388
|
}
|
|
389
389
|
}, [mcpServer, disconnectOAuth, credentials, refetch, activeOrg, org]);
|
|
390
390
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
if (!dialog) return;
|
|
395
|
-
if (showByoaForm && !dialog.open) {
|
|
396
|
-
dialog.showModal();
|
|
397
|
-
} else if (!showByoaForm && dialog.open) {
|
|
398
|
-
dialog.close();
|
|
399
|
-
}
|
|
400
|
-
}, [showByoaForm]);
|
|
401
|
-
|
|
402
|
-
const handleByoaDialogCancel = useCallback(
|
|
403
|
-
(e: React.SyntheticEvent) => {
|
|
404
|
-
e.preventDefault();
|
|
391
|
+
const handleByoaOpenChange = useCallback(
|
|
392
|
+
(nextOpen: boolean) => {
|
|
393
|
+
if (nextOpen) return;
|
|
405
394
|
setShowByoaForm(false);
|
|
406
395
|
orgOAuthApp.clearErrors();
|
|
407
396
|
},
|
|
@@ -706,14 +695,13 @@ export function McpServerDetailView({
|
|
|
706
695
|
)}
|
|
707
696
|
</Section>
|
|
708
697
|
|
|
709
|
-
{/* BYOA dialog
|
|
710
|
-
<
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
)}
|
|
698
|
+
{/* BYOA dialog */}
|
|
699
|
+
<DialogShell
|
|
700
|
+
open={showByoaForm}
|
|
701
|
+
onOpenChange={handleByoaOpenChange}
|
|
702
|
+
width="md"
|
|
703
|
+
className="stg:p-6"
|
|
704
|
+
aria-label="Use your own OAuth app"
|
|
717
705
|
>
|
|
718
706
|
<h3 className="stg:mb-4 stg:text-base stg:font-semibold stg:text-foreground">
|
|
719
707
|
Use your own OAuth app
|
|
@@ -729,7 +717,7 @@ export function McpServerDetailView({
|
|
|
729
717
|
isSubmitting={orgOAuthApp.isSetting}
|
|
730
718
|
error={orgOAuthApp.setError}
|
|
731
719
|
/>
|
|
732
|
-
</
|
|
720
|
+
</DialogShell>
|
|
733
721
|
|
|
734
722
|
<Section title="Capabilities" scrollTarget="mcp-capabilities">
|
|
735
723
|
<Tabs
|