diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..91582a8 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,37 @@ +# Dependencies +node_modules +.pnp +.pnp.js + +# Testing +coverage + +# Next.js +.next +out +build +dist + +# Production +*.min.js +*.min.css + +# Misc +.DS_Store +*.pem + +# Debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Local env files +.env*.local + +# Vercel +.vercel + +# Typescript +*.tsbuildinfo +next-env.d.ts + diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..115ffb9 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,10 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": false, + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "arrowParens": "always", + "endOfLine": "lf" +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..783bfbb --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,34 @@ +{ + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[json]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[jsonc]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "prettier.requireConfig": true, + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true, + "typescript.preferences.importModuleSpecifier": "non-relative", + "editor.tabSize": 2, + "editor.insertSpaces": true, + "files.eol": "\n", + "files.insertFinalNewline": true, + "files.trimTrailingWhitespace": true +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a71f62a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,377 @@ +# Contributing Guide + +This guide explains how to add new components and blocks to UITripleD. + +## Table of Contents + +- [Adding a New Component](#adding-a-new-component) +- [Adding a New Block](#adding-a-new-block) +- [File Structure](#file-structure) +- [Component Categories](#component-categories) + +--- + +## Adding a New Component + +Components are reusable UI elements organized by category (microinteractions, components, page, data, decorative, blocks). + +### Step 1: Create Component File + +Create the component file in the appropriate category directory: + +``` +components/{category}/{component-id}.tsx +``` + +Examples: + +- `components/micro/buttons/new-button.tsx` (for microinteractions) +- `components/components/cards/new-card.tsx` (for components) +- `components/sections/new-section.tsx` (for blocks) +- `components/motion-core/new-animation.tsx` (for motion-core components) + +**Note:** The file path should match the component's category and subcategory structure. + +### Step 2: Update Components Registry + +Edit `lib/components-registry.tsx`: + +1. **Import the component** at the top: + + ```tsx + import { NewComponent } from "@/components/{category}/{component-id}"; + ``` + +2. **Add to `componentsRegistry`** array: + ```tsx + { + id: "new-component", + name: "New Component", + description: "Description of what this component does.", + category: "components", // or "microinteractions", "page", "data", "decorative", "blocks" + tags: ["tag1", "tag2", "tag3"], + component: NewComponent, + codePath: "@/components/{category}/{component-id}.tsx", + duration: "300ms", + easing: "easeOut", + display: true, // Set to false if component needs fixes or is not ready + }, + ``` + +**Important:** + +- Use kebab-case for `id` (e.g., `new-component`) +- Provide a clear `description` +- Add relevant `tags` for searchability +- Set `display: false` if the component needs fixes or isn't ready for production +- The `codePath` should match the actual file location + +### Step 3: Sync Registry JSON + +Run the sync script to update `registry.json`: + +```bash +npm run sync-registry +``` + +This script automatically: + +- Reads components from `lib/components-registry.tsx` +- Detects dependencies from component imports +- Updates `registry.json` with the correct structure +- Preserves existing dependencies if they exist + +**Note:** The sync script will automatically: + +- Map categories to registry types (e.g., `microinteractions` → `registry:ui`) +- Detect `registryDependencies` from `@/components/ui/` imports +- Detect external `dependencies` from npm packages +- Set appropriate `category` and `subcategory` based on file path + +### Step 4: Verify + +1. Check that the component appears in the components list +2. Verify the component page loads correctly +3. Test the component functionality +4. Ensure all dependencies are correctly listed in `registry.json` + +--- + +## Adding a New Block + +Blocks are complex, feature-rich sections typically used in landing pages (hero sections, pricing tables, testimonials, etc.). + +### Step 1: Create Block File + +Create the block file in the sections directory: + +``` +components/sections/{block-id}.tsx +``` + +Example: `components/sections/new-feature-block.tsx` + +### Step 2: Update Components Registry + +Edit `lib/components-registry.tsx`: + +1. **Import the block** at the top: + + ```tsx + import { NewFeatureBlock } from "@/components/sections/new-feature-block"; + ``` + +2. **Add to `componentsRegistry`** array with `category: "blocks"`: + ```tsx + { + id: "new-feature-block", + name: "New Feature Block", + description: "Description of what this block does.", + category: "blocks", + tags: ["feature", "landing", "section"], + component: NewFeatureBlock, + codePath: "@/components/sections/new-feature-block.tsx", + duration: "600ms", + easing: "easeOut", + display: true, + }, + ``` + +### Step 3: Sync Registry JSON + +Run the sync script: + +```bash +npm run sync-registry +``` + +### Step 4: Verify + +1. Check that the block appears in the blocks category +2. Verify the block page loads correctly +3. Test the block functionality +4. Ensure responsive design works on different screen sizes + +--- + +## File Structure + +``` +UITripleD/ +├── components/ +│ ├── micro/ # Microinteractions (buttons, toggles, icons, badges, links) +│ │ ├── buttons/ +│ │ ├── toggles/ +│ │ ├── icons/ +│ │ ├── badges/ +│ │ └── links/ +│ ├── components/ # Reusable UI components +│ │ ├── cards/ +│ │ ├── chat/ +│ │ ├── forms/ +│ │ ├── inputs/ +│ │ ├── lists/ +│ │ ├── modal/ +│ │ ├── notifications/ +│ │ ├── tabs/ +│ │ └── ... +│ ├── sections/ # Block sections (landing page components) +│ ├── motion-core/ # Advanced motion components +│ ├── navigation/ # Navigation components +│ ├── forms/ # Form components +│ ├── modals/ # Modal components +│ ├── tooltips/ # Tooltip components +│ ├── decorative/ # Decorative components (backgrounds, text) +│ ├── data/ # Data visualization components +│ ├── page/ # Page-level components +│ └── ui/ # Base UI components (shadcn/ui) +├── lib/ +│ ├── components-registry.tsx # Component metadata and mapping +│ ├── file-reader.ts # Code loading utilities +│ └── utils.ts # Utility functions +├── scripts/ +│ └── sync-registry.js # Auto-sync registry.json +├── registry.json # Shadcn registry configuration (auto-generated) +└── types/ + └── index.ts # TypeScript types +``` + +--- + +## Component Categories + +### Microinteractions (`microinteractions`) + +Small, delightful interactions for buttons, toggles, and icons. + +- **Location:** `components/micro/` +- **Registry Type:** `registry:ui` +- **Examples:** Magnetic buttons, shimmer effects, animated badges + +### Components (`components`) + +Animated UI components like modals, dropdowns, and cards. + +- **Location:** `components/components/` +- **Registry Type:** `registry:component` +- **Examples:** Chat interfaces, animated cards, form components + +### Page (`page`) + +Smooth transitions and hero sections for pages. + +- **Location:** `components/page/` or `components/sections/` +- **Registry Type:** `registry:page` +- **Examples:** Hero sections, scroll reveals, page transitions + +### Data (`data`) + +Bring your data to life with counters, progress bars, and lists. + +- **Location:** `components/data/` +- **Registry Type:** `registry:ui` +- **Examples:** Animated counters, progress bars, charts + +### Decorative (`decorative`) + +Beautiful text and background effects. + +- **Location:** `components/decorative/` +- **Registry Type:** `registry:ui` +- **Examples:** Gradient animations, typewriter text, floating effects + +### Blocks (`blocks`) + +Reusable block sections for landing pages and portfolios. + +- **Location:** `components/sections/` +- **Registry Type:** `registry:block` +- **Examples:** Hero blocks, pricing sections, testimonials + +--- + +## Quick Checklist + +### For Components: + +- [ ] Component file created in appropriate category directory +- [ ] Component imported in `lib/components-registry.tsx` +- [ ] Added to `componentsRegistry` array with all required fields +- [ ] Ran `npm run sync-registry` to update `registry.json` +- [ ] Verified component appears in the UI +- [ ] Tested component functionality +- [ ] Checked dependencies in `registry.json` + +### For Blocks: + +- [ ] Block file created in `components/sections/` +- [ ] Block imported in `lib/components-registry.tsx` +- [ ] Added to `componentsRegistry` with `category: "blocks"` +- [ ] Ran `npm run sync-registry` to update `registry.json` +- [ ] Verified block appears in blocks category +- [ ] Tested responsive design +- [ ] Checked dependencies in `registry.json` + +--- + +## Tips + +1. **Naming Convention:** + - Use kebab-case for component IDs (e.g., `new-component`, `hero-section`) + - Use PascalCase for component names (e.g., `NewComponent`, `HeroSection`) + - File names should match component IDs + +2. **Dependencies:** + - The sync script automatically detects dependencies from imports + - `registryDependencies` are detected from `@/components/ui/` imports + - External `dependencies` are detected from npm package imports + - Always verify dependencies after syncing + +3. **Component Metadata:** + - Provide clear, descriptive `description` fields + - Add relevant `tags` for better searchability + - Set appropriate `duration` and `easing` for animations + - Use `display: false` for components that need fixes + +4. **Code Quality:** + - Follow TypeScript best practices + - Use proper React patterns (hooks, composition) + - Ensure accessibility (ARIA labels, keyboard navigation) + - Support reduced motion preferences where applicable + - Make components responsive + +5. **Testing:** + - Always test components after adding + - Verify the component appears in the UI + - Test on different screen sizes + - Check browser console for errors + - Verify dependencies are correctly listed + +6. **Sync Script:** + - Run `npm run sync-registry` after adding new components + - The script preserves existing dependencies + - Check the output for any warnings or errors + - Verify `registry.json` was updated correctly + +--- + +## Registry Sync Details + +The `sync-registry.js` script automatically: + +1. **Parses** `lib/components-registry.tsx` to extract component metadata +2. **Detects** dependencies from component file imports +3. **Maps** categories to registry types: + - `microinteractions` → `registry:ui` + - `components` → `registry:component` + - `page` → `registry:page` + - `data` → `registry:ui` + - `decorative` → `registry:ui` + - `blocks` → `registry:block` +4. **Updates** `registry.json` with new/updated entries +5. **Preserves** existing dependencies if they exist + +**Important:** Always run `npm run sync-registry` after adding new components to ensure `registry.json` stays in sync. + +--- + +## Need Help? + +If you encounter issues: + +1. **Check existing components** for reference patterns +2. **Verify file paths** match the `codePath` in registry +3. **Ensure TypeScript types** match the `Component` interface +4. **Run the linter** to catch errors: `npm run lint` +5. **Check browser console** for runtime errors +6. **Verify dependencies** are correctly listed in `registry.json` +7. **Test the sync script** output for warnings + +--- + +## Code Style + +- Use TypeScript for all components +- Follow React best practices +- Use functional components with hooks +- Prefer composition over inheritance +- Use meaningful variable and function names +- Add comments for complex logic +- Keep components focused and single-purpose + +--- + +## Accessibility + +When creating components, consider: + +- **Keyboard Navigation:** Ensure all interactive elements are keyboard accessible +- **Screen Readers:** Add appropriate ARIA labels and roles +- **Reduced Motion:** Respect `prefers-reduced-motion` media query +- **Focus Management:** Provide visible focus indicators +- **Color Contrast:** Ensure sufficient contrast ratios +- **Semantic HTML:** Use appropriate HTML elements + +--- + +Thank you for contributing to UITripleD! 🎉 diff --git a/app/api/registry/[name]/route.ts b/app/api/registry/[name]/route.ts index f54386f..e61cc05 100644 --- a/app/api/registry/[name]/route.ts +++ b/app/api/registry/[name]/route.ts @@ -1,11 +1,15 @@ import { NextResponse } from "next/server"; -import { componentsRegistry, getComponentById, loadComponentCode } from "@/lib/components-registry"; +import { + componentsRegistry, + getComponentById, + loadComponentCode, +} from "@/lib/components-registry"; /** * GET handler for registry * Returns component data from components-registry.tsx * Compatible with shadcn registry format - * + * * @param request - Next.js request object * @param params - Route parameters containing the component name */ @@ -19,7 +23,7 @@ export async function GET( // If name is provided, return specific component if (name && name !== "index") { const component = getComponentById(name); - + if (!component) { return NextResponse.json( { error: `Component "${name}" not found` }, @@ -54,7 +58,7 @@ export async function GET( } // Return full registry (all components) - const registry = componentsRegistry.map((component) => ({ + const registry = componentsRegistry.map((component) => ({ id: component.id, name: component.name, description: component.description, diff --git a/app/builder/page.tsx b/app/builder/page.tsx index 69f459c..47c249a 100644 --- a/app/builder/page.tsx +++ b/app/builder/page.tsx @@ -49,7 +49,7 @@ export default function BuilderPage() { activationConstraint: { distance: 8, }, - }), + }) ); useEffect(() => { @@ -127,10 +127,10 @@ export default function BuilderPage() { }; }), }; - }), + }) ); }, - [activePageId], + [activePageId] ); const handleUpdateTextNode = useCallback( @@ -163,16 +163,16 @@ export default function BuilderPage() { }; }), }; - }), + }) ); }, - [activePageId], + [activePageId] ); const handleAddComponentToPage = useCallback( (animationId: string) => { const animation = componentsRegistry.find( - (item) => item.id === animationId, + (item) => item.id === animationId ); if (!animation || animation.category !== "blocks") { return; @@ -198,11 +198,11 @@ export default function BuilderPage() { return prev.map((page) => page.id === targetPageId ? { ...page, components: [...page.components, newComponent] } - : page, + : page ); }); }, - [activePageId], + [activePageId] ); const handleMobileComponentSelect = useCallback( @@ -210,7 +210,7 @@ export default function BuilderPage() { handleAddComponentToPage(animationId); setMobileSidebarOpen(false); }, - [handleAddComponentToPage], + [handleAddComponentToPage] ); const handleDragStart = (event: DragStartEvent) => { @@ -240,7 +240,7 @@ export default function BuilderPage() { // Check if we're reordering components within the canvas const activeIndex = currentPage.components.findIndex( - (c) => c.id === activeId, + (c) => c.id === activeId ); const overIndex = currentPage.components.findIndex((c) => c.id === overId); @@ -253,8 +253,8 @@ export default function BuilderPage() { ...page, components: arrayMove(page.components, activeIndex, overIndex), } - : page, - ), + : page + ) ); setActiveId(null); return; @@ -289,7 +289,7 @@ export default function BuilderPage() { ...page, components: newItems, }; - }), + }) ); } else { // If dropped on canvas, append to end @@ -297,8 +297,8 @@ export default function BuilderPage() { prev.map((page) => page.id === currentPage.id ? { ...page, components: [...page.components, newComponent] } - : page, - ), + : page + ) ); } } @@ -315,11 +315,11 @@ export default function BuilderPage() { ? { ...page, components: page.components.filter( - (component) => component.id !== id, + (component) => component.id !== id ), } - : page, - ), + : page + ) ); }; @@ -368,8 +368,8 @@ export default function BuilderPage() { name: normalized, slug: newSlug, } - : item, - ), + : item + ) ); }; @@ -397,13 +397,13 @@ export default function BuilderPage() { const loadSavedProjects = () => { const projects = JSON.parse( - localStorage.getItem("builderProjects") || "{}", + localStorage.getItem("builderProjects") || "{}" ); const projectList = Object.values(projects) as SavedProject[]; setSavedProjects( projectList.sort( - (a, b) => new Date(b.savedAt).getTime() - new Date(a.savedAt).getTime(), - ), + (a, b) => new Date(b.savedAt).getTime() - new Date(a.savedAt).getTime() + ) ); setLoadDialogOpen(true); }; @@ -429,7 +429,7 @@ export default function BuilderPage() { const builderComponents = (page.components ?? []) .map((comp) => { const animation = componentsRegistry.find( - (a) => a.id === comp.animationId, + (a) => a.id === comp.animationId ); if (!animation) { return null; @@ -474,7 +474,7 @@ export default function BuilderPage() { const deleteProject = (projectName: string) => { const projects = JSON.parse( - localStorage.getItem("builderProjects") || "{}", + localStorage.getItem("builderProjects") || "{}" ); delete projects[projectName]; localStorage.setItem("builderProjects", JSON.stringify(projects)); @@ -491,10 +491,13 @@ export default function BuilderPage() { for (const page of pages) { const canvasComponent = page.components.find( - (component) => component.id === activeId, + (component) => component.id === activeId ); if (canvasComponent) { - return { name: canvasComponent.animation.name, type: "canvas" as const }; + return { + name: canvasComponent.animation.name, + type: "canvas" as const, + }; } } diff --git a/app/components/[id]/AnimationDetailPage.client.tsx b/app/components/[id]/AnimationDetailPage.client.tsx index 27bce8e..12a0bad 100644 --- a/app/components/[id]/AnimationDetailPage.client.tsx +++ b/app/components/[id]/AnimationDetailPage.client.tsx @@ -34,10 +34,7 @@ export default function AnimationDetailPageClient({ code }: { code: string }) { const Component = component.component; const requiresShadcn = component.tags.includes("shadcn"); - const codeLineCount = React.useMemo( - () => code.split("\n").length, - [code], - ); + const codeLineCount = React.useMemo(() => code.split("\n").length, [code]); const showLongCodeNote = codeLineCount > 400; const handleRefresh = () => { @@ -227,7 +224,7 @@ export default function AnimationDetailPageClient({ code }: { code: string }) { onClick={() => handleCopyInstall( `npx uitripled add ${component.id}`, - "npx", + "npx" ) } className="flex items-center gap-1.5 rounded border border-border bg-muted px-3 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:border-ring hover:text-foreground" @@ -276,7 +273,7 @@ export default function AnimationDetailPageClient({ code }: { code: string }) { onClick={() => handleCopyInstall( `npm install -g uitripled && uitripled add ${component.id}`, - "npm", + "npm" ) } className="flex items-center gap-1.5 rounded border border-border bg-muted px-3 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:border-ring hover:text-foreground" @@ -326,7 +323,7 @@ export default function AnimationDetailPageClient({ code }: { code: string }) { onClick={() => handleCopyInstall( `yarn dlx uitripled add ${component.id}`, - "yarn", + "yarn" ) } className="flex items-center gap-1.5 rounded border border-border bg-muted px-3 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:border-ring hover:text-foreground" @@ -375,7 +372,7 @@ export default function AnimationDetailPageClient({ code }: { code: string }) { onClick={() => handleCopyInstall( `pnpm dlx uitripled add ${component.id}`, - "pnpm", + "pnpm" ) } className="flex items-center gap-1.5 rounded border border-border bg-muted px-3 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:border-ring hover:text-foreground" @@ -437,11 +434,10 @@ export default function AnimationDetailPageClient({ code }: { code: string }) { lines)

