From de07acfb4f34a968d034ac703324e05af83f1f76 Mon Sep 17 00:00:00 2001 From: EJ Date: Thu, 11 Dec 2025 13:55:50 -0800 Subject: [PATCH 1/4] add plans for updates to home page features section --- ...section_interactive_cards_b6f1d608.plan.md | 178 ++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 .cursor/plans/featuressection_interactive_cards_b6f1d608.plan.md diff --git a/.cursor/plans/featuressection_interactive_cards_b6f1d608.plan.md b/.cursor/plans/featuressection_interactive_cards_b6f1d608.plan.md new file mode 100644 index 0000000..6c77e85 --- /dev/null +++ b/.cursor/plans/featuressection_interactive_cards_b6f1d608.plan.md @@ -0,0 +1,178 @@ +--- +name: FeaturesSection Interactive Cards +overview: Refactor FeaturesSection component to add interactive left-click expansion and right-click context menus with unique actions for each feature card, including a special context-aware card with expandable context types and notification system for actions. +todos: + - id: "1" + content: Refactor FeaturesSection to client component with state management for expanded cards + status: pending + - id: "2" + content: Implement left-click expansion/collapse functionality for all cards + status: pending + - id: "3" + content: Create notification/toast system component with success/warning/error/info types + status: pending + - id: "4" + content: Implement special context-aware card with expandable context types (current element, parent, copy selector) + status: pending + - id: "5" + content: Add scoped AnyclickProvider to each card with unique right-click menu items + status: pending + - id: "6" + content: Implement right-click menu actions (navigation and delayed notifications) for each card + status: pending + - id: "7" + content: Add expanded content sections with animations and visual indicators + status: pending + - id: "8" + content: Add copy-to-clipboard functionality for selector copying in context-aware card + status: pending +--- + +# FeaturesSection Interactive Cards Refactor + +## Overview + +Transform the static feature cards into interactive components with: + +- Left-click: Expand inline to show more information +- Right-click: Show unique context menu with card-specific actions +- Special handling for "Context-Aware Capture" card with expandable context types +- Notification system for action feedback + +## Implementation Details + +### 1. Component Structure Refactor + +**File**: `apps/web/src/components/FeaturesSection.tsx` + +- Convert to client component (`"use client"`) +- Add state management for expanded cards (`useState>`) +- Wrap each card with scoped `AnyclickProvider` for right-click menus +- Add left-click handlers to toggle expansion +- Create expanded content sections for each card + +### 2. Context-Aware Card Special Implementation + +**Card**: "Context-Aware Capture" (Feature 1) + +**Left-click behavior**: + +- Expand to show context type options: +- Current Element (with visual indicator) +- Parent Element (with visual indicator) +- Copy Selector (button to copy CSS selector) +- Each option should be interactive and show visual feedback + +**Right-click menu items**: + +- "Capture Page Screenshot" → Shows success notification after 1-2s +- "Save Reference to Element" → Shows success notification after 1-2s + +### 3. Unique Actions Per Card + +**Visual Capture** (Feature 2): + +- Left-click: Expand to show screenshot examples/preview +- Right-click menu: +- "Take Screenshot Now" → Navigate to examples page or show success notification +- "View Gallery" → Navigate to gallery or show info notification + +**Code Source Integration** (Feature 3): + +- Left-click: Expand to show GitHub integration details +- Right-click menu: +- "Create GitHub Issue" → Navigate to GitHub integration example +- "View Integration Docs" → Navigate to docs page + +**AI Agent** (Feature 4): + +- Left-click: Expand to show AI agent capabilities +- Right-click menu: +- "Launch AI Agent" → Show warning notification (feature coming soon) +- "View Agent Docs" → Navigate to docs + +**Framework Agnostic** (Feature 5): + +- Left-click: Expand to show framework examples +- Right-click menu: +- "View React Example" → Navigate to React example page +- "View Integration Guide" → Navigate to integration docs + +**Zero Config** (Feature 6): + +- Left-click: Expand to show setup steps +- Right-click menu: +- "Try Demo" → Navigate to demo page +- "Get Started" → Navigate to getting started page + +### 4. Notification System + +**New file**: `apps/web/src/components/Toast.tsx` or integrate into FeaturesSection + +Create a simple toast notification component with: + +- Types: `success`, `warning`, `error`, `info` +- Auto-dismiss after 3-4 seconds +- Position: bottom-right or top-right +- Smooth animations (fade in/out) +- Support for delayed notifications (1-2 seconds as specified) + +### 5. Scoped AnyclickProvider Integration + +Each card needs its own scoped `AnyclickProvider` with: + +- Unique `menuItems` array per card +- `onClick` handlers that trigger notifications or navigation +- Prevent default context menu behavior on cards +- Custom menu styling to match card theme colors + +### 6. Expanded Content Design + +- Smooth height transition animations +- Additional details, examples, or interactive elements +- Visual indicators for context types (for context-aware card) +- Copy-to-clipboard functionality for selectors + +## Technical Considerations + +- Use `useState` for managing expanded state +- Use `useCallback` for event handlers to prevent re-renders +- Implement proper TypeScript types for card configurations +- Ensure accessibility (keyboard navigation, ARIA labels) +- Handle edge cases (clicking outside to collapse, preventing menu on left-click) +- Use Tailwind classes for styling and animations +- Leverage existing `AnyclickProvider` from `@ewjdev/anyclick-react` + +## File Changes + +1. **Modify**: `apps/web/src/components/FeaturesSection.tsx` + +- Add state management +- Add left/right click handlers +- Add expanded content sections +- Integrate scoped AnyclickProvider per card +- Add notification triggers + +2. **Create** (optional): `apps/web/src/components/Toast.tsx` + +- Simple toast notification component +- Or use inline notification state within FeaturesSection + +3. **Create** (optional): `apps/web/src/components/FeatureCard.tsx` + +- Extract card logic into reusable component +- Handle expansion, menu, and notifications + +## Navigation Targets + +- GitHub integration: `/examples/github-integration` or `/examples/jira-integration` +- Docs: `/docs` or `/docs/react` +- Examples: `/examples` or specific example pages +- Getting started: `/getting-started` or homepage + +## Notification Messages Examples + +- Success: "Screenshot captured successfully", "Element reference saved" +- Warning: "Feature coming soon", "This action requires setup" +- Error: "Failed to capture screenshot", "Unable to save reference" +- Info: "Navigating to documentation...", "Opening example..." \ No newline at end of file From b9cd3ceae34bf20d9bbcd26788c95dc1badaf475 Mon Sep 17 00:00:00 2001 From: Eric johnson Date: Thu, 11 Dec 2025 14:33:47 -0800 Subject: [PATCH 2/4] update and organize plans --- .../context_menu_ux_redesign_04cf3a1b.plan.md | 0 ...m-pointer-performance-optimization.plan.md | 0 ...tools-inspect-opens-panel_e0886321.plan.md | 18 - ...section_interactive_cards_b6f1d608.plan.md | 842 +++++++++++++++--- 4 files changed, 713 insertions(+), 147 deletions(-) rename .cursor/plans/{ => complete}/context_menu_ux_redesign_04cf3a1b.plan.md (100%) rename .cursor/plans/{ => complete}/custom-pointer-performance-optimization.plan.md (100%) rename .cursor/plans/{ => complete}/devtools-inspect-opens-panel_e0886321.plan.md (67%) diff --git a/.cursor/plans/context_menu_ux_redesign_04cf3a1b.plan.md b/.cursor/plans/complete/context_menu_ux_redesign_04cf3a1b.plan.md similarity index 100% rename from .cursor/plans/context_menu_ux_redesign_04cf3a1b.plan.md rename to .cursor/plans/complete/context_menu_ux_redesign_04cf3a1b.plan.md diff --git a/.cursor/plans/custom-pointer-performance-optimization.plan.md b/.cursor/plans/complete/custom-pointer-performance-optimization.plan.md similarity index 100% rename from .cursor/plans/custom-pointer-performance-optimization.plan.md rename to .cursor/plans/complete/custom-pointer-performance-optimization.plan.md diff --git a/.cursor/plans/devtools-inspect-opens-panel_e0886321.plan.md b/.cursor/plans/complete/devtools-inspect-opens-panel_e0886321.plan.md similarity index 67% rename from .cursor/plans/devtools-inspect-opens-panel_e0886321.plan.md rename to .cursor/plans/complete/devtools-inspect-opens-panel_e0886321.plan.md index 1cd49ea..6bc1fd8 100644 --- a/.cursor/plans/devtools-inspect-opens-panel_e0886321.plan.md +++ b/.cursor/plans/complete/devtools-inspect-opens-panel_e0886321.plan.md @@ -1,21 +1,3 @@ ---- -name: devtools-inspect-opens-panel -overview: Route the context menu Inspect action to open/focus the Anyclick DevTools panel and stop sending element context payloads elsewhere. -todos: - - id: trace-flow - content: Trace current inspect flow across content/background/devtools - status: completed - - id: stop-send - content: Remove inspect element sending to adapters/queue - status: completed - - id: open-panel - content: Open/focus Anyclick DevTools panel on inspect - status: completed - - id: verify - content: Manual test inspect menu opens Anyclick panel - status: completed ---- - # DevTools Inspect Opens Panel 1) Trace current Inspect flow diff --git a/.cursor/plans/featuressection_interactive_cards_b6f1d608.plan.md b/.cursor/plans/featuressection_interactive_cards_b6f1d608.plan.md index 6c77e85..eedd0b7 100644 --- a/.cursor/plans/featuressection_interactive_cards_b6f1d608.plan.md +++ b/.cursor/plans/featuressection_interactive_cards_b6f1d608.plan.md @@ -1,178 +1,762 @@ ---- -name: FeaturesSection Interactive Cards -overview: Refactor FeaturesSection component to add interactive left-click expansion and right-click context menus with unique actions for each feature card, including a special context-aware card with expandable context types and notification system for actions. -todos: - - id: "1" - content: Refactor FeaturesSection to client component with state management for expanded cards - status: pending - - id: "2" - content: Implement left-click expansion/collapse functionality for all cards - status: pending - - id: "3" - content: Create notification/toast system component with success/warning/error/info types - status: pending - - id: "4" - content: Implement special context-aware card with expandable context types (current element, parent, copy selector) - status: pending - - id: "5" - content: Add scoped AnyclickProvider to each card with unique right-click menu items - status: pending - - id: "6" - content: Implement right-click menu actions (navigation and delayed notifications) for each card - status: pending - - id: "7" - content: Add expanded content sections with animations and visual indicators - status: pending - - id: "8" - content: Add copy-to-clipboard functionality for selector copying in context-aware card - status: pending +# FeaturesSection Interactive Cards + Global Submission UX + +## Overview + +This plan covers two related improvements: + +1. **FeaturesSection Enhancement** - Expandable feature cards with animated workflow visualizations that demonstrate what each Anyclick capability does +2. **Global Submission Feedback** - Improve the submission UX across all Anyclick interactions with proper loading states, success/error notifications, and status indicators + --- -# FeaturesSection Interactive Cards Refactor +## Part 1: FeaturesSection Interactive Cards -## Overview +### Goals + +- Each feature card expands on click to show detailed visualization +- Expanded view demonstrates the workflow with animated status progression +- Auto-advancing 4-step workflow shows what happens during a real interaction +- Links lead to examples/docs for implementation details + +### Component Structure + +``` +FeaturesSection (client component) +├── FeatureCard (×6) +│ ├── Collapsed State (default) +│ │ └── Icon, Title, Description, "Learn more" link +│ └── Expanded State (on click) +│ ├── Close button +│ ├── Extended description +│ ├── WorkflowVisualization +│ │ └── 4-step animated progression +│ └── CTA link to docs/examples +└── No scoped providers needed (uses global provider) +``` + +### Expandable Card Design + +**Collapsed State:** + +``` +┌──────────────────────────────────┐ +│ [Icon] │ +│ Context-Aware Capture │ +│ Right-click any element to... │ +│ │ +│ Learn more → │ +└──────────────────────────────────┘ +``` + +**Expanded State:** + +``` +┌──────────────────────────────────────────────────────────────────┐ +│ [Icon] Context-Aware Capture [×] │ +├──────────────────────────────────────────────────────────────────┤ +│ │ +│ When you right-click an element, Anyclick captures comprehensive │ +│ context about that element and its surroundings. │ +│ │ +│ ┌──────────────────────────────────────────────────────────────┐ │ +│ │ Workflow Visualization │ │ +│ │ │ │ +│ │ ○ ─────── ◉ ─────── ○ ─────── ○ │ │ +│ │ Step 1 Step 2 Step 3 Step 4 │ │ +│ │ │ │ +│ │ ┌─────────────────────────────────────┐ │ │ +│ │ │ [✓] Capturing element context... │ │ │ +│ │ │ CSS selector: button.primary │ │ │ +│ │ │ Attributes: 3 captured │ │ │ +│ │ └─────────────────────────────────────┘ │ │ +│ └──────────────────────────────────────────────────────────────┘ │ +│ │ +│ Try it in the demo → │ +└──────────────────────────────────────────────────────────────────┘ +``` + +### Workflow Configurations Per Card + +Each card has a unique 4-step workflow that auto-advances every ~1.5s: + +#### 1. Context-Aware Capture + +| Step | Label | Status Detail | + +|------|-------|---------------| + +| 1 | Detecting element | Finding target under cursor... | + +| 2 | Capturing context | CSS selector, attributes, ancestors | + +| 3 | Analyzing hierarchy | Parent containers, siblings | + +| 4 | Context ready | Full element context captured ✓ | + +#### 2. Visual Capture + +| Step | Label | Status Detail | + +|------|-------|---------------| + +| 1 | Preparing capture | Initializing screenshot engine... | + +| 2 | Capturing element | Taking element screenshot... | + +| 3 | Capturing viewport | Taking full page screenshot... | + +| 4 | Screenshots ready | 2 screenshots captured ✓ | + +#### 3. Code Source Integration + +| Step | Label | Status Detail | + +|------|-------|---------------| + +| 1 | Gathering context | Element data + screenshots... | -Transform the static feature cards into interactive components with: +| 2 | Formatting issue | Creating markdown with context... | -- Left-click: Expand inline to show more information -- Right-click: Show unique context menu with card-specific actions -- Special handling for "Context-Aware Capture" card with expandable context types -- Notification system for action feedback +| 3 | Creating issue | Sending to GitHub API... | -## Implementation Details +| 4 | Issue created | Issue #234 created ✓ | -### 1. Component Structure Refactor +#### 4. AI Agent -**File**: `apps/web/src/components/FeaturesSection.tsx` +| Step | Label | Status Detail | -- Convert to client component (`"use client"`) -- Add state management for expanded cards (`useState>`) -- Wrap each card with scoped `AnyclickProvider` for right-click menus -- Add left-click handlers to toggle expansion -- Create expanded content sections for each card +|------|-------|---------------| -### 2. Context-Aware Card Special Implementation +| 1 | Analyzing context | AI processing element... | -**Card**: "Context-Aware Capture" (Feature 1) +| 2 | Understanding intent | Determining action type... | -**Left-click behavior**: +| 3 | Generating response | Creating suggestions... | -- Expand to show context type options: -- Current Element (with visual indicator) -- Parent Element (with visual indicator) -- Copy Selector (button to copy CSS selector) -- Each option should be interactive and show visual feedback +| 4 | Ready | AI response ready ✓ | -**Right-click menu items**: +#### 5. Framework Agnostic -- "Capture Page Screenshot" → Shows success notification after 1-2s -- "Save Reference to Element" → Shows success notification after 1-2s +| Step | Label | Status Detail | -### 3. Unique Actions Per Card +|------|-------|---------------| -**Visual Capture** (Feature 2): +| 1 | Detecting framework | Checking for React/Vue/Svelte... | -- Left-click: Expand to show screenshot examples/preview -- Right-click menu: -- "Take Screenshot Now" → Navigate to examples page or show success notification -- "View Gallery" → Navigate to gallery or show info notification +| 2 | Loading adapter | Initializing React adapter... | -**Code Source Integration** (Feature 3): +| 3 | Attaching listeners | Context menu ready... | -- Left-click: Expand to show GitHub integration details -- Right-click menu: -- "Create GitHub Issue" → Navigate to GitHub integration example -- "View Integration Docs" → Navigate to docs page +| 4 | Active | Anyclick enabled ✓ | -**AI Agent** (Feature 4): +#### 6. Zero Config -- Left-click: Expand to show AI agent capabilities -- Right-click menu: -- "Launch AI Agent" → Show warning notification (feature coming soon) -- "View Agent Docs" → Navigate to docs +| Step | Label | Status Detail | -**Framework Agnostic** (Feature 5): +|------|-------|---------------| -- Left-click: Expand to show framework examples -- Right-click menu: -- "View React Example" → Navigate to React example page -- "View Integration Guide" → Navigate to integration docs +| 1 | Installing | npm install @ewjdev/anyclick-react | -**Zero Config** (Feature 6): +| 2 | Wrapping app | Adding AnyclickProvider... | -- Left-click: Expand to show setup steps -- Right-click menu: -- "Try Demo" → Navigate to demo page -- "Get Started" → Navigate to getting started page +| 3 | Configuring | Default settings applied... | -### 4. Notification System +| 4 | Ready | Start right-clicking! ✓ | -**New file**: `apps/web/src/components/Toast.tsx` or integrate into FeaturesSection +### Implementation Details -Create a simple toast notification component with: +**File: `apps/web/src/components/FeaturesSection.tsx`** -- Types: `success`, `warning`, `error`, `info` -- Auto-dismiss after 3-4 seconds -- Position: bottom-right or top-right -- Smooth animations (fade in/out) -- Support for delayed notifications (1-2 seconds as specified) +```tsx +"use client"; -### 5. Scoped AnyclickProvider Integration +import { useState, useEffect, useCallback } from "react"; +import Link from "next/link"; +import { motion, AnimatePresence } from "framer-motion"; +import { ArrowRight, X, Check, Loader2 } from "lucide-react"; +// ... other icon imports -Each card needs its own scoped `AnyclickProvider` with: +interface WorkflowStep { + label: string; + detail: string; +} -- Unique `menuItems` array per card -- `onClick` handlers that trigger notifications or navigation -- Prevent default context menu behavior on cards -- Custom menu styling to match card theme colors +interface FeatureConfig { + id: string; + icon: ReactNode; + title: string; + description: string; + expandedDescription: string; + workflow: WorkflowStep[]; + linkHref: string; + linkText: string; + color: string; // tailwind color class +} + +// Feature configurations with workflows +const features: FeatureConfig[] = [ + { + id: "context-capture", + icon: , + title: "Context-Aware Capture", + description: "Right-click any element to capture its full DOM context...", + expandedDescription: "When you right-click an element, Anyclick captures...", + workflow: [ + { label: "Detecting element", detail: "Finding target under cursor..." }, + { label: "Capturing context", detail: "CSS selector, attributes, ancestors" }, + { label: "Analyzing hierarchy", detail: "Parent containers, siblings" }, + { label: "Context ready", detail: "Full element context captured ✓" }, + ], + linkHref: "/examples/basic", + linkText: "Try the demo", + color: "violet", + }, + // ... other features +]; + +function WorkflowVisualization({ + steps, + isActive, + color +}: { + steps: WorkflowStep[]; + isActive: boolean; + color: string; +}) { + const [currentStep, setCurrentStep] = useState(0); + + useEffect(() => { + if (!isActive) { + setCurrentStep(0); + return; + } + + const interval = setInterval(() => { + setCurrentStep((prev) => (prev + 1) % steps.length); + }, 1500); + + return () => clearInterval(interval); + }, [isActive, steps.length]); + + return ( +
+ {/* Progress dots */} +
+ {steps.map((_, i) => ( +
+ ))} +
+ + {/* Current step display */} +
+
+ {currentStep === steps.length - 1 ? ( + + ) : ( + + )} + {steps[currentStep].label} +
+

