@grackle-ai/web-components 0.107.2
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/.rush/temp/3ae72563f781afd72723475938136f113846603e.untar.log +10 -0
- package/.rush/temp/bc1d5bf9201ce71abeaeaddd096deb9b0805d703.untar.log +10 -0
- package/.rush/temp/operation/_phase_build/all.log +18 -0
- package/.rush/temp/operation/_phase_build/log-chunks.jsonl +18 -0
- package/.rush/temp/operation/_phase_build/state.json +3 -0
- package/.rush/temp/operation/_phase_test/all.log +121 -0
- package/.rush/temp/operation/_phase_test/log-chunks.jsonl +121 -0
- package/.rush/temp/operation/_phase_test/state.json +3 -0
- package/.rush/temp/shrinkwrap-deps.json +938 -0
- package/.storybook/main.ts +22 -0
- package/.storybook/preview.tsx +30 -0
- package/config/rig.json +4 -0
- package/config/rush-project.json +12 -0
- package/dist/index.css +1 -0
- package/dist/index.js +39221 -0
- package/eslint.config.cjs +5 -0
- package/package.json +83 -0
- package/rush-logs/web-components._phase_build.cache.log +4 -0
- package/rush-logs/web-components._phase_test.cache.log +4 -0
- package/src/components/chat/ChatInput.module.scss +81 -0
- package/src/components/chat/ChatInput.stories.tsx +91 -0
- package/src/components/chat/ChatInput.tsx +168 -0
- package/src/components/chat/index.ts +6 -0
- package/src/components/dag/DagView.module.scss +149 -0
- package/src/components/dag/DagView.stories.tsx +125 -0
- package/src/components/dag/DagView.tsx +109 -0
- package/src/components/dag/TaskNode.stories.tsx +133 -0
- package/src/components/dag/TaskNode.tsx +40 -0
- package/src/components/dag/useDagLayout.ts +139 -0
- package/src/components/display/Breadcrumbs.module.scss +71 -0
- package/src/components/display/Breadcrumbs.stories.tsx +80 -0
- package/src/components/display/Breadcrumbs.tsx +46 -0
- package/src/components/display/Button.module.scss +110 -0
- package/src/components/display/Button.stories.tsx +88 -0
- package/src/components/display/Button.tsx +40 -0
- package/src/components/display/ConfirmDialog.module.scss +67 -0
- package/src/components/display/ConfirmDialog.stories.tsx +81 -0
- package/src/components/display/ConfirmDialog.tsx +88 -0
- package/src/components/display/CopyButton.module.scss +41 -0
- package/src/components/display/CopyButton.stories.tsx +78 -0
- package/src/components/display/CopyButton.tsx +64 -0
- package/src/components/display/DemoBanner.module.scss +37 -0
- package/src/components/display/DemoBanner.stories.tsx +40 -0
- package/src/components/display/DemoBanner.tsx +23 -0
- package/src/components/display/EventHoverRow.module.scss +102 -0
- package/src/components/display/EventHoverRow.stories.tsx +99 -0
- package/src/components/display/EventHoverRow.tsx +154 -0
- package/src/components/display/EventRenderer.module.scss +272 -0
- package/src/components/display/EventRenderer.stories.tsx +186 -0
- package/src/components/display/EventRenderer.tsx +271 -0
- package/src/components/display/EventStream.module.scss +93 -0
- package/src/components/display/EventStream.stories.tsx +249 -0
- package/src/components/display/EventStream.tsx +369 -0
- package/src/components/display/FloatingActionBar.module.scss +107 -0
- package/src/components/display/FloatingActionBar.stories.tsx +122 -0
- package/src/components/display/FloatingActionBar.tsx +119 -0
- package/src/components/display/SessionAttemptSelector.module.scss +50 -0
- package/src/components/display/SessionAttemptSelector.stories.tsx +78 -0
- package/src/components/display/SessionAttemptSelector.tsx +49 -0
- package/src/components/display/SessionPicker.module.scss +200 -0
- package/src/components/display/SessionPicker.stories.tsx +169 -0
- package/src/components/display/SessionPicker.tsx +214 -0
- package/src/components/display/Skeleton.module.scss +58 -0
- package/src/components/display/Skeleton.stories.tsx +94 -0
- package/src/components/display/Skeleton.tsx +127 -0
- package/src/components/display/Spinner.module.scss +41 -0
- package/src/components/display/Spinner.stories.tsx +66 -0
- package/src/components/display/Spinner.tsx +32 -0
- package/src/components/display/SplashScreen.module.scss +20 -0
- package/src/components/display/SplashScreen.stories.tsx +26 -0
- package/src/components/display/SplashScreen.tsx +16 -0
- package/src/components/display/SplitButton.module.scss +166 -0
- package/src/components/display/SplitButton.stories.tsx +95 -0
- package/src/components/display/SplitButton.tsx +128 -0
- package/src/components/display/Tooltip.module.scss +84 -0
- package/src/components/display/Tooltip.stories.tsx +240 -0
- package/src/components/display/Tooltip.tsx +184 -0
- package/src/components/display/extractText.test.tsx +48 -0
- package/src/components/display/index.ts +20 -0
- package/src/components/editable/EditableCheckbox.stories.tsx +54 -0
- package/src/components/editable/EditableCheckbox.tsx +39 -0
- package/src/components/editable/EditableField.module.scss +135 -0
- package/src/components/editable/EditableSelect.tsx +164 -0
- package/src/components/editable/EditableTextArea.stories.tsx +50 -0
- package/src/components/editable/EditableTextArea.tsx +148 -0
- package/src/components/editable/EditableTextField.stories.tsx +62 -0
- package/src/components/editable/EditableTextField.tsx +153 -0
- package/src/components/editable/EnvironmentSelect.module.scss +17 -0
- package/src/components/editable/EnvironmentSelect.stories.tsx +61 -0
- package/src/components/editable/EnvironmentSelect.tsx +87 -0
- package/src/components/editable/index.ts +13 -0
- package/src/components/editable/useEditableField.test.tsx +233 -0
- package/src/components/editable/useEditableField.ts +173 -0
- package/src/components/index.ts +20 -0
- package/src/components/knowledge/KnowledgeDetailPanel.module.scss +162 -0
- package/src/components/knowledge/KnowledgeDetailPanel.stories.tsx +208 -0
- package/src/components/knowledge/KnowledgeDetailPanel.tsx +122 -0
- package/src/components/knowledge/KnowledgeGraph.module.scss +110 -0
- package/src/components/knowledge/KnowledgeGraph.stories.tsx +180 -0
- package/src/components/knowledge/KnowledgeGraph.tsx +455 -0
- package/src/components/knowledge/KnowledgeNav.module.scss +130 -0
- package/src/components/knowledge/KnowledgeNav.stories.tsx +108 -0
- package/src/components/knowledge/KnowledgeNav.tsx +138 -0
- package/src/components/knowledge/index.ts +3 -0
- package/src/components/layout/AppNav.module.scss +82 -0
- package/src/components/layout/AppNav.stories.tsx +115 -0
- package/src/components/layout/AppNav.tsx +133 -0
- package/src/components/layout/BottomStatusBar.module.scss +58 -0
- package/src/components/layout/BottomStatusBar.stories.tsx +35 -0
- package/src/components/layout/BottomStatusBar.tsx +206 -0
- package/src/components/layout/Sidebar.module.scss +60 -0
- package/src/components/layout/Sidebar.stories.tsx +46 -0
- package/src/components/layout/Sidebar.tsx +84 -0
- package/src/components/layout/StatusBar.module.scss +108 -0
- package/src/components/layout/StatusBar.stories.tsx +119 -0
- package/src/components/layout/StatusBar.tsx +70 -0
- package/src/components/layout/index.ts +9 -0
- package/src/components/lists/EnvironmentNav.module.scss +118 -0
- package/src/components/lists/EnvironmentNav.stories.tsx +121 -0
- package/src/components/lists/EnvironmentNav.tsx +133 -0
- package/src/components/lists/FindingsNav.module.scss +126 -0
- package/src/components/lists/FindingsNav.tsx +146 -0
- package/src/components/lists/TaskList.module.scss +206 -0
- package/src/components/lists/TaskList.stories.tsx +401 -0
- package/src/components/lists/TaskList.tsx +509 -0
- package/src/components/lists/index.ts +6 -0
- package/src/components/lists/listHelpers.tsx +130 -0
- package/src/components/notifications/Callout.module.scss +83 -0
- package/src/components/notifications/Callout.stories.tsx +81 -0
- package/src/components/notifications/Callout.tsx +64 -0
- package/src/components/notifications/Toast.module.scss +86 -0
- package/src/components/notifications/Toast.stories.tsx +71 -0
- package/src/components/notifications/Toast.tsx +51 -0
- package/src/components/notifications/ToastContainer.module.scss +23 -0
- package/src/components/notifications/ToastContainer.stories.tsx +66 -0
- package/src/components/notifications/ToastContainer.tsx +29 -0
- package/src/components/notifications/UpdateBanner.stories.tsx +77 -0
- package/src/components/notifications/UpdateBanner.test.tsx +64 -0
- package/src/components/notifications/UpdateBanner.tsx +44 -0
- package/src/components/notifications/index.ts +8 -0
- package/src/components/panels/AboutPanel.stories.tsx +70 -0
- package/src/components/panels/AboutPanel.tsx +66 -0
- package/src/components/panels/AppearancePanel.stories.tsx +45 -0
- package/src/components/panels/AppearancePanel.tsx +97 -0
- package/src/components/panels/CredentialProvidersPanel.stories.tsx +62 -0
- package/src/components/panels/CredentialProvidersPanel.tsx +111 -0
- package/src/components/panels/EnvironmentEditPanel.module.scss +170 -0
- package/src/components/panels/EnvironmentEditPanel.stories.tsx +206 -0
- package/src/components/panels/EnvironmentEditPanel.tsx +785 -0
- package/src/components/panels/FindingsPanel.module.scss +94 -0
- package/src/components/panels/FindingsPanel.stories.tsx +109 -0
- package/src/components/panels/FindingsPanel.tsx +76 -0
- package/src/components/panels/KeyboardShortcutsPanel.module.scss +65 -0
- package/src/components/panels/KeyboardShortcutsPanel.stories.tsx +40 -0
- package/src/components/panels/KeyboardShortcutsPanel.tsx +104 -0
- package/src/components/panels/PluginsPanel.tsx +77 -0
- package/src/components/panels/SettingsPanel.module.scss +336 -0
- package/src/components/panels/TaskActionButtons.module.scss +22 -0
- package/src/components/panels/TaskActionButtons.stories.tsx +125 -0
- package/src/components/panels/TaskActionButtons.tsx +87 -0
- package/src/components/panels/TaskEditPanel.module.scss +202 -0
- package/src/components/panels/TaskEditPanel.stories.tsx +75 -0
- package/src/components/panels/TaskEditPanel.tsx +328 -0
- package/src/components/panels/TaskOverviewPanel.module.scss +236 -0
- package/src/components/panels/TaskOverviewPanel.stories.tsx +219 -0
- package/src/components/panels/TaskOverviewPanel.tsx +270 -0
- package/src/components/panels/TokensPanel.stories.tsx +131 -0
- package/src/components/panels/TokensPanel.tsx +143 -0
- package/src/components/panels/WorkpadPanel.module.scss +39 -0
- package/src/components/panels/WorkpadPanel.stories.tsx +56 -0
- package/src/components/panels/WorkpadPanel.tsx +63 -0
- package/src/components/panels/index.ts +13 -0
- package/src/components/personas/McpToolSelector.module.scss +109 -0
- package/src/components/personas/McpToolSelector.stories.tsx +129 -0
- package/src/components/personas/McpToolSelector.tsx +180 -0
- package/src/components/personas/PersonaManager.module.scss +233 -0
- package/src/components/personas/PersonaManager.stories.tsx +139 -0
- package/src/components/personas/PersonaManager.tsx +122 -0
- package/src/components/schedules/ScheduleManager.module.scss +98 -0
- package/src/components/schedules/ScheduleManager.stories.tsx +78 -0
- package/src/components/schedules/ScheduleManager.tsx +160 -0
- package/src/components/settings/SettingsNav.module.scss +82 -0
- package/src/components/settings/SettingsNav.stories.tsx +83 -0
- package/src/components/settings/SettingsNav.tsx +104 -0
- package/src/components/streams/StreamDetailPanel.module.scss +206 -0
- package/src/components/streams/StreamDetailPanel.stories.tsx +132 -0
- package/src/components/streams/StreamDetailPanel.tsx +119 -0
- package/src/components/streams/StreamList.module.scss +92 -0
- package/src/components/streams/StreamList.stories.tsx +99 -0
- package/src/components/streams/StreamList.tsx +114 -0
- package/src/components/streams/index.ts +10 -0
- package/src/components/tools/AgentToolCard.module.scss +118 -0
- package/src/components/tools/AgentToolCard.stories.tsx +304 -0
- package/src/components/tools/AgentToolCard.tsx +247 -0
- package/src/components/tools/FileEditCard.stories.tsx +138 -0
- package/src/components/tools/FileEditCard.tsx +160 -0
- package/src/components/tools/FileReadCard.stories.tsx +120 -0
- package/src/components/tools/FileReadCard.tsx +106 -0
- package/src/components/tools/FindingCard.stories.tsx +124 -0
- package/src/components/tools/FindingCard.tsx +178 -0
- package/src/components/tools/GenericToolCard.stories.tsx +80 -0
- package/src/components/tools/GenericToolCard.tsx +111 -0
- package/src/components/tools/IpcCard.stories.tsx +129 -0
- package/src/components/tools/IpcCard.tsx +178 -0
- package/src/components/tools/KnowledgeCard.stories.tsx +112 -0
- package/src/components/tools/KnowledgeCard.tsx +165 -0
- package/src/components/tools/MetadataCard.stories.tsx +32 -0
- package/src/components/tools/MetadataCard.tsx +39 -0
- package/src/components/tools/SearchCard.stories.tsx +74 -0
- package/src/components/tools/SearchCard.tsx +86 -0
- package/src/components/tools/ShellCard.stories.tsx +112 -0
- package/src/components/tools/ShellCard.tsx +106 -0
- package/src/components/tools/TaskCard.stories.tsx +123 -0
- package/src/components/tools/TaskCard.tsx +203 -0
- package/src/components/tools/TodoCard.module.scss +131 -0
- package/src/components/tools/TodoCard.stories.tsx +202 -0
- package/src/components/tools/TodoCard.tsx +200 -0
- package/src/components/tools/ToolCard.stories.tsx +177 -0
- package/src/components/tools/ToolCard.tsx +60 -0
- package/src/components/tools/ToolCardProps.ts +20 -0
- package/src/components/tools/ToolSearchCard.stories.tsx +81 -0
- package/src/components/tools/ToolSearchCard.tsx +86 -0
- package/src/components/tools/WorkpadCard.stories.tsx +106 -0
- package/src/components/tools/WorkpadCard.tsx +125 -0
- package/src/components/tools/classifyTool.test.ts +44 -0
- package/src/components/tools/classifyTool.ts +134 -0
- package/src/components/tools/parseDiff.ts +95 -0
- package/src/components/tools/parseShellOutput.ts +28 -0
- package/src/components/tools/toolCardHelpers.test.ts +53 -0
- package/src/components/tools/toolCards.module.scss +234 -0
- package/src/components/workspace/WorkspaceBoard.module.scss +238 -0
- package/src/components/workspace/WorkspaceBoard.stories.tsx +240 -0
- package/src/components/workspace/WorkspaceBoard.tsx +232 -0
- package/src/components/workspace/WorkspaceFormFields.module.scss +79 -0
- package/src/components/workspace/WorkspaceFormFields.stories.tsx +133 -0
- package/src/components/workspace/WorkspaceFormFields.tsx +185 -0
- package/src/context/GrackleContext.ts +28 -0
- package/src/context/GrackleContextTypes.ts +64 -0
- package/src/context/SidebarContext.tsx +53 -0
- package/src/context/ThemeContext.tsx +21 -0
- package/src/context/ToastContext.tsx +56 -0
- package/src/hooks/types.ts +864 -0
- package/src/hooks/useEventSelection.test.ts +204 -0
- package/src/hooks/useEventSelection.ts +158 -0
- package/src/hooks/useSmartScroll.ts +151 -0
- package/src/hooks/useTheme.ts +228 -0
- package/src/index.ts +210 -0
- package/src/mocks/MockGrackleProvider.tsx +1397 -0
- package/src/mocks/mockData.ts +1966 -0
- package/src/mocks/mockKnowledgeData.ts +294 -0
- package/src/scss.d.ts +12 -0
- package/src/styles/global.scss +244 -0
- package/src/styles/mixins.scss +278 -0
- package/src/styles/prism-theme.scss +148 -0
- package/src/styles/theme.scss +1102 -0
- package/src/test-utils/storybook-decorators.tsx +50 -0
- package/src/test-utils/storybook-helpers.ts +262 -0
- package/src/themes.ts +142 -0
- package/src/utils/boardColumns.ts +141 -0
- package/src/utils/breadcrumbs.test.ts +285 -0
- package/src/utils/breadcrumbs.ts +222 -0
- package/src/utils/dashboard.test.ts +156 -0
- package/src/utils/dashboard.ts +195 -0
- package/src/utils/eventContent.test.ts +353 -0
- package/src/utils/eventContent.ts +209 -0
- package/src/utils/findingCategory.ts +33 -0
- package/src/utils/format.ts +27 -0
- package/src/utils/iconSize.ts +18 -0
- package/src/utils/navigation.ts +205 -0
- package/src/utils/route-config.test.ts +128 -0
- package/src/utils/scrollUtils.test.ts +65 -0
- package/src/utils/scrollUtils.ts +49 -0
- package/src/utils/sessionEvents.test.ts +302 -0
- package/src/utils/sessionEvents.ts +233 -0
- package/src/utils/taskStatus.tsx +137 -0
- package/src/utils/time.ts +92 -0
- package/tsconfig.json +8 -0
- package/vite.config.ts +20 -0
- package/vitest.config.ts +10 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState, type JSX, type ReactNode } from "react";
|
|
2
|
+
import { AlertTriangle, ArrowDown, ArrowUp } from "lucide-react";
|
|
3
|
+
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
|
4
|
+
import { EventRenderer } from "./EventRenderer.js";
|
|
5
|
+
import { EventHoverRow } from "./EventHoverRow.js";
|
|
6
|
+
import { FloatingActionBar } from "./FloatingActionBar.js";
|
|
7
|
+
import { SessionPicker } from "./SessionPicker.js";
|
|
8
|
+
import { ConfirmDialog } from "./ConfirmDialog.js";
|
|
9
|
+
import { Tooltip } from "./Tooltip.js";
|
|
10
|
+
import { useSmartScroll } from "../../hooks/useSmartScroll.js";
|
|
11
|
+
import { useEventSelection } from "../../hooks/useEventSelection.js";
|
|
12
|
+
import { isContentBearingEvent, getEventCopyText, formatEventsAsMarkdown, formatForwardEnvelope } from "../../utils/eventContent.js";
|
|
13
|
+
import type { ToastVariant } from "../../context/ToastContext.js";
|
|
14
|
+
import { ICON_MD } from "../../utils/iconSize.js";
|
|
15
|
+
import type { DisplayEvent } from "../../utils/sessionEvents.js";
|
|
16
|
+
import type { Session, Environment, PersonaData, SessionEvent } from "../../hooks/types.js";
|
|
17
|
+
import styles from "./EventStream.module.scss";
|
|
18
|
+
|
|
19
|
+
/** Byte size threshold above which a large-message confirmation is shown (10 KB). */
|
|
20
|
+
const LARGE_MESSAGE_THRESHOLD_BYTES: number = 10 * 1024;
|
|
21
|
+
|
|
22
|
+
/** Active session statuses eligible as forward targets. */
|
|
23
|
+
const ACTIVE_STATUSES: ReadonlySet<string> = new Set(["running", "idle"]);
|
|
24
|
+
|
|
25
|
+
/** Build a descriptive label for the selection checkbox aria-label. */
|
|
26
|
+
function buildCheckboxLabel(event: SessionEvent): string {
|
|
27
|
+
const time = new Date(event.timestamp).toLocaleTimeString();
|
|
28
|
+
switch (event.eventType) {
|
|
29
|
+
case "text":
|
|
30
|
+
case "output":
|
|
31
|
+
return `Select message from assistant at ${time}`;
|
|
32
|
+
case "user_input":
|
|
33
|
+
return `Select message from user at ${time}`;
|
|
34
|
+
case "tool_result":
|
|
35
|
+
case "tool_use":
|
|
36
|
+
return `Select tool event at ${time}`;
|
|
37
|
+
case "error":
|
|
38
|
+
return `Select error at ${time}`;
|
|
39
|
+
default:
|
|
40
|
+
return `Select event at ${time}`;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** localStorage key for persisting the direction preference. */
|
|
45
|
+
const DIRECTION_STORAGE_KEY: string = "grackle-stream-direction";
|
|
46
|
+
|
|
47
|
+
/** Read initial direction from localStorage. */
|
|
48
|
+
function readStoredDirection(): boolean {
|
|
49
|
+
try {
|
|
50
|
+
return localStorage.getItem(DIRECTION_STORAGE_KEY) === "reversed";
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Overflow warning banner shown when events exceed the in-memory cap. */
|
|
57
|
+
function EventOverflowBanner({ eventsDropped }: { eventsDropped: number }): JSX.Element {
|
|
58
|
+
if (eventsDropped <= 0) {
|
|
59
|
+
return <></>;
|
|
60
|
+
}
|
|
61
|
+
return (
|
|
62
|
+
<div className={styles.eventOverflowWarning} role="alert">
|
|
63
|
+
<AlertTriangle size={ICON_MD} aria-hidden="true" /> {eventsDropped.toLocaleString()} older event{eventsDropped === 1 ? "" : "s"} were dropped — only the most recent 5,000 are shown. Full history is available in the session log.
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Props for the EventStream component. */
|
|
69
|
+
interface EventStreamProps {
|
|
70
|
+
/** Events to render. */
|
|
71
|
+
events: DisplayEvent[];
|
|
72
|
+
/** Number of events dropped due to the in-memory cap. */
|
|
73
|
+
eventsDropped: number;
|
|
74
|
+
/** Custom empty state content (e.g., CTA button or waiting message). */
|
|
75
|
+
emptyState?: ReactNode;
|
|
76
|
+
/** Toast callback for copy feedback. If omitted, no toast is shown. */
|
|
77
|
+
onShowToast?: (message: string, variant?: ToastVariant) => void;
|
|
78
|
+
/** All known sessions (used to build the forward-target picker). */
|
|
79
|
+
sessions?: Session[];
|
|
80
|
+
/** ID of the session currently being viewed (excluded from forward picker). */
|
|
81
|
+
currentSessionId?: string;
|
|
82
|
+
/** All known environments (used to look up display names in the forward picker). */
|
|
83
|
+
environments?: Environment[];
|
|
84
|
+
/** All known personas (used to show persona name in the session picker). */
|
|
85
|
+
personas?: PersonaData[];
|
|
86
|
+
/**
|
|
87
|
+
* Called when the user forwards selected events to another session.
|
|
88
|
+
* Receives the target session ID and the formatted envelope text.
|
|
89
|
+
*/
|
|
90
|
+
onForward?: (sessionId: string, text: string) => Promise<void>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Scrollable event stream with smart auto-scroll, direction toggle,
|
|
95
|
+
* animated entry for new events, hover actions, and multi-select mode.
|
|
96
|
+
*/
|
|
97
|
+
export function EventStream({
|
|
98
|
+
events,
|
|
99
|
+
eventsDropped,
|
|
100
|
+
emptyState,
|
|
101
|
+
onShowToast,
|
|
102
|
+
sessions,
|
|
103
|
+
currentSessionId,
|
|
104
|
+
environments,
|
|
105
|
+
personas,
|
|
106
|
+
onForward,
|
|
107
|
+
}: EventStreamProps): JSX.Element {
|
|
108
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
109
|
+
const [isReversed, setIsReversed] = useState(readStoredDirection);
|
|
110
|
+
const shouldReduceMotion = useReducedMotion();
|
|
111
|
+
|
|
112
|
+
// Forward flow state
|
|
113
|
+
const [showSessionPicker, setShowSessionPicker] = useState(false);
|
|
114
|
+
const [confirmLargeMessage, setConfirmLargeMessage] = useState(false);
|
|
115
|
+
const [pendingForward, setPendingForward] = useState<{ sessionId: string; text: string } | undefined>(undefined);
|
|
116
|
+
|
|
117
|
+
// Multi-select state
|
|
118
|
+
const selection = useEventSelection({
|
|
119
|
+
events,
|
|
120
|
+
formatForClipboard: formatEventsAsMarkdown,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Count of selectable events (for floating action bar)
|
|
124
|
+
const totalSelectable = useMemo(
|
|
125
|
+
() => events.filter(isContentBearingEvent).length,
|
|
126
|
+
[events],
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// Active sessions that can receive a forwarded message (excluding current)
|
|
130
|
+
const forwardTargets = useMemo<Session[]>(() => {
|
|
131
|
+
if (!sessions) {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
return sessions.filter(
|
|
135
|
+
(s) => ACTIVE_STATUSES.has(s.status) && s.id !== currentSessionId,
|
|
136
|
+
);
|
|
137
|
+
}, [sessions, currentSessionId]);
|
|
138
|
+
|
|
139
|
+
const displayEvents = useMemo(() => {
|
|
140
|
+
if (!isReversed) {
|
|
141
|
+
return events;
|
|
142
|
+
}
|
|
143
|
+
return [...events].reverse();
|
|
144
|
+
}, [events, isReversed]);
|
|
145
|
+
|
|
146
|
+
const { isAtAnchor, scrollToAnchor } = useSmartScroll({
|
|
147
|
+
scrollRef,
|
|
148
|
+
contentLength: events.length,
|
|
149
|
+
isReversed,
|
|
150
|
+
paused: selection.isSelecting,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const handleToggleDirection = (): void => {
|
|
154
|
+
const next = !isReversed;
|
|
155
|
+
setIsReversed(next);
|
|
156
|
+
try {
|
|
157
|
+
localStorage.setItem(DIRECTION_STORAGE_KEY, next ? "reversed" : "default");
|
|
158
|
+
} catch { /* storage unavailable */ }
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Escape key exits selection mode, but not while a modal is open
|
|
162
|
+
// (the modal's own Escape handler takes priority)
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
if (!selection.isSelecting) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const handler = (e: KeyboardEvent): void => {
|
|
168
|
+
if (e.key === "Escape" && !showSessionPicker && !confirmLargeMessage) {
|
|
169
|
+
selection.cancelSelection();
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
window.addEventListener("keydown", handler);
|
|
173
|
+
return () => { window.removeEventListener("keydown", handler); };
|
|
174
|
+
}, [selection.isSelecting, selection.cancelSelection, showSessionPicker, confirmLargeMessage]);
|
|
175
|
+
|
|
176
|
+
// Copy handler for the floating action bar
|
|
177
|
+
const handleCopySelected = useCallback(async () => {
|
|
178
|
+
const ok = await selection.copySelected();
|
|
179
|
+
if (ok) {
|
|
180
|
+
onShowToast?.(`Copied ${selection.selectedCount} message${selection.selectedCount === 1 ? "" : "s"} to clipboard`, "success");
|
|
181
|
+
}
|
|
182
|
+
}, [selection, onShowToast]);
|
|
183
|
+
|
|
184
|
+
/** Build the sorted list of selected DisplayEvents in chronological order. */
|
|
185
|
+
const getSelectedEvents = useCallback((): DisplayEvent[] => {
|
|
186
|
+
const sorted = [...selection.selectedIndices].sort((a, b) => a - b);
|
|
187
|
+
return sorted
|
|
188
|
+
.filter((i) => i < events.length)
|
|
189
|
+
.map((i) => events[i]);
|
|
190
|
+
}, [selection.selectedIndices, events]);
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Returns a human-readable label for a session by its ID.
|
|
194
|
+
* Falls back to "this session" when sessionId is undefined/empty.
|
|
195
|
+
*/
|
|
196
|
+
const getSessionLabel = useCallback((sessionId: string | undefined): string => {
|
|
197
|
+
if (!sessionId) {
|
|
198
|
+
return "this session";
|
|
199
|
+
}
|
|
200
|
+
const session = sessions?.find((s) => s.id === sessionId);
|
|
201
|
+
if (!session) {
|
|
202
|
+
return sessionId.slice(0, 8);
|
|
203
|
+
}
|
|
204
|
+
const env = environments?.find((e) => e.id === session.environmentId);
|
|
205
|
+
return env?.displayName ?? session.environmentId.slice(0, 8);
|
|
206
|
+
}, [sessions, environments]);
|
|
207
|
+
|
|
208
|
+
/** Execute the actual forward after all confirmations. */
|
|
209
|
+
const executeForward = useCallback(async (sessionId: string, text: string) => {
|
|
210
|
+
if (!onForward) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const targetLabel = getSessionLabel(sessionId);
|
|
214
|
+
try {
|
|
215
|
+
await onForward(sessionId, text);
|
|
216
|
+
const count = selection.selectedCount;
|
|
217
|
+
onShowToast?.(`Forwarded ${count} message${count === 1 ? "" : "s"} to ${targetLabel}`, "success");
|
|
218
|
+
selection.cancelSelection();
|
|
219
|
+
} catch {
|
|
220
|
+
onShowToast?.("Failed to forward messages", "error");
|
|
221
|
+
}
|
|
222
|
+
}, [onForward, getSessionLabel, onShowToast, selection]);
|
|
223
|
+
|
|
224
|
+
/** Called when the user picks a target session in the picker. */
|
|
225
|
+
const handlePickSession = useCallback((sessionId: string) => {
|
|
226
|
+
setShowSessionPicker(false);
|
|
227
|
+
|
|
228
|
+
const selectedEvents = getSelectedEvents();
|
|
229
|
+
const sourceLabel = getSessionLabel(currentSessionId);
|
|
230
|
+
const envelope = formatForwardEnvelope(sourceLabel, selectedEvents);
|
|
231
|
+
|
|
232
|
+
if (new TextEncoder().encode(envelope).length > LARGE_MESSAGE_THRESHOLD_BYTES) {
|
|
233
|
+
setPendingForward({ sessionId, text: envelope });
|
|
234
|
+
setConfirmLargeMessage(true);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
executeForward(sessionId, envelope).catch(() => {});
|
|
239
|
+
}, [getSelectedEvents, getSessionLabel, currentSessionId, executeForward]);
|
|
240
|
+
|
|
241
|
+
const handleConfirmLargeMessage = useCallback(() => {
|
|
242
|
+
setConfirmLargeMessage(false);
|
|
243
|
+
if (pendingForward) {
|
|
244
|
+
executeForward(pendingForward.sessionId, pendingForward.text).catch(() => {});
|
|
245
|
+
setPendingForward(undefined);
|
|
246
|
+
}
|
|
247
|
+
}, [pendingForward, executeForward]);
|
|
248
|
+
|
|
249
|
+
const handleCancelLargeMessage = useCallback(() => {
|
|
250
|
+
setConfirmLargeMessage(false);
|
|
251
|
+
setPendingForward(undefined);
|
|
252
|
+
}, []);
|
|
253
|
+
|
|
254
|
+
const animationDuration = shouldReduceMotion ? 0 : 0.2;
|
|
255
|
+
const enterY = isReversed ? -8 : 8;
|
|
256
|
+
|
|
257
|
+
const largeMessageSizeKb = pendingForward
|
|
258
|
+
? Math.round(new TextEncoder().encode(pendingForward.text).length / 1024)
|
|
259
|
+
: 0;
|
|
260
|
+
|
|
261
|
+
return (
|
|
262
|
+
<div className={styles.wrapper}>
|
|
263
|
+
{/* Direction toggle */}
|
|
264
|
+
<div className={styles.toolbar}>
|
|
265
|
+
<Tooltip text={isReversed ? "Showing newest first" : "Showing oldest first"}>
|
|
266
|
+
<button
|
|
267
|
+
className={styles.directionToggle}
|
|
268
|
+
onClick={handleToggleDirection}
|
|
269
|
+
aria-label={isReversed ? "Switch to newest at bottom" : "Switch to newest at top"}
|
|
270
|
+
data-testid="direction-toggle"
|
|
271
|
+
>
|
|
272
|
+
{isReversed ? <ArrowDown size={ICON_MD} aria-hidden="true" /> : <ArrowUp size={ICON_MD} aria-hidden="true" />}
|
|
273
|
+
</button>
|
|
274
|
+
</Tooltip>
|
|
275
|
+
</div>
|
|
276
|
+
|
|
277
|
+
{/* Scroll container */}
|
|
278
|
+
<div
|
|
279
|
+
ref={scrollRef}
|
|
280
|
+
className={`${styles.scrollContainer} ${selection.isSelecting ? styles.selectingPadding : ""}`}
|
|
281
|
+
data-testid="event-stream-scroll"
|
|
282
|
+
>
|
|
283
|
+
{events.length === 0 && emptyState}
|
|
284
|
+
<EventOverflowBanner eventsDropped={eventsDropped} />
|
|
285
|
+
<AnimatePresence initial={false}>
|
|
286
|
+
{displayEvents.map((event, displayIndex) => {
|
|
287
|
+
// Use original index for stable keys regardless of direction
|
|
288
|
+
const originalIndex = isReversed ? events.length - 1 - displayIndex : displayIndex;
|
|
289
|
+
return (
|
|
290
|
+
<motion.div
|
|
291
|
+
key={`${event.sessionId}-${event.timestamp}-${originalIndex}`}
|
|
292
|
+
initial={{ opacity: 0, y: enterY }}
|
|
293
|
+
animate={{ opacity: 1, y: 0 }}
|
|
294
|
+
transition={{ duration: animationDuration, ease: "easeOut" }}
|
|
295
|
+
>
|
|
296
|
+
<EventHoverRow
|
|
297
|
+
copyText={getEventCopyText(event)}
|
|
298
|
+
isContentBearing={isContentBearingEvent(event)}
|
|
299
|
+
isSelecting={selection.isSelecting}
|
|
300
|
+
isSelected={selection.selectedIndices.has(originalIndex)}
|
|
301
|
+
checkboxLabel={buildCheckboxLabel(event)}
|
|
302
|
+
onSelect={() => { selection.enterSelectionMode(originalIndex); }}
|
|
303
|
+
onToggle={(shiftKey) => { selection.toggleEvent(originalIndex, shiftKey); }}
|
|
304
|
+
onCopied={() => { onShowToast?.("Copied to clipboard", "success"); }}
|
|
305
|
+
>
|
|
306
|
+
<EventRenderer event={event} toolUseCtx={event.toolUseCtx} settled={event.settled} />
|
|
307
|
+
</EventHoverRow>
|
|
308
|
+
</motion.div>
|
|
309
|
+
);
|
|
310
|
+
})}
|
|
311
|
+
</AnimatePresence>
|
|
312
|
+
</div>
|
|
313
|
+
|
|
314
|
+
{/* Floating action bar for multi-select mode */}
|
|
315
|
+
<AnimatePresence>
|
|
316
|
+
{selection.isSelecting && (
|
|
317
|
+
<FloatingActionBar
|
|
318
|
+
selectedCount={selection.selectedCount}
|
|
319
|
+
totalSelectable={totalSelectable}
|
|
320
|
+
onSelectAll={selection.selectAll}
|
|
321
|
+
onDeselectAll={selection.deselectAll}
|
|
322
|
+
onCopy={() => { handleCopySelected().catch(() => {}); }}
|
|
323
|
+
onForward={onForward !== undefined ? () => { setShowSessionPicker(true); } : undefined}
|
|
324
|
+
forwardDisabled={forwardTargets.length === 0}
|
|
325
|
+
onCancel={selection.cancelSelection}
|
|
326
|
+
/>
|
|
327
|
+
)}
|
|
328
|
+
</AnimatePresence>
|
|
329
|
+
|
|
330
|
+
{/* Session picker for forwarding */}
|
|
331
|
+
<SessionPicker
|
|
332
|
+
isOpen={showSessionPicker}
|
|
333
|
+
sessions={forwardTargets}
|
|
334
|
+
environments={environments ?? []}
|
|
335
|
+
personas={personas}
|
|
336
|
+
onSelect={handlePickSession}
|
|
337
|
+
onCancel={() => { setShowSessionPicker(false); }}
|
|
338
|
+
/>
|
|
339
|
+
|
|
340
|
+
{/* Large message confirmation */}
|
|
341
|
+
<ConfirmDialog
|
|
342
|
+
isOpen={confirmLargeMessage}
|
|
343
|
+
title="Send large message?"
|
|
344
|
+
description={`This will forward a large message (${largeMessageSizeKb} KB). Continue?`}
|
|
345
|
+
confirmLabel="Send"
|
|
346
|
+
onConfirm={handleConfirmLargeMessage}
|
|
347
|
+
onCancel={handleCancelLargeMessage}
|
|
348
|
+
/>
|
|
349
|
+
|
|
350
|
+
{/* Floating "scroll to anchor" button */}
|
|
351
|
+
<AnimatePresence>
|
|
352
|
+
{!isAtAnchor && (
|
|
353
|
+
<motion.button
|
|
354
|
+
className={`${styles.scrollToAnchor} ${isReversed ? styles.scrollToAnchorTop : styles.scrollToAnchorBottom}`}
|
|
355
|
+
onClick={scrollToAnchor}
|
|
356
|
+
initial={{ opacity: 0, scale: 0.9 }}
|
|
357
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
358
|
+
exit={{ opacity: 0, scale: 0.9 }}
|
|
359
|
+
transition={{ duration: 0.15 }}
|
|
360
|
+
aria-label="Scroll to latest"
|
|
361
|
+
data-testid="scroll-to-anchor"
|
|
362
|
+
>
|
|
363
|
+
{isReversed ? <ArrowUp size={ICON_MD} aria-hidden="true" /> : <ArrowDown size={ICON_MD} aria-hidden="true" />} New events
|
|
364
|
+
</motion.button>
|
|
365
|
+
)}
|
|
366
|
+
</AnimatePresence>
|
|
367
|
+
</div>
|
|
368
|
+
);
|
|
369
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
@use '../../styles/mixins' as *;
|
|
2
|
+
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// FloatingActionBar -- multi-select mode action bar at bottom of EventStream
|
|
5
|
+
// =============================================================================
|
|
6
|
+
|
|
7
|
+
.bar {
|
|
8
|
+
position: absolute;
|
|
9
|
+
bottom: var(--space-md);
|
|
10
|
+
left: var(--space-md);
|
|
11
|
+
right: var(--space-md);
|
|
12
|
+
z-index: 20;
|
|
13
|
+
|
|
14
|
+
@include surface-card;
|
|
15
|
+
display: flex;
|
|
16
|
+
align-items: center;
|
|
17
|
+
justify-content: space-between;
|
|
18
|
+
padding: var(--space-sm) var(--space-md);
|
|
19
|
+
gap: var(--space-sm);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.left {
|
|
23
|
+
display: flex;
|
|
24
|
+
align-items: center;
|
|
25
|
+
gap: var(--space-sm);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.right {
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
gap: var(--space-xs);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.count {
|
|
35
|
+
font-size: var(--font-size-sm);
|
|
36
|
+
font-weight: var(--font-weight-bold);
|
|
37
|
+
color: var(--text-primary);
|
|
38
|
+
white-space: nowrap;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.toggleButton {
|
|
42
|
+
@include btn-ghost(var(--accent-blue));
|
|
43
|
+
font-size: var(--font-size-sm);
|
|
44
|
+
padding: var(--space-xs) var(--space-sm);
|
|
45
|
+
border-radius: var(--radius-sm);
|
|
46
|
+
white-space: nowrap;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.copyButton {
|
|
50
|
+
@include btn-ghost(var(--text-primary));
|
|
51
|
+
display: flex;
|
|
52
|
+
align-items: center;
|
|
53
|
+
gap: var(--space-xs);
|
|
54
|
+
font-size: var(--font-size-sm);
|
|
55
|
+
padding: var(--space-xs) var(--space-sm);
|
|
56
|
+
border-radius: var(--radius-sm);
|
|
57
|
+
background: var(--accent-blue);
|
|
58
|
+
color: var(--accent-blue-fg, #fff);
|
|
59
|
+
white-space: nowrap;
|
|
60
|
+
|
|
61
|
+
&:hover:not(:disabled) {
|
|
62
|
+
filter: brightness(1.1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
&:disabled {
|
|
66
|
+
opacity: 0.4;
|
|
67
|
+
cursor: not-allowed;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.forwardButton {
|
|
72
|
+
@include btn-ghost(var(--text-primary));
|
|
73
|
+
display: flex;
|
|
74
|
+
align-items: center;
|
|
75
|
+
gap: var(--space-xs);
|
|
76
|
+
font-size: var(--font-size-sm);
|
|
77
|
+
padding: var(--space-xs) var(--space-sm);
|
|
78
|
+
border-radius: var(--radius-sm);
|
|
79
|
+
background: var(--bg-overlay);
|
|
80
|
+
color: var(--text-primary);
|
|
81
|
+
white-space: nowrap;
|
|
82
|
+
|
|
83
|
+
&:hover:not([aria-disabled='true']) {
|
|
84
|
+
background: var(--bg-hover);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
&[aria-disabled='true'] {
|
|
88
|
+
opacity: 0.4;
|
|
89
|
+
cursor: not-allowed;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.cancelButton {
|
|
94
|
+
@include btn-ghost(var(--text-secondary));
|
|
95
|
+
display: flex;
|
|
96
|
+
align-items: center;
|
|
97
|
+
justify-content: center;
|
|
98
|
+
width: 28px;
|
|
99
|
+
height: 28px;
|
|
100
|
+
padding: 0;
|
|
101
|
+
border-radius: var(--radius-sm);
|
|
102
|
+
|
|
103
|
+
&:hover {
|
|
104
|
+
color: var(--text-primary);
|
|
105
|
+
background: var(--bg-overlay);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { expect, fn, userEvent } from "@storybook/test";
|
|
3
|
+
import { AnimatePresence } from "motion/react";
|
|
4
|
+
import { FloatingActionBar } from "./FloatingActionBar.js";
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof FloatingActionBar> = {
|
|
7
|
+
component: FloatingActionBar,
|
|
8
|
+
title: "Grackle/Display/FloatingActionBar",
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
decorators: [
|
|
11
|
+
(Story) => (
|
|
12
|
+
<div style={{ position: "relative", height: 200, background: "var(--bg-primary)" }}>
|
|
13
|
+
<AnimatePresence>
|
|
14
|
+
<Story />
|
|
15
|
+
</AnimatePresence>
|
|
16
|
+
</div>
|
|
17
|
+
),
|
|
18
|
+
],
|
|
19
|
+
args: {
|
|
20
|
+
selectedCount: 3,
|
|
21
|
+
totalSelectable: 10,
|
|
22
|
+
onSelectAll: fn(),
|
|
23
|
+
onDeselectAll: fn(),
|
|
24
|
+
onCopy: fn(),
|
|
25
|
+
onForward: fn(),
|
|
26
|
+
onCancel: fn(),
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
export default meta;
|
|
30
|
+
type Story = StoryObj<typeof meta>;
|
|
31
|
+
|
|
32
|
+
/** Default state with some events selected. */
|
|
33
|
+
export const Default: Story = {
|
|
34
|
+
play: async ({ canvas }) => {
|
|
35
|
+
const count = canvas.getByTestId("floating-bar-count");
|
|
36
|
+
await expect(count).toHaveTextContent("3 selected");
|
|
37
|
+
|
|
38
|
+
const selectAll = canvas.getByTestId("floating-bar-select-all");
|
|
39
|
+
await expect(selectAll).toHaveTextContent("Select all");
|
|
40
|
+
|
|
41
|
+
const copyBtn = canvas.getByTestId("floating-bar-copy");
|
|
42
|
+
await expect(copyBtn).toBeEnabled();
|
|
43
|
+
|
|
44
|
+
const cancelBtn = canvas.getByTestId("floating-bar-cancel");
|
|
45
|
+
await expect(cancelBtn).toBeInTheDocument();
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** All events selected - shows "Deselect all". */
|
|
50
|
+
export const AllSelected: Story = {
|
|
51
|
+
args: {
|
|
52
|
+
selectedCount: 10,
|
|
53
|
+
totalSelectable: 10,
|
|
54
|
+
},
|
|
55
|
+
play: async ({ canvas }) => {
|
|
56
|
+
const count = canvas.getByTestId("floating-bar-count");
|
|
57
|
+
await expect(count).toHaveTextContent("10 selected");
|
|
58
|
+
|
|
59
|
+
const toggle = canvas.getByTestId("floating-bar-select-all");
|
|
60
|
+
await expect(toggle).toHaveTextContent("Deselect all");
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** No events selected - Copy button is disabled. */
|
|
65
|
+
export const NoneSelected: Story = {
|
|
66
|
+
args: {
|
|
67
|
+
selectedCount: 0,
|
|
68
|
+
},
|
|
69
|
+
play: async ({ canvas }) => {
|
|
70
|
+
const count = canvas.getByTestId("floating-bar-count");
|
|
71
|
+
await expect(count).toHaveTextContent("0 selected");
|
|
72
|
+
|
|
73
|
+
const copyBtn = canvas.getByTestId("floating-bar-copy");
|
|
74
|
+
await expect(copyBtn).toBeDisabled();
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** Forward button is visible and enabled when active sessions exist. */
|
|
79
|
+
export const WithForwardEnabled: Story = {
|
|
80
|
+
args: {
|
|
81
|
+
forwardDisabled: false,
|
|
82
|
+
},
|
|
83
|
+
play: async ({ canvas }) => {
|
|
84
|
+
const forwardBtn = canvas.getByTestId("floating-bar-forward");
|
|
85
|
+
await expect(forwardBtn).toBeInTheDocument();
|
|
86
|
+
await expect(forwardBtn).toBeEnabled();
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/** Forward button is disabled when no active sessions are available. */
|
|
91
|
+
export const ForwardDisabled: Story = {
|
|
92
|
+
args: {
|
|
93
|
+
forwardDisabled: true,
|
|
94
|
+
},
|
|
95
|
+
play: async ({ canvas }) => {
|
|
96
|
+
const forwardBtn = canvas.getByTestId("floating-bar-forward");
|
|
97
|
+
await expect(forwardBtn).toHaveAttribute("aria-disabled", "true");
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/** Forward button is hidden when onForward is not provided. */
|
|
102
|
+
export const NoForwardButton: Story = {
|
|
103
|
+
args: {
|
|
104
|
+
onForward: undefined,
|
|
105
|
+
},
|
|
106
|
+
play: async ({ canvas }) => {
|
|
107
|
+
const forwardBtn = canvas.queryByTestId("floating-bar-forward");
|
|
108
|
+
await expect(forwardBtn).not.toBeInTheDocument();
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/** Clicking Forward calls onForward. */
|
|
113
|
+
export const ForwardClick: Story = {
|
|
114
|
+
args: {
|
|
115
|
+
forwardDisabled: false,
|
|
116
|
+
},
|
|
117
|
+
play: async ({ canvas, args }) => {
|
|
118
|
+
const forwardBtn = canvas.getByTestId("floating-bar-forward");
|
|
119
|
+
await userEvent.click(forwardBtn);
|
|
120
|
+
await expect(args.onForward).toHaveBeenCalled();
|
|
121
|
+
},
|
|
122
|
+
};
|