diff --git a/.serena/cache/typescript/document_symbols_cache_v23-06-25.pkl b/.serena/cache/typescript/document_symbols_cache_v23-06-25.pkl new file mode 100644 index 0000000..8834a45 Binary files /dev/null and b/.serena/cache/typescript/document_symbols_cache_v23-06-25.pkl differ diff --git a/.serena/memories/architecture_patterns.md b/.serena/memories/architecture_patterns.md new file mode 100644 index 0000000..8c26f7b --- /dev/null +++ b/.serena/memories/architecture_patterns.md @@ -0,0 +1,279 @@ +# Architecture Patterns and Guidelines + +## Monorepo Architecture + +### Workspace Dependencies + +```json +// Package dependency pattern +{ + "dependencies": { + "@rite/backend": "workspace:*", // Always latest + "@rite/shared-types": "workspace:^", // Compatible version + "@rite/ui": "workspace:*" // Always latest + } +} +``` + +### Build Order (Turborepo) + +1. `@rite/shared-types` (foundational types) +2. `@rite/posthog-config` (analytics config) +3. `@rite/backend` (Convex functions) +4. `@rite/ui` (components) +5. `next-app` & `mobile` (applications) + +## Cross-Platform Component Design + +### Platform-Specific Implementation Pattern + +```tsx +// button/button.web.tsx +import { Slot } from '@radix-ui/react-slot'; +export const Button = React.forwardRef(...); + +// button/button.native.tsx +import { Pressable } from 'react-native'; +export const Button: React.FC = (...); + +// button/index.ts +export { Button } from './button.web'; +// OR +export { Button } from './button.native'; +``` + +### "use dom" Components (Mobile) + +```tsx +// For advanced web features in mobile +// qr-code/qr-code.dom.tsx +'use dom'; +import QRCode from 'qrcode'; +export const QRCodeComponent = (...); + +// Provides direct DOM access for Canvas, File API, etc. +``` + +## Authentication Architecture + +### Modular Auth System (Mobile) + +``` +lib/auth/ +├── types.ts # AuthError class, interfaces +├── oauth-config.ts # Platform-specific OAuth config +├── secure-storage.ts # Cross-platform storage +├── session-utils.ts # Session management +└── index.ts # Centralized exports + +hooks/auth/ +├── useGoogleAuth.ts # Provider-specific logic +├── useSession.ts # Session state +└── useOAuthFlow.ts # Complete flow orchestration + +contexts/ +└── AuthContext.tsx # Clean orchestration (88 lines) +``` + +### OAuth Flow Types + +- **Web**: Implicit flow (`response_type=token`) +- **Mobile**: Authorization code flow with auto-exchange +- **Platform Detection**: Automatic flow selection based on environment + +## Theme System Architecture + +### CSS Variables Pattern + +```css +:root { + --brand-primary: hsl(293deg 100% 66%); + --bg-primary: hsl(254deg 35% 15%); + --text-primary: hsl(0deg 0% 100%); + --button-primary-text: white; +} +``` + +### Theme Switching + +```tsx +// Automatic CSS variable updates +const applyTheme = (theme: Theme) => { + const css = generateThemeCSS(theme); + document.documentElement.style.cssText = css; + localStorage.setItem('theme', theme.name); +}; +``` + +## Data Protection Pattern + +### Basic Obfuscation (Convex V8 Runtime) + +```tsx +// Synchronous XOR + Base64 encoding +const encryptSensitiveData = (data: string): string => { + // XOR with encryption key + Base64 encoding + // NOT cryptographically secure - for basic privacy only +}; + +// Usage in mutations +const obfuscatedData = encryptSensitiveData(sensitiveInput); +await ctx.db.insert('submissions', { data: obfuscatedData }); +``` + +## Testing Patterns + +### TDD Workflow + +```tsx +// 1. RED - Write failing test +test('should export guest list as CSV', () => { + expect(exportToCSV(mockData)).toContain('Name,Email'); +}); + +// 2. GREEN - Minimal implementation +const exportToCSV = (data) => 'Name,Email\n...'; + +// 3. REFACTOR - Improve while tests pass +const exportToCSV = (data: GuestData[]) => { + // Proper implementation +}; +``` + +### Visual Testing + +```tsx +// Playwright visual regression +test('button variants', async ({ page }) => { + await page.goto('/visual-test'); + await expect(page.locator('[data-testid="buttons"]')).toHaveScreenshot(); +}); +``` + +## Effect.ts Integration + +### Functional Programming Pattern + +```tsx +import { Effect, pipe } from 'effect'; + +// Error handling with Effect +const processExportEffect = (data: ExportData) => + pipe( + Effect.tryPromise(() => generateExcel(data)), + Effect.mapError((error) => new ExportProcessingError(error)), + Effect.map((buffer) => ({ buffer, filename: 'export.xlsx' })) + ); +``` + +## File Upload Pattern + +### Cross-Platform File Handling + +```tsx +// Web (react-dropzone) +const { getRootProps, getInputProps } = useDropzone({ + accept: { 'image/*': ['.jpg', '.png'] }, + onDrop: handleFiles, +}); + +// Mobile (expo-image-picker) +const result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ImagePicker.MediaTypeOptions.Images, + allowsEditing: true, +}); +``` + +## Convex Backend Patterns + +### Database Schema + +```tsx +// Schema with proper relationships +export default defineSchema({ + events: defineTable({ + title: v.string(), + organizerId: v.id('users'), + // ... other fields + }).index('by_organizer', ['organizerId']), + + timeslots: defineTable({ + eventId: v.id('events'), + djId: v.optional(v.id('users')), + submissionToken: v.string(), + }).index('by_event', ['eventId']), +}); +``` + +### Mutation Pattern + +```tsx +export const createEvent = mutation({ + args: { title: v.string(), description: v.string() }, + handler: async (ctx, { title, description }) => { + const userId = await getAuthUserId(ctx); + if (!userId) throw new Error('Not authenticated'); + + return await ctx.db.insert('events', { + title, + description, + organizerId: userId, + createdAt: Date.now(), + }); + }, +}); +``` + +## Internationalization Pattern + +### next-intl Setup + +```tsx +// Locale routing: /[locale]/page +// Messages: /messages/en.json, /messages/ko.json + +const t = useTranslations('dashboard'); +return