{steps[currentStep].detail}

+
+
+ ); +} + +function FeatureCard({ feature }: { feature: FeatureConfig }) { + const [isExpanded, setIsExpanded] = useState(false); + + return ( + !isExpanded && setIsExpanded(true)} + > + + {!isExpanded ? ( + // Collapsed content + + {/* Icon, title, description, learn more link */} + + ) : ( + // Expanded content + + + +

{feature.title}

+

{feature.expandedDescription}

+ + + + + {feature.linkText} + +
+ )} +
+
+ ); +} +``` + +### Link Destinations + +| Feature | Link | Target | + +|---------|------|--------| + +| Context-Aware Capture | `/examples/basic` | Basic example with context capture | + +| Visual Capture | `/examples/basic` | Basic example (has screenshot demo) | + +| Code Source Integration | `/examples/github-integration` | GitHub integration example | + +| AI Agent | `/examples/cursor-local` | Cursor local AI integration | + +| Framework Agnostic | `/docs/react` | React integration docs | + +| Zero Config | `/docs/getting-started` | Getting started guide | -### 6. Expanded Content Design +--- + +## Part 2: Global Submission Feedback System + +### Current Problems + +1. **No success notification** - Menu closes, user doesn't know if feedback was received +2. **No error notification** - Errors are silent unless developer adds `onSubmitError` callback +3. **Submitting state** - Only shows "Sending..." text, no clear loading indicator +4. **No persistence** - User loses all feedback if something fails + +### Existing Pattern: `showToast` in anyclick-extension + +The extension already has a well-established toast pattern in two places: + +**`content.ts` (vanilla JS):** + +```typescript +function showToast( + message: string, + type: "success" | "error" | "warning" | "info" = "info", +): void { + // Creates DOM element, positions bottom-right + // Auto-hides after 4000ms +} +``` + +**`PopupApp.tsx` (React):** + +```typescript +const showToast = useCallback((message: string, error = false) => { + setToast({ message, error }); + setTimeout(() => setToast(null), 2500); +}, []); +``` + +**Standard colors across the codebase:** + +- success: `#10b981` (emerald) +- error: `#ef4444` (red) +- warning: `#f59e0b` (amber) +- info: `#3b82f6` (blue) + +### Proposed Solution: Unified Toast System + +Add a toast system to `@ewjdev/anyclick-react` that: + +- Follows the existing `showToast(message, type)` API pattern +- Uses the same 4 types and colors as the extension +- Auto-dismisses after 4 seconds (matching `content.ts`) +- Exports a `showToast` function for imperative use +- Auto-triggers on submission success/error by default + +### Implementation Architecture + +``` +packages/anyclick-react/src/ +├── Toast/ +│ ├── index.ts +│ ├── Toast.tsx # Individual toast component +│ ├── ToastContainer.tsx # Portal container for toasts +│ ├── toastStore.ts # Zustand store for toast state +│ ├── types.ts # Toast types +│ └── styles.ts # Toast styles (using standard colors) +├── AnyclickProvider.tsx # Add ToastContainer + auto-toast on submit +└── index.ts # Export showToast + toast utilities +``` + +### Toast Types + +```typescript +/** Toast type - matches extension pattern */ +type ToastType = 'success' | 'error' | 'warning' | 'info'; + +/** Internal toast state */ +interface Toast { + id: string; + type: ToastType; + message: string; + duration?: number; // ms, default 4000 +} + +/** Configuration for automatic toasts */ +interface ToastConfig { + enabled?: boolean; // default: true + position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'; + maxToasts?: number; // default: 3 + successMessage?: string; // default: "Feedback sent!" + errorMessage?: string; // default: "Failed to send feedback" +} + +/** Standard colors - matching extension */ +const TOAST_COLORS = { + success: { bg: "#10b981", border: "#059669" }, + error: { bg: "#ef4444", border: "#dc2626" }, + warning: { bg: "#f59e0b", border: "#d97706" }, + info: { bg: "#3b82f6", border: "#2563eb" }, +} as const; +``` + +### Public API + +```typescript +// Imperative API - matches extension pattern +import { showToast } from "@ewjdev/anyclick-react"; + +showToast("Settings saved!", "success"); +showToast("Upload failed", "error"); +showToast("Feature coming soon", "warning"); +showToast("Processing...", "info"); + +// Provider config for automatic toasts + +``` + +### Provider Integration + +```tsx +// In AnyclickProvider.tsx +import { showToast, ToastContainer } from "./Toast"; + +export function AnyclickProvider({ + // ... existing props + toastConfig = { enabled: true }, +}: AnyclickProviderProps) { + + const submitAnyclick = useCallback(async (...) => { + setIsSubmitting(true); + try { + await client.submitAnyclick(...); + + // Show success toast - uses imperative API like extension + if (toastConfig.enabled !== false) { + showToast( + toastConfig.successMessage ?? "Feedback sent!", + "success" + ); + } + + onSubmitSuccess?.(payload); + } catch (error) { + // Show error toast + if (toastConfig.enabled !== false) { + showToast( + toastConfig.errorMessage ?? "Failed to send feedback", + "error" + ); + } + + onSubmitError?.(error, payload); + } finally { + setIsSubmitting(false); + // ... close menu + } + }, [...]); + + return ( + + {/* Toast container renders via portal - only one needed */} + + {/* ... rest of provider */} + + ); +} +``` + +### Usage Examples + +```tsx +// Automatic toasts on submission (default behavior) + + + + +// Custom messages + + + + +// Disable automatic toasts (handle manually) + showToast("Custom success!", "success")} +> + + + +// Imperative use anywhere in the app +import { showToast } from "@ewjdev/anyclick-react"; + +function MyComponent() { + const handleAction = () => { + // Do something + showToast("Action completed", "success"); + }; +} +``` + +### Toast Component Design + +Follows the extension's visual style - simple, colored background, single message: + +```tsx +import { motion, AnimatePresence } from "motion/react"; + +const TOAST_COLORS = { + success: { bg: "#10b981", border: "#059669" }, + error: { bg: "#ef4444", border: "#dc2626" }, + warning: { bg: "#f59e0b", border: "#d97706" }, + info: { bg: "#3b82f6", border: "#2563eb" }, +} as const; + +function Toast({ + toast, + onDismiss +}: { + toast: { id: string; type: ToastType; message: string }; + onDismiss: () => void; +}) { + const { bg, border } = TOAST_COLORS[toast.type]; + + return ( + + {toast.message} + + + ); +} + +/** Zustand store for toast state */ +const useToastStore = create<{ + toasts: Toast[]; + addToast: (message: string, type: ToastType) => void; + removeToast: (id: string) => void; +}>((set) => ({ + toasts: [], + addToast: (message, type) => { + const id = Math.random().toString(36).slice(2); + set((state) => ({ + toasts: [...state.toasts.slice(-2), { id, message, type }], // max 3 + })); + // Auto-dismiss after 4 seconds (matching extension) + setTimeout(() => { + set((state) => ({ + toasts: state.toasts.filter((t) => t.id !== id), + })); + }, 4000); + }, + removeToast: (id) => + set((state) => ({ + toasts: state.toasts.filter((t) => t.id !== id), + })), +})); + +/** Public imperative API - matches extension pattern */ +export function showToast( + message: string, + type: ToastType = "info" +): void { + useToastStore.getState().addToast(message, type); +} +``` + +### Enhanced Loading States in ContextMenu + +Update the submission UI to be clearer: + +**Current:** + +``` +[Send] → "Sending..." +``` + +**Proposed:** + +``` +[Send] → [Spinner] Sending... → [Success] ✓ Sent! +``` +```tsx +// In CommentForm + +``` + +--- + +## File Changes Summary + +### Part 1: FeaturesSection + +| File | Action | Description | + +|------|--------|-------------| + +| `apps/web/src/components/FeaturesSection.tsx` | **Modify** | Convert to client component, add expandable cards with workflow visualization | + +### Part 2: Toast System + +| File | Action | Description | + +|------|--------|-------------| + +| `packages/anyclick-react/src/Toast/Toast.tsx` | **Create** | Toast component | + +| `packages/anyclick-react/src/Toast/ToastContainer.tsx` | **Create** | Container with portal | + +| `packages/anyclick-react/src/Toast/toastStore.ts` | **Create** | Zustand store | + +| `packages/anyclick-react/src/Toast/types.ts` | **Create** | Type definitions | + +| `packages/anyclick-react/src/Toast/styles.ts` | **Create** | Style constants | + +| `packages/anyclick-react/src/Toast/index.ts` | **Create** | Barrel export | -- Smooth height transition animations -- Additional details, examples, or interactive elements -- Visual indicators for context types (for context-aware card) -- Copy-to-clipboard functionality for selectors +| `packages/anyclick-react/src/AnyclickProvider.tsx` | **Modify** | Add toast on submit | + +| `packages/anyclick-react/src/types.ts` | **Modify** | Add ToastConfig type | + +| `packages/anyclick-react/src/index.ts` | **Modify** | Export `showToast`, `ToastContainer`, `ToastConfig` | + +| `packages/anyclick-react/src/ContextMenu.tsx` | **Modify** | Improve loading UI | + +--- + +## Implementation Order + +### Phase 1: Global Toast System (Do First) + +1. Create Toast component and store +2. Integrate into AnyclickProvider +3. Test submission flow end-to-end + +### Phase 2: FeaturesSection Enhancement + +1. Create WorkflowVisualization component +2. Define feature configurations with workflows +3. Implement expandable FeatureCard +4. Add animation with framer-motion +5. Test all cards expand/collapse properly + +--- ## Technical Considerations -- Use `useState` for managing expanded state -- Use `useCallback` for event handlers to prevent re-renders -- Implement proper TypeScript types for card configurations -- Ensure accessibility (keyboard navigation, ARIA labels) -- Handle edge cases (clicking outside to collapse, preventing menu on left-click) -- Use Tailwind classes for styling and animations -- Leverage existing `AnyclickProvider` from `@ewjdev/anyclick-react` +### Dependencies + +- **framer-motion** - Already in project, use for animations +- **zustand** - Already in project, use for toast store +- No new dependencies required -## File Changes +### Accessibility -1. **Modify**: `apps/web/src/components/FeaturesSection.tsx` +- Toast announcements via `aria-live="polite"` +- Escape key to dismiss +- Focus management when cards expand +- Reduced motion support -- Add state management -- Add left/right click handlers -- Add expanded content sections -- Integrate scoped AnyclickProvider per card -- Add notification triggers +### Performance -2. **Create** (optional): `apps/web/src/components/Toast.tsx` +- Toast store is minimal (just array of toasts) +- WorkflowVisualization uses `setInterval`, cleanup on unmount +- Card expansion uses CSS Grid layout shift (GPU accelerated) -- Simple toast notification component -- Or use inline notification state within FeaturesSection +--- + +## Success Criteria -3. **Create** (optional): `apps/web/src/components/FeatureCard.tsx` +### FeaturesSection -- Extract card logic into reusable component -- Handle expansion, menu, and notifications +- [ ] All 6 cards expand on click +- [ ] Workflow visualization auto-advances through 4 steps +- [ ] Animation is smooth (60fps) +- [ ] "Learn more" links navigate correctly +- [ ] Cards collapse when clicking X or clicking outside -## Navigation Targets +### Toast System -- GitHub integration: `/examples/github-integration` or `/examples/jira-integration` -- Docs: `/docs` or `/docs/react` -- Examples: `/examples` or specific example pages -- Getting started: `/getting-started` or homepage +- [ ] Success toast appears after feedback submission +- [ ] Error toast appears on submission failure +- [ ] Toasts auto-dismiss after 4 seconds +- [ ] Multiple toasts stack properly +- [ ] Toast can be manually dismissed +- [ ] Works in all browsers + +--- -## Notification Messages Examples +## Out of Scope -- Success: "Screenshot captured successfully", "Element reference saved" -- Warning: "Feature coming soon", "This action requires setup" -- Error: "Failed to capture screenshot", "Unable to save reference" -- Info: "Navigating to documentation...", "Opening example..." \ No newline at end of file +- Toast persistence across page navigation +- Toast action buttons (undo, retry) +- Custom toast themes per provider +- Sound effects on toast \ No newline at end of file From d0903f6bfdb8556f98100f8f3bbe8c8a432a6bee Mon Sep 17 00:00:00 2001 From: Eric johnson Date: Thu, 11 Dec 2025 14:51:49 -0800 Subject: [PATCH 3/4] updated features to show multiple steps --- apps/web/src/components/FeaturesSection.tsx | 532 +++++++++++++++--- .../anyclick-react/src/AnyclickProvider.tsx | 25 +- packages/anyclick-react/src/ContextMenu.tsx | 14 +- packages/anyclick-react/src/Toast/Toast.tsx | 76 +++ .../src/Toast/ToastContainer.tsx | 80 +++ packages/anyclick-react/src/Toast/index.ts | 43 ++ packages/anyclick-react/src/Toast/styles.ts | 117 ++++ .../anyclick-react/src/Toast/toastStore.ts | 120 ++++ packages/anyclick-react/src/Toast/types.ts | 89 +++ packages/anyclick-react/src/index.ts | 46 ++ packages/anyclick-react/src/types.ts | 23 + 11 files changed, 1088 insertions(+), 77 deletions(-) create mode 100644 packages/anyclick-react/src/Toast/Toast.tsx create mode 100644 packages/anyclick-react/src/Toast/ToastContainer.tsx create mode 100644 packages/anyclick-react/src/Toast/index.ts create mode 100644 packages/anyclick-react/src/Toast/styles.ts create mode 100644 packages/anyclick-react/src/Toast/toastStore.ts create mode 100644 packages/anyclick-react/src/Toast/types.ts diff --git a/apps/web/src/components/FeaturesSection.tsx b/apps/web/src/components/FeaturesSection.tsx index 873f675..ee5577e 100644 --- a/apps/web/src/components/FeaturesSection.tsx +++ b/apps/web/src/components/FeaturesSection.tsx @@ -1,13 +1,456 @@ +"use client"; + +import { type ReactNode, useCallback, useEffect, useState } from "react"; import { + ArrowRight, Camera, + Check, GitBranch, Layers, + Loader2, MousePointerClick, Terminal, + X, Zap, } from "lucide-react"; +import { AnimatePresence, motion } from "motion/react"; +import Link from "next/link"; + +// ============================================================================ +// Types +// ============================================================================ + +interface WorkflowStep { + label: string; + detail: string; +} + +interface FeatureConfig { + id: string; + icon: ReactNode; + title: string; + description: string; + expandedDescription: string; + workflow: WorkflowStep[]; + linkHref: string; + linkText: string; + color: string; + colorClasses: { + border: string; + bg: string; + text: string; + iconBg: string; + dot: string; + dotMuted: string; + }; +} + +// ============================================================================ +// Feature Configurations +// ============================================================================ + +const features: FeatureConfig[] = [ + { + id: "context-capture", + icon: , + title: "Context-Aware Capture", + description: + "Right-click any element to capture its full DOM context, including selectors, data attributes, ancestors, and surrounding page information.", + expandedDescription: + "When you right-click an element, Anyclick captures comprehensive context about that element and its surroundings—CSS selectors, attributes, parent containers, and more.", + workflow: [ + { label: "Detecting element", detail: "Finding target under cursor..." }, + { + label: "Capturing context", + detail: "CSS selector, attributes, ancestors", + }, + { label: "Analyzing hierarchy", detail: "Parent containers, siblings" }, + { label: "Context ready", detail: "Full element context captured ✓" }, + ], + linkHref: "/examples/basic", + linkText: "Try the demo", + color: "violet", + colorClasses: { + border: "border-violet-500/30", + bg: "bg-violet-500", + text: "text-violet-400", + iconBg: "from-violet-500/20 to-violet-500/5", + dot: "bg-violet-500", + dotMuted: "bg-violet-500/50", + }, + }, + { + id: "visual-capture", + icon: , + title: "Visual Capture", + description: + "Automatically capture screenshots of the target element, its container, and the full page—all included in the feedback payload.", + expandedDescription: + "Anyclick captures multiple screenshots automatically—the clicked element, its container, and the full viewport—providing complete visual context for debugging.", + workflow: [ + { + label: "Preparing capture", + detail: "Initializing screenshot engine...", + }, + { label: "Capturing element", detail: "Taking element screenshot..." }, + { label: "Capturing viewport", detail: "Taking full page screenshot..." }, + { label: "Screenshots ready", detail: "2 screenshots captured ✓" }, + ], + linkHref: "/examples/basic", + linkText: "See it in action", + color: "cyan", + colorClasses: { + border: "border-cyan-500/30", + bg: "bg-cyan-500", + text: "text-cyan-400", + iconBg: "from-cyan-500/20 to-cyan-500/5", + dot: "bg-cyan-500", + dotMuted: "bg-cyan-500/50", + }, + }, + { + id: "github-integration", + icon: , + title: "Code Source Integration", + description: + "Automatically create GitHub Issues with rich context, formatted markdown, and embedded screenshots for seamless issue tracking.", + expandedDescription: + "Connect to GitHub and automatically create issues with all captured context—formatted markdown, embedded screenshots, and element details ready for developers.", + workflow: [ + { label: "Gathering context", detail: "Element data + screenshots..." }, + { + label: "Formatting issue", + detail: "Creating markdown with context...", + }, + { label: "Creating issue", detail: "Sending to GitHub API..." }, + { label: "Issue created", detail: "Issue #234 created ✓" }, + ], + linkHref: "/examples/github-integration", + linkText: "View integration", + color: "emerald", + colorClasses: { + border: "border-emerald-500/30", + bg: "bg-emerald-500", + text: "text-emerald-400", + iconBg: "from-emerald-500/20 to-emerald-500/5", + dot: "bg-emerald-500", + dotMuted: "bg-emerald-500/50", + }, + }, + { + id: "ai-agent", + icon: , + title: "AI Agent", + description: + "Launch Cursor's AI agent directly from feedback—locally during development or via cloud agent for instant code fixes.", + expandedDescription: + "Trigger AI-powered code assistance directly from captured feedback. The AI agent receives full element context to understand and fix issues automatically.", + workflow: [ + { label: "Analyzing context", detail: "AI processing element..." }, + { label: "Understanding intent", detail: "Determining action type..." }, + { label: "Generating response", detail: "Creating suggestions..." }, + { label: "Ready", detail: "AI response ready ✓" }, + ], + linkHref: "/examples/cursor-local", + linkText: "Explore AI features", + color: "amber", + colorClasses: { + border: "border-amber-500/30", + bg: "bg-amber-500", + text: "text-amber-400", + iconBg: "from-amber-500/20 to-amber-500/5", + dot: "bg-amber-500", + dotMuted: "bg-amber-500/50", + }, + }, + { + id: "framework-agnostic", + icon: , + title: "Framework Agnostic", + description: + "Core library works with any JavaScript framework. Use the React provider for React apps, or build your own integration.", + expandedDescription: + "Anyclick's core is framework-agnostic. Use our React provider for React/Next.js apps, or integrate the core library with Vue, Svelte, or vanilla JavaScript.", + workflow: [ + { + label: "Detecting framework", + detail: "Checking for React/Vue/Svelte...", + }, + { label: "Loading adapter", detail: "Initializing React adapter..." }, + { label: "Attaching listeners", detail: "Context menu ready..." }, + { label: "Active", detail: "Anyclick enabled ✓" }, + ], + linkHref: "/docs/react", + linkText: "View docs", + color: "rose", + colorClasses: { + border: "border-rose-500/30", + bg: "bg-rose-500", + text: "text-rose-400", + iconBg: "from-rose-500/20 to-rose-500/5", + dot: "bg-rose-500", + dotMuted: "bg-rose-500/50", + }, + }, + { + id: "zero-config", + icon: , + title: "Zero Config", + description: + "Works out of the box with sensible defaults. Right-click anywhere in your app and start capturing feedback immediately.", + expandedDescription: + "Get started in seconds. Wrap your app with AnyclickProvider, and you're ready to capture feedback. No complex configuration required.", + workflow: [ + { label: "Installing", detail: "npm install @ewjdev/anyclick-react" }, + { label: "Wrapping app", detail: "Adding AnyclickProvider..." }, + { label: "Configuring", detail: "Default settings applied..." }, + { label: "Ready", detail: "Start right-clicking! ✓" }, + ], + linkHref: "/docs/getting-started", + linkText: "Get started", + color: "indigo", + colorClasses: { + border: "border-indigo-500/30", + bg: "bg-indigo-500", + text: "text-indigo-400", + iconBg: "from-indigo-500/20 to-indigo-500/5", + dot: "bg-indigo-500", + dotMuted: "bg-indigo-500/50", + }, + }, +]; + +// ============================================================================ +// WorkflowVisualization Component +// ============================================================================ + +function WorkflowVisualization({ + steps, + isActive, + colorClasses, +}: { + steps: WorkflowStep[]; + isActive: boolean; + colorClasses: FeatureConfig["colorClasses"]; +}) { + const [currentStep, setCurrentStep] = useState(0); + + useEffect(() => { + if (!isActive) { + setCurrentStep(0); + return; + } + + const interval = setInterval(() => { + setCurrentStep((prev) => (prev + 1) % steps.length); + }, 1500); + + return () => clearInterval(interval); + }, [isActive, steps.length]); + + const isLastStep = currentStep === steps.length - 1; + + return ( +
+ {/* Progress dots */} +
+ {steps.map((_, i) => ( +
+ ))} +
+ + {/* Current step display */} +
+
+ {isLastStep ? ( + + ) : ( + + )} + + {steps[currentStep].label} + +
+

+ {steps[currentStep].detail} +

+
+
+ ); +} + +// ============================================================================ +// FeatureCard Component +// ============================================================================ + +function FeatureCard({ + feature, + isExpanded, + onToggle, +}: { + feature: FeatureConfig; + isExpanded: boolean; + onToggle: () => void; +}) { + const handleClick = useCallback(() => { + if (!isExpanded) { + onToggle(); + } + }, [isExpanded, onToggle]); + + const handleClose = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + onToggle(); + }, + [onToggle], + ); + + return ( + + + {!isExpanded ? ( + // Collapsed content + +
+ {feature.icon} +
+

