clawport-ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/.env.example +35 -0
  2. package/BRANDING.md +131 -0
  3. package/CLAUDE.md +252 -0
  4. package/README.md +262 -0
  5. package/SETUP.md +337 -0
  6. package/app/agents/[id]/page.tsx +727 -0
  7. package/app/api/agents/route.ts +12 -0
  8. package/app/api/chat/[id]/route.ts +139 -0
  9. package/app/api/cron-runs/route.ts +13 -0
  10. package/app/api/crons/route.ts +12 -0
  11. package/app/api/kanban/chat/[id]/route.ts +119 -0
  12. package/app/api/kanban/chat-history/[ticketId]/route.ts +36 -0
  13. package/app/api/memory/route.ts +12 -0
  14. package/app/api/transcribe/route.ts +37 -0
  15. package/app/api/tts/route.ts +42 -0
  16. package/app/chat/[id]/page.tsx +10 -0
  17. package/app/chat/page.tsx +200 -0
  18. package/app/crons/page.tsx +870 -0
  19. package/app/docs/page.tsx +399 -0
  20. package/app/favicon.ico +0 -0
  21. package/app/globals.css +692 -0
  22. package/app/kanban/page.tsx +327 -0
  23. package/app/layout.tsx +45 -0
  24. package/app/memory/page.tsx +685 -0
  25. package/app/page.tsx +817 -0
  26. package/app/providers.tsx +37 -0
  27. package/app/settings/page.tsx +901 -0
  28. package/app/settings-provider.tsx +209 -0
  29. package/components/AgentAvatar.tsx +54 -0
  30. package/components/AgentNode.tsx +122 -0
  31. package/components/Breadcrumbs.tsx +126 -0
  32. package/components/DynamicFavicon.tsx +62 -0
  33. package/components/ErrorState.tsx +97 -0
  34. package/components/FeedView.tsx +494 -0
  35. package/components/GlobalSearch.tsx +571 -0
  36. package/components/GridView.tsx +532 -0
  37. package/components/ManorMap.tsx +157 -0
  38. package/components/MobileSidebar.tsx +251 -0
  39. package/components/NavLinks.tsx +271 -0
  40. package/components/OnboardingWizard.tsx +1067 -0
  41. package/components/Sidebar.tsx +115 -0
  42. package/components/ThemeToggle.tsx +108 -0
  43. package/components/chat/AgentList.tsx +537 -0
  44. package/components/chat/ConversationView.tsx +1047 -0
  45. package/components/chat/FileAttachment.tsx +140 -0
  46. package/components/chat/MediaPreview.tsx +111 -0
  47. package/components/chat/VoiceMessage.tsx +139 -0
  48. package/components/crons/PipelineGraph.tsx +327 -0
  49. package/components/crons/WeeklySchedule.tsx +630 -0
  50. package/components/docs/AgentsSection.tsx +209 -0
  51. package/components/docs/ApiReferenceSection.tsx +256 -0
  52. package/components/docs/ArchitectureSection.tsx +221 -0
  53. package/components/docs/ComponentsSection.tsx +253 -0
  54. package/components/docs/CronSystemSection.tsx +235 -0
  55. package/components/docs/DocSection.tsx +346 -0
  56. package/components/docs/GettingStartedSection.tsx +169 -0
  57. package/components/docs/ThemingSection.tsx +257 -0
  58. package/components/docs/TroubleshootingSection.tsx +200 -0
  59. package/components/kanban/AgentPicker.tsx +321 -0
  60. package/components/kanban/CreateTicketModal.tsx +333 -0
  61. package/components/kanban/KanbanBoard.tsx +70 -0
  62. package/components/kanban/KanbanColumn.tsx +166 -0
  63. package/components/kanban/TicketCard.tsx +245 -0
  64. package/components/kanban/TicketDetailPanel.tsx +850 -0
  65. package/components/ui/badge.tsx +48 -0
  66. package/components/ui/button.tsx +64 -0
  67. package/components/ui/card.tsx +92 -0
  68. package/components/ui/dialog.tsx +158 -0
  69. package/components/ui/scroll-area.tsx +58 -0
  70. package/components/ui/separator.tsx +28 -0
  71. package/components/ui/skeleton.tsx +27 -0
  72. package/components/ui/tabs.tsx +91 -0
  73. package/components/ui/tooltip.tsx +57 -0
  74. package/components.json +23 -0
  75. package/docs/API.md +648 -0
  76. package/docs/COMPONENTS.md +1059 -0
  77. package/docs/THEMING.md +795 -0
  78. package/lib/agents-registry.ts +35 -0
  79. package/lib/agents.json +282 -0
  80. package/lib/agents.test.ts +367 -0
  81. package/lib/agents.ts +32 -0
  82. package/lib/anthropic.test.ts +422 -0
  83. package/lib/anthropic.ts +220 -0
  84. package/lib/api-error.ts +16 -0
  85. package/lib/audio-recorder.test.ts +72 -0
  86. package/lib/audio-recorder.ts +169 -0
  87. package/lib/conversations.test.ts +331 -0
  88. package/lib/conversations.ts +117 -0
  89. package/lib/cron-pipelines.test.ts +69 -0
  90. package/lib/cron-pipelines.ts +58 -0
  91. package/lib/cron-runs.test.ts +118 -0
  92. package/lib/cron-runs.ts +67 -0
  93. package/lib/cron-utils.test.ts +222 -0
  94. package/lib/cron-utils.ts +160 -0
  95. package/lib/crons.test.ts +502 -0
  96. package/lib/crons.ts +114 -0
  97. package/lib/env.test.ts +44 -0
  98. package/lib/env.ts +14 -0
  99. package/lib/kanban/automation.test.ts +245 -0
  100. package/lib/kanban/automation.ts +143 -0
  101. package/lib/kanban/chat-store.test.ts +149 -0
  102. package/lib/kanban/chat-store.ts +81 -0
  103. package/lib/kanban/store.test.ts +238 -0
  104. package/lib/kanban/store.ts +98 -0
  105. package/lib/kanban/types.ts +50 -0
  106. package/lib/kanban/useAgentWork.ts +78 -0
  107. package/lib/memory.ts +45 -0
  108. package/lib/multimodal.test.ts +219 -0
  109. package/lib/multimodal.ts +68 -0
  110. package/lib/pipeline.integration.test.ts +343 -0
  111. package/lib/sanitize.ts +194 -0
  112. package/lib/settings.test.ts +137 -0
  113. package/lib/settings.ts +94 -0
  114. package/lib/styles.ts +24 -0
  115. package/lib/themes.ts +9 -0
  116. package/lib/transcribe.test.ts +141 -0
  117. package/lib/transcribe.ts +111 -0
  118. package/lib/types.ts +66 -0
  119. package/lib/utils.ts +6 -0
  120. package/lib/validation.test.ts +132 -0
  121. package/lib/validation.ts +80 -0
  122. package/next.config.ts +7 -0
  123. package/package.json +56 -0
  124. package/postcss.config.mjs +7 -0
  125. package/public/file.svg +1 -0
  126. package/public/globe.svg +1 -0
  127. package/public/next.svg +1 -0
  128. package/public/vercel.svg +1 -0
  129. package/public/window.svg +1 -0
  130. package/scripts/setup.mjs +215 -0
  131. package/tsconfig.json +34 -0
  132. package/vitest.config.ts +17 -0
