@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,173 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+
3
+ /** Options for the useEditableField hook. */
4
+ export interface UseEditableFieldOptions {
5
+ /** Current persisted value. */
6
+ value: string;
7
+ /** Called when the user saves a new value. */
8
+ onSave: (value: string) => void;
9
+ /** Optional validation — return an error string, or undefined if valid. */
10
+ validate?: (value: string) => string | undefined;
11
+ /** Unique identifier for this field (used for coordination). */
12
+ fieldId: string;
13
+ /** Which field is currently being edited (coordination from parent). */
14
+ activeFieldId?: string | null; // eslint-disable-line @rushstack/no-new-null
15
+ /** Callback to tell the parent which field is active. */
16
+ onActivate?: (fieldId: string | null) => void; // eslint-disable-line @rushstack/no-new-null
17
+ /** Whether Enter key triggers save (true for text inputs, false for textarea). */
18
+ enterToSave?: boolean;
19
+ /** Whether to trim whitespace before saving. Default true. */
20
+ trimOnSave?: boolean;
21
+ }
22
+
23
+ /** Return type for the useEditableField hook. */
24
+ export interface UseEditableFieldReturn {
25
+ /** Whether this field is currently in edit mode. */
26
+ isEditing: boolean;
27
+ /** The current draft value while editing. */
28
+ draft: string;
29
+ /** Validation error message, or empty string. */
30
+ error: string;
31
+ /** Whether the draft differs from the persisted value. */
32
+ isDirty: boolean;
33
+ /** Enter edit mode with the current value as the draft. */
34
+ startEdit: () => void;
35
+ /** Exit edit mode without saving. */
36
+ cancelEdit: () => void;
37
+ /** Validate and save the current draft. */
38
+ save: () => void;
39
+ /** Update the draft value. Also clears any validation error. */
40
+ setDraft: (value: string) => void;
41
+ /** Clear the validation error. */
42
+ clearError: () => void;
43
+ /** Blur handler that auto-saves, respecting ignoreInitialBlur and data-edit-action. */
44
+ handleBlur: (event: React.FocusEvent) => void;
45
+ /** Keyboard handler for Escape (cancel) and optionally Enter (save). */
46
+ handleKeyDown: (event: React.KeyboardEvent) => void;
47
+ /**
48
+ * Ref that prevents the initial blur (caused by clicking the edit button)
49
+ * from triggering a save. Set to true when startEdit is called, reset on
50
+ * first blur.
51
+ */
52
+ ignoreInitialBlurRef: React.RefObject<boolean>;
53
+ }
54
+
55
+ /**
56
+ * Shared hook that encapsulates the click-to-edit state machine used by
57
+ * EditableTextField, EditableTextArea, and EditableSelect.
58
+ */
59
+ export function useEditableField(options: UseEditableFieldOptions): UseEditableFieldReturn {
60
+ const {
61
+ value,
62
+ onSave,
63
+ validate,
64
+ fieldId,
65
+ activeFieldId,
66
+ onActivate,
67
+ enterToSave = true,
68
+ trimOnSave = true,
69
+ } = options;
70
+
71
+ const [draft, setDraftRaw] = useState("");
72
+ const [error, setError] = useState("");
73
+ const ignoreInitialBlurRef = useRef<boolean>(false);
74
+
75
+ const isEditing = activeFieldId === fieldId;
76
+
77
+ const setDraft = useCallback((v: string) => {
78
+ setDraftRaw(v);
79
+ setError("");
80
+ }, []);
81
+
82
+ const clearError = useCallback(() => {
83
+ setError("");
84
+ }, []);
85
+
86
+ const cancelEdit = useCallback(() => {
87
+ ignoreInitialBlurRef.current = false;
88
+ onActivate?.(null);
89
+ setDraftRaw("");
90
+ setError("");
91
+ }, [onActivate]);
92
+
93
+ const save = useCallback(() => {
94
+ const saveValue = trimOnSave ? draft.trim() : draft;
95
+
96
+ if (validate) {
97
+ const validationError = validate(draft);
98
+ if (validationError) {
99
+ setError(validationError);
100
+ return;
101
+ }
102
+ }
103
+
104
+ // No-op when value hasn't changed
105
+ const compareValue = trimOnSave ? value.trim() : value;
106
+ if (saveValue === compareValue) {
107
+ cancelEdit();
108
+ return;
109
+ }
110
+
111
+ onSave(saveValue);
112
+ cancelEdit();
113
+ }, [draft, value, trimOnSave, validate, onSave, cancelEdit]);
114
+
115
+ const startEdit = useCallback(() => {
116
+ ignoreInitialBlurRef.current = true;
117
+ onActivate?.(fieldId);
118
+ setDraftRaw(value);
119
+ setError("");
120
+ }, [fieldId, value, onActivate]);
121
+
122
+ const handleBlur = useCallback((event: React.FocusEvent) => {
123
+ if (ignoreInitialBlurRef.current) {
124
+ ignoreInitialBlurRef.current = false;
125
+ return;
126
+ }
127
+ if (
128
+ event.relatedTarget instanceof HTMLElement &&
129
+ event.relatedTarget.dataset.editAction === fieldId
130
+ ) {
131
+ return;
132
+ }
133
+ save();
134
+ }, [fieldId, save]);
135
+
136
+ const handleKeyDown = useCallback((event: React.KeyboardEvent) => {
137
+ if (event.key === "Escape") {
138
+ cancelEdit();
139
+ } else if (event.key === "Enter" && enterToSave) {
140
+ save();
141
+ }
142
+ }, [cancelEdit, enterToSave, save]);
143
+
144
+ const isDirty = (() => {
145
+ if (!isEditing) return false;
146
+ const compareValue = trimOnSave ? value.trim() : value;
147
+ const draftValue = trimOnSave ? draft.trim() : draft;
148
+ return draftValue !== compareValue;
149
+ })();
150
+
151
+ // If another field becomes active, reset our local state
152
+ useEffect(() => {
153
+ if (!isEditing && (draft !== "" || error !== "")) {
154
+ setDraftRaw("");
155
+ setError("");
156
+ }
157
+ }, [isEditing, draft, error]);
158
+
159
+ return {
160
+ isEditing,
161
+ draft,
162
+ error,
163
+ isDirty,
164
+ startEdit,
165
+ cancelEdit,
166
+ save,
167
+ setDraft,
168
+ clearError,
169
+ handleBlur,
170
+ handleKeyDown,
171
+ ignoreInitialBlurRef,
172
+ };
173
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Component exports organized by category.
3
+ * @module components
4
+ */
5
+
6
+ // Layout components - application shell structure
7
+ export { StatusBar, Sidebar, BottomStatusBar } from "./layout/index.js";
8
+
9
+ // Panel components - main content areas
10
+ export { FindingsPanel } from "./panels/index.js";
11
+
12
+ // List components - sidebar navigation
13
+ export { EnvironmentNav } from "./lists/index.js";
14
+
15
+ // Display components - content rendering
16
+ export { EventRenderer } from "./display/index.js";
17
+
18
+ // Notification components - toasts and callouts
19
+ export { Toast, ToastContainer, Callout } from "./notifications/index.js";
20
+ export type { CalloutVariant } from "./notifications/index.js";
@@ -0,0 +1,162 @@
1
+ .panel {
2
+ position: absolute;
3
+ top: 0;
4
+ right: 0;
5
+ bottom: 0;
6
+ width: 350px;
7
+ background: var(--bg-surface, #1a1a2e);
8
+ border-left: 1px solid var(--border-default, #333);
9
+ display: flex;
10
+ flex-direction: column;
11
+ overflow-y: auto;
12
+ z-index: 10;
13
+ animation: slideIn 0.2s ease-out;
14
+ }
15
+
16
+ @keyframes slideIn {
17
+ from {
18
+ transform: translateX(100%);
19
+ }
20
+ to {
21
+ transform: translateX(0);
22
+ }
23
+ }
24
+
25
+ .header {
26
+ display: flex;
27
+ align-items: center;
28
+ justify-content: space-between;
29
+ padding: 16px;
30
+ border-bottom: 1px solid var(--border-default, #333);
31
+ }
32
+
33
+ .title {
34
+ margin: 0;
35
+ font-size: 16px;
36
+ font-weight: 600;
37
+ overflow: hidden;
38
+ text-overflow: ellipsis;
39
+ white-space: nowrap;
40
+ flex: 1;
41
+ }
42
+
43
+ .closeButton {
44
+ background: none;
45
+ border: none;
46
+ color: var(--text-secondary, #999);
47
+ font-size: 24px;
48
+ cursor: pointer;
49
+ padding: 0 4px;
50
+ line-height: 1;
51
+
52
+ &:hover {
53
+ color: var(--text-primary, #fff);
54
+ }
55
+ }
56
+
57
+ .body {
58
+ padding: 16px;
59
+ flex: 1;
60
+ overflow-y: auto;
61
+ }
62
+
63
+ .badge {
64
+ display: inline-block;
65
+ padding: 2px 8px;
66
+ border-radius: 4px;
67
+ font-size: 12px;
68
+ font-weight: 500;
69
+ background: var(--bg-inset, #222);
70
+ color: var(--text-secondary, #999);
71
+ margin-bottom: 12px;
72
+ }
73
+
74
+ .section {
75
+ margin-bottom: 16px;
76
+ }
77
+
78
+ .sectionLabel {
79
+ font-size: 11px;
80
+ font-weight: 600;
81
+ text-transform: uppercase;
82
+ color: var(--text-disabled, #666);
83
+ margin-bottom: 6px;
84
+ }
85
+
86
+ .content {
87
+ font-size: 14px;
88
+ line-height: 1.5;
89
+ color: var(--text-primary, #eee);
90
+ margin: 0;
91
+ white-space: pre-wrap;
92
+ }
93
+
94
+ .tags {
95
+ display: flex;
96
+ flex-wrap: wrap;
97
+ gap: 4px;
98
+ }
99
+
100
+ .tag {
101
+ display: inline-block;
102
+ padding: 2px 8px;
103
+ border-radius: 12px;
104
+ font-size: 12px;
105
+ background: var(--bg-inset, #222);
106
+ color: var(--text-secondary, #aaa);
107
+ }
108
+
109
+ .viewLink {
110
+ background: none;
111
+ border: 1px solid var(--accent-blue, #4A9EFF);
112
+ color: var(--accent-blue, #4A9EFF);
113
+ padding: 6px 12px;
114
+ border-radius: 6px;
115
+ cursor: pointer;
116
+ font-size: 13px;
117
+
118
+ &:hover {
119
+ background: var(--accent-blue, #4A9EFF);
120
+ color: #fff;
121
+ }
122
+ }
123
+
124
+ .edgeList {
125
+ list-style: none;
126
+ padding: 0;
127
+ margin: 0;
128
+ }
129
+
130
+ .edgeItem {
131
+ display: flex;
132
+ align-items: center;
133
+ gap: 8px;
134
+ padding: 4px 0;
135
+ border-bottom: 1px solid var(--border-default, #2a2a2a);
136
+ font-size: 13px;
137
+ }
138
+
139
+ .edgeType {
140
+ font-family: monospace;
141
+ font-size: 11px;
142
+ color: var(--text-disabled, #666);
143
+ }
144
+
145
+ .edgeNodeLink {
146
+ background: none;
147
+ border: none;
148
+ color: var(--accent-blue, #4A9EFF);
149
+ cursor: pointer;
150
+ font-size: 12px;
151
+ font-family: monospace;
152
+
153
+ &:hover {
154
+ text-decoration: underline;
155
+ }
156
+ }
157
+
158
+ .timestamps {
159
+ margin-top: 16px;
160
+ font-size: 12px;
161
+ color: var(--text-disabled, #666);
162
+ }
@@ -0,0 +1,208 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { expect, fn, userEvent } from "@storybook/test";
3
+ import type { GraphNode, NodeDetail } from "../../hooks/types.js";
4
+ import { makeGraphNode } from "../../test-utils/storybook-helpers.js";
5
+ import { KnowledgeDetailPanel } from "./KnowledgeDetailPanel.js";
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Mock data
9
+ // ---------------------------------------------------------------------------
10
+
11
+ const defaultNode: GraphNode = makeGraphNode({
12
+ id: "node-001",
13
+ label: "Authentication Flow",
14
+ content: "OAuth2 flow with PKCE for CLI clients.",
15
+ tags: ["auth", "security", "autodocs"],
16
+ val: 3,
17
+ });
18
+
19
+ const connectedNodeA: GraphNode = makeGraphNode({
20
+ id: "node-002",
21
+ label: "Session Token Storage",
22
+ category: "decision",
23
+ val: 1,
24
+ });
25
+
26
+ const connectedNodeB: GraphNode = makeGraphNode({
27
+ id: "node-003",
28
+ label: "Token Rotation Policy",
29
+ category: "insight",
30
+ val: 2,
31
+ });
32
+
33
+ const unknownNodeId: string = "62d111f7-aaaa-bbbb-cccc-123456789abc";
34
+
35
+ const defaultDetail: NodeDetail = {
36
+ node: defaultNode,
37
+ edges: [
38
+ { fromId: "node-001", toId: "node-002", type: "relates_to" },
39
+ { fromId: "node-003", toId: "node-001", type: "derived_from" },
40
+ ],
41
+ };
42
+
43
+ const allNodes: GraphNode[] = [defaultNode, connectedNodeA, connectedNodeB];
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Story meta
47
+ // ---------------------------------------------------------------------------
48
+
49
+ const meta: Meta<typeof KnowledgeDetailPanel> = {
50
+ title: "Grackle/Knowledge/KnowledgeDetailPanel",
51
+ component: KnowledgeDetailPanel,
52
+ args: {
53
+ detail: defaultDetail,
54
+ nodes: allNodes,
55
+ onClose: fn(),
56
+ onSelectNode: fn(),
57
+ },
58
+ };
59
+
60
+ export default meta;
61
+ type Story = StoryObj<typeof KnowledgeDetailPanel>;
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Stories
65
+ // ---------------------------------------------------------------------------
66
+
67
+ /** Panel renders the node label, badge, content, tags, and timestamps. */
68
+ export const BasicRendering: Story = {
69
+ play: async ({ canvas }) => {
70
+ // Title
71
+ await expect(canvas.getByText("Authentication Flow")).toBeInTheDocument();
72
+
73
+ // Category badge
74
+ await expect(canvas.getByText("concept")).toBeInTheDocument();
75
+
76
+ // Content
77
+ await expect(canvas.getByText("OAuth2 flow with PKCE for CLI clients.")).toBeInTheDocument();
78
+
79
+ // Tags
80
+ await expect(canvas.getByText("auth")).toBeInTheDocument();
81
+ await expect(canvas.getByText("security")).toBeInTheDocument();
82
+
83
+ // Timestamps
84
+ await expect(canvas.getByText(/Created:/)).toBeInTheDocument();
85
+ await expect(canvas.getByText(/Updated:/)).toBeInTheDocument();
86
+ },
87
+ };
88
+
89
+ /** Edge links display connected node titles, not truncated UUIDs. */
90
+ export const EdgeLinksShowNodeTitles: Story = {
91
+ play: async ({ canvas }) => {
92
+ // Should show resolved titles, not truncated UUIDs
93
+ await expect(canvas.getByText("Session Token Storage")).toBeInTheDocument();
94
+ await expect(canvas.getByText("Token Rotation Policy")).toBeInTheDocument();
95
+
96
+ // Edge types should be displayed
97
+ const edgeTypes = canvas.getAllByTestId("edge-type");
98
+ await expect(edgeTypes.length).toBe(2);
99
+ await expect(edgeTypes[0]).toHaveTextContent("relates_to");
100
+ await expect(edgeTypes[1]).toHaveTextContent("derived_from");
101
+ },
102
+ };
103
+
104
+ /** Edge links fall back to truncated UUID when node is not in the graph. */
105
+ export const EdgeLinksFallbackToTruncatedId: Story = {
106
+ args: {
107
+ detail: {
108
+ node: defaultNode,
109
+ edges: [
110
+ { fromId: "node-001", toId: unknownNodeId, type: "mentions" },
111
+ ],
112
+ },
113
+ nodes: [defaultNode],
114
+ },
115
+ play: async ({ canvas }) => {
116
+ // Should show truncated UUID since the connected node is not in the nodes list
117
+ await expect(canvas.getByText("62d111f7...")).toBeInTheDocument();
118
+ },
119
+ };
120
+
121
+ /** Clicking an edge link calls onSelectNode with the connected node ID. */
122
+ export const EdgeLinkClickCallsOnSelectNode: Story = {
123
+ play: async ({ canvas, args }) => {
124
+ const edgeLinks = canvas.getAllByTestId("edge-node-link");
125
+ await userEvent.click(edgeLinks[0]);
126
+ await expect(args.onSelectNode).toHaveBeenCalledWith("node-002");
127
+ },
128
+ };
129
+
130
+ /** Close button calls onClose. */
131
+ export const CloseButtonCallsOnClose: Story = {
132
+ play: async ({ canvas, args }) => {
133
+ const closeButton = canvas.getByRole("button", { name: "Close" });
134
+ await userEvent.click(closeButton);
135
+ await expect(args.onClose).toHaveBeenCalled();
136
+ },
137
+ };
138
+
139
+ /** No edges section when the node has no edges. */
140
+ export const NoEdges: Story = {
141
+ args: {
142
+ detail: {
143
+ node: defaultNode,
144
+ edges: [],
145
+ },
146
+ },
147
+ play: async ({ canvas }) => {
148
+ // Title should still render
149
+ await expect(canvas.getByText("Authentication Flow")).toBeInTheDocument();
150
+
151
+ // No "Edges" section label
152
+ const panel = canvas.getByTestId("knowledge-detail-panel");
153
+ await expect(panel.textContent).not.toContain("Edges (");
154
+ },
155
+ };
156
+
157
+ /** Reference nodes show the "View in Grackle" link and a reference badge. */
158
+ export const ReferenceNode: Story = {
159
+ args: {
160
+ detail: {
161
+ node: makeGraphNode({
162
+ id: "ref-001",
163
+ kind: "reference",
164
+ sourceType: "task",
165
+ sourceId: "task-123",
166
+ label: "Fix login bug",
167
+ content: undefined,
168
+ tags: [],
169
+ }),
170
+ edges: [],
171
+ },
172
+ nodes: [],
173
+ },
174
+ play: async ({ canvas }) => {
175
+ // Reference badge
176
+ await expect(canvas.getByText("Reference (task)")).toBeInTheDocument();
177
+
178
+ // View in Grackle link
179
+ const viewLink = canvas.getByRole("button", { name: /View in Grackle/ });
180
+ await expect(viewLink).toBeInTheDocument();
181
+ },
182
+ };
183
+
184
+ /** Node without content or tags omits those sections. */
185
+ export const MinimalNode: Story = {
186
+ args: {
187
+ detail: {
188
+ node: makeGraphNode({
189
+ label: "Minimal Node",
190
+ content: undefined,
191
+ tags: undefined,
192
+ createdAt: undefined,
193
+ updatedAt: undefined,
194
+ }),
195
+ edges: [],
196
+ },
197
+ nodes: [],
198
+ },
199
+ play: async ({ canvas }) => {
200
+ // Title renders
201
+ await expect(canvas.getByText("Minimal Node")).toBeInTheDocument();
202
+
203
+ // No Content or Tags sections
204
+ const panel = canvas.getByTestId("knowledge-detail-panel");
205
+ await expect(panel.textContent).not.toContain("Content");
206
+ await expect(panel.textContent).not.toContain("Tags");
207
+ },
208
+ };
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Slide-in detail panel for a selected knowledge graph node.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ import { useMemo, type JSX } from "react";
8
+ import type { GraphNode, NodeDetail } from "../../hooks/types.js";
9
+ import { taskUrl, sessionUrl, findingUrl } from "../../utils/navigation.js";
10
+ import { useAppNavigate } from "../../utils/navigation.js";
11
+ import styles from "./KnowledgeDetailPanel.module.scss";
12
+
13
+ interface KnowledgeDetailPanelProps {
14
+ detail: NodeDetail;
15
+ nodes: GraphNode[];
16
+ onClose: () => void;
17
+ onSelectNode: (id: string) => void;
18
+ }
19
+
20
+ /** Slide-in panel showing full details for a selected knowledge node. */
21
+ export function KnowledgeDetailPanel({
22
+ detail,
23
+ nodes,
24
+ onClose,
25
+ onSelectNode,
26
+ }: KnowledgeDetailPanelProps): JSX.Element {
27
+ const navigate = useAppNavigate();
28
+ const { node, edges } = detail;
29
+ const nodeById = useMemo(() => new Map(nodes.map((n) => [n.id, n])), [nodes]);
30
+
31
+ /** Navigate to the source entity for reference nodes. */
32
+ function handleViewInGrackle(): void {
33
+ if (node.kind !== "reference" || !node.sourceId) {
34
+ return;
35
+ }
36
+ switch (node.sourceType) {
37
+ case "task":
38
+ navigate(taskUrl(node.sourceId));
39
+ break;
40
+ case "session":
41
+ navigate(sessionUrl(node.sourceId));
42
+ break;
43
+ case "finding":
44
+ navigate(findingUrl(node.sourceId));
45
+ break;
46
+ default:
47
+ break;
48
+ }
49
+ }
50
+
51
+ return (
52
+ <div className={styles.panel} data-testid="knowledge-detail-panel">
53
+ <div className={styles.header}>
54
+ <h3 className={styles.title}>{node.label}</h3>
55
+ <button className={styles.closeButton} onClick={onClose} aria-label="Close">
56
+ &times;
57
+ </button>
58
+ </div>
59
+
60
+ <div className={styles.body}>
61
+ <div className={styles.badge}>
62
+ {node.kind === "reference" ? `Reference (${node.sourceType})` : node.category}
63
+ </div>
64
+
65
+ {node.content && (
66
+ <div className={styles.section}>
67
+ <div className={styles.sectionLabel}>Content</div>
68
+ <p className={styles.content}>{node.content}</p>
69
+ </div>
70
+ )}
71
+
72
+ {node.tags && node.tags.length > 0 && (
73
+ <div className={styles.section}>
74
+ <div className={styles.sectionLabel}>Tags</div>
75
+ <div className={styles.tags}>
76
+ {node.tags.map((tag) => (
77
+ <span key={tag} className={styles.tag}>{tag}</span>
78
+ ))}
79
+ </div>
80
+ </div>
81
+ )}
82
+
83
+ {node.kind === "reference" && node.sourceId && (
84
+ <div className={styles.section}>
85
+ <button className={styles.viewLink} onClick={handleViewInGrackle}>
86
+ View in Grackle &rarr;
87
+ </button>
88
+ </div>
89
+ )}
90
+
91
+ {edges.length > 0 && (
92
+ <div className={styles.section}>
93
+ <div className={styles.sectionLabel}>Edges ({edges.length})</div>
94
+ <ul className={styles.edgeList}>
95
+ {edges.map((edge) => {
96
+ const otherId: string = edge.fromId === node.id ? edge.toId : edge.fromId;
97
+ const edgeKey: string = `${edge.fromId}:${edge.toId}:${edge.type}`;
98
+ return (
99
+ <li key={edgeKey} className={styles.edgeItem} data-testid="edge-item">
100
+ <span className={styles.edgeType} data-testid="edge-type">{edge.type}</span>
101
+ <button
102
+ className={styles.edgeNodeLink}
103
+ data-testid="edge-node-link"
104
+ onClick={() => { onSelectNode(otherId); }}
105
+ >
106
+ {nodeById.get(otherId)?.label ?? `${otherId.substring(0, 8)}...`}
107
+ </button>
108
+ </li>
109
+ );
110
+ })}
111
+ </ul>
112
+ </div>
113
+ )}
114
+
115
+ <div className={styles.timestamps}>
116
+ {node.createdAt && <div>Created: {new Date(node.createdAt).toLocaleDateString()}</div>}
117
+ {node.updatedAt && <div>Updated: {new Date(node.updatedAt).toLocaleDateString()}</div>}
118
+ </div>
119
+ </div>
120
+ </div>
121
+ );
122
+ }