@grackle-ai/web-components 0.115.2 → 0.117.0
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/{49e5384757b767ffca6c218faf139f6813911f4a.untar.log → 8d9be4152bfcbf796b578ac87621b34484202bd0.untar.log} +2 -2
- package/.rush/temp/{b32d9c7748f6c2c43df816a4bdd427ae0c7f1e32.untar.log → be1751e9cb123b206e39fdb59b24fd82523d77e2.untar.log} +2 -2
- package/.rush/temp/operation/_phase_build/all.log +13 -6
- package/.rush/temp/operation/_phase_build/log-chunks.jsonl +13 -6
- package/.rush/temp/operation/_phase_build/state.json +1 -1
- package/.rush/temp/operation/_phase_test/all.log +29 -28
- package/.rush/temp/operation/_phase_test/log-chunks.jsonl +29 -28
- package/.rush/temp/operation/_phase_test/state.json +1 -1
- package/.rush/temp/shrinkwrap-deps.json +13 -1
- package/README.md +3 -3
- package/config/rush-project.json +1 -1
- package/dist/index.css +1 -1
- package/dist/index.js +9068 -9498
- package/package.json +4 -2
- package/rush-logs/web-components._phase_build.cache.log +2 -2
- package/rush-logs/web-components._phase_test.cache.log +1 -1
- package/src/components/display/EventRenderer.stories.tsx +22 -0
- package/src/components/display/EventRenderer.tsx +28 -4
- package/src/components/index.ts +0 -3
- package/src/components/knowledge/KnowledgeDetailPanel.tsx +1 -4
- package/src/components/layout/AppNav.stories.tsx +3 -6
- package/src/components/layout/AppNav.tsx +3 -7
- package/src/components/layout/BottomStatusBar.tsx +4 -6
- package/src/components/lists/index.ts +0 -1
- package/src/components/panels/KeyboardShortcutsPanel.tsx +0 -1
- package/src/components/panels/index.ts +0 -1
- package/src/components/personas/McpToolSelector.stories.tsx +12 -12
- package/src/components/tools/ToolCard.stories.tsx +0 -26
- package/src/components/tools/ToolCard.tsx +0 -3
- package/src/components/tools/ToolSearchCard.stories.tsx +8 -8
- package/src/components/tools/WorkpadCard.stories.tsx +5 -5
- package/src/components/tools/classifyTool.test.ts +0 -1
- package/src/components/tools/classifyTool.ts +2 -7
- package/src/context/GrackleContextTypes.ts +1 -3
- package/src/hooks/types.ts +1 -44
- package/src/index.ts +4 -8
- package/src/mcp-runtime/index.tsx +99 -0
- package/src/mocks/MockGrackleProvider.tsx +0 -75
- package/src/mocks/mockData.ts +8 -99
- package/src/test-utils/storybook-helpers.ts +0 -19
- package/src/utils/breadcrumbs.test.ts +0 -43
- package/src/utils/breadcrumbs.ts +1 -37
- package/src/utils/navigation.ts +1 -20
- package/src/utils/route-config.test.ts +0 -31
- package/vite.config.ts +46 -2
- package/.rush/temp/49e5384757b767ffca6c218faf139f6813911f4a.tar.log +0 -12
- package/.rush/temp/b32d9c7748f6c2c43df816a4bdd427ae0c7f1e32.tar.log +0 -236
- package/.rush/temp/chunked-rush-logs/web-components._phase_build.chunks.jsonl +0 -19
- package/.rush/temp/chunked-rush-logs/web-components._phase_test.chunks.jsonl +0 -126
- package/rush-logs/web-components._phase_build.log +0 -19
- package/rush-logs/web-components._phase_test.log +0 -126
- package/src/components/lists/FindingsNav.module.scss +0 -126
- package/src/components/lists/FindingsNav.tsx +0 -146
- package/src/components/panels/FindingsPanel.module.scss +0 -94
- package/src/components/panels/FindingsPanel.stories.tsx +0 -109
- package/src/components/panels/FindingsPanel.tsx +0 -76
- package/src/components/tools/FindingCard.stories.tsx +0 -124
- package/src/components/tools/FindingCard.tsx +0 -178
- package/src/utils/findingCategory.ts +0 -33
- package/temp/build/lint/_eslint-5eVG3S6w.json +0 -850
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
Invoking: heft run --only test
|
|
2
|
-
The provided list of phases does not contain all phase dependencies. You may need to run the excluded phases manually.
|
|
3
|
-
---- test started ----
|
|
4
|
-
[test:vitest] Running vitest...
|
|
5
|
-
|
|
6
|
-
RUN v3.2.4 /home/runner/work/grackle/grackle/packages/web-components
|
|
7
|
-
|
|
8
|
-
✓ src/utils/sessionEvents.test.ts (14 tests) 51ms
|
|
9
|
-
✓ src/utils/eventContent.test.ts (38 tests) 149ms
|
|
10
|
-
✓ src/utils/dashboard.test.ts (4 tests) 32ms
|
|
11
|
-
✓ src/utils/route-config.test.ts (23 tests) 64ms
|
|
12
|
-
✓ src/utils/streamCoordination.test.ts (10 tests) 25ms
|
|
13
|
-
✓ src/utils/breadcrumbs.test.ts (18 tests) 37ms
|
|
14
|
-
✓ src/utils/scrollUtils.test.ts (11 tests) 21ms
|
|
15
|
-
✓ src/components/tools/classifyTool.test.ts (6 tests) 15ms
|
|
16
|
-
✓ src/utils/assetUrl.test.ts (3 tests) 17ms
|
|
17
|
-
✓ src/components/tools/toolCardHelpers.test.ts (10 tests) 25ms
|
|
18
|
-
✓ src/components/display/extractText.test.tsx (8 tests) 14ms
|
|
19
|
-
✓ src/components/editable/useEditableField.test.tsx (17 tests) 236ms
|
|
20
|
-
✓ src/hooks/useEventSelection.test.ts (13 tests) 241ms
|
|
21
|
-
✓ src/utils/grackleHostStyleVariables.test.ts (2 tests) 414ms
|
|
22
|
-
✓ grackleHostStyleVariables > always returns the MCP-standard fallback variables 383ms
|
|
23
|
-
✓ src/components/notifications/UpdateBanner.test.tsx (4 tests) 270ms
|
|
24
|
-
✓ src/components/display/McpAppWidget.test.tsx (3 tests) 302ms
|
|
25
|
-
|
|
26
|
-
Test Files 16 passed (16)
|
|
27
|
-
Tests 184 passed (184)
|
|
28
|
-
Start at 20:50:45
|
|
29
|
-
Duration 17.91s (transform 2.37s, setup 0ms, collect 19.13s, tests 1.91s, environment 15.06s, prepare 5.96s)
|
|
30
|
-
|
|
31
|
-
[test:vitest] Vitest completed.
|
|
32
|
-
[test:storybook-test] Starting Storybook static server on port 41393...
|
|
33
|
-
[test:storybook-test] Storybook server ready. Running interaction tests...
|
|
34
|
-
jest-haste-map: duplicate manual mock found: adapter-manager
|
|
35
|
-
The following files share their name; please delete one of them:
|
|
36
|
-
* <rootDir>/packages/server/dist/__mocks__/adapter-manager.js
|
|
37
|
-
* <rootDir>/packages/server/src/__mocks__/adapter-manager.ts
|
|
38
|
-
|
|
39
|
-
jest-haste-map: duplicate manual mock found: auto-reconnect
|
|
40
|
-
The following files share their name; please delete one of them:
|
|
41
|
-
* <rootDir>/packages/server/dist/__mocks__/auto-reconnect.js
|
|
42
|
-
* <rootDir>/packages/server/src/__mocks__/auto-reconnect.ts
|
|
43
|
-
|
|
44
|
-
jest-haste-map: duplicate manual mock found: event-bus
|
|
45
|
-
The following files share their name; please delete one of them:
|
|
46
|
-
* <rootDir>/packages/server/dist/__mocks__/event-bus.js
|
|
47
|
-
* <rootDir>/packages/server/src/__mocks__/event-bus.ts
|
|
48
|
-
|
|
49
|
-
jest-haste-map: duplicate manual mock found: event-processor
|
|
50
|
-
The following files share their name; please delete one of them:
|
|
51
|
-
* <rootDir>/packages/server/dist/__mocks__/event-processor.js
|
|
52
|
-
* <rootDir>/packages/server/src/__mocks__/event-processor.ts
|
|
53
|
-
|
|
54
|
-
jest-haste-map: duplicate manual mock found: github-import
|
|
55
|
-
The following files share their name; please delete one of them:
|
|
56
|
-
* <rootDir>/packages/server/dist/__mocks__/github-import.js
|
|
57
|
-
* <rootDir>/packages/server/src/__mocks__/github-import.ts
|
|
58
|
-
|
|
59
|
-
jest-haste-map: duplicate manual mock found: lifecycle
|
|
60
|
-
The following files share their name; please delete one of them:
|
|
61
|
-
* <rootDir>/packages/server/dist/__mocks__/lifecycle.js
|
|
62
|
-
* <rootDir>/packages/server/src/__mocks__/lifecycle.ts
|
|
63
|
-
|
|
64
|
-
jest-haste-map: duplicate manual mock found: log-writer
|
|
65
|
-
The following files share their name; please delete one of them:
|
|
66
|
-
* <rootDir>/packages/server/dist/__mocks__/log-writer.js
|
|
67
|
-
* <rootDir>/packages/server/src/__mocks__/log-writer.ts
|
|
68
|
-
|
|
69
|
-
jest-haste-map: duplicate manual mock found: logger
|
|
70
|
-
The following files share their name; please delete one of them:
|
|
71
|
-
* <rootDir>/packages/server/dist/__mocks__/logger.js
|
|
72
|
-
* <rootDir>/packages/server/src/__mocks__/logger.ts
|
|
73
|
-
|
|
74
|
-
jest-haste-map: duplicate manual mock found: pipe-delivery
|
|
75
|
-
The following files share their name; please delete one of them:
|
|
76
|
-
* <rootDir>/packages/server/dist/__mocks__/pipe-delivery.js
|
|
77
|
-
* <rootDir>/packages/server/src/__mocks__/pipe-delivery.ts
|
|
78
|
-
|
|
79
|
-
jest-haste-map: duplicate manual mock found: processor-registry
|
|
80
|
-
The following files share their name; please delete one of them:
|
|
81
|
-
* <rootDir>/packages/server/dist/__mocks__/processor-registry.js
|
|
82
|
-
* <rootDir>/packages/server/src/__mocks__/processor-registry.ts
|
|
83
|
-
|
|
84
|
-
jest-haste-map: duplicate manual mock found: reanimate-agent
|
|
85
|
-
The following files share their name; please delete one of them:
|
|
86
|
-
* <rootDir>/packages/server/dist/__mocks__/reanimate-agent.js
|
|
87
|
-
* <rootDir>/packages/server/src/__mocks__/reanimate-agent.ts
|
|
88
|
-
|
|
89
|
-
jest-haste-map: duplicate manual mock found: session-recovery
|
|
90
|
-
The following files share their name; please delete one of them:
|
|
91
|
-
* <rootDir>/packages/server/dist/__mocks__/session-recovery.js
|
|
92
|
-
* <rootDir>/packages/server/src/__mocks__/session-recovery.ts
|
|
93
|
-
|
|
94
|
-
jest-haste-map: duplicate manual mock found: stream-hub
|
|
95
|
-
The following files share their name; please delete one of them:
|
|
96
|
-
* <rootDir>/packages/server/dist/__mocks__/stream-hub.js
|
|
97
|
-
* <rootDir>/packages/server/src/__mocks__/stream-hub.ts
|
|
98
|
-
|
|
99
|
-
jest-haste-map: duplicate manual mock found: stream-registry
|
|
100
|
-
The following files share their name; please delete one of them:
|
|
101
|
-
* <rootDir>/packages/server/dist/__mocks__/stream-registry.js
|
|
102
|
-
* <rootDir>/packages/server/src/__mocks__/stream-registry.ts
|
|
103
|
-
|
|
104
|
-
jest-haste-map: duplicate manual mock found: token-push
|
|
105
|
-
The following files share their name; please delete one of them:
|
|
106
|
-
* <rootDir>/packages/server/dist/__mocks__/token-push.js
|
|
107
|
-
* <rootDir>/packages/server/src/__mocks__/token-push.ts
|
|
108
|
-
|
|
109
|
-
jest-haste-map: duplicate manual mock found: utils/exec
|
|
110
|
-
The following files share their name; please delete one of them:
|
|
111
|
-
* <rootDir>/packages/server/dist/__mocks__/utils/exec.js
|
|
112
|
-
* <rootDir>/packages/server/src/__mocks__/utils/exec.ts
|
|
113
|
-
|
|
114
|
-
jest-haste-map: duplicate manual mock found: utils/format-gh-error
|
|
115
|
-
The following files share their name; please delete one of them:
|
|
116
|
-
* <rootDir>/packages/server/dist/__mocks__/utils/format-gh-error.js
|
|
117
|
-
* <rootDir>/packages/server/src/__mocks__/utils/format-gh-error.ts
|
|
118
|
-
|
|
119
|
-
jest-haste-map: duplicate manual mock found: utils/network
|
|
120
|
-
The following files share their name; please delete one of them:
|
|
121
|
-
* <rootDir>/packages/server/dist/__mocks__/utils/network.js
|
|
122
|
-
* <rootDir>/packages/server/src/__mocks__/utils/network.ts
|
|
123
|
-
|
|
124
|
-
[test:storybook-test] Storybook interaction tests completed.
|
|
125
|
-
---- test finished (122.666s) ----
|
|
126
|
-
-------------------- Finished (122.677s) --------------------
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
@use '../../styles/mixins' as *;
|
|
2
|
-
|
|
3
|
-
.nav {
|
|
4
|
-
display: flex;
|
|
5
|
-
flex-direction: column;
|
|
6
|
-
gap: var(--space-xs);
|
|
7
|
-
width: 100%;
|
|
8
|
-
padding: var(--space-md);
|
|
9
|
-
overflow-y: auto;
|
|
10
|
-
|
|
11
|
-
@include mobile {
|
|
12
|
-
flex-direction: row;
|
|
13
|
-
width: 100%;
|
|
14
|
-
min-width: unset;
|
|
15
|
-
border-right: none;
|
|
16
|
-
border-bottom: 1px solid var(--border-subtle);
|
|
17
|
-
overflow-x: auto;
|
|
18
|
-
overflow-y: hidden;
|
|
19
|
-
padding: var(--space-xs) var(--space-sm);
|
|
20
|
-
gap: 0;
|
|
21
|
-
flex-wrap: nowrap;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
.categoryPills {
|
|
26
|
-
display: flex;
|
|
27
|
-
gap: var(--space-xs);
|
|
28
|
-
flex-wrap: wrap;
|
|
29
|
-
padding-bottom: var(--space-sm);
|
|
30
|
-
border-bottom: 1px solid var(--border-subtle);
|
|
31
|
-
margin-bottom: var(--space-xs);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
.categoryPill {
|
|
35
|
-
font-size: 10px;
|
|
36
|
-
font-weight: var(--font-weight-bold);
|
|
37
|
-
text-transform: uppercase;
|
|
38
|
-
padding: 1px var(--space-xs);
|
|
39
|
-
border-radius: var(--radius-full);
|
|
40
|
-
background: var(--bg-elevated);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
.tab {
|
|
44
|
-
display: flex;
|
|
45
|
-
align-items: flex-start;
|
|
46
|
-
gap: var(--space-sm);
|
|
47
|
-
padding: var(--space-sm) var(--space-md);
|
|
48
|
-
border: none;
|
|
49
|
-
border-left: 3px solid transparent;
|
|
50
|
-
border-radius: var(--radius-md);
|
|
51
|
-
background: transparent;
|
|
52
|
-
color: var(--text-secondary);
|
|
53
|
-
font-size: var(--font-size-sm);
|
|
54
|
-
font-family: var(--font-ui);
|
|
55
|
-
cursor: pointer;
|
|
56
|
-
transition: background var(--transition-fast),
|
|
57
|
-
color var(--transition-fast),
|
|
58
|
-
border-color var(--transition-fast);
|
|
59
|
-
text-align: left;
|
|
60
|
-
width: 100%;
|
|
61
|
-
|
|
62
|
-
&:hover {
|
|
63
|
-
background: var(--bg-overlay);
|
|
64
|
-
color: var(--text-primary);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
&:focus-visible {
|
|
68
|
-
outline: 2px solid var(--accent-blue);
|
|
69
|
-
outline-offset: -2px;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
@include mobile {
|
|
73
|
-
border-left: none;
|
|
74
|
-
border-bottom: 2px solid transparent;
|
|
75
|
-
white-space: nowrap;
|
|
76
|
-
flex-shrink: 0;
|
|
77
|
-
padding: var(--space-xs) var(--space-sm);
|
|
78
|
-
font-size: var(--font-size-xs);
|
|
79
|
-
width: auto;
|
|
80
|
-
border-radius: 0;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
.tabActive {
|
|
85
|
-
border-left-color: var(--accent-blue);
|
|
86
|
-
background: var(--bg-overlay);
|
|
87
|
-
color: var(--text-primary);
|
|
88
|
-
font-weight: var(--font-weight-medium);
|
|
89
|
-
|
|
90
|
-
@include mobile {
|
|
91
|
-
border-left-color: transparent;
|
|
92
|
-
border-bottom-color: var(--accent-blue);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
.tabContent {
|
|
97
|
-
display: flex;
|
|
98
|
-
flex-direction: column;
|
|
99
|
-
gap: 2px;
|
|
100
|
-
min-width: 0;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
.tabLabel {
|
|
104
|
-
overflow: hidden;
|
|
105
|
-
text-overflow: ellipsis;
|
|
106
|
-
white-space: nowrap;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
.tabMeta {
|
|
110
|
-
font-size: var(--font-size-xs);
|
|
111
|
-
color: var(--text-tertiary);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
.categoryDot {
|
|
115
|
-
flex-shrink: 0;
|
|
116
|
-
font-size: var(--font-size-xs);
|
|
117
|
-
line-height: 1;
|
|
118
|
-
margin-top: 3px;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
.empty {
|
|
122
|
-
padding: var(--space-md);
|
|
123
|
-
font-size: var(--font-size-sm);
|
|
124
|
-
color: var(--text-tertiary);
|
|
125
|
-
text-align: center;
|
|
126
|
-
}
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sidebar navigation for the Findings pages.
|
|
3
|
-
*
|
|
4
|
-
* Displays a list of findings with category pills and relative timestamps.
|
|
5
|
-
*
|
|
6
|
-
* @module
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { useCallback, useMemo, useRef, type JSX, type KeyboardEvent } from "react";
|
|
10
|
-
import { Circle } from "lucide-react";
|
|
11
|
-
import { ICON_XS } from "../../utils/iconSize.js";
|
|
12
|
-
import { useMatch } from "react-router";
|
|
13
|
-
import type { FindingData } from "../../hooks/types.js";
|
|
14
|
-
import { findingUrl, useAppNavigate } from "../../utils/navigation.js";
|
|
15
|
-
import { formatRelativeTime } from "../../utils/time.js";
|
|
16
|
-
import { getCategoryColor } from "../../utils/findingCategory.js";
|
|
17
|
-
import styles from "./FindingsNav.module.scss";
|
|
18
|
-
|
|
19
|
-
/** Props for the FindingsNav component. */
|
|
20
|
-
interface FindingsNavProps {
|
|
21
|
-
/** All loaded findings to display. */
|
|
22
|
-
findings: FindingData[];
|
|
23
|
-
/** Optional workspace ID for scoped navigation. */
|
|
24
|
-
workspaceId?: string;
|
|
25
|
-
/** Optional environment ID for scoped navigation. */
|
|
26
|
-
environmentId?: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Sidebar nav listing findings with category badges and relative timestamps. */
|
|
30
|
-
export function FindingsNav({ findings, workspaceId, environmentId }: FindingsNavProps): JSX.Element {
|
|
31
|
-
const navigate = useAppNavigate();
|
|
32
|
-
const tabListRef = useRef<HTMLElement>(null);
|
|
33
|
-
|
|
34
|
-
// Match both global and workspace-scoped finding detail routes.
|
|
35
|
-
const globalMatch = useMatch("/findings/:findingId");
|
|
36
|
-
const scopedMatch = useMatch("/environments/:environmentId/workspaces/:workspaceId/findings/:findingId");
|
|
37
|
-
const activeFindingId = globalMatch?.params.findingId ?? scopedMatch?.params.findingId;
|
|
38
|
-
|
|
39
|
-
/** Unique categories derived from the current findings list. */
|
|
40
|
-
const categories = useMemo(() => {
|
|
41
|
-
const cats = new Set(findings.map((f) => f.category).filter(Boolean));
|
|
42
|
-
return Array.from(cats).sort();
|
|
43
|
-
}, [findings]);
|
|
44
|
-
|
|
45
|
-
const handleClick = useCallback((findingId: string) => {
|
|
46
|
-
navigate(findingUrl(findingId, workspaceId, environmentId));
|
|
47
|
-
}, [navigate, workspaceId, environmentId]);
|
|
48
|
-
|
|
49
|
-
const handleKeyDown = useCallback((e: KeyboardEvent<HTMLElement>) => {
|
|
50
|
-
const buttons = tabListRef.current?.querySelectorAll<HTMLButtonElement>('[role="tab"]');
|
|
51
|
-
if (!buttons || buttons.length === 0) {
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
const focusedIndex = Array.from(buttons).findIndex((b) => b === document.activeElement);
|
|
55
|
-
const currentIndex = focusedIndex >= 0 ? focusedIndex : findings.findIndex((f) => f.id === activeFindingId);
|
|
56
|
-
let nextIndex = currentIndex;
|
|
57
|
-
|
|
58
|
-
if (e.key === "ArrowDown" || e.key === "j" || e.key === "J") {
|
|
59
|
-
e.preventDefault();
|
|
60
|
-
nextIndex = (currentIndex + 1) % buttons.length;
|
|
61
|
-
} else if (e.key === "ArrowUp" || e.key === "k" || e.key === "K") {
|
|
62
|
-
e.preventDefault();
|
|
63
|
-
nextIndex = (currentIndex - 1 + buttons.length) % buttons.length;
|
|
64
|
-
} else if (e.key === "Home") {
|
|
65
|
-
e.preventDefault();
|
|
66
|
-
nextIndex = 0;
|
|
67
|
-
} else if (e.key === "End") {
|
|
68
|
-
e.preventDefault();
|
|
69
|
-
nextIndex = buttons.length - 1;
|
|
70
|
-
} else {
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (nextIndex < findings.length) {
|
|
75
|
-
navigate(findingUrl(findings[nextIndex].id, workspaceId, environmentId));
|
|
76
|
-
}
|
|
77
|
-
buttons[nextIndex].focus();
|
|
78
|
-
}, [activeFindingId, findings, navigate, workspaceId, environmentId]);
|
|
79
|
-
|
|
80
|
-
const focusableId = activeFindingId ?? (findings.length > 0 ? findings[0].id : undefined);
|
|
81
|
-
|
|
82
|
-
return (
|
|
83
|
-
<div className={styles.nav} data-testid="findings-nav">
|
|
84
|
-
{categories.length > 1 && (
|
|
85
|
-
<div className={styles.categoryPills} data-testid="findings-nav-categories">
|
|
86
|
-
{categories.map((cat) => (
|
|
87
|
-
<span
|
|
88
|
-
key={cat}
|
|
89
|
-
className={styles.categoryPill}
|
|
90
|
-
style={{ color: getCategoryColor(cat).text }}
|
|
91
|
-
>
|
|
92
|
-
{cat}
|
|
93
|
-
</span>
|
|
94
|
-
))}
|
|
95
|
-
</div>
|
|
96
|
-
)}
|
|
97
|
-
|
|
98
|
-
<nav
|
|
99
|
-
ref={tabListRef}
|
|
100
|
-
role="tablist"
|
|
101
|
-
aria-orientation="vertical"
|
|
102
|
-
aria-label="Findings"
|
|
103
|
-
onKeyDown={handleKeyDown}
|
|
104
|
-
>
|
|
105
|
-
{findings.map((f) => {
|
|
106
|
-
const isActive = f.id === activeFindingId;
|
|
107
|
-
const isFocusable = f.id === focusableId;
|
|
108
|
-
return (
|
|
109
|
-
<button
|
|
110
|
-
key={f.id}
|
|
111
|
-
role="tab"
|
|
112
|
-
type="button"
|
|
113
|
-
aria-selected={isActive}
|
|
114
|
-
tabIndex={isFocusable ? 0 : -1}
|
|
115
|
-
className={`${styles.tab} ${isActive ? styles.tabActive : ""}`}
|
|
116
|
-
onClick={() => handleClick(f.id)}
|
|
117
|
-
data-testid="finding-nav-item"
|
|
118
|
-
>
|
|
119
|
-
<span
|
|
120
|
-
className={styles.categoryDot}
|
|
121
|
-
style={{ color: getCategoryColor(f.category).text }}
|
|
122
|
-
aria-hidden="true"
|
|
123
|
-
>
|
|
124
|
-
<Circle size={ICON_XS} fill="currentColor" />
|
|
125
|
-
</span>
|
|
126
|
-
<span className={styles.tabContent}>
|
|
127
|
-
<span className={styles.tabLabel} title={f.title}>
|
|
128
|
-
{f.title}
|
|
129
|
-
</span>
|
|
130
|
-
<span className={styles.tabMeta} title={f.createdAt}>
|
|
131
|
-
{formatRelativeTime(f.createdAt)}
|
|
132
|
-
</span>
|
|
133
|
-
</span>
|
|
134
|
-
</button>
|
|
135
|
-
);
|
|
136
|
-
})}
|
|
137
|
-
</nav>
|
|
138
|
-
|
|
139
|
-
{findings.length === 0 && (
|
|
140
|
-
<div className={styles.empty}>
|
|
141
|
-
No findings yet. Agents will post discoveries here.
|
|
142
|
-
</div>
|
|
143
|
-
)}
|
|
144
|
-
</div>
|
|
145
|
-
);
|
|
146
|
-
}
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
@use '../../styles/mixins' as *;
|
|
2
|
-
|
|
3
|
-
// =============================================================================
|
|
4
|
-
// Findings Panel — workspace findings cards with staggered animation
|
|
5
|
-
// =============================================================================
|
|
6
|
-
|
|
7
|
-
.container {
|
|
8
|
-
padding: var(--space-md);
|
|
9
|
-
display: flex;
|
|
10
|
-
flex-direction: column;
|
|
11
|
-
gap: var(--space-sm);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
.emptyState {
|
|
15
|
-
padding: var(--space-xl);
|
|
16
|
-
color: var(--text-tertiary);
|
|
17
|
-
text-align: center;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
.card {
|
|
21
|
-
@include surface-card;
|
|
22
|
-
@include card-hover;
|
|
23
|
-
padding: var(--space-md);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
.cardClickable {
|
|
27
|
-
cursor: pointer;
|
|
28
|
-
|
|
29
|
-
&:focus-visible {
|
|
30
|
-
outline: 2px solid var(--accent-blue);
|
|
31
|
-
outline-offset: 2px;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
.cardHeader {
|
|
36
|
-
display: flex;
|
|
37
|
-
align-items: center;
|
|
38
|
-
gap: var(--space-sm);
|
|
39
|
-
margin-bottom: var(--space-xs);
|
|
40
|
-
|
|
41
|
-
@include mobile {
|
|
42
|
-
flex-wrap: wrap;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
.categoryBadge {
|
|
47
|
-
border-radius: var(--radius-full);
|
|
48
|
-
padding: 2px var(--space-md);
|
|
49
|
-
font-size: var(--font-size-xs);
|
|
50
|
-
font-weight: var(--font-weight-bold);
|
|
51
|
-
text-transform: uppercase;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
.findingTitle {
|
|
55
|
-
font-weight: var(--font-weight-bold);
|
|
56
|
-
color: var(--text-primary);
|
|
57
|
-
font-size: var(--font-size-md);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
.findingDate {
|
|
61
|
-
margin-left: auto;
|
|
62
|
-
font-size: var(--font-size-xs);
|
|
63
|
-
color: var(--text-tertiary);
|
|
64
|
-
|
|
65
|
-
@include mobile {
|
|
66
|
-
margin-left: 0;
|
|
67
|
-
width: 100%;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
.findingContent {
|
|
72
|
-
font-size: var(--font-size-sm);
|
|
73
|
-
color: var(--text-secondary);
|
|
74
|
-
white-space: pre-wrap;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
.tags {
|
|
78
|
-
margin-top: var(--space-xs);
|
|
79
|
-
display: flex;
|
|
80
|
-
gap: var(--space-xs);
|
|
81
|
-
|
|
82
|
-
@include mobile {
|
|
83
|
-
flex-wrap: wrap;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Frosted inset chip for tags — third glass tier with category-tinted border
|
|
88
|
-
.tag {
|
|
89
|
-
@include surface-inset;
|
|
90
|
-
font-size: 10px;
|
|
91
|
-
padding: 1px var(--space-xs);
|
|
92
|
-
border-radius: var(--radius-full);
|
|
93
|
-
color: var(--text-secondary);
|
|
94
|
-
}
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
-
import { expect } from "@storybook/test";
|
|
3
|
-
import { FindingsPanel } from "./FindingsPanel.js";
|
|
4
|
-
import { buildFinding } from "../../test-utils/storybook-helpers.js";
|
|
5
|
-
|
|
6
|
-
const meta: Meta<typeof FindingsPanel> = {
|
|
7
|
-
title: "Grackle/Panels/FindingsPanel",
|
|
8
|
-
component: FindingsPanel,
|
|
9
|
-
tags: ["autodocs"],
|
|
10
|
-
args: {
|
|
11
|
-
findings: [],
|
|
12
|
-
},
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export default meta;
|
|
16
|
-
type Story = StoryObj<typeof FindingsPanel>;
|
|
17
|
-
|
|
18
|
-
/** Empty state shows a placeholder message when there are no findings. */
|
|
19
|
-
export const EmptyState: Story = {
|
|
20
|
-
args: {
|
|
21
|
-
findings: [],
|
|
22
|
-
},
|
|
23
|
-
play: async ({ canvas }) => {
|
|
24
|
-
await expect(
|
|
25
|
-
canvas.getByText("No findings yet. Agents will post discoveries here."),
|
|
26
|
-
).toBeInTheDocument();
|
|
27
|
-
},
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
/** Renders a single finding card with category badge, title, content, and tags. */
|
|
31
|
-
export const SingleFinding: Story = {
|
|
32
|
-
args: {
|
|
33
|
-
findings: [
|
|
34
|
-
buildFinding({
|
|
35
|
-
id: "f-1",
|
|
36
|
-
category: "architecture",
|
|
37
|
-
title: "Service boundary issue",
|
|
38
|
-
content: "The auth service is tightly coupled to the user service.",
|
|
39
|
-
tags: ["coupling", "refactor"],
|
|
40
|
-
}),
|
|
41
|
-
],
|
|
42
|
-
},
|
|
43
|
-
play: async ({ canvas }) => {
|
|
44
|
-
await expect(canvas.getByText("architecture")).toBeInTheDocument();
|
|
45
|
-
await expect(canvas.getByText("Service boundary issue")).toBeInTheDocument();
|
|
46
|
-
await expect(canvas.getByText(/tightly coupled/)).toBeInTheDocument();
|
|
47
|
-
await expect(canvas.getByText("coupling")).toBeInTheDocument();
|
|
48
|
-
await expect(canvas.getByText("refactor")).toBeInTheDocument();
|
|
49
|
-
},
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
/** Renders multiple findings across different categories. */
|
|
53
|
-
export const MultipleFindings: Story = {
|
|
54
|
-
args: {
|
|
55
|
-
findings: [
|
|
56
|
-
buildFinding({
|
|
57
|
-
id: "f-1",
|
|
58
|
-
category: "bug",
|
|
59
|
-
title: "Race condition in session cleanup",
|
|
60
|
-
content: "When two sessions end simultaneously, the cleanup handler may skip one.",
|
|
61
|
-
tags: ["concurrency"],
|
|
62
|
-
}),
|
|
63
|
-
buildFinding({
|
|
64
|
-
id: "f-2",
|
|
65
|
-
category: "api",
|
|
66
|
-
title: "Missing pagination on list endpoints",
|
|
67
|
-
content: "The list_tasks endpoint returns all tasks without pagination support.",
|
|
68
|
-
tags: ["api", "performance"],
|
|
69
|
-
}),
|
|
70
|
-
buildFinding({
|
|
71
|
-
id: "f-3",
|
|
72
|
-
category: "decision",
|
|
73
|
-
title: "Chose SQLite over PostgreSQL",
|
|
74
|
-
content: "SQLite with WAL mode provides sufficient concurrency for single-server deployment.",
|
|
75
|
-
tags: ["database"],
|
|
76
|
-
}),
|
|
77
|
-
],
|
|
78
|
-
},
|
|
79
|
-
play: async ({ canvas }) => {
|
|
80
|
-
// Use getAllByText for "api" since it appears as both a category badge and a tag
|
|
81
|
-
await expect(canvas.getByText("bug")).toBeInTheDocument();
|
|
82
|
-
const apiElements = canvas.getAllByText("api");
|
|
83
|
-
await expect(apiElements.length).toBeGreaterThanOrEqual(1);
|
|
84
|
-
await expect(canvas.getByText("decision")).toBeInTheDocument();
|
|
85
|
-
await expect(canvas.getByText("Race condition in session cleanup")).toBeInTheDocument();
|
|
86
|
-
await expect(canvas.getByText("Missing pagination on list endpoints")).toBeInTheDocument();
|
|
87
|
-
await expect(canvas.getByText("Chose SQLite over PostgreSQL")).toBeInTheDocument();
|
|
88
|
-
},
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
/** Long content is truncated at 300 characters with an ellipsis. */
|
|
92
|
-
export const LongContentTruncated: Story = {
|
|
93
|
-
args: {
|
|
94
|
-
findings: [
|
|
95
|
-
buildFinding({
|
|
96
|
-
id: "f-long",
|
|
97
|
-
category: "pattern",
|
|
98
|
-
title: "Verbose finding",
|
|
99
|
-
content: "A".repeat(400),
|
|
100
|
-
tags: [],
|
|
101
|
-
}),
|
|
102
|
-
],
|
|
103
|
-
},
|
|
104
|
-
play: async ({ canvas }) => {
|
|
105
|
-
// The rendered content should end with "..." since it exceeds 300 chars
|
|
106
|
-
const contentEl = canvas.getByText(/A{10,}\.\.\.$/);
|
|
107
|
-
await expect(contentEl).toBeInTheDocument();
|
|
108
|
-
},
|
|
109
|
-
};
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import type { JSX } from "react";
|
|
2
|
-
import { motion } from "motion/react";
|
|
3
|
-
import type { FindingData } from "../../hooks/types.js";
|
|
4
|
-
import styles from "./FindingsPanel.module.scss";
|
|
5
|
-
import { formatRelativeTime } from "../../utils/time.js";
|
|
6
|
-
import { getCategoryColor } from "../../utils/findingCategory.js";
|
|
7
|
-
|
|
8
|
-
/** Props for the FindingsPanel component. */
|
|
9
|
-
interface Props {
|
|
10
|
-
/** Pre-filtered findings to display. */
|
|
11
|
-
findings: FindingData[];
|
|
12
|
-
/** Optional click handler for finding cards. When provided, cards become clickable. */
|
|
13
|
-
onFindingClick?: (findingId: string) => void;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** Displays workspace findings as styled cards with staggered entrance animation. */
|
|
17
|
-
export function FindingsPanel({ findings, onFindingClick }: Props): JSX.Element {
|
|
18
|
-
if (findings.length === 0) {
|
|
19
|
-
return (
|
|
20
|
-
<div className={styles.emptyState}>
|
|
21
|
-
No findings yet. Agents will post discoveries here.
|
|
22
|
-
</div>
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
return (
|
|
27
|
-
<div className={styles.container}>
|
|
28
|
-
{findings.map((f, index) => {
|
|
29
|
-
const categoryColor = getCategoryColor(f.category);
|
|
30
|
-
const Tag = onFindingClick ? motion.button : motion.div;
|
|
31
|
-
return (
|
|
32
|
-
<Tag
|
|
33
|
-
key={f.id}
|
|
34
|
-
type={onFindingClick ? "button" : undefined}
|
|
35
|
-
className={`${styles.card} ${onFindingClick ? styles.cardClickable : ""}`}
|
|
36
|
-
initial={{ opacity: 0, y: 8 }}
|
|
37
|
-
animate={{ opacity: 1, y: 0 }}
|
|
38
|
-
transition={{ delay: index * 0.05, duration: 0.2 }}
|
|
39
|
-
onClick={onFindingClick ? () => { onFindingClick(f.id); } : undefined}
|
|
40
|
-
>
|
|
41
|
-
<div className={styles.cardHeader}>
|
|
42
|
-
<span
|
|
43
|
-
className={styles.categoryBadge}
|
|
44
|
-
style={{ background: categoryColor.bg, color: categoryColor.text }}
|
|
45
|
-
>
|
|
46
|
-
{f.category}
|
|
47
|
-
</span>
|
|
48
|
-
<span className={styles.findingTitle}>
|
|
49
|
-
{f.title}
|
|
50
|
-
</span>
|
|
51
|
-
<span className={styles.findingDate} title={f.createdAt}>
|
|
52
|
-
{formatRelativeTime(f.createdAt)}
|
|
53
|
-
</span>
|
|
54
|
-
</div>
|
|
55
|
-
<div className={styles.findingContent}>
|
|
56
|
-
{f.content.length > 300 ? f.content.slice(0, 300) + "..." : f.content}
|
|
57
|
-
</div>
|
|
58
|
-
{f.tags.length > 0 && (
|
|
59
|
-
<div className={styles.tags}>
|
|
60
|
-
{f.tags.map((tag) => (
|
|
61
|
-
<span
|
|
62
|
-
key={tag}
|
|
63
|
-
className={styles.tag}
|
|
64
|
-
style={{ color: categoryColor.text, textShadow: `0 0 8px ${categoryColor.text}` }}
|
|
65
|
-
>
|
|
66
|
-
{tag}
|
|
67
|
-
</span>
|
|
68
|
-
))}
|
|
69
|
-
</div>
|
|
70
|
-
)}
|
|
71
|
-
</Tag>
|
|
72
|
-
);
|
|
73
|
-
})}
|
|
74
|
-
</div>
|
|
75
|
-
);
|
|
76
|
-
}
|