initial commit
Some checks failed
CI / Go — node-agent tests (push) Has been cancelled
CI / Go — edge-gateway build (push) Has been cancelled
CI / Node — lint, typecheck, test, build (push) Has been cancelled

This commit is contained in:
smueller
2026-06-26 10:45:08 +02:00
commit e37ea87b35
118 changed files with 7726 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
{
"name": "@hexahost/auth",
"version": "0.0.0",
"private": true,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsc",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@hexahost/typescript-config": "workspace:*",
"typescript": "^5.7.3"
}
}

View File

@@ -0,0 +1,29 @@
export enum UserRole {
USER = 'USER',
SUPPORT = 'SUPPORT',
MODERATOR = 'MODERATOR',
ADMIN = 'ADMIN',
SUPER_ADMIN = 'SUPER_ADMIN',
}
export interface AuthenticatedUser {
id: string;
email: string;
username: string;
roles: UserRole[];
}
export interface SessionContext {
user: AuthenticatedUser;
sessionId: string;
issuedAt: Date;
expiresAt: Date;
}
export interface AuthTokenPayload {
sub: string;
sessionId: string;
roles: UserRole[];
iat?: number;
exp?: number;
}

View File

@@ -0,0 +1,9 @@
{
"extends": "@hexahost/typescript-config/library.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,18 @@
{
"name": "@hexahost/config",
"version": "0.0.0",
"private": true,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsc",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"zod": "^3.24.1"
},
"devDependencies": {
"@hexahost/typescript-config": "workspace:*",
"typescript": "^5.7.3"
}
}

View File

@@ -0,0 +1,95 @@
import { z } from 'zod';
const logLevelSchema = z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace', 'silent']);
const appEnvSchema = z.enum(['development', 'test', 'staging', 'production']);
export const configSchema = z.object({
APP_NAME: z.string().default('HexaHost GameCloud'),
APP_ENV: appEnvSchema.default('development'),
APP_URL: z.string().url(),
API_URL: z.string().url(),
DATABASE_URL: z.string().min(1),
REDIS_URL: z.string().min(1),
SESSION_SECRET: z.string().min(32),
LOG_LEVEL: logLevelSchema.default('info'),
API_PORT: z.coerce.number().int().min(1).max(65535).default(3001),
WORKER_PORT: z.coerce.number().int().min(1).max(65535).default(3002),
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
});
export type AppConfig = z.infer<typeof configSchema>;
function readEnv(): Record<string, string | undefined> {
return { ...process.env };
}
export function loadConfig(env: Record<string, string | undefined> = readEnv()): AppConfig {
return configSchema.parse({
APP_NAME: env['APP_NAME'],
APP_ENV: env['APP_ENV'],
APP_URL: env['APP_URL'],
API_URL: env['API_URL'],
DATABASE_URL: env['DATABASE_URL'],
REDIS_URL: env['REDIS_URL'],
SESSION_SECRET: env['SESSION_SECRET'],
LOG_LEVEL: env['LOG_LEVEL'],
API_PORT: env['API_PORT'],
WORKER_PORT: env['WORKER_PORT'],
NODE_ENV: env['NODE_ENV'],
});
}
export function validateConfig(env: Record<string, string | undefined> = readEnv()): AppConfig {
const requiredKeys = [
'APP_URL',
'API_URL',
'DATABASE_URL',
'REDIS_URL',
'SESSION_SECRET',
] as const;
const missing = requiredKeys.filter((key) => !env[key] || env[key]!.trim() === '');
if (missing.length > 0) {
throw new Error(
`Missing required environment variables: ${missing.join(', ')}`,
);
}
const result = configSchema.safeParse({
APP_NAME: env['APP_NAME'],
APP_ENV: env['APP_ENV'],
APP_URL: env['APP_URL'],
API_URL: env['API_URL'],
DATABASE_URL: env['DATABASE_URL'],
REDIS_URL: env['REDIS_URL'],
SESSION_SECRET: env['SESSION_SECRET'],
LOG_LEVEL: env['LOG_LEVEL'],
API_PORT: env['API_PORT'],
WORKER_PORT: env['WORKER_PORT'],
NODE_ENV: env['NODE_ENV'],
});
if (!result.success) {
const issues = result.error.issues
.map((issue) => `${issue.path.join('.')}: ${issue.message}`)
.join('; ');
throw new Error(`Invalid configuration: ${issues}`);
}
return result.data;
}
let cachedConfig: AppConfig | undefined;
export function getConfig(): AppConfig {
if (!cachedConfig) {
cachedConfig = validateConfig();
}
return cachedConfig;
}
export function resetConfigCache(): void {
cachedConfig = undefined;
}

