fix un-registered components

This commit is contained in:
moumensoliman 2025-11-17 04:03:42 +02:00
parent 5e38b5c859
commit acef3fb079
88 changed files with 1163 additions and 1381 deletions

37
.prettierignore Normal file
View file

@ -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

10
.prettierrc.json Normal file
View file

@ -0,0 +1,10 @@
{
"semi": true,
"trailingComma": "es5",
"singleQuote": false,
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"arrowParens": "always",
"endOfLine": "lf"
}

34
.vscode/settings.json vendored Normal file
View file

@ -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
}

377
CONTRIBUTING.md Normal file
View file

@ -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! 🎉

View file

@ -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,

View file

@ -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<BuilderComponent | null>((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,
};
}
}

View file

@ -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)
</p>
<p>
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).
</p>
</div>
</motion.div>
@ -467,8 +463,8 @@ export default function AnimationDetailPageClient({ code }: { code: string }) {
</h3>
<p className="text-xs text-muted-foreground/80">
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{" "}
<code className="rounded border border-border px-1.5 py-0.5 text-[11px]">
globals.css
</code>{" "}

View file

@ -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 = {

View file

@ -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

View file

@ -134,7 +134,6 @@
}
}
@layer utilities {
.text-balance {
text-wrap: balance;
@ -156,4 +155,4 @@
flex-direction: row !important;
display: flex !important;
}
}
}

View file

@ -75,7 +75,7 @@ function NotFoundOGImage(componentId: string, faviconUrl: string) {
function ComponentOGImage(
name: string,
description: string,
faviconUrl: string,
faviconUrl: string
) {
return (
<div
@ -163,7 +163,7 @@ export async function GET(request: Request) {
const jsx = ComponentOGImage(
metadata.name,
metadata.description || "A component from UI TripleD",
faviconUrl,
faviconUrl
);
return new ImageResponse(jsx, {
width: 1200,
@ -179,7 +179,7 @@ export async function GET(request: Request) {
: "Unknown error";
return Response.json(
{ error: `Failed to generate the image: ${message}` },
{ status: 500 },
{ status: 500 }
);
}
}

View file

@ -135,7 +135,7 @@ function normalizeProject(project: SavedProject): NormalizedProject | null {
const componentInstances = (page.components ?? [])
.map<ComponentInstance | null>((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<HTMLElement>(selector),
container.querySelectorAll<HTMLElement>(selector)
);
const editableElements = allElements.filter((el) => {
@ -291,7 +291,7 @@ export default function PreviewProjectPageClient() {
if (!text) return false;
const hasChildWithText = Array.from(
el.querySelectorAll<HTMLElement>(selector),
el.querySelectorAll<HTMLElement>(selector)
).some((child) => {
if (child === el) return false;
const childText = child.textContent?.trim();

View file

@ -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"
>
<Link
href={`/components/${animation.id}`}
>
<Link href={`/components/${animation.id}`}>
{/* Preview Area */}
<div className="relative h-48 overflow-hidden bg-card">
<div className="absolute inset-0 flex items-center justify-center">

View file

@ -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))
);
}

View file

@ -53,7 +53,7 @@ export function AvatarGroup() {
mass: 0.7,
}),
},
[shouldReduceMotion],
[shouldReduceMotion]
);
return (

View file

@ -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<HTMLElement>(selector),
container.querySelectorAll<HTMLElement>(selector)
);
const editableElements = allElements.filter((el) => {
@ -81,7 +81,7 @@ function CanvasComponent({
}
const hasChildWithText = Array.from(
el.querySelectorAll<HTMLElement>(selector),
el.querySelectorAll<HTMLElement>(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;
};

View file

@ -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<string> => {
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}
</Link>`,
</Link>`
)
.join("\n")
: ` <Link
@ -281,7 +283,7 @@ const buildProjectLayout = (
className="text-xs text-muted-foreground transition-colors hover:text-foreground"
>
${link.label}
</Link>`,
</Link>`
)
.join("\n")
: ` <Link
@ -350,13 +352,13 @@ export function BuilderCodeView({ pages, activePageId }: BuilderCodeViewProps) {
const totalComponentCount = useMemo(
() => 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]);

View file

@ -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"
)}
>
<div className="text-sm font-medium">{component.name}</div>
@ -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}
/>
),
)
)}
</div>
)}

View file

@ -86,4 +86,3 @@ export function BuilderHeader({
</div>
);
}

View file

@ -22,4 +22,3 @@ export function DragOverlay({ activeComponentInfo }: DragOverlayProps) {
</DndDragOverlay>
);
}

View file

