because I got bored of customising my CV for every job
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(docs): add documentation site with MDX support

+5851
+105
apps/docs/Dockerfile
··· 1 + # ============================================================================= 2 + # Development Stage 3 + # ============================================================================= 4 + FROM node:22-alpine AS development 5 + 6 + # Install curl for health checks 7 + RUN apk add --no-cache curl 8 + 9 + WORKDIR /app 10 + 11 + # Copy monorepo root files needed for workspace setup 12 + # Note: package-lock.json is ignored in .dockerignore for faster builds 13 + COPY package.json lerna.json ./ 14 + 15 + # Copy package.json files for dependency resolution (monorepo workspace) 16 + # Copy these in order of least likely to change to most likely to change 17 + COPY packages/tsconfig/package.json ./packages/tsconfig/ 18 + COPY packages/biome-config/package.json ./packages/biome-config/ 19 + COPY packages/utils/package.json ./packages/utils/ 20 + COPY packages/ui/package.json ./packages/ui/ 21 + COPY apps/docs/package.json ./apps/docs/ 22 + 23 + # Install dependencies first - this layer will cache well when dependencies don't change 24 + # Using npm install instead of npm ci since package-lock.json is ignored for faster builds 25 + RUN npm install --ignore-scripts 26 + 27 + # Copy package source code (needed for workspace packages) 28 + COPY packages/tsconfig ./packages/tsconfig 29 + COPY packages/ui ./packages/ui 30 + COPY packages/utils ./packages/utils 31 + 32 + # Copy docs config files (changes less frequently than source) 33 + COPY apps/docs/tsconfig*.json ./apps/docs/ 34 + COPY apps/docs/vite.config.ts ./apps/docs/ 35 + COPY apps/docs/postcss.config.cjs ./apps/docs/ 36 + COPY apps/docs/index.html ./apps/docs/ 37 + 38 + # Source code is copied via volumes in docker-compose, but we copy content 39 + # since it's needed for MDX processing 40 + COPY apps/docs/content ./apps/docs/content 41 + 42 + WORKDIR /app/apps/docs 43 + 44 + EXPOSE 3001 45 + 46 + CMD ["npm", "run", "dev"] 47 + 48 + # ============================================================================= 49 + # Build Stage 50 + # ============================================================================= 51 + FROM node:22-alpine AS builder 52 + 53 + WORKDIR /app 54 + 55 + # Copy monorepo root files 56 + # Note: package-lock.json is ignored in .dockerignore for faster builds 57 + COPY package.json lerna.json ./ 58 + 59 + # Copy package.json files for dependency resolution 60 + COPY packages/tsconfig/package.json ./packages/tsconfig/ 61 + COPY packages/biome-config/package.json ./packages/biome-config/ 62 + COPY packages/utils/package.json ./packages/utils/ 63 + COPY packages/ui/package.json ./packages/ui/ 64 + COPY apps/docs/package.json ./apps/docs/ 65 + 66 + # Install dependencies (cached layer) 67 + # Using npm install instead of npm ci since package-lock.json is ignored for faster builds 68 + RUN npm install --ignore-scripts 69 + 70 + # Copy package source code 71 + COPY packages/tsconfig ./packages/tsconfig 72 + COPY packages/ui ./packages/ui 73 + COPY packages/utils ./packages/utils 74 + 75 + # Copy docs config files 76 + COPY apps/docs/tsconfig*.json ./apps/docs/ 77 + COPY apps/docs/vite.config.ts ./apps/docs/ 78 + COPY apps/docs/postcss.config.cjs ./apps/docs/ 79 + COPY apps/docs/index.html ./apps/docs/ 80 + 81 + # Copy docs source code and content 82 + COPY apps/docs/src ./apps/docs/src 83 + COPY apps/docs/content ./apps/docs/content 84 + 85 + # Build the docs app 86 + WORKDIR /app/apps/docs 87 + RUN npm run build 88 + 89 + # ============================================================================= 90 + # Production Stage 91 + # ============================================================================= 92 + FROM nginx:alpine AS production 93 + 94 + WORKDIR /usr/share/nginx/html 95 + 96 + # Copy built assets from builder 97 + COPY --from=builder /app/apps/docs/dist . 98 + 99 + # Copy nginx configuration 100 + COPY apps/docs/nginx.conf /etc/nginx/conf.d/default.conf 101 + 102 + EXPOSE 80 103 + 104 + CMD ["nginx", "-g", "daemon off;"] 105 +
+152
apps/docs/README.md
··· 1 + # Documentation Site 2 + 3 + This is the documentation site for the CV Generator project, built with React, Vite, and React Router. 4 + 5 + ## Architecture 6 + 7 + The docs app follows a clean separation of concerns: 8 + 9 + ``` 10 + apps/docs/src/ 11 + ├── components/ # UI components (presentation layer) 12 + │ ├── DocPage.tsx # Main documentation page component 13 + │ ├── Sidebar.tsx # Navigation sidebar 14 + │ ├── Layout.tsx # Page layout wrapper 15 + │ └── MarkdownRenderer.tsx # Markdown rendering logic 16 + 17 + ├── config/ # Configuration (content & structure) 18 + │ ├── docs.config.ts # Documentation content mapping 19 + │ ├── navigation.config.ts # Sidebar navigation structure 20 + │ ├── env.config.ts # Environment variable configuration 21 + │ └── index.ts # Barrel export 22 + 23 + └── router/ # Application routing 24 + └── AppRouter.tsx 25 + ``` 26 + 27 + ### Design Principles 28 + 29 + 1. **Separation of Concerns**: Components render, configs define content/structure 30 + 2. **Single Responsibility**: Each config file has one clear purpose 31 + 3. **Easy Maintenance**: Add new docs by updating config files, not components 32 + 33 + ## Features 34 + 35 + - **Markdown Rendering**: Documentation is written in Markdown and rendered with syntax highlighting 36 + - **Environment Variable Interpolation**: Dynamic values are injected at runtime 37 + - **Catppuccin Theme**: Consistent design system with the main application 38 + - **Hot Module Replacement**: Changes to documentation update instantly 39 + - **Configuration-Driven**: Content and structure defined separately from UI components 40 + 41 + ## Environment Variable Interpolation 42 + 43 + The documentation supports interpolating environment variables into markdown files at runtime. This allows for dynamic URLs and configuration values based on the deployment environment. 44 + 45 + ### Usage 46 + 47 + In any markdown file, use double curly braces to reference an environment variable: 48 + 49 + ```markdown 50 + - **Client App**: [{{CLIENT_URL}}]({{CLIENT_URL}}) 51 + - **GraphQL Playground**: [{{GRAPHQL_URL}}]({{GRAPHQL_URL}}) 52 + ``` 53 + 54 + ### Available Variables 55 + 56 + The following variables are available for interpolation: 57 + 58 + | Variable | Default Value | Description | 59 + |----------|---------------|-------------| 60 + | `CLIENT_URL` | `http://localhost:5173` | React client application URL | 61 + | `SERVER_URL` | `http://localhost:3000` | NestJS API server URL | 62 + | `DOCS_URL` | `http://localhost:3001` | Documentation site URL | 63 + | `GRAPHQL_URL` | `http://localhost:3000/graphql` | GraphQL Playground URL | 64 + | `DB_HOST` | `localhost` | Database host | 65 + | `DB_PORT` | `5432` | Database port | 66 + 67 + ### Configuration 68 + 69 + Environment variables are configured in two places: 70 + 71 + 1. **Docker Compose** (`docker-compose.yml`): 72 + ```yaml 73 + docs: 74 + environment: 75 + VITE_CLIENT_URL: ${VITE_CLIENT_URL:-http://localhost:5173} 76 + VITE_SERVER_URL: ${VITE_SERVER_URL:-http://localhost:3000} 77 + # ... other variables 78 + ``` 79 + 80 + 2. **Code** (`apps/docs/src/config/env.config.ts`): 81 + ```typescript 82 + export const ENV_VARS: Record<string, string> = { 83 + CLIENT_URL: import.meta.env.VITE_CLIENT_URL || "http://localhost:5173", 84 + SERVER_URL: import.meta.env.VITE_SERVER_URL || "http://localhost:3000", 85 + // ... other variables 86 + }; 87 + ``` 88 + 89 + ### Adding New Variables 90 + 91 + To add a new interpolation variable: 92 + 93 + 1. Add it to the `ENV_VARS` object in `apps/docs/src/config/env.config.ts` 94 + 2. Add the corresponding `VITE_*` environment variable to `docker-compose.yml` 95 + 3. Use it in markdown files with `{{VARIABLE_NAME}}` 96 + 97 + ### How It Works 98 + 99 + The interpolation happens in `env.config.ts` using a simple regex replacement: 100 + 101 + ```typescript 102 + export const interpolateEnvVars = (content: string): string => { 103 + return content.replace(/\{\{(\w+)\}\}/g, (match, varName) => { 104 + return ENV_VARS[varName] ?? match; 105 + }); 106 + }; 107 + ``` 108 + 109 + This ensures that: 110 + - Variables are replaced at render time (not build time) 111 + - Missing variables fall back to the placeholder text 112 + - The same markdown files work across different environments 113 + 114 + ## Adding New Documentation 115 + 116 + To add a new documentation page: 117 + 118 + 1. **Create the markdown file** in the appropriate location 119 + 2. **Import it** in `apps/docs/src/config/docs.config.ts`: 120 + ```typescript 121 + import myNewDocMd from "../../../../MY_NEW_DOC.md?raw"; 122 + ``` 123 + 3. **Add to the mapping**: 124 + ```typescript 125 + export const DOC_CONTENT: Record<string, string> = { 126 + // ... existing docs 127 + "my-new-doc": myNewDocMd, 128 + }; 129 + ``` 130 + 4. **Add navigation link** in `apps/docs/src/config/navigation.config.ts`: 131 + ```typescript 132 + export const DOC_LINKS: DocLink[] = [ 133 + // ... existing links 134 + { title: "My New Doc", slug: "my-new-doc", icon: "📄" }, 135 + ]; 136 + ``` 137 + 138 + That's it! The UI components will automatically pick up the new documentation. 139 + 140 + ## Development 141 + 142 + ```bash 143 + # Install dependencies 144 + npm install 145 + 146 + # Run development server 147 + npm run dev 148 + 149 + # Build for production 150 + npm run build 151 + ``` 152 +
+23
apps/docs/content/README.md
··· 1 + # Documentation Content 2 + 3 + This directory contains all the markdown documentation files for the CV Generator project. 4 + 5 + ## How It Works 6 + 7 + All `.md` and `.mdx` files in this directory are automatically discovered and made available in the documentation site. The file name (without the `.md` or `.mdx` extension) becomes the URL slug. 8 + 9 + ## Adding New Documentation 10 + 11 + 1. Create a new `.md` or `.mdx` file in the content directory 12 + 2. The file will automatically be picked up by the docs site 13 + 3. Add a navigation entry in `apps/docs/src/config/navigation.config.ts` to make it appear in the sidebar 14 + 15 + ## File Naming 16 + 17 + - Use kebab-case for file names (e.g., `docker-strategy.md`) 18 + - The file name becomes the URL slug (e.g., `/docs/docker-strategy`) 19 + - Avoid special characters and spaces in file names 20 + 21 + ## Auto-Discovery 22 + 23 + The docs site uses Vite's `import.meta.glob` to automatically discover all markdown files in this directory. No manual imports or configuration needed!
+109
apps/docs/content/api/README.md
··· 1 + # API Documentation 2 + 3 + This section contains comprehensive documentation for the CV Generator API. 4 + 5 + ## Overview 6 + 7 + The CV Generator API is built with NestJS and GraphQL, providing a type-safe and efficient way to interact with the application's data. 8 + 9 + ## API Structure 10 + 11 + ### GraphQL Endpoint 12 + 13 + - **URL**: `/graphql` 14 + - **Method**: POST 15 + - **Content-Type**: `application/json` 16 + 17 + ### Authentication 18 + 19 + The API uses JWT-based authentication. Include the token in the `Authorization` header: 20 + 21 + ``` 22 + Authorization: Bearer <your-jwt-token> 23 + ``` 24 + 25 + ## Core Modules 26 + 27 + ### Authentication 28 + 29 + - **Login** - User authentication 30 + - **Register** - User registration 31 + - **Me** - Current user information 32 + 33 + ### Job Experience 34 + 35 + - **Companies** - Company management 36 + - **Roles** - Job role management 37 + - **Levels** - Experience level management 38 + - **Skills** - Skill management 39 + - **User Job Experience** - User's employment history 40 + 41 + ### Organizations 42 + 43 + - **Organization Management** - Organization CRUD operations 44 + - **User-Organization Relationships** - Membership management 45 + 46 + ### Vacancies 47 + 48 + - **Vacancy Management** - Job posting management 49 + 50 + ## Pagination 51 + 52 + The API supports cursor-based pagination for efficient data loading: 53 + 54 + ```graphql 55 + query Skills($first: Int, $after: String, $searchTerm: String) { 56 + skills(first: $first, after: $after, searchTerm: $searchTerm) { 57 + edges { 58 + cursor 59 + node { 60 + id 61 + name 62 + description 63 + } 64 + } 65 + pageInfo { 66 + hasNextPage 67 + hasPreviousPage 68 + startCursor 69 + endCursor 70 + } 71 + totalCount 72 + } 73 + } 74 + ``` 75 + 76 + ## Error Handling 77 + 78 + The API returns structured error responses: 79 + 80 + ```json 81 + { 82 + "errors": [ 83 + { 84 + "message": "Error description", 85 + "extensions": { 86 + "code": "ERROR_CODE", 87 + "exception": { 88 + "stacktrace": ["..."] 89 + } 90 + } 91 + } 92 + ] 93 + } 94 + ``` 95 + 96 + ## Health Check 97 + 98 + Monitor API health with the health endpoint: 99 + 100 + ```graphql 101 + query Health { 102 + health { 103 + status 104 + timestamp 105 + timezone 106 + uptime 107 + } 108 + } 109 + ```
+50
apps/docs/content/components/README.md
··· 1 + # Components 2 + 3 + This section contains documentation for the UI components used in the CV Generator application. 4 + 5 + ## Overview 6 + 7 + The component library is built with React and TypeScript, using Tailwind CSS for styling and Class Variance Authority (CVA) for dynamic styling configurations. 8 + 9 + ## Components 10 + 11 + All components are listed alphabetically: 12 + 13 + - [**Badge**](/components/badge) - Status and category indicators 14 + - [**Button**](/components/button) - Primary, secondary, and ghost button variants 15 + - [**Calendar**](/components/calendar) - Date picker component 16 + - [**Card**](/components/card) - Card container component 17 + - [**Checkbox**](/components/checkbox) - Toggle input component 18 + - [**FormattedDate**](/components/formatteddate) - Formatted date display component 19 + - [**FormattedDateRange**](/components/formatteddaterange) - Formatted date range display component 20 + - [**IconButton**](/components/iconbutton) - Icon button component 21 + - [**PageHeader**](/components/pageheader) - Page header with title and actions 22 + - [**Placeholder**](/components/placeholder) - Empty state and loading placeholders 23 + - [**RangeSlider**](/components/rangeslider) - Range slider input component 24 + - [**SearchableSelect**](/components/searchableselect) - Searchable dropdown with infinite scroll support 25 + - [**Select**](/components/select) - Dropdown select component 26 + - [**StatusBadge**](/components/statusbadge) - Status badge component 27 + - [**Table**](/components/table) - Data table with sorting and pagination 28 + - [**Textarea**](/components/textarea) - Multi-line text input 29 + - [**TextInput**](/components/textinput) - Text input with validation support 30 + 31 + ## Design System 32 + 33 + All components follow a consistent design system based on the Catppuccin color palette, ensuring a cohesive user experience across the application. 34 + 35 + ## Usage 36 + 37 + Components are exported from the `@cv/ui` package and can be imported as follows: 38 + 39 + ```typescript 40 + import { Button, TextInput, Table } from "@cv/ui"; 41 + ``` 42 + 43 + ## Styling 44 + 45 + Components use Tailwind CSS utility classes with CVA for dynamic styling. The design system includes: 46 + 47 + - Consistent spacing and typography 48 + - Catppuccin color palette integration 49 + - Responsive design patterns 50 + - Accessibility features
+299
apps/docs/content/components/badge.mdx
··· 1 + import { Badge } from "@cv/ui"; 2 + import { ComponentExample } from "@/components/ComponentExample"; 3 + 4 + # Badge 5 + 6 + A versatile badge component for displaying status, categories, and labels with multiple color variants. 7 + 8 + ## Features 9 + 10 + - **Multiple colors**: 16 Catppuccin color variants 11 + - **Consistent styling**: Rounded design with proper padding 12 + - **Accessibility**: Proper semantic HTML 13 + - **CVA Integration**: Dynamic styling with color variants 14 + - **Flexible content**: Supports any React content 15 + 16 + ## Basic Usage 17 + 18 + ```tsx 19 + <Badge>Default Badge</Badge> 20 + ``` 21 + 22 + <ComponentExample className="my-4"> 23 + <Badge>Default Badge</Badge> 24 + </ComponentExample> 25 + 26 + ## Color Variants 27 + 28 + ### Primary Colors 29 + 30 + ```tsx 31 + <Badge color="ctp-blue">Blue Badge</Badge> 32 + <Badge color="ctp-green">Green Badge</Badge> 33 + <Badge color="ctp-red">Red Badge</Badge> 34 + <Badge color="ctp-yellow">Yellow Badge</Badge> 35 + ``` 36 + 37 + <ComponentExample className="my-4 flex gap-2 flex-wrap"> 38 + <Badge color="ctp-blue">Blue Badge</Badge> 39 + <Badge color="ctp-green">Green Badge</Badge> 40 + <Badge color="ctp-red">Red Badge</Badge> 41 + <Badge color="ctp-yellow">Yellow Badge</Badge> 42 + </ComponentExample> 43 + 44 + ### Secondary Colors 45 + 46 + ```tsx 47 + <Badge color="ctp-orange">Orange Badge</Badge> 48 + <Badge color="ctp-teal">Teal Badge</Badge> 49 + <Badge color="ctp-sky">Sky Badge</Badge> 50 + <Badge color="ctp-sapphire">Sapphire Badge</Badge> 51 + ``` 52 + 53 + <ComponentExample className="my-4 flex gap-2 flex-wrap"> 54 + <Badge color="ctp-orange">Orange Badge</Badge> 55 + <Badge color="ctp-teal">Teal Badge</Badge> 56 + <Badge color="ctp-sky">Sky Badge</Badge> 57 + <Badge color="ctp-sapphire">Sapphire Badge</Badge> 58 + </ComponentExample> 59 + 60 + ### Accent Colors 61 + 62 + ```tsx 63 + <Badge color="ctp-lavender">Lavender Badge</Badge> 64 + <Badge color="ctp-mauve">Mauve Badge</Badge> 65 + <Badge color="ctp-pink">Pink Badge</Badge> 66 + <Badge color="ctp-maroon">Maroon Badge</Badge> 67 + ``` 68 + 69 + <ComponentExample className="my-4 flex gap-2 flex-wrap"> 70 + <Badge color="ctp-lavender">Lavender Badge</Badge> 71 + <Badge color="ctp-mauve">Mauve Badge</Badge> 72 + <Badge color="ctp-pink">Pink Badge</Badge> 73 + <Badge color="ctp-maroon">Maroon Badge</Badge> 74 + </ComponentExample> 75 + 76 + ### Neutral Colors 77 + 78 + ```tsx 79 + <Badge color="ctp-peach">Peach Badge</Badge> 80 + <Badge color="ctp-rosewater">Rosewater Badge</Badge> 81 + <Badge color="ctp-gray">Gray Badge</Badge> 82 + ``` 83 + 84 + <ComponentExample className="my-4 flex gap-2 flex-wrap"> 85 + <Badge color="ctp-peach">Peach Badge</Badge> 86 + <Badge color="ctp-rosewater">Rosewater Badge</Badge> 87 + <Badge color="ctp-gray">Gray Badge</Badge> 88 + </ComponentExample> 89 + 90 + ## Props 91 + 92 + | Prop | Type | Default | Description | 93 + | ----------- | ----------------- | ------------ | ---------------------- | 94 + | `children` | `React.ReactNode` | - | Badge content | 95 + | `color` | `BadgeColor` | `"ctp-blue"` | Badge color variant | 96 + | `className` | `string` | `""` | Additional CSS classes | 97 + 98 + ## BadgeColor Type 99 + 100 + ```typescript 101 + type BadgeColor = 102 + | "ctp-red" 103 + | "ctp-orange" 104 + | "ctp-yellow" 105 + | "ctp-green" 106 + | "ctp-teal" 107 + | "ctp-sky" 108 + | "ctp-sapphire" 109 + | "ctp-blue" 110 + | "ctp-lavender" 111 + | "ctp-mauve" 112 + | "ctp-pink" 113 + | "ctp-maroon" 114 + | "ctp-peach" 115 + | "ctp-rosewater" 116 + | "ctp-gray"; 117 + ``` 118 + 119 + ## Examples 120 + 121 + ### Status Badges 122 + 123 + ```tsx 124 + <Badge color="ctp-green">Active</Badge> 125 + <Badge color="ctp-red">Inactive</Badge> 126 + <Badge color="ctp-yellow">Pending</Badge> 127 + <Badge color="ctp-blue">Processing</Badge> 128 + ``` 129 + 130 + ### Category Badges 131 + 132 + ```tsx 133 + <Badge color="ctp-purple">Technology</Badge> 134 + <Badge color="ctp-teal">Design</Badge> 135 + <Badge color="ctp-orange">Marketing</Badge> 136 + <Badge color="ctp-pink">Sales</Badge> 137 + ``` 138 + 139 + ### Priority Badges 140 + 141 + ```tsx 142 + <Badge color="ctp-red">High Priority</Badge> 143 + <Badge color="ctp-yellow">Medium Priority</Badge> 144 + <Badge color="ctp-green">Low Priority</Badge> 145 + ``` 146 + 147 + ### Custom Content 148 + 149 + ```tsx 150 + <Badge color="ctp-blue"> 151 + <span className="flex items-center gap-1"> 152 + <CheckIcon className="w-3 h-3" /> 153 + Verified 154 + </span> 155 + </Badge> 156 + ``` 157 + 158 + ### With Custom Styling 159 + 160 + ```tsx 161 + <Badge color="ctp-green" className="font-bold uppercase tracking-wide"> 162 + New 163 + </Badge> 164 + ``` 165 + 166 + ## Use Cases 167 + 168 + ### User Status 169 + 170 + ```tsx 171 + const UserStatus = ({ 172 + status, 173 + }: { 174 + status: "active" | "inactive" | "pending"; 175 + }) => { 176 + const statusConfig = { 177 + active: { color: "ctp-green", label: "Active" }, 178 + inactive: { color: "ctp-red", label: "Inactive" }, 179 + pending: { color: "ctp-yellow", label: "Pending" }, 180 + }; 181 + 182 + const config = statusConfig[status]; 183 + 184 + return <Badge color={config.color}>{config.label}</Badge>; 185 + }; 186 + ``` 187 + 188 + ### Task Priority 189 + 190 + ```tsx 191 + const TaskPriority = ({ priority }: { priority: number }) => { 192 + const priorityConfig = { 193 + 1: { color: "ctp-red", label: "Critical" }, 194 + 2: { color: "ctp-orange", label: "High" }, 195 + 3: { color: "ctp-yellow", label: "Medium" }, 196 + 4: { color: "ctp-green", label: "Low" }, 197 + }; 198 + 199 + const config = priorityConfig[priority] || priorityConfig[4]; 200 + 201 + return <Badge color={config.color}>{config.label}</Badge>; 202 + }; 203 + ``` 204 + 205 + ### Feature Flags 206 + 207 + ```tsx 208 + const FeatureBadge = ({ enabled }: { enabled: boolean }) => ( 209 + <Badge color={enabled ? "ctp-green" : "ctp-gray"}> 210 + {enabled ? "Enabled" : "Disabled"} 211 + </Badge> 212 + ); 213 + ``` 214 + 215 + ## Accessibility 216 + 217 + The Badge component includes proper accessibility features: 218 + 219 + - **Semantic HTML**: Uses `<span>` element for inline display 220 + - **Screen readers**: Content is properly announced 221 + - **Color contrast**: All color variants meet accessibility standards 222 + - **Focus management**: Inherits parent focus behavior 223 + 224 + ## Styling 225 + 226 + The component uses Tailwind CSS with Catppuccin color palette: 227 + 228 + - **Base styles**: `inline-flex items-center rounded-full px-3 py-1 text-xs font-medium` 229 + - **Color variants**: Each color uses `bg-{color}/20 text-{color}` pattern 230 + - **Consistent spacing**: `px-3 py-1` for all badges 231 + - **Typography**: `text-xs font-medium` for readability 232 + 233 + ## CVA Configuration 234 + 235 + The badge variants are defined using Class Variance Authority: 236 + 237 + ```typescript 238 + const badgeVariants = cva( 239 + "inline-flex items-center rounded-full px-3 py-1 text-xs font-medium", 240 + { 241 + variants: { 242 + color: { 243 + "ctp-red": "bg-ctp-red/20 text-ctp-red", 244 + "ctp-green": "bg-ctp-green/20 text-ctp-green", 245 + // ... other colors 246 + }, 247 + }, 248 + defaultVariants: { 249 + color: "ctp-blue", 250 + }, 251 + }, 252 + ); 253 + ``` 254 + 255 + ## Color System 256 + 257 + The badge uses a consistent color system: 258 + 259 + - **Background**: 20% opacity of the color (`bg-{color}/20`) 260 + - **Text**: Full color intensity (`text-{color}`) 261 + - **Contrast**: Ensures good readability on all backgrounds 262 + - **Consistency**: All colors follow the same pattern 263 + 264 + ## Integration Examples 265 + 266 + ### With Tables 267 + 268 + ```tsx 269 + <TableCell> 270 + <Badge color={user.active ? "ctp-green" : "ctp-red"}> 271 + {user.active ? "Active" : "Inactive"} 272 + </Badge> 273 + </TableCell> 274 + ``` 275 + 276 + ### With Cards 277 + 278 + ```tsx 279 + <div className="flex items-center gap-2"> 280 + <h3>Project Title</h3> 281 + <Badge color="ctp-blue">In Progress</Badge> 282 + </div> 283 + ``` 284 + 285 + ### With Lists 286 + 287 + ```tsx 288 + <li className="flex items-center justify-between"> 289 + <span>Task Name</span> 290 + <Badge color="ctp-green">Completed</Badge> 291 + </li> 292 + ``` 293 + 294 + ## Performance Considerations 295 + 296 + - **Lightweight**: Minimal DOM footprint 297 + - **No animations**: Static component for better performance 298 + - **Efficient rendering**: Simple component structure 299 + - **Memory usage**: Minimal memory footprint
+173
apps/docs/content/components/button.md
··· 1 + # Button 2 + 3 + A versatile button component with multiple variants and sizes, built with Class Variance Authority (CVA) for consistent styling. 4 + 5 + ## Features 6 + 7 + - **Multiple variants**: Primary, secondary, outline, ghost, and destructive 8 + - **Three sizes**: Small, medium, and large 9 + - **Accessibility**: Focus states and disabled states 10 + - **TypeScript**: Fully typed with proper prop interfaces 11 + - **CVA Integration**: Dynamic styling with variant props 12 + 13 + ## Variants 14 + 15 + ### Primary 16 + 17 + The default button style with blue background. 18 + 19 + ```tsx 20 + <Button variant="primary">Primary Button</Button> 21 + ``` 22 + 23 + <ComponentExample className="my-4"> 24 + <Button variant="primary">Primary Button</Button> 25 + </ComponentExample> 26 + 27 + ### Secondary 28 + 29 + A subtle button with surface background. 30 + 31 + ```tsx 32 + <Button variant="secondary">Secondary Button</Button> 33 + ``` 34 + 35 + <ComponentExample className="my-4"> 36 + <Button variant="secondary">Secondary Button</Button> 37 + </ComponentExample> 38 + 39 + ### Outline 40 + 41 + A button with border and transparent background. 42 + 43 + ```tsx 44 + <Button variant="outline">Outline Button</Button> 45 + ``` 46 + 47 + <ComponentExample className="my-4"> 48 + <Button variant="outline">Outline Button</Button> 49 + </ComponentExample> 50 + 51 + ### Ghost 52 + 53 + A minimal button with no background. 54 + 55 + ```tsx 56 + <Button variant="ghost">Ghost Button</Button> 57 + ``` 58 + 59 + ### Destructive 60 + 61 + A button for destructive actions with red styling. 62 + 63 + ```tsx 64 + <Button variant="destructive">Delete</Button> 65 + ``` 66 + 67 + ## Sizes 68 + 69 + ### Small 70 + 71 + ```tsx 72 + <Button size="sm">Small Button</Button> 73 + ``` 74 + 75 + ### Medium (Default) 76 + 77 + ```tsx 78 + <Button size="md">Medium Button</Button> 79 + ``` 80 + 81 + ### Large 82 + 83 + ```tsx 84 + <Button size="lg">Large Button</Button> 85 + ``` 86 + 87 + ## Props 88 + 89 + | Prop | Type | Default | Description | 90 + | ----------- | ------------------------------------------------------------------- | ----------- | ---------------------- | 91 + | `children` | `React.ReactNode` | - | Button content | 92 + | `variant` | `"primary" \| "secondary" \| "outline" \| "ghost" \| "destructive"` | `"primary"` | Button style variant | 93 + | `size` | `"sm" \| "md" \| "lg"` | `"md"` | Button size | 94 + | `onClick` | `() => void` | - | Click handler | 95 + | `disabled` | `boolean` | `false` | Disabled state | 96 + | `type` | `"button" \| "submit" \| "reset"` | `"button"` | HTML button type | 97 + | `className` | `string` | `""` | Additional CSS classes | 98 + 99 + ## Examples 100 + 101 + ### Form Submit Button 102 + 103 + ```tsx 104 + <Button type="submit" variant="primary"> 105 + Submit Form 106 + </Button> 107 + ``` 108 + 109 + ### Disabled Button 110 + 111 + ```tsx 112 + <Button disabled variant="secondary"> 113 + Disabled Button 114 + </Button> 115 + ``` 116 + 117 + ### Custom Styling 118 + 119 + ```tsx 120 + <Button variant="outline" size="lg" className="w-full"> 121 + Full Width Button 122 + </Button> 123 + ``` 124 + 125 + ### Event Handling 126 + 127 + ```tsx 128 + <Button variant="destructive" onClick={() => handleDelete()}> 129 + Delete Item 130 + </Button> 131 + ``` 132 + 133 + ## Accessibility 134 + 135 + The Button component includes proper accessibility features: 136 + 137 + - **Focus states**: Visible focus ring with `focus-visible:ring-2` 138 + - **Disabled states**: Proper disabled styling and pointer events 139 + - **Keyboard navigation**: Full keyboard support 140 + - **Screen readers**: Proper semantic HTML button element 141 + 142 + ## Styling 143 + 144 + The component uses Tailwind CSS with Catppuccin color palette: 145 + 146 + - **Primary**: `bg-ctp-blue` with hover states 147 + - **Secondary**: `bg-ctp-surface1` with subtle hover effects 148 + - **Outline**: Border with `border-ctp-surface1` 149 + - **Ghost**: Transparent with hover background 150 + - **Destructive**: `bg-ctp-red` for dangerous actions 151 + 152 + ## CVA Configuration 153 + 154 + The button variants are defined using Class Variance Authority: 155 + 156 + ```typescript 157 + const buttonVariants = cva("base-styles", { 158 + variants: { 159 + variant: { 160 + /* variant styles */ 161 + }, 162 + size: { 163 + /* size styles */ 164 + }, 165 + }, 166 + defaultVariants: { 167 + variant: "primary", 168 + size: "md", 169 + }, 170 + }); 171 + ``` 172 + 173 + This ensures consistent styling and easy customization across the application.
+176
apps/docs/content/components/button.mdx
··· 1 + import { Button } from "@cv/ui"; 2 + import { ComponentExample } from "@/components/ComponentExample"; 3 + 4 + # Button 5 + 6 + A versatile button component with multiple variants and sizes, built with Class Variance Authority (CVA) for consistent styling. 7 + 8 + ## Features 9 + 10 + - **Multiple variants**: Primary, secondary, outline, ghost, and destructive 11 + - **Three sizes**: Small, medium, and large 12 + - **Accessibility**: Focus states and disabled states 13 + - **TypeScript**: Fully typed with proper prop interfaces 14 + - **CVA Integration**: Dynamic styling with variant props 15 + 16 + ## Variants 17 + 18 + ### Primary 19 + 20 + The default button style with blue background. 21 + 22 + ```tsx 23 + <Button variant="primary">Primary Button</Button> 24 + ``` 25 + 26 + <ComponentExample className="my-4"> 27 + <Button variant="primary">Primary Button</Button> 28 + </ComponentExample> 29 + 30 + ### Secondary 31 + 32 + A subtle button with surface background. 33 + 34 + ```tsx 35 + <Button variant="secondary">Secondary Button</Button> 36 + ``` 37 + 38 + <ComponentExample className="my-4"> 39 + <Button variant="secondary">Secondary Button</Button> 40 + </ComponentExample> 41 + 42 + ### Outline 43 + 44 + A button with border and transparent background. 45 + 46 + ```tsx 47 + <Button variant="outline">Outline Button</Button> 48 + ``` 49 + 50 + <ComponentExample className="my-4"> 51 + <Button variant="outline">Outline Button</Button> 52 + </ComponentExample> 53 + 54 + ### Ghost 55 + 56 + A minimal button with no background. 57 + 58 + ```tsx 59 + <Button variant="ghost">Ghost Button</Button> 60 + ``` 61 + 62 + ### Destructive 63 + 64 + A button for destructive actions with red styling. 65 + 66 + ```tsx 67 + <Button variant="destructive">Delete</Button> 68 + ``` 69 + 70 + ## Sizes 71 + 72 + ### Small 73 + 74 + ```tsx 75 + <Button size="sm">Small Button</Button> 76 + ``` 77 + 78 + ### Medium (Default) 79 + 80 + ```tsx 81 + <Button size="md">Medium Button</Button> 82 + ``` 83 + 84 + ### Large 85 + 86 + ```tsx 87 + <Button size="lg">Large Button</Button> 88 + ``` 89 + 90 + ## Props 91 + 92 + | Prop | Type | Default | Description | 93 + | ----------- | ------------------------------------------------------------------- | ----------- | ---------------------- | 94 + | `children` | `React.ReactNode` | - | Button content | 95 + | `variant` | `"primary" \| "secondary" \| "outline" \| "ghost" \| "destructive"` | `"primary"` | Button style variant | 96 + | `size` | `"sm" \| "md" \| "lg"` | `"md"` | Button size | 97 + | `onClick` | `() => void` | - | Click handler | 98 + | `disabled` | `boolean` | `false` | Disabled state | 99 + | `type` | `"button" \| "submit" \| "reset"` | `"button"` | HTML button type | 100 + | `className` | `string` | `""` | Additional CSS classes | 101 + 102 + ## Examples 103 + 104 + ### Form Submit Button 105 + 106 + ```tsx 107 + <Button type="submit" variant="primary"> 108 + Submit Form 109 + </Button> 110 + ``` 111 + 112 + ### Disabled Button 113 + 114 + ```tsx 115 + <Button disabled variant="secondary"> 116 + Disabled Button 117 + </Button> 118 + ``` 119 + 120 + ### Custom Styling 121 + 122 + ```tsx 123 + <Button variant="outline" size="lg" className="w-full"> 124 + Full Width Button 125 + </Button> 126 + ``` 127 + 128 + ### Event Handling 129 + 130 + ```tsx 131 + <Button variant="destructive" onClick={() => handleDelete()}> 132 + Delete Item 133 + </Button> 134 + ``` 135 + 136 + ## Accessibility 137 + 138 + The Button component includes proper accessibility features: 139 + 140 + - **Focus states**: Visible focus ring with `focus-visible:ring-2` 141 + - **Disabled states**: Proper disabled styling and pointer events 142 + - **Keyboard navigation**: Full keyboard support 143 + - **Screen readers**: Proper semantic HTML button element 144 + 145 + ## Styling 146 + 147 + The component uses Tailwind CSS with Catppuccin color palette: 148 + 149 + - **Primary**: `bg-ctp-blue` with hover states 150 + - **Secondary**: `bg-ctp-surface1` with subtle hover effects 151 + - **Outline**: Border with `border-ctp-surface1` 152 + - **Ghost**: Transparent with hover background 153 + - **Destructive**: `bg-ctp-red` for dangerous actions 154 + 155 + ## CVA Configuration 156 + 157 + The button variants are defined using Class Variance Authority: 158 + 159 + ```typescript 160 + const buttonVariants = cva("base-styles", { 161 + variants: { 162 + variant: { 163 + /* variant styles */ 164 + }, 165 + size: { 166 + /* size styles */ 167 + }, 168 + }, 169 + defaultVariants: { 170 + variant: "primary", 171 + size: "md", 172 + }, 173 + }); 174 + ``` 175 + 176 + This ensures consistent styling and easy customization across the application.
+96
apps/docs/content/components/calendar.mdx
··· 1 + import { Calendar } from "@cv/ui"; 2 + import { 3 + ComponentExample, 4 + ExampleStateProvider, 5 + } from "@/components/ComponentExample"; 6 + 7 + # Calendar 8 + 9 + A date picker component with calendar popup. 10 + 11 + ## Features 12 + 13 + - **Date selection**: Interactive calendar interface 14 + - **Date formatting**: Multiple format options (short, long) 15 + - **Date constraints**: Min and max date support 16 + - **Keyboard navigation**: Full keyboard support 17 + - **Accessibility**: ARIA labels and focus management 18 + 19 + ## Basic Usage 20 + 21 + ```tsx 22 + import { Calendar } from "@cv/ui"; 23 + 24 + function MyForm() { 25 + const [date, setDate] = useState<Date | null>(null); 26 + 27 + return ( 28 + <Calendar value={date} onChange={setDate} placeholder="Select a date" /> 29 + ); 30 + } 31 + ``` 32 + 33 + <ComponentExample className="my-4"> 34 + <ExampleStateProvider initialValue={null}> 35 + {(date, setDate) => ( 36 + <Calendar value={date} onChange={setDate} placeholder="Select a date" /> 37 + )} 38 + </ExampleStateProvider> 39 + </ComponentExample> 40 + 41 + ## Date Formatting 42 + 43 + ### Short Format (default) 44 + 45 + ```tsx 46 + <Calendar value={date} onChange={setDate} format="short" /> 47 + // Displays: "Jan 1, 2025" 48 + ``` 49 + 50 + ### Long Format 51 + 52 + ```tsx 53 + <Calendar value={date} onChange={setDate} format="long" /> 54 + // Displays: "Wednesday, January 1, 2025" 55 + ``` 56 + 57 + ## Date Constraints 58 + 59 + ```tsx 60 + <Calendar 61 + value={date} 62 + onChange={setDate} 63 + minDate={new Date()} // Only allow future dates 64 + maxDate={new Date(2026, 0, 1)} // Up to Jan 1, 2026 65 + /> 66 + ``` 67 + 68 + ## Disabled State 69 + 70 + ```tsx 71 + <Calendar value={date} onChange={setDate} disabled /> 72 + ``` 73 + 74 + <ComponentExample className="my-4"> 75 + <Calendar disabled placeholder="Disabled calendar" onChange={() => {}} /> 76 + </ComponentExample> 77 + 78 + ## Props 79 + 80 + | Prop | Type | Default | Description | 81 + | ------------- | ------------------------------ | --------------- | -------------------------------- | 82 + | `value` | `Date \| null` | `undefined` | Selected date value | 83 + | `onChange` | `(date: Date \| null) => void` | **required** | Callback when date changes | 84 + | `placeholder` | `string` | `"Select date"` | Placeholder text | 85 + | `disabled` | `boolean` | `false` | Whether the calendar is disabled | 86 + | `className` | `string` | `undefined` | Additional CSS classes | 87 + | `minDate` | `Date` | `undefined` | Minimum selectable date | 88 + | `maxDate` | `Date` | `undefined` | Maximum selectable date | 89 + | `format` | `"short" \| "long"` | `"short"` | Date format option | 90 + 91 + ## Keyboard Navigation 92 + 93 + - **Arrow keys**: Navigate between dates 94 + - **Enter/Space**: Select date 95 + - **Escape**: Close calendar 96 + - **Tab**: Navigate between controls
+69
apps/docs/content/components/card.mdx
··· 1 + import { ComponentExample } from "@/components/ComponentExample"; 2 + 3 + # Card 4 + 5 + A container component for grouping related content with consistent styling. 6 + 7 + ## Features 8 + 9 + - **Clickable**: Optional onClick handler for interactive cards 10 + - **Consistent styling**: Border, shadow, and padding 11 + - **Hover effects**: Visual feedback on clickable cards 12 + - **Flexible content**: Accepts any children 13 + 14 + ## Basic Usage 15 + 16 + ```tsx 17 + import { Card } from "@cv/ui"; 18 + 19 + function MyCard() { 20 + return ( 21 + <Card> 22 + <h3>Card Title</h3> 23 + <p>Card content goes here</p> 24 + </Card> 25 + ); 26 + } 27 + ``` 28 + 29 + <ComponentExample className="my-4"> 30 + <div className="p-4 bg-ctp-base border border-ctp-surface0 rounded-lg"> 31 + <h3 className="text-lg font-semibold mb-2">Card Title</h3> 32 + <p className="text-ctp-text">Card content goes here</p> 33 + </div> 34 + </ComponentExample> 35 + 36 + ## Clickable Card 37 + 38 + ```tsx 39 + <Card onClick={() => console.log("Card clicked")}> 40 + <h3>Clickable Card</h3> 41 + <p>This card has a click handler</p> 42 + </Card> 43 + ``` 44 + 45 + ## Custom Styling 46 + 47 + ```tsx 48 + <Card className="bg-ctp-mantle border-ctp-blue"> 49 + <p>Custom styled card</p> 50 + </Card> 51 + ``` 52 + 53 + ## Props 54 + 55 + | Prop | Type | Default | Description | 56 + | ----------- | ----------------- | ------------ | ---------------------- | 57 + | `children` | `React.ReactNode` | **required** | Card content | 58 + | `className` | `string` | `""` | Additional CSS classes | 59 + | `onClick` | `() => void` | `undefined` | Optional click handler | 60 + 61 + ## Styling 62 + 63 + Default styling: 64 + 65 + - Border: `ctp-surface1` 66 + - Background: `ctp-surface0` 67 + - Padding: `p-4` 68 + - Border radius: `rounded-lg` 69 + - Shadow: `shadow-sm` (increases to `shadow-md` on hover if clickable)
+69
apps/docs/content/components/checkbox.mdx
··· 1 + import { Checkbox } from "@cv/ui"; 2 + import { 3 + ComponentExample, 4 + ExampleStateProvider, 5 + } from "@/components/ComponentExample"; 6 + 7 + # Checkbox 8 + 9 + A checkbox input component with optional label and consistent styling. 10 + 11 + ## Features 12 + 13 + - **Optional label**: Can be used with or without a label 14 + - **Controlled/Uncontrolled**: Supports both controlled and uncontrolled usage 15 + - **Accessibility**: Proper label association and keyboard support 16 + - **Disabled state**: Visual feedback for disabled state 17 + - **TypeScript**: Fully typed props 18 + 19 + ## Basic Usage 20 + 21 + ```tsx 22 + import { Checkbox } from "@cv/ui"; 23 + 24 + function MyForm() { 25 + const [checked, setChecked] = useState(false); 26 + 27 + return ( 28 + <Checkbox 29 + label="Accept terms and conditions" 30 + checked={checked} 31 + onChange={setChecked} 32 + /> 33 + ); 34 + } 35 + ``` 36 + 37 + ## Without Label 38 + 39 + The checkbox can be used without a label for custom layouts: 40 + 41 + ```tsx 42 + <Checkbox checked={checked} onChange={setChecked} /> 43 + ``` 44 + 45 + ## Disabled State 46 + 47 + ```tsx 48 + <Checkbox label="Disabled checkbox" checked={true} disabled /> 49 + ``` 50 + 51 + ## Props 52 + 53 + | Prop | Type | Default | Description | 54 + | ----------- | ---------------------------- | ----------- | ----------------------------------------------------- | 55 + | `label` | `string` | `undefined` | Optional label text | 56 + | `checked` | `boolean` | `false` | Controlled checked state | 57 + | `onChange` | `(checked: boolean) => void` | `undefined` | Callback when checked state changes | 58 + | `disabled` | `boolean` | `false` | Whether the checkbox is disabled | 59 + | `className` | `string` | `""` | Additional CSS classes | 60 + | `id` | `string` | `undefined` | Custom ID (auto-generated from label if not provided) | 61 + 62 + ## Styling 63 + 64 + The checkbox uses Catppuccin color scheme: 65 + 66 + - Border: `ctp-surface1` 67 + - Focus ring: `ctp-blue` 68 + - Text: `ctp-text` 69 + - Disabled: 50% opacity
+89
apps/docs/content/components/formatteddate.mdx
··· 1 + import { FormattedDate } from "@cv/ui"; 2 + 3 + # FormattedDate 4 + 5 + A component for displaying formatted dates with customizable size and variant. 6 + 7 + ## Features 8 + 9 + - **Date formatting**: Formats ISO date strings to readable format 10 + - **Size variants**: Small, medium, and large text sizes 11 + - **Style variants**: Default, muted, and subtle text colors 12 + - **Tooltip**: Cursor help indicator for full date information 13 + - **CVA variants**: Dynamic styling with Class Variance Authority 14 + 15 + ## Basic Usage 16 + 17 + ```tsx 18 + import { FormattedDate } from "@cv/ui"; 19 + 20 + function MyComponent() { 21 + return <FormattedDate date="2025-01-15T10:30:00Z" />; 22 + } 23 + // Displays: "Jan 2025" 24 + ``` 25 + 26 + ## Size Variants 27 + 28 + ### Small 29 + 30 + ```tsx 31 + <FormattedDate date="2025-01-15" size="sm" /> 32 + ``` 33 + 34 + ### Medium (default) 35 + 36 + ```tsx 37 + <FormattedDate date="2025-01-15" size="md" /> 38 + ``` 39 + 40 + ### Large 41 + 42 + ```tsx 43 + <FormattedDate date="2025-01-15" size="lg" /> 44 + ``` 45 + 46 + ## Style Variants 47 + 48 + ### Default 49 + 50 + ```tsx 51 + <FormattedDate date="2025-01-15" variant="default" /> 52 + // Uses: text-ctp-text 53 + ``` 54 + 55 + ### Muted 56 + 57 + ```tsx 58 + <FormattedDate date="2025-01-15" variant="muted" /> 59 + // Uses: text-ctp-subtext0 60 + ``` 61 + 62 + ### Subtle 63 + 64 + ```tsx 65 + <FormattedDate date="2025-01-15" variant="subtle" /> 66 + // Uses: text-ctp-subtext1 67 + ``` 68 + 69 + ## Format 70 + 71 + The component formats dates as "Month Year" (e.g., "Jan 2025") using `toLocaleDateString` with: 72 + 73 + - Year: numeric 74 + - Month: short 75 + 76 + ## Props 77 + 78 + | Prop | Type | Default | Description | 79 + | ----------- | ---------------------------------- | ------------ | ------------------------- | 80 + | `date` | `string` | **required** | ISO date string to format | 81 + | `size` | `"sm" \| "md" \| "lg"` | `"md"` | Text size variant | 82 + | `variant` | `"default" \| "muted" \| "subtle"` | `"default"` | Text color variant | 83 + | `className` | `string` | `undefined` | Additional CSS classes | 84 + 85 + ## Styling 86 + 87 + - **Cursor**: `cursor-help` (indicates tooltip on hover) 88 + - **Size classes**: `text-sm`, `text-base`, `text-lg` 89 + - **Color classes**: `text-ctp-text`, `text-ctp-subtext0`, `text-ctp-subtext1`
+113
apps/docs/content/components/formatteddaterange.mdx
··· 1 + import { FormattedDateRange } from "@cv/ui"; 2 + 3 + # FormattedDateRange 4 + 5 + A component for displaying formatted date ranges with customizable size and variant. 6 + 7 + ## Features 8 + 9 + - **Date range formatting**: Formats start and end dates as a range 10 + - **Open-ended ranges**: Supports ongoing ranges (end date optional) 11 + - **Size variants**: Small, medium, and large text sizes 12 + - **Style variants**: Default, muted, and subtle text colors 13 + - **Tooltip**: Cursor help indicator for full date information 14 + - **CVA variants**: Dynamic styling with Class Variance Authority 15 + 16 + ## Basic Usage 17 + 18 + ```tsx 19 + import { FormattedDateRange } from "@cv/ui"; 20 + 21 + function MyComponent() { 22 + return ( 23 + <FormattedDateRange 24 + startDate="2024-01-15T00:00:00Z" 25 + endDate="2025-01-15T00:00:00Z" 26 + /> 27 + ); 28 + } 29 + // Displays: "Jan 2024 - Jan 2025" 30 + ``` 31 + 32 + ## Open-Ended Range 33 + 34 + ```tsx 35 + <FormattedDateRange 36 + startDate="2024-01-15T00:00:00Z" 37 + // endDate is optional for ongoing ranges 38 + /> 39 + // Displays: "Jan 2024 - Present" 40 + ``` 41 + 42 + ## Size Variants 43 + 44 + ### Small 45 + 46 + ```tsx 47 + <FormattedDateRange startDate="2024-01-15" endDate="2025-01-15" size="sm" /> 48 + ``` 49 + 50 + ### Medium (default) 51 + 52 + ```tsx 53 + <FormattedDateRange startDate="2024-01-15" endDate="2025-01-15" size="md" /> 54 + ``` 55 + 56 + ### Large 57 + 58 + ```tsx 59 + <FormattedDateRange startDate="2024-01-15" endDate="2025-01-15" size="lg" /> 60 + ``` 61 + 62 + ## Style Variants 63 + 64 + ### Default 65 + 66 + ```tsx 67 + <FormattedDateRange 68 + startDate="2024-01-15" 69 + endDate="2025-01-15" 70 + variant="default" 71 + /> 72 + ``` 73 + 74 + ### Muted 75 + 76 + ```tsx 77 + <FormattedDateRange 78 + startDate="2024-01-15" 79 + endDate="2025-01-15" 80 + variant="muted" 81 + /> 82 + ``` 83 + 84 + ### Subtle 85 + 86 + ```tsx 87 + <FormattedDateRange 88 + startDate="2024-01-15" 89 + endDate="2025-01-15" 90 + variant="subtle" 91 + /> 92 + ``` 93 + 94 + ## Format 95 + 96 + The component formats date ranges as "Month Year - Month Year" (e.g., "Jan 2024 - Jan 2025") or "Month Year - Present" for open-ended ranges. 97 + 98 + ## Props 99 + 100 + | Prop | Type | Default | Description | 101 + | ----------- | ---------------------------------- | ------------ | ------------------------------------- | 102 + | `startDate` | `string` | **required** | ISO date string for start date | 103 + | `endDate` | `string` | `undefined` | Optional ISO date string for end date | 104 + | `size` | `"sm" \| "md" \| "lg"` | `"md"` | Text size variant | 105 + | `variant` | `"default" \| "muted" \| "subtle"` | `"default"` | Text color variant | 106 + | `className` | `string` | `undefined` | Additional CSS classes | 107 + 108 + ## Styling 109 + 110 + - **Cursor**: `cursor-help` (indicates tooltip on hover) 111 + - **Size classes**: `text-sm`, `text-base`, `text-lg` 112 + - **Color classes**: `text-ctp-text`, `text-ctp-subtext0`, `text-ctp-subtext1` 113 + - **Separator**: " - " between dates, " - Present" for open-ended
+155
apps/docs/content/components/iconbutton.mdx
··· 1 + import { IconButton } from "@cv/ui"; 2 + 3 + # IconButton 4 + 5 + A button component optimized for icon-only interactions with multiple variants and sizes. 6 + 7 + ## Features 8 + 9 + - **Multiple variants**: Primary, secondary, outline, ghost, and destructive 10 + - **Four sizes**: Extra small, small, medium, and large 11 + - **Hover behavior**: Option to show color on hover or always 12 + - **Accessibility**: Focus states and disabled states 13 + - **TypeScript**: Fully typed with CVA variants 14 + 15 + ## Basic Usage 16 + 17 + ```tsx 18 + import { IconButton } from "@cv/ui"; 19 + import { DeleteIcon } from "@cv/ui"; 20 + 21 + function MyComponent() { 22 + return ( 23 + <IconButton onClick={() => console.log("Clicked")}> 24 + <DeleteIcon /> 25 + </IconButton> 26 + ); 27 + } 28 + ``` 29 + 30 + ## Variants 31 + 32 + ### Primary 33 + 34 + ```tsx 35 + <IconButton variant="primary"> 36 + <DeleteIcon /> 37 + </IconButton> 38 + ``` 39 + 40 + ### Secondary 41 + 42 + ```tsx 43 + <IconButton variant="secondary"> 44 + <EditIcon /> 45 + </IconButton> 46 + ``` 47 + 48 + ### Outline 49 + 50 + ```tsx 51 + <IconButton variant="outline"> 52 + <CloseIcon /> 53 + </IconButton> 54 + ``` 55 + 56 + ### Ghost 57 + 58 + ```tsx 59 + <IconButton variant="ghost"> 60 + <EditIcon /> 61 + </IconButton> 62 + ``` 63 + 64 + ### Destructive 65 + 66 + ```tsx 67 + <IconButton variant="destructive"> 68 + <DeleteIcon /> 69 + </IconButton> 70 + ``` 71 + 72 + ## Sizes 73 + 74 + ### Extra Small 75 + 76 + ```tsx 77 + <IconButton size="xs"> 78 + <CloseIcon /> 79 + </IconButton> 80 + ``` 81 + 82 + ### Small 83 + 84 + ```tsx 85 + <IconButton size="sm"> 86 + <EditIcon /> 87 + </IconButton> 88 + ``` 89 + 90 + ### Medium (default) 91 + 92 + ```tsx 93 + <IconButton size="md"> 94 + <DeleteIcon /> 95 + </IconButton> 96 + ``` 97 + 98 + ### Large 99 + 100 + ```tsx 101 + <IconButton size="lg"> 102 + <UploadIcon /> 103 + </IconButton> 104 + ``` 105 + 106 + ## Hover Behavior 107 + 108 + ### Show Color on Hover (default) 109 + 110 + Shows color background only when hovering: 111 + 112 + ```tsx 113 + <IconButton variant="primary" showColorOnHover={true}> 114 + <EditIcon /> 115 + </IconButton> 116 + ``` 117 + 118 + ### Always Show Color 119 + 120 + Shows color background by default: 121 + 122 + ```tsx 123 + <IconButton variant="primary" showColorOnHover={false}> 124 + <EditIcon /> 125 + </IconButton> 126 + ``` 127 + 128 + ## Disabled State 129 + 130 + ```tsx 131 + <IconButton disabled onClick={() => {}}> 132 + <DeleteIcon /> 133 + </IconButton> 134 + ``` 135 + 136 + ## Props 137 + 138 + | Prop | Type | Default | Description | 139 + | ------------------ | ------------------------------------------------------------------- | ------------ | ------------------------------ | 140 + | `children` | `React.ReactNode` | **required** | Icon component | 141 + | `onClick` | `() => void` | `undefined` | Click handler | 142 + | `disabled` | `boolean` | `false` | Whether the button is disabled | 143 + | `variant` | `"primary" \| "secondary" \| "outline" \| "ghost" \| "destructive"` | `"primary"` | Button variant | 144 + | `size` | `"xs" \| "sm" \| "md" \| "lg"` | `"md"` | Button size | 145 + | `showColorOnHover` | `boolean` | `true` | Show color on hover or always | 146 + | `className` | `string` | `""` | Additional CSS classes | 147 + | `type` | `"button" \| "submit" \| "reset"` | `"button"` | Button type | 148 + 149 + ## Compound Variants 150 + 151 + The component uses compound variants to combine `variant` and `showColorOnHover`: 152 + 153 + - Primary + showColorOnHover: Text with blue background on hover 154 + - Primary + !showColorOnHover: Always blue background 155 + - Similar patterns for all variants
+75
apps/docs/content/components/pageheader.mdx
··· 1 + import { PageHeader, Button } from "@cv/ui"; 2 + 3 + # PageHeader 4 + 5 + A page header component for consistent page titles, descriptions, and action buttons. 6 + 7 + ## Features 8 + 9 + - **Title and description**: Main title with optional description 10 + - **Action slot**: Optional action button area 11 + - **Consistent spacing**: Standardized margin and layout 12 + - **Flexible layout**: Responsive flex layout 13 + 14 + ## Basic Usage 15 + 16 + ```tsx 17 + import { PageHeader } from "@cv/ui"; 18 + import { Button } from "@cv/ui"; 19 + 20 + function MyPage() { 21 + return ( 22 + <PageHeader title="My Page" description="This is the page description" /> 23 + ); 24 + } 25 + ``` 26 + 27 + ## With Action Button 28 + 29 + ```tsx 30 + <PageHeader 31 + title="Users" 32 + description="Manage user accounts" 33 + action={ 34 + <Button onClick={() => console.log("Create user")}>Create User</Button> 35 + } 36 + /> 37 + ``` 38 + 39 + ## Title Only 40 + 41 + ```tsx 42 + <PageHeader title="Dashboard" /> 43 + ``` 44 + 45 + ## Custom Styling 46 + 47 + ```tsx 48 + <PageHeader 49 + title="Settings" 50 + description="Configure your preferences" 51 + className="mb-8" 52 + /> 53 + ``` 54 + 55 + ## Props 56 + 57 + | Prop | Type | Default | Description | 58 + | ------------- | ----------- | ------------ | -------------------------------------- | 59 + | `title` | `string` | **required** | Page title | 60 + | `description` | `string` | `undefined` | Optional description text | 61 + | `action` | `ReactNode` | `undefined` | Optional action element (e.g., button) | 62 + | `className` | `string` | `undefined` | Additional CSS classes | 63 + 64 + ## Layout 65 + 66 + The component uses a flex layout: 67 + 68 + - **Left side**: Title and description (stacked vertically) 69 + - **Right side**: Action element (if provided) 70 + - **Spacing**: `mb-6` default margin bottom 71 + 72 + ## Styling 73 + 74 + - Title: `text-2xl font-bold text-ctp-text` 75 + - Description: `text-ctp-subtext0 mt-1`
+100
apps/docs/content/components/placeholder.mdx
··· 1 + import { Placeholder } from "@cv/ui"; 2 + 3 + # Placeholder 4 + 5 + A placeholder component for displaying empty states, loading states, and error messages. 6 + 7 + ## Features 8 + 9 + - **Multiple variants**: Loading, error, and empty states 10 + - **Title and message**: Optional title and message text 11 + - **Custom content**: Support for custom children 12 + - **Centered layout**: Centered content with proper spacing 13 + - **CVA variants**: Dynamic styling based on variant 14 + 15 + ## Basic Usage 16 + 17 + ```tsx 18 + import { Placeholder } from "@cv/ui"; 19 + 20 + function MyComponent() { 21 + return ( 22 + <Placeholder 23 + variant="empty" 24 + title="No items" 25 + message="There are no items to display" 26 + /> 27 + ); 28 + } 29 + ``` 30 + 31 + ## Variants 32 + 33 + ### Empty State 34 + 35 + ```tsx 36 + <Placeholder 37 + variant="empty" 38 + title="No data" 39 + message="There is no data available" 40 + /> 41 + ``` 42 + 43 + ### Loading State 44 + 45 + ```tsx 46 + <Placeholder 47 + variant="loading" 48 + title="Loading..." 49 + message="Please wait while we load the data" 50 + /> 51 + ``` 52 + 53 + ### Error State 54 + 55 + ```tsx 56 + <Placeholder 57 + variant="error" 58 + title="Error" 59 + message="Something went wrong. Please try again." 60 + /> 61 + ``` 62 + 63 + ## With Custom Content 64 + 65 + ```tsx 66 + <Placeholder variant="empty" title="No results"> 67 + <Button onClick={handleRetry}>Try Again</Button> 68 + </Placeholder> 69 + ``` 70 + 71 + ## Title Only 72 + 73 + ```tsx 74 + <Placeholder variant="loading" title="Loading..." /> 75 + ``` 76 + 77 + ## Message Only 78 + 79 + ```tsx 80 + <Placeholder variant="empty" message="No items found" /> 81 + ``` 82 + 83 + ## Props 84 + 85 + | Prop | Type | Default | Description | 86 + | ----------- | --------------------------------- | ----------- | ---------------------- | 87 + | `variant` | `"loading" \| "error" \| "empty"` | `"loading"` | Placeholder variant | 88 + | `title` | `string` | `undefined` | Optional title text | 89 + | `message` | `string` | `undefined` | Optional message text | 90 + | `children` | `React.ReactNode` | `undefined` | Custom content | 91 + | `className` | `string` | `undefined` | Additional CSS classes | 92 + 93 + ## Styling 94 + 95 + - **Layout**: Centered flex column 96 + - **Loading variant**: `text-ctp-subtext0` 97 + - **Error variant**: `text-ctp-red` 98 + - **Empty variant**: `text-ctp-subtext0` 99 + - **Title**: `text-2xl font-bold text-ctp-text mb-2` 100 + - **Message**: `text-sm`
+115
apps/docs/content/components/rangeslider.mdx
··· 1 + import { RangeSlider } from "@cv/ui"; 2 + 3 + # RangeSlider 4 + 5 + A dual-handle range slider component for selecting numeric ranges with customizable formatting and labels. 6 + 7 + ## Features 8 + 9 + - **Dual handles**: Select min and max values 10 + - **Custom formatting**: Format displayed values (e.g., currency, percentages) 11 + - **Labels and values**: Optional labels and current value display 12 + - **Step control**: Configurable step value 13 + - **Keyboard support**: Full keyboard accessibility 14 + - **Mouse and touch**: Support for mouse and touch interactions 15 + 16 + ## Basic Usage 17 + 18 + ```tsx 19 + import { RangeSlider } from "@cv/ui"; 20 + 21 + function MyComponent() { 22 + const [range, setRange] = useState<[number, number]>([0, 100]); 23 + 24 + return <RangeSlider min={0} max={100} value={range} onChange={setRange} />; 25 + } 26 + ``` 27 + 28 + ## With Labels 29 + 30 + ```tsx 31 + <RangeSlider 32 + min={0} 33 + max={100000} 34 + value={[20000, 80000]} 35 + onChange={setRange} 36 + showLabels={true} 37 + label="Salary Range" 38 + /> 39 + ``` 40 + 41 + ## Custom Formatting 42 + 43 + Format values as currency: 44 + 45 + ```tsx 46 + <RangeSlider 47 + min={0} 48 + max={100000} 49 + value={[20000, 80000]} 50 + onChange={setRange} 51 + formatValue={(val) => `$${val.toLocaleString()}`} 52 + /> 53 + ``` 54 + 55 + ## With Step 56 + 57 + ```tsx 58 + <RangeSlider 59 + min={0} 60 + max={100} 61 + value={[20, 80]} 62 + onChange={setRange} 63 + step={5} // Increments of 5 64 + /> 65 + ``` 66 + 67 + ## Hide Values 68 + 69 + ```tsx 70 + <RangeSlider 71 + min={0} 72 + max={100} 73 + value={[20, 80]} 74 + onChange={setRange} 75 + showValues={false} 76 + /> 77 + ``` 78 + 79 + ## Disabled State 80 + 81 + ```tsx 82 + <RangeSlider min={0} max={100} value={[20, 80]} onChange={setRange} disabled /> 83 + ``` 84 + 85 + ## Props 86 + 87 + | Prop | Type | Default | Description | 88 + | ------------- | ----------------------------------- | ------------------------------- | --------------------------------- | 89 + | `min` | `number` | **required** | Minimum value | 90 + | `max` | `number` | **required** | Maximum value | 91 + | `value` | `[number, number]` | **required** | Current range as [min, max] tuple | 92 + | `onChange` | `(value: [number, number]) => void` | **required** | Callback when range changes | 93 + | `step` | `number` | `1` | Step increment | 94 + | `className` | `string` | `undefined` | Additional CSS classes | 95 + | `disabled` | `boolean` | `false` | Whether the slider is disabled | 96 + | `formatValue` | `(value: number) => string` | `(val) => val.toLocaleString()` | Value formatter function | 97 + | `showLabels` | `boolean` | `true` | Show min/max labels | 98 + | `showValues` | `boolean` | `true` | Show current values above handles | 99 + 100 + ## Interaction 101 + 102 + - **Mouse**: Click and drag handles or track 103 + - **Touch**: Touch and drag on mobile devices 104 + - **Keyboard**: Arrow keys to adjust values (when focused) 105 + - **Click track**: Jump to position on track click 106 + 107 + ## Styling 108 + 109 + Uses Catppuccin color scheme: 110 + 111 + - Track: Gray background 112 + - Active range: Blue highlight 113 + - Handles: Blue with hover state 114 + - Labels: `ctp-text` color 115 + - Values: Displayed above handles
+276
apps/docs/content/components/searchableselect.mdx
··· 1 + import { SearchableSelect } from "@cv/ui"; 2 + import { ComponentExample, ExampleStateProvider } from "@/components/ComponentExample"; 3 + 4 + # SearchableSelect 5 + 6 + An advanced select component with search functionality, infinite scroll support, and keyboard navigation. 7 + 8 + ## Features 9 + 10 + - **Search functionality**: Real-time filtering of options 11 + - **Infinite scroll**: Load more options as user scrolls 12 + - **Keyboard navigation**: Full keyboard support 13 + - **Loading states**: Visual feedback during data loading 14 + - **Accessibility**: Screen reader support 15 + - **Custom styling**: CVA-based dynamic styling 16 + 17 + ## Basic Usage 18 + 19 + ```tsx 20 + const options = [ 21 + { value: "1", label: "Option 1" }, 22 + { value: "2", label: "Option 2" }, 23 + { value: "3", label: "Option 3" }, 24 + ]; 25 + 26 + <SearchableSelect 27 + label="Select Option" 28 + options={options} 29 + value={selectedValue} 30 + onChange={setSelectedValue} 31 + placeholder="Search options..." 32 + />; 33 + ``` 34 + 35 + <ComponentExample className="my-4"> 36 + <ExampleStateProvider initialValue={null}> 37 + {(value, setValue) => { 38 + const options = [ 39 + { value: "1", label: "Option 1" }, 40 + { value: "2", label: "Option 2" }, 41 + { value: "3", label: "Option 3" }, 42 + ]; 43 + return ( 44 + <SearchableSelect 45 + label="Select Option" 46 + options={options} 47 + value={value} 48 + onChange={setValue} 49 + placeholder="Search options..." 50 + /> 51 + ); 52 + }} 53 + </ExampleStateProvider> 54 + </ComponentExample> 55 + 56 + ## With Infinite Scroll 57 + 58 + ```tsx 59 + <SearchableSelect 60 + label="Skills" 61 + options={skills} 62 + value={selectedSkill} 63 + onChange={setSelectedSkill} 64 + hasNextPage={hasNextPage} 65 + onLoadMore={loadMoreSkills} 66 + isLoading={isLoadingMore} 67 + placeholder="Search skills..." 68 + /> 69 + ``` 70 + 71 + ## Props 72 + 73 + | Prop | Type | Default | Description | 74 + | ------------- | -------------------------- | ------------- | ------------------------------------- | 75 + | `label` | `string` | - | Select label | 76 + | `placeholder` | `string` | `"Search..."` | Placeholder text | 77 + | `value` | `string` | - | Selected value | 78 + | `onChange` | `(value: string) => void` | - | Change handler | 79 + | `options` | `SearchableSelectOption[]` | `[]` | Available options | 80 + | `disabled` | `boolean` | `false` | Disabled state | 81 + | `error` | `string` | - | Error message | 82 + | `className` | `string` | `""` | Additional CSS classes | 83 + | `id` | `string` | - | Select ID (auto-generated from label) | 84 + | `required` | `boolean` | `false` | Required field | 85 + | `hasNextPage` | `boolean` | `false` | Whether more data is available | 86 + | `onLoadMore` | `() => void` | - | Load more data handler | 87 + | `isLoading` | `boolean` | `false` | Loading state for infinite scroll | 88 + 89 + ## SearchableSelectOption Interface 90 + 91 + ```typescript 92 + interface SearchableSelectOption { 93 + value: string; 94 + label: string; 95 + } 96 + ``` 97 + 98 + ## Examples 99 + 100 + ### Basic Select 101 + 102 + ```tsx 103 + const countries = [ 104 + { value: "us", label: "United States" }, 105 + { value: "ca", label: "Canada" }, 106 + { value: "uk", label: "United Kingdom" }, 107 + ]; 108 + 109 + <SearchableSelect 110 + label="Country" 111 + options={countries} 112 + value={country} 113 + onChange={setCountry} 114 + placeholder="Search countries..." 115 + />; 116 + ``` 117 + 118 + ### With Error Handling 119 + 120 + ```tsx 121 + <SearchableSelect 122 + label="Category" 123 + options={categories} 124 + value={category} 125 + onChange={setCategory} 126 + error={categoryError} 127 + required 128 + /> 129 + ``` 130 + 131 + ### Infinite Scroll Integration 132 + 133 + ```tsx 134 + const { 135 + data: skillsData, 136 + fetchNextPage, 137 + hasNextPage, 138 + isFetchingNextPage, 139 + } = useInfiniteSkillsQuery(/* ... */); 140 + 141 + <SearchableSelect 142 + label="Skills" 143 + options={skills} 144 + value={selectedSkill} 145 + onChange={setSelectedSkill} 146 + hasNextPage={hasNextPage} 147 + onLoadMore={fetchNextPage} 148 + isLoading={isFetchingNextPage} 149 + />; 150 + ``` 151 + 152 + ### Disabled State 153 + 154 + ```tsx 155 + <SearchableSelect 156 + label="Disabled Select" 157 + options={options} 158 + value={value} 159 + onChange={setValue} 160 + disabled 161 + /> 162 + ``` 163 + 164 + ## Search Functionality 165 + 166 + The component automatically filters options based on user input: 167 + 168 + ```tsx 169 + // Options are filtered in real-time as user types 170 + const filteredOptions = options.filter((option) => 171 + option.label.toLowerCase().includes(searchTerm.toLowerCase()), 172 + ); 173 + ``` 174 + 175 + ## Infinite Scroll Implementation 176 + 177 + The component includes built-in infinite scroll support: 178 + 179 + ```tsx 180 + // Intersection Observer setup 181 + const handleObserver = useCallback( 182 + (entries: IntersectionObserverEntry[]) => { 183 + const [target] = entries; 184 + if (target?.isIntersecting && hasNextPage && !isLoading && onLoadMore) { 185 + onLoadMore(); 186 + } 187 + }, 188 + [hasNextPage, isLoading, onLoadMore], 189 + ); 190 + ``` 191 + 192 + ## Keyboard Navigation 193 + 194 + The component supports full keyboard navigation: 195 + 196 + - **Arrow keys**: Navigate through options 197 + - **Enter**: Select highlighted option 198 + - **Escape**: Close dropdown 199 + - **Tab**: Move to next form element 200 + 201 + ## Accessibility 202 + 203 + The SearchableSelect component includes comprehensive accessibility features: 204 + 205 + - **Label association**: Automatic `htmlFor` and `id` linking 206 + - **Screen reader support**: Proper ARIA attributes 207 + - **Keyboard navigation**: Full keyboard support 208 + - **Focus management**: Proper focus states 209 + - **Error announcements**: Screen reader support for error messages 210 + 211 + ## Styling 212 + 213 + The component uses Tailwind CSS with Catppuccin color palette: 214 + 215 + - **Default state**: `border-ctp-surface1` with `focus-visible:ring-ctp-blue` 216 + - **Error state**: `border-ctp-red` with `focus-visible:ring-ctp-red` 217 + - **Dropdown**: `bg-ctp-surface0` with `border-ctp-surface1` 218 + - **Hover states**: `hover:bg-ctp-surface1` for options 219 + 220 + ## CVA Configuration 221 + 222 + The select variants are defined using Class Variance Authority: 223 + 224 + ```typescript 225 + const searchableSelectVariants = cva("base-select-styles", { 226 + variants: { 227 + state: { 228 + default: "border-ctp-surface1 focus-visible:ring-ctp-blue", 229 + error: "border-ctp-red focus-visible:ring-ctp-red", 230 + }, 231 + }, 232 + defaultVariants: { 233 + state: "default", 234 + }, 235 + }); 236 + ``` 237 + 238 + ## Performance Considerations 239 + 240 + - **Debounced search**: Search is performed on every keystroke 241 + - **Virtual scrolling**: Consider implementing for very large datasets 242 + - **Memoization**: Options are filtered on every render 243 + - **Observer cleanup**: Intersection Observer is properly cleaned up 244 + 245 + ## Integration with Data Fetching 246 + 247 + The component works seamlessly with data fetching libraries: 248 + 249 + ```tsx 250 + // With TanStack Query 251 + const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = 252 + useInfiniteQuery({ 253 + queryKey: ["skills"], 254 + queryFn: fetchSkills, 255 + getNextPageParam: (lastPage) => lastPage.nextCursor, 256 + }); 257 + 258 + <SearchableSelect 259 + options={data?.pages.flatMap((page) => page.items) || []} 260 + hasNextPage={hasNextPage} 261 + onLoadMore={fetchNextPage} 262 + isLoading={isFetchingNextPage} 263 + />; 264 + ``` 265 + 266 + ## Custom Styling 267 + 268 + ```tsx 269 + <SearchableSelect 270 + label="Custom Select" 271 + options={options} 272 + value={value} 273 + onChange={setValue} 274 + className="w-full max-w-md" 275 + /> 276 + ```
+100
apps/docs/content/components/select.mdx
··· 1 + import { Select } from "@cv/ui"; 2 + 3 + # Select 4 + 5 + A select dropdown component with label, error handling, and validation states. 6 + 7 + ## Features 8 + 9 + - **Label support**: Optional label with proper association 10 + - **Error handling**: Error state with message display 11 + - **Validation states**: Default and error states 12 + - **Disabled state**: Visual feedback for disabled select 13 + - **Required field**: Support for required validation 14 + - **Placeholder**: Optional placeholder option 15 + 16 + ## Basic Usage 17 + 18 + ```tsx 19 + import { Select } from "@cv/ui"; 20 + 21 + function MyForm() { 22 + const [value, setValue] = useState(""); 23 + 24 + const options = [ 25 + { value: "option1", label: "Option 1" }, 26 + { value: "option2", label: "Option 2" }, 27 + { value: "option3", label: "Option 3" }, 28 + ]; 29 + 30 + return ( 31 + <Select 32 + label="Choose an option" 33 + placeholder="Select..." 34 + value={value} 35 + onChange={setValue} 36 + options={options} 37 + /> 38 + ); 39 + } 40 + ``` 41 + 42 + ## With Error State 43 + 44 + ```tsx 45 + <Select 46 + label="Choose an option" 47 + value={value} 48 + onChange={setValue} 49 + options={options} 50 + error="Please select an option" 51 + required 52 + /> 53 + ``` 54 + 55 + ## Disabled State 56 + 57 + ```tsx 58 + <Select label="Disabled select" value={value} options={options} disabled /> 59 + ``` 60 + 61 + ## Without Label 62 + 63 + ```tsx 64 + <Select 65 + placeholder="Select..." 66 + value={value} 67 + onChange={setValue} 68 + options={options} 69 + /> 70 + ``` 71 + 72 + ## Props 73 + 74 + | Prop | Type | Default | Description | 75 + | ------------- | --------------------------------------- | ------------ | ------------------------------------- | 76 + | `label` | `string` | `undefined` | Optional label text | 77 + | `placeholder` | `string` | `undefined` | Placeholder option text | 78 + | `value` | `string` | `undefined` | Selected value | 79 + | `onChange` | `(value: string) => void` | `undefined` | Callback when value changes | 80 + | `disabled` | `boolean` | `false` | Whether the select is disabled | 81 + | `error` | `string` | `undefined` | Error message (shows error state) | 82 + | `options` | `Array<{value: string, label: string}>` | **required** | Select options | 83 + | `className` | `string` | `""` | Additional CSS classes | 84 + | `id` | `string` | `undefined` | Custom ID (auto-generated from label) | 85 + | `required` | `boolean` | `false` | Whether the field is required | 86 + 87 + ## Validation States 88 + 89 + - **Default**: Blue focus ring, gray border 90 + - **Error**: Red border and focus ring, error message displayed below 91 + 92 + ## Styling 93 + 94 + Uses Catppuccin color scheme: 95 + 96 + - Default border: `ctp-surface1` 97 + - Default focus: `ctp-blue` 98 + - Error border: `ctp-red` 99 + - Error focus: `ctp-red` 100 + - Disabled: 50% opacity
+94
apps/docs/content/components/statusbadge.mdx
··· 1 + import { StatusBadge } from "@cv/ui"; 2 + 3 + # StatusBadge 4 + 5 + A status indicator component built on top of the Badge component with predefined status types. 6 + 7 + ## Features 8 + 9 + - **Predefined statuses**: Active, inactive, pending, success, error, warning 10 + - **Color coding**: Automatic color assignment based on status 11 + - **Consistent styling**: Built on Badge component for consistency 12 + - **TypeScript**: Type-safe status values 13 + 14 + ## Basic Usage 15 + 16 + ```tsx 17 + import { StatusBadge } from "@cv/ui"; 18 + 19 + function MyComponent() { 20 + return <StatusBadge status="active" />; 21 + } 22 + ``` 23 + 24 + ## Available Statuses 25 + 26 + ### Active 27 + 28 + ```tsx 29 + <StatusBadge status="active" /> 30 + // Displays: Green badge with "Active" label 31 + ``` 32 + 33 + ### Inactive 34 + 35 + ```tsx 36 + <StatusBadge status="inactive" /> 37 + // Displays: Gray badge with "Inactive" label 38 + ``` 39 + 40 + ### Pending 41 + 42 + ```tsx 43 + <StatusBadge status="pending" /> 44 + // Displays: Yellow badge with "Pending" label 45 + ``` 46 + 47 + ### Success 48 + 49 + ```tsx 50 + <StatusBadge status="success" /> 51 + // Displays: Green badge with "Success" label 52 + ``` 53 + 54 + ### Error 55 + 56 + ```tsx 57 + <StatusBadge status="error" /> 58 + // Displays: Red badge with "Error" label 59 + ``` 60 + 61 + ### Warning 62 + 63 + ```tsx 64 + <StatusBadge status="warning" /> 65 + // Displays: Orange badge with "Warning" label 66 + ``` 67 + 68 + ## Custom Styling 69 + 70 + ```tsx 71 + <StatusBadge status="active" className="text-lg" /> 72 + ``` 73 + 74 + ## Props 75 + 76 + | Prop | Type | Default | Description | 77 + | ----------- | -------------------------------------------------------------------------- | ------------ | ---------------------- | 78 + | `status` | `"active" \| "inactive" \| "pending" \| "success" \| "error" \| "warning"` | **required** | Status type | 79 + | `className` | `string` | `""` | Additional CSS classes | 80 + 81 + ## Status Configuration 82 + 83 + | Status | Label | Color | 84 + | ---------- | ---------- | ------------ | 85 + | `active` | "Active" | `ctp-green` | 86 + | `inactive` | "Inactive" | `ctp-gray` | 87 + | `pending` | "Pending" | `ctp-yellow` | 88 + | `success` | "Success" | `ctp-green` | 89 + | `error` | "Error" | `ctp-red` | 90 + | `warning` | "Warning" | `ctp-orange` | 91 + 92 + ## Implementation 93 + 94 + This component wraps the `Badge` component with predefined configurations for common status indicators, ensuring consistency across the application.
+363
apps/docs/content/components/table.mdx
··· 1 + import { 2 + Table, 3 + TableHeader, 4 + TableBody, 5 + TableRow, 6 + TableHeaderCell, 7 + TableCell, 8 + } from "@cv/ui"; 9 + import { ComponentExample } from "@/components/ComponentExample"; 10 + 11 + # Table 12 + 13 + A comprehensive table component system with header, body, row, and cell components for structured data display. 14 + 15 + ## Features 16 + 17 + - **Modular design**: Separate components for table parts 18 + - **Full width support**: Optional full-width table variant 19 + - **Responsive**: Horizontal scroll for overflow content 20 + - **Accessibility**: Proper semantic HTML structure 21 + - **Hover effects**: Row hover states 22 + - **CVA Integration**: Dynamic styling with variants 23 + 24 + ## Components 25 + 26 + The Table system consists of multiple components: 27 + 28 + - `Table` - Main table container 29 + - `TableHeader` - Table header section 30 + - `TableBody` - Table body section 31 + - `TableRow` - Table row 32 + - `TableHeaderCell` - Header cell 33 + - `TableCell` - Data cell 34 + 35 + ## Basic Usage 36 + 37 + ```tsx 38 + <Table> 39 + <TableHeader> 40 + <TableRow> 41 + <TableHeaderCell>Name</TableHeaderCell> 42 + <TableHeaderCell>Email</TableHeaderCell> 43 + <TableHeaderCell>Role</TableHeaderCell> 44 + </TableRow> 45 + </TableHeader> 46 + <TableBody> 47 + <TableRow> 48 + <TableCell>John Doe</TableCell> 49 + <TableCell>john@example.com</TableCell> 50 + <TableCell>Admin</TableCell> 51 + </TableRow> 52 + </TableBody> 53 + </Table> 54 + ``` 55 + 56 + <ComponentExample className="my-4"> 57 + <Table> 58 + <TableHeader> 59 + <TableRow> 60 + <TableHeaderCell>Name</TableHeaderCell> 61 + <TableHeaderCell>Email</TableHeaderCell> 62 + <TableHeaderCell>Role</TableHeaderCell> 63 + </TableRow> 64 + </TableHeader> 65 + <TableBody> 66 + <TableRow> 67 + <TableCell>John Doe</TableCell> 68 + <TableCell>john@example.com</TableCell> 69 + <TableCell>Admin</TableCell> 70 + </TableRow> 71 + <TableRow> 72 + <TableCell>Jane Smith</TableCell> 73 + <TableCell>jane@example.com</TableCell> 74 + <TableCell>User</TableCell> 75 + </TableRow> 76 + </TableBody> 77 + </Table> 78 + </ComponentExample> 79 + 80 + ## Full Width Table 81 + 82 + ```tsx 83 + <Table fullWidth> 84 + <TableHeader> 85 + <TableRow> 86 + <TableHeaderCell>Product</TableHeaderCell> 87 + <TableHeaderCell>Price</TableHeaderCell> 88 + <TableHeaderCell>Stock</TableHeaderCell> 89 + </TableRow> 90 + </TableHeader> 91 + <TableBody>{/* Table rows */}</TableBody> 92 + </Table> 93 + ``` 94 + 95 + <ComponentExample className="my-4"> 96 + <Table fullWidth> 97 + <TableHeader> 98 + <TableRow> 99 + <TableHeaderCell>Product</TableHeaderCell> 100 + <TableHeaderCell>Price</TableHeaderCell> 101 + <TableHeaderCell>Stock</TableHeaderCell> 102 + </TableRow> 103 + </TableHeader> 104 + <TableBody> 105 + <TableRow> 106 + <TableCell>Widget A</TableCell> 107 + <TableCell>$19.99</TableCell> 108 + <TableCell>42</TableCell> 109 + </TableRow> 110 + <TableRow> 111 + <TableCell>Widget B</TableCell> 112 + <TableCell>$29.99</TableCell> 113 + <TableCell>15</TableCell> 114 + </TableRow> 115 + </TableBody> 116 + </Table> 117 + </ComponentExample> 118 + 119 + ## Props 120 + 121 + ### Table Props 122 + 123 + | Prop | Type | Default | Description | 124 + | ----------- | ----------------- | ------- | ---------------------- | 125 + | `children` | `React.ReactNode` | - | Table content | 126 + | `fullWidth` | `boolean` | `false` | Full width table | 127 + | `className` | `string` | `""` | Additional CSS classes | 128 + 129 + ### TableHeader Props 130 + 131 + | Prop | Type | Default | Description | 132 + | ----------- | ----------------- | ------- | ---------------------- | 133 + | `children` | `React.ReactNode` | - | Header content | 134 + | `className` | `string` | `""` | Additional CSS classes | 135 + 136 + ### TableBody Props 137 + 138 + | Prop | Type | Default | Description | 139 + | ----------- | ----------------- | ------- | ---------------------- | 140 + | `children` | `React.ReactNode` | - | Body content | 141 + | `className` | `string` | `""` | Additional CSS classes | 142 + 143 + ### TableRow Props 144 + 145 + | Prop | Type | Default | Description | 146 + | ----------- | ----------------- | ------- | ---------------------- | 147 + | `children` | `React.ReactNode` | - | Row content | 148 + | `className` | `string` | `""` | Additional CSS classes | 149 + 150 + ### TableHeaderCell Props 151 + 152 + | Prop | Type | Default | Description | 153 + | ----------- | ----------------- | ------- | ---------------------- | 154 + | `children` | `React.ReactNode` | - | Cell content | 155 + | `className` | `string` | `""` | Additional CSS classes | 156 + | `colSpan` | `number` | - | Column span | 157 + 158 + ### TableCell Props 159 + 160 + | Prop | Type | Default | Description | 161 + | ----------- | ----------------- | ------- | ---------------------- | 162 + | `children` | `React.ReactNode` | - | Cell content | 163 + | `className` | `string` | `""` | Additional CSS classes | 164 + | `colSpan` | `number` | - | Column span | 165 + 166 + ## Examples 167 + 168 + ### User List Table 169 + 170 + ```tsx 171 + <Table> 172 + <TableHeader> 173 + <TableRow> 174 + <TableHeaderCell>Name</TableHeaderCell> 175 + <TableHeaderCell>Email</TableHeaderCell> 176 + <TableHeaderCell>Status</TableHeaderCell> 177 + <TableHeaderCell>Actions</TableHeaderCell> 178 + </TableRow> 179 + </TableHeader> 180 + <TableBody> 181 + {users.map((user) => ( 182 + <TableRow key={user.id}> 183 + <TableCell>{user.name}</TableCell> 184 + <TableCell>{user.email}</TableCell> 185 + <TableCell> 186 + <Badge color={user.active ? "ctp-green" : "ctp-red"}> 187 + {user.active ? "Active" : "Inactive"} 188 + </Badge> 189 + </TableCell> 190 + <TableCell> 191 + <Button size="sm" variant="outline"> 192 + Edit 193 + </Button> 194 + </TableCell> 195 + </TableRow> 196 + ))} 197 + </TableBody> 198 + </Table> 199 + ``` 200 + 201 + ### Full Width Data Table 202 + 203 + ```tsx 204 + <Table fullWidth> 205 + <TableHeader> 206 + <TableRow> 207 + <TableHeaderCell>ID</TableHeaderCell> 208 + <TableHeaderCell>Title</TableHeaderCell> 209 + <TableHeaderCell>Description</TableHeaderCell> 210 + <TableHeaderCell>Created</TableHeaderCell> 211 + <TableHeaderCell>Updated</TableHeaderCell> 212 + </TableRow> 213 + </TableHeader> 214 + <TableBody> 215 + {items.map((item) => ( 216 + <TableRow key={item.id}> 217 + <TableCell>{item.id}</TableCell> 218 + <TableCell>{item.title}</TableCell> 219 + <TableCell>{item.description}</TableCell> 220 + <TableCell>{formatDate(item.createdAt)}</TableCell> 221 + <TableCell>{formatDate(item.updatedAt)}</TableCell> 222 + </TableRow> 223 + ))} 224 + </TableBody> 225 + </Table> 226 + ``` 227 + 228 + ### Table with Custom Styling 229 + 230 + ```tsx 231 + <Table className="shadow-lg"> 232 + <TableHeader> 233 + <TableRow> 234 + <TableHeaderCell className="text-center">Status</TableHeaderCell> 235 + <TableHeaderCell>Name</TableHeaderCell> 236 + <TableHeaderCell>Value</TableHeaderCell> 237 + </TableRow> 238 + </TableHeader> 239 + <TableBody> 240 + <TableRow className="bg-ctp-surface0"> 241 + <TableCell className="text-center"> 242 + <Badge color="ctp-green">Active</Badge> 243 + </TableCell> 244 + <TableCell>Item 1</TableCell> 245 + <TableCell>$100</TableCell> 246 + </TableRow> 247 + </TableBody> 248 + </Table> 249 + ``` 250 + 251 + ### Table with Column Span 252 + 253 + ```tsx 254 + <Table> 255 + <TableHeader> 256 + <TableRow> 257 + <TableHeaderCell>Name</TableHeaderCell> 258 + <TableHeaderCell>Details</TableHeaderCell> 259 + </TableRow> 260 + <TableRow> 261 + <TableHeaderCell>First Name</TableHeaderCell> 262 + <TableHeaderCell>Last Name</TableHeaderCell> 263 + <TableHeaderCell>Email</TableHeaderCell> 264 + <TableHeaderCell>Phone</TableHeaderCell> 265 + </TableRow> 266 + </TableHeader> 267 + <TableBody> 268 + <TableRow> 269 + <TableCell>John</TableCell> 270 + <TableCell>Doe</TableCell> 271 + <TableCell>john@example.com</TableCell> 272 + <TableCell>+1 234 567 8900</TableCell> 273 + </TableRow> 274 + </TableBody> 275 + </Table> 276 + ``` 277 + 278 + ## Accessibility 279 + 280 + The Table components include proper accessibility features: 281 + 282 + - **Semantic HTML**: Proper `<table>`, `<thead>`, `<tbody>`, `<tr>`, `<th>`, `<td>` elements 283 + - **Screen readers**: Proper table structure for screen readers 284 + - **Keyboard navigation**: Full keyboard support 285 + - **Focus management**: Proper focus states 286 + 287 + ## Styling 288 + 289 + The components use Tailwind CSS with Catppuccin color palette: 290 + 291 + - **Table**: `min-w-full divide-y divide-ctp-surface1` 292 + - **Header**: `bg-ctp-surface1` 293 + - **Header cells**: `text-ctp-subtext0 uppercase tracking-wider` 294 + - **Body**: `divide-y divide-ctp-surface1` 295 + - **Rows**: `hover:bg-ctp-surface0/50` 296 + - **Cells**: `text-ctp-text` 297 + 298 + ## CVA Configuration 299 + 300 + The table variants are defined using Class Variance Authority: 301 + 302 + ```typescript 303 + const tableVariants = cva("overflow-x-auto", { 304 + variants: { 305 + fullWidth: { 306 + true: "w-full", 307 + false: "", 308 + }, 309 + }, 310 + defaultVariants: { 311 + fullWidth: false, 312 + }, 313 + }); 314 + ``` 315 + 316 + ## Responsive Design 317 + 318 + The table includes responsive features: 319 + 320 + - **Horizontal scroll**: `overflow-x-auto` for mobile devices 321 + - **Full width option**: `fullWidth` prop for full container width 322 + - **Flexible cells**: Cells adapt to content 323 + 324 + ## Integration with Data 325 + 326 + ```tsx 327 + // With data fetching 328 + const { data: users, isLoading } = useUsersQuery(); 329 + 330 + <Table> 331 + <TableHeader> 332 + <TableRow> 333 + <TableHeaderCell>Name</TableHeaderCell> 334 + <TableHeaderCell>Email</TableHeaderCell> 335 + <TableHeaderCell>Role</TableHeaderCell> 336 + </TableRow> 337 + </TableHeader> 338 + <TableBody> 339 + {isLoading ? ( 340 + <TableRow> 341 + <TableCell colSpan={3} className="text-center"> 342 + Loading... 343 + </TableCell> 344 + </TableRow> 345 + ) : ( 346 + users?.map((user) => ( 347 + <TableRow key={user.id}> 348 + <TableCell>{user.name}</TableCell> 349 + <TableCell>{user.email}</TableCell> 350 + <TableCell>{user.role}</TableCell> 351 + </TableRow> 352 + )) 353 + )} 354 + </TableBody> 355 + </Table>; 356 + ``` 357 + 358 + ## Performance Considerations 359 + 360 + - **Virtual scrolling**: Consider for very large datasets 361 + - **Memoization**: Use `React.memo` for row components 362 + - **Pagination**: Implement pagination for large datasets 363 + - **Lazy loading**: Load data as needed
+111
apps/docs/content/components/textarea.mdx
··· 1 + import { Textarea } from "@cv/ui"; 2 + 3 + # Textarea 4 + 5 + A multi-line text input component with label, error handling, and validation states. 6 + 7 + ## Features 8 + 9 + - **Label support**: Optional label with proper association 10 + - **Error handling**: Error state with message display 11 + - **Validation states**: Default, success, and error states 12 + - **Disabled state**: Visual feedback for disabled textarea 13 + - **Required field**: Support for required validation 14 + - **Resizable**: Vertical resize enabled by default 15 + - **Customizable rows**: Control initial height 16 + 17 + ## Basic Usage 18 + 19 + ```tsx 20 + import { Textarea } from "@cv/ui"; 21 + 22 + function MyForm() { 23 + const [value, setValue] = useState(""); 24 + 25 + return ( 26 + <Textarea 27 + label="Description" 28 + placeholder="Enter description..." 29 + value={value} 30 + onChange={setValue} 31 + rows={4} 32 + /> 33 + ); 34 + } 35 + ``` 36 + 37 + ## With Error State 38 + 39 + ```tsx 40 + <Textarea 41 + label="Description" 42 + value={value} 43 + onChange={setValue} 44 + error="Description is required" 45 + required 46 + /> 47 + ``` 48 + 49 + ## Success State 50 + 51 + ```tsx 52 + <Textarea 53 + label="Description" 54 + value={value} 55 + onChange={setValue} 56 + state="success" 57 + /> 58 + ``` 59 + 60 + ## Custom Rows 61 + 62 + ```tsx 63 + <Textarea label="Large text area" value={value} onChange={setValue} rows={10} /> 64 + ``` 65 + 66 + ## Disabled State 67 + 68 + ```tsx 69 + <Textarea label="Description" value="Read-only content" disabled /> 70 + ``` 71 + 72 + ## Without Label 73 + 74 + ```tsx 75 + <Textarea placeholder="Enter text..." value={value} onChange={setValue} /> 76 + ``` 77 + 78 + ## Props 79 + 80 + | Prop | Type | Default | Description | 81 + | ------------- | ----------------------------------- | ----------- | ------------------------------------------ | 82 + | `label` | `string` | `undefined` | Optional label text | 83 + | `placeholder` | `string` | `undefined` | Placeholder text | 84 + | `value` | `string` | `undefined` | Textarea value | 85 + | `onChange` | `(value: string) => void` | `undefined` | Callback when value changes | 86 + | `disabled` | `boolean` | `false` | Whether the textarea is disabled | 87 + | `error` | `string` | `undefined` | Error message (overrides state to "error") | 88 + | `state` | `"default" \| "success" \| "error"` | `"default"` | Validation state | 89 + | `className` | `string` | `""` | Additional CSS classes | 90 + | `id` | `string` | `undefined` | Custom ID (auto-generated from label) | 91 + | `required` | `boolean` | `false` | Whether the field is required | 92 + | `rows` | `number` | `4` | Number of visible rows | 93 + 94 + ## Validation States 95 + 96 + - **Default**: Gray border, blue focus ring 97 + - **Success**: Green border and focus ring 98 + - **Error**: Red border and focus ring, error message displayed below 99 + 100 + ## Styling 101 + 102 + Uses Catppuccin color scheme: 103 + 104 + - Default border: `ctp-surface1` 105 + - Default focus: `ctp-blue` 106 + - Success border: `ctp-green` 107 + - Success focus: `ctp-green` 108 + - Error border: `ctp-red` 109 + - Error focus: `ctp-red` 110 + - Placeholder: `ctp-subtext0` 111 + - Resizable: Vertical resize enabled
+257
apps/docs/content/components/textinput.mdx
··· 1 + import { TextInput } from "@cv/ui"; 2 + import { ComponentExample, ExampleStateProvider } from "@/components/ComponentExample"; 3 + 4 + # TextInput 5 + 6 + A comprehensive text input component with validation states, error handling, and accessibility features. 7 + 8 + ## Features 9 + 10 + - **Validation states**: Default, success, and error states 11 + - **Error handling**: Built-in error message display 12 + - **Accessibility**: Proper labeling and focus management 13 + - **Type support**: Multiple input types (text, email, password, etc.) 14 + - **Validation**: Required field support 15 + - **CVA Integration**: Dynamic styling with state variants 16 + 17 + ## Basic Usage 18 + 19 + ```tsx 20 + <TextInput 21 + label="Email" 22 + placeholder="Enter your email" 23 + value={email} 24 + onChange={setEmail} 25 + /> 26 + ``` 27 + 28 + <ComponentExample className="my-4"> 29 + <ExampleStateProvider initialValue=""> 30 + {(value, setValue) => ( 31 + <TextInput 32 + label="Email" 33 + placeholder="Enter your email" 34 + value={value} 35 + onChange={setValue} 36 + /> 37 + )} 38 + </ExampleStateProvider> 39 + </ComponentExample> 40 + 41 + ## Input Types 42 + 43 + ### Text 44 + 45 + ```tsx 46 + <TextInput label="Name" type="text" placeholder="Enter your name" /> 47 + ``` 48 + 49 + <ComponentExample className="my-4"> 50 + <ExampleStateProvider initialValue=""> 51 + {(value, setValue) => ( 52 + <TextInput label="Name" type="text" placeholder="Enter your name" value={value} onChange={setValue} /> 53 + )} 54 + </ExampleStateProvider> 55 + </ComponentExample> 56 + 57 + ### Email 58 + 59 + ```tsx 60 + <TextInput label="Email" type="email" placeholder="Enter your email" /> 61 + ``` 62 + 63 + <ComponentExample className="my-4"> 64 + <ExampleStateProvider initialValue=""> 65 + {(value, setValue) => ( 66 + <TextInput label="Email" type="email" placeholder="Enter your email" value={value} onChange={setValue} /> 67 + )} 68 + </ExampleStateProvider> 69 + </ComponentExample> 70 + 71 + ### Password 72 + 73 + ```tsx 74 + <TextInput label="Password" type="password" placeholder="Enter your password" /> 75 + ``` 76 + 77 + <ComponentExample className="my-4"> 78 + <ExampleStateProvider initialValue=""> 79 + {(value, setValue) => ( 80 + <TextInput label="Password" type="password" placeholder="Enter your password" value={value} onChange={setValue} /> 81 + )} 82 + </ExampleStateProvider> 83 + </ComponentExample> 84 + 85 + ### Date 86 + 87 + ```tsx 88 + <TextInput label="Birth Date" type="date" /> 89 + ``` 90 + 91 + ### Number 92 + 93 + ```tsx 94 + <TextInput label="Age" type="number" placeholder="Enter your age" /> 95 + ``` 96 + 97 + ## Validation States 98 + 99 + ### Default State 100 + 101 + ```tsx 102 + <TextInput label="Username" placeholder="Enter username" state="default" /> 103 + ``` 104 + 105 + ### Success State 106 + 107 + ```tsx 108 + <TextInput label="Email" placeholder="Enter email" state="success" /> 109 + ``` 110 + 111 + ### Error State 112 + 113 + ```tsx 114 + <TextInput 115 + label="Password" 116 + placeholder="Enter password" 117 + state="error" 118 + error="Password must be at least 8 characters" 119 + /> 120 + ``` 121 + 122 + ## Props 123 + 124 + | Prop | Type | Default | Description | 125 + | ------------- | --------------------------------------------------------------------------------------------- | ----------- | ---------------------------------------------------- | 126 + | `label` | `string` | - | Input label | 127 + | `placeholder` | `string` | - | Placeholder text | 128 + | `value` | `string` | - | Input value | 129 + | `onChange` | `(value: string) => void` | - | Change handler | 130 + | `disabled` | `boolean` | `false` | Disabled state | 131 + | `error` | `string` | - | Error message | 132 + | `state` | `"default" \| "success" \| "error"` | `"default"` | Validation state | 133 + | `type` | `"text" \| "email" \| "password" \| "number" \| "tel" \| "url" \| "date" \| "datetime-local"` | `"text"` | Input type | 134 + | `className` | `string` | `""` | Additional CSS classes | 135 + | `id` | `string` | - | Input ID (auto-generated from label if not provided) | 136 + | `required` | `boolean` | `false` | Required field | 137 + 138 + ## Examples 139 + 140 + ### Form Field with Validation 141 + 142 + ```tsx 143 + <TextInput 144 + label="Email Address" 145 + type="email" 146 + placeholder="user@example.com" 147 + value={email} 148 + onChange={setEmail} 149 + required 150 + error={emailError} 151 + /> 152 + ``` 153 + 154 + ### Disabled Input 155 + 156 + ```tsx 157 + <TextInput label="Read-only Field" value="Cannot be edited" disabled /> 158 + ``` 159 + 160 + ### Custom Styling 161 + 162 + ```tsx 163 + <TextInput 164 + label="Custom Input" 165 + className="w-full max-w-md" 166 + placeholder="Custom styled input" 167 + /> 168 + ``` 169 + 170 + ### Required Field 171 + 172 + ```tsx 173 + <TextInput 174 + label="Required Field" 175 + placeholder="This field is required" 176 + required 177 + /> 178 + ``` 179 + 180 + ## Error Handling 181 + 182 + The component automatically handles error display: 183 + 184 + ```tsx 185 + const [email, setEmail] = useState(""); 186 + const [emailError, setEmailError] = useState(""); 187 + 188 + <TextInput 189 + label="Email" 190 + value={email} 191 + onChange={setEmail} 192 + error={emailError} 193 + state={emailError ? "error" : "default"} 194 + />; 195 + ``` 196 + 197 + ## Accessibility 198 + 199 + The TextInput component includes comprehensive accessibility features: 200 + 201 + - **Label association**: Automatic `htmlFor` and `id` linking 202 + - **Error announcements**: Screen reader support for error messages 203 + - **Focus management**: Proper focus states and keyboard navigation 204 + - **Required fields**: Semantic `required` attribute 205 + - **Input types**: Proper semantic input types for better UX 206 + 207 + ## Styling 208 + 209 + The component uses Tailwind CSS with Catppuccin color palette: 210 + 211 + - **Default state**: `border-ctp-surface1` with `focus-visible:ring-ctp-blue` 212 + - **Success state**: `border-ctp-green` with `focus-visible:ring-ctp-green` 213 + - **Error state**: `border-ctp-red` with `focus-visible:ring-ctp-red` 214 + - **Disabled state**: `disabled:opacity-50` with `disabled:cursor-not-allowed` 215 + 216 + ## CVA Configuration 217 + 218 + The input variants are defined using Class Variance Authority: 219 + 220 + ```typescript 221 + const inputVariants = cva("base-input-styles", { 222 + variants: { 223 + state: { 224 + default: "border-ctp-surface1 focus-visible:ring-ctp-blue", 225 + success: "border-ctp-green focus-visible:ring-ctp-green", 226 + error: "border-ctp-red focus-visible:ring-ctp-red", 227 + }, 228 + }, 229 + defaultVariants: { 230 + state: "default", 231 + }, 232 + }); 233 + ``` 234 + 235 + ## Form Integration 236 + 237 + The TextInput component works seamlessly with form libraries: 238 + 239 + ```tsx 240 + // With React Hook Form 241 + <TextInput 242 + label="Email" 243 + value={watch("email")} 244 + onChange={(value) => setValue("email", value)} 245 + error={errors.email?.message} 246 + /> 247 + 248 + // With custom validation 249 + <TextInput 250 + label="Password" 251 + type="password" 252 + value={password} 253 + onChange={setPassword} 254 + error={passwordError} 255 + required 256 + /> 257 + ```
+253
apps/docs/content/docs/architecture.md
··· 1 + # Architecture Overview 2 + 3 + This project follows a three-layer architecture with clear separation of concerns and strong typing across boundaries. 4 + 5 + ## Project Overview 6 + 7 + The CV Generator is a comprehensive platform for creating and managing CVs (resumes), with both B2C and B2B capabilities. It provides features for job experience tracking, education history, skills management, vacancy applications, CV template management, and organization-based multi-tenant support. 8 + 9 + ## Development Tools 10 + 11 + ### GraphQL Playground 12 + 13 + - **URL**: [{{GRAPHQL_URL}}]({{GRAPHQL_URL}}) 14 + - Interactive API explorer with schema documentation 15 + - Test queries and mutations in real-time 16 + - Full schema introspection and auto-completion 17 + 18 + ### Application URLs 19 + 20 + - **Client App**: [{{CLIENT_URL}}]({{CLIENT_URL}}) - React frontend with Vite 21 + - **Docs Site**: [{{DOCS_URL}}]({{DOCS_URL}}) - MDX-based documentation 22 + - **API Server**: [{{SERVER_URL}}]({{SERVER_URL}}) - NestJS GraphQL backend 23 + 24 + ## Technology Stack 25 + 26 + ### Backend 27 + 28 + - **Framework**: NestJS (Node.js 22) 29 + - **API**: GraphQL (Apollo Server) 30 + - **ORM**: Prisma 31 + - **Database**: PostgreSQL 16 32 + - **Authentication**: JWT with refresh tokens 33 + - **Validation**: Zod schemas 34 + 35 + ### Frontend 36 + 37 + - **Framework**: React 18 with TypeScript 38 + - **Build Tool**: Vite 39 + - **Styling**: Tailwind CSS with Catppuccin theme 40 + - **State Management**: React Query (TanStack Query v5) 41 + - **Routing**: React Router v7 42 + - **Form Validation**: Zod 43 + - **UI Components**: Custom component library with CVA (Class Variance Authority) 44 + 45 + ### Infrastructure 46 + 47 + - **Containerization**: Docker with docker-compose 48 + - **Package Management**: npm with Lerna workspaces 49 + - **Linting/Formatting**: Biome 2.2.6 50 + - **Testing**: Jest (E2E and integration tests) 51 + 52 + ## Architecture Layers 53 + 54 + ### 1. GraphQL Layer (API Layer) 55 + 56 + - **Location**: `apps/server/src/modules/**/graphql/` 57 + - Implements resolvers and GraphQL object types (classes) 58 + - GraphQL classes expose `fromDomain()` static factories to convert domain entities to API types 59 + - Handles connection composition from service `findMany()` and `count()` methods 60 + - No database access; no business logic 61 + 62 + ### 2. Domain Layer (Core Model Layer) 63 + 64 + - **Location**: `apps/server/src/modules/**/*.entity.ts` 65 + - Plain classes representing core business entities 66 + - Encapsulate business logic and domain rules 67 + - Domain entities contain full related entities, not foreign keys 68 + - Use `findFor` methods instead of `findBy` when working with related entities 69 + - No framework-specific decorators (except GraphQL field metadata on API classes) 70 + 71 + ### 3. Persistence Layer (Prisma Layer) 72 + 73 + - **Location**: Prisma client via `PrismaService` 74 + - Database operations only; no domain logic 75 + - Prisma models are database representations 76 + - Use bracket notation: `prisma["model"]` for TypeScript strict index checking 77 + 78 + ## Data Flow Pattern 79 + 80 + ``` 81 + Resolver → Service → Prisma → Mapper.toDomain → Domain Entity → GraphQLType.fromDomain 82 + ``` 83 + 84 + ## Module Structure 85 + 86 + ### Backend Modules 87 + 88 + - `app` - Application health and status 89 + - `auth` - Authentication and authorization (JWT) 90 + - `user` - User management and user-related queries 91 + - `education` - Education history with skills support 92 + - `job-experience` - Job experience tracking (companies, roles, levels, skills) 93 + - `organization` - Multi-tenant organization management with RBAC 94 + - `vacancies` - Job vacancy management 95 + - `application` - Job application management 96 + - `cv-template` - CV template system 97 + - `base` - Shared pagination and cursor services 98 + - `database` - Database configuration (PrismaService) 99 + - `seed` - Database seeding utilities 100 + 101 + ### Frontend Features 102 + 103 + - `app` - App-level queries and utilities 104 + - `auth` - Authentication forms and flows 105 + - `user` - User profile and queries 106 + - `education` - Education history management with skills 107 + - `job-experience` - Job experience CRUD operations 108 + - `organizations` - Organization membership management 109 + - `vacancies` - Vacancy browsing and management 110 + - `applications` - Application flow with Progress component 111 + - `cv-templates` - CV template selection and management 112 + 113 + ## Mapping Pattern 114 + 115 + - Each entity has a dedicated Mapper service implementing `BaseMapper<PrismaModel, DomainEntity>` 116 + - Mappers are `@Injectable()` Nest providers 117 + - Mappers support overloads for nullable inputs: `(value: T | null) => R | null` 118 + - Services inject mappers and use them to convert from Prisma models to domain entities 119 + - GraphQL classes convert domain entities via `fromDomain()` when returning data 120 + - **Maximize mapper usage**: Services must use injected mappers for all conversions 121 + 122 + ## Service Method Conventions 123 + 124 + - **Accept full entities** instead of IDs when possible 125 + - **Use `findFor` methods** (e.g., `findForUser(user)`) instead of `findBy` with IDs 126 + - **Consolidate methods**: `findMany` accepts `userId` as part of filter instead of separate `findForUser` 127 + - **OrFail delegation**: `findByXOrFail` delegates to `findByX` and only handles error throwing 128 + - **Connection classes**: Must include `fromPaginationResult()` static factory for edge management 129 + - **Use PaginationService.buildQueryOptions()** for cursor-based pagination 130 + 131 + ## GraphQL Connection Pattern 132 + 133 + All list queries use Relay-style connections: 134 + 135 + - Connection types with `edges`, `pageInfo`, and `totalCount` 136 + - Edge types extending `BaseEdge` 137 + - Connection classes handle their own edge creation via `fromPaginationResult()` 138 + - Services provide `findMany()` and `count()` methods 139 + - Resolvers compose connections using `PaginationService` 140 + 141 + ## Entity Instantiation 142 + 143 + **All entities and GraphQL types MUST use constructor-based instantiation:** 144 + 145 + - ✅ Proper constructor with nullable types 146 + - ✅ Use `??` nullish coalescing for default values 147 + - ❌ NEVER use non-null assertion operator (`!`) 148 + - ❌ NEVER use `Object.assign(this, partial)` pattern 149 + 150 + Example: 151 + 152 + ```typescript 153 + constructor(data: { 154 + id: string; 155 + name: string; 156 + description?: string | null; 157 + }) { 158 + this.id = data.id; 159 + this.name = data.name; 160 + this.description = data.description ?? null; 161 + } 162 + ``` 163 + 164 + ## Module Dependencies 165 + 166 + - **DatabaseModule**: Non-global (import where needed) 167 + - **UserModule**: Separate from AuthModule (houses user services, mappers, resolvers) 168 + - **AuthModule**: Handles authentication only (imports UserModule) 169 + - No circular dependencies (removed `forwardRef` usage) 170 + 171 + ## Frontend Components 172 + 173 + ### Progress Component 174 + 175 + - Generic progress indicator with compound component pattern 176 + - `<Progress><Progress.Step/></Progress>` 177 + - Manages step state, navigation (`next`, `back`, `canGoNext`, `canGoBack`) via context 178 + - Uses CVA for step state variants (selected, completed, pending, error) 179 + - Uses Zod for validating step props 180 + - Used in application flow for step-by-step wizard 181 + 182 + ### Query Invalidation 183 + 184 + - React Query invalidation implemented for all mutations 185 + - Invalidate relevant queries after create/update/delete operations 186 + - Ensures UI stays in sync with backend changes 187 + 188 + ## Configuration 189 + 190 + - Use `getOrThrow()` for configuration values instead of manual error throwing 191 + - Inject ConfigService where needed for environment variable access 192 + - Use bracket notation: `process.env["VARIABLE_NAME"]` for TypeScript strict checking 193 + 194 + ## Testing 195 + 196 + - E2E tests under `apps/server/test` exercise GraphQL endpoints 197 + - Tests expect services to return proper domain entities 198 + - GraphQL types handle API shaping 199 + - Database operations run in Docker containers for consistency 200 + 201 + ## Code Quality 202 + 203 + ### TypeScript Configuration 204 + 205 + - Strict mode enabled 206 + - `noUncheckedIndexedAccess: true` (use bracket notation for arrays/objects) 207 + - `exactOptionalPropertyTypes: true` (backend), `false` (frontend/docs) 208 + - `noImplicitAny: true` 209 + - Path aliases: `@/` for app imports 210 + 211 + ### Biome Rules 212 + 213 + - Prefer arrow functions 214 + - Prefer early returns 215 + - Prefer ternary operations 216 + - Prefer destructuring 217 + - Prefer named exports 218 + - Prefer `const` over `let` 219 + - **NEVER use non-null assertion operator (`!`)** 220 + - Use object property shorthand 221 + - Use optional chaining 222 + 223 + ## Recent Architecture Improvements 224 + 225 + ### Education with Skills 226 + 227 + - Education entities now support multiple skills 228 + - Skills are shared across education and job experience 229 + - Skills rendering on CV pages 230 + 231 + ### Application Flow 232 + 233 + - Progress component for multi-step application process 234 + - Vacancy selector with consistent styling (blue selected state, hover effects) 235 + - CV template selector integrated into application flow 236 + 237 + ### Module Separation 238 + 239 + - UserModule extracted from AuthModule 240 + - Removed forwardRef and module cycles 241 + - DatabaseModule made non-global for better dependency management 242 + 243 + ### Query Invalidation 244 + 245 + - All mutations invalidate relevant queries 246 + - Ensures UI consistency after data changes 247 + 248 + ### Documentation 249 + 250 + - MDX-based documentation site 251 + - Environment variable interpolation in markdown 252 + - Copy-to-clipboard for heading links 253 + - Markdown styling applied to MDX content
+599
apps/docs/content/docs/docker-strategy.md
··· 1 + # Docker Strategy 2 + 3 + This document outlines the Docker architecture and layer caching strategy used in the CV Generator project. 4 + 5 + ## Quick Start 6 + 7 + After running `docker compose up`, the following services will be available: 8 + 9 + - **Client App**: [{{CLIENT_URL}}]({{CLIENT_URL}}) - React frontend 10 + - **API Server**: [{{SERVER_URL}}]({{SERVER_URL}}) - NestJS backend 11 + - **GraphQL Playground**: [{{GRAPHQL_URL}}]({{GRAPHQL_URL}}) - Interactive API explorer 12 + - **Docs Site**: [{{DOCS_URL}}]({{DOCS_URL}}) - Documentation 13 + - **Database**: `{{DB_HOST}}:{{DB_PORT}}` - PostgreSQL (credentials in docker-compose.yml) 14 + 15 + ## Table of Contents 16 + 17 + - [Overview](#overview) 18 + - [Docker Compose Services](#docker-compose-services) 19 + - [Layer Caching Strategy](#layer-caching-strategy) 20 + - [Health Checks](#health-checks) 21 + - [Development Workflow](#development-workflow) 22 + - [Performance Optimization](#performance-optimization) 23 + 24 + ## Overview 25 + 26 + The project uses a multi-container Docker setup with optimized Dockerfiles for both development and production. The architecture consists of three main services: 27 + 28 + 1. **Database (PostgreSQL)** - Data persistence 29 + 2. **Server (NestJS)** - GraphQL API backend 30 + 3. **Client (React + Vite)** - Frontend application 31 + 32 + ### Multi-Stage Dockerfiles 33 + 34 + Both server and client use **multi-stage Dockerfiles** with: 35 + 36 + - **Development Stage** - Optimized for fast rebuilds with layer caching 37 + - **Production Stage** - Optimized for minimal size and performance 38 + 39 + The stage is selected via the `target` parameter in `docker-compose.yml`: 40 + 41 + ```yaml 42 + build: 43 + dockerfile: apps/client/Dockerfile 44 + target: development # or 'production' 45 + ``` 46 + 47 + All services use Docker layer caching to minimize rebuild times during development. 48 + 49 + ## Docker Compose Services 50 + 51 + ### Database Service 52 + 53 + ```yaml 54 + db: 55 + image: postgres:16-alpine 56 + environment: 57 + POSTGRES_USER: ${POSTGRES_USER:-cv_user} 58 + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-cv_password} 59 + POSTGRES_DB: ${POSTGRES_DB:-cv_database} 60 + ports: 61 + - "${DB_PORT:-5432}:5432" 62 + volumes: 63 + - db-data:/var/lib/postgresql/data 64 + healthcheck: 65 + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] 66 + interval: 5s 67 + timeout: 5s 68 + retries: 5 69 + start_period: 10s 70 + ``` 71 + 72 + **Key Features:** 73 + 74 + - Uses Alpine-based PostgreSQL for smaller image size 75 + - Persistent data storage via named volume 76 + - Health check ensures database is ready before dependent services start 77 + - Environment variable configuration 78 + 79 + ### Server Service 80 + 81 + ```yaml 82 + server: 83 + build: 84 + context: . 85 + dockerfile: apps/server/Dockerfile 86 + working_dir: /app 87 + command: sh -c "cd apps/server && npm run prisma:deploy && npm run dev" 88 + environment: 89 + PORT: ${SERVER_PORT:-3000} 90 + JWT_SECRET: ${JWT_SECRET:-your-super-secret-jwt-key-here} 91 + JWT_ACCESS_TOKEN_EXPIRY: ${JWT_ACCESS_TOKEN_EXPIRY:-15m} 92 + JWT_REFRESH_TOKEN_EXPIRY: ${JWT_REFRESH_TOKEN_EXPIRY:-7d} 93 + DATABASE_URL: ${DATABASE_URL:-postgresql://cv:cv@db:5432/cv} 94 + depends_on: 95 + db: 96 + condition: service_healthy 97 + ports: 98 + - "${SERVER_PORT:-3000}:${SERVER_PORT:-3000}" 99 + volumes: 100 + - .:/app 101 + - npm-cache:/root/.npm 102 + healthcheck: 103 + test: ["CMD", "sh", "/app/scripts/health-check-server.sh"] 104 + interval: 5s 105 + timeout: 5s 106 + retries: 5 107 + start_period: 30s 108 + ``` 109 + 110 + **Key Features:** 111 + 112 + - Custom Dockerfile with optimized layer caching 113 + - Waits for database health check before starting 114 + - Runs Prisma migrations on startup 115 + - Custom health check with GraphQL query validation 116 + - Shared npm cache for faster installs 117 + 118 + ### Client Service 119 + 120 + ```yaml 121 + client: 122 + build: 123 + context: . 124 + dockerfile: apps/client/Dockerfile 125 + target: development 126 + working_dir: /app 127 + command: sh -c "cd apps/client && npm run codegen && npm run dev" 128 + environment: 129 + VITE_SERVER_URL: ${VITE_SERVER_URL:-http://localhost:3000} 130 + GRAPHQL_SCHEMA_URL: ${GRAPHQL_SCHEMA_URL:-http://localhost:3000/graphql} 131 + depends_on: 132 + server: 133 + condition: service_healthy 134 + restart: true 135 + ports: 136 + - "${CLIENT_PORT:-5173}:${CLIENT_PORT:-5173}" 137 + volumes: 138 + - .:/app 139 + - npm-cache:/root/.npm 140 + healthcheck: 141 + test: ["CMD", "/app/scripts/health-check-client.sh"] 142 + interval: 10s 143 + timeout: 5s 144 + retries: 3 145 + start_period: 30s 146 + ``` 147 + 148 + **Key Features:** 149 + 150 + - Multi-stage Dockerfile with separate `development` and `production` targets 151 + - Development target uses optimized layer caching 152 + - Production target uses Nginx for static file serving 153 + - Waits for server health check before starting 154 + - Runs GraphQL codegen before starting dev server 155 + - Custom health check validates generated types and Vite server 156 + - Shared npm cache for faster installs 157 + 158 + ## Layer Caching Strategy 159 + 160 + ### The Problem 161 + 162 + Without optimization, Docker rebuilds all layers on any code change: 163 + 164 + ```dockerfile 165 + # ❌ BAD: Everything invalidates on ANY change 166 + COPY . . # Copies EVERYTHING 167 + RUN npm install # Runs on EVERY code change (slow!) 168 + ``` 169 + 170 + ### The Solution: 5-Layer Strategy 171 + 172 + Both server and client use a carefully ordered 5-layer strategy: 173 + 174 + #### Layer 1: System Dependencies 175 + 176 + ```dockerfile 177 + # ============================================================================= 178 + # Layer 1: System dependencies (rarely changes) 179 + # ============================================================================= 180 + RUN apk add --no-cache openssl curl 181 + ``` 182 + 183 + **Invalidates:** Almost never (only when system packages change) 184 + **Build Time:** ~1 second 185 + **Cache Hit Rate:** 99% 186 + 187 + #### Layer 2: Package Dependencies 188 + 189 + ```dockerfile 190 + # ============================================================================= 191 + # Layer 2: Package dependencies (changes when dependencies update) 192 + # ============================================================================= 193 + COPY package*.json ./ 194 + COPY lerna.json ./ 195 + COPY apps/server/package*.json ./apps/server/ 196 + COPY apps/client/package*.json ./apps/client/ 197 + COPY packages/*/package.json ./packages/*/ 198 + 199 + RUN npm install --ignore-scripts 200 + ``` 201 + 202 + **Invalidates:** When `package.json` files change 203 + **Build Time:** ~2 minutes 204 + **Cache Hit Rate:** 90% 205 + 206 + #### Layer 3: Configuration Files 207 + 208 + ```dockerfile 209 + # ============================================================================= 210 + # Layer 3: Configuration files (changes less frequently than source code) 211 + # ============================================================================= 212 + COPY packages/tsconfig/ ./packages/tsconfig/ 213 + COPY packages/biome-config/ ./packages/biome-config/ 214 + COPY packages/utils/ ./packages/utils/ 215 + ``` 216 + 217 + **Invalidates:** When config files change 218 + **Build Time:** ~1 second 219 + **Cache Hit Rate:** 95% 220 + 221 + #### Layer 4: Schema & Generation (Server Only) 222 + 223 + ```dockerfile 224 + # ============================================================================= 225 + # Layer 4: Prisma schema and generation (changes when schema updates) 226 + # ============================================================================= 227 + COPY apps/server/prisma/ ./apps/server/prisma/ 228 + 229 + RUN npx prisma generate --schema=apps/server/prisma/schema.prisma 230 + ``` 231 + 232 + **Invalidates:** When Prisma schema changes 233 + **Build Time:** ~5 seconds 234 + **Cache Hit Rate:** 98% 235 + 236 + #### Layer 5: Source Code 237 + 238 + ```dockerfile 239 + # ============================================================================= 240 + # Layer 5: Source code (changes most frequently) 241 + # ============================================================================= 242 + COPY apps/server/src/ ./apps/server/src/ 243 + COPY apps/server/tsconfig*.json ./apps/server/ 244 + COPY scripts/ ./scripts/ 245 + ``` 246 + 247 + **Invalidates:** On every code change 248 + **Build Time:** ~1 second 249 + **Cache Hit Rate:** 0% (intentional - this is what we're editing!) 250 + 251 + ### Cache Invalidation Hierarchy 252 + 253 + ``` 254 + ┌─────────────────────────────────────────┐ 255 + │ Layer 1: System Packages │ ← Invalidates all layers below 256 + │ (apk add openssl curl) │ 257 + └─────────────────────────────────────────┘ 258 + ↓ invalidates 259 + ┌─────────────────────────────────────────┐ 260 + │ Layer 2: Dependencies │ ← Invalidates layers 3-5 261 + │ (npm install) │ 262 + └─────────────────────────────────────────┘ 263 + ↓ invalidates 264 + ┌─────────────────────────────────────────┐ 265 + │ Layer 3: Config Files │ ← Invalidates layers 4-5 266 + │ (tsconfig, biome, utils) │ 267 + └─────────────────────────────────────────┘ 268 + ↓ invalidates 269 + ┌─────────────────────────────────────────┐ 270 + │ Layer 4: Schema/Generated │ ← Invalidates layer 5 271 + │ (prisma generate) │ 272 + └─────────────────────────────────────────┘ 273 + ↓ invalidates 274 + ┌─────────────────────────────────────────┐ 275 + │ Layer 5: Source Code │ ← Only invalidates itself ✅ 276 + │ (src/, test/) │ 277 + └─────────────────────────────────────────┘ 278 + ``` 279 + 280 + ## Health Checks 281 + 282 + ### Database Health Check 283 + 284 + ```bash 285 + pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB} 286 + ``` 287 + 288 + **Checks:** 289 + 290 + - PostgreSQL is running 291 + - Database is accepting connections 292 + - User credentials are valid 293 + 294 + **Frequency:** Every 5 seconds 295 + **Start Period:** 10 seconds 296 + 297 + ### Server Health Check 298 + 299 + Located at: `scripts/health-check-server.sh` 300 + 301 + ```bash 302 + #!/bin/sh 303 + 304 + # Test GraphQL server with a health query 305 + RESPONSE=$(curl -s -X POST http://localhost:${SERVER_PORT:-3000}/graphql \ 306 + -H "Content-Type: application/json" \ 307 + -d '{"query":"{ health { status } }"}') 308 + 309 + # Check if the response contains "ok" 310 + if echo "$RESPONSE" | grep -q '"status":"ok"'; then 311 + echo "Server health check passed" 312 + exit 0 313 + else 314 + echo "Server health check failed: $RESPONSE" 315 + exit 1 316 + fi 317 + ``` 318 + 319 + **Checks:** 320 + 321 + - HTTP server is responding 322 + - GraphQL endpoint is accessible 323 + - Health query returns "ok" 324 + - TypeScript compilation succeeded 325 + 326 + **Frequency:** Every 5 seconds 327 + **Start Period:** 30 seconds (allows time for Prisma migration) 328 + 329 + ### Client Health Check 330 + 331 + Located at: `scripts/health-check-client.sh` 332 + 333 + ```bash 334 + #!/bin/sh 335 + 336 + # Check if Vite server is responding 337 + curl -f -s "http://localhost:${CLIENT_PORT:-5173}" > /dev/null || exit 1 338 + 339 + # Check for HMR WebSocket client availability 340 + curl -f -s "http://localhost:${CLIENT_PORT:-5173}/@vite/client" > /dev/null || exit 1 341 + 342 + # Check for import binding errors (GraphQL codegen issues) 343 + RESPONSE=$(curl -s "http://localhost:${CLIENT_PORT:-5173}") 344 + echo "$RESPONSE" | grep -qi "importing binding" && exit 1 345 + 346 + # Check for TypeScript compilation errors 347 + echo "$RESPONSE" | grep -qi "syntaxerror\|referenceerror" && exit 1 348 + 349 + # Verify generated GraphQL file exists and is not empty 350 + [ -f "/app/apps/client/src/generated/graphql.ts" ] || exit 1 351 + [ -s "/app/apps/client/src/generated/graphql.ts" ] || exit 1 352 + 353 + echo "Client health check passed" 354 + exit 0 355 + ``` 356 + 357 + **Checks:** 358 + 359 + - Vite dev server is responding 360 + - HMR WebSocket client is available 361 + - No import binding errors (GraphQL codegen sync) 362 + - No TypeScript/JavaScript errors 363 + - Generated GraphQL types file exists and is not empty 364 + - HTML page loads properly 365 + 366 + **Frequency:** Every 10 seconds 367 + **Start Period:** 30 seconds (allows time for codegen) 368 + 369 + ## Development Workflow 370 + 371 + ### Initial Setup 372 + 373 + ```bash 374 + # Start all services 375 + docker compose up -d 376 + 377 + # Check service status 378 + docker compose ps 379 + 380 + # View logs 381 + docker compose logs -f 382 + ``` 383 + 384 + ### Making Code Changes 385 + 386 + #### 1. Server Code Change 387 + 388 + ```bash 389 + # Edit server source code in apps/server/src/ 390 + # Docker volume mount reflects changes immediately 391 + # Nodemon auto-restarts server 392 + 393 + # Build time: ~0 seconds (hot reload) 394 + # Layer cache: All layers cached 395 + ``` 396 + 397 + #### 2. Client Code Change 398 + 399 + ```bash 400 + # Edit client source code in apps/client/src/ 401 + # Docker volume mount reflects changes immediately 402 + # Vite HMR updates browser instantly 403 + 404 + # Build time: ~0 seconds (hot reload) 405 + # Layer cache: All layers cached 406 + ``` 407 + 408 + #### 3. Adding Dependencies 409 + 410 + ```bash 411 + # Edit package.json 412 + # Rebuild service 413 + docker compose build server # or client 414 + 415 + # Build time: ~1.5 minutes 416 + # Layer cache: Layers 1 cached, layers 2-5 rebuilt 417 + ``` 418 + 419 + #### 4. Schema Change 420 + 421 + ```bash 422 + # Edit apps/server/prisma/schema.prisma 423 + # Rebuild server 424 + docker compose build server 425 + 426 + # Build time: ~30 seconds 427 + # Layer cache: Layers 1-3 cached, layers 4-5 rebuilt 428 + ``` 429 + 430 + #### 5. Configuration Change 431 + 432 + ```bash 433 + # Edit tsconfig.json, biome.json, etc. 434 + # Rebuild service 435 + docker compose build server # or client 436 + 437 + # Build time: ~5 seconds 438 + # Layer cache: Layers 1-2 cached, layers 3-5 rebuilt 439 + ``` 440 + 441 + ### Rebuilding Services 442 + 443 + ```bash 444 + # Rebuild all services 445 + docker compose build 446 + 447 + # Rebuild specific service 448 + docker compose build server 449 + docker compose build client 450 + 451 + # Rebuild and restart 452 + docker compose up -d --build 453 + ``` 454 + 455 + ### Cleanup 456 + 457 + ```bash 458 + # Stop all services 459 + docker compose down 460 + 461 + # Remove volumes (database data) 462 + docker compose down -v 463 + 464 + # Remove images 465 + docker compose down --rmi all 466 + ``` 467 + 468 + ## Performance Optimization 469 + 470 + ### Build Time Comparison 471 + 472 + | Scenario | Before Optimization | After Optimization | Improvement | 473 + | --------------------- | ------------------- | ------------------ | ----------------- | 474 + | **Code Change** | ~2 minutes | ~5 seconds | **96% faster** ✅ | 475 + | **Config Change** | ~2 minutes | ~30 seconds | **75% faster** ✅ | 476 + | **Schema Change** | ~2 minutes | ~35 seconds | **71% faster** ✅ | 477 + | **Dependency Change** | ~2 minutes | ~1.5 minutes | **25% faster** ✅ | 478 + | **First Build** | ~2 minutes | ~2 minutes | Same | 479 + 480 + ### Layer Cache Hit Rates 481 + 482 + ``` 483 + Layer 1: System Dependencies → 99% cache hit rate 484 + Layer 2: Package Dependencies → 90% cache hit rate 485 + Layer 3: Configuration Files → 95% cache hit rate 486 + Layer 4: Schema/Generated → 98% cache hit rate 487 + Layer 5: Source Code → 0% cache hit rate (by design) 488 + ``` 489 + 490 + ### Development Experience Impact 491 + 492 + **Typical Development Day:** 493 + 494 + - 100 code changes × 5 seconds = **8 minutes total build time** 495 + - Without optimization: 100 × 2 minutes = **200 minutes** ❌ 496 + - **Time saved per day: 192 minutes (~3 hours)** ✅ 497 + 498 + **Key Benefits:** 499 + 500 + - ✅ Near-instant rebuilds for code changes (95% of work) 501 + - ✅ Fast rebuilds for config changes (4% of work) 502 + - ✅ Moderate rebuilds for schema changes (1% of work) 503 + - ✅ Shared npm cache across all containers 504 + - ✅ Volume mounts for instant file sync 505 + - ✅ Hot module replacement (HMR) for client 506 + - ✅ Auto-restart for server changes 507 + 508 + ### Best Practices 509 + 510 + 1. **Never** put `COPY . .` early in the Dockerfile 511 + 2. **Always** copy `package*.json` before running `npm install` 512 + 3. **Order** layers from least-frequently-changed to most-frequently-changed 513 + 4. **Use** `.dockerignore` to exclude unnecessary files 514 + 5. **Share** npm cache volume across containers 515 + 6. **Leverage** volume mounts for development hot-reload 516 + 7. **Implement** health checks for all services 517 + 8. **Test** health checks to catch deployment issues early 518 + 519 + ## Environment Configuration 520 + 521 + All services use environment variables for configuration. See `.env.example` for available options: 522 + 523 + ```env 524 + # Database Configuration 525 + POSTGRES_USER=cv_user 526 + POSTGRES_PASSWORD=cv_password 527 + POSTGRES_DB=cv_database 528 + DATABASE_URL=postgresql://cv:cv@db:5432/cv 529 + DB_PORT=5432 530 + 531 + # Server Configuration 532 + SERVER_PORT=3000 533 + JWT_SECRET=your-super-secret-jwt-key-change-this-in-production 534 + JWT_ACCESS_TOKEN_EXPIRY=15m 535 + JWT_REFRESH_TOKEN_EXPIRY=7d 536 + PRISMA_ENABLE_TRACING=false 537 + 538 + # Client Configuration 539 + VITE_SERVER_URL=http://localhost:3000 540 + GRAPHQL_SCHEMA_URL=http://localhost:3000/graphql 541 + CLIENT_PORT=5173 542 + ``` 543 + 544 + ## Troubleshooting 545 + 546 + ### Container won't start 547 + 548 + ```bash 549 + # Check logs 550 + docker compose logs service-name 551 + 552 + # Check health status 553 + docker compose ps 554 + 555 + # Restart service 556 + docker compose restart service-name 557 + ``` 558 + 559 + ### Rebuild not picking up changes 560 + 561 + ```bash 562 + # Force rebuild without cache 563 + docker compose build --no-cache service-name 564 + 565 + # Remove and rebuild 566 + docker compose down 567 + docker compose up -d --build 568 + ``` 569 + 570 + ### Port conflicts 571 + 572 + ```bash 573 + # Change ports in .env file 574 + SERVER_PORT=3001 575 + CLIENT_PORT=5174 576 + DB_PORT=5433 577 + 578 + # Restart services 579 + docker compose down 580 + docker compose up -d 581 + ``` 582 + 583 + ### Database issues 584 + 585 + ```bash 586 + # Reset database 587 + docker compose down -v 588 + docker compose up -d 589 + 590 + # Run migrations manually 591 + docker compose exec server npm run prisma:deploy 592 + ``` 593 + 594 + ## Related Documentation 595 + 596 + - [Architecture Overview](ARCHITECTURE.md) 597 + - [GraphQL Architecture](GRAPHQL_ARCHITECTURE.md) 598 + - [Development Roadmap](ROADMAP.md) 599 + - [Changelog](CHANGELOG.md)
+305
apps/docs/content/docs/frontend-structure.md
··· 1 + # Frontend Application Structure 2 + 3 + This document reflects the current, up-to-date structure of the client app and the co-location pattern for GraphQL and validation. 4 + 5 + ``` 6 + apps/client/src/ 7 + ├── components/ # Reusable UI components 8 + │ ├── ConfirmationModal.tsx # Modal for confirmations 9 + │ ├── ErrorBoundary.tsx # Error boundary component 10 + │ ├── Navbar.tsx # Navigation bar component 11 + │ ├── Progress.tsx # Generic progress indicator with compound component pattern 12 + │ ├── ServerStatusIndicator/ # Server health indicator 13 + │ │ ├── ServerTooltip.tsx # Tooltip for server info 14 + │ │ ├── StatusDot.tsx # Status indicator dot 15 + │ │ ├── constants.ts # Status constants 16 + │ │ ├── index.tsx # Main component 17 + │ │ ├── types.ts # Type definitions 18 + │ │ └── utils.ts # Utility functions 19 + │ ├── Toast.tsx # Toast notification component 20 + │ ├── ToastContainer.tsx # Toast container 21 + │ └── navLinks.ts # Navigation links configuration 22 + 23 + ├── constants/ # Application constants 24 + │ └── auth.ts # Authentication constants 25 + 26 + ├── contexts/ # React contexts 27 + │ ├── ConfirmationModalContext.tsx # Confirmation modal context 28 + │ └── ToastContext.tsx # Toast notifications context 29 + 30 + ├── features/ # Feature-based modules 31 + │ ├── app/ # App-level features 32 + │ │ └── queries/ 33 + │ │ └── app.graphql # App health query 34 + │ │ 35 + │ ├── applications/ # Application flow feature 36 + │ │ ├── components/ 37 + │ │ │ ├── ApplicationFlow.tsx # Main application flow with Progress component 38 + │ │ │ ├── ApplicationsOverview.tsx # Applications list view 39 + │ │ │ ├── CVTemplateSelector.tsx # CV template selection with blue selected state 40 + │ │ │ ├── VacancySelector.tsx # Vacancy selection with blue selected state 41 + │ │ │ ├── VacancyFilterPanel.tsx # Vacancy filtering 42 + │ │ │ └── MatchIndicator.tsx # Match percentage indicator 43 + │ │ └── queries/ 44 + │ │ └── application.graphql # Application queries and mutations 45 + │ │ 46 + │ ├── auth/ # Authentication feature 47 + │ │ ├── queries/ 48 + │ │ │ └── auth.graphql # Authentication queries 49 + │ │ ├── LoginForm.tsx # Login form 50 + │ │ └── RegisterForm.tsx # Registration form 51 + │ │ 52 + │ ├── cv-templates/ # CV template feature 53 + │ │ └── queries/ 54 + │ │ └── cv-template.graphql # CV template queries 55 + │ │ 56 + │ ├── education/ # Education history feature 57 + │ │ ├── queries/ 58 + │ │ │ ├── create-education.graphql # Create education mutation 59 + │ │ │ └── me-education.graphql # User education query 60 + │ │ └── components/ 61 + │ │ ├── EducationForm.tsx # Education form with skills selection 62 + │ │ ├── EducationTable.tsx # Education history table 63 + │ │ ├── SkillsSelect.tsx # Skills selector component 64 + │ │ └── SelectedSkillsDisplay.tsx # Selected skills display 65 + │ │ 66 + │ ├── job-experience/ # Job experience feature 67 + │ │ ├── queries/ # GraphQL queries & mutations 68 + │ │ │ ├── companies-query.graphql 69 + │ │ │ ├── create-job-experience.graphql 70 + │ │ │ ├── delete-job-experience.graphql 71 + │ │ │ ├── job-experience-form-data.graphql 72 + │ │ │ ├── levels-query.graphql 73 + │ │ │ ├── me-job-experience.graphql 74 + │ │ │ ├── roles-query.graphql 75 + │ │ │ └── skills-query.graphql 76 + │ │ └── components/ 77 + │ │ ├── JobExperienceCard.tsx 78 + │ │ ├── JobExperienceCreationSelector.tsx 79 + │ │ ├── JobExperienceEmpty.tsx 80 + │ │ ├── JobExperienceForm.tsx 81 + │ │ ├── JobExperienceHeader.tsx 82 + │ │ ├── JobExperienceList.tsx 83 + │ │ ├── JobExperienceLoading.tsx 84 + │ │ ├── JobExperienceTable.tsx 85 + │ │ ├── jobExperience.schema.ts # Zod schema co-located with form 86 + │ │ └── index.ts 87 + │ │ 88 + │ ├── organizations/ # Organizations feature 89 + │ │ └── components/ 90 + │ │ ├── MembersTableBody.tsx 91 + │ │ ├── MembersTableHeader.tsx 92 + │ │ ├── OrganizationMemberRow.tsx 93 + │ │ ├── OrganizationMembersTable.tsx 94 + │ │ └── CollapsibleOrganizationTable.tsx 95 + │ │ 96 + │ ├── user/ # Shared user queries 97 + │ │ └── queries/ 98 + │ │ ├── me-minimal.graphql 99 + │ │ ├── me-with-organizations.graphql 100 + │ │ └── me.graphql 101 + │ │ 102 + │ └── vacancies/ # Vacancies feature 103 + │ ├── queries/ 104 + │ │ ├── create-vacancy.graphql 105 + │ │ ├── delete-vacancy.graphql 106 + │ │ └── my-vacancies.graphql 107 + │ └── components/ 108 + │ ├── VacancyCard.tsx # Vacancy card with blue selected state 109 + │ ├── VacancyCreationSelector/ 110 + │ │ ├── CreationMethodCard.tsx 111 + │ │ ├── PlaceholderForm.tsx 112 + │ │ ├── VacancyCreationSelector.tsx 113 + │ │ ├── constants.ts 114 + │ │ ├── index.ts 115 + │ │ ├── types.ts 116 + │ │ └── variants.ts 117 + │ ├── VacancyForm.tsx 118 + │ ├── VacancyList.tsx 119 + │ ├── vacancy.schema.ts # Zod schema co-located with form 120 + │ └── index.ts 121 + 122 + ├── generated/ # Generated GraphQL types & hooks 123 + │ ├── graphql.ts # GraphQL types and React Query hooks 124 + │ └── infinite-scroll-hooks.ts # Infinite scroll hooks for connections 125 + 126 + ├── hooks/ 127 + │ ├── useAuth.ts # Authentication hook 128 + │ └── useServerHealth.ts # Server health check hook 129 + 130 + ├── layouts/ 131 + │ └── AuthenticatedLayout.tsx # Layout for authenticated routes 132 + 133 + ├── pages/ 134 + │ ├── CreateJobExperiencePage.tsx 135 + │ ├── CreateVacancyPage.tsx 136 + │ ├── DashboardPage.tsx 137 + │ ├── JobExperiencePage.tsx 138 + │ ├── OrganizationsPage.tsx 139 + │ ├── ProfilePage.tsx 140 + │ ├── VacanciesPage.tsx 141 + │ └── CVsPage.tsx # CV preview page with education and job experience 142 + 143 + ├── providers/ 144 + │ └── TokenProvider.tsx # Token management provider 145 + 146 + ├── router/ 147 + │ └── AppRouter.tsx # Application routing configuration 148 + 149 + ├── types/ 150 + │ ├── auth.ts # Authentication types 151 + │ └── graphql.d.ts # GraphQL type definitions 152 + 153 + ├── utils/ 154 + │ ├── auth.ts # Authentication utilities 155 + │ ├── cn.ts # className utility (clsx + tailwind-merge) 156 + │ ├── config.ts # Application configuration 157 + │ ├── dateUtils.ts # Date formatting utilities 158 + │ └── graphql-fetcher.ts # GraphQL request fetcher for React Query 159 + 160 + ├── App.tsx # Root application component 161 + ├── index.css # Global styles with Tailwind CSS 162 + └── main.tsx # Application entry point 163 + ``` 164 + 165 + ## Conventions 166 + 167 + ### GraphQL Queries & Mutations 168 + 169 + - **Co-location**: GraphQL files are co-located per feature under `features/*/queries/` 170 + - **Naming**: Use kebab-case for file names (e.g., `create-education.graphql`) 171 + - **Codegen**: All `.graphql` files are automatically processed by GraphQL Codegen 172 + 173 + ### Form Validation 174 + 175 + - **Zod schemas**: Co-located with form components (e.g., `*.schema.ts`) 176 + - **Naming**: Schema files match component name (e.g., `EducationForm.tsx` → `education.schema.ts`) 177 + 178 + ### UI Components 179 + 180 + - **Reusable components**: Live in `components/` directory 181 + - **Feature-specific components**: Live in `features/*/components/` 182 + - **UI library**: Shared components in `packages/ui` (imported as `@cv/ui`) 183 + 184 + ### State Management 185 + 186 + - **React Query**: All data fetching via GraphQL Codegen hooks 187 + - **Query invalidation**: Mutations invalidate relevant queries automatically 188 + - **Local state**: Use React `useState` for component-specific state 189 + 190 + ### Styling 191 + 192 + - **Tailwind CSS**: Utility-first CSS framework 193 + - **Catppuccin theme**: Color scheme (ctp-\* classes) 194 + - **CVA**: Class Variance Authority for component variants 195 + - **cn utility**: Merges Tailwind classes with clsx + tailwind-merge 196 + 197 + ### Component Patterns 198 + 199 + #### Progress Component 200 + 201 + The Progress component uses a compound component pattern: 202 + 203 + ```tsx 204 + <Progress> 205 + <Progress.Step name="Step 1">Content</Progress.Step> 206 + <Progress.Step name="Step 2">Content</Progress.Step> 207 + </Progress> 208 + ``` 209 + 210 + - Manages step state internally via context 211 + - Provides navigation (`next`, `back`, `canGoNext`, `canGoBack`) 212 + - Uses CVA for step state styling (selected, completed, pending, error) 213 + - Validates step props with Zod 214 + 215 + #### Query Invalidation Pattern 216 + 217 + All mutations invalidate relevant queries: 218 + 219 + ```tsx 220 + const queryClient = useQueryClient(); 221 + const mutation = useCreateEducationMutation({ 222 + onSuccess: () => { 223 + queryClient.invalidateQueries({ queryKey: ["MeEducation"] }); 224 + }, 225 + }); 226 + ``` 227 + 228 + ### Type Safety 229 + 230 + - **TypeScript**: Strict mode enabled 231 + - **GraphQL types**: Auto-generated from schema 232 + - **Zod schemas**: Runtime validation with TypeScript inference 233 + 234 + ## GraphQL Codegen 235 + 236 + ### Configuration 237 + 238 + - **Source glob**: `src/**/*.graphql` 239 + - **Generated output**: `src/generated/graphql.ts` 240 + - **Client**: React Query hooks generated for queries/mutations 241 + - **Config**: `apps/client/codegen.ts` 242 + 243 + ### Generated Hooks 244 + 245 + - **Queries**: `useMyEducationQuery()`, `useMyJobExperienceQuery()`, etc. 246 + - **Mutations**: `useCreateEducationMutation()`, `useDeleteJobExperienceMutation()`, etc. 247 + - **Infinite queries**: For connection-based queries with pagination 248 + - **Query keys**: Exposed for manual invalidation 249 + 250 + ### Usage Pattern 251 + 252 + ```typescript 253 + // Import generated hooks 254 + import { 255 + useMyEducationQuery, 256 + useCreateEducationMutation, 257 + } from "@/generated/graphql"; 258 + 259 + // Use in components 260 + const { data, loading, error } = useMyEducationQuery(); 261 + const createEducation = useCreateEducationMutation(); 262 + ``` 263 + 264 + ### Regeneration 265 + 266 + ```bash 267 + # From root (runs server schema generation + client codegen) 268 + npm run codegen 269 + 270 + # From client directory (client only) 271 + cd apps/client && npm run codegen 272 + ``` 273 + 274 + **Requirements:** 275 + 276 + - Server must be running and accessible via `GRAPHQL_SCHEMA_URL` 277 + - Server schema must be generated first (`npm run prisma:generate`) 278 + 279 + ## Development Workflow 280 + 281 + ### Adding a New Feature 282 + 283 + 1. **Create feature directory** under `features/` 284 + 2. **Add GraphQL queries/mutations** in `queries/*.graphql` 285 + 3. **Generate types** with `npm run codegen` 286 + 4. **Create components** in `components/` directory 287 + 5. **Add Zod schemas** if forms are needed 288 + 6. **Implement query invalidation** in mutations 289 + 7. **Add route** in `router/AppRouter.tsx` 290 + 291 + ### Styling Guidelines 292 + 293 + - **Use Tailwind utilities** for styling 294 + - **Use CVA** for component variants (e.g., button sizes, colors) 295 + - **Use cn utility** for conditional class merging 296 + - **Follow Catppuccin color scheme** (ctp-\* classes) 297 + - **Maintain consistency** with existing components 298 + 299 + ### Component Guidelines 300 + 301 + - **Use named prop types** with `PropsWithChildren` where applicable 302 + - **Prefer arrow functions** for components 303 + - **Use early returns** for conditional rendering 304 + - **Extract reusable logic** to custom hooks 305 + - **Keep components focused** on single responsibility
+439
apps/docs/content/docs/graphql-architecture.md
··· 1 + # GraphQL Architecture Documentation 2 + 3 + ## Overview 4 + 5 + This document describes the GraphQL architecture additions implemented in the CV Generator project, including Relay-compatible pagination, connection types, and organized file structure. 6 + 7 + ## GraphQL Playground 8 + 9 + Access the interactive GraphQL Playground for testing and exploring the API: 10 + 11 + - **Local Development**: [{{GRAPHQL_URL}}]({{GRAPHQL_URL}}) 12 + - **Features**: Schema explorer, query auto-completion, real-time query execution 13 + 14 + The GraphQL Playground provides an interactive interface to: 15 + 16 + - Explore the complete schema with documentation 17 + - Test queries and mutations 18 + - View query results in real-time 19 + - Access schema introspection 20 + 21 + ## Table of Contents 22 + 23 + 1. [Relay-Compatible Pagination](#relay-compatible-pagination) 24 + 2. [Connection Types](#connection-types) 25 + 3. [File Organization](#file-organization) 26 + 4. [Base Services](#base-services) 27 + 5. [Usage Examples](#usage-examples) 28 + 6. [Best Practices](#best-practices) 29 + 30 + ## Relay-Compatible Pagination 31 + 32 + ### Core Concepts 33 + 34 + The pagination system implements Relay's cursor-based pagination specification, providing efficient and consistent pagination across all GraphQL queries. 35 + 36 + ### Key Components 37 + 38 + #### 1. Pagination Types (`/modules/base/pagination.types.ts`) 39 + 40 + ```typescript 41 + @ObjectType() 42 + export class PageInfo { 43 + @Field(() => Boolean) 44 + hasNextPage: boolean; 45 + 46 + @Field(() => Boolean) 47 + hasPreviousPage: boolean; 48 + 49 + @Field(() => GraphQLString, { nullable: true }) 50 + startCursor: string | null; 51 + 52 + @Field(() => GraphQLString, { nullable: true }) 53 + endCursor: string | null; 54 + } 55 + 56 + @InputType() 57 + export abstract class BasePaginationArgs { 58 + @Field(() => Int, { nullable: true }) 59 + first?: number | null; 60 + 61 + @Field(() => GraphQLString, { nullable: true }) 62 + after?: string | null; 63 + 64 + @Field(() => Int, { nullable: true }) 65 + last?: number | null; 66 + 67 + @Field(() => GraphQLString, { nullable: true }) 68 + before?: string | null; 69 + } 70 + ``` 71 + 72 + #### 2. Pagination Result Interface 73 + 74 + ```typescript 75 + export interface PaginationResult<T> { 76 + edges: Array<{ 77 + node: T; 78 + cursor: string; 79 + }>; 80 + pageInfo: PageInfo; 81 + totalCount: number; 82 + } 83 + ``` 84 + 85 + ### Pagination Service (`/modules/base/pagination.service.ts`) 86 + 87 + The `PaginationService` handles: 88 + 89 + - Parsing GraphQL pagination arguments 90 + - Building pagination results with cursors 91 + - Managing page info (hasNextPage, hasPreviousPage, etc.) 92 + 93 + ```typescript 94 + @Injectable() 95 + export class PaginationService { 96 + constructor(private cursorService: CursorService) {} 97 + 98 + parsePaginationArgs(args: { 99 + first?: number | null; 100 + after?: string | null; 101 + last?: number | null; 102 + before?: string | null; 103 + searchTerm?: string | null; 104 + }): PaginationOptions; 105 + 106 + buildPaginationResult<T>( 107 + items: T[], 108 + totalCount: number, 109 + options: PaginationOptions, 110 + getId: (item: T) => string, 111 + ): PaginationResult<T>; 112 + } 113 + ``` 114 + 115 + ### Cursor Service (`/modules/base/cursor.service.ts`) 116 + 117 + Handles encoding and decoding of pagination cursors: 118 + 119 + ```typescript 120 + @Injectable() 121 + export class CursorService { 122 + encode(id: string): string; 123 + decode(cursor: string): string; 124 + } 125 + ``` 126 + 127 + ## Connection Types 128 + 129 + ### Base Edge Class (`/modules/base/connection.types.ts`) 130 + 131 + ```typescript 132 + export abstract class BaseEdge<T> { 133 + cursor: string; 134 + node: T; 135 + 136 + constructor(cursor: string, node: T) { 137 + this.cursor = cursor; 138 + this.node = node; 139 + } 140 + 141 + static fromPaginationEdge<T, TEdge extends BaseEdge<T>>( 142 + this: new (cursor: string, node: T) => TEdge, 143 + edge: { node: T; cursor: string }, 144 + ): TEdge; 145 + } 146 + ``` 147 + 148 + ### Concrete Edge Types 149 + 150 + Each domain module has its own edge type extending `BaseEdge`: 151 + 152 + ```typescript 153 + // Example: CompanyEdge 154 + @ObjectType() 155 + export class CompanyEdge extends BaseEdge<Company> { 156 + @Field(() => GraphQLString) 157 + declare cursor: string; 158 + 159 + @Field(() => Company) 160 + declare node: Company; 161 + } 162 + ``` 163 + 164 + ### Connection Types 165 + 166 + Each domain module has its own connection type: 167 + 168 + ```typescript 169 + // Example: CompanyConnection 170 + @ObjectType() 171 + export class CompanyConnection { 172 + @Field(() => [CompanyEdge]) 173 + edges: CompanyEdge[]; 174 + 175 + @Field(() => PageInfo) 176 + pageInfo: PageInfo; 177 + 178 + @Field(() => Int) 179 + totalCount: number; 180 + 181 + static fromPaginationResult( 182 + result: PaginationResult<CompanyDomain>, 183 + ): CompanyConnection; 184 + } 185 + ``` 186 + 187 + ### Connection Arguments 188 + 189 + Each domain module has specific connection arguments: 190 + 191 + ```typescript 192 + // Example: CompanyConnectionArgs 193 + @ArgsType() 194 + export class CompanyConnectionArgs extends BasePaginationArgs { 195 + @Field(() => GraphQLString, { nullable: true }) 196 + searchTerm?: string | null; 197 + } 198 + ``` 199 + 200 + ## File Organization 201 + 202 + ### GraphQL Subfolder Structure 203 + 204 + All GraphQL-related files are organized in `graphql/` subfolders within each domain module: 205 + 206 + ``` 207 + src/modules/ 208 + ├── job-experience/ 209 + │ ├── company/ 210 + │ │ ├── graphql/ 211 + │ │ │ ├── company-connection-args.type.ts 212 + │ │ │ ├── company-connection.type.ts 213 + │ │ │ ├── company-edge.type.ts 214 + │ │ │ ├── company.resolver.ts 215 + │ │ │ └── company.type.ts 216 + │ │ ├── company.entity.ts 217 + │ │ ├── company.service.ts 218 + │ │ └── company.module.ts 219 + │ ├── role/ 220 + │ ├── level/ 221 + │ └── skill/ 222 + ├── employment/ 223 + │ ├── graphql/ 224 + │ │ ├── employment.resolver.ts 225 + │ │ ├── user-field.resolver.ts 226 + │ │ └── user-job-experience.type.ts 227 + │ └── [other files...] 228 + ├── vacancies/ 229 + │ ├── graphql/ 230 + │ │ ├── vacancy.resolver.ts 231 + │ │ └── vacancy.type.ts 232 + │ └── [other files...] 233 + └── organization/ 234 + ├── graphql/ 235 + │ ├── organization.resolver.ts 236 + │ ├── user-field.resolver.ts 237 + │ └── user-organization.type.ts 238 + └── [other files...] 239 + ``` 240 + 241 + ### Benefits of This Organization 242 + 243 + 1. **Clear Separation**: GraphQL concerns are separated from business logic 244 + 2. **Consistent Structure**: All modules follow the same organization pattern 245 + 3. **Easy Navigation**: GraphQL files are grouped together 246 + 4. **Scalable**: New modules can follow the same pattern 247 + 248 + ## Base Services 249 + 250 + ### Base Module (`/modules/base/base.module.ts`) 251 + 252 + Provides common services for pagination: 253 + 254 + ```typescript 255 + @Module({ 256 + providers: [PaginationService, CursorService], 257 + exports: [PaginationService, CursorService], 258 + }) 259 + export class BaseModule {} 260 + ``` 261 + 262 + ### Service Integration 263 + 264 + Domain services inject pagination services: 265 + 266 + ```typescript 267 + @Injectable() 268 + export class CompanyService { 269 + constructor( 270 + private prisma: PrismaService, 271 + private companyMapper: CompanyMapper, 272 + private paginationService: PaginationService, 273 + private cursorService: CursorService, 274 + ) {} 275 + 276 + async findPaginated( 277 + options: PaginationOptions, 278 + searchTerm?: string, 279 + ): Promise<PaginationResult<Company>>; 280 + } 281 + ``` 282 + 283 + ## Usage Examples 284 + 285 + ### GraphQL Query Example 286 + 287 + ```graphql 288 + query CompaniesConnection($searchTerm: String) { 289 + companies(searchTerm: $searchTerm) { 290 + edges { 291 + cursor 292 + node { 293 + id 294 + name 295 + description 296 + createdAt 297 + updatedAt 298 + } 299 + } 300 + pageInfo { 301 + hasNextPage 302 + hasPreviousPage 303 + startCursor 304 + endCursor 305 + } 306 + totalCount 307 + } 308 + } 309 + ``` 310 + 311 + ### Resolver Implementation 312 + 313 + ```typescript 314 + @Resolver(() => Company) 315 + export class CompanyResolver { 316 + constructor(private readonly companyService: CompanyService) {} 317 + 318 + @Query(() => CompanyConnection) 319 + async companies( 320 + @Args() args: CompanyConnectionArgs = {}, 321 + ): Promise<CompanyConnection> { 322 + const options = 323 + this.companyService["paginationService"].parsePaginationArgs(args); 324 + const result = await this.companyService.findPaginated( 325 + options, 326 + args.searchTerm || undefined, 327 + ); 328 + 329 + return CompanyConnection.fromPaginationResult(result); 330 + } 331 + } 332 + ``` 333 + 334 + ### Frontend Usage 335 + 336 + ```typescript 337 + // Using the generated GraphQL hooks 338 + const { data, loading, fetchMore } = useCompaniesConnectionQuery({ 339 + variables: { searchTerm: "tech" }, 340 + }); 341 + 342 + // Accessing paginated data 343 + const companies = data?.companies?.edges?.map((edge) => edge.node) || []; 344 + const pageInfo = data?.companies?.pageInfo; 345 + const totalCount = data?.companies?.totalCount; 346 + ``` 347 + 348 + ## Best Practices 349 + 350 + ### 1. Consistent Naming 351 + 352 + - Connection types: `{Entity}Connection` 353 + - Edge types: `{Entity}Edge` 354 + - Connection args: `{Entity}ConnectionArgs` 355 + - GraphQL types: `{Entity}` (in graphql folder) 356 + 357 + ### 2. Import Paths 358 + 359 + When files are in `graphql/` subfolders: 360 + 361 + - Import services: `../service.service` 362 + - Import entities: `../entity.entity` 363 + - Import base types: `../../../base/pagination.types` 364 + - Import other GraphQL types: `../../other-module/graphql/type.type` 365 + 366 + ### 3. Type Safety 367 + 368 + - Use specific domain entity types in connection factories 369 + - Avoid `any` types in pagination services 370 + - Use proper TypeScript generics for reusable components 371 + 372 + ### 4. Error Handling 373 + 374 + - Implement proper error handling in resolvers 375 + - Use early returns for validation 376 + - Handle pagination edge cases (empty results, invalid cursors) 377 + 378 + ### 5. Performance Considerations 379 + 380 + - Use database-level pagination (LIMIT/OFFSET or cursor-based) 381 + - Implement proper indexing for search fields 382 + - Consider caching for frequently accessed data 383 + 384 + ## Migration Guide 385 + 386 + ### From Array-Based Queries 387 + 388 + **Before:** 389 + 390 + ```typescript 391 + // Old array-based query 392 + @Query(() => [Company]) 393 + async companies(): Promise<Company[]> { 394 + return this.companyService.findAll(); 395 + } 396 + ``` 397 + 398 + **After:** 399 + 400 + ```typescript 401 + // New connection-based query 402 + @Query(() => CompanyConnection) 403 + async companies( 404 + @Args() args: CompanyConnectionArgs = {}, 405 + ): Promise<CompanyConnection> { 406 + const options = this.companyService["paginationService"].parsePaginationArgs(args); 407 + const result = await this.companyService.findPaginated(options, args.searchTerm || undefined); 408 + 409 + return CompanyConnection.fromPaginationResult(result); 410 + } 411 + ``` 412 + 413 + ### Frontend Updates 414 + 415 + **Before:** 416 + 417 + ```typescript 418 + const { data } = useCompaniesQuery(); 419 + const companies = data?.companies || []; 420 + ``` 421 + 422 + **After:** 423 + 424 + ```typescript 425 + const { data } = useCompaniesConnectionQuery(); 426 + const companies = data?.companies?.edges?.map((edge) => edge.node) || []; 427 + ``` 428 + 429 + ## Conclusion 430 + 431 + This GraphQL architecture provides: 432 + 433 + 1. **Scalable Pagination**: Relay-compatible cursor-based pagination 434 + 2. **Type Safety**: Strong typing throughout the system 435 + 3. **Consistent Structure**: Organized file structure across all modules 436 + 4. **Reusable Components**: Base classes and services for common functionality 437 + 5. **Search Integration**: Built-in search capabilities with pagination 438 + 439 + The architecture supports efficient data fetching, consistent user experience, and maintainable code organization across the entire application.
+74
apps/docs/content/docs/graphql-query-analysis.md
··· 1 + # GraphQL Query Analysis: Root vs User Resource 2 + 3 + ## Current Root-Level Queries 4 + 5 + ### User-Specific Queries (Should Move to User Resource) 6 + 7 + These queries are user-specific and should be accessible via `me { ... }` or `user(id) { ... }`: 8 + 9 + 1. **`myEducationHistory`** → Should be `me.educationHistory` 10 + 11 + - Location: `EducationResolver` 12 + - Current: `Query.myEducationHistory` 13 + - Should be: `User.educationHistory` (field resolver) 14 + 15 + 2. **`myApplications`** → Should be `me.applications` 16 + 17 + - Location: `ApplicationResolver` 18 + - Current: `Query.myApplications` 19 + - Should be: `User.applications` (field resolver) 20 + 21 + 3. **`myVacancies`** → Should be `me.vacancies` 22 + 23 + - Location: `VacancyResolver` 24 + - Current: `Query.myVacancies` 25 + - Should be: `User.vacancies` (field resolver) 26 + 27 + 4. **`myCVs`** → Should be `me.cvs` 28 + 29 + - Location: `CVTemplateResolver` 30 + - Current: `Query.myCVs` 31 + - Should be: `User.cvs` (field resolver) 32 + 33 + 5. **`myEmploymentHistory`** → Already has field resolver 34 + - Location: `EmploymentResolver` (root query) + `UserFieldResolver` (field resolver) 35 + - Current: Both `Query.myEmploymentHistory` AND `User.experience` 36 + - **Recommendation**: Remove root query, keep `User.experience` field 37 + 38 + ### Non-User-Specific Queries (Should Stay at Root) 39 + 40 + These are reference data or ID-based lookups, appropriate at root level: 41 + 42 + 1. **`institutions`** - Reference data query (all institutions) 43 + 2. **`organization(id)`** - Fetch organization by ID 44 + 3. **`cvTemplates`** - Reference data query (all templates) 45 + 4. **`cvTemplate(id)`** - Fetch template by ID 46 + 5. **`application(id)`** - Fetch application by ID (with ownership check) 47 + 6. **`cv(id)`** - Fetch CV by ID (with ownership check) 48 + 7. **`me`** - Convenience query for current user (can stay at root) 49 + 50 + ## Recommended Changes 51 + 52 + ### Pattern to Follow 53 + 54 + - **User-specific data**: Access via `me { ... }` or `user(id) { ... }` 55 + - **Reference data**: Keep at root level 56 + - **ID-based lookups**: Keep at root level 57 + 58 + ### Migration Path 59 + 60 + 1. Add field resolvers to `User` type for: 61 + - `educationHistory: [Education]` 62 + - `applications: ApplicationConnection` 63 + - `vacancies: [Vacancy]` 64 + - `cvs: CVConnection` 65 + 2. Keep root queries for backward compatibility during migration 66 + 3. Update frontend to use `me { educationHistory }` instead of `myEducationHistory` 67 + 4. Remove deprecated root queries after migration 68 + 69 + ### Benefits 70 + 71 + - Better organization: user data under user resource 72 + - More REST-like structure 73 + - Easier to query multiple user resources in one query 74 + - Consistent with existing `User.experience` and `User.organizations` fields
+16
apps/docs/index.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <link rel="icon" type="image/svg+xml" href="/vite.svg" /> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 + <title>CV Generator - Documentation</title> 8 + </head> 9 + <body> 10 + <div id="root"></div> 11 + <script type="module" src="/src/main.tsx"></script> 12 + </body> 13 + </html> 14 + 15 + 16 +
+36
apps/docs/nginx.conf
··· 1 + server { 2 + listen 80; 3 + server_name localhost; 4 + root /usr/share/nginx/html; 5 + index index.html; 6 + 7 + # Enable gzip compression 8 + gzip on; 9 + gzip_vary on; 10 + gzip_min_length 1024; 11 + gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json; 12 + 13 + # Security headers 14 + add_header X-Frame-Options "SAMEORIGIN" always; 15 + add_header X-Content-Type-Options "nosniff" always; 16 + add_header X-XSS-Protection "1; mode=block" always; 17 + 18 + # Serve markdown files 19 + location ~ \.md$ { 20 + add_header Content-Type text/plain; 21 + } 22 + 23 + # Handle client-side routing 24 + location / { 25 + try_files $uri $uri/ /index.html; 26 + } 27 + 28 + # Cache static assets 29 + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { 30 + expires 1y; 31 + add_header Cache-Control "public, immutable"; 32 + } 33 + } 34 + 35 + 36 +
+41
apps/docs/package.json
··· 1 + { 2 + "name": "@cv/docs", 3 + "version": "0.0.0", 4 + "private": true, 5 + "type": "module", 6 + "scripts": { 7 + "dev": "vite", 8 + "build": "tsc && vite build", 9 + "preview": "vite preview", 10 + "lint": "biome check .", 11 + "lint:fix": "biome check --write ." 12 + }, 13 + "dependencies": { 14 + "@catppuccin/palette": "^1.4.0", 15 + "@cv/ui": "*", 16 + "@mdx-js/react": "^3.1.1", 17 + "@mdx-js/rollup": "^3.1.1", 18 + "@types/mdx": "^2.0.13", 19 + "highlight.js": "^11.10.0", 20 + "react": "^18.3.1", 21 + "react-dom": "^18.3.1", 22 + "react-markdown": "^9.0.1", 23 + "react-router-dom": "^6.26.1", 24 + "rehype-highlight": "^7.0.0", 25 + "remark-gfm": "^4.0.0" 26 + }, 27 + "devDependencies": { 28 + "@tailwindcss/postcss": "^4.0.0", 29 + "@tailwindcss/vite": "^4.0.0", 30 + "@types/mdast": "^4.0.4", 31 + "@types/react": "^18.3.5", 32 + "@types/react-dom": "^18.3.0", 33 + "@types/unist": "^3.0.3", 34 + "@vitejs/plugin-react": "^4.3.1", 35 + "autoprefixer": "^10.4.20", 36 + "postcss": "^8.4.47", 37 + "tailwindcss": "^4.0.0", 38 + "typescript": "^5.5.3", 39 + "vite": "^5.4.3" 40 + } 41 + }
+5
apps/docs/postcss.config.cjs
··· 1 + module.exports = { 2 + plugins: { 3 + "@tailwindcss/postcss": {}, 4 + }, 5 + };
+14
apps/docs/src/App.tsx
··· 1 + import { Navigate, Route, Routes } from "react-router-dom"; 2 + import { DocPage } from "./components/DocPage"; 3 + import { Layout } from "./components/Layout"; 4 + 5 + export const App = () => { 6 + return ( 7 + <Routes> 8 + <Route path="/" element={<Layout />}> 9 + <Route index element={<Navigate to="/docs/architecture" replace />} /> 10 + <Route path=":slug/*" element={<DocPage />} /> 11 + </Route> 12 + </Routes> 13 + ); 14 + };
+42
apps/docs/src/components/ContentRenderer.tsx
··· 1 + import { Suspense, useMemo } from "react"; 2 + import { useLocation } from "react-router-dom"; 3 + import { getDocComponent } from "../config"; 4 + import { MDXRenderer } from "./MDXRenderer"; 5 + 6 + export const ContentRenderer = () => { 7 + const location = useLocation(); 8 + 9 + // Get the full path from the location 10 + const fullPath = location.pathname.slice(1); // Remove leading slash 11 + 12 + // Get MDX component (both .md and .mdx files are processed as MDX) 13 + const mdxComponent = useMemo( 14 + () => (fullPath ? getDocComponent(fullPath) : null), 15 + [fullPath], 16 + ); 17 + 18 + if (!fullPath) { 19 + return ( 20 + <div className="p-4 bg-ctp-surface0 border border-ctp-red rounded-lg"> 21 + <h2 className="text-lg font-semibold text-ctp-red mb-2">Error</h2> 22 + <p className="text-ctp-text">No document specified</p> 23 + </div> 24 + ); 25 + } 26 + 27 + if (!mdxComponent) { 28 + return ( 29 + <div className="p-4 bg-ctp-surface0 border border-ctp-red rounded-lg"> 30 + <h2 className="text-lg font-semibold text-ctp-red mb-2">Error</h2> 31 + <p className="text-ctp-text">Document not found: {fullPath}</p> 32 + </div> 33 + ); 34 + } 35 + 36 + // Render MDX component 37 + return ( 38 + <Suspense fallback={<div className="p-4">Loading...</div>}> 39 + <MDXRenderer content={mdxComponent} /> 40 + </Suspense> 41 + ); 42 + };
+5
apps/docs/src/components/DocPage.tsx
··· 1 + import { ContentRenderer } from "./ContentRenderer"; 2 + 3 + export const DocPage = () => { 4 + return <ContentRenderer />; 5 + };
+91
apps/docs/src/components/HeadingWithAnchor.tsx
··· 1 + import { cn, LinkIcon } from "@cv/ui"; 2 + import { type PropsWithChildren, useState } from "react"; 3 + 4 + interface HeadingWithAnchorProps extends PropsWithChildren { 5 + level: 1 | 2 | 3 | 4 | 5 | 6; 6 + className?: string; 7 + } 8 + 9 + const generateId = (text: string): string => { 10 + return text 11 + .toLowerCase() 12 + .replace(/[^a-z0-9]+/g, "-") 13 + .replace(/^-|-$/g, ""); 14 + }; 15 + 16 + const copyToClipboard = async (text: string): Promise<boolean> => { 17 + try { 18 + await navigator.clipboard.writeText(text); 19 + return true; 20 + } catch { 21 + return false; 22 + } 23 + }; 24 + 25 + export const HeadingWithAnchor = ({ 26 + children, 27 + level, 28 + className, 29 + }: HeadingWithAnchorProps) => { 30 + const [copied, setCopied] = useState(false); 31 + 32 + // Extract text content from React children recursively 33 + const extractText = (node: React.ReactNode): string => { 34 + if (typeof node === "string" || typeof node === "number") { 35 + return String(node); 36 + } 37 + if (Array.isArray(node)) { 38 + return node.map(extractText).join(""); 39 + } 40 + if (node && typeof node === "object" && "props" in node) { 41 + return extractText(node.props.children); 42 + } 43 + return ""; 44 + }; 45 + 46 + const textContent = extractText(children); 47 + const id = generateId(textContent); 48 + 49 + const handleCopy = async () => { 50 + const url = `${window.location.origin}${window.location.pathname}#${id}`; 51 + const success = await copyToClipboard(url); 52 + if (success) { 53 + setCopied(true); 54 + setTimeout(() => { 55 + setCopied(false); 56 + }, 2000); 57 + } 58 + }; 59 + 60 + const HeadingTag = `h${level}` as keyof JSX.IntrinsicElements; 61 + const baseClasses = 62 + level === 1 63 + ? "text-3xl font-bold mt-8 mb-4 text-ctp-text" 64 + : level === 2 65 + ? "text-2xl font-semibold mt-6 mb-3 text-ctp-text" 66 + : level === 3 67 + ? "text-xl font-semibold mt-4 mb-2 text-ctp-text" 68 + : level === 4 69 + ? "text-lg font-semibold mt-3 mb-2 text-ctp-text" 70 + : level === 5 71 + ? "text-base font-semibold mt-2 mb-1 text-ctp-text" 72 + : "text-sm font-semibold mt-2 mb-1 text-ctp-text"; 73 + 74 + return ( 75 + <HeadingTag 76 + id={id} 77 + className={cn("group relative", baseClasses, className)} 78 + > 79 + <span>{children}</span> 80 + <button 81 + type="button" 82 + onClick={handleCopy} 83 + className="ml-2 inline-flex items-center opacity-0 group-hover:opacity-100 transition-opacity text-ctp-subtext0 hover:text-ctp-text" 84 + aria-label={copied ? "Link copied!" : "Copy link to heading"} 85 + > 86 + <LinkIcon /> 87 + {copied && <span className="ml-1 text-xs text-ctp-green">Copied!</span>} 88 + </button> 89 + </HeadingTag> 90 + ); 91 + };
+15
apps/docs/src/components/Layout.tsx
··· 1 + import { Outlet } from "react-router-dom"; 2 + import { Sidebar } from "./Sidebar"; 3 + 4 + export const Layout = () => { 5 + return ( 6 + <div className="flex min-h-screen bg-ctp-base"> 7 + <Sidebar /> 8 + <main className="flex-1 p-8"> 9 + <div className="max-w-4xl mx-auto"> 10 + <Outlet /> 11 + </div> 12 + </main> 13 + </div> 14 + ); 15 + };
+280
apps/docs/src/components/MDXProvider.tsx
··· 1 + import { 2 + Badge, 3 + Button, 4 + Calendar, 5 + Checkbox, 6 + cn, 7 + FormattedDate, 8 + FormattedDateRange, 9 + IconButton, 10 + PageHeader, 11 + Placeholder, 12 + RangeSlider, 13 + SearchableSelect, 14 + Select, 15 + StatusBadge, 16 + Table, 17 + TableBody, 18 + TableCell, 19 + TableHeader, 20 + TableHeaderCell, 21 + TableRow, 22 + Textarea, 23 + TextInput, 24 + } from "@cv/ui"; 25 + import { MDXProvider } from "@mdx-js/react"; 26 + import React from "react"; 27 + import { ComponentExample, ExampleStateProvider } from "./ComponentExample"; 28 + import { HeadingWithAnchor } from "./HeadingWithAnchor"; 29 + 30 + const components = { 31 + // Default markdown components with copy-to-clipboard functionality 32 + h1: ({ children, className }: React.ComponentPropsWithoutRef<"h1">) => ( 33 + <HeadingWithAnchor level={1} className={className}> 34 + {children} 35 + </HeadingWithAnchor> 36 + ), 37 + h2: ({ children, className }: React.ComponentPropsWithoutRef<"h2">) => ( 38 + <HeadingWithAnchor level={2} className={className}> 39 + {children} 40 + </HeadingWithAnchor> 41 + ), 42 + h3: ({ children, className }: React.ComponentPropsWithoutRef<"h3">) => ( 43 + <HeadingWithAnchor level={3} className={className}> 44 + {children} 45 + </HeadingWithAnchor> 46 + ), 47 + h4: ({ children, className }: React.ComponentPropsWithoutRef<"h4">) => ( 48 + <HeadingWithAnchor level={4} className={className}> 49 + {children} 50 + </HeadingWithAnchor> 51 + ), 52 + h5: ({ children, className }: React.ComponentPropsWithoutRef<"h5">) => ( 53 + <HeadingWithAnchor level={5} className={className}> 54 + {children} 55 + </HeadingWithAnchor> 56 + ), 57 + h6: ({ children, className }: React.ComponentPropsWithoutRef<"h6">) => ( 58 + <HeadingWithAnchor level={6} className={className}> 59 + {children} 60 + </HeadingWithAnchor> 61 + ), 62 + p: ({ 63 + children, 64 + className, 65 + ...props 66 + }: React.HTMLProps<HTMLParagraphElement>) => ( 67 + <p 68 + className={cn("mb-4 text-ctp-text leading-relaxed", className)} 69 + {...props} 70 + > 71 + {children} 72 + </p> 73 + ), 74 + ul: ({ 75 + children, 76 + className, 77 + ...props 78 + }: React.HTMLProps<HTMLUListElement>) => ( 79 + <ul 80 + className={cn("list-disc list-inside mb-4 ml-4 text-ctp-text", className)} 81 + {...props} 82 + > 83 + {children} 84 + </ul> 85 + ), 86 + ol: ({ 87 + children, 88 + className, 89 + ...props 90 + }: React.ComponentPropsWithoutRef<"ol">) => ( 91 + <ol 92 + className={cn( 93 + "list-decimal list-inside mb-4 ml-4 text-ctp-text", 94 + className, 95 + )} 96 + {...props} 97 + > 98 + {children} 99 + </ol> 100 + ), 101 + li: ({ children, className, ...props }: React.HTMLProps<HTMLLIElement>) => ( 102 + <li className={cn("mb-1 text-ctp-text", className)} {...props}> 103 + {children} 104 + </li> 105 + ), 106 + blockquote: ({ 107 + children, 108 + className, 109 + ...props 110 + }: React.HTMLProps<HTMLQuoteElement>) => ( 111 + <blockquote 112 + className={cn( 113 + "border-l-4 border-ctp-blue pl-4 italic my-4 text-ctp-subtext1", 114 + className, 115 + )} 116 + {...props} 117 + > 118 + {children} 119 + </blockquote> 120 + ), 121 + code: ({ children, className, ...props }: React.HTMLProps<HTMLElement>) => { 122 + const isInline = !className?.includes("language-"); 123 + const hasHljs = className?.includes("hljs"); 124 + 125 + return isInline ? ( 126 + <code 127 + className={cn( 128 + "bg-ctp-surface0 px-1.5 py-0.5 rounded text-ctp-red text-sm font-mono", 129 + className, 130 + )} 131 + {...props} 132 + > 133 + {children} 134 + </code> 135 + ) : ( 136 + <code 137 + className={cn( 138 + hasHljs 139 + ? "" 140 + : "bg-ctp-surface0 px-1.5 py-0.5 rounded text-ctp-red text-sm font-mono", 141 + className, 142 + )} 143 + {...props} 144 + > 145 + {children} 146 + </code> 147 + ); 148 + }, 149 + pre: ({ children, className, ...props }: React.HTMLProps<HTMLPreElement>) => { 150 + // Check if this pre contains a code element with hljs class (syntax highlighted) 151 + const hasHljs = 152 + React.isValidElement(children) && 153 + children.props?.className?.includes("hljs"); 154 + 155 + return ( 156 + <pre 157 + className={cn( 158 + hasHljs 159 + ? "bg-ctp-mantle p-4 rounded-lg overflow-x-auto mb-4 border border-ctp-surface0" 160 + : "bg-ctp-base p-4 rounded-lg overflow-x-auto mb-4 border border-ctp-surface0", 161 + className, 162 + )} 163 + {...props} 164 + > 165 + {children} 166 + </pre> 167 + ); 168 + }, 169 + a: ({ 170 + children, 171 + className, 172 + ...props 173 + }: React.HTMLProps<HTMLAnchorElement>) => ( 174 + <a 175 + className={cn( 176 + "text-ctp-blue hover:text-ctp-sapphire underline", 177 + className, 178 + )} 179 + {...props} 180 + > 181 + {children} 182 + </a> 183 + ), 184 + hr: ({ className, ...props }: React.HTMLProps<HTMLHRElement>) => ( 185 + <hr className={cn("my-8 border-ctp-surface0", className)} {...props} /> 186 + ), 187 + table: ({ 188 + children, 189 + className, 190 + ...props 191 + }: React.HTMLProps<HTMLTableElement>) => ( 192 + <div className="overflow-x-auto my-4"> 193 + <table className={cn("min-w-full border-collapse", className)} {...props}> 194 + {children} 195 + </table> 196 + </div> 197 + ), 198 + thead: ({ 199 + children, 200 + className, 201 + ...props 202 + }: React.HTMLProps<HTMLTableSectionElement>) => ( 203 + <thead className={cn("bg-ctp-surface0", className)} {...props}> 204 + {children} 205 + </thead> 206 + ), 207 + tbody: ({ 208 + children, 209 + className, 210 + ...props 211 + }: React.HTMLProps<HTMLTableSectionElement>) => ( 212 + <tbody className={className} {...props}> 213 + {children} 214 + </tbody> 215 + ), 216 + tr: ({ 217 + children, 218 + className, 219 + ...props 220 + }: React.HTMLProps<HTMLTableRowElement>) => ( 221 + <tr className={cn("border-b border-ctp-surface0", className)} {...props}> 222 + {children} 223 + </tr> 224 + ), 225 + th: ({ 226 + children, 227 + className, 228 + ...props 229 + }: React.HTMLProps<HTMLTableCellElement>) => ( 230 + <th 231 + className={cn( 232 + "px-4 py-2 text-left font-semibold text-ctp-text", 233 + className, 234 + )} 235 + {...props} 236 + > 237 + {children} 238 + </th> 239 + ), 240 + td: ({ 241 + children, 242 + className, 243 + ...props 244 + }: React.HTMLProps<HTMLTableCellElement>) => ( 245 + <td className={cn("px-4 py-2 text-ctp-text", className)} {...props}> 246 + {children} 247 + </td> 248 + ), 249 + // Custom UI components 250 + Badge, 251 + Button, 252 + Calendar, 253 + Checkbox, 254 + FormattedDate, 255 + FormattedDateRange, 256 + IconButton, 257 + PageHeader, 258 + Placeholder, 259 + RangeSlider, 260 + SearchableSelect, 261 + Select, 262 + StatusBadge, 263 + Table, 264 + TableHeader, 265 + TableBody, 266 + TableRow, 267 + TableHeaderCell, 268 + TableCell, 269 + Textarea, 270 + TextInput, 271 + // Wrapper components for examples 272 + ComponentExample, 273 + ExampleStateProvider, 274 + }; 275 + 276 + interface CustomMDXProviderProps extends React.PropsWithChildren {} 277 + 278 + export const CustomMDXProvider = ({ children }: CustomMDXProviderProps) => { 279 + return <MDXProvider components={components}>{children}</MDXProvider>; 280 + };
+15
apps/docs/src/components/MDXRenderer.tsx
··· 1 + import { CustomMDXProvider } from "./MDXProvider"; 2 + 3 + interface MDXRendererProps { 4 + content: React.ComponentType; 5 + } 6 + 7 + export const MDXRenderer = ({ content: Content }: MDXRendererProps) => { 8 + return ( 9 + <div className="markdown"> 10 + <CustomMDXProvider> 11 + <Content /> 12 + </CustomMDXProvider> 13 + </div> 14 + ); 15 + };
+111
apps/docs/src/config/docs.config.ts
··· 1 + import type { ComponentType } from "react"; 2 + 3 + /** 4 + * Type guard to check if a value is a React component 5 + */ 6 + const isReactComponent = (value: unknown): value is ComponentType => { 7 + return typeof value === "function"; 8 + }; 9 + 10 + /** 11 + * Type guard to check if a value is an object with a default property 12 + */ 13 + const hasDefaultProperty = (value: unknown): value is { default: unknown } => { 14 + return ( 15 + value !== null && 16 + typeof value === "object" && 17 + "default" in value && 18 + value.default !== undefined 19 + ); 20 + }; 21 + 22 + /** 23 + * Extract MDX component from a module (for both .md and .mdx files) 24 + * Handles both direct component exports and default exports 25 + */ 26 + const extractMDXComponent = (module: unknown): ComponentType | null => { 27 + if (isReactComponent(module)) { 28 + return module; 29 + } 30 + 31 + if (hasDefaultProperty(module)) { 32 + const defaultExport = module.default; 33 + if (isReactComponent(defaultExport)) { 34 + return defaultExport; 35 + } 36 + } 37 + 38 + return null; 39 + }; 40 + 41 + /** 42 + * Auto-discovered markdown and MDX files as MDX components 43 + * Both .md and .mdx files are processed by the MDX plugin 44 + */ 45 + const mdxModules = import.meta.glob("../../content/**/*.{md,mdx}", { 46 + eager: true, 47 + }); 48 + 49 + /** 50 + * Get the MDX component for a given documentation slug 51 + * 52 + * Normalizes the slug and searches for matching files in the content directory. 53 + * Handles README files as index files (e.g., "api/readme" matches slug "api"). 54 + * 55 + * @param slug - The documentation slug (e.g., "docs/architecture", "components/button") 56 + * @returns The React component for the documentation page, or null if not found 57 + */ 58 + export const getDocComponent = (slug: string): ComponentType | null => { 59 + const normalizedSlug = slug.toLowerCase().replace(/^\/+|\/+$/g, ""); 60 + 61 + for (const [path, module] of Object.entries(mdxModules)) { 62 + const normalizedPath = path 63 + .replace(/^\.\.\/\.\.\/content\//, "") 64 + .replace(/\.(md|mdx)$/, "") 65 + .toLowerCase(); 66 + 67 + let matches = normalizedPath === normalizedSlug; 68 + 69 + if (!matches && normalizedPath.endsWith("/readme")) { 70 + const dirPath = normalizedPath.replace(/\/readme$/, ""); 71 + matches = 72 + dirPath === normalizedSlug || dirPath === `${normalizedSlug}/readme`; 73 + } 74 + 75 + if (!matches && normalizedPath === `${normalizedSlug}/readme`) { 76 + matches = true; 77 + } 78 + 79 + if (matches) { 80 + const component = extractMDXComponent(module); 81 + 82 + if (component) { 83 + return component; 84 + } 85 + } 86 + } 87 + 88 + return null; 89 + }; 90 + 91 + /** 92 + * Get a list of all available documentation slugs 93 + * 94 + * Extracts slugs from all discovered markdown and MDX files, 95 + * excluding README files and empty slugs. 96 + * 97 + * @returns Array of normalized documentation slugs 98 + */ 99 + export const getAvailableDocs = (): string[] => { 100 + return Object.keys(mdxModules) 101 + .map((path) => { 102 + const normalizedPath = path 103 + .replace(/^\.\.\/\.\.\/content\//, "") 104 + .replace(/\.(md|mdx)$/, ""); 105 + return normalizedPath.toLowerCase(); 106 + }) 107 + .filter((slug) => { 108 + const filename = slug.split("/").pop() ?? ""; 109 + return filename !== "readme" && slug.length > 0; 110 + }); 111 + };
+16
apps/docs/src/config/index.ts
··· 1 + /** 2 + * Configuration barrel export 3 + * 4 + * Central export point for all documentation configuration 5 + */ 6 + 7 + export { getAvailableDocs, getDocComponent } from "./docs.config"; 8 + export { ENV_VARS, interpolateEnvVars } from "./env.config"; 9 + export { 10 + APP_META, 11 + DOC_LINKS, 12 + type DocLink, 13 + type DocSection, 14 + NAVIGATION, 15 + type NavigationItem, 16 + } from "./navigation.config";
+237
apps/docs/src/index.css
··· 1 + @import "tailwindcss"; 2 + @import "@cv/ui/styles"; 3 + @source "../node_modules/@cv/ui"; 4 + 5 + body { 6 + margin: 0; 7 + font-family: 8 + -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", 9 + "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; 10 + -webkit-font-smoothing: antialiased; 11 + -moz-osx-font-smoothing: grayscale; 12 + background-color: var(--color-ctp-base); 13 + color: var(--color-ctp-text); 14 + } 15 + 16 + code { 17 + font-family: 18 + source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; 19 + } 20 + 21 + /* Syntax Highlighting */ 22 + .hljs { 23 + background-color: var(--color-ctp-mantle); 24 + color: var(--color-ctp-text); 25 + } 26 + 27 + /* Highlight.js theme colors using Catppuccin palette */ 28 + .hljs-keyword, 29 + .hljs-selector-tag, 30 + .hljs-title, 31 + .hljs-section, 32 + .hljs-doctag, 33 + .hljs-name, 34 + .hljs-strong { 35 + color: var(--color-ctp-mauve); 36 + font-weight: bold; 37 + } 38 + 39 + .hljs-comment, 40 + .hljs-quote { 41 + color: var(--color-ctp-surface2); 42 + font-style: italic; 43 + } 44 + 45 + .hljs-string, 46 + .hljs-literal, 47 + .hljs-number, 48 + .hljs-regexp, 49 + .hljs-variable, 50 + .hljs-template-variable { 51 + color: var(--color-ctp-green); 52 + } 53 + 54 + .hljs-type, 55 + .hljs-class .hljs-title { 56 + color: var(--color-ctp-yellow); 57 + } 58 + 59 + .hljs-function .hljs-title { 60 + color: var(--color-ctp-blue); 61 + } 62 + 63 + .hljs-attr, 64 + .hljs-attribute, 65 + .hljs-tag, 66 + .hljs-name { 67 + color: var(--color-ctp-sapphire); 68 + } 69 + 70 + .hljs-symbol, 71 + .hljs-bullet, 72 + .hljs-link { 73 + color: var(--color-ctp-peach); 74 + } 75 + 76 + .hljs-built_in, 77 + .hljs-builtin-name { 78 + color: var(--color-ctp-maroon); 79 + } 80 + 81 + .hljs-meta, 82 + .hljs-meta .hljs-keyword { 83 + color: var(--color-ctp-subtext0); 84 + } 85 + 86 + .hljs-deletion { 87 + background-color: var(--color-ctp-red); 88 + color: var(--color-ctp-base); 89 + } 90 + 91 + .hljs-addition { 92 + background-color: var(--color-ctp-green); 93 + color: var(--color-ctp-base); 94 + } 95 + 96 + .hljs-emphasis { 97 + font-style: italic; 98 + } 99 + 100 + .hljs-strong { 101 + font-weight: bold; 102 + } 103 + 104 + /* Markdown Styles */ 105 + .markdown { 106 + line-height: 1.7; 107 + } 108 + 109 + .markdown h1 { 110 + font-size: 2.5rem; 111 + font-weight: bold; 112 + margin-top: 2rem; 113 + margin-bottom: 1rem; 114 + color: var(--color-ctp-text); 115 + border-bottom: 2px solid var(--color-ctp-surface1); 116 + padding-bottom: 0.5rem; 117 + } 118 + 119 + .markdown h2 { 120 + font-size: 2rem; 121 + font-weight: bold; 122 + margin-top: 1.5rem; 123 + margin-bottom: 0.75rem; 124 + color: var(--color-ctp-text); 125 + border-bottom: 1px solid var(--color-ctp-surface0); 126 + padding-bottom: 0.25rem; 127 + } 128 + 129 + .markdown h3 { 130 + font-size: 1.5rem; 131 + font-weight: semibold; 132 + margin-top: 1.25rem; 133 + margin-bottom: 0.5rem; 134 + color: var(--color-ctp-text); 135 + } 136 + 137 + .markdown h4 { 138 + font-size: 1.25rem; 139 + font-weight: semibold; 140 + margin-top: 1rem; 141 + margin-bottom: 0.5rem; 142 + color: var(--color-ctp-subtext0); 143 + } 144 + 145 + .markdown p { 146 + margin-bottom: 1rem; 147 + color: var(--color-ctp-text); 148 + } 149 + 150 + .markdown a { 151 + color: var(--color-ctp-blue); 152 + text-decoration: underline; 153 + } 154 + 155 + .markdown a:hover { 156 + color: var(--color-ctp-sapphire); 157 + } 158 + 159 + .markdown ul, 160 + .markdown ol { 161 + margin-left: 1.5rem; 162 + margin-bottom: 1rem; 163 + } 164 + 165 + .markdown li { 166 + margin-bottom: 0.5rem; 167 + color: var(--color-ctp-text); 168 + } 169 + 170 + .markdown code { 171 + background-color: var(--color-ctp-surface0); 172 + padding: 0.125rem 0.375rem; 173 + border-radius: 0.25rem; 174 + font-size: 0.875rem; 175 + color: var(--color-ctp-pink); 176 + } 177 + 178 + .markdown pre { 179 + background-color: var(--color-ctp-mantle); 180 + padding: 1rem; 181 + border-radius: 0.5rem; 182 + overflow-x: auto; 183 + margin-bottom: 1rem; 184 + border: 1px solid var(--color-ctp-surface0); 185 + } 186 + 187 + .markdown pre code { 188 + background-color: transparent; 189 + padding: 0; 190 + color: var(--color-ctp-text); 191 + } 192 + 193 + .markdown blockquote { 194 + border-left: 4px solid var(--color-ctp-blue); 195 + padding-left: 1rem; 196 + margin-left: 0; 197 + margin-bottom: 1rem; 198 + color: var(--color-ctp-subtext0); 199 + font-style: italic; 200 + } 201 + 202 + .markdown table { 203 + width: 100%; 204 + border-collapse: collapse; 205 + margin-bottom: 1rem; 206 + } 207 + 208 + .markdown th, 209 + .markdown td { 210 + border: 1px solid var(--color-ctp-surface1); 211 + padding: 0.5rem; 212 + text-align: left; 213 + } 214 + 215 + .markdown th { 216 + background-color: var(--color-ctp-surface0); 217 + font-weight: semibold; 218 + color: var(--color-ctp-text); 219 + } 220 + 221 + .markdown td { 222 + background-color: var(--color-ctp-base); 223 + color: var(--color-ctp-text); 224 + } 225 + 226 + .markdown img { 227 + max-width: 100%; 228 + height: auto; 229 + border-radius: 0.5rem; 230 + margin-bottom: 1rem; 231 + } 232 + 233 + .markdown hr { 234 + border: none; 235 + border-top: 2px solid var(--color-ctp-surface1); 236 + margin: 2rem 0; 237 + }
+21
apps/docs/src/main.tsx
··· 1 + import React from "react"; 2 + import ReactDOM from "react-dom/client"; 3 + import { BrowserRouter } from "react-router-dom"; 4 + import { App } from "./App"; 5 + import "./index.css"; 6 + import { raise } from "@cv/utils"; 7 + 8 + const root = document.getElementById("root") ?? raise("Root element not found"); 9 + 10 + ReactDOM.createRoot(root).render( 11 + <React.StrictMode> 12 + <BrowserRouter 13 + future={{ 14 + v7_startTransition: true, 15 + v7_relativeSplatPath: true, 16 + }} 17 + > 18 + <App /> 19 + </BrowserRouter> 20 + </React.StrictMode>, 21 + );
+28
apps/docs/tsconfig.json
··· 1 + { 2 + "extends": "../../packages/tsconfig/tsconfig.base.json", 3 + "compilerOptions": { 4 + "target": "ES2020", 5 + "useDefineForClassFields": true, 6 + "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 + "module": "ESNext", 8 + "skipLibCheck": true, 9 + "moduleResolution": "bundler", 10 + "allowImportingTsExtensions": true, 11 + "resolveJsonModule": true, 12 + "isolatedModules": true, 13 + "noEmit": true, 14 + "jsx": "react-jsx", 15 + "strict": true, 16 + "noUnusedLocals": true, 17 + "noUnusedParameters": true, 18 + "noFallthroughCasesInSwitch": true, 19 + "esModuleInterop": true, 20 + "exactOptionalPropertyTypes": false, 21 + "baseUrl": ".", 22 + "paths": { 23 + "@/*": ["./src/*"] 24 + } 25 + }, 26 + "include": ["src"], 27 + "references": [{ "path": "./tsconfig.node.json" }] 28 + }
+39
apps/docs/vite.config.ts
··· 1 + import path from "node:path"; 2 + 3 + import tailwindcss from "@tailwindcss/vite"; 4 + import react from "@vitejs/plugin-react"; 5 + import rehypeHighlight from "rehype-highlight"; 6 + import remarkGfm from "remark-gfm"; 7 + import { defineConfig, type PluginOption, type UserConfig } from "vite"; 8 + 9 + import { envInterpolationPlugin } from "./src/config/env-interpolation-remark-plugin"; 10 + 11 + // For React 18 + MDX v2, use async defineConfig and dynamically import MDX 12 + // See: https://github.com/brillout/vite-plugin-mdx/issues/44#issuecomment-1106448872 13 + export default defineConfig(async (): Promise<UserConfig> => { 14 + const mdx = await import("@mdx-js/rollup"); 15 + 16 + return { 17 + optimizeDeps: { 18 + include: ["react/jsx-runtime"], 19 + }, 20 + plugins: [ 21 + react(), 22 + mdx.default({ 23 + include: ["**/*.mdx", "**/*.md"], 24 + remarkPlugins: [envInterpolationPlugin, remarkGfm], 25 + rehypePlugins: [rehypeHighlight], 26 + }), 27 + tailwindcss(), 28 + ] as PluginOption[], 29 + resolve: { 30 + alias: { 31 + "@": path.resolve(__dirname, "./src"), 32 + }, 33 + }, 34 + server: { 35 + host: true, 36 + port: 3001, 37 + }, 38 + }; 39 + });