7404 lines
212 KiB
Markdown
7404 lines
212 KiB
Markdown
# Project Backup
|
|
|
|
This file contains a full backup of all your project files. You can copy the contents of this file to your local machine to recreate the project.
|
|
|
|
---
|
|
|
|
## FILE: .env
|
|
|
|
```
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: .gitignore
|
|
|
|
```
|
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
|
|
# dependencies
|
|
/node_modules
|
|
/.pnp
|
|
.pnp.js
|
|
|
|
# testing
|
|
/coverage
|
|
|
|
# next.js
|
|
/.next/
|
|
/out/
|
|
|
|
# production
|
|
/build
|
|
|
|
# misc
|
|
.DS_Store
|
|
*.pem
|
|
|
|
# debug
|
|
npm-debug.log*
|
|
yarn-debug.log*
|
|
yarn-error.log*
|
|
|
|
# local env files
|
|
.env.local
|
|
.env.development.local
|
|
.env.test.local
|
|
.env.production.local
|
|
|
|
# vercel
|
|
.vercel
|
|
|
|
# typescript
|
|
*.tsbuildinfo
|
|
next-env.d.ts
|
|
|
|
# archives
|
|
*.zip
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: README.md
|
|
|
|
```md
|
|
# Firebase Studio
|
|
|
|
This is a NextJS starter in Firebase Studio.
|
|
|
|
To get started, take a look at src/app/page.tsx.
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: apphosting.yaml
|
|
|
|
```yaml
|
|
# Settings to manage and configure a Firebase App Hosting backend.
|
|
# https://firebase.google.com/docs/app-hosting/configure
|
|
|
|
runConfig:
|
|
# Increase this value if you'd like to automatically spin up
|
|
# more instances in response to increased traffic.
|
|
maxInstances: 1
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: components.json
|
|
|
|
```json
|
|
{
|
|
"$schema": "https://ui.shadcn.com/schema.json",
|
|
"style": "default",
|
|
"rsc": true,
|
|
"tsx": true,
|
|
"tailwind": {
|
|
"config": "tailwind.config.ts",
|
|
"css": "src/app/globals.css",
|
|
"baseColor": "neutral",
|
|
"cssVariables": true,
|
|
"prefix": ""
|
|
},
|
|
"aliases": {
|
|
"components": "@/components",
|
|
"utils": "@/lib/utils",
|
|
"ui": "@/components/ui",
|
|
"lib": "@/lib",
|
|
"hooks": "@/hooks"
|
|
},
|
|
"iconLibrary": "lucide"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: next-env.d.ts
|
|
|
|
```ts
|
|
/// <reference types="next" />
|
|
/// <reference types="next/image-types/global" />
|
|
/// <reference path="./.next/types/routes.d.ts" />
|
|
|
|
// NOTE: This file should not be edited
|
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: next.config.ts
|
|
|
|
```ts
|
|
import type {NextConfig} from 'next';
|
|
|
|
const nextConfig: NextConfig = {
|
|
/* config options here */
|
|
typescript: {
|
|
ignoreBuildErrors: true,
|
|
},
|
|
eslint: {
|
|
ignoreDuringBuilds: true,
|
|
},
|
|
images: {
|
|
remotePatterns: [
|
|
{
|
|
protocol: 'https',
|
|
hostname: 'placehold.co',
|
|
port: '',
|
|
pathname: '/**',
|
|
},
|
|
{
|
|
protocol: 'https',
|
|
hostname: 'images.unsplash.com',
|
|
port: '',
|
|
pathname: '/**',
|
|
},
|
|
{
|
|
protocol: 'https',
|
|
hostname: 'picsum.photos',
|
|
port: '',
|
|
pathname: '/**',
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
export default nextConfig;
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: package.json
|
|
|
|
```json
|
|
{
|
|
"name": "nextn",
|
|
"version": "0.1.0",
|
|
"private": true,
|
|
"scripts": {
|
|
"dev": "next dev --turbopack -p 9002",
|
|
"genkit:dev": "genkit start -- tsx src/ai/dev.ts",
|
|
"genkit:watch": "genkit start -- tsx --watch src/ai/dev.ts",
|
|
"build": "NODE_ENV=production next build",
|
|
"start": "next start",
|
|
"lint": "next lint",
|
|
"typecheck": "tsc --noEmit"
|
|
},
|
|
"dependencies": {
|
|
"@genkit-ai/google-genai": "^1.20.0",
|
|
"@genkit-ai/next": "^1.20.0",
|
|
"@hookform/resolvers": "^4.1.3",
|
|
"@radix-ui/react-accordion": "^1.2.3",
|
|
"@radix-ui/react-alert-dialog": "^1.1.6",
|
|
"@radix-ui/react-avatar": "^1.1.3",
|
|
"@radix-ui/react-checkbox": "^1.1.4",
|
|
"@radix-ui/react-collapsible": "^1.1.11",
|
|
"@radix-ui/react-dialog": "^1.1.6",
|
|
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
|
"@radix-ui/react-label": "^2.1.2",
|
|
"@radix-ui/react-menubar": "^1.1.6",
|
|
"@radix-ui/react-popover": "^1.1.6",
|
|
"@radix-ui/react-progress": "^1.1.2",
|
|
"@radix-ui/react-radio-group": "^1.2.3",
|
|
"@radix-ui/react-scroll-area": "^1.2.3",
|
|
"@radix-ui/react-select": "^2.1.6",
|
|
"@radix-ui/react-separator": "^1.1.2",
|
|
"@radix-ui/react-slider": "^1.2.3",
|
|
"@radix-ui/react-slot": "^1.2.3",
|
|
"@radix-ui/react-switch": "^1.1.3",
|
|
"@radix-ui/react-tabs": "^1.1.3",
|
|
"@radix-ui/react-toast": "^1.2.6",
|
|
"@radix-ui/react-tooltip": "^1.1.8",
|
|
"@tailwindcss/typography": "^0.5.13",
|
|
"class-variance-authority": "^0.7.1",
|
|
"clsx": "^2.1.1",
|
|
"date-fns": "^3.6.0",
|
|
"dotenv": "^16.5.0",
|
|
"embla-carousel-react": "^8.6.0",
|
|
"firebase": "^11.9.1",
|
|
"genkit": "^1.20.0",
|
|
"lucide-react": "^0.475.0",
|
|
"marked": "^12.0.2",
|
|
"next": "15.5.9",
|
|
"patch-package": "^8.0.0",
|
|
"react": "^19.2.1",
|
|
"react-day-picker": "^9.11.3",
|
|
"react-dom": "^19.2.1",
|
|
"react-hook-form": "^7.54.2",
|
|
"react-syntax-highlighter": "^15.5.0",
|
|
"recharts": "^2.15.1",
|
|
"tailwind-merge": "^3.0.1",
|
|
"tailwindcss-animate": "^1.0.7",
|
|
"zod": "^3.24.2"
|
|
},
|
|
"devDependencies": {
|
|
"@types/node": "^20",
|
|
"@types/react": "^19.2.1",
|
|
"@types/react-dom": "^19.2.1",
|
|
"@types/react-syntax-highlighter": "^15.5.13",
|
|
"genkit-cli": "^1.20.0",
|
|
"postcss": "^8",
|
|
"tailwindcss": "^3.4.1",
|
|
"typescript": "^5"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/ai/dev.ts
|
|
|
|
```ts
|
|
import { config } from 'dotenv';
|
|
config();
|
|
|
|
import '@/ai/flows/ai-suggested-sync-conflict-resolution.ts';
|
|
import '@/ai/flows/contextual-code-suggestions.ts';
|
|
import '@/ai/flows/ai-help-from-prompt.ts';
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/ai/flows/ai-help-from-prompt.ts
|
|
|
|
```ts
|
|
'use server';
|
|
|
|
/**
|
|
* @fileOverview This file defines a Genkit flow that helps new users by suggesting an initial set of code files
|
|
* and project structure based on a simple prompt describing the desired application.
|
|
*
|
|
* - aiHelpFromPrompt - A function that takes a prompt and returns suggested code files and project structure.
|
|
* - AIHelpFromPromptInput - The input type for the aiHelpFromPrompt function.
|
|
* - AIHelpFromPromptOutput - The return type for the aiHelpFromPrompt function.
|
|
*/
|
|
|
|
import {ai} from '@/ai/genkit';
|
|
import {z} from 'genkit';
|
|
|
|
const AIHelpFromPromptInputSchema = z.object({
|
|
prompt: z.string().describe('A prompt describing the type of application to build.'),
|
|
});
|
|
export type AIHelpFromPromptInput = z.infer<typeof AIHelpFromPromptInputSchema>;
|
|
|
|
const AIHelpFromPromptOutputSchema = z.object({
|
|
suggestedFiles: z.array(z.object({
|
|
filePath: z.string().describe('The path for the suggested file.'),
|
|
fileContent: z.string().describe('The content of the suggested file.'),
|
|
})).describe('An array of suggested code files and their content.'),
|
|
explanation: z.string().describe('An explanation of the suggested file structure and code.'),
|
|
});
|
|
export type AIHelpFromPromptOutput = z.infer<typeof AIHelpFromPromptOutputSchema>;
|
|
|
|
export async function aiHelpFromPrompt(input: AIHelpFromPromptInput): Promise<AIHelpFromPromptOutput> {
|
|
return aiHelpFromPromptFlow(input);
|
|
}
|
|
|
|
const prompt = ai.definePrompt({
|
|
name: 'aiHelpFromPromptPrompt',
|
|
input: {schema: AIHelpFromPromptInputSchema},
|
|
output: {schema: AIHelpFromPromptOutputSchema},
|
|
prompt: `You are an AI assistant designed to help new users quickly start developing applications.
|
|
|
|
Based on the user's prompt describing the desired application, suggest an initial set of code files and a project structure to get them started.
|
|
|
|
Provide the suggested files as an array of objects, each containing the file path and the file content.
|
|
Explain the suggested file structure and the code in detail so that the user understands the purpose of each file and how they fit together.
|
|
|
|
User Prompt: {{{prompt}}}
|
|
|
|
Example Output:
|
|
{
|
|
"suggestedFiles": [
|
|
{
|
|
"filePath": "src/components/MyComponent.tsx",
|
|
"fileContent": "// MyComponent.tsx\nimport React from 'react';\n\nconst MyComponent = () => {\n return (\n <div>\n <h1>Hello, world!</h1>\n </div>\n );\n};\n\nexport default MyComponent;"
|
|
},
|
|
{
|
|
"filePath": "src/pages/index.tsx",
|
|
"fileContent": "// index.tsx\nimport MyComponent from '../components/MyComponent';\n\nconst Home = () => {\n return (\n <div>\n <MyComponent />\n </div>\n );\n};\n\nexport default Home;"
|
|
}
|
|
],
|
|
"explanation": "This project structure includes a component (MyComponent.tsx) and a page (index.tsx) that uses the component. This is a basic structure for a React application."
|
|
}
|
|
`,
|
|
});
|
|
|
|
const aiHelpFromPromptFlow = ai.defineFlow(
|
|
{
|
|
name: 'aiHelpFromPromptFlow',
|
|
inputSchema: AIHelpFromPromptInputSchema,
|
|
outputSchema: AIHelpFromPromptOutputSchema,
|
|
},
|
|
async input => {
|
|
const {output} = await prompt(input, {
|
|
config: {
|
|
safetySettings: [
|
|
{
|
|
category: 'HARM_CATEGORY_HATE_SPEECH',
|
|
threshold: 'BLOCK_ONLY_HIGH',
|
|
},
|
|
{
|
|
category: 'HARM_CATEGORY_DANGEROUS_CONTENT',
|
|
threshold: 'BLOCK_NONE',
|
|
},
|
|
{
|
|
category: 'HARM_CATEGORY_HARASSMENT',
|
|
threshold: 'BLOCK_MEDIUM_AND_ABOVE',
|
|
},
|
|
{
|
|
category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
|
|
threshold: 'BLOCK_LOW_AND_ABOVE',
|
|
},
|
|
],
|
|
},
|
|
});
|
|
return output!;
|
|
}
|
|
);
|
|
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/ai/flows/ai-suggested-sync-conflict-resolution.ts
|
|
|
|
```ts
|
|
'use server';
|
|
|
|
/**
|
|
* @fileOverview An AI agent that detects synchronization conflicts between platform-specific code and data,
|
|
* and suggests solutions to resolve them.
|
|
*
|
|
* - aiSuggestedSyncConflictResolution - A function that handles the conflict resolution process.
|
|
* - AISuggestedSyncConflictResolutionInput - The input type for the aiSuggestedSyncConflictResolution function.
|
|
* - AISuggestedSyncConflictResolutionOutput - The return type for the aiSuggestedSyncConflictResolution function.
|
|
*/
|
|
|
|
import {ai} from '@/ai/genkit';
|
|
import {z} from 'genkit';
|
|
|
|
const AISuggestedSyncConflictResolutionInputSchema = z.object({
|
|
robloxCode: z.string().describe('The Lua code for the Roblox platform.'),
|
|
webCode: z.string().describe('The JavaScript code for the web platform.'),
|
|
mobileCode: z.string().describe('The React Native code for the mobile platform.'),
|
|
sharedState: z.string().describe('The shared state data in JSON format.'),
|
|
});
|
|
export type AISuggestedSyncConflictResolutionInput = z.infer<typeof AISuggestedSyncConflictResolutionInputSchema>;
|
|
|
|
const AISuggestedSyncConflictResolutionOutputSchema = z.object({
|
|
conflictDetected: z.boolean().describe('Whether a synchronization conflict was detected.'),
|
|
suggestedSolutions: z.array(z.string()).describe('An array of suggested solutions to resolve the conflicts.'),
|
|
explanation: z.string().describe('Explanation of the detected conflicts and suggested solutions.'),
|
|
});
|
|
export type AISuggestedSyncConflictResolutionOutput = z.infer<typeof AISuggestedSyncConflictResolutionOutputSchema>;
|
|
|
|
export async function aiSuggestedSyncConflictResolution(input: AISuggestedSyncConflictResolutionInput): Promise<AISuggestedSyncConflictResolutionOutput> {
|
|
return aiSuggestedSyncConflictResolutionFlow(input);
|
|
}
|
|
|
|
const prompt = ai.definePrompt({
|
|
name: 'aiSuggestedSyncConflictResolutionPrompt',
|
|
input: {schema: AISuggestedSyncConflictResolutionInputSchema},
|
|
output: {schema: AISuggestedSyncConflictResolutionOutputSchema},
|
|
prompt: `You are an AI assistant specialized in detecting synchronization conflicts between different platform codebases and suggesting solutions.
|
|
|
|
You are given the code for Roblox (Lua), Web (JavaScript), and Mobile (React Native), as well as the shared state data in JSON format. Analyze the code and the shared state to identify any inconsistencies or conflicts.
|
|
|
|
Based on your analysis, determine if there are any conflicts, and suggest solutions to resolve them. Explain the conflicts and the suggested solutions in detail.
|
|
|
|
Roblox Code:
|
|
{{robloxCode}}
|
|
|
|
Web Code:
|
|
{{webCode}}
|
|
|
|
Mobile Code:
|
|
{{mobileCode}}
|
|
|
|
Shared State:
|
|
{{sharedState}}`,
|
|
});
|
|
|
|
const aiSuggestedSyncConflictResolutionFlow = ai.defineFlow(
|
|
{
|
|
name: 'aiSuggestedSyncConflictResolutionFlow',
|
|
inputSchema: AISuggestedSyncConflictResolutionInputSchema,
|
|
outputSchema: AISuggestedSyncConflictResolutionOutputSchema,
|
|
},
|
|
async input => {
|
|
const {output} = await prompt(input, {
|
|
config: {
|
|
safetySettings: [
|
|
{
|
|
category: 'HARM_CATEGORY_HATE_SPEECH',
|
|
threshold: 'BLOCK_ONLY_HIGH',
|
|
},
|
|
{
|
|
category: 'HARM_CATEGORY_DANGEROUS_CONTENT',
|
|
threshold: 'BLOCK_NONE',
|
|
},
|
|
{
|
|
category: 'HARM_CATEGORY_HARASSMENT',
|
|
threshold: 'BLOCK_MEDIUM_AND_ABOVE',
|
|
},
|
|
{
|
|
category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
|
|
threshold: 'BLOCK_LOW_AND_ABOVE',
|
|
},
|
|
],
|
|
},
|
|
});
|
|
return output!;
|
|
}
|
|
);
|
|
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/ai/flows/contextual-code-suggestions.ts
|
|
|
|
```ts
|
|
'use server';
|
|
/**
|
|
* @fileOverview This file defines a Genkit flow for providing contextual code suggestions.
|
|
*
|
|
* - contextualCodeSuggestions - A function that takes the current file content and cursor position
|
|
* and returns code suggestions.
|
|
* - ContextualCodeSuggestionsInput - The input type for the contextualCodeSuggestions function.
|
|
* - ContextualCodeSuggestionsOutput - The return type for the contextualCodeSuggestions function.
|
|
*/
|
|
|
|
import {ai} from '@/ai/genkit';
|
|
import {z} from 'genkit';
|
|
|
|
const ContextualCodeSuggestionsInputSchema = z.object({
|
|
fileContent: z.string().describe('The content of the currently open file.'),
|
|
cursorPosition: z.number().describe('The cursor position within the file.'),
|
|
language: z.string().describe('The programming language of the file.'),
|
|
context: z.string().optional().describe('Additional context for code suggestions, e.g., error messages or related code snippets.'),
|
|
});
|
|
export type ContextualCodeSuggestionsInput = z.infer<
|
|
typeof ContextualCodeSuggestionsInputSchema
|
|
>;
|
|
|
|
const ContextualCodeSuggestionsOutputSchema = z.object({
|
|
suggestions: z
|
|
.array(z.string())
|
|
.describe('An array of code suggestions based on the context.'),
|
|
});
|
|
export type ContextualCodeSuggestionsOutput = z.infer<
|
|
typeof ContextualCodeSuggestionsOutputSchema
|
|
>;
|
|
|
|
export async function contextualCodeSuggestions(
|
|
input: ContextualCodeSuggestionsInput
|
|
): Promise<ContextualCodeSuggestionsOutput> {
|
|
return contextualCodeSuggestionsFlow(input);
|
|
}
|
|
|
|
const prompt = ai.definePrompt({
|
|
name: 'contextualCodeSuggestionsPrompt',
|
|
input: {schema: ContextualCodeSuggestionsInputSchema},
|
|
output: {schema: ContextualCodeSuggestionsOutputSchema},
|
|
prompt: `You are an AI assistant that provides code suggestions and autocompletions based on the context of the currently open file and cursor position.
|
|
|
|
Given the following file content, cursor position, programming language, and any available context, provide a list of code suggestions that would be helpful to the developer.
|
|
|
|
File Content:
|
|
{{fileContent}}
|
|
|
|
Cursor Position: {{cursorPosition}}
|
|
|
|
Programming Language: {{language}}
|
|
|
|
Context: {{context}}
|
|
|
|
Suggestions should be relevant to the current context, incorporate best practices, and avoid common mistakes. Return the suggestions as an array of strings.
|
|
|
|
Example:
|
|
[
|
|
"console.log('Hello, world!');",
|
|
"// Add a comment to explain the code",
|
|
"function myFunction() {\n // Function body\n }",
|
|
]`,
|
|
});
|
|
|
|
const contextualCodeSuggestionsFlow = ai.defineFlow(
|
|
{
|
|
name: 'contextualCodeSuggestionsFlow',
|
|
inputSchema: ContextualCodeSuggestionsInputSchema,
|
|
outputSchema: ContextualCodeSuggestionsOutputSchema,
|
|
},
|
|
async input => {
|
|
const {output} = await prompt(input);
|
|
return output!;
|
|
}
|
|
);
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/ai/genkit.ts
|
|
|
|
```ts
|
|
import {genkit} from 'genkit';
|
|
import {googleAI} from '@genkit-ai/google-genai';
|
|
|
|
export const ai = genkit({
|
|
plugins: [googleAI()],
|
|
model: 'googleai/gemini-2.5-flash',
|
|
});
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/app/dashboard/page.tsx
|
|
|
|
```tsx
|
|
import { DashboardPage } from "@/components/aethex/dashboard-page";
|
|
|
|
export default function Page() {
|
|
return <DashboardPage />;
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/app/globals.css
|
|
|
|
```css
|
|
@tailwind base;
|
|
@tailwind components;
|
|
@tailwind utilities;
|
|
|
|
@layer base {
|
|
:root {
|
|
--background: 0 0% 100%;
|
|
--foreground: 0 0% 3.9%;
|
|
--card: 0 0% 100%;
|
|
--card-foreground: 0 0% 3.9%;
|
|
--popover: 0 0% 100%;
|
|
--popover-foreground: 0 0% 3.9%;
|
|
--primary: 278 52% 49%;
|
|
--primary-foreground: 0 0% 98%;
|
|
--secondary: 277 100% 25%;
|
|
--secondary-foreground: 0 0% 98%;
|
|
--muted: 0 0% 96.1%;
|
|
--muted-foreground: 0 0% 45.1%;
|
|
--accent: 180 100% 25%;
|
|
--accent-foreground: 0 0% 9%;
|
|
--destructive: 0 84.2% 60.2%;
|
|
--destructive-foreground: 0 0% 98%;
|
|
--border: 0 0% 89.8%;
|
|
--input: 0 0% 89.8%;
|
|
--ring: 278 52% 49%;
|
|
--chart-1: 12 76% 61%;
|
|
--chart-2: 173 58% 39%;
|
|
--chart-3: 197 37% 24%;
|
|
--chart-4: 43 74% 66%;
|
|
--chart-5: 27 87% 67%;
|
|
--radius: 0.5rem;
|
|
}
|
|
.dark {
|
|
--background: 0 0% 13.3%;
|
|
--foreground: 0 0% 98%;
|
|
--card: 0 0% 20%;
|
|
--card-foreground: 0 0% 98%;
|
|
--popover: 0 0% 13.3%;
|
|
--popover-foreground: 0 0% 98%;
|
|
--primary: 278 52% 49%;
|
|
--primary-foreground: 0 0% 98%;
|
|
--secondary: 277 100% 25%;
|
|
--secondary-foreground: 0 0% 98%;
|
|
--muted: 0 0% 20%;
|
|
--muted-foreground: 0 0% 63.9%;
|
|
--accent: 180 100% 25%;
|
|
--accent-foreground: 0 0% 98%;
|
|
--destructive: 0 62.8% 30.6%;
|
|
--destructive-foreground: 0 0% 98%;
|
|
--border: 0 0% 25%;
|
|
--input: 0 0% 25%;
|
|
--ring: 278 52% 49%;
|
|
--chart-1: 220 70% 50%;
|
|
--chart-2: 160 60% 45%;
|
|
--chart-3: 30 80% 55%;
|
|
--chart-4: 280 65% 60%;
|
|
--chart-5: 340 75% 55%;
|
|
}
|
|
}
|
|
|
|
@layer base {
|
|
* {
|
|
@apply border-border;
|
|
}
|
|
body {
|
|
@apply bg-background text-foreground;
|
|
}
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/app/ide/page.tsx
|
|
|
|
```tsx
|
|
import { AethexStudio } from "@/components/aethex/aethex-studio";
|
|
|
|
export default function IdePage() {
|
|
return (
|
|
<main className="h-[100svh] w-screen overflow-hidden bg-background">
|
|
<AethexStudio />
|
|
</main>
|
|
);
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/app/layout.tsx
|
|
|
|
```tsx
|
|
import type { Metadata } from "next";
|
|
import { Toaster } from "@/components/ui/toaster";
|
|
import "./globals.css";
|
|
|
|
export const metadata: Metadata = {
|
|
title: "AeThex Studio",
|
|
description: "The Next-Generation Cross-Platform IDE",
|
|
};
|
|
|
|
export default function RootLayout({
|
|
children,
|
|
}: Readonly<{
|
|
children: React.ReactNode;
|
|
}>) {
|
|
return (
|
|
<html lang="en" className="dark">
|
|
<head>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
<link
|
|
rel="preconnect"
|
|
href="https://fonts.gstatic.com"
|
|
crossOrigin="anonymous"
|
|
/>
|
|
<link
|
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&family=Source+Code+Pro:wght@400&family=Space+Grotesk:wght@400;700&display=swap"
|
|
rel="stylesheet"
|
|
/>
|
|
</head>
|
|
<body className="font-body antialiased">
|
|
{children}
|
|
<Toaster />
|
|
</body>
|
|
</html>
|
|
);
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/app/page.tsx
|
|
|
|
```tsx
|
|
import { LoginPage } from "@/components/aethex/login-page";
|
|
|
|
export default function Page() {
|
|
return <LoginPage />;
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/aethex/aethex-studio.tsx
|
|
|
|
```tsx
|
|
"use client";
|
|
|
|
import { useState } from "react";
|
|
import {
|
|
ResizableHandle,
|
|
ResizablePanel,
|
|
ResizablePanelGroup,
|
|
} from "@/components/ui/resizable";
|
|
import { Navbar } from "./navbar";
|
|
import { FileNavigator } from "./file-navigator";
|
|
import { MainView } from "./main-view";
|
|
import { BottomPanel } from "./bottom-panel";
|
|
import { AiAssistant } from "./ai-assistant";
|
|
import {
|
|
openFiles as initialOpenFiles,
|
|
fileTree as initialFileTree,
|
|
File as OpenFileType,
|
|
FolderNode,
|
|
FileNode,
|
|
} from "@/lib/aethex-data";
|
|
import { NewProjectModal } from "./new-project-modal";
|
|
import {
|
|
ProjectTemplate,
|
|
generateFileContent,
|
|
} from "@/lib/templates";
|
|
import type { NewProjectFormValues } from "./new-project-modal";
|
|
|
|
export type { OpenFileType };
|
|
|
|
export function AethexStudio() {
|
|
const [openFiles, setOpenFiles] = useState<OpenFileType[]>(initialOpenFiles);
|
|
const [activeTab, setActiveTab] = useState<string>(openFiles[0]?.id || "");
|
|
const [fileTree, setFileTree] = useState<FolderNode>(initialFileTree);
|
|
const [isNewProjectModalOpen, setIsNewProjectModalOpen] = useState(false);
|
|
|
|
const handleOpenFile = (file: OpenFileType) => {
|
|
if (!openFiles.find((f) => f.id === file.id)) {
|
|
setOpenFiles((prev) => [...prev, file]);
|
|
}
|
|
setActiveTab(file.id);
|
|
};
|
|
|
|
const handleCloseFile = (fileId: string) => {
|
|
const newOpenFiles = openFiles.filter((file) => file.id !== fileId);
|
|
setOpenFiles(newOpenFiles);
|
|
|
|
if (activeTab === fileId) {
|
|
if (newOpenFiles.length > 0) {
|
|
setActiveTab(newOpenFiles[newOpenFiles.length - 1].id);
|
|
} else {
|
|
setActiveTab("");
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleCreateProject = (
|
|
template: ProjectTemplate,
|
|
config: NewProjectFormValues
|
|
) => {
|
|
const newFileTree: FolderNode = {
|
|
...template.fileTree,
|
|
name: config.projectName,
|
|
};
|
|
setFileTree(newFileTree);
|
|
|
|
let mainFileToOpen: OpenFileType | undefined;
|
|
|
|
const findMainFile = (node: FolderNode | FileNode, currentPath: string) => {
|
|
if (mainFileToOpen) return;
|
|
const newPath = currentPath ? `${currentPath}/${node.name}` : node.name;
|
|
if (node.type === "file" && node.name === template.mainFile) {
|
|
mainFileToOpen = {
|
|
id: newPath,
|
|
name: node.name,
|
|
language: node.language,
|
|
content: generateFileContent(node.name, node.language),
|
|
};
|
|
} else if (node.type === "folder" && node.children) {
|
|
for (const child of node.children) {
|
|
findMainFile(child, newPath);
|
|
}
|
|
}
|
|
};
|
|
findMainFile(newFileTree, "");
|
|
|
|
if (mainFileToOpen) {
|
|
setOpenFiles([mainFileToOpen]);
|
|
setActiveTab(mainFileToOpen.id);
|
|
} else {
|
|
setOpenFiles([]);
|
|
setActiveTab("");
|
|
}
|
|
|
|
setIsNewProjectModalOpen(false);
|
|
};
|
|
|
|
const handleAiGeneratedFiles = (files: { filePath: string, fileContent: string }[]) => {
|
|
const addNodeToTree = (
|
|
root: FolderNode,
|
|
path: string
|
|
): FolderNode => {
|
|
const parts = path.split('/');
|
|
// The AI generates paths relative to the project root, e.g., "src/components/new.tsx"
|
|
// The file tree's root is the project folder itself, so we start traversing from its children.
|
|
let currentNode: FolderNode | undefined = root;
|
|
|
|
// Handle cases where AI gives a full path vs relative
|
|
const pathParts = parts[0] === root.name ? parts.slice(1) : parts;
|
|
|
|
for (let i = 0; i < pathParts.length - 1; i++) {
|
|
const part = pathParts[i];
|
|
let nextNode = currentNode.children.find(
|
|
(child) => child.name === part && child.type === 'folder'
|
|
) as FolderNode | undefined;
|
|
|
|
if (!nextNode) {
|
|
nextNode = { name: part, type: 'folder', children: [] };
|
|
currentNode.children.push(nextNode);
|
|
}
|
|
currentNode = nextNode;
|
|
}
|
|
|
|
// Add the file node if it doesn't exist
|
|
const fileName = pathParts[pathParts.length - 1];
|
|
if (currentNode && !currentNode.children.some((child) => child.name === fileName)) {
|
|
currentNode.children.push({
|
|
name: fileName,
|
|
type: 'file',
|
|
language: fileName.split('.').pop() || 'text',
|
|
});
|
|
}
|
|
|
|
return { ...root };
|
|
};
|
|
|
|
let newFileTree = fileTree;
|
|
files.forEach(file => {
|
|
newFileTree = addNodeToTree(newFileTree, file.filePath);
|
|
});
|
|
setFileTree(newFileTree);
|
|
|
|
const newFilesToOpen: OpenFileType[] = files.map(file => ({
|
|
id: file.filePath.startsWith(fileTree.name) ? file.filePath : `${fileTree.name}/${file.filePath}`,
|
|
name: file.filePath.split('/').pop() || 'untitled',
|
|
language: file.filePath.split('.').pop() || 'text',
|
|
content: file.fileContent,
|
|
}));
|
|
|
|
setOpenFiles(prevOpenFiles => {
|
|
const updatedOpenFiles = [...prevOpenFiles];
|
|
newFilesToOpen.forEach(newFile => {
|
|
const existingFileIndex = updatedOpenFiles.findIndex(f => f.id === newFile.id);
|
|
if (existingFileIndex !== -1) {
|
|
updatedOpenFiles[existingFileIndex].content = newFile.content;
|
|
} else {
|
|
updatedOpenFiles.push(newFile);
|
|
}
|
|
});
|
|
return updatedOpenFiles;
|
|
});
|
|
|
|
if (newFilesToOpen.length > 0) {
|
|
setActiveTab(newFilesToOpen[newFilesToOpen.length - 1].id);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div className="flex h-full flex-col text-sm">
|
|
<Navbar onNewProjectClick={() => setIsNewProjectModalOpen(true)} />
|
|
<div className="flex-grow overflow-hidden">
|
|
<ResizablePanelGroup direction="horizontal" className="!h-full">
|
|
<ResizablePanel defaultSize={15} minSize={10} maxSize={25}>
|
|
<FileNavigator fileTree={fileTree} onOpenFile={handleOpenFile} />
|
|
</ResizablePanel>
|
|
<ResizableHandle withHandle />
|
|
<ResizablePanel defaultSize={60}>
|
|
<ResizablePanelGroup direction="vertical">
|
|
<ResizablePanel defaultSize={70}>
|
|
<MainView
|
|
openFiles={openFiles}
|
|
activeTab={activeTab}
|
|
setActiveTab={setActiveTab}
|
|
onCloseFile={handleCloseFile}
|
|
/>
|
|
</ResizablePanel>
|
|
<ResizableHandle withHandle />
|
|
<ResizablePanel defaultSize={30} minSize={10} maxSize={40}>
|
|
<BottomPanel />
|
|
</ResizablePanel>
|
|
</ResizablePanelGroup>
|
|
</ResizablePanel>
|
|
<ResizableHandle withHandle />
|
|
<ResizablePanel defaultSize={25} minSize={15} maxSize={35}>
|
|
<AiAssistant onFilesGenerated={handleAiGeneratedFiles} />
|
|
</ResizablePanel>
|
|
</ResizablePanelGroup>
|
|
</div>
|
|
</div>
|
|
<NewProjectModal
|
|
isOpen={isNewProjectModalOpen}
|
|
onClose={() => setIsNewProjectModalOpen(false)}
|
|
onCreateProject={handleCreateProject}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/aethex/ai-assistant.tsx
|
|
|
|
```tsx
|
|
"use client";
|
|
|
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import {
|
|
Send,
|
|
Bot,
|
|
Loader2,
|
|
Copy,
|
|
Code,
|
|
Sparkles,
|
|
MessageSquarePlus,
|
|
FlaskConical,
|
|
BookText,
|
|
} from "lucide-react";
|
|
import { AethexLogo } from "./icons";
|
|
import { useState, useRef, useEffect, memo } from "react";
|
|
import { aiHelpFromPrompt } from "@/ai/flows/ai-help-from-prompt";
|
|
import { cn } from "@/lib/utils";
|
|
import { marked } from "marked";
|
|
import SyntaxHighlighter from "react-syntax-highlighter";
|
|
import { atomOneDark } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipProvider,
|
|
TooltipTrigger,
|
|
} from "@/components/ui/tooltip";
|
|
|
|
type Message = {
|
|
id: string;
|
|
role: "user" | "assistant" | "system";
|
|
content: string;
|
|
};
|
|
|
|
const CodeBlock = memo(
|
|
({ language, code }: { language: string; code: string }) => {
|
|
const { toast } = useToast();
|
|
|
|
const handleCopy = () => {
|
|
navigator.clipboard.writeText(code);
|
|
toast({ title: "Code copied to clipboard!" });
|
|
};
|
|
|
|
const handleInsert = () => {
|
|
toast({
|
|
title: "Coming Soon!",
|
|
description: "Inserting code into the editor is not yet implemented.",
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div className="group relative my-2 rounded-md bg-background">
|
|
<div className="flex items-center justify-between rounded-t-md bg-muted px-3 py-1.5 text-xs text-muted-foreground">
|
|
<span>{language}</span>
|
|
<div className="flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100">
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-6 w-6"
|
|
onClick={handleInsert}
|
|
>
|
|
<Code className="h-3 w-3" />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p>Insert into editor</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-6 w-6"
|
|
onClick={handleCopy}
|
|
>
|
|
<Copy className="h-3 w-3" />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p>Copy code</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
</div>
|
|
</div>
|
|
<SyntaxHighlighter
|
|
language={language}
|
|
style={atomOneDark}
|
|
customStyle={{
|
|
padding: "1rem",
|
|
margin: 0,
|
|
borderBottomLeftRadius: "0.375rem",
|
|
borderBottomRightRadius: "0.375rem",
|
|
}}
|
|
PreTag="div"
|
|
>
|
|
{code}
|
|
</SyntaxHighlighter>
|
|
</div>
|
|
);
|
|
}
|
|
);
|
|
CodeBlock.displayName = "CodeBlock";
|
|
|
|
const ChatMessage = memo(({ message }: { message: Message }) => {
|
|
if (message.role === "system") {
|
|
return (
|
|
<div className="text-center text-xs text-muted-foreground">
|
|
{message.content}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const isUser = message.role === "user";
|
|
const parts = message.content
|
|
.split(/(\`\`\`(?:[a-zA-Z0-9-]*)\n[\s\S]+?\n\`\`\`)/g)
|
|
.filter(Boolean);
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"flex w-full items-start gap-3",
|
|
!isUser && "justify-end"
|
|
)}
|
|
>
|
|
{isUser && (
|
|
<Avatar className="h-8 w-8">
|
|
<AvatarFallback>U</AvatarFallback>
|
|
</Avatar>
|
|
)}
|
|
<div
|
|
className={cn(
|
|
"max-w-[85%] space-y-2 rounded-lg p-3 text-sm",
|
|
isUser
|
|
? "bg-primary text-primary-foreground"
|
|
: "border border-primary bg-card"
|
|
)}
|
|
>
|
|
{parts.map((part, index) => {
|
|
const codeBlockMatch = part.match(
|
|
/\`\`\`(.*?)\n([\s\S]+?)\n\`\`\`/
|
|
);
|
|
if (codeBlockMatch) {
|
|
const language = codeBlockMatch[1] || "text";
|
|
const code = codeBlockMatch[2];
|
|
return <CodeBlock key={index} language={language} code={code} />;
|
|
} else {
|
|
return (
|
|
<div
|
|
key={index}
|
|
className="prose prose-sm prose-invert max-w-none prose-p:my-0"
|
|
dangerouslySetInnerHTML={{
|
|
__html: marked(part, { gfm: true, breaks: true }),
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
})}
|
|
</div>
|
|
{!isUser && (
|
|
<Avatar className="h-8 w-8 border">
|
|
<AvatarFallback className="bg-transparent">
|
|
<AethexLogo className="h-5 w-5" />
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
)}
|
|
</div>
|
|
);
|
|
});
|
|
ChatMessage.displayName = "ChatMessage";
|
|
|
|
type AiAssistantProps = {
|
|
onFilesGenerated: (files: { filePath: string, fileContent: string }[]) => void;
|
|
};
|
|
|
|
export function AiAssistant({ onFilesGenerated }: AiAssistantProps) {
|
|
const [messages, setMessages] = useState<Message[]>([
|
|
{
|
|
id: "1",
|
|
role: "assistant",
|
|
content:
|
|
"Hello! I'm your AI assistant. How can I help you with your project today? You can ask me to explain code, generate tests, or even create a new project structure.",
|
|
},
|
|
]);
|
|
const [input, setInput] = useState("");
|
|
const [loading, setLoading] = useState(false);
|
|
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
|
const { toast } = useToast();
|
|
|
|
useEffect(() => {
|
|
if (scrollAreaRef.current) {
|
|
scrollAreaRef.current.scrollTo({
|
|
top: scrollAreaRef.current.scrollHeight,
|
|
behavior: "smooth",
|
|
});
|
|
}
|
|
}, [messages]);
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!input.trim() || loading) return;
|
|
|
|
const userMessage: Message = {
|
|
id: Date.now().toString(),
|
|
role: "user",
|
|
content: input,
|
|
};
|
|
setMessages((prev) => [...prev, userMessage]);
|
|
const currentInput = input;
|
|
setInput("");
|
|
setLoading(true);
|
|
|
|
try {
|
|
const result = await aiHelpFromPrompt({ prompt: currentInput });
|
|
|
|
let assistantContent = result.explanation;
|
|
|
|
if (result.suggestedFiles && result.suggestedFiles.length > 0) {
|
|
onFilesGenerated(result.suggestedFiles);
|
|
assistantContent += `\n\nHere are the files I've generated for you:\n`;
|
|
result.suggestedFiles.forEach((file) => {
|
|
const lang = file.filePath.split(".").pop() || "";
|
|
assistantContent += `\n**${file.filePath}**\n\`\`\`${lang}\n${file.fileContent}\n\`\`\``;
|
|
});
|
|
const systemMessage: Message = {
|
|
id: Date.now().toString() + "-system",
|
|
role: "system",
|
|
content: "File suggestions have been opened in the editor for you to review.",
|
|
};
|
|
setMessages((prev) => [...prev, systemMessage]);
|
|
}
|
|
|
|
const assistantMessage: Message = {
|
|
id: Date.now().toString(),
|
|
role: "assistant",
|
|
content: assistantContent,
|
|
};
|
|
setMessages((prev) => [...prev, assistantMessage]);
|
|
} catch (error) {
|
|
console.error("AI assistant error:", error);
|
|
const errorMessage: Message = {
|
|
id: Date.now().toString(),
|
|
role: "assistant",
|
|
content:
|
|
"Sorry, I encountered an issue while processing your request. Please try again.",
|
|
};
|
|
setMessages((prev) => [...prev, errorMessage]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleQuickAction = (action: string) => {
|
|
toast({
|
|
title: "Coming Soon!",
|
|
description: `The "${action}" feature is not yet implemented.`,
|
|
});
|
|
};
|
|
|
|
const quickActions = [
|
|
{ label: "Explain selected code", icon: BookText, action: "Explain selected code" },
|
|
{ label: "Add comments", icon: MessageSquarePlus, action: "Add comments" },
|
|
{ label: "Convert to cross-platform", icon: Sparkles, action: "Convert to cross-platform" },
|
|
{ label: "Generate tests", icon: FlaskConical, action: "Generate tests" },
|
|
]
|
|
|
|
return (
|
|
<div className="flex h-full flex-col bg-card">
|
|
<div className="flex items-center justify-between border-b p-3">
|
|
<div className="flex items-center gap-2">
|
|
<Bot className="h-6 w-6 text-primary" />
|
|
<h2 className="font-headline text-lg font-semibold">AI Assistant</h2>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<div className="text-right text-xs">
|
|
<p className="text-muted-foreground">Token Usage</p>
|
|
<p className="font-medium">12.5K / 500K</p>
|
|
</div>
|
|
<Select defaultValue="claude-sonnet">
|
|
<SelectTrigger className="w-[150px]">
|
|
<SelectValue placeholder="Select a model" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="claude-sonnet">Claude 3.5 Sonnet</SelectItem>
|
|
<SelectItem value="gpt-4o" disabled>GPT-4o</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
<ScrollArea className="flex-1" viewportRef={scrollAreaRef}>
|
|
<div className="space-y-6 p-4">
|
|
{messages.map((message) => (
|
|
<ChatMessage key={message.id} message={message} />
|
|
))}
|
|
{loading && (
|
|
<div className="flex w-full items-start gap-3">
|
|
<Avatar className="h-8 w-8">
|
|
<AvatarFallback>U</AvatarFallback>
|
|
</Avatar>
|
|
<div className="flex items-center space-x-2 rounded-lg bg-primary p-3 text-primary-foreground">
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
<span>Waiting for response...</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</ScrollArea>
|
|
|
|
<div className="border-t p-3">
|
|
<div className="mb-2 flex flex-wrap gap-2">
|
|
{quickActions.map(({label, icon: Icon, action}) => (
|
|
<Button key={label} size="sm" variant="outline" onClick={() => handleQuickAction(action)}>
|
|
<Icon className="mr-2 h-3.5 w-3.5" />
|
|
{label}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
<form onSubmit={handleSubmit} className="relative">
|
|
<Textarea
|
|
value={input}
|
|
onChange={(e) => setInput(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
e.preventDefault();
|
|
handleSubmit(e);
|
|
}
|
|
}}
|
|
placeholder="Ask the AI assistant, or describe what you want to build..."
|
|
className="min-h-[60px] pr-12"
|
|
disabled={loading}
|
|
/>
|
|
<div className="absolute bottom-2 right-1 flex flex-col items-center gap-1">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
type="submit"
|
|
disabled={loading || !input.trim()}
|
|
>
|
|
<Send className="h-4 w-4" />
|
|
<span className="sr-only">Send message</span>
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/aethex/bottom-panel.tsx
|
|
|
|
```tsx
|
|
"use client";
|
|
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { consoleLogs } from "@/lib/aethex-data";
|
|
import { ChevronRight, HardDrive } from "lucide-react";
|
|
|
|
export function BottomPanel() {
|
|
return (
|
|
<div className="flex h-full flex-col">
|
|
<Tabs defaultValue="console" className="flex h-full flex-col">
|
|
<TabsList className="mx-2 mt-2 self-start rounded-md">
|
|
<TabsTrigger value="console">Console</TabsTrigger>
|
|
<TabsTrigger value="terminal">Terminal</TabsTrigger>
|
|
</TabsList>
|
|
<TabsContent value="console" className="flex-1 overflow-auto p-4 text-xs">
|
|
<div className="font-code">
|
|
{consoleLogs.map((log, index) => (
|
|
<div
|
|
key={index}
|
|
className={`flex items-start gap-2 border-b border-border/50 py-1 ${
|
|
log.type === "error"
|
|
? "text-destructive"
|
|
: log.type === "warn"
|
|
? "text-yellow-400"
|
|
: "text-muted-foreground"
|
|
}`}
|
|
>
|
|
<span className="w-20 shrink-0 text-foreground/50">
|
|
{log.timestamp}
|
|
</span>
|
|
<span
|
|
className={`w-12 shrink-0 font-bold ${
|
|
log.platform === "Roblox"
|
|
? "text-red-500"
|
|
: log.platform === "Web"
|
|
? "text-blue-500"
|
|
: "text-green-500"
|
|
}`}
|
|
>
|
|
[{log.platform}]
|
|
</span>
|
|
<p className="flex-1 whitespace-pre-wrap">{log.message}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</TabsContent>
|
|
<TabsContent value="terminal" className="h-full">
|
|
<div className="flex h-full flex-col bg-background p-4 font-code text-xs">
|
|
<p>AeThex Terminal</p>
|
|
<p>Copyright (c) 2024. All rights reserved.</p>
|
|
<div className="mt-4 flex items-center gap-2">
|
|
<HardDrive className="h-3 w-3 text-accent" />
|
|
<span className="text-accent">~/aethex-project</span>
|
|
<ChevronRight className="h-3 w-3" />
|
|
<span className="flex-1"></span>
|
|
</div>
|
|
</div>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/aethex/code-editor.tsx
|
|
|
|
```tsx
|
|
"use client";
|
|
|
|
import type { Dispatch, SetStateAction } from "react";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { Button } from "@/components/ui/button";
|
|
import { X } from "lucide-react";
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
import type { OpenFileType } from "./aethex-studio";
|
|
|
|
type CodeEditorProps = {
|
|
openFiles: OpenFileType[];
|
|
activeTab: string;
|
|
setActiveTab: Dispatch<SetStateAction<string>>;
|
|
onCloseFile: (fileId: string) => void;
|
|
};
|
|
|
|
export function CodeEditor({
|
|
openFiles,
|
|
activeTab,
|
|
setActiveTab,
|
|
onCloseFile,
|
|
}: CodeEditorProps) {
|
|
|
|
const handleCloseTab = (
|
|
e: React.MouseEvent<HTMLButtonElement>,
|
|
fileId: string
|
|
) => {
|
|
e.stopPropagation();
|
|
onCloseFile(fileId);
|
|
};
|
|
|
|
if (openFiles.length === 0) {
|
|
return (
|
|
<div className="flex h-full items-center justify-center bg-card text-muted-foreground">
|
|
<p>No files open. Select a file from the navigator.</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Tabs
|
|
value={activeTab}
|
|
onValueChange={setActiveTab}
|
|
className="flex h-full flex-col"
|
|
>
|
|
<TabsList className="m-0 flex h-auto justify-start rounded-none border-b bg-transparent p-0">
|
|
{openFiles.map((file) => (
|
|
<TabsTrigger
|
|
key={file.id}
|
|
value={file.id}
|
|
className="group relative h-10 rounded-none border-r border-t-2 border-t-transparent bg-card px-4 py-2 text-muted-foreground shadow-none data-[state=active]:border-t-primary data-[state=active]:bg-background data-[state=active]:text-foreground"
|
|
>
|
|
{file.name}
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="absolute right-1 top-1/2 h-5 w-5 -translate-y-1/2 opacity-0 group-hover:opacity-100"
|
|
onClick={(e) => handleCloseTab(e, file.id)}
|
|
>
|
|
<X className="h-3 w-3" />
|
|
</Button>
|
|
</TabsTrigger>
|
|
))}
|
|
</TabsList>
|
|
{openFiles.map((file) => (
|
|
<TabsContent
|
|
key={file.id}
|
|
value={file.id}
|
|
className="m-0 flex-1 overflow-hidden"
|
|
>
|
|
<ScrollArea className="h-full">
|
|
<pre className="p-4 font-code text-sm">
|
|
<code
|
|
dangerouslySetInnerHTML={{ __html: file.content }}
|
|
></code>
|
|
</pre>
|
|
</ScrollArea>
|
|
</TabsContent>
|
|
))}
|
|
</Tabs>
|
|
);
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/aethex/cross-platform-view.tsx
|
|
|
|
```tsx
|
|
"use client";
|
|
|
|
import Image from "next/image";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/components/ui/card";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import {
|
|
ResizableHandle,
|
|
ResizablePanel,
|
|
ResizablePanelGroup,
|
|
} from "@/components/ui/resizable";
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table";
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
} from "@/components/ui/popover";
|
|
import {
|
|
platformCode,
|
|
crossPlatformState,
|
|
} from "@/lib/aethex-data";
|
|
import { PlaceHolderImages } from "@/lib/placeholder-images";
|
|
import { MobileIcon, RobloxIcon, WebIcon } from "./icons";
|
|
import {
|
|
AlertCircle,
|
|
CheckCircle2,
|
|
Bot,
|
|
Loader2,
|
|
ServerCrash,
|
|
ChevronsUpDown,
|
|
} from "lucide-react";
|
|
import { Button } from "../ui/button";
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
AlertDialogTrigger,
|
|
} from "@/components/ui/alert-dialog";
|
|
import { ScrollArea } from "../ui/scroll-area";
|
|
import { useState } from "react";
|
|
import {
|
|
aiSuggestedSyncConflictResolution,
|
|
AISuggestedSyncConflictResolutionOutput,
|
|
} from "@/ai/flows/ai-suggested-sync-conflict-resolution";
|
|
|
|
export function CrossPlatformView() {
|
|
const robloxViewport = PlaceHolderImages.find((p) => p.id === "roblox-vp");
|
|
const webViewport = PlaceHolderImages.find((p) => p.id === "web-vp");
|
|
const mobileViewport = PlaceHolderImages.find((p) => p.id === "mobile-vp");
|
|
|
|
return (
|
|
<ResizablePanelGroup direction="vertical" className="h-full w-full">
|
|
<ResizablePanel defaultSize={50} minSize={30}>
|
|
<div className="grid h-full grid-cols-3 gap-2 p-2">
|
|
{robloxViewport && (
|
|
<Viewport
|
|
platform="Roblox"
|
|
icon={<RobloxIcon />}
|
|
imageUrl={robloxViewport.imageUrl}
|
|
imageHint={robloxViewport.imageHint}
|
|
/>
|
|
)}
|
|
{webViewport && (
|
|
<Viewport
|
|
platform="Web"
|
|
icon={<WebIcon />}
|
|
imageUrl={webViewport.imageUrl}
|
|
imageHint={webViewport.imageHint}
|
|
/>
|
|
)}
|
|
{mobileViewport && (
|
|
<Viewport
|
|
platform="Mobile"
|
|
icon={<MobileIcon />}
|
|
imageUrl={mobileViewport.imageUrl}
|
|
imageHint={mobileViewport.imageHint}
|
|
/>
|
|
)}
|
|
</div>
|
|
</ResizablePanel>
|
|
<ResizableHandle withHandle />
|
|
<ResizablePanel defaultSize={50} minSize={30}>
|
|
<div className="grid h-full grid-cols-3 gap-2 p-2">
|
|
<div className="col-span-2">
|
|
<PlatformCodeEditor />
|
|
</div>
|
|
<div className="flex flex-col">
|
|
<StateInspector />
|
|
</div>
|
|
</div>
|
|
</ResizablePanel>
|
|
</ResizablePanelGroup>
|
|
);
|
|
}
|
|
|
|
function Viewport({
|
|
platform,
|
|
icon,
|
|
imageUrl,
|
|
imageHint,
|
|
}: {
|
|
platform: string;
|
|
icon: React.ReactNode;
|
|
imageUrl: string;
|
|
imageHint: string;
|
|
}) {
|
|
return (
|
|
<Card className="flex flex-col">
|
|
<CardHeader className="flex flex-row items-center justify-between p-3">
|
|
<div className="flex items-center gap-2">
|
|
{icon}
|
|
<CardTitle className="text-base font-headline">{platform}</CardTitle>
|
|
</div>
|
|
<div className="flex items-center gap-1.5 text-green-400">
|
|
<CheckCircle2 className="h-3 w-3" />
|
|
<span className="text-xs">Synced</span>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="flex-1 p-0">
|
|
<div className="relative h-full w-full">
|
|
<Image
|
|
src={imageUrl}
|
|
alt={`${platform} viewport`}
|
|
fill
|
|
className="object-cover"
|
|
data-ai-hint={imageHint}
|
|
/>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
function PlatformCodeEditor() {
|
|
return (
|
|
<Card className="h-full">
|
|
<Tabs defaultValue="lua" className="flex h-full flex-col">
|
|
<TabsList className="m-0 flex h-auto justify-start rounded-none border-b bg-card p-0">
|
|
{Object.entries(platformCode).map(([lang, { name }]) => (
|
|
<TabsTrigger
|
|
key={lang}
|
|
value={lang}
|
|
className="relative h-10 rounded-none border-r border-t-2 border-t-transparent bg-card px-4 py-2 text-muted-foreground shadow-none data-[state=active]:border-t-accent data-[state=active]:bg-background data-[state=active]:text-foreground"
|
|
>
|
|
{name}
|
|
</TabsTrigger>
|
|
))}
|
|
</TabsList>
|
|
{Object.entries(platformCode).map(([lang, { code }]) => (
|
|
<TabsContent
|
|
key={lang}
|
|
value={lang}
|
|
className="m-0 flex-1 overflow-hidden"
|
|
>
|
|
<ScrollArea className="h-full">
|
|
<pre className="p-4 font-code text-sm">
|
|
<code>{code}</code>
|
|
</pre>
|
|
</ScrollArea>
|
|
</TabsContent>
|
|
))}
|
|
</Tabs>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
function StateInspector() {
|
|
return (
|
|
<Card className="flex-1">
|
|
<CardHeader className="p-3">
|
|
<CardTitle className="text-base font-headline">
|
|
State Inspector
|
|
</CardTitle>
|
|
<CardDescription className="text-xs">
|
|
Real-time variable synchronization.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="p-0">
|
|
<ScrollArea className="h-[calc(100%-70px)]">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="pl-3">Variable</TableHead>
|
|
<TableHead>Value</TableHead>
|
|
<TableHead className="pr-3 text-right">Status</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{crossPlatformState.map((item) => (
|
|
<StateTableRow key={item.variable} item={item} />
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</ScrollArea>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
function StateTableRow({
|
|
item,
|
|
}: {
|
|
item: (typeof crossPlatformState)[0];
|
|
}) {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const [suggestion, setSuggestion] =
|
|
useState<AISuggestedSyncConflictResolutionOutput | null>(null);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const fetchSuggestion = async () => {
|
|
if (isLoading) return;
|
|
setIsLoading(true);
|
|
setError(null);
|
|
setSuggestion(null);
|
|
try {
|
|
const result = await aiSuggestedSyncConflictResolution({
|
|
robloxCode: platformCode.lua.code,
|
|
webCode: platformCode.javascript.code,
|
|
mobileCode: platformCode.typescript.code,
|
|
sharedState: JSON.stringify(
|
|
Object.fromEntries(
|
|
crossPlatformState.map((i) => [i.variable, i.web])
|
|
),
|
|
null,
|
|
2
|
|
),
|
|
});
|
|
setSuggestion(result);
|
|
} catch (e) {
|
|
setError("Failed to get AI suggestion. Please try again.");
|
|
console.error(e);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const renderStatus = () => {
|
|
switch (item.status) {
|
|
case "synced":
|
|
return (
|
|
<div className="flex items-center justify-end gap-1.5 text-green-400">
|
|
<CheckCircle2 className="h-3 w-3" />
|
|
<span className="text-xs">Synced</span>
|
|
</div>
|
|
);
|
|
case "syncing":
|
|
return (
|
|
<div className="flex items-center justify-end gap-1.5 text-yellow-400">
|
|
<Loader2 className="h-3 w-3 animate-spin" />
|
|
<span className="text-xs">Syncing</span>
|
|
</div>
|
|
);
|
|
case "conflict":
|
|
return (
|
|
<AlertDialog open={isOpen} onOpenChange={setIsOpen}>
|
|
<AlertDialogTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={fetchSuggestion}
|
|
disabled={isLoading}
|
|
className="h-auto p-1 text-red-500 hover:bg-red-500/10 hover:text-red-500"
|
|
>
|
|
<AlertCircle className="h-3 w-3" />
|
|
<span className="ml-1.5 text-xs">Conflict</span>
|
|
</Button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent className="max-w-2xl">
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle className="flex items-center gap-2 font-headline">
|
|
<Bot /> AI Conflict Resolution
|
|
</AlertDialogTitle>
|
|
{isLoading && (
|
|
<div className="flex items-center justify-center p-12">
|
|
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
|
</div>
|
|
)}
|
|
{error && (
|
|
<div className="flex flex-col items-center justify-center p-12 text-center">
|
|
<ServerCrash className="h-8 w-8 text-destructive" />
|
|
<p className="mt-4 text-destructive">{error}</p>
|
|
</div>
|
|
)}
|
|
{suggestion && (
|
|
<AlertDialogDescription>
|
|
{suggestion.explanation}
|
|
</AlertDialogDescription>
|
|
)}
|
|
</AlertDialogHeader>
|
|
{suggestion?.suggestedSolutions &&
|
|
suggestion.suggestedSolutions.length > 0 && (
|
|
<div className="my-4 rounded-md border bg-muted/50 p-4">
|
|
<h4 className="mb-2 font-semibold">Suggested Solutions:</h4>
|
|
<ul className="list-disc space-y-2 pl-5 font-code text-xs">
|
|
{suggestion.suggestedSolutions.map((solution, i) => (
|
|
<li key={i}>{solution}</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
{suggestion && (
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
<AlertDialogAction>Apply Suggestion</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
)}
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<TableRow>
|
|
<TableCell className="pl-3 font-code text-xs font-medium">
|
|
{item.variable}
|
|
</TableCell>
|
|
<TableCell>
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="h-auto w-full justify-between p-1.5 font-code text-xs"
|
|
>
|
|
<span className="truncate">{JSON.stringify(item.web)}</span>
|
|
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-auto p-3 font-code text-xs">
|
|
<div className="grid grid-cols-[auto_1fr] gap-x-2">
|
|
<span className="text-red-500">Roblox:</span>
|
|
<span className="text-purple-400">
|
|
{JSON.stringify(item.roblox)}
|
|
</span>
|
|
<span className="text-blue-500">Web:</span>
|
|
<span className="text-purple-400">
|
|
{JSON.stringify(item.web)}
|
|
</span>
|
|
<span className="text-green-500">Mobile:</span>
|
|
<span className="text-purple-400">
|
|
{JSON.stringify(item.mobile)}
|
|
</span>
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</TableCell>
|
|
<TableCell className="pr-3 text-right">{renderStatus()}</TableCell>
|
|
</TableRow>
|
|
);
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/aethex/dashboard-page.tsx
|
|
|
|
```tsx
|
|
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Gamepad2, Plus } from "lucide-react";
|
|
import { WorkspaceCard } from "./workspace-card";
|
|
import { WorkspaceCardSkeleton } from "./workspace-card-skeleton";
|
|
import { workspaces as initialWorkspaces } from "@/lib/workspaces";
|
|
import { NewProjectModal } from "./new-project-modal";
|
|
import { ProjectTemplate } from "@/lib/templates";
|
|
import { NewProjectFormValues } from "./new-project-modal";
|
|
import { MobileIcon, RobloxIcon, WebIcon } from "./icons";
|
|
|
|
type Workspace = typeof initialWorkspaces[0];
|
|
|
|
export function DashboardPage() {
|
|
const [workspaces, setWorkspaces] =
|
|
useState<Workspace[]>(initialWorkspaces);
|
|
const [loading, setLoading] = useState(true);
|
|
const [isNewProjectModalOpen, setIsNewProjectModalOpen] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const timer = setTimeout(() => {
|
|
setLoading(false);
|
|
}, 1500);
|
|
return () => clearTimeout(timer);
|
|
}, []);
|
|
|
|
const handleCreateProject = (
|
|
template: ProjectTemplate,
|
|
config: NewProjectFormValues
|
|
) => {
|
|
// This is a mock implementation. In a real app, this would involve
|
|
// an API call to create a new project in the backend.
|
|
const newWorkspace: Workspace = {
|
|
id: `proj-${Date.now()}`,
|
|
name: config.projectName,
|
|
lastModified: "Just now",
|
|
platforms: config.platforms.map((p) => {
|
|
if (p === "roblox") return RobloxIcon;
|
|
if (p === "web") return WebIcon;
|
|
return MobileIcon;
|
|
}),
|
|
thumbnailUrlId: "workspace-thumb-4",
|
|
thumbnailImageHint: "futuristic city",
|
|
};
|
|
setWorkspaces((prev) => [newWorkspace, ...prev]);
|
|
setIsNewProjectModalOpen(false);
|
|
};
|
|
|
|
const renderContent = () => {
|
|
if (loading) {
|
|
return (
|
|
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
|
{Array.from({ length: 3 }).map((_, i) => (
|
|
<WorkspaceCardSkeleton key={i} />
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (workspaces.length === 0) {
|
|
return (
|
|
<div className="text-center">
|
|
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-primary/10">
|
|
<Gamepad2 className="h-6 w-6 text-primary" />
|
|
</div>
|
|
<h3 className="mt-4 text-lg font-semibold text-foreground">
|
|
No projects yet
|
|
</h3>
|
|
<p className="mt-1 text-sm text-muted-foreground">
|
|
Get started by creating a new project.
|
|
</p>
|
|
<div className="mt-6">
|
|
<Button onClick={() => setIsNewProjectModalOpen(true)}>
|
|
<Plus className="-ml-0.5 mr-1.5 h-5 w-5" />
|
|
New Project
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
|
{workspaces.map((ws) => (
|
|
<WorkspaceCard key={ws.id} workspace={ws} />
|
|
))}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div className="min-h-screen bg-background">
|
|
<div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
|
|
<header className="mb-8 flex items-center justify-between">
|
|
<h1 className="font-headline text-3xl font-bold text-foreground">
|
|
My Workspaces
|
|
</h1>
|
|
<Button onClick={() => setIsNewProjectModalOpen(true)}>
|
|
<Plus className="-ml-1 mr-2" /> New Workspace
|
|
</Button>
|
|
</header>
|
|
<main>{renderContent()}</main>
|
|
</div>
|
|
</div>
|
|
<NewProjectModal
|
|
isOpen={isNewProjectModalOpen}
|
|
onClose={() => setIsNewProjectModalOpen(false)}
|
|
onCreateProject={handleCreateProject}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/aethex/file-navigator.tsx
|
|
|
|
```tsx
|
|
"use client";
|
|
|
|
import React from "react";
|
|
import {
|
|
File,
|
|
Folder,
|
|
ChevronRight,
|
|
FolderPlus,
|
|
FilePlus,
|
|
} from "lucide-react";
|
|
import {
|
|
Collapsible,
|
|
CollapsibleContent,
|
|
CollapsibleTrigger,
|
|
} from "@/components/ui/collapsible";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { FileNode, FolderNode, File as OpenFileType } from "@/lib/aethex-data";
|
|
import { generateFileContent } from "@/lib/templates";
|
|
|
|
type FileNavigatorProps = {
|
|
onOpenFile: (file: OpenFileType) => void;
|
|
fileTree: FolderNode;
|
|
};
|
|
|
|
export function FileNavigator({ onOpenFile, fileTree }: FileNavigatorProps) {
|
|
return (
|
|
<div className="flex h-full flex-col bg-card">
|
|
<div className="flex items-center justify-between border-b p-3">
|
|
<h2 className="font-headline text-lg font-semibold">Explorer</h2>
|
|
<div className="flex items-center gap-1">
|
|
<Button variant="ghost" size="icon" className="h-7 w-7">
|
|
<FilePlus className="h-4 w-4" />
|
|
</Button>
|
|
<Button variant="ghost" size="icon" className="h-7 w-7">
|
|
<FolderPlus className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<div className="flex-1 overflow-auto p-2">
|
|
<FileTree aKey="root" node={fileTree} onOpenFile={onOpenFile} path="" />
|
|
</div>
|
|
<div className="border-t p-2">
|
|
<Input placeholder="Search files..." />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
type FileTreeProps = {
|
|
node: FolderNode | FileNode;
|
|
aKey: string;
|
|
onOpenFile: (file: OpenFileType) => void;
|
|
path: string;
|
|
};
|
|
|
|
function FileTree({ node, aKey, onOpenFile, path }: FileTreeProps) {
|
|
const handleFileClick = () => {
|
|
if (node.type === "file") {
|
|
const filePath = path ? `${path}/${node.name}` : node.name;
|
|
onOpenFile({
|
|
id: filePath,
|
|
name: node.name,
|
|
language: (node as FileNode).language,
|
|
content: generateFileContent(node.name, (node as FileNode).language),
|
|
});
|
|
}
|
|
};
|
|
|
|
if (node.type === "file") {
|
|
return (
|
|
<div
|
|
className="ml-5 flex cursor-pointer items-center justify-between gap-2 rounded-md py-1 pr-2 hover:bg-muted"
|
|
onClick={handleFileClick}
|
|
>
|
|
<div className="flex items-center gap-2 truncate pl-1">
|
|
<File className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
<span className="truncate text-sm">{node.name}</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const currentPath = path ? `${path}/${node.name}` : node.name;
|
|
|
|
return (
|
|
<Collapsible defaultOpen={aKey === "root" || node.name === "roblox"}>
|
|
<CollapsibleTrigger asChild>
|
|
<div className="group flex cursor-pointer items-center justify-between gap-2 rounded-md p-1 pr-2 hover:bg-muted">
|
|
<div className="flex items-center gap-2 truncate">
|
|
<ChevronRight className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200 group-data-[state=open]:rotate-90" />
|
|
<Folder className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
<span className="truncate font-semibold text-sm">{node.name}</span>
|
|
</div>
|
|
</div>
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent>
|
|
<div className="pl-4">
|
|
{node.children.map((child, index) => (
|
|
<FileTree
|
|
key={index}
|
|
aKey={child.name}
|
|
node={child}
|
|
onOpenFile={onOpenFile}
|
|
path={currentPath}
|
|
/>
|
|
))}
|
|
</div>
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
);
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/aethex/icons.tsx
|
|
|
|
```tsx
|
|
import type { SVGProps } from "react";
|
|
|
|
export function AethexLogo(props: SVGProps<SVGSVGElement>) {
|
|
return (
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
{...props}
|
|
>
|
|
<path d="M14.5 13.03a3 3 0 1 0-3.5-3.53" />
|
|
<path d="M12 2a10 10 0 1 0 10 10" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
export function RobloxIcon(props: SVGProps<SVGSVGElement>) {
|
|
return (
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
className="h-4 w-4 text-red-500"
|
|
{...props}
|
|
>
|
|
<path d="m11.9 2.7-8.2 3.4 3.4 8.2 8.2-3.4Z" />
|
|
<path d="m13.4 7.2-5.7 2.4" />
|
|
<path d="m19.2 8.5-8.2 3.4-3.4-8.2 8.2-3.4Z" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
export function WebIcon(props: SVGProps<SVGSVGElement>) {
|
|
return (
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
className="h-4 w-4 text-blue-500"
|
|
{...props}
|
|
>
|
|
<circle cx="12" cy="12" r="10" />
|
|
<path d="M2 12h20" />
|
|
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
export function MobileIcon(props: SVGProps<SVGSVGElement>) {
|
|
return (
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
className="h-4 w-4 text-green-500"
|
|
{...props}
|
|
>
|
|
<rect width="14" height="20" x="5" y="2" rx="2" ry="2" />
|
|
<path d="M12 18h.01" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
export function GoogleIcon(props: SVGProps<SVGSVGElement>) {
|
|
return (
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48px" height="48px" {...props}>
|
|
<path fill="#FFC107" d="M43.611,20.083H42V20H24v8h11.303c-1.649,4.657-6.08,8-11.303,8c-6.627,0-12-5.373-12-12c0-6.627,5.373-12,12-12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C12.955,4,4,12.955,4,24c0,11.045,8.955,20,20,20c11.045,0,20-8.955,20-20C44,22.659,43.862,21.35,43.611,20.083z"/>
|
|
<path fill="#FF3D00" d="M6.306,14.691l6.571,4.819C14.655,15.108,18.961,12,24,12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C16.318,4,9.656,8.337,6.306,14.691z"/>
|
|
<path fill="#4CAF50" d="M24,44c5.166,0,9.86-1.977,13.409-5.192l-6.19-5.238C29.211,35.091,26.715,36,24,36c-5.202,0-9.619-3.317-11.283-7.946l-6.522,5.025C9.505,39.556,16.227,44,24,44z"/>
|
|
<path fill="#1976D2" d="M43.611,20.083H42V20H24v8h11.303c-0.792,2.237-2.231,4.166-4.087,5.574l6.19,5.238C39.99,36.596,44,30.85,44,24C44,22.659,43.862,21.35,43.611,20.083z"/>
|
|
</svg>
|
|
)
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/aethex/login-page.tsx
|
|
|
|
```tsx
|
|
"use client";
|
|
|
|
import Link from "next/link";
|
|
import Image from "next/image";
|
|
import { AethexLogo, GoogleIcon } from "@/components/aethex/icons";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Github } from "lucide-react";
|
|
import { PlaceHolderImages } from "@/lib/placeholder-images";
|
|
|
|
export function LoginPage() {
|
|
const loginIllustration = PlaceHolderImages.find(
|
|
(p) => p.id === "login-illustration"
|
|
);
|
|
|
|
return (
|
|
<div className="flex min-h-screen w-full bg-background">
|
|
<div className="flex flex-1 flex-col justify-center px-4 py-12 sm:px-6 lg:flex-none lg:px-20 xl:px-24">
|
|
<div className="mx-auto w-full max-w-sm lg:w-96">
|
|
<div>
|
|
<AethexLogo className="h-10 w-auto text-primary" />
|
|
<h1 className="mt-6 font-headline text-3xl font-bold tracking-tight text-foreground">
|
|
Welcome to AeThex Studio
|
|
</h1>
|
|
<p className="mt-2 text-sm text-muted-foreground">
|
|
The Next-Generation Cross-Platform IDE.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="mt-8">
|
|
<div className="space-y-3">
|
|
<Link href="/dashboard" passHref>
|
|
<Button
|
|
size="lg"
|
|
className="w-full bg-gradient-to-r from-primary via-purple-500 to-fuchsia-500 text-primary-foreground transition-all hover:opacity-90"
|
|
>
|
|
Sign in with AeThex Passport
|
|
</Button>
|
|
</Link>
|
|
<Link href="/dashboard" passHref>
|
|
<Button size="lg" variant="outline" className="w-full">
|
|
<GoogleIcon className="mr-3 h-5 w-5" />
|
|
Sign in with Google
|
|
</Button>
|
|
</Link>
|
|
<Link href="/dashboard" passHref>
|
|
<Button size="lg" variant="outline" className="w-full">
|
|
<Github className="mr-3 h-5 w-5" />
|
|
Sign in with GitHub
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="relative hidden w-0 flex-1 lg:block">
|
|
{loginIllustration && (
|
|
<Image
|
|
className="absolute inset-0 h-full w-full object-cover"
|
|
src={loginIllustration.imageUrl}
|
|
alt="Cross-platform game development illustration"
|
|
data-ai-hint={loginIllustration.imageHint}
|
|
fill
|
|
priority
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/aethex/main-view.tsx
|
|
|
|
```tsx
|
|
"use client";
|
|
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { CodeEditor } from "./code-editor";
|
|
import { CrossPlatformView } from "./cross-platform-view";
|
|
import type { Dispatch, SetStateAction } from "react";
|
|
import type { OpenFileType } from "./aethex-studio";
|
|
|
|
type MainViewProps = {
|
|
openFiles: OpenFileType[];
|
|
activeTab: string;
|
|
setActiveTab: Dispatch<SetStateAction<string>>;
|
|
onCloseFile: (fileId: string) => void;
|
|
};
|
|
|
|
export function MainView({ openFiles, activeTab, setActiveTab, onCloseFile }: MainViewProps) {
|
|
return (
|
|
<div className="h-full bg-background">
|
|
<Tabs defaultValue="editor" className="flex h-full flex-col">
|
|
<TabsList className="ml-2 mt-2 h-auto self-start rounded-md bg-card p-1">
|
|
<TabsTrigger value="editor">Editor</TabsTrigger>
|
|
<TabsTrigger value="cross-platform">Cross-Platform View</TabsTrigger>
|
|
</TabsList>
|
|
<TabsContent value="editor" className="m-0 flex-1 overflow-hidden">
|
|
<CodeEditor
|
|
openFiles={openFiles}
|
|
activeTab={activeTab}
|
|
setActiveTab={setActiveTab}
|
|
onCloseFile={onCloseFile}
|
|
/>
|
|
</TabsContent>
|
|
<TabsContent
|
|
value="cross-platform"
|
|
className="m-0 flex-1 overflow-hidden"
|
|
>
|
|
<CrossPlatformView />
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/aethex/navbar.tsx
|
|
|
|
```tsx
|
|
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Check, FilePlus, Loader, Play, Bot } from "lucide-react";
|
|
import { AethexLogo, MobileIcon, RobloxIcon, WebIcon } from "./icons";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
type NavbarProps = {
|
|
onNewProjectClick: () => void;
|
|
};
|
|
|
|
export function Navbar({ onNewProjectClick }: NavbarProps) {
|
|
const [saveStatus, setSaveStatus] = useState("Saved");
|
|
const [syncStatus, setSyncStatus] = useState("synced");
|
|
|
|
useEffect(() => {
|
|
const interval = setInterval(() => {
|
|
setSaveStatus("Saving...");
|
|
setTimeout(() => setSaveStatus("Saved"), 1000);
|
|
}, 5000);
|
|
return () => clearInterval(interval);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const statuses = ["synced", "syncing", "error"];
|
|
let i = 0;
|
|
const interval = setInterval(() => {
|
|
i = (i + 1) % statuses.length;
|
|
setSyncStatus(statuses[i]);
|
|
}, 7000);
|
|
return () => clearInterval(interval);
|
|
}, []);
|
|
|
|
return (
|
|
<header className="flex h-12 shrink-0 items-center justify-between border-b bg-card px-4">
|
|
<div className="flex items-center gap-4">
|
|
<div className="flex items-center gap-2">
|
|
<AethexLogo className="h-6 w-6 text-primary" />
|
|
<h1 className="font-headline text-xl font-bold">AeThex Studio</h1>
|
|
</div>
|
|
<Button variant="outline" size="sm" onClick={onNewProjectClick}>
|
|
<FilePlus className="mr-2 h-4 w-4" />
|
|
New Project
|
|
</Button>
|
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
{saveStatus === "Saving..." ? (
|
|
<>
|
|
<Loader className="h-3 w-3 animate-spin" />
|
|
<span>Saving...</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Check className="h-3 w-3" />
|
|
<span>Auto-Saved</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-4">
|
|
<Button variant="secondary" size="sm">
|
|
<Bot className="mr-2 h-4 w-4" />
|
|
AI Actions
|
|
</Button>
|
|
<div
|
|
className="flex items-center gap-2"
|
|
title={`Status: ${syncStatus}`}
|
|
>
|
|
<span className="text-xs text-muted-foreground">Sync Status</span>
|
|
<div
|
|
className={cn("h-2.5 w-2.5 rounded-full", {
|
|
"bg-green-500": syncStatus === "synced",
|
|
"animate-pulse bg-yellow-500": syncStatus === "syncing",
|
|
"bg-red-500": syncStatus === "error",
|
|
})}
|
|
/>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Button variant="outline" size="sm">
|
|
<RobloxIcon className="mr-2" />
|
|
Deploy
|
|
</Button>
|
|
<Button variant="outline" size="sm">
|
|
<WebIcon className="mr-2" />
|
|
Deploy
|
|
</Button>
|
|
<Button variant="outline" size="sm">
|
|
<MobileIcon className="mr-2" />
|
|
Deploy
|
|
</Button>
|
|
</div>
|
|
<Button size="sm">
|
|
<Play className="mr-2 h-4 w-4" />
|
|
Run All
|
|
</Button>
|
|
</div>
|
|
</header>
|
|
);
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/aethex/new-project-modal.tsx
|
|
|
|
```tsx
|
|
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useForm } from "react-hook-form";
|
|
import { z } from "zod";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogFooter,
|
|
DialogDescription,
|
|
} from "@/components/ui/dialog";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Stepper } from "@/components/ui/stepper";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardHeader,
|
|
} from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import {
|
|
Form,
|
|
FormControl,
|
|
FormField,
|
|
FormItem,
|
|
FormLabel,
|
|
FormMessage,
|
|
} from "@/components/ui/form";
|
|
import { projectTemplates, ProjectTemplate } from "@/lib/templates";
|
|
import { cn } from "@/lib/utils";
|
|
import { Crosshair, Fingerprint } from "lucide-react";
|
|
|
|
const steps = [
|
|
{ label: "Choose Template" },
|
|
{ label: "Configure" },
|
|
{ label: "Review & Create" },
|
|
];
|
|
|
|
const formSchema = z.object({
|
|
projectName: z.string().min(1, "Project name is required."),
|
|
platforms: z.array(z.string()).refine((value) => value.some((item) => item), {
|
|
message: "You have to select at least one platform.",
|
|
}),
|
|
enableNexus: z.boolean(),
|
|
enablePassport: z.boolean(),
|
|
});
|
|
|
|
export type NewProjectFormValues = z.infer<typeof formSchema>;
|
|
|
|
type NewProjectModalProps = {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onCreateProject: (
|
|
template: ProjectTemplate,
|
|
config: NewProjectFormValues
|
|
) => void;
|
|
};
|
|
|
|
export function NewProjectModal({
|
|
isOpen,
|
|
onClose,
|
|
onCreateProject,
|
|
}: NewProjectModalProps) {
|
|
const [currentStep, setCurrentStep] = useState(1);
|
|
const [selectedTemplate, setSelectedTemplate] =
|
|
useState<ProjectTemplate | null>(null);
|
|
|
|
const form = useForm<NewProjectFormValues>({
|
|
resolver: zodResolver(formSchema),
|
|
defaultValues: {
|
|
projectName: "",
|
|
platforms: ["web"],
|
|
enableNexus: true,
|
|
enablePassport: false,
|
|
},
|
|
});
|
|
|
|
const handleNext = () => {
|
|
if (currentStep === 1 && selectedTemplate) {
|
|
form.setValue(
|
|
"projectName",
|
|
selectedTemplate.id === "blank"
|
|
? ""
|
|
: selectedTemplate.name.toLowerCase().replace(/\s/g, "-")
|
|
);
|
|
setCurrentStep(2);
|
|
} else if (currentStep === 2) {
|
|
form.trigger().then((isValid) => {
|
|
if (isValid) {
|
|
setCurrentStep(3);
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleBack = () => {
|
|
if (currentStep > 1) {
|
|
setCurrentStep(currentStep - 1);
|
|
}
|
|
};
|
|
|
|
const handleCreate = () => {
|
|
if (selectedTemplate) {
|
|
onCreateProject(selectedTemplate, form.getValues());
|
|
resetAndClose();
|
|
}
|
|
};
|
|
|
|
const resetAndClose = () => {
|
|
form.reset();
|
|
setCurrentStep(1);
|
|
setSelectedTemplate(null);
|
|
onClose();
|
|
};
|
|
|
|
const platforms = [
|
|
{ id: "roblox", label: "Roblox" },
|
|
{ id: "web", label: "Web" },
|
|
{ id: "mobile", label: "Mobile" },
|
|
];
|
|
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={resetAndClose}>
|
|
<DialogContent className="max-w-4xl">
|
|
<DialogHeader>
|
|
<DialogTitle>Create New Project</DialogTitle>
|
|
<DialogDescription>
|
|
Start a new project from a template or from scratch.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="my-6">
|
|
<Stepper steps={steps} currentStep={currentStep} />
|
|
</div>
|
|
|
|
{currentStep === 1 && (
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
{projectTemplates.map((template) => (
|
|
<Card
|
|
key={template.id}
|
|
onClick={() => setSelectedTemplate(template)}
|
|
className={cn(
|
|
"cursor-pointer hover:border-primary",
|
|
selectedTemplate?.id === template.id &&
|
|
"border-2 border-primary"
|
|
)}
|
|
>
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<template.icon className="h-8 w-8 text-primary" />
|
|
{template.isPopular && (
|
|
<Badge variant="secondary">Popular</Badge>
|
|
)}
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<h3 className="font-semibold">{template.name}</h3>
|
|
<p className="text-sm text-muted-foreground">
|
|
{template.description}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{currentStep === 2 && (
|
|
<Form {...form}>
|
|
<form className="space-y-8">
|
|
<FormField
|
|
control={form.control}
|
|
name="projectName"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Project Name</FormLabel>
|
|
<FormControl>
|
|
<Input placeholder="my-awesome-project" {...field} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="platforms"
|
|
render={() => (
|
|
<FormItem>
|
|
<FormLabel>Target Platforms</FormLabel>
|
|
<div className="flex items-center space-x-4 pt-2">
|
|
{platforms.map((item) => (
|
|
<FormField
|
|
key={item.id}
|
|
control={form.control}
|
|
name="platforms"
|
|
render={({ field }) => {
|
|
return (
|
|
<FormItem
|
|
key={item.id}
|
|
className="flex flex-row items-start space-x-3 space-y-0"
|
|
>
|
|
<FormControl>
|
|
<Checkbox
|
|
checked={field.value?.includes(item.id)}
|
|
onCheckedChange={(checked) => {
|
|
return checked
|
|
? field.onChange([
|
|
...field.value,
|
|
item.id,
|
|
])
|
|
: field.onChange(
|
|
field.value?.filter(
|
|
(value) => value !== item.id
|
|
)
|
|
);
|
|
}}
|
|
/>
|
|
</FormControl>
|
|
<FormLabel className="font-normal">
|
|
{item.label}
|
|
</FormLabel>
|
|
</FormItem>
|
|
);
|
|
}}
|
|
/>
|
|
))}
|
|
</div>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<div className="space-y-4">
|
|
<FormLabel>Add-on Features</FormLabel>
|
|
<p className="text-sm text-muted-foreground">
|
|
Enhance your project with powerful AeThex services.
|
|
</p>
|
|
<div className="grid grid-cols-1 gap-4 pt-2 md:grid-cols-2">
|
|
<FormField
|
|
control={form.control}
|
|
name="enableNexus"
|
|
render={({ field }) => (
|
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
|
<div className="space-y-0.5">
|
|
<FormLabel className="flex items-center gap-2">
|
|
<Crosshair className="h-4 w-4 text-accent" />
|
|
Enable Nexus Engine
|
|
</FormLabel>
|
|
<p className="text-xs text-muted-foreground">
|
|
Real-time state synchronization.
|
|
</p>
|
|
</div>
|
|
<FormControl>
|
|
<Switch
|
|
checked={field.value}
|
|
onCheckedChange={field.onChange}
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="enablePassport"
|
|
render={({ field }) => (
|
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
|
<div className="space-y-0.5">
|
|
<FormLabel className="flex items-center gap-2">
|
|
<Fingerprint className="h-4 w-4 text-accent" />
|
|
Enable Passport Auth
|
|
</FormLabel>
|
|
<p className="text-xs text-muted-foreground">
|
|
Unified identity across platforms.
|
|
</p>
|
|
</div>
|
|
<FormControl>
|
|
<Switch
|
|
checked={field.value}
|
|
onCheckedChange={field.onChange}
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</Form>
|
|
)}
|
|
|
|
{currentStep === 3 && selectedTemplate && (
|
|
<div className="space-y-4">
|
|
<h3 className="text-lg font-semibold">Review your project</h3>
|
|
<Card>
|
|
<CardContent className="grid grid-cols-1 gap-y-6 p-6 sm:grid-cols-2 sm:gap-x-8">
|
|
<div>
|
|
<p className="text-sm font-semibold text-muted-foreground">
|
|
Project Name
|
|
</p>
|
|
<p className="font-medium">
|
|
{form.getValues("projectName")}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm font-semibold text-muted-foreground">
|
|
Template
|
|
</p>
|
|
<p className="font-medium">{selectedTemplate.name}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm font-semibold text-muted-foreground">
|
|
Platforms
|
|
</p>
|
|
<p className="font-medium">
|
|
{form.getValues("platforms").join(", ")}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm font-semibold text-muted-foreground">
|
|
Features
|
|
</p>
|
|
<div className="flex flex-col gap-2 pt-1">
|
|
{form.getValues("enableNexus") && (
|
|
<span className="flex items-center gap-2 font-medium">
|
|
<Crosshair className="h-4 w-4 text-accent" /> Nexus
|
|
Engine
|
|
</span>
|
|
)}
|
|
{form.getValues("enablePassport") && (
|
|
<span className="flex items-center gap-2 font-medium">
|
|
<Fingerprint className="h-4 w-4 text-accent" /> Passport
|
|
Auth
|
|
</span>
|
|
)}
|
|
{!form.getValues("enableNexus") &&
|
|
!form.getValues("enablePassport") && (
|
|
<p className="font-medium text-muted-foreground">
|
|
None
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)}
|
|
|
|
<DialogFooter>
|
|
{currentStep > 1 && (
|
|
<Button variant="outline" onClick={handleBack}>
|
|
Back
|
|
</Button>
|
|
)}
|
|
{currentStep < 3 && (
|
|
<Button
|
|
onClick={handleNext}
|
|
disabled={currentStep === 1 && !selectedTemplate}
|
|
>
|
|
Next
|
|
</Button>
|
|
)}
|
|
{currentStep === 3 && (
|
|
<Button onClick={handleCreate}>Create Project</Button>
|
|
)}
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/aethex/workspace-card-skeleton.tsx
|
|
|
|
```tsx
|
|
"use client";
|
|
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardFooter,
|
|
CardHeader,
|
|
} from "@/components/ui/card";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
|
|
export function WorkspaceCardSkeleton() {
|
|
return (
|
|
<Card className="overflow-hidden">
|
|
<CardHeader className="p-0">
|
|
<Skeleton className="aspect-[4/3] w-full" />
|
|
</CardHeader>
|
|
<CardContent className="p-4">
|
|
<Skeleton className="h-6 w-3/4" />
|
|
<Skeleton className="mt-2 h-3 w-1/2" />
|
|
</CardContent>
|
|
<CardFooter className="flex items-center justify-between p-4 pt-0">
|
|
<div className="flex -space-x-2">
|
|
<Skeleton className="h-6 w-6 rounded-full" />
|
|
<Skeleton className="h-6 w-6 rounded-full" />
|
|
</div>
|
|
<Skeleton className="h-8 w-8" />
|
|
</CardFooter>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/aethex/workspace-card.tsx
|
|
|
|
```tsx
|
|
"use client";
|
|
|
|
import Link from "next/link";
|
|
import Image from "next/image";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardFooter,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/components/ui/card";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
import { Button } from "@/components/ui/button";
|
|
import { MoreHorizontal } from "lucide-react";
|
|
import type { Workspace } from "@/lib/workspaces";
|
|
import { PlaceHolderImages } from "@/lib/placeholder-images";
|
|
|
|
type WorkspaceCardProps = {
|
|
workspace: Workspace;
|
|
};
|
|
|
|
export function WorkspaceCard({ workspace }: WorkspaceCardProps) {
|
|
const thumbnail = PlaceHolderImages.find(
|
|
(p) => p.id === workspace.thumbnailUrlId
|
|
);
|
|
|
|
return (
|
|
<Card className="overflow-hidden transition-all hover:shadow-lg hover:shadow-primary/10">
|
|
<CardHeader className="p-0">
|
|
<Link href="/ide" className="block aspect-[4/3] w-full">
|
|
<div className="relative h-full w-full">
|
|
{thumbnail && (
|
|
<Image
|
|
src={thumbnail.imageUrl}
|
|
alt={workspace.name}
|
|
fill
|
|
className="object-cover"
|
|
data-ai-hint={thumbnail.imageHint}
|
|
/>
|
|
)}
|
|
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent" />
|
|
</div>
|
|
</Link>
|
|
</CardHeader>
|
|
<CardContent className="p-4">
|
|
<CardTitle className="text-lg font-bold tracking-normal">
|
|
<Link href="/ide" className="hover:text-primary">
|
|
{workspace.name}
|
|
</Link>
|
|
</CardTitle>
|
|
<p className="text-xs text-muted-foreground">
|
|
Last modified {workspace.lastModified}
|
|
</p>
|
|
</CardContent>
|
|
<CardFooter className="flex items-center justify-between p-4 pt-0">
|
|
<div className="flex -space-x-2">
|
|
{workspace.platforms.map((Icon, index) => (
|
|
<div
|
|
key={index}
|
|
className="z-10 flex h-6 w-6 items-center justify-center rounded-full border-2 border-card bg-card"
|
|
>
|
|
<Icon className="h-4 w-4" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="icon" className="h-8 w-8">
|
|
<MoreHorizontal className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem>Rename</DropdownMenuItem>
|
|
<DropdownMenuItem>Share</DropdownMenuItem>
|
|
<DropdownMenuItem className="text-destructive focus:text-destructive">
|
|
Delete
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</CardFooter>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/ui/accordion.tsx
|
|
|
|
```tsx
|
|
"use client"
|
|
|
|
import * as React from "react"
|
|
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
|
import { ChevronDown } from "lucide-react"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
const Accordion = AccordionPrimitive.Root
|
|
|
|
const AccordionItem = React.forwardRef<
|
|
React.ElementRef<typeof AccordionPrimitive.Item>,
|
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
|
>(({ className, ...props }, ref) => (
|
|
<AccordionPrimitive.Item
|
|
ref={ref}
|
|
className={cn("border-b", className)}
|
|
{...props}
|
|
/>
|
|
))
|
|
AccordionItem.displayName = "AccordionItem"
|
|
|
|
const AccordionTrigger = React.forwardRef<
|
|
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
|
>(({ className, children, ...props }, ref) => (
|
|
<AccordionPrimitive.Header className="flex">
|
|
<AccordionPrimitive.Trigger
|
|
ref={ref}
|
|
className={cn(
|
|
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
|
</AccordionPrimitive.Trigger>
|
|
</AccordionPrimitive.Header>
|
|
))
|
|
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
|
|
|
const AccordionContent = React.forwardRef<
|
|
React.ElementRef<typeof AccordionPrimitive.Content>,
|
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
|
>(({ className, children, ...props }, ref) => (
|
|
<AccordionPrimitive.Content
|
|
ref={ref}
|
|
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
|
{...props}
|
|
>
|
|
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
|
</AccordionPrimitive.Content>
|
|
))
|
|
|
|
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
|
|
|
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/ui/alert-dialog.tsx
|
|
|
|
```tsx
|
|
"use client"
|
|
|
|
import * as React from "react"
|
|
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
import { buttonVariants } from "@/components/ui/button"
|
|
|
|
const AlertDialog = AlertDialogPrimitive.Root
|
|
|
|
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
|
|
|
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
|
|
|
const AlertDialogOverlay = React.forwardRef<
|
|
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
|
>(({ className, ...props }, ref) => (
|
|
<AlertDialogPrimitive.Overlay
|
|
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
|
|
)}
|
|
{...props}
|
|
ref={ref}
|
|
/>
|
|
))
|
|
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
|
|
|
const AlertDialogContent = React.forwardRef<
|
|
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
|
>(({ className, ...props }, ref) => (
|
|
<AlertDialogPortal>
|
|
<AlertDialogOverlay />
|
|
<AlertDialogPrimitive.Content
|
|
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 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
|
|
)}
|
|
{...props}
|
|
/>
|
|
</AlertDialogPortal>
|
|
))
|
|
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
|
|
|
const AlertDialogHeader = ({
|
|
className,
|
|
...props
|
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
<div
|
|
className={cn(
|
|
"flex flex-col space-y-2 text-center sm:text-left",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
)
|
|
AlertDialogHeader.displayName = "AlertDialogHeader"
|
|
|
|
const AlertDialogFooter = ({
|
|
className,
|
|
...props
|
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
<div
|
|
className={cn(
|
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
)
|
|
AlertDialogFooter.displayName = "AlertDialogFooter"
|
|
|
|
const AlertDialogTitle = React.forwardRef<
|
|
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
|
>(({ className, ...props }, ref) => (
|
|
<AlertDialogPrimitive.Title
|
|
ref={ref}
|
|
className={cn("text-lg font-semibold", className)}
|
|
{...props}
|
|
/>
|
|
))
|
|
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
|
|
|
const AlertDialogDescription = React.forwardRef<
|
|
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
|
>(({ className, ...props }, ref) => (
|
|
<AlertDialogPrimitive.Description
|
|
ref={ref}
|
|
className={cn("text-sm text-muted-foreground", className)}
|
|
{...props}
|
|
/>
|
|
))
|
|
AlertDialogDescription.displayName =
|
|
AlertDialogPrimitive.Description.displayName
|
|
|
|
const AlertDialogAction = React.forwardRef<
|
|
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
|
>(({ className, ...props }, ref) => (
|
|
<AlertDialogPrimitive.Action
|
|
ref={ref}
|
|
className={cn(buttonVariants(), className)}
|
|
{...props}
|
|
/>
|
|
))
|
|
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
|
|
|
const AlertDialogCancel = React.forwardRef<
|
|
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
|
>(({ className, ...props }, ref) => (
|
|
<AlertDialogPrimitive.Cancel
|
|
ref={ref}
|
|
className={cn(
|
|
buttonVariants({ variant: "outline" }),
|
|
"mt-2 sm:mt-0",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
))
|
|
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
|
|
|
export {
|
|
AlertDialog,
|
|
AlertDialogPortal,
|
|
AlertDialogOverlay,
|
|
AlertDialogTrigger,
|
|
AlertDialogContent,
|
|
AlertDialogHeader,
|
|
AlertDialogFooter,
|
|
AlertDialogTitle,
|
|
AlertDialogDescription,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/ui/alert.tsx
|
|
|
|
```tsx
|
|
import * as React from "react"
|
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
const alertVariants = cva(
|
|
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
|
{
|
|
variants: {
|
|
variant: {
|
|
default: "bg-background text-foreground",
|
|
destructive:
|
|
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
|
},
|
|
},
|
|
defaultVariants: {
|
|
variant: "default",
|
|
},
|
|
}
|
|
)
|
|
|
|
const Alert = React.forwardRef<
|
|
HTMLDivElement,
|
|
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
|
>(({ className, variant, ...props }, ref) => (
|
|
<div
|
|
ref={ref}
|
|
role="alert"
|
|
className={cn(alertVariants({ variant }), className)}
|
|
{...props}
|
|
/>
|
|
))
|
|
Alert.displayName = "Alert"
|
|
|
|
const AlertTitle = React.forwardRef<
|
|
HTMLParagraphElement,
|
|
React.HTMLAttributes<HTMLHeadingElement>
|
|
>(({ className, ...props }, ref) => (
|
|
<h5
|
|
ref={ref}
|
|
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
|
{...props}
|
|
/>
|
|
))
|
|
AlertTitle.displayName = "AlertTitle"
|
|
|
|
const AlertDescription = React.forwardRef<
|
|
HTMLParagraphElement,
|
|
React.HTMLAttributes<HTMLParagraphElement>
|
|
>(({ className, ...props }, ref) => (
|
|
<div
|
|
ref={ref}
|
|
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
|
{...props}
|
|
/>
|
|
))
|
|
AlertDescription.displayName = "AlertDescription"
|
|
|
|
export { Alert, AlertTitle, AlertDescription }
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/ui/avatar.tsx
|
|
|
|
```tsx
|
|
"use client"
|
|
|
|
import * as React from "react"
|
|
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
const Avatar = React.forwardRef<
|
|
React.ElementRef<typeof AvatarPrimitive.Root>,
|
|
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
|
>(({ className, ...props }, ref) => (
|
|
<AvatarPrimitive.Root
|
|
ref={ref}
|
|
className={cn(
|
|
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
))
|
|
Avatar.displayName = AvatarPrimitive.Root.displayName
|
|
|
|
const AvatarImage = React.forwardRef<
|
|
React.ElementRef<typeof AvatarPrimitive.Image>,
|
|
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
|
>(({ className, ...props }, ref) => (
|
|
<AvatarPrimitive.Image
|
|
ref={ref}
|
|
className={cn("aspect-square h-full w-full", className)}
|
|
{...props}
|
|
/>
|
|
))
|
|
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
|
|
|
const AvatarFallback = React.forwardRef<
|
|
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
|
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
|
>(({ className, ...props }, ref) => (
|
|
<AvatarPrimitive.Fallback
|
|
ref={ref}
|
|
className={cn(
|
|
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
))
|
|
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
|
|
|
export { Avatar, AvatarImage, AvatarFallback }
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/ui/badge.tsx
|
|
|
|
```tsx
|
|
import * as React from "react"
|
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
const badgeVariants = cva(
|
|
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
|
{
|
|
variants: {
|
|
variant: {
|
|
default:
|
|
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
|
secondary:
|
|
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
destructive:
|
|
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
|
outline: "text-foreground",
|
|
},
|
|
},
|
|
defaultVariants: {
|
|
variant: "default",
|
|
},
|
|
}
|
|
)
|
|
|
|
export interface BadgeProps
|
|
extends React.HTMLAttributes<HTMLDivElement>,
|
|
VariantProps<typeof badgeVariants> {}
|
|
|
|
function Badge({ className, variant, ...props }: BadgeProps) {
|
|
return (
|
|
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
|
)
|
|
}
|
|
|
|
export { Badge, badgeVariants }
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/ui/button.tsx
|
|
|
|
```tsx
|
|
import * as React from "react"
|
|
import { Slot } from "@radix-ui/react-slot"
|
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
const buttonVariants = cva(
|
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
{
|
|
variants: {
|
|
variant: {
|
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
destructive:
|
|
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
outline:
|
|
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
|
secondary:
|
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
link: "text-primary underline-offset-4 hover:underline",
|
|
},
|
|
size: {
|
|
default: "h-10 px-4 py-2",
|
|
sm: "h-9 rounded-md px-3",
|
|
lg: "h-11 rounded-md px-8",
|
|
icon: "h-10 w-10",
|
|
},
|
|
},
|
|
defaultVariants: {
|
|
variant: "default",
|
|
size: "default",
|
|
},
|
|
}
|
|
)
|
|
|
|
export interface ButtonProps
|
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
VariantProps<typeof buttonVariants> {
|
|
asChild?: boolean
|
|
}
|
|
|
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
const Comp = asChild ? Slot : "button"
|
|
return (
|
|
<Comp
|
|
className={cn(buttonVariants({ variant, size, className }))}
|
|
ref={ref}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
)
|
|
Button.displayName = "Button"
|
|
|
|
export { Button, buttonVariants }
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/ui/calendar.tsx
|
|
|
|
```tsx
|
|
"use client"
|
|
|
|
import * as React from "react"
|
|
import { ChevronLeft, ChevronRight } from "lucide-react"
|
|
import { DayPicker } from "react-day-picker"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
import { buttonVariants } from "@/components/ui/button"
|
|
|
|
export type CalendarProps = React.ComponentProps<typeof DayPicker>
|
|
|
|
function Calendar({
|
|
className,
|
|
classNames,
|
|
showOutsideDays = true,
|
|
...props
|
|
}: CalendarProps) {
|
|
return (
|
|
<DayPicker
|
|
showOutsideDays={showOutsideDays}
|
|
className={cn("p-3", className)}
|
|
classNames={{
|
|
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
|
|
month: "space-y-4",
|
|
caption: "flex justify-center pt-1 relative items-center",
|
|
caption_label: "text-sm font-medium",
|
|
nav: "space-x-1 flex items-center",
|
|
nav_button: cn(
|
|
buttonVariants({ variant: "outline" }),
|
|
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
|
|
),
|
|
nav_button_previous: "absolute left-1",
|
|
nav_button_next: "absolute right-1",
|
|
table: "w-full border-collapse space-y-1",
|
|
head_row: "flex",
|
|
head_cell:
|
|
"text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
|
|
row: "flex w-full mt-2",
|
|
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
|
|
day: cn(
|
|
buttonVariants({ variant: "ghost" }),
|
|
"h-9 w-9 p-0 font-normal aria-selected:opacity-100"
|
|
),
|
|
day_range_end: "day-range-end",
|
|
day_selected:
|
|
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
|
day_today: "bg-accent text-accent-foreground",
|
|
day_outside:
|
|
"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
|
|
day_disabled: "text-muted-foreground opacity-50",
|
|
day_range_middle:
|
|
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
|
day_hidden: "invisible",
|
|
...classNames,
|
|
}}
|
|
components={{
|
|
IconLeft: ({ className, ...props }) => (
|
|
<ChevronLeft className={cn("h-4 w-4", className)} {...props} />
|
|
),
|
|
IconRight: ({ className, ...props }) => (
|
|
<ChevronRight className={cn("h-4 w-4", className)} {...props} />
|
|
),
|
|
}}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
Calendar.displayName = "Calendar"
|
|
|
|
export { Calendar }
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/ui/card.tsx
|
|
|
|
```tsx
|
|
import * as React from "react"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
const Card = React.forwardRef<
|
|
HTMLDivElement,
|
|
React.HTMLAttributes<HTMLDivElement>
|
|
>(({ className, ...props }, ref) => (
|
|
<div
|
|
ref={ref}
|
|
className={cn(
|
|
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
))
|
|
Card.displayName = "Card"
|
|
|
|
const CardHeader = React.forwardRef<
|
|
HTMLDivElement,
|
|
React.HTMLAttributes<HTMLDivElement>
|
|
>(({ className, ...props }, ref) => (
|
|
<div
|
|
ref={ref}
|
|
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
|
{...props}
|
|
/>
|
|
))
|
|
CardHeader.displayName = "CardHeader"
|
|
|
|
const CardTitle = React.forwardRef<
|
|
HTMLDivElement,
|
|
React.HTMLAttributes<HTMLDivElement>
|
|
>(({ className, ...props }, ref) => (
|
|
<div
|
|
ref={ref}
|
|
className={cn(
|
|
"text-2xl font-semibold leading-none tracking-tight",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
))
|
|
CardTitle.displayName = "CardTitle"
|
|
|
|
const CardDescription = React.forwardRef<
|
|
HTMLDivElement,
|
|
React.HTMLAttributes<HTMLDivElement>
|
|
>(({ className, ...props }, ref) => (
|
|
<div
|
|
ref={ref}
|
|
className={cn("text-sm text-muted-foreground", className)}
|
|
{...props}
|
|
/>
|
|
))
|
|
CardDescription.displayName = "CardDescription"
|
|
|
|
const CardContent = React.forwardRef<
|
|
HTMLDivElement,
|
|
React.HTMLAttributes<HTMLDivElement>
|
|
>(({ className, ...props }, ref) => (
|
|
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
|
))
|
|
CardContent.displayName = "CardContent"
|
|
|
|
const CardFooter = React.forwardRef<
|
|
HTMLDivElement,
|
|
React.HTMLAttributes<HTMLDivElement>
|
|
>(({ className, ...props }, ref) => (
|
|
<div
|
|
ref={ref}
|
|
className={cn("flex items-center p-6 pt-0", className)}
|
|
{...props}
|
|
/>
|
|
))
|
|
CardFooter.displayName = "CardFooter"
|
|
|
|
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/ui/carousel.tsx
|
|
|
|
```tsx
|
|
"use client"
|
|
|
|
import * as React from "react"
|
|
import useEmblaCarousel, {
|
|
type UseEmblaCarouselType,
|
|
} from "embla-carousel-react"
|
|
import { ArrowLeft, ArrowRight } from "lucide-react"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
import { Button } from "@/components/ui/button"
|
|
|
|
type CarouselApi = UseEmblaCarouselType[1]
|
|
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
|
|
type CarouselOptions = UseCarouselParameters[0]
|
|
type CarouselPlugin = UseCarouselParameters[1]
|
|
|
|
type CarouselProps = {
|
|
opts?: CarouselOptions
|
|
plugins?: CarouselPlugin
|
|
orientation?: "horizontal" | "vertical"
|
|
setApi?: (api: CarouselApi) => void
|
|
}
|
|
|
|
type CarouselContextProps = {
|
|
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
|
|
api: ReturnType<typeof useEmblaCarousel>[1]
|
|
scrollPrev: () => void
|
|
scrollNext: () => void
|
|
canScrollPrev: boolean
|
|
canScrollNext: boolean
|
|
} & CarouselProps
|
|
|
|
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
|
|
|
|
function useCarousel() {
|
|
const context = React.useContext(CarouselContext)
|
|
|
|
if (!context) {
|
|
throw new Error("useCarousel must be used within a <Carousel />")
|
|
}
|
|
|
|
return context
|
|
}
|
|
|
|
const Carousel = React.forwardRef<
|
|
HTMLDivElement,
|
|
React.HTMLAttributes<HTMLDivElement> & CarouselProps
|
|
>(
|
|
(
|
|
{
|
|
orientation = "horizontal",
|
|
opts,
|
|
setApi,
|
|
plugins,
|
|
className,
|
|
children,
|
|
...props
|
|
},
|
|
ref
|
|
) => {
|
|
const [carouselRef, api] = useEmblaCarousel(
|
|
{
|
|
...opts,
|
|
axis: orientation === "horizontal" ? "x" : "y",
|
|
},
|
|
plugins
|
|
)
|
|
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
|
|
const [canScrollNext, setCanScrollNext] = React.useState(false)
|
|
|
|
const onSelect = React.useCallback((api: CarouselApi) => {
|
|
if (!api) {
|
|
return
|
|
}
|
|
|
|
setCanScrollPrev(api.canScrollPrev())
|
|
setCanScrollNext(api.canScrollNext())
|
|
}, [])
|
|
|
|
const scrollPrev = React.useCallback(() => {
|
|
api?.scrollPrev()
|
|
}, [api])
|
|
|
|
const scrollNext = React.useCallback(() => {
|
|
api?.scrollNext()
|
|
}, [api])
|
|
|
|
const handleKeyDown = React.useCallback(
|
|
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
if (event.key === "ArrowLeft") {
|
|
event.preventDefault()
|
|
scrollPrev()
|
|
} else if (event.key === "ArrowRight") {
|
|
event.preventDefault()
|
|
scrollNext()
|
|
}
|
|
},
|
|
[scrollPrev, scrollNext]
|
|
)
|
|
|
|
React.useEffect(() => {
|
|
if (!api || !setApi) {
|
|
return
|
|
}
|
|
|
|
setApi(api)
|
|
}, [api, setApi])
|
|
|
|
React.useEffect(() => {
|
|
if (!api) {
|
|
return
|
|
}
|
|
|
|
onSelect(api)
|
|
api.on("reInit", onSelect)
|
|
api.on("select", onSelect)
|
|
|
|
return () => {
|
|
api?.off("select", onSelect)
|
|
}
|
|
}, [api, onSelect])
|
|
|
|
return (
|
|
<CarouselContext.Provider
|
|
value={{
|
|
carouselRef,
|
|
api: api,
|
|
opts,
|
|
orientation:
|
|
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
|
scrollPrev,
|
|
scrollNext,
|
|
canScrollPrev,
|
|
canScrollNext,
|
|
}}
|
|
>
|
|
<div
|
|
ref={ref}
|
|
onKeyDownCapture={handleKeyDown}
|
|
className={cn("relative", className)}
|
|
role="region"
|
|
aria-roledescription="carousel"
|
|
{...props}
|
|
>
|
|
{children}
|
|
</div>
|
|
</CarouselContext.Provider>
|
|
)
|
|
}
|
|
)
|
|
Carousel.displayName = "Carousel"
|
|
|
|
const CarouselContent = React.forwardRef<
|
|
HTMLDivElement,
|
|
React.HTMLAttributes<HTMLDivElement>
|
|
>(({ className, ...props }, ref) => {
|
|
const { carouselRef, orientation } = useCarousel()
|
|
|
|
return (
|
|
<div ref={carouselRef} className="overflow-hidden">
|
|
<div
|
|
ref={ref}
|
|
className={cn(
|
|
"flex",
|
|
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
</div>
|
|
)
|
|
})
|
|
CarouselContent.displayName = "CarouselContent"
|
|
|
|
const CarouselItem = React.forwardRef<
|
|
HTMLDivElement,
|
|
React.HTMLAttributes<HTMLDivElement>
|
|
>(({ className, ...props }, ref) => {
|
|
const { orientation } = useCarousel()
|
|
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
role="group"
|
|
aria-roledescription="slide"
|
|
className={cn(
|
|
"min-w-0 shrink-0 grow-0 basis-full",
|
|
orientation === "horizontal" ? "pl-4" : "pt-4",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
)
|
|
})
|
|
CarouselItem.displayName = "CarouselItem"
|
|
|
|
const CarouselPrevious = React.forwardRef<
|
|
HTMLButtonElement,
|
|
React.ComponentProps<typeof Button>
|
|
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
|
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
|
|
|
|
return (
|
|
<Button
|
|
ref={ref}
|
|
variant={variant}
|
|
size={size}
|
|
className={cn(
|
|
"absolute h-8 w-8 rounded-full",
|
|
orientation === "horizontal"
|
|
? "-left-12 top-1/2 -translate-y-1/2"
|
|
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
|
className
|
|
)}
|
|
disabled={!canScrollPrev}
|
|
onClick={scrollPrev}
|
|
{...props}
|
|
>
|
|
<ArrowLeft className="h-4 w-4" />
|
|
<span className="sr-only">Previous slide</span>
|
|
</Button>
|
|
)
|
|
})
|
|
CarouselPrevious.displayName = "CarouselPrevious"
|
|
|
|
const CarouselNext = React.forwardRef<
|
|
HTMLButtonElement,
|
|
React.ComponentProps<typeof Button>
|
|
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
|
const { orientation, scrollNext, canScrollNext } = useCarousel()
|
|
|
|
return (
|
|
<Button
|
|
ref={ref}
|
|
variant={variant}
|
|
size={size}
|
|
className={cn(
|
|
"absolute h-8 w-8 rounded-full",
|
|
orientation === "horizontal"
|
|
? "-right-12 top-1/2 -translate-y-1/2"
|
|
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
|
className
|
|
)}
|
|
disabled={!canScrollNext}
|
|
onClick={scrollNext}
|
|
{...props}
|
|
>
|
|
<ArrowRight className="h-4 w-4" />
|
|
<span className="sr-only">Next slide</span>
|
|
</Button>
|
|
)
|
|
})
|
|
CarouselNext.displayName = "CarouselNext"
|
|
|
|
export {
|
|
type CarouselApi,
|
|
Carousel,
|
|
CarouselContent,
|
|
CarouselItem,
|
|
CarouselPrevious,
|
|
CarouselNext,
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/ui/chart.tsx
|
|
|
|
```tsx
|
|
"use client"
|
|
|
|
import * as React from "react"
|
|
import * as RechartsPrimitive from "recharts"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
// Format: { THEME_NAME: CSS_SELECTOR }
|
|
const THEMES = { light: "", dark: ".dark" } as const
|
|
|
|
export type ChartConfig = {
|
|
[k in string]: {
|
|
label?: React.ReactNode
|
|
icon?: React.ComponentType
|
|
} & (
|
|
| { color?: string; theme?: never }
|
|
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
|
)
|
|
}
|
|
|
|
type ChartContextProps = {
|
|
config: ChartConfig
|
|
}
|
|
|
|
const ChartContext = React.createContext<ChartContextProps | null>(null)
|
|
|
|
function useChart() {
|
|
const context = React.useContext(ChartContext)
|
|
|
|
if (!context) {
|
|
throw new Error("useChart must be used within a <ChartContainer />")
|
|
}
|
|
|
|
return context
|
|
}
|
|
|
|
const ChartContainer = React.forwardRef<
|
|
HTMLDivElement,
|
|
React.ComponentProps<"div"> & {
|
|
config: ChartConfig
|
|
children: React.ComponentProps<
|
|
typeof RechartsPrimitive.ResponsiveContainer
|
|
>["children"]
|
|
}
|
|
>(({ id, className, children, config, ...props }, ref) => {
|
|
const uniqueId = React.useId()
|
|
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
|
|
|
|
return (
|
|
<ChartContext.Provider value={{ config }}>
|
|
<div
|
|
data-chart={chartId}
|
|
ref={ref}
|
|
className={cn(
|
|
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
<ChartStyle id={chartId} config={config} />
|
|
<RechartsPrimitive.ResponsiveContainer>
|
|
{children}
|
|
</RechartsPrimitive.ResponsiveContainer>
|
|
</div>
|
|
</ChartContext.Provider>
|
|
)
|
|
})
|
|
ChartContainer.displayName = "Chart"
|
|
|
|
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
|
const colorConfig = Object.entries(config).filter(
|
|
([, config]) => config.theme || config.color
|
|
)
|
|
|
|
if (!colorConfig.length) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<style
|
|
dangerouslySetInnerHTML={{
|
|
__html: Object.entries(THEMES)
|
|
.map(
|
|
([theme, prefix]) => `
|
|
${prefix} [data-chart=${id}] {
|
|
${colorConfig
|
|
.map(([key, itemConfig]) => {
|
|
const color =
|
|
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
|
itemConfig.color
|
|
return color ? ` --color-${key}: ${color};` : null
|
|
})
|
|
.join("\n")}
|
|
}
|
|
`
|
|
)
|
|
.join("\n"),
|
|
}}
|
|
/>
|
|
)
|
|
}
|
|
|
|
const ChartTooltip = RechartsPrimitive.Tooltip
|
|
|
|
const ChartTooltipContent = React.forwardRef<
|
|
HTMLDivElement,
|
|
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
|
React.ComponentProps<"div"> & {
|
|
hideLabel?: boolean
|
|
hideIndicator?: boolean
|
|
indicator?: "line" | "dot" | "dashed"
|
|
nameKey?: string
|
|
labelKey?: string
|
|
}
|
|
>(
|
|
(
|
|
{
|
|
active,
|
|
payload,
|
|
className,
|
|
indicator = "dot",
|
|
hideLabel = false,
|
|
hideIndicator = false,
|
|
label,
|
|
labelFormatter,
|
|
labelClassName,
|
|
formatter,
|
|
color,
|
|
nameKey,
|
|
labelKey,
|
|
},
|
|
ref
|
|
) => {
|
|
const { config } = useChart()
|
|
|
|
const tooltipLabel = React.useMemo(() => {
|
|
if (hideLabel || !payload?.length) {
|
|
return null
|
|
}
|
|
|
|
const [item] = payload
|
|
const key = `${labelKey || item.dataKey || item.name || "value"}`
|
|
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
|
const value =
|
|
!labelKey && typeof label === "string"
|
|
? config[label as keyof typeof config]?.label || label
|
|
: itemConfig?.label
|
|
|
|
if (labelFormatter) {
|
|
return (
|
|
<div className={cn("font-medium", labelClassName)}>
|
|
{labelFormatter(value, payload)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (!value) {
|
|
return null
|
|
}
|
|
|
|
return <div className={cn("font-medium", labelClassName)}>{value}</div>
|
|
}, [
|
|
label,
|
|
labelFormatter,
|
|
payload,
|
|
hideLabel,
|
|
labelClassName,
|
|
config,
|
|
labelKey,
|
|
])
|
|
|
|
if (!active || !payload?.length) {
|
|
return null
|
|
}
|
|
|
|
const nestLabel = payload.length === 1 && indicator !== "dot"
|
|
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={cn(
|
|
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
|
|
className
|
|
)}
|
|
>
|
|
{!nestLabel ? tooltipLabel : null}
|
|
<div className="grid gap-1.5">
|
|
{payload.map((item, index) => {
|
|
const key = `${nameKey || item.name || item.dataKey || "value"}`
|
|
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
|
const indicatorColor = color || item.payload.fill || item.color
|
|
|
|
return (
|
|
<div
|
|
key={item.dataKey}
|
|
className={cn(
|
|
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
|
indicator === "dot" && "items-center"
|
|
)}
|
|
>
|
|
{formatter && item?.value !== undefined && item.name ? (
|
|
formatter(item.value, item.name, item, index, item.payload)
|
|
) : (
|
|
<>
|
|
{itemConfig?.icon ? (
|
|
<itemConfig.icon />
|
|
) : (
|
|
!hideIndicator && (
|
|
<div
|
|
className={cn(
|
|
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
|
|
{
|
|
"h-2.5 w-2.5": indicator === "dot",
|
|
"w-1": indicator === "line",
|
|
"w-0 border-[1.5px] border-dashed bg-transparent":
|
|
indicator === "dashed",
|
|
"my-0.5": nestLabel && indicator === "dashed",
|
|
}
|
|
)}
|
|
style={
|
|
{
|
|
"--color-bg": indicatorColor,
|
|
"--color-border": indicatorColor,
|
|
} as React.CSSProperties
|
|
}
|
|
/>
|
|
)
|
|
)}
|
|
<div
|
|
className={cn(
|
|
"flex flex-1 justify-between leading-none",
|
|
nestLabel ? "items-end" : "items-center"
|
|
)}
|
|
>
|
|
<div className="grid gap-1.5">
|
|
{nestLabel ? tooltipLabel : null}
|
|
<span className="text-muted-foreground">
|
|
{itemConfig?.label || item.name}
|
|
</span>
|
|
</div>
|
|
{item.value && (
|
|
<span className="font-mono font-medium tabular-nums text-foreground">
|
|
{item.value.toLocaleString()}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
)
|
|
ChartTooltipContent.displayName = "ChartTooltip"
|
|
|
|
const ChartLegend = RechartsPrimitive.Legend
|
|
|
|
const ChartLegendContent = React.forwardRef<
|
|
HTMLDivElement,
|
|
React.ComponentProps<"div"> &
|
|
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
|
hideIcon?: boolean
|
|
nameKey?: string
|
|
}
|
|
>(
|
|
(
|
|
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
|
|
ref
|
|
) => {
|
|
const { config } = useChart()
|
|
|
|
if (!payload?.length) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={cn(
|
|
"flex items-center justify-center gap-4",
|
|
verticalAlign === "top" ? "pb-3" : "pt-3",
|
|
className
|
|
)}
|
|
>
|
|
{payload.map((item) => {
|
|
const key = `${nameKey || item.dataKey || "value"}`
|
|
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
|
|
|
return (
|
|
<div
|
|
key={item.value}
|
|
className={cn(
|
|
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
|
|
)}
|
|
>
|
|
{itemConfig?.icon && !hideIcon ? (
|
|
<itemConfig.icon />
|
|
) : (
|
|
<div
|
|
className="h-2 w-2 shrink-0 rounded-[2px]"
|
|
style={{
|
|
backgroundColor: item.color,
|
|
}}
|
|
/>
|
|
)}
|
|
{itemConfig?.label}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
}
|
|
)
|
|
ChartLegendContent.displayName = "ChartLegend"
|
|
|
|
// Helper to extract item config from a payload.
|
|
function getPayloadConfigFromPayload(
|
|
config: ChartConfig,
|
|
payload: unknown,
|
|
key: string
|
|
) {
|
|
if (typeof payload !== "object" || payload === null) {
|
|
return undefined
|
|
}
|
|
|
|
const payloadPayload =
|
|
"payload" in payload &&
|
|
typeof payload.payload === "object" &&
|
|
payload.payload !== null
|
|
? payload.payload
|
|
: undefined
|
|
|
|
let configLabelKey: string = key
|
|
|
|
if (
|
|
key in payload &&
|
|
typeof payload[key as keyof typeof payload] === "string"
|
|
) {
|
|
configLabelKey = payload[key as keyof typeof payload] as string
|
|
} else if (
|
|
payloadPayload &&
|
|
key in payloadPayload &&
|
|
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
|
) {
|
|
configLabelKey = payloadPayload[
|
|
key as keyof typeof payloadPayload
|
|
] as string
|
|
}
|
|
|
|
return configLabelKey in config
|
|
? config[configLabelKey]
|
|
: config[key as keyof typeof config]
|
|
}
|
|
|
|
export {
|
|
ChartContainer,
|
|
ChartTooltip,
|
|
ChartTooltipContent,
|
|
ChartLegend,
|
|
ChartLegendContent,
|
|
ChartStyle,
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/ui/checkbox.tsx
|
|
|
|
```tsx
|
|
"use client"
|
|
|
|
import * as React from "react"
|
|
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
|
import { Check } from "lucide-react"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
const Checkbox = React.forwardRef<
|
|
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
|
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
|
>(({ className, ...props }, ref) => (
|
|
<CheckboxPrimitive.Root
|
|
ref={ref}
|
|
className={cn(
|
|
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
<CheckboxPrimitive.Indicator
|
|
className={cn("flex items-center justify-center text-current")}
|
|
>
|
|
<Check className="h-4 w-4" />
|
|
</CheckboxPrimitive.Indicator>
|
|
</CheckboxPrimitive.Root>
|
|
))
|
|
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
|
|
|
export { Checkbox }
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/ui/collapsible.tsx
|
|
|
|
```tsx
|
|
"use client"
|
|
|
|
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
|
|
|
const Collapsible = CollapsiblePrimitive.Root
|
|
|
|
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
|
|
|
|
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
|
|
|
|
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/ui/dialog.tsx
|
|
|
|
```tsx
|
|
"use client"
|
|
|
|
import * as React from "react"
|
|
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
|
import { X } from "lucide-react"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
const Dialog = DialogPrimitive.Root
|
|
|
|
const DialogTrigger = DialogPrimitive.Trigger
|
|
|
|
const DialogPortal = DialogPrimitive.Portal
|
|
|
|
const DialogClose = DialogPrimitive.Close
|
|
|
|
const DialogOverlay = React.forwardRef<
|
|
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
|
>(({ className, ...props }, ref) => (
|
|
<DialogPrimitive.Overlay
|
|
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
|
|
)}
|
|
{...props}
|
|
/>
|
|
))
|
|
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
|
|
|
const DialogContent = React.forwardRef<
|
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
|
>(({ className, children, ...props }, ref) => (
|
|
<DialogPortal>
|
|
<DialogOverlay />
|
|
<DialogPrimitive.Content
|
|
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 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
|
|
)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
|
<X className="h-4 w-4" />
|
|
<span className="sr-only">Close</span>
|
|
</DialogPrimitive.Close>
|
|
</DialogPrimitive.Content>
|
|
</DialogPortal>
|
|
))
|
|
DialogContent.displayName = DialogPrimitive.Content.displayName
|
|
|
|
const DialogHeader = ({
|
|
className,
|
|
...props
|
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
<div
|
|
className={cn(
|
|
"flex flex-col space-y-1.5 text-center sm:text-left",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
)
|
|
DialogHeader.displayName = "DialogHeader"
|
|
|
|
const DialogFooter = ({
|
|
className,
|
|
...props
|
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
<div
|
|
className={cn(
|
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
)
|
|
DialogFooter.displayName = "DialogFooter"
|
|
|
|
const DialogTitle = React.forwardRef<
|
|
React.ElementRef<typeof DialogPrimitive.Title>,
|
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
|
>(({ className, ...props }, ref) => (
|
|
<DialogPrimitive.Title
|
|
ref={ref}
|
|
className={cn(
|
|
"text-lg font-semibold leading-none tracking-tight",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
))
|
|
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
|
|
|
const DialogDescription = React.forwardRef<
|
|
React.ElementRef<typeof DialogPrimitive.Description>,
|
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
|
>(({ className, ...props }, ref) => (
|
|
<DialogPrimitive.Description
|
|
ref={ref}
|
|
className={cn("text-sm text-muted-foreground", className)}
|
|
{...props}
|
|
/>
|
|
))
|
|
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
|
|
|
export {
|
|
Dialog,
|
|
DialogPortal,
|
|
DialogOverlay,
|
|
DialogClose,
|
|
DialogTrigger,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogFooter,
|
|
DialogTitle,
|
|
DialogDescription,
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/ui/dropdown-menu.tsx
|
|
|
|
```tsx
|
|
"use client"
|
|
|
|
import * as React from "react"
|
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
|
import { Check, ChevronRight, Circle } from "lucide-react"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
const DropdownMenu = DropdownMenuPrimitive.Root
|
|
|
|
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
|
|
|
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
|
|
|
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
|
|
|
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
|
|
|
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
|
|
|
const DropdownMenuSubTrigger = React.forwardRef<
|
|
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
|
inset?: boolean
|
|
}
|
|
>(({ className, inset, children, ...props }, ref) => (
|
|
<DropdownMenuPrimitive.SubTrigger
|
|
ref={ref}
|
|
className={cn(
|
|
"flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
inset && "pl-8",
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
<ChevronRight className="ml-auto" />
|
|
</DropdownMenuPrimitive.SubTrigger>
|
|
))
|
|
DropdownMenuSubTrigger.displayName =
|
|
DropdownMenuPrimitive.SubTrigger.displayName
|
|
|
|
const DropdownMenuSubContent = React.forwardRef<
|
|
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
|
>(({ className, ...props }, ref) => (
|
|
<DropdownMenuPrimitive.SubContent
|
|
ref={ref}
|
|
className={cn(
|
|
"z-50 min-w-[8rem] overflow-hidden rounded-md 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
|
|
)}
|
|
{...props}
|
|
/>
|
|
))
|
|
DropdownMenuSubContent.displayName =
|
|
DropdownMenuPrimitive.SubContent.displayName
|
|
|
|
const DropdownMenuContent = React.forwardRef<
|
|
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
|
<DropdownMenuPrimitive.Portal>
|
|
<DropdownMenuPrimitive.Content
|
|
ref={ref}
|
|
sideOffset={sideOffset}
|
|
className={cn(
|
|
"z-50 min-w-[8rem] overflow-hidden rounded-md 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
|
|
)}
|
|
{...props}
|
|
/>
|
|
</DropdownMenuPrimitive.Portal>
|
|
))
|
|
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
|
|
|
const DropdownMenuItem = React.forwardRef<
|
|
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
|
inset?: boolean
|
|
}
|
|
>(({ className, inset, ...props }, ref) => (
|
|
<DropdownMenuPrimitive.Item
|
|
ref={ref}
|
|
className={cn(
|
|
"relative flex cursor-default select-none items-center gap-2 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 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
inset && "pl-8",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
))
|
|
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
|
|
|
const DropdownMenuCheckboxItem = React.forwardRef<
|
|
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
|
>(({ className, children, checked, ...props }, ref) => (
|
|
<DropdownMenuPrimitive.CheckboxItem
|
|
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
|
|
)}
|
|
checked={checked}
|
|
{...props}
|
|
>
|
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
<DropdownMenuPrimitive.ItemIndicator>
|
|
<Check className="h-4 w-4" />
|
|
</DropdownMenuPrimitive.ItemIndicator>
|
|
</span>
|
|
{children}
|
|
</DropdownMenuPrimitive.CheckboxItem>
|
|
))
|
|
DropdownMenuCheckboxItem.displayName =
|
|
DropdownMenuPrimitive.CheckboxItem.displayName
|
|
|
|
const DropdownMenuRadioItem = React.forwardRef<
|
|
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
|
>(({ className, children, ...props }, ref) => (
|
|
<DropdownMenuPrimitive.RadioItem
|
|
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
|
|
)}
|
|
{...props}
|
|
>
|
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
<DropdownMenuPrimitive.ItemIndicator>
|
|
<Circle className="h-2 w-2 fill-current" />
|
|
</DropdownMenuPrimitive.ItemIndicator>
|
|
</span>
|
|
{children}
|
|
</DropdownMenuPrimitive.RadioItem>
|
|
))
|
|
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
|
|
|
const DropdownMenuLabel = React.forwardRef<
|
|
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
|
inset?: boolean
|
|
}
|
|
>(({ className, inset, ...props }, ref) => (
|
|
<DropdownMenuPrimitive.Label
|
|
ref={ref}
|
|
className={cn(
|
|
"px-2 py-1.5 text-sm font-semibold",
|
|
inset && "pl-8",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
))
|
|
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
|
|
|
const DropdownMenuSeparator = React.forwardRef<
|
|
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
|
>(({ className, ...props }, ref) => (
|
|
<DropdownMenuPrimitive.Separator
|
|
ref={ref}
|
|
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
|
{...props}
|
|
/>
|
|
))
|
|
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
|
|
|
const DropdownMenuShortcut = ({
|
|
className,
|
|
...props
|
|
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
|
return (
|
|
<span
|
|
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
|
|
|
export {
|
|
DropdownMenu,
|
|
DropdownMenuTrigger,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuCheckboxItem,
|
|
DropdownMenuRadioItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuShortcut,
|
|
DropdownMenuGroup,
|
|
DropdownMenuPortal,
|
|
DropdownMenuSub,
|
|
DropdownMenuSubContent,
|
|
DropdownMenuSubTrigger,
|
|
DropdownMenuRadioGroup,
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/ui/form.tsx
|
|
|
|
```tsx
|
|
"use client"
|
|
|
|
import * as React from "react"
|
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
|
import { Slot } from "@radix-ui/react-slot"
|
|
import {
|
|
Controller,
|
|
FormProvider,
|
|
useFormContext,
|
|
type ControllerProps,
|
|
type FieldPath,
|
|
type FieldValues,
|
|
} from "react-hook-form"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
import { Label } from "@/components/ui/label"
|
|
|
|
const Form = FormProvider
|
|
|
|
type FormFieldContextValue<
|
|
TFieldValues extends FieldValues = FieldValues,
|
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
|
> = {
|
|
name: TName
|
|
}
|
|
|
|
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
|
{} as FormFieldContextValue
|
|
)
|
|
|
|
const FormField = <
|
|
TFieldValues extends FieldValues = FieldValues,
|
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
|
>({
|
|
...props
|
|
}: ControllerProps<TFieldValues, TName>) => {
|
|
return (
|
|
<FormFieldContext.Provider value={{ name: props.name }}>
|
|
<Controller {...props} />
|
|
</FormFieldContext.Provider>
|
|
)
|
|
}
|
|
|
|
const useFormField = () => {
|
|
const fieldContext = React.useContext(FormFieldContext)
|
|
const itemContext = React.useContext(FormItemContext)
|
|
const { getFieldState, formState } = useFormContext()
|
|
|
|
const fieldState = getFieldState(fieldContext.name, formState)
|
|
|
|
if (!fieldContext) {
|
|
throw new Error("useFormField should be used within <FormField>")
|
|
}
|
|
|
|
const { id } = itemContext
|
|
|
|
return {
|
|
id,
|
|
name: fieldContext.name,
|
|
formItemId: `${id}-form-item`,
|
|
formDescriptionId: `${id}-form-item-description`,
|
|
formMessageId: `${id}-form-item-message`,
|
|
...fieldState,
|
|
}
|
|
}
|
|
|
|
type FormItemContextValue = {
|
|
id: string
|
|
}
|
|
|
|
const FormItemContext = React.createContext<FormItemContextValue>(
|
|
{} as FormItemContextValue
|
|
)
|
|
|
|
const FormItem = React.forwardRef<
|
|
HTMLDivElement,
|
|
React.HTMLAttributes<HTMLDivElement>
|
|
>(({ className, ...props }, ref) => {
|
|
const id = React.useId()
|
|
|
|
return (
|
|
<FormItemContext.Provider value={{ id }}>
|
|
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
|
</FormItemContext.Provider>
|
|
)
|
|
})
|
|
FormItem.displayName = "FormItem"
|
|
|
|
const FormLabel = React.forwardRef<
|
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
|
>(({ className, ...props }, ref) => {
|
|
const { error, formItemId } = useFormField()
|
|
|
|
return (
|
|
<Label
|
|
ref={ref}
|
|
className={cn(error && "text-destructive", className)}
|
|
htmlFor={formItemId}
|
|
{...props}
|
|
/>
|
|
)
|
|
})
|
|
FormLabel.displayName = "FormLabel"
|
|
|
|
const FormControl = React.forwardRef<
|
|
React.ElementRef<typeof Slot>,
|
|
React.ComponentPropsWithoutRef<typeof Slot>
|
|
>(({ ...props }, ref) => {
|
|
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
|
|
|
return (
|
|
<Slot
|
|
ref={ref}
|
|
id={formItemId}
|
|
aria-describedby={
|
|
!error
|
|
? `${formDescriptionId}`
|
|
: `${formDescriptionId} ${formMessageId}`
|
|
}
|
|
aria-invalid={!!error}
|
|
{...props}
|
|
/>
|
|
)
|
|
})
|
|
FormControl.displayName = "FormControl"
|
|
|
|
const FormDescription = React.forwardRef<
|
|
HTMLParagraphElement,
|
|
React.HTMLAttributes<HTMLParagraphElement>
|
|
>(({ className, ...props }, ref) => {
|
|
const { formDescriptionId } = useFormField()
|
|
|
|
return (
|
|
<p
|
|
ref={ref}
|
|
id={formDescriptionId}
|
|
className={cn("text-sm text-muted-foreground", className)}
|
|
{...props}
|
|
/>
|
|
)
|
|
})
|
|
FormDescription.displayName = "FormDescription"
|
|
|
|
const FormMessage = React.forwardRef<
|
|
HTMLParagraphElement,
|
|
React.HTMLAttributes<HTMLParagraphElement>
|
|
>(({ className, children, ...props }, ref) => {
|
|
const { error, formMessageId } = useFormField()
|
|
const body = error ? String(error?.message ?? "") : children
|
|
|
|
if (!body) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<p
|
|
ref={ref}
|
|
id={formMessageId}
|
|
className={cn("text-sm font-medium text-destructive", className)}
|
|
{...props}
|
|
>
|
|
{body}
|
|
</p>
|
|
)
|
|
})
|
|
FormMessage.displayName = "FormMessage"
|
|
|
|
export {
|
|
useFormField,
|
|
Form,
|
|
FormItem,
|
|
FormLabel,
|
|
FormControl,
|
|
FormDescription,
|
|
FormMessage,
|
|
FormField,
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/ui/input.tsx
|
|
|
|
```tsx
|
|
import * as React from "react"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
|
({ className, type, ...props }, ref) => {
|
|
return (
|
|
<input
|
|
type={type}
|
|
className={cn(
|
|
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground 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 md:text-sm",
|
|
className
|
|
)}
|
|
ref={ref}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
)
|
|
Input.displayName = "Input"
|
|
|
|
export { Input }
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/ui/label.tsx
|
|
|
|
```tsx
|
|
"use client"
|
|
|
|
import * as React from "react"
|
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
const labelVariants = cva(
|
|
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
)
|
|
|
|
const Label = React.forwardRef<
|
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
|
VariantProps<typeof labelVariants>
|
|
>(({ className, ...props }, ref) => (
|
|
<LabelPrimitive.Root
|
|
ref={ref}
|
|
className={cn(labelVariants(), className)}
|
|
{...props}
|
|
/>
|
|
))
|
|
Label.displayName = LabelPrimitive.Root.displayName
|
|
|
|
export { Label }
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/ui/menubar.tsx
|
|
|
|
```tsx
|
|
"use client"
|
|
|
|
import * as React from "react"
|
|
import * as MenubarPrimitive from "@radix-ui/react-menubar"
|
|
import { Check, ChevronRight, Circle } from "lucide-react"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
function MenubarMenu({
|
|
...props
|
|
}: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
|
|
return <MenubarPrimitive.Menu {...props} />
|
|
}
|
|
|
|
function MenubarGroup({
|
|
...props
|
|
}: React.ComponentProps<typeof MenubarPrimitive.Group>) {
|
|
return <MenubarPrimitive.Group {...props} />
|
|
}
|
|
|
|
function MenubarPortal({
|
|
...props
|
|
}: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
|
|
return <MenubarPrimitive.Portal {...props} />
|
|
}
|
|
|
|
function MenubarRadioGroup({
|
|
...props
|
|
}: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
|
|
return <MenubarPrimitive.RadioGroup {...props} />
|
|
}
|
|
|
|
function MenubarSub({
|
|
...props
|
|
}: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
|
|
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />
|
|
}
|
|
|
|
const Menubar = React.forwardRef<
|
|
React.ElementRef<typeof MenubarPrimitive.Root>,
|
|
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
|
|
>(({ className, ...props }, ref) => (
|
|
<MenubarPrimitive.Root
|
|
ref={ref}
|
|
className={cn(
|
|
"flex h-10 items-center space-x-1 rounded-md border bg-background p-1",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
))
|
|
Menubar.displayName = MenubarPrimitive.Root.displayName
|
|
|
|
const MenubarTrigger = React.forwardRef<
|
|
React.ElementRef<typeof MenubarPrimitive.Trigger>,
|
|
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
|
|
>(({ className, ...props }, ref) => (
|
|
<MenubarPrimitive.Trigger
|
|
ref={ref}
|
|
className={cn(
|
|
"flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
))
|
|
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName
|
|
|
|
const MenubarSubTrigger = React.forwardRef<
|
|
React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
|
|
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
|
|
inset?: boolean
|
|
}
|
|
>(({ className, inset, children, ...props }, ref) => (
|
|
<MenubarPrimitive.SubTrigger
|
|
ref={ref}
|
|
className={cn(
|
|
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
|
inset && "pl-8",
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
<ChevronRight className="ml-auto h-4 w-4" />
|
|
</MenubarPrimitive.SubTrigger>
|
|
))
|
|
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName
|
|
|
|
const MenubarSubContent = React.forwardRef<
|
|
React.ElementRef<typeof MenubarPrimitive.SubContent>,
|
|
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
|
|
>(({ className, ...props }, ref) => (
|
|
<MenubarPrimitive.SubContent
|
|
ref={ref}
|
|
className={cn(
|
|
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground 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
|
|
)}
|
|
{...props}
|
|
/>
|
|
))
|
|
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName
|
|
|
|
const MenubarContent = React.forwardRef<
|
|
React.ElementRef<typeof MenubarPrimitive.Content>,
|
|
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
|
|
>(
|
|
(
|
|
{ className, align = "start", alignOffset = -4, sideOffset = 8, ...props },
|
|
ref
|
|
) => (
|
|
<MenubarPrimitive.Portal>
|
|
<MenubarPrimitive.Content
|
|
ref={ref}
|
|
align={align}
|
|
alignOffset={alignOffset}
|
|
sideOffset={sideOffset}
|
|
className={cn(
|
|
"z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in 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
|
|
)}
|
|
{...props}
|
|
/>
|
|
</MenubarPrimitive.Portal>
|
|
)
|
|
)
|
|
MenubarContent.displayName = MenubarPrimitive.Content.displayName
|
|
|
|
const MenubarItem = React.forwardRef<
|
|
React.ElementRef<typeof MenubarPrimitive.Item>,
|
|
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
|
|
inset?: boolean
|
|
}
|
|
>(({ className, inset, ...props }, ref) => (
|
|
<MenubarPrimitive.Item
|
|
ref={ref}
|
|
className={cn(
|
|
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
inset && "pl-8",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
))
|
|
MenubarItem.displayName = MenubarPrimitive.Item.displayName
|
|
|
|
const MenubarCheckboxItem = React.forwardRef<
|
|
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
|
|
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
|
|
>(({ className, children, checked, ...props }, ref) => (
|
|
<MenubarPrimitive.CheckboxItem
|
|
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 focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
className
|
|
)}
|
|
checked={checked}
|
|
{...props}
|
|
>
|
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
<MenubarPrimitive.ItemIndicator>
|
|
<Check className="h-4 w-4" />
|
|
</MenubarPrimitive.ItemIndicator>
|
|
</span>
|
|
{children}
|
|
</MenubarPrimitive.CheckboxItem>
|
|
))
|
|
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName
|
|
|
|
const MenubarRadioItem = React.forwardRef<
|
|
React.ElementRef<typeof MenubarPrimitive.RadioItem>,
|
|
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
|
|
>(({ className, children, ...props }, ref) => (
|
|
<MenubarPrimitive.RadioItem
|
|
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 focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
<MenubarPrimitive.ItemIndicator>
|
|
<Circle className="h-2 w-2 fill-current" />
|
|
</MenubarPrimitive.ItemIndicator>
|
|
</span>
|
|
{children}
|
|
</MenubarPrimitive.RadioItem>
|
|
))
|
|
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName
|
|
|
|
const MenubarLabel = React.forwardRef<
|
|
React.ElementRef<typeof MenubarPrimitive.Label>,
|
|
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
|
|
inset?: boolean
|
|
}
|
|
>(({ className, inset, ...props }, ref) => (
|
|
<MenubarPrimitive.Label
|
|
ref={ref}
|
|
className={cn(
|
|
"px-2 py-1.5 text-sm font-semibold",
|
|
inset && "pl-8",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
))
|
|
MenubarLabel.displayName = MenubarPrimitive.Label.displayName
|
|
|
|
const MenubarSeparator = React.forwardRef<
|
|
React.ElementRef<typeof MenubarPrimitive.Separator>,
|
|
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
|
|
>(({ className, ...props }, ref) => (
|
|
<MenubarPrimitive.Separator
|
|
ref={ref}
|
|
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
|
{...props}
|
|
/>
|
|
))
|
|
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName
|
|
|
|
const MenubarShortcut = ({
|
|
className,
|
|
...props
|
|
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
|
return (
|
|
<span
|
|
className={cn(
|
|
"ml-auto text-xs tracking-widest text-muted-foreground",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
MenubarShortcut.displayname = "MenubarShortcut"
|
|
|
|
export {
|
|
Menubar,
|
|
MenubarMenu,
|
|
MenubarTrigger,
|
|
MenubarContent,
|
|
MenubarItem,
|
|
MenubarSeparator,
|
|
MenubarLabel,
|
|
MenubarCheckboxItem,
|
|
MenubarRadioGroup,
|
|
MenubarRadioItem,
|
|
MenubarPortal,
|
|
MenubarSubContent,
|
|
MenubarSubTrigger,
|
|
MenubarGroup,
|
|
MenubarSub,
|
|
MenubarShortcut,
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/ui/popover.tsx
|
|
|
|
```tsx
|
|
"use client"
|
|
|
|
import * as React from "react"
|
|
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
const Popover = PopoverPrimitive.Root
|
|
|
|
const PopoverTrigger = PopoverPrimitive.Trigger
|
|
|
|
const PopoverContent = React.forwardRef<
|
|
React.ElementRef<typeof PopoverPrimitive.Content>,
|
|
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
|
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
|
<PopoverPrimitive.Portal>
|
|
<PopoverPrimitive.Content
|
|
ref={ref}
|
|
align={align}
|
|
sideOffset={sideOffset}
|
|
className={cn(
|
|
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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
|
|
)}
|
|
{...props}
|
|
/>
|
|
</PopoverPrimitive.Portal>
|
|
))
|
|
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
|
|
|
export { Popover, PopoverTrigger, PopoverContent }
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/ui/progress.tsx
|
|
|
|
```tsx
|
|
"use client"
|
|
|
|
import * as React from "react"
|
|
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
const Progress = React.forwardRef<
|
|
React.ElementRef<typeof ProgressPrimitive.Root>,
|
|
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
|
>(({ className, value, ...props }, ref) => (
|
|
<ProgressPrimitive.Root
|
|
ref={ref}
|
|
className={cn(
|
|
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
<ProgressPrimitive.Indicator
|
|
className="h-full w-full flex-1 bg-primary transition-all"
|
|
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
|
/>
|
|
</ProgressPrimitive.Root>
|
|
))
|
|
Progress.displayName = ProgressPrimitive.Root.displayName
|
|
|
|
export { Progress }
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/ui/radio-group.tsx
|
|
|
|
```tsx
|
|
"use client"
|
|
|
|
import * as React from "react"
|
|
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
|
import { Circle } from "lucide-react"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
const RadioGroup = React.forwardRef<
|
|
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
|
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
|
>(({ className, ...props }, ref) => {
|
|
return (
|
|
<RadioGroupPrimitive.Root
|
|
className={cn("grid gap-2", className)}
|
|
{...props}
|
|
ref={ref}
|
|
/>
|
|
)
|
|
})
|
|
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
|
|
|
|
const RadioGroupItem = React.forwardRef<
|
|
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
|
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
|
>(({ className, ...props }, ref) => {
|
|
return (
|
|
<RadioGroupPrimitive.Item
|
|
ref={ref}
|
|
className={cn(
|
|
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
|
<Circle className="h-2.5 w-2.5 fill-current text-current" />
|
|
</RadioGroupPrimitive.Indicator>
|
|
</RadioGroupPrimitive.Item>
|
|
)
|
|
})
|
|
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
|
|
|
|
export { RadioGroup, RadioGroupItem }
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/ui/resizable.tsx
|
|
|
|
```tsx
|
|
"use client";
|
|
|
|
import { cn } from "@/lib/utils";
|
|
import { GripVertical } from "lucide-react";
|
|
import * as React from "react";
|
|
|
|
type Direction = "horizontal" | "vertical";
|
|
|
|
type ResizablePanelGroupContextType = {
|
|
direction: Direction;
|
|
sizes: number[];
|
|
setSizes: (sizes: number[]) => void;
|
|
minSizes: number[];
|
|
maxSizes: number[];
|
|
};
|
|
|
|
const ResizablePanelGroupContext = React.createContext<ResizablePanelGroupContextType | null>(null);
|
|
|
|
const useResizablePanelGroup = () => {
|
|
const context = React.useContext(ResizablePanelGroupContext);
|
|
if (!context) {
|
|
throw new Error("useResizablePanelGroup must be used within a ResizablePanelGroup");
|
|
}
|
|
return context;
|
|
};
|
|
|
|
export const ResizablePanelGroup = React.forwardRef<
|
|
HTMLDivElement,
|
|
React.HTMLAttributes<HTMLDivElement> & {
|
|
direction: Direction;
|
|
}
|
|
>(({ direction, children, className, ...props }, ref) => {
|
|
const [sizes, setSizes] = React.useState<number[]>([]);
|
|
const [minSizes, setMinSizes] = React.useState<number[]>([]);
|
|
const [maxSizes, setMaxSizes] = React.useState<number[]>([]);
|
|
|
|
React.useEffect(() => {
|
|
const panels = React.Children.toArray(children).filter(
|
|
(child) => React.isValidElement(child) && child.type === ResizablePanel
|
|
);
|
|
const panelCount = panels.length;
|
|
|
|
if (panelCount > 0) {
|
|
const initialSizes = panels.map(
|
|
(child) => (React.isValidElement(child) && child.props.defaultSize ? child.props.defaultSize : 100 / panelCount)
|
|
);
|
|
|
|
const sum = initialSizes.reduce((a, b) => a + b, 0);
|
|
|
|
if (sum > 100.1 || sum < 99.9) {
|
|
const factor = 100 / sum;
|
|
setSizes(initialSizes.map(s => s * factor));
|
|
} else {
|
|
setSizes(initialSizes);
|
|
}
|
|
|
|
setMinSizes(panels.map(
|
|
(child) => (React.isValidElement(child) ? child.props.minSize || 0 : 0)
|
|
));
|
|
setMaxSizes(panels.map(
|
|
(child) => (React.isValidElement(child) ? child.props.maxSize || 100 : 100)
|
|
));
|
|
}
|
|
}, [children]);
|
|
|
|
let panelIndex = 0;
|
|
const childrenWithInjectedProps = React.Children.map(children, (child) => {
|
|
if (React.isValidElement(child)) {
|
|
if (child.type === ResizablePanel) {
|
|
const index = panelIndex;
|
|
panelIndex++;
|
|
return React.cloneElement(child, { panelIndex: index });
|
|
}
|
|
if (child.type === ResizableHandle) {
|
|
return React.cloneElement(child, { handleIndex: panelIndex - 1 });
|
|
}
|
|
}
|
|
return child;
|
|
});
|
|
|
|
return (
|
|
<ResizablePanelGroupContext.Provider value={{ direction, sizes, setSizes, minSizes, maxSizes }}>
|
|
<div
|
|
ref={ref}
|
|
className={cn(
|
|
"flex w-full h-full",
|
|
direction === "vertical" && "flex-col",
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
{childrenWithInjectedProps}
|
|
</div>
|
|
</ResizablePanelGroupContext.Provider>
|
|
);
|
|
});
|
|
ResizablePanelGroup.displayName = "ResizablePanelGroup";
|
|
|
|
export const ResizablePanel = React.forwardRef<
|
|
HTMLDivElement,
|
|
React.HTMLAttributes<HTMLDivElement> & {
|
|
defaultSize?: number;
|
|
minSize?: number;
|
|
maxSize?: number;
|
|
panelIndex?: number; // Injected prop
|
|
}
|
|
>(({ className, children, style, panelIndex, ...props }, ref) => {
|
|
const { direction, sizes } = useResizablePanelGroup();
|
|
const size = (panelIndex !== undefined && sizes[panelIndex]) ? sizes[panelIndex] : undefined;
|
|
|
|
if (size === undefined) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={cn("overflow-hidden", className)}
|
|
style={{
|
|
...style,
|
|
flexGrow: size,
|
|
flexShrink: 1,
|
|
flexBasis: 0,
|
|
}}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
});
|
|
ResizablePanel.displayName = "ResizablePanel";
|
|
|
|
export const ResizableHandle = React.forwardRef<
|
|
HTMLDivElement,
|
|
React.HTMLAttributes<HTMLDivElement> & {
|
|
withHandle?: boolean;
|
|
handleIndex?: number; // Injected prop
|
|
}
|
|
>(({ className, withHandle, handleIndex, ...props }, ref) => {
|
|
const { direction, sizes, setSizes, minSizes, maxSizes } = useResizablePanelGroup();
|
|
const [isDragging, setIsDragging] = React.useState(false);
|
|
const handleRef = (ref as React.RefObject<HTMLDivElement>) || React.createRef<HTMLDivElement>();
|
|
|
|
const onDrag = React.useCallback(
|
|
(e: MouseEvent | TouchEvent) => {
|
|
if (!isDragging || handleIndex === undefined) return;
|
|
|
|
const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
|
|
const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
|
|
|
|
const groupElement = handleRef.current?.parentElement;
|
|
if (!groupElement) return;
|
|
|
|
const { left, top, width, height } = groupElement.getBoundingClientRect();
|
|
|
|
const cursorPosition = direction === 'horizontal' ? clientX - left : clientY - top;
|
|
const groupSize = direction === 'horizontal' ? width : height;
|
|
const cursorPercentage = (cursorPosition / groupSize) * 100;
|
|
|
|
const prevPanelsSize = sizes.slice(0, handleIndex + 1).reduce((sum, s) => sum + s, 0);
|
|
|
|
const delta = cursorPercentage - prevPanelsSize;
|
|
|
|
let prevPanelSize = sizes[handleIndex];
|
|
let nextPanelSize = sizes[handleIndex + 1];
|
|
|
|
let newPrevSize = prevPanelSize + delta;
|
|
let newNextSize = nextPanelSize - delta;
|
|
|
|
const minPrev = minSizes[handleIndex];
|
|
const maxPrev = maxSizes[handleIndex];
|
|
const minNext = minSizes[handleIndex + 1];
|
|
const maxNext = maxSizes[handleIndex + 1];
|
|
|
|
if (newPrevSize < minPrev) {
|
|
newPrevSize = minPrev;
|
|
newNextSize = prevPanelSize + nextPanelSize - newPrevSize;
|
|
} else if (newNextSize < minNext) {
|
|
newNextSize = minNext;
|
|
newPrevSize = prevPanelSize + nextPanelSize - newNextSize;
|
|
} else if (newPrevSize > maxPrev) {
|
|
newPrevSize = maxPrev;
|
|
newNextSize = prevPanelSize + nextPanelSize - newPrevSize;
|
|
} else if (newNextSize > maxNext) {
|
|
newNextSize = maxNext;
|
|
newPrevSize = prevPanelSize + nextPanelSize - newNextSize;
|
|
}
|
|
|
|
const newSizes = [...sizes];
|
|
newSizes[handleIndex] = newPrevSize;
|
|
newSizes[handleIndex + 1] = newNextSize;
|
|
|
|
setSizes(newSizes);
|
|
},
|
|
[isDragging, direction, sizes, handleIndex, setSizes, minSizes, maxSizes, handleRef]
|
|
);
|
|
|
|
React.useEffect(() => {
|
|
const stopDragging = () => setIsDragging(false);
|
|
|
|
const ownerDocument = handleRef.current?.ownerDocument || document;
|
|
|
|
if (isDragging) {
|
|
ownerDocument.addEventListener("mousemove", onDrag);
|
|
ownerDocument.addEventListener("touchmove", onDrag);
|
|
ownerDocument.addEventListener("mouseup", stopDragging);
|
|
ownerDocument.addEventListener("touchend", stopDragging);
|
|
ownerDocument.body.style.cursor = direction === 'horizontal' ? 'col-resize' : 'row-resize';
|
|
ownerDocument.body.style.userSelect = 'none';
|
|
}
|
|
|
|
return () => {
|
|
ownerDocument.removeEventListener("mousemove", onDrag);
|
|
ownerDocument.removeEventListener("touchmove", onDrag);
|
|
ownerDocument.removeEventListener("mouseup", stopDragging);
|
|
ownerDocument.removeEventListener("touchend", stopDragging);
|
|
ownerDocument.body.style.cursor = '';
|
|
ownerDocument.body.style.userSelect = '';
|
|
};
|
|
}, [isDragging, onDrag, direction, handleRef]);
|
|
|
|
return (
|
|
<div
|
|
ref={handleRef}
|
|
onMouseDown={(e) => { e.preventDefault(); setIsDragging(true); }}
|
|
onTouchStart={(e) => { e.preventDefault(); setIsDragging(true); }}
|
|
className={cn(
|
|
"relative flex-shrink-0 items-center justify-center bg-transparent transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
direction === "horizontal"
|
|
? "w-2 mx-[-1px] cursor-col-resize"
|
|
: "h-2 my-[-1px] cursor-row-resize",
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
<div
|
|
className={cn(
|
|
"z-10 bg-border transition-colors",
|
|
direction === "horizontal" ? "h-full w-[1px]" : "w-full h-[1px]",
|
|
isDragging && "bg-primary"
|
|
)}
|
|
/>
|
|
{withHandle && (
|
|
<div className="z-20 absolute flex items-center justify-center rounded-full border bg-card p-1 shadow-sm">
|
|
<GripVertical
|
|
className={cn(
|
|
"h-2.5 w-2.5 text-muted-foreground",
|
|
direction === "vertical" && "rotate-90"
|
|
)}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
});
|
|
ResizableHandle.displayName = "ResizableHandle";
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/ui/scroll-area.tsx
|
|
|
|
```tsx
|
|
"use client"
|
|
|
|
import * as React from "react"
|
|
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
const ScrollArea = React.forwardRef<
|
|
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
|
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
|
>(({ className, children, ...props }, ref) => (
|
|
<ScrollAreaPrimitive.Root
|
|
ref={ref}
|
|
className={cn("relative overflow-hidden", className)}
|
|
{...props}
|
|
>
|
|
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
|
{children}
|
|
</ScrollAreaPrimitive.Viewport>
|
|
<ScrollBar />
|
|
<ScrollAreaPrimitive.Corner />
|
|
</ScrollAreaPrimitive.Root>
|
|
))
|
|
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
|
|
|
const ScrollBar = React.forwardRef<
|
|
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
|
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
|
>(({ className, orientation = "vertical", ...props }, ref) => (
|
|
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
|
ref={ref}
|
|
orientation={orientation}
|
|
className={cn(
|
|
"flex touch-none select-none transition-colors",
|
|
orientation === "vertical" &&
|
|
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
|
orientation === "horizontal" &&
|
|
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
|
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
|
))
|
|
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
|
|
|
export { ScrollArea, ScrollBar }
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/ui/select.tsx
|
|
|
|
```tsx
|
|
"use client"
|
|
|
|
import * as React from "react"
|
|
import * as SelectPrimitive from "@radix-ui/react-select"
|
|
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
const Select = SelectPrimitive.Root
|
|
|
|
const SelectGroup = SelectPrimitive.Group
|
|
|
|
const SelectValue = SelectPrimitive.Value
|
|
|
|
const SelectTrigger = React.forwardRef<
|
|
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
|
>(({ className, children, ...props }, ref) => (
|
|
<SelectPrimitive.Trigger
|
|
ref={ref}
|
|
className={cn(
|
|
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
<SelectPrimitive.Icon asChild>
|
|
<ChevronDown className="h-4 w-4 opacity-50" />
|
|
</SelectPrimitive.Icon>
|
|
</SelectPrimitive.Trigger>
|
|
))
|
|
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
|
|
|
const SelectScrollUpButton = React.forwardRef<
|
|
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
|
>(({ className, ...props }, ref) => (
|
|
<SelectPrimitive.ScrollUpButton
|
|
ref={ref}
|
|
className={cn(
|
|
"flex cursor-default items-center justify-center py-1",
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
<ChevronUp className="h-4 w-4" />
|
|
</SelectPrimitive.ScrollUpButton>
|
|
))
|
|
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
|
|
|
const SelectScrollDownButton = React.forwardRef<
|
|
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
|
>(({ className, ...props }, ref) => (
|
|
<SelectPrimitive.ScrollDownButton
|
|
ref={ref}
|
|
className={cn(
|
|
"flex cursor-default items-center justify-center py-1",
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
<ChevronDown className="h-4 w-4" />
|
|
</SelectPrimitive.ScrollDownButton>
|
|
))
|
|
SelectScrollDownButton.displayName =
|
|
SelectPrimitive.ScrollDownButton.displayName
|
|
|
|
const SelectContent = React.forwardRef<
|
|
React.ElementRef<typeof SelectPrimitive.Content>,
|
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
|
>(({ className, children, position = "popper", ...props }, ref) => (
|
|
<SelectPrimitive.Portal>
|
|
<SelectPrimitive.Content
|
|
ref={ref}
|
|
className={cn(
|
|
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover 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",
|
|
position === "popper" &&
|
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
|
className
|
|
)}
|
|
position={position}
|
|
{...props}
|
|
>
|
|
<SelectScrollUpButton />
|
|
<SelectPrimitive.Viewport
|
|
className={cn(
|
|
"p-1",
|
|
position === "popper" &&
|
|
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
|
)}
|
|
>
|
|
{children}
|
|
</SelectPrimitive.Viewport>
|
|
<SelectScrollDownButton />
|
|
</SelectPrimitive.Content>
|
|
</SelectPrimitive.Portal>
|
|
))
|
|
SelectContent.displayName = SelectPrimitive.Content.displayName
|
|
|
|
const SelectLabel = React.forwardRef<
|
|
React.ElementRef<typeof SelectPrimitive.Label>,
|
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
|
>(({ className, ...props }, ref) => (
|
|
<SelectPrimitive.Label
|
|
ref={ref}
|
|
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
|
{...props}
|
|
/>
|
|
))
|
|
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
|
|
|
const SelectItem = React.forwardRef<
|
|
React.ElementRef<typeof SelectPrimitive.Item>,
|
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
|
>(({ className, children, ...props }, ref) => (
|
|
<SelectPrimitive.Item
|
|
ref={ref}
|
|
className={cn(
|
|
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
<SelectPrimitive.ItemIndicator>
|
|
<Check className="h-4 w-4" />
|
|
</SelectPrimitive.ItemIndicator>
|
|
</span>
|
|
|
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
|
</SelectPrimitive.Item>
|
|
))
|
|
SelectItem.displayName = SelectPrimitive.Item.displayName
|
|
|
|
const SelectSeparator = React.forwardRef<
|
|
React.ElementRef<typeof SelectPrimitive.Separator>,
|
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
|
>(({ className, ...props }, ref) => (
|
|
<SelectPrimitive.Separator
|
|
ref={ref}
|
|
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
|
{...props}
|
|
/>
|
|
))
|
|
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
|
|
|
export {
|
|
Select,
|
|
SelectGroup,
|
|
SelectValue,
|
|
SelectTrigger,
|
|
SelectContent,
|
|
SelectLabel,
|
|
SelectItem,
|
|
SelectSeparator,
|
|
SelectScrollUpButton,
|
|
SelectScrollDownButton,
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/ui/separator.tsx
|
|
|
|
```tsx
|
|
"use client"
|
|
|
|
import * as React from "react"
|
|
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
const Separator = React.forwardRef<
|
|
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
|
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
|
>(
|
|
(
|
|
{ className, orientation = "horizontal", decorative = true, ...props },
|
|
ref
|
|
) => (
|
|
<SeparatorPrimitive.Root
|
|
ref={ref}
|
|
decorative={decorative}
|
|
orientation={orientation}
|
|
className={cn(
|
|
"shrink-0 bg-border",
|
|
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
)
|
|
)
|
|
Separator.displayName = SeparatorPrimitive.Root.displayName
|
|
|
|
export { Separator }
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/ui/sheet.tsx
|
|
|
|
```tsx
|
|
"use client"
|
|
|
|
import * as React from "react"
|
|
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
import { X } from "lucide-react"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
const Sheet = SheetPrimitive.Root
|
|
|
|
const SheetTrigger = SheetPrimitive.Trigger
|
|
|
|
const SheetClose = SheetPrimitive.Close
|
|
|
|
const SheetPortal = SheetPrimitive.Portal
|
|
|
|
const SheetOverlay = React.forwardRef<
|
|
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
|
>(({ className, ...props }, ref) => (
|
|
<SheetPrimitive.Overlay
|
|
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
|
|
)}
|
|
{...props}
|
|
ref={ref}
|
|
/>
|
|
))
|
|
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
|
|
|
|
const sheetVariants = cva(
|
|
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
|
{
|
|
variants: {
|
|
side: {
|
|
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
|
bottom:
|
|
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
|
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
|
right:
|
|
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
|
},
|
|
},
|
|
defaultVariants: {
|
|
side: "right",
|
|
},
|
|
}
|
|
)
|
|
|
|
interface SheetContentProps
|
|
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
|
VariantProps<typeof sheetVariants> {}
|
|
|
|
const SheetContent = React.forwardRef<
|
|
React.ElementRef<typeof SheetPrimitive.Content>,
|
|
SheetContentProps
|
|
>(({ side = "right", className, children, ...props }, ref) => (
|
|
<SheetPortal>
|
|
<SheetOverlay />
|
|
<SheetPrimitive.Content
|
|
ref={ref}
|
|
className={cn(sheetVariants({ side }), className)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
|
<X className="h-4 w-4" />
|
|
<span className="sr-only">Close</span>
|
|
</SheetPrimitive.Close>
|
|
</SheetPrimitive.Content>
|
|
</SheetPortal>
|
|
))
|
|
SheetContent.displayName = SheetPrimitive.Content.displayName
|
|
|
|
const SheetHeader = ({
|
|
className,
|
|
...props
|
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
<div
|
|
className={cn(
|
|
"flex flex-col space-y-2 text-center sm:text-left",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
)
|
|
SheetHeader.displayName = "SheetHeader"
|
|
|
|
const SheetFooter = ({
|
|
className,
|
|
...props
|
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
<div
|
|
className={cn(
|
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
)
|
|
SheetFooter.displayName = "SheetFooter"
|
|
|
|
const SheetTitle = React.forwardRef<
|
|
React.ElementRef<typeof SheetPrimitive.Title>,
|
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
|
>(({ className, ...props }, ref) => (
|
|
<SheetPrimitive.Title
|
|
ref={ref}
|
|
className={cn("text-lg font-semibold text-foreground", className)}
|
|
{...props}
|
|
/>
|
|
))
|
|
SheetTitle.displayName = SheetPrimitive.Title.displayName
|
|
|
|
const SheetDescription = React.forwardRef<
|
|
React.ElementRef<typeof SheetPrimitive.Description>,
|
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
|
>(({ className, ...props }, ref) => (
|
|
<SheetPrimitive.Description
|
|
ref={ref}
|
|
className={cn("text-sm text-muted-foreground", className)}
|
|
{...props}
|
|
/>
|
|
))
|
|
SheetDescription.displayName = SheetPrimitive.Description.displayName
|
|
|
|
export {
|
|
Sheet,
|
|
SheetPortal,
|
|
SheetOverlay,
|
|
SheetTrigger,
|
|
SheetClose,
|
|
SheetContent,
|
|
SheetHeader,
|
|
SheetFooter,
|
|
SheetTitle,
|
|
SheetDescription,
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/ui/skeleton.tsx
|
|
|
|
```tsx
|
|
import { cn } from "@/lib/utils"
|
|
|
|
function Skeleton({
|
|
className,
|
|
...props
|
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
return (
|
|
<div
|
|
className={cn("animate-pulse rounded-md bg-muted", className)}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
export { Skeleton }
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/ui/slider.tsx
|
|
|
|
```tsx
|
|
"use client"
|
|
|
|
import * as React from "react"
|
|
import * as SliderPrimitive from "@radix-ui/react-slider"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
const Slider = React.forwardRef<
|
|
React.ElementRef<typeof SliderPrimitive.Root>,
|
|
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
|
>(({ className, ...props }, ref) => (
|
|
<SliderPrimitive.Root
|
|
ref={ref}
|
|
className={cn(
|
|
"relative flex w-full touch-none select-none items-center",
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
|
|
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
|
</SliderPrimitive.Track>
|
|
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
|
|
</SliderPrimitive.Root>
|
|
))
|
|
Slider.displayName = SliderPrimitive.Root.displayName
|
|
|
|
export { Slider }
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/ui/stepper.tsx
|
|
|
|
```tsx
|
|
"use client";
|
|
|
|
import * as React from "react";
|
|
import { cn } from "@/lib/utils";
|
|
import { Check } from "lucide-react";
|
|
|
|
interface Step {
|
|
label: string;
|
|
}
|
|
|
|
interface StepperProps {
|
|
steps: Step[];
|
|
currentStep: number;
|
|
}
|
|
|
|
export function Stepper({ steps, currentStep }: StepperProps) {
|
|
return (
|
|
<div className="flex w-full items-center justify-center">
|
|
{steps.map((step, index) => {
|
|
const stepNumber = index + 1;
|
|
const isActive = stepNumber === currentStep;
|
|
const isCompleted = stepNumber < currentStep;
|
|
|
|
return (
|
|
<React.Fragment key={index}>
|
|
<div className="flex flex-col items-center">
|
|
<div
|
|
className={cn(
|
|
"flex h-8 w-8 items-center justify-center rounded-full border-2 text-sm font-semibold",
|
|
isActive
|
|
? "border-primary bg-primary text-primary-foreground"
|
|
: isCompleted
|
|
? "border-primary bg-primary text-primary-foreground"
|
|
: "border-border bg-card"
|
|
)}
|
|
>
|
|
{isCompleted ? <Check className="h-5 w-5" /> : stepNumber}
|
|
</div>
|
|
<p
|
|
className={cn(
|
|
"mt-2 w-24 text-center text-xs",
|
|
isActive ? "font-semibold text-primary" : "text-muted-foreground",
|
|
isCompleted ? "font-semibold" : ""
|
|
)}
|
|
>
|
|
{step.label}
|
|
</p>
|
|
</div>
|
|
{index < steps.length - 1 && (
|
|
<div className="mb-6 h-px w-full flex-1 bg-border" />
|
|
)}
|
|
</React.Fragment>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/ui/switch.tsx
|
|
|
|
```tsx
|
|
"use client"
|
|
|
|
import * as React from "react"
|
|
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
const Switch = React.forwardRef<
|
|
React.ElementRef<typeof SwitchPrimitives.Root>,
|
|
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
|
>(({ className, ...props }, ref) => (
|
|
<SwitchPrimitives.Root
|
|
className={cn(
|
|
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent 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
|
|
)}
|
|
{...props}
|
|
ref={ref}
|
|
>
|
|
<SwitchPrimitives.Thumb
|
|
className={cn(
|
|
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
|
|
)}
|
|
/>
|
|
</SwitchPrimitives.Root>
|
|
))
|
|
Switch.displayName = SwitchPrimitives.Root.displayName
|
|
|
|
export { Switch }
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/ui/table.tsx
|
|
|
|
```tsx
|
|
import * as React from "react"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
const Table = React.forwardRef<
|
|
HTMLTableElement,
|
|
React.HTMLAttributes<HTMLTableElement>
|
|
>(({ className, ...props }, ref) => (
|
|
<div className="relative w-full overflow-auto">
|
|
<table
|
|
ref={ref}
|
|
className={cn("w-full caption-bottom text-sm", className)}
|
|
{...props}
|
|
/>
|
|
</div>
|
|
))
|
|
Table.displayName = "Table"
|
|
|
|
const TableHeader = React.forwardRef<
|
|
HTMLTableSectionElement,
|
|
React.HTMLAttributes<HTMLTableSectionElement>
|
|
>(({ className, ...props }, ref) => (
|
|
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
|
))
|
|
TableHeader.displayName = "TableHeader"
|
|
|
|
const TableBody = React.forwardRef<
|
|
HTMLTableSectionElement,
|
|
React.HTMLAttributes<HTMLTableSectionElement>
|
|
>(({ className, ...props }, ref) => (
|
|
<tbody
|
|
ref={ref}
|
|
className={cn("[&_tr:last-child]:border-0", className)}
|
|
{...props}
|
|
/>
|
|
))
|
|
TableBody.displayName = "TableBody"
|
|
|
|
const TableFooter = React.forwardRef<
|
|
HTMLTableSectionElement,
|
|
React.HTMLAttributes<HTMLTableSectionElement>
|
|
>(({ className, ...props }, ref) => (
|
|
<tfoot
|
|
ref={ref}
|
|
className={cn(
|
|
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
))
|
|
TableFooter.displayName = "TableFooter"
|
|
|
|
const TableRow = React.forwardRef<
|
|
HTMLTableRowElement,
|
|
React.HTMLAttributes<HTMLTableRowElement>
|
|
>(({ className, ...props }, ref) => (
|
|
<tr
|
|
ref={ref}
|
|
className={cn(
|
|
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
))
|
|
TableRow.displayName = "TableRow"
|
|
|
|
const TableHead = React.forwardRef<
|
|
HTMLTableCellElement,
|
|
React.ThHTMLAttributes<HTMLTableCellElement>
|
|
>(({ className, ...props }, ref) => (
|
|
<th
|
|
ref={ref}
|
|
className={cn(
|
|
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
))
|
|
TableHead.displayName = "TableHead"
|
|
|
|
const TableCell = React.forwardRef<
|
|
HTMLTableCellElement,
|
|
React.TdHTMLAttributes<HTMLTableCellElement>
|
|
>(({ className, ...props }, ref) => (
|
|
<td
|
|
ref={ref}
|
|
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
|
|
{...props}
|
|
/>
|
|
))
|
|
TableCell.displayName = "TableCell"
|
|
|
|
const TableCaption = React.forwardRef<
|
|
HTMLTableCaptionElement,
|
|
React.HTMLAttributes<HTMLTableCaptionElement>
|
|
>(({ className, ...props }, ref) => (
|
|
<caption
|
|
ref={ref}
|
|
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
|
{...props}
|
|
/>
|
|
))
|
|
TableCaption.displayName = "TableCaption"
|
|
|
|
export {
|
|
Table,
|
|
TableHeader,
|
|
TableBody,
|
|
TableFooter,
|
|
TableHead,
|
|
TableRow,
|
|
TableCell,
|
|
TableCaption,
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/ui/tabs.tsx
|
|
|
|
```tsx
|
|
"use client"
|
|
|
|
import * as React from "react"
|
|
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
const Tabs = TabsPrimitive.Root
|
|
|
|
const TabsList = React.forwardRef<
|
|
React.ElementRef<typeof TabsPrimitive.List>,
|
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
|
>(({ className, ...props }, ref) => (
|
|
<TabsPrimitive.List
|
|
ref={ref}
|
|
className={cn(
|
|
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
))
|
|
TabsList.displayName = TabsPrimitive.List.displayName
|
|
|
|
const TabsTrigger = React.forwardRef<
|
|
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
|
>(({ className, ...props }, ref) => (
|
|
<TabsPrimitive.Trigger
|
|
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-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
))
|
|
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
|
|
|
const TabsContent = React.forwardRef<
|
|
React.ElementRef<typeof TabsPrimitive.Content>,
|
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
|
>(({ className, ...props }, ref) => (
|
|
<TabsPrimitive.Content
|
|
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
|
|
)}
|
|
{...props}
|
|
/>
|
|
))
|
|
TabsContent.displayName = TabsPrimitive.Content.displayName
|
|
|
|
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/ui/textarea.tsx
|
|
|
|
```tsx
|
|
import * as React from 'react';
|
|
|
|
import {cn} from '@/lib/utils';
|
|
|
|
const Textarea = React.forwardRef<HTMLTextAreaElement, React.ComponentProps<'textarea'>>(
|
|
({className, ...props}, ref) => {
|
|
return (
|
|
<textarea
|
|
className={cn(
|
|
'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base 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 md:text-sm',
|
|
className
|
|
)}
|
|
ref={ref}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
);
|
|
Textarea.displayName = 'Textarea';
|
|
|
|
export {Textarea};
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/ui/toast.tsx
|
|
|
|
```tsx
|
|
"use client"
|
|
|
|
import * as React from "react"
|
|
import * as ToastPrimitives from "@radix-ui/react-toast"
|
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
import { X } from "lucide-react"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
const ToastProvider = ToastPrimitives.Provider
|
|
|
|
const ToastViewport = React.forwardRef<
|
|
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
|
>(({ className, ...props }, ref) => (
|
|
<ToastPrimitives.Viewport
|
|
ref={ref}
|
|
className={cn(
|
|
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
))
|
|
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
|
|
|
const toastVariants = cva(
|
|
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
|
{
|
|
variants: {
|
|
variant: {
|
|
default: "border bg-background text-foreground",
|
|
destructive:
|
|
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
|
},
|
|
},
|
|
defaultVariants: {
|
|
variant: "default",
|
|
},
|
|
}
|
|
)
|
|
|
|
const Toast = React.forwardRef<
|
|
React.ElementRef<typeof ToastPrimitives.Root>,
|
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
|
VariantProps<typeof toastVariants>
|
|
>(({ className, variant, ...props }, ref) => {
|
|
return (
|
|
<ToastPrimitives.Root
|
|
ref={ref}
|
|
className={cn(toastVariants({ variant }), className)}
|
|
{...props}
|
|
/>
|
|
)
|
|
})
|
|
Toast.displayName = ToastPrimitives.Root.displayName
|
|
|
|
const ToastAction = React.forwardRef<
|
|
React.ElementRef<typeof ToastPrimitives.Action>,
|
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
|
>(({ className, ...props }, ref) => (
|
|
<ToastPrimitives.Action
|
|
ref={ref}
|
|
className={cn(
|
|
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
))
|
|
ToastAction.displayName = ToastPrimitives.Action.displayName
|
|
|
|
const ToastClose = React.forwardRef<
|
|
React.ElementRef<typeof ToastPrimitives.Close>,
|
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
|
>(({ className, ...props }, ref) => (
|
|
<ToastPrimitives.Close
|
|
ref={ref}
|
|
className={cn(
|
|
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
|
className
|
|
)}
|
|
toast-close=""
|
|
{...props}
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</ToastPrimitives.Close>
|
|
))
|
|
ToastClose.displayName = ToastPrimitives.Close.displayName
|
|
|
|
const ToastTitle = React.forwardRef<
|
|
React.ElementRef<typeof ToastPrimitives.Title>,
|
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
|
>(({ className, ...props }, ref) => (
|
|
<ToastPrimitives.Title
|
|
ref={ref}
|
|
className={cn("text-sm font-semibold", className)}
|
|
{...props}
|
|
/>
|
|
))
|
|
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
|
|
|
const ToastDescription = React.forwardRef<
|
|
React.ElementRef<typeof ToastPrimitives.Description>,
|
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
|
>(({ className, ...props }, ref) => (
|
|
<ToastPrimitives.Description
|
|
ref={ref}
|
|
className={cn("text-sm opacity-90", className)}
|
|
{...props}
|
|
/>
|
|
))
|
|
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
|
|
|
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
|
|
|
|
type ToastActionElement = React.ReactElement<typeof ToastAction>
|
|
|
|
export {
|
|
type ToastProps,
|
|
type ToastActionElement,
|
|
ToastProvider,
|
|
ToastViewport,
|
|
Toast,
|
|
ToastTitle,
|
|
ToastDescription,
|
|
ToastClose,
|
|
ToastAction,
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/ui/toaster.tsx
|
|
|
|
```tsx
|
|
"use client"
|
|
|
|
import { useToast } from "@/hooks/use-toast"
|
|
import {
|
|
Toast,
|
|
ToastClose,
|
|
ToastDescription,
|
|
ToastProvider,
|
|
ToastTitle,
|
|
ToastViewport,
|
|
} from "@/components/ui/toast"
|
|
|
|
export function Toaster() {
|
|
const { toasts } = useToast()
|
|
|
|
return (
|
|
<ToastProvider>
|
|
{toasts.map(function ({ id, title, description, action, ...props }) {
|
|
return (
|
|
<Toast key={id} {...props}>
|
|
<div className="grid gap-1">
|
|
{title && <ToastTitle>{title}</ToastTitle>}
|
|
{description && (
|
|
<ToastDescription>{description}</ToastDescription>
|
|
)}
|
|
</div>
|
|
{action}
|
|
<ToastClose />
|
|
</Toast>
|
|
)
|
|
})}
|
|
<ToastViewport />
|
|
</ToastProvider>
|
|
)
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/components/ui/tooltip.tsx
|
|
|
|
```tsx
|
|
"use client"
|
|
|
|
import * as React from "react"
|
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
const TooltipProvider = TooltipPrimitive.Provider
|
|
|
|
const Tooltip = TooltipPrimitive.Root
|
|
|
|
const TooltipTrigger = TooltipPrimitive.Trigger
|
|
|
|
const TooltipContent = React.forwardRef<
|
|
React.ElementRef<typeof TooltipPrimitive.Content>,
|
|
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
|
<TooltipPrimitive.Content
|
|
ref={ref}
|
|
sideOffset={sideOffset}
|
|
className={cn(
|
|
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md 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",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
))
|
|
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
|
|
|
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/hooks/use-toast.ts
|
|
|
|
```ts
|
|
"use client"
|
|
|
|
// Inspired by react-hot-toast library
|
|
import * as React from "react"
|
|
|
|
import type {
|
|
ToastActionElement,
|
|
ToastProps,
|
|
} from "@/components/ui/toast"
|
|
|
|
const TOAST_LIMIT = 1
|
|
const TOAST_REMOVE_DELAY = 1000000
|
|
|
|
type ToasterToast = ToastProps & {
|
|
id: string
|
|
title?: React.ReactNode
|
|
description?: React.ReactNode
|
|
action?: ToastActionElement
|
|
}
|
|
|
|
const actionTypes = {
|
|
ADD_TOAST: "ADD_TOAST",
|
|
UPDATE_TOAST: "UPDATE_TOAST",
|
|
DISMISS_TOAST: "DISMISS_TOAST",
|
|
REMOVE_TOAST: "REMOVE_TOAST",
|
|
} as const
|
|
|
|
let count = 0
|
|
|
|
function genId() {
|
|
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
|
return count.toString()
|
|
}
|
|
|
|
type ActionType = typeof actionTypes
|
|
|
|
type Action =
|
|
| {
|
|
type: ActionType["ADD_TOAST"]
|
|
toast: ToasterToast
|
|
}
|
|
| {
|
|
type: ActionType["UPDATE_TOAST"]
|
|
toast: Partial<ToasterToast>
|
|
}
|
|
| {
|
|
type: ActionType["DISMISS_TOAST"]
|
|
toastId?: ToasterToast["id"]
|
|
}
|
|
| {
|
|
type: ActionType["REMOVE_TOAST"]
|
|
toastId?: ToasterToast["id"]
|
|
}
|
|
|
|
interface State {
|
|
toasts: ToasterToast[]
|
|
}
|
|
|
|
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
|
|
|
const addToRemoveQueue = (toastId: string) => {
|
|
if (toastTimeouts.has(toastId)) {
|
|
return
|
|
}
|
|
|
|
const timeout = setTimeout(() => {
|
|
toastTimeouts.delete(toastId)
|
|
dispatch({
|
|
type: "REMOVE_TOAST",
|
|
toastId: toastId,
|
|
})
|
|
}, TOAST_REMOVE_DELAY)
|
|
|
|
toastTimeouts.set(toastId, timeout)
|
|
}
|
|
|
|
export const reducer = (state: State, action: Action): State => {
|
|
switch (action.type) {
|
|
case "ADD_TOAST":
|
|
return {
|
|
...state,
|
|
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
|
}
|
|
|
|
case "UPDATE_TOAST":
|
|
return {
|
|
...state,
|
|
toasts: state.toasts.map((t) =>
|
|
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
|
),
|
|
}
|
|
|
|
case "DISMISS_TOAST": {
|
|
const { toastId } = action
|
|
|
|
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
|
// but I'll keep it here for simplicity
|
|
if (toastId) {
|
|
addToRemoveQueue(toastId)
|
|
} else {
|
|
state.toasts.forEach((toast) => {
|
|
addToRemoveQueue(toast.id)
|
|
})
|
|
}
|
|
|
|
return {
|
|
...state,
|
|
toasts: state.toasts.map((t) =>
|
|
t.id === toastId || toastId === undefined
|
|
? {
|
|
...t,
|
|
open: false,
|
|
}
|
|
: t
|
|
),
|
|
}
|
|
}
|
|
case "REMOVE_TOAST":
|
|
if (action.toastId === undefined) {
|
|
return {
|
|
...state,
|
|
toasts: [],
|
|
}
|
|
}
|
|
return {
|
|
...state,
|
|
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
|
}
|
|
}
|
|
}
|
|
|
|
const listeners: Array<(state: State) => void> = []
|
|
|
|
let memoryState: State = { toasts: [] }
|
|
|
|
function dispatch(action: Action) {
|
|
memoryState = reducer(memoryState, action)
|
|
listeners.forEach((listener) => {
|
|
listener(memoryState)
|
|
})
|
|
}
|
|
|
|
type Toast = Omit<ToasterToast, "id">
|
|
|
|
function toast({ ...props }: Toast) {
|
|
const id = genId()
|
|
|
|
const update = (props: ToasterToast) =>
|
|
dispatch({
|
|
type: "UPDATE_TOAST",
|
|
toast: { ...props, id },
|
|
})
|
|
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
|
|
|
dispatch({
|
|
type: "ADD_TOAST",
|
|
toast: {
|
|
...props,
|
|
id,
|
|
open: true,
|
|
onOpenChange: (open) => {
|
|
if (!open) dismiss()
|
|
},
|
|
},
|
|
})
|
|
|
|
return {
|
|
id: id,
|
|
dismiss,
|
|
update,
|
|
}
|
|
}
|
|
|
|
function useToast() {
|
|
const [state, setState] = React.useState<State>(memoryState)
|
|
|
|
React.useEffect(() => {
|
|
listeners.push(setState)
|
|
return () => {
|
|
const index = listeners.indexOf(setState)
|
|
if (index > -1) {
|
|
listeners.splice(index, 1)
|
|
}
|
|
}
|
|
}, [state])
|
|
|
|
return {
|
|
...state,
|
|
toast,
|
|
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
|
}
|
|
}
|
|
|
|
export { useToast, toast }
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/lib/aethex-data.ts
|
|
|
|
```ts
|
|
export type FileNode = { name: string; type: "file"; language: string };
|
|
export type FolderNode = {
|
|
name: string;
|
|
type: "folder";
|
|
children: (FileNode | FolderNode)[];
|
|
};
|
|
export type File = {
|
|
id: string;
|
|
name: string;
|
|
language: string;
|
|
content: string;
|
|
};
|
|
export type { File as OpenFileType };
|
|
|
|
|
|
export const fileTree: FolderNode = {
|
|
name: "aethex-project",
|
|
type: "folder",
|
|
children: [
|
|
{
|
|
name: "roblox",
|
|
type: "folder",
|
|
children: [
|
|
{ name: "main.lua", type: "file", language: "lua" },
|
|
{ name: "player.lua", type: "file", language: "lua" },
|
|
],
|
|
},
|
|
{
|
|
name: "web",
|
|
type: "folder",
|
|
children: [
|
|
{ name: "index.js", type: "file", language: "javascript" },
|
|
{ name: "style.css", type: "file", language: "css" },
|
|
],
|
|
},
|
|
{
|
|
name: "mobile",
|
|
type: "folder",
|
|
children: [{ name: "App.tsx", type: "file", language: "typescript" }],
|
|
},
|
|
{ name: "README.md", type: "file", language: "markdown" },
|
|
],
|
|
};
|
|
|
|
export const openFiles: File[] = [
|
|
{
|
|
id: "aethex-project/roblox/main.lua",
|
|
name: "main.lua",
|
|
language: "lua",
|
|
content: `-- Roblox main script
|
|
local Players = game:GetService("Players")
|
|
|
|
local function onPlayerAdded(player)
|
|
print("Player " .. player.Name .. " has joined the game.")
|
|
-- Initialize player data
|
|
end
|
|
|
|
Players.PlayerAdded:Connect(onPlayerAdded)
|
|
`,
|
|
},
|
|
{
|
|
id: "aethex-project/web/index.js",
|
|
name: "index.js",
|
|
language: "javascript",
|
|
content: `// Web client entry point
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const app = document.getElementById('app');
|
|
app.innerHTML = '<h1>Welcome to AeThex on the Web!</h1>';
|
|
console.log('Web client initialized.');
|
|
});
|
|
`,
|
|
},
|
|
{
|
|
id: "aethex-project/mobile/App.tsx",
|
|
name: "App.tsx",
|
|
language: "typescript",
|
|
content: `// React Native App Component
|
|
import React from 'react';
|
|
import { View, Text, StyleSheet } from 'react-native';
|
|
|
|
const App = () => {
|
|
return (
|
|
<View style={styles.container}>
|
|
<Text style={styles.text}>AeThex Mobile</Text>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
text: {
|
|
fontSize: 24,
|
|
},
|
|
});
|
|
|
|
export default App;
|
|
`,
|
|
},
|
|
];
|
|
|
|
export const consoleLogs = [
|
|
{ timestamp: "14:02:10.112", platform: "Web", type: "log", message: "Web client initialized." },
|
|
{ timestamp: "14:02:10.453", platform: "Roblox", type: "log", message: "Player AeThexDev has joined the game." },
|
|
{ timestamp: "14:02:11.231", platform: "Mobile", type: "log", message: "React Native running in development mode." },
|
|
{ timestamp: "14:02:15.899", platform: "Roblox", type: "warn", message: "DeprecationWarning: Use 'Task.Wait' instead of 'wait'." },
|
|
{ timestamp: "14:02:18.050", platform: "Web", type: "error", message: "Uncaught TypeError: Cannot read properties of null (reading 'innerHTML')" },
|
|
{ timestamp: "14:02:20.333", platform: "Mobile", type: "log", message: "User tapped the screen." },
|
|
];
|
|
|
|
export const platformCode = {
|
|
lua: {
|
|
name: "player.lua",
|
|
code: `local PlayerState = {
|
|
position = Vector3.new(0, 5, 0),
|
|
health = 100,
|
|
inventory = {}
|
|
}
|
|
|
|
-- Other Lua code...
|
|
`,
|
|
},
|
|
javascript: {
|
|
name: "player.js",
|
|
code: `let playerState = {
|
|
position: { x: 0, y: 5, z: 0 },
|
|
health: 100,
|
|
inventory: []
|
|
}
|
|
|
|
// Other JS code...
|
|
`,
|
|
},
|
|
typescript: {
|
|
name: "player.ts",
|
|
code: `interface PlayerState {
|
|
position: { x: number; y: number; z: number };
|
|
health: number;
|
|
inventory: string[];
|
|
}
|
|
|
|
const playerState: PlayerState = {
|
|
position: { x: 0, y: 5, z: 0 },
|
|
health: 100,
|
|
inventory: []
|
|
};
|
|
// Other RN/TS code...
|
|
`,
|
|
},
|
|
};
|
|
|
|
export const sharedState = {
|
|
playerScore: 1250,
|
|
gameTime: "15:32",
|
|
levelName: "CyberCity",
|
|
isActive: true,
|
|
};
|
|
|
|
export const crossPlatformState = [
|
|
{ variable: 'playerScore', roblox: 1250, web: 1250, mobile: 1250, status: 'synced' as const },
|
|
{ variable: 'gameTime', roblox: '15:32', web: '15:33', mobile: '15:32', status: 'syncing' as const },
|
|
{ variable: 'levelName', roblox: 'CyberCity', web: 'CyberCity', mobile: 'CyberCity', status: 'synced' as const },
|
|
{ variable: 'isActive', roblox: true, web: true, mobile: true, status: 'synced' as const },
|
|
{ variable: 'inventory', roblox: 'table', web: 'any[]', mobile: 'string[]', status: 'conflict' as const },
|
|
];
|
|
|
|
|
|
export const aiConflictResolutionSuggestion = {
|
|
conflictDetected: true,
|
|
suggestedSolutions: [
|
|
"In `player.lua`, change `inventory = {}` to an array-like table to match JS/TS.",
|
|
"In `player.js`, add type checks or use a class to ensure `inventory` items are strings like in `player.ts`.",
|
|
"Unify the `position` object/Vector3 structure across all platforms. Suggest using a simple `{x, y, z}` object in the shared state and converting to platform-specific types like `Vector3` within each environment."
|
|
],
|
|
explanation: "A synchronization conflict was detected in the `inventory` field of the player state. The TypeScript definition expects an array of strings (`string[]`), while the Lua code initializes it as a generic table (`{}`), and the JavaScript code as a generic array (`[]`). This can lead to runtime errors when synchronizing state. It is recommended to enforce consistent data types across all platforms."
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/lib/nexus.ts
|
|
|
|
```ts
|
|
export type Position = { x: number; y: number; z: number };
|
|
|
|
/**
|
|
* Generates platform-specific code snippets to sync player position.
|
|
* This is a mock implementation for the "Nexus Engine".
|
|
* @param position - The new position of the player.
|
|
* @returns An object with code snippets for each platform.
|
|
*/
|
|
export const syncPlayerPosition = (position: Position) => {
|
|
console.log("NEXUS ENGINE: Syncing player position", position);
|
|
|
|
return {
|
|
roblox: `-- Sync position for player
|
|
player.Character.HumanoidRootPart.CFrame = CFrame.new(${position.x}, ${position.y}, ${position.z})`,
|
|
web: `// Sync position for web
|
|
playerState.position = { x: ${position.x}, y: ${position.y}, z: ${position.z} };
|
|
updatePlayerOnMap(playerState.position);`,
|
|
mobile: `// Sync position for mobile
|
|
setPlayerState(prevState => ({
|
|
...prevState,
|
|
position: { x: ${position.x}, y: ${position.y}, z: ${position.z} },
|
|
}));`,
|
|
};
|
|
};
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/lib/placeholder-images.json
|
|
|
|
```json
|
|
{
|
|
"placeholderImages": [
|
|
{
|
|
"id": "roblox-vp",
|
|
"description": "A futuristic cityscape rendered in a blocky, stylized aesthetic typical of Roblox games.",
|
|
"imageUrl": "https://images.unsplash.com/photo-1675326570919-946d728e9a25?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3NDE5ODJ8MHwxfHNlYXJjaHwyfHxmdXR1cmlzdGljJTIwY2l0eXxlbnwwfHx8fDE3Njg1NTM4OTl8MA&ixlib=rb-4.1.0&q=80&w=1080",
|
|
"imageHint": "futuristic city"
|
|
},
|
|
{
|
|
"id": "web-vp",
|
|
"description": "A sleek, modern web interface with glowing neon elements and a dark theme, showing a game dashboard.",
|
|
"imageUrl": "https://images.unsplash.com/photo-1722834228772-01d16b9bf83b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3NDE5ODJ8MHwxfHNlYXJjaHwzfHxuZW9uJTIwaW50ZXJmYWNlfGVufDB8fHx8MTc2ODYxNDYwOHww&ixlib=rb-4.1.0&q=80&w=1080",
|
|
"imageHint": "neon interface"
|
|
},
|
|
{
|
|
"id": "mobile-vp",
|
|
"description": "A mobile game screen with touch controls overlaid on a vibrant, abstract world.",
|
|
"imageUrl": "https://images.unsplash.com/photo-1565870100382-f0a510db3cd1?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3NDE5ODJ8MHwxfHNlYXJjaHw0fHxtb2JpbGUlMjBnYW1lfGVufDB8fHx8MTc2ODU2MjY4MHww&ixlib=rb-4.1.0&q=80&w=1080",
|
|
"imageHint": "mobile game"
|
|
},
|
|
{
|
|
"id": "login-illustration",
|
|
"description": "An abstract visualization of cross-platform code and interfaces.",
|
|
"imageUrl": "https://images.unsplash.com/photo-1681130315503-f0709a634ee3?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3NDE5ODJ8MHwxfHNlYXJjaHw4fHxjcm9zcy1wbGF0Zm9ybSUyMGRldmVsb3BtZW50fGVufDB8fHx8MTc2ODYxNjg3M3ww&ixlib=rb-4.1.0&q=80&w=1080",
|
|
"imageHint": "cross-platform development"
|
|
},
|
|
{
|
|
"id": "workspace-thumb-1",
|
|
"description": "Abstract neon lights in a dark environment.",
|
|
"imageUrl": "https://images.unsplash.com/photo-1642538302053-ad49ca70936d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3NDE5ODJ8MHwxfHNlYXJjaHw1fHxhYnN0cmFjdCUyMG5lb258ZW58MHx8fHwxNzY4NTQ0NzIwfDA&ixlib=rb-4.1.0&q=80&w=1080",
|
|
"imageHint": "abstract neon"
|
|
},
|
|
{
|
|
"id": "workspace-thumb-2",
|
|
"description": "A vast and strange sci-fi landscape under a colorful sky.",
|
|
"imageUrl": "https://images.unsplash.com/photo-1641200658067-b5a56ebba866?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3NDE5ODJ8MHwxfHNlYXJjaHw2fHxzY2ktZmklMjBsYW5kc2NhcGV8ZW58MHx8fHwxNzY4NTUyNTAzfDA&ixlib=rb-4.1.0&q=80&w=1080",
|
|
"imageHint": "sci-fi landscape"
|
|
},
|
|
{
|
|
"id": "workspace-thumb-3",
|
|
"description": "A cluster of glowing cubes in a digital space.",
|
|
"imageUrl": "https://images.unsplash.com/photo-1758775212970-30458a9df7ae?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3NDE5ODJ8MHwxfHNlYXJjaHwzfHxnbG93aW5nJTIwY3ViZXN8ZW58MHx8fHwxNzY4NjE2ODczfDA&ixlib=rb-4.1.0&q=80&w=1080",
|
|
"imageHint": "glowing cubes"
|
|
},
|
|
{
|
|
"id": "workspace-thumb-4",
|
|
"description": "A neon-lit futuristic city at night.",
|
|
"imageUrl": "https://images.unsplash.com/photo-1536768139911-e290a59011e4?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3NDE5ODJ8MHwxfHNlYXJjaHwxMHx8ZnV0dXJpc3RpYyUyMGNpdHl8ZW58MHx8fHwxNzY4NTUzODk5fDA&ixlib=rb-4.1.0&q=80&w=1080",
|
|
"imageHint": "futuristic city"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/lib/placeholder-images.ts
|
|
|
|
```ts
|
|
import data from './placeholder-images.json';
|
|
|
|
export type ImagePlaceholder = {
|
|
id: string;
|
|
description: string;
|
|
imageUrl: string;
|
|
imageHint: string;
|
|
};
|
|
|
|
export const PlaceHolderImages: ImagePlaceholder[] = data.placeholderImages;
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/lib/templates.ts
|
|
|
|
```ts
|
|
import { Gamepad2, Users, Film, FileCode } from "lucide-react";
|
|
import type { FolderNode, FileNode } from "./aethex-data";
|
|
import type { ElementType } from "react";
|
|
|
|
export interface ProjectTemplate {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
icon: ElementType;
|
|
isPopular?: boolean;
|
|
fileTree: FolderNode;
|
|
mainFile: string;
|
|
}
|
|
|
|
const fileContents: Record<string, string> = {
|
|
"main.server.lua": `-- Roblox Server Script\nprint("Hello from the server!")`,
|
|
"player.client.lua": `-- Roblox Client Script\nprint("Hello from the client!")`,
|
|
"main.lua": `-- Roblox main script
|
|
local Players = game:GetService("Players")
|
|
|
|
local function onPlayerAdded(player)
|
|
print("Player " .. player.Name .. " has joined the game.")
|
|
-- Initialize player data
|
|
end
|
|
|
|
Players.PlayerAdded:Connect(onPlayerAdded)
|
|
`,
|
|
"player.lua": `-- Player script for Roblox`,
|
|
"index.js": `// Web client entry point
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const app = document.getElementById('app');
|
|
app.innerHTML = '<h1>Welcome to your new AeThex Project!</h1>';
|
|
console.log('Web client initialized.');
|
|
});
|
|
`,
|
|
"style.css": `/* Add your web styles here */
|
|
body {
|
|
background-color: #1a1a1a;
|
|
color: #fafafa;
|
|
font-family: sans-serif;
|
|
display: grid;
|
|
place-content: center;
|
|
min-height: 100vh;
|
|
}`,
|
|
"App.tsx": `// React Native App Component
|
|
import React from 'react';
|
|
import { View, Text, StyleSheet } from 'react-native';
|
|
|
|
const App = () => {
|
|
return (
|
|
<View style={styles.container}>
|
|
<Text style={styles.text}>Welcome to your AeThex Project!</Text>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
backgroundColor: '#1a1a1a',
|
|
},
|
|
text: {
|
|
fontSize: 24,
|
|
color: '#fafafa',
|
|
},
|
|
});
|
|
|
|
export default App;
|
|
`,
|
|
"README.md": `# New Project
|
|
|
|
Welcome to your new AeThex Studio project!
|
|
|
|
This project was generated by AeThex Studio. Start by exploring the files in the explorer.
|
|
`,
|
|
"story.json": `{
|
|
"title": "My Awesome Story",
|
|
"author": "",
|
|
"chapters": [
|
|
{
|
|
"id": "chapter-1",
|
|
"title": "The Beginning",
|
|
"scenes": []
|
|
}
|
|
]
|
|
}
|
|
`,
|
|
"viewer.js": `// Story viewer for web
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
console.log('Story viewer initialized.');
|
|
});
|
|
`,
|
|
};
|
|
|
|
export function generateFileContent(fileName: string, language: string): string {
|
|
return fileContents[fileName] || `// ${fileName} - ${language} file`;
|
|
}
|
|
|
|
export const projectTemplates: ProjectTemplate[] = [
|
|
{
|
|
id: "roblox-starter",
|
|
name: "Roblox Game Starter",
|
|
description: "A basic setup for a new Roblox game with player scripts.",
|
|
icon: Gamepad2,
|
|
fileTree: {
|
|
name: "roblox-game",
|
|
type: "folder",
|
|
children: [
|
|
{
|
|
name: "roblox",
|
|
type: "folder",
|
|
children: [
|
|
{ name: "main.server.lua", type: "file", language: "lua" },
|
|
{ name: "player.client.lua", type: "file", language: "lua" },
|
|
],
|
|
},
|
|
{ name: "README.md", type: "file", language: "markdown" },
|
|
],
|
|
},
|
|
mainFile: "main.server.lua",
|
|
},
|
|
{
|
|
id: "cross-platform-multiplayer",
|
|
name: "Cross-Platform Multiplayer",
|
|
description: "Multiplayer game structure for Roblox, Web, and Mobile.",
|
|
icon: Users,
|
|
isPopular: true,
|
|
fileTree: {
|
|
name: "multiplayer-project",
|
|
type: "folder",
|
|
children: [
|
|
{
|
|
name: "roblox",
|
|
type: "folder",
|
|
children: [
|
|
{ name: "main.lua", type: "file", language: "lua" },
|
|
{ name: "player.lua", type: "file", language: "lua" },
|
|
],
|
|
},
|
|
{
|
|
name: "web",
|
|
type: "folder",
|
|
children: [
|
|
{ name: "index.js", type: "file", language: "javascript" },
|
|
{ name: "style.css", type: "file", language: "css" },
|
|
],
|
|
},
|
|
{
|
|
name: "mobile",
|
|
type: "folder",
|
|
children: [{ name: "App.tsx", type: "file", language: "typescript" }],
|
|
},
|
|
{ name: "README.md", type: "file", language: "markdown" },
|
|
],
|
|
},
|
|
mainFile: "README.md",
|
|
},
|
|
{
|
|
id: "transmedia-story",
|
|
name: "Transmedia Story Project",
|
|
description: "A project for interactive stories across multiple platforms.",
|
|
icon: Film,
|
|
fileTree: {
|
|
name: "story-project",
|
|
type: "folder",
|
|
children: [
|
|
{ name: "story.json", type: "file", language: "json" },
|
|
{
|
|
name: "web",
|
|
type: "folder",
|
|
children: [
|
|
{ name: "viewer.js", type: "file", language: "javascript" },
|
|
],
|
|
},
|
|
{ name: "README.md", type: "file", language: "markdown" },
|
|
],
|
|
},
|
|
mainFile: "story.json",
|
|
},
|
|
{
|
|
id: "blank",
|
|
name: "Blank Project",
|
|
description: "Start fresh with an empty project directory.",
|
|
icon: FileCode,
|
|
fileTree: {
|
|
name: "blank-project",
|
|
type: "folder",
|
|
children: [{ name: "README.md", type: "file", language: "markdown" }],
|
|
},
|
|
mainFile: "README.md",
|
|
},
|
|
];
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/lib/utils.ts
|
|
|
|
```ts
|
|
import { clsx, type ClassValue } from "clsx"
|
|
import { twMerge } from "tailwind-merge"
|
|
|
|
export function cn(...inputs: ClassValue[]) {
|
|
return twMerge(clsx(inputs))
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: src/lib/workspaces.ts
|
|
|
|
```ts
|
|
import {
|
|
MobileIcon,
|
|
RobloxIcon,
|
|
WebIcon,
|
|
} from "@/components/aethex/icons";
|
|
import type { ElementType } from "react";
|
|
|
|
export type Workspace = {
|
|
id: string;
|
|
name: string;
|
|
lastModified: string;
|
|
platforms: ElementType[];
|
|
thumbnailUrlId: string;
|
|
thumbnailImageHint: string;
|
|
};
|
|
|
|
export const workspaces: Workspace[] = [
|
|
{
|
|
id: "proj-1",
|
|
name: "Cyber Runner",
|
|
lastModified: "2 days ago",
|
|
platforms: [RobloxIcon, WebIcon, MobileIcon],
|
|
thumbnailUrlId: "workspace-thumb-1",
|
|
thumbnailImageHint: "abstract neon",
|
|
},
|
|
{
|
|
id: "proj-2",
|
|
name: "Project Chimera",
|
|
lastModified: "5 days ago",
|
|
platforms: [RobloxIcon, WebIcon],
|
|
thumbnailUrlId: "workspace-thumb-2",
|
|
thumbnailImageHint: "sci-fi landscape",
|
|
},
|
|
{
|
|
id: "proj-3",
|
|
name: "Story-Verse",
|
|
lastModified: "1 week ago",
|
|
platforms: [WebIcon],
|
|
thumbnailUrlId: "workspace-thumb-3",
|
|
thumbnailImageHint: "glowing cubes",
|
|
},
|
|
];
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: tailwind.config.ts
|
|
|
|
```ts
|
|
import type {Config} from 'tailwindcss';
|
|
|
|
export default {
|
|
darkMode: ['class'],
|
|
content: [
|
|
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
|
|
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
|
|
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
|
|
],
|
|
theme: {
|
|
extend: {
|
|
fontFamily: {
|
|
body: ['Inter', 'sans-serif'],
|
|
headline: ['Space Grotesk', 'sans-serif'],
|
|
code: ['Source Code Pro', 'monospace'],
|
|
},
|
|
colors: {
|
|
background: 'hsl(var(--background))',
|
|
foreground: 'hsl(var(--foreground))',
|
|
card: {
|
|
DEFAULT: 'hsl(var(--card))',
|
|
foreground: 'hsl(var(--card-foreground))',
|
|
},
|
|
popover: {
|
|
DEFAULT: 'hsl(var(--popover))',
|
|
foreground: 'hsl(var(--popover-foreground))',
|
|
},
|
|
primary: {
|
|
DEFAULT: 'hsl(var(--primary))',
|
|
foreground: 'hsl(var(--primary-foreground))',
|
|
},
|
|
secondary: {
|
|
DEFAULT: 'hsl(var(--secondary))',
|
|
foreground: 'hsl(var(--secondary-foreground))',
|
|
},
|
|
muted: {
|
|
DEFAULT: 'hsl(var(--muted))',
|
|
foreground: 'hsl(var(--muted-foreground))',
|
|
},
|
|
accent: {
|
|
DEFAULT: 'hsl(var(--accent))',
|
|
foreground: 'hsl(var(--accent-foreground))',
|
|
},
|
|
destructive: {
|
|
DEFAULT: 'hsl(var(--destructive))',
|
|
foreground: 'hsl(var(--destructive-foreground))',
|
|
},
|
|
border: 'hsl(var(--border))',
|
|
input: 'hsl(var(--input))',
|
|
ring: 'hsl(var(--ring))',
|
|
chart: {
|
|
'1': 'hsl(var(--chart-1))',
|
|
'2': 'hsl(var(--chart-2))',
|
|
'3': 'hsl(var(--chart-3))',
|
|
'4': 'hsl(var(--chart-4))',
|
|
'5': 'hsl(var(--chart-5))',
|
|
},
|
|
sidebar: {
|
|
DEFAULT: 'hsl(var(--sidebar-background))',
|
|
foreground: 'hsl(var(--sidebar-foreground))',
|
|
primary: 'hsl(var(--sidebar-primary))',
|
|
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
|
|
accent: 'hsl(var(--sidebar-accent))',
|
|
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
|
|
border: 'hsl(var(--sidebar-border))',
|
|
ring: 'hsl(var(--sidebar-ring))',
|
|
},
|
|
},
|
|
borderRadius: {
|
|
lg: 'var(--radius)',
|
|
md: 'calc(var(--radius) - 2px)',
|
|
sm: 'calc(var(--radius) - 4px)',
|
|
},
|
|
keyframes: {
|
|
'accordion-down': {
|
|
from: {
|
|
height: '0',
|
|
},
|
|
to: {
|
|
height: 'var(--radix-accordion-content-height)',
|
|
},
|
|
},
|
|
'accordion-up': {
|
|
from: {
|
|
height: 'var(--radix-accordion-content-height)',
|
|
},
|
|
to: {
|
|
height: '0',
|
|
},
|
|
},
|
|
},
|
|
animation: {
|
|
'accordion-down': 'accordion-down 0.2s ease-out',
|
|
'accordion-up': 'accordion-up 0.2s ease-out',
|
|
},
|
|
},
|
|
},
|
|
plugins: [require('tailwindcss-animate'), require('@tailwindcss/typography')],
|
|
} satisfies Config;
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## FILE: tsconfig.json
|
|
|
|
```json
|
|
{
|
|
"compilerOptions": {
|
|
"target": "ES2017",
|
|
"lib": ["dom", "dom.iterable", "esnext"],
|
|
"allowJs": true,
|
|
"skipLibCheck": true,
|
|
"strict": true,
|
|
"noEmit": true,
|
|
"esModuleInterop": true,
|
|
"module": "esnext",
|
|
"moduleResolution": "bundler",
|
|
"resolveJsonModule": true,
|
|
"isolatedModules": true,
|
|
"jsx": "preserve",
|
|
"incremental": true,
|
|
"plugins": [
|
|
{
|
|
"name": "next"
|
|
}
|
|
],
|
|
"paths": {
|
|
"@/*": ["./src/*"]
|
|
}
|
|
},
|
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
"exclude": ["node_modules"]
|
|
}
|
|
|
|
```
|