@ -10,10 +10,7 @@ type InstructionsBannerProps = {
onHide: () => void;
};
export function InstructionsBanner({
show,
onHide,
}: InstructionsBannerProps) {
export function InstructionsBanner({ show, onHide }: InstructionsBannerProps) {
return (
<AnimatePresence>
{show && (
@ -65,4 +62,3 @@ export function InstructionsBanner({
</AnimatePresence>
);
}

View file

@ -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({
</Dialog>
);
}

View file

@ -66,7 +66,12 @@ export function PageTabs({
)}
</div>
))}
<Button variant="outline" size="sm" className="gap-1" onClick={onAddPage}>
<Button
variant="outline"
size="sm"
className="gap-1"
onClick={onAddPage}
>
<Plus className="h-4 w-4" />
Add page
</Button>
@ -80,4 +85,3 @@ export function PageTabs({
</div>
);
}

View file

@ -23,4 +23,3 @@ export function TextEditingBanner({ show }: TextEditingBannerProps) {
</AnimatePresence>
);
}

View file

@ -46,7 +46,7 @@ export function MultipleAccounts() {
() =>
accountOptions.find((account) => account.id === activeId) ??
accountOptions[0],
[activeId],
[activeId]
);
const statusMessage = activeAccount

View file

@ -120,7 +120,7 @@ export function DetailTaskCard() {
const [title, setTitle] = useState("Edit Design System");
const [priority, setPriority] = useState<Priority>("high");
const [assignees, setAssignees] = useState<TeamMember[]>(
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"

View file

@ -77,7 +77,7 @@ export function AIChatInterface() {
}, []);
const handleTextareaChange = (
event: React.ChangeEvent<HTMLTextAreaElement>,
event: React.ChangeEvent<HTMLTextAreaElement>
) => {
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"
) => (
<AnimatePresence>
{activeDropdown === type && (
@ -171,7 +171,7 @@ export function AIChatInterface() {
<Button
onClick={() =>
setActiveDropdown(
activeDropdown === "share" ? null : "share",
activeDropdown === "share" ? null : "share"
)
}
className="group rounded-xl p-2 bg-background/80 transition-all hover:bg-foreground/[0.05] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground/40"
@ -193,7 +193,7 @@ export function AIChatInterface() {
<Button
onClick={() =>
setActiveDropdown(
activeDropdown === "quick" ? null : "quick",
activeDropdown === "quick" ? null : "quick"
)
}
className="group rounded-xl p-2 bg-background/80 transition-all hover:bg-foreground/[0.05] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground/40"
@ -215,7 +215,7 @@ export function AIChatInterface() {
<Button
onClick={() =>
setActiveDropdown(
activeDropdown === "history" ? null : "history",
activeDropdown === "history" ? null : "history"
)
}
className="group rounded-xl p-2 bg-background/80 transition-all hover:bg-foreground/[0.05] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground/40"
@ -237,7 +237,7 @@ export function AIChatInterface() {
<Button
onClick={() =>
setActiveDropdown(
activeDropdown === "magic" ? null : "magic",
activeDropdown === "magic" ? null : "magic"
)
}
className="group rounded-xl p-2 bg-background/80 transition-all hover:bg-foreground/[0.05] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground/40"
@ -259,7 +259,7 @@ export function AIChatInterface() {
<Button
onClick={() =>
setActiveDropdown(
activeDropdown === "model" ? null : "model",
activeDropdown === "model" ? null : "model"
)
}
className="flex items-center gap-2 rounded-xl border border-border/30 bg-background/80 px-3 py-1.5 text-sm font-medium text-foreground/80 transition hover:border-border/40 hover:bg-background/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground/40"

View file

@ -47,7 +47,7 @@ export function ChatApp() {
// Auto-scroll to bottom when new messages arrive
if (scrollAreaRef.current) {
const scrollContainer = scrollAreaRef.current.querySelector(
"[data-radix-scroll-area-viewport]",
"[data-radix-scroll-area-viewport]"
);
if (scrollContainer) {
scrollContainer.scrollTop = scrollContainer.scrollHeight;

View file

@ -161,7 +161,7 @@ type ReplyCursorState = Record<string, number>;
export function Messenger() {
const [selectedConversationId, setSelectedConversationId] = useState<string>(
initialConversations[0]?.id ?? "",
initialConversations[0]?.id ?? ""
);
const [conversations, setConversations] =
useState<Conversation[]>(initialConversations);
@ -181,7 +181,7 @@ export function Messenger() {
const activeConversation = useMemo(() => {
return conversations.find(
(conversation) => conversation.id === selectedConversationId,
(conversation) => conversation.id === selectedConversationId
);
}, [conversations, selectedConversationId]);
@ -191,8 +191,8 @@ export function Messenger() {
prev.map((conversation) =>
conversation.id === selectedConversationId
? { ...conversation, unread: 0 }
: conversation,
),
: conversation
)
);
}, [selectedConversationId]);
@ -274,8 +274,8 @@ export function Messenger() {
messages: [...conversation.messages, outgoing],
unread: 0,
}
: conversation,
),
: conversation
)
);
setDraft("");
@ -313,7 +313,7 @@ export function Messenger() {
messages: [...conversation.messages, incoming],
unread: isActive ? 0 : conversation.unread + 1,
};
}),
})
);
setReplyCursor((prev) => ({
@ -432,7 +432,7 @@ export function Messenger() {
"group relative flex w-full items-start gap-3 rounded-2xl border border-transparent p-3 text-left transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background",
isActive
? "border-primary/40 bg-primary/10"
: "bg-background/70 hover:border-border/40 hover:bg-muted/40",
: "bg-background/70 hover:border-border/40 hover:bg-muted/40"
)}
role="listitem"
>
@ -455,7 +455,7 @@ export function Messenger() {
variant="outline"
className={cn(
"rounded-full px-2.5 py-1 text-[0.65rem] font-medium hover:bg-inherit hover:text-inherit",
statusTone[conversation.status],
statusTone[conversation.status]
)}
>
{statusCopy[conversation.status]}
@ -504,7 +504,7 @@ export function Messenger() {
<span
className={cn(
"absolute right-1 top-1 inline-flex h-2.5 w-2.5 rounded-full",
statusTone[activeConversation.status],
statusTone[activeConversation.status]
)}
aria-hidden="true"
/>
@ -580,7 +580,7 @@ export function Messenger() {
className={cn(
"relative max-w-[82%] rounded-2xl border border-border/40 bg-background/80 px-4 py-3 text-sm leading-relaxed text-foreground backdrop-blur",
message.sender === "user" &&
"ml-auto border-primary/40 bg-primary text-primary-foreground",
"ml-auto border-primary/40 bg-primary text-primary-foreground"
)}
>
<p className="font-medium text-foreground/80">
@ -591,7 +591,7 @@ export function Messenger() {
"mt-1 text-[0.95rem]",
message.sender === "user"
? "text-primary-foreground/90"
: "text-foreground/90",
: "text-foreground/90"
)}
>
{message.text}
@ -601,7 +601,7 @@ export function Messenger() {
className={cn(
"text-muted-foreground",
message.sender === "user" &&
"text-primary-foreground/80",
"text-primary-foreground/80"
)}
>
{message.timestamp}

View file

@ -16,7 +16,7 @@ export function GlassProfileSettingsCard() {
const [notifications, setNotifications] = useState(true);
const [newsletter, setNewsletter] = useState(false);
const [bio, setBio] = useState(
"Designing expressive interfaces that feel alive.",
"Designing expressive interfaces that feel alive."
);
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {

View file

@ -93,14 +93,14 @@ export function NotificationCenter() {
window.setTimeout(() => {
setNotifications((prev) =>
prev.filter((notification) => notification.id !== id),
prev.filter((notification) => notification.id !== id)
);
}, 8000);
}, []);
const removeNotification = useCallback((id: string) => {
setNotifications((prev) =>
prev.filter((notification) => notification.id !== id),
prev.filter((notification) => notification.id !== id)
);
}, []);
@ -195,7 +195,7 @@ function NotificationBar({
aria-hidden="true"
className={cn(
"flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-muted/80",
toneClassName,
toneClassName
)}
>
<Icon className="h-5 w-5" />

View file

@ -110,11 +110,11 @@ function formatNumber(num: number): string {
function StatusSection() {
const totalValue = mockStocks.reduce(
(sum, stock) => sum + stock.price * 100,
0,
0
);
const totalChange = mockStocks.reduce(
(sum, stock) => sum + stock.change * 100,
0,
0
);
const totalChangePercent = (totalChange / (totalValue - totalChange)) * 100;

View file

@ -416,7 +416,7 @@ export function WeatherDashboard(): React.ReactElement {
label: "Precipitation",
value: weatherData.current.precipitationChance + "%",
description: describePrecipitation(
weatherData.current.precipitationChance,
weatherData.current.precipitationChance
),
icon: Umbrella,
},
@ -430,7 +430,7 @@ export function WeatherDashboard(): React.ReactElement {
temperature: hour.temperature,
feelsLike: hour.feelsLike,
})),
[weatherData],
[weatherData]
);
const hourlyForecast = weatherData.hourly.slice(0, 8);
@ -441,7 +441,7 @@ export function WeatherDashboard(): React.ReactElement {
const updatedMinutesAgo = useMemo(() => {
const updatedDate = new Date(weatherData.updatedAt);
const diffMinutes = Math.floor(
(Date.now() - updatedDate.getTime()) / (1000 * 60),
(Date.now() - updatedDate.getTime()) / (1000 * 60)
);
if (diffMinutes <= 1) return "Updated moments ago";
if (diffMinutes < 60) return "Updated " + diffMinutes + " minutes ago";

View file

@ -139,12 +139,12 @@ export function CashFlowChart() {
const displayData = useMemo(
() => (activeView === "monthly" ? monthlyData : yearlyData),
[activeView],
[activeView]
);
const maxValue = useMemo(
() => Math.max(...displayData.map((d) => d.value)),
[displayData],
[displayData]
);
const summaryText = useMemo(() => {
@ -153,22 +153,22 @@ export function CashFlowChart() {
}
const highest = displayData.reduce((prev, current) =>
current.value > prev.value ? current : prev,
current.value > prev.value ? current : prev
);
const lowest = displayData.reduce((prev, current) =>
current.value < prev.value ? current : prev,
current.value < prev.value ? current : prev
);
const first = displayData[0];
const last = displayData[displayData.length - 1];
if (activeView === "monthly") {
return `Monthly cash flow from ${first.label} to ${last.label}. Highest cash flow in ${highest.label} at ${formatCurrency(
highest.value,
highest.value
)}. Lowest in ${lowest.label} at ${formatCurrency(lowest.value)}.`;
}
return `Yearly cash flow from ${first.label} to ${last.label}. Highest cash flow in ${highest.label} at ${formatCurrency(
highest.value,
highest.value
)}. Lowest in ${lowest.label} at ${formatCurrency(lowest.value)}.`;
}, [activeView, displayData]);
@ -208,7 +208,7 @@ export function CashFlowChart() {
className="text-5xl font-bold text-foreground"
>
{formatCurrency(
displayData.reduce((total, datum) => total + datum.cashflow, 0),
displayData.reduce((total, datum) => total + datum.cashflow, 0)
)}
</motion.h2>
<p id={chartSummaryId} className="sr-only" aria-live="polite">
@ -228,7 +228,7 @@ export function CashFlowChart() {
"px-6 py-2 rounded-full text-sm font-medium transition-all duration-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-secondary",
activeView === "monthly"
? "bg-primary text-primary-foreground shadow-lg"
: "text-foreground hover:text-primary",
: "text-foreground hover:text-primary"
)}
aria-pressed={activeView === "monthly"}
aria-controls="cashflow-chart-bars"
@ -242,7 +242,7 @@ export function CashFlowChart() {
"px-6 py-2 rounded-full text-sm font-medium transition-all duration-300 flex items-center gap-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-secondary",
activeView === "yearly"
? "bg-primary text-primary-foreground shadow-lg"
: "text-foreground hover:text-primary",
: "text-foreground hover:text-primary"
)}
aria-pressed={activeView === "yearly"}
aria-controls="cashflow-chart-bars"
@ -358,13 +358,13 @@ export function CashFlowChart() {
className={`w-full rounded-t-2xl relative overflow-hidden cursor-pointer transition-colors duration-300 min-h-[0.5rem] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background ${tooltipVisible ? "bg-gradient-to-b from-primary to-primary/20" : "bg-primary/30"}
`}
aria-label={`${data.label} cash flow ${formatCurrency(
data.cashflow,
data.cashflow
)} with ${formatCurrency(data.inflow)} in inflow`}
aria-describedby={`${data.shortLabel}-summary`}
>
<span id={`${data.shortLabel}-summary`} className="sr-only">
{`${data.label}: cash flow ${formatCurrency(
data.cashflow,
data.cashflow
)}, inflow ${formatCurrency(data.inflow)}`}
</span>
</motion.button>

View file

@ -3,16 +3,16 @@ interface GradientOverlayProps {
}
export default function GradientOverlay({
className = '',
className = "",
}: GradientOverlayProps) {
return (
<div
className={`fixed bottom-0 w-full h-[50px] backdrop-blur-xl z-40 ${className}`}
style={{
maskImage:
'linear-gradient(to top, rgba(0,0,0,1) 0%, rgba(0,0,0,0) 100%)',
"linear-gradient(to top, rgba(0,0,0,1) 0%, rgba(0,0,0,0) 100%)",
WebkitMaskImage:
'linear-gradient(to top, rgba(0,0,0,1) 0%, rgba(0,0,0,0) 100%)',
"linear-gradient(to top, rgba(0,0,0,1) 0%, rgba(0,0,0,0) 100%)",
}}
/>
);

View file

@ -7,7 +7,6 @@ import { Badge } from "./ui/badge";
import { ThemeToggle } from "@/components/theme-toggle";
export function Header() {
return (
<>
<motion.header

View file

@ -26,9 +26,18 @@ export default function HomePageContent() {
{/* Top centered actions */}
<section className="bg-background/80">
<div className="container relative mx-auto flex flex-col items-center gap-4 px-4 py-32">
<div aria-hidden="true" className="pointer-events-none absolute -top-24 left-1/2 h-64 w-64 -translate-x-1/2 rounded-full bg-primary/20 blur-[160px]"></div>
<div aria-hidden="true" className="pointer-events-none absolute -top-24 left-0 h-64 w-64 -translate-x-1/2 rounded-full bg-primary/20 blur-[160px]"></div>
<div aria-hidden="true" className="pointer-events-none absolute -top-24 right-0 h-64 w-64 -translate-x-1/2 rounded-full bg-primary/20 blur-[160px]"></div>
<div
aria-hidden="true"
className="pointer-events-none absolute -top-24 left-1/2 h-64 w-64 -translate-x-1/2 rounded-full bg-primary/20 blur-[160px]"
></div>
<div
aria-hidden="true"
className="pointer-events-none absolute -top-24 left-0 h-64 w-64 -translate-x-1/2 rounded-full bg-primary/20 blur-[160px]"
></div>
<div
aria-hidden="true"
className="pointer-events-none absolute -top-24 right-0 h-64 w-64 -translate-x-1/2 rounded-full bg-primary/20 blur-[160px]"
></div>
<motion.p
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }}

View file

@ -42,7 +42,7 @@ export function LiveEditor({ initialCode }: LiveEditorProps) {
if (!processed.includes("export default")) {
// If there's a named export, convert it to default
const namedExportMatch = processed.match(
/export\s+(function|const)\s+(\w+)/,
/export\s+(function|const)\s+(\w+)/
);
if (namedExportMatch) {
const componentName = namedExportMatch[2];

View file

@ -49,7 +49,7 @@ export function PreviewDetailsCard() {
: { duration: 0.2, ease: [0.4, 0, 0.2, 1] },
},
}),
[shouldReduceMotion],
[shouldReduceMotion]
);
const hoverMotion = shouldReduceMotion ? undefined : { scale: 1.02, y: -2 };

View file

@ -51,7 +51,7 @@ export function AIGlowInput({
top: `${Math.random() * 100}%`,
delay: Math.random() * 1.8,
})),
[],
[]
);
const springIntensity = useSpring(glowIntensityMotion, {
@ -121,7 +121,7 @@ export function AIGlowInput({
<motion.div
className={cn(
"absolute -top-24 left-1/2 h-48 w-48 -translate-x-1/2 rounded-full blur-[140px]",
isDarkMode ? "bg-white/15" : "bg-[#ddd]",
isDarkMode ? "bg-white/15" : "bg-[#ddd]"
)}
animate={
shouldReduceMotion
@ -176,7 +176,7 @@ export function AIGlowInput({
"relative w-full rounded-[calc(theme(borderRadius.2xl)-0.25rem)] border border-border/60 px-6 py-4 text-base placeholder:text-muted-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-background transition-colors",
isDarkMode
? "bg-white/5 text-muted-foreground focus-visible:ring-white/40"
: "bg-white/90 text-foreground focus-visible:ring-[#ddd]",
: "bg-white/90 text-foreground focus-visible:ring-[#ddd]"
)}
style={{
boxShadow: `0 0 ${shadowBlur}px rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${glowShadowAlpha})`,

View file

@ -289,7 +289,7 @@ export function AIUnlockAnimation({ autoPlay = true }: AIUnlockAnimationProps) {
animate={{ opacity: [0, 0.4, 0], scale: [0.8, 1, 1] }}
transition={{ duration: 0.8, delay: i * 0.05 }}
/>
),
)
)}
</>
)}

View file

@ -26,7 +26,7 @@ export function DragToConfirmSlider({
const handleDragEnd = (
_: MouseEvent | TouchEvent | PointerEvent,
info: PanInfo,
info: PanInfo
) => {
const currentX = x.get();
if (currentX >= 180) {

View file

@ -41,7 +41,7 @@ export function DynamicSpotlightCTA({
duration: Math.random() * 3 + 2,
delay: Math.random() * 2,
})),
[],
[]
);
const handleMouseMove = (event: MouseEvent<HTMLDivElement>) => {

View file

@ -86,7 +86,7 @@ export function HolographicWall({
const distance = mousePosition
? Math.sqrt(
Math.pow(letter.x - mousePosition.x, 2) +
Math.pow(letter.y - mousePosition.y, 2),
Math.pow(letter.y - mousePosition.y, 2)
)
: Infinity;

View file

@ -37,7 +37,7 @@ export function MoodGradientButton({
// Use a state-based approach for the gradient since useTransform with arrays isn't directly supported
const [bgStyle, setBgStyle] = useState(
"radial-gradient(circle at 50% 50%, rgba(99, 102, 241, 0.8), rgba(139, 92, 246, 0.6), rgba(236, 72, 153, 0.4))",
"radial-gradient(circle at 50% 50%, rgba(99, 102, 241, 0.8), rgba(139, 92, 246, 0.6), rgba(236, 72, 153, 0.4))"
);
// Update gradient on mouse move
@ -45,13 +45,13 @@ export function MoodGradientButton({
const unsubscribeX = springX.on("change", (xVal) => {
const yVal = springY.get();
setBgStyle(
`radial-gradient(circle at ${xVal}% ${yVal}%, rgba(99, 102, 241, 0.8), rgba(139, 92, 246, 0.6), rgba(236, 72, 153, 0.4))`,
`radial-gradient(circle at ${xVal}% ${yVal}%, rgba(99, 102, 241, 0.8), rgba(139, 92, 246, 0.6), rgba(236, 72, 153, 0.4))`
);
});
const unsubscribeY = springY.on("change", (yVal) => {
const xVal = springX.get();
setBgStyle(
`radial-gradient(circle at ${xVal}% ${yVal}%, rgba(99, 102, 241, 0.8), rgba(139, 92, 246, 0.6), rgba(236, 72, 153, 0.4))`,
`radial-gradient(circle at ${xVal}% ${yVal}%, rgba(99, 102, 241, 0.8), rgba(139, 92, 246, 0.6), rgba(236, 72, 153, 0.4))`
);
});
return () => {

View file

@ -16,7 +16,7 @@ export function ReactiveBackgroundGrid({
Array<{ id: number; x: number; y: number }>
>([]);
const [hoveredDot, setHoveredDot] = useState<{ x: number; y: number } | null>(
null,
null
);
const handleClick = (e: MouseEvent<HTMLDivElement>) => {
@ -81,7 +81,7 @@ export function ReactiveBackgroundGrid({
const distance = hoveredDot
? Math.sqrt(
Math.pow(x - (hoveredDot.x / containerSize.width) * 100, 2) +
Math.pow(y - (hoveredDot.y / containerSize.height) * 100, 2),
Math.pow(y - (hoveredDot.y / containerSize.height) * 100, 2)
)
: Infinity;

View file

@ -49,7 +49,7 @@ function ScrollProgressTrackerInner({
const colorProgress = useTransform(
scrollYProgress,
[0, 0.5, 1],
["rgb(99, 102, 241)", "rgb(139, 92, 246)", "rgb(236, 72, 153)"],
["rgb(99, 102, 241)", "rgb(139, 92, 246)", "rgb(236, 72, 153)"]
);
return (

View file

@ -54,9 +54,9 @@ export function CommandPalette() {
const filteredCommands = useMemo(
() =>
commands.filter((cmd) =>
cmd.label.toLowerCase().includes(query.toLowerCase()),
cmd.label.toLowerCase().includes(query.toLowerCase())
),
[query],
[query]
);
const panelVariants: Variants = shouldReduceMotion

View file

@ -44,7 +44,7 @@ export function MacSearchbar() {
];
const filteredItems = items.filter((item) =>
item.title.toLowerCase().includes(searchQuery.toLowerCase()),
item.title.toLowerCase().includes(searchQuery.toLowerCase())
);
return (

View file

@ -319,7 +319,7 @@ export function BentoGridBlock() {
>
{tag}
</span>
),
)
)}
</div>
</div>

View file

@ -49,7 +49,7 @@ export function ContactFormSection() {
};
const handleChange = (
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
setFormData((prev) => ({
...prev,

View file

@ -44,7 +44,7 @@ export function CTABannerSection() {
},
},
}),
[shouldReduceMotion],
[shouldReduceMotion]
);
const itemVariants: Variants = useMemo(
@ -63,7 +63,7 @@ export function CTABannerSection() {
: { type: "spring", stiffness: 120, damping: 18, mass: 0.9 },
},
}),
[shouldReduceMotion],
[shouldReduceMotion]
);
const arrowAnimation = shouldReduceMotion

View file

@ -63,7 +63,7 @@ export function GalleryGridBlock() {
const handleNext = () => {
if (selectedImage !== null) {
const currentIndex = galleryImages.findIndex(
(img) => img.id === selectedImage,
(img) => img.id === selectedImage
);
const nextIndex = (currentIndex + 1) % galleryImages.length;
setSelectedImage(galleryImages[nextIndex].id);
@ -73,7 +73,7 @@ export function GalleryGridBlock() {
const handlePrev = () => {
if (selectedImage !== null) {
const currentIndex = galleryImages.findIndex(
(img) => img.id === selectedImage,
(img) => img.id === selectedImage
);
const prevIndex =
(currentIndex - 1 + galleryImages.length) % galleryImages.length;
@ -82,12 +82,12 @@ export function GalleryGridBlock() {
};
const selectedImageData = galleryImages.find(
(img) => img.id === selectedImage,
(img) => img.id === selectedImage
);
const handleCardKeyDown = (
event: KeyboardEvent<HTMLDivElement>,
imageId: number,
imageId: number
) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();

View file

@ -75,7 +75,7 @@ export function GlowyWavesHero() {
const computeThemeColors = () => {
const rootStyles = getComputedStyle(document.documentElement);
// Helper to convert any CSS color to a Canvas-compatible format
const resolveColor = (variables: string[], alpha = 1) => {
// Create a temporary element to get computed color
@ -94,11 +94,13 @@ export function GlowyWavesHero() {
// Try to set the background color using the CSS variable
tempEl.style.backgroundColor = `var(${variable})`;
const computedColor = getComputedStyle(tempEl).backgroundColor;
if (computedColor && computedColor !== "rgba(0, 0, 0, 0)") {
// Convert RGB to RGBA with alpha if needed
if (alpha < 1) {
const rgbMatch = computedColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*[\d.]+)?\)/);
const rgbMatch = computedColor.match(
/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*[\d.]+)?\)/
);
if (rgbMatch) {
color = `rgba(${rgbMatch[1]}, ${rgbMatch[2]}, ${rgbMatch[3]}, ${alpha})`;
} else {
@ -172,7 +174,7 @@ export function GlowyWavesHero() {
});
const prefersReducedMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)",
"(prefers-reduced-motion: reduce)"
).matches;
const mouseInfluence = prefersReducedMotion ? 10 : 70;

View file

@ -280,7 +280,7 @@ function FilterPanel({
};
const hasActiveFilters = Object.values(filters).some(
(group) => group.length > 0,
(group) => group.length > 0
);
return (
@ -506,7 +506,7 @@ export function InteractiveLogsTable() {
expanded={expandedId === log.id}
onToggle={() =>
setExpandedId((current) =>
current === log.id ? null : log.id,
current === log.id ? null : log.id
)
}
/>

View file

@ -165,10 +165,10 @@ export function N8nWorkflowBlock() {
const [draggingNodeId, setDraggingNodeId] = useState<string | null>(null);
const [contentSize, setContentSize] = useState(() => {
const maxX = Math.max(
...initialNodes.map((n) => n.position.x + NODE_WIDTH),
...initialNodes.map((n) => n.position.x + NODE_WIDTH)
);
const maxY = Math.max(
...initialNodes.map((n) => n.position.y + NODE_HEIGHT),
...initialNodes.map((n) => n.position.y + NODE_HEIGHT)
);
return { width: maxX + 50, height: maxY + 50 };
});
@ -196,8 +196,8 @@ export function N8nWorkflowBlock() {
prev.map((node) =>
node.id === nodeId
? { ...node, position: { x: constrainedX, y: constrainedY } }
: node,
),
: node
)
);
});

View file

@ -94,7 +94,7 @@ export function NewsletterSignupBlock() {
{feature}
</Badge>
</motion.div>
),
)
)}
</div>
</motion.div>

View file

@ -57,7 +57,7 @@ export function ScrollReveal() {
},
},
}),
[shouldReduceMotion],
[shouldReduceMotion]
);
const cardVariants: Variants = useMemo(
@ -76,7 +76,7 @@ export function ScrollReveal() {
: { type: "spring", stiffness: 160, damping: 22, mass: 0.8 },
},
}),
[shouldReduceMotion],
[shouldReduceMotion]
);
const iconVariants: Variants = useMemo(
@ -93,7 +93,7 @@ export function ScrollReveal() {
: { duration: 0.45, ease: [0.18, 0.89, 0.32, 1.28] },
},
}),
[shouldReduceMotion],
[shouldReduceMotion]
);
return (

View file

@ -64,7 +64,7 @@ export function StatsSection() {
},
},
}),
[shouldReduceMotion],
[shouldReduceMotion]
);
const cardVariants: Variants = useMemo(
@ -83,7 +83,7 @@ export function StatsSection() {
: { type: "spring", mass: 0.8, stiffness: 160, damping: 24 },
},
}),
[shouldReduceMotion],
[shouldReduceMotion]
);
const iconVariants: Variants = useMemo(
@ -100,7 +100,7 @@ export function StatsSection() {
: { duration: 0.45, ease: [0.18, 0.89, 0.32, 1.28] },
},
}),
[shouldReduceMotion],
[shouldReduceMotion]
);
return (

View file

@ -47,7 +47,7 @@ export function TestimonialSection() {
const prevTestimonial = () => {
setCurrentIndex(
(prev) => (prev - 1 + testimonials.length) % testimonials.length,
(prev) => (prev - 1 + testimonials.length) % testimonials.length
);
};
@ -91,7 +91,7 @@ export function TestimonialSection() {
>
<Star className="h-5 w-5 fill-yellow-400 text-yellow-400" />
</motion.div>
),
)
)}
</div>
<motion.div

View file

@ -84,7 +84,7 @@ export function ThemeProvider({
}
applyTheme(nextTheme);
},
[applyTheme],
[applyTheme]
);
const toggleTheme = useCallback(() => {
@ -149,7 +149,7 @@ export function ThemeProvider({
setTheme,
toggleTheme,
}),
[resolvedTheme, setTheme, theme, toggleTheme],
[resolvedTheme, setTheme, theme, toggleTheme]
);
return (

View file

@ -42,7 +42,7 @@ export function ThemeToggle() {
size="icon"
className={cn(
"relative h-10 w-10",
!isMounted && "animate-pulse bg-muted/30 text-muted-foreground",
!isMounted && "animate-pulse bg-muted/30 text-muted-foreground"
)}
aria-label="Toggle theme"
>

View file

@ -13,7 +13,7 @@ const Avatar = React.forwardRef<
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className,
className
)}
{...props}
/>
@ -40,7 +40,7 @@ const AvatarFallback = React.forwardRef<
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className,
className
)}
{...props}
/>

View file

@ -18,7 +18,7 @@ const badgeVariants = cva(
defaultVariants: {
variant: "default",
},
},
}
);
export interface BadgeProps

View file

@ -29,7 +29,7 @@ const buttonVariants = cva(
variant: "default",
size: "default",
},
},
}
);
export interface ButtonProps
@ -48,7 +48,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
{...props}
/>
);
},
}
);
Button.displayName = "Button";

View file

@ -9,7 +9,7 @@ const Card = React.forwardRef<
ref={ref}
className={cn(
"rounded-lg border border-border bg-card text-card-foreground shadow-sm",
className,
className
)}
{...props}
/>
@ -36,7 +36,7 @@ const CardTitle = React.forwardRef<
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className,
className
)}
{...props}
/>

View file

@ -14,7 +14,7 @@ const Checkbox = React.forwardRef<
ref={ref}
className={cn(
"grid place-content-center peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className,
className
)}
{...props}
>

View file

@ -22,7 +22,7 @@ const DialogOverlay = React.forwardRef<
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
className
)}
{...props}
/>
@ -39,7 +39,7 @@ const DialogContent = React.forwardRef<
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className,
className
)}
{...props}
>
@ -60,7 +60,7 @@ const DialogHeader = ({
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className,
className
)}
{...props}
/>
@ -74,7 +74,7 @@ const DialogFooter = ({
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className,
className
)}
{...props}
/>
@ -89,7 +89,7 @@ const DialogTitle = React.forwardRef<
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className,
className
)}
{...props}
/>