+ {feature.title} +

+

+ {feature.description} +

+ + Learn more + +
+ ) : ( + // Expanded content + + {/* Header with close button */} +
+
+
+ + {feature.icon} + +
+

+ {feature.title} +

+
+ +
+ + {/* Extended description */} +

+ {feature.expandedDescription} +

+ + {/* Workflow visualization */} + + + {/* CTA link */} + e.stopPropagation()} + className={` + inline-flex items-center gap-2 mt-4 px-4 py-2 rounded-lg + ${feature.colorClasses.bg} text-white text-sm font-medium + hover:opacity-90 transition-opacity + `} + > + {feature.linkText} + + +
+ )} +
+
+ ); +} + +// ============================================================================ +// FeaturesSection Component +// ============================================================================ const FeaturesSection = () => { + const [expandedId, setExpandedId] = useState(null); + + const handleToggle = useCallback((id: string) => { + setExpandedId((prev) => (prev === id ? null : id)); + }, []); + + // Close expanded card when clicking outside + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + const target = e.target as HTMLElement; + // If clicking on the grid background (not a card), close expanded card + if ( + expandedId && + target.closest("[data-features-grid]") && + !target.closest("[data-feature-card]") + ) { + setExpandedId(null); + } + }; + + document.addEventListener("click", handleClickOutside); + return () => document.removeEventListener("click", handleClickOutside); + }, [expandedId]); + return (
@@ -20,82 +463,21 @@ const FeaturesSection = () => {

-
- {/* Feature 1 */} -
-
- -
-

Context-Aware Capture

-

- Right-click any element to capture its full DOM context, including - selectors, data attributes, ancestors, and surrounding page - information. -

-
- - {/* Feature 2 */} -
-
- -
-

Visual Capture

-

- Automatically capture screenshots of the target element, its - container, and the full page—all included in the feedback payload. -

-
- - {/* Feature 3 */} -
-
- + + {features.map((feature) => ( +
+ handleToggle(feature.id)} + />
-

- Code Source Integration -

-

- Automatically create GitHub Issues with rich context, formatted - markdown, and embedded screenshots for seamless issue tracking. -

-
- - {/* Feature 4 */} -
-
- -
-

AI Agent

-

- Launch Cursor's AI agent directly from feedback—locally during - development or via cloud agent for instant code fixes. -

-
- - {/* Feature 5 */} -
-
- -
-

Framework Agnostic

-

- Core library works with any JavaScript framework. Use the React - provider for React apps, or build your own integration. -

-
- - {/* Feature 6 */} -
-
- -
-

Zero Config

-

- Works out of the box with sensible defaults. Right-click anywhere in - your app and start capturing feedback immediately. -

-
-
+ ))} +
); }; diff --git a/packages/anyclick-react/src/AnyclickProvider.tsx b/packages/anyclick-react/src/AnyclickProvider.tsx index c577b07..657fde9 100644 --- a/packages/anyclick-react/src/AnyclickProvider.tsx +++ b/packages/anyclick-react/src/AnyclickProvider.tsx @@ -26,6 +26,7 @@ import type { } from "@ewjdev/anyclick-core"; import { createAnyclickClient } from "@ewjdev/anyclick-core"; import { ContextMenu } from "./ContextMenu"; +import { ToastContainer, showToast } from "./Toast"; import { AnyclickContext, useAnyclick } from "./context"; import { findContainerParent } from "./highlight"; import { type ProviderInstance, useProviderStore } from "./store"; @@ -90,6 +91,7 @@ export function AnyclickProvider({ stripAttributes, targetFilter, theme, + toastConfig, touchHoldDurationMs, touchMoveThreshold, }: AnyclickProviderProps) { @@ -343,6 +345,21 @@ export function AnyclickProvider({ metadata, screenshots, }); + + // Show success toast if enabled (default: true) + if (toastConfig?.enabled !== false) { + showToast(toastConfig?.successMessage ?? "Feedback sent!", "success"); + } + } catch (error) { + // Show error toast if enabled (default: true) + if (toastConfig?.enabled !== false) { + showToast( + toastConfig?.errorMessage ?? "Failed to send feedback", + "error", + ); + } + // Re-throw so the client's onSubmitError callback can handle it + throw error; } finally { setIsSubmitting(false); setMenuVisible(false); @@ -351,7 +368,7 @@ export function AnyclickProvider({ setContainerElement(null); } }, - [metadata], + [metadata, toastConfig], ); // Open menu programmatically @@ -458,6 +475,9 @@ export function AnyclickProvider({ children ); + // Only render ToastContainer for non-scoped (root) providers to avoid duplicates + const shouldRenderToastContainer = !scoped && toastConfig?.enabled !== false; + return (
@@ -478,6 +498,9 @@ export function AnyclickProvider({ targetElement={targetElement} visible={menuVisible && !effectiveDisabled} /> + {shouldRenderToastContainer && ( + + )}
); diff --git a/packages/anyclick-react/src/ContextMenu.tsx b/packages/anyclick-react/src/ContextMenu.tsx index 8e6737d..97027ea 100644 --- a/packages/anyclick-react/src/ContextMenu.tsx +++ b/packages/anyclick-react/src/ContextMenu.tsx @@ -13,6 +13,7 @@ import { ChevronRightIcon, FlagIcon, GripVertical, + Loader2, PlusIcon, Sparkles, ThumbsUpIcon, @@ -212,9 +213,20 @@ const CommentForm = React.memo(function CommentForm({ ...menuStyles.button, ...menuStyles.submitButton, ...(isSubmitting ? menuStyles.submitButtonDisabled : {}), + display: "flex", + alignItems: "center", + justifyContent: "center", + gap: 6, }} > - {isSubmitting ? "Sending..." : "Send"} + {isSubmitting ? ( + <> + + Sending... + + ) : ( + "Send" + )}
diff --git a/packages/anyclick-react/src/Toast/Toast.tsx b/packages/anyclick-react/src/Toast/Toast.tsx new file mode 100644 index 0000000..57188ea --- /dev/null +++ b/packages/anyclick-react/src/Toast/Toast.tsx @@ -0,0 +1,76 @@ +"use client"; + +/** + * Toast component for @ewjdev/anyclick-react + * + * Individual toast notification with animation support. + * + * @module Toast/Toast + * @since 1.6.0 + */ +import React, { useCallback } from "react"; +import { X } from "lucide-react"; +import { motion } from "motion/react"; +import { TOAST_COLORS, closeButtonStyles, toastStyles } from "./styles"; +import type { Toast as ToastType } from "./types"; + +export interface ToastProps { + /** Toast data */ + toast: ToastType; + /** Callback when toast is dismissed */ + onDismiss: (id: string) => void; +} + +/** + * Individual toast notification component. + * + * Renders a toast with: + * - Colored background based on type + * - Message text + * - Dismiss button + * - Enter/exit animations + */ +export const Toast = React.memo(function Toast({ + toast, + onDismiss, +}: ToastProps) { + const { bg, border, text } = TOAST_COLORS[toast.type]; + + const handleDismiss = useCallback(() => { + onDismiss(toast.id); + }, [onDismiss, toast.id]); + + return ( + + {toast.message} + + + ); +}); diff --git a/packages/anyclick-react/src/Toast/ToastContainer.tsx b/packages/anyclick-react/src/Toast/ToastContainer.tsx new file mode 100644 index 0000000..3aa5e84 --- /dev/null +++ b/packages/anyclick-react/src/Toast/ToastContainer.tsx @@ -0,0 +1,80 @@ +"use client"; + +/** + * ToastContainer component for @ewjdev/anyclick-react + * + * Renders toasts in a fixed position container using a portal. + * + * @module Toast/ToastContainer + * @since 1.6.0 + */ +import React, { useCallback, useEffect, useState } from "react"; +import { createPortal } from "react-dom"; +import { AnimatePresence } from "motion/react"; +import { Toast } from "./Toast"; +import { POSITION_STYLES, containerStyles } from "./styles"; +import { useToastStore } from "./toastStore"; +import type { ToastConfig } from "./types"; + +export interface ToastContainerProps { + /** Position of the toast container */ + position?: ToastConfig["position"]; +} + +/** + * Container component that renders all active toasts. + * + * Uses a portal to render toasts at the document body level, + * ensuring they appear above all other content. + * + * @example + * ```tsx + * // Typically included automatically by AnyclickProvider + * + * ``` + */ +export function ToastContainer({ + position = "bottom-right", +}: ToastContainerProps) { + const [mounted, setMounted] = useState(false); + const toasts = useToastStore((state) => state.toasts); + const removeToast = useToastStore((state) => state.removeToast); + + // Only render after mount to avoid SSR hydration issues + useEffect(() => { + setMounted(true); + }, []); + + const handleDismiss = useCallback( + (id: string) => { + removeToast(id); + }, + [removeToast], + ); + + // Don't render on server or if no toasts + if (!mounted) { + return null; + } + + const positionStyle = POSITION_STYLES[position]; + + const container = ( +
+ + {toasts.map((toast) => ( + + ))} + +
+ ); + + // Render via portal to document body + return createPortal(container, document.body); +} diff --git a/packages/anyclick-react/src/Toast/index.ts b/packages/anyclick-react/src/Toast/index.ts new file mode 100644 index 0000000..0e51409 --- /dev/null +++ b/packages/anyclick-react/src/Toast/index.ts @@ -0,0 +1,43 @@ +/** + * Toast module for @ewjdev/anyclick-react + * + * Provides toast notifications following the showToast pattern + * from anyclick-extension. + * + * @module Toast + * @since 1.6.0 + */ + +// Components +export { Toast } from "./Toast"; +export type { ToastProps } from "./Toast"; + +export { ToastContainer } from "./ToastContainer"; +export type { ToastContainerProps } from "./ToastContainer"; + +// Store and imperative API +export { + useToastStore, + showToast, + dismissToast, + clearToasts, +} from "./toastStore"; + +// Types +export type { + Toast as ToastData, + ToastType, + ToastConfig, + ToastStore, +} from "./types"; + +// Styles (for advanced customization) +export { + TOAST_COLORS, + DEFAULT_TOAST_DURATION, + DEFAULT_MAX_TOASTS, + POSITION_STYLES, + containerStyles, + toastStyles, + closeButtonStyles, +} from "./styles"; diff --git a/packages/anyclick-react/src/Toast/styles.ts b/packages/anyclick-react/src/Toast/styles.ts new file mode 100644 index 0000000..879ef53 --- /dev/null +++ b/packages/anyclick-react/src/Toast/styles.ts @@ -0,0 +1,117 @@ +/** + * Toast styles for @ewjdev/anyclick-react + * + * Uses the same color palette as anyclick-extension for consistency. + * + * @module Toast/styles + * @since 1.6.0 + */ +import type { ToastType } from "./types"; + +/** + * Standard toast colors matching the extension. + * - success: emerald + * - error: red + * - warning: amber + * - info: blue + */ +export const TOAST_COLORS: Record< + ToastType, + { bg: string; border: string; text: string } +> = { + success: { bg: "#10b981", border: "#059669", text: "#ffffff" }, + error: { bg: "#ef4444", border: "#dc2626", text: "#ffffff" }, + warning: { bg: "#f59e0b", border: "#d97706", text: "#ffffff" }, + info: { bg: "#3b82f6", border: "#2563eb", text: "#ffffff" }, +} as const; + +/** + * Default duration for toast auto-dismiss (4 seconds). + * Matches the extension's content.ts behavior. + */ +export const DEFAULT_TOAST_DURATION = 4000; + +/** + * Maximum number of toasts to show at once. + */ +export const DEFAULT_MAX_TOASTS = 3; + +/** + * Position styles for the toast container. + */ +export const POSITION_STYLES: Record< + "top-right" | "top-left" | "bottom-right" | "bottom-left", + React.CSSProperties +> = { + "top-right": { + top: 20, + right: 20, + flexDirection: "column", + }, + "top-left": { + top: 20, + left: 20, + flexDirection: "column", + }, + "bottom-right": { + bottom: 20, + right: 20, + flexDirection: "column-reverse", + }, + "bottom-left": { + bottom: 20, + left: 20, + flexDirection: "column-reverse", + }, +}; + +/** + * Base container styles for the toast stack. + */ +export const containerStyles: React.CSSProperties = { + position: "fixed", + display: "flex", + gap: 8, + zIndex: 999999, + pointerEvents: "none", +}; + +/** + * Base toast styles. + */ +export const toastStyles: React.CSSProperties = { + display: "flex", + alignItems: "center", + gap: 8, + padding: "12px 16px", + borderRadius: 8, + fontSize: 14, + fontWeight: 500, + fontFamily: + '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', + boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15), 0 0 1px rgba(0, 0, 0, 0.1)", + backdropFilter: "blur(8px)", + pointerEvents: "auto", + maxWidth: 360, + minWidth: 200, +}; + +/** + * Close button styles. + */ +export const closeButtonStyles: React.CSSProperties = { + display: "flex", + alignItems: "center", + justifyContent: "center", + width: 20, + height: 20, + padding: 0, + border: "none", + background: "transparent", + color: "inherit", + opacity: 0.7, + cursor: "pointer", + borderRadius: 4, + transition: "opacity 0.15s ease", + flexShrink: 0, +}; diff --git a/packages/anyclick-react/src/Toast/toastStore.ts b/packages/anyclick-react/src/Toast/toastStore.ts new file mode 100644 index 0000000..7ca8167 --- /dev/null +++ b/packages/anyclick-react/src/Toast/toastStore.ts @@ -0,0 +1,120 @@ +/** + * Toast store for @ewjdev/anyclick-react + * + * Uses Zustand for lightweight state management. + * Provides both hook-based and imperative API access. + * + * @module Toast/toastStore + * @since 1.6.0 + */ +import { create } from "zustand"; +import { DEFAULT_MAX_TOASTS, DEFAULT_TOAST_DURATION } from "./styles"; +import type { Toast, ToastStore, ToastType } from "./types"; + +/** + * Generate a unique ID for toasts. + */ +function generateId(): string { + return Math.random().toString(36).slice(2, 11); +} + +/** + * Zustand store for managing toast state. + * + * @internal + */ +export const useToastStore = create((set, get) => ({ + toasts: [], + + addToast: (message: string, type: ToastType, duration?: number) => { + const id = generateId(); + const actualDuration = duration ?? DEFAULT_TOAST_DURATION; + + set((state) => ({ + // Keep only the last (maxToasts - 1) toasts to make room for new one + toasts: [ + ...state.toasts.slice(-(DEFAULT_MAX_TOASTS - 1)), + { id, message, type, duration: actualDuration }, + ], + })); + + // Auto-dismiss after duration + setTimeout(() => { + const { toasts } = get(); + // Only remove if the toast still exists (wasn't manually dismissed) + if (toasts.some((t) => t.id === id)) { + set((state) => ({ + toasts: state.toasts.filter((t) => t.id !== id), + })); + } + }, actualDuration); + }, + + removeToast: (id: string) => { + set((state) => ({ + toasts: state.toasts.filter((t) => t.id !== id), + })); + }, + + clearToasts: () => { + set({ toasts: [] }); + }, +})); + +/** + * Show a toast notification. + * + * This is the primary imperative API for showing toasts. + * Matches the `showToast(message, type)` pattern from anyclick-extension. + * + * @param message - The message to display + * @param type - Toast type: 'success' | 'error' | 'warning' | 'info' + * @param duration - Optional duration in ms (default: 4000) + * + * @example + * ```ts + * import { showToast } from "@ewjdev/anyclick-react"; + * + * // Success notification + * showToast("Settings saved!", "success"); + * + * // Error notification + * showToast("Upload failed", "error"); + * + * // Warning notification + * showToast("Feature coming soon", "warning"); + * + * // Info notification (default type) + * showToast("Processing...", "info"); + * showToast("Processing..."); // type defaults to "info" + * ``` + * + * @since 1.6.0 + */ +export function showToast( + message: string, + type: ToastType = "info", + duration?: number, +): void { + useToastStore.getState().addToast(message, type, duration); +} + +/** + * Remove a specific toast by ID. + * + * @param id - The toast ID to remove + * + * @since 1.6.0 + */ +export function dismissToast(id: string): void { + useToastStore.getState().removeToast(id); +} + +/** + * Clear all toasts. + * + * @since 1.6.0 + */ +export function clearToasts(): void { + useToastStore.getState().clearToasts(); +} diff --git a/packages/anyclick-react/src/Toast/types.ts b/packages/anyclick-react/src/Toast/types.ts new file mode 100644 index 0000000..f3e8978 --- /dev/null +++ b/packages/anyclick-react/src/Toast/types.ts @@ -0,0 +1,89 @@ +/** + * Toast types for @ewjdev/anyclick-react + * + * Matches the showToast pattern from anyclick-extension for consistency. + * + * @module Toast/types + * @since 1.6.0 + */ + +/** + * Toast notification type. + * Matches the extension pattern: success | error | warning | info + */ +export type ToastType = "success" | "error" | "warning" | "info"; + +/** + * Internal toast state representation. + */ +export interface Toast { + /** Unique identifier */ + id: string; + /** Toast type determines color */ + type: ToastType; + /** Message to display */ + message: string; + /** Duration in ms before auto-dismiss (default: 4000) */ + duration?: number; +} + +/** + * Configuration for automatic toasts on submission. + * + * @example + * ```tsx + * + * ``` + */ +export interface ToastConfig { + /** + * Whether automatic toasts are enabled. + * @default true + */ + enabled?: boolean; + + /** + * Position of the toast container. + * @default "bottom-right" + */ + position?: "top-right" | "top-left" | "bottom-right" | "bottom-left"; + + /** + * Maximum number of toasts to show at once. + * @default 3 + */ + maxToasts?: number; + + /** + * Custom success message for submissions. + * @default "Feedback sent!" + */ + successMessage?: string; + + /** + * Custom error message for failed submissions. + * @default "Failed to send feedback" + */ + errorMessage?: string; +} + +/** + * Toast store state interface. + */ +export interface ToastStore { + /** Current toasts */ + toasts: Toast[]; + /** Add a new toast */ + addToast: (message: string, type: ToastType, duration?: number) => void; + /** Remove a toast by id */ + removeToast: (id: string) => void; + /** Clear all toasts */ + clearToasts: () => void; +} diff --git a/packages/anyclick-react/src/index.ts b/packages/anyclick-react/src/index.ts index 8cfa092..8abbb63 100644 --- a/packages/anyclick-react/src/index.ts +++ b/packages/anyclick-react/src/index.ts @@ -428,6 +428,52 @@ export type { AnyclickLogoProps, } from "./AnyclickLogo"; +// ============================================================================ +// Toast Notifications +// ============================================================================ + +/** + * Show a toast notification. + * Matches the `showToast(message, type)` pattern from anyclick-extension. + * + * @param message - The message to display + * @param type - Toast type: 'success' | 'error' | 'warning' | 'info' + * @param duration - Optional duration in ms (default: 4000) + * + * @example + * ```ts + * import { showToast } from "@ewjdev/anyclick-react"; + * + * showToast("Settings saved!", "success"); + * showToast("Upload failed", "error"); + * showToast("Feature coming soon", "warning"); + * showToast("Processing...", "info"); + * ``` + * + * @since 1.6.0 + */ +export { showToast, dismissToast, clearToasts } from "./Toast"; + +/** + * Toast container component. + * Automatically included by AnyclickProvider, but can be used standalone. + */ +export { ToastContainer } from "./Toast"; + +/** + * Toast store hook for advanced use cases. + */ +export { useToastStore } from "./Toast"; + +export type { + /** Toast notification type */ + ToastType, + /** Toast configuration for AnyclickProvider */ + ToastConfig, + /** Internal toast data structure */ + ToastData, +} from "./Toast"; + // ============================================================================ // QuickChat // ============================================================================ diff --git a/packages/anyclick-react/src/types.ts b/packages/anyclick-react/src/types.ts index 4a9a449..6b4c265 100644 --- a/packages/anyclick-react/src/types.ts +++ b/packages/anyclick-react/src/types.ts @@ -19,6 +19,9 @@ import type { import { CompactModeConfig } from "./InspectDialog/InspectSimple"; import type { QuickChatConfig } from "./QuickChat/types"; +// Re-export ToastConfig from Toast module for convenience +export type { ToastConfig } from "./Toast/types"; + // ============================================================================ // Theme Configuration // ============================================================================ @@ -387,6 +390,26 @@ export interface AnyclickProviderProps { * Set to null or { disabled: true } to disable anyclick in this subtree. */ theme?: AnyclickTheme | null; + /** + * Configuration for toast notifications. + * By default, toasts are shown on submission success/error. + * Set `enabled: false` to disable automatic toasts. + * + * @example + * ```tsx + * + * ``` + * + * @since 1.6.0 + */ + toastConfig?: import("./Toast/types").ToastConfig; /** Duration in ms to hold touch before triggering context menu (default: 500) */ touchHoldDurationMs?: number; /** Maximum movement in px before touch hold is cancelled (default: 10) */ From 9c8b81e1c6740f11a7dd088530d4d1008e127abf Mon Sep 17 00:00:00 2001 From: Eric johnson Date: Thu, 11 Dec 2025 15:53:09 -0800 Subject: [PATCH 4/4] wip home page features showcasing the information available --- apps/web/src/components/FeaturesSection.tsx | 657 +++++----- .../features/CaptureDetailsPanel.tsx | 67 + .../features/CaptureWorkflowTimeline.tsx | 146 +++ .../features/FeatureCaptureDemo.tsx | 1153 +++++++++++++++++ .../src/components/features/FeatureCard.tsx | 143 ++ apps/web/src/components/features/types.ts | 77 ++ 6 files changed, 1916 insertions(+), 327 deletions(-) create mode 100644 apps/web/src/components/features/CaptureDetailsPanel.tsx create mode 100644 apps/web/src/components/features/CaptureWorkflowTimeline.tsx create mode 100644 apps/web/src/components/features/FeatureCaptureDemo.tsx create mode 100644 apps/web/src/components/features/FeatureCard.tsx create mode 100644 apps/web/src/components/features/types.ts diff --git a/apps/web/src/components/FeaturesSection.tsx b/apps/web/src/components/FeaturesSection.tsx index ee5577e..c3850a7 100644 --- a/apps/web/src/components/FeaturesSection.tsx +++ b/apps/web/src/components/FeaturesSection.tsx @@ -1,72 +1,35 @@ "use client"; -import { type ReactNode, useCallback, useEffect, useState } from "react"; +import * as React from "react"; import { - ArrowRight, Camera, - Check, + Clipboard, + Eye, + FileText, GitBranch, + Info, Layers, - Loader2, MousePointerClick, + RotateCcw, + Sparkles, Terminal, - X, + Target, Zap, } from "lucide-react"; -import { AnimatePresence, motion } from "motion/react"; -import Link from "next/link"; - -// ============================================================================ -// Types -// ============================================================================ - -interface WorkflowStep { - label: string; - detail: string; -} - -interface FeatureConfig { - id: string; - icon: ReactNode; - title: string; - description: string; - expandedDescription: string; - workflow: WorkflowStep[]; - linkHref: string; - linkText: string; - color: string; - colorClasses: { - border: string; - bg: string; - text: string; - iconBg: string; - dot: string; - dotMuted: string; - }; -} - -// ============================================================================ -// Feature Configurations -// ============================================================================ +import { motion } from "motion/react"; +import { cn } from "@/lib/utils"; +import { FeatureCard } from "@/components/features/FeatureCard"; +import type { FeatureConfig } from "@/components/features/types"; const features: FeatureConfig[] = [ { id: "context-capture", - icon: , - title: "Context-Aware Capture", + icon: , + title: "Context-aware container", description: - "Right-click any element to capture its full DOM context, including selectors, data attributes, ancestors, and surrounding page information.", + "Click to expand and explore a capture that starts with element + container context (no auto-play).", expandedDescription: - "When you right-click an element, Anyclick captures comprehensive context about that element and its surroundings—CSS selectors, attributes, parent containers, and more.", - workflow: [ - { label: "Detecting element", detail: "Finding target under cursor..." }, - { - label: "Capturing context", - detail: "CSS selector, attributes, ancestors", - }, - { label: "Analyzing hierarchy", detail: "Parent containers, siblings" }, - { label: "Context ready", detail: "Full element context captured ✓" }, - ], + "Start a capture to see element context, container context, connected data, and how Anyclick metadata is shaped for the next action.", linkHref: "/examples/basic", linkText: "Try the demo", color: "violet", @@ -78,24 +41,62 @@ const features: FeatureConfig[] = [ dot: "bg-violet-500", dotMuted: "bg-violet-500/50", }, + demo: { + defaultPanel: "data", + primaryCta: { + label: "Start capture", + action: { kind: "startCapture", setActivePanel: "data" }, + }, + secondaryCta: { + label: "Pick a target", + action: { kind: "toggleTargetPicker" }, + }, + menuItems: [ + { + id: "view-data", + label: "View capture data", + icon: , + action: { kind: "setPanel", panel: "data" }, + }, + { + id: "view-status", + label: "View status", + icon: , + action: { kind: "setPanel", panel: "status" }, + }, + { + id: "view-next", + label: "View next action", + icon: , + action: { kind: "setPanel", panel: "next" }, + }, + { + id: "copy-payload", + label: "Copy payload JSON", + icon: , + action: { kind: "copy", target: "payloadJson" }, + }, + { + id: "reset", + label: "Reset capture", + icon: , + action: { kind: "reset" }, + }, + ], + runner: { + enableScreenshots: false, + includeInspectInfo: true, + }, + }, }, { id: "visual-capture", - icon: , - title: "Visual Capture", + icon: , + title: "Visual capture", description: - "Automatically capture screenshots of the target element, its container, and the full page—all included in the feedback payload.", + "Capture element/container/viewport screenshots (when supported) and inspect sizes + errors.", expandedDescription: - "Anyclick captures multiple screenshots automatically—the clicked element, its container, and the full viewport—providing complete visual context for debugging.", - workflow: [ - { - label: "Preparing capture", - detail: "Initializing screenshot engine...", - }, - { label: "Capturing element", detail: "Taking element screenshot..." }, - { label: "Capturing viewport", detail: "Taking full page screenshot..." }, - { label: "Screenshots ready", detail: "2 screenshots captured ✓" }, - ], + "Run a visual capture to see screenshots (or fallback errors), masking behavior, and how visuals attach to downstream workflows.", linkHref: "/examples/basic", linkText: "See it in action", color: "cyan", @@ -107,24 +108,62 @@ const features: FeatureConfig[] = [ dot: "bg-cyan-500", dotMuted: "bg-cyan-500/50", }, + demo: { + defaultPanel: "data", + primaryCta: { + label: "Capture screenshots", + action: { kind: "startCapture", setActivePanel: "data" }, + }, + secondaryCta: { + label: "Retake", + action: { kind: "retakeScreenshots" }, + }, + menuItems: [ + { + id: "view-screenshots", + label: "View screenshots", + icon: , + action: { kind: "setPanel", panel: "data" }, + }, + { + id: "view-mask", + label: "View mask list", + icon: , + action: { kind: "setPanel", panel: "data" }, + }, + { + id: "view-status", + label: "View status", + icon: , + action: { kind: "setPanel", panel: "status" }, + }, + { + id: "copy-sizes", + label: "Copy screenshot sizes", + icon: , + action: { kind: "copy", target: "screenshotSummary" }, + }, + { + id: "reset", + label: "Reset capture", + icon: , + action: { kind: "reset" }, + }, + ], + runner: { + enableScreenshots: true, + includeInspectInfo: false, + }, + }, }, { id: "github-integration", - icon: , - title: "Code Source Integration", + icon: , + title: "Code source integration", description: - "Automatically create GitHub Issues with rich context, formatted markdown, and embedded screenshots for seamless issue tracking.", + "Draft rich GitHub issue content from captured context (no network calls in the demo).", expandedDescription: - "Connect to GitHub and automatically create issues with all captured context—formatted markdown, embedded screenshots, and element details ready for developers.", - workflow: [ - { label: "Gathering context", detail: "Element data + screenshots..." }, - { - label: "Formatting issue", - detail: "Creating markdown with context...", - }, - { label: "Creating issue", detail: "Sending to GitHub API..." }, - { label: "Issue created", detail: "Issue #234 created ✓" }, - ], + "Capture context and assemble an issue draft (markdown) showing how element + page context become developer-ready artifacts.", linkHref: "/examples/github-integration", linkText: "View integration", color: "emerald", @@ -136,21 +175,62 @@ const features: FeatureConfig[] = [ dot: "bg-emerald-500", dotMuted: "bg-emerald-500/50", }, + demo: { + defaultPanel: "next", + primaryCta: { + label: "Draft GitHub issue", + action: { kind: "startCapture", setActivePanel: "next" }, + }, + secondaryCta: { + label: "Copy issue markdown", + action: { kind: "copy", target: "issueMarkdown" }, + }, + menuItems: [ + { + id: "view-issue", + label: "View issue draft", + icon: , + action: { kind: "setPanel", panel: "next" }, + }, + { + id: "view-payload", + label: "View payload JSON", + icon: , + action: { kind: "setPanel", panel: "data" }, + }, + { + id: "view-status", + label: "View status", + icon: , + action: { kind: "setPanel", panel: "status" }, + }, + { + id: "copy-issue", + label: "Copy issue markdown", + icon: , + action: { kind: "copy", target: "issueMarkdown" }, + }, + { + id: "reset", + label: "Reset", + icon: , + action: { kind: "reset" }, + }, + ], + runner: { + enableScreenshots: true, + includeInspectInfo: false, + }, + }, }, { id: "ai-agent", - icon: , - title: "AI Agent", + icon: , + title: "AI agent", description: - "Launch Cursor's AI agent directly from feedback—locally during development or via cloud agent for instant code fixes.", + "Shape capture context into a structured agent input for fast debugging and next steps.", expandedDescription: - "Trigger AI-powered code assistance directly from captured feedback. The AI agent receives full element context to understand and fix issues automatically.", - workflow: [ - { label: "Analyzing context", detail: "AI processing element..." }, - { label: "Understanding intent", detail: "Determining action type..." }, - { label: "Generating response", detail: "Creating suggestions..." }, - { label: "Ready", detail: "AI response ready ✓" }, - ], + "Run capture and see the exact context bundle (prompt + structured data) that an agent would receive.", linkHref: "/examples/cursor-local", linkText: "Explore AI features", color: "amber", @@ -162,24 +242,62 @@ const features: FeatureConfig[] = [ dot: "bg-amber-500", dotMuted: "bg-amber-500/50", }, + demo: { + defaultPanel: "next", + primaryCta: { + label: "Ask AI about this", + action: { kind: "startCapture", setActivePanel: "next" }, + }, + secondaryCta: { + label: "Copy agent context", + action: { kind: "copy", target: "agentContext" }, + }, + menuItems: [ + { + id: "view-agent", + label: "View agent input", + icon: , + action: { kind: "setPanel", panel: "next" }, + }, + { + id: "view-payload", + label: "View payload JSON", + icon: , + action: { kind: "setPanel", panel: "data" }, + }, + { + id: "view-status", + label: "View status", + icon: , + action: { kind: "setPanel", panel: "status" }, + }, + { + id: "copy-agent", + label: "Copy agent context", + icon: , + action: { kind: "copy", target: "agentContext" }, + }, + { + id: "reset", + label: "Reset", + icon: , + action: { kind: "reset" }, + }, + ], + runner: { + enableScreenshots: false, + includeInspectInfo: false, + }, + }, }, { id: "framework-agnostic", - icon: , - title: "Framework Agnostic", + icon: , + title: "Framework agnostic", description: - "Core library works with any JavaScript framework. Use the React provider for React apps, or build your own integration.", + "Switch between integration modes and copy the right snippet for your stack.", expandedDescription: - "Anyclick's core is framework-agnostic. Use our React provider for React/Next.js apps, or integrate the core library with Vue, Svelte, or vanilla JavaScript.", - workflow: [ - { - label: "Detecting framework", - detail: "Checking for React/Vue/Svelte...", - }, - { label: "Loading adapter", detail: "Initializing React adapter..." }, - { label: "Attaching listeners", detail: "Context menu ready..." }, - { label: "Active", detail: "Anyclick enabled ✓" }, - ], + "Toggle integration wiring (React vs vanilla, scoped vs global) and see how the same capture payload powers your next action.", linkHref: "/docs/react", linkText: "View docs", color: "rose", @@ -191,21 +309,68 @@ const features: FeatureConfig[] = [ dot: "bg-rose-500", dotMuted: "bg-rose-500/50", }, + demo: { + defaultPanel: "next", + primaryCta: { + label: "Toggle integration mode", + action: { kind: "toggleIntegrationMode" }, + }, + secondaryCta: { + label: "Copy snippet", + action: { kind: "copy", target: "snippet" }, + }, + menuItems: [ + { + id: "view-wiring", + label: "View wiring", + icon: , + action: { kind: "setPanel", panel: "next" }, + }, + { + id: "run-capture", + label: "Run sample capture", + icon: , + action: { kind: "startCapture", setActivePanel: "status" }, + }, + { + id: "view-payload", + label: "View payload JSON", + icon: , + action: { kind: "setPanel", panel: "data" }, + }, + { + id: "view-status", + label: "View status", + icon: , + action: { kind: "setPanel", panel: "status" }, + }, + { + id: "copy-snippet", + label: "Copy snippet", + icon: , + action: { kind: "copy", target: "snippet" }, + }, + { + id: "reset", + label: "Reset", + icon: , + action: { kind: "reset" }, + }, + ], + runner: { + enableScreenshots: false, + includeInspectInfo: false, + }, + }, }, { id: "zero-config", - icon: , - title: "Zero Config", + icon: , + title: "Zero config", description: - "Works out of the box with sensible defaults. Right-click anywhere in your app and start capturing feedback immediately.", + "Copy the minimal setup, then run a sample capture to see what comes out.", expandedDescription: - "Get started in seconds. Wrap your app with AnyclickProvider, and you're ready to capture feedback. No complex configuration required.", - workflow: [ - { label: "Installing", detail: "npm install @ewjdev/anyclick-react" }, - { label: "Wrapping app", detail: "Adding AnyclickProvider..." }, - { label: "Configuring", detail: "Default settings applied..." }, - { label: "Ready", detail: "Start right-clicking! ✓" }, - ], + "Get started in seconds. Copy the setup snippet, then run a sample capture to see the payload shape and default behavior.", linkHref: "/docs/getting-started", linkText: "Get started", color: "indigo", @@ -217,227 +382,67 @@ const features: FeatureConfig[] = [ dot: "bg-indigo-500", dotMuted: "bg-indigo-500/50", }, + demo: { + defaultPanel: "next", + primaryCta: { + label: "Copy install + wrap snippet", + action: { kind: "copy", target: "commands" }, + }, + secondaryCta: { + label: "Run sample capture", + action: { kind: "startCapture", setActivePanel: "data" }, + }, + menuItems: [ + { + id: "copy-snippet", + label: "Copy commands + snippet", + icon: , + action: { kind: "copy", target: "commands" }, + }, + { + id: "view-payload", + label: "View payload JSON", + icon: , + action: { kind: "setPanel", panel: "data" }, + }, + { + id: "view-status", + label: "View status", + icon: , + action: { kind: "setPanel", panel: "status" }, + }, + { + id: "reset", + label: "Reset", + icon: , + action: { kind: "reset" }, + }, + ], + runner: { + enableScreenshots: false, + includeInspectInfo: false, + }, + }, }, ]; -// ============================================================================ -// WorkflowVisualization Component -// ============================================================================ - -function WorkflowVisualization({ - steps, - isActive, - colorClasses, -}: { - steps: WorkflowStep[]; - isActive: boolean; - colorClasses: FeatureConfig["colorClasses"]; -}) { - const [currentStep, setCurrentStep] = useState(0); - - useEffect(() => { - if (!isActive) { - setCurrentStep(0); - return; - } - - const interval = setInterval(() => { - setCurrentStep((prev) => (prev + 1) % steps.length); - }, 1500); - - return () => clearInterval(interval); - }, [isActive, steps.length]); - - const isLastStep = currentStep === steps.length - 1; - - return ( -
- {/* Progress dots */} -
- {steps.map((_, i) => ( -
- ))} -
- - {/* Current step display */} -
-
- {isLastStep ? ( - - ) : ( - - )} - - {steps[currentStep].label} - -
-

- {steps[currentStep].detail} -

-
-
- ); -} - -// ============================================================================ -// FeatureCard Component -// ============================================================================ - -function FeatureCard({ - feature, - isExpanded, - onToggle, -}: { - feature: FeatureConfig; - isExpanded: boolean; - onToggle: () => void; -}) { - const handleClick = useCallback(() => { - if (!isExpanded) { - onToggle(); - } - }, [isExpanded, onToggle]); - - const handleClose = useCallback( - (e: React.MouseEvent) => { - e.stopPropagation(); - onToggle(); - }, - [onToggle], - ); - - return ( - - - {!isExpanded ? ( - // Collapsed content - -
- {feature.icon} -
-

- {feature.title} -

-

- {feature.description} -

- - Learn more - -
- ) : ( - // Expanded content - - {/* Header with close button */} -
-
-
- - {feature.icon} - -
-

- {feature.title} -

-
- -
- - {/* Extended description */} -

- {feature.expandedDescription} -

+export interface FeaturesSectionProps + extends React.HTMLAttributes {} - {/* Workflow visualization */} - - - {/* CTA link */} - e.stopPropagation()} - className={` - inline-flex items-center gap-2 mt-4 px-4 py-2 rounded-lg - ${feature.colorClasses.bg} text-white text-sm font-medium - hover:opacity-90 transition-opacity - `} - > - {feature.linkText} - - -
- )} -
-
- ); -} +export default function FeaturesSection({ + className, + ...props +}: FeaturesSectionProps) { + const [expandedId, setExpandedId] = React.useState(null); -// ============================================================================ -// FeaturesSection Component -// ============================================================================ - -const FeaturesSection = () => { - const [expandedId, setExpandedId] = useState(null); - - const handleToggle = useCallback((id: string) => { + const handleToggle = React.useCallback((id: string) => { setExpandedId((prev) => (prev === id ? null : id)); }, []); - // Close expanded card when clicking outside - useEffect(() => { + // Close expanded card when clicking outside the card area. + React.useEffect(() => { const handleClickOutside = (e: MouseEvent) => { const target = e.target as HTMLElement; - // If clicking on the grid background (not a card), close expanded card if ( expandedId && target.closest("[data-features-grid]") && @@ -452,7 +457,7 @@ const FeaturesSection = () => { }, [expandedId]); return ( -
+

Everything you need for UI context @@ -480,6 +485,4 @@ const FeaturesSection = () => {

); -}; - -export default FeaturesSection; +} diff --git a/apps/web/src/components/features/CaptureDetailsPanel.tsx b/apps/web/src/components/features/CaptureDetailsPanel.tsx new file mode 100644 index 0000000..97e5312 --- /dev/null +++ b/apps/web/src/components/features/CaptureDetailsPanel.tsx @@ -0,0 +1,67 @@ +"use client"; + +import * as React from "react"; +import { cn } from "@/lib/utils"; +import type { DemoPanel } from "./types"; + +export interface CaptureDetailsPanelProps + extends React.HTMLAttributes { + activePanel: DemoPanel; + onChangePanel: (panel: DemoPanel) => void; + panels: { + data: React.ReactNode; + next: React.ReactNode; + status: React.ReactNode; + }; +} + +const panelLabels: Record = { + data: "Data", + next: "Next action", + status: "Status", +}; + +export function CaptureDetailsPanel({ + activePanel, + className, + onChangePanel, + panels, + ...props +}: CaptureDetailsPanelProps) { + return ( +
+
+ {(Object.keys(panelLabels) as DemoPanel[]).map((panel) => ( + + ))} +
+ +
+ {activePanel === "data" + ? panels.data + : activePanel === "status" + ? panels.status + : panels.next} +
+
+ ); +} + diff --git a/apps/web/src/components/features/CaptureWorkflowTimeline.tsx b/apps/web/src/components/features/CaptureWorkflowTimeline.tsx new file mode 100644 index 0000000..e510305 --- /dev/null +++ b/apps/web/src/components/features/CaptureWorkflowTimeline.tsx @@ -0,0 +1,146 @@ +"use client"; + +import * as React from "react"; +import { Check, Loader2, TriangleAlert } from "lucide-react"; +import { cn } from "@/lib/utils"; + +export type CaptureStepId = "connected" | "container" | "element" | "metadata"; +export type CaptureStepStatus = + | "error" + | "idle" + | "running" + | "simulated" + | "success"; + +export interface CaptureStep { + id: CaptureStepId; + label: string; +} + +export interface CaptureStepState { + detail?: string; + durationMs?: number; + errorMessage?: string; + status: CaptureStepStatus; +} + +export interface CaptureWorkflowTimelineProps + extends React.HTMLAttributes { + accent: { + dot: string; + dotMuted: string; + text: string; + }; + steps: CaptureStep[]; + stepStates: Record; +} + +function formatDuration(durationMs?: number) { + if (typeof durationMs !== "number") return null; + if (durationMs < 1000) return `${Math.round(durationMs)}ms`; + return `${(durationMs / 1000).toFixed(2)}s`; +} + +export function CaptureWorkflowTimeline({ + accent, + className, + steps, + stepStates, + ...props +}: CaptureWorkflowTimelineProps) { + return ( +
+ {/* Progress dots */} +
+ {steps.map((step) => { + const state = stepStates[step.id]; + const dotClass = + state.status === "running" + ? `${accent.dot} scale-125` + : state.status === "success" || state.status === "simulated" + ? accent.dotMuted + : state.status === "error" + ? "bg-red-500" + : "bg-white/20"; + + return ( +
+ ); + })} +
+ + {/* Step list */} +
+ {steps.map((step) => { + const state = stepStates[step.id]; + const durationLabel = formatDuration(state.durationMs); + + return ( +
+
+ {state.status === "running" ? ( + + ) : state.status === "success" ? ( + + ) : state.status === "simulated" ? ( + + ) : state.status === "error" ? ( + + ) : ( +
+ )} +
+ +
+
+
+ {step.label} + {state.status === "simulated" ? ( + + simulated + + ) : null} +
+ {durationLabel ? ( +
+ {durationLabel} +
+ ) : null} +
+ + {state.status === "error" && state.errorMessage ? ( +
+ {state.errorMessage} +
+ ) : state.detail ? ( +
+ {state.detail} +
+ ) : ( +
+ {state.status === "idle" ? "Waiting…" : ""} +
+ )} +
+
+ ); + })} +
+
+ ); +} + diff --git a/apps/web/src/components/features/FeatureCaptureDemo.tsx b/apps/web/src/components/features/FeatureCaptureDemo.tsx new file mode 100644 index 0000000..d0042de --- /dev/null +++ b/apps/web/src/components/features/FeatureCaptureDemo.tsx @@ -0,0 +1,1153 @@ +"use client"; + +import * as React from "react"; +import type { + AnyclickPayload, + ElementContext, + ElementInspectInfo, + PageContext, + ScreenshotCapture, + ScreenshotData, +} from "@ewjdev/anyclick-core"; +import { + buildAnyclickPayload, + buildElementContext, + buildPageContext, + captureAllScreenshots, + formatBytes, + getElementInspectInfo, + isScreenshotSupported, +} from "@ewjdev/anyclick-core"; +import { + ContextMenu, + type ContextMenuItem, + showToast, +} from "@ewjdev/anyclick-react"; +import { + Braces, + Clipboard, + Info, + RotateCcw, + Sparkles, + Target, +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import { CaptureDetailsPanel } from "./CaptureDetailsPanel"; +import { + CaptureWorkflowTimeline, + type CaptureStep, + type CaptureStepId, + type CaptureStepState, +} from "./CaptureWorkflowTimeline"; +import type { DemoPanel, FeatureConfig, FeatureDemoAction } from "./types"; + +type CaptureRunStatus = "idle" | "running" | "success"; + +type IntegrationMode = { + framework: "react" | "vanilla"; + scope: "global" | "scoped"; +}; + +type CaptureArtifacts = { + agentInput?: unknown; + connectedData?: unknown; + containerContext?: ElementContext; + elementContext?: ElementContext; + inspectInfo?: ElementInspectInfo; + integrationSnippet?: string; + issueMarkdown?: string; + metadata?: Record; + pageContext?: PageContext; + payload?: AnyclickPayload; + screenshots?: ScreenshotData | null; +}; + +const OFFSCREEN_POSITION = { x: -9999, y: -9999 }; + +function nowMs() { + if (typeof performance !== "undefined" && typeof performance.now === "function") + return performance.now(); + return Date.now(); +} + +async function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function safeJson(value: unknown) { + try { + return JSON.stringify(value, null, 2); + } catch { + return JSON.stringify({ error: "Failed to serialize JSON" }, null, 2); + } +} + +function summarizeScreenshotCapture(capture?: ScreenshotCapture) { + if (!capture) return null; + return { + height: capture.height, + sizeBytes: capture.sizeBytes, + sizeHuman: formatBytes(capture.sizeBytes), + width: capture.width, + }; +} + +function summarizeScreenshots(screenshots: ScreenshotData | null | undefined) { + if (!screenshots) return null; + return { + capturedAt: screenshots.capturedAt, + container: summarizeScreenshotCapture(screenshots.container), + element: summarizeScreenshotCapture(screenshots.element), + errors: screenshots.errors, + viewport: summarizeScreenshotCapture(screenshots.viewport), + }; +} + +function buildGitHubIssueMarkdown(input: { + element: ElementContext; + page: PageContext; + screenshots?: ScreenshotData | null; +}) { + const { element, page } = input; + const screenshots = summarizeScreenshots(input.screenshots); + + const lines: string[] = [ + `## Summary`, + ``, + `Right-click capture on **${element.tag}** at \`${element.selector}\`.`, + ``, + `## Page`, + `- URL: ${page.url}`, + `- Title: ${page.title}`, + ``, + `## Element`, + `- Tag: \`${element.tag}\``, + element.id ? `- ID: \`${element.id}\`` : `- ID: _(none)_`, + element.classes.length + ? `- Classes: \`${element.classes.slice(0, 6).join(" ")}\`` + : `- Classes: _(none)_`, + ``, + `## Context`, + `- Ancestors captured: ${element.ancestors.length}`, + `- data-* keys: ${Object.keys(element.dataAttributes).length}`, + ``, + `## Screenshots`, + ]; + + if (!screenshots) { + lines.push(`- Not captured`); + } else { + lines.push( + `- Element: ${screenshots.element ? screenshots.element.sizeHuman : "n/a"}`, + `- Container: ${ + screenshots.container ? screenshots.container.sizeHuman : "n/a" + }`, + `- Viewport: ${ + screenshots.viewport ? screenshots.viewport.sizeHuman : "n/a" + }`, + screenshots.errors + ? `- Errors: ${Object.keys(screenshots.errors).join(", ")}` + : `- Errors: none`, + ); + } + + lines.push( + ``, + `---`, + `_(This is a demo draft; no network calls are made.)_`, + ); + + return lines.join("\n"); +} + +function buildAgentInput(input: { + element: ElementContext; + page: PageContext; + screenshots?: ScreenshotData | null; +}) { + const screenshots = summarizeScreenshots(input.screenshots); + return { + title: "Quick Ask Agent Input (demo)", + prompt: + "Explain what this element is, why it matters, and what the next debugging action should be.", + context: { + page: { + title: input.page.title, + url: input.page.url, + }, + element: { + selector: input.element.selector, + tag: input.element.tag, + id: input.element.id ?? null, + classes: input.element.classes, + dataAttributes: input.element.dataAttributes, + ancestors: input.element.ancestors, + }, + screenshots, + }, + }; +} + +function buildIntegrationSnippet(mode: IntegrationMode) { + const scopeComment = + mode.scope === "scoped" + ? `// Scoped: only captures inside a subtree` + : `// Global: captures across the document`; + + if (mode.framework === "react") { + return [ + `import { AnyclickProvider } from "@ewjdev/anyclick-react";`, + `import { createHttpAdapter } from "@ewjdev/anyclick-github";`, + ``, + `const adapter = createHttpAdapter({ endpoint: "/api/feedback" });`, + ``, + scopeComment, + ``, + ` `, + ``, + ].join("\n"); + } + + return [ + `import { createAnyclickClient } from "@ewjdev/anyclick-core";`, + ``, + `const client = createAnyclickClient({`, + ` adapter: { submitAnyclick: async (payload) => console.log(payload) },`, + ` // targetFilter: (event, el) => true,`, + `});`, + ``, + scopeComment, + `client.attach();`, + ].join("\n"); +} + +function buildZeroConfigSnippet() { + return [ + `# install`, + `yarn add @ewjdev/anyclick-react @ewjdev/anyclick-github`, + ``, + `# wrap`, + `import { AnyclickProvider } from "@ewjdev/anyclick-react";`, + `import { createHttpAdapter } from "@ewjdev/anyclick-github";`, + ``, + `const adapter = createHttpAdapter({ endpoint: "/api/feedback" });`, + ``, + ``, + ` `, + ``, + ].join("\n"); +} + +function createInitialStepStates(): Record { + return { + connected: { status: "idle" }, + container: { status: "idle" }, + element: { status: "idle" }, + metadata: { status: "idle" }, + }; +} + +export interface FeatureCaptureDemoProps + extends React.HTMLAttributes { + feature: FeatureConfig; +} + +export function FeatureCaptureDemo({ + className, + feature, + ...props +}: FeatureCaptureDemoProps) { + const [activePanel, setActivePanel] = React.useState( + feature.demo.defaultPanel, + ); + const [runStatus, setRunStatus] = React.useState("idle"); + const [stepStates, setStepStates] = React.useState< + Record + >(createInitialStepStates); + const [artifacts, setArtifacts] = React.useState({}); + + // Subtle interaction variants + const [isTargetPickerEnabled, setIsTargetPickerEnabled] = React.useState(false); + const [selectedTargetKey, setSelectedTargetKey] = React.useState("primary"); + const [integrationMode, setIntegrationMode] = React.useState({ + framework: "react", + scope: "global", + }); + + // Demo element refs + const containerRef = React.useRef(null); + const targetRefs = React.useRef>({}); + const setTargetRef = React.useCallback( + (key: string) => (node: HTMLElement | null) => { + targetRefs.current[key] = node; + }, + [], + ); + + // Right-click menu state (card-local) + const [menuVisible, setMenuVisible] = React.useState(false); + const [menuPosition, setMenuPosition] = React.useState(OFFSCREEN_POSITION); + + const runTokenRef = React.useRef(0); + + const steps: CaptureStep[] = React.useMemo( + () => [ + { id: "element", label: "Capture element" }, + { id: "container", label: "Capture container" }, + { id: "connected", label: "Get connected data" }, + { id: "metadata", label: "Shape anyclick metadata" }, + ], + [], + ); + + const reset = React.useCallback(() => { + runTokenRef.current += 1; + setRunStatus("idle"); + setStepStates(createInitialStepStates()); + setArtifacts({}); + setActivePanel(feature.demo.defaultPanel); + setMenuVisible(false); + setMenuPosition(OFFSCREEN_POSITION); + }, [feature.demo.defaultPanel]); + + const updateStep = React.useCallback( + (stepId: CaptureStepId, patch: Partial) => { + setStepStates((prev) => ({ + ...prev, + [stepId]: { ...prev[stepId], ...patch }, + })); + }, + [], + ); + + const getActiveTargetElement = React.useCallback((): HTMLElement | null => { + const explicit = targetRefs.current[selectedTargetKey]; + if (explicit) return explicit; + return targetRefs.current.primary ?? null; + }, [selectedTargetKey]); + + const copyToClipboard = React.useCallback(async (text: string) => { + try { + await navigator.clipboard.writeText(text); + showToast("Copied to clipboard", "success"); + } catch { + showToast("Failed to copy", "error"); + } + }, []); + + const handleAction = React.useCallback( + async (action: FeatureDemoAction) => { + if (action.kind === "setPanel") { + setActivePanel(action.panel); + return; + } + if (action.kind === "reset") { + reset(); + return; + } + if (action.kind === "toggleTargetPicker") { + setIsTargetPickerEnabled((prev) => !prev); + return; + } + if (action.kind === "toggleIntegrationMode") { + setIntegrationMode((prev) => ({ + framework: prev.framework === "react" ? "vanilla" : "react", + scope: prev.scope === "global" ? "scoped" : "global", + })); + return; + } + if (action.kind === "retakeScreenshots") { + // Just re-run capture with screenshots enabled (if this card supports it) + // and keep the user on Data panel. + setActivePanel("data"); + action = { kind: "startCapture", setActivePanel: "data" }; + } + if (action.kind === "copy") { + const screenshotsSummary = summarizeScreenshots(artifacts.screenshots); + const payloadForCopy = artifacts.payload + ? { + ...artifacts.payload, + screenshots: artifacts.payload.screenshots + ? summarizeScreenshots(artifacts.payload.screenshots) + : screenshotsSummary ?? undefined, + } + : null; + + if (action.target === "payloadJson") { + await copyToClipboard(payloadForCopy ? safeJson(payloadForCopy) : "{}"); + return; + } + if (action.target === "issueMarkdown") { + await copyToClipboard(artifacts.issueMarkdown ?? ""); + return; + } + if (action.target === "agentContext") { + await copyToClipboard(safeJson(artifacts.agentInput ?? {})); + return; + } + if (action.target === "snippet") { + const snippet = + feature.id === "framework-agnostic" + ? buildIntegrationSnippet(integrationMode) + : artifacts.integrationSnippet ?? ""; + await copyToClipboard(snippet); + return; + } + if (action.target === "commands") { + await copyToClipboard(buildZeroConfigSnippet()); + return; + } + if (action.target === "screenshotSummary") { + await copyToClipboard(safeJson(screenshotsSummary ?? {})); + return; + } + } + + if (action.kind === "startCapture") { + const token = (runTokenRef.current += 1); + setRunStatus("running"); + setStepStates(createInitialStepStates()); + setArtifacts({}); + + const targetEl = getActiveTargetElement(); + const containerEl = containerRef.current; + + if (!targetEl || !containerEl) { + setRunStatus("idle"); + updateStep("element", { + status: "error", + errorMessage: "Demo elements not ready", + }); + return; + } + + let elementContext: ElementContext | undefined; + let containerContext: ElementContext | undefined; + let inspectInfo: ElementInspectInfo | undefined; + let pageContext: PageContext | undefined; + let screenshots: ScreenshotData | null | undefined; + let connectedData: unknown; + let issueMarkdown: string | undefined; + let agentInput: unknown; + let integrationSnippet: string | undefined; + + // Step 1: element + { + const start = nowMs(); + updateStep("element", { + status: "running", + detail: "Building element selector + attributes…", + }); + try { + elementContext = buildElementContext(targetEl); + inspectInfo = feature.demo.runner.includeInspectInfo + ? getElementInspectInfo(targetEl) + : undefined; + + // ensure the UI updates even when capture is instant + await sleep(200); + if (runTokenRef.current !== token) return; + + setArtifacts((prev) => ({ + ...prev, + elementContext: elementContext!, + inspectInfo, + })); + updateStep("element", { + status: "success", + durationMs: nowMs() - start, + detail: `Selector: ${elementContext!.selector}`, + }); + } catch (err) { + if (runTokenRef.current !== token) return; + updateStep("element", { + status: "error", + durationMs: nowMs() - start, + errorMessage: err instanceof Error ? err.message : String(err), + }); + setRunStatus("idle"); + return; + } + } + + // Step 2: container + { + const start = nowMs(); + updateStep("container", { + status: "running", + detail: "Capturing container context…", + }); + try { + containerContext = buildElementContext(containerEl); + await sleep(200); + if (runTokenRef.current !== token) return; + setArtifacts((prev) => ({ ...prev, containerContext: containerContext! })); + updateStep("container", { + status: "success", + durationMs: nowMs() - start, + detail: `Selector: ${containerContext!.selector}`, + }); + } catch (err) { + if (runTokenRef.current !== token) return; + // Fallback: simulate container + containerContext = elementContext; + setArtifacts((prev) => ({ + ...prev, + containerContext: elementContext, + })); + updateStep("container", { + status: "simulated", + durationMs: nowMs() - start, + detail: "Using element context as container fallback", + }); + } + } + + // Step 3: connected data (screenshots, page context, feature-specific outputs) + { + const start = nowMs(); + updateStep("connected", { + status: "running", + detail: "Collecting page + connected context…", + }); + + try { + pageContext = buildPageContext(); + screenshots = undefined; + + if (feature.demo.runner.enableScreenshots) { + updateStep("connected", { + status: "running", + detail: isScreenshotSupported() + ? "Capturing element/container/viewport…" + : "Screenshots unsupported; capturing errors…", + }); + screenshots = await captureAllScreenshots( + targetEl, + containerEl, + // keep safe defaults for homepage demo; don’t show the preview UI + { + enabled: true, + showPreview: false, + }, + ); + } + + // Feature-specific connected data + next action prep + connectedData = + feature.id === "github-integration" + ? { + issueTitle: `Issue: ${elementContext!.tag} @ ${elementContext!.selector}`, + labels: ["anyclick", "demo"], + } + : feature.id === "ai-agent" + ? { + intent: "Ask AI", + suggestedPrompts: [ + "Why is this element here?", + "What should I debug next?", + ], + } + : feature.id === "visual-capture" + ? { + screenshots: summarizeScreenshots(screenshots), + masking: { sensitiveSelectors: "defaults" }, + } + : feature.id === "framework-agnostic" + ? { + mode: integrationMode, + note: "Adapter + client wiring varies by environment", + } + : feature.id === "zero-config" + ? { + defaults: { + highlight: true, + screenshots: true, + menu: "preset", + }, + } + : { + ancestors: elementContext!.ancestors.slice(0, 3), + dataAttributes: elementContext!.dataAttributes, + }; + + // Next action outputs + issueMarkdown = + feature.id === "github-integration" + ? buildGitHubIssueMarkdown({ + element: elementContext!, + page: pageContext!, + screenshots: screenshots ?? null, + }) + : undefined; + + agentInput = + feature.id === "ai-agent" + ? buildAgentInput({ + element: elementContext!, + page: pageContext!, + screenshots: screenshots ?? null, + }) + : undefined; + + integrationSnippet = + feature.id === "framework-agnostic" + ? buildIntegrationSnippet(integrationMode) + : undefined; + + await sleep(250); + if (runTokenRef.current !== token) return; + + setArtifacts((prev) => ({ + ...prev, + agentInput, + connectedData, + integrationSnippet, + issueMarkdown, + pageContext, + screenshots: screenshots ?? null, + })); + + updateStep("connected", { + status: "success", + durationMs: nowMs() - start, + detail: + feature.demo.runner.enableScreenshots && screenshots + ? `Screenshots: ${ + summarizeScreenshots(screenshots)?.errors ? "partial" : "ok" + }` + : `Connected context captured`, + }); + } catch (err) { + if (runTokenRef.current !== token) return; + updateStep("connected", { + status: "error", + durationMs: nowMs() - start, + errorMessage: err instanceof Error ? err.message : String(err), + }); + setRunStatus("idle"); + return; + } + } + + // Step 4: shape metadata + payload + { + const start = nowMs(); + updateStep("metadata", { + status: "running", + detail: "Shaping metadata + payload…", + }); + try { + const screenshotSummary = summarizeScreenshots(screenshots ?? null); + + const metadata: Record = { + demo: true, + featureId: feature.id, + selectedTargetKey, + containerContext: containerContext ?? null, + connectedData: connectedData ?? null, + screenshots: screenshotSummary, + integrationMode: + feature.id === "framework-agnostic" ? integrationMode : undefined, + }; + + const payload = buildAnyclickPayload(targetEl, feature.id, { + metadata, + }); + + // Attach screenshots for demo purposes (payload supports it) + if (screenshots) { + payload.screenshots = screenshots; + } + + await sleep(200); + if (runTokenRef.current !== token) return; + + setArtifacts((prev) => ({ + ...prev, + metadata, + payload, + // Keep the page context captured in step 3 for display + pageContext: prev.pageContext ?? pageContext, + })); + updateStep("metadata", { + status: "success", + durationMs: nowMs() - start, + detail: `Payload type: ${payload.type}`, + }); + setRunStatus("success"); + + if (action.setActivePanel) { + setActivePanel(action.setActivePanel); + } + } catch (err) { + if (runTokenRef.current !== token) return; + updateStep("metadata", { + status: "error", + durationMs: nowMs() - start, + errorMessage: err instanceof Error ? err.message : String(err), + }); + setRunStatus("idle"); + return; + } + } + } + }, + [ + artifacts, + copyToClipboard, + feature.demo.defaultPanel, + feature.demo.runner.enableScreenshots, + feature.demo.runner.includeInspectInfo, + feature.id, + getActiveTargetElement, + integrationMode, + reset, + selectedTargetKey, + updateStep, + ], + ); + + const handleTargetClick = React.useCallback( + (key: string) => { + if (!isTargetPickerEnabled) return; + setSelectedTargetKey(key); + setIsTargetPickerEnabled(false); + showToast(`Target selected: ${key}`, "info"); + }, + [isTargetPickerEnabled], + ); + + const menuItems: ContextMenuItem[] = React.useMemo(() => { + return feature.demo.menuItems.map((item) => ({ + icon: item.icon, + label: item.label, + showComment: false, + type: `demo-${feature.id}-${item.id}`, + onClick: async ({ closeMenu }) => { + closeMenu(); + await handleAction(item.action); + }, + })); + }, [feature.demo.menuItems, feature.id, handleAction]); + + const onOpenMenu = React.useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + setMenuPosition({ x: e.clientX, y: e.clientY }); + setMenuVisible(true); + }, + [], + ); + + const onCloseMenu = React.useCallback(() => { + setMenuVisible(false); + setMenuPosition(OFFSCREEN_POSITION); + }, []); + + const demoHeader = ( +
+
+ Right-click inside this demo for actions. +
+
+ {runStatus === "running" + ? "Running…" + : runStatus === "success" + ? "Capture complete" + : "Idle"} +
+
+ ); + + const screenshotsSummary = React.useMemo( + () => summarizeScreenshots(artifacts.screenshots), + [artifacts.screenshots], + ); + + const dataPanel = ( +
+
+ {demoHeader} +
+ +
+ + Element context + +
+          {safeJson(artifacts.elementContext ?? null)}
+        
+
+ + {artifacts.inspectInfo ? ( +
+ + Inspect info + +
+            {safeJson(artifacts.inspectInfo)}
+          
+
+ ) : null} + +
+ + Container context + +
+          {safeJson(artifacts.containerContext ?? null)}
+        
+
+ +
+ + Page context + +
+          {safeJson(artifacts.pageContext ?? null)}
+        
+
+ +
+ + Connected data + +
+          {safeJson(artifacts.connectedData ?? null)}
+        
+
+ +
+ + Metadata + payload (sanitized) + +
+          {safeJson({
+            metadata: artifacts.metadata ?? null,
+            payload: artifacts.payload
+              ? {
+                  ...artifacts.payload,
+                  screenshots: artifacts.payload.screenshots
+                    ? summarizeScreenshots(artifacts.payload.screenshots)
+                    : undefined,
+                }
+              : null,
+          })}
+        
+
+ + {feature.demo.runner.enableScreenshots ? ( +
+ + Screenshots + +
+
+              {safeJson(screenshotsSummary)}
+            
+
+ {( + [ + ["element", artifacts.screenshots?.element?.dataUrl], + ["container", artifacts.screenshots?.container?.dataUrl], + ["viewport", artifacts.screenshots?.viewport?.dataUrl], + ] as const + ).map(([label, dataUrl]) => ( +
+
+ {label} +
+ {dataUrl ? ( + // eslint-disable-next-line @next/next/no-img-element + {`${label} + ) : ( +
+ n/a +
+ )} +
+ ))} +
+
+
+ ) : null} +
+ ); + + const statusPanel = ( +
+
+
+
+ Run status:{" "} + {runStatus} +
+
+ Screenshot support:{" "} + + {isScreenshotSupported() ? "yes" : "no"} + +
+
+
+ +
+        {safeJson({
+          steps: stepStates,
+          simulatedFallbacks: Object.entries(stepStates)
+            .filter(([, s]) => s.status === "simulated")
+            .map(([k]) => k),
+          screenshotErrors: screenshotsSummary?.errors ?? null,
+        })}
+      
+
+ ); + + const nextPanel = ( +
+ {feature.id === "github-integration" ? ( +
+          {artifacts.issueMarkdown ?? "Run the capture to draft an issue…"}
+        
+ ) : feature.id === "ai-agent" ? ( +
+          {safeJson(artifacts.agentInput ?? {})}
+        
+ ) : feature.id === "framework-agnostic" ? ( +
+
+
+ Mode:{" "} + + {integrationMode.framework} / {integrationMode.scope} + +
+
+
+            {buildIntegrationSnippet(integrationMode)}
+          
+
+ ) : feature.id === "zero-config" ? ( +
+          {buildZeroConfigSnippet()}
+        
+ ) : ( +
+          {safeJson({
+            metadata: artifacts.metadata ?? null,
+            payload: artifacts.payload
+              ? {
+                  ...artifacts.payload,
+                  screenshots: artifacts.payload.screenshots
+                    ? summarizeScreenshots(artifacts.payload.screenshots)
+                    : undefined,
+                }
+              : null,
+          })}
+        
+ )} +
+ ); + + const primary = feature.demo.primaryCta; + const secondary = feature.demo.secondaryCta; + + return ( +
+ {/* CTA row */} +
+ + + {secondary ? ( + (() => { + const isSecondaryDisabled = + runStatus === "running" || + (secondary.action.kind === "retakeScreenshots" && + runStatus !== "success"); + + return ( + + ); + })() + ) : null} + + +
+ + {/* Demo sandbox (captures real DOM context from these elements) */} +
+
+
+ Demo sandbox + + (selected:{" "} + {selectedTargetKey}) + +
+
+ {isTargetPickerEnabled + ? "Click an element below to select it" + : "Click primary CTA to run capture"} +
+
+ +
+ + + + +
handleTargetClick("info")} + onKeyDown={(e) => { + if (e.key === "Enter") handleTargetClick("info"); + }} + className={cn( + "rounded-lg border px-3 py-2 text-sm transition-colors select-none", + selectedTargetKey === "info" + ? "border-white/25 bg-white/10" + : "border-white/10 bg-white/5 hover:bg-white/8", + )} + > +
+ Info row + +
+
+ data-attrs, text, hierarchy +
+
+
+
+ + {/* Workflow timeline */} + + + {/* Details */} + + + {/* Card-local context menu */} + } + highlightConfig={{ enabled: false }} + isSubmitting={false} + items={menuItems} + onClose={onCloseMenu} + onSelect={() => { + // no-op: all items are handled via onClick hooks + }} + position={menuPosition} + screenshotConfig={{ enabled: false, showPreview: false }} + targetElement={getActiveTargetElement() ?? containerRef.current} + visible={menuVisible} + /> +
+ ); +} + diff --git a/apps/web/src/components/features/FeatureCard.tsx b/apps/web/src/components/features/FeatureCard.tsx new file mode 100644 index 0000000..858919b --- /dev/null +++ b/apps/web/src/components/features/FeatureCard.tsx @@ -0,0 +1,143 @@ +"use client"; + +import * as React from "react"; +import Link from "next/link"; +import { AnimatePresence, motion } from "motion/react"; +import { ArrowRight, X } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { FeatureCaptureDemo } from "./FeatureCaptureDemo"; +import type { FeatureConfig } from "./types"; + +export interface FeatureCardProps { + className?: string; + feature: FeatureConfig; + isExpanded: boolean; + onToggle: () => void; +} + +export function FeatureCard({ + className, + feature, + isExpanded, + onToggle, +}: FeatureCardProps) { + const handleClick = React.useCallback(() => { + if (!isExpanded) { + onToggle(); + } + }, [isExpanded, onToggle]); + + const handleClose = React.useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + onToggle(); + }, + [onToggle], + ); + + return ( + + + {!isExpanded ? ( + +
+ {feature.icon} +
+

+ {feature.title} +

+

+ {feature.description} +

+ + Learn more + +
+ ) : ( + +
+
+
+ + {feature.icon} + +
+

+ {feature.title} +

+
+ +
+ +

+ {feature.expandedDescription} +

+ + + + e.stopPropagation()} + className={cn( + "mt-4 inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium text-white transition-opacity hover:opacity-90", + feature.colorClasses.bg, + )} + > + {feature.linkText} + + +
+ )} +
+
+ ); +} + diff --git a/apps/web/src/components/features/types.ts b/apps/web/src/components/features/types.ts new file mode 100644 index 0000000..5be9e85 --- /dev/null +++ b/apps/web/src/components/features/types.ts @@ -0,0 +1,77 @@ +import type { ReactNode } from "react"; + +export type DemoPanel = "data" | "next" | "status"; + +export type FeatureDemoActionKind = + | "copy" + | "reset" + | "retakeScreenshots" + | "setPanel" + | "startCapture" + | "toggleIntegrationMode" + | "toggleTargetPicker"; + +export type CopyTarget = + | "agentContext" + | "commands" + | "issueMarkdown" + | "payloadJson" + | "screenshotSummary" + | "snippet"; + +export type FeatureDemoAction = + | { + kind: "startCapture"; + /** Optional hint to switch panels after starting capture. */ + setActivePanel?: DemoPanel; + } + | { kind: "reset" } + | { kind: "toggleTargetPicker" } + | { kind: "retakeScreenshots" } + | { kind: "toggleIntegrationMode" } + | { kind: "setPanel"; panel: DemoPanel } + | { kind: "copy"; target: CopyTarget }; + +export interface FeatureDemoMenuItem { + action: FeatureDemoAction; + icon?: ReactNode; + id: string; + label: string; +} + +export interface FeatureDemoConfig { + /** Default panel when the card expands (and after reset). */ + defaultPanel: DemoPanel; + /** Primary CTA shown in the card. */ + primaryCta: { action: FeatureDemoAction; label: string }; + /** Optional secondary CTA shown in the card. */ + secondaryCta?: { action: FeatureDemoAction; label: string }; + /** Right-click menu items for the demo area. */ + menuItems: FeatureDemoMenuItem[]; + /** Capture runner options for this card. */ + runner: { + enableScreenshots?: boolean; + includeInspectInfo?: boolean; + }; +} + +export interface FeatureConfig { + color: string; + colorClasses: { + bg: string; + border: string; + dot: string; + dotMuted: string; + iconBg: string; + text: string; + }; + demo: FeatureDemoConfig; + description: string; + expandedDescription: string; + icon: ReactNode; + id: string; + linkHref: string; + linkText: string; + title: string; +} +