@crmy/web 0.5.5 → 0.5.9

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 (121) hide show
  1. package/dist/assets/index-CskfWp8E.js +560 -0
  2. package/dist/assets/index-D763l57m.css +1 -0
  3. package/{index.html → dist/index.html} +2 -1
  4. package/package.json +4 -1
  5. package/postcss.config.js +0 -6
  6. package/src/App.tsx +0 -158
  7. package/src/api/client.ts +0 -82
  8. package/src/api/hooks.ts +0 -689
  9. package/src/components/CustomFields.tsx +0 -240
  10. package/src/components/NavLink.tsx +0 -28
  11. package/src/components/crm/AIFab.tsx +0 -37
  12. package/src/components/crm/AccountDrawer.tsx +0 -372
  13. package/src/components/crm/ActivityTimeline.tsx +0 -115
  14. package/src/components/crm/AssignmentDrawer.tsx +0 -396
  15. package/src/components/crm/BriefingPanel.tsx +0 -217
  16. package/src/components/crm/CommandPalette.tsx +0 -254
  17. package/src/components/crm/ContactAvatar.tsx +0 -49
  18. package/src/components/crm/ContactDrawer.tsx +0 -438
  19. package/src/components/crm/ContextPanel.tsx +0 -200
  20. package/src/components/crm/CrmWidgets.tsx +0 -417
  21. package/src/components/crm/DrawerShell.tsx +0 -77
  22. package/src/components/crm/ListToolbar.tsx +0 -252
  23. package/src/components/crm/OpportunityDrawer.tsx +0 -372
  24. package/src/components/crm/PaginationBar.tsx +0 -111
  25. package/src/components/crm/QuickAddDrawer.tsx +0 -652
  26. package/src/components/crm/ShortcutsOverlay.tsx +0 -65
  27. package/src/components/crm/UseCaseDrawer.tsx +0 -454
  28. package/src/components/layout/MobileNav.tsx +0 -49
  29. package/src/components/layout/Sidebar.tsx +0 -157
  30. package/src/components/layout/TopBar.tsx +0 -54
  31. package/src/components/settings/ActorsSettings.tsx +0 -1190
  32. package/src/components/ui/accordion.tsx +0 -52
  33. package/src/components/ui/alert-dialog.tsx +0 -104
  34. package/src/components/ui/alert.tsx +0 -43
  35. package/src/components/ui/aspect-ratio.tsx +0 -5
  36. package/src/components/ui/avatar.tsx +0 -38
  37. package/src/components/ui/badge.tsx +0 -29
  38. package/src/components/ui/breadcrumb.tsx +0 -90
  39. package/src/components/ui/button.tsx +0 -47
  40. package/src/components/ui/calendar.tsx +0 -54
  41. package/src/components/ui/card.tsx +0 -43
  42. package/src/components/ui/carousel.tsx +0 -224
  43. package/src/components/ui/chart.tsx +0 -303
  44. package/src/components/ui/checkbox.tsx +0 -26
  45. package/src/components/ui/collapsible.tsx +0 -9
  46. package/src/components/ui/command.tsx +0 -132
  47. package/src/components/ui/context-menu.tsx +0 -178
  48. package/src/components/ui/date-picker.tsx +0 -313
  49. package/src/components/ui/dialog.tsx +0 -95
  50. package/src/components/ui/drawer.tsx +0 -87
  51. package/src/components/ui/dropdown-menu.tsx +0 -179
  52. package/src/components/ui/form.tsx +0 -129
  53. package/src/components/ui/hover-card.tsx +0 -27
  54. package/src/components/ui/input-otp.tsx +0 -61
  55. package/src/components/ui/input.tsx +0 -22
  56. package/src/components/ui/label.tsx +0 -17
  57. package/src/components/ui/menubar.tsx +0 -207
  58. package/src/components/ui/navigation-menu.tsx +0 -120
  59. package/src/components/ui/pagination.tsx +0 -81
  60. package/src/components/ui/popover.tsx +0 -29
  61. package/src/components/ui/progress.tsx +0 -23
  62. package/src/components/ui/radio-group.tsx +0 -36
  63. package/src/components/ui/resizable.tsx +0 -37
  64. package/src/components/ui/scroll-area.tsx +0 -38
  65. package/src/components/ui/select.tsx +0 -143
  66. package/src/components/ui/separator.tsx +0 -20
  67. package/src/components/ui/sheet.tsx +0 -107
  68. package/src/components/ui/sidebar.tsx +0 -637
  69. package/src/components/ui/skeleton.tsx +0 -7
  70. package/src/components/ui/slider.tsx +0 -23
  71. package/src/components/ui/sonner.tsx +0 -24
  72. package/src/components/ui/switch.tsx +0 -27
  73. package/src/components/ui/table.tsx +0 -72
  74. package/src/components/ui/tabs.tsx +0 -53
  75. package/src/components/ui/textarea.tsx +0 -21
  76. package/src/components/ui/toast.tsx +0 -111
  77. package/src/components/ui/toaster.tsx +0 -24
  78. package/src/components/ui/toggle-group.tsx +0 -49
  79. package/src/components/ui/toggle.tsx +0 -37
  80. package/src/components/ui/tooltip.tsx +0 -28
  81. package/src/components/ui/use-toast.ts +0 -1
  82. package/src/components/ui/utils.ts +0 -9
  83. package/src/contexts/AgentSettingsContext.tsx +0 -24
  84. package/src/hooks/use-mobile.tsx +0 -19
  85. package/src/hooks/use-toast.ts +0 -186
  86. package/src/hooks/useKeyboardShortcuts.ts +0 -95
  87. package/src/hooks/useTheme.ts +0 -24
  88. package/src/index.css +0 -245
  89. package/src/lib/entityColors.ts +0 -18
  90. package/src/lib/stageConfig.ts +0 -32
  91. package/src/lib/utils.ts +0 -6
  92. package/src/main.tsx +0 -25
  93. package/src/pages/Accounts.tsx +0 -205
  94. package/src/pages/Activities.tsx +0 -251
  95. package/src/pages/Agent.tsx +0 -237
  96. package/src/pages/AgentSettings.tsx +0 -544
  97. package/src/pages/Assignments.tsx +0 -750
  98. package/src/pages/Contacts.tsx +0 -200
  99. package/src/pages/Dashboard.tsx +0 -143
  100. package/src/pages/Inbox.tsx +0 -615
  101. package/src/pages/NotFound.tsx +0 -24
  102. package/src/pages/Opportunities.tsx +0 -386
  103. package/src/pages/SearchResults.tsx +0 -49
  104. package/src/pages/Settings.tsx +0 -1884
  105. package/src/pages/UseCases.tsx +0 -396
  106. package/src/pages/auth/Login.tsx +0 -261
  107. package/src/pages/hitl/HITL.tsx +0 -101
  108. package/src/store/appStore.ts +0 -103
  109. package/src/vite-env.d.ts +0 -14
  110. package/tailwind.config.js +0 -121
  111. package/tsconfig.json +0 -24
  112. package/vite.config.ts +0 -27
  113. /package/{public → dist}/android-chrome-192x192.png +0 -0
  114. /package/{public → dist}/android-chrome-512x512.png +0 -0
  115. /package/{public → dist}/apple-touch-icon.png +0 -0
  116. /package/{src/assets/crmy-logo.png → dist/assets/crmy-logo-DWN0xBPW.png} +0 -0
  117. /package/{public → dist}/favicon-16x16.png +0 -0
  118. /package/{public → dist}/favicon-32x32.png +0 -0
  119. /package/{public → dist}/favicon.ico +0 -0
  120. /package/{public → dist}/favicon.svg +0 -0
  121. /package/{public → dist}/site.webmanifest +0 -0
