@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.
Files changed (279) hide show
  1. package/.rush/temp/3ae72563f781afd72723475938136f113846603e.untar.log +10 -0
  2. package/.rush/temp/bc1d5bf9201ce71abeaeaddd096deb9b0805d703.untar.log +10 -0
  3. package/.rush/temp/operation/_phase_build/all.log +18 -0
  4. package/.rush/temp/operation/_phase_build/log-chunks.jsonl +18 -0
  5. package/.rush/temp/operation/_phase_build/state.json +3 -0
  6. package/.rush/temp/operation/_phase_test/all.log +121 -0
  7. package/.rush/temp/operation/_phase_test/log-chunks.jsonl +121 -0
  8. package/.rush/temp/operation/_phase_test/state.json +3 -0
  9. package/.rush/temp/shrinkwrap-deps.json +938 -0
  10. package/.storybook/main.ts +22 -0
  11. package/.storybook/preview.tsx +30 -0
  12. package/config/rig.json +4 -0
  13. package/config/rush-project.json +12 -0
  14. package/dist/index.css +1 -0
  15. package/dist/index.js +39221 -0
  16. package/eslint.config.cjs +5 -0
  17. package/package.json +83 -0
  18. package/rush-logs/web-components._phase_build.cache.log +4 -0
  19. package/rush-logs/web-components._phase_test.cache.log +4 -0
  20. package/src/components/chat/ChatInput.module.scss +81 -0
  21. package/src/components/chat/ChatInput.stories.tsx +91 -0
  22. package/src/components/chat/ChatInput.tsx +168 -0
  23. package/src/components/chat/index.ts +6 -0
  24. package/src/components/dag/DagView.module.scss +149 -0
  25. package/src/components/dag/DagView.stories.tsx +125 -0
  26. package/src/components/dag/DagView.tsx +109 -0
  27. package/src/components/dag/TaskNode.stories.tsx +133 -0
  28. package/src/components/dag/TaskNode.tsx +40 -0
  29. package/src/components/dag/useDagLayout.ts +139 -0
  30. package/src/components/display/Breadcrumbs.module.scss +71 -0
  31. package/src/components/display/Breadcrumbs.stories.tsx +80 -0
  32. package/src/components/display/Breadcrumbs.tsx +46 -0
  33. package/src/components/display/Button.module.scss +110 -0
  34. package/src/components/display/Button.stories.tsx +88 -0
  35. package/src/components/display/Button.tsx +40 -0
  36. package/src/components/display/ConfirmDialog.module.scss +67 -0
  37. package/src/components/display/ConfirmDialog.stories.tsx +81 -0
  38. package/src/components/display/ConfirmDialog.tsx +88 -0
  39. package/src/components/display/CopyButton.module.scss +41 -0
  40. package/src/components/display/CopyButton.stories.tsx +78 -0
  41. package/src/components/display/CopyButton.tsx +64 -0
  42. package/src/components/display/DemoBanner.module.scss +37 -0
  43. package/src/components/display/DemoBanner.stories.tsx +40 -0
  44. package/src/components/display/DemoBanner.tsx +23 -0
  45. package/src/components/display/EventHoverRow.module.scss +102 -0
  46. package/src/components/display/EventHoverRow.stories.tsx +99 -0
  47. package/src/components/display/EventHoverRow.tsx +154 -0
  48. package/src/components/display/EventRenderer.module.scss +272 -0
  49. package/src/components/display/EventRenderer.stories.tsx +186 -0
  50. package/src/components/display/EventRenderer.tsx +271 -0
  51. package/src/components/display/EventStream.module.scss +93 -0
  52. package/src/components/display/EventStream.stories.tsx +249 -0
  53. package/src/components/display/EventStream.tsx +369 -0
  54. package/src/components/display/FloatingActionBar.module.scss +107 -0
  55. package/src/components/display/FloatingActionBar.stories.tsx +122 -0
  56. package/src/components/display/FloatingActionBar.tsx +119 -0
  57. package/src/components/display/SessionAttemptSelector.module.scss +50 -0
  58. package/src/components/display/SessionAttemptSelector.stories.tsx +78 -0
  59. package/src/components/display/SessionAttemptSelector.tsx +49 -0
  60. package/src/components/display/SessionPicker.module.scss +200 -0
  61. package/src/components/display/SessionPicker.stories.tsx +169 -0
  62. package/src/components/display/SessionPicker.tsx +214 -0
  63. package/src/components/display/Skeleton.module.scss +58 -0
  64. package/src/components/display/Skeleton.stories.tsx +94 -0
  65. package/src/components/display/Skeleton.tsx +127 -0
  66. package/src/components/display/Spinner.module.scss +41 -0
  67. package/src/components/display/Spinner.stories.tsx +66 -0
  68. package/src/components/display/Spinner.tsx +32 -0
  69. package/src/components/display/SplashScreen.module.scss +20 -0
  70. package/src/components/display/SplashScreen.stories.tsx +26 -0
  71. package/src/components/display/SplashScreen.tsx +16 -0
  72. package/src/components/display/SplitButton.module.scss +166 -0
  73. package/src/components/display/SplitButton.stories.tsx +95 -0
  74. package/src/components/display/SplitButton.tsx +128 -0
  75. package/src/components/display/Tooltip.module.scss +84 -0
  76. package/src/components/display/Tooltip.stories.tsx +240 -0
  77. package/src/components/display/Tooltip.tsx +184 -0
  78. package/src/components/display/extractText.test.tsx +48 -0
  79. package/src/components/display/index.ts +20 -0
  80. package/src/components/editable/EditableCheckbox.stories.tsx +54 -0
  81. package/src/components/editable/EditableCheckbox.tsx +39 -0
  82. package/src/components/editable/EditableField.module.scss +135 -0
  83. package/src/components/editable/EditableSelect.tsx +164 -0
  84. package/src/components/editable/EditableTextArea.stories.tsx +50 -0
  85. package/src/components/editable/EditableTextArea.tsx +148 -0
  86. package/src/components/editable/EditableTextField.stories.tsx +62 -0
  87. package/src/components/editable/EditableTextField.tsx +153 -0
  88. package/src/components/editable/EnvironmentSelect.module.scss +17 -0
  89. package/src/components/editable/EnvironmentSelect.stories.tsx +61 -0
  90. package/src/components/editable/EnvironmentSelect.tsx +87 -0
  91. package/src/components/editable/index.ts +13 -0
  92. package/src/components/editable/useEditableField.test.tsx +233 -0
  93. package/src/components/editable/useEditableField.ts +173 -0
  94. package/src/components/index.ts +20 -0
  95. package/src/components/knowledge/KnowledgeDetailPanel.module.scss +162 -0
  96. package/src/components/knowledge/KnowledgeDetailPanel.stories.tsx +208 -0
  97. package/src/components/knowledge/KnowledgeDetailPanel.tsx +122 -0
  98. package/src/components/knowledge/KnowledgeGraph.module.scss +110 -0
  99. package/src/components/knowledge/KnowledgeGraph.stories.tsx +180 -0
  100. package/src/components/knowledge/KnowledgeGraph.tsx +455 -0
  101. package/src/components/knowledge/KnowledgeNav.module.scss +130 -0
  102. package/src/components/knowledge/KnowledgeNav.stories.tsx +108 -0
  103. package/src/components/knowledge/KnowledgeNav.tsx +138 -0
  104. package/src/components/knowledge/index.ts +3 -0
  105. package/src/components/layout/AppNav.module.scss +82 -0
  106. package/src/components/layout/AppNav.stories.tsx +115 -0
  107. package/src/components/layout/AppNav.tsx +133 -0
  108. package/src/components/layout/BottomStatusBar.module.scss +58 -0
  109. package/src/components/layout/BottomStatusBar.stories.tsx +35 -0
  110. package/src/components/layout/BottomStatusBar.tsx +206 -0
  111. package/src/components/layout/Sidebar.module.scss +60 -0
  112. package/src/components/layout/Sidebar.stories.tsx +46 -0
  113. package/src/components/layout/Sidebar.tsx +84 -0
  114. package/src/components/layout/StatusBar.module.scss +108 -0
  115. package/src/components/layout/StatusBar.stories.tsx +119 -0
  116. package/src/components/layout/StatusBar.tsx +70 -0
  117. package/src/components/layout/index.ts +9 -0
  118. package/src/components/lists/EnvironmentNav.module.scss +118 -0
  119. package/src/components/lists/EnvironmentNav.stories.tsx +121 -0
  120. package/src/components/lists/EnvironmentNav.tsx +133 -0
  121. package/src/components/lists/FindingsNav.module.scss +126 -0
  122. package/src/components/lists/FindingsNav.tsx +146 -0
  123. package/src/components/lists/TaskList.module.scss +206 -0
  124. package/src/components/lists/TaskList.stories.tsx +401 -0
  125. package/src/components/lists/TaskList.tsx +509 -0
  126. package/src/components/lists/index.ts +6 -0
  127. package/src/components/lists/listHelpers.tsx +130 -0
  128. package/src/components/notifications/Callout.module.scss +83 -0
  129. package/src/components/notifications/Callout.stories.tsx +81 -0
  130. package/src/components/notifications/Callout.tsx +64 -0
  131. package/src/components/notifications/Toast.module.scss +86 -0
  132. package/src/components/notifications/Toast.stories.tsx +71 -0
  133. package/src/components/notifications/Toast.tsx +51 -0
  134. package/src/components/notifications/ToastContainer.module.scss +23 -0
  135. package/src/components/notifications/ToastContainer.stories.tsx +66 -0
  136. package/src/components/notifications/ToastContainer.tsx +29 -0
  137. package/src/components/notifications/UpdateBanner.stories.tsx +77 -0
  138. package/src/components/notifications/UpdateBanner.test.tsx +64 -0
  139. package/src/components/notifications/UpdateBanner.tsx +44 -0
  140. package/src/components/notifications/index.ts +8 -0
  141. package/src/components/panels/AboutPanel.stories.tsx +70 -0
  142. package/src/components/panels/AboutPanel.tsx +66 -0
  143. package/src/components/panels/AppearancePanel.stories.tsx +45 -0
  144. package/src/components/panels/AppearancePanel.tsx +97 -0
  145. package/src/components/panels/CredentialProvidersPanel.stories.tsx +62 -0
  146. package/src/components/panels/CredentialProvidersPanel.tsx +111 -0
  147. package/src/components/panels/EnvironmentEditPanel.module.scss +170 -0
  148. package/src/components/panels/EnvironmentEditPanel.stories.tsx +206 -0
  149. package/src/components/panels/EnvironmentEditPanel.tsx +785 -0
  150. package/src/components/panels/FindingsPanel.module.scss +94 -0
  151. package/src/components/panels/FindingsPanel.stories.tsx +109 -0
  152. package/src/components/panels/FindingsPanel.tsx +76 -0
  153. package/src/components/panels/KeyboardShortcutsPanel.module.scss +65 -0
  154. package/src/components/panels/KeyboardShortcutsPanel.stories.tsx +40 -0
  155. package/src/components/panels/KeyboardShortcutsPanel.tsx +104 -0
  156. package/src/components/panels/PluginsPanel.tsx +77 -0
  157. package/src/components/panels/SettingsPanel.module.scss +336 -0
  158. package/src/components/panels/TaskActionButtons.module.scss +22 -0
  159. package/src/components/panels/TaskActionButtons.stories.tsx +125 -0
  160. package/src/components/panels/TaskActionButtons.tsx +87 -0
  161. package/src/components/panels/TaskEditPanel.module.scss +202 -0
  162. package/src/components/panels/TaskEditPanel.stories.tsx +75 -0
  163. package/src/components/panels/TaskEditPanel.tsx +328 -0
  164. package/src/components/panels/TaskOverviewPanel.module.scss +236 -0
  165. package/src/components/panels/TaskOverviewPanel.stories.tsx +219 -0
  166. package/src/components/panels/TaskOverviewPanel.tsx +270 -0
  167. package/src/components/panels/TokensPanel.stories.tsx +131 -0
  168. package/src/components/panels/TokensPanel.tsx +143 -0
  169. package/src/components/panels/WorkpadPanel.module.scss +39 -0
  170. package/src/components/panels/WorkpadPanel.stories.tsx +56 -0
  171. package/src/components/panels/WorkpadPanel.tsx +63 -0
  172. package/src/components/panels/index.ts +13 -0
  173. package/src/components/personas/McpToolSelector.module.scss +109 -0
  174. package/src/components/personas/McpToolSelector.stories.tsx +129 -0
  175. package/src/components/personas/McpToolSelector.tsx +180 -0
  176. package/src/components/personas/PersonaManager.module.scss +233 -0
  177. package/src/components/personas/PersonaManager.stories.tsx +139 -0
  178. package/src/components/personas/PersonaManager.tsx +122 -0
  179. package/src/components/schedules/ScheduleManager.module.scss +98 -0
  180. package/src/components/schedules/ScheduleManager.stories.tsx +78 -0
  181. package/src/components/schedules/ScheduleManager.tsx +160 -0
  182. package/src/components/settings/SettingsNav.module.scss +82 -0
  183. package/src/components/settings/SettingsNav.stories.tsx +83 -0
  184. package/src/components/settings/SettingsNav.tsx +104 -0
  185. package/src/components/streams/StreamDetailPanel.module.scss +206 -0
  186. package/src/components/streams/StreamDetailPanel.stories.tsx +132 -0
  187. package/src/components/streams/StreamDetailPanel.tsx +119 -0
  188. package/src/components/streams/StreamList.module.scss +92 -0
  189. package/src/components/streams/StreamList.stories.tsx +99 -0
  190. package/src/components/streams/StreamList.tsx +114 -0
  191. package/src/components/streams/index.ts +10 -0
  192. package/src/components/tools/AgentToolCard.module.scss +118 -0
  193. package/src/components/tools/AgentToolCard.stories.tsx +304 -0
  194. package/src/components/tools/AgentToolCard.tsx +247 -0
  195. package/src/components/tools/FileEditCard.stories.tsx +138 -0
  196. package/src/components/tools/FileEditCard.tsx +160 -0
  197. package/src/components/tools/FileReadCard.stories.tsx +120 -0
  198. package/src/components/tools/FileReadCard.tsx +106 -0
  199. package/src/components/tools/FindingCard.stories.tsx +124 -0
  200. package/src/components/tools/FindingCard.tsx +178 -0
  201. package/src/components/tools/GenericToolCard.stories.tsx +80 -0
  202. package/src/components/tools/GenericToolCard.tsx +111 -0
  203. package/src/components/tools/IpcCard.stories.tsx +129 -0
  204. package/src/components/tools/IpcCard.tsx +178 -0
  205. package/src/components/tools/KnowledgeCard.stories.tsx +112 -0
  206. package/src/components/tools/KnowledgeCard.tsx +165 -0
  207. package/src/components/tools/MetadataCard.stories.tsx +32 -0
  208. package/src/components/tools/MetadataCard.tsx +39 -0
  209. package/src/components/tools/SearchCard.stories.tsx +74 -0
  210. package/src/components/tools/SearchCard.tsx +86 -0
  211. package/src/components/tools/ShellCard.stories.tsx +112 -0
  212. package/src/components/tools/ShellCard.tsx +106 -0
  213. package/src/components/tools/TaskCard.stories.tsx +123 -0
  214. package/src/components/tools/TaskCard.tsx +203 -0
  215. package/src/components/tools/TodoCard.module.scss +131 -0
  216. package/src/components/tools/TodoCard.stories.tsx +202 -0
  217. package/src/components/tools/TodoCard.tsx +200 -0
  218. package/src/components/tools/ToolCard.stories.tsx +177 -0
  219. package/src/components/tools/ToolCard.tsx +60 -0
  220. package/src/components/tools/ToolCardProps.ts +20 -0
  221. package/src/components/tools/ToolSearchCard.stories.tsx +81 -0
  222. package/src/components/tools/ToolSearchCard.tsx +86 -0
  223. package/src/components/tools/WorkpadCard.stories.tsx +106 -0
  224. package/src/components/tools/WorkpadCard.tsx +125 -0
  225. package/src/components/tools/classifyTool.test.ts +44 -0
  226. package/src/components/tools/classifyTool.ts +134 -0
  227. package/src/components/tools/parseDiff.ts +95 -0
  228. package/src/components/tools/parseShellOutput.ts +28 -0
  229. package/src/components/tools/toolCardHelpers.test.ts +53 -0
  230. package/src/components/tools/toolCards.module.scss +234 -0
  231. package/src/components/workspace/WorkspaceBoard.module.scss +238 -0
  232. package/src/components/workspace/WorkspaceBoard.stories.tsx +240 -0
  233. package/src/components/workspace/WorkspaceBoard.tsx +232 -0
  234. package/src/components/workspace/WorkspaceFormFields.module.scss +79 -0
  235. package/src/components/workspace/WorkspaceFormFields.stories.tsx +133 -0
  236. package/src/components/workspace/WorkspaceFormFields.tsx +185 -0
  237. package/src/context/GrackleContext.ts +28 -0
  238. package/src/context/GrackleContextTypes.ts +64 -0
  239. package/src/context/SidebarContext.tsx +53 -0
  240. package/src/context/ThemeContext.tsx +21 -0
  241. package/src/context/ToastContext.tsx +56 -0
  242. package/src/hooks/types.ts +864 -0
  243. package/src/hooks/useEventSelection.test.ts +204 -0
  244. package/src/hooks/useEventSelection.ts +158 -0
  245. package/src/hooks/useSmartScroll.ts +151 -0
  246. package/src/hooks/useTheme.ts +228 -0
  247. package/src/index.ts +210 -0
  248. package/src/mocks/MockGrackleProvider.tsx +1397 -0
  249. package/src/mocks/mockData.ts +1966 -0
  250. package/src/mocks/mockKnowledgeData.ts +294 -0
  251. package/src/scss.d.ts +12 -0
  252. package/src/styles/global.scss +244 -0
  253. package/src/styles/mixins.scss +278 -0
  254. package/src/styles/prism-theme.scss +148 -0
  255. package/src/styles/theme.scss +1102 -0
  256. package/src/test-utils/storybook-decorators.tsx +50 -0
  257. package/src/test-utils/storybook-helpers.ts +262 -0
  258. package/src/themes.ts +142 -0
  259. package/src/utils/boardColumns.ts +141 -0
  260. package/src/utils/breadcrumbs.test.ts +285 -0
  261. package/src/utils/breadcrumbs.ts +222 -0
  262. package/src/utils/dashboard.test.ts +156 -0
  263. package/src/utils/dashboard.ts +195 -0
  264. package/src/utils/eventContent.test.ts +353 -0
  265. package/src/utils/eventContent.ts +209 -0
  266. package/src/utils/findingCategory.ts +33 -0
  267. package/src/utils/format.ts +27 -0
  268. package/src/utils/iconSize.ts +18 -0
  269. package/src/utils/navigation.ts +205 -0
  270. package/src/utils/route-config.test.ts +128 -0
  271. package/src/utils/scrollUtils.test.ts +65 -0
  272. package/src/utils/scrollUtils.ts +49 -0
  273. package/src/utils/sessionEvents.test.ts +302 -0
  274. package/src/utils/sessionEvents.ts +233 -0
  275. package/src/utils/taskStatus.tsx +137 -0
  276. package/src/utils/time.ts +92 -0
  277. package/tsconfig.json +8 -0
  278. package/vite.config.ts +20 -0
  279. package/vitest.config.ts +10 -0
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Knowledge graph sidebar navigation — search, workspace filter, and node list.
3
+ *
4
+ * Rendered inside the shared Sidebar component when the Knowledge tab is active.
5
+ *
6
+ * @module
7
+ */
8
+
9
+ import { useState, useCallback, type FormEvent, type JSX } from "react";
10
+ import type { Workspace } from "../../hooks/types.js";
11
+ import type { GraphNode } from "../../hooks/types.js";
12
+ import styles from "./KnowledgeNav.module.scss";
13
+
14
+ /** Props for the KnowledgeNav sidebar component. */
15
+ export interface KnowledgeNavProps {
16
+ /** Nodes currently in the graph. */
17
+ nodes: GraphNode[];
18
+ /** All workspaces for the filter dropdown. */
19
+ workspaces: Workspace[];
20
+ /** Whether a search or load operation is in progress. */
21
+ loading: boolean;
22
+ /** Active search query (non-empty means search is active). */
23
+ searchQuery: string;
24
+ /** Execute a semantic search. */
25
+ onSearch: (query: string) => void;
26
+ /** Clear search and reload recent nodes. */
27
+ onClearSearch: () => void;
28
+ /** Select a node by ID (e.g., open detail panel). */
29
+ onSelectNode: (nodeId: string) => void;
30
+ /** Filter by workspace (empty string means all workspaces). */
31
+ onWorkspaceChange: (workspaceId: string) => void;
32
+ }
33
+
34
+ /** Sidebar content for the Knowledge tab. */
35
+ export function KnowledgeNav({
36
+ nodes,
37
+ workspaces,
38
+ loading,
39
+ searchQuery,
40
+ onSearch,
41
+ onClearSearch,
42
+ onSelectNode,
43
+ onWorkspaceChange,
44
+ }: KnowledgeNavProps): JSX.Element {
45
+ const [searchInput, setSearchInput] = useState("");
46
+
47
+ const handleSearch = useCallback((e: FormEvent) => {
48
+ e.preventDefault();
49
+ if (searchInput.trim()) {
50
+ onSearch(searchInput.trim());
51
+ }
52
+ }, [searchInput, onSearch]);
53
+
54
+ const handleClear = useCallback(() => {
55
+ setSearchInput("");
56
+ onClearSearch();
57
+ }, [onClearSearch]);
58
+
59
+ const handleNodeClick = useCallback((nodeId: string) => {
60
+ onSelectNode(nodeId);
61
+ }, [onSelectNode]);
62
+
63
+ const handleWorkspaceChange = useCallback((wsId: string) => {
64
+ setSearchInput("");
65
+ onWorkspaceChange(wsId);
66
+ }, [onWorkspaceChange]);
67
+
68
+ return (
69
+ <div className={styles.nav} data-testid="knowledge-nav">
70
+ {/* Search */}
71
+ <form className={styles.searchForm} onSubmit={handleSearch}>
72
+ <input
73
+ className={styles.searchInput}
74
+ type="text"
75
+ placeholder="Search..."
76
+ value={searchInput}
77
+ onChange={(e) => { setSearchInput(e.target.value); }}
78
+ data-testid="knowledge-search-input"
79
+ aria-label="Search knowledge nodes"
80
+ />
81
+ <button type="submit" className={styles.searchButton} disabled={loading}>
82
+ Go
83
+ </button>
84
+ </form>
85
+ {searchQuery && (
86
+ <button type="button" className={styles.clearButton} onClick={handleClear}>
87
+ Clear search
88
+ </button>
89
+ )}
90
+
91
+ {/* Workspace filter */}
92
+ <select
93
+ className={styles.workspaceSelect}
94
+ onChange={(e) => { handleWorkspaceChange(e.target.value); }}
95
+ data-testid="knowledge-workspace-filter"
96
+ aria-label="Filter by workspace"
97
+ >
98
+ <option value="">All workspaces</option>
99
+ {workspaces.map((ws) => (
100
+ <option key={ws.id} value={ws.id}>{ws.name}</option>
101
+ ))}
102
+ </select>
103
+
104
+ {/* Node list */}
105
+ <div className={styles.listHeader}>
106
+ Nodes ({nodes.length})
107
+ </div>
108
+ <ul className={styles.nodeList}>
109
+ {nodes.map((node: GraphNode) => (
110
+ <li
111
+ key={node.id}
112
+ className={styles.nodeItem}
113
+ onClick={() => { handleNodeClick(node.id); }}
114
+ onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); handleNodeClick(node.id); } }}
115
+ role="button"
116
+ tabIndex={0}
117
+ >
118
+ <span
119
+ className={styles.indicator}
120
+ style={{
121
+ backgroundColor:
122
+ node.kind === "reference" ? "#4A9EFF"
123
+ : node.category === "decision" ? "#22C55E"
124
+ : node.category === "concept" ? "#A855F7"
125
+ : node.category === "snippet" ? "#6B7280"
126
+ : "#EAB308",
127
+ }}
128
+ />
129
+ <span className={styles.label}>{node.label}</span>
130
+ <span className={styles.badge}>
131
+ {node.kind === "reference" ? node.sourceType : node.category}
132
+ </span>
133
+ </li>
134
+ ))}
135
+ </ul>
136
+ </div>
137
+ );
138
+ }
@@ -0,0 +1,3 @@
1
+ export { KnowledgeGraph } from "./KnowledgeGraph.js";
2
+ export { KnowledgeDetailPanel } from "./KnowledgeDetailPanel.js";
3
+ export { KnowledgeNav } from "./KnowledgeNav.js";
@@ -0,0 +1,82 @@
1
+ @use '../../styles/mixins' as *;
2
+
3
+ // =============================================================================
4
+ // AppNav — full-width navigation bar below the StatusBar
5
+ // =============================================================================
6
+
7
+ .nav {
8
+ @include surface-panel;
9
+ display: flex;
10
+ align-items: stretch;
11
+ gap: 0;
12
+ padding: 0 var(--space-lg);
13
+ border-bottom: 1px solid var(--border-subtle);
14
+ flex-shrink: 0;
15
+
16
+ @include mobile {
17
+ padding: 0 var(--space-xs);
18
+ overflow-x: auto;
19
+ overflow-y: hidden;
20
+ -webkit-overflow-scrolling: touch;
21
+ }
22
+ }
23
+
24
+ .tab {
25
+ display: flex;
26
+ align-items: center;
27
+ justify-content: center;
28
+ gap: var(--space-sm);
29
+ padding: var(--space-sm) var(--space-lg);
30
+ border: none;
31
+ border-bottom: 3px solid transparent;
32
+ background: transparent;
33
+ color: var(--text-secondary);
34
+ font-size: var(--font-size-base);
35
+ font-family: var(--font-ui);
36
+ cursor: pointer;
37
+ transition: background var(--transition-fast),
38
+ color var(--transition-fast),
39
+ border-color var(--transition-fast);
40
+ white-space: nowrap;
41
+
42
+ &:hover {
43
+ background: var(--bg-overlay);
44
+ color: var(--text-primary);
45
+ }
46
+
47
+ &:focus-visible {
48
+ outline: 2px solid var(--accent-green);
49
+ outline-offset: -2px;
50
+ }
51
+
52
+ @include mobile {
53
+ padding: var(--space-sm) var(--space-md);
54
+ font-size: var(--font-size-sm);
55
+ gap: var(--space-xs);
56
+ flex-shrink: 0;
57
+ }
58
+ }
59
+
60
+ .tabIcon {
61
+ font-size: var(--font-size-lg);
62
+ line-height: 1;
63
+
64
+ @include mobile {
65
+ font-size: var(--font-size-base);
66
+ }
67
+ }
68
+
69
+ .tabLabel {
70
+ @include mobile {
71
+ // Hide labels on very small screens, show only icons
72
+ @media (max-width: 480px) {
73
+ display: none;
74
+ }
75
+ }
76
+ }
77
+
78
+ .tabActive {
79
+ border-bottom-color: var(--accent-green);
80
+ color: var(--text-primary);
81
+ font-weight: var(--font-weight-medium);
82
+ }
@@ -0,0 +1,115 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { expect, userEvent } from "@storybook/test";
3
+ import { Brain, ClipboardList, Home, MessageSquare, Monitor, Search, Settings } from "lucide-react";
4
+ import { AppNav } from "./AppNav.js";
5
+ import { ICON_LG } from "../../utils/iconSize.js";
6
+ import { HOME_URL, CHAT_URL, ENVIRONMENTS_URL, SETTINGS_CREDENTIALS_URL, TASKS_URL, FINDINGS_URL, KNOWLEDGE_URL } from "../../utils/navigation.js";
7
+
8
+ const meta: Meta<typeof AppNav> = {
9
+ title: "Grackle/Layout/AppNav",
10
+ tags: ["autodocs"],
11
+ component: AppNav,
12
+ };
13
+ export default meta;
14
+ type Story = StoryObj<typeof meta>;
15
+
16
+ /** All tabs rendered (default behavior, no tabs prop). */
17
+ export const AllTabsRendered: Story = {
18
+ play: async ({ canvas }) => {
19
+ await expect(canvas.getByRole("tab", { name: /Dashboard/ })).toBeInTheDocument();
20
+ await expect(canvas.getByRole("tab", { name: /Chat/ })).toBeInTheDocument();
21
+ await expect(canvas.getByRole("tab", { name: /Tasks/ })).toBeInTheDocument();
22
+ await expect(canvas.getByRole("tab", { name: /Environments/ })).toBeInTheDocument();
23
+ await expect(canvas.getByRole("tab", { name: /Knowledge/ })).toBeInTheDocument();
24
+ await expect(canvas.getByRole("tab", { name: /Settings/ })).toBeInTheDocument();
25
+ },
26
+ };
27
+
28
+ /** Core-only tabs: orchestration (Tasks, Findings) and knowledge tabs are absent. */
29
+ export const CoreOnlyTabs: Story = {
30
+ args: {
31
+ tabs: [
32
+ { view: "dashboard", label: "Dashboard", icon: <Home size={ICON_LG} />, route: HOME_URL, testId: "sidebar-tab-dashboard" },
33
+ { view: "chat", label: "Chat", icon: <MessageSquare size={ICON_LG} />, route: CHAT_URL, testId: "sidebar-tab-chat" },
34
+ { view: "environments", label: "Environments", icon: <Monitor size={ICON_LG} />, route: ENVIRONMENTS_URL, testId: "sidebar-tab-environments" },
35
+ { view: "settings", label: "Settings", icon: <Settings size={ICON_LG} />, route: SETTINGS_CREDENTIALS_URL, testId: "sidebar-tab-settings" },
36
+ ],
37
+ },
38
+ play: async ({ canvas }) => {
39
+ await expect(canvas.getByRole("tab", { name: /Dashboard/ })).toBeInTheDocument();
40
+ await expect(canvas.getByRole("tab", { name: /Chat/ })).toBeInTheDocument();
41
+ await expect(canvas.getByRole("tab", { name: /Environments/ })).toBeInTheDocument();
42
+ await expect(canvas.getByRole("tab", { name: /Settings/ })).toBeInTheDocument();
43
+ await expect(canvas.queryByRole("tab", { name: /Tasks/ })).not.toBeInTheDocument();
44
+ await expect(canvas.queryByRole("tab", { name: /Findings/ })).not.toBeInTheDocument();
45
+ await expect(canvas.queryByRole("tab", { name: /Knowledge/ })).not.toBeInTheDocument();
46
+ },
47
+ };
48
+
49
+ /** All tabs explicitly provided via tabs prop. */
50
+ export const AllTabsExplicit: Story = {
51
+ args: {
52
+ tabs: [
53
+ { view: "dashboard", label: "Dashboard", icon: <Home size={ICON_LG} />, route: HOME_URL, testId: "sidebar-tab-dashboard" },
54
+ { view: "chat", label: "Chat", icon: <MessageSquare size={ICON_LG} />, route: CHAT_URL, testId: "sidebar-tab-chat" },
55
+ { view: "tasks", label: "Tasks", icon: <ClipboardList size={ICON_LG} />, route: TASKS_URL, testId: "sidebar-tab-tasks" },
56
+ { view: "environments", label: "Environments", icon: <Monitor size={ICON_LG} />, route: ENVIRONMENTS_URL, testId: "sidebar-tab-environments" },
57
+ { view: "knowledge", label: "Knowledge", icon: <Brain size={ICON_LG} />, route: KNOWLEDGE_URL, testId: "sidebar-tab-knowledge" },
58
+ { view: "findings", label: "Findings", icon: <Search size={ICON_LG} />, route: FINDINGS_URL, testId: "sidebar-tab-findings" },
59
+ { view: "settings", label: "Settings", icon: <Settings size={ICON_LG} />, route: SETTINGS_CREDENTIALS_URL, testId: "sidebar-tab-settings" },
60
+ ],
61
+ },
62
+ play: async ({ canvas }) => {
63
+ await expect(canvas.getByRole("tab", { name: /Tasks/ })).toBeInTheDocument();
64
+ await expect(canvas.getByRole("tab", { name: /Findings/ })).toBeInTheDocument();
65
+ await expect(canvas.getByRole("tab", { name: /Knowledge/ })).toBeInTheDocument();
66
+ },
67
+ };
68
+
69
+ /** Arrow keys navigate between tabs horizontally. */
70
+ export const KeyboardNavigation: Story = {
71
+ play: async ({ canvas }) => {
72
+ const tabs = canvas.getAllByRole("tab");
73
+ tabs[0].focus();
74
+ await expect(tabs[0]).toHaveFocus();
75
+
76
+ // ArrowRight moves to next tab
77
+ await userEvent.keyboard("{ArrowRight}");
78
+ await expect(tabs[1]).toHaveFocus();
79
+
80
+ // ArrowLeft moves back
81
+ await userEvent.keyboard("{ArrowLeft}");
82
+ await expect(tabs[0]).toHaveFocus();
83
+
84
+ // End jumps to last, Home jumps to first
85
+ await userEvent.keyboard("{End}");
86
+ await expect(tabs[tabs.length - 1]).toHaveFocus();
87
+ await userEvent.keyboard("{Home}");
88
+ await expect(tabs[0]).toHaveFocus();
89
+ },
90
+ };
91
+
92
+ /** J/K keys navigate between tabs (vim-style aliases). */
93
+ export const JKNavigation: Story = {
94
+ play: async ({ canvas }) => {
95
+ const tabs = canvas.getAllByRole("tab");
96
+ tabs[0].focus();
97
+
98
+ // J moves to next tab
99
+ await userEvent.keyboard("j");
100
+ await expect(tabs[1]).toHaveFocus();
101
+
102
+ // K moves back
103
+ await userEvent.keyboard("k");
104
+ await expect(tabs[0]).toHaveFocus();
105
+ },
106
+ };
107
+
108
+ /** Tab list has correct ARIA attributes. */
109
+ export const AriaAttributes: Story = {
110
+ play: async ({ canvas }) => {
111
+ const tablist = canvas.getByRole("tablist");
112
+ await expect(tablist).toHaveAttribute("aria-orientation", "horizontal");
113
+ await expect(tablist).toHaveAttribute("aria-label", "App navigation");
114
+ },
115
+ };
@@ -0,0 +1,133 @@
1
+ import { useCallback, useRef, type JSX, type KeyboardEvent, type ReactNode } from "react";
2
+ import { useLocation } from "react-router";
3
+ import { Brain, ClipboardList, Home, MessageSquare, Monitor, Search, Settings } from "lucide-react";
4
+ import { CHAT_URL, ENVIRONMENTS_URL, FINDINGS_URL, HOME_URL, KNOWLEDGE_URL, SETTINGS_URL, SETTINGS_CREDENTIALS_URL, TASKS_URL, useAppNavigate } from "../../utils/navigation.js";
5
+ import { ICON_LG } from "../../utils/iconSize.js";
6
+ import { Tooltip } from "../display/Tooltip.js";
7
+ import styles from "./AppNav.module.scss";
8
+
9
+ /** Application view identifiers. */
10
+ export type AppView = "dashboard" | "chat" | "tasks" | "environments" | "knowledge" | "findings" | "settings";
11
+
12
+ /** Tab definition for the application navigation bar. */
13
+ export interface AppTab {
14
+ /** View identifier. */
15
+ view: AppView;
16
+ /** Display label. */
17
+ label: string;
18
+ /** Icon element displayed before the label. */
19
+ icon: ReactNode;
20
+ /** Route to navigate to when clicked. */
21
+ route: string;
22
+ /** data-testid suffix. */
23
+ testId: string;
24
+ }
25
+
26
+ /** Ordered list of all app navigation tabs. Exported for plugin registry use. */
27
+ export const TABS: AppTab[] = [
28
+ { view: "dashboard", label: "Dashboard", icon: <Home size={ICON_LG} />, route: HOME_URL, testId: "sidebar-tab-dashboard" },
29
+ { view: "chat", label: "Chat", icon: <MessageSquare size={ICON_LG} />, route: CHAT_URL, testId: "sidebar-tab-chat" },
30
+ { view: "tasks", label: "Tasks", icon: <ClipboardList size={ICON_LG} />, route: TASKS_URL, testId: "sidebar-tab-tasks" },
31
+ { view: "environments", label: "Environments", icon: <Monitor size={ICON_LG} />, route: ENVIRONMENTS_URL, testId: "sidebar-tab-environments" },
32
+ { view: "knowledge", label: "Knowledge", icon: <Brain size={ICON_LG} />, route: KNOWLEDGE_URL, testId: "sidebar-tab-knowledge" },
33
+ { view: "findings", label: "Findings", icon: <Search size={ICON_LG} />, route: FINDINGS_URL, testId: "sidebar-tab-findings" },
34
+ { view: "settings", label: "Settings", icon: <Settings size={ICON_LG} />, route: SETTINGS_CREDENTIALS_URL, testId: "sidebar-tab-settings" },
35
+ ];
36
+
37
+ /** Derive the active application view from a URL pathname. */
38
+ export function getActiveView(pathname: string): AppView {
39
+ if (pathname === HOME_URL || pathname === "/") {
40
+ return "dashboard";
41
+ }
42
+ if (pathname.startsWith("/chat") || pathname.startsWith("/sessions")) {
43
+ return "chat";
44
+ }
45
+ if (pathname.startsWith("/workspaces") || pathname.startsWith("/environments")) {
46
+ return "environments";
47
+ }
48
+ if (pathname.startsWith(KNOWLEDGE_URL)) {
49
+ return "knowledge";
50
+ }
51
+ if (pathname.startsWith(FINDINGS_URL)) {
52
+ return "findings";
53
+ }
54
+ if (pathname.startsWith(SETTINGS_URL)) {
55
+ return "settings";
56
+ }
57
+ return "tasks";
58
+ }
59
+
60
+ /** Full-width navigation bar below the StatusBar for switching between app views. */
61
+ export function AppNav({ tabs = TABS }: { tabs?: AppTab[] }): JSX.Element {
62
+ const location = useLocation();
63
+ const navigate = useAppNavigate();
64
+ const tabListRef = useRef<HTMLElement>(null);
65
+
66
+ const activeView = getActiveView(location.pathname);
67
+
68
+ const handleClick = useCallback((tab: AppTab) => {
69
+ navigate(tab.route);
70
+ }, [navigate]);
71
+
72
+ const handleKeyDown = useCallback((e: KeyboardEvent<HTMLElement>) => {
73
+ const buttons = tabListRef.current?.querySelectorAll<HTMLButtonElement>('[role="tab"]');
74
+ if (!buttons) {
75
+ return;
76
+ }
77
+ const focusedIndex = Array.from(buttons).findIndex((b) => b === document.activeElement);
78
+ const currentIndex = focusedIndex >= 0 ? focusedIndex : tabs.findIndex((t) => t.view === activeView);
79
+ let nextIndex = currentIndex;
80
+
81
+ if (e.key === "ArrowRight" || e.key === "j" || e.key === "J") {
82
+ e.preventDefault();
83
+ nextIndex = (currentIndex + 1) % tabs.length;
84
+ } else if (e.key === "ArrowLeft" || e.key === "k" || e.key === "K") {
85
+ e.preventDefault();
86
+ nextIndex = (currentIndex - 1 + tabs.length) % tabs.length;
87
+ } else if (e.key === "Home") {
88
+ e.preventDefault();
89
+ nextIndex = 0;
90
+ } else if (e.key === "End") {
91
+ e.preventDefault();
92
+ nextIndex = tabs.length - 1;
93
+ } else {
94
+ return;
95
+ }
96
+
97
+ navigate(tabs[nextIndex].route);
98
+ buttons[nextIndex]?.focus(); // eslint-disable-line @typescript-eslint/no-unnecessary-condition -- index may be out of bounds
99
+ }, [activeView, navigate, tabs]);
100
+
101
+ return (
102
+ <nav
103
+ className={styles.nav}
104
+ ref={tabListRef}
105
+ role="tablist"
106
+ aria-orientation="horizontal"
107
+ aria-label="App navigation"
108
+ onKeyDown={handleKeyDown}
109
+ data-testid="sidebar-nav"
110
+ >
111
+ {tabs.map((tab) => {
112
+ const isActive = tab.view === activeView;
113
+ return (
114
+ <Tooltip key={tab.view} text={tab.label} placement="bottom">
115
+ <button
116
+ role="tab"
117
+ type="button"
118
+ aria-selected={isActive}
119
+ tabIndex={isActive ? 0 : -1}
120
+ className={`${styles.tab} ${isActive ? styles.tabActive : ""}`}
121
+ onClick={() => handleClick(tab)}
122
+ data-testid={tab.testId}
123
+ aria-label={tab.label}
124
+ >
125
+ <span className={styles.tabIcon} aria-hidden="true">{tab.icon}</span>
126
+ <span className={styles.tabLabel}>{tab.label}</span>
127
+ </button>
128
+ </Tooltip>
129
+ );
130
+ })}
131
+ </nav>
132
+ );
133
+ }
@@ -0,0 +1,58 @@
1
+ @use '../../styles/mixins' as *;
2
+
3
+ // =============================================================================
4
+ // BottomStatusBar — read-only status bar with contextual hints
5
+ // =============================================================================
6
+
7
+ .bar {
8
+ @include surface-panel;
9
+ display: flex;
10
+ align-items: center;
11
+ gap: var(--space-sm);
12
+ padding: var(--space-sm) var(--space-md);
13
+ border-top: 1px solid var(--border-subtle);
14
+
15
+ @include mobile {
16
+ flex-wrap: wrap;
17
+ padding: var(--space-xs) var(--space-sm);
18
+ }
19
+ }
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Buttons
23
+ // ---------------------------------------------------------------------------
24
+
25
+ .btnPrimary {
26
+ @include btn-primary;
27
+ }
28
+
29
+ .btnGhost {
30
+ @include btn-ghost;
31
+ }
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Status indicators
35
+ // ---------------------------------------------------------------------------
36
+
37
+ .statusText {
38
+ font-size: var(--font-size-md);
39
+ flex: 1;
40
+ }
41
+
42
+ .statusCompleted {
43
+ color: var(--accent-green);
44
+ }
45
+
46
+ .statusFailed {
47
+ color: var(--accent-red);
48
+ }
49
+
50
+ .statusBlocked {
51
+ color: var(--accent-yellow);
52
+ font-size: var(--font-size-sm);
53
+ }
54
+
55
+ .hintText {
56
+ color: var(--text-tertiary);
57
+ font-size: var(--font-size-md);
58
+ }
@@ -0,0 +1,35 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { expect } from "@storybook/test";
3
+ import { MemoryRouter } from "react-router";
4
+ import { BottomStatusBar } from "./BottomStatusBar.js";
5
+
6
+ const meta: Meta<typeof BottomStatusBar> = {
7
+ component: BottomStatusBar,
8
+ title: "Grackle/Layout/BottomStatusBar",
9
+ tags: ["autodocs"],
10
+ parameters: { skipRouter: true },
11
+ };
12
+ export default meta;
13
+ type Story = StoryObj<typeof meta>;
14
+
15
+ /** Empty state on the root route returns an empty fragment. */
16
+ export const EmptyState: Story = {
17
+ args: {
18
+ sessions: [],
19
+ tasks: [],
20
+ environments: [],
21
+ },
22
+ decorators: [
23
+ (Story) => (
24
+ <MemoryRouter initialEntries={["/"]}>
25
+ <Story />
26
+ </MemoryRouter>
27
+ ),
28
+ ],
29
+ play: async ({ canvas }) => {
30
+ // On the root "/" route with no data, the bar renders an empty fragment.
31
+ // The container should exist but the bar itself should not render any visible hint.
32
+ const bar = canvas.queryByText("Loading...");
33
+ await expect(bar).not.toBeInTheDocument();
34
+ },
35
+ };