View File

@@ -0,0 +1,9 @@
{
"extends": "@hexahost/typescript-config/library.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,18 @@
{
"name": "@hexahost/contracts",
"version": "0.0.0",
"private": true,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsc",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"zod": "^3.24.1"
},
"devDependencies": {
"@hexahost/typescript-config": "workspace:*",
"typescript": "^5.7.3"
}
}

View File

@@ -0,0 +1,32 @@
import { z } from 'zod';
export const problemDetailsSchema = z.object({
type: z.string().url().or(z.literal('about:blank')),
title: z.string(),
status: z.number().int(),
detail: z.string().optional(),
instance: z.string().optional(),
errors: z
.array(
z.object({
field: z.string().optional(),
message: z.string(),
}),
)
.optional(),
});
export type ProblemDetails = z.infer<typeof problemDetailsSchema>;
export function createProblemDetails(
input: Omit<ProblemDetails, 'type'> & { type?: string },
): ProblemDetails {
return problemDetailsSchema.parse({
type: input.type ?? 'about:blank',
title: input.title,
status: input.status,
detail: input.detail,
instance: input.instance,
errors: input.errors,
});
}

View File

@@ -0,0 +1,22 @@
import { z } from 'zod';
export const healthStatusSchema = z.enum(['ok', 'degraded', 'error']);
export const healthCheckSchema = z.object({
name: z.string(),
status: healthStatusSchema,
message: z.string().optional(),
durationMs: z.number().nonnegative().optional(),
});
export const healthResponseSchema = z.object({
status: healthStatusSchema,
service: z.string(),
version: z.string().optional(),
timestamp: z.string().datetime(),
checks: z.array(healthCheckSchema).optional(),
});
export type HealthStatus = z.infer<typeof healthStatusSchema>;
export type HealthCheck = z.infer<typeof healthCheckSchema>;
export type HealthResponse = z.infer<typeof healthResponseSchema>;

View File

@@ -0,0 +1,23 @@
export {
healthStatusSchema,
healthCheckSchema,
healthResponseSchema,
type HealthStatus,
type HealthCheck,
type HealthResponse,
} from './health';
export {
problemDetailsSchema,
createProblemDetails,
type ProblemDetails,
} from './error';
export {
paginationQuerySchema,
paginationMetaSchema,
createPaginatedResponseSchema,
buildPaginationMeta,
type PaginationQuery,
type PaginationMeta,
} from './pagination';

View File

@@ -0,0 +1,43 @@
import { z } from 'zod';
export const paginationQuerySchema = z.object({
page: z.coerce.number().int().min(1).default(1),
pageSize: z.coerce.number().int().min(1).max(100).default(20),
sortBy: z.string().optional(),
sortOrder: z.enum(['asc', 'desc']).default('desc'),
});
export const paginationMetaSchema = z.object({
page: z.number().int().min(1),
pageSize: z.number().int().min(1),
totalItems: z.number().int().nonnegative(),
totalPages: z.number().int().nonnegative(),
hasNextPage: z.boolean(),
hasPreviousPage: z.boolean(),
});
export function createPaginatedResponseSchema<T extends z.ZodTypeAny>(itemSchema: T) {
return z.object({
data: z.array(itemSchema),
meta: paginationMetaSchema,
});
}
export type PaginationQuery = z.infer<typeof paginationQuerySchema>;
export type PaginationMeta = z.infer<typeof paginationMetaSchema>;
export function buildPaginationMeta(
page: number,
pageSize: number,
totalItems: number,
): PaginationMeta {
const totalPages = totalItems === 0 ? 0 : Math.ceil(totalItems / pageSize);
return paginationMetaSchema.parse({
page,
pageSize,
totalItems,
totalPages,
hasNextPage: page < totalPages,
hasPreviousPage: page > 1,
});
}

View File

@@ -0,0 +1,9 @@
{
"extends": "@hexahost/typescript-config/library.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,22 @@
{
"name": "@hexahost/database",
"version": "0.0.0",
"private": true,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsc",
"typecheck": "tsc --noEmit",
"db:generate": "prisma generate",
"db:migrate": "prisma migrate dev",
"db:push": "prisma db push"
},
"dependencies": {
"@prisma/client": "^6.3.1"
},
"devDependencies": {
"@hexahost/typescript-config": "workspace:*",
"prisma": "^6.3.1",
"typescript": "^5.7.3"
}
}