@@ -1,217 +0,0 @@
1
- // Copyright 2026 CRMy Contributors
2
- // SPDX-License-Identifier: Apache-2.0
3
-
4
- import { useState } from 'react';
5
- import { useBriefing } from '@/api/hooks';
6
- import { FileText, ChevronDown, ChevronUp, AlertTriangle, Activity, ClipboardList, Brain, X } from 'lucide-react';
7
-
8
- interface BriefingPanelProps {
9
- subjectType: string;
10
- subjectId: string;
11
- onClose: () => void;
12
- }
13
-
14
- export function BriefingPanel({ subjectType, subjectId, onClose }: BriefingPanelProps) {
15
- const [includeStale, setIncludeStale] = useState(true);
16
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
- const { data, isLoading, error } = useBriefing(subjectType, subjectId, { format: 'json', include_stale: includeStale }) as any;
18
-
19
- if (isLoading) {
20
- return (
21
- <div className="flex flex-col h-full">
22
- <BriefingHeader onClose={onClose} />
23
- <div className="flex-1 p-5 space-y-4 animate-pulse">
24
- <div className="h-6 bg-muted rounded w-1/2" />
25
- <div className="h-20 bg-muted rounded" />
26
- <div className="h-20 bg-muted rounded" />
27
- <div className="h-20 bg-muted rounded" />
28
- </div>
29
- </div>
30
- );
31
- }
32
-
33
- if (error) {
34
- return (
35
- <div className="flex flex-col h-full">
36
- <BriefingHeader onClose={onClose} />
37
- <div className="p-5 text-sm text-destructive">Failed to load briefing.</div>
38
- </div>
39
- );
40
- }
41
-
42
- const briefing = data?.briefing ?? data;
43
-
44
- return (
45
- <div className="flex flex-col h-full">
46
- <BriefingHeader onClose={onClose} />
47
- <div className="flex-1 overflow-y-auto p-5 space-y-4">
48
- {/* Staleness warnings */}
49
- {briefing?.staleness_warnings?.length > 0 && (
50
- <BriefingSection
51
- icon={<AlertTriangle className="w-3.5 h-3.5 text-warning" />}
52
- title="Stale Context"
53
- defaultOpen
54
- >
55
- <div className="space-y-2">
56
- {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
57
- {briefing.staleness_warnings.map((w: any) => (
58
- <div key={w.id} className="flex items-start gap-2 text-xs">
59
- <AlertTriangle className="w-3 h-3 text-warning flex-shrink-0 mt-0.5" />
60
- <div>
61
- <span className="font-medium text-foreground">{w.title ?? w.context_type}</span>
62
- <span className="text-muted-foreground ml-1.5">
63
- expired {w.valid_until ? new Date(w.valid_until).toLocaleDateString() : ''}
64
- </span>
65
- </div>
66
- </div>
67
- ))}
68
- </div>
69
- </BriefingSection>
70
- )}
71
-
72
- {/* Activities */}
73
- {briefing?.activities?.length > 0 && (
74
- <BriefingSection
75
- icon={<Activity className="w-3.5 h-3.5 text-primary" />}
76
- title={`Recent Activities (${briefing.activities.length})`}
77
- defaultOpen
78
- >
79
- <div className="space-y-1.5">
80
- {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
81
- {briefing.activities.slice(0, 10).map((a: any) => (
82
- <div key={a.id} className="flex items-start gap-2 text-xs">
83
- <span className="w-16 flex-shrink-0 text-muted-foreground">
84
- {new Date(a.occurred_at ?? a.created_at).toLocaleDateString()}
85
- </span>
86
- <span className="px-1.5 py-0.5 rounded bg-muted text-[10px] font-medium capitalize flex-shrink-0">
87
- {(a.type ?? a.activity_type ?? '').replace(/_/g, ' ')}
88
- </span>
89
- <span className="text-foreground truncate">{a.description ?? a.body ?? ''}</span>
90
- </div>
91
- ))}
92
- </div>
93
- </BriefingSection>
94
- )}
95
-
96
- {/* Open Assignments */}
97
- {briefing?.open_assignments?.length > 0 && (
98
- <BriefingSection
99
- icon={<ClipboardList className="w-3.5 h-3.5 text-primary" />}
100
- title={`Open Assignments (${briefing.open_assignments.length})`}
101
- defaultOpen
102
- >
103
- <div className="space-y-1.5">
104
- {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
105
- {briefing.open_assignments.map((a: any) => (
106
- <div key={a.id} className="flex items-center gap-2 text-xs">
107
- <StatusBadge status={a.status} />
108
- <span className="text-foreground flex-1 truncate">{a.title}</span>
109
- <PriorityDot priority={a.priority} />
110
- </div>
111
- ))}
112
- </div>
113
- </BriefingSection>
114
- )}
115
-
116
- {/* Context Entries (grouped by type) */}
117
- {briefing?.context_entries && Object.keys(briefing.context_entries).length > 0 && (
118
- <BriefingSection
119
- icon={<Brain className="w-3.5 h-3.5 text-primary" />}
120
- title="Context"
121
- defaultOpen
122
- >
123
- <div className="space-y-3">
124
- {Object.entries(briefing.context_entries).map(([type, entries]) => (
125
- <div key={type}>
126
- <p className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wide mb-1">
127
- {type.replace(/_/g, ' ')}
128
- </p>
129
- <div className="space-y-1.5">
130
- {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
131
- {(entries as any[]).map((c: any) => (
132
- <div key={c.id} className="rounded-lg bg-muted/50 p-2">
133
- {c.title && <p className="text-xs font-medium text-foreground">{c.title}</p>}
134
- <p className="text-[11px] text-muted-foreground line-clamp-2">{c.body}</p>
135
- </div>
136
- ))}
137
- </div>
138
- </div>
139
- ))}
140
- </div>
141
- </BriefingSection>
142
- )}
143
-
144
- {/* Toggle stale */}
145
- <div className="flex items-center gap-2 pt-2 border-t border-border">
146
- <input
147
- type="checkbox"
148
- checked={includeStale}
149
- onChange={e => setIncludeStale(e.target.checked)}
150
- className="w-3.5 h-3.5 rounded border-border accent-primary"
151
- />
152
- <span className="text-xs text-muted-foreground">Include stale context</span>
153
- </div>
154
- </div>
155
- </div>
156
- );
157
- }
158
-
159
- function BriefingHeader({ onClose }: { onClose: () => void }) {
160
- return (
161
- <div className="flex items-center gap-2 px-5 py-3 border-b border-border">
162
- <FileText className="w-4 h-4 text-primary" />
163
- <h2 className="font-display font-bold text-foreground flex-1">Briefing</h2>
164
- <button onClick={onClose} className="p-1 rounded-lg hover:bg-muted transition-colors">
165
- <X className="w-4 h-4 text-muted-foreground" />
166
- </button>
167
- </div>
168
- );
169
- }
170
-
171
- function BriefingSection({ icon, title, children, defaultOpen = false }: {
172
- icon: React.ReactNode;
173
- title: string;
174
- children: React.ReactNode;
175
- defaultOpen?: boolean;
176
- }) {
177
- const [open, setOpen] = useState(defaultOpen);
178
- return (
179
- <div className="rounded-xl border border-border">
180
- <button
181
- onClick={() => setOpen(!open)}
182
- className="flex items-center gap-2 w-full p-3 text-left"
183
- >
184
- {icon}
185
- <span className="text-xs font-display font-bold text-foreground flex-1">{title}</span>
186
- {open ? <ChevronUp className="w-3.5 h-3.5 text-muted-foreground" /> : <ChevronDown className="w-3.5 h-3.5 text-muted-foreground" />}
187
- </button>
188
- {open && <div className="px-3 pb-3">{children}</div>}
189
- </div>
190
- );
191
- }
192
-
193
- function StatusBadge({ status }: { status: string }) {
194
- const colors: Record<string, string> = {
195
- pending: '#f59e0b',
196
- accepted: '#3b82f6',
197
- in_progress: '#8b5cf6',
198
- blocked: '#ef4444',
199
- completed: '#22c55e',
200
- declined: '#94a3b8',
201
- cancelled: '#94a3b8',
202
- };
203
- const color = colors[status] ?? '#94a3b8';
204
- return (
205
- <span
206
- className="px-1.5 py-0.5 rounded text-[10px] font-medium capitalize flex-shrink-0"
207
- style={{ backgroundColor: color + '18', color }}
208
- >
209
- {status.replace(/_/g, ' ')}
210
- </span>
211
- );
212
- }
213
-
214
- function PriorityDot({ priority }: { priority: string }) {
215
- const colors: Record<string, string> = { urgent: '#ef4444', high: '#f97316', normal: '#3b82f6', low: '#94a3b8' };
216
- return <div className="w-2 h-2 rounded-full flex-shrink-0" style={{ backgroundColor: colors[priority] ?? '#94a3b8' }} title={priority} />;
217
- }
@@ -1,254 +0,0 @@
1
- // Copyright 2026 CRMy Contributors
2
- // SPDX-License-Identifier: Apache-2.0
3
-
4
- import { useEffect, useRef, useState } from 'react';
5
- import { useNavigate } from 'react-router-dom';
6
- import { Command } from 'cmdk';
7
- import { motion, AnimatePresence } from 'framer-motion';
8
- import { Users, Briefcase, LayoutDashboard, FolderKanban, Activity, Settings, Search, Building2, ClipboardList } from 'lucide-react';
9
- import { useAppStore } from '@/store/appStore';
10
- import { cn } from '@/lib/utils';
11
- import { ContactAvatar } from './ContactAvatar';
12
- import { useSearch } from '@/api/hooks';
13
- import { useIsMobile } from '@/hooks/use-mobile';
14
- import { ENTITY_COLORS } from '@/lib/entityColors';
15
-
16
- export function CommandPalette() {
17
- const { commandPaletteOpen, setCommandPaletteOpen, openDrawer } = useAppStore();
18
- const navigate = useNavigate();
19
- const inputRef = useRef<HTMLInputElement>(null);
20
- const isMobile = useIsMobile();
21
- const [query, setQuery] = useState('');
22
-
23
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
- const { data: searchResults } = useSearch(query) as any;
25
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
- const contacts: any[] = searchResults?.contacts ?? [];
27
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
- const accounts: any[] = searchResults?.accounts ?? [];
29
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
- const opportunities: any[] = searchResults?.opportunities ?? [];
31
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
32
- const activities: any[] = searchResults?.activities ?? [];
33
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
34
- const useCases: any[] = searchResults?.useCases ?? [];
35
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
- const assignments: any[] = searchResults?.assignments ?? [];
37
-
38
- useEffect(() => {
39
- if (commandPaletteOpen) {
40
- setTimeout(() => inputRef.current?.focus(), 50);
41
- } else {
42
- setQuery('');
43
- }
44
- }, [commandPaletteOpen]);
45
-
46
- const runAction = (fn: () => void) => {
47
- fn();
48
- setCommandPaletteOpen(false);
49
- };
50
-
51
- const itemClass = cn(
52
- 'flex items-center gap-3 px-2 py-2 rounded-md text-sm text-foreground cursor-pointer',
53
- 'data-[selected=true]:bg-primary/15 data-[selected=true]:text-primary',
54
- isMobile && 'py-3 px-3'
55
- );
56
-
57
- const commandContent = (
58
- <Command className="bg-card border-x border-border shadow-2xl overflow-hidden" shouldFilter={false}>
59
- <div className="flex items-center gap-2 px-4 border-b border-border">
60
- <Search className="w-4 h-4 text-muted-foreground" />
61
- <Command.Input
62
- ref={inputRef}
63
- value={query}
64
- onValueChange={setQuery}
65
- placeholder="Search contacts, opportunities, or type a command..."
66
- className="flex-1 py-3 text-sm bg-transparent text-foreground placeholder:text-muted-foreground outline-none"
67
- />
68
- </div>
69
- <Command.List className="max-h-[60vh] overflow-y-auto p-2">
70
- <Command.Empty className="py-6 text-center text-sm text-muted-foreground">
71
- No results found.
72
- </Command.Empty>
73
-
74
- {!query && (
75
- <Command.Group heading="Pages" className="text-xs text-muted-foreground px-2 py-1.5 font-display">
76
- {[
77
- { label: 'Dashboard', icon: LayoutDashboard, path: '/', color: ENTITY_COLORS.dashboard },
78
- { label: 'Contacts', icon: Users, path: '/contacts', color: ENTITY_COLORS.contacts },
79
- { label: 'Accounts', icon: Building2, path: '/accounts', color: ENTITY_COLORS.accounts },
80
- { label: 'Opportunities', icon: Briefcase, path: '/opportunities', color: ENTITY_COLORS.opportunities },
81
- { label: 'Use Cases', icon: FolderKanban, path: '/use-cases', color: ENTITY_COLORS.useCases },
82
- { label: 'Activities', icon: Activity, path: '/activities', color: ENTITY_COLORS.activities },
83
- { label: 'Assignments', icon: ClipboardList, path: '/assignments', color: ENTITY_COLORS.assignments },
84
- { label: 'Settings', icon: Settings, path: '/settings', color: null },
85
- ].map((page) => (
86
- <Command.Item
87
- key={page.path}
88
- value={page.label}
89
- onSelect={() => runAction(() => navigate(page.path))}
90
- className={itemClass}
91
- >
92
- <page.icon className={cn('w-4 h-4', page.color?.text ?? 'text-muted-foreground')} />
93
- {page.label}
94
- </Command.Item>
95
- ))}
96
- </Command.Group>
97
- )}
98
-
99
- {contacts.length > 0 && (
100
- <Command.Group heading="Contacts" className="text-xs text-muted-foreground px-2 py-1.5 font-display">
101
- {contacts.slice(0, 5).map((c) => (
102
- <Command.Item
103
- key={c.id as string}
104
- value={`${c.name} ${c.company} ${c.email}`}
105
- onSelect={() => runAction(() => { navigate('/contacts'); openDrawer('contact', c.id as string); })}
106
- className={itemClass}
107
- >
108
- <ContactAvatar name={c.name as string} className="w-5 h-5 rounded-full text-[8px]" />
109
- <span>{c.name as string}</span>
110
- {c.company && <span className="text-muted-foreground text-xs">— {c.company as string}</span>}
111
- </Command.Item>
112
- ))}
113
- </Command.Group>
114
- )}
115
-
116
- {accounts.length > 0 && (
117
- <Command.Group heading="Accounts" className="text-xs text-muted-foreground px-2 py-1.5 font-display">
118
- {accounts.slice(0, 5).map((a) => (
119
- <Command.Item
120
- key={a.id as string}
121
- value={`${a.name} ${a.domain ?? ''}`}
122
- onSelect={() => runAction(() => { navigate('/accounts'); openDrawer('account', a.id as string); })}
123
- className={itemClass}
124
- >
125
- <Building2 className={cn('w-4 h-4', ENTITY_COLORS.accounts.text)} />
126
- <span>{a.name as string}</span>
127
- {a.domain && <span className="text-muted-foreground text-xs">— {a.domain as string}</span>}
128
- </Command.Item>
129
- ))}
130
- </Command.Group>
131
- )}
132
-
133
- {opportunities.length > 0 && (
134
- <Command.Group heading="Opportunities" className="text-xs text-muted-foreground px-2 py-1.5 font-display">
135
- {opportunities.slice(0, 5).map((d) => (
136
- <Command.Item
137
- key={d.id as string}
138
- value={`${d.name} ${d.contact_name}`}
139
- onSelect={() => runAction(() => { navigate('/opportunities'); openDrawer('opportunity', d.id as string); })}
140
- className={itemClass}
141
- >
142
- <Briefcase className={cn('w-4 h-4', ENTITY_COLORS.opportunities.text)} />
143
- <span>{d.name as string}</span>
144
- {d.amount && (
145
- <span className="text-muted-foreground text-xs ml-auto">
146
- ${((d.amount as number) / 1000).toFixed(0)}K
147
- </span>
148
- )}
149
- </Command.Item>
150
- ))}
151
- </Command.Group>
152
- )}
153
-
154
- {activities.length > 0 && (
155
- <Command.Group heading="Activities" className="text-xs text-muted-foreground px-2 py-1.5 font-display">
156
- {activities.slice(0, 5).map((a) => (
157
- <Command.Item
158
- key={a.id as string}
159
- value={`${a.body ?? ''} ${a.activity_type ?? ''}`}
160
- onSelect={() => runAction(() => navigate('/activities'))}
161
- className={itemClass}
162
- >
163
- <Activity className={cn('w-4 h-4', ENTITY_COLORS.activities.text)} />
164
- <span className="truncate">{(a.body as string)?.slice(0, 60)}</span>
165
- </Command.Item>
166
- ))}
167
- </Command.Group>
168
- )}
169
-
170
- {useCases.length > 0 && (
171
- <Command.Group heading="Use Cases" className="text-xs text-muted-foreground px-2 py-1.5 font-display">
172
- {useCases.slice(0, 5).map((u) => (
173
- <Command.Item
174
- key={u.id as string}
175
- value={`${u.name ?? ''}`}
176
- onSelect={() => runAction(() => navigate('/use-cases'))}
177
- className={itemClass}
178
- >
179
- <FolderKanban className={cn('w-4 h-4', ENTITY_COLORS.useCases.text)} />
180
- <span>{u.name as string}</span>
181
- </Command.Item>
182
- ))}
183
- </Command.Group>
184
- )}
185
-
186
- {assignments.length > 0 && (
187
- <Command.Group heading="Assignments" className="text-xs text-muted-foreground px-2 py-1.5 font-display">
188
- {assignments.slice(0, 5).map((a) => (
189
- <Command.Item
190
- key={a.id as string}
191
- value={`${a.title ?? ''}`}
192
- onSelect={() => runAction(() => navigate('/assignments'))}
193
- className={itemClass}
194
- >
195
- <ClipboardList className={cn('w-4 h-4', ENTITY_COLORS.assignments.text)} />
196
- <span>{a.title as string}</span>
197
- </Command.Item>
198
- ))}
199
- </Command.Group>
200
- )}
201
- </Command.List>
202
- {!isMobile && (
203
- <div className="flex items-center justify-between px-4 py-2 border-t border-border text-xs text-muted-foreground">
204
- <span>Navigate with ↑↓ · Select with ↵</span>
205
- <span>ESC to close</span>
206
- </div>
207
- )}
208
- </Command>
209
- );
210
-
211
- return (
212
- <AnimatePresence>
213
- {commandPaletteOpen && (
214
- <>
215
- <motion.div
216
- initial={{ opacity: 0 }}
217
- animate={{ opacity: 1 }}
218
- exit={{ opacity: 0 }}
219
- className="fixed inset-0 z-[70] bg-black/60 backdrop-blur-sm"
220
- onClick={() => setCommandPaletteOpen(false)}
221
- />
222
- {isMobile ? (
223
- <motion.div
224
- initial={{ y: '100%' }}
225
- animate={{ y: 0 }}
226
- exit={{ y: '100%' }}
227
- transition={{ type: 'spring', damping: 30, stiffness: 300 }}
228
- className="fixed left-0 right-0 bottom-0 z-[70] flex flex-col max-h-[85vh]"
229
- >
230
- <div className="flex justify-center py-2 bg-card rounded-t-2xl border-t border-x border-border">
231
- <div className="w-10 h-1 rounded-full bg-muted-foreground/20" />
232
- </div>
233
- {commandContent}
234
- </motion.div>
235
- ) : (
236
- <div className="fixed inset-0 z-[70] flex items-center justify-center px-4 pointer-events-none">
237
- <motion.div
238
- initial={{ opacity: 0, scale: 0.96 }}
239
- animate={{ opacity: 1, scale: 1 }}
240
- exit={{ opacity: 0, scale: 0.96 }}
241
- transition={{ duration: 0.15 }}
242
- className="w-full max-w-2xl pointer-events-auto"
243
- >
244
- <div className="rounded-2xl overflow-hidden border border-border shadow-2xl">
245
- {commandContent}
246
- </div>
247
- </motion.div>
248
- </div>
249
- )}
250
- </>
251
- )}
252
- </AnimatePresence>
253
- );
254
- }
@@ -1,49 +0,0 @@
1
- import { cn } from '@/lib/utils';
2
-
3
- const AVATAR_COLORS: { bg: string; dark?: boolean }[] = [
4
- { bg: '#FB990C' }, // primary
5
- { bg: '#F8BB59' }, // primary light
6
- { bg: '#C94408' }, // primary deep
7
- { bg: '#FDBA74', dark: true }, // warm peach
8
- { bg: '#F59E0B' }, // amber
9
- { bg: '#EA580C' }, // burnt orange
10
- { bg: '#FED7AA', dark: true }, // warm cream
11
- { bg: '#FCF1C1', dark: true }, // primary pale
12
- ];
13
-
14
- function hashName(name: string): number {
15
- let hash = 0;
16
- for (let i = 0; i < name.length; i++) {
17
- hash = name.charCodeAt(i) + ((hash << 5) - hash);
18
- }
19
- return Math.abs(hash);
20
- }
21
-
22
- function getInitials(name: string): string {
23
- const parts = name.trim().split(/\s+/);
24
- if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
25
- return (parts[0]?.[0] ?? '?').toUpperCase();
26
- }
27
-
28
- interface ContactAvatarProps {
29
- name: string;
30
- className?: string;
31
- textClassName?: string;
32
- }
33
-
34
- export function ContactAvatar({ name, className, textClassName }: ContactAvatarProps) {
35
- const safeName = name || '?';
36
- const entry = AVATAR_COLORS[hashName(safeName) % AVATAR_COLORS.length];
37
- const initials = getInitials(safeName);
38
-
39
- return (
40
- <div
41
- className={cn('flex items-center justify-center flex-shrink-0 rounded-xl', className)}
42
- style={{ backgroundColor: entry.bg }}
43
- >
44
- <span className={cn('font-display font-bold select-none', entry.dark ? 'text-[#2A2A2E]' : 'text-white', textClassName)}>
45
- {safeName === '?' ? '?' : initials}
46
- </span>
47
- </div>
48
- );
49
- }