View file

@ -29,7 +29,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className,
className
)}
{...props}
>
@ -48,7 +48,7 @@ const DropdownMenuSubContent = React.forwardRef<
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
className
)}
{...props}
/>
@ -66,7 +66,7 @@ const DropdownMenuContent = React.forwardRef<
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
className
)}
{...props}
/>
@ -85,7 +85,7 @@ const DropdownMenuItem = React.forwardRef<
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className,
className
)}
{...props}
/>
@ -100,7 +100,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
className
)}
checked={checked}
{...props}
@ -124,7 +124,7 @@ const DropdownMenuRadioItem = React.forwardRef<
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
className
)}
{...props}
>
@ -149,7 +149,7 @@ const DropdownMenuLabel = React.forwardRef<
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className,
className
)}
{...props}
/>

View file

@ -11,14 +11,14 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
className
)}
autoComplete="off"
ref={ref}
{...props}
/>
);
},
}
);
Input.displayName = "Input";

View file

@ -11,12 +11,12 @@ const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
ref={ref}
className={cn(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
className,
className
)}
{...props}
/>
);
},
}
);
Label.displayName = "Label";

View file

@ -19,7 +19,7 @@ const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(
type={showPassword ? "text" : "password"}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 pr-10 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
className
)}
ref={ref}
{...props}
@ -56,7 +56,7 @@ const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(
</button>
</div>
);
},
}
);
PasswordInput.displayName = "PasswordInput";