- We include everything in one file for easy - copy-paste (including dummy data), but keep in mind - you should split your logic when integrating it - (e.g., move data fetching to loaders, hooks, or API - utilities). + We include everything in one file for easy copy-paste + (including dummy data), but keep in mind you should + split your logic when integrating it (e.g., move data + fetching to loaders, hooks, or API utilities).

@@ -467,8 +463,8 @@ export default function AnimationDetailPageClient({ code }: { code: string }) {

The colors and theme are customizable via Tailwind CSS - classes. The default theme uses dark mode colors - defined in your{" "} + classes. The default theme uses dark mode colors defined + in your{" "} globals.css {" "} diff --git a/app/components/[id]/page.tsx b/app/components/[id]/page.tsx index d457528..47a352a 100644 --- a/app/components/[id]/page.tsx +++ b/app/components/[id]/page.tsx @@ -1,6 +1,10 @@ import AnimationDetailPageClient from "./AnimationDetailPage.client"; import { createMetadata } from "@/lib/seo"; -import { getComponentById, componentsRegistry, loadComponentCode } from "@/lib/components-registry"; +import { + getComponentById, + componentsRegistry, + loadComponentCode, +} from "@/lib/components-registry"; import { notFound } from "next/navigation"; type PageParams = { diff --git a/app/components/layout.tsx b/app/components/layout.tsx index 2fbc879..298d138 100644 --- a/app/components/layout.tsx +++ b/app/components/layout.tsx @@ -4,7 +4,10 @@ import { useState, useMemo, useEffect, useCallback } from "react"; import { useParams, usePathname, useRouter } from "next/navigation"; import { motion, AnimatePresence } from "framer-motion"; import { ChevronLeft, ChevronRight, Menu, X } from "lucide-react"; -import { getComponentById, componentsRegistry } from "@/lib/components-registry"; +import { + getComponentById, + componentsRegistry, +} from "@/lib/components-registry"; import { AnimationsSidebar } from "@/components/animation-sidebar"; import { Button } from "@/components/ui/button"; import { @@ -33,14 +36,16 @@ export default function ComponentsLayout({ // Get visible animations (display !== false) const visibleAnimations = useMemo(() => { - return componentsRegistry.filter((component) => component.display !== false); + return componentsRegistry.filter( + (component) => component.display !== false + ); }, []); // Find current animation index and navigation const currentIndex = useMemo(() => { if (!selectedAnimation) return -1; return visibleAnimations.findIndex( - (anim) => anim.id === selectedAnimation.id, + (anim) => anim.id === selectedAnimation.id ); }, [selectedAnimation, visibleAnimations]); @@ -55,7 +60,7 @@ export default function ComponentsLayout({ (animationId: string) => { router.push(`/components/${animationId}`); }, - [router], + [router] ); const handleMobileSelect = useCallback( @@ -63,7 +68,7 @@ export default function ComponentsLayout({ handleNavigate(animationId); setMobileSidebarOpen(false); }, - [handleNavigate], + [handleNavigate] ); // Keyboard navigation diff --git a/app/globals.css b/app/globals.css index 72694c0..541f971 100644 --- a/app/globals.css +++ b/app/globals.css @@ -134,7 +134,6 @@ } } - @layer utilities { .text-balance { text-wrap: balance; @@ -156,4 +155,4 @@ flex-direction: row !important; display: flex !important; } -} \ No newline at end of file +} diff --git a/app/og/route.tsx b/app/og/route.tsx index 3014cfc..13eded5 100644 --- a/app/og/route.tsx +++ b/app/og/route.tsx @@ -75,7 +75,7 @@ function NotFoundOGImage(componentId: string, faviconUrl: string) { function ComponentOGImage( name: string, description: string, - faviconUrl: string, + faviconUrl: string ) { return (

((comp, componentIndex) => { const animation = componentsRegistry.find( - (a) => a.id === comp.animationId, + (a) => a.id === comp.animationId ); if (!animation) { return null; @@ -196,7 +196,7 @@ export default function PreviewProjectPageClient() { useEffect(() => { // Load project from localStorage const projects = JSON.parse( - localStorage.getItem("builderProjects") || "{}", + localStorage.getItem("builderProjects") || "{}" ); const foundProject = projects[projectName]; @@ -205,7 +205,7 @@ export default function PreviewProjectPageClient() { if (normalized) { setProject(normalized); setActivePageId( - normalized.entryPageId || normalized.pages[0]?.id || null, + normalized.entryPageId || normalized.pages[0]?.id || null ); } } @@ -283,7 +283,7 @@ export default function PreviewProjectPageClient() { if (!container || !instance.textContent) return; const allElements = Array.from( - container.querySelectorAll(selector), + container.querySelectorAll(selector) ); const editableElements = allElements.filter((el) => { @@ -291,7 +291,7 @@ export default function PreviewProjectPageClient() { if (!text) return false; const hasChildWithText = Array.from( - el.querySelectorAll(selector), + el.querySelectorAll(selector) ).some((child) => { if (child === el) return false; const childText = child.textContent?.trim(); diff --git a/components/animation-card.tsx b/components/animation-card.tsx index 92b4c03..5664b09 100644 --- a/components/animation-card.tsx +++ b/components/animation-card.tsx @@ -23,9 +23,7 @@ export function AnimationCard({ animation }: AnimationCardProps) { whileHover={{ y: -2 }} className="group relative h-full overflow-hidden rounded-lg border border-border bg-card transition-shadow hover:border-ring" > - + {/* Preview Area */}
diff --git a/components/animation-sidebar.tsx b/components/animation-sidebar.tsx index 84da1a3..4d3ecfd 100644 --- a/components/animation-sidebar.tsx +++ b/components/animation-sidebar.tsx @@ -45,7 +45,7 @@ export function AnimationsSidebar({ (anim) => anim.name.toLowerCase().includes(lowerQuery) || anim.description.toLowerCase().includes(lowerQuery) || - anim.tags.some((tag) => tag.toLowerCase().includes(lowerQuery)), + anim.tags.some((tag) => tag.toLowerCase().includes(lowerQuery)) ); } diff --git a/components/avatars/avatar-group.tsx b/components/avatars/avatar-group.tsx index 29009b7..a7ac089 100644 --- a/components/avatars/avatar-group.tsx +++ b/components/avatars/avatar-group.tsx @@ -53,7 +53,7 @@ export function AvatarGroup() { mass: 0.7, }), }, - [shouldReduceMotion], + [shouldReduceMotion] ); return ( diff --git a/components/builder-canvas.tsx b/components/builder-canvas.tsx index 969d2b9..4f24329 100644 --- a/components/builder-canvas.tsx +++ b/components/builder-canvas.tsx @@ -19,12 +19,12 @@ type CanvasComponentProps = { onRegisterTextNode: ( componentId: string, nodeId: string, - originalText: string, + originalText: string ) => void; onUpdateTextNode: ( componentId: string, nodeId: string, - newValue: string, + newValue: string ) => void; }; @@ -61,7 +61,7 @@ function CanvasComponent({ const newValue = target.textContent ?? ""; onUpdateTextNode(component.id, textId, newValue); }, - [component.id, onUpdateTextNode], + [component.id, onUpdateTextNode] ); useEffect(() => { @@ -71,7 +71,7 @@ function CanvasComponent({ const selector = "h1,h2,h3,h4,h5,h6,p,span,button,a,li,blockquote,figcaption,label,strong,em,small,div"; const allElements = Array.from( - container.querySelectorAll(selector), + container.querySelectorAll(selector) ); const editableElements = allElements.filter((el) => { @@ -81,7 +81,7 @@ function CanvasComponent({ } const hasChildWithText = Array.from( - el.querySelectorAll(selector), + el.querySelectorAll(selector) ).some((child) => { if (child === el) return false; const childText = child.textContent?.trim(); @@ -212,12 +212,12 @@ type BuilderCanvasProps = { onRegisterTextNode: ( componentId: string, nodeId: string, - originalText: string, + originalText: string ) => void; onUpdateTextNode: ( componentId: string, nodeId: string, - newValue: string, + newValue: string ) => void; }; diff --git a/components/builder-code-view.tsx b/components/builder-code-view.tsx index 004e5cd..4269a42 100644 --- a/components/builder-code-view.tsx +++ b/components/builder-code-view.tsx @@ -14,10 +14,7 @@ import { Loader2, } from "lucide-react"; import { CodeBlock } from "./code-block"; -import type { - BuilderComponent, - BuilderProjectPage, -} from "@/types/builder"; +import type { BuilderComponent, BuilderProjectPage } from "@/types/builder"; import { mergeComponentImports } from "@/lib/merge-imports"; import { Dialog, @@ -43,7 +40,7 @@ const escapeForJSXText = (value: string) => const replaceNextOccurrence = ( source: string, search: string, - replacement: string, + replacement: string ) => { if (!search) return source; const index = source.indexOf(search); @@ -53,7 +50,7 @@ const replaceNextOccurrence = ( const applyTextOverrides = ( code: string, - textContent?: BuilderComponent["textContent"], + textContent?: BuilderComponent["textContent"] ) => { if (!textContent) return code; @@ -97,7 +94,7 @@ const slugifyName = (value: string) => const buildPageCode = async ( currentPage: BuilderProjectPage, - allPages: BuilderProjectPage[], + allPages: BuilderProjectPage[] ): Promise => { const components = currentPage.components; @@ -107,7 +104,7 @@ const buildPageCode = async ( for (const component of components) { let animationCode = component.animation.code; - + // If code is not available, fetch it from the API if (!animationCode && component.animation.id) { try { @@ -116,26 +113,31 @@ const buildPageCode = async ( const data = await response.json(); animationCode = data.code; } else { - console.warn(`Failed to load code for component ${component.animation.id}: ${response.status}`); + console.warn( + `Failed to load code for component ${component.animation.id}: ${response.status}` + ); continue; } } catch (error) { - console.error(`Error loading code for component ${component.animation.id}:`, error); + console.error( + `Error loading code for component ${component.animation.id}:`, + error + ); continue; } } - + if (!animationCode) { console.warn(`Component ${component.animation.id} has no code available`); continue; } const codeWithOverrides = applyTextOverrides( animationCode, - component.textContent, + component.textContent ); const functionMatch = codeWithOverrides.match( - /export\s+(?:function|const)\s+(\w+)/, + /export\s+(?:function|const)\s+(\w+)/ ); const componentFunctionName = functionMatch ? functionMatch[1] @@ -204,7 +206,7 @@ const buildPageCode = async ( if (componentCode.length > 0) { componentDefinitions.push( - `// ${component.animation.name}\n${componentCode}`, + `// ${component.animation.name}\n${componentCode}` ); } } @@ -243,7 +245,7 @@ ${mainContent} const buildProjectLayout = ( allPages: BuilderProjectPage[], - projectName: string, + projectName: string ): string => { const brandTitle = allPages[0]?.name ?? projectName; const brandLabel = escapeForTemplateLiteral(brandTitle); @@ -262,7 +264,7 @@ const buildProjectLayout = ( className="text-sm font-medium text-muted-foreground transition-colors hover:text-foreground" > ${link.label} - `, + ` ) .join("\n") : ` ${link.label} - `, + ` ) .join("\n") : ` pages.reduce((sum, page) => sum + page.components.length, 0), - [pages], + [pages] ); // Load code for all pages asynchronously useEffect(() => { let cancelled = false; - + async function loadPageCodes() { setLoadingCode(true); try { @@ -369,7 +371,7 @@ export function BuilderCodeView({ pages, activePageId }: BuilderCodeViewProps) { componentCount: page.components.length, })) ); - + if (!cancelled) { setPageArtifacts(artifacts); } @@ -384,9 +386,9 @@ export function BuilderCodeView({ pages, activePageId }: BuilderCodeViewProps) { } } } - + loadPageCodes(); - + return () => { cancelled = true; }; @@ -407,7 +409,7 @@ export function BuilderCodeView({ pages, activePageId }: BuilderCodeViewProps) { if (activePageId) { const match = pageArtifacts.find( - (artifact) => artifact.id === activePageId, + (artifact) => artifact.id === activePageId ); if (match) { return match; @@ -425,9 +427,9 @@ export function BuilderCodeView({ pages, activePageId }: BuilderCodeViewProps) { animationId: component.animationId, textContent: component.textContent ?? {}, })), - })), + })) ), - [pages], + [pages] ); const handleCopy = async () => { @@ -443,7 +445,7 @@ export function BuilderCodeView({ pages, activePageId }: BuilderCodeViewProps) { if (!normalizedName || totalComponentCount === 0) return; const existingProjects = JSON.parse( - localStorage.getItem("builderProjects") || "{}", + localStorage.getItem("builderProjects") || "{}" ); const existingNormalizedEntry = existingProjects[normalizedName]; @@ -460,7 +462,7 @@ export function BuilderCodeView({ pages, activePageId }: BuilderCodeViewProps) { typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID().replace(/-/g, "").slice(0, 8) : Array.from({ length: 8 }, () => - chars.charAt(Math.floor(Math.random() * chars.length)), + chars.charAt(Math.floor(Math.random() * chars.length)) ).join(""); finalProjectName = baseSlug @@ -563,7 +565,7 @@ export function BuilderCodeView({ pages, activePageId }: BuilderCodeViewProps) { const findMatchingProject = useCallback(() => { const existingProjects = JSON.parse( - localStorage.getItem("builderProjects") || "{}", + localStorage.getItem("builderProjects") || "{}" ); return Object.values(existingProjects).find((project: any) => { const savedPages = @@ -581,7 +583,7 @@ export function BuilderCodeView({ pages, activePageId }: BuilderCodeViewProps) { animationId: component.animationId, textContent: component.textContent ?? {}, })), - })), + })) ); return signature === projectSignature; @@ -607,7 +609,7 @@ export function BuilderCodeView({ pages, activePageId }: BuilderCodeViewProps) { if (existingProject) { router.push( - `/deploy?project=${encodeURIComponent(existingProject.name)}`, + `/deploy?project=${encodeURIComponent(existingProject.name)}` ); } else { openSaveDialog("deploy"); @@ -746,7 +748,7 @@ export function BuilderCodeView({ pages, activePageId }: BuilderCodeViewProps) { setDisplayedCode(code); } } - + loadDisplayedCode(); }, [activeArtifact]); diff --git a/components/builder-sidebar.tsx b/components/builder-sidebar.tsx index 229b6c2..bd9620b 100644 --- a/components/builder-sidebar.tsx +++ b/components/builder-sidebar.tsx @@ -39,7 +39,7 @@ function DraggableComponent({ component }: { component: ComponentItem }) { whileTap={{ scale: 0.98 }} className={cn( "cursor-grab rounded-lg border border-border bg-card p-3 transition-colors hover:border-primary hover:bg-accent/5 active:cursor-grabbing", - isDragging && "opacity-50", + isDragging && "opacity-50" )} >
{component.name}
@@ -106,7 +106,7 @@ export function BuilderSidebar({ return componentsRegistry .filter( (component) => - component.display !== false && component.category === "blocks", + component.display !== false && component.category === "blocks" ) .filter((component) => { if (!query) return true; @@ -166,7 +166,7 @@ export function BuilderSidebar({ component={component} onSelect={onSelectComponent} /> - ), + ) )}
)} diff --git a/components/builder/builder-header.tsx b/components/builder/builder-header.tsx index 67e806c..5d0fea4 100644 --- a/components/builder/builder-header.tsx +++ b/components/builder/builder-header.tsx @@ -86,4 +86,3 @@ export function BuilderHeader({
); } - diff --git a/components/builder/drag-overlay.tsx b/components/builder/drag-overlay.tsx index d8010a6..6400978 100644 --- a/components/builder/drag-overlay.tsx +++ b/components/builder/drag-overlay.tsx @@ -22,4 +22,3 @@ export function DragOverlay({ activeComponentInfo }: DragOverlayProps) { ); } - diff --git a/components/builder/instructions-banner.tsx b/components/builder/instructions-banner.tsx index 488b5cf..8b70ccb 100644 --- a/components/builder/instructions-banner.tsx +++ b/components/builder/instructions-banner.tsx @@ -10,10 +10,7 @@ type InstructionsBannerProps = { onHide: () => void; }; -export function InstructionsBanner({ - show, - onHide, -}: InstructionsBannerProps) { +export function InstructionsBanner({ show, onHide }: InstructionsBannerProps) { return ( {show && ( @@ -65,4 +62,3 @@ export function InstructionsBanner({ ); } - diff --git a/components/builder/load-project-dialog.tsx b/components/builder/load-project-dialog.tsx index b2e12f4..605a89b 100644 --- a/components/builder/load-project-dialog.tsx +++ b/components/builder/load-project-dialog.tsx @@ -51,7 +51,7 @@ export function LoadProjectDialog({ const pagesForProject = extractSavedPages(project); const totalComponents = pagesForProject.reduce( (total, page) => total + (page.components?.length ?? 0), - 0, + 0 ); const savedDate = new Date(project.savedAt); @@ -86,9 +86,7 @@ export function LoadProjectDialog({ variant="ghost" size="sm" onClick={() => { - if ( - confirm(`Delete project "${project.name}"?`) - ) { + if (confirm(`Delete project "${project.name}"?`)) { onDeleteProject(project.name); } }} @@ -119,4 +117,3 @@ export function LoadProjectDialog({ ); } - diff --git a/components/builder/page-tabs.tsx b/components/builder/page-tabs.tsx index 1d327f6..7b3a0ce 100644 --- a/components/builder/page-tabs.tsx +++ b/components/builder/page-tabs.tsx @@ -66,7 +66,12 @@ export function PageTabs({ )}
))} - @@ -80,4 +85,3 @@ export function PageTabs({ ); } - diff --git a/components/builder/text-editing-banner.tsx b/components/builder/text-editing-banner.tsx index fbc3f12..8f65df4 100644 --- a/components/builder/text-editing-banner.tsx +++ b/components/builder/text-editing-banner.tsx @@ -23,4 +23,3 @@ export function TextEditingBanner({ show }: TextEditingBannerProps) { ); } - diff --git a/components/components/account-switcher/multiple-accounts.tsx b/components/components/account-switcher/multiple-accounts.tsx index b05f2d1..1587dad 100644 --- a/components/components/account-switcher/multiple-accounts.tsx +++ b/components/components/account-switcher/multiple-accounts.tsx @@ -46,7 +46,7 @@ export function MultipleAccounts() { () => accountOptions.find((account) => account.id === activeId) ?? accountOptions[0], - [activeId], + [activeId] ); const statusMessage = activeAccount diff --git a/components/components/cards/detail-task.tsx b/components/components/cards/detail-task.tsx index 5ba7759..515197d 100644 --- a/components/components/cards/detail-task.tsx +++ b/components/components/cards/detail-task.tsx @@ -120,7 +120,7 @@ export function DetailTaskCard() { const [title, setTitle] = useState("Edit Design System"); const [priority, setPriority] = useState("high"); const [assignees, setAssignees] = useState( - allMembers.slice(0, 3), + allMembers.slice(0, 3) ); const [description, setDescription] = useState(defaultDescription); const [reminderEnabled, setReminderEnabled] = useState(true); @@ -132,9 +132,9 @@ export function DetailTaskCard() { const availableMembers = useMemo( () => allMembers.filter( - (member) => !assignees.some((assigned) => assigned.id === member.id), + (member) => !assignees.some((assigned) => assigned.id === member.id) ), - [assignees], + [assignees] ); const handleRemoveAssignee = (id: string) => { @@ -370,7 +370,7 @@ export function DetailTaskCard() { value={description} onChange={(event) => setDescription( - event.target.value.slice(0, maxDescriptionLength), + event.target.value.slice(0, maxDescriptionLength) ) } className="h-32 resize-none border-0 bg-transparent px-3 py-3 text-sm text-foreground/80 focus-visible:ring-0 focus-visible:ring-offset-0" diff --git a/components/components/chat/ai-chat-interface.tsx b/components/components/chat/ai-chat-interface.tsx index 55242d9..ac3878e 100644 --- a/components/components/chat/ai-chat-interface.tsx +++ b/components/components/chat/ai-chat-interface.tsx @@ -77,7 +77,7 @@ export function AIChatInterface() { }, []); const handleTextareaChange = ( - event: React.ChangeEvent, + event: React.ChangeEvent ) => { setInputValue(event.target.value); @@ -90,7 +90,7 @@ export function AIChatInterface() { const renderDropdown = ( type: DropdownType, options: ActionOption[], - align: "left" | "right" = "left", + align: "left" | "right" = "left" ) => ( {activeDropdown === type && ( @@ -171,7 +171,7 @@ export function AIChatInterface() { ); - }, + } ); PasswordInput.displayName = "PasswordInput"; diff --git a/components/ui/separator.tsx b/components/ui/separator.tsx index 9ac3b95..5b6774d 100644 --- a/components/ui/separator.tsx +++ b/components/ui/separator.tsx @@ -11,7 +11,7 @@ const Separator = React.forwardRef< >( ( { className, orientation = "horizontal", decorative = true, ...props }, - ref, + ref ) => ( - ), + ) ); Separator.displayName = SeparatorPrimitive.Root.displayName; diff --git a/components/ui/switch.tsx b/components/ui/switch.tsx index d692565..b35a408 100644 --- a/components/ui/switch.tsx +++ b/components/ui/switch.tsx @@ -12,14 +12,14 @@ const Switch = React.forwardRef< diff --git a/components/ui/tabs.tsx b/components/ui/tabs.tsx index 2c92860..916efca 100644 --- a/components/ui/tabs.tsx +++ b/components/ui/tabs.tsx @@ -14,7 +14,7 @@ const TabsList = React.forwardRef< ref={ref} className={cn( "inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground", - className, + className )} {...props} /> @@ -29,7 +29,7 @@ const TabsTrigger = React.forwardRef< ref={ref} className={cn( "inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-card data-[state=active]:text-foreground data-[state=active]:shadow-sm", - className, + className )} {...props} /> @@ -44,7 +44,7 @@ const TabsContent = React.forwardRef< ref={ref} className={cn( "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", - className, + className )} {...props} /> diff --git a/components/ui/textarea.tsx b/components/ui/textarea.tsx index 24eb6c2..5e77d51 100644 --- a/components/ui/textarea.tsx +++ b/components/ui/textarea.tsx @@ -10,13 +10,13 @@ const Textarea = React.forwardRef(