@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,214 @@
1
+ /**
2
+ * Session picker dialog for forwarding messages.
3
+ *
4
+ * Displays a filterable list of active sessions (excluding the current one)
5
+ * so the user can choose a target for a forwarded message.
6
+ * Pure presentational component -- no useGrackle().
7
+ */
8
+
9
+ import { useEffect, useRef, useState, type JSX } from "react";
10
+ import { X, Search } from "lucide-react";
11
+ import { AnimatePresence, motion } from "motion/react";
12
+ import type { Session, Environment, PersonaData } from "../../hooks/types.js";
13
+ import { ICON_SM, ICON_MD } from "../../utils/iconSize.js";
14
+ import styles from "./SessionPicker.module.scss";
15
+
16
+ /** A session entry enriched with its environment display name. */
17
+ export interface SessionPickerEntry {
18
+ /** The session. */
19
+ session: Session;
20
+ /** Display name of the session's environment. */
21
+ environmentName: string;
22
+ }
23
+
24
+ /** Props for the SessionPicker component. */
25
+ export interface SessionPickerProps {
26
+ /** Whether the picker is visible. */
27
+ isOpen: boolean;
28
+ /** Active sessions to display (already filtered: active status, not current session). */
29
+ sessions: Session[];
30
+ /** Environments for name lookup. */
31
+ environments: Environment[];
32
+ /** Personas for name lookup (optional — persona name is shown when available). */
33
+ personas?: PersonaData[];
34
+ /** Called when the user selects a target session. */
35
+ onSelect: (sessionId: string) => void;
36
+ /** Called when the user dismisses the picker without selecting. */
37
+ onCancel: () => void;
38
+ }
39
+
40
+ /** Returns a short status badge label for a session. */
41
+ function statusLabel(status: string): string {
42
+ if (status === "running") {
43
+ return "running";
44
+ }
45
+ if (status === "idle") {
46
+ return "idle";
47
+ }
48
+ return status;
49
+ }
50
+
51
+ /**
52
+ * Modal dialog listing active sessions for the forward-message feature.
53
+ *
54
+ * The list is filterable by environment name or prompt snippet. If there are
55
+ * no active sessions the picker is not rendered (parent should keep it closed).
56
+ */
57
+ export function SessionPicker({
58
+ isOpen,
59
+ sessions,
60
+ environments,
61
+ personas,
62
+ onSelect,
63
+ onCancel,
64
+ }: SessionPickerProps): JSX.Element {
65
+ const [filter, setFilter] = useState("");
66
+ const dialogRef = useRef<HTMLDivElement>(null);
67
+ const closeButtonRef = useRef<HTMLButtonElement>(null);
68
+
69
+ // Focus the dialog when it opens so Escape is reliably captured
70
+ useEffect(() => {
71
+ if (isOpen) {
72
+ // Filter input has autoFocus when shown; otherwise fall back to close button
73
+ if (sessions.length <= 4) {
74
+ closeButtonRef.current?.focus();
75
+ }
76
+ }
77
+ }, [isOpen, sessions.length]);
78
+
79
+ // Build lookup for environment names
80
+ const envNameById = new Map<string, string>(
81
+ environments.map((e) => [e.id, e.displayName]),
82
+ );
83
+
84
+ // Build lookup for persona names
85
+ const personaNameById = new Map<string, string>(
86
+ (personas ?? []).map((p) => [p.id, p.name]),
87
+ );
88
+
89
+ const entries: SessionPickerEntry[] = sessions.map((s) => ({
90
+ session: s,
91
+ environmentName: envNameById.get(s.environmentId) ?? s.environmentId,
92
+ }));
93
+
94
+ const showFilter = sessions.length > 4;
95
+
96
+ const filtered = filter.trim()
97
+ ? entries.filter(
98
+ ({ session, environmentName }) =>
99
+ environmentName.toLowerCase().includes(filter.toLowerCase()) ||
100
+ session.prompt.toLowerCase().includes(filter.toLowerCase()),
101
+ )
102
+ : entries;
103
+
104
+ return (
105
+ <AnimatePresence>
106
+ {isOpen && (
107
+ <motion.div
108
+ className={styles.overlay}
109
+ initial={{ opacity: 0 }}
110
+ animate={{ opacity: 1 }}
111
+ exit={{ opacity: 0 }}
112
+ transition={{ duration: 0.15 }}
113
+ onClick={onCancel}
114
+ onKeyDown={(e) => { if (e.key === "Escape") { onCancel(); } }}
115
+ role="dialog"
116
+ aria-modal="true"
117
+ aria-label="Forward to session"
118
+ data-testid="session-picker-overlay"
119
+ >
120
+ <motion.div
121
+ ref={dialogRef}
122
+ className={styles.dialog}
123
+ initial={{ opacity: 0, scale: 0.93, y: -10 }}
124
+ animate={{ opacity: 1, scale: 1, y: 0 }}
125
+ exit={{ opacity: 0, scale: 0.93, y: -10 }}
126
+ transition={{ duration: 0.15, ease: [0.16, 1, 0.3, 1] }}
127
+ onClick={(e) => { e.stopPropagation(); }}
128
+ data-testid="session-picker-dialog"
129
+ >
130
+ <div className={styles.header}>
131
+ <h3 className={styles.title}>Forward to session</h3>
132
+ <button
133
+ ref={closeButtonRef}
134
+ type="button"
135
+ className={styles.closeButton}
136
+ onClick={onCancel}
137
+ aria-label="Close session picker"
138
+ data-testid="session-picker-close"
139
+ >
140
+ <X size={ICON_SM} aria-hidden="true" />
141
+ </button>
142
+ </div>
143
+
144
+ {showFilter && (
145
+ <div className={styles.filterRow}>
146
+ <Search size={ICON_SM} className={styles.searchIcon} aria-hidden="true" />
147
+ <input
148
+ type="text"
149
+ className={styles.filterInput}
150
+ placeholder="Filter sessions..."
151
+ value={filter}
152
+ onChange={(e) => { setFilter(e.target.value); }}
153
+ data-testid="session-picker-filter"
154
+ autoFocus
155
+ />
156
+ </div>
157
+ )}
158
+
159
+ {sessions.length === 0 ? (
160
+ <div className={styles.noSessions} data-testid="session-picker-no-sessions">
161
+ <Search size={ICON_MD} aria-hidden="true" />
162
+ <p>No active sessions to forward to.</p>
163
+ </div>
164
+ ) : (
165
+ <ul className={styles.list} data-testid="session-picker-list">
166
+ {filtered.length === 0 ? (
167
+ <li className={styles.emptyItem} data-testid="session-picker-empty">
168
+ No matching sessions
169
+ </li>
170
+ ) : (
171
+ filtered.map(({ session, environmentName }) => {
172
+ const personaName = session.personaId
173
+ ? (personaNameById.get(session.personaId) ?? undefined)
174
+ : undefined;
175
+ return (
176
+ <li key={session.id}>
177
+ <button
178
+ type="button"
179
+ className={styles.sessionRow}
180
+ onClick={() => { onSelect(session.id); }}
181
+ data-testid={`session-picker-item-${session.id}`}
182
+ >
183
+ <div className={styles.sessionMain}>
184
+ <span className={styles.envName}>{environmentName}</span>
185
+ {personaName !== undefined && (
186
+ <span className={styles.personaName} data-testid={`session-picker-persona-${session.id}`}>
187
+ {personaName}
188
+ </span>
189
+ )}
190
+ <span
191
+ className={`${styles.statusBadge} ${styles[`status_${session.status}`] ?? styles.status_other}`}
192
+ data-testid={`session-picker-status-${session.id}`}
193
+ >
194
+ {statusLabel(session.status)}
195
+ </span>
196
+ </div>
197
+ <div className={styles.sessionPrompt}>
198
+ {session.prompt.length > 80
199
+ ? `${session.prompt.slice(0, 80)}...`
200
+ : session.prompt}
201
+ </div>
202
+ </button>
203
+ </li>
204
+ );
205
+ })
206
+ )}
207
+ </ul>
208
+ )}
209
+ </motion.div>
210
+ </motion.div>
211
+ )}
212
+ </AnimatePresence>
213
+ );
214
+ }
@@ -0,0 +1,58 @@
1
+ // =============================================================================
2
+ // Skeleton — shimmer loading placeholders
3
+ // =============================================================================
4
+
5
+ @use '../../styles/mixins' as *;
6
+
7
+ // ─── Base shimmer block ──────────────────────────────────────────────────────
8
+
9
+ @keyframes shimmer {
10
+ 0% {
11
+ background-position: -200% 0;
12
+ }
13
+
14
+ 100% {
15
+ background-position: 200% 0;
16
+ }
17
+ }
18
+
19
+ .skeleton {
20
+ display: block;
21
+ background: linear-gradient(
22
+ 90deg,
23
+ var(--bg-inset) 25%,
24
+ var(--bg-surface) 50%,
25
+ var(--bg-inset) 75%
26
+ );
27
+ background-size: 200% 100%;
28
+ animation: shimmer 1.5s ease-in-out infinite;
29
+ border-radius: var(--radius-sm);
30
+ }
31
+
32
+ .circular {
33
+ border-radius: 50%;
34
+ }
35
+
36
+ @media (prefers-reduced-motion: reduce) {
37
+ .skeleton {
38
+ animation: none;
39
+ }
40
+ }
41
+
42
+ // ─── SkeletonText ────────────────────────────────────────────────────────────
43
+
44
+ .textContainer {
45
+ display: flex;
46
+ flex-direction: column;
47
+ }
48
+
49
+ // ─── SkeletonCard ────────────────────────────────────────────────────────────
50
+
51
+ .card {
52
+ @include surface-card;
53
+
54
+ display: flex;
55
+ flex-direction: column;
56
+ gap: var(--space-md);
57
+ padding: var(--space-lg);
58
+ }
@@ -0,0 +1,94 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { expect } from "@storybook/test";
3
+ import { Skeleton, SkeletonText, SkeletonCard } from "./Skeleton.js";
4
+
5
+ // ─── Skeleton (base) ─────────────────────────────────────────────────────────
6
+
7
+ const skeletonMeta: Meta<typeof Skeleton> = {
8
+ component: Skeleton,
9
+ title: "Primitives/Display/Skeleton",
10
+ tags: ["autodocs"],
11
+ };
12
+ export default skeletonMeta;
13
+ type Story = StoryObj<typeof skeletonMeta>;
14
+
15
+ /** Default full-width skeleton block. */
16
+ export const Default: Story = {
17
+ play: async ({ canvas }) => {
18
+ const el = canvas.getByTestId("skeleton");
19
+ await expect(el).toBeInTheDocument();
20
+ await expect(el).toHaveAttribute("aria-hidden", "true");
21
+ await expect(el.className).toContain("skeleton");
22
+ },
23
+ };
24
+
25
+ /** Circular skeleton (avatar placeholder). */
26
+ export const Circular: Story = {
27
+ args: { variant: "circular", width: "48px", height: "48px" },
28
+ play: async ({ canvas }) => {
29
+ const el = canvas.getByTestId("skeleton");
30
+ await expect(el.className).toContain("circular");
31
+ },
32
+ };
33
+
34
+ /** Custom width and height. */
35
+ export const CustomSize: Story = {
36
+ args: { width: "200px", height: "2rem" },
37
+ play: async ({ canvas }) => {
38
+ const el = canvas.getByTestId("skeleton");
39
+ await expect(el.style.width).toBe("200px");
40
+ await expect(el.style.height).toBe("2rem");
41
+ },
42
+ };
43
+
44
+ // ─── SkeletonText ────────────────────────────────────────────────────────────
45
+
46
+ /** Multi-line text placeholder (3 lines, last line shorter). */
47
+ export const Text: Story = {
48
+ render: (args) => <SkeletonText {...args} />,
49
+ play: async ({ canvas }) => {
50
+ const container = canvas.getByTestId("skeleton-text");
51
+ await expect(container).toBeInTheDocument();
52
+ const lines = container.querySelectorAll("[data-testid='skeleton']");
53
+ await expect(lines.length).toBe(3);
54
+ },
55
+ };
56
+
57
+ /** Single-line text placeholder. */
58
+ export const TextSingleLine: Story = {
59
+ render: () => <SkeletonText lines={1} />,
60
+ play: async ({ canvas }) => {
61
+ const container = canvas.getByTestId("skeleton-text");
62
+ const lines = container.querySelectorAll("[data-testid='skeleton']");
63
+ await expect(lines.length).toBe(1);
64
+ },
65
+ };
66
+
67
+ // ─── SkeletonCard ────────────────────────────────────────────────────────────
68
+
69
+ /** Card-shaped skeleton with title and body text. */
70
+ export const Card: Story = {
71
+ render: (args) => <SkeletonCard {...args} />,
72
+ play: async ({ canvas }) => {
73
+ const card = canvas.getByTestId("skeleton-card");
74
+ await expect(card).toBeInTheDocument();
75
+ // Title skeleton + text container with 2 lines = 3 skeleton elements total
76
+ const skeletons = card.querySelectorAll("[data-testid='skeleton']");
77
+ await expect(skeletons.length).toBe(3);
78
+ },
79
+ };
80
+
81
+ /** Grid of skeleton cards (composition demo). */
82
+ export const CardGrid: Story = {
83
+ render: () => (
84
+ <div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: "var(--space-lg)" }}>
85
+ <SkeletonCard />
86
+ <SkeletonCard />
87
+ <SkeletonCard />
88
+ </div>
89
+ ),
90
+ play: async ({ canvas }) => {
91
+ const cards = canvas.getAllByTestId("skeleton-card");
92
+ await expect(cards.length).toBe(3);
93
+ },
94
+ };
@@ -0,0 +1,127 @@
1
+ import type { JSX } from "react";
2
+ import styles from "./Skeleton.module.scss";
3
+
4
+ // ─── Skeleton (base shimmer block) ───────────────────────────────────────────
5
+
6
+ /** Shape variant for the Skeleton component. */
7
+ type SkeletonVariant = "rectangular" | "circular";
8
+
9
+ /** Props for the {@link Skeleton} component. */
10
+ interface SkeletonProps {
11
+ /** CSS width. Defaults to `"100%"`. */
12
+ width?: string;
13
+ /** CSS height. Defaults to `"1rem"`. */
14
+ height?: string;
15
+ /** CSS border-radius override. Ignored when `variant` is `"circular"`. */
16
+ borderRadius?: string;
17
+ /** Shape variant. `"circular"` forces 50% border-radius. Defaults to `"rectangular"`. */
18
+ variant?: SkeletonVariant;
19
+ /** Additional CSS class name. */
20
+ className?: string;
21
+ }
22
+
23
+ /**
24
+ * Animated shimmer placeholder that indicates loading content.
25
+ * Renders a decorative `<div>` with a gradient sweep animation.
26
+ */
27
+ export function Skeleton({
28
+ width = "100%",
29
+ height = "1rem",
30
+ borderRadius,
31
+ variant = "rectangular",
32
+ className,
33
+ }: SkeletonProps): JSX.Element {
34
+ const classNames = [
35
+ styles.skeleton,
36
+ variant === "circular" ? styles.circular : "",
37
+ className ?? "",
38
+ ].filter(Boolean).join(" ");
39
+
40
+ return (
41
+ <div
42
+ className={classNames}
43
+ style={{
44
+ width,
45
+ height,
46
+ ...(borderRadius && variant !== "circular" ? { borderRadius } : {}),
47
+ }}
48
+ aria-hidden="true"
49
+ data-testid="skeleton"
50
+ />
51
+ );
52
+ }
53
+
54
+ // ─── SkeletonText (multi-line text placeholder) ──────────────────────────────
55
+
56
+ /** Props for the {@link SkeletonText} component. */
57
+ interface SkeletonTextProps {
58
+ /** Number of text lines. Defaults to `3`. */
59
+ lines?: number;
60
+ /** Width of the last line. Defaults to `"60%"`. */
61
+ lastLineWidth?: string;
62
+ /** Height of each line. Defaults to `"0.75rem"`. */
63
+ lineHeight?: string;
64
+ /** Gap between lines. Defaults to `"var(--space-sm)"`. */
65
+ gap?: string;
66
+ /** Additional CSS class name. */
67
+ className?: string;
68
+ }
69
+
70
+ /**
71
+ * Multi-line skeleton text placeholder. Renders `lines` shimmer blocks
72
+ * with the last line at a shorter width to simulate trailing text.
73
+ */
74
+ export function SkeletonText({
75
+ lines = 3,
76
+ lastLineWidth = "60%",
77
+ lineHeight = "0.75rem",
78
+ gap = "var(--space-sm)",
79
+ className,
80
+ }: SkeletonTextProps): JSX.Element {
81
+ return (
82
+ <div
83
+ className={`${styles.textContainer} ${className ?? ""}`}
84
+ style={{ gap }}
85
+ aria-hidden="true"
86
+ data-testid="skeleton-text"
87
+ >
88
+ {Array.from({ length: lines }, (_, i) => (
89
+ <Skeleton
90
+ key={i}
91
+ width={i === lines - 1 && lines > 1 ? lastLineWidth : "100%"}
92
+ height={lineHeight}
93
+ />
94
+ ))}
95
+ </div>
96
+ );
97
+ }
98
+
99
+ // ─── SkeletonCard (card-shaped placeholder) ──────────────────────────────────
100
+
101
+ /** Props for the {@link SkeletonCard} component. */
102
+ interface SkeletonCardProps {
103
+ /** Number of body text lines inside the card. Defaults to `2`. */
104
+ lines?: number;
105
+ /** Additional CSS class name. */
106
+ className?: string;
107
+ }
108
+
109
+ /**
110
+ * Card-shaped skeleton placeholder matching the standard card layout.
111
+ * Contains a title-width shimmer block and body text lines.
112
+ */
113
+ export function SkeletonCard({
114
+ lines = 2,
115
+ className,
116
+ }: SkeletonCardProps): JSX.Element {
117
+ return (
118
+ <div
119
+ className={`${styles.card} ${className ?? ""}`}
120
+ aria-hidden="true"
121
+ data-testid="skeleton-card"
122
+ >
123
+ <Skeleton width="40%" height="1.25rem" />
124
+ <SkeletonText lines={lines} />
125
+ </div>
126
+ );
127
+ }
@@ -0,0 +1,41 @@
1
+ // =============================================================================
2
+ // Spinner — inline loading indicator
3
+ // =============================================================================
4
+
5
+ .spinner {
6
+ display: inline-block;
7
+ width: 12px;
8
+ height: 12px;
9
+ border: 2px solid transparent;
10
+ border-top-color: currentColor;
11
+ border-right-color: currentColor;
12
+ border-radius: 50%;
13
+ animation: spin 0.7s linear infinite;
14
+ vertical-align: middle;
15
+ flex-shrink: 0;
16
+ }
17
+
18
+ // Size variants
19
+ .sm {
20
+ width: 10px;
21
+ height: 10px;
22
+ border-width: 1.5px;
23
+ }
24
+
25
+ .md {
26
+ width: 12px;
27
+ height: 12px;
28
+ border-width: 2px;
29
+ }
30
+
31
+ .lg {
32
+ width: 16px;
33
+ height: 16px;
34
+ border-width: 2px;
35
+ }
36
+
37
+ .xl {
38
+ width: 48px;
39
+ height: 48px;
40
+ border-width: 4px;
41
+ }
@@ -0,0 +1,66 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { expect } from "@storybook/test";
3
+ import { Spinner } from "./Spinner.js";
4
+
5
+ const meta: Meta<typeof Spinner> = {
6
+ component: Spinner,
7
+ title: "Primitives/Display/Spinner",
8
+ tags: ["autodocs"],
9
+ };
10
+ export default meta;
11
+ type Story = StoryObj<typeof meta>;
12
+
13
+ /** Default medium spinner. */
14
+ export const Default: Story = {
15
+ play: async ({ canvas }) => {
16
+ const spinner = canvas.getByLabelText("Loading");
17
+ await expect(spinner).toBeInTheDocument();
18
+ await expect(spinner.className).toContain("md");
19
+ },
20
+ };
21
+
22
+ /** Small spinner. */
23
+ export const Small: Story = {
24
+ args: { size: "sm" },
25
+ play: async ({ canvas }) => {
26
+ const spinner = canvas.getByLabelText("Loading");
27
+ await expect(spinner.className).toContain("sm");
28
+ },
29
+ };
30
+
31
+ /** Large spinner. */
32
+ export const Large: Story = {
33
+ args: { size: "lg" },
34
+ play: async ({ canvas }) => {
35
+ const spinner = canvas.getByLabelText("Loading");
36
+ await expect(spinner.className).toContain("lg");
37
+ },
38
+ };
39
+
40
+ /** Extra large spinner. */
41
+ export const ExtraLarge: Story = {
42
+ args: { size: "xl" },
43
+ play: async ({ canvas }) => {
44
+ const spinner = canvas.getByLabelText("Loading");
45
+ await expect(spinner.className).toContain("xl");
46
+ },
47
+ };
48
+
49
+ /** Spinner with a custom accessible label. */
50
+ export const WithLabel: Story = {
51
+ args: { label: "Saving changes" },
52
+ play: async ({ canvas }) => {
53
+ const spinner = canvas.getByLabelText("Saving changes");
54
+ await expect(spinner).toBeInTheDocument();
55
+ },
56
+ };
57
+
58
+ /** Spinner as a live region announces changes to screen readers. */
59
+ export const LiveRegion: Story = {
60
+ args: { liveRegion: true, label: "Processing" },
61
+ play: async ({ canvas }) => {
62
+ const spinner = canvas.getByRole("status");
63
+ await expect(spinner).toBeInTheDocument();
64
+ await expect(spinner).toHaveAttribute("aria-label", "Processing");
65
+ },
66
+ };
@@ -0,0 +1,32 @@
1
+ import type { JSX } from "react";
2
+ import styles from "./Spinner.module.scss";
3
+
4
+ /** Size variants for the Spinner component. */
5
+ type SpinnerSize = "sm" | "md" | "lg" | "xl";
6
+
7
+ /** Props for the Spinner component. */
8
+ interface Props {
9
+ /** Size of the spinner. Defaults to "md". */
10
+ size?: SpinnerSize;
11
+ /** Additional CSS class name. */
12
+ className?: string;
13
+ /** Accessible label for screen readers. Defaults to "Loading". */
14
+ label?: string;
15
+ /** Whether this spinner is the primary live region. Defaults to false to avoid multiple live regions. */
16
+ liveRegion?: boolean;
17
+ }
18
+
19
+ /**
20
+ * Inline spinning loader that inherits the current text color.
21
+ * Use alongside disabled buttons or hint text to signal in-flight async operations.
22
+ */
23
+ export function Spinner({ size = "md", className, label = "Loading", liveRegion = false }: Props): JSX.Element {
24
+ return (
25
+ <span
26
+ className={`${styles.spinner} ${styles[size]} ${className ?? ""}`}
27
+ role={liveRegion ? "status" : undefined}
28
+ aria-label={label}
29
+ aria-hidden={liveRegion ? undefined : true}
30
+ />
31
+ );
32
+ }
@@ -0,0 +1,20 @@
1
+ // =============================================================================
2
+ // SplashScreen — full-viewport loading state shown before initial data arrives
3
+ // =============================================================================
4
+
5
+ .splash {
6
+ min-height: 100vh;
7
+ display: flex;
8
+ flex-direction: column;
9
+ align-items: center;
10
+ justify-content: center;
11
+ gap: var(--space-lg);
12
+ background: var(--bg-base);
13
+ }
14
+
15
+ .logo {
16
+ display: block;
17
+ width: 128px;
18
+ height: 128px;
19
+ object-fit: contain;
20
+ }
@@ -0,0 +1,26 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { expect } from "@storybook/test";
3
+ import { SplashScreen } from "./SplashScreen.js";
4
+
5
+ const meta: Meta<typeof SplashScreen> = {
6
+ component: SplashScreen,
7
+ title: "Primitives/Display/SplashScreen",
8
+ tags: ["autodocs"],
9
+ };
10
+ export default meta;
11
+ type Story = StoryObj<typeof meta>;
12
+
13
+ /** Default splash screen with logo and spinner. */
14
+ export const Default: Story = {
15
+ play: async ({ canvas }) => {
16
+ const splash = canvas.getByTestId("splash-screen");
17
+ await expect(splash).toBeInTheDocument();
18
+ // Should contain the logo image
19
+ const logo = canvas.getByAltText("Grackle");
20
+ await expect(logo).toBeInTheDocument();
21
+ // Should contain a spinner with live region
22
+ const spinner = canvas.getByRole("status");
23
+ await expect(spinner).toBeInTheDocument();
24
+ await expect(spinner).toHaveAttribute("aria-label", "Loading Grackle");
25
+ },
26
+ };