initial commit

This commit is contained in:
Syahdan 2026-04-29 17:01:16 +07:00
commit 33e75c056f
77 changed files with 4329 additions and 0 deletions

51
.gitignore vendored Normal file
View file

@ -0,0 +1,51 @@
# Dependencies
node_modules
.pnp
.pnp.js
# Build outputs
dist
build
*.tsbuildinfo
# Environment variables
.env
.env*.local
# IDEs and editors
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.idea
*.swp
*.swo
*~
.DS_Store
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Turbo
.turbo
.nx
# Better-T-Stack
.alchemy
# Testing
coverage
.nyc_output
# Misc
*.tgz
.cache
tmp
temp

103
README.md Normal file
View file

@ -0,0 +1,103 @@
# minmon
This project was created with [Better-T-Stack](https://github.com/AmanVarshney01/create-better-t-stack), a modern TypeScript stack that combines React, TanStack Router, Elysia, TRPC, and more.
## Features
- **TypeScript** - For type safety and improved developer experience
- **TanStack Router** - File-based routing with full type safety
- **TailwindCSS** - Utility-first CSS for rapid UI development
- **Shared UI package** - shadcn/ui primitives live in `packages/ui`
- **Elysia** - Type-safe, high-performance framework
- **tRPC** - End-to-end type-safe APIs
- **Bun** - Runtime environment
- **Drizzle** - TypeScript-first ORM
- **PostgreSQL** - Database engine
- **Authentication** - Better-Auth
- **Electrobun** - Lightweight desktop shell for web frontends
- **Turborepo** - Optimized monorepo build system
## Getting Started
First, install the dependencies:
```bash
bun install
```
## Database Setup
This project uses PostgreSQL with Drizzle ORM.
1. Make sure you have a PostgreSQL database set up.
2. Update your `apps/server/.env` file with your PostgreSQL connection details.
3. Apply the schema to your database:
```bash
bun run db:push
```
Then, run the development server:
```bash
bun run dev
```
Open [http://localhost:5173](http://localhost:5173) in your browser to see the web application.
The API is running at [http://localhost:3000](http://localhost:3000).
## UI Customization
React web apps in this stack share shadcn/ui primitives through `packages/ui`.
- Change design tokens and global styles in `packages/ui/src/styles/globals.css`
- Update shared primitives in `packages/ui/src/components/*`
- Adjust shadcn aliases or style config in `packages/ui/components.json` and `apps/web/components.json`
### Add more shared components
Run this from the project root to add more primitives to the shared UI package:
```bash
npx shadcn@latest add accordion dialog popover sheet table -c packages/ui
```
Import shared components like this:
```tsx
import { Button } from "@minmon/ui/components/button";
```
### Add app-specific blocks
If you want to add app-specific blocks instead of shared primitives, run the shadcn CLI from `apps/web`.
## Project Structure
```
minmon/
├── apps/
│ ├── web/ # Frontend application (React + TanStack Router)
│ └── server/ # Backend API (Elysia, TRPC)
├── packages/
│ ├── ui/ # Shared shadcn/ui components and styles
│ ├── api/ # API layer / business logic
│ ├── auth/ # Authentication configuration & logic
│ └── db/ # Database schema & queries
```
## Available Scripts
- `bun run dev`: Start all applications in development mode
- `bun run build`: Build all applications
- `bun run dev:web`: Start only the web application
- `bun run dev:server`: Start only the server
- `bun run check-types`: Check TypeScript types across all apps
- `bun run db:push`: Push schema changes to database
- `bun run db:generate`: Generate database client/types
- `bun run db:migrate`: Run database migrations
- `bun run db:studio`: Open database studio UI
- `bun run dev:desktop`: Start the Electrobun desktop app with HMR
- `bun run build:desktop`: Build the stable Electrobun desktop app
- `bun run build:desktop:canary`: Build the canary Electrobun desktop app

1
apps/desktop/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
artifacts

View file

@ -0,0 +1,35 @@
import type { ElectrobunConfig } from "electrobun";
const webBuildDir = "../web/dist";
export default {
app: {
name: "minmon",
identifier: "dev.bettertstack.minmon.desktop",
version: "0.0.1",
},
runtime: {
exitOnLastWindowClosed: true,
},
build: {
bun: {
entrypoint: "src/bun/index.ts",
},
copy: {
[webBuildDir]: "views/mainview",
},
watchIgnore: [`${webBuildDir}/**`],
mac: {
bundleCEF: true,
defaultRenderer: "cef",
},
linux: {
bundleCEF: true,
defaultRenderer: "cef",
},
win: {
bundleCEF: true,
defaultRenderer: "cef",
},
},
} satisfies ElectrobunConfig;

23
apps/desktop/package.json Normal file
View file

@ -0,0 +1,23 @@
{
"name": "desktop",
"private": true,
"type": "module",
"scripts": {
"start": "turbo -F web build && electrobun dev",
"dev": "electrobun dev --watch",
"dev:hmr": "concurrently \"bun run hmr\" \"bun run start\"",
"hmr": "turbo -F web dev",
"build": "turbo -F web build && electrobun build",
"build:stable": "turbo -F web build && electrobun build --env=stable",
"build:canary": "turbo -F web build && electrobun build --env=canary",
"check-types": "tsc --noEmit"
},
"dependencies": {
"electrobun": "^1.15.1"
},
"devDependencies": {
"@types/bun": "catalog:",
"concurrently": "^9.1.0",
"typescript": "catalog:"
}
}

View file

@ -0,0 +1,35 @@
import { BrowserWindow, Updater } from "electrobun/bun";
const DEV_SERVER_PORT = 3001;
const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`;
// Check if the web dev server is running for HMR
async function getMainViewUrl(): Promise<string> {
const channel = await Updater.localInfo.channel();
if (channel === "dev") {
try {
await fetch(DEV_SERVER_URL, { method: "HEAD" });
console.log(`HMR enabled: Using web dev server at ${DEV_SERVER_URL}`);
return DEV_SERVER_URL;
} catch {
console.log('Web dev server not running. Run "bun run dev:hmr" for HMR support.');
}
}
return "views://mainview/index.html";
}
const url = await getMainViewUrl();
new BrowserWindow({
title: "minmon",
url,
frame: {
width: 1280,
height: 820,
x: 120,
y: 120,
},
});
console.log("Electrobun desktop shell started.");

View file

@ -0,0 +1,14 @@
{
"extends": "../../packages/config/tsconfig.base.json",
"compilerOptions": {
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"noEmit": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*.ts", "electrobun.config.ts"]
}

55
apps/server/.gitignore vendored Normal file
View file

@ -0,0 +1,55 @@
# prod
dist/
/build
/out/
# dev
.yarn/
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
.vscode/*
!.vscode/launch.json
!.vscode/*.code-snippets
.idea/workspace.xml
.idea/usage.statistics.xml
.idea/shelf
.wrangler
.alchemy
/.next/
.vercel
prisma/generated/
# deps
node_modules/
/node_modules
/.pnp
.pnp.*
# env
.env*
.env.production
!.env.example
.dev.vars
# logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# misc
.DS_Store
*.pem
# local db
*.db*
# typescript
*.tsbuildinfo
next-env.d.ts

33
apps/server/package.json Normal file
View file

@ -0,0 +1,33 @@
{
"name": "server",
"type": "module",
"main": "src/index.ts",
"scripts": {
"build": "tsdown",
"check-types": "tsc -b",
"compile": "bun build --compile --minify --sourcemap --bytecode ./src/index.ts --outfile server",
"dev": "bun run --hot src/index.ts",
"start": "bun run dist/index.mjs"
},
"dependencies": {
"@elysiajs/cors": "^1.4.1",
"@elysiajs/trpc": "^1.1.0",
"@hono/trpc-server": "^0.4.0",
"@minmon/api": "workspace:*",
"@minmon/auth": "workspace:*",
"@minmon/db": "workspace:*",
"@minmon/env": "workspace:*",
"@sinclair/typebox": "^0.34.49",
"@trpc/server": "catalog:",
"better-auth": "catalog:",
"dotenv": "catalog:",
"elysia": "catalog:",
"zod": "catalog:"
},
"devDependencies": {
"@minmon/config": "workspace:*",
"@types/bun": "catalog:",
"tsdown": "^0.21.9",
"typescript": "catalog:"
}
}

37
apps/server/src/index.ts Normal file
View file

@ -0,0 +1,37 @@
import { cors } from "@elysiajs/cors";
import { createContext } from "@minmon/api/context";
import { appRouter } from "@minmon/api/routers/index";
import { auth } from "@minmon/auth";
import { env } from "@minmon/env/server";
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { Elysia } from "elysia";
const app = new Elysia()
.use(
cors({
origin: env.CORS_ORIGIN,
methods: ["GET", "POST", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization"],
credentials: true,
}),
)
.all("/api/auth/*", async (context) => {
const { request, status } = context;
if (["POST", "GET"].includes(request.method)) {
return auth.handler(request);
}
return status(405);
})
.all("/trpc/*", async (context) => {
const res = await fetchRequestHandler({
endpoint: "/trpc",
router: appRouter,
req: context.request,
createContext: () => createContext({ context }),
});
return res;
})
.get("/", () => "OK")
.listen(3000, () => {
console.log("Server is running on http://localhost:3000");
});

11
apps/server/tsconfig.json Normal file
View file

@ -0,0 +1,11 @@
{
"extends": "@minmon/config/tsconfig.base.json",
"compilerOptions": {
"composite": true,
"outDir": "dist",
"paths": {
"@/*": ["./src/*"]
},
"jsx": "react-jsx"
}
}

View file

@ -0,0 +1,9 @@
import { defineConfig } from "tsdown";
export default defineConfig({
entry: "./src/index.ts",
format: "esm",
outDir: "./dist",
clean: true,
noExternal: [/@minmon\/.*/],
});

60
apps/web/.gitignore vendored Normal file
View file

@ -0,0 +1,60 @@
# Dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# Testing
/coverage
# Build outputs
/.next/
/out/
/build/
/dist/
.vinxi
.output
.react-router/
.tanstack/
.nitro/
# Deployment
.vercel
.netlify
.wrangler
.alchemy
# Environment & local files
.env*
!.env.example
.DS_Store
*.pem
*.local
# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
*.log*
# TypeScript
*.tsbuildinfo
next-env.d.ts
# IDE
.vscode/*
!.vscode/extensions.json
.idea
# Other
dev-dist
.wrangler
.dev.vars*
.open-next

24
apps/web/components.json Normal file
View file

@ -0,0 +1,24 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "base-lyra",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "../../packages/ui/src/styles/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@minmon/ui/lib/utils",
"ui": "@minmon/ui/components",
"lib": "@/lib",
"hooks": "@/hooks"
},
"menuColor": "default",
"menuAccent": "subtle",
"registries": {}
}

13
apps/web/index.html Normal file
View file

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>minmon</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

49
apps/web/package.json Normal file
View file

@ -0,0 +1,49 @@
{
"name": "web",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"serve": "vite preview",
"start": "vite",
"check-types": "vite build && tsc --noEmit"
},
"dependencies": {
"@hookform/resolvers": "^5.2.2",
"@minmon/api": "workspace:*",
"@minmon/auth": "workspace:*",
"@minmon/env": "workspace:*",
"@minmon/ui": "workspace:*",
"@tailwindcss/vite": "^4.2.2",
"@tanstack/react-form": "^1.28.0",
"@tanstack/react-query": "^5.90.12",
"@tanstack/react-router": "^1.168.22",
"@trpc/client": "catalog:",
"@trpc/server": "catalog:",
"@trpc/tanstack-react-query": "^11.16.0",
"better-auth": "catalog:",
"dotenv": "catalog:",
"lucide-react": "^1.8.0",
"next-themes": "catalog:",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"sonner": "^2.0.7",
"zod": "catalog:"
},
"devDependencies": {
"@minmon/config": "workspace:*",
"@tanstack/react-query-devtools": "^5.91.1",
"@tanstack/react-router-devtools": "^1.166.13",
"@tanstack/router-plugin": "^1.167.22",
"@types/node": "^22.13.14",
"@types/react": "^19.2.14",
"@types/react-dom": "catalog:",
"@vitejs/plugin-react": "^6.0.1",
"postcss": "^8.5.10",
"tailwindcss": "^4.2.2",
"typescript": "catalog:",
"vite": "^8.0.8"
}
}

View file

@ -0,0 +1,32 @@
import { Link } from "@tanstack/react-router";
import { ModeToggle } from "./mode-toggle";
import UserMenu from "./user-menu";
export default function Header() {
const links = [
{ to: "/", label: "Home" },
{ to: "/dashboard", label: "Dashboard" },
] as const;
return (
<div>
<div className="flex flex-row items-center justify-between px-2 py-1">
<nav className="flex gap-4 text-lg">
{links.map(({ to, label }) => {
return (
<Link key={to} to={to}>
{label}
</Link>
);
})}
</nav>
<div className="flex items-center gap-2">
<ModeToggle />
<UserMenu />
</div>
</div>
<hr />
</div>
);
}

View file

@ -0,0 +1,9 @@
import { Loader2 } from "lucide-react";
export default function Loader() {
return (
<div className="flex h-full items-center justify-center pt-8">
<Loader2 className="animate-spin" />
</div>
);
}

View file

@ -0,0 +1,29 @@
import { Button } from "@minmon/ui/components/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@minmon/ui/components/dropdown-menu";
import { Moon, Sun } from "lucide-react";
import { useTheme } from "@/components/theme-provider";
export function ModeToggle() {
const { setTheme } = useTheme();
return (
<DropdownMenu>
<DropdownMenuTrigger render={<Button variant="outline" size="icon" />}>
<Sun className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
<span className="sr-only">Toggle theme</span>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>Light</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>Dark</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>System</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View file

@ -0,0 +1,135 @@
import { Button } from "@minmon/ui/components/button";
import { Input } from "@minmon/ui/components/input";
import { Label } from "@minmon/ui/components/label";
import { useForm } from "@tanstack/react-form";
import { useNavigate } from "@tanstack/react-router";
import { toast } from "sonner";
import z from "zod";
import { authClient } from "@/lib/auth-client";
import Loader from "./loader";
export default function SignInForm({ onSwitchToSignUp }: { onSwitchToSignUp: () => void }) {
const navigate = useNavigate({
from: "/",
});
const { isPending } = authClient.useSession();
const form = useForm({
defaultValues: {
email: "",
password: "",
},
onSubmit: async ({ value }) => {
await authClient.signIn.email(
{
email: value.email,
password: value.password,
},
{
onSuccess: () => {
navigate({
to: "/dashboard",
});
toast.success("Sign in successful");
},
onError: (error) => {
toast.error(error.error.message || error.error.statusText);
},
},
);
},
validators: {
onSubmit: z.object({
email: z.email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"),
}),
},
});
if (isPending) {
return <Loader />;
}
return (
<div className="mx-auto w-full mt-10 max-w-md p-6">
<h1 className="mb-6 text-center text-3xl font-bold">Welcome Back</h1>
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}}
className="space-y-4"
>
<div>
<form.Field name="email">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Email</Label>
<Input
id={field.name}
name={field.name}
type="email"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors.map((error) => (
<p key={error?.message} className="text-red-500">
{error?.message}
</p>
))}
</div>
)}
</form.Field>
</div>
<div>
<form.Field name="password">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Password</Label>
<Input
id={field.name}
name={field.name}
type="password"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors.map((error) => (
<p key={error?.message} className="text-red-500">
{error?.message}
</p>
))}
</div>
)}
</form.Field>
</div>
<form.Subscribe
selector={(state) => ({ canSubmit: state.canSubmit, isSubmitting: state.isSubmitting })}
>
{({ canSubmit, isSubmitting }) => (
<Button type="submit" className="w-full" disabled={!canSubmit || isSubmitting}>
{isSubmitting ? "Submitting..." : "Sign In"}
</Button>
)}
</form.Subscribe>
</form>
<div className="mt-4 text-center">
<Button
variant="link"
onClick={onSwitchToSignUp}
className="text-indigo-600 hover:text-indigo-800"
>
Need an account? Sign Up
</Button>
</div>
</div>
);
}

View file

@ -0,0 +1,160 @@
import { Button } from "@minmon/ui/components/button";
import { Input } from "@minmon/ui/components/input";
import { Label } from "@minmon/ui/components/label";
import { useForm } from "@tanstack/react-form";
import { useNavigate } from "@tanstack/react-router";
import { toast } from "sonner";
import z from "zod";
import { authClient } from "@/lib/auth-client";
import Loader from "./loader";
export default function SignUpForm({ onSwitchToSignIn }: { onSwitchToSignIn: () => void }) {
const navigate = useNavigate({
from: "/",
});
const { isPending } = authClient.useSession();
const form = useForm({
defaultValues: {
email: "",
password: "",
name: "",
},
onSubmit: async ({ value }) => {
await authClient.signUp.email(
{
email: value.email,
password: value.password,
name: value.name,
},
{
onSuccess: () => {
navigate({
to: "/dashboard",
});
toast.success("Sign up successful");
},
onError: (error) => {
toast.error(error.error.message || error.error.statusText);
},
},
);
},
validators: {
onSubmit: z.object({
name: z.string().min(2, "Name must be at least 2 characters"),
email: z.email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"),
}),
},
});
if (isPending) {
return <Loader />;
}
return (
<div className="mx-auto w-full mt-10 max-w-md p-6">
<h1 className="mb-6 text-center text-3xl font-bold">Create Account</h1>
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}}
className="space-y-4"
>
<div>
<form.Field name="name">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Name</Label>
<Input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors.map((error) => (
<p key={error?.message} className="text-red-500">
{error?.message}
</p>
))}
</div>
)}
</form.Field>
</div>
<div>
<form.Field name="email">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Email</Label>
<Input
id={field.name}
name={field.name}
type="email"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors.map((error) => (
<p key={error?.message} className="text-red-500">
{error?.message}
</p>
))}
</div>
)}
</form.Field>
</div>
<div>
<form.Field name="password">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Password</Label>
<Input
id={field.name}
name={field.name}
type="password"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors.map((error) => (
<p key={error?.message} className="text-red-500">
{error?.message}
</p>
))}
</div>
)}
</form.Field>
</div>
<form.Subscribe
selector={(state) => ({ canSubmit: state.canSubmit, isSubmitting: state.isSubmitting })}
>
{({ canSubmit, isSubmitting }) => (
<Button type="submit" className="w-full" disabled={!canSubmit || isSubmitting}>
{isSubmitting ? "Submitting..." : "Sign Up"}
</Button>
)}
</form.Subscribe>
</form>
<div className="mt-4 text-center">
<Button
variant="link"
onClick={onSwitchToSignIn}
className="text-indigo-600 hover:text-indigo-800"
>
Already have an account? Sign In
</Button>
</div>
</div>
);
}

View file

@ -0,0 +1,11 @@
import { ThemeProvider as NextThemesProvider } from "next-themes";
import * as React from "react";
export function ThemeProvider({
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}
export { useTheme } from "next-themes";

View file

@ -0,0 +1,62 @@
import { Button } from "@minmon/ui/components/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@minmon/ui/components/dropdown-menu";
import { Skeleton } from "@minmon/ui/components/skeleton";
import { Link, useNavigate } from "@tanstack/react-router";
import { authClient } from "@/lib/auth-client";
export default function UserMenu() {
const navigate = useNavigate();
const { data: session, isPending } = authClient.useSession();
if (isPending) {
return <Skeleton className="h-9 w-24" />;
}
if (!session) {
return (
<Link to="/login">
<Button variant="outline">Sign In</Button>
</Link>
);
}
return (
<DropdownMenu>
<DropdownMenuTrigger render={<Button variant="outline" />}>
{session.user.name}
</DropdownMenuTrigger>
<DropdownMenuContent className="bg-card">
<DropdownMenuGroup>
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>{session.user.email}</DropdownMenuItem>
<DropdownMenuItem
variant="destructive"
onClick={() => {
authClient.signOut({
fetchOptions: {
onSuccess: () => {
navigate({
to: "/",
});
},
},
});
}}
>
Sign Out
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
}

1
apps/web/src/index.css Normal file
View file

@ -0,0 +1 @@
@import "@minmon/ui/globals.css";

View file

@ -0,0 +1,6 @@
import { env } from "@minmon/env/web";
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
baseURL: env.VITE_SERVER_URL,
});

35
apps/web/src/main.tsx Normal file
View file

@ -0,0 +1,35 @@
import { QueryClientProvider } from "@tanstack/react-query";
import { RouterProvider, createRouter } from "@tanstack/react-router";
import ReactDOM from "react-dom/client";
import Loader from "./components/loader";
import { routeTree } from "./routeTree.gen";
import { queryClient, trpc } from "./utils/trpc";
const router = createRouter({
routeTree,
defaultPreload: "intent",
scrollRestoration: true,
defaultPendingComponent: () => <Loader />,
context: { trpc, queryClient },
Wrap: function WrapComponent({ children }: { children: React.ReactNode }) {
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
},
});
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
const rootElement = document.getElementById("app");
if (!rootElement) {
throw new Error("Root element not found");
}
if (!rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement);
root.render(<RouterProvider router={router} />);
}

View file

@ -0,0 +1,59 @@
import { Toaster } from "@minmon/ui/components/sonner";
import type { QueryClient } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { HeadContent, Outlet, createRootRouteWithContext } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
import Header from "@/components/header";
import { ThemeProvider } from "@/components/theme-provider";
import type { trpc } from "@/utils/trpc";
import "../index.css";
export interface RouterAppContext {
trpc: typeof trpc;
queryClient: QueryClient;
}
export const Route = createRootRouteWithContext<RouterAppContext>()({
component: RootComponent,
head: () => ({
meta: [
{
title: "minmon",
},
{
name: "description",
content: "minmon is a web application",
},
],
links: [
{
rel: "icon",
href: "/favicon.ico",
},
],
}),
});
function RootComponent() {
return (
<>
<HeadContent />
<ThemeProvider
attribute="class"
defaultTheme="dark"
disableTransitionOnChange
storageKey="vite-ui-theme"
>
<div className="grid grid-rows-[auto_1fr] h-svh">
<Header />
<Outlet />
</div>
<Toaster richColors />
</ThemeProvider>
<TanStackRouterDevtools position="bottom-left" />
<ReactQueryDevtools position="bottom" buttonPosition="bottom-right" />
</>
);
}

View file

@ -0,0 +1,33 @@
import { useQuery } from "@tanstack/react-query";
import { createFileRoute, redirect } from "@tanstack/react-router";
import { authClient } from "@/lib/auth-client";
import { trpc } from "@/utils/trpc";
export const Route = createFileRoute("/dashboard")({
component: RouteComponent,
beforeLoad: async () => {
const session = await authClient.getSession();
if (!session.data) {
redirect({
to: "/login",
throw: true,
});
}
return { session };
},
});
function RouteComponent() {
const { session } = Route.useRouteContext();
const privateData = useQuery(trpc.privateData.queryOptions());
return (
<div>
<h1>Dashboard</h1>
<p>Welcome {session.data?.user.name}</p>
<p>API: {privateData.data?.message}</p>
</div>
);
}

View file

@ -0,0 +1,51 @@
import { useQuery } from "@tanstack/react-query";
import { createFileRoute } from "@tanstack/react-router";
import { trpc } from "@/utils/trpc";
export const Route = createFileRoute("/")({
component: HomeComponent,
});
const TITLE_TEXT = `
`;
function HomeComponent() {
const healthCheck = useQuery(trpc.healthCheck.queryOptions());
return (
<div className="container mx-auto max-w-3xl px-4 py-2">
<pre className="overflow-x-auto font-mono text-sm">{TITLE_TEXT}</pre>
<div className="grid gap-6">
<section className="rounded-lg border p-4">
<h2 className="mb-2 font-medium">API Status</h2>
<div className="flex items-center gap-2">
<div
className={`h-2 w-2 rounded-full ${healthCheck.data ? "bg-green-500" : "bg-red-500"}`}
/>
<span className="text-sm text-muted-foreground">
{healthCheck.isLoading
? "Checking..."
: healthCheck.data
? "Connected"
: "Disconnected"}
</span>
</div>
</section>
</div>
</div>
);
}

View file

@ -0,0 +1,19 @@
import { createFileRoute } from "@tanstack/react-router";
import { useState } from "react";
import SignInForm from "@/components/sign-in-form";
import SignUpForm from "@/components/sign-up-form";
export const Route = createFileRoute("/login")({
component: RouteComponent,
});
function RouteComponent() {
const [showSignIn, setShowSignIn] = useState(false);
return showSignIn ? (
<SignInForm onSwitchToSignUp={() => setShowSignIn(false)} />
) : (
<SignUpForm onSwitchToSignIn={() => setShowSignIn(true)} />
);
}

View file

@ -0,0 +1,38 @@
import type { AppRouter } from "@minmon/api/routers/index";
import { env } from "@minmon/env/web";
import { QueryCache, QueryClient } from "@tanstack/react-query";
import { createTRPCClient, httpBatchLink } from "@trpc/client";
import { createTRPCOptionsProxy } from "@trpc/tanstack-react-query";
import { toast } from "sonner";
export const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error, query) => {
toast.error(error.message, {
action: {
label: "retry",
onClick: query.invalidate,
},
});
},
}),
});
export const trpcClient = createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: `${env.VITE_SERVER_URL}/trpc`,
fetch(url, options) {
return fetch(url, {
...options,
credentials: "include",
});
},
}),
],
});
export const trpc = createTRPCOptionsProxy<AppRouter>({
client: trpcClient,
queryClient,
});

17
apps/web/tsconfig.json Normal file
View file

@ -0,0 +1,17 @@
{
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"jsx": "react-jsx",
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"skipLibCheck": true,
"types": ["vite/client"],
"rootDirs": ["."],
"paths": {
"@/*": ["./src/*"],
"@minmon/ui/*": ["../../packages/ui/src/*"]
}
}
}

21
apps/web/vite.config.ts Normal file
View file

@ -0,0 +1,21 @@
import tailwindcss from "@tailwindcss/vite";
import { tanstackRouter } from "@tanstack/router-plugin/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
export default defineConfig({
server: {
port: 3001,
},
resolve: {
tsconfigPaths: true,
},
plugins: [
tailwindcss(),
tanstackRouter({
target: "react",
autoCodeSplitting: true,
}),
react(),
],
});

31
bts.jsonc Normal file
View file

@ -0,0 +1,31 @@
// Better-T-Stack
//
// Website: https://www.better-t-stack.dev/
// Stack Builder: https://www.better-t-stack.dev/new
// Analytics: https://www.better-t-stack.dev/analytics
// Showcase: https://www.better-t-stack.dev/showcase
// Sponsor: https://github.com/sponsors/AmanVarshney01
//
// Add new addons with: bun create better-t-stack add
// This file is safe to delete
{
"$schema": "https://r2.better-t-stack.dev/schema.json",
"version": "3.27.5",
"createdAt": "2026-04-29T09:59:41.200Z",
"reproducibleCommand": "bun create better-t-stack@latest minmon --frontend tanstack-router --backend elysia --runtime bun --database postgres --orm drizzle --api trpc --auth better-auth --payments none --addons electrobun turborepo --examples none --db-setup docker --web-deploy none --server-deploy none --git --package-manager bun --install",
"database": "postgres",
"orm": "drizzle",
"backend": "elysia",
"runtime": "bun",
"frontend": ["tanstack-router"],
"addons": ["electrobun", "turborepo"],
"examples": [],
"auth": "better-auth",
"payments": "none",
"packageManager": "bun",
"dbSetup": "docker",
"api": "trpc",
"webDeploy": "none",
"serverDeploy": "none",
}

1663
bun.lock Normal file

File diff suppressed because it is too large Load diff

53
package.json Normal file
View file

@ -0,0 +1,53 @@
{
"name": "minmon",
"private": true,
"workspaces": {
"packages": [
"apps/*",
"packages/*"
],
"catalog": {
"dotenv": "^17.2.2",
"zod": "^4.1.13",
"typescript": "^6",
"@types/bun": "^1.3.4",
"elysia": "^1.4.28",
"@trpc/server": "^11.16.0",
"better-auth": "1.5.5",
"next-themes": "^0.4.6",
"@trpc/client": "^11.16.0",
"@types/react-dom": "^19.2.3"
}
},
"type": "module",
"scripts": {
"dev": "turbo dev",
"build": "turbo build",
"check-types": "turbo check-types",
"dev:web": "turbo -F web dev",
"dev:desktop": "turbo -F desktop dev:hmr",
"build:desktop": "turbo -F desktop build:stable",
"build:desktop:canary": "turbo -F desktop build:canary",
"dev:server": "turbo -F server dev",
"db:push": "turbo -F @minmon/db db:push",
"db:studio": "turbo -F @minmon/db db:studio",
"db:generate": "turbo -F @minmon/db db:generate",
"db:migrate": "turbo -F @minmon/db db:migrate",
"db:start": "turbo -F @minmon/db db:start",
"db:watch": "turbo -F @minmon/db db:watch",
"db:stop": "turbo -F @minmon/db db:stop",
"db:down": "turbo -F @minmon/db db:down"
},
"dependencies": {
"@minmon/env": "workspace:*",
"dotenv": "catalog:",
"zod": "catalog:"
},
"devDependencies": {
"@minmon/config": "workspace:*",
"@types/bun": "catalog:",
"turbo": "^2.8.12",
"typescript": "catalog:"
},
"packageManager": "bun@1.3.12"
}

34
packages/api/.gitignore vendored Normal file
View file

@ -0,0 +1,34 @@
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

27
packages/api/package.json Normal file
View file

@ -0,0 +1,27 @@
{
"name": "@minmon/api",
"type": "module",
"exports": {
".": {
"default": "./src/index.ts"
},
"./*": {
"default": "./src/*.ts"
}
},
"scripts": {},
"dependencies": {
"@minmon/auth": "workspace:*",
"@minmon/db": "workspace:*",
"@minmon/env": "workspace:*",
"@trpc/client": "catalog:",
"@trpc/server": "catalog:",
"dotenv": "catalog:",
"zod": "catalog:"
},
"devDependencies": {
"@minmon/config": "workspace:*",
"elysia": "catalog:",
"typescript": "catalog:"
}
}

View file

@ -0,0 +1,18 @@
import { auth } from "@minmon/auth";
import type { Context as ElysiaContext } from "elysia";
export type CreateContextOptions = {
context: ElysiaContext;
};
export async function createContext({ context }: CreateContextOptions) {
const session = await auth.api.getSession({
headers: context.request.headers,
});
return {
auth: null,
session,
};
}
export type Context = Awaited<ReturnType<typeof createContext>>;

25
packages/api/src/index.ts Normal file
View file

@ -0,0 +1,25 @@
import { initTRPC, TRPCError } from "@trpc/server";
import type { Context } from "./context";
export const t = initTRPC.context<Context>().create();
export const router = t.router;
export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Authentication required",
cause: "No session",
});
}
return next({
ctx: {
...ctx,
session: ctx.session,
},
});
});

View file

@ -0,0 +1,14 @@
import { protectedProcedure, publicProcedure, router } from "../index";
export const appRouter = router({
healthCheck: publicProcedure.query(() => {
return "OK";
}),
privateData: protectedProcedure.query(({ ctx }) => {
return {
message: "This is private",
user: ctx.session.user,
};
}),
});
export type AppRouter = typeof appRouter;

View file

@ -0,0 +1,10 @@
{
"extends": "@minmon/config/tsconfig.base.json",
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "dist",
"composite": true
}
}

34
packages/auth/.gitignore vendored Normal file
View file

@ -0,0 +1,34 @@
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

View file

@ -0,0 +1,24 @@
{
"name": "@minmon/auth",
"type": "module",
"exports": {
".": {
"default": "./src/index.ts"
},
"./*": {
"default": "./src/*.ts"
}
},
"scripts": {},
"dependencies": {
"@minmon/db": "workspace:*",
"@minmon/env": "workspace:*",
"better-auth": "catalog:",
"dotenv": "catalog:",
"zod": "catalog:"
},
"devDependencies": {
"@minmon/config": "workspace:*",
"typescript": "catalog:"
}
}

View file

@ -0,0 +1,33 @@
import { createDb } from "@minmon/db";
import * as schema from "@minmon/db/schema/auth";
import { env } from "@minmon/env/server";
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
export function createAuth() {
const db = createDb();
return betterAuth({
database: drizzleAdapter(db, {
provider: "pg",
schema: schema,
}),
trustedOrigins: [env.CORS_ORIGIN],
emailAndPassword: {
enabled: true,
},
secret: env.BETTER_AUTH_SECRET,
baseURL: env.BETTER_AUTH_URL,
advanced: {
defaultCookieAttributes: {
sameSite: "none",
secure: true,
httpOnly: true,
},
},
plugins: [],
});
}
export const auth = createAuth();

View file

@ -0,0 +1,10 @@
{
"extends": "@minmon/config/tsconfig.base.json",
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "dist",
"composite": true
}
}

View file

@ -0,0 +1,5 @@
{
"name": "@minmon/config",
"version": "0.0.0",
"private": true
}

View file

@ -0,0 +1,22 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ESNext"],
"verbatimModuleSyntax": true,
"strict": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"types": ["bun"]
}
}

35
packages/db/.gitignore vendored Normal file
View file

@ -0,0 +1,35 @@
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
/prisma/generated
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

View file

@ -0,0 +1,23 @@
name: minmon
services:
postgres:
image: postgres
container_name: minmon-postgres
environment:
POSTGRES_DB: minmon
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
ports:
- "5432:5432"
volumes:
- minmon_postgres_data:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
volumes:
minmon_postgres_data:

View file

@ -0,0 +1,15 @@
import dotenv from "dotenv";
import { defineConfig } from "drizzle-kit";
dotenv.config({
path: "../../apps/server/.env",
});
export default defineConfig({
schema: "./src/schema",
out: "./src/migrations",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL || "",
},
});

35
packages/db/package.json Normal file
View file

@ -0,0 +1,35 @@
{
"name": "@minmon/db",
"type": "module",
"exports": {
".": {
"default": "./src/index.ts"
},
"./*": {
"default": "./src/*.ts"
}
},
"scripts": {
"db:push": "drizzle-kit push",
"db:generate": "drizzle-kit generate",
"db:studio": "drizzle-kit studio",
"db:migrate": "drizzle-kit migrate",
"db:start": "docker compose up -d",
"db:watch": "docker compose up",
"db:stop": "docker compose stop",
"db:down": "docker compose down"
},
"dependencies": {
"@minmon/env": "workspace:*",
"dotenv": "catalog:",
"drizzle-orm": "^0.45.1",
"pg": "^8.17.1",
"zod": "catalog:"
},
"devDependencies": {
"@minmon/config": "workspace:*",
"@types/pg": "^8.16.0",
"drizzle-kit": "^0.31.8",
"typescript": "catalog:"
}
}

10
packages/db/src/index.ts Normal file
View file

@ -0,0 +1,10 @@
import { env } from "@minmon/env/server";
import { drizzle } from "drizzle-orm/node-postgres";
import * as schema from "./schema";
export function createDb() {
return drizzle(env.DATABASE_URL, { schema });
}
export const db = createDb();

View file

@ -0,0 +1,93 @@
import { relations } from "drizzle-orm";
import { pgTable, text, timestamp, boolean, index } from "drizzle-orm/pg-core";
export const user = pgTable("user", {
id: text("id").primaryKey(),
name: text("name").notNull(),
email: text("email").notNull().unique(),
emailVerified: boolean("email_verified").default(false).notNull(),
image: text("image"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
});
export const session = pgTable(
"session",
{
id: text("id").primaryKey(),
expiresAt: timestamp("expires_at").notNull(),
token: text("token").notNull().unique(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
ipAddress: text("ip_address"),
userAgent: text("user_agent"),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
},
(table) => [index("session_userId_idx").on(table.userId)],
);
export const account = pgTable(
"account",
{
id: text("id").primaryKey(),
accountId: text("account_id").notNull(),
providerId: text("provider_id").notNull(),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
accessToken: text("access_token"),
refreshToken: text("refresh_token"),
idToken: text("id_token"),
accessTokenExpiresAt: timestamp("access_token_expires_at"),
refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
scope: text("scope"),
password: text("password"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
},
(table) => [index("account_userId_idx").on(table.userId)],
);
export const verification = pgTable(
"verification",
{
id: text("id").primaryKey(),
identifier: text("identifier").notNull(),
value: text("value").notNull(),
expiresAt: timestamp("expires_at").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
},
(table) => [index("verification_identifier_idx").on(table.identifier)],
);
export const userRelations = relations(user, ({ many }) => ({
sessions: many(session),
accounts: many(account),
}));
export const sessionRelations = relations(session, ({ one }) => ({
user: one(user, {
fields: [session.userId],
references: [user.id],
}),
}));
export const accountRelations = relations(account, ({ one }) => ({
user: one(user, {
fields: [account.userId],
references: [user.id],
}),
}));

View file

@ -0,0 +1,2 @@
export * from "./auth";
export {};

10
packages/db/tsconfig.json Normal file
View file

@ -0,0 +1,10 @@
{
"extends": "@minmon/config/tsconfig.base.json",
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "dist",
"composite": true
}
}

20
packages/env/package.json vendored Normal file
View file

@ -0,0 +1,20 @@
{
"name": "@minmon/env",
"version": "0.0.0",
"private": true,
"type": "module",
"exports": {
"./server": "./src/server.ts",
"./web": "./src/web.ts"
},
"dependencies": {
"@t3-oss/env-core": "^0.13.1",
"dotenv": "catalog:",
"zod": "catalog:"
},
"devDependencies": {
"@minmon/config": "workspace:*",
"@types/bun": "catalog:",
"typescript": "catalog:"
}
}

15
packages/env/src/server.ts vendored Normal file
View file

@ -0,0 +1,15 @@
import "dotenv/config";
import { createEnv } from "@t3-oss/env-core";
import { z } from "zod";
export const env = createEnv({
server: {
DATABASE_URL: z.string().min(1),
BETTER_AUTH_SECRET: z.string().min(32),
BETTER_AUTH_URL: z.url(),
CORS_ORIGIN: z.url(),
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
},
runtimeEnv: process.env,
emptyStringAsUndefined: true,
});

11
packages/env/src/web.ts vendored Normal file
View file

@ -0,0 +1,11 @@
import { createEnv } from "@t3-oss/env-core";
import { z } from "zod";
export const env = createEnv({
clientPrefix: "VITE_",
client: {
VITE_SERVER_URL: z.url(),
},
runtimeEnv: (import.meta as any).env,
emptyStringAsUndefined: true,
});

3
packages/env/tsconfig.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"extends": "@minmon/config/tsconfig.base.json"
}

View file

@ -0,0 +1,24 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "base-lyra",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/styles/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@minmon/ui/components",
"utils": "@minmon/ui/lib/utils",
"hooks": "@minmon/ui/hooks",
"lib": "@minmon/ui/lib",
"ui": "@minmon/ui/components"
},
"menuColor": "default",
"menuAccent": "subtle",
"registries": {}
}

36
packages/ui/package.json Normal file
View file

@ -0,0 +1,36 @@
{
"name": "@minmon/ui",
"version": "0.0.0",
"private": true,
"type": "module",
"exports": {
"./globals.css": "./src/styles/globals.css",
"./lib/*": "./src/lib/*.ts",
"./components/*": "./src/components/*.tsx",
"./hooks/*": "./src/hooks/*.ts",
"./postcss.config": "./postcss.config.mjs"
},
"scripts": {
"check-types": "tsc --noEmit"
},
"dependencies": {
"@base-ui/react": "^1.0.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.546.0",
"next-themes": "catalog:",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"shadcn": "^3.6.2",
"sonner": "^2.0.5",
"tailwind-merge": "^3.3.1",
"tw-animate-css": "^1.3.4"
},
"devDependencies": {
"@minmon/config": "workspace:*",
"@types/react": "^19.2.10",
"@types/react-dom": "catalog:",
"tailwindcss": "^4.1.18",
"typescript": "catalog:"
}
}

View file

@ -0,0 +1,5 @@
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};

View file

@ -0,0 +1,55 @@
import { Button as ButtonPrimitive } from "@base-ui/react/button";
import { cn } from "@minmon/ui/lib/utils";
import { cva, type VariantProps } from "class-variance-authority";
const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-none border border-transparent bg-clip-padding text-xs font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
outline:
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
ghost:
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
destructive:
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default:
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
xs: "h-6 gap-1 rounded-none px-2 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-none px-2.5 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
icon: "size-8",
"icon-xs": "size-6 rounded-none [&_svg:not([class*='size-'])]:size-3",
"icon-sm": "size-7 rounded-none",
"icon-lg": "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
function Button({
className,
variant = "default",
size = "default",
...props
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
return (
<ButtonPrimitive
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}
export { Button, buttonVariants };

View file

@ -0,0 +1,88 @@
import { cn } from "@minmon/ui/lib/utils";
import * as React from "react";
function Card({
className,
size = "default",
...props
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
return (
<div
data-slot="card"
data-size={size}
className={cn(
"group/card flex flex-col gap-4 overflow-hidden rounded-none bg-card py-4 text-xs/relaxed text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-2 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-none *:[img:last-child]:rounded-none",
className,
)}
{...props}
/>
);
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-none px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
className,
)}
{...props}
/>
);
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("text-sm font-medium group-data-[size=sm]/card:text-sm", className)}
{...props}
/>
);
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-xs/relaxed text-muted-foreground", className)}
{...props}
/>
);
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
{...props}
/>
);
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-4 group-data-[size=sm]/card:px-3", className)}
{...props}
/>
);
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn(
"flex items-center rounded-none border-t p-4 group-data-[size=sm]/card:p-3",
className,
)}
{...props}
/>
);
}
export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent };

View file

@ -0,0 +1,27 @@
"use client";
import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox";
import { cn } from "@minmon/ui/lib/utils";
import { CheckIcon } from "lucide-react";
function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer relative flex size-4 shrink-0 items-center justify-center rounded-none border border-input transition-colors outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="grid place-content-center text-current transition-none [&>svg]:size-3.5"
>
<CheckIcon />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
);
}
export { Checkbox };

View file

@ -0,0 +1,254 @@
"use client";
import { Menu as MenuPrimitive } from "@base-ui/react/menu";
import { cn } from "@minmon/ui/lib/utils";
import { ChevronRightIcon, CheckIcon } from "lucide-react";
import * as React from "react";
function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) {
return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
}
function DropdownMenuPortal({ ...props }: MenuPrimitive.Portal.Props) {
return <MenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />;
}
function DropdownMenuTrigger({ ...props }: MenuPrimitive.Trigger.Props) {
return <MenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />;
}
function DropdownMenuContent({
align = "start",
alignOffset = 0,
side = "bottom",
sideOffset = 4,
className,
...props
}: MenuPrimitive.Popup.Props &
Pick<MenuPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset">) {
return (
<MenuPrimitive.Portal>
<MenuPrimitive.Positioner
className="isolate z-50 outline-none"
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
>
<MenuPrimitive.Popup
data-slot="dropdown-menu-content"
className={cn(
"z-50 max-h-(--available-height) w-(--anchor-width) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-none bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-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 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:overflow-hidden data-closed:fade-out-0 data-closed:zoom-out-95",
className,
)}
{...props}
/>
</MenuPrimitive.Positioner>
</MenuPrimitive.Portal>
);
}
function DropdownMenuGroup({ ...props }: MenuPrimitive.Group.Props) {
return <MenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />;
}
function DropdownMenuLabel({
className,
inset,
...props
}: MenuPrimitive.GroupLabel.Props & {
inset?: boolean;
}) {
return (
<MenuPrimitive.GroupLabel
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn("px-2 py-2 text-xs text-muted-foreground data-inset:pl-7", className)}
{...props}
/>
);
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: MenuPrimitive.Item.Props & {
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<MenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"group/dropdown-menu-item relative flex cursor-default items-center gap-2 rounded-none px-2 py-2 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive",
className,
)}
{...props}
/>
);
}
function DropdownMenuSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) {
return <MenuPrimitive.SubmenuRoot data-slot="dropdown-menu-sub" {...props} />;
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: MenuPrimitive.SubmenuTrigger.Props & {
inset?: boolean;
}) {
return (
<MenuPrimitive.SubmenuTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"flex cursor-default items-center gap-2 rounded-none px-2 py-2 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-popup-open:bg-accent data-popup-open:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto" />
</MenuPrimitive.SubmenuTrigger>
);
}
function DropdownMenuSubContent({
align = "start",
alignOffset = -3,
side = "right",
sideOffset = 0,
className,
...props
}: React.ComponentProps<typeof DropdownMenuContent>) {
return (
<DropdownMenuContent
data-slot="dropdown-menu-sub-content"
className={cn(
"w-auto min-w-[96px] rounded-none bg-popover text-popover-foreground shadow-lg ring-1 ring-foreground/10 duration-100 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 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className,
)}
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
{...props}
/>
);
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
inset,
...props
}: MenuPrimitive.CheckboxItem.Props & {
inset?: boolean;
}) {
return (
<MenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
data-inset={inset}
className={cn(
"relative flex cursor-default items-center gap-2 rounded-none py-2 pr-8 pl-2 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
checked={checked}
{...props}
>
<span
className="pointer-events-none absolute right-2 flex items-center justify-center"
data-slot="dropdown-menu-checkbox-item-indicator"
>
<MenuPrimitive.CheckboxItemIndicator>
<CheckIcon />
</MenuPrimitive.CheckboxItemIndicator>
</span>
{children}
</MenuPrimitive.CheckboxItem>
);
}
function DropdownMenuRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) {
return <MenuPrimitive.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />;
}
function DropdownMenuRadioItem({
className,
children,
inset,
...props
}: MenuPrimitive.RadioItem.Props & {
inset?: boolean;
}) {
return (
<MenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
data-inset={inset}
className={cn(
"relative flex cursor-default items-center gap-2 rounded-none py-2 pr-8 pl-2 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
<span
className="pointer-events-none absolute right-2 flex items-center justify-center"
data-slot="dropdown-menu-radio-item-indicator"
>
<MenuPrimitive.RadioItemIndicator>
<CheckIcon />
</MenuPrimitive.RadioItemIndicator>
</span>
{children}
</MenuPrimitive.RadioItem>
);
}
function DropdownMenuSeparator({ className, ...props }: MenuPrimitive.Separator.Props) {
return (
<MenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
);
}
function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground",
className,
)}
{...props}
/>
);
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
};

View file

@ -0,0 +1,19 @@
import { Input as InputPrimitive } from "@base-ui/react/input";
import { cn } from "@minmon/ui/lib/utils";
import * as React from "react";
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<InputPrimitive
type={type}
data-slot="input"
className={cn(
"h-8 w-full min-w-0 rounded-none border border-input bg-transparent px-2.5 py-1 text-xs transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-xs file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 md:text-xs dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className,
)}
{...props}
/>
);
}
export { Input };

View file

@ -0,0 +1,17 @@
import { cn } from "@minmon/ui/lib/utils";
import * as React from "react";
function Label({ className, ...props }: React.ComponentProps<"label">) {
return (
<label
data-slot="label"
className={cn(
"flex items-center gap-2 text-xs leading-none select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className,
)}
{...props}
/>
);
}
export { Label };

View file

@ -0,0 +1,13 @@
import { cn } from "@minmon/ui/lib/utils";
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="skeleton"
className={cn("animate-pulse rounded-none bg-muted", className)}
{...props}
/>
);
}
export { Skeleton };

View file

@ -0,0 +1,45 @@
"use client";
import {
CircleCheckIcon,
InfoIcon,
TriangleAlertIcon,
OctagonXIcon,
Loader2Icon,
} from "lucide-react";
import { useTheme } from "next-themes";
import { Toaster as Sonner, type ToasterProps } from "sonner";
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme();
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
icons={{
success: <CircleCheckIcon className="size-4" />,
info: <InfoIcon className="size-4" />,
warning: <TriangleAlertIcon className="size-4" />,
error: <OctagonXIcon className="size-4" />,
loading: <Loader2Icon className="size-4 animate-spin" />,
}}
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
"--border-radius": "var(--radius)",
} as React.CSSProperties
}
toastOptions={{
classNames: {
toast: "cn-toast",
},
}}
{...props}
/>
);
};
export { Toaster };

View file

View file

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

View file

@ -0,0 +1,130 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@source "../../../apps/**/*.{ts,tsx}";
@source "../**/*.{ts,tsx}";
@custom-variant dark (&:is(.dark *));
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.58 0.22 27);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.809 0.105 251.813);
--chart-2: oklch(0.623 0.214 259.815);
--chart-3: oklch(0.546 0.245 262.881);
--chart-4: oklch(0.488 0.243 264.376);
--chart-5: oklch(0.424 0.199 265.638);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.87 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.371 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.809 0.105 251.813);
--chart-2: oklch(0.623 0.214 259.815);
--chart-3: oklch(0.546 0.245 262.881);
--chart-4: oklch(0.488 0.243 264.376);
--chart-5: oklch(0.424 0.199 265.638);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@theme inline {
--font-sans: "Inter Variable", sans-serif;
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--color-foreground: var(--foreground);
--color-background: var(--background);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--radius-3xl: calc(var(--radius) + 12px);
--radius-4xl: calc(var(--radius) + 16px);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply font-sans bg-background text-foreground;
}
html {
@apply font-sans;
}
}

12
packages/ui/tsconfig.json Normal file
View file

@ -0,0 +1,12 @@
{
"extends": "@minmon/config/tsconfig.base.json",
"compilerOptions": {
"jsx": "react-jsx",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"paths": {
"@minmon/ui/*": ["./src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules"]
}

3
tsconfig.json Normal file
View file

@ -0,0 +1,3 @@
{
"extends": "@minmon/config/tsconfig.base.json"
}

49
turbo.json Normal file
View file

@ -0,0 +1,49 @@
{
"$schema": "https://turbo.build/schema.json",
"ui": "tui",
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", ".env*"],
"outputs": ["dist/**"]
},
"lint": {
"dependsOn": ["^lint"]
},
"check-types": {
"dependsOn": ["^check-types"]
},
"dev": {
"cache": false,
"persistent": true
},
"db:push": {
"cache": false
},
"db:generate": {
"cache": false
},
"db:migrate": {
"cache": false,
"persistent": true
},
"db:studio": {
"cache": false,
"persistent": true
},
"db:start": {
"cache": false,
"persistent": true
},
"db:stop": {
"cache": false
},
"db:watch": {
"cache": false,
"persistent": true
},
"db:down": {
"cache": false
}
}
}