mirror of
https://github.com/moumen-soliman/uitripled
synced 2026-04-21 13:37:20 +00:00
fix un-registered components
This commit is contained in:
parent
5e38b5c859
commit
acef3fb079
88 changed files with 1163 additions and 1381 deletions
37
.prettierignore
Normal file
37
.prettierignore
Normal 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
10
.prettierrc.json
Normal 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
34
.vscode/settings.json
vendored
Normal 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
377
CONTRIBUTING.md
Normal 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! 🎉
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>{" "}
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -134,7 +134,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
@layer utilities {
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
|
|
@ -156,4 +155,4 @@
|
|||
flex-direction: row !important;
|
||||
display: flex !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export function AvatarGroup() {
|
|||
mass: 0.7,
|
||||
}),
|
||||
},
|
||||
[shouldReduceMotion],
|
||||
[shouldReduceMotion]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -86,4 +86,3 @@ export function BuilderHeader({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,4 +22,3 @@ export function DragOverlay({ activeComponentInfo }: DragOverlayProps) {
|
|||
</DndDragOverlay>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,4 +23,3 @@ export function TextEditingBanner({ show }: TextEditingBannerProps) {
|
|||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ export function MultipleAccounts() {
|
|||
() =>
|
||||
accountOptions.find((account) => account.id === activeId) ??
|
||||
accountOptions[0],
|
||||
[activeId],
|
||||
[activeId]
|
||||
);
|
||||
|
||||
const statusMessage = activeAccount
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>) => {
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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%)",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { Badge } from "./ui/badge";
|
|||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
|
||||
export function Header() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<motion.header
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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})`,
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
/>
|
||||
),
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export function DragToConfirmSlider({
|
|||
|
||||
const handleDragEnd = (
|
||||
_: MouseEvent | TouchEvent | PointerEvent,
|
||||
info: PanInfo,
|
||||
info: PanInfo
|
||||
) => {
|
||||
const currentX = x.get();
|
||||
if (currentX >= 180) {
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export function DynamicSpotlightCTA({
|
|||
duration: Math.random() * 3 + 2,
|
||||
delay: Math.random() * 2,
|
||||
})),
|
||||
[],
|
||||
[]
|
||||
);
|
||||
|
||||
const handleMouseMove = (event: MouseEvent<HTMLDivElement>) => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -319,7 +319,7 @@ export function BentoGridBlock() {
|
|||
>
|
||||
{tag}
|
||||
</span>
|
||||
),
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export function ContactFormSection() {
|
|||
};
|
||||
|
||||
const handleChange = (
|
||||
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||
) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ export function NewsletterSignupBlock() {
|
|||
{feature}
|
||||
</Badge>
|
||||
</motion.div>
|
||||
),
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ const badgeVariants = cva(
|
|||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export interface BadgeProps
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ const noIndexRobots: RobotsConfig = {
|
|||
|
||||
const defaultImageAbsolute = new URL(
|
||||
siteConfig.defaultImage,
|
||||
metadataBase,
|
||||
metadataBase
|
||||
).toString();
|
||||
|
||||
const defaultOpenGraph: NonNullable<Metadata["openGraph"]> = {
|
||||
|
|
|
|||
|
|
@ -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
16
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
1214
registry.json
1214
registry.json
File diff suppressed because it is too large
Load diff
|
|
@ -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 };
|
||||
|
||||
|
|
|
|||
|
|
@ -45,4 +45,3 @@ export type SavedProject = {
|
|||
deploymentId?: string;
|
||||
deploymentUrl?: string;
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue