flock-core 0.5.0b50__py3-none-any.whl → 0.5.0b52__py3-none-any.whl

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.

Potentially problematic release.


This version of flock-core might be problematic. Click here for more details.

Files changed (117) hide show
  1. flock/dashboard/launcher.py +1 -1
  2. flock/frontend/README.md +678 -0
  3. flock/frontend/docs/DESIGN_SYSTEM.md +1980 -0
  4. flock/frontend/index.html +12 -0
  5. flock/frontend/package-lock.json +4347 -0
  6. flock/frontend/package.json +48 -0
  7. flock/frontend/src/App.tsx +79 -0
  8. flock/frontend/src/__tests__/e2e/critical-scenarios.test.tsx +587 -0
  9. flock/frontend/src/__tests__/integration/filtering-e2e.test.tsx +387 -0
  10. flock/frontend/src/__tests__/integration/graph-rendering.test.tsx +640 -0
  11. flock/frontend/src/__tests__/integration/indexeddb-persistence.test.tsx +699 -0
  12. flock/frontend/src/components/common/BuildInfo.tsx +39 -0
  13. flock/frontend/src/components/common/EmptyState.module.css +115 -0
  14. flock/frontend/src/components/common/EmptyState.tsx +128 -0
  15. flock/frontend/src/components/common/ErrorBoundary.module.css +169 -0
  16. flock/frontend/src/components/common/ErrorBoundary.tsx +118 -0
  17. flock/frontend/src/components/common/KeyboardShortcutsDialog.css +251 -0
  18. flock/frontend/src/components/common/KeyboardShortcutsDialog.tsx +151 -0
  19. flock/frontend/src/components/common/LoadingSpinner.module.css +97 -0
  20. flock/frontend/src/components/common/LoadingSpinner.tsx +29 -0
  21. flock/frontend/src/components/controls/PublishControl.css +547 -0
  22. flock/frontend/src/components/controls/PublishControl.test.tsx +543 -0
  23. flock/frontend/src/components/controls/PublishControl.tsx +432 -0
  24. flock/frontend/src/components/details/DetailWindowContainer.tsx +62 -0
  25. flock/frontend/src/components/details/LiveOutputTab.test.tsx +792 -0
  26. flock/frontend/src/components/details/LiveOutputTab.tsx +220 -0
  27. flock/frontend/src/components/details/MessageHistoryTab.tsx +299 -0
  28. flock/frontend/src/components/details/NodeDetailWindow.test.tsx +501 -0
  29. flock/frontend/src/components/details/NodeDetailWindow.tsx +218 -0
  30. flock/frontend/src/components/details/RunStatusTab.tsx +307 -0
  31. flock/frontend/src/components/details/tabs.test.tsx +1015 -0
  32. flock/frontend/src/components/filters/CorrelationIDFilter.module.css +102 -0
  33. flock/frontend/src/components/filters/CorrelationIDFilter.test.tsx +197 -0
  34. flock/frontend/src/components/filters/CorrelationIDFilter.tsx +121 -0
  35. flock/frontend/src/components/filters/FilterBar.module.css +29 -0
  36. flock/frontend/src/components/filters/FilterBar.test.tsx +133 -0
  37. flock/frontend/src/components/filters/FilterBar.tsx +33 -0
  38. flock/frontend/src/components/filters/FilterPills.module.css +79 -0
  39. flock/frontend/src/components/filters/FilterPills.test.tsx +173 -0
  40. flock/frontend/src/components/filters/FilterPills.tsx +67 -0
  41. flock/frontend/src/components/filters/TimeRangeFilter.module.css +91 -0
  42. flock/frontend/src/components/filters/TimeRangeFilter.test.tsx +154 -0
  43. flock/frontend/src/components/filters/TimeRangeFilter.tsx +105 -0
  44. flock/frontend/src/components/graph/AgentNode.test.tsx +75 -0
  45. flock/frontend/src/components/graph/AgentNode.tsx +322 -0
  46. flock/frontend/src/components/graph/GraphCanvas.tsx +406 -0
  47. flock/frontend/src/components/graph/MessageFlowEdge.tsx +128 -0
  48. flock/frontend/src/components/graph/MessageNode.test.tsx +62 -0
  49. flock/frontend/src/components/graph/MessageNode.tsx +116 -0
  50. flock/frontend/src/components/graph/MiniMap.tsx +47 -0
  51. flock/frontend/src/components/graph/TransformEdge.tsx +123 -0
  52. flock/frontend/src/components/layout/DashboardLayout.css +407 -0
  53. flock/frontend/src/components/layout/DashboardLayout.tsx +300 -0
  54. flock/frontend/src/components/layout/Header.module.css +88 -0
  55. flock/frontend/src/components/layout/Header.tsx +52 -0
  56. flock/frontend/src/components/modules/EventLogModule.test.tsx +401 -0
  57. flock/frontend/src/components/modules/EventLogModule.tsx +396 -0
  58. flock/frontend/src/components/modules/EventLogModuleWrapper.tsx +17 -0
  59. flock/frontend/src/components/modules/ModuleRegistry.test.ts +333 -0
  60. flock/frontend/src/components/modules/ModuleRegistry.ts +85 -0
  61. flock/frontend/src/components/modules/ModuleWindow.tsx +155 -0
  62. flock/frontend/src/components/modules/registerModules.ts +20 -0
  63. flock/frontend/src/components/settings/AdvancedSettings.tsx +175 -0
  64. flock/frontend/src/components/settings/AppearanceSettings.tsx +185 -0
  65. flock/frontend/src/components/settings/GraphSettings.tsx +110 -0
  66. flock/frontend/src/components/settings/SettingsPanel.css +327 -0
  67. flock/frontend/src/components/settings/SettingsPanel.tsx +131 -0
  68. flock/frontend/src/components/settings/ThemeSelector.tsx +298 -0
  69. flock/frontend/src/hooks/useKeyboardShortcuts.ts +148 -0
  70. flock/frontend/src/hooks/useModulePersistence.test.ts +442 -0
  71. flock/frontend/src/hooks/useModulePersistence.ts +154 -0
  72. flock/frontend/src/hooks/useModules.ts +139 -0
  73. flock/frontend/src/hooks/usePersistence.ts +139 -0
  74. flock/frontend/src/main.tsx +13 -0
  75. flock/frontend/src/services/api.ts +213 -0
  76. flock/frontend/src/services/indexeddb.test.ts +793 -0
  77. flock/frontend/src/services/indexeddb.ts +794 -0
  78. flock/frontend/src/services/layout.test.ts +437 -0
  79. flock/frontend/src/services/layout.ts +146 -0
  80. flock/frontend/src/services/themeApplicator.ts +140 -0
  81. flock/frontend/src/services/themeService.ts +77 -0
  82. flock/frontend/src/services/websocket.test.ts +595 -0
  83. flock/frontend/src/services/websocket.ts +685 -0
  84. flock/frontend/src/store/filterStore.test.ts +242 -0
  85. flock/frontend/src/store/filterStore.ts +103 -0
  86. flock/frontend/src/store/graphStore.test.ts +186 -0
  87. flock/frontend/src/store/graphStore.ts +414 -0
  88. flock/frontend/src/store/moduleStore.test.ts +253 -0
  89. flock/frontend/src/store/moduleStore.ts +57 -0
  90. flock/frontend/src/store/settingsStore.ts +188 -0
  91. flock/frontend/src/store/streamStore.ts +68 -0
  92. flock/frontend/src/store/uiStore.test.ts +54 -0
  93. flock/frontend/src/store/uiStore.ts +110 -0
  94. flock/frontend/src/store/wsStore.ts +34 -0
  95. flock/frontend/src/styles/index.css +15 -0
  96. flock/frontend/src/styles/scrollbar.css +47 -0
  97. flock/frontend/src/styles/variables.css +488 -0
  98. flock/frontend/src/test/setup.ts +1 -0
  99. flock/frontend/src/types/filters.ts +14 -0
  100. flock/frontend/src/types/graph.ts +55 -0
  101. flock/frontend/src/types/modules.ts +7 -0
  102. flock/frontend/src/types/theme.ts +55 -0
  103. flock/frontend/src/utils/mockData.ts +85 -0
  104. flock/frontend/src/utils/performance.ts +16 -0
  105. flock/frontend/src/utils/transforms.test.ts +860 -0
  106. flock/frontend/src/utils/transforms.ts +323 -0
  107. flock/frontend/src/vite-env.d.ts +17 -0
  108. flock/frontend/tsconfig.json +27 -0
  109. flock/frontend/tsconfig.node.json +11 -0
  110. flock/frontend/vite.config.ts +25 -0
  111. flock/frontend/vitest.config.ts +11 -0
  112. flock/helper/cli_helper.py +1 -1
  113. {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b52.dist-info}/METADATA +1 -1
  114. {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b52.dist-info}/RECORD +117 -7
  115. {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b52.dist-info}/WHEEL +0 -0
  116. {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b52.dist-info}/entry_points.txt +0 -0
  117. {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b52.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,300 @@
1
+ import React, { useState } from 'react';
2
+ import { ReactFlowProvider } from '@xyflow/react';
3
+ import GraphCanvas from '../graph/GraphCanvas';
4
+ import DetailWindowContainer from '../details/DetailWindowContainer';
5
+ import FilterBar from '../filters/FilterBar';
6
+ import PublishControl from '../controls/PublishControl';
7
+ import ModuleWindow from '../modules/ModuleWindow';
8
+ import SettingsPanel from '../settings/SettingsPanel';
9
+ import KeyboardShortcutsDialog from '../common/KeyboardShortcutsDialog';
10
+ import { useUIStore } from '../../store/uiStore';
11
+ import { useModuleStore } from '../../store/moduleStore';
12
+ import { useGraphStore } from '../../store/graphStore';
13
+ import { useFilterStore } from '../../store/filterStore';
14
+ import { useSettingsStore } from '../../store/settingsStore';
15
+ import { useModulePersistence } from '../../hooks/useModulePersistence';
16
+ import { useKeyboardShortcuts } from '../../hooks/useKeyboardShortcuts';
17
+ import Header from './Header';
18
+ import './DashboardLayout.css';
19
+
20
+ const DashboardLayout: React.FC = () => {
21
+ const mode = useUIStore((state) => state.mode);
22
+ const setMode = useUIStore((state) => state.setMode);
23
+ const moduleInstances = useModuleStore((state) => state.instances);
24
+ const detailWindows = useUIStore((state) => state.detailWindows);
25
+
26
+ // UI visibility from settings store
27
+ const showFilters = useSettingsStore((state) => state.ui.showFilters);
28
+ const showControls = useSettingsStore((state) => state.ui.showControls);
29
+ const showSettings = useSettingsStore((state) => state.ui.showSettings);
30
+ const setShowFilters = useSettingsStore((state) => state.setShowFilters);
31
+ const setShowControls = useSettingsStore((state) => state.setShowControls);
32
+ const setShowSettings = useSettingsStore((state) => state.setShowSettings);
33
+
34
+ // Keyboard shortcuts help dialog
35
+ const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState(false);
36
+
37
+ // Enable module window persistence
38
+ useModulePersistence();
39
+
40
+ const handleToggleAgentDetails = () => {
41
+ const detailWindows = useUIStore.getState().detailWindows;
42
+ const agents = useGraphStore.getState().agents;
43
+
44
+ // Check if any detail windows are open
45
+ if (detailWindows.size > 0) {
46
+ // Close all detail windows
47
+ detailWindows.forEach((_, nodeId) => {
48
+ useUIStore.getState().closeDetailWindow(nodeId);
49
+ });
50
+ } else {
51
+ // Open detail windows for all agents
52
+ agents.forEach((agent) => {
53
+ useUIStore.getState().openDetailWindow(agent.id);
54
+ });
55
+ }
56
+ };
57
+
58
+ // Enable keyboard shortcuts with help dialog toggle
59
+ useKeyboardShortcuts({
60
+ onToggleHelp: () => setShowKeyboardShortcuts((prev) => !prev),
61
+ onToggleAgentDetails: handleToggleAgentDetails,
62
+ });
63
+
64
+ const handleClearStore = () => {
65
+ if (confirm('Clear all dashboard data? This will remove all agents, messages, and session data.')) {
66
+ // Clear graph store
67
+ useGraphStore.setState({
68
+ agents: new Map(),
69
+ messages: new Map(),
70
+ events: [],
71
+ runs: new Map(),
72
+ nodes: [],
73
+ edges: []
74
+ });
75
+
76
+ // Clear UI store
77
+ useUIStore.setState({ mode: 'agent' });
78
+
79
+ // Clear filter store
80
+ useFilterStore.setState({
81
+ correlationId: undefined,
82
+ timeRange: undefined,
83
+ });
84
+
85
+ // Clear module store
86
+ useModuleStore.getState().instances.clear();
87
+
88
+ // Clear IndexedDB
89
+ indexedDB.databases().then((dbs) => {
90
+ dbs.forEach((db) => {
91
+ if (db.name) indexedDB.deleteDatabase(db.name);
92
+ });
93
+ });
94
+
95
+ // Clear localStorage
96
+ localStorage.clear();
97
+
98
+ // Reload page
99
+ window.location.reload();
100
+ }
101
+ };
102
+
103
+ return (
104
+ <div className="dashboard-layout">
105
+ {/* Header */}
106
+ <header className="dashboard-header">
107
+ <h1 className="dashboard-title">🦆🐓 Flock 🐤🐧</h1>
108
+
109
+ <div className="view-toggle-container">
110
+ <span className="view-toggle-label">View:</span>
111
+ <div className="view-toggle-group">
112
+ <button
113
+ type="button"
114
+ onClick={() => setMode('agent')}
115
+ className={`view-toggle-button ${mode === 'agent' ? 'active' : ''}`}
116
+ >
117
+ Agent View
118
+ </button>
119
+ <button
120
+ type="button"
121
+ onClick={() => setMode('blackboard')}
122
+ className={`view-toggle-button ${mode === 'blackboard' ? 'active' : ''}`}
123
+ >
124
+ Blackboard View
125
+ </button>
126
+ </div>
127
+ </div>
128
+
129
+ <div className="dashboard-actions">
130
+ {/* Publish - Primary action, first in order */}
131
+ <button
132
+ type="button"
133
+ onClick={() => setShowControls(!showControls)}
134
+ className={`controls-toggle primary ${showControls ? 'active' : ''}`}
135
+ title="Publish artifacts to the blackboard (Ctrl+Shift+P)"
136
+ aria-pressed={showControls ? 'true' : 'false'}
137
+ aria-label={showControls ? 'Publish panel open' : 'Publish panel closed'}
138
+ >
139
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
140
+ <path
141
+ d="M8 2.667v10.666M2.667 8h10.666"
142
+ stroke="currentColor"
143
+ strokeWidth="1.5"
144
+ strokeLinecap="round"
145
+ strokeLinejoin="round"
146
+ />
147
+ </svg>
148
+ <span>Publish</span>
149
+ </button>
150
+
151
+ {/* Agent Details */}
152
+ <button
153
+ type="button"
154
+ onClick={handleToggleAgentDetails}
155
+ className={`controls-toggle ${detailWindows.size > 0 ? 'active' : ''}`}
156
+ title={`${detailWindows.size > 0 ? 'Close' : 'Show'} agent detail windows (Ctrl+Shift+D)`}
157
+ aria-pressed={detailWindows.size > 0 ? 'true' : 'false'}
158
+ aria-label={detailWindows.size > 0 ? 'Agent details shown' : 'Agent details hidden'}
159
+ >
160
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
161
+ <path
162
+ d="M2 4.667h12M2 8h12M2 11.333h12"
163
+ stroke="currentColor"
164
+ strokeWidth="1.5"
165
+ strokeLinecap="round"
166
+ strokeLinejoin="round"
167
+ />
168
+ </svg>
169
+ <span>Agent Details</span>
170
+ </button>
171
+
172
+ {/* Filter */}
173
+ <button
174
+ type="button"
175
+ onClick={() => setShowFilters(!showFilters)}
176
+ className={`controls-toggle ${showFilters ? 'active' : ''}`}
177
+ title={`${showFilters ? 'Hide' : 'Show'} filter panel (Ctrl+Shift+F)`}
178
+ aria-pressed={showFilters ? 'true' : 'false'}
179
+ aria-label={showFilters ? 'Filters shown' : 'Filters hidden'}
180
+ >
181
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
182
+ <path
183
+ d="M2.667 2.667h10.666L9.333 7v4.667l-2.666 1.333V7L2.667 2.667z"
184
+ stroke="currentColor"
185
+ strokeWidth="1.5"
186
+ strokeLinecap="round"
187
+ strokeLinejoin="round"
188
+ />
189
+ </svg>
190
+ <span>Filter</span>
191
+ </button>
192
+
193
+ {/* Settings */}
194
+ <button
195
+ type="button"
196
+ onClick={() => setShowSettings(!showSettings)}
197
+ className={`controls-toggle ${showSettings ? 'active' : ''}`}
198
+ title={`${showSettings ? 'Hide' : 'Show'} settings panel (Ctrl+,)`}
199
+ aria-pressed={showSettings ? 'true' : 'false'}
200
+ aria-label={showSettings ? 'Settings shown' : 'Settings hidden'}
201
+ >
202
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
203
+ <path
204
+ d="M8 10a2 2 0 100-4 2 2 0 000 4z"
205
+ stroke="currentColor"
206
+ strokeWidth="1.5"
207
+ />
208
+ <path
209
+ d="M13.333 8c0-.367-.2-.7-.533-.867l-.933-.467a.933.933 0 01-.467-.8v-.133c0-.334.133-.6.467-.8l.933-.467c.333-.166.533-.5.533-.866 0-.367-.2-.7-.533-.867L11.867 2.4a.933.933 0 01-.467-.8V1.333c0-.366-.233-.666-.6-.666H9.2c-.366 0-.666.3-.666.666V1.6c0 .334-.267.634-.6.8l-.934.467c-.333.166-.7.166-1 0l-.933-.467a.933.933 0 00-.8 0L2.533 3.067c-.333.166-.533.5-.533.866 0 .367.2.7.533.867l.933.467c.334.2.467.466.467.8v.133c0 .334-.133.6-.467.8l-.933.467c-.333.166-.533.5-.533.866 0 .367.2.7.533.867l.934.533c.333.2.6.5.6.8v.267c0 .366.3.666.666.666h1.6c.367 0 .667-.3.667-.666v-.267c0-.3.267-.6.6-.8l.933-.533c.334-.167.7-.167 1 0l.934.533c.333.2.6.5.6.8v.267c0 .366.3.666.666.666H9.2c.367 0 .667-.3.667-.666v-.267c0-.3.266-.6.6-.8l.933-.533c.333-.167.533-.5.533-.867z"
210
+ stroke="currentColor"
211
+ strokeWidth="1.5"
212
+ />
213
+ </svg>
214
+ <span>Settings</span>
215
+ </button>
216
+
217
+ <button
218
+ type="button"
219
+ onClick={() => setShowKeyboardShortcuts(true)}
220
+ className="icon-button help-button"
221
+ title="Keyboard shortcuts (Ctrl+/)"
222
+ aria-label="Show keyboard shortcuts"
223
+ >
224
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
225
+ <circle
226
+ cx="8"
227
+ cy="8"
228
+ r="6.5"
229
+ stroke="currentColor"
230
+ strokeWidth="1.5"
231
+ />
232
+ <path
233
+ d="M8 11.5v-.5M8 8.5v-2a1.5 1.5 0 10-1.5-1.5"
234
+ stroke="currentColor"
235
+ strokeWidth="1.5"
236
+ strokeLinecap="round"
237
+ />
238
+ <circle cx="8" cy="11.5" r="0.5" fill="currentColor" />
239
+ </svg>
240
+ </button>
241
+
242
+ <button
243
+ type="button"
244
+ onClick={handleClearStore}
245
+ className="icon-button clear-button"
246
+ title="Clear all dashboard data"
247
+ aria-label="Clear all dashboard data"
248
+ >
249
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
250
+ <path
251
+ d="M2 4h12M5.333 4V2.667a1.333 1.333 0 011.334-1.334h2.666a1.333 1.333 0 011.334 1.334V4m2 0v9.333a1.333 1.333 0 01-1.334 1.334H4.667a1.333 1.333 0 01-1.334-1.334V4h9.334z"
252
+ stroke="currentColor"
253
+ strokeWidth="1.5"
254
+ strokeLinecap="round"
255
+ strokeLinejoin="round"
256
+ />
257
+ </svg>
258
+ </button>
259
+
260
+ <Header />
261
+ </div>
262
+ </header>
263
+
264
+ {/* Filter Bar - Collapsible */}
265
+ {showFilters && <FilterBar />}
266
+
267
+ {/* Main Content */}
268
+ <div className="dashboard-main">
269
+ {/* Graph Canvas */}
270
+ <main className="graph-container">
271
+ <ReactFlowProvider>
272
+ <GraphCanvas />
273
+ </ReactFlowProvider>
274
+ </main>
275
+ </div>
276
+
277
+ {/* Detail Windows */}
278
+ <DetailWindowContainer />
279
+
280
+ {/* Module Windows */}
281
+ {Array.from(moduleInstances.values()).map((instance) => (
282
+ <ModuleWindow key={instance.id} instanceId={instance.id} />
283
+ ))}
284
+
285
+ {/* Publish Control Panel - Slides in from right */}
286
+ {showControls && <PublishControl />}
287
+
288
+ {/* Settings Panel - Slides in from right */}
289
+ {showSettings && <SettingsPanel />}
290
+
291
+ {/* Keyboard Shortcuts Dialog */}
292
+ <KeyboardShortcutsDialog
293
+ isOpen={showKeyboardShortcuts}
294
+ onClose={() => setShowKeyboardShortcuts(false)}
295
+ />
296
+ </div>
297
+ );
298
+ };
299
+
300
+ export default DashboardLayout;
@@ -0,0 +1,88 @@
1
+ .connectionBadge {
2
+ display: flex;
3
+ align-items: center;
4
+ gap: var(--gap-sm);
5
+ padding: var(--spacing-1-5) var(--spacing-3);
6
+ border-radius: var(--radius-full);
7
+ font-size: var(--font-size-caption);
8
+ font-weight: var(--font-weight-medium);
9
+ box-shadow: var(--shadow-xs);
10
+ transition: var(--transition-all);
11
+ cursor: default;
12
+ }
13
+
14
+ .connectionBadge.hasError {
15
+ cursor: help;
16
+ }
17
+
18
+ /* Status Variants */
19
+ .connectionBadge.connected {
20
+ background-color: var(--color-success-bg);
21
+ color: var(--color-success-dark);
22
+ border: var(--border-width-1) solid var(--color-success-border);
23
+ }
24
+
25
+ .connectionBadge.connecting,
26
+ .connectionBadge.reconnecting {
27
+ background-color: var(--color-warning-bg);
28
+ color: var(--color-warning-dark);
29
+ border: var(--border-width-1) solid var(--color-warning-border);
30
+ }
31
+
32
+ .connectionBadge.disconnected {
33
+ background-color: var(--color-error-bg);
34
+ color: var(--color-error-dark);
35
+ border: var(--border-width-1) solid var(--color-error-border);
36
+ }
37
+
38
+ /* Status Indicator Dot */
39
+ .statusIndicator {
40
+ width: 8px;
41
+ height: 8px;
42
+ border-radius: var(--radius-circle);
43
+ transition: var(--transition-opacity);
44
+ }
45
+
46
+ .statusIndicator.connected {
47
+ background-color: var(--color-success);
48
+ }
49
+
50
+ .statusIndicator.connecting,
51
+ .statusIndicator.reconnecting {
52
+ background-color: var(--color-warning);
53
+ animation: statusPulse 2s var(--ease-smooth) infinite;
54
+ }
55
+
56
+ .statusIndicator.disconnected {
57
+ background-color: var(--color-error);
58
+ }
59
+
60
+ /* Status Text */
61
+ .statusText {
62
+ line-height: var(--line-height-snug);
63
+ letter-spacing: var(--letter-spacing-normal);
64
+ }
65
+
66
+ /* Pulse Animation for Connecting States */
67
+ @keyframes statusPulse {
68
+ 0%, 100% {
69
+ opacity: 1;
70
+ transform: scale(1);
71
+ }
72
+ 50% {
73
+ opacity: 0.5;
74
+ transform: scale(0.95);
75
+ }
76
+ }
77
+
78
+ /* Accessibility - Reduced Motion Support */
79
+ @media (prefers-reduced-motion: reduce) {
80
+ .statusIndicator.connecting,
81
+ .statusIndicator.reconnecting {
82
+ animation: none;
83
+ }
84
+
85
+ .connectionBadge {
86
+ transition: none;
87
+ }
88
+ }
@@ -0,0 +1,52 @@
1
+ import React from 'react';
2
+ import { useWSStore } from '../../store/wsStore';
3
+ import styles from './Header.module.css';
4
+
5
+ interface ConnectionStatusBadgeProps {
6
+ status: 'connecting' | 'connected' | 'disconnected' | 'reconnecting';
7
+ attempts: number;
8
+ error: string | null;
9
+ }
10
+
11
+ const ConnectionStatusBadge: React.FC<ConnectionStatusBadgeProps> = ({ status, attempts, error }) => {
12
+ const statusText = {
13
+ connected: 'Connected',
14
+ connecting: 'Connecting...',
15
+ reconnecting: `Reconnecting (${attempts})...`,
16
+ disconnected: 'Disconnected',
17
+ };
18
+
19
+ const badgeClassName = [
20
+ styles.connectionBadge,
21
+ styles[status],
22
+ error ? styles.hasError : '',
23
+ ].filter(Boolean).join(' ');
24
+
25
+ const indicatorClassName = [
26
+ styles.statusIndicator,
27
+ styles[status],
28
+ ].join(' ');
29
+
30
+ return (
31
+ <div
32
+ className={badgeClassName}
33
+ title={error || undefined}
34
+ role="status"
35
+ aria-live="polite"
36
+ aria-label={`WebSocket connection status: ${statusText[status]}`}
37
+ >
38
+ <div className={indicatorClassName} aria-hidden="true" />
39
+ <span className={styles.statusText}>{statusText[status]}</span>
40
+ </div>
41
+ );
42
+ };
43
+
44
+ export const Header: React.FC = () => {
45
+ const status = useWSStore((state) => state.status);
46
+ const attempts = useWSStore((state) => state.reconnectAttempts);
47
+ const error = useWSStore((state) => state.lastError);
48
+
49
+ return <ConnectionStatusBadge status={status} attempts={attempts} error={error} />;
50
+ };
51
+
52
+ export default Header;