View File

@@ -0,0 +1,263 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum UserStatus {
ACTIVE
SUSPENDED
DELETED
}
enum GameServerStatus {
PENDING
PROVISIONING
STOPPED
STARTING
RUNNING
STOPPING
ERROR
DELETED
}
enum GameNodeStatus {
ONLINE
OFFLINE
MAINTENANCE
DRAINING
}
enum JobStatus {
PENDING
RUNNING
COMPLETED
FAILED
CANCELLED
}
model User {
id String @id @default(uuid())
email String @unique
username String @unique
status UserStatus @default(ACTIVE)
createdAt DateTime @default(now()) @db.Timestamptz(3)
updatedAt DateTime @updatedAt @db.Timestamptz(3)
credentials UserCredential[]
sessions UserSession[]
platformRoles UserPlatformRole[]
gameServers GameServer[]
auditEvents AuditEvent[]
@@map("users")
}
model UserCredential {
id String @id @default(uuid())
userId String
passwordHash String
createdAt DateTime @default(now()) @db.Timestamptz(3)
updatedAt DateTime @updatedAt @db.Timestamptz(3)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@map("user_credentials")
}
model UserSession {
id String @id @default(uuid())
userId String
tokenHash String @unique
expiresAt DateTime @db.Timestamptz(3)
revokedAt DateTime? @db.Timestamptz(3)
ipAddress String?
userAgent String?
createdAt DateTime @default(now()) @db.Timestamptz(3)
updatedAt DateTime @updatedAt @db.Timestamptz(3)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([expiresAt])
@@map("user_sessions")
}
model PlatformRole {
id String @id @default(uuid())
name String @unique
description String?
createdAt DateTime @default(now()) @db.Timestamptz(3)
updatedAt DateTime @updatedAt @db.Timestamptz(3)
userRoles UserPlatformRole[]
@@map("platform_roles")
}
model UserPlatformRole {
id String @id @default(uuid())
userId String
roleId String
createdAt DateTime @default(now()) @db.Timestamptz(3)
updatedAt DateTime @updatedAt @db.Timestamptz(3)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
role PlatformRole @relation(fields: [roleId], references: [id], onDelete: Cascade)
@@unique([userId, roleId])
@@index([userId])
@@index([roleId])
@@map("user_platform_roles")
}
model GameServer {
id String @id @default(uuid())
userId String
planId String?
nodeId String?
name String
status GameServerStatus @default(PENDING)
version Int @default(1)
createdAt DateTime @default(now()) @db.Timestamptz(3)
updatedAt DateTime @updatedAt @db.Timestamptz(3)
user User @relation(fields: [userId], references: [id])
plan Plan? @relation(fields: [planId], references: [id])
node GameNode? @relation(fields: [nodeId], references: [id])
stateTransitions GameServerStateTransition[]
@@index([userId])
@@index([nodeId])
@@index([status])
@@map("game_servers")
}
model GameServerStateTransition {
id String @id @default(uuid())
gameServerId String
fromStatus GameServerStatus?
toStatus GameServerStatus
reason String?
metadata Json?
createdAt DateTime @default(now()) @db.Timestamptz(3)
gameServer GameServer @relation(fields: [gameServerId], references: [id], onDelete: Cascade)
@@index([gameServerId])
@@index([createdAt])
@@map("game_server_state_transitions")
}
model GameNode {
id String @id @default(uuid())
name String @unique
hostname String
status GameNodeStatus @default(OFFLINE)
maxServers Int @default(0)
activeServers Int @default(0)
metadata Json?
createdAt DateTime @default(now()) @db.Timestamptz(3)
updatedAt DateTime @updatedAt @db.Timestamptz(3)
gameServers GameServer[]
heartbeats GameNodeHeartbeat[]
@@map("game_nodes")
}
model GameNodeHeartbeat {
id String @id @default(uuid())
nodeId String
payload Json?
createdAt DateTime @default(now()) @db.Timestamptz(3)
node GameNode @relation(fields: [nodeId], references: [id], onDelete: Cascade)
@@index([nodeId])
@@index([createdAt])
@@map("game_node_heartbeats")
}
model Plan {
id String @id @default(uuid())
name String @unique
slug String @unique
description String?
isFree Boolean @default(false)
isActive Boolean @default(true)
maxRamMb Int
maxCpuCores Float
maxStorageMb Int
metadata Json?
createdAt DateTime @default(now()) @db.Timestamptz(3)
updatedAt DateTime @updatedAt @db.Timestamptz(3)
gameServers GameServer[]
@@map("plans")
}
model FeatureFlag {
id String @id @default(uuid())
key String @unique
description String?
enabled Boolean @default(false)
metadata Json?
createdAt DateTime @default(now()) @db.Timestamptz(3)
updatedAt DateTime @updatedAt @db.Timestamptz(3)
@@map("feature_flags")
}
model SystemSetting {
id String @id @default(uuid())
key String @unique
value Json
createdAt DateTime @default(now()) @db.Timestamptz(3)
updatedAt DateTime @updatedAt @db.Timestamptz(3)
@@map("system_settings")
}
model AuditEvent {
id String @id @default(uuid())
userId String?
action String
entityType String?
entityId String?
metadata Json?
ipAddress String?
createdAt DateTime @default(now()) @db.Timestamptz(3)
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
@@index([userId])
@@index([action])
@@index([entityType, entityId])
@@index([createdAt])
@@map("audit_events")
}
model JobRecord {
id String @id @default(uuid())
queue String
jobName String
status JobStatus @default(PENDING)
payload Json?
result Json?
error String?
attempts Int @default(0)
startedAt DateTime? @db.Timestamptz(3)
completedAt DateTime? @db.Timestamptz(3)
createdAt DateTime @default(now()) @db.Timestamptz(3)
updatedAt DateTime @updatedAt @db.Timestamptz(3)
@@index([queue, status])
@@index([createdAt])
@@map("job_records")
}

View File

@@ -0,0 +1,40 @@
export { PrismaClient, Prisma } from '@prisma/client';
export type {
User,
UserCredential,
UserSession,
PlatformRole,
UserPlatformRole,
GameServer,
GameServerStateTransition,
GameNode,
GameNodeHeartbeat,
Plan,
FeatureFlag,
SystemSetting,
AuditEvent,
JobRecord,
} from '@prisma/client';
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log:
process.env['LOG_LEVEL'] === 'debug'
? ['query', 'info', 'warn', 'error']
: ['warn', 'error'],
});
if (process.env['NODE_ENV'] !== 'production') {
globalForPrisma.prisma = prisma;
}
export function createPrismaClient(): PrismaClient {
return new PrismaClient();
}

View File

@@ -0,0 +1,9 @@
{
"extends": "@hexahost/typescript-config/library.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,18 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"moduleResolution": "node",
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"]
}
}

View File

@@ -0,0 +1,11 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./base.json",
"compilerOptions": {
"composite": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./base.json",
"compilerOptions": {
"module": "commonjs",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictBindCallApply": true,
"noFallthroughCasesInSwitch": true,
"outDir": "./dist",
"incremental": true
}
}

View File

@@ -0,0 +1,10 @@
{
"name": "@hexahost/typescript-config",
"version": "0.0.0",
"private": true,
"exports": {
"./base.json": "./base.json",
"./nestjs.json": "./nestjs.json",
"./library.json": "./library.json"
}
}

24
packages/ui/package.json Normal file
View File

@@ -0,0 +1,24 @@
{
"name": "@hexahost/ui",
"version": "0.0.1",
"private": true,
"sideEffects": false,
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"typecheck": "tsc --noEmit",
"lint": "tsc --noEmit"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"devDependencies": {
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"typescript": "^5.7.3"
}
}

View File

@@ -0,0 +1,79 @@
import { forwardRef, type ButtonHTMLAttributes } from "react";
import { cn } from "../lib/cn";
export type ButtonVariant = "primary" | "secondary" | "ghost" | "danger";
export type ButtonSize = "sm" | "md" | "lg";
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
size?: ButtonSize;
isLoading?: boolean;
}
const variantStyles: Record<ButtonVariant, string> = {
primary:
"bg-sky-600 text-white hover:bg-sky-500 focus-visible:ring-sky-500 dark:bg-sky-500 dark:hover:bg-sky-400",
secondary:
"border border-zinc-300 bg-white text-zinc-900 hover:bg-zinc-50 focus-visible:ring-zinc-400 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-100 dark:hover:bg-zinc-800",
ghost:
"text-zinc-700 hover:bg-zinc-100 focus-visible:ring-zinc-400 dark:text-zinc-300 dark:hover:bg-zinc-800",
danger:
"bg-red-600 text-white hover:bg-red-500 focus-visible:ring-red-500 dark:bg-red-500 dark:hover:bg-red-400",
};
const sizeStyles: Record<ButtonSize, string> = {
sm: "h-8 px-3 text-sm",
md: "h-10 px-4 text-sm",
lg: "h-11 px-6 text-base",
};
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{
className,
variant = "primary",
size = "md",
isLoading = false,
disabled,
type = "button",
children,
...props
},
ref,
) => {
const isDisabled = disabled || isLoading;
return (
<button
ref={ref}
type={type}
disabled={isDisabled}
aria-busy={isLoading || undefined}
aria-disabled={isDisabled || undefined}
className={cn(
"inline-flex items-center justify-center gap-2 rounded-md font-medium transition-colors",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
"dark:focus-visible:ring-offset-zinc-950",
"disabled:pointer-events-none disabled:opacity-50",
variantStyles[variant],
sizeStyles[size],
className,
)}
{...props}
>
{isLoading ? (
<>
<span
className="size-4 animate-spin rounded-full border-2 border-current border-t-transparent"
aria-hidden="true"
/>
<span className="sr-only">Loading</span>
</>
) : null}
{children}
</button>
);
},
);
Button.displayName = "Button";

View File

@@ -0,0 +1,97 @@
import { forwardRef, type HTMLAttributes } from "react";
import { cn } from "../lib/cn";
export interface CardProps extends HTMLAttributes<HTMLDivElement> {}
export const Card = forwardRef<HTMLDivElement, CardProps>(
({ className, children, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border border-zinc-200 bg-white shadow-sm",
"dark:border-zinc-800 dark:bg-zinc-900",
className,
)}
{...props}
>
{children}
</div>
),
);
Card.displayName = "Card";
export interface CardHeaderProps extends HTMLAttributes<HTMLDivElement> {}
export const CardHeader = forwardRef<HTMLDivElement, CardHeaderProps>(
({ className, children, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col gap-1.5 border-b border-zinc-200 px-6 py-4 dark:border-zinc-800", className)}
{...props}
>
{children}
</div>
),
);
CardHeader.displayName = "CardHeader";
export interface CardTitleProps extends HTMLAttributes<HTMLHeadingElement> {}
export const CardTitle = forwardRef<HTMLHeadingElement, CardTitleProps>(
({ className, children, ...props }, ref) => (
<h3
ref={ref}
className={cn("text-base font-semibold tracking-tight text-zinc-900 dark:text-zinc-50", className)}
{...props}
>
{children}
</h3>
),
);
CardTitle.displayName = "CardTitle";
export interface CardDescriptionProps extends HTMLAttributes<HTMLParagraphElement> {}
export const CardDescription = forwardRef<HTMLParagraphElement, CardDescriptionProps>(
({ className, children, ...props }, ref) => (
<p ref={ref} className={cn("text-sm text-zinc-600 dark:text-zinc-400", className)} {...props}>
{children}
</p>
),
);
CardDescription.displayName = "CardDescription";
export interface CardContentProps extends HTMLAttributes<HTMLDivElement> {}
export const CardContent = forwardRef<HTMLDivElement, CardContentProps>(
({ className, children, ...props }, ref) => (
<div ref={ref} className={cn("px-6 py-4", className)} {...props}>
{children}
</div>
),
);
CardContent.displayName = "CardContent";
export interface CardFooterProps extends HTMLAttributes<HTMLDivElement> {}
export const CardFooter = forwardRef<HTMLDivElement, CardFooterProps>(
({ className, children, ...props }, ref) => (
<div
ref={ref}
className={cn(
"flex items-center border-t border-zinc-200 px-6 py-4 dark:border-zinc-800",
className,
)}
{...props}
>
{children}
</div>
),
);
CardFooter.displayName = "CardFooter";

16
packages/ui/src/index.ts Normal file
View File

@@ -0,0 +1,16 @@
export { Button, type ButtonProps, type ButtonSize, type ButtonVariant } from "./components/Button";
export {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
type CardContentProps,
type CardDescriptionProps,
type CardFooterProps,
type CardHeaderProps,
type CardProps,
type CardTitleProps,
} from "./components/Card";
export { cn } from "./lib/cn";

View File

@@ -0,0 +1,3 @@
export function cn(...classes: Array<string | false | null | undefined>): string {
return classes.filter(Boolean).join(" ");
}

20
packages/ui/tsconfig.json Normal file
View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"noEmit": true,
"skipLibCheck": true,
"isolatedModules": true,
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}