diff --git a/.npmrc b/.npmrc index 78351aa..50267dc 100644 --- a/.npmrc +++ b/.npmrc @@ -1,2 +1,5 @@ +node-linker=hoisted public-hoist-pattern[]=*react* public-hoist-pattern[]=*@types/react* +public-hoist-pattern[]=*expo* +public-hoist-pattern[]=*react-native* diff --git a/README.md b/README.md index cf161ec..2ed82df 100644 --- a/README.md +++ b/README.md @@ -40,10 +40,23 @@ The toolbar appears in the bottom-right corner. Click to activate, then click an Agentation captures class names, selectors, and element positions so AI agents can `grep` for the exact code you're referring to. Instead of describing "the blue button in the sidebar," you give the agent `.sidebar > button.primary` and your feedback. +## Packages + +| Package | Platform | Install | +|---------|----------|---------| +| `agentation` | Web (React) | `npm install agentation` | +| `agentation-rn` | React Native | `npm install agentation-rn` | + ## Requirements +### Web - React 18+ -- Desktop browser (mobile not supported) +- Desktop browser + +### React Native +- React Native >= 0.72.0 +- React >= 18.0.0 +- iOS & Android supported ## Docs diff --git a/packages/agentation-rn/.gitignore b/packages/agentation-rn/.gitignore new file mode 100644 index 0000000..b947077 --- /dev/null +++ b/packages/agentation-rn/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/packages/agentation-rn/CLAUDE.md b/packages/agentation-rn/CLAUDE.md new file mode 100644 index 0000000..75b889c --- /dev/null +++ b/packages/agentation-rn/CLAUDE.md @@ -0,0 +1,79 @@ +# Agentation RN Package + +This is the publishable npm package for React Native. Changes here affect everyone who installs `agentation-rn`. + +## Critical Rules + +1. **NEVER run `npm publish`** - Only publish when explicitly instructed +2. **NEVER bump version** in package.json without explicit instruction +3. **NEVER modify exports** in index.ts without discussing breaking changes + +## What Gets Published + +- `dist/` folder (compiled from `src/`) +- `package.json`, `README.md`, `LICENSE` + +## Before Modifying `src/` + +- Consider: Is this a breaking change? +- Consider: Does this affect the API surface? +- Consider: Will existing users' code still work? +- Consider: Does this break parity with the web package? + +## Main Export + +```tsx +import { Agentation } from 'agentation-rn'; +``` + +## Mobile-Specific APIs + +These are React Native additions not in the web package: + +- `useAgentationScroll` - ScrollView marker positioning +- `AgenationView` - Modal/sheet annotation context + +## Programmatic API (Web Parity) + +The component exposes these callback props (matching web 1.2.0+): + +- `onAnnotationAdd(annotation)` - when annotation created +- `onAnnotationDelete(annotation)` - when annotation deleted +- `onAnnotationUpdate(annotation)` - when annotation edited +- `onAnnotationsClear(annotations[])` - when all cleared +- `onCopy(markdown)` - when copy button clicked +- `copyToClipboard` (boolean, default: true) + +**API stability**: These are public contracts. Changing signatures or removing callbacks is a breaking change requiring a major version bump. + +## Testing Changes + +1. Run `pnpm build` to ensure it compiles +2. Run `pnpm typecheck` for TypeScript validation +3. Test the example app on iOS: `cd example && pnpm ios` +4. Test the example app on Android: `cd example && pnpm android` + +## Publishing + +When instructed to publish a new npm version: + +1. Bump version in `package.json` +2. Run `pnpm build` +3. Commit the version bump +4. Run `npm publish --access public` (will prompt for OTP) +5. Push to main + +## Web API Parity + +Maintain compatibility with the web `agentation` package: + +| Feature | Web | RN | Notes | +|---------|-----|-----|-------| +| Annotation callbacks | ✅ | ✅ | Same signatures | +| Demo mode | ✅ | ✅ | Same props | +| Output levels | ✅ | ✅ | compact/standard/detailed/forensic | +| Settings persistence | ✅ | ✅ | localStorage vs AsyncStorage | +| `identifyElement` | ✅ | ✅ | Alias for `detectComponent` | +| `getElementPath` | ✅ | ✅ | Alias for `formatElementPath` | + +When adding features, consider if they should also be added to the web package. diff --git a/packages/agentation-rn/LICENSE b/packages/agentation-rn/LICENSE new file mode 100644 index 0000000..4dc169a --- /dev/null +++ b/packages/agentation-rn/LICENSE @@ -0,0 +1,27 @@ +PolyForm Shield License 1.0.0 + +Copyright (c) 2026 Benji Taylor + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to use, +copy, modify, and distribute the Software, subject to the following conditions: + +1. You may not use the Software to provide a product or service that competes + with the Software or any product or service offered by the Licensor that + includes the Software. + +2. You may not remove or obscure any licensing, copyright, or other notices + included in the Software. + +3. If you distribute the Software or any derivative works, you must include a + copy of this license. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +For more information, see https://polyformproject.org/licenses/shield/1.0.0 diff --git a/packages/agentation-rn/README.md b/packages/agentation-rn/README.md new file mode 100644 index 0000000..cf01d70 --- /dev/null +++ b/packages/agentation-rn/README.md @@ -0,0 +1,212 @@ +# agentation-rn + +React Native support for [Agentation](https://agentation.dev) - Visual feedback for AI coding agents. + +Tap components in your app to create annotations, then copy structured markdown output for AI tools like Claude Code. + +## Installation + +```bash +npm install agentation-rn +# or +yarn add agentation-rn +# or +pnpm add agentation-rn +``` + +### Peer Dependencies + +```bash +npm install react-native-safe-area-context +``` + +Optional (for enhanced features): +```bash +npm install @callstack/liquid-glass react-native-svg +``` + +## Quick Start + +```tsx +import { Agentation } from 'agentation-rn'; + +export default function App() { + return ( + console.log('Added:', annotation)} + onCopy={(markdown) => { + // Send to your AI tool + console.log('Markdown:', markdown); + }} + > + + + ); +} +``` + +## Features + +- **Tap to Annotate** - Tap any component to create an annotation +- **Component Detection** - Automatically identifies React components with file paths and line numbers +- **Structured Output** - Generates AI-ready markdown with element context +- **4 Detail Levels** - compact, standard, detailed, forensic +- **Settings Persistence** - Saves preferences via AsyncStorage +- **Navigation Support** - Detects React Navigation routes + +## API + +### `` + +Main wrapper component. Provides annotation functionality to your entire app. + +```tsx + {}} // Called when annotation created + onAnnotationUpdate={(annotation) => {}} // Called when annotation edited + onAnnotationDelete={(annotation) => {}} // Called when annotation deleted + onAnnotationsClear={(annotations) => {}} // Called when all cleared + onCopy={(markdown) => {}} // Called when markdown copied +> + + +``` + +### Mobile-Specific APIs + +#### `useAgentationScroll` + +Required for ScrollViews - keeps annotation markers positioned correctly during scroll. + +```tsx +import { useAgentationScroll } from 'agentation-rn'; + +function MyScreen() { + const { onScroll, scrollEventThrottle } = useAgentationScroll(); + + return ( + + {/* Your content */} + + ); +} +``` + +#### `` + +For Modals and Sheets - provides isolated annotation context since iOS modals render outside the normal view hierarchy. + +```tsx +import { AgenationView } from 'agentation-rn'; +import { Modal } from 'react-native'; + +function MyModal({ visible, onClose }) { + return ( + + + + + + ); +} +``` + +### Hooks + +#### `useAnnotations` + +Low-level hook for custom annotation UIs. + +```tsx +import { useAnnotations } from 'agentation-rn'; + +const { + annotations, + addAnnotation, + updateAnnotation, + deleteAnnotation, + clearAnnotations, + copyToClipboard, + settings, + updateSettings, +} = useAnnotations({ screenName: 'MyScreen' }); +``` + +### Utilities + +```tsx +import { + // Component detection + detectComponent, + identifyElement, // alias for detectComponent + formatElementPath, + getElementPath, // alias for formatElementPath + + // Markdown generation + generateMarkdown, + generateSimpleMarkdown, + + // Storage + saveAnnotations, + loadAnnotations, + clearAnnotations, + + // Helpers + copyToClipboard, + generateId, +} from 'agentation-rn'; +``` + +## Platform Support + +| Platform | Status | +|----------|--------| +| iOS | Supported | +| Android | Supported | +| React Native | >= 0.72.0 | +| React | >= 18.0.0 | + +### Navigation Support + +| Library | Status | +|---------|--------| +| React Navigation | Supported | +| Expo Router | Coming soon | + +## Output Example + +When you copy annotations, you get structured markdown: + +```markdown +# App Feedback - MyScreen + +## Annotation 1 +- **Element:** Button (src/components/Button.tsx:42) +- **Comment:** This button should be larger +- **Position:** x: 150, y: 320 + +## Annotation 2 +- **Element:** Text (src/screens/Home.tsx:18) +- **Comment:** Font size too small on mobile +- **Position:** x: 20, y: 180 +``` + +## Running the Example + +```bash +cd packages/agentation-rn/example +pnpm install + +# iOS +pnpm ios + +# Android +pnpm android +``` + +## License + +PolyForm-Shield-1.0.0 diff --git a/packages/agentation-rn/example/.gitignore b/packages/agentation-rn/example/.gitignore new file mode 100644 index 0000000..999c4e5 --- /dev/null +++ b/packages/agentation-rn/example/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +.expo/ + +# Native folders - generated by `npx expo prebuild` +ios/ +android/ diff --git a/packages/agentation-rn/example/App.tsx b/packages/agentation-rn/example/App.tsx new file mode 100644 index 0000000..cdc96bc --- /dev/null +++ b/packages/agentation-rn/example/App.tsx @@ -0,0 +1,93 @@ +import React, { useRef } from 'react'; +import { StyleSheet, View } from 'react-native'; +import { SafeAreaProvider } from 'react-native-safe-area-context'; +import { NavigationContainer, NavigationContainerRef } from '@react-navigation/native'; +import { createNativeStackNavigator } from '@react-navigation/native-stack'; +import { Agentation } from 'agentation-rn'; +import { colors, typography } from './theme'; +import { EventsPanel } from './components'; +import { useApiEvents } from './hooks'; + +import { HomeScreen, SettingsScreen, ProfileScreen, ScrollExampleScreen, ModalExampleScreen } from './screens'; + +export type RootStackParamList = { + Home: undefined; + Settings: undefined; + Profile: { userId: string }; + ScrollExample: undefined; + ModalExample: undefined; +}; + +const Stack = createNativeStackNavigator(); + +export default function App() { + const navigationRef = useRef>(null); + const { events, clearEvents, callbacks } = useApiEvents(); + + return ( + + + + { + // Expose navigation ref for route detection + (global as any).__REACT_NAVIGATION_DEVTOOLS__ = { + navigatorRef: navigationRef, + }; + }} + > + + + + + + + + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.light.bg, + }, +}); diff --git a/packages/agentation-rn/example/README.md b/packages/agentation-rn/example/README.md new file mode 100644 index 0000000..79d7a6b --- /dev/null +++ b/packages/agentation-rn/example/README.md @@ -0,0 +1,27 @@ +# agentation-rn example + +Demo app showcasing agentation-rn features. + +## Setup + +```bash +pnpm install +``` + +## Run + +```bash +# iOS +pnpm ios + +# Android +pnpm android +``` + +## Features Demonstrated + +- Basic annotation workflow +- ScrollView integration (`useAgentationScroll`) +- Modal support (`AgenationView`) +- React Navigation route detection +- Event callbacks and markdown export diff --git a/packages/agentation-rn/example/app.json b/packages/agentation-rn/example/app.json new file mode 100644 index 0000000..fe11b2f --- /dev/null +++ b/packages/agentation-rn/example/app.json @@ -0,0 +1,36 @@ +{ + "expo": { + "name": "agentation-rn-example", + "slug": "agentation-rn-example", + "autolinking": { + "exclude": ["react-native-worklets", "react-native-reanimated"] + }, + "version": "1.0.0", + "orientation": "portrait", + "icon": "./assets/icon.png", + "userInterfaceStyle": "light", + "newArchEnabled": true, + "splash": { + "image": "./assets/splash-icon.png", + "resizeMode": "contain", + "backgroundColor": "#ffffff" + }, + "ios": { + "supportsTablet": true, + "bundleIdentifier": "com.agentation.rn.example", + "deploymentTarget": "26.0" + }, + "android": { + "adaptiveIcon": { + "foregroundImage": "./assets/adaptive-icon.png", + "backgroundColor": "#ffffff" + }, + "edgeToEdgeEnabled": true, + "predictiveBackGestureEnabled": false, + "package": "com.agentation.rn.example" + }, + "web": { + "favicon": "./assets/favicon.png" + } + } +} diff --git a/packages/agentation-rn/example/assets/adaptive-icon.png b/packages/agentation-rn/example/assets/adaptive-icon.png new file mode 100644 index 0000000..03d6f6b Binary files /dev/null and b/packages/agentation-rn/example/assets/adaptive-icon.png differ diff --git a/packages/agentation-rn/example/assets/favicon.png b/packages/agentation-rn/example/assets/favicon.png new file mode 100644 index 0000000..e75f697 Binary files /dev/null and b/packages/agentation-rn/example/assets/favicon.png differ diff --git a/packages/agentation-rn/example/assets/icon.png b/packages/agentation-rn/example/assets/icon.png new file mode 100644 index 0000000..a0b1526 Binary files /dev/null and b/packages/agentation-rn/example/assets/icon.png differ diff --git a/packages/agentation-rn/example/assets/splash-icon.png b/packages/agentation-rn/example/assets/splash-icon.png new file mode 100644 index 0000000..03d6f6b Binary files /dev/null and b/packages/agentation-rn/example/assets/splash-icon.png differ diff --git a/packages/agentation-rn/example/babel.config.js b/packages/agentation-rn/example/babel.config.js new file mode 100644 index 0000000..ece5ab9 --- /dev/null +++ b/packages/agentation-rn/example/babel.config.js @@ -0,0 +1,12 @@ +const { inspectorBabelPlugin } = require('react-native-dev-inspector/metro'); + +module.exports = function (api) { + api.cache(true); + return { + presets: ['babel-preset-expo'], + plugins: [ + // Injects __callerSource prop into JSX elements with file, line, and column info + inspectorBabelPlugin, + ], + }; +}; diff --git a/packages/agentation-rn/example/components/Badge.tsx b/packages/agentation-rn/example/components/Badge.tsx new file mode 100644 index 0000000..422bfb7 --- /dev/null +++ b/packages/agentation-rn/example/components/Badge.tsx @@ -0,0 +1,30 @@ + +import React from 'react'; +import { View, Text, StyleSheet } from 'react-native'; +import { spacing, radius, typography } from '../theme'; + +interface BadgeProps { + label: string; + color: string; +} + +export function Badge({ label, color }: BadgeProps) { + return ( + + {label} + + ); +} + +const styles = StyleSheet.create({ + badge: { + paddingHorizontal: spacing.sm + 2, + paddingVertical: spacing.xs, + borderRadius: radius.pill, + }, + badgeText: { + color: '#fff', + fontSize: typography.sizes.small, + fontWeight: typography.weights.medium, + }, +}); diff --git a/packages/agentation-rn/example/components/BulletList.tsx b/packages/agentation-rn/example/components/BulletList.tsx new file mode 100644 index 0000000..de74ae0 --- /dev/null +++ b/packages/agentation-rn/example/components/BulletList.tsx @@ -0,0 +1,42 @@ + +import React from 'react'; +import { View, Text, StyleSheet } from 'react-native'; +import { colors, spacing, typography } from '../theme'; + +interface BulletListProps { + items: string[]; +} + +export function BulletList({ items }: BulletListProps) { + return ( + + {items.map((item, index) => ( + + + {item} + + ))} + + ); +} + +const styles = StyleSheet.create({ + bulletList: { + marginBottom: spacing.sm, + }, + bulletItem: { + flexDirection: 'row', + marginBottom: spacing.sm, + }, + bulletDot: { + width: 20, + fontSize: typography.sizes.body, + color: colors.primary, + }, + bulletText: { + flex: 1, + fontSize: typography.sizes.body, + color: colors.light.textSecondary, + lineHeight: 20, + }, +}); diff --git a/packages/agentation-rn/example/components/Button.tsx b/packages/agentation-rn/example/components/Button.tsx new file mode 100644 index 0000000..7eb1d2c --- /dev/null +++ b/packages/agentation-rn/example/components/Button.tsx @@ -0,0 +1,77 @@ + +import React from 'react'; +import { TouchableOpacity, Text, StyleSheet } from 'react-native'; +import { colors, spacing, radius, typography } from '../theme'; + +interface ButtonProps { + title: string; + onPress: () => void; + variant?: 'primary' | 'secondary' | 'danger'; + disabled?: boolean; +} + +export function Button({ + title, + onPress, + variant = 'primary', + disabled = false, +}: ButtonProps) { + const buttonStyle = [ + styles.button, + variant === 'primary' && styles.buttonPrimary, + variant === 'secondary' && styles.buttonSecondary, + variant === 'danger' && styles.buttonDanger, + disabled && styles.buttonDisabled, + ]; + const textStyle = [ + styles.buttonText, + variant === 'secondary' && styles.buttonTextSecondary, + disabled && styles.buttonTextDisabled, + ]; + + return ( + + {title} + + ); +} + +const styles = StyleSheet.create({ + button: { + paddingVertical: spacing.sm, + paddingHorizontal: spacing.md + 2, + borderRadius: radius.md, + minWidth: 80, + }, + buttonPrimary: { + backgroundColor: colors.light.text, + }, + buttonSecondary: { + backgroundColor: colors.light.bgCard, + borderWidth: 1, + borderColor: colors.light.borderMedium, + }, + buttonDanger: { + backgroundColor: colors.danger, + }, + buttonDisabled: { + opacity: 0.5, + }, + buttonText: { + fontSize: typography.sizes.ui, + fontWeight: typography.weights.medium, + color: '#fff', + textAlign: 'center', + }, + buttonTextSecondary: { + color: colors.light.text, + }, + buttonTextDisabled: { + color: colors.light.textMuted, + }, +}); diff --git a/packages/agentation-rn/example/components/Card.tsx b/packages/agentation-rn/example/components/Card.tsx new file mode 100644 index 0000000..3321c07 --- /dev/null +++ b/packages/agentation-rn/example/components/Card.tsx @@ -0,0 +1,35 @@ + +import React from 'react'; +import { View, Text, StyleSheet } from 'react-native'; +import { colors, spacing, radius, typography, shadows } from '../theme'; + +interface CardProps { + title?: string; + children: React.ReactNode; +} + +export function Card({ title, children }: CardProps) { + return ( + + {title && {title}} + {children} + + ); +} + +const styles = StyleSheet.create({ + card: { + backgroundColor: colors.light.bgCard, + borderRadius: radius.xl, + padding: spacing.lg, + marginBottom: spacing.md, + ...shadows.md, + }, + cardTitle: { + fontSize: typography.sizes.ui, + fontWeight: typography.weights.semibold, + color: colors.light.text, + marginBottom: spacing.md, + letterSpacing: typography.letterSpacing.tight, + }, +}); diff --git a/packages/agentation-rn/example/components/DemoCard.tsx b/packages/agentation-rn/example/components/DemoCard.tsx new file mode 100644 index 0000000..7f82d14 --- /dev/null +++ b/packages/agentation-rn/example/components/DemoCard.tsx @@ -0,0 +1,41 @@ + +import React from 'react'; +import { View, Text, StyleSheet } from 'react-native'; +import { colors, spacing, radius, typography, shadows } from '../theme'; + +interface DemoCardProps { + title: string; + description: string; +} + +export function DemoCard({ title, description }: DemoCardProps) { + return ( + + {title} + {description} + + ); +} + +const styles = StyleSheet.create({ + demoCard: { + backgroundColor: colors.light.bgCard, + borderRadius: radius.lg, + padding: spacing.lg, + marginBottom: spacing.md, + borderWidth: 1, + borderColor: colors.light.border, + ...shadows.sm, + }, + demoCardTitle: { + fontSize: typography.sizes.title, + fontWeight: typography.weights.semibold, + color: colors.light.text, + marginBottom: spacing.sm, + }, + demoCardDescription: { + fontSize: typography.sizes.body, + color: colors.light.textSecondary, + lineHeight: 20, + }, +}); diff --git a/packages/agentation-rn/example/components/EventsPanel.tsx b/packages/agentation-rn/example/components/EventsPanel.tsx new file mode 100644 index 0000000..c4a1af7 --- /dev/null +++ b/packages/agentation-rn/example/components/EventsPanel.tsx @@ -0,0 +1,233 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { + View, + Text, + StyleSheet, + TouchableOpacity, + ScrollView, + Animated, + Dimensions, +} from 'react-native'; + +export interface ApiEvent { + id: string; + type: 'onAnnotationAdd' | 'onAnnotationUpdate' | 'onAnnotationDelete' | 'onCopy' | 'onAnnotationsClear'; + data: Record; + timestamp: number; +} + +interface EventsPanelProps { + events: ApiEvent[]; + onClear: () => void; + enabled?: boolean; +} + +const EVENT_COLORS: Record = { + onAnnotationAdd: '#34c759', + onAnnotationUpdate: '#ff9500', + onAnnotationDelete: '#ff3b30', + onCopy: '#007aff', + onAnnotationsClear: '#ff3b30', +}; + +export function EventsPanel({ events, onClear, enabled = true }: EventsPanelProps) { + const [isExpanded, setIsExpanded] = useState(true); + const slideAnim = useRef(new Animated.Value(1)).current; + const scrollRef = useRef(null); + + const screenWidth = Dimensions.get('window').width; + const panelWidth = Math.min(300, screenWidth * 0.45); + + if (!enabled) { + return null; + } + + useEffect(() => { + Animated.timing(slideAnim, { + toValue: isExpanded ? 1 : 0, + duration: 200, + useNativeDriver: true, + }).start(); + }, [isExpanded, slideAnim]); + + useEffect(() => { + // Auto-scroll to bottom when new events arrive + if (scrollRef.current && events.length > 0) { + setTimeout(() => { + scrollRef.current?.scrollToEnd({ animated: true }); + }, 100); + } + }, [events.length]); + + const formatData = (data: Record) => { + const entries = Object.entries(data).slice(0, 3); + return entries.map(([key, value]) => { + let displayValue = value; + if (typeof value === 'string' && value.length > 25) { + displayValue = value.slice(0, 25) + '...'; + } + return `${key}: ${JSON.stringify(displayValue)}`; + }); + }; + + return ( + + + setIsExpanded(!isExpanded)} + > + {isExpanded ? '›' : '‹'} + + Events + {events.length > 0 && ( + + Clear + + )} + + + {isExpanded && ( + + {events.length === 0 ? ( + No events yet + ) : ( + events.map((event) => ( + + + {event.type} + + {Object.keys(event.data).length > 0 && ( + + {formatData(event.data).map((line, i) => ( + + {line} + + ))} + + )} + + )) + )} + + )} + + ); +} + +const styles = StyleSheet.create({ + container: { + position: 'absolute', + top: 100, + right: 0, + maxHeight: 350, + backgroundColor: 'rgba(28, 28, 30, 0.95)', + borderTopLeftRadius: 12, + borderBottomLeftRadius: 12, + overflow: 'hidden', + shadowColor: '#000', + shadowOffset: { width: -2, height: 2 }, + shadowOpacity: 0.3, + shadowRadius: 8, + elevation: 8, + zIndex: 999, + }, + header: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 8, + paddingHorizontal: 12, + borderBottomWidth: 1, + borderBottomColor: 'rgba(255, 255, 255, 0.1)', + }, + collapseButton: { + width: 20, + height: 20, + alignItems: 'center', + justifyContent: 'center', + }, + collapseIcon: { + color: 'rgba(255, 255, 255, 0.6)', + fontSize: 16, + fontWeight: '600', + }, + title: { + flex: 1, + color: '#fff', + fontSize: 13, + fontWeight: '600', + marginLeft: 4, + }, + clearButton: { + paddingHorizontal: 8, + paddingVertical: 2, + }, + clearText: { + color: 'rgba(255, 255, 255, 0.5)', + fontSize: 11, + }, + eventsList: { + flex: 1, + paddingVertical: 8, + }, + emptyText: { + color: 'rgba(255, 255, 255, 0.4)', + fontSize: 12, + textAlign: 'center', + paddingVertical: 20, + }, + eventItem: { + marginHorizontal: 12, + marginVertical: 4, + paddingLeft: 10, + paddingRight: 8, + paddingVertical: 6, + borderLeftWidth: 3, + backgroundColor: 'rgba(255, 255, 255, 0.05)', + borderRadius: 4, + }, + eventType: { + fontSize: 12, + fontWeight: '600', + fontFamily: 'Menlo', + }, + eventData: { + marginTop: 4, + }, + eventDataLine: { + fontSize: 10, + color: 'rgba(255, 255, 255, 0.6)', + fontFamily: 'Menlo', + lineHeight: 14, + }, +}); diff --git a/packages/agentation-rn/example/components/HighlightBox.tsx b/packages/agentation-rn/example/components/HighlightBox.tsx new file mode 100644 index 0000000..3f1aea7 --- /dev/null +++ b/packages/agentation-rn/example/components/HighlightBox.tsx @@ -0,0 +1,29 @@ + +import React from 'react'; +import { View, Text, StyleSheet } from 'react-native'; +import { colors, spacing, radius, typography } from '../theme'; + +interface HighlightBoxProps { + children: React.ReactNode; +} + +export function HighlightBox({ children }: HighlightBoxProps) { + return ( + + {children} + + ); +} + +const styles = StyleSheet.create({ + highlightBox: { + backgroundColor: colors.light.bgSubtle, + borderRadius: radius.md, + padding: spacing.md, + }, + highlightText: { + fontSize: typography.sizes.body, + color: colors.light.textSecondary, + lineHeight: 22, + }, +}); diff --git a/packages/agentation-rn/example/components/Icons.tsx b/packages/agentation-rn/example/components/Icons.tsx new file mode 100644 index 0000000..760a40f --- /dev/null +++ b/packages/agentation-rn/example/components/Icons.tsx @@ -0,0 +1,46 @@ + +import React from 'react'; +import Svg, { Path } from 'react-native-svg'; +import { colors } from '../theme'; + +interface IconProps { + size?: number; + color?: string; +} + +export function SparkleIcon({ size = 20, color = colors.light.text }: IconProps) { + return ( + + + + + + + ); +} + +export function CopyIcon({ size = 20, color = colors.light.text }: IconProps) { + return ( + + + + + ); +} diff --git a/packages/agentation-rn/example/components/InputField.tsx b/packages/agentation-rn/example/components/InputField.tsx new file mode 100644 index 0000000..59d174f --- /dev/null +++ b/packages/agentation-rn/example/components/InputField.tsx @@ -0,0 +1,56 @@ + +import React from 'react'; +import { View, Text, TextInput, StyleSheet } from 'react-native'; +import { colors, spacing, radius, typography } from '../theme'; + +interface InputFieldProps { + label?: string; + placeholder?: string; + value: string; + onChangeText: (text: string) => void; + secureTextEntry?: boolean; +} + +export function InputField({ + label, + placeholder, + value, + onChangeText, + secureTextEntry = false, +}: InputFieldProps) { + return ( + + {label && {label}} + + + ); +} + +const styles = StyleSheet.create({ + inputContainer: { + marginBottom: spacing.md, + }, + inputLabel: { + fontSize: typography.sizes.caption, + fontWeight: typography.weights.medium, + color: colors.light.textSecondary, + marginBottom: spacing.xs, + }, + input: { + backgroundColor: colors.light.bg, + borderRadius: radius.md, + paddingVertical: spacing.sm + 2, + paddingHorizontal: spacing.md, + borderWidth: 1, + borderColor: colors.light.border, + fontSize: typography.sizes.body, + color: colors.light.text, + }, +}); diff --git a/packages/agentation-rn/example/components/ListItem.tsx b/packages/agentation-rn/example/components/ListItem.tsx new file mode 100644 index 0000000..d3c11ea --- /dev/null +++ b/packages/agentation-rn/example/components/ListItem.tsx @@ -0,0 +1,55 @@ + +import React from 'react'; +import { View, Text, TouchableOpacity, StyleSheet } from 'react-native'; +import { colors, spacing, typography } from '../theme'; + +interface ListItemProps { + title: string; + subtitle?: string; + onPress: () => void; + showChevron?: boolean; +} + +export function ListItem({ + title, + subtitle, + onPress, + showChevron = true, +}: ListItemProps) { + return ( + + + {title} + {subtitle && {subtitle}} + + {showChevron && } + + ); +} + +const styles = StyleSheet.create({ + listItem: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: spacing.md, + borderBottomWidth: 1, + borderBottomColor: colors.light.border, + }, + listItemContent: { + flex: 1, + }, + listItemTitle: { + fontSize: typography.sizes.body, + fontWeight: typography.weights.medium, + color: colors.light.text, + }, + listItemSubtitle: { + fontSize: typography.sizes.caption, + color: colors.light.textTertiary, + marginTop: 2, + }, + listItemChevron: { + fontSize: 20, + color: colors.light.textMuted, + }, +}); diff --git a/packages/agentation-rn/example/components/NoteBox.tsx b/packages/agentation-rn/example/components/NoteBox.tsx new file mode 100644 index 0000000..6313ee4 --- /dev/null +++ b/packages/agentation-rn/example/components/NoteBox.tsx @@ -0,0 +1,32 @@ + +import React from 'react'; +import { View, Text, StyleSheet } from 'react-native'; +import { colors, spacing, radius, typography } from '../theme'; + +interface NoteBoxProps { + children: React.ReactNode; +} + +export function NoteBox({ children }: NoteBoxProps) { + return ( + + {children} + + ); +} + +const styles = StyleSheet.create({ + noteBox: { + backgroundColor: colors.light.bgSubtle, + borderRadius: radius.md, + padding: spacing.md, + borderLeftWidth: 3, + borderLeftColor: colors.light.textMuted, + }, + noteText: { + fontSize: typography.sizes.caption, + color: colors.light.textTertiary, + lineHeight: 18, + fontStyle: 'italic', + }, +}); diff --git a/packages/agentation-rn/example/components/NumberedList.tsx b/packages/agentation-rn/example/components/NumberedList.tsx new file mode 100644 index 0000000..66594a3 --- /dev/null +++ b/packages/agentation-rn/example/components/NumberedList.tsx @@ -0,0 +1,43 @@ + +import React from 'react'; +import { View, Text, StyleSheet } from 'react-native'; +import { colors, spacing, typography } from '../theme'; + +interface NumberedListProps { + items: string[]; +} + +export function NumberedList({ items }: NumberedListProps) { + return ( + + {items.map((item, index) => ( + + {index + 1}. + {item} + + ))} + + ); +} + +const styles = StyleSheet.create({ + numberedList: { + marginBottom: spacing.sm, + }, + numberedItem: { + flexDirection: 'row', + marginBottom: spacing.sm, + }, + numberedNumber: { + width: 24, + fontSize: typography.sizes.body, + fontWeight: typography.weights.semibold, + color: colors.primary, + }, + numberedText: { + flex: 1, + fontSize: typography.sizes.body, + color: colors.light.textSecondary, + lineHeight: 20, + }, +}); diff --git a/packages/agentation-rn/example/components/Paragraph.tsx b/packages/agentation-rn/example/components/Paragraph.tsx new file mode 100644 index 0000000..000bada --- /dev/null +++ b/packages/agentation-rn/example/components/Paragraph.tsx @@ -0,0 +1,21 @@ + +import React from 'react'; +import { Text, StyleSheet } from 'react-native'; +import { colors, spacing, typography } from '../theme'; + +interface ParagraphProps { + children: React.ReactNode; +} + +export function Paragraph({ children }: ParagraphProps) { + return {children}; +} + +const styles = StyleSheet.create({ + paragraph: { + fontSize: typography.sizes.body, + color: colors.light.textSecondary, + lineHeight: 22, + marginBottom: spacing.md, + }, +}); diff --git a/packages/agentation-rn/example/components/SectionHeader.tsx b/packages/agentation-rn/example/components/SectionHeader.tsx new file mode 100644 index 0000000..139fe8a --- /dev/null +++ b/packages/agentation-rn/example/components/SectionHeader.tsx @@ -0,0 +1,23 @@ + +import React from 'react'; +import { Text, StyleSheet } from 'react-native'; +import { colors, spacing, typography } from '../theme'; + +interface SectionHeaderProps { + title: string; +} + +export function SectionHeader({ title }: SectionHeaderProps) { + return {title}; +} + +const styles = StyleSheet.create({ + sectionHeader: { + fontSize: typography.sizes.large, + fontWeight: typography.weights.semibold, + color: colors.light.text, + marginTop: spacing.lg, + marginBottom: spacing.md, + letterSpacing: typography.letterSpacing.tight, + }, +}); diff --git a/packages/agentation-rn/example/components/ToggleRow.tsx b/packages/agentation-rn/example/components/ToggleRow.tsx new file mode 100644 index 0000000..efc8d58 --- /dev/null +++ b/packages/agentation-rn/example/components/ToggleRow.tsx @@ -0,0 +1,58 @@ + +import React from 'react'; +import { View, Text, Switch, StyleSheet } from 'react-native'; +import { colors, spacing, typography } from '../theme'; + +interface ToggleRowProps { + label: string; + description?: string; + value: boolean; + onValueChange: (value: boolean) => void; +} + +export function ToggleRow({ + label, + description, + value, + onValueChange, +}: ToggleRowProps) { + return ( + + + {label} + {description && {description}} + + + + ); +} + +const styles = StyleSheet.create({ + toggleRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: spacing.md, + borderBottomWidth: 1, + borderBottomColor: colors.light.border, + }, + toggleInfo: { + flex: 1, + marginRight: spacing.lg, + }, + toggleLabel: { + fontSize: typography.sizes.body, + fontWeight: typography.weights.medium, + color: colors.light.text, + }, + toggleDescription: { + fontSize: typography.sizes.caption, + color: colors.light.textTertiary, + marginTop: 2, + }, +}); diff --git a/packages/agentation-rn/example/components/index.ts b/packages/agentation-rn/example/components/index.ts new file mode 100644 index 0000000..597cad6 --- /dev/null +++ b/packages/agentation-rn/example/components/index.ts @@ -0,0 +1,17 @@ + +export { Card } from './Card'; +export { Button } from './Button'; +export { Badge } from './Badge'; +export { ListItem } from './ListItem'; +export { InputField } from './InputField'; +export { ToggleRow } from './ToggleRow'; +export { DemoCard } from './DemoCard'; +export { SectionHeader } from './SectionHeader'; +export { Paragraph } from './Paragraph'; +export { NumberedList } from './NumberedList'; +export { BulletList } from './BulletList'; +export { HighlightBox } from './HighlightBox'; +export { NoteBox } from './NoteBox'; +export { SparkleIcon, CopyIcon } from './Icons'; +export { EventsPanel } from './EventsPanel'; +export type { ApiEvent } from './EventsPanel'; diff --git a/packages/agentation-rn/example/hooks/index.ts b/packages/agentation-rn/example/hooks/index.ts new file mode 100644 index 0000000..f175f38 --- /dev/null +++ b/packages/agentation-rn/example/hooks/index.ts @@ -0,0 +1,2 @@ +export { useApiEvents } from './useApiEvents'; +export type { AgenationCallbacks, UseApiEventsReturn } from './useApiEvents'; diff --git a/packages/agentation-rn/example/hooks/useApiEvents.ts b/packages/agentation-rn/example/hooks/useApiEvents.ts new file mode 100644 index 0000000..e045b01 --- /dev/null +++ b/packages/agentation-rn/example/hooks/useApiEvents.ts @@ -0,0 +1,72 @@ +import { useState, useCallback } from 'react'; +import type { Annotation } from 'agentation-rn'; +import type { ApiEvent } from '../components/EventsPanel'; + +export interface AgenationCallbacks { + onAnnotationAdd: (annotation: Annotation) => void; + onAnnotationUpdate: (annotation: Annotation) => void; + onAnnotationDelete: (annotation: Annotation) => void; + onCopy: (markdown: string) => void; + onAnnotationsClear: (cleared: Annotation[]) => void; +} + +export interface UseApiEventsReturn { + events: ApiEvent[]; + clearEvents: () => void; + callbacks: AgenationCallbacks; +} + +export function useApiEvents(maxEvents = 20): UseApiEventsReturn { + const [events, setEvents] = useState([]); + + const addEvent = useCallback((type: ApiEvent['type'], data: Record) => { + const event: ApiEvent = { + id: `${Date.now()}-${Math.random().toString(36).slice(2, 7)}`, + type, + data, + timestamp: Date.now(), + }; + setEvents(prev => [...prev.slice(-(maxEvents - 1)), event]); + }, [maxEvents]); + + const clearEvents = useCallback(() => { + setEvents([]); + }, []); + + const callbacks: AgenationCallbacks = { + onAnnotationAdd: useCallback((annotation: Annotation) => { + addEvent('onAnnotationAdd', { + element: annotation.element, + comment: annotation.comment, + }); + }, [addEvent]), + + onAnnotationUpdate: useCallback((annotation: Annotation) => { + addEvent('onAnnotationUpdate', { + element: annotation.element, + comment: annotation.comment, + }); + }, [addEvent]), + + onAnnotationDelete: useCallback((annotation: Annotation) => { + addEvent('onAnnotationDelete', { + element: annotation.element, + }); + }, [addEvent]), + + onCopy: useCallback((markdown: string) => { + addEvent('onCopy', { + length: markdown.length, + format: 'markdown', + }); + }, [addEvent]), + + onAnnotationsClear: useCallback((cleared: Annotation[]) => { + addEvent('onAnnotationsClear', { + count: cleared.length, + }); + }, [addEvent]), + }; + + return { events, clearEvents, callbacks }; +} diff --git a/packages/agentation-rn/example/index.ts b/packages/agentation-rn/example/index.ts new file mode 100644 index 0000000..5fd059f --- /dev/null +++ b/packages/agentation-rn/example/index.ts @@ -0,0 +1,4 @@ +import { registerRootComponent } from 'expo'; +import App from './App'; + +registerRootComponent(App); diff --git a/packages/agentation-rn/example/metro.config.js b/packages/agentation-rn/example/metro.config.js new file mode 100644 index 0000000..17947d4 --- /dev/null +++ b/packages/agentation-rn/example/metro.config.js @@ -0,0 +1,49 @@ +const { getDefaultConfig } = require('expo/metro-config'); +const path = require('path'); + +const projectRoot = __dirname; +const monorepoRoot = path.resolve(projectRoot, '../../..'); +const libraryRoot = path.resolve(projectRoot, '..'); +const librarySrc = path.resolve(libraryRoot, 'src'); + +const config = getDefaultConfig(projectRoot); + +// Watch library and monorepo for hot reload +config.watchFolders = [libraryRoot, monorepoRoot]; + +// Resolve from monorepo root first to avoid duplicate React +config.resolver.nodeModulesPaths = [ + path.resolve(monorepoRoot, 'node_modules'), + path.resolve(projectRoot, 'node_modules'), +]; + +// Disable package exports to avoid dist/ resolution +config.resolver.unstable_enablePackageExports = false; + +// Ensure single copy of React packages +const reactPackages = ['react', 'react-native', 'react/jsx-runtime', 'react/jsx-dev-runtime']; + +// Custom resolver +config.resolver.resolveRequest = (context, moduleName, platform) => { + // Redirect agentation-rn to src/ + if (moduleName === 'agentation-rn') { + return { + filePath: path.resolve(librarySrc, 'index.ts'), + type: 'sourceFile', + }; + } + + // Ensure React packages resolve from monorepo root + if (reactPackages.includes(moduleName)) { + return context.resolveRequest( + { ...context, originModulePath: path.resolve(monorepoRoot, 'index.js') }, + moduleName, + platform + ); + } + + // Fall back to default resolution + return context.resolveRequest(context, moduleName, platform); +}; + +module.exports = config; diff --git a/packages/agentation-rn/example/package.json b/packages/agentation-rn/example/package.json new file mode 100644 index 0000000..3139ec5 --- /dev/null +++ b/packages/agentation-rn/example/package.json @@ -0,0 +1,35 @@ +{ + "name": "@agentation-rn/example", + "version": "1.0.0", + "private": true, + "main": "index.ts", + "scripts": { + "start": "expo start", + "android": "expo run:android", + "ios": "expo run:ios", + "web": "expo start --web", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@callstack/liquid-glass": "^0.7.0", + "@react-native-async-storage/async-storage": "^2.2.0", + "@react-navigation/native": "^7.1.28", + "@react-navigation/native-stack": "^7.10.1", + "agentation-rn": "workspace:*", + "expo": "~54.0.32", + "expo-clipboard": "^8.0.8", + "expo-status-bar": "~3.0.9", + "react": "19.1.0", + "react-native": "0.81.5", + "react-native-dev-inspector": "^1.1.0", + "react-native-safe-area-context": "~5.6.0", + "react-native-screens": "~4.16.0", + "react-native-svg": "15.12.1" + }, + "devDependencies": { + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@types/react": "~19.1.0", + "babel-preset-expo": "^54.0.10", + "typescript": "~5.9.2" + } +} diff --git a/packages/agentation-rn/example/screens/HomeScreen.tsx b/packages/agentation-rn/example/screens/HomeScreen.tsx new file mode 100644 index 0000000..10c0040 --- /dev/null +++ b/packages/agentation-rn/example/screens/HomeScreen.tsx @@ -0,0 +1,386 @@ + +import React, { useState } from 'react'; +import { + View, + Text, + StyleSheet, + ScrollView, +} from 'react-native'; +import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { colors, spacing, radius, typography } from '../theme'; +import { + Card, + Button, + InputField, + DemoCard, + SectionHeader, + Paragraph, + NumberedList, + BulletList, + HighlightBox, + NoteBox, + SparkleIcon, + CopyIcon, +} from '../components'; + +// ============================================================================= +// Types +// ============================================================================= + +type RootStackParamList = { + Home: undefined; + Settings: undefined; + Profile: { userId: string }; + ScrollExample: undefined; + ModalExample: undefined; +}; + +type HomeScreenProps = { + navigation: NativeStackNavigationProp; +}; + +// ============================================================================= +// HomeScreen +// ============================================================================= + +export function HomeScreen({ navigation }: HomeScreenProps) { + const [demoInput, setDemoInput] = useState(''); + + return ( + + {/* Header */} + + Overview + Point at problems, not code + + + {/* Introduction */} + + + Agentation (agent + annotation) is a dev tool that lets you annotate + elements in your app and generate structured feedback for AI coding agents. + + + Tap elements, add notes, and paste the output into Claude Code, Cursor, + or any agent that has access to your codebase. It's fully agent-agnostic, + so the markdown output works with any AI tool. + + + The key insight: agents can find and fix code much faster when they + know exactly which element you're referring to. Agentation captures + component names, file paths, and positions so the agent can locate + the corresponding source files. Based on agentation.dev by Benji + Taylor, Dennis Jin, and Alex Vanderzon. + + + + {/* Quick Start */} + + + + + 1. + + Tap the + + icon to activate + + + + 2. + + Tap any element to see its name highlighted + + + + 3. + + Long press to add an annotation + + + + 4. + + Write your feedback and tap Save + + + + 5. + + Tap + + to copy formatted markdown + + + + 6. + Paste into your agent + + + + + {/* How It Works */} + + + + Agentation works best with AI tools that have access to your codebase + (Claude Code, Cursor, Windsurf, etc.): + + + + + Without Agentation:{'\n'} + "the blue button in the sidebar"{'\n\n'} + With Agentation:{'\n'} + components/Button.tsx:42 (Button) + + + + + {/* Try It - Demo Section */} + + + + The toolbar is active on this screen. Try annotating these demo elements: + + + {/* Demo Buttons */} + + Button Components + +