From c1bcd358cc611594d851414687c04bf7a03759a1 Mon Sep 17 00:00:00 2001 From: Dries Augustyns Date: Fri, 24 Apr 2026 13:35:25 +0200 Subject: [PATCH] fix: Consistency across buttons and labels --- apps/web/src/components/ActivityFeed.tsx | 3 +- apps/web/src/components/ActivityItem.tsx | 52 +++++++++--------- .../web/src/components/BillingConsumption.tsx | 16 +++--- apps/web/src/components/BillingInvoices.tsx | 8 +-- apps/web/src/components/BillingLimits.tsx | 16 +++--- .../components/CampaignSelectionDialog.tsx | 25 ++++----- .../src/components/DataManagementSettings.tsx | 2 +- apps/web/src/components/DomainsSettings.tsx | 5 +- apps/web/src/components/EmptyState.tsx | 23 -------- apps/web/src/components/KeyValueEditor.tsx | 8 +-- apps/web/src/components/SecuritySettings.tsx | 2 +- .../src/components/SegmentFilterBuilder.tsx | 26 +++------ .../components/TemplateSelectionDialog.tsx | 11 ++-- .../src/components/UnpaidInvoiceBanner.tsx | 7 +-- apps/web/src/pages/activity/index.tsx | 16 +++--- apps/web/src/pages/analytics/index.tsx | 54 ++++++++----------- apps/web/src/pages/campaigns/index.tsx | 18 +++---- apps/web/src/pages/contacts/index.tsx | 2 +- apps/web/src/pages/index.tsx | 7 +-- apps/web/src/pages/onboarding/developer.tsx | 4 +- apps/web/src/pages/onboarding/workflows.tsx | 4 +- apps/web/src/pages/segments/[id].tsx | 14 ++--- apps/web/src/pages/segments/index.tsx | 4 +- apps/web/src/pages/segments/new.tsx | 2 +- apps/web/src/pages/settings/index.tsx | 27 +++++----- apps/web/src/pages/templates/index.tsx | 4 +- apps/web/src/pages/workflows/[id].tsx | 44 +++++++-------- apps/web/src/pages/workflows/index.tsx | 4 +- packages/ui/src/components/atoms/Alert.tsx | 5 +- packages/ui/src/components/atoms/Badge.tsx | 13 ++--- packages/ui/src/components/atoms/Button.tsx | 3 +- packages/ui/src/components/atoms/Card.tsx | 2 +- packages/ui/src/components/atoms/Checkbox.tsx | 2 +- packages/ui/src/components/atoms/Dialog.tsx | 2 +- packages/ui/src/components/atoms/Form.tsx | 2 +- .../ui/src/components/atoms/RadioGroup.tsx | 2 +- packages/ui/src/components/atoms/Select.tsx | 2 +- packages/ui/src/components/atoms/Switch.tsx | 2 +- packages/ui/src/components/atoms/Tabs.tsx | 4 +- .../src/components/molecules/EmptyState.tsx | 24 ++++----- 40 files changed, 195 insertions(+), 276 deletions(-) delete mode 100644 apps/web/src/components/EmptyState.tsx diff --git a/apps/web/src/components/ActivityFeed.tsx b/apps/web/src/components/ActivityFeed.tsx index 213108d..59849fd 100644 --- a/apps/web/src/components/ActivityFeed.tsx +++ b/apps/web/src/components/ActivityFeed.tsx @@ -1,8 +1,7 @@ -import {Button, IconSpinner} from '@plunk/ui'; +import {Button, EmptyState, IconSpinner} from '@plunk/ui'; import type {Activity, CursorPaginatedResponse} from '@plunk/types'; import {network} from '../lib/network'; import {ActivityItem} from './ActivityItem'; -import {EmptyState} from './EmptyState'; import {Activity as ActivityIcon} from 'lucide-react'; import {useCallback, useEffect, useMemo, useState} from 'react'; diff --git a/apps/web/src/components/ActivityItem.tsx b/apps/web/src/components/ActivityItem.tsx index 9d90c98..b66892d 100644 --- a/apps/web/src/components/ActivityItem.tsx +++ b/apps/web/src/components/ActivityItem.tsx @@ -133,8 +133,8 @@ function getActivityConfig(activity: Activity): ActivityConfig { case 'event.triggered': return { icon: Zap, - color: 'text-blue-600', - bgColor: 'bg-blue-100', + color: 'text-neutral-600', + bgColor: 'bg-neutral-100', title: (typeof metadata.eventName === 'string' ? metadata.eventName : undefined) || 'Event triggered', description: undefined, badge: { @@ -150,8 +150,8 @@ function getActivityConfig(activity: Activity): ActivityConfig { case 'email.sent': return { icon: Send, - color: 'text-green-600', - bgColor: 'bg-green-100', + color: 'text-green-700', + bgColor: 'bg-green-50', title: (typeof metadata.subject === 'string' ? metadata.subject : undefined) || 'Email sent', description: metadata.campaignName ? `Campaign: ${String(metadata.campaignName)}` @@ -169,8 +169,8 @@ function getActivityConfig(activity: Activity): ActivityConfig { case 'email.delivered': return { icon: CheckCircle, - color: 'text-green-600', - bgColor: 'bg-green-100', + color: 'text-green-700', + bgColor: 'bg-green-50', title: (typeof metadata.subject === 'string' ? metadata.subject : undefined) || 'Email delivered', description: metadata.campaignName ? `Campaign: ${String(metadata.campaignName)}` @@ -186,8 +186,8 @@ function getActivityConfig(activity: Activity): ActivityConfig { case 'email.received': return { icon: Inbox, - color: 'text-blue-600', - bgColor: 'bg-blue-100', + color: 'text-neutral-600', + bgColor: 'bg-neutral-100', title: (typeof metadata.subject === 'string' ? metadata.subject : undefined) || 'Email received', description: typeof metadata.from === 'string' ? `From: ${metadata.from}` : 'Inbound email', badge: { @@ -199,8 +199,8 @@ function getActivityConfig(activity: Activity): ActivityConfig { case 'email.opened': return { icon: Eye, - color: 'text-purple-600', - bgColor: 'bg-purple-100', + color: 'text-neutral-600', + bgColor: 'bg-neutral-100', title: (typeof metadata.subject === 'string' ? metadata.subject : undefined) || 'Email opened', description: typeof metadata.totalOpens === 'number' && metadata.totalOpens > 1 @@ -219,8 +219,8 @@ function getActivityConfig(activity: Activity): ActivityConfig { case 'email.clicked': return { icon: MousePointerClick, - color: 'text-orange-600', - bgColor: 'bg-orange-100', + color: 'text-neutral-600', + bgColor: 'bg-neutral-100', title: (typeof metadata.subject === 'string' ? metadata.subject : undefined) || 'Email clicked', description: typeof metadata.totalClicks === 'number' && metadata.totalClicks > 1 @@ -239,8 +239,8 @@ function getActivityConfig(activity: Activity): ActivityConfig { case 'email.bounced': return { icon: XCircle, - color: 'text-red-600', - bgColor: 'bg-red-100', + color: 'text-red-700', + bgColor: 'bg-red-50', title: (typeof metadata.subject === 'string' ? metadata.subject : undefined) || 'Email bounced', description: (typeof metadata.error === 'string' ? metadata.error : undefined) || 'Email failed to deliver', badge: { @@ -252,8 +252,8 @@ function getActivityConfig(activity: Activity): ActivityConfig { case 'email.complaint': return { icon: ShieldAlert, - color: 'text-red-600', - bgColor: 'bg-red-100', + color: 'text-red-700', + bgColor: 'bg-red-50', title: (typeof metadata.subject === 'string' ? metadata.subject : undefined) || 'Spam complaint', description: metadata.campaignName ? `Campaign: ${String(metadata.campaignName)}` @@ -269,8 +269,8 @@ function getActivityConfig(activity: Activity): ActivityConfig { case 'workflow.started': return { icon: Workflow, - color: 'text-indigo-600', - bgColor: 'bg-indigo-100', + color: 'text-neutral-600', + bgColor: 'bg-neutral-100', title: (typeof metadata.workflowName === 'string' ? metadata.workflowName : undefined) || 'Workflow started', description: `Status: ${String(metadata.status || 'unknown')}`, badge: { @@ -282,8 +282,8 @@ function getActivityConfig(activity: Activity): ActivityConfig { case 'workflow.completed': return { icon: CheckCheck, - color: 'text-green-600', - bgColor: 'bg-green-100', + color: 'text-green-700', + bgColor: 'bg-green-50', title: (typeof metadata.workflowName === 'string' ? metadata.workflowName : undefined) || 'Workflow completed', description: metadata.exitReason ? `Exit: ${String(metadata.exitReason)}` @@ -297,8 +297,8 @@ function getActivityConfig(activity: Activity): ActivityConfig { case 'campaign.scheduled': return { icon: Calendar, - color: 'text-blue-600', - bgColor: 'bg-blue-50', + color: 'text-neutral-600', + bgColor: 'bg-neutral-100', title: (typeof metadata.campaignName === 'string' ? metadata.campaignName : undefined) || 'Campaign scheduled', description: metadata.subject ? `${String(metadata.subject)}${metadata.totalRecipients ? ` • ${metadata.totalRecipients} recipients` : ''}` @@ -314,8 +314,8 @@ function getActivityConfig(activity: Activity): ActivityConfig { case 'workflow.email.scheduled': return { icon: Calendar, - color: 'text-indigo-600', - bgColor: 'bg-indigo-50', + color: 'text-neutral-600', + bgColor: 'bg-neutral-100', title: (typeof metadata.stepName === 'string' ? metadata.stepName : undefined) || 'Workflow email scheduled', description: metadata.workflowName ? `Workflow: ${String(metadata.workflowName)}${metadata.subject ? ` • ${String(metadata.subject)}` : ''}` @@ -353,7 +353,7 @@ export const ActivityItem = memo(function ActivityItem({activity, isUpcoming = f
{/* Icon */}
@@ -410,7 +410,7 @@ export const ActivityItem = memo(function ActivityItem({activity, isUpcoming = f )}
{relativeTime} diff --git a/apps/web/src/components/BillingConsumption.tsx b/apps/web/src/components/BillingConsumption.tsx index af91581..ff3c596 100644 --- a/apps/web/src/components/BillingConsumption.tsx +++ b/apps/web/src/components/BillingConsumption.tsx @@ -111,8 +111,8 @@ export function BillingConsumption({projectId, hasSubscription}: BillingConsumpt {/* Total Usage */}
-
- +
+

Total Emails Sent

@@ -123,19 +123,19 @@ export function BillingConsumption({projectId, hasSubscription}: BillingConsumpt {/* Account Credits */} {consumptionData.credits && consumptionData.credits.hasCredits && ( -
+
-
- +
+
-

Account Credits

-

+

Account Credits

+

{formatCurrency(consumptionData.credits.creditAmount, consumptionData.credits.currency)}

-

+

Credits will be automatically applied to your upcoming invoices

diff --git a/apps/web/src/components/BillingInvoices.tsx b/apps/web/src/components/BillingInvoices.tsx index 33a8fff..b4f7ed5 100644 --- a/apps/web/src/components/BillingInvoices.tsx +++ b/apps/web/src/components/BillingInvoices.tsx @@ -90,16 +90,16 @@ export function BillingInvoices({projectId, hasSubscription, onManageBilling}: B const getStatusBadge = (status: string, paid: boolean) => { // Stripe invoices have status 'paid' when paid, or paid boolean is true if (paid || status === 'paid') { - return Paid; + return Paid; } switch (status) { case 'open': - return Unpaid; + return Unpaid; case 'draft': return Draft; case 'uncollectible': - return Uncollectible; + return Uncollectible; case 'void': return Void; default: @@ -169,7 +169,7 @@ export function BillingInvoices({projectId, hasSubscription, onManageBilling}: B Total:{' '} {formatCurrency(invoice.total, invoice.currency)}

-

+

Amount Due:{' '} {formatCurrency(invoice.amountDue, invoice.currency)}

diff --git a/apps/web/src/components/BillingLimits.tsx b/apps/web/src/components/BillingLimits.tsx index eb9013c..48e52ab 100644 --- a/apps/web/src/components/BillingLimits.tsx +++ b/apps/web/src/components/BillingLimits.tsx @@ -384,15 +384,15 @@ interface UsageDisplayProps { const UsageDisplay = memo(function UsageDisplay({category, usage, currency}: UsageDisplayProps) { const statusColor = useMemo(() => { - if (usage.isBlocked) return 'text-red-600'; - if (usage.isWarning) return 'text-orange-600'; - return 'text-green-600'; + if (usage.isBlocked) return 'text-red-700'; + if (usage.isWarning) return 'text-amber-700'; + return 'text-neutral-600'; }, [usage.isBlocked, usage.isWarning]); const progressColor = useMemo(() => { if (usage.isBlocked) return 'bg-red-600'; - if (usage.isWarning) return 'bg-orange-500'; - return 'bg-green-600'; + if (usage.isWarning) return 'bg-amber-500'; + return 'bg-neutral-900'; }, [usage.isBlocked, usage.isWarning]); const statusIcon = useMemo(() => { @@ -440,10 +440,10 @@ const UsageDisplay = memo(function UsageDisplay({category, usage, currency}: Usa {usage.isBlocked && ( - +
-

+

Limit reached: No more {category.toLowerCase()} emails can be sent this month.

@@ -451,7 +451,7 @@ const UsageDisplay = memo(function UsageDisplay({category, usage, currency}: Usa )} {usage.isWarning && !usage.isBlocked && ( - +

diff --git a/apps/web/src/components/CampaignSelectionDialog.tsx b/apps/web/src/components/CampaignSelectionDialog.tsx index e4e8841..94f37dc 100644 --- a/apps/web/src/components/CampaignSelectionDialog.tsx +++ b/apps/web/src/components/CampaignSelectionDialog.tsx @@ -12,6 +12,7 @@ import { DialogDescription, DialogHeader, DialogTitle, + EmptyState, Label, Select, SelectContent, @@ -26,7 +27,6 @@ import type {PaginatedResponse} from '@plunk/types'; import {ArrowLeft, Calendar, Mail, Users} from 'lucide-react'; import {useState} from 'react'; import useSWR from 'swr'; -import {EmptyState} from './EmptyState'; interface CampaignSelectionDialogProps { open: boolean; @@ -97,23 +97,16 @@ export function CampaignSelectionDialog({open, onOpenChange, onSelectCampaign}: }; const getStatusBadge = (status: CampaignStatus) => { - const variants: Record< - CampaignStatus, - {variant: 'default' | 'secondary' | 'destructive' | 'outline'; label: string; className?: string} - > = { - DRAFT: {variant: 'secondary', label: 'Draft', className: 'bg-neutral-100 text-neutral-700'}, - SCHEDULED: {variant: 'default', label: 'Scheduled', className: 'bg-blue-100 text-blue-700'}, - SENDING: {variant: 'default', label: 'Sending', className: 'bg-purple-100 text-purple-700'}, - SENT: {variant: 'default', label: 'Sent', className: 'bg-green-100 text-green-700'}, - CANCELLED: {variant: 'destructive', label: 'Cancelled', className: 'bg-red-100 text-red-700'}, + const variants: Record = { + DRAFT: {variant: 'neutral', label: 'Draft'}, + SCHEDULED: {variant: 'default', label: 'Scheduled'}, + SENDING: {variant: 'default', label: 'Sending'}, + SENT: {variant: 'success', label: 'Sent'}, + CANCELLED: {variant: 'neutral', label: 'Cancelled'}, }; const config = variants[status]; - return ( - - {config.label} - - ); + return {config.label}; }; const getAudienceLabel = (campaign: Campaign) => { @@ -132,7 +125,7 @@ export function CampaignSelectionDialog({open, onOpenChange, onSelectCampaign}: {step === 'configure' && ( - )} diff --git a/apps/web/src/components/DataManagementSettings.tsx b/apps/web/src/components/DataManagementSettings.tsx index 10b0474..0da3fb3 100644 --- a/apps/web/src/components/DataManagementSettings.tsx +++ b/apps/web/src/components/DataManagementSettings.tsx @@ -14,6 +14,7 @@ import { DialogFooter, DialogHeader, DialogTitle, + EmptyState, Table, TableBody, TableCell, @@ -23,7 +24,6 @@ import { IconSpinner, } from '@plunk/ui'; import {AlertCircle, Database, Trash2, Zap} from 'lucide-react'; -import {EmptyState} from './EmptyState'; import {toast} from 'sonner'; import useSWR from 'swr'; import {useActiveProject} from '../lib/contexts/ActiveProjectProvider'; diff --git a/apps/web/src/components/DomainsSettings.tsx b/apps/web/src/components/DomainsSettings.tsx index 2449337..a2bc5cb 100644 --- a/apps/web/src/components/DomainsSettings.tsx +++ b/apps/web/src/components/DomainsSettings.tsx @@ -11,6 +11,7 @@ import { CardHeader, CardTitle, ConfirmDialog, + EmptyState, Form, FormControl, FormField, @@ -22,7 +23,6 @@ import { } from '@plunk/ui'; import {AnimatePresence, motion} from 'framer-motion'; import {Check, CheckCircle2, ChevronDown, Copy, Globe, RefreshCw, Trash2, XCircle} from 'lucide-react'; -import {EmptyState} from './EmptyState'; import {useConfig} from '../lib/hooks/useConfig'; import {useAddDomain, useCheckDomainVerification, useDomains, useRemoveDomain} from '../lib/hooks/useDomains'; @@ -369,13 +369,12 @@ export function DomainsSettings({projectId}: DomainsSettingsProps) { )} diff --git a/apps/web/src/components/EmptyState.tsx b/apps/web/src/components/EmptyState.tsx deleted file mode 100644 index 5d1f01a..0000000 --- a/apps/web/src/components/EmptyState.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import type {LucideIcon} from 'lucide-react'; -import type {ReactNode} from 'react'; - -interface EmptyStateProps { - icon: LucideIcon; - title: string; - description: string; - action?: ReactNode; - className?: string; -} - -export function EmptyState({icon: Icon, title, description, action, className}: EmptyStateProps) { - return ( -

-
- -
-

{title}

-

{description}

- {action} -
- ); -} diff --git a/apps/web/src/components/KeyValueEditor.tsx b/apps/web/src/components/KeyValueEditor.tsx index 27c1d28..8d405e3 100644 --- a/apps/web/src/components/KeyValueEditor.tsx +++ b/apps/web/src/components/KeyValueEditor.tsx @@ -114,13 +114,7 @@ export function KeyValueEditor({initialData, onChange}: KeyValueEditorProps) { className="text-sm" />
-
diff --git a/apps/web/src/components/SecuritySettings.tsx b/apps/web/src/components/SecuritySettings.tsx index 0153d0c..ca9093e 100644 --- a/apps/web/src/components/SecuritySettings.tsx +++ b/apps/web/src/components/SecuritySettings.tsx @@ -9,7 +9,7 @@ interface SecuritySettingsProps { const STATUS_CONFIG: Record = { healthy: {color: 'text-green-600', icon: CheckCircle, bg: 'bg-green-100', label: 'Healthy'}, - warning: {color: 'text-orange-600', icon: AlertTriangle, bg: 'bg-orange-100', label: 'Warning'}, + warning: {color: 'text-amber-700', icon: AlertTriangle, bg: 'bg-amber-100', label: 'Warning'}, critical: {color: 'text-red-600', icon: AlertCircle, bg: 'bg-red-100', label: 'Critical'}, }; diff --git a/apps/web/src/components/SegmentFilterBuilder.tsx b/apps/web/src/components/SegmentFilterBuilder.tsx index 82d9d22..97b43da 100644 --- a/apps/web/src/components/SegmentFilterBuilder.tsx +++ b/apps/web/src/components/SegmentFilterBuilder.tsx @@ -528,13 +528,7 @@ const FilterRow = memo(function FilterRow({filter, onChange, onRemove, available
-
@@ -603,12 +597,12 @@ function FilterGroupComponent({group, onChange, onRemove, depth = 0, availableFi onChange(rest); }, [group, onChange]); - const bgColors = ['bg-white', 'bg-blue-50/50', 'bg-purple-50/50', 'bg-green-50/50']; - const borderColors = ['border-neutral-300', 'border-blue-300', 'border-purple-300', 'border-green-300']; + const bgColors = ['bg-white', 'bg-neutral-50', 'bg-white', 'bg-neutral-50']; + const borderColors = ['border-neutral-200', 'border-neutral-200', 'border-neutral-300', 'border-neutral-300']; return (
@@ -616,13 +610,7 @@ function FilterGroupComponent({group, onChange, onRemove, depth = 0, availableFi Filter Group {depth > 0 && `(Nested)`}
{onRemove && ( - )} @@ -645,10 +633,10 @@ function FilterGroupComponent({group, onChange, onRemove, depth = 0, availableFi Nested Conditions diff --git a/apps/web/src/components/TemplateSelectionDialog.tsx b/apps/web/src/components/TemplateSelectionDialog.tsx index b64076d..bd597a1 100644 --- a/apps/web/src/components/TemplateSelectionDialog.tsx +++ b/apps/web/src/components/TemplateSelectionDialog.tsx @@ -12,6 +12,7 @@ import { DialogDescription, DialogHeader, DialogTitle, + EmptyState, IconSpinner, Input, Label, @@ -21,7 +22,6 @@ import type {PaginatedResponse} from '@plunk/types'; import {ArrowLeft, FileText, Search} from 'lucide-react'; import {useState} from 'react'; import useSWR from 'swr'; -import {EmptyState} from './EmptyState'; interface TemplateSelectionDialogProps { open: boolean; @@ -106,7 +106,7 @@ export function TemplateSelectionDialog({open, onOpenChange, onSelectTemplate}: {step === 'configure' && ( - )} @@ -228,7 +228,7 @@ export function TemplateSelectionDialog({open, onOpenChange, onSelectTemplate}:
{template.name} - + {template.type.toLowerCase()}
@@ -285,10 +285,7 @@ export function TemplateSelectionDialog({open, onOpenChange, onSelectTemplate}:
{selectedTemplate.name} - + {selectedTemplate.type.toLowerCase()}
diff --git a/apps/web/src/components/UnpaidInvoiceBanner.tsx b/apps/web/src/components/UnpaidInvoiceBanner.tsx index 4eb7422..c87a7e9 100644 --- a/apps/web/src/components/UnpaidInvoiceBanner.tsx +++ b/apps/web/src/components/UnpaidInvoiceBanner.tsx @@ -73,12 +73,7 @@ export function UnpaidInvoiceBanner({projectId, hasSubscription}: UnpaidInvoiceB Please pay your outstanding invoices to avoid service interruption.

- diff --git a/apps/web/src/pages/activity/index.tsx b/apps/web/src/pages/activity/index.tsx index a3266bf..b928c39 100644 --- a/apps/web/src/pages/activity/index.tsx +++ b/apps/web/src/pages/activity/index.tsx @@ -42,32 +42,32 @@ export default function ActivityPage() { value: stats?.totalEvents?.toLocaleString() || '0', icon: Zap, description: 'Last 30 days', - color: 'text-blue-600', - bgColor: 'bg-blue-100', + color: 'text-neutral-600', + bgColor: 'bg-neutral-100', }, { name: 'Emails Sent', value: stats?.totalEmailsSent?.toLocaleString() || '0', icon: Send, description: 'Last 30 days', - color: 'text-green-600', - bgColor: 'bg-green-100', + color: 'text-neutral-600', + bgColor: 'bg-neutral-100', }, { name: 'Open Rate', value: stats?.openRate ? `${stats.openRate.toFixed(1)}%` : '0%', icon: Eye, description: `${stats?.totalEmailsOpened?.toLocaleString() || '0'} opens`, - color: 'text-purple-600', - bgColor: 'bg-purple-100', + color: 'text-neutral-600', + bgColor: 'bg-neutral-100', }, { name: 'Click Rate', value: stats?.clickRate ? `${stats.clickRate.toFixed(1)}%` : '0%', icon: MousePointerClick, description: `${stats?.totalEmailsClicked?.toLocaleString() || '0'} clicks`, - color: 'text-orange-600', - bgColor: 'bg-orange-100', + color: 'text-neutral-600', + bgColor: 'bg-neutral-100', }, ]; diff --git a/apps/web/src/pages/analytics/index.tsx b/apps/web/src/pages/analytics/index.tsx index 231c182..1be4b01 100644 --- a/apps/web/src/pages/analytics/index.tsx +++ b/apps/web/src/pages/analytics/index.tsx @@ -17,8 +17,8 @@ import { SelectTrigger, SelectValue, } from '@plunk/ui'; +import {EmptyState} from '@plunk/ui'; import {DashboardLayout} from '../../components/DashboardLayout'; -import {EmptyState} from '../../components/EmptyState'; import {useAnalytics} from '../../lib/hooks/useAnalytics'; import useSWR from 'swr'; import { @@ -149,8 +149,8 @@ export default function AnalyticsPage() { value: stats?.totalEmailsSent?.toLocaleString() || cumulativeTotals.emails.toLocaleString(), icon: Send, description: `Last ${days} days`, - color: 'text-blue-600', - bgColor: 'bg-blue-100', + color: 'text-neutral-600', + bgColor: 'bg-neutral-100', trend: stats?.totalEmailsSent ? (stats.totalEmailsSent > 0 ? 'positive' : 'neutral') : 'neutral', }, { @@ -158,8 +158,8 @@ export default function AnalyticsPage() { value: stats?.openRate ? `${stats.openRate.toFixed(1)}%` : '0%', icon: Eye, description: `${stats?.totalEmailsOpened?.toLocaleString() || cumulativeTotals.opens.toLocaleString()} opens`, - color: 'text-green-600', - bgColor: 'bg-green-100', + color: 'text-neutral-600', + bgColor: 'bg-neutral-100', trend: stats?.openRate && stats.openRate > 20 ? 'positive' : 'neutral', }, { @@ -167,8 +167,8 @@ export default function AnalyticsPage() { value: stats?.clickRate ? `${stats.clickRate.toFixed(1)}%` : '0%', icon: MousePointerClick, description: `${stats?.totalEmailsClicked?.toLocaleString() || cumulativeTotals.clicks.toLocaleString()} clicks`, - color: 'text-purple-600', - bgColor: 'bg-purple-100', + color: 'text-neutral-600', + bgColor: 'bg-neutral-100', trend: stats?.clickRate && stats.clickRate > 3 ? 'positive' : 'neutral', }, { @@ -176,8 +176,8 @@ export default function AnalyticsPage() { value: campaignStats?.active?.toLocaleString() || '0', icon: Megaphone, description: `${campaignStats?.total || 0} total campaigns`, - color: 'text-indigo-600', - bgColor: 'bg-indigo-100', + color: 'text-neutral-600', + bgColor: 'bg-neutral-100', trend: campaignStats?.active ? 'positive' : 'neutral', }, { @@ -185,8 +185,8 @@ export default function AnalyticsPage() { value: stats?.totalWorkflowsStarted?.toLocaleString() || '0', icon: Activity, description: 'Automations triggered', - color: 'text-cyan-600', - bgColor: 'bg-cyan-100', + color: 'text-neutral-600', + bgColor: 'bg-neutral-100', trend: stats?.totalWorkflowsStarted && stats.totalWorkflowsStarted > 0 ? 'positive' : 'neutral', }, { @@ -194,8 +194,8 @@ export default function AnalyticsPage() { value: stats?.totalEvents?.toLocaleString() || '0', icon: Zap, description: 'Custom events tracked', - color: 'text-orange-600', - bgColor: 'bg-orange-100', + color: 'text-neutral-600', + bgColor: 'bg-neutral-100', trend: stats?.totalEvents && stats.totalEvents > 0 ? 'positive' : 'neutral', }, ]; @@ -457,8 +457,8 @@ export default function AnalyticsPage() {
-
- +
+

Open Rate

@@ -470,8 +470,8 @@ export default function AnalyticsPage() {
-
- +
+

Click Rate

@@ -483,8 +483,8 @@ export default function AnalyticsPage() {
-
- +
+

Engagement

@@ -509,8 +509,8 @@ export default function AnalyticsPage() {

{stats?.totalEvents?.toLocaleString() || '0'}

Total Events

-
- +
+
@@ -566,11 +566,7 @@ export default function AnalyticsPage() { 30 - ? 'text-green-600' - : campaign.openRate > 20 - ? 'text-blue-600' - : 'text-neutral-600' + campaign.openRate > 30 ? 'text-green-700' : 'text-neutral-700' }`} > {campaign.openRate.toFixed(1)}% @@ -579,11 +575,7 @@ export default function AnalyticsPage() { 5 - ? 'text-green-600' - : campaign.clickRate > 3 - ? 'text-blue-600' - : 'text-neutral-600' + campaign.clickRate > 5 ? 'text-green-700' : 'text-neutral-700' }`} > {campaign.clickRate.toFixed(1)}% diff --git a/apps/web/src/pages/campaigns/index.tsx b/apps/web/src/pages/campaigns/index.tsx index bd6ac8f..5579fcb 100644 --- a/apps/web/src/pages/campaigns/index.tsx +++ b/apps/web/src/pages/campaigns/index.tsx @@ -17,8 +17,8 @@ import { import type {Campaign, Template} from '@plunk/db'; import {CampaignStatus} from '@plunk/db'; import type {PaginatedResponse} from '@plunk/types'; +import {EmptyState} from '@plunk/ui'; import {DashboardLayout} from '../../components/DashboardLayout'; -import {EmptyState} from '../../components/EmptyState'; import {TemplateSelectionDialog} from '../../components/TemplateSelectionDialog'; import {CampaignSelectionDialog} from '../../components/CampaignSelectionDialog'; import {network} from '../../lib/network'; @@ -49,15 +49,15 @@ export default function CampaignsPage() { ); const getStatusBadge = (status: CampaignStatus) => { - const config: Record = { - DRAFT: {label: 'Draft', className: 'bg-neutral-100 text-neutral-600 border-transparent'}, - SCHEDULED: {label: 'Scheduled', className: 'bg-blue-100 text-blue-700 border-transparent'}, - SENDING: {label: 'Sending', className: 'bg-purple-100 text-purple-700 border-transparent'}, - SENT: {label: 'Sent', className: 'bg-green-100 text-green-700 border-transparent'}, - CANCELLED: {label: 'Cancelled', className: 'bg-red-100 text-red-700 border-transparent'}, + const config: Record = { + DRAFT: {label: 'Draft', variant: 'neutral'}, + SCHEDULED: {label: 'Scheduled', variant: 'default'}, + SENDING: {label: 'Sending', variant: 'default'}, + SENT: {label: 'Sent', variant: 'success'}, + CANCELLED: {label: 'Cancelled', variant: 'neutral'}, }; - const {label, className} = config[status]; - return {label}; + const {label, variant} = config[status]; + return {label}; }; const handleCancel = async () => { diff --git a/apps/web/src/pages/contacts/index.tsx b/apps/web/src/pages/contacts/index.tsx index 6dab53a..cdb8a9e 100644 --- a/apps/web/src/pages/contacts/index.tsx +++ b/apps/web/src/pages/contacts/index.tsx @@ -19,8 +19,8 @@ import { } from '@plunk/ui'; import type {Contact} from '@plunk/db'; import type {CursorPaginatedResponse} from '@plunk/types'; +import {EmptyState} from '@plunk/ui'; import {DashboardLayout} from '../../components/DashboardLayout'; -import {EmptyState} from '../../components/EmptyState'; import {KeyValueEditor} from '../../components/KeyValueEditor'; import {network} from '../../lib/network'; import {formatRelativeTime} from '../../lib/dateUtils'; diff --git a/apps/web/src/pages/index.tsx b/apps/web/src/pages/index.tsx index a2cabf9..79755ad 100644 --- a/apps/web/src/pages/index.tsx +++ b/apps/web/src/pages/index.tsx @@ -8,6 +8,7 @@ import { CardDescription, CardHeader, CardTitle, + Skeleton, } from '@plunk/ui'; import {AlertCircle, Mail, Send, TrendingUp, Users} from 'lucide-react'; import {NextSeo} from 'next-seo'; @@ -192,11 +193,7 @@ export default function Index() {
- {isLoading ? ( -
- ) : ( - stat.value - )} + {isLoading ? : stat.value} diff --git a/apps/web/src/pages/onboarding/developer.tsx b/apps/web/src/pages/onboarding/developer.tsx index 4aa6582..e8d4e28 100644 --- a/apps/web/src/pages/onboarding/developer.tsx +++ b/apps/web/src/pages/onboarding/developer.tsx @@ -1,4 +1,4 @@ -import {Button} from '@plunk/ui'; +import {Button, Skeleton} from '@plunk/ui'; import {motion} from 'framer-motion'; import {ArrowLeft, ArrowRight, KeyRound, Mail, ShieldCheck} from 'lucide-react'; import {NextSeo} from 'next-seo'; @@ -192,7 +192,7 @@ export default function OnboardingDeveloper() { isSecret /> ) : ( -
+ )}
)} diff --git a/apps/web/src/pages/onboarding/workflows.tsx b/apps/web/src/pages/onboarding/workflows.tsx index f1be843..c3e8478 100644 --- a/apps/web/src/pages/onboarding/workflows.tsx +++ b/apps/web/src/pages/onboarding/workflows.tsx @@ -1,4 +1,4 @@ -import {Button} from '@plunk/ui'; +import {Button, Skeleton} from '@plunk/ui'; import {motion} from 'framer-motion'; import {ArrowLeft, ArrowRight, FileText, ShieldCheck, Workflow, Zap} from 'lucide-react'; import {NextSeo} from 'next-seo'; @@ -201,7 +201,7 @@ export default function OnboardingWorkflows() { description="Use this key to track events from browsers or clients." /> ) : ( -
+ )}
diff --git a/apps/web/src/pages/segments/[id].tsx b/apps/web/src/pages/segments/[id].tsx index 216d70e..0742a42 100644 --- a/apps/web/src/pages/segments/[id].tsx +++ b/apps/web/src/pages/segments/[id].tsx @@ -1,4 +1,5 @@ import { + Badge, Button, Card, CardContent, @@ -219,14 +220,10 @@ export default function SegmentDetailPage() {
-

{segment.name}

- +

{segment.name}

+ {isStatic ? 'Static' : 'Dynamic'} -
+
{segment.description &&

{segment.description}

}
@@ -395,11 +392,10 @@ export default function SegmentDetailPage() { {isStatic && ( diff --git a/apps/web/src/pages/segments/index.tsx b/apps/web/src/pages/segments/index.tsx index 0d11d17..6290399 100644 --- a/apps/web/src/pages/segments/index.tsx +++ b/apps/web/src/pages/segments/index.tsx @@ -11,8 +11,8 @@ import { } from '@plunk/ui'; import type {Segment} from '@plunk/db'; import type {FilterCondition} from '@plunk/types'; +import {EmptyState} from '@plunk/ui'; import {DashboardLayout} from '../../components/DashboardLayout'; -import {EmptyState} from '../../components/EmptyState'; import {network} from '../../lib/network'; import {formatRelativeTime} from '../../lib/dateUtils'; import {AlertTriangle, Calendar, Edit, Filter, Plus, Search, Trash2, X} from 'lucide-react'; @@ -170,7 +170,7 @@ export default function SegmentsPage() { >

{segment.name}

- + {isDynamic ? 'Dynamic' : 'Static'}
diff --git a/apps/web/src/pages/segments/new.tsx b/apps/web/src/pages/segments/new.tsx index 97605e2..c5ad4b8 100644 --- a/apps/web/src/pages/segments/new.tsx +++ b/apps/web/src/pages/segments/new.tsx @@ -85,7 +85,7 @@ export default function NewSegmentPage() {
-

Create Segment

+

Create Segment

{segmentType === 'DYNAMIC' ? 'Build complex audience filters with AND/OR logic' diff --git a/apps/web/src/pages/settings/index.tsx b/apps/web/src/pages/settings/index.tsx index c2e814e..d449460 100644 --- a/apps/web/src/pages/settings/index.tsx +++ b/apps/web/src/pages/settings/index.tsx @@ -375,7 +375,7 @@ export default function Settings() {

{/* Header */}
-

Settings

+

Settings

Manage your project settings and preferences

@@ -574,7 +574,7 @@ export default function Settings() {
- +

Reset Project Data

@@ -586,12 +586,7 @@ export default function Settings() { API keys, domains, billing information

-
@@ -823,7 +818,7 @@ export default function Settings() { - + Regenerate API Keys @@ -841,7 +836,7 @@ export default function Settings() { - @@ -852,8 +847,8 @@ export default function Settings() { -
- +
+
Reset Project Data? @@ -865,7 +860,7 @@ export default function Settings() {
@@ -946,9 +942,10 @@ export default function Settings() { Cancel diff --git a/apps/web/src/pages/templates/index.tsx b/apps/web/src/pages/templates/index.tsx index 13d9cbb..1499286 100644 --- a/apps/web/src/pages/templates/index.tsx +++ b/apps/web/src/pages/templates/index.tsx @@ -9,8 +9,8 @@ import { } from '@plunk/ui'; import type {Template} from '@plunk/db'; import type {PaginatedResponse} from '@plunk/types'; +import {EmptyState} from '@plunk/ui'; import {DashboardLayout} from '../../components/DashboardLayout'; -import {EmptyState} from '../../components/EmptyState'; import {network} from '../../lib/network'; import {formatRelativeTime} from '../../lib/dateUtils'; import {Calendar, Copy, Edit, FileText, Plus, Search, Trash2, X} from 'lucide-react'; @@ -174,7 +174,7 @@ export default function TemplatesPage() {

{template.name}

{template.type.toLowerCase()} diff --git a/apps/web/src/pages/workflows/[id].tsx b/apps/web/src/pages/workflows/[id].tsx index 10411b2..bc7889b 100644 --- a/apps/web/src/pages/workflows/[id].tsx +++ b/apps/web/src/pages/workflows/[id].tsx @@ -3,6 +3,7 @@ import { Alert, AlertDescription, AlertTitle, + Badge, Button, Card, CardContent, @@ -18,6 +19,7 @@ import { DialogFooter, DialogHeader, DialogTitle, + EmptyState, Input, Label, Select, @@ -36,7 +38,6 @@ import { import type {Template, Workflow, WorkflowExecution, WorkflowStep, WorkflowTransition} from '@plunk/db'; import type {PaginatedResponse} from '@plunk/types'; import {DashboardLayout} from '../../components/DashboardLayout'; -import {EmptyState} from '../../components/EmptyState'; import {network} from '../../lib/network'; import { AlertTriangle, @@ -659,23 +660,21 @@ export default function WorkflowEditorPage() { {execution.contact.email} - {execution.status} - + {execution.currentStep?.name ?? '-'} @@ -690,13 +689,14 @@ export default function WorkflowEditorPage() { {(execution.status === 'RUNNING' || execution.status === 'WAITING') && ( - + )} @@ -2471,13 +2471,15 @@ function EditStepDialog({step, workflowId, open, onOpenChange, onSuccess}: EditS
Branch {idx + 1} {conditionBranches.length > 1 && ( - + )}
@@ -2551,7 +2553,7 @@ function EditStepDialog({step, workflowId, open, onOpenChange, onSuccess}: EditS {id: crypto.randomUUID().slice(0, 8), name: '', operator: 'equals', value: ''}, ]) } - className="flex items-center gap-1.5 text-sm text-purple-600 hover:text-purple-800 font-medium" + className="flex items-center gap-1.5 text-sm text-neutral-700 hover:text-neutral-900 font-medium" > Add Branch diff --git a/apps/web/src/pages/workflows/index.tsx b/apps/web/src/pages/workflows/index.tsx index b8697a9..1a6d5e0 100644 --- a/apps/web/src/pages/workflows/index.tsx +++ b/apps/web/src/pages/workflows/index.tsx @@ -19,8 +19,8 @@ import { } from '@plunk/ui'; import type {Workflow} from '@plunk/db'; import type {PaginatedResponse} from '@plunk/types'; +import {EmptyState} from '@plunk/ui'; import {DashboardLayout} from '../../components/DashboardLayout'; -import {EmptyState} from '../../components/EmptyState'; import {network} from '../../lib/network'; import {formatRelativeTime} from '../../lib/dateUtils'; import {Calendar, Edit, Plus, Power, PowerOff, Search, Trash2, Workflow as WorkflowIcon, X, Zap} from 'lucide-react'; @@ -165,7 +165,7 @@ export default function WorkflowsPage() { >

{workflow.name}

- + {workflow.enabled ? ( <>Active ) : ( diff --git a/packages/ui/src/components/atoms/Alert.tsx b/packages/ui/src/components/atoms/Alert.tsx index 675b053..a1aa27d 100644 --- a/packages/ui/src/components/atoms/Alert.tsx +++ b/packages/ui/src/components/atoms/Alert.tsx @@ -8,8 +8,9 @@ const alertVariants = cva( variants: { variant: { default: 'bg-white text-neutral-950', - destructive: 'border-red-500/50 text-red-900 [&>svg]:text-red-900', - warning: 'bg-amber-50 border-amber-200 text-amber-900 [&>svg]:text-amber-600', + destructive: 'border-red-200 bg-red-50 text-red-900 [&>svg]:text-red-600', + warning: 'border-amber-200 bg-amber-50 text-amber-900 [&>svg]:text-amber-600', + success: 'border-green-200 bg-green-50 text-green-900 [&>svg]:text-green-600', }, }, defaultVariants: { diff --git a/packages/ui/src/components/atoms/Badge.tsx b/packages/ui/src/components/atoms/Badge.tsx index 2a0ce87..317a1ac 100644 --- a/packages/ui/src/components/atoms/Badge.tsx +++ b/packages/ui/src/components/atoms/Badge.tsx @@ -4,22 +4,17 @@ import * as React from 'react'; import {cn} from '../../lib'; const badgeVariants = cva( - 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', + 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-1 focus:ring-ring focus:ring-offset-2', { variants: { variant: { default: 'border-transparent bg-neutral-100 text-neutral-900', secondary: 'border-transparent bg-neutral-100 text-neutral-900', - destructive: 'border-transparent bg-red-100 text-red-900', outline: 'text-neutral-950', - success: 'border-transparent bg-green-100 text-green-900', - warning: 'border-transparent bg-yellow-100 text-yellow-900', - // Additional semantic variants for common use cases - info: 'border-transparent bg-blue-100 text-blue-900', - orange: 'border-transparent bg-orange-100 text-orange-800', - green: 'border-transparent bg-green-100 text-green-800', - red: 'border-transparent bg-red-100 text-red-800', neutral: 'border-transparent bg-neutral-50 text-neutral-600', + destructive: 'border-transparent bg-red-100 text-red-900', + success: 'border-transparent bg-green-100 text-green-900', + warning: 'border-transparent bg-amber-100 text-amber-900', }, }, defaultVariants: { diff --git a/packages/ui/src/components/atoms/Button.tsx b/packages/ui/src/components/atoms/Button.tsx index 2875e0a..cc0ccf9 100644 --- a/packages/ui/src/components/atoms/Button.tsx +++ b/packages/ui/src/components/atoms/Button.tsx @@ -10,10 +10,11 @@ const buttonVariants = cva( variants: { variant: { default: 'bg-neutral-900 text-neutral-50 hover:bg-neutral-900/90', - destructive: 'bg-red-500 text-neutral-50 hover:bg-red-500/90', + destructive: 'bg-red-600 text-white hover:bg-red-600/90', outline: 'border border-neutral-200 bg-white hover:bg-neutral-100 hover:text-neutral-900', secondary: 'bg-neutral-100 text-neutral-900 hover:bg-neutral-100/80', ghost: 'hover:bg-neutral-100 hover:text-neutral-900', + destructiveGhost: 'text-red-600 hover:text-red-700 hover:bg-red-50', link: 'text-neutral-900 underline-offset-4 hover:underline', }, size: { diff --git a/packages/ui/src/components/atoms/Card.tsx b/packages/ui/src/components/atoms/Card.tsx index 13211c7..fd121f4 100644 --- a/packages/ui/src/components/atoms/Card.tsx +++ b/packages/ui/src/components/atoms/Card.tsx @@ -5,7 +5,7 @@ import {cn} from '../../lib'; const Card = React.forwardRef>(({className, ...props}, ref) => (
)); diff --git a/packages/ui/src/components/atoms/Checkbox.tsx b/packages/ui/src/components/atoms/Checkbox.tsx index 9441316..6342040 100644 --- a/packages/ui/src/components/atoms/Checkbox.tsx +++ b/packages/ui/src/components/atoms/Checkbox.tsx @@ -13,7 +13,7 @@ const Checkbox = React.forwardRef< {children} - + Close diff --git a/packages/ui/src/components/atoms/Form.tsx b/packages/ui/src/components/atoms/Form.tsx index ee52096..7bccc3e 100644 --- a/packages/ui/src/components/atoms/Form.tsx +++ b/packages/ui/src/components/atoms/Form.tsx @@ -122,7 +122,7 @@ const FormMessage = React.forwardRef +

{body}

); diff --git a/packages/ui/src/components/atoms/RadioGroup.tsx b/packages/ui/src/components/atoms/RadioGroup.tsx index fbb44f6..f7642e9 100644 --- a/packages/ui/src/components/atoms/RadioGroup.tsx +++ b/packages/ui/src/components/atoms/RadioGroup.tsx @@ -22,7 +22,7 @@ const RadioGroupItem = React.forwardRef< span]:line-clamp-1', + 'flex h-10 w-full items-center justify-between rounded-md border border-neutral-200 bg-white px-3 py-2 text-sm ring-offset-white placeholder:text-neutral-500 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1', className, )} {...props} diff --git a/packages/ui/src/components/atoms/Switch.tsx b/packages/ui/src/components/atoms/Switch.tsx index 28473ae..8cd3966 100644 --- a/packages/ui/src/components/atoms/Switch.tsx +++ b/packages/ui/src/components/atoms/Switch.tsx @@ -8,7 +8,7 @@ const Switch = React.forwardRef< >(({className, ...props}, ref) => ( void; - }; + action?: ReactNode; className?: string; } export function EmptyState({icon: Icon, title, description, action, className}: EmptyStateProps) { return ( -
- -

{title}

- {description &&

{description}

} - {action && ( - +
+
+ +
+

{title}

+ {description && ( +

{description}

)} + {action}
); }