@@ -0,0 +1,251 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback } from 'react';
4
+ import { usePathname } from 'next/navigation';
5
+ import { Menu, X } from 'lucide-react';
6
+ import { NavLinks } from '@/components/NavLinks';
7
+ import { ThemeToggle } from '@/components/ThemeToggle';
8
+ import { SearchTrigger } from '@/components/GlobalSearch';
9
+ import { useSettings } from '@/app/settings-provider';
10
+
11
+ export function MobileSidebar({
12
+ onOpenSearch,
13
+ }: {
14
+ onOpenSearch?: () => void;
15
+ }) {
16
+ const [open, setOpen] = useState(false);
17
+ const pathname = usePathname();
18
+ const { settings } = useSettings();
19
+
20
+ // Close sidebar on route change
21
+ useEffect(() => {
22
+ setOpen(false);
23
+ }, [pathname]);
24
+
25
+ // Close on ESC
26
+ useEffect(() => {
27
+ if (!open) return;
28
+ function handleKeyDown(e: KeyboardEvent) {
29
+ if (e.key === 'Escape') setOpen(false);
30
+ }
31
+ window.addEventListener('keydown', handleKeyDown);
32
+ return () => window.removeEventListener('keydown', handleKeyDown);
33
+ }, [open]);
34
+
35
+ // Prevent body scroll when open
36
+ useEffect(() => {
37
+ if (open) {
38
+ document.body.style.overflow = 'hidden';
39
+ } else {
40
+ document.body.style.overflow = '';
41
+ }
42
+ return () => {
43
+ document.body.style.overflow = '';
44
+ };
45
+ }, [open]);
46
+
47
+ const toggle = useCallback(() => setOpen((prev) => !prev), []);
48
+
49
+ const handleSearchClick = useCallback(() => {
50
+ setOpen(false);
51
+ onOpenSearch?.();
52
+ }, [onOpenSearch]);
53
+
54
+ return (
55
+ <>
56
+ {/* Mobile header bar */}
57
+ <header
58
+ className="flex md:hidden items-center"
59
+ style={{
60
+ position: 'fixed',
61
+ top: 0,
62
+ left: 0,
63
+ right: 0,
64
+ zIndex: 60,
65
+ height: '48px',
66
+ gap: '12px',
67
+ padding: '0 12px',
68
+ background: 'var(--sidebar-bg)',
69
+ backdropFilter: 'blur(40px) saturate(180%)',
70
+ WebkitBackdropFilter: 'blur(40px) saturate(180%)',
71
+ borderBottom: '1px solid var(--separator)',
72
+ }}
73
+ >
74
+ {/* Hamburger / close toggle */}
75
+ <button
76
+ onClick={toggle}
77
+ className="btn-ghost focus-ring"
78
+ aria-label={open ? 'Close navigation menu' : 'Open navigation menu'}
79
+ aria-expanded={open}
80
+ style={{
81
+ width: '36px',
82
+ height: '36px',
83
+ borderRadius: '8px',
84
+ display: 'flex',
85
+ alignItems: 'center',
86
+ justifyContent: 'center',
87
+ background: 'transparent',
88
+ border: 'none',
89
+ cursor: 'pointer',
90
+ color: 'var(--text-secondary)',
91
+ transition: 'color 100ms var(--ease-smooth)',
92
+ }}
93
+ >
94
+ {open ? <X size={20} /> : <Menu size={20} />}
95
+ </button>
96
+
97
+ {/* App title */}
98
+ <div className="flex items-center gap-2" style={{ flex: 1 }}>
99
+ {settings.portalIcon ? (
100
+ <img
101
+ src={settings.portalIcon}
102
+ alt=""
103
+ style={{
104
+ width: '24px',
105
+ height: '24px',
106
+ borderRadius: '6px',
107
+ objectFit: 'cover',
108
+ flexShrink: 0,
109
+ }}
110
+ />
111
+ ) : (
112
+ <span
113
+ style={{
114
+ width: '24px',
115
+ height: '24px',
116
+ borderRadius: '6px',
117
+ background: settings.accentColor
118
+ ? `linear-gradient(135deg, ${settings.accentColor}, ${settings.accentColor}dd)`
119
+ : 'linear-gradient(135deg, #f5c518, #e8b800)',
120
+ display: 'flex',
121
+ alignItems: 'center',
122
+ justifyContent: 'center',
123
+ fontSize: '13px',
124
+ flexShrink: 0,
125
+ }}
126
+ >
127
+ {settings.portalEmoji ?? '\ud83e\udd9e'}
128
+ </span>
129
+ )}
130
+ <span
131
+ style={{
132
+ fontSize: '15px',
133
+ fontWeight: 600,
134
+ color: 'var(--text-primary)',
135
+ letterSpacing: '-0.2px',
136
+ }}
137
+ >
138
+ {settings.portalName ?? 'ClawPort'} {settings.portalSubtitle ?? 'Command Centre'}
139
+ </span>
140
+ </div>
141
+ </header>
142
+
143
+ {/* Backdrop */}
144
+ <div
145
+ className="block md:hidden"
146
+ style={{
147
+ position: 'fixed',
148
+ inset: 0,
149
+ zIndex: 50,
150
+ background: 'rgba(0,0,0,0.5)',
151
+ backdropFilter: 'blur(2px)',
152
+ WebkitBackdropFilter: 'blur(2px)',
153
+ opacity: open ? 1 : 0,
154
+ pointerEvents: open ? 'auto' : 'none',
155
+ transition: 'opacity 200ms var(--ease-smooth)',
156
+ }}
157
+ onClick={() => setOpen(false)}
158
+ aria-hidden="true"
159
+ />
160
+
161
+ {/* Slide-out sidebar panel */}
162
+ <aside
163
+ className="flex md:hidden flex-col"
164
+ style={{
165
+ position: 'fixed',
166
+ top: 0,
167
+ left: 0,
168
+ bottom: 0,
169
+ zIndex: 55,
170
+ width: '280px',
171
+ background: 'var(--sidebar-bg)',
172
+ backdropFilter: 'var(--sidebar-backdrop)',
173
+ WebkitBackdropFilter: 'var(--sidebar-backdrop)',
174
+ borderRight: '1px solid var(--separator)',
175
+ transform: open ? 'translateX(0)' : 'translateX(-100%)',
176
+ transition: 'transform 300ms cubic-bezier(0.32, 0.72, 0, 1)',
177
+ boxShadow: open ? 'var(--shadow-overlay)' : 'none',
178
+ }}
179
+ aria-hidden={!open}
180
+ >
181
+ {/* App icon + title */}
182
+ <div className="px-4 pt-5 pb-3">
183
+ <div className="flex items-center gap-3">
184
+ {settings.portalIcon ? (
185
+ <img
186
+ src={settings.portalIcon}
187
+ alt=""
188
+ style={{
189
+ width: '36px',
190
+ height: '36px',
191
+ borderRadius: '10px',
192
+ objectFit: 'cover',
193
+ boxShadow: 'var(--shadow-card)',
194
+ flexShrink: 0,
195
+ }}
196
+ />
197
+ ) : (
198
+ <div
199
+ style={{
200
+ width: '36px',
201
+ height: '36px',
202
+ borderRadius: '10px',
203
+ background: settings.accentColor
204
+ ? `linear-gradient(135deg, ${settings.accentColor}, ${settings.accentColor}dd)`
205
+ : 'linear-gradient(135deg, #f5c518, #e8b800)',
206
+ boxShadow: 'var(--shadow-card)',
207
+ display: 'flex',
208
+ alignItems: 'center',
209
+ justifyContent: 'center',
210
+ fontSize: '18px',
211
+ flexShrink: 0,
212
+ }}
213
+ >
214
+ {settings.portalEmoji ?? '\ud83e\udd9e'}
215
+ </div>
216
+ )}
217
+ <div>
218
+ <div
219
+ style={{
220
+ fontSize: '17px',
221
+ fontWeight: 600,
222
+ letterSpacing: '-0.3px',
223
+ color: 'var(--text-primary)',
224
+ }}
225
+ >
226
+ {settings.portalName ?? 'ClawPort'}
227
+ </div>
228
+ <div
229
+ style={{
230
+ fontSize: '12px',
231
+ color: 'var(--text-secondary)',
232
+ letterSpacing: '0.01em',
233
+ }}
234
+ >
235
+ {settings.portalSubtitle ?? 'Command Centre'}
236
+ </div>
237
+ </div>
238
+ </div>
239
+ </div>
240
+
241
+ {/* Search trigger */}
242
+ <div className="px-3 pb-2">
243
+ <SearchTrigger onClick={handleSearchClick} />
244
+ </div>
245
+
246
+ <NavLinks />
247
+ <ThemeToggle />
248
+ </aside>
249
+ </>
250
+ );
251
+ }
@@ -0,0 +1,271 @@
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import { usePathname } from 'next/navigation';
5
+ import { useEffect, useState } from 'react';
6
+ import { Map, MessageSquare, Clock, Brain, Columns3, BookOpen, Settings } from 'lucide-react';
7
+ import type { LucideIcon } from 'lucide-react';
8
+ import type { CronJob } from '@/lib/types';
9
+ import { useSettings } from '@/app/settings-provider';
10
+
11
+ function getInitials(name: string | null): string {
12
+ if (!name) return '??'
13
+ const parts = name.trim().split(/\s+/)
14
+ if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase()
15
+ return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase()
16
+ }
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Nav item definition
20
+ // ---------------------------------------------------------------------------
21
+
22
+ interface NavItem {
23
+ href: string;
24
+ label: string;
25
+ icon: LucideIcon;
26
+ badge?: 'agents' | 'unread' | 'errors';
27
+ }
28
+
29
+ const NAV_ITEMS: NavItem[] = [
30
+ { href: '/', label: 'Map', icon: Map, badge: 'agents' },
31
+ { href: '/kanban', label: 'Kanban', icon: Columns3 },
32
+ { href: '/chat', label: 'Messages', icon: MessageSquare, badge: 'unread' },
33
+ { href: '/crons', label: 'Crons', icon: Clock, badge: 'errors' },
34
+ { href: '/memory', label: 'Memory', icon: Brain },
35
+ { href: '/docs', label: 'Docs', icon: BookOpen },
36
+ { href: '/settings', label: 'Settings', icon: Settings },
37
+ ];
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // NavLinks component
41
+ // ---------------------------------------------------------------------------
42
+
43
+ export function NavLinks() {
44
+ const pathname = usePathname();
45
+ const { settings } = useSettings();
46
+ const [agentCount, setAgentCount] = useState<number | null>(null);
47
+ const [cronCount, setCronCount] = useState<number | null>(null);
48
+ const [cronErrorCount, setCronErrorCount] = useState<number | null>(null);
49
+
50
+ // Fetch agent count
51
+ useEffect(() => {
52
+ fetch('/api/agents')
53
+ .then((r) => {
54
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
55
+ return r.json();
56
+ })
57
+ .then((data: unknown) => {
58
+ if (Array.isArray(data)) {
59
+ setAgentCount(data.length);
60
+ }
61
+ })
62
+ .catch(() => {
63
+ setAgentCount(null);
64
+ });
65
+ }, []);
66
+
67
+ // Fetch cron error count
68
+ useEffect(() => {
69
+ fetch('/api/crons')
70
+ .then((r) => {
71
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
72
+ return r.json();
73
+ })
74
+ .then((data: unknown) => {
75
+ if (Array.isArray(data)) {
76
+ const crons = data as CronJob[];
77
+ setCronCount(crons.length);
78
+ setCronErrorCount(crons.filter((c) => c.status === 'error').length);
79
+ }
80
+ })
81
+ .catch(() => {
82
+ setCronErrorCount(null);
83
+ });
84
+ }, []);
85
+
86
+ // Resolve badge content per nav item
87
+ function getBadge(item: NavItem): React.ReactNode {
88
+ if (item.badge === 'agents' && agentCount !== null) {
89
+ return (
90
+ <span
91
+ className="nav-badge"
92
+ style={{
93
+ marginLeft: 'auto',
94
+ fontSize: '10px',
95
+ fontFamily: 'var(--font-mono)',
96
+ padding: '1px 6px',
97
+ borderRadius: 'var(--radius-sm)',
98
+ background: 'var(--fill-quaternary)',
99
+ color: 'var(--text-tertiary)',
100
+ lineHeight: '16px',
101
+ }}
102
+ >
103
+ {agentCount}
104
+ </span>
105
+ );
106
+ }
107
+ if (item.badge === 'errors' && cronCount !== null) {
108
+ const hasErrors = cronErrorCount !== null && cronErrorCount > 0;
109
+ return (
110
+ <span
111
+ style={{
112
+ marginLeft: 'auto',
113
+ display: 'flex',
114
+ alignItems: 'center',
115
+ gap: '6px',
116
+ }}
117
+ >
118
+ <span
119
+ className="nav-badge"
120
+ style={{
121
+ fontSize: '10px',
122
+ fontFamily: 'var(--font-mono)',
123
+ padding: '1px 6px',
124
+ borderRadius: 'var(--radius-sm)',
125
+ background: hasErrors ? 'rgba(255,69,58,0.1)' : 'var(--fill-quaternary)',
126
+ color: hasErrors ? 'var(--system-red)' : 'var(--text-tertiary)',
127
+ lineHeight: '16px',
128
+ fontWeight: hasErrors ? 600 : undefined,
129
+ }}
130
+ >
131
+ {hasErrors ? `${cronErrorCount} err` : cronCount}
132
+ </span>
133
+ {hasErrors && (
134
+ <span
135
+ aria-label={`${cronErrorCount} cron error${cronErrorCount > 1 ? 's' : ''}`}
136
+ style={{
137
+ width: '6px',
138
+ height: '6px',
139
+ borderRadius: '50%',
140
+ background: 'var(--system-red)',
141
+ flexShrink: 0,
142
+ animation: 'pulse-red 1.5s ease-in-out infinite',
143
+ }}
144
+ />
145
+ )}
146
+ </span>
147
+ );
148
+ }
149
+ return null;
150
+ }
151
+
152
+ return (
153
+ <nav className="flex-1 flex flex-col" aria-label="Main navigation">
154
+ <div className="px-3 pt-2 pb-3">
155
+ {/* Section header */}
156
+ <div
157
+ style={{
158
+ fontSize: '11px',
159
+ fontWeight: 600,
160
+ letterSpacing: '0.06em',
161
+ color: 'var(--text-tertiary)',
162
+ textTransform: 'uppercase',
163
+ padding: '0 8px',
164
+ marginBottom: '4px',
165
+ }}
166
+ >
167
+ Workspace
168
+ </div>
169
+
170
+ <div className="flex flex-col gap-0.5">
171
+ {NAV_ITEMS.map((item) => {
172
+ const isActive =
173
+ item.href === '/'
174
+ ? pathname === '/'
175
+ : pathname.startsWith(item.href);
176
+
177
+ const Icon = item.icon;
178
+
179
+ return (
180
+ <Link
181
+ key={item.href}
182
+ href={item.href}
183
+ className={`nav-item focus-ring ${isActive ? 'nav-item-active' : ''}`}
184
+ aria-label={item.label}
185
+ aria-current={isActive ? 'page' : undefined}
186
+ style={{
187
+ display: 'flex',
188
+ alignItems: 'center',
189
+ gap: '10px',
190
+ minHeight: '44px',
191
+ padding: '0 10px 0 12px',
192
+ borderRadius: '8px',
193
+ fontSize: '13px',
194
+ fontWeight: isActive ? 600 : 500,
195
+ color: isActive ? 'var(--accent)' : 'var(--text-secondary)',
196
+ background: isActive ? 'var(--accent-fill)' : 'transparent',
197
+ textDecoration: 'none',
198
+ transition: 'all 100ms var(--ease-smooth)',
199
+ }}
200
+ >
201
+ <Icon
202
+ size={18}
203
+ style={{
204
+ flexShrink: 0,
205
+ color: isActive ? 'var(--accent)' : 'var(--text-tertiary)',
206
+ transition: 'color 100ms var(--ease-smooth)',
207
+ }}
208
+ />
209
+ <span style={{ flex: 1 }}>{item.label}</span>
210
+ {getBadge(item)}
211
+ </Link>
212
+ );
213
+ })}
214
+ </div>
215
+ </div>
216
+
217
+ <div className="flex-1" />
218
+
219
+ {/* User footer */}
220
+ <div
221
+ style={{
222
+ borderTop: '1px solid var(--separator)',
223
+ padding: '10px 16px',
224
+ }}
225
+ >
226
+ <div className="flex items-center gap-2.5">
227
+ <div
228
+ style={{
229
+ width: '28px',
230
+ height: '28px',
231
+ borderRadius: '7px',
232
+ background: 'var(--accent-fill)',
233
+ display: 'flex',
234
+ alignItems: 'center',
235
+ justifyContent: 'center',
236
+ fontSize: '11px',
237
+ fontWeight: 700,
238
+ color: 'var(--accent)',
239
+ flexShrink: 0,
240
+ letterSpacing: '-0.02em',
241
+ }}
242
+ >
243
+ {getInitials(settings.operatorName)}
244
+ </div>
245
+ <div style={{ minWidth: 0 }}>
246
+ <div
247
+ style={{
248
+ fontSize: '13px',
249
+ fontWeight: 500,
250
+ color: 'var(--text-primary)',
251
+ overflow: 'hidden',
252
+ textOverflow: 'ellipsis',
253
+ whiteSpace: 'nowrap',
254
+ }}
255
+ >
256
+ {settings.operatorName ?? 'Operator'}
257
+ </div>
258
+ <div
259
+ style={{
260
+ fontSize: '11px',
261
+ color: 'var(--text-tertiary)',
262
+ }}
263
+ >
264
+ Owner
265
+ </div>
266
+ </div>
267
+ </div>
268
+ </div>
269
+ </nav>
270
+ );
271
+ }