{t('title')}

; + +// Type-safe with generated types +type Messages = typeof import('../messages/en.json'); +``` + +## Design Token System + +### Token Hierarchy + +```tsx +// Base tokens +const colors = { + brand: { primary: '#E946FF' }, + neutral: { 0: '#FFFFFF', 900: '#000000' }, +}; + +// Semantic tokens +const semanticColors = { + bg: { primary: colors.neutral[900] }, + text: { primary: colors.neutral[0] }, +}; + +// Component tokens +const buttonTokens = { + primary: { bg: colors.brand.primary, text: 'white' }, +}; +``` + +## Performance Patterns + +### Bundle Optimization + +- **Dynamic imports** for large components +- **Tree shaking** with proper ES modules +- **Code splitting** at route level +- **Font optimization** with variable fonts + +### Mobile Performance + +- **Image optimization** with expo-image +- **Navigation optimization** with React Navigation +- **State management** with minimal re-renders +- **Platform-specific optimizations** diff --git a/.serena/memories/code_style_conventions.md b/.serena/memories/code_style_conventions.md new file mode 100644 index 0000000..ae88c6f --- /dev/null +++ b/.serena/memories/code_style_conventions.md @@ -0,0 +1,208 @@ +# Code Style and Conventions + +## Mandatory Rules (from CLAUDE.md) + +### React Imports + +```tsx +// ✅ ALWAYS use namespace import for React +import * as React from 'react'; + +// ❌ NEVER use default import +import React from 'react'; +``` + +### TypeScript + +```tsx +// ❌ NEVER use any or non-null assertion +const data: any = getValue(); +const value = data!.property; + +// ✅ Use proper typing +const data: ExpectedType = getValue(); +const value = data?.property; +``` + +### Design System + +```tsx +// ✅ Use @rite/ui components exclusively +import { Button, Card } from '@rite/ui'; + +// ✅ Use CSS variables for colors +className = 'bg-[var(--brand-primary)]'; + +// ❌ Never use hardcoded colors +className = 'bg-purple-500'; +``` + +## ESLint Configuration + +### Key Rules + +- **Indentation**: Tabs (2-space width), SwitchCase: 1 +- **TypeScript**: No explicit `any`, no unsafe operations disabled +- **React**: Hooks rules enforced, refresh warnings +- **Unused vars**: Warn for vars/args starting with `_` + +### File Ignores + +- `dist/`, `convex/_generated/`, `instagram-oauth-proxy/` +- Config files: `eslint.config.mjs`, `postcss.config.js`, etc. + +## Prettier Configuration + +### Formatting Rules + +```json +{ + "useTabs": true, + "tabWidth": 2, + "semi": true, + "singleQuote": true, + "trailingComma": "es5", + "printWidth": 100, + "bracketSpacing": true, + "arrowParens": "always", + "endOfLine": "lf" +} +``` + +### File Overrides + +- **YAML**: Uses spaces (tabWidth: 2) +- **Markdown**: Uses spaces, preserves prose wrapping + +## Component Structure + +### Platform-Specific Components + +``` +components/button/ +├── button.web.tsx # Web implementation (Radix UI) +├── button.native.tsx # React Native implementation +├── index.ts # Platform-specific exports +└── button.test.tsx # Shared tests +``` + +### Import/Export Pattern + +```tsx +// index.ts - Platform-specific exports +export { Button } from './button.web'; +// or +export { Button } from './button.native'; + +// Usage - same import works everywhere +import { Button } from '@rite/ui'; +``` + +## TypeScript Conventions + +### Type Definitions + +```tsx +// ✅ Use 'type' instead of 'interface' unless absolutely necessary +type UserData = { + id: string; + name: string; + email?: string; +}; + +// ✅ Use proper component prop types +type ButtonProps = { + variant?: 'primary' | 'secondary'; + children: React.ReactNode; + onPress?: () => void; +}; +``` + +### Naming Conventions + +- **Components**: PascalCase (`ExportGuestList`, `ThemeSwitcher`) +- **Hooks**: camelCase with `use` prefix (`useGoogleAuth`, `useSession`) +- **Types**: PascalCase (`ExportData`, `ButtonProps`) +- **Constants**: UPPER_SNAKE_CASE (`API_ENDPOINTS`) +- **Files**: kebab-case or PascalCase for components + +## CSS/Styling Conventions + +### Tailwind Usage + +```tsx +// ✅ Use design tokens and CSS variables +className = 'bg-[var(--bg-primary)] text-[var(--text-primary)]'; + +// ✅ Use flex and gap (React Native compatible) +className = 'flex flex-row gap-2'; + +// ❌ Never use space-x or space-y (not React Native compatible) +className = 'space-x-2'; + +// ✅ Theme-agnostic design +className = 'bg-brand-primary text-button-primary-text'; +``` + +### Component Patterns + +```tsx +// ✅ Use forwardRef for web components +const Button = React.forwardRef( + ({ className, variant, ...props }, ref) => { + return ); + const button = screen.getByRole('button', { name: 'Click me' }); + expect(button).toBeInTheDocument(); + expect(button).toHaveClass('inline-flex', 'items-center', 'justify-center'); + }); + + it('applies variant classes correctly', () => { + render(); + const button = screen.getByRole('button', { name: 'Delete' }); + expect(button).toHaveClass('bg-error'); + }); + + it('applies size classes correctly', () => { + render(); + const button = screen.getByRole('button', { name: 'Small Button' }); + expect(button).toHaveClass('h-10', 'rounded-md', 'px-4', 'py-2', 'text-sm'); + }); + + it('applies large size classes correctly', () => { + render(); + const button = screen.getByRole('button', { name: 'Large Button' }); + expect(button).toHaveClass('h-14', 'rounded-xl', 'px-8', 'py-4', 'text-lg'); + }); + + it('handles click events', () => { + const handleClick = vi.fn(); + render(); + const button = screen.getByRole('button', { name: 'Click me' }); + + fireEvent.click(button); + expect(handleClick).toHaveBeenCalledTimes(1); + }); + + it('is disabled when disabled prop is true', () => { + render(); + const button = screen.getByRole('button', { name: 'Disabled Button' }); + expect(button).toBeDisabled(); + expect(button).toHaveClass('disabled:pointer-events-none', 'disabled:opacity-50'); + }); + + it('forwards ref correctly', () => { + const ref = React.createRef(); + render(); + expect(ref.current).toBeInstanceOf(HTMLButtonElement); + }); + + it('applies custom className', () => { + render(); + const button = screen.getByRole('button', { name: 'Custom Button' }); + expect(button).toHaveClass('custom-class'); + }); + + it('renders all variant types without errors', () => { + const variants = ['default', 'destructive', 'outline', 'secondary', 'ghost', 'link'] as const; + + variants.forEach((variant) => { + render(); + const button = screen.getByRole('button', { name: `${variant} Button` }); + expect(button).toBeInTheDocument(); + }); + }); + + it('renders all size types without errors', () => { + const sizes = ['default', 'sm', 'lg', 'icon'] as const; + + sizes.forEach((size) => { + render(); + const button = screen.getByRole('button', { name: `${size} Button` }); + expect(button).toBeInTheDocument(); + }); + }); + + it('has correct accessibility attributes', () => { + render(); + const button = screen.getByRole('button', { name: 'Accessible button' }); + expect(button).toHaveAttribute('aria-label', 'Accessible button'); + }); + + it('applies focus styles correctly', () => { + render(); + const button = screen.getByRole('button', { name: 'Focus Button' }); + expect(button).toHaveClass('focus-visible:ring-2', 'focus-visible:ring-brand-primary'); + }); +}); \ No newline at end of file diff --git a/packages/ui/src/components/card/card.test.tsx b/packages/ui/src/components/card/card.test.tsx new file mode 100644 index 0000000..8fba234 --- /dev/null +++ b/packages/ui/src/components/card/card.test.tsx @@ -0,0 +1,159 @@ +import * as React from 'react'; +import { render, screen } from '@testing-library/react'; +import { describe, it, expect } from 'vitest'; +import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './card.web'; + +describe('Card Components', () => { + describe('Card', () => { + it('renders correctly with default props', () => { + render( + + Card content + + ); + const card = screen.getByTestId('card'); + expect(card).toBeInTheDocument(); + expect(card).toHaveClass('rounded-lg', 'bg-card', 'text-card-foreground', 'border', 'border-border', 'shadow-sm'); + }); + + it('applies custom className', () => { + render( + + Card content + + ); + const card = screen.getByTestId('card'); + expect(card).toHaveClass('custom-card'); + }); + + it('forwards ref correctly', () => { + const ref = React.createRef(); + render( + + Card content + + ); + expect(ref.current).toBeInstanceOf(HTMLDivElement); + }); + }); + + describe('CardHeader', () => { + it('renders correctly', () => { + render( + + + Title + + + ); + const header = screen.getByTestId('card-header'); + expect(header).toBeInTheDocument(); + expect(header).toHaveClass('flex', 'flex-col', 'gap-2', 'p-6'); + }); + }); + + describe('CardTitle', () => { + it('renders correctly as div element', () => { + render( + + + Card Title + + + ); + const title = screen.getByText('Card Title'); + expect(title).toBeInTheDocument(); + expect(title).toHaveClass('text-2xl', 'font-bold', 'leading-tight', 'text-text-primary'); + }); + + it('applies custom className', () => { + render( + + + Card Title + + + ); + const title = screen.getByText('Card Title'); + expect(title).toHaveClass('custom-title'); + }); + }); + + describe('CardDescription', () => { + it('renders correctly', () => { + render( + + + Card description text + + + ); + const description = screen.getByText('Card description text'); + expect(description).toBeInTheDocument(); + expect(description).toHaveClass('text-sm', 'text-text-secondary'); + }); + }); + + describe('CardContent', () => { + it('renders correctly', () => { + render( + + + Content goes here + + + ); + const content = screen.getByTestId('card-content'); + expect(content).toBeInTheDocument(); + expect(content).toHaveClass('p-6', 'pt-0'); + expect(content).toHaveTextContent('Content goes here'); + }); + }); + + describe('CardFooter', () => { + it('renders correctly', () => { + render( + + + Footer content + + + ); + const footer = screen.getByTestId('card-footer'); + expect(footer).toBeInTheDocument(); + expect(footer).toHaveClass('flex', 'items-center', 'p-6', 'pt-0'); + expect(footer).toHaveTextContent('Footer content'); + }); + }); + + describe('Complete Card Structure', () => { + it('renders a complete card with all components', () => { + render( + + + Complete Card + This is a complete card example + + +

