diff --git a/.impeccable.md b/.impeccable.md new file mode 100644 index 0000000..d6257d4 --- /dev/null +++ b/.impeccable.md @@ -0,0 +1,17 @@ +## Design Context + +### Users +Developer-founders and indie hackers building SaaS products. They use Plunk to handle transactional and marketing email without the complexity of tools like Mailchimp or Customer.io. They notice tiny details — inconsistent spacing, placeholder text that adds no value, a button that doesn't communicate state. Context: professional environment, desktop-first. + +### Brand Personality +Sharp, minimal, confident. The product earns trust by being simple and correct, not by being flashy. Testimonials emphasize "transparent UI", "easy setup", "clean design" — the brand is *care without noise*. + +### Aesthetic Direction +Light mode only. Palette: black (`neutral-900`), neutral grays, white. No accent colors. No color for decoration — only for semantics (red = error, green = success). Backgrounds are near-white with subtle texture. Cards use white with a neutral border and light shadow. Typography should feel precise and legible, not editorial. Spacing should feel considered, not generous. + +### Design Principles +1. **Every pixel earns its place.** If something doesn't communicate information or provide affordance, remove it. +2. **Neutral by default, semantic by exception.** Color is reserved for error/success/warning states, not decoration. +3. **Interaction should feel fast.** Loading states communicate exactly what's happening. No silent actions. +4. **Developer-grade precision.** Copy is short and direct. Placeholders only appear when they add value. Labels are unambiguous. +5. **Consistency is trust.** The same pattern everywhere. One way to show errors. One way to show success. No creative variation in functional UI. diff --git a/apps/web/src/components/ActivityFeed.tsx b/apps/web/src/components/ActivityFeed.tsx index 2634175..9e19c29 100644 --- a/apps/web/src/components/ActivityFeed.tsx +++ b/apps/web/src/components/ActivityFeed.tsx @@ -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 ( -
-

- No activity found for the selected filters. Activities will appear here as they happen. -

-
+ ); } diff --git a/apps/web/src/components/ApiKeyDisplay.tsx b/apps/web/src/components/ApiKeyDisplay.tsx index 2d35500..5cc98b7 100644 --- a/apps/web/src/components/ApiKeyDisplay.tsx +++ b/apps/web/src/components/ApiKeyDisplay.tsx @@ -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 ? : } + + {copied ? ( + + + + ) : ( + + + + )} + {showRegenerate && onRegenerate && ( {/* Project Dropdown */} {showProjectMenu && ( -
+
{sortedProjects.map(project => ( {/* User Dropdown */} {showUserMenu && ( -
+
+
+

{user?.email}

+