@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,64 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState, type JSX } from "react";
|
|
2
|
+
import { Check, Clipboard } from "lucide-react";
|
|
3
|
+
import { ICON_MD } from "../../utils/iconSize.js";
|
|
4
|
+
import styles from "./CopyButton.module.scss";
|
|
5
|
+
|
|
6
|
+
/** Duration in milliseconds to show the "copied" checkmark before reverting. */
|
|
7
|
+
const COPIED_FEEDBACK_DURATION: number = 2000;
|
|
8
|
+
|
|
9
|
+
/** Props for the CopyButton component. */
|
|
10
|
+
interface CopyButtonProps {
|
|
11
|
+
/** Plain text to copy to the clipboard. */
|
|
12
|
+
text: string;
|
|
13
|
+
/** Additional CSS class name for positioning variants. */
|
|
14
|
+
className?: string;
|
|
15
|
+
/** Test ID for Storybook and E2E tests. */
|
|
16
|
+
"data-testid"?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Small copy-to-clipboard button with visual feedback.
|
|
21
|
+
*
|
|
22
|
+
* Shows a clipboard emoji by default, switches to a checkmark on click,
|
|
23
|
+
* then reverts after 2 seconds.
|
|
24
|
+
*/
|
|
25
|
+
export function CopyButton({ text, className, "data-testid": testId }: CopyButtonProps): JSX.Element {
|
|
26
|
+
const [copied, setCopied] = useState(false);
|
|
27
|
+
const timerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
return () => {
|
|
31
|
+
if (timerRef.current !== undefined) {
|
|
32
|
+
clearTimeout(timerRef.current);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
const handleClick = useCallback(async (): Promise<void> => {
|
|
38
|
+
try {
|
|
39
|
+
await navigator.clipboard.writeText(text);
|
|
40
|
+
setCopied(true);
|
|
41
|
+
if (timerRef.current !== undefined) {
|
|
42
|
+
clearTimeout(timerRef.current);
|
|
43
|
+
}
|
|
44
|
+
timerRef.current = setTimeout(() => {
|
|
45
|
+
setCopied(false);
|
|
46
|
+
timerRef.current = undefined;
|
|
47
|
+
}, COPIED_FEEDBACK_DURATION);
|
|
48
|
+
} catch {
|
|
49
|
+
/* clipboard API unavailable — fail silently */
|
|
50
|
+
}
|
|
51
|
+
}, [text]);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<button
|
|
55
|
+
type="button"
|
|
56
|
+
className={`${styles.copyButton} ${className ?? ""}`}
|
|
57
|
+
onClick={() => { handleClick().catch(() => { /* clipboard unavailable */ }); }}
|
|
58
|
+
aria-label={copied ? "Copied" : "Copy to clipboard"}
|
|
59
|
+
data-testid={testId ?? "copy-button"}
|
|
60
|
+
>
|
|
61
|
+
{copied ? <Check size={ICON_MD} aria-hidden="true" /> : <Clipboard size={ICON_MD} aria-hidden="true" />}
|
|
62
|
+
</button>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
.banner {
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
justify-content: center;
|
|
5
|
+
gap: 8px;
|
|
6
|
+
padding: 6px 16px;
|
|
7
|
+
background: var(--accent-blue-dim, rgba(59, 130, 246, 0.15));
|
|
8
|
+
border-bottom: 1px solid var(--accent-blue, #3b82f6);
|
|
9
|
+
font-size: 13px;
|
|
10
|
+
line-height: 1.4;
|
|
11
|
+
flex-shrink: 0;
|
|
12
|
+
z-index: 200;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.label {
|
|
16
|
+
font-weight: 700;
|
|
17
|
+
font-size: 11px;
|
|
18
|
+
letter-spacing: 0.05em;
|
|
19
|
+
padding: 1px 6px;
|
|
20
|
+
border-radius: 4px;
|
|
21
|
+
background: var(--accent-blue, #3b82f6);
|
|
22
|
+
color: #fff;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.text {
|
|
26
|
+
color: var(--text-secondary, #aaa);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.link {
|
|
30
|
+
color: var(--accent-blue, #3b82f6);
|
|
31
|
+
text-decoration: underline;
|
|
32
|
+
text-underline-offset: 2px;
|
|
33
|
+
|
|
34
|
+
&:hover {
|
|
35
|
+
color: var(--text-primary, #fff);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { expect } from "@storybook/test";
|
|
3
|
+
import { DemoBanner } from "./DemoBanner.js";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof DemoBanner> = {
|
|
6
|
+
title: "App/Display/DemoBanner",
|
|
7
|
+
component: DemoBanner,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default meta;
|
|
11
|
+
|
|
12
|
+
type Story = StoryObj<typeof DemoBanner>;
|
|
13
|
+
|
|
14
|
+
/** Banner renders with the DEMO label and install link. */
|
|
15
|
+
export const Default: Story = {
|
|
16
|
+
play: async ({ canvas }) => {
|
|
17
|
+
const banner = canvas.getByTestId("demo-banner");
|
|
18
|
+
await expect(banner).toBeInTheDocument();
|
|
19
|
+
|
|
20
|
+
// DEMO label is visible
|
|
21
|
+
await expect(canvas.getByText("DEMO")).toBeInTheDocument();
|
|
22
|
+
|
|
23
|
+
// Descriptive text is present
|
|
24
|
+
await expect(canvas.getByText(/interactive demo with mock data/)).toBeInTheDocument();
|
|
25
|
+
|
|
26
|
+
// Install link points to the GitHub repo
|
|
27
|
+
const link = canvas.getByRole("link", { name: "Install Grackle" });
|
|
28
|
+
await expect(link).toBeInTheDocument();
|
|
29
|
+
await expect(link).toHaveAttribute("href", "https://github.com/nick-pape/grackle");
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Install link opens in a new tab with security attributes. */
|
|
34
|
+
export const LinkOpensInNewTab: Story = {
|
|
35
|
+
play: async ({ canvas }) => {
|
|
36
|
+
const link = canvas.getByRole("link", { name: "Install Grackle" });
|
|
37
|
+
await expect(link).toHaveAttribute("target", "_blank");
|
|
38
|
+
await expect(link).toHaveAttribute("rel", "noopener noreferrer");
|
|
39
|
+
},
|
|
40
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import styles from "./DemoBanner.module.scss";
|
|
2
|
+
import type { JSX } from "react";
|
|
3
|
+
|
|
4
|
+
/** Persistent banner indicating the app is running in demo/mock mode. */
|
|
5
|
+
export function DemoBanner(): JSX.Element {
|
|
6
|
+
return (
|
|
7
|
+
<div className={styles.banner} data-testid="demo-banner">
|
|
8
|
+
<span className={styles.label}>DEMO</span>
|
|
9
|
+
<span className={styles.text}>
|
|
10
|
+
This is an interactive demo with mock data.{" "}
|
|
11
|
+
<a
|
|
12
|
+
href="https://github.com/nick-pape/grackle"
|
|
13
|
+
target="_blank"
|
|
14
|
+
rel="noopener noreferrer"
|
|
15
|
+
className={styles.link}
|
|
16
|
+
>
|
|
17
|
+
Install Grackle
|
|
18
|
+
</a>{" "}
|
|
19
|
+
to use it for real.
|
|
20
|
+
</span>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
@use '../../styles/mixins' as *;
|
|
2
|
+
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// EventHoverRow -- hover actions + selection mode wrapper for events
|
|
5
|
+
// =============================================================================
|
|
6
|
+
|
|
7
|
+
.row {
|
|
8
|
+
position: relative;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// --- Hover action row (normal mode) ---
|
|
12
|
+
|
|
13
|
+
.hoverActions {
|
|
14
|
+
position: absolute;
|
|
15
|
+
top: -28px;
|
|
16
|
+
right: var(--space-sm);
|
|
17
|
+
z-index: 5;
|
|
18
|
+
display: flex;
|
|
19
|
+
gap: 2px;
|
|
20
|
+
padding: 2px;
|
|
21
|
+
border-radius: var(--radius-sm);
|
|
22
|
+
background: var(--bg-secondary);
|
|
23
|
+
border: 1px solid var(--border-subtle);
|
|
24
|
+
opacity: 0;
|
|
25
|
+
transition: opacity var(--transition-fast);
|
|
26
|
+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
|
27
|
+
|
|
28
|
+
@include mobile {
|
|
29
|
+
opacity: 1;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.row:hover .hoverActions,
|
|
34
|
+
.row:focus-within .hoverActions {
|
|
35
|
+
opacity: 1;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.hoverButton {
|
|
39
|
+
@include btn-ghost(var(--text-secondary));
|
|
40
|
+
display: flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
justify-content: center;
|
|
43
|
+
width: 24px;
|
|
44
|
+
height: 24px;
|
|
45
|
+
padding: 0;
|
|
46
|
+
border-radius: var(--radius-sm);
|
|
47
|
+
cursor: pointer;
|
|
48
|
+
font-size: var(--font-size-sm);
|
|
49
|
+
|
|
50
|
+
&:hover {
|
|
51
|
+
color: var(--text-primary);
|
|
52
|
+
background: var(--bg-overlay);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
&:focus-visible {
|
|
56
|
+
outline: 2px solid var(--accent-blue);
|
|
57
|
+
outline-offset: 1px;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// --- Selection mode ---
|
|
62
|
+
|
|
63
|
+
.selectingRow {
|
|
64
|
+
display: flex;
|
|
65
|
+
align-items: flex-start;
|
|
66
|
+
cursor: pointer;
|
|
67
|
+
border-radius: var(--radius-sm);
|
|
68
|
+
transition: background var(--transition-fast);
|
|
69
|
+
|
|
70
|
+
&:hover {
|
|
71
|
+
background: color-mix(in srgb, var(--accent-blue) 5%, transparent);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.selected {
|
|
76
|
+
background: color-mix(in srgb, var(--accent-blue) 10%, transparent);
|
|
77
|
+
|
|
78
|
+
&:hover {
|
|
79
|
+
background: color-mix(in srgb, var(--accent-blue) 14%, transparent);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.checkboxArea {
|
|
84
|
+
flex-shrink: 0;
|
|
85
|
+
display: flex;
|
|
86
|
+
align-items: flex-start;
|
|
87
|
+
padding-top: var(--space-xs);
|
|
88
|
+
width: 28px;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.checkbox {
|
|
92
|
+
width: 16px;
|
|
93
|
+
height: 16px;
|
|
94
|
+
margin: 0;
|
|
95
|
+
cursor: pointer;
|
|
96
|
+
accent-color: var(--accent-blue);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.contentArea {
|
|
100
|
+
flex: 1;
|
|
101
|
+
min-width: 0;
|
|
102
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { expect, fn } from "@storybook/test";
|
|
3
|
+
import { EventHoverRow } from "./EventHoverRow.js";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof EventHoverRow> = {
|
|
6
|
+
component: EventHoverRow,
|
|
7
|
+
title: "Grackle/Display/EventHoverRow",
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
args: {
|
|
10
|
+
copyText: "Sample event content for clipboard",
|
|
11
|
+
isContentBearing: true,
|
|
12
|
+
isSelecting: false,
|
|
13
|
+
isSelected: false,
|
|
14
|
+
onSelect: fn(),
|
|
15
|
+
onToggle: fn(),
|
|
16
|
+
onCopied: fn(),
|
|
17
|
+
children: (
|
|
18
|
+
<div style={{ padding: "8px", background: "var(--bg-secondary)", borderRadius: 4 }}>
|
|
19
|
+
Sample event content for clipboard
|
|
20
|
+
</div>
|
|
21
|
+
),
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
export default meta;
|
|
25
|
+
type Story = StoryObj<typeof meta>;
|
|
26
|
+
|
|
27
|
+
/** Default state - hover to see action buttons. */
|
|
28
|
+
export const Default: Story = {
|
|
29
|
+
play: async ({ canvas }) => {
|
|
30
|
+
const row = canvas.getByTestId("event-hover-row");
|
|
31
|
+
await expect(row).toBeInTheDocument();
|
|
32
|
+
// Hover actions exist in DOM (opacity controlled by CSS)
|
|
33
|
+
const actions = canvas.getByTestId("event-hover-actions");
|
|
34
|
+
await expect(actions).toBeInTheDocument();
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Hover shows Copy and Select buttons. */
|
|
39
|
+
export const HoverActions: Story = {
|
|
40
|
+
play: async ({ canvas }) => {
|
|
41
|
+
const copyBtn = canvas.getByTestId("event-hover-copy");
|
|
42
|
+
await expect(copyBtn).toBeInTheDocument();
|
|
43
|
+
await expect(copyBtn).toHaveAccessibleName("Copy event content");
|
|
44
|
+
|
|
45
|
+
const selectBtn = canvas.getByTestId("event-hover-select");
|
|
46
|
+
await expect(selectBtn).toBeInTheDocument();
|
|
47
|
+
await expect(selectBtn).toHaveAccessibleName("Select this event");
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** Selection mode with event NOT selected - shows unchecked checkbox. */
|
|
52
|
+
export const SelectionModeUnselected: Story = {
|
|
53
|
+
args: {
|
|
54
|
+
isSelecting: true,
|
|
55
|
+
isSelected: false,
|
|
56
|
+
},
|
|
57
|
+
play: async ({ canvas }) => {
|
|
58
|
+
const row = canvas.getByTestId("event-selectable-row");
|
|
59
|
+
await expect(row).toBeInTheDocument();
|
|
60
|
+
|
|
61
|
+
const checkbox = canvas.getByTestId("event-select-checkbox");
|
|
62
|
+
await expect(checkbox).toBeInTheDocument();
|
|
63
|
+
await expect(checkbox).not.toBeChecked();
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** Selection mode with event selected - shows checked checkbox + highlight. */
|
|
68
|
+
export const SelectionModeSelected: Story = {
|
|
69
|
+
args: {
|
|
70
|
+
isSelecting: true,
|
|
71
|
+
isSelected: true,
|
|
72
|
+
},
|
|
73
|
+
play: async ({ canvas }) => {
|
|
74
|
+
const row = canvas.getByTestId("event-selectable-row");
|
|
75
|
+
await expect(row).toBeInTheDocument();
|
|
76
|
+
|
|
77
|
+
const checkbox = canvas.getByTestId("event-select-checkbox");
|
|
78
|
+
await expect(checkbox).toBeChecked();
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** Non-content-bearing event - no hover actions or checkbox. */
|
|
83
|
+
export const NonContentBearing: Story = {
|
|
84
|
+
args: {
|
|
85
|
+
isContentBearing: false,
|
|
86
|
+
children: (
|
|
87
|
+
<div style={{ padding: "4px", color: "gray", fontSize: "12px" }}>
|
|
88
|
+
--- status: running ---
|
|
89
|
+
</div>
|
|
90
|
+
),
|
|
91
|
+
},
|
|
92
|
+
play: async ({ canvasElement }) => {
|
|
93
|
+
// No hover actions or checkbox should exist
|
|
94
|
+
const actions = canvasElement.querySelector("[data-testid='event-hover-actions']");
|
|
95
|
+
await expect(actions).toBeFalsy();
|
|
96
|
+
const checkbox = canvasElement.querySelector("[data-testid='event-select-checkbox']");
|
|
97
|
+
await expect(checkbox).toBeFalsy();
|
|
98
|
+
},
|
|
99
|
+
};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wrapper component for events in the EventStream that provides:
|
|
3
|
+
* - Hover action row (Copy + Select) in normal mode
|
|
4
|
+
* - Checkbox + full-row click target in selection mode
|
|
5
|
+
*
|
|
6
|
+
* Presentational component decoupled from useGrackle(). Performs clipboard
|
|
7
|
+
* side effects via navigator.clipboard when the user clicks Copy.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useState, useCallback, useEffect, useRef, type JSX, type ReactNode, type MouseEvent } from "react";
|
|
11
|
+
import { Clipboard, Check, CheckSquare } from "lucide-react";
|
|
12
|
+
import { ICON_SM } from "../../utils/iconSize.js";
|
|
13
|
+
import styles from "./EventHoverRow.module.scss";
|
|
14
|
+
|
|
15
|
+
/** Props for the EventHoverRow component. */
|
|
16
|
+
export interface EventHoverRowProps {
|
|
17
|
+
/** Text to copy when the hover Copy button is clicked. */
|
|
18
|
+
copyText: string;
|
|
19
|
+
/** Whether this event has copyable content (shows hover actions). */
|
|
20
|
+
isContentBearing: boolean;
|
|
21
|
+
/** Whether multi-select mode is active. */
|
|
22
|
+
isSelecting: boolean;
|
|
23
|
+
/** Whether this event is currently selected in multi-select mode. */
|
|
24
|
+
isSelected: boolean;
|
|
25
|
+
/** Accessible label for the selection checkbox (e.g. "Select message from assistant at 2:34 PM"). */
|
|
26
|
+
checkboxLabel?: string;
|
|
27
|
+
/** Called when the Select button in the hover row is clicked (enters selection mode). */
|
|
28
|
+
onSelect: () => void;
|
|
29
|
+
/** Called when the row is clicked in selection mode. Receives the shiftKey state. */
|
|
30
|
+
onToggle: (shiftKey: boolean) => void;
|
|
31
|
+
/** Called after a successful single-event copy from the hover row. */
|
|
32
|
+
onCopied?: () => void;
|
|
33
|
+
/** The event content to wrap. */
|
|
34
|
+
children: ReactNode;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Wraps an event in the EventStream with hover actions (normal mode) or
|
|
39
|
+
* selection affordances (multi-select mode).
|
|
40
|
+
*/
|
|
41
|
+
export function EventHoverRow({
|
|
42
|
+
copyText,
|
|
43
|
+
isContentBearing,
|
|
44
|
+
isSelecting,
|
|
45
|
+
isSelected,
|
|
46
|
+
checkboxLabel,
|
|
47
|
+
onSelect,
|
|
48
|
+
onToggle,
|
|
49
|
+
onCopied,
|
|
50
|
+
children,
|
|
51
|
+
}: EventHoverRowProps): JSX.Element {
|
|
52
|
+
const [copied, setCopied] = useState(false);
|
|
53
|
+
const copiedTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
54
|
+
|
|
55
|
+
// Clear the "copied" feedback timer on unmount
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
return () => {
|
|
58
|
+
if (copiedTimerRef.current !== undefined) {
|
|
59
|
+
clearTimeout(copiedTimerRef.current);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
64
|
+
const handleCopy = useCallback(async () => {
|
|
65
|
+
try {
|
|
66
|
+
await navigator.clipboard.writeText(copyText);
|
|
67
|
+
setCopied(true);
|
|
68
|
+
onCopied?.();
|
|
69
|
+
if (copiedTimerRef.current !== undefined) {
|
|
70
|
+
clearTimeout(copiedTimerRef.current);
|
|
71
|
+
}
|
|
72
|
+
copiedTimerRef.current = setTimeout(() => { setCopied(false); }, 2000);
|
|
73
|
+
} catch {
|
|
74
|
+
// Clipboard write failed silently
|
|
75
|
+
}
|
|
76
|
+
}, [copyText, onCopied]);
|
|
77
|
+
|
|
78
|
+
const handleRowClick = useCallback(
|
|
79
|
+
(e: MouseEvent<HTMLDivElement>) => {
|
|
80
|
+
if (!isSelecting) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
// Don't intercept if the user clicked inside an interactive element
|
|
84
|
+
const target = e.target as HTMLElement;
|
|
85
|
+
if (target.closest("a, button, input, textarea, select, [role=button]")) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
e.preventDefault();
|
|
89
|
+
onToggle(e.shiftKey);
|
|
90
|
+
},
|
|
91
|
+
[isSelecting, onToggle],
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// Non-content-bearing events: render plain, no interactivity
|
|
95
|
+
if (!isContentBearing) {
|
|
96
|
+
return <div className={styles.row}>{children}</div>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Selection mode: checkbox + clickable row
|
|
100
|
+
if (isSelecting) {
|
|
101
|
+
return (
|
|
102
|
+
<div
|
|
103
|
+
className={`${styles.row} ${styles.selectingRow} ${isSelected ? styles.selected : ""}`}
|
|
104
|
+
onClick={handleRowClick}
|
|
105
|
+
data-testid="event-selectable-row"
|
|
106
|
+
>
|
|
107
|
+
<div className={styles.checkboxArea}>
|
|
108
|
+
<input
|
|
109
|
+
type="checkbox"
|
|
110
|
+
checked={isSelected}
|
|
111
|
+
onChange={(e) => { onToggle((e.nativeEvent as globalThis.MouseEvent).shiftKey); }}
|
|
112
|
+
onClick={(e) => { e.stopPropagation(); }}
|
|
113
|
+
className={styles.checkbox}
|
|
114
|
+
aria-label={checkboxLabel ?? "Select this event"}
|
|
115
|
+
data-testid="event-select-checkbox"
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
<div className={styles.contentArea}>{children}</div>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Normal mode: hover action row
|
|
124
|
+
return (
|
|
125
|
+
<div
|
|
126
|
+
className={styles.row}
|
|
127
|
+
data-testid="event-hover-row"
|
|
128
|
+
>
|
|
129
|
+
<div className={styles.hoverActions} data-testid="event-hover-actions">
|
|
130
|
+
<button
|
|
131
|
+
type="button"
|
|
132
|
+
className={styles.hoverButton}
|
|
133
|
+
onClick={() => { handleCopy().catch(() => {}); }}
|
|
134
|
+
aria-label="Copy event content"
|
|
135
|
+
data-testid="event-hover-copy"
|
|
136
|
+
>
|
|
137
|
+
{copied
|
|
138
|
+
? <Check size={ICON_SM} aria-hidden="true" />
|
|
139
|
+
: <Clipboard size={ICON_SM} aria-hidden="true" />}
|
|
140
|
+
</button>
|
|
141
|
+
<button
|
|
142
|
+
type="button"
|
|
143
|
+
className={styles.hoverButton}
|
|
144
|
+
onClick={onSelect}
|
|
145
|
+
aria-label="Select this event"
|
|
146
|
+
data-testid="event-hover-select"
|
|
147
|
+
>
|
|
148
|
+
<CheckSquare size={ICON_SM} aria-hidden="true" />
|
|
149
|
+
</button>
|
|
150
|
+
</div>
|
|
151
|
+
{children}
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
}
|