ui: Clean up empty states, modals, animations
This commit is contained in:
@@ -2,7 +2,8 @@ import {Button} from '@plunk/ui';
|
||||
import type {Activity, CursorPaginatedResponse} from '@plunk/types';
|
||||
import {network} from '../lib/network';
|
||||
import {ActivityItem} from './ActivityItem';
|
||||
import {Loader2} from 'lucide-react';
|
||||
import {EmptyState} from './EmptyState';
|
||||
import {Activity as ActivityIcon, Loader2} from 'lucide-react';
|
||||
import {useCallback, useEffect, useMemo, useState} from 'react';
|
||||
|
||||
export interface ActivityFeedProps {
|
||||
@@ -186,11 +187,11 @@ export function ActivityFeed({typeFilter, dateRangeDays = 30, contactId}: Activi
|
||||
|
||||
if (activities.length === 0 && upcomingActivities.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-neutral-500 text-sm">
|
||||
No activity found for the selected filters. Activities will appear here as they happen.
|
||||
</p>
|
||||
</div>
|
||||
<EmptyState
|
||||
icon={ActivityIcon}
|
||||
title="No activity yet"
|
||||
description="Events will appear here as contacts interact with your emails."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {AnimatePresence, motion} from 'framer-motion';
|
||||
import {Check, Copy, Eye, EyeOff, RefreshCw} from 'lucide-react';
|
||||
import {useState} from 'react';
|
||||
import {Button} from '@plunk/ui';
|
||||
import {Check, Copy, Eye, EyeOff, RefreshCw} from 'lucide-react';
|
||||
|
||||
interface ApiKeyDisplayProps {
|
||||
label: string;
|
||||
@@ -28,8 +29,8 @@ export function ApiKeyDisplay({
|
||||
await navigator.clipboard.writeText(value);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch (error) {
|
||||
console.error('Failed to copy:', error);
|
||||
} catch {
|
||||
// clipboard API unavailable — silent
|
||||
}
|
||||
};
|
||||
|
||||
@@ -39,8 +40,8 @@ export function ApiKeyDisplay({
|
||||
try {
|
||||
setIsRegenerating(true);
|
||||
await onRegenerate();
|
||||
} catch (error) {
|
||||
console.error('Failed to regenerate:', error);
|
||||
} catch {
|
||||
// error surfaced via isRegenerating state reset
|
||||
} finally {
|
||||
setIsRegenerating(false);
|
||||
}
|
||||
@@ -74,9 +75,31 @@ export function ApiKeyDisplay({
|
||||
size="icon"
|
||||
onClick={handleCopy}
|
||||
title="Copy to clipboard"
|
||||
className="h-9 w-9"
|
||||
className="h-9 w-9 overflow-hidden"
|
||||
>
|
||||
{copied ? <Check className="h-4 w-4 text-green-600" /> : <Copy className="h-4 w-4" />}
|
||||
<AnimatePresence mode="wait" initial={false}>
|
||||
{copied ? (
|
||||
<motion.span
|
||||
key="copied"
|
||||
initial={{opacity: 0, y: 6}}
|
||||
animate={{opacity: 1, y: 0}}
|
||||
exit={{opacity: 0, y: -6}}
|
||||
transition={{duration: 0.15}}
|
||||
>
|
||||
<Check className="h-4 w-4 text-green-600" />
|
||||
</motion.span>
|
||||
) : (
|
||||
<motion.span
|
||||
key="idle"
|
||||
initial={{opacity: 0, y: 6}}
|
||||
animate={{opacity: 1, y: 0}}
|
||||
exit={{opacity: 0, y: -6}}
|
||||
transition={{duration: 0.15}}
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
</motion.span>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</Button>
|
||||
{showRegenerate && onRegenerate && (
|
||||
<Button
|
||||
|
||||
@@ -198,7 +198,7 @@ export function CampaignSelectionDialog({open, onOpenChange, onSelectCampaign}:
|
||||
{data?.data.map(campaign => (
|
||||
<Card
|
||||
key={campaign.id}
|
||||
className="cursor-pointer hover:border-primary/50 hover:shadow-md transition-all"
|
||||
className="cursor-pointer hover:border-neutral-400 transition-colors"
|
||||
onClick={() => handleCampaignClick(campaign)}
|
||||
>
|
||||
<CardHeader className="pb-3">
|
||||
@@ -266,30 +266,24 @@ export function CampaignSelectionDialog({open, onOpenChange, onSelectCampaign}:
|
||||
) : (
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
{/* Scrollable Content */}
|
||||
<div className="flex-1 overflow-y-auto space-y-6 pr-2">
|
||||
<div className="flex-1 overflow-y-auto pr-2">
|
||||
{/* Campaign Preview */}
|
||||
{selectedCampaign && (
|
||||
<Card className="bg-neutral-50">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<CardTitle className="text-base">{selectedCampaign.name}</CardTitle>
|
||||
{getStatusBadge(selectedCampaign.status)}
|
||||
</div>
|
||||
{selectedCampaign.description && (
|
||||
<CardDescription className="text-xs">{selectedCampaign.description}</CardDescription>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<div className="pb-4 mb-1 border-b border-neutral-100">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-neutral-900">{selectedCampaign.name}</span>
|
||||
{getStatusBadge(selectedCampaign.status)}
|
||||
</div>
|
||||
{selectedCampaign.description && (
|
||||
<p className="text-xs text-neutral-500 mt-1">{selectedCampaign.description}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Field Selection */}
|
||||
<div className="space-y-3">
|
||||
<div className="divide-y divide-neutral-100">
|
||||
<div
|
||||
className="flex items-center space-x-3 p-3 rounded-lg border border-neutral-200 hover:bg-neutral-50 transition-colors cursor-pointer"
|
||||
className="flex items-center gap-3 py-3 cursor-pointer hover:text-neutral-900 transition-colors"
|
||||
onClick={() => toggleField('subject')}
|
||||
>
|
||||
<Checkbox
|
||||
@@ -302,13 +296,13 @@ export function CampaignSelectionDialog({open, onOpenChange, onSelectCampaign}:
|
||||
Email Subject
|
||||
</Label>
|
||||
{selectedCampaign?.subject && (
|
||||
<p className="text-xs text-neutral-500 mt-0.5">{selectedCampaign.subject}</p>
|
||||
<p className="text-xs text-neutral-400 mt-0.5 truncate">{selectedCampaign.subject}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex items-center space-x-3 p-3 rounded-lg border border-neutral-200 hover:bg-neutral-50 transition-colors cursor-pointer"
|
||||
className="flex items-center gap-3 py-3 cursor-pointer hover:text-neutral-900 transition-colors"
|
||||
onClick={() => toggleField('body')}
|
||||
>
|
||||
<Checkbox id="body" checked={selectedFields.body} onCheckedChange={() => toggleField('body')} />
|
||||
@@ -316,12 +310,12 @@ export function CampaignSelectionDialog({open, onOpenChange, onSelectCampaign}:
|
||||
<Label htmlFor="body" className="text-sm font-medium cursor-pointer">
|
||||
Email Body
|
||||
</Label>
|
||||
<p className="text-xs text-neutral-500 mt-0.5">The full email content and design</p>
|
||||
<p className="text-xs text-neutral-400 mt-0.5">Full email content and design</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex items-center space-x-3 p-3 rounded-lg border border-neutral-200 hover:bg-neutral-50 transition-colors cursor-pointer"
|
||||
className="flex items-center gap-3 py-3 cursor-pointer hover:text-neutral-900 transition-colors"
|
||||
onClick={() => toggleField('from')}
|
||||
>
|
||||
<Checkbox id="from" checked={selectedFields.from} onCheckedChange={() => toggleField('from')} />
|
||||
@@ -330,13 +324,13 @@ export function CampaignSelectionDialog({open, onOpenChange, onSelectCampaign}:
|
||||
From Email
|
||||
</Label>
|
||||
{selectedCampaign?.from && (
|
||||
<p className="text-xs text-neutral-500 mt-0.5">{selectedCampaign.from}</p>
|
||||
<p className="text-xs text-neutral-400 mt-0.5">{selectedCampaign.from}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex items-center space-x-3 p-3 rounded-lg border border-neutral-200 hover:bg-neutral-50 transition-colors cursor-pointer"
|
||||
className="flex items-center gap-3 py-3 cursor-pointer hover:text-neutral-900 transition-colors"
|
||||
onClick={() => toggleField('fromName')}
|
||||
>
|
||||
<Checkbox
|
||||
@@ -349,13 +343,13 @@ export function CampaignSelectionDialog({open, onOpenChange, onSelectCampaign}:
|
||||
From Name
|
||||
</Label>
|
||||
{selectedCampaign?.fromName && (
|
||||
<p className="text-xs text-neutral-500 mt-0.5">{selectedCampaign.fromName}</p>
|
||||
<p className="text-xs text-neutral-400 mt-0.5">{selectedCampaign.fromName}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex items-center space-x-3 p-3 rounded-lg border border-neutral-200 hover:bg-neutral-50 transition-colors cursor-pointer"
|
||||
className="flex items-center gap-3 py-3 cursor-pointer hover:text-neutral-900 transition-colors"
|
||||
onClick={() => toggleField('replyTo')}
|
||||
>
|
||||
<Checkbox
|
||||
@@ -368,13 +362,13 @@ export function CampaignSelectionDialog({open, onOpenChange, onSelectCampaign}:
|
||||
Reply-To Email
|
||||
</Label>
|
||||
{selectedCampaign?.replyTo && (
|
||||
<p className="text-xs text-neutral-500 mt-0.5">{selectedCampaign.replyTo}</p>
|
||||
<p className="text-xs text-neutral-400 mt-0.5">{selectedCampaign.replyTo}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex items-center space-x-3 p-3 rounded-lg border border-neutral-200 hover:bg-neutral-50 transition-colors cursor-pointer"
|
||||
className="flex items-center gap-3 py-3 cursor-pointer hover:text-neutral-900 transition-colors"
|
||||
onClick={() => toggleField('audience')}
|
||||
>
|
||||
<Checkbox
|
||||
@@ -387,7 +381,7 @@ export function CampaignSelectionDialog({open, onOpenChange, onSelectCampaign}:
|
||||
Audience Settings
|
||||
</Label>
|
||||
{selectedCampaign && (
|
||||
<p className="text-xs text-neutral-500 mt-0.5">{getAudienceLabel(selectedCampaign)}</p>
|
||||
<p className="text-xs text-neutral-400 mt-0.5">{getAudienceLabel(selectedCampaign)}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
Menu,
|
||||
Plus,
|
||||
Settings,
|
||||
User,
|
||||
Users,
|
||||
Workflow,
|
||||
} from 'lucide-react';
|
||||
@@ -57,7 +56,6 @@ const navigation: NavSection[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Campaigns',
|
||||
items: [{name: 'Campaigns', href: '/campaigns', icon: Megaphone}],
|
||||
},
|
||||
];
|
||||
@@ -127,8 +125,7 @@ export function DashboardLayout({children}: DashboardLayoutProps) {
|
||||
|
||||
// Redirect to login
|
||||
await router.push('/auth/login');
|
||||
} catch (error) {
|
||||
console.error('Logout failed:', error);
|
||||
} catch {
|
||||
// Even if the API call fails, try to redirect to login
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('activeProjectId');
|
||||
@@ -179,12 +176,14 @@ export function DashboardLayout({children}: DashboardLayoutProps) {
|
||||
</div>
|
||||
<span className="font-medium text-neutral-900 truncate">{activeProject?.name || 'Select project'}</span>
|
||||
</div>
|
||||
<ChevronDown className="h-4 w-4 text-neutral-500 flex-shrink-0" />
|
||||
<ChevronDown
|
||||
className={`h-4 w-4 text-neutral-500 flex-shrink-0 transition-transform duration-200 ${showProjectMenu ? 'rotate-180' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{/* Project Dropdown */}
|
||||
{showProjectMenu && (
|
||||
<div className="absolute top-full left-0 right-0 mt-1 bg-white border border-neutral-200 rounded-lg shadow-lg z-50 py-1 max-h-[400px] overflow-y-auto min-w-full w-max">
|
||||
<div className="absolute top-full left-0 right-0 mt-1 bg-white border border-neutral-200 rounded-lg shadow-md z-50 py-1 max-h-[400px] overflow-y-auto min-w-full w-max">
|
||||
{sortedProjects.map(project => (
|
||||
<button
|
||||
key={project.id}
|
||||
@@ -196,7 +195,7 @@ export function DashboardLayout({children}: DashboardLayoutProps) {
|
||||
}}
|
||||
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-neutral-50 transition-colors whitespace-nowrap"
|
||||
>
|
||||
<div className="h-6 w-6 rounded bg-neutral-900 text-white flex items-center justify-center text-xs font-medium flex-shrink-0">
|
||||
<div className="h-6 w-6 rounded-md bg-neutral-900 text-white flex items-center justify-center text-xs font-medium flex-shrink-0">
|
||||
{project.name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<span className="text-neutral-900 text-left flex-1">{project.name}</span>
|
||||
@@ -230,15 +229,16 @@ export function DashboardLayout({children}: DashboardLayoutProps) {
|
||||
)}
|
||||
<div className="space-y-1">
|
||||
{section.items.map(item => {
|
||||
const isActive = router.pathname === item.href;
|
||||
const isActive =
|
||||
item.href === '/' ? router.pathname === item.href : router.pathname.startsWith(item.href);
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
onClick={() => setShowMobileMenu(false)}
|
||||
className={`flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-lg transition-colors text-neutral-700 ${
|
||||
isActive ? 'bg-neutral-100' : 'hover:bg-neutral-50 hover:text-neutral-900'
|
||||
className={`flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-lg transition-colors ${
|
||||
isActive ? 'bg-neutral-100 text-neutral-900' : 'text-neutral-600 hover:bg-neutral-50 hover:text-neutral-900'
|
||||
}`}
|
||||
>
|
||||
<Icon className="h-5 w-5" />
|
||||
@@ -257,7 +257,7 @@ export function DashboardLayout({children}: DashboardLayoutProps) {
|
||||
href={WIKI_URI}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-lg transition-colors text-neutral-700 hover:bg-neutral-50 hover:text-neutral-900"
|
||||
className="flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-lg transition-colors text-neutral-600 hover:bg-neutral-50 hover:text-neutral-900"
|
||||
>
|
||||
<BookOpen className="h-5 w-5" />
|
||||
Documentation
|
||||
@@ -266,8 +266,10 @@ export function DashboardLayout({children}: DashboardLayoutProps) {
|
||||
<Link
|
||||
href="/settings"
|
||||
onClick={() => setShowMobileMenu(false)}
|
||||
className={`flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-lg transition-colors text-neutral-700 ${
|
||||
router.pathname.startsWith('/settings') ? 'bg-neutral-100' : 'hover:bg-neutral-50 hover:text-neutral-900'
|
||||
className={`flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-lg transition-colors ${
|
||||
router.pathname.startsWith('/settings')
|
||||
? 'bg-neutral-100 text-neutral-900'
|
||||
: 'text-neutral-600 hover:bg-neutral-50 hover:text-neutral-900'
|
||||
}`}
|
||||
>
|
||||
<Settings className="h-5 w-5" />
|
||||
@@ -277,16 +279,23 @@ export function DashboardLayout({children}: DashboardLayoutProps) {
|
||||
<div className="relative" ref={userMenuRef}>
|
||||
<button
|
||||
onClick={handleToggleUserMenu}
|
||||
className="w-full flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-lg text-neutral-700 hover:bg-neutral-50 hover:text-neutral-900 transition-colors"
|
||||
className="w-full flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-lg text-neutral-600 hover:bg-neutral-50 hover:text-neutral-900 transition-colors"
|
||||
>
|
||||
<User className="h-5 w-5" />
|
||||
<div className="h-5 w-5 rounded-full bg-neutral-900 text-white flex items-center justify-center text-[10px] font-semibold flex-shrink-0">
|
||||
{user?.email?.charAt(0).toUpperCase() ?? '?'}
|
||||
</div>
|
||||
<span className="flex-1 text-left truncate">{user?.email}</span>
|
||||
<ChevronDown className="h-4 w-4 text-neutral-500" />
|
||||
<ChevronDown
|
||||
className={`h-4 w-4 text-neutral-500 transition-transform duration-200 ${showUserMenu ? 'rotate-180' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{/* User Dropdown */}
|
||||
{showUserMenu && (
|
||||
<div className="absolute bottom-full left-0 right-0 mb-1 bg-white border border-neutral-200 rounded-lg shadow-lg z-50 py-1">
|
||||
<div className="absolute bottom-full left-0 right-0 mb-1 bg-white border border-neutral-200 rounded-lg shadow-md z-50 py-1">
|
||||
<div className="px-3 py-2 border-b border-neutral-100">
|
||||
<p className="text-xs text-neutral-500 truncate">{user?.email}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLogoutClick}
|
||||
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-neutral-50 transition-colors text-red-600"
|
||||
|
||||
@@ -151,7 +151,7 @@ export function DataManagementSettings() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{customFields.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">No custom fields found</p>
|
||||
<p className="text-sm text-neutral-500">No custom fields found</p>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
@@ -170,7 +170,7 @@ export function DataManagementSettings() {
|
||||
<Badge variant="secondary">{field.type}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm text-muted-foreground">{field.coverage}%</span>
|
||||
<span className="text-sm text-neutral-500">{field.coverage}%</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button variant="ghost" size="sm" onClick={() => openFieldDeleteDialog(field.field)}>
|
||||
@@ -196,7 +196,7 @@ export function DataManagementSettings() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{customEvents.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">No custom events found</p>
|
||||
<p className="text-sm text-neutral-500">No custom events found</p>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
|
||||
@@ -24,6 +24,34 @@ import {Check, CheckCircle2, ChevronDown, Copy, Loader2, RefreshCw, Trash2, XCir
|
||||
import {useConfig} from '../lib/hooks/useConfig';
|
||||
import {useAddDomain, useCheckDomainVerification, useDomains, useRemoveDomain} from '../lib/hooks/useDomains';
|
||||
|
||||
function AnimatedCopyIcon({isCopied}: {isCopied: boolean}) {
|
||||
return (
|
||||
<AnimatePresence mode="wait" initial={false}>
|
||||
{isCopied ? (
|
||||
<motion.span
|
||||
key="copied"
|
||||
initial={{opacity: 0, y: 4}}
|
||||
animate={{opacity: 1, y: 0}}
|
||||
exit={{opacity: 0, y: -4}}
|
||||
transition={{duration: 0.15}}
|
||||
>
|
||||
<Check className="h-3 w-3 text-green-600" />
|
||||
</motion.span>
|
||||
) : (
|
||||
<motion.span
|
||||
key="idle"
|
||||
initial={{opacity: 0, y: 4}}
|
||||
animate={{opacity: 1, y: 0}}
|
||||
exit={{opacity: 0, y: -4}}
|
||||
transition={{duration: 0.15}}
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
</motion.span>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
|
||||
interface DomainsSettingsProps {
|
||||
projectId: string;
|
||||
}
|
||||
@@ -419,14 +447,13 @@ export function DomainsSettings({projectId}: DomainsSettingsProps) {
|
||||
onClick={() =>
|
||||
handleCopyToken(`${token}._domainkey.${domain.domain}`, index + 2000)
|
||||
}
|
||||
className="shrink-0 h-6 w-6 p-0"
|
||||
className="shrink-0 h-6 w-6 p-0 overflow-hidden"
|
||||
>
|
||||
{copiedToken ===
|
||||
`${token}._domainkey.${domain.domain}-${index + 2000}` ? (
|
||||
<Check className="h-3 w-3 text-green-600" />
|
||||
) : (
|
||||
<Copy className="h-3 w-3" />
|
||||
)}
|
||||
<AnimatedCopyIcon
|
||||
isCopied={
|
||||
copiedToken === `${token}._domainkey.${domain.domain}-${index + 2000}`
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
@@ -439,13 +466,11 @@ export function DomainsSettings({projectId}: DomainsSettingsProps) {
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleCopyToken(`${token}.dkim.amazonses.com`, index)}
|
||||
className="shrink-0 h-6 w-6 p-0"
|
||||
className="shrink-0 h-6 w-6 p-0 overflow-hidden"
|
||||
>
|
||||
{copiedToken === `${token}.dkim.amazonses.com-${index}` ? (
|
||||
<Check className="h-3 w-3 text-green-600" />
|
||||
) : (
|
||||
<Copy className="h-3 w-3" />
|
||||
)}
|
||||
<AnimatedCopyIcon
|
||||
isCopied={copiedToken === `${token}.dkim.amazonses.com-${index}`}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
@@ -500,13 +525,11 @@ export function DomainsSettings({projectId}: DomainsSettingsProps) {
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleCopyToken(`plunk.${domain.domain}`, 3000)}
|
||||
className="shrink-0 h-6 w-6 p-0"
|
||||
className="shrink-0 h-6 w-6 p-0 overflow-hidden"
|
||||
>
|
||||
{copiedToken === `plunk.${domain.domain}-3000` ? (
|
||||
<Check className="h-3 w-3 text-green-600" />
|
||||
) : (
|
||||
<Copy className="h-3 w-3" />
|
||||
)}
|
||||
<AnimatedCopyIcon
|
||||
isCopied={copiedToken === `plunk.${domain.domain}-3000`}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
@@ -524,14 +547,14 @@ export function DomainsSettings({projectId}: DomainsSettingsProps) {
|
||||
1000,
|
||||
)
|
||||
}
|
||||
className="shrink-0 h-6 w-6 p-0"
|
||||
className="shrink-0 h-6 w-6 p-0 overflow-hidden"
|
||||
>
|
||||
{copiedToken ===
|
||||
`10 feedback-smtp.${config.aws.sesRegion}.amazonses.com-1000` ? (
|
||||
<Check className="h-3 w-3 text-green-600" />
|
||||
) : (
|
||||
<Copy className="h-3 w-3" />
|
||||
)}
|
||||
<AnimatedCopyIcon
|
||||
isCopied={
|
||||
copiedToken ===
|
||||
`10 feedback-smtp.${config.aws.sesRegion}.amazonses.com-1000`
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
@@ -551,13 +574,11 @@ export function DomainsSettings({projectId}: DomainsSettingsProps) {
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleCopyToken(`plunk.${domain.domain}`, 3001)}
|
||||
className="shrink-0 h-6 w-6 p-0"
|
||||
className="shrink-0 h-6 w-6 p-0 overflow-hidden"
|
||||
>
|
||||
{copiedToken === `plunk.${domain.domain}-3001` ? (
|
||||
<Check className="h-3 w-3 text-green-600" />
|
||||
) : (
|
||||
<Copy className="h-3 w-3" />
|
||||
)}
|
||||
<AnimatedCopyIcon
|
||||
isCopied={copiedToken === `plunk.${domain.domain}-3001`}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
@@ -572,13 +593,11 @@ export function DomainsSettings({projectId}: DomainsSettingsProps) {
|
||||
onClick={() =>
|
||||
handleCopyToken('"v=spf1 include:amazonses.com ~all"', 1001)
|
||||
}
|
||||
className="shrink-0 h-6 w-6 p-0"
|
||||
className="shrink-0 h-6 w-6 p-0 overflow-hidden"
|
||||
>
|
||||
{copiedToken === '"v=spf1 include:amazonses.com ~all"-1001' ? (
|
||||
<Check className="h-3 w-3 text-green-600" />
|
||||
) : (
|
||||
<Copy className="h-3 w-3" />
|
||||
)}
|
||||
<AnimatedCopyIcon
|
||||
isCopied={copiedToken === '"v=spf1 include:amazonses.com ~all"-1001'}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
@@ -632,13 +651,9 @@ export function DomainsSettings({projectId}: DomainsSettingsProps) {
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleCopyToken(domain.domain, 3002)}
|
||||
className="shrink-0 h-6 w-6 p-0"
|
||||
className="shrink-0 h-6 w-6 p-0 overflow-hidden"
|
||||
>
|
||||
{copiedToken === `${domain.domain}-3002` ? (
|
||||
<Check className="h-3 w-3 text-green-600" />
|
||||
) : (
|
||||
<Copy className="h-3 w-3" />
|
||||
)}
|
||||
<AnimatedCopyIcon isCopied={copiedToken === `${domain.domain}-3002`} />
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
@@ -656,14 +671,14 @@ export function DomainsSettings({projectId}: DomainsSettingsProps) {
|
||||
1002,
|
||||
)
|
||||
}
|
||||
className="shrink-0 h-6 w-6 p-0"
|
||||
className="shrink-0 h-6 w-6 p-0 overflow-hidden"
|
||||
>
|
||||
{copiedToken ===
|
||||
`10 inbound-smtp.${config.aws.sesRegion}.amazonaws.com-1002` ? (
|
||||
<Check className="h-3 w-3 text-green-600" />
|
||||
) : (
|
||||
<Copy className="h-3 w-3" />
|
||||
)}
|
||||
<AnimatedCopyIcon
|
||||
isCopied={
|
||||
copiedToken ===
|
||||
`10 inbound-smtp.${config.aws.sesRegion}.amazonaws.com-1002`
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
@@ -674,8 +689,8 @@ export function DomainsSettings({projectId}: DomainsSettingsProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-start gap-2 p-3 bg-blue-50 rounded-lg border border-blue-200 mt-3">
|
||||
<div className="text-blue-600 mt-0.5">
|
||||
<div className="flex items-start gap-2 p-3 bg-neutral-50 rounded-lg border border-neutral-200 mt-3">
|
||||
<div className="text-neutral-500 mt-0.5">
|
||||
<svg className="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
@@ -684,7 +699,7 @@ export function DomainsSettings({projectId}: DomainsSettingsProps) {
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-xs text-blue-900">
|
||||
<p className="text-xs text-neutral-600">
|
||||
Click the copy icon to copy record values. After adding all records to your DNS
|
||||
provider, use the refresh button above to verify your domain.
|
||||
</p>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import type {LucideIcon} from 'lucide-react';
|
||||
import type {ReactNode} from 'react';
|
||||
|
||||
interface EmptyStateProps {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
description: string;
|
||||
action?: ReactNode;
|
||||
}
|
||||
|
||||
export function EmptyState({icon: Icon, title, description, action}: EmptyStateProps) {
|
||||
return (
|
||||
<div className="text-center py-14">
|
||||
<div className="inline-flex items-center justify-center w-10 h-10 rounded-md border border-neutral-200 bg-neutral-50 mb-4">
|
||||
<Icon className="h-5 w-5 text-neutral-400" />
|
||||
</div>
|
||||
<h3 className="text-sm font-semibold text-neutral-900 mb-1">{title}</h3>
|
||||
<p className="text-sm text-neutral-500 max-w-xs mx-auto leading-relaxed mb-5">{description}</p>
|
||||
{action}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -51,7 +51,7 @@ function HelpResources() {
|
||||
<motion.button
|
||||
onClick={copyEmail}
|
||||
whileTap={{scale: 0.97}}
|
||||
className="flex-1 relative flex items-center justify-center gap-1.5 rounded-md border border-neutral-200 bg-white px-3 py-1.5 text-xs font-medium text-neutral-700 overflow-hidden transition-colors hover:bg-neutral-50 hover:text-neutral-900 hover:border-neutral-300"
|
||||
className="flex-1 relative flex items-center justify-center gap-1.5 h-9 rounded-md border border-neutral-200 bg-white px-3 text-sm font-medium text-neutral-700 overflow-hidden transition-colors hover:bg-neutral-50 hover:text-neutral-900 hover:border-neutral-300"
|
||||
>
|
||||
<AnimatePresence mode="wait" initial={false}>
|
||||
{copied ? (
|
||||
@@ -230,8 +230,7 @@ export function QuickStart({setupState, isLoading}: QuickStartProps) {
|
||||
<div className="flex-1 pt-0.5">
|
||||
<p className="text-sm font-semibold text-green-900 mb-1">All set!</p>
|
||||
<p className="text-xs text-green-700 leading-relaxed">
|
||||
Your project is fully configured and you're actively engaging your audience. Keep up the great
|
||||
work!
|
||||
Domain verified, contacts imported, campaigns running. Everything is set up correctly.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -775,19 +775,9 @@ interface SegmentFilterBuilderProps {
|
||||
export function SegmentFilterBuilder({condition, onChange}: SegmentFilterBuilderProps) {
|
||||
const {fields, loading} = useAvailableOptions();
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-neutral-900">Filter Conditions</h3>
|
||||
<p className="text-sm text-neutral-500 mt-1">Build complex audience filters with AND/OR logic</p>
|
||||
</div>
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="text-sm text-neutral-500 py-4">Loading available fields and events...</div>
|
||||
) : (
|
||||
<FilterConditionComponent condition={condition} onChange={onChange} availableFields={fields} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
if (loading) {
|
||||
return <div className="text-sm text-neutral-500 py-4">Loading available fields and events...</div>;
|
||||
}
|
||||
|
||||
return <FilterConditionComponent condition={condition} onChange={onChange} availableFields={fields} />;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {useState} from 'react';
|
||||
import useSWR from 'swr';
|
||||
import {
|
||||
Alert,
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
@@ -40,7 +39,8 @@ import {
|
||||
TableRow,
|
||||
} from '@plunk/ui';
|
||||
import {MembershipSchemas} from '@plunk/shared';
|
||||
import {AlertTriangle, Mail, MoreVertical, Trash2, UserPlus} from 'lucide-react';
|
||||
import {MoreVertical, Trash2, UserPlus} from 'lucide-react';
|
||||
import {AnimatePresence, motion} from 'framer-motion';
|
||||
import {useForm} from 'react-hook-form';
|
||||
import {zodResolver} from '@hookform/resolvers/zod';
|
||||
import type {z} from 'zod';
|
||||
@@ -173,23 +173,30 @@ export function TeamSettings({projectId, currentUserRole, currentUserId}: TeamSe
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{success && (
|
||||
<Alert>
|
||||
<Mail className="h-4 w-4" />
|
||||
<div className="ml-2">
|
||||
<p className="text-sm font-medium">{success}</p>
|
||||
</div>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<div className="ml-2">
|
||||
<p className="text-sm font-medium">{error}</p>
|
||||
</div>
|
||||
</Alert>
|
||||
)}
|
||||
<AnimatePresence mode="wait">
|
||||
{success && (
|
||||
<motion.div
|
||||
key="success"
|
||||
initial={{opacity: 0, y: -10}}
|
||||
animate={{opacity: 1, y: 0}}
|
||||
exit={{opacity: 0}}
|
||||
className="p-3 bg-green-50 border border-green-200 rounded-lg text-sm text-green-800"
|
||||
>
|
||||
{success}
|
||||
</motion.div>
|
||||
)}
|
||||
{error && (
|
||||
<motion.div
|
||||
key="error"
|
||||
initial={{opacity: 0, y: -10}}
|
||||
animate={{opacity: 1, y: 0}}
|
||||
exit={{opacity: 0}}
|
||||
className="p-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-800"
|
||||
>
|
||||
{error}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -214,10 +221,10 @@ export function TeamSettings({projectId, currentUserRole, currentUserId}: TeamSe
|
||||
<CardContent>
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-8">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-gray-300 border-t-blue-600" />
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-neutral-200 border-t-neutral-900" />
|
||||
</div>
|
||||
) : members.length === 0 ? (
|
||||
<div className="py-8 text-center text-sm text-gray-500">No members found</div>
|
||||
<div className="py-8 text-center text-sm text-neutral-500">No members found</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
@@ -314,12 +321,7 @@ export function TeamSettings({projectId, currentUserRole, currentUserId}: TeamSe
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleAddMember)} className="space-y-4">
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<div className="ml-2">
|
||||
<p className="text-sm font-medium">{error}</p>
|
||||
</div>
|
||||
</Alert>
|
||||
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-800">{error}</div>
|
||||
)}
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
@@ -236,7 +236,7 @@ export function TemplateSelectionDialog({open, onOpenChange, onSelectTemplate}:
|
||||
{data?.data.map(template => (
|
||||
<Card
|
||||
key={template.id}
|
||||
className="cursor-pointer hover:border-primary/50 hover:shadow-md transition-all"
|
||||
className="cursor-pointer hover:border-neutral-400 transition-colors"
|
||||
onClick={() => handleTemplateClick(template)}
|
||||
>
|
||||
<CardHeader className="pb-3">
|
||||
@@ -295,35 +295,29 @@ export function TemplateSelectionDialog({open, onOpenChange, onSelectTemplate}:
|
||||
) : (
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
{/* Scrollable Content */}
|
||||
<div className="flex-1 overflow-y-auto space-y-6 pr-2">
|
||||
<div className="flex-1 overflow-y-auto pr-2">
|
||||
{/* Template Preview */}
|
||||
{selectedTemplate && (
|
||||
<Card className="bg-neutral-50">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<CardTitle className="text-base">{selectedTemplate.name}</CardTitle>
|
||||
<Badge
|
||||
className="capitalize"
|
||||
variant={selectedTemplate.type === 'MARKETING' ? 'info' : 'success'}
|
||||
>
|
||||
{selectedTemplate.type.toLowerCase()}
|
||||
</Badge>
|
||||
</div>
|
||||
{selectedTemplate.description && (
|
||||
<CardDescription className="text-xs">{selectedTemplate.description}</CardDescription>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<div className="pb-4 mb-1 border-b border-neutral-100">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-neutral-900">{selectedTemplate.name}</span>
|
||||
<Badge
|
||||
className="capitalize"
|
||||
variant={selectedTemplate.type === 'MARKETING' ? 'info' : 'success'}
|
||||
>
|
||||
{selectedTemplate.type.toLowerCase()}
|
||||
</Badge>
|
||||
</div>
|
||||
{selectedTemplate.description && (
|
||||
<p className="text-xs text-neutral-500 mt-1">{selectedTemplate.description}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Field Selection */}
|
||||
<div className="space-y-3">
|
||||
<div className="divide-y divide-neutral-100">
|
||||
<div
|
||||
className="flex items-center space-x-3 p-3 rounded-lg border border-neutral-200 hover:bg-neutral-50 transition-colors cursor-pointer"
|
||||
className="flex items-center gap-3 py-3 cursor-pointer hover:text-neutral-900 transition-colors"
|
||||
onClick={() => toggleField('subject')}
|
||||
>
|
||||
<Checkbox
|
||||
@@ -336,13 +330,13 @@ export function TemplateSelectionDialog({open, onOpenChange, onSelectTemplate}:
|
||||
Email Subject
|
||||
</Label>
|
||||
{selectedTemplate?.subject && (
|
||||
<p className="text-xs text-neutral-500 mt-0.5">{selectedTemplate.subject}</p>
|
||||
<p className="text-xs text-neutral-400 mt-0.5 truncate">{selectedTemplate.subject}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex items-center space-x-3 p-3 rounded-lg border border-neutral-200 hover:bg-neutral-50 transition-colors cursor-pointer"
|
||||
className="flex items-center gap-3 py-3 cursor-pointer hover:text-neutral-900 transition-colors"
|
||||
onClick={() => toggleField('body')}
|
||||
>
|
||||
<Checkbox id="body" checked={selectedFields.body} onCheckedChange={() => toggleField('body')} />
|
||||
@@ -350,12 +344,12 @@ export function TemplateSelectionDialog({open, onOpenChange, onSelectTemplate}:
|
||||
<Label htmlFor="body" className="text-sm font-medium cursor-pointer">
|
||||
Email Body
|
||||
</Label>
|
||||
<p className="text-xs text-neutral-500 mt-0.5">The full email content and design</p>
|
||||
<p className="text-xs text-neutral-400 mt-0.5">Full email content and design</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex items-center space-x-3 p-3 rounded-lg border border-neutral-200 hover:bg-neutral-50 transition-colors cursor-pointer"
|
||||
className="flex items-center gap-3 py-3 cursor-pointer hover:text-neutral-900 transition-colors"
|
||||
onClick={() => toggleField('from')}
|
||||
>
|
||||
<Checkbox id="from" checked={selectedFields.from} onCheckedChange={() => toggleField('from')} />
|
||||
@@ -364,13 +358,13 @@ export function TemplateSelectionDialog({open, onOpenChange, onSelectTemplate}:
|
||||
From Email
|
||||
</Label>
|
||||
{selectedTemplate?.from && (
|
||||
<p className="text-xs text-neutral-500 mt-0.5">{selectedTemplate.from}</p>
|
||||
<p className="text-xs text-neutral-400 mt-0.5">{selectedTemplate.from}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex items-center space-x-3 p-3 rounded-lg border border-neutral-200 hover:bg-neutral-50 transition-colors cursor-pointer"
|
||||
className="flex items-center gap-3 py-3 cursor-pointer hover:text-neutral-900 transition-colors"
|
||||
onClick={() => toggleField('fromName')}
|
||||
>
|
||||
<Checkbox
|
||||
@@ -383,13 +377,13 @@ export function TemplateSelectionDialog({open, onOpenChange, onSelectTemplate}:
|
||||
From Name
|
||||
</Label>
|
||||
{selectedTemplate?.fromName && (
|
||||
<p className="text-xs text-neutral-500 mt-0.5">{selectedTemplate.fromName}</p>
|
||||
<p className="text-xs text-neutral-400 mt-0.5">{selectedTemplate.fromName}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex items-center space-x-3 p-3 rounded-lg border border-neutral-200 hover:bg-neutral-50 transition-colors cursor-pointer"
|
||||
className="flex items-center gap-3 py-3 cursor-pointer hover:text-neutral-900 transition-colors"
|
||||
onClick={() => toggleField('replyTo')}
|
||||
>
|
||||
<Checkbox
|
||||
@@ -402,7 +396,7 @@ export function TemplateSelectionDialog({open, onOpenChange, onSelectTemplate}:
|
||||
Reply-To Email
|
||||
</Label>
|
||||
{selectedTemplate?.replyTo && (
|
||||
<p className="text-xs text-neutral-500 mt-0.5">{selectedTemplate.replyTo}</p>
|
||||
<p className="text-xs text-neutral-400 mt-0.5">{selectedTemplate.replyTo}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -209,8 +209,8 @@ function AddStepNode({data}: {data: {label: string; onClick?: () => void}}) {
|
||||
/>
|
||||
|
||||
<div className="cursor-pointer hover:scale-105 transition-transform" onClick={data.onClick}>
|
||||
<div className="w-16 h-16 rounded-full bg-gradient-to-br from-neutral-100 to-neutral-200 border-2 border-dashed border-neutral-400 hover:border-neutral-600 hover:from-blue-50 hover:to-blue-100 hover:border-blue-400 flex items-center justify-center shadow-md transition-all">
|
||||
<Plus className="h-8 w-8 text-neutral-500 transition-colors" />
|
||||
<div className="w-16 h-16 rounded-full bg-neutral-100 border-2 border-dashed border-neutral-400 hover:border-neutral-600 hover:bg-white flex items-center justify-center transition-all">
|
||||
<Plus className="h-8 w-8 text-neutral-500 hover:text-neutral-700 transition-colors" />
|
||||
</div>
|
||||
{data.label && <div className="text-xs text-neutral-500 text-center mt-2 font-medium">{data.label}</div>}
|
||||
</div>
|
||||
@@ -255,7 +255,7 @@ function CustomNode({
|
||||
/>
|
||||
|
||||
<div
|
||||
className="px-5 py-4 rounded-xl border-2 bg-white shadow-lg hover:shadow-xl transition-all relative group"
|
||||
className="px-5 py-4 rounded-xl border-2 bg-white shadow-sm hover:shadow-md transition-all relative group"
|
||||
style={{
|
||||
borderColor: color,
|
||||
minWidth: '280px',
|
||||
@@ -274,7 +274,7 @@ function CustomNode({
|
||||
}}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-7 w-7 shadow-md"
|
||||
className="h-7 w-7"
|
||||
title="Edit trigger settings"
|
||||
>
|
||||
<Settings className="h-3.5 w-3.5" />
|
||||
@@ -290,7 +290,7 @@ function CustomNode({
|
||||
}}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-7 w-7 shadow-md"
|
||||
className="h-7 w-7"
|
||||
title="Edit step"
|
||||
>
|
||||
<Settings className="h-3.5 w-3.5" />
|
||||
@@ -302,7 +302,7 @@ function CustomNode({
|
||||
}}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-7 w-7 shadow-md hover:bg-red-50 hover:border-red-400"
|
||||
className="h-7 w-7 hover:bg-red-50 hover:border-red-400"
|
||||
title="Delete step"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
@@ -905,19 +905,19 @@ export function WorkflowBuilder({workflowId, steps, onUpdate}: WorkflowBuilderPr
|
||||
<Background color="#e5e7eb" gap={16} size={1} />
|
||||
<Controls
|
||||
showInteractive={false}
|
||||
className="bg-white/90 backdrop-blur-sm border border-neutral-200 rounded-lg shadow-lg"
|
||||
className="bg-white border border-neutral-200 rounded-lg shadow-md"
|
||||
/>
|
||||
<MiniMap
|
||||
nodeColor={node => {
|
||||
const step = steps.find(s => s.id === node.id);
|
||||
return step ? STEP_TYPE_COLORS[step.type as keyof typeof STEP_TYPE_COLORS] : '#6b7280';
|
||||
}}
|
||||
className="bg-white/90 backdrop-blur-sm border border-neutral-200 rounded-lg shadow-lg"
|
||||
className="bg-white border border-neutral-200 rounded-lg shadow-md"
|
||||
maskColor="rgba(0, 0, 0, 0.05)"
|
||||
/>
|
||||
<Panel
|
||||
position="top-left"
|
||||
className="bg-white/95 backdrop-blur-sm px-4 py-2.5 rounded-lg shadow-lg border border-neutral-200"
|
||||
className="bg-white px-4 py-2.5 rounded-lg shadow-md border border-neutral-200"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<GitBranch className="h-4 w-4 text-neutral-700" />
|
||||
@@ -933,7 +933,7 @@ export function WorkflowBuilder({workflowId, steps, onUpdate}: WorkflowBuilderPr
|
||||
<Panel position="top-right" className="flex gap-2">
|
||||
<button
|
||||
onClick={handleAutoLayout}
|
||||
className="bg-white/95 backdrop-blur-sm px-4 py-2 rounded-lg shadow-lg border border-neutral-200 text-sm font-medium text-neutral-700 hover:bg-white hover:text-neutral-900 transition-all"
|
||||
className="bg-white px-4 py-2 rounded-lg shadow-md border border-neutral-200 text-sm font-medium text-neutral-700 hover:bg-neutral-50 hover:text-neutral-900 transition-colors"
|
||||
>
|
||||
Auto Layout
|
||||
</button>
|
||||
@@ -941,11 +941,11 @@ export function WorkflowBuilder({workflowId, steps, onUpdate}: WorkflowBuilderPr
|
||||
{rawEdges.length === 0 && steps.length > 1 && (
|
||||
<Panel
|
||||
position="bottom-center"
|
||||
className="bg-blue-50 border border-blue-200 px-4 py-2.5 rounded-lg shadow-lg"
|
||||
className="bg-white border border-neutral-200 px-4 py-2.5 rounded-lg shadow-sm"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-sm text-blue-900">
|
||||
<div className="flex items-center gap-2 text-sm text-neutral-600">
|
||||
<Lightbulb className="h-4 w-4" />
|
||||
<span>Click the + buttons to add and connect steps!</span>
|
||||
<span>Click the + buttons to add and connect steps.</span>
|
||||
</div>
|
||||
</Panel>
|
||||
)}
|
||||
@@ -965,16 +965,7 @@ export function WorkflowBuilder({workflowId, steps, onUpdate}: WorkflowBuilderPr
|
||||
<button
|
||||
key={option.value}
|
||||
onClick={() => handleCreateStep(option.value)}
|
||||
className="flex flex-col items-center gap-2 p-4 rounded-lg border-2 border-neutral-200 hover:border-neutral-400 hover:bg-neutral-50 transition-all group"
|
||||
style={{
|
||||
borderColor: 'transparent',
|
||||
}}
|
||||
onMouseEnter={e => {
|
||||
e.currentTarget.style.borderColor = option.color;
|
||||
}}
|
||||
onMouseLeave={e => {
|
||||
e.currentTarget.style.borderColor = 'transparent';
|
||||
}}
|
||||
className="flex flex-col items-center gap-2 p-4 rounded-lg border border-neutral-200 hover:border-neutral-400 hover:bg-neutral-50 transition-all group"
|
||||
>
|
||||
<div
|
||||
className="w-12 h-12 rounded-lg flex items-center justify-center transition-transform group-hover:scale-110"
|
||||
|
||||
@@ -143,7 +143,7 @@ function CustomNode({
|
||||
/>
|
||||
|
||||
<div
|
||||
className="px-5 py-4 rounded-xl border-2 bg-white shadow-lg hover:shadow-xl transition-all cursor-grab active:cursor-grabbing"
|
||||
className="px-5 py-4 rounded-xl border-2 bg-white shadow-sm hover:shadow-md transition-all cursor-grab active:cursor-grabbing"
|
||||
style={{
|
||||
borderColor: color,
|
||||
minWidth: '250px',
|
||||
@@ -496,11 +496,11 @@ export function WorkflowVisualizer({steps}: WorkflowVisualizerProps) {
|
||||
<Background color="#e5e7eb" gap={16} size={1} />
|
||||
<Controls
|
||||
showInteractive={false}
|
||||
className="bg-white/90 backdrop-blur-sm border border-neutral-200 rounded-lg shadow-lg"
|
||||
className="bg-white border border-neutral-200 rounded-lg shadow-md"
|
||||
/>
|
||||
<Panel
|
||||
position="top-left"
|
||||
className="bg-white/95 backdrop-blur-sm px-4 py-2.5 rounded-lg shadow-lg border border-neutral-200"
|
||||
className="bg-white px-4 py-2.5 rounded-lg shadow-md border border-neutral-200"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<GitBranch className="h-4 w-4 text-neutral-700" />
|
||||
@@ -516,9 +516,9 @@ export function WorkflowVisualizer({steps}: WorkflowVisualizerProps) {
|
||||
{rawEdges.length === 0 && steps.length > 1 && (
|
||||
<Panel
|
||||
position="bottom-center"
|
||||
className="bg-amber-50 border border-amber-200 px-4 py-2.5 rounded-lg shadow-lg"
|
||||
className="bg-white border border-neutral-200 px-4 py-2.5 rounded-lg shadow-sm"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-sm text-amber-900">
|
||||
<div className="flex items-center gap-2 text-sm text-neutral-600">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<span>No transitions found. Connect your steps to see the flow.</span>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user