@vertesia/ui 1.4.0-dev.20260629.130134Z → 1.4.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/lib/core/components/SidePanel.js +1 -1
- package/lib/core/components/SidePanel.js.map +1 -1
- package/lib/core/components/shadcn/button.d.ts +10 -3
- package/lib/core/components/shadcn/button.d.ts.map +1 -1
- package/lib/core/components/shadcn/button.js +41 -9
- package/lib/core/components/shadcn/button.js.map +1 -1
- package/lib/core/components/shadcn/modal/dialog.js +1 -1
- package/lib/core/components/shadcn/modal/dialog.js.map +1 -1
- package/lib/core/components/shadcn/resizeable.d.ts +1 -1
- package/lib/core/components/shadcn/resizeable.d.ts.map +1 -1
- package/lib/core/components/shadcn/resizeable.js +12 -2
- package/lib/core/components/shadcn/resizeable.js.map +1 -1
- package/lib/core/components/shadcn/tabs.d.ts +12 -1
- package/lib/core/components/shadcn/tabs.d.ts.map +1 -1
- package/lib/core/components/shadcn/tabs.js +12 -4
- package/lib/core/components/shadcn/tabs.js.map +1 -1
- package/lib/core/components/shadcn/theme/ThemeSwitcher.js +1 -1
- package/lib/core/components/shadcn/theme/ThemeSwitcher.js.map +1 -1
- package/lib/core/hooks/useIntersectionObserver.d.ts.map +1 -1
- package/lib/core/hooks/useIntersectionObserver.js +3 -1
- package/lib/core/hooks/useIntersectionObserver.js.map +1 -1
- package/lib/features/agent/chat/AgentChatPlaybackControls.d.ts.map +1 -1
- package/lib/features/agent/chat/AgentChatPlaybackControls.js +2 -2
- package/lib/features/agent/chat/AgentChatPlaybackControls.js.map +1 -1
- package/lib/features/agent/chat/AgentRequestInputOverlay.d.ts.map +1 -1
- package/lib/features/agent/chat/AgentRequestInputOverlay.js +2 -2
- package/lib/features/agent/chat/AgentRequestInputOverlay.js.map +1 -1
- package/lib/features/agent/chat/AgentRightPanel.js +1 -1
- package/lib/features/agent/chat/AgentRightPanel.js.map +1 -1
- package/lib/features/agent/chat/AskUserWidget.d.ts.map +1 -1
- package/lib/features/agent/chat/AskUserWidget.js +4 -3
- package/lib/features/agent/chat/AskUserWidget.js.map +1 -1
- package/lib/features/agent/chat/ImageLightbox.d.ts.map +1 -1
- package/lib/features/agent/chat/ImageLightbox.js +1 -1
- package/lib/features/agent/chat/ImageLightbox.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentConversation.d.ts.map +1 -1
- package/lib/features/agent/chat/ModernAgentConversation.js +57 -12
- package/lib/features/agent/chat/ModernAgentConversation.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.d.ts +8 -1
- package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.d.ts.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js +45 -36
- package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/Header.d.ts.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/Header.js +4 -2
- package/lib/features/agent/chat/ModernAgentOutput/Header.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/MessageInput.d.ts.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/MessageInput.js +7 -3
- package/lib/features/agent/chat/ModernAgentOutput/MessageInput.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/MessageItem.d.ts.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/MessageItem.js +2 -1
- package/lib/features/agent/chat/ModernAgentOutput/MessageItem.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/WorkstreamTabs.d.ts.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/WorkstreamTabs.js +158 -8
- package/lib/features/agent/chat/ModernAgentOutput/WorkstreamTabs.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/requestInputMessages.d.ts +6 -0
- package/lib/features/agent/chat/ModernAgentOutput/requestInputMessages.d.ts.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/requestInputMessages.js +15 -1
- package/lib/features/agent/chat/ModernAgentOutput/requestInputMessages.js.map +1 -1
- package/lib/features/agent/chat/hooks/useFileProcessing.d.ts +1 -0
- package/lib/features/agent/chat/hooks/useFileProcessing.d.ts.map +1 -1
- package/lib/features/agent/chat/hooks/useFileProcessing.js +37 -3
- package/lib/features/agent/chat/hooks/useFileProcessing.js.map +1 -1
- package/lib/features/magic-pdf/AnnotatedImageSlider.js +13 -13
- package/lib/features/magic-pdf/AnnotatedImageSlider.js.map +1 -1
- package/lib/features/magic-pdf/DownloadPopover.js +2 -2
- package/lib/features/magic-pdf/DownloadPopover.js.map +1 -1
- package/lib/features/magic-pdf/MagicPdfView.js +3 -3
- package/lib/features/magic-pdf/MagicPdfView.js.map +1 -1
- package/lib/features/oauth/McpConnectionsButton.d.ts.map +1 -1
- package/lib/features/oauth/McpConnectionsButton.js +2 -2
- package/lib/features/oauth/McpConnectionsButton.js.map +1 -1
- package/lib/features/oauth/McpConnectionsDialog.d.ts.map +1 -1
- package/lib/features/oauth/McpConnectionsDialog.js +1 -1
- package/lib/features/oauth/McpConnectionsDialog.js.map +1 -1
- package/lib/features/oauth/RemoteMcpConnectionButton.d.ts.map +1 -1
- package/lib/features/oauth/RemoteMcpConnectionButton.js +1 -1
- package/lib/features/oauth/RemoteMcpConnectionButton.js.map +1 -1
- package/lib/features/pdf-viewer/PdfPageSlider.js +2 -2
- package/lib/features/pdf-viewer/PdfPageSlider.js.map +1 -1
- package/lib/features/pdf-viewer/SimplePdfViewer.d.ts.map +1 -1
- package/lib/features/pdf-viewer/SimplePdfViewer.js +3 -3
- package/lib/features/pdf-viewer/SimplePdfViewer.js.map +1 -1
- package/lib/features/store/objects/DocumentQuickFilter.d.ts.map +1 -1
- package/lib/features/store/objects/DocumentQuickFilter.js +5 -5
- package/lib/features/store/objects/DocumentQuickFilter.js.map +1 -1
- package/lib/features/store/objects/DocumentSearchResults.d.ts.map +1 -1
- package/lib/features/store/objects/DocumentSearchResults.js +2 -2
- package/lib/features/store/objects/DocumentSearchResults.js.map +1 -1
- package/lib/features/store/objects/components/ContentDispositionButton.d.ts.map +1 -1
- package/lib/features/store/objects/components/ContentDispositionButton.js +1 -1
- package/lib/features/store/objects/components/ContentDispositionButton.js.map +1 -1
- package/lib/features/store/objects/components/ContentOverview.js +3 -3
- package/lib/features/store/objects/components/ContentOverview.js.map +1 -1
- package/lib/features/store/objects/components/DocumentIcon.js +1 -1
- package/lib/features/store/objects/components/VectorSearchWidget.js +1 -1
- package/lib/features/store/objects/components/VectorSearchWidget.js.map +1 -1
- package/lib/features/store/objects/layout/renderers.d.ts.map +1 -1
- package/lib/features/store/objects/layout/renderers.js +2 -2
- package/lib/features/store/objects/layout/renderers.js.map +1 -1
- package/lib/features/store/objects/selection/SelectionActions.js +1 -1
- package/lib/features/store/objects/selection/SelectionActions.js.map +1 -1
- package/lib/features/store/objects/selection/actions/StartWorkflowComponent.js +4 -4
- package/lib/features/store/objects/selection/actions/StartWorkflowComponent.js.map +1 -1
- package/lib/features/store/types/ContentObjectTypesTable.d.ts.map +1 -1
- package/lib/features/store/types/ContentObjectTypesTable.js +4 -4
- package/lib/features/store/types/ContentObjectTypesTable.js.map +1 -1
- package/lib/features/store/types/SelectContentTypeModal.js +1 -1
- package/lib/features/store/types/SelectContentTypeModal.js.map +1 -1
- package/lib/features/user/UserInfo.js +7 -3
- package/lib/features/user/UserInfo.js.map +1 -1
- package/lib/router/HistoryNavigator.d.ts.map +1 -1
- package/lib/router/HistoryNavigator.js +5 -0
- package/lib/router/HistoryNavigator.js.map +1 -1
- package/lib/router/Nav.d.ts.map +1 -1
- package/lib/router/Nav.js +9 -1
- package/lib/router/Nav.js.map +1 -1
- package/lib/vertesia-ui-core.js +1 -1
- package/lib/vertesia-ui-core.js.map +1 -1
- package/lib/vertesia-ui-features.js +1 -1
- package/lib/vertesia-ui-features.js.map +1 -1
- package/lib/vertesia-ui-router.js +1 -1
- package/lib/vertesia-ui-router.js.map +1 -1
- package/lib/vertesia-ui-widgets.js +1 -1
- package/lib/vertesia-ui-widgets.js.map +1 -1
- package/lib/widgets/form/Form.js +1 -1
- package/lib/widgets/form/Form.js.map +1 -1
- package/lib/widgets/json-view/JSONSwitcher.js +1 -1
- package/lib/widgets/json-view/JSONSwitcher.js.map +1 -1
- package/lib/widgets/json-view/JSONView.js +6 -6
- package/lib/widgets/json-view/JSONView.js.map +1 -1
- package/lib/widgets/monacoEditor/MonacoEditor.d.ts.map +1 -1
- package/lib/widgets/monacoEditor/MonacoEditor.js +29 -0
- package/lib/widgets/monacoEditor/MonacoEditor.js.map +1 -1
- package/lib/widgets/xml-viewer/hooks/useXMLViewer.d.ts +9 -0
- package/lib/widgets/xml-viewer/hooks/useXMLViewer.d.ts.map +1 -1
- package/lib/widgets/xml-viewer/hooks/useXMLViewer.js +20 -17
- package/lib/widgets/xml-viewer/hooks/useXMLViewer.js.map +1 -1
- package/package.json +29 -30
- package/src/__tests__/a11y.test.tsx +73 -0
- package/src/core/components/SidePanel.tsx +1 -1
- package/src/core/components/shadcn/button.tsx +67 -12
- package/src/core/components/shadcn/modal/dialog.tsx +1 -1
- package/src/core/components/shadcn/resizeable.test.tsx +77 -0
- package/src/core/components/shadcn/resizeable.tsx +28 -2
- package/src/core/components/shadcn/tabs.test.tsx +65 -0
- package/src/core/components/shadcn/tabs.tsx +25 -3
- package/src/core/components/shadcn/theme/ThemeSwitcher.tsx +1 -1
- package/src/core/hooks/useIntersectionObserver.tsx +3 -1
- package/src/features/agent/chat/AgentChatPlaybackControls.tsx +0 -5
- package/src/features/agent/chat/AgentRequestInputOverlay.test.tsx +31 -0
- package/src/features/agent/chat/AgentRequestInputOverlay.tsx +6 -2
- package/src/features/agent/chat/AgentRightPanel.tsx +1 -1
- package/src/features/agent/chat/AskUserWidget.test.tsx +49 -0
- package/src/features/agent/chat/AskUserWidget.tsx +32 -22
- package/src/features/agent/chat/ImageLightbox.tsx +0 -1
- package/src/features/agent/chat/ModernAgentConversation.test.tsx +246 -1
- package/src/features/agent/chat/ModernAgentConversation.tsx +62 -17
- package/src/features/agent/chat/ModernAgentOutput/AllMessagesMixed.test.tsx +6 -1
- package/src/features/agent/chat/ModernAgentOutput/AllMessagesMixed.tsx +81 -49
- package/src/features/agent/chat/ModernAgentOutput/Header.tsx +3 -3
- package/src/features/agent/chat/ModernAgentOutput/MessageInput.test.tsx +25 -0
- package/src/features/agent/chat/ModernAgentOutput/MessageInput.tsx +5 -3
- package/src/features/agent/chat/ModernAgentOutput/MessageItem.tsx +4 -1
- package/src/features/agent/chat/ModernAgentOutput/WorkstreamTabs.tsx +328 -42
- package/src/features/agent/chat/ModernAgentOutput/requestInputMessages.test.ts +80 -0
- package/src/features/agent/chat/ModernAgentOutput/requestInputMessages.ts +21 -1
- package/src/features/agent/chat/hooks/useFileProcessing.test.tsx +70 -0
- package/src/features/agent/chat/hooks/useFileProcessing.ts +37 -6
- package/src/features/magic-pdf/AnnotatedImageSlider.tsx +62 -66
- package/src/features/magic-pdf/DownloadPopover.tsx +2 -2
- package/src/features/magic-pdf/MagicPdfView.tsx +3 -3
- package/src/features/oauth/McpConnectionsButton.tsx +0 -2
- package/src/features/oauth/McpConnectionsDialog.tsx +0 -1
- package/src/features/oauth/RemoteMcpConnectionButton.tsx +0 -1
- package/src/features/pdf-viewer/PdfPageSlider.tsx +5 -5
- package/src/features/pdf-viewer/SimplePdfViewer.tsx +11 -12
- package/src/features/store/objects/DocumentQuickFilter.tsx +13 -15
- package/src/features/store/objects/DocumentSearchResults.tsx +3 -6
- package/src/features/store/objects/components/ContentDispositionButton.tsx +5 -1
- package/src/features/store/objects/components/ContentOverview.tsx +11 -11
- package/src/features/store/objects/components/DocumentIcon.tsx +1 -1
- package/src/features/store/objects/components/VectorSearchWidget.tsx +2 -2
- package/src/features/store/objects/layout/renderers.tsx +1 -2
- package/src/features/store/objects/selection/SelectionActions.tsx +1 -1
- package/src/features/store/objects/selection/actions/StartWorkflowComponent.tsx +6 -5
- package/src/features/store/types/ContentObjectTypesTable.tsx +12 -14
- package/src/features/store/types/SelectContentTypeModal.tsx +2 -2
- package/src/features/user/UserInfo.tsx +7 -7
- package/src/router/HistoryNavigator.ts +5 -0
- package/src/router/Nav.tsx +12 -3
- package/src/widgets/form/Form.tsx +1 -1
- package/src/widgets/json-view/JSONSwitcher.tsx +2 -2
- package/src/widgets/json-view/JSONView.tsx +7 -7
- package/src/widgets/monacoEditor/MonacoEditor.tsx +29 -0
- package/src/widgets/xml-viewer/hooks/useXMLViewer.test.ts +26 -0
- package/src/widgets/xml-viewer/hooks/useXMLViewer.ts +23 -18
|
@@ -23,6 +23,22 @@ function useDeprecationWarning(propName: string, isUsed: boolean, message: strin
|
|
|
23
23
|
}, [isUsed, message, propName]);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
// Does this subtree contribute any text to the accessible name? Used to decide whether `title`
|
|
27
|
+
// may supply that name. Icons (lucide elements) have no string children and return false, and
|
|
28
|
+
// `aria-hidden` subtrees are skipped because they are excluded from the name computation.
|
|
29
|
+
function hasVisibleText(node: React.ReactNode): boolean {
|
|
30
|
+
return React.Children.toArray(node).some((child) => {
|
|
31
|
+
if (typeof child === 'string') return child.trim().length > 0;
|
|
32
|
+
if (typeof child === 'number') return true;
|
|
33
|
+
if (React.isValidElement(child)) {
|
|
34
|
+
const props = child.props as { children?: React.ReactNode; 'aria-hidden'?: boolean | string };
|
|
35
|
+
if (props['aria-hidden'] === true || props['aria-hidden'] === 'true') return false;
|
|
36
|
+
return hasVisibleText(props.children);
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
26
42
|
const buttonVariants = cva(
|
|
27
43
|
'hover:cursor-pointer inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
|
28
44
|
{
|
|
@@ -60,35 +76,66 @@ export interface ButtonProps
|
|
|
60
76
|
VariantProps<typeof buttonVariants> {
|
|
61
77
|
asChild?: boolean;
|
|
62
78
|
/**
|
|
63
|
-
* @deprecated Use `
|
|
64
|
-
*
|
|
65
|
-
*
|
|
79
|
+
* @deprecated Use `title` instead. `alt` drives the same tooltip but, unlike `title`, it
|
|
80
|
+
* overrides the accessible name even when the button shows a text label — which usually
|
|
81
|
+
* violates WCAG 2.5.3 (Label in Name). That legacy behavior is preserved so existing call
|
|
82
|
+
* sites keep announcing what they always did. Slated for removal in the next major.
|
|
66
83
|
*/
|
|
67
84
|
alt?: string;
|
|
85
|
+
/**
|
|
86
|
+
* Tooltip text. Renders a `<VTooltip>` around the button and doubles as the accessible name,
|
|
87
|
+
* so icon-only buttons need nothing else. Pass `aria-label` to override the name independently.
|
|
88
|
+
*/
|
|
68
89
|
title?: string;
|
|
90
|
+
/** Which side the `title` tooltip opens on. Defaults to `top`. */
|
|
91
|
+
tooltipPlacement?: 'top' | 'right' | 'bottom' | 'left';
|
|
69
92
|
isDisabled?: boolean;
|
|
70
93
|
isLoading?: boolean;
|
|
71
94
|
}
|
|
72
95
|
|
|
73
96
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
74
97
|
(
|
|
75
|
-
{
|
|
98
|
+
{
|
|
99
|
+
className,
|
|
100
|
+
variant,
|
|
101
|
+
size,
|
|
102
|
+
asChild = false,
|
|
103
|
+
alt,
|
|
104
|
+
isDisabled,
|
|
105
|
+
isLoading,
|
|
106
|
+
title,
|
|
107
|
+
tooltipPlacement = 'top',
|
|
108
|
+
onClick,
|
|
109
|
+
type,
|
|
110
|
+
...props
|
|
111
|
+
},
|
|
76
112
|
ref,
|
|
77
113
|
) => {
|
|
78
114
|
useDeprecationWarning(
|
|
79
115
|
'Button.alt',
|
|
80
116
|
alt !== undefined,
|
|
81
|
-
'use
|
|
82
|
-
'`alt`
|
|
117
|
+
'use `title` instead — it drives the same tooltip and the accessible name. ' +
|
|
118
|
+
'`alt` remains an alias for one release.',
|
|
83
119
|
);
|
|
84
120
|
|
|
85
121
|
const Comp = asChild ? Slot : 'button';
|
|
86
122
|
// Default type="button" only when rendering an actual <button>. With asChild,
|
|
87
123
|
// the rendered element may be an <a> or other tag where injecting `type` is wrong.
|
|
88
124
|
const resolvedType = asChild ? type : (type ?? 'button');
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
|
|
125
|
+
// `title` (or its deprecated alias `alt`) drives the VTooltip below. `title` also back-fills
|
|
126
|
+
// the accessible name, so an icon-only <Button title="Refresh"> is labelled without any
|
|
127
|
+
// extra props or a manual <VTooltip> wrap at the call site.
|
|
128
|
+
//
|
|
129
|
+
// That back-fill is skipped when the button already shows text: turning
|
|
130
|
+
// <Button title="Show only installed apps">Installed</Button> into aria-label="Show only
|
|
131
|
+
// installed apps" would replace the visible name with the tooltip, which breaks WCAG 2.5.3
|
|
132
|
+
// (Label in Name) and makes the control unreachable by the label users actually see.
|
|
133
|
+
//
|
|
134
|
+
// `alt` deliberately does NOT get that treatment: it has always overridden the name
|
|
135
|
+
// unconditionally, and callers rely on that. Preserving it keeps every existing call site
|
|
136
|
+
// announcing exactly what it announced before, and confines the new rule to `title`.
|
|
137
|
+
const tooltip = title ?? alt;
|
|
138
|
+
const ariaLabel = props['aria-label'] ?? alt ?? (hasVisibleText(props.children) ? undefined : title);
|
|
92
139
|
|
|
93
140
|
// asChild renders via Radix Slot, which requires exactly one React child.
|
|
94
141
|
// Skip the loader wrap in that case — Slot would reject the multi-child array.
|
|
@@ -116,9 +163,18 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
116
163
|
</Comp>
|
|
117
164
|
);
|
|
118
165
|
|
|
119
|
-
|
|
166
|
+
// Note: the native `title` attribute is deliberately NOT forwarded to the DOM — it would
|
|
167
|
+
// stack a second, delayed browser tooltip on top of the VTooltip. `aria-label` above is
|
|
168
|
+
// what exposes the text to screen readers and to getByRole('button', { name }).
|
|
169
|
+
if (tooltip) {
|
|
120
170
|
return (
|
|
121
|
-
<VTooltip
|
|
171
|
+
<VTooltip
|
|
172
|
+
description={tooltip}
|
|
173
|
+
asChild
|
|
174
|
+
className="cursor-pointer"
|
|
175
|
+
size="xs"
|
|
176
|
+
placement={tooltipPlacement}
|
|
177
|
+
>
|
|
122
178
|
{buttonElement}
|
|
123
179
|
</VTooltip>
|
|
124
180
|
);
|
|
@@ -190,7 +246,6 @@ const CopyButton = React.forwardRef<HTMLButtonElement, CopyButtonProps>(
|
|
|
190
246
|
size={size || 'sm'}
|
|
191
247
|
onClick={handleCopy}
|
|
192
248
|
{...props}
|
|
193
|
-
aria-label={label}
|
|
194
249
|
title={label}
|
|
195
250
|
>
|
|
196
251
|
{isCopied ? <Check className="text-success" /> : <CopyIcon className="size-4" />}
|
|
@@ -88,7 +88,7 @@ export function Modal({
|
|
|
88
88
|
<DialogClose onClick={() => handleOpenChange(false)} asChild autoFocus={false}>
|
|
89
89
|
<Button
|
|
90
90
|
variant="outline"
|
|
91
|
-
|
|
91
|
+
title="Close"
|
|
92
92
|
className="data-[state=open]:bg-accent opacity-70 hover:opacity-100 rounded-sm focus:outline-none focus:ring-2 focus:ring-ring ring-offset-background focus:ring-offset-2 data-[state=open]:text-muted-foreground transition-opacity disabled:pointer-events-none"
|
|
93
93
|
>
|
|
94
94
|
<X className="size-4" />
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { render } from '@testing-library/react';
|
|
2
|
+
import type React from 'react';
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
|
|
5
|
+
// Capture the props the wrapper forwards to the underlying react-resizable-panels
|
|
6
|
+
// primitive, and stub out its layout machinery so the assertions run in jsdom.
|
|
7
|
+
const mocks = vi.hoisted(() => ({ panelProps: vi.fn() }));
|
|
8
|
+
|
|
9
|
+
vi.mock('react-resizable-panels', () => ({
|
|
10
|
+
Group: ({ children }: { children?: React.ReactNode }) => <div data-testid="group">{children}</div>,
|
|
11
|
+
Panel: (props: Record<string, unknown>) => {
|
|
12
|
+
mocks.panelProps(props);
|
|
13
|
+
return <div data-testid="panel" />;
|
|
14
|
+
},
|
|
15
|
+
Separator: ({ children }: { children?: React.ReactNode }) => <div data-testid="separator">{children}</div>,
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
import { ResizablePanel, ResizablePanelGroup } from './resizeable';
|
|
19
|
+
|
|
20
|
+
function forwardedPanelProps() {
|
|
21
|
+
return mocks.panelProps.mock.calls[0]?.[0] as Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('ResizablePanel size coercion', () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
mocks.panelProps.mockClear();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Regression guard for the react-resizable-panels v3 -> v4 upgrade: v4 treats bare
|
|
30
|
+
// numeric sizes as PIXELS (v3 and below treated them as percentages). Every call site
|
|
31
|
+
// passes percentage-intended numbers, so the wrapper must re-add the "%" unit — without
|
|
32
|
+
// it, panels get clamped to a few pixels and the split can no longer be resized.
|
|
33
|
+
it('coerces bare numeric sizes to percentage strings', () => {
|
|
34
|
+
render(
|
|
35
|
+
<ResizablePanelGroup direction="horizontal">
|
|
36
|
+
<ResizablePanel defaultSize={50} minSize={20} maxSize={80} collapsedSize={5} />
|
|
37
|
+
</ResizablePanelGroup>,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
expect(mocks.panelProps).toHaveBeenCalledTimes(1);
|
|
41
|
+
expect(forwardedPanelProps()).toMatchObject({
|
|
42
|
+
defaultSize: '50%',
|
|
43
|
+
minSize: '20%',
|
|
44
|
+
maxSize: '80%',
|
|
45
|
+
collapsedSize: '5%',
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('passes explicit unit strings through unchanged', () => {
|
|
50
|
+
render(
|
|
51
|
+
<ResizablePanelGroup direction="horizontal">
|
|
52
|
+
<ResizablePanel defaultSize="300px" minSize="10rem" maxSize="50%" />
|
|
53
|
+
</ResizablePanelGroup>,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
expect(mocks.panelProps).toHaveBeenCalledTimes(1);
|
|
57
|
+
expect(forwardedPanelProps()).toMatchObject({
|
|
58
|
+
defaultSize: '300px',
|
|
59
|
+
minSize: '10rem',
|
|
60
|
+
maxSize: '50%',
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('leaves unspecified sizes undefined', () => {
|
|
65
|
+
render(
|
|
66
|
+
<ResizablePanelGroup direction="horizontal">
|
|
67
|
+
<ResizablePanel />
|
|
68
|
+
</ResizablePanelGroup>,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
expect(mocks.panelProps).toHaveBeenCalledTimes(1);
|
|
72
|
+
const props = forwardedPanelProps();
|
|
73
|
+
expect(props.defaultSize).toBeUndefined();
|
|
74
|
+
expect(props.minSize).toBeUndefined();
|
|
75
|
+
expect(props.maxSize).toBeUndefined();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -23,8 +23,34 @@ function ResizablePanelGroup({ className, direction = 'horizontal', orientation,
|
|
|
23
23
|
);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
/**
|
|
27
|
+
* react-resizable-panels v4 treats bare numeric sizes as **pixels**; earlier majors
|
|
28
|
+
* (v3 and below) treated them as **percentages** of the group. Every call site in this
|
|
29
|
+
* codebase passes percentage-intended numbers (e.g. `defaultSize={50}`), so coerce bare
|
|
30
|
+
* numbers to percentage strings to preserve that contract after the v3→v4 upgrade.
|
|
31
|
+
* Explicit unit strings ("300px", "50%", "10rem", ...) are passed through untouched.
|
|
32
|
+
*/
|
|
33
|
+
function toPercentSize(value: number | string | undefined): number | string | undefined {
|
|
34
|
+
return typeof value === 'number' ? `${value}%` : value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function ResizablePanel({
|
|
38
|
+
defaultSize,
|
|
39
|
+
minSize,
|
|
40
|
+
maxSize,
|
|
41
|
+
collapsedSize,
|
|
42
|
+
...props
|
|
43
|
+
}: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
|
|
44
|
+
return (
|
|
45
|
+
<ResizablePrimitive.Panel
|
|
46
|
+
data-slot="resizable-panel"
|
|
47
|
+
defaultSize={toPercentSize(defaultSize)}
|
|
48
|
+
minSize={toPercentSize(minSize)}
|
|
49
|
+
maxSize={toPercentSize(maxSize)}
|
|
50
|
+
collapsedSize={toPercentSize(collapsedSize)}
|
|
51
|
+
{...props}
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
28
54
|
}
|
|
29
55
|
|
|
30
56
|
function ResizableHandle({
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { Tabs, TabsBar, TabsPanel } from './tabs';
|
|
4
|
+
|
|
5
|
+
const TABS = [
|
|
6
|
+
{ name: 'first', label: 'First', href: '#first', content: <div>First panel</div> },
|
|
7
|
+
{ name: 'second', label: 'Second', href: '#second', content: <div>Second panel</div> },
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
function renderTabs(canChangeTab?: (nextTab: string, currentTab?: string) => boolean) {
|
|
11
|
+
return render(
|
|
12
|
+
<Tabs tabs={TABS} defaultValue="first" canChangeTab={canChangeTab}>
|
|
13
|
+
<TabsBar />
|
|
14
|
+
<TabsPanel />
|
|
15
|
+
</Tabs>,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('Tabs canChangeTab', () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
window.history.replaceState(null, '', '/');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('cancels the switch and leaves the hash alone when the guard returns false', () => {
|
|
25
|
+
const canChangeTab = vi.fn().mockReturnValue(false);
|
|
26
|
+
renderTabs(canChangeTab);
|
|
27
|
+
|
|
28
|
+
fireEvent.click(screen.getByRole('tab', { name: 'Second' }));
|
|
29
|
+
|
|
30
|
+
expect(canChangeTab).toHaveBeenCalledWith('second', 'first');
|
|
31
|
+
expect(screen.getByText('First panel')).toBeDefined();
|
|
32
|
+
expect(screen.queryByText('Second panel')).toBeNull();
|
|
33
|
+
expect(window.location.hash).toBe('');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('allows the switch when the guard returns true', () => {
|
|
37
|
+
const canChangeTab = vi.fn().mockReturnValue(true);
|
|
38
|
+
renderTabs(canChangeTab);
|
|
39
|
+
|
|
40
|
+
fireEvent.click(screen.getByRole('tab', { name: 'Second' }));
|
|
41
|
+
|
|
42
|
+
expect(screen.getByText('Second panel')).toBeDefined();
|
|
43
|
+
expect(window.location.hash).toBe('#second');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('does not consult the guard when the active tab is clicked again', () => {
|
|
47
|
+
const canChangeTab = vi.fn().mockReturnValue(false);
|
|
48
|
+
renderTabs(canChangeTab);
|
|
49
|
+
|
|
50
|
+
fireEvent.click(screen.getByRole('tab', { name: 'First' }));
|
|
51
|
+
|
|
52
|
+
expect(canChangeTab).not.toHaveBeenCalled();
|
|
53
|
+
expect(screen.getByText('First panel')).toBeDefined();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('still switches from a hash change so a cancelled switch can be resumed', () => {
|
|
57
|
+
const canChangeTab = vi.fn().mockReturnValue(false);
|
|
58
|
+
renderTabs(canChangeTab);
|
|
59
|
+
|
|
60
|
+
window.location.hash = 'second';
|
|
61
|
+
fireEvent(window, new HashChangeEvent('hashchange'));
|
|
62
|
+
|
|
63
|
+
expect(screen.getByText('Second panel')).toBeDefined();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -15,6 +15,17 @@ export interface Tab {
|
|
|
15
15
|
is_allowed?: boolean;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Guard consulted before switching to another tab. Return `false` to cancel the switch — the active
|
|
20
|
+
* tab, the URL hash and the browser history are all left untouched, so the guard can hold the user
|
|
21
|
+
* on a tab that has unsaved changes while a confirmation is shown.
|
|
22
|
+
*
|
|
23
|
+
* It is only called when the target tab differs from the active one, but it may be called more than
|
|
24
|
+
* once for a single attempted switch (a click reaches both the trigger and the Radix root), so any
|
|
25
|
+
* side effect it performs must be idempotent.
|
|
26
|
+
*/
|
|
27
|
+
export type CanChangeTab = (nextTab: string, currentTab?: string) => boolean;
|
|
28
|
+
|
|
18
29
|
const TabsContext = React.createContext<{
|
|
19
30
|
size?: number;
|
|
20
31
|
tabs?: Tab[];
|
|
@@ -23,6 +34,7 @@ const TabsContext = React.createContext<{
|
|
|
23
34
|
responsive?: boolean;
|
|
24
35
|
variant?: 'tabs' | 'pills';
|
|
25
36
|
updateHash?: boolean;
|
|
37
|
+
canChangeTab?: CanChangeTab;
|
|
26
38
|
}>({
|
|
27
39
|
size: undefined,
|
|
28
40
|
tabs: undefined,
|
|
@@ -45,6 +57,7 @@ interface TabsProps {
|
|
|
45
57
|
responsive?: boolean;
|
|
46
58
|
variant?: 'tabs' | 'pills';
|
|
47
59
|
updateHash?: boolean;
|
|
60
|
+
canChangeTab?: CanChangeTab;
|
|
48
61
|
}
|
|
49
62
|
|
|
50
63
|
const Tabs = ({
|
|
@@ -59,6 +72,7 @@ const Tabs = ({
|
|
|
59
72
|
responsive = false,
|
|
60
73
|
variant = 'tabs',
|
|
61
74
|
updateHash = true,
|
|
75
|
+
canChangeTab,
|
|
62
76
|
}: TabsProps) => {
|
|
63
77
|
// Filter tabs based on is_allowed (undefined or true means visible)
|
|
64
78
|
const visibleTabs = React.useMemo(
|
|
@@ -121,6 +135,9 @@ const Tabs = ({
|
|
|
121
135
|
|
|
122
136
|
const handleValueChange = React.useCallback(
|
|
123
137
|
(newValue: string) => {
|
|
138
|
+
// Bail before touching the value or the history so a cancelled switch is a no-op.
|
|
139
|
+
if (newValue !== value && canChangeTab && !canChangeTab(newValue, value)) return;
|
|
140
|
+
|
|
124
141
|
setValue(newValue);
|
|
125
142
|
|
|
126
143
|
// Update the URL hash when tab changes (only if updateHash is true and not controlled by parent)
|
|
@@ -135,7 +152,7 @@ const Tabs = ({
|
|
|
135
152
|
onTabChange(newValue);
|
|
136
153
|
}
|
|
137
154
|
},
|
|
138
|
-
[current, onTabChange, updateHash],
|
|
155
|
+
[current, onTabChange, updateHash, canChangeTab, value],
|
|
139
156
|
);
|
|
140
157
|
|
|
141
158
|
const setTab = React.useCallback(
|
|
@@ -155,6 +172,7 @@ const Tabs = ({
|
|
|
155
172
|
responsive: responsive,
|
|
156
173
|
variant,
|
|
157
174
|
updateHash,
|
|
175
|
+
canChangeTab,
|
|
158
176
|
}}
|
|
159
177
|
>
|
|
160
178
|
<TabsPrimitive.Root
|
|
@@ -176,13 +194,17 @@ interface TabsBarProps {
|
|
|
176
194
|
}
|
|
177
195
|
|
|
178
196
|
const TabsBar = ({ className, sticky, direction }: TabsBarProps) => {
|
|
179
|
-
const { tabs, size, current, setTab, responsive, variant, updateHash } =
|
|
197
|
+
const { tabs, size, current, setTab, responsive, variant, updateHash, canChangeTab } =
|
|
198
|
+
React.useContext(TabsContext);
|
|
180
199
|
|
|
181
200
|
const fullWidth = size !== 0;
|
|
182
201
|
|
|
183
202
|
const handleTabChange = React.useCallback(
|
|
184
203
|
(tabName: string) => {
|
|
185
204
|
if (!tabs || !setTab) return;
|
|
205
|
+
// Consulted here too: this path pushes the tab href before `setTab` reaches the guard in
|
|
206
|
+
// `Tabs`, so a cancelled switch would otherwise leave the URL pointing at the other tab.
|
|
207
|
+
if (tabName !== current && canChangeTab && !canChangeTab(tabName, current)) return;
|
|
186
208
|
|
|
187
209
|
const tab = tabs.find((t) => t.name === tabName);
|
|
188
210
|
|
|
@@ -194,7 +216,7 @@ const TabsBar = ({ className, sticky, direction }: TabsBarProps) => {
|
|
|
194
216
|
|
|
195
217
|
setTab(tabName);
|
|
196
218
|
},
|
|
197
|
-
[tabs, setTab, updateHash],
|
|
219
|
+
[tabs, setTab, updateHash, canChangeTab, current],
|
|
198
220
|
);
|
|
199
221
|
|
|
200
222
|
if (!tabs || !setTab) {
|
|
@@ -45,5 +45,7 @@ export function useIntersectionObserver(
|
|
|
45
45
|
observer.unobserve(element);
|
|
46
46
|
}
|
|
47
47
|
};
|
|
48
|
-
|
|
48
|
+
// opts.deps let callers re-attach the observer once the target element mounts
|
|
49
|
+
// (e.g. an infinite-scroll sentinel that renders only after the first page loads).
|
|
50
|
+
}, [target, threshold, ...(opts.deps ?? [])]);
|
|
49
51
|
}
|
|
@@ -108,7 +108,6 @@ export function AgentChatPlaybackControls({
|
|
|
108
108
|
type="button"
|
|
109
109
|
variant="ghost"
|
|
110
110
|
size="icon"
|
|
111
|
-
aria-label={t('agent.rewind.jumpToStart')}
|
|
112
111
|
title={t('agent.rewind.jumpToStart')}
|
|
113
112
|
disabled={!canJumpToStart}
|
|
114
113
|
onClick={jumpToStart}
|
|
@@ -120,7 +119,6 @@ export function AgentChatPlaybackControls({
|
|
|
120
119
|
type="button"
|
|
121
120
|
variant="ghost"
|
|
122
121
|
size="icon"
|
|
123
|
-
aria-label={t('agent.rewind.previousMessage')}
|
|
124
122
|
title={t('agent.rewind.previousMessage')}
|
|
125
123
|
disabled={!canStepPrevious}
|
|
126
124
|
onClick={setPreviousMessage}
|
|
@@ -165,7 +163,6 @@ export function AgentChatPlaybackControls({
|
|
|
165
163
|
type="button"
|
|
166
164
|
variant="ghost"
|
|
167
165
|
size="icon"
|
|
168
|
-
aria-label={t('agent.rewind.nextMessage')}
|
|
169
166
|
title={t('agent.rewind.nextMessage')}
|
|
170
167
|
disabled={!canStepNext}
|
|
171
168
|
onClick={setNextMessage}
|
|
@@ -177,7 +174,6 @@ export function AgentChatPlaybackControls({
|
|
|
177
174
|
type="button"
|
|
178
175
|
variant="ghost"
|
|
179
176
|
size="icon"
|
|
180
|
-
aria-label={t('agent.rewind.jumpToLatest')}
|
|
181
177
|
title={t('agent.rewind.jumpToLatest')}
|
|
182
178
|
disabled={!canJumpToLatest}
|
|
183
179
|
onClick={jumpToLatest}
|
|
@@ -189,7 +185,6 @@ export function AgentChatPlaybackControls({
|
|
|
189
185
|
type="button"
|
|
190
186
|
variant={isLive ? 'secondary' : 'ghost'}
|
|
191
187
|
size="icon"
|
|
192
|
-
aria-label={t('agent.rewind.jumpToLive')}
|
|
193
188
|
title={t('agent.rewind.jumpToLive')}
|
|
194
189
|
disabled={isLive}
|
|
195
190
|
onClick={() => onChangeCursor('live')}
|
|
@@ -110,6 +110,37 @@ describe('AgentRequestInputOverlay', () => {
|
|
|
110
110
|
});
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
+
it('sends a tool approval option with structured metadata', () => {
|
|
114
|
+
const onSendMessage = vi.fn();
|
|
115
|
+
|
|
116
|
+
renderWithProviders(
|
|
117
|
+
<AgentRequestInputOverlay message={createToolApprovalRequestMessage()} onSendMessage={onSendMessage} />,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
fireEvent.click(screen.getByRole('button', { name: 'Allow this action for this run' }));
|
|
121
|
+
|
|
122
|
+
expect(onSendMessage).toHaveBeenCalledTimes(1);
|
|
123
|
+
expect(onSendMessage).toHaveBeenCalledWith('allow_for_run', {
|
|
124
|
+
tool_approval_response: {
|
|
125
|
+
decision: 'allow_for_run',
|
|
126
|
+
approval_key: 'write_artifact:name:quotes.md',
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('sends a bare option id for prompts that are not tool approvals', () => {
|
|
132
|
+
const onSendMessage = vi.fn();
|
|
133
|
+
const message = createToolApprovalRequestMessage();
|
|
134
|
+
delete (message.details as Record<string, unknown>).tool_approval;
|
|
135
|
+
|
|
136
|
+
renderWithProviders(<AgentRequestInputOverlay message={message} onSendMessage={onSendMessage} />);
|
|
137
|
+
|
|
138
|
+
fireEvent.click(screen.getByRole('button', { name: 'Allow once' }));
|
|
139
|
+
|
|
140
|
+
expect(onSendMessage).toHaveBeenCalledTimes(1);
|
|
141
|
+
expect(onSendMessage).toHaveBeenCalledWith('allow_once');
|
|
142
|
+
});
|
|
143
|
+
|
|
113
144
|
it('renders legacy field-prefixed tool approval prompts with a friendly target', () => {
|
|
114
145
|
const message = createToolApprovalRequestMessage();
|
|
115
146
|
message.message = 'Approve Write Artifact: name quotes.md?';
|
|
@@ -5,7 +5,11 @@ import { useUserSession } from '@vertesia/ui/session';
|
|
|
5
5
|
import { XIcon } from 'lucide-react';
|
|
6
6
|
import { RemoteMcpConnectionButton } from '../../oauth/RemoteMcpConnectionButton.js';
|
|
7
7
|
import { AskUserWidget } from './AskUserWidget';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
getRequestInputDisplayText,
|
|
10
|
+
getToolApprovalResponseMetadata,
|
|
11
|
+
type RequestInputMessageWithUx,
|
|
12
|
+
} from './ModernAgentOutput/requestInputMessages';
|
|
9
13
|
|
|
10
14
|
export interface AgentRequestInputOverlayProps {
|
|
11
15
|
message?: RequestInputMessageWithUx;
|
|
@@ -118,7 +122,7 @@ export function AgentRequestInputOverlay({
|
|
|
118
122
|
allowFreeResponse={options.length === 0 || !!freeResponse}
|
|
119
123
|
placeholder={freeResponse?.placeholder}
|
|
120
124
|
submitLabel={freeResponse?.submit_label}
|
|
121
|
-
onSelect={send}
|
|
125
|
+
onSelect={(optionId) => send(optionId, getToolApprovalResponseMetadata(message, optionId))}
|
|
122
126
|
onMultiSelect={(optionIds) => send(optionIds.join(', '))}
|
|
123
127
|
onSubmit={(value) => send(value, freeResponse?.metadata)}
|
|
124
128
|
hideBorder
|
|
@@ -537,7 +537,7 @@ function OverflowTabsBar({ tabs, current, onTabChange, className }: OverflowTabs
|
|
|
537
537
|
<div ref={containerRef} className={cn('relative', className)}>
|
|
538
538
|
{/* Hidden measurement row: all tabs + More at natural width, kept separate
|
|
539
539
|
from the visible row so measuring can't feed back into the layout. */}
|
|
540
|
-
<div aria-hidden className="pointer-events-none invisible absolute
|
|
540
|
+
<div aria-hidden className="pointer-events-none invisible absolute start-0 top-0 flex w-max gap-1">
|
|
541
541
|
{tabs.map((tab, i) => (
|
|
542
542
|
<button
|
|
543
543
|
type="button"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { renderWithProviders } from '../../../__tests__/test-utils.js';
|
|
4
|
+
import { AskUserWidget } from './AskUserWidget';
|
|
5
|
+
|
|
6
|
+
vi.mock('../../../widgets/markdown/MarkdownRenderer', () => ({
|
|
7
|
+
MarkdownRenderer: ({ children }: { children: ReactNode }) => <>{children}</>,
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
function getScrollablePrompt(container: HTMLElement): HTMLElement {
|
|
11
|
+
const question = container.querySelector('.agent-ask-question');
|
|
12
|
+
const scrollable = question?.parentElement;
|
|
13
|
+
if (!scrollable) {
|
|
14
|
+
throw new Error('Expected ask user question to be wrapped in a scrollable prompt container');
|
|
15
|
+
}
|
|
16
|
+
return scrollable;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('AskUserWidget', () => {
|
|
20
|
+
it('keeps compact prompt text in a bounded scrollable area', () => {
|
|
21
|
+
const { container } = renderWithProviders(
|
|
22
|
+
<AskUserWidget
|
|
23
|
+
compact
|
|
24
|
+
hideBorder
|
|
25
|
+
question={'Long prompt\n\n'.repeat(80)}
|
|
26
|
+
allowFreeResponse
|
|
27
|
+
submitLabel="Send"
|
|
28
|
+
/>,
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const scrollable = getScrollablePrompt(container);
|
|
32
|
+
|
|
33
|
+
expect(scrollable.className).toContain('max-h-80');
|
|
34
|
+
expect(scrollable.className).toContain('overflow-y-auto');
|
|
35
|
+
expect(scrollable.className).toContain('overscroll-contain');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('keeps default prompt text in a bounded scrollable area', () => {
|
|
39
|
+
const { container } = renderWithProviders(
|
|
40
|
+
<AskUserWidget question={'Long prompt\n\n'.repeat(80)} allowFreeResponse submitLabel="Send" />,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const scrollable = getScrollablePrompt(container);
|
|
44
|
+
|
|
45
|
+
expect(scrollable.className).toContain('max-h-80');
|
|
46
|
+
expect(scrollable.className).toContain('overflow-y-auto');
|
|
47
|
+
expect(scrollable.className).toContain('overscroll-contain');
|
|
48
|
+
});
|
|
49
|
+
});
|