@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,240 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { expect, userEvent, waitFor, within } from "@storybook/test";
|
|
3
|
+
import { Tooltip } from "./Tooltip.js";
|
|
4
|
+
|
|
5
|
+
/** Find the portaled tooltip element in document.body by data-testid. */
|
|
6
|
+
function getTooltip(testId: string = "tooltip"): HTMLElement {
|
|
7
|
+
const el = document.querySelector<HTMLElement>(`[data-testid="${testId}"]`);
|
|
8
|
+
if (!el) {
|
|
9
|
+
throw new Error(`Tooltip with data-testid="${testId}" not found`);
|
|
10
|
+
}
|
|
11
|
+
return el;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const meta: Meta<typeof Tooltip> = {
|
|
15
|
+
component: Tooltip,
|
|
16
|
+
title: "Primitives/Display/Tooltip",
|
|
17
|
+
tags: ["autodocs"],
|
|
18
|
+
decorators: [
|
|
19
|
+
(Story) => (
|
|
20
|
+
<div style={{ padding: 80, display: "flex", justifyContent: "center", alignItems: "center" }}>
|
|
21
|
+
<Story />
|
|
22
|
+
</div>
|
|
23
|
+
),
|
|
24
|
+
],
|
|
25
|
+
};
|
|
26
|
+
export default meta;
|
|
27
|
+
type Story = StoryObj<typeof meta>;
|
|
28
|
+
|
|
29
|
+
/** Default tooltip renders with role="tooltip" and starts hidden. */
|
|
30
|
+
export const Default: Story = {
|
|
31
|
+
args: {
|
|
32
|
+
text: "Hello tooltip",
|
|
33
|
+
children: <button type="button">Hover me</button>,
|
|
34
|
+
},
|
|
35
|
+
play: async () => {
|
|
36
|
+
const tooltip = getTooltip();
|
|
37
|
+
await expect(tooltip).toBeInTheDocument();
|
|
38
|
+
await expect(tooltip).toHaveTextContent("Hello tooltip");
|
|
39
|
+
// Starts hidden (opacity 0 via CSS class)
|
|
40
|
+
await expect(tooltip.className).not.toContain("visible");
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** Tooltip appears on hover and hides on unhover. */
|
|
45
|
+
export const ShowsOnHover: Story = {
|
|
46
|
+
args: {
|
|
47
|
+
text: "Hover tooltip",
|
|
48
|
+
delayMs: 0,
|
|
49
|
+
children: <button type="button">Hover me</button>,
|
|
50
|
+
},
|
|
51
|
+
play: async ({ canvasElement }) => {
|
|
52
|
+
const canvas = within(canvasElement);
|
|
53
|
+
const trigger = canvas.getByRole("button", { name: "Hover me" });
|
|
54
|
+
const tooltip = getTooltip();
|
|
55
|
+
|
|
56
|
+
// Hover shows the tooltip
|
|
57
|
+
await userEvent.hover(trigger);
|
|
58
|
+
await waitFor(async () => {
|
|
59
|
+
await expect(tooltip.className).toContain("visible");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Unhover hides it
|
|
63
|
+
await userEvent.unhover(trigger);
|
|
64
|
+
await waitFor(async () => {
|
|
65
|
+
await expect(tooltip.className).not.toContain("visible");
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** Tooltip appears when child receives keyboard focus. */
|
|
71
|
+
export const ShowsOnFocus: Story = {
|
|
72
|
+
args: {
|
|
73
|
+
text: "Focus tooltip",
|
|
74
|
+
delayMs: 0,
|
|
75
|
+
children: <button type="button">Focus me</button>,
|
|
76
|
+
},
|
|
77
|
+
play: async ({ canvasElement }) => {
|
|
78
|
+
const canvas = within(canvasElement);
|
|
79
|
+
const trigger = canvas.getByRole("button", { name: "Focus me" });
|
|
80
|
+
const tooltip = getTooltip();
|
|
81
|
+
|
|
82
|
+
// Tab into the button
|
|
83
|
+
trigger.focus();
|
|
84
|
+
await waitFor(async () => {
|
|
85
|
+
await expect(tooltip.className).toContain("visible");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Blur hides it
|
|
89
|
+
trigger.blur();
|
|
90
|
+
await waitFor(async () => {
|
|
91
|
+
await expect(tooltip.className).not.toContain("visible");
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/** Escape key dismisses a visible tooltip. */
|
|
97
|
+
export const DismissOnEscape: Story = {
|
|
98
|
+
args: {
|
|
99
|
+
text: "Escape me",
|
|
100
|
+
delayMs: 0,
|
|
101
|
+
children: <button type="button">Hover me</button>,
|
|
102
|
+
},
|
|
103
|
+
play: async ({ canvasElement }) => {
|
|
104
|
+
const canvas = within(canvasElement);
|
|
105
|
+
const trigger = canvas.getByRole("button", { name: "Hover me" });
|
|
106
|
+
const tooltip = getTooltip();
|
|
107
|
+
|
|
108
|
+
// Show via hover
|
|
109
|
+
await userEvent.hover(trigger);
|
|
110
|
+
await waitFor(async () => {
|
|
111
|
+
await expect(tooltip.className).toContain("visible");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Escape dismisses
|
|
115
|
+
await userEvent.keyboard("{Escape}");
|
|
116
|
+
await waitFor(async () => {
|
|
117
|
+
await expect(tooltip.className).not.toContain("visible");
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/** Bottom placement applies the correct CSS class. */
|
|
123
|
+
export const PlacementBottom: Story = {
|
|
124
|
+
args: {
|
|
125
|
+
text: "Bottom tooltip",
|
|
126
|
+
placement: "bottom",
|
|
127
|
+
delayMs: 0,
|
|
128
|
+
children: <button type="button">Below me</button>,
|
|
129
|
+
},
|
|
130
|
+
play: async ({ canvasElement }) => {
|
|
131
|
+
const canvas = within(canvasElement);
|
|
132
|
+
const tooltip = getTooltip();
|
|
133
|
+
const trigger = canvas.getByRole("button", { name: "Below me" });
|
|
134
|
+
|
|
135
|
+
await userEvent.hover(trigger);
|
|
136
|
+
await waitFor(async () => {
|
|
137
|
+
await expect(tooltip.className).toContain("visible");
|
|
138
|
+
});
|
|
139
|
+
await expect(tooltip.className).toContain("bottom");
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/** Left placement applies the correct CSS class. */
|
|
144
|
+
export const PlacementLeft: Story = {
|
|
145
|
+
args: {
|
|
146
|
+
text: "Left tooltip",
|
|
147
|
+
placement: "left",
|
|
148
|
+
delayMs: 0,
|
|
149
|
+
children: <button type="button">Left of me</button>,
|
|
150
|
+
},
|
|
151
|
+
play: async ({ canvasElement }) => {
|
|
152
|
+
const canvas = within(canvasElement);
|
|
153
|
+
const tooltip = getTooltip();
|
|
154
|
+
const trigger = canvas.getByRole("button", { name: "Left of me" });
|
|
155
|
+
|
|
156
|
+
await userEvent.hover(trigger);
|
|
157
|
+
await waitFor(async () => {
|
|
158
|
+
await expect(tooltip.className).toContain("visible");
|
|
159
|
+
});
|
|
160
|
+
await expect(tooltip.className).toContain("left");
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/** Right placement applies the correct CSS class. */
|
|
165
|
+
export const PlacementRight: Story = {
|
|
166
|
+
args: {
|
|
167
|
+
text: "Right tooltip",
|
|
168
|
+
placement: "right",
|
|
169
|
+
delayMs: 0,
|
|
170
|
+
children: <button type="button">Right of me</button>,
|
|
171
|
+
},
|
|
172
|
+
play: async ({ canvasElement }) => {
|
|
173
|
+
const canvas = within(canvasElement);
|
|
174
|
+
const tooltip = getTooltip();
|
|
175
|
+
const trigger = canvas.getByRole("button", { name: "Right of me" });
|
|
176
|
+
|
|
177
|
+
await userEvent.hover(trigger);
|
|
178
|
+
await waitFor(async () => {
|
|
179
|
+
await expect(tooltip.className).toContain("visible");
|
|
180
|
+
});
|
|
181
|
+
await expect(tooltip.className).toContain("right");
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/** Accessibility: aria-describedby links trigger wrapper to tooltip id. */
|
|
186
|
+
export const AccessibilityAttributes: Story = {
|
|
187
|
+
args: {
|
|
188
|
+
text: "Accessible tooltip",
|
|
189
|
+
delayMs: 0,
|
|
190
|
+
children: <button type="button">Accessible</button>,
|
|
191
|
+
},
|
|
192
|
+
play: async ({ canvasElement }) => {
|
|
193
|
+
const canvas = within(canvasElement);
|
|
194
|
+
const tooltip = getTooltip();
|
|
195
|
+
const trigger = canvas.getByRole("button", { name: "Accessible" });
|
|
196
|
+
|
|
197
|
+
// Tooltip has an id and role="tooltip"
|
|
198
|
+
const tooltipId = tooltip.getAttribute("id");
|
|
199
|
+
await expect(tooltipId).toBeTruthy();
|
|
200
|
+
await expect(tooltip).toHaveAttribute("role", "tooltip");
|
|
201
|
+
|
|
202
|
+
// Show tooltip so aria-describedby is set
|
|
203
|
+
await userEvent.hover(trigger);
|
|
204
|
+
await waitFor(async () => {
|
|
205
|
+
await expect(tooltip.className).toContain("visible");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// The trigger element itself carries aria-describedby (injected via cloneElement)
|
|
209
|
+
await expect(trigger.getAttribute("aria-describedby")).toBe(tooltipId);
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/** inline={false} renders a div wrapper instead of span. */
|
|
214
|
+
export const BlockWrapper: Story = {
|
|
215
|
+
args: {
|
|
216
|
+
text: "Block tooltip",
|
|
217
|
+
inline: false,
|
|
218
|
+
children: <button type="button">Block child</button>,
|
|
219
|
+
},
|
|
220
|
+
play: async ({ canvasElement }) => {
|
|
221
|
+
const canvas = within(canvasElement);
|
|
222
|
+
const trigger = canvas.getByRole("button", { name: "Block child" });
|
|
223
|
+
const wrapper = trigger.parentElement;
|
|
224
|
+
await expect(wrapper).toBeTruthy();
|
|
225
|
+
await expect(wrapper!.tagName).toBe("DIV");
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
/** Custom data-testid is applied to the tooltip element. */
|
|
230
|
+
export const CustomTestId: Story = {
|
|
231
|
+
args: {
|
|
232
|
+
text: "Custom id",
|
|
233
|
+
"data-testid": "my-tooltip",
|
|
234
|
+
children: <button type="button">Custom</button>,
|
|
235
|
+
},
|
|
236
|
+
play: async () => {
|
|
237
|
+
const tooltip = getTooltip("my-tooltip");
|
|
238
|
+
await expect(tooltip).toBeInTheDocument();
|
|
239
|
+
},
|
|
240
|
+
};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { cloneElement, isValidElement, useCallback, useEffect, useId, useRef, useState, type JSX, type ReactElement, type ReactNode } from "react";
|
|
2
|
+
import { createPortal } from "react-dom";
|
|
3
|
+
import styles from "./Tooltip.module.scss";
|
|
4
|
+
|
|
5
|
+
/** Placement direction for the tooltip relative to its trigger. */
|
|
6
|
+
export type TooltipPlacement = "top" | "bottom" | "left" | "right";
|
|
7
|
+
|
|
8
|
+
/** Props for the {@link Tooltip} component. */
|
|
9
|
+
export interface TooltipProps {
|
|
10
|
+
/** Text content to display in the tooltip. */
|
|
11
|
+
text: string;
|
|
12
|
+
/** Placement relative to the trigger element. Defaults to `"top"`. */
|
|
13
|
+
placement?: TooltipPlacement;
|
|
14
|
+
/** Delay in milliseconds before showing. Defaults to `300`. */
|
|
15
|
+
delayMs?: number;
|
|
16
|
+
/** Whether the wrapper is inline (`span`) or block (`div`). Defaults to `true`. */
|
|
17
|
+
inline?: boolean;
|
|
18
|
+
/** The trigger element to wrap. */
|
|
19
|
+
children: ReactNode;
|
|
20
|
+
/** Additional CSS class for the wrapper element. */
|
|
21
|
+
className?: string;
|
|
22
|
+
/** Test ID for the tooltip content element. */
|
|
23
|
+
"data-testid"?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Default delay in milliseconds before the tooltip appears. */
|
|
27
|
+
const DEFAULT_DELAY_MS: number = 300;
|
|
28
|
+
|
|
29
|
+
/** Gap in pixels between the tooltip and the trigger element. */
|
|
30
|
+
const TOOLTIP_GAP_PX: number = 6;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Lightweight tooltip wrapper that shows text on hover or keyboard focus.
|
|
34
|
+
*
|
|
35
|
+
* Wraps a single child element with a hover/focus-triggered tooltip.
|
|
36
|
+
* Renders the tooltip bubble via a portal to `document.body` so it escapes
|
|
37
|
+
* all stacking contexts, `overflow: hidden`, and `backdrop-filter` traps.
|
|
38
|
+
*/
|
|
39
|
+
export function Tooltip({
|
|
40
|
+
text,
|
|
41
|
+
placement = "top",
|
|
42
|
+
delayMs = DEFAULT_DELAY_MS,
|
|
43
|
+
inline = true,
|
|
44
|
+
children,
|
|
45
|
+
className,
|
|
46
|
+
"data-testid": testId,
|
|
47
|
+
}: TooltipProps): JSX.Element {
|
|
48
|
+
const [visible, setVisible] = useState(false);
|
|
49
|
+
const [coords, setCoords] = useState<{ top: number; left: number }>({ top: 0, left: 0 });
|
|
50
|
+
const wrapperRef = useRef<HTMLElement>(null);
|
|
51
|
+
const tooltipRef = useRef<HTMLDivElement>(null);
|
|
52
|
+
const timerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
53
|
+
const tooltipId = useId();
|
|
54
|
+
|
|
55
|
+
const canPortal = typeof document !== "undefined";
|
|
56
|
+
|
|
57
|
+
const computePosition = useCallback((): void => {
|
|
58
|
+
if (!wrapperRef.current || !tooltipRef.current) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const rect = wrapperRef.current.getBoundingClientRect();
|
|
62
|
+
const tipRect = tooltipRef.current.getBoundingClientRect();
|
|
63
|
+
let top = 0;
|
|
64
|
+
let left = 0;
|
|
65
|
+
switch (placement) {
|
|
66
|
+
case "top":
|
|
67
|
+
top = rect.top - tipRect.height - TOOLTIP_GAP_PX;
|
|
68
|
+
left = rect.left + rect.width / 2 - tipRect.width / 2;
|
|
69
|
+
break;
|
|
70
|
+
case "bottom":
|
|
71
|
+
top = rect.bottom + TOOLTIP_GAP_PX;
|
|
72
|
+
left = rect.left + rect.width / 2 - tipRect.width / 2;
|
|
73
|
+
break;
|
|
74
|
+
case "left":
|
|
75
|
+
top = rect.top + rect.height / 2 - tipRect.height / 2;
|
|
76
|
+
left = rect.left - tipRect.width - TOOLTIP_GAP_PX;
|
|
77
|
+
break;
|
|
78
|
+
case "right":
|
|
79
|
+
top = rect.top + rect.height / 2 - tipRect.height / 2;
|
|
80
|
+
left = rect.right + TOOLTIP_GAP_PX;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
setCoords({ top, left });
|
|
84
|
+
}, [placement]);
|
|
85
|
+
|
|
86
|
+
const showWithDelay = useCallback((delay: number): void => {
|
|
87
|
+
if (timerRef.current !== undefined) {
|
|
88
|
+
clearTimeout(timerRef.current);
|
|
89
|
+
}
|
|
90
|
+
timerRef.current = setTimeout(() => {
|
|
91
|
+
computePosition();
|
|
92
|
+
setVisible(true);
|
|
93
|
+
timerRef.current = undefined;
|
|
94
|
+
}, delay);
|
|
95
|
+
}, [computePosition]);
|
|
96
|
+
|
|
97
|
+
const showHover = useCallback((): void => { showWithDelay(delayMs); }, [delayMs, showWithDelay]);
|
|
98
|
+
const showFocus = useCallback((): void => { showWithDelay(0); }, [showWithDelay]);
|
|
99
|
+
|
|
100
|
+
const hide = useCallback((): void => {
|
|
101
|
+
if (timerRef.current !== undefined) {
|
|
102
|
+
clearTimeout(timerRef.current);
|
|
103
|
+
timerRef.current = undefined;
|
|
104
|
+
}
|
|
105
|
+
setVisible(false);
|
|
106
|
+
}, []);
|
|
107
|
+
|
|
108
|
+
// Dismiss on Escape key; reposition on scroll/resize while visible.
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (!visible) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const handleKeyDown = (event: KeyboardEvent): void => {
|
|
114
|
+
if (event.key === "Escape") {
|
|
115
|
+
hide();
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
119
|
+
window.addEventListener("scroll", computePosition, true);
|
|
120
|
+
window.addEventListener("resize", computePosition);
|
|
121
|
+
return () => {
|
|
122
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
123
|
+
window.removeEventListener("scroll", computePosition, true);
|
|
124
|
+
window.removeEventListener("resize", computePosition);
|
|
125
|
+
};
|
|
126
|
+
}, [visible, hide, computePosition]);
|
|
127
|
+
|
|
128
|
+
// Cleanup timer on unmount
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
return () => {
|
|
131
|
+
if (timerRef.current !== undefined) {
|
|
132
|
+
clearTimeout(timerRef.current);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}, []);
|
|
136
|
+
|
|
137
|
+
const Tag = inline ? "span" : "div";
|
|
138
|
+
const wrapperClass = [
|
|
139
|
+
inline ? styles.wrapper : styles.wrapperBlock,
|
|
140
|
+
className,
|
|
141
|
+
].filter(Boolean).join(" ");
|
|
142
|
+
|
|
143
|
+
// Inject aria-describedby onto the child element when it is a single
|
|
144
|
+
// ReactElement so screen readers announce the tooltip from the focused node.
|
|
145
|
+
// Merges with any existing aria-describedby value on the child.
|
|
146
|
+
let renderedChildren: React.ReactNode = children;
|
|
147
|
+
if (isValidElement(children)) {
|
|
148
|
+
const child = children as ReactElement<{ "aria-describedby"?: string }>;
|
|
149
|
+
const existing = child.props["aria-describedby"];
|
|
150
|
+
const mergedDescribedBy = visible
|
|
151
|
+
? (existing ? `${existing} ${tooltipId}` : tooltipId)
|
|
152
|
+
: existing;
|
|
153
|
+
renderedChildren = cloneElement(child, {
|
|
154
|
+
"aria-describedby": mergedDescribedBy,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const tooltipElement = (
|
|
159
|
+
<div
|
|
160
|
+
ref={tooltipRef}
|
|
161
|
+
id={tooltipId}
|
|
162
|
+
role="tooltip"
|
|
163
|
+
className={`${styles.tooltip} ${styles[placement]} ${visible ? styles.visible : ""}`}
|
|
164
|
+
style={{ top: coords.top, left: coords.left }}
|
|
165
|
+
data-testid={testId ?? "tooltip"}
|
|
166
|
+
>
|
|
167
|
+
{text}
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<Tag
|
|
173
|
+
ref={wrapperRef as React.Ref<HTMLSpanElement & HTMLDivElement>}
|
|
174
|
+
className={wrapperClass}
|
|
175
|
+
onMouseEnter={showHover}
|
|
176
|
+
onMouseLeave={hide}
|
|
177
|
+
onFocus={showFocus}
|
|
178
|
+
onBlur={hide}
|
|
179
|
+
>
|
|
180
|
+
{renderedChildren}
|
|
181
|
+
{canPortal ? createPortal(tooltipElement, document.body) : null}
|
|
182
|
+
</Tag>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { createElement } from "react";
|
|
3
|
+
import { extractText } from "./EventRenderer.js";
|
|
4
|
+
|
|
5
|
+
describe("extractText", () => {
|
|
6
|
+
it("extracts plain string", () => {
|
|
7
|
+
expect(extractText("hello")).toBe("hello");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("extracts number", () => {
|
|
11
|
+
expect(extractText(42)).toBe("42");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("returns empty string for null/undefined", () => {
|
|
15
|
+
expect(extractText(null)).toBe("");
|
|
16
|
+
expect(extractText(undefined)).toBe("");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("concatenates array children", () => {
|
|
20
|
+
expect(extractText(["hello", " ", "world"])).toBe("hello world");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("extracts text from nested React elements", () => {
|
|
24
|
+
// Simulates: <span><span>function</span> <span>fibonacci</span></span>
|
|
25
|
+
const inner = createElement("span", null, "function");
|
|
26
|
+
const inner2 = createElement("span", null, "fibonacci");
|
|
27
|
+
const outer = createElement("span", null, inner, " ", inner2);
|
|
28
|
+
expect(extractText(outer)).toBe("function fibonacci");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("handles deeply nested prism-like token structure", () => {
|
|
32
|
+
// Simulates rehype-prism-plus output: <code><span class="token keyword">const</span> x = <span class="token number">42</span>;</code>
|
|
33
|
+
const keyword = createElement("span", { className: "token keyword" }, "const");
|
|
34
|
+
const num = createElement("span", { className: "token number" }, "42");
|
|
35
|
+
const code = createElement("code", null, keyword, " x = ", num, ";");
|
|
36
|
+
expect(extractText(code)).toBe("const x = 42;");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("handles mixed arrays and elements", () => {
|
|
40
|
+
const span = createElement("span", null, "bold");
|
|
41
|
+
expect(extractText(["text ", span, " more"])).toBe("text bold more");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("returns empty string for boolean children", () => {
|
|
45
|
+
expect(extractText(true)).toBe("");
|
|
46
|
+
expect(extractText(false)).toBe("");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content rendering and display components.
|
|
3
|
+
* @module display
|
|
4
|
+
*/
|
|
5
|
+
export { Breadcrumbs } from "./Breadcrumbs.js";
|
|
6
|
+
export { Button } from "./Button.js";
|
|
7
|
+
export { CopyButton } from "./CopyButton.js";
|
|
8
|
+
export { DemoBanner } from "./DemoBanner.js";
|
|
9
|
+
export { SplitButton } from "./SplitButton.js";
|
|
10
|
+
export { EventRenderer } from "./EventRenderer.js";
|
|
11
|
+
export { ConfirmDialog } from "./ConfirmDialog.js";
|
|
12
|
+
export { Skeleton, SkeletonText, SkeletonCard } from "./Skeleton.js";
|
|
13
|
+
export { Spinner } from "./Spinner.js";
|
|
14
|
+
export { SplashScreen } from "./SplashScreen.js";
|
|
15
|
+
export { Tooltip } from "./Tooltip.js";
|
|
16
|
+
export { SessionAttemptSelector } from "./SessionAttemptSelector.js";
|
|
17
|
+
|
|
18
|
+
export type { ButtonProps, ButtonVariant, ButtonSize } from "./Button.js";
|
|
19
|
+
export type { TooltipProps, TooltipPlacement } from "./Tooltip.js";
|
|
20
|
+
export type { SessionAttemptSelectorProps } from "./SessionAttemptSelector.js";
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { expect, fn, userEvent } from "@storybook/test";
|
|
3
|
+
import { EditableCheckbox } from "./EditableCheckbox.js";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof EditableCheckbox> = {
|
|
6
|
+
component: EditableCheckbox,
|
|
7
|
+
title: "Primitives/Editable/EditableCheckbox",
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
args: {
|
|
10
|
+
onChange: fn(),
|
|
11
|
+
label: "Enable feature",
|
|
12
|
+
"data-testid": "test-checkbox",
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
export default meta;
|
|
16
|
+
type Story = StoryObj<typeof meta>;
|
|
17
|
+
|
|
18
|
+
/** Unchecked checkbox in its default state. */
|
|
19
|
+
export const Unchecked: Story = {
|
|
20
|
+
args: {
|
|
21
|
+
checked: false,
|
|
22
|
+
},
|
|
23
|
+
play: async ({ canvas }) => {
|
|
24
|
+
const label = canvas.getByTestId("test-checkbox");
|
|
25
|
+
await expect(label).toBeInTheDocument();
|
|
26
|
+
const checkbox = canvas.getByRole("checkbox");
|
|
27
|
+
await expect(checkbox).not.toBeChecked();
|
|
28
|
+
await expect(canvas.getByText("Enable feature")).toBeInTheDocument();
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** Checked checkbox. */
|
|
33
|
+
export const Checked: Story = {
|
|
34
|
+
args: {
|
|
35
|
+
checked: true,
|
|
36
|
+
},
|
|
37
|
+
play: async ({ canvas }) => {
|
|
38
|
+
const checkbox = canvas.getByRole("checkbox");
|
|
39
|
+
await expect(checkbox).toBeChecked();
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Clicking the checkbox triggers onChange with the new value. */
|
|
44
|
+
export const ToggleOn: Story = {
|
|
45
|
+
args: {
|
|
46
|
+
checked: false,
|
|
47
|
+
},
|
|
48
|
+
play: async ({ canvas, args }) => {
|
|
49
|
+
const checkbox = canvas.getByRole("checkbox");
|
|
50
|
+
await expect(checkbox).not.toBeChecked();
|
|
51
|
+
await userEvent.click(checkbox);
|
|
52
|
+
await expect(args.onChange).toHaveBeenCalledWith(true);
|
|
53
|
+
},
|
|
54
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { JSX } from "react";
|
|
2
|
+
import styles from "./EditableField.module.scss";
|
|
3
|
+
|
|
4
|
+
/** Props for EditableCheckbox. */
|
|
5
|
+
export interface EditableCheckboxProps {
|
|
6
|
+
/** Whether the checkbox is checked. */
|
|
7
|
+
checked: boolean;
|
|
8
|
+
/** Called when the checkbox value changes. */
|
|
9
|
+
onChange: (checked: boolean) => void;
|
|
10
|
+
/** Label text displayed next to the checkbox. */
|
|
11
|
+
label: string;
|
|
12
|
+
/** Accessible label for the checkbox. */
|
|
13
|
+
ariaLabel?: string;
|
|
14
|
+
/** Test ID for the wrapping label element. */
|
|
15
|
+
"data-testid"?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Simple checkbox toggle — no edit/display mode, always interactive. */
|
|
19
|
+
export function EditableCheckbox(props: EditableCheckboxProps): JSX.Element {
|
|
20
|
+
const {
|
|
21
|
+
checked,
|
|
22
|
+
onChange,
|
|
23
|
+
label,
|
|
24
|
+
ariaLabel,
|
|
25
|
+
"data-testid": testId,
|
|
26
|
+
} = props;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<label className={styles.worktreeToggle} data-testid={testId}>
|
|
30
|
+
<input
|
|
31
|
+
type="checkbox"
|
|
32
|
+
checked={checked}
|
|
33
|
+
onChange={(e) => onChange(e.target.checked)}
|
|
34
|
+
aria-label={ariaLabel}
|
|
35
|
+
/>
|
|
36
|
+
<span>{label}</span>
|
|
37
|
+
</label>
|
|
38
|
+
);
|
|
39
|
+
}
|