View file

@ -11,7 +11,7 @@ const Separator = React.forwardRef<
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref,
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
@ -20,11 +20,11 @@ const Separator = React.forwardRef<
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className,
className
)}
{...props}
/>
),
)
);
Separator.displayName = SeparatorPrimitive.Root.displayName;

View file

@ -12,14 +12,14 @@ const Switch = React.forwardRef<
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className,
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0",
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>

View file

@ -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}
/>

View file

@ -10,13 +10,13 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
className
)}
ref={ref}
{...props}
/>
);
},
}
);
Textarea.displayName = "Textarea";

View file

@ -21,7 +21,7 @@ const TooltipContent = React.forwardRef<
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]",
className,
className
)}
{...props}
/>

View file

@ -13,7 +13,7 @@ export const sanitizeSlug = (value: string) =>
export function generateUniqueSlug(
baseName: string,
existingSlugs: string[],
existingSlugs: string[]
): string {
const baseSlug = sanitizeSlug(baseName) || "page";
if (!existingSlugs.includes(baseSlug)) {
@ -32,7 +32,7 @@ export function generateUniqueSlug(
export function createPage(
name: string,
existingSlugs: string[],
existingSlugs: string[]
): BuilderProjectPage {
const id =
typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
@ -70,7 +70,6 @@ export function extractSavedPages(project: SavedProject): SavedProjectPage[] {
export function countSavedProjectComponents(project: SavedProject): number {
return extractSavedPages(project).reduce(
(total, page) => total + (page.components?.length ?? 0),
0,
0
);
}

View file

@ -1903,9 +1903,11 @@ export function getComponentById(id: string): Component | undefined {
}
export function getAnimationsByCategory(
category: ComponentCategory,
category: ComponentCategory
): Component[] {
return componentsRegistry.filter((component) => component.category === category);
return componentsRegistry.filter(
(component) => component.category === category
);
}
export function searchComponents(query: string): Component[] {
@ -1914,7 +1916,7 @@ export function searchComponents(query: string): Component[] {
(component) =>
component.name.toLowerCase().includes(lowerQuery) ||
component.description.toLowerCase().includes(lowerQuery) ||
component.tags.some((tag) => tag.toLowerCase().includes(lowerQuery)),
component.tags.some((tag) => tag.toLowerCase().includes(lowerQuery))
);
}

View file

@ -1,5 +1,5 @@
// lib/get-component-code.ts
'use server'
"use server";
import { readFileSync } from "fs";
import { join } from "path";
@ -30,4 +30,4 @@ export async function getComponentCode(filePath: string): Promise<string> {
`Failed to read file at ${filePath}: ${error instanceof Error ? error.message : String(error)}`
);
}
}
}

View file

@ -46,7 +46,7 @@ export function mergeComponentImports(code: string): string {
// Check if this line completes the import (has closing brace and from)
if (trimmedLine.includes("}") && trimmedLine.includes("from")) {
const match = currentMultilineImport.match(
/import\s*{\s*([^}]+)\s*}\s*from\s*['"]([^'"]+)['"]/,
/import\s*{\s*([^}]+)\s*}\s*from\s*['"]([^'"]+)['"]/
);
if (match) {
const [, namedImports, source] = match;

View file

@ -67,7 +67,7 @@ const noIndexRobots: RobotsConfig = {
const defaultImageAbsolute = new URL(
siteConfig.defaultImage,
metadataBase,
metadataBase
).toString();
const defaultOpenGraph: NonNullable<Metadata["openGraph"]> = {

View file

@ -1,6 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
return twMerge(clsx(inputs));
}

16
package-lock.json generated
View file

@ -46,6 +46,7 @@
"eslint": "^9",
"eslint-config-next": "16.0.3",
"postcss": "^8.5.6",
"prettier": "^3.6.2",
"tailwindcss": "^4.1.17",
"tw-animate-css": "^1.4.0",
"typescript": "^5"
@ -8467,6 +8468,21 @@
"node": ">= 0.8.0"
}
},
"node_modules/prettier": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/prismjs": {
"version": "1.30.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",

View file

@ -7,7 +7,9 @@
"dev": "npm run sync-registry && next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
"lint": "eslint",
"format": "prettier --write .",
"format:check": "prettier --check ."
},
"dependencies": {
"@codesandbox/sandpack-react": "^2.20.0",
@ -48,6 +50,7 @@
"eslint": "^9",
"eslint-config-next": "16.0.3",
"postcss": "^8.5.6",
"prettier": "^3.6.2",
"tailwindcss": "^4.1.17",
"tw-animate-css": "^1.4.0",
"typescript": "^5"

File diff suppressed because it is too large Load diff

View file

@ -1,105 +1,116 @@
const fs = require('fs');
const path = require('path');
const fs = require("fs");
const path = require("path");
const COMPONENTS_REGISTRY_PATH = path.join(__dirname, '../lib/components-registry.tsx');
const REGISTRY_JSON_PATH = path.join(__dirname, '../registry.json');
const COMPONENTS_REGISTRY_PATH = path.join(
__dirname,
"../lib/components-registry.tsx"
);
const REGISTRY_JSON_PATH = path.join(__dirname, "../registry.json");
// Category to registry type mapping
const CATEGORY_TO_TYPE = {
microinteractions: 'registry:ui',
components: 'registry:component',
page: 'registry:page',
data: 'registry:ui',
decorative: 'registry:ui',
blocks: 'registry:block',
'motion-core': 'registry:lib',
microinteractions: "registry:ui",
components: "registry:component",
page: "registry:page",
data: "registry:ui",
decorative: "registry:ui",
blocks: "registry:block",
"motion-core": "registry:lib",
};
// Category to registry category mapping
const CATEGORY_MAPPING = {
microinteractions: 'micro',
components: 'components',
page: 'page',
data: 'data',
decorative: 'decorative',
blocks: 'sections',
'motion-core': 'motion-core',
microinteractions: "micro",
components: "components",
page: "page",
data: "data",
decorative: "decorative",
blocks: "sections",
"motion-core": "motion-core",
};
// Common dependencies based on imports
const DEFAULT_DEPENDENCIES = {
framer: ['framer-motion'],
lucide: ['lucide-react'],
react: ['react'],
framer: ["framer-motion"],
lucide: ["lucide-react"],
react: ["react"],
};
/**
* Extract component imports and registry entries from components-registry.tsx
*/
function parseComponentsRegistry() {
const content = fs.readFileSync(COMPONENTS_REGISTRY_PATH, 'utf-8');
const content = fs.readFileSync(COMPONENTS_REGISTRY_PATH, "utf-8");
// Extract component imports - handle both single and multiple imports
const componentImports = {};
const importRegex = /import\s+{\s*([^}]+)\s*}\s+from\s+["']@\/components\/([^"']+)["']/g;
const importRegex =
/import\s+{\s*([^}]+)\s*}\s+from\s+["']@\/components\/([^"']+)["']/g;
let match;
while ((match = importRegex.exec(content)) !== null) {
const imports = match[1].split(',').map(i => i.trim());
const imports = match[1].split(",").map((i) => i.trim());
const importPath = match[2];
imports.forEach(imp => {
imports.forEach((imp) => {
// Extract component name (remove type annotations if any)
const componentName = imp.split(':')[0].trim();
const componentName = imp.split(":")[0].trim();
componentImports[componentName] = importPath;
});
}
// Extract componentsRegistry entries
const registryStart = content.indexOf('export const componentsRegistry: Component[] = [');
const registryEnd = content.lastIndexOf('];', content.length);
const registryStart = content.indexOf(
"export const componentsRegistry: Component[] = ["
);
const registryEnd = content.lastIndexOf("];", content.length);
if (registryStart === -1 || registryEnd === -1) {
throw new Error('Could not find componentsRegistry in components-registry.tsx');
throw new Error(
"Could not find componentsRegistry in components-registry.tsx"
);
}
const registryContent = content.substring(registryStart, registryEnd);
// Parse entries - split by entry boundaries
const entries = [];
const entryPattern = /\{\s*id:\s*"([^"]+)"/g;
let lastIndex = 0;
const entryIndices = [];
while ((match = entryPattern.exec(registryContent)) !== null) {
entryIndices.push(match.index);
}
// Parse each entry
for (let i = 0; i < entryIndices.length; i++) {
const start = entryIndices[i];
const end = i < entryIndices.length - 1 ? entryIndices[i + 1] : registryContent.length;
const end =
i < entryIndices.length - 1
? entryIndices[i + 1]
: registryContent.length;
const entryText = registryContent.substring(start, end);
// Extract id
const idMatch = entryText.match(/id:\s*"([^"]+)"/);
if (!idMatch) continue;
const id = idMatch[1];
// Extract category
const categoryMatch = entryText.match(/category:\s*"([^"]+)"/);
if (!categoryMatch) continue;
const category = categoryMatch[1];
// Extract component name
const componentMatch = entryText.match(/component:\s*(\w+)/);
if (!componentMatch) continue;
const componentName = componentMatch[1];
const componentPath = componentImports[componentName];
if (componentPath) {
entries.push({
id,
@ -108,10 +119,12 @@ function parseComponentsRegistry() {
componentPath,
});
} else {
console.warn(`⚠️ Could not find import path for component: ${componentName}`);
console.warn(
`⚠️ Could not find import path for component: ${componentName}`
);
}
}
return entries;
}
@ -131,26 +144,39 @@ function convertToRegistryPath(componentPath) {
*/
function getRegistryDependencies(componentPath, category) {
const deps = [];
// Common shadcn components that might be used
const shadcnComponents = [
'button', 'card', 'badge', 'input', 'textarea', 'label',
'checkbox', 'radio', 'select', 'tabs', 'dialog', 'dropdown-menu',
'avatar', 'scroll-area', 'separator', 'switch',
"button",
"card",
"badge",
"input",
"textarea",
"label",
"checkbox",
"radio",
"select",
"tabs",
"dialog",
"dropdown-menu",
"avatar",
"scroll-area",
"separator",
"switch",
];
// Check if path suggests use of shadcn components
// This is a heuristic - you might need to adjust based on actual usage
if (category === 'components' || category === 'blocks') {
if (category === "components" || category === "blocks") {
// Most components use button and card
if (!componentPath.includes('micro/buttons')) {
deps.push('button');
if (!componentPath.includes("micro/buttons")) {
deps.push("button");
}
if (componentPath.includes('cards') || componentPath.includes('card')) {
deps.push('card');
if (componentPath.includes("cards") || componentPath.includes("card")) {
deps.push("card");
}
}
return deps;
}
@ -158,18 +184,20 @@ function getRegistryDependencies(componentPath, category) {
* Determine dependencies based on imports in the file
*/
function getDependencies(componentPath) {
const deps = ['framer-motion']; // Almost all components use framer-motion
const deps = ["framer-motion"]; // Almost all components use framer-motion
// Check if likely to use lucide-react
if (componentPath.includes('icons') ||
componentPath.includes('navigation') ||
componentPath.includes('tooltips')) {
deps.push('lucide-react');
if (
componentPath.includes("icons") ||
componentPath.includes("navigation") ||
componentPath.includes("tooltips")
) {
deps.push("lucide-react");
}
// React is always needed
deps.push('react');
deps.push("react");
return [...new Set(deps)]; // Remove duplicates
}
@ -177,42 +205,46 @@ function getDependencies(componentPath) {
* Determine subcategory based on component path
*/
function getSubcategory(componentPath, category) {
if (category === 'micro') {
if (componentPath.includes('buttons')) return 'buttons';
if (componentPath.includes('toggles')) return 'toggles';
if (componentPath.includes('icons')) return 'icons';
if (componentPath.includes('badges')) return 'badges';
if (componentPath.includes('links')) return 'links';
if (category === "micro") {
if (componentPath.includes("buttons")) return "buttons";
if (componentPath.includes("toggles")) return "toggles";
if (componentPath.includes("icons")) return "icons";
if (componentPath.includes("badges")) return "badges";
if (componentPath.includes("links")) return "links";
}
if (category === 'components') {
if (componentPath.includes('cards')) return 'cards';
if (componentPath.includes('modal') || componentPath.includes('dialog')) return 'modal';
if (componentPath.includes('dropdown')) return 'dropdown';
if (componentPath.includes('inputs') || componentPath.includes('input')) return 'inputs';
if (componentPath.includes('tabs')) return 'tabs';
if (componentPath.includes('lists') || componentPath.includes('list')) return 'lists';
if (componentPath.includes('chat')) return 'chat';
if (componentPath.includes('notifications')) return 'notifications';
if (componentPath.includes('forms')) return 'forms';
if (category === "components") {
if (componentPath.includes("cards")) return "cards";
if (componentPath.includes("modal") || componentPath.includes("dialog"))
return "modal";
if (componentPath.includes("dropdown")) return "dropdown";
if (componentPath.includes("inputs") || componentPath.includes("input"))
return "inputs";
if (componentPath.includes("tabs")) return "tabs";
if (componentPath.includes("lists") || componentPath.includes("list"))
return "lists";
if (componentPath.includes("chat")) return "chat";
if (componentPath.includes("notifications")) return "notifications";
if (componentPath.includes("forms")) return "forms";
}
if (category === 'page') {
if (componentPath.includes('hero')) return 'hero';
if (componentPath.includes('about')) return 'about';
if (componentPath.includes('notifications')) return 'notifications';
if (category === "page") {
if (componentPath.includes("hero")) return "hero";
if (componentPath.includes("about")) return "about";
if (componentPath.includes("notifications")) return "notifications";
}
if (category === 'data') {
if (componentPath.includes('progress')) return 'progress';
if (componentPath.includes('charts') || componentPath.includes('chart')) return 'charts';
if (category === "data") {
if (componentPath.includes("progress")) return "progress";
if (componentPath.includes("charts") || componentPath.includes("chart"))
return "charts";
}
if (category === 'decorative') {
if (componentPath.includes('text')) return 'text';
if (componentPath.includes('background')) return 'background';
if (category === "decorative") {
if (componentPath.includes("text")) return "text";
if (componentPath.includes("background")) return "background";
}
return null;
}
@ -222,8 +254,8 @@ function getSubcategory(componentPath, category) {
function createRegistryEntry(componentData) {
const { id, name, category, componentPath } = componentData;
const registryCategory = CATEGORY_MAPPING[category] || category;
const registryType = CATEGORY_TO_TYPE[category] || 'registry:component';
const registryType = CATEGORY_TO_TYPE[category] || "registry:component";
return {
name: id,
type: registryType,
@ -245,25 +277,27 @@ function createRegistryEntry(componentData) {
*/
function syncRegistry() {
try {
console.log('🔄 Syncing registry.json with components-registry.tsx...');
console.log("🔄 Syncing registry.json with components-registry.tsx...");
// Parse components registry
const components = parseComponentsRegistry();
console.log(`📦 Found ${components.length} components in registry`);
// Read existing registry.json
const registry = JSON.parse(fs.readFileSync(REGISTRY_JSON_PATH, 'utf-8'));
const registry = JSON.parse(fs.readFileSync(REGISTRY_JSON_PATH, "utf-8"));
const existingItems = registry.items || [];
const existingItemsMap = new Map(existingItems.map(item => [item.name, item]));
const existingItemsMap = new Map(
existingItems.map((item) => [item.name, item])
);
// Create/update entries
let added = 0;
let updated = 0;
for (const component of components) {
const registryEntry = createRegistryEntry(component);
const existing = existingItemsMap.get(component.id);
if (existing) {
// Update existing entry - always update path, preserve dependencies if they exist
const updatedEntry = {
@ -273,14 +307,16 @@ function syncRegistry() {
category: registryEntry.category,
subcategory: registryEntry.subcategory,
// Preserve existing dependencies if they exist, otherwise use defaults
dependencies: existing.dependencies?.length > 0
? existing.dependencies
: registryEntry.dependencies,
registryDependencies: existing.registryDependencies?.length > 0
? existing.registryDependencies
: registryEntry.registryDependencies,
dependencies:
existing.dependencies?.length > 0
? existing.dependencies
: registryEntry.dependencies,
registryDependencies:
existing.registryDependencies?.length > 0
? existing.registryDependencies
: registryEntry.registryDependencies,
};
existingItemsMap.set(component.id, updatedEntry);
updated++;
} else {
@ -289,25 +325,24 @@ function syncRegistry() {
added++;
}
}
// Update registry
registry.items = Array.from(existingItemsMap.values());
// Sort items by name for consistency
registry.items.sort((a, b) => a.name.localeCompare(b.name));
// Write back to file
fs.writeFileSync(
REGISTRY_JSON_PATH,
JSON.stringify(registry, null, 2) + '\n',
'utf-8'
JSON.stringify(registry, null, 2) + "\n",
"utf-8"
);
console.log(`✅ Registry synced! Added: ${added}, Updated: ${updated}`);
console.log(`📊 Total items in registry: ${registry.items.length}`);
} catch (error) {
console.error('❌ Error syncing registry:', error);
console.error("❌ Error syncing registry:", error);
process.exit(1);
}
}
@ -318,4 +353,3 @@ if (require.main === module) {
}
module.exports = { syncRegistry };

View file

@ -45,4 +45,3 @@ export type SavedProject = {
deploymentId?: string;
deploymentUrl?: string;
};