Add project scaffolding and documentation
Add .env.example with configuration for database, Redis, security, SMTP, workers, and plugins. Add .gitignore for Python, Node.js, Next.js, Docker volumes, and IDE files. Add MIT License. Update README.md with feature overview, quick start guide, architecture description, plugin system documentation, security details, backup/restore instructions, and developer setup. Add Alembic configuration files and placeholder directories for API, web, worker, and plugin components
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "@nexadash/plugin-sdk",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "NexaDash Plugin SDK for external developers",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"typescript": "^5.6.0",
|
||||
"vitest": "^2.1.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./types";
|
||||
export * from "./manifest";
|
||||
export * from "./widgets";
|
||||
export * from "./validation";
|
||||
@@ -0,0 +1,30 @@
|
||||
import { PluginManifest } from "./types";
|
||||
|
||||
export function createManifest(manifest: PluginManifest): PluginManifest {
|
||||
const defaults: PluginManifest = {
|
||||
id: manifest.id,
|
||||
name: manifest.name,
|
||||
version: manifest.version,
|
||||
nexadashPluginApi: manifest.nexadashPluginApi || "1.0",
|
||||
category: "Generic",
|
||||
permissions: [],
|
||||
settingsSchema: {},
|
||||
credentialsSchema: {},
|
||||
widgets: [],
|
||||
apiRoutes: [],
|
||||
hasFrontend: true,
|
||||
healthcheck: {
|
||||
method: "GET",
|
||||
intervalSeconds: 60,
|
||||
timeoutSeconds: 30,
|
||||
},
|
||||
};
|
||||
return { ...defaults, ...manifest };
|
||||
}
|
||||
|
||||
export const REQUIRED_MANIFEST_FIELDS = [
|
||||
"id",
|
||||
"name",
|
||||
"version",
|
||||
"nexadashPluginApi",
|
||||
];
|
||||
@@ -0,0 +1,77 @@
|
||||
export interface PluginManifest {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
nexadashPluginApi: string;
|
||||
description?: string;
|
||||
author?: string;
|
||||
category?: string;
|
||||
icon?: string;
|
||||
permissions?: string[];
|
||||
settingsSchema?: Record<string, any>;
|
||||
credentialsSchema?: Record<string, any>;
|
||||
widgets?: WidgetDefinition[];
|
||||
apiRoutes?: string[];
|
||||
hasFrontend?: boolean;
|
||||
healthcheck?: HealthcheckDefinition;
|
||||
}
|
||||
|
||||
export interface WidgetDefinition {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
defaultWidth?: number;
|
||||
defaultHeight?: number;
|
||||
minWidth?: number;
|
||||
minHeight?: number;
|
||||
maxWidth?: number;
|
||||
maxHeight?: number;
|
||||
settingsSchema?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface HealthcheckDefinition {
|
||||
endpoint?: string;
|
||||
method?: "GET" | "POST" | "PUT" | "DELETE";
|
||||
intervalSeconds?: number;
|
||||
timeoutSeconds?: number;
|
||||
}
|
||||
|
||||
export interface PluginContext<T = any> {
|
||||
settings: Record<string, any>;
|
||||
credentials: T;
|
||||
baseUrl: string;
|
||||
headers: Record<string, string>;
|
||||
fetch: (path: string, options?: RequestInit) => Promise<any>;
|
||||
log: (level: "info" | "warn" | "error", message: string, meta?: Record<string, any>) => void;
|
||||
}
|
||||
|
||||
export interface PluginConnector<T = any> {
|
||||
healthcheck(context: PluginContext<T>): Promise<HealthcheckResult>;
|
||||
fetchWidgetData(
|
||||
context: PluginContext<T>,
|
||||
widgetType: string,
|
||||
settings: Record<string, any>
|
||||
): Promise<any>;
|
||||
}
|
||||
|
||||
export interface HealthcheckResult {
|
||||
status: "ok" | "warning" | "error" | "unknown";
|
||||
message?: string;
|
||||
details?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface JsonSchemaProperty {
|
||||
type: "string" | "number" | "boolean" | "array" | "object";
|
||||
title?: string;
|
||||
description?: string;
|
||||
default?: any;
|
||||
enum?: any[];
|
||||
format?: string;
|
||||
minLength?: number;
|
||||
maxLength?: number;
|
||||
minimum?: number;
|
||||
maximum?: number;
|
||||
properties?: Record<string, JsonSchemaProperty>;
|
||||
required?: string[];
|
||||
items?: JsonSchemaProperty;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { REQUIRED_MANIFEST_FIELDS } from "./manifest";
|
||||
import { PluginManifest } from "./types";
|
||||
|
||||
export interface ValidationResult {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
export function validateManifest(manifest: any): ValidationResult {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!manifest || typeof manifest !== "object") {
|
||||
return { valid: false, errors: ["Manifest must be an object"] };
|
||||
}
|
||||
|
||||
for (const field of REQUIRED_MANIFEST_FIELDS) {
|
||||
if (!manifest[field] || typeof manifest[field] !== "string") {
|
||||
errors.push(`Missing or invalid required field: ${field}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (manifest.id && !/^[a-z0-9-]+$/.test(manifest.id)) {
|
||||
errors.push("Plugin id must be lowercase alphanumeric with hyphens");
|
||||
}
|
||||
|
||||
if (manifest.version && !/^\d+\.\d+\.\d+/.test(manifest.version)) {
|
||||
errors.push("Version must follow semantic versioning (e.g. 1.0.0)");
|
||||
}
|
||||
|
||||
if (manifest.widgets && !Array.isArray(manifest.widgets)) {
|
||||
errors.push("widgets must be an array");
|
||||
}
|
||||
|
||||
if (manifest.permissions && !Array.isArray(manifest.permissions)) {
|
||||
errors.push("permissions must be an array");
|
||||
}
|
||||
|
||||
return { valid: errors.length === 0, errors };
|
||||
}
|
||||
|
||||
export function isValidPluginApiVersion(version: string): boolean {
|
||||
return version === "1.0";
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { WidgetDefinition } from "./types";
|
||||
|
||||
export function defineWidget(widget: WidgetDefinition): WidgetDefinition {
|
||||
return {
|
||||
defaultWidth: 2,
|
||||
defaultHeight: 2,
|
||||
minWidth: 1,
|
||||
minHeight: 1,
|
||||
settingsSchema: {},
|
||||
...widget,
|
||||
};
|
||||
}
|
||||
|
||||
export const commonWidgetSizes = {
|
||||
small: { defaultWidth: 1, defaultHeight: 1 },
|
||||
medium: { defaultWidth: 2, defaultHeight: 2 },
|
||||
large: { defaultWidth: 3, defaultHeight: 2 },
|
||||
wide: { defaultWidth: 4, defaultHeight: 2 },
|
||||
tall: { defaultWidth: 2, defaultHeight: 4 },
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { createManifest, defineWidget, validateManifest, REQUIRED_MANIFEST_FIELDS } from "../src";
|
||||
|
||||
const baseManifest = {
|
||||
id: "test-plugin",
|
||||
name: "Test Plugin",
|
||||
version: "1.0.0",
|
||||
nexadashPluginApi: "1.0",
|
||||
};
|
||||
|
||||
describe("plugin-sdk manifest", () => {
|
||||
it("createManifest applies defaults", () => {
|
||||
const manifest = createManifest(baseManifest);
|
||||
expect(manifest.name).toBe("Test Plugin");
|
||||
expect(manifest.nexadashPluginApi).toBe("1.0");
|
||||
expect(manifest.widgets).toEqual([]);
|
||||
expect(manifest.hasFrontend).toBe(false);
|
||||
});
|
||||
|
||||
it("defineWidget applies defaults", () => {
|
||||
const widget = defineWidget({
|
||||
id: "test-widget",
|
||||
name: "Test Widget",
|
||||
});
|
||||
expect(widget.defaultWidth).toBe(2);
|
||||
expect(widget.defaultHeight).toBe(2);
|
||||
});
|
||||
|
||||
it("validateManifest validates required fields", () => {
|
||||
const result = validateManifest(createManifest(baseManifest));
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it("validateManifest rejects invalid ids", () => {
|
||||
const result = validateManifest(
|
||||
createManifest({
|
||||
...baseManifest,
|
||||
id: "Test Plugin",
|
||||
})
|
||||
);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("REQUIRED_MANIFEST_FIELDS", () => {
|
||||
it("contains required keys", () => {
|
||||
expect(REQUIRED_MANIFEST_FIELDS).toContain("id");
|
||||
expect(REQUIRED_MANIFEST_FIELDS).toContain("name");
|
||||
expect(REQUIRED_MANIFEST_FIELDS).toContain("version");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "@nexadash/shared",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.6.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
export const APP_NAME = "NexaDash";
|
||||
export const DEFAULT_LOCALE = "en";
|
||||
export const SUPPORTED_LOCALES = ["en", "de"];
|
||||
export const DEFAULT_THEME = "system";
|
||||
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
|
||||
|
||||
export const PERMISSIONS = {
|
||||
USER_READ: "user:read",
|
||||
USER_WRITE: "user:write",
|
||||
USER_DELETE: "user:delete",
|
||||
USER_INVITE: "user:invite",
|
||||
ROLE_READ: "role:read",
|
||||
ROLE_WRITE: "role:write",
|
||||
DASHBOARD_READ: "dashboard:read",
|
||||
DASHBOARD_WRITE: "dashboard:write",
|
||||
DASHBOARD_DELETE: "dashboard:delete",
|
||||
DASHBOARD_SHARE: "dashboard:share",
|
||||
PLUGIN_READ: "plugin:read",
|
||||
PLUGIN_WRITE: "plugin:write",
|
||||
PLUGIN_DELETE: "plugin:delete",
|
||||
PLUGIN_ADMIN: "plugin:admin",
|
||||
PLUGIN_INSTALL: "plugin:install",
|
||||
CONNECTION_READ: "connection:read",
|
||||
CONNECTION_WRITE: "connection:write",
|
||||
CONNECTION_DELETE: "connection:delete",
|
||||
CONNECTION_TEST: "connection:test",
|
||||
SYSTEM_READ: "system:read",
|
||||
SYSTEM_WRITE: "system:write",
|
||||
AUDIT_READ: "audit:read",
|
||||
BACKUP_RESTORE: "backup:restore",
|
||||
API_TOKEN_READ: "api_token:read",
|
||||
API_TOKEN_WRITE: "api_token:write",
|
||||
} as const;
|
||||
|
||||
export const STATUS_COLORS = {
|
||||
ok: "bg-emerald-500",
|
||||
warning: "bg-amber-500",
|
||||
error: "bg-rose-500",
|
||||
critical: "bg-rose-600",
|
||||
info: "bg-blue-500",
|
||||
unknown: "bg-slate-400",
|
||||
} as const;
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./types";
|
||||
export * from "./constants";
|
||||
@@ -0,0 +1,147 @@
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
is_active: boolean;
|
||||
is_superuser: boolean;
|
||||
is_owner: boolean;
|
||||
locale: string;
|
||||
theme: string;
|
||||
timezone: string;
|
||||
role?: Role;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface Role {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
permissions: string[];
|
||||
is_system: boolean;
|
||||
}
|
||||
|
||||
export interface Dashboard {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
folder: string;
|
||||
is_favorite: boolean;
|
||||
is_public: boolean;
|
||||
owner_id: string;
|
||||
layout: Record<string, any>;
|
||||
layouts_by_breakpoint: Record<string, any>;
|
||||
refresh_interval_seconds?: number;
|
||||
order_index: number;
|
||||
tags: string[];
|
||||
widgets: Widget[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface Widget {
|
||||
id: string;
|
||||
dashboard_id: string;
|
||||
plugin_id: string;
|
||||
widget_type: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
position_x: number;
|
||||
position_y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
min_width?: number;
|
||||
min_height?: number;
|
||||
max_width?: number;
|
||||
max_height?: number;
|
||||
order_index: number;
|
||||
settings: Record<string, any>;
|
||||
instance_id?: string;
|
||||
is_visible: boolean;
|
||||
is_static: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface Plugin {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
description?: string;
|
||||
author?: string;
|
||||
category: string;
|
||||
icon?: string;
|
||||
is_active: boolean;
|
||||
is_builtin: boolean;
|
||||
is_installed: boolean;
|
||||
permissions: string[];
|
||||
settings_schema: Record<string, any>;
|
||||
credentials_schema: Record<string, any>;
|
||||
widget_types: any[];
|
||||
healthcheck_definition: Record<string, any>;
|
||||
api_routes: string[];
|
||||
has_frontend: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface PluginInstance {
|
||||
id: string;
|
||||
plugin_id: string;
|
||||
service_connection_id?: string;
|
||||
name: string;
|
||||
settings: Record<string, any>;
|
||||
is_enabled: boolean;
|
||||
health_status: string;
|
||||
health_message?: string;
|
||||
last_sync_at?: string;
|
||||
last_error?: string;
|
||||
error_count: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ServiceConnection {
|
||||
id: string;
|
||||
name: string;
|
||||
plugin_id: string;
|
||||
base_url: string;
|
||||
verify_tls: boolean;
|
||||
timeout_seconds: number;
|
||||
credentials_id?: string;
|
||||
is_enabled: boolean;
|
||||
health_status: string;
|
||||
health_message?: string;
|
||||
extra_headers: Record<string, string>;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface Notification {
|
||||
id: string;
|
||||
user_id: string;
|
||||
title: string;
|
||||
message?: string;
|
||||
type: string;
|
||||
is_read: boolean;
|
||||
link?: string;
|
||||
metadata: Record<string, any>;
|
||||
read_at?: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface AuditLog {
|
||||
id: string;
|
||||
user_id?: string;
|
||||
action: string;
|
||||
resource_type: string;
|
||||
resource_id?: string;
|
||||
ip_address?: string;
|
||||
user_agent?: string;
|
||||
details: Record<string, any>;
|
||||
severity: string;
|
||||
timestamp: string;
|
||||
created_at: string;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "@nexadash/ui",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-accordion": "^1.2.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.0",
|
||||
"@radix-ui/react-avatar": "^1.1.0",
|
||||
"@radix-ui/react-checkbox": "^1.1.0",
|
||||
"@radix-ui/react-dialog": "^1.1.0",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.0",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-popover": "^1.1.0",
|
||||
"@radix-ui/react-select": "^2.1.0",
|
||||
"@radix-ui/react-separator": "^1.1.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-switch": "^1.1.0",
|
||||
"@radix-ui/react-tabs": "^1.1.0",
|
||||
"@radix-ui/react-toast": "^1.2.0",
|
||||
"@radix-ui/react-tooltip": "^1.1.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
"lucide-react": "^0.454.0",
|
||||
"tailwind-merge": "^2.5.0",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/react": "^18.3.0",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"autoprefixer": "^10.4.0",
|
||||
"postcss": "^8.4.0",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"typescript": "^5.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "../lib/utils";
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
|
||||
{
|
||||
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 };
|
||||
@@ -0,0 +1,31 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
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 };
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "../lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||
outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||
secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
icon: "h-9 w-9",
|
||||
},
|
||||
},
|
||||
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 };
|
||||
@@ -0,0 +1,54 @@
|
||||
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-xl border bg-card text-card-foreground shadow backdrop-blur-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<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<h3 ref={ref} className={cn("font-semibold leading-none tracking-tight", className)} {...props} />
|
||||
)
|
||||
);
|
||||
CardTitle.displayName = "CardTitle";
|
||||
|
||||
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<p 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 };
|
||||
@@ -0,0 +1,22 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "../lib/utils";
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input };
|
||||
@@ -0,0 +1,20 @@
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
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 };
|
||||
@@ -0,0 +1,12 @@
|
||||
import { cn } from "../lib/utils";
|
||||
|
||||
function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn("animate-pulse rounded-md bg-primary/10", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Skeleton };
|
||||
@@ -0,0 +1,27 @@
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch";
|
||||
import * as React from "react";
|
||||
|
||||
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-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
));
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName;
|
||||
|
||||
export { Switch };
|
||||
@@ -0,0 +1,9 @@
|
||||
export * from "./components/button";
|
||||
export * from "./components/card";
|
||||
export * from "./components/input";
|
||||
export * from "./components/label";
|
||||
export * from "./components/switch";
|
||||
export * from "./components/badge";
|
||||
export * from "./components/alert";
|
||||
export * from "./components/skeleton";
|
||||
export * from "./lib/utils";
|
||||
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"types": ["node", "react", "react-dom"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user