@crmy/web 0.5.1 → 0.5.6
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.
- package/dist/assets/index-CskfWp8E.js +560 -0
- package/dist/assets/index-D763l57m.css +1 -0
- package/{index.html → dist/index.html} +2 -1
- package/package.json +4 -1
- package/postcss.config.js +0 -6
- package/src/App.tsx +0 -158
- package/src/api/client.ts +0 -82
- package/src/api/hooks.ts +0 -689
- package/src/components/CustomFields.tsx +0 -240
- package/src/components/NavLink.tsx +0 -28
- package/src/components/crm/AIFab.tsx +0 -37
- package/src/components/crm/AccountDrawer.tsx +0 -372
- package/src/components/crm/ActivityTimeline.tsx +0 -115
- package/src/components/crm/AssignmentDrawer.tsx +0 -396
- package/src/components/crm/BriefingPanel.tsx +0 -217
- package/src/components/crm/CommandPalette.tsx +0 -254
- package/src/components/crm/ContactAvatar.tsx +0 -49
- package/src/components/crm/ContactDrawer.tsx +0 -438
- package/src/components/crm/ContextPanel.tsx +0 -200
- package/src/components/crm/CrmWidgets.tsx +0 -417
- package/src/components/crm/DrawerShell.tsx +0 -77
- package/src/components/crm/ListToolbar.tsx +0 -252
- package/src/components/crm/OpportunityDrawer.tsx +0 -372
- package/src/components/crm/PaginationBar.tsx +0 -111
- package/src/components/crm/QuickAddDrawer.tsx +0 -652
- package/src/components/crm/ShortcutsOverlay.tsx +0 -65
- package/src/components/crm/UseCaseDrawer.tsx +0 -454
- package/src/components/layout/MobileNav.tsx +0 -49
- package/src/components/layout/Sidebar.tsx +0 -157
- package/src/components/layout/TopBar.tsx +0 -54
- package/src/components/settings/ActorsSettings.tsx +0 -1190
- package/src/components/ui/accordion.tsx +0 -52
- package/src/components/ui/alert-dialog.tsx +0 -104
- package/src/components/ui/alert.tsx +0 -43
- package/src/components/ui/aspect-ratio.tsx +0 -5
- package/src/components/ui/avatar.tsx +0 -38
- package/src/components/ui/badge.tsx +0 -29
- package/src/components/ui/breadcrumb.tsx +0 -90
- package/src/components/ui/button.tsx +0 -47
- package/src/components/ui/calendar.tsx +0 -54
- package/src/components/ui/card.tsx +0 -43
- package/src/components/ui/carousel.tsx +0 -224
- package/src/components/ui/chart.tsx +0 -303
- package/src/components/ui/checkbox.tsx +0 -26
- package/src/components/ui/collapsible.tsx +0 -9
- package/src/components/ui/command.tsx +0 -132
- package/src/components/ui/context-menu.tsx +0 -178
- package/src/components/ui/date-picker.tsx +0 -313
- package/src/components/ui/dialog.tsx +0 -95
- package/src/components/ui/drawer.tsx +0 -87
- package/src/components/ui/dropdown-menu.tsx +0 -179
- package/src/components/ui/form.tsx +0 -129
- package/src/components/ui/hover-card.tsx +0 -27
- package/src/components/ui/input-otp.tsx +0 -61
- package/src/components/ui/input.tsx +0 -22
- package/src/components/ui/label.tsx +0 -17
- package/src/components/ui/menubar.tsx +0 -207
- package/src/components/ui/navigation-menu.tsx +0 -120
- package/src/components/ui/pagination.tsx +0 -81
- package/src/components/ui/popover.tsx +0 -29
- package/src/components/ui/progress.tsx +0 -23
- package/src/components/ui/radio-group.tsx +0 -36
- package/src/components/ui/resizable.tsx +0 -37
- package/src/components/ui/scroll-area.tsx +0 -38
- package/src/components/ui/select.tsx +0 -143
- package/src/components/ui/separator.tsx +0 -20
- package/src/components/ui/sheet.tsx +0 -107
- package/src/components/ui/sidebar.tsx +0 -637
- package/src/components/ui/skeleton.tsx +0 -7
- package/src/components/ui/slider.tsx +0 -23
- package/src/components/ui/sonner.tsx +0 -24
- package/src/components/ui/switch.tsx +0 -27
- package/src/components/ui/table.tsx +0 -72
- package/src/components/ui/tabs.tsx +0 -53
- package/src/components/ui/textarea.tsx +0 -21
- package/src/components/ui/toast.tsx +0 -111
- package/src/components/ui/toaster.tsx +0 -24
- package/src/components/ui/toggle-group.tsx +0 -49
- package/src/components/ui/toggle.tsx +0 -37
- package/src/components/ui/tooltip.tsx +0 -28
- package/src/components/ui/use-toast.ts +0 -1
- package/src/components/ui/utils.ts +0 -9
- package/src/contexts/AgentSettingsContext.tsx +0 -24
- package/src/hooks/use-mobile.tsx +0 -19
- package/src/hooks/use-toast.ts +0 -186
- package/src/hooks/useKeyboardShortcuts.ts +0 -95
- package/src/hooks/useTheme.ts +0 -24
- package/src/index.css +0 -245
- package/src/lib/entityColors.ts +0 -18
- package/src/lib/stageConfig.ts +0 -32
- package/src/lib/utils.ts +0 -6
- package/src/main.tsx +0 -25
- package/src/pages/Accounts.tsx +0 -205
- package/src/pages/Activities.tsx +0 -251
- package/src/pages/Agent.tsx +0 -237
- package/src/pages/AgentSettings.tsx +0 -544
- package/src/pages/Assignments.tsx +0 -750
- package/src/pages/Contacts.tsx +0 -200
- package/src/pages/Dashboard.tsx +0 -143
- package/src/pages/Inbox.tsx +0 -615
- package/src/pages/NotFound.tsx +0 -24
- package/src/pages/Opportunities.tsx +0 -386
- package/src/pages/SearchResults.tsx +0 -49
- package/src/pages/Settings.tsx +0 -1884
- package/src/pages/UseCases.tsx +0 -396
- package/src/pages/auth/Login.tsx +0 -261
- package/src/pages/hitl/HITL.tsx +0 -101
- package/src/store/appStore.ts +0 -103
- package/src/vite-env.d.ts +0 -14
- package/tailwind.config.js +0 -121
- package/tsconfig.json +0 -24
- package/vite.config.ts +0 -27
- /package/{public → dist}/android-chrome-192x192.png +0 -0
- /package/{public → dist}/android-chrome-512x512.png +0 -0
- /package/{public → dist}/apple-touch-icon.png +0 -0
- /package/{src/assets/crmy-logo.png → dist/assets/crmy-logo-DWN0xBPW.png} +0 -0
- /package/{public → dist}/favicon-16x16.png +0 -0
- /package/{public → dist}/favicon-32x32.png +0 -0
- /package/{public → dist}/favicon.ico +0 -0
- /package/{public → dist}/favicon.svg +0 -0
- /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
|
-
}
|