Main card content goes here

+
+ + + +
+ ); + + const card = screen.getByTestId('complete-card'); + const title = screen.getByText('Complete Card'); + const description = screen.getByText('This is a complete card example'); + const content = screen.getByText('Main card content goes here'); + const button = screen.getByRole('button', { name: 'Action Button' }); + + expect(card).toBeInTheDocument(); + expect(title).toBeInTheDocument(); + expect(description).toBeInTheDocument(); + expect(content).toBeInTheDocument(); + expect(button).toBeInTheDocument(); + }); + }); +}); \ No newline at end of file diff --git a/packages/ui/src/components/dropzone/dropzone.web.tsx b/packages/ui/src/components/dropzone/dropzone.web.tsx index 0e3edef..50b4aa4 100644 --- a/packages/ui/src/components/dropzone/dropzone.web.tsx +++ b/packages/ui/src/components/dropzone/dropzone.web.tsx @@ -1,8 +1,7 @@ 'use client'; import { UploadIcon } from 'lucide-react'; -import type { ReactNode } from 'react'; -import { createContext, useContext } from 'react'; +import * as React from 'react'; import type { DropEvent, DropzoneOptions, FileRejection } from 'react-dropzone'; import { useDropzone } from 'react-dropzone'; import { Button } from '../button'; @@ -29,13 +28,13 @@ const renderBytes = (bytes: number) => { return `${size.toFixed(2)}${units[unitIndex]}`; }; -const DropzoneContext = createContext(undefined); +const DropzoneContext = React.createContext(undefined); export type DropzoneProps = Omit & { src?: File[]; className?: string; onDrop?: (acceptedFiles: File[], fileRejections: FileRejection[], event: DropEvent) => void; - children?: ReactNode; + children?: React.ReactNode; }; export const Dropzone = ({ @@ -94,7 +93,7 @@ export const Dropzone = ({ }; const useDropzoneContext = () => { - const context = useContext(DropzoneContext); + const context = React.useContext(DropzoneContext); if (!context) { throw new Error('useDropzoneContext must be used within a Dropzone'); @@ -104,7 +103,7 @@ const useDropzoneContext = () => { }; export type DropzoneContentProps = { - children?: ReactNode; + children?: React.ReactNode; className?: string; }; @@ -141,7 +140,7 @@ export const DropzoneContent = ({ children, className }: DropzoneContentProps) = }; export type DropzoneEmptyStateProps = { - children?: ReactNode; + children?: React.ReactNode; className?: string; }; diff --git a/packages/ui/src/components/event-card/event-card.native.tsx b/packages/ui/src/components/event-card/event-card.native.tsx index e843d56..38119fa 100644 --- a/packages/ui/src/components/event-card/event-card.native.tsx +++ b/packages/ui/src/components/event-card/event-card.native.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import { Pressable, Text, View } from 'react-native'; import { Link2 } from 'lucide-react-native'; import '@rite/ui/types/nativewind'; diff --git a/packages/ui/src/components/input/input.test.tsx b/packages/ui/src/components/input/input.test.tsx new file mode 100644 index 0000000..222c6e5 --- /dev/null +++ b/packages/ui/src/components/input/input.test.tsx @@ -0,0 +1,103 @@ +import * as React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import { Input } from './input.web'; + +describe('Input Component', () => { + it('renders correctly with default props', () => { + render(); + const input = screen.getByPlaceholderText('Enter text'); + expect(input).toBeInTheDocument(); + expect(input).toHaveClass('flex', 'h-12', 'w-full', 'rounded-lg', 'border', 'border-border'); + }); + + it('applies custom className', () => { + render(); + const input = screen.getByPlaceholderText('Test input'); + expect(input).toHaveClass('custom-input'); + }); + + it('forwards ref correctly', () => { + const ref = React.createRef(); + render(); + expect(ref.current).toBeInstanceOf(HTMLInputElement); + }); + + it('handles value changes', () => { + const handleChange = vi.fn(); + render(); + const input = screen.getByPlaceholderText('Change test'); + + fireEvent.change(input, { target: { value: 'new value' } }); + expect(handleChange).toHaveBeenCalledTimes(1); + }); + + it('can be disabled', () => { + render(); + const input = screen.getByPlaceholderText('Disabled input'); + expect(input).toBeDisabled(); + expect(input).toHaveClass('disabled:cursor-not-allowed', 'disabled:opacity-50'); + }); + + it('supports different input types', () => { + render(); + const input = screen.getByPlaceholderText('Email input'); + expect(input).toHaveAttribute('type', 'email'); + }); + + it('supports password type', () => { + render(); + const input = screen.getByPlaceholderText('Password input'); + expect(input).toHaveAttribute('type', 'password'); + }); + + it('applies focus styles', () => { + render(); + const input = screen.getByPlaceholderText('Focus test'); + expect(input).toHaveClass('focus:border-brand-primary', 'focus:ring-[3px]', 'focus:ring-brand-primary'); + }); + + it('handles controlled value', () => { + const TestComponent = () => { + const [value, setValue] = React.useState('initial value'); + return ( + setValue(e.target.value)} + placeholder="Controlled input" + /> + ); + }; + + render(); + const input = screen.getByDisplayValue('initial value'); + expect(input).toBeInTheDocument(); + + fireEvent.change(input, { target: { value: 'updated value' } }); + expect(screen.getByDisplayValue('updated value')).toBeInTheDocument(); + }); + + it('supports required attribute', () => { + render(); + const input = screen.getByPlaceholderText('Required input'); + expect(input).toBeRequired(); + }); + + it('supports maxLength attribute', () => { + render(); + const input = screen.getByPlaceholderText('Max length input'); + expect(input).toHaveAttribute('maxLength', '10'); + }); + + it('supports readOnly attribute', () => { + render(); + const input = screen.getByDisplayValue('Read only value'); + expect(input).toHaveAttribute('readOnly'); + }); + + it('renders without type attribute when no type specified', () => { + render(); + const input = screen.getByPlaceholderText('Default type'); + expect(input).not.toHaveAttribute('type'); + }); +}); \ No newline at end of file diff --git a/packages/ui/src/components/label/label.native.tsx b/packages/ui/src/components/label/label.native.tsx index 55c3b1a..18fff9c 100644 --- a/packages/ui/src/components/label/label.native.tsx +++ b/packages/ui/src/components/label/label.native.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import { Text, View, Pressable } from 'react-native'; import type { TextProps } from 'react-native'; import '@rite/ui/types/nativewind'; diff --git a/packages/ui/src/components/list-item/list-item.native.tsx b/packages/ui/src/components/list-item/list-item.native.tsx index bcf7472..773b28e 100644 --- a/packages/ui/src/components/list-item/list-item.native.tsx +++ b/packages/ui/src/components/list-item/list-item.native.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import { Pressable, Text, View } from 'react-native'; import { ChevronRight } from 'lucide-react-native'; import '@rite/ui/types/nativewind'; diff --git a/packages/ui/src/components/loading-indicator/loading-indicator.native.tsx b/packages/ui/src/components/loading-indicator/loading-indicator.native.tsx index 3bfc2a2..96ab928 100644 --- a/packages/ui/src/components/loading-indicator/loading-indicator.native.tsx +++ b/packages/ui/src/components/loading-indicator/loading-indicator.native.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import { View, Text, ActivityIndicator } from 'react-native'; import '@rite/ui/types/nativewind'; diff --git a/packages/ui/src/components/loading-indicator/loading-indicator.web.tsx b/packages/ui/src/components/loading-indicator/loading-indicator.web.tsx index ded173f..0bfe727 100644 --- a/packages/ui/src/components/loading-indicator/loading-indicator.web.tsx +++ b/packages/ui/src/components/loading-indicator/loading-indicator.web.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; interface LoadingIndicatorProps { className?: string; diff --git a/packages/ui/src/components/select/select.native.tsx b/packages/ui/src/components/select/select.native.tsx index 28f3970..de6dcd3 100644 --- a/packages/ui/src/components/select/select.native.tsx +++ b/packages/ui/src/components/select/select.native.tsx @@ -1,15 +1,15 @@ -import React, { useState } from 'react'; +import * as React from 'react'; import { View, Text, TouchableOpacity, Modal, ScrollView, Pressable } from 'react-native'; import { ChevronDownIcon, CheckIcon } from 'lucide-react-native'; import '@rite/ui/types/nativewind'; // Root Select component - manages state -export interface SelectProps { +export type SelectProps = { value?: string; onValueChange?: (value: string) => void; disabled?: boolean; children?: React.ReactNode; -} +}; interface SelectContextType { value?: string; @@ -22,7 +22,7 @@ interface SelectContextType { const SelectContext = React.createContext(undefined); export function Select({ value, onValueChange, disabled, children }: SelectProps) { - const [open, setOpen] = useState(false); + const [open, setOpen] = React.useState(false); return ( diff --git a/packages/ui/src/components/textarea/textarea.native.tsx b/packages/ui/src/components/textarea/textarea.native.tsx index 7ffd243..b74f194 100644 --- a/packages/ui/src/components/textarea/textarea.native.tsx +++ b/packages/ui/src/components/textarea/textarea.native.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import { TextInput, Platform } from 'react-native'; import type { TextInputProps } from 'react-native'; import '@rite/ui/types/nativewind'; diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json index 7a6b452..0950d3a 100644 --- a/packages/ui/tsconfig.json +++ b/packages/ui/tsconfig.json @@ -25,5 +25,5 @@ "typeRoots": ["./node_modules/@types", "./src/types"] }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist", "src/**/*.test.*", "src/**/*.spec.*"] }