finished the site
This commit is contained in:
+19
-37
@@ -1,42 +1,24 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
# Logs
|
||||||
|
logs
|
||||||
# dependencies
|
*.log
|
||||||
/node_modules
|
|
||||||
/.pnp
|
|
||||||
.pnp.js
|
|
||||||
|
|
||||||
# testing
|
|
||||||
/coverage
|
|
||||||
|
|
||||||
# next.js
|
|
||||||
/.next/
|
|
||||||
/out/
|
|
||||||
|
|
||||||
# production
|
|
||||||
/build
|
|
||||||
|
|
||||||
# misc
|
|
||||||
.DS_Store
|
|
||||||
*.pem
|
|
||||||
|
|
||||||
# debug
|
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
# local env files
|
node_modules
|
||||||
.env.local
|
dist
|
||||||
.env.development.local
|
dist-ssr
|
||||||
.env.test.local
|
*.local
|
||||||
.env.production.local
|
|
||||||
|
|
||||||
# vercel
|
# Editor directories and files
|
||||||
.vercel
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
**/*.trace
|
.idea
|
||||||
**/*.zip
|
.DS_Store
|
||||||
**/*.tar.gz
|
*.suo
|
||||||
**/*.tgz
|
*.ntvs*
|
||||||
**/*.log
|
*.njsproj
|
||||||
package-lock.json
|
*.sln
|
||||||
**/*.bun
|
*.sw?
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
|
|
||||||
Default to using Bun instead of Node.js.
|
|
||||||
|
|
||||||
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
|
|
||||||
- Use `bun test` instead of `jest` or `vitest`
|
|
||||||
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
|
|
||||||
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
|
|
||||||
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
|
|
||||||
- Use `bunx <package> <command>` instead of `npx <package> <command>`
|
|
||||||
- Bun automatically loads .env, so don't use dotenv.
|
|
||||||
|
|
||||||
## APIs
|
|
||||||
|
|
||||||
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
|
|
||||||
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
|
|
||||||
- `Bun.redis` for Redis. Don't use `ioredis`.
|
|
||||||
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
|
|
||||||
- `WebSocket` is built-in. Don't use `ws`.
|
|
||||||
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
|
|
||||||
- Bun.$`ls` instead of execa.
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
Use `bun test` to run tests.
|
|
||||||
|
|
||||||
```ts#index.test.ts
|
|
||||||
import { test, expect } from "bun:test";
|
|
||||||
|
|
||||||
test("hello world", () => {
|
|
||||||
expect(1).toBe(1);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Frontend
|
|
||||||
|
|
||||||
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
|
|
||||||
|
|
||||||
Server:
|
|
||||||
|
|
||||||
```ts#index.ts
|
|
||||||
import index from "./index.html"
|
|
||||||
|
|
||||||
Bun.serve({
|
|
||||||
routes: {
|
|
||||||
"/": index,
|
|
||||||
"/api/users/:id": {
|
|
||||||
GET: (req) => {
|
|
||||||
return new Response(JSON.stringify({ id: req.params.id }));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// optional websocket support
|
|
||||||
websocket: {
|
|
||||||
open: (ws) => {
|
|
||||||
ws.send("Hello, world!");
|
|
||||||
},
|
|
||||||
message: (ws, message) => {
|
|
||||||
ws.send(message);
|
|
||||||
},
|
|
||||||
close: (ws) => {
|
|
||||||
// handle close
|
|
||||||
}
|
|
||||||
},
|
|
||||||
development: {
|
|
||||||
hmr: true,
|
|
||||||
console: true,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
|
|
||||||
|
|
||||||
```html#index.html
|
|
||||||
<html>
|
|
||||||
<body>
|
|
||||||
<h1>Hello, world!</h1>
|
|
||||||
<script type="module" src="./frontend.tsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
```
|
|
||||||
|
|
||||||
With the following `frontend.tsx`:
|
|
||||||
|
|
||||||
```tsx#frontend.tsx
|
|
||||||
import React from "react";
|
|
||||||
import { createRoot } from "react-dom/client";
|
|
||||||
|
|
||||||
// import .css files directly and it works
|
|
||||||
import './index.css';
|
|
||||||
|
|
||||||
const root = createRoot(document.body);
|
|
||||||
|
|
||||||
export default function Frontend() {
|
|
||||||
return <h1>Hello, world!</h1>;
|
|
||||||
}
|
|
||||||
|
|
||||||
root.render(<Frontend />);
|
|
||||||
```
|
|
||||||
|
|
||||||
Then, run index.ts
|
|
||||||
|
|
||||||
```sh
|
|
||||||
bun --hot ./index.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`.
|
|
||||||
@@ -1,21 +1,75 @@
|
|||||||
# bun-react-tailwind-shadcn-template
|
# React + TypeScript + Vite
|
||||||
|
|
||||||
To install dependencies:
|
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||||
|
|
||||||
```bash
|
Currently, two official plugins are available:
|
||||||
bun install
|
|
||||||
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
|
||||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
||||||
|
|
||||||
|
## React Compiler
|
||||||
|
|
||||||
|
The React Compiler is enabled on this template. See [this documentation](https://react.dev/learn/react-compiler) for more information.
|
||||||
|
|
||||||
|
Note: This will impact Vite dev & build performances.
|
||||||
|
|
||||||
|
## Expanding the ESLint configuration
|
||||||
|
|
||||||
|
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
// Other configs...
|
||||||
|
|
||||||
|
// Remove tseslint.configs.recommended and replace with this
|
||||||
|
tseslint.configs.recommendedTypeChecked,
|
||||||
|
// Alternatively, use this for stricter rules
|
||||||
|
tseslint.configs.strictTypeChecked,
|
||||||
|
// Optionally, add this for stylistic rules
|
||||||
|
tseslint.configs.stylisticTypeChecked,
|
||||||
|
|
||||||
|
// Other configs...
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
// other options...
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
```
|
```
|
||||||
|
|
||||||
To start a development server:
|
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||||
|
|
||||||
```bash
|
```js
|
||||||
bun dev
|
// eslint.config.js
|
||||||
|
import reactX from 'eslint-plugin-react-x'
|
||||||
|
import reactDom from 'eslint-plugin-react-dom'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
// Other configs...
|
||||||
|
// Enable lint rules for React
|
||||||
|
reactX.configs['recommended-typescript'],
|
||||||
|
// Enable lint rules for React DOM
|
||||||
|
reactDom.configs.recommended,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
// other options...
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
```
|
```
|
||||||
|
|
||||||
To run for production:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bun start
|
|
||||||
```
|
|
||||||
|
|
||||||
This project was created using `bun init` in bun v1.3.14. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
|
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
import tailwind from "bun-plugin-tailwind";
|
|
||||||
import { rm } from "node:fs/promises";
|
|
||||||
import path from "node:path";
|
|
||||||
|
|
||||||
const outdir = path.join(process.cwd(), "dist");
|
|
||||||
await rm(outdir, { recursive: true, force: true });
|
|
||||||
|
|
||||||
const entrypoints = [...new Bun.Glob("src/**/*.html").scanSync()];
|
|
||||||
|
|
||||||
const result = await Bun.build({
|
|
||||||
entrypoints,
|
|
||||||
outdir,
|
|
||||||
plugins: [tailwind],
|
|
||||||
minify: true,
|
|
||||||
target: "browser",
|
|
||||||
sourcemap: "linked",
|
|
||||||
define: {
|
|
||||||
"process.env.NODE_ENV": JSON.stringify("production"),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const output of result.outputs) {
|
|
||||||
console.log(` ${path.relative(process.cwd(), output.path)} ${(output.size / 1024).toFixed(1)} KB`);
|
|
||||||
}
|
|
||||||
Vendored
-19
@@ -1,19 +0,0 @@
|
|||||||
// Generated by `bun init`
|
|
||||||
|
|
||||||
declare module "*.svg" {
|
|
||||||
/**
|
|
||||||
* A path to the SVG file
|
|
||||||
*/
|
|
||||||
const path: `${string}.svg`;
|
|
||||||
export = path;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module "*.css" {}
|
|
||||||
|
|
||||||
declare module "*.module.css" {
|
|
||||||
/**
|
|
||||||
* A record of class names to their corresponding CSS module classes
|
|
||||||
*/
|
|
||||||
const classes: { readonly [key: string]: string };
|
|
||||||
export = classes;
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
[serve.static]
|
|
||||||
plugins = ["bun-plugin-tailwind"]
|
|
||||||
env = "BUN_PUBLIC_*"
|
|
||||||
+8
-5
@@ -1,15 +1,16 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://ui.shadcn.com/schema.json",
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
"style": "new-york",
|
"style": "radix-vega",
|
||||||
"rsc": false,
|
"rsc": false,
|
||||||
"tsx": true,
|
"tsx": true,
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"config": "",
|
"config": "",
|
||||||
"css": "styles/globals.css",
|
"css": "src/index.css",
|
||||||
"baseColor": "neutral",
|
"baseColor": "neutral",
|
||||||
"cssVariables": true,
|
"cssVariables": true
|
||||||
"prefix": ""
|
|
||||||
},
|
},
|
||||||
|
"iconLibrary": "lucide",
|
||||||
|
"rtl": false,
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"components": "@/components",
|
"components": "@/components",
|
||||||
"utils": "@/lib/utils",
|
"utils": "@/lib/utils",
|
||||||
@@ -17,5 +18,7 @@
|
|||||||
"lib": "@/lib",
|
"lib": "@/lib",
|
||||||
"hooks": "@/hooks"
|
"hooks": "@/hooks"
|
||||||
},
|
},
|
||||||
"iconLibrary": "lucide"
|
"menuColor": "default",
|
||||||
|
"menuAccent": "subtle",
|
||||||
|
"registries": {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
tseslint.configs.recommended,
|
||||||
|
reactHooks.configs.flat.recommended,
|
||||||
|
reactRefresh.configs.vite,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
@@ -2,12 +2,12 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="icon" type="image/svg+xml" href="./logo.svg" />
|
<title>drone-hub</title>
|
||||||
<title>Bun + React</title>
|
|
||||||
<script type="module" src="./frontend.tsx" async></script>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+33
-17
@@ -1,30 +1,46 @@
|
|||||||
{
|
{
|
||||||
"name": "bun-react-template",
|
"name": "drone-hub",
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun --hot src/index.ts",
|
"dev": "vite",
|
||||||
"start": "NODE_ENV=production bun src/index.ts",
|
"build": "tsc -b && vite build",
|
||||||
"build": "bun run build.ts"
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@fontsource-variable/geist": "^5.2.8",
|
||||||
"@radix-ui/react-select": "^2.2.6",
|
"@fontsource-variable/inter": "^5.2.8",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@tailwindcss/vite": "^4.3.0",
|
||||||
"bun-plugin-tailwind": "^0.1.2",
|
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^1",
|
"framer-motion": "^12.38.0",
|
||||||
"react": "^19",
|
"lucide-react": "^1.16.0",
|
||||||
"react-dom": "^19",
|
"radix-ui": "^1.4.3",
|
||||||
"tailwind-merge": "^3.3.1",
|
"react": "^19.2.6",
|
||||||
|
"react-dom": "^19.2.6",
|
||||||
|
"shadcn": "^4.7.0",
|
||||||
|
"tailwind-merge": "^3.6.0",
|
||||||
|
"tailwindcss": "^4.3.0",
|
||||||
"tw-animate-css": "^1.4.0"
|
"tw-animate-css": "^1.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^19",
|
"@babel/core": "^7.29.0",
|
||||||
"@types/react-dom": "^19",
|
"@eslint/js": "^10.0.1",
|
||||||
"@types/bun": "latest",
|
"@rolldown/plugin-babel": "^0.2.3",
|
||||||
"tailwindcss": "^4.1.11"
|
"@types/babel__core": "^7.20.5",
|
||||||
|
"@types/node": "^24.12.3",
|
||||||
|
"@types/react": "^19.2.14",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
|
"babel-plugin-react-compiler": "^1.0.0",
|
||||||
|
"eslint": "^10.3.0",
|
||||||
|
"eslint-plugin-react-hooks": "^7.1.1",
|
||||||
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
|
"globals": "^17.6.0",
|
||||||
|
"typescript": "~6.0.2",
|
||||||
|
"typescript-eslint": "^8.59.2",
|
||||||
|
"vite": "^8.0.12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 106 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 622 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 647 KiB |
@@ -1,64 +0,0 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { useRef, type FormEvent } from "react";
|
|
||||||
|
|
||||||
export function APITester() {
|
|
||||||
const responseInputRef = useRef<HTMLTextAreaElement>(null);
|
|
||||||
|
|
||||||
const testEndpoint = async (e: FormEvent<HTMLFormElement>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const form = e.currentTarget;
|
|
||||||
const formData = new FormData(form);
|
|
||||||
const endpoint = formData.get("endpoint") as string;
|
|
||||||
const url = new URL(endpoint, location.href);
|
|
||||||
const method = formData.get("method") as string;
|
|
||||||
const res = await fetch(url, { method });
|
|
||||||
|
|
||||||
const data = await res.json();
|
|
||||||
responseInputRef.current!.value = JSON.stringify(data, null, 2);
|
|
||||||
} catch (error) {
|
|
||||||
responseInputRef.current!.value = String(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-6">
|
|
||||||
<form onSubmit={testEndpoint} className="flex items-center gap-2">
|
|
||||||
<Label htmlFor="method" className="sr-only">
|
|
||||||
Method
|
|
||||||
</Label>
|
|
||||||
<Select name="method" defaultValue="GET">
|
|
||||||
<SelectTrigger className="w-[100px]" id="method">
|
|
||||||
<SelectValue placeholder="Method" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent align="start">
|
|
||||||
<SelectItem value="GET">GET</SelectItem>
|
|
||||||
<SelectItem value="PUT">PUT</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<Label htmlFor="endpoint" className="sr-only">
|
|
||||||
Endpoint
|
|
||||||
</Label>
|
|
||||||
<Input id="endpoint" type="text" name="endpoint" defaultValue="/api/hello" placeholder="/api/hello" />
|
|
||||||
<Button type="submit" variant="secondary">
|
|
||||||
Send
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
<Label htmlFor="response" className="sr-only">
|
|
||||||
Response
|
|
||||||
</Label>
|
|
||||||
<Textarea
|
|
||||||
ref={responseInputRef}
|
|
||||||
id="response"
|
|
||||||
readOnly
|
|
||||||
placeholder="Response will appear here..."
|
|
||||||
className="min-h-[140px] font-mono resize-y"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* App styles are now provided by src/index.css. */
|
||||||
+255
-33
@@ -1,39 +1,261 @@
|
|||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { ArrowRight } from "lucide-react";
|
||||||
import { APITester } from "./APITester";
|
|
||||||
import "./index.css";
|
|
||||||
|
|
||||||
import logo from "./logo.svg";
|
import {
|
||||||
import reactLogo from "./react.svg";
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
|
||||||
export function App() {
|
// (pilotCards removed — unused sample data)
|
||||||
|
|
||||||
|
const introTopics = [
|
||||||
|
{
|
||||||
|
title: "How drones fly",
|
||||||
|
detail:
|
||||||
|
"Understand lift, stabilization, and controller logic before comparing models.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "What sensors collect",
|
||||||
|
detail:
|
||||||
|
"See how RGB, thermal, and mapping sensors support different real-world jobs.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "How to compare platforms",
|
||||||
|
detail:
|
||||||
|
"Use speed, endurance, camera system, and range to evaluate fit for each task.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const droneSpecs = [
|
||||||
|
{
|
||||||
|
name: "DJI Mavic Air 2",
|
||||||
|
speed: "68 kph",
|
||||||
|
battery: "34 min",
|
||||||
|
camera: "4K/60fps, 48MP",
|
||||||
|
range: "10 km",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "DJI Air 2S",
|
||||||
|
speed: "68 kph",
|
||||||
|
battery: "31 min",
|
||||||
|
camera: "1-inch sensor, 20MP, 5.4K",
|
||||||
|
range: "10 km",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Autel EVO II Pro",
|
||||||
|
speed: "72 kph",
|
||||||
|
battery: "40-42 min",
|
||||||
|
camera: "48MP, 6K/8K options",
|
||||||
|
range: "13 km",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const featurePoints = [
|
||||||
|
"Gimbals keep the horizon level even in strong wind.",
|
||||||
|
"Thermal sensors expose heat patterns that normal cameras cannot see.",
|
||||||
|
"Flight software helps with return-to-home, orbit shots, and waypoint paths.",
|
||||||
|
];
|
||||||
|
|
||||||
|
const droneCards = [
|
||||||
|
{
|
||||||
|
name: "DJI Mavic Air 2",
|
||||||
|
company: "DJI",
|
||||||
|
role: "Photography",
|
||||||
|
tone: "Versatile imaging, smooth 4K video, and exceptional portability.",
|
||||||
|
image:
|
||||||
|
"https://upload.wikimedia.org/wikipedia/commons/b/b0/DJIMavicAir.jpg?utm_source=www.wikidata.org&utm_campaign=imageinfo&utm_content=original",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "DJI Air 2S",
|
||||||
|
company: "DJI",
|
||||||
|
role: "Professional",
|
||||||
|
tone: "Premium 1-inch sensor, exceptional low-light performance.",
|
||||||
|
image:
|
||||||
|
"https://upload.wikimedia.org/wikipedia/commons/6/62/DJI_Air_2S_in_flight_%28cropped%29.jpg?utm_source=www.wikidata.org&utm_campaign=imageinfo&utm_content=original",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Autel EVO II Pro",
|
||||||
|
company: "Autel",
|
||||||
|
role: "Inspection",
|
||||||
|
tone: "Extended flight time and professional imaging for surveys.",
|
||||||
|
image:
|
||||||
|
"https://upload.wikimedia.org/wikipedia/commons/7/70/Drone_equiped_with_spectrophotometer.jpg?utm_source=www.wikidata.org&utm_campaign=imageinfo&utm_content=original",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto p-8 text-center relative z-10">
|
<main className="min-h-screen bg-[#f6f7fb] text-slate-950">
|
||||||
<div className="flex justify-center items-center gap-8 mb-8">
|
<div className="pointer-events-none absolute inset-x-0 top-0 h-80 bg-sky-100/70" />
|
||||||
<img
|
|
||||||
src={logo}
|
<div className="relative mx-auto flex w-full max-w-6xl flex-col gap-8 px-4 py-6 sm:px-6 lg:px-8 lg:py-8">
|
||||||
alt="Bun Logo"
|
<section className="grid gap-6 rounded-[1.5rem] bg-white p-6 shadow-sm ring-1 ring-slate-200 sm:p-8 lg:grid-cols-[1.05fr_0.95fr] lg:items-start lg:p-10">
|
||||||
className="h-36 p-6 transition-all duration-300 hover:drop-shadow-[0_0_2em_#646cffaa] scale-120"
|
<div className="space-y-4">
|
||||||
/>
|
<h1 className="text-3xl font-semibold tracking-tight sm:text-4xl">
|
||||||
<img
|
Drone Hub
|
||||||
src={reactLogo}
|
</h1>
|
||||||
alt="React Logo"
|
<p className="max-w-2xl text-slate-600">
|
||||||
className="h-36 p-6 transition-all duration-300 hover:drop-shadow-[0_0_2em_#61dafbaa] [animation:spin_20s_linear_infinite]"
|
The Drone Hub introduces how drones work, where they are useful,
|
||||||
/>
|
and what technical details matter when comparing aircraft.
|
||||||
|
</p>
|
||||||
|
<p className="max-w-2xl text-slate-600">
|
||||||
|
Start with core concepts, then move through platform snapshots and
|
||||||
|
side-by-side specs to build a grounded understanding.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-3 rounded-2xl border border-slate-200 bg-slate-50/80 p-4 sm:p-5">
|
||||||
|
{introTopics.map((topic) => (
|
||||||
|
<div
|
||||||
|
key={topic.title}
|
||||||
|
className="rounded-xl border border-slate-200 bg-white p-4"
|
||||||
|
>
|
||||||
|
<p className="flex items-center gap-2 text-sm font-semibold text-slate-900">
|
||||||
|
<ArrowRight className="h-4 w-4 text-sky-700" />
|
||||||
|
{topic.title}
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-sm leading-6 text-slate-600">
|
||||||
|
{topic.detail}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section
|
||||||
|
id="drone-section"
|
||||||
|
className="grid gap-6 rounded-[1.5rem] bg-white p-6 shadow-sm ring-1 ring-slate-200 sm:p-8 lg:grid-cols-[0.78fr_1.22fr] lg:items-center lg:p-10"
|
||||||
|
>
|
||||||
|
<div className="flex justify-center lg:justify-start">
|
||||||
|
<img
|
||||||
|
src="/wireframe.png"
|
||||||
|
alt="Drone body close-up"
|
||||||
|
className="h-auto w-full max-w-3xl rounded-lg border border-slate-200 object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h2 className="text-2xl font-semibold tracking-tight sm:text-3xl">
|
||||||
|
Why drones matter
|
||||||
|
</h2>
|
||||||
|
<p className="max-w-2xl text-slate-600">
|
||||||
|
Aerial systems do much more than capture footage. They rescue
|
||||||
|
people, view places that are hard to reach, and gather data that
|
||||||
|
would be impossible to collect otherwise.
|
||||||
|
</p>
|
||||||
|
<ul className="grid gap-3 text-slate-700 sm:grid-cols-3">
|
||||||
|
{featurePoints.map((point) => (
|
||||||
|
<li
|
||||||
|
key={point}
|
||||||
|
className="flex items-start gap-3 rounded-lg border border-slate-200 bg-slate-50 p-4"
|
||||||
|
>
|
||||||
|
<span className="text-sm leading-6">{point}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="space-y-6 rounded-[1.5rem] bg-white p-6 shadow-sm ring-1 ring-slate-200 sm:p-8 lg:p-10">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h2 className="text-2xl font-semibold tracking-tight sm:text-3xl">
|
||||||
|
Meet the drones
|
||||||
|
</h2>
|
||||||
|
<p className="text-slate-600">
|
||||||
|
Drones come in all shapes and sizes, but they all have the same
|
||||||
|
core components: a frame, motors, propellers, a flight controller,
|
||||||
|
and a power source. The best drone for you depends on your needs
|
||||||
|
and experience level. Consider what you want to do with it, how
|
||||||
|
much time you want to spend learning, and your budget before
|
||||||
|
making a decision.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-6 md:grid-cols-3">
|
||||||
|
{droneCards.map((drone) => (
|
||||||
|
<Card
|
||||||
|
key={drone.name}
|
||||||
|
className="rounded-2xl border-slate-300/80 bg-white px-0 py-0 shadow-none"
|
||||||
|
>
|
||||||
|
<CardHeader className="px-0 pt-10 text-center">
|
||||||
|
<img
|
||||||
|
src={drone.image}
|
||||||
|
alt={drone.name}
|
||||||
|
className="mx-auto h-32 w-32 rounded-2xl border border-slate-200 object-cover bg-slate-50"
|
||||||
|
/>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3 px-5 pb-8 text-center">
|
||||||
|
<div>
|
||||||
|
<CardTitle className="text-4xl font-medium">
|
||||||
|
{drone.name}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="mt-2 text-sm text-slate-500">
|
||||||
|
{drone.company}
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-medium uppercase tracking-[0.2em] text-sky-700">
|
||||||
|
{drone.role}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm leading-6 text-slate-600">
|
||||||
|
{drone.tone}
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="grid gap-6 rounded-[1.5rem] bg-white p-6 shadow-sm ring-1 ring-slate-200 lg:grid-cols-[0.9fr_1.1fr] lg:items-start lg:p-10">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h2 className="text-2xl font-semibold tracking-tight sm:text-3xl">
|
||||||
|
Drone specs comparison
|
||||||
|
</h2>
|
||||||
|
<p className="text-slate-600">
|
||||||
|
A quick look at three fictional platforms. Speed, battery life,
|
||||||
|
camera quality, and range are the fastest way to compare them at a
|
||||||
|
glance.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="overflow-hidden rounded-2xl border border-slate-200 bg-white">
|
||||||
|
<table className="w-full border-collapse text-left text-sm">
|
||||||
|
<thead className="bg-slate-50 text-slate-500">
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-3 font-medium">Name</th>
|
||||||
|
<th className="px-4 py-3 font-medium">Speed</th>
|
||||||
|
<th className="px-4 py-3 font-medium">Battery</th>
|
||||||
|
<th className="px-4 py-3 font-medium">Camera</th>
|
||||||
|
<th className="px-4 py-3 font-medium">Range</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{droneSpecs.map((drone, index) => (
|
||||||
|
<tr
|
||||||
|
key={drone.name}
|
||||||
|
className={
|
||||||
|
index !== droneSpecs.length - 1
|
||||||
|
? "border-b border-slate-200"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<td className="px-4 py-3 font-medium text-slate-900">
|
||||||
|
{drone.name}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-slate-600">{drone.speed}</td>
|
||||||
|
<td className="px-4 py-3 text-slate-600">
|
||||||
|
{drone.battery}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-slate-600">{drone.camera}</td>
|
||||||
|
<td className="px-4 py-3 text-slate-600">{drone.range}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<Card>
|
</main>
|
||||||
<CardHeader className="gap-4">
|
|
||||||
<CardTitle className="text-3xl font-bold">Bun + React</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Edit <code className="rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono">src/App.tsx</code> and save to
|
|
||||||
test HMR
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<APITester />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
|
||||||
|
|||||||
@@ -1,29 +1,36 @@
|
|||||||
import { Slot } from "@radix-ui/react-slot";
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
import { Slot } from "radix-ui";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
"group/button inline-flex shrink-0 items-center justify-center rounded-md border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
default: "bg-primary text-primary-foreground hover:bg-primary/80",
|
||||||
destructive:
|
|
||||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
|
||||||
outline:
|
outline:
|
||||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
"border-border bg-background shadow-xs 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",
|
secondary:
|
||||||
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
"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",
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
default:
|
||||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
"h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
xs: "h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||||
|
sm: "h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5",
|
||||||
|
lg: "h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||||
icon: "size-9",
|
icon: "size-9",
|
||||||
"icon-sm": "size-8",
|
"icon-xs":
|
||||||
|
"size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md [&_svg:not([class*='size-'])]:size-3",
|
||||||
|
"icon-sm":
|
||||||
|
"size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md",
|
||||||
"icon-lg": "size-10",
|
"icon-lg": "size-10",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -36,17 +43,25 @@ const buttonVariants = cva(
|
|||||||
|
|
||||||
function Button({
|
function Button({
|
||||||
className,
|
className,
|
||||||
variant,
|
variant = "default",
|
||||||
size,
|
size = "default",
|
||||||
asChild = false,
|
asChild = false,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"button"> &
|
}: React.ComponentProps<"button"> &
|
||||||
VariantProps<typeof buttonVariants> & {
|
VariantProps<typeof buttonVariants> & {
|
||||||
asChild?: boolean;
|
asChild?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const Comp = asChild ? Slot : "button";
|
const Comp = asChild ? Slot.Root : "button";
|
||||||
|
|
||||||
return <Comp data-slot="button" className={cn(buttonVariants({ variant, size, className }))} {...props} />;
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="button"
|
||||||
|
data-variant={variant}
|
||||||
|
data-size={size}
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Button, buttonVariants };
|
export { Button };
|
||||||
|
|||||||
+63
-16
@@ -1,15 +1,23 @@
|
|||||||
import * as React from "react";
|
import * as React from "react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
function Card({
|
||||||
|
className,
|
||||||
|
size = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="card"
|
data-slot="card"
|
||||||
className={cn("bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm", className)}
|
data-size={size}
|
||||||
|
className={cn(
|
||||||
|
"group/card flex flex-col gap-6 overflow-hidden rounded-xl bg-card py-6 text-sm text-card-foreground shadow-xs ring-1 ring-foreground/10 has-[>img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
|
||||||
|
className
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
@@ -17,40 +25,79 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|||||||
<div
|
<div
|
||||||
data-slot="card-header"
|
data-slot="card-header"
|
||||||
className={cn(
|
className={cn(
|
||||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-6 group-data-[size=sm]/card:px-4 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-6 group-data-[size=sm]/card:[.border-b]:pb-4",
|
||||||
className,
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return <div data-slot="card-title" className={cn("leading-none font-semibold", className)} {...props} />;
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-title"
|
||||||
|
className={cn(
|
||||||
|
"font-heading text-base leading-normal font-medium group-data-[size=sm]/card:text-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return <div data-slot="card-description" className={cn("text-muted-foreground text-sm", className)} {...props} />;
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-description"
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="card-action"
|
data-slot="card-action"
|
||||||
className={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
|
className={cn(
|
||||||
|
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||||
|
className
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return <div data-slot="card-content" className={cn("px-6", className)} {...props} />;
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-content"
|
||||||
|
className={cn("px-6 group-data-[size=sm]/card:px-4", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div data-slot="card-footer" className={cn("flex items-center px-6 [.border-t]:pt-6", className)} {...props} />
|
<div
|
||||||
);
|
data-slot="card-footer"
|
||||||
|
className={cn(
|
||||||
|
"flex items-center rounded-b-xl px-6 group-data-[size=sm]/card:px-4 [.border-t]:pt-6 group-data-[size=sm]/card:[.border-t]:pt-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle };
|
export {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardFooter,
|
||||||
|
CardTitle,
|
||||||
|
CardAction,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|
||||||
return (
|
|
||||||
<input
|
|
||||||
type={type}
|
|
||||||
data-slot="input"
|
|
||||||
className={cn(
|
|
||||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
|
||||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Input };
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
function Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
|
||||||
return (
|
|
||||||
<LabelPrimitive.Root
|
|
||||||
data-slot="label"
|
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-2 text-sm leading-none font-medium 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 };
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
|
||||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
|
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
function Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
|
||||||
return <SelectPrimitive.Root data-slot="select" {...props} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectGroup({ ...props }: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
|
||||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
|
||||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectTrigger({
|
|
||||||
className,
|
|
||||||
size = "default",
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
|
||||||
size?: "sm" | "default";
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.Trigger
|
|
||||||
data-slot="select-trigger"
|
|
||||||
data-size={size}
|
|
||||||
className={cn(
|
|
||||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<SelectPrimitive.Icon asChild>
|
|
||||||
<ChevronDownIcon className="size-4 opacity-50" />
|
|
||||||
</SelectPrimitive.Icon>
|
|
||||||
</SelectPrimitive.Trigger>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectContent({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
position = "popper",
|
|
||||||
align = "center",
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.Portal>
|
|
||||||
<SelectPrimitive.Content
|
|
||||||
data-slot="select-content"
|
|
||||||
className={cn(
|
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
|
||||||
position === "popper" &&
|
|
||||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
position={position}
|
|
||||||
align={align}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<SelectScrollUpButton />
|
|
||||||
<SelectPrimitive.Viewport
|
|
||||||
className={cn(
|
|
||||||
"p-1",
|
|
||||||
position === "popper" &&
|
|
||||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</SelectPrimitive.Viewport>
|
|
||||||
<SelectScrollDownButton />
|
|
||||||
</SelectPrimitive.Content>
|
|
||||||
</SelectPrimitive.Portal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.Label
|
|
||||||
data-slot="select-label"
|
|
||||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectItem({ className, children, ...props }: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.Item
|
|
||||||
data-slot="select-item"
|
|
||||||
className={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
|
||||||
<SelectPrimitive.ItemIndicator>
|
|
||||||
<CheckIcon className="size-4" />
|
|
||||||
</SelectPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
|
||||||
</SelectPrimitive.Item>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectSeparator({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.Separator
|
|
||||||
data-slot="select-separator"
|
|
||||||
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectScrollUpButton({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.ScrollUpButton
|
|
||||||
data-slot="select-scroll-up-button"
|
|
||||||
className={cn("flex cursor-default items-center justify-center py-1", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ChevronUpIcon className="size-4" />
|
|
||||||
</SelectPrimitive.ScrollUpButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectScrollDownButton({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.ScrollDownButton
|
|
||||||
data-slot="select-scroll-down-button"
|
|
||||||
className={cn("flex cursor-default items-center justify-center py-1", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ChevronDownIcon className="size-4" />
|
|
||||||
</SelectPrimitive.ScrollDownButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectGroup,
|
|
||||||
SelectItem,
|
|
||||||
SelectLabel,
|
|
||||||
SelectScrollDownButton,
|
|
||||||
SelectScrollUpButton,
|
|
||||||
SelectSeparator,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
};
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
|
||||||
return (
|
|
||||||
<textarea
|
|
||||||
data-slot="textarea"
|
|
||||||
className={cn(
|
|
||||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Textarea };
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
/**
|
|
||||||
* This file is the entry point for the React app, it sets up the root
|
|
||||||
* element and renders the App component to the DOM.
|
|
||||||
*
|
|
||||||
* It is included in `src/index.html`.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { StrictMode } from "react";
|
|
||||||
import { createRoot } from "react-dom/client";
|
|
||||||
import { App } from "./App";
|
|
||||||
|
|
||||||
const elem = document.getElementById("root")!;
|
|
||||||
const app = (
|
|
||||||
<StrictMode>
|
|
||||||
<App />
|
|
||||||
</StrictMode>
|
|
||||||
);
|
|
||||||
|
|
||||||
// https://bun.com/docs/bundler/hot-reloading#import-meta-hot-data
|
|
||||||
(import.meta.hot.data.root ??= createRoot(elem)).render(app);
|
|
||||||
+153
-47
@@ -1,51 +1,157 @@
|
|||||||
@import "../styles/globals.css";
|
@import "tailwindcss";
|
||||||
|
@import "tw-animate-css";
|
||||||
|
@import "shadcn/tailwind.css";
|
||||||
|
@import "@fontsource-variable/inter";
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
|
--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.577 0.245 27.325);
|
||||||
|
--destructive-foreground: oklch(0.985 0 0);
|
||||||
|
--border: oklch(0.922 0 0);
|
||||||
|
--input: oklch(0.922 0 0);
|
||||||
|
--ring: oklch(0.708 0 0);
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--chart-1: oklch(0.87 0 0);
|
||||||
|
--chart-2: oklch(0.556 0 0);
|
||||||
|
--chart-3: oklch(0.439 0 0);
|
||||||
|
--chart-4: oklch(0.371 0 0);
|
||||||
|
--chart-5: oklch(0.269 0 0);
|
||||||
|
--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 {
|
||||||
|
color-scheme: 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.922 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.269 0 0);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--destructive-foreground: oklch(0.985 0 0);
|
||||||
|
--border: oklch(1 0 0 / 10%);
|
||||||
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
--ring: oklch(0.556 0 0);
|
||||||
|
--chart-1: oklch(0.87 0 0);
|
||||||
|
--chart-2: oklch(0.556 0 0);
|
||||||
|
--chart-3: oklch(0.439 0 0);
|
||||||
|
--chart-4: oklch(0.371 0 0);
|
||||||
|
--chart-5: oklch(0.269 0 0);
|
||||||
|
--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 {
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-destructive-foreground: var(--destructive-foreground);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
--font-heading: var(--font-sans);
|
||||||
|
--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);
|
||||||
|
--radius-2xl: calc(var(--radius) * 1.8);
|
||||||
|
--radius-3xl: calc(var(--radius) * 2.2);
|
||||||
|
--radius-4xl: calc(var(--radius) * 2.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
background: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground antialiased;
|
||||||
|
margin: 0;
|
||||||
|
font-family: "Inter Variable", system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
min-height: 100svh;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
html {
|
||||||
@apply font-sans;
|
@apply font-sans;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
body {
|
|
||||||
@apply grid place-items-center min-w-[320px] min-h-screen relative m-0 bg-background text-foreground;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* cool Bun background animation 😎 */
|
|
||||||
body::before {
|
|
||||||
content: "";
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
z-index: -1;
|
|
||||||
opacity: 0.05;
|
|
||||||
background: url("./logo.svg");
|
|
||||||
background-size: 256px;
|
|
||||||
transform: rotate(-12deg) scale(1.35);
|
|
||||||
animation: slide 30s linear infinite;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slide {
|
|
||||||
from {
|
|
||||||
background-position: 0 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
background-position: 256px 224px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
from {
|
|
||||||
transform: rotate(0);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion) {
|
|
||||||
*,
|
|
||||||
::before,
|
|
||||||
::after {
|
|
||||||
animation: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { serve } from "bun";
|
|
||||||
import index from "./index.html";
|
|
||||||
|
|
||||||
const server = serve({
|
|
||||||
routes: {
|
|
||||||
// Serve index.html for all unmatched routes.
|
|
||||||
"/*": index,
|
|
||||||
|
|
||||||
"/api/hello": {
|
|
||||||
async GET(req) {
|
|
||||||
return Response.json({
|
|
||||||
message: "Hello, world!",
|
|
||||||
method: "GET",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
async PUT(req) {
|
|
||||||
return Response.json({
|
|
||||||
message: "Hello, world!",
|
|
||||||
method: "PUT",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
"/api/hello/:name": async req => {
|
|
||||||
const name = req.params.name;
|
|
||||||
return Response.json({
|
|
||||||
message: `Hello, ${name}!`,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
development: process.env.NODE_ENV !== "production" && {
|
|
||||||
// Enable browser hot reloading in development
|
|
||||||
hmr: true,
|
|
||||||
|
|
||||||
// Echo console logs from the browser to the server
|
|
||||||
console: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`🚀 Server running at ${server.url}`);
|
|
||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
import { type ClassValue, clsx } from "clsx";
|
import { clsx, type ClassValue } from "clsx"
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<svg id="Bun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 70"><title>Bun Logo</title><path id="Shadow" d="M71.09,20.74c-.16-.17-.33-.34-.5-.5s-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5A26.46,26.46,0,0,1,75.5,35.7c0,16.57-16.82,30.05-37.5,30.05-11.58,0-21.94-4.23-28.83-10.86l.5.5.5.5.5.5.5.5.5.5.5.5.5.5C19.55,65.3,30.14,69.75,42,69.75c20.68,0,37.5-13.48,37.5-30C79.5,32.69,76.46,26,71.09,20.74Z"/><g id="Body"><path id="Background" d="M73,35.7c0,15.21-15.67,27.54-35,27.54S3,50.91,3,35.7C3,26.27,9,17.94,18.22,13S33.18,3,38,3s8.94,4.13,19.78,10C67,17.94,73,26.27,73,35.7Z" style="fill:#fbf0df"/><path id="Bottom_Shadow" data-name="Bottom Shadow" d="M73,35.7a21.67,21.67,0,0,0-.8-5.78c-2.73,33.3-43.35,34.9-59.32,24.94A40,40,0,0,0,38,63.24C57.3,63.24,73,50.89,73,35.7Z" style="fill:#f6dece"/><path id="Light_Shine" data-name="Light Shine" d="M24.53,11.17C29,8.49,34.94,3.46,40.78,3.45A9.29,9.29,0,0,0,38,3c-2.42,0-5,1.25-8.25,3.13-1.13.66-2.3,1.39-3.54,2.15-2.33,1.44-5,3.07-8,4.7C8.69,18.13,3,26.62,3,35.7c0,.4,0,.8,0,1.19C9.06,15.48,20.07,13.85,24.53,11.17Z" style="fill:#fffefc"/><path id="Top" d="M35.12,5.53A16.41,16.41,0,0,1,29.49,18c-.28.25-.06.73.3.59,3.37-1.31,7.92-5.23,6-13.14C35.71,5,35.12,5.12,35.12,5.53Zm2.27,0A16.24,16.24,0,0,1,39,19c-.12.35.31.65.55.36C41.74,16.56,43.65,11,37.93,5,37.64,4.74,37.19,5.14,37.39,5.49Zm2.76-.17A16.42,16.42,0,0,1,47,17.12a.33.33,0,0,0,.65.11c.92-3.49.4-9.44-7.17-12.53C40.08,4.54,39.82,5.08,40.15,5.32ZM21.69,15.76a16.94,16.94,0,0,0,10.47-9c.18-.36.75-.22.66.18-1.73,8-7.52,9.67-11.12,9.45C21.32,16.4,21.33,15.87,21.69,15.76Z" style="fill:#ccbea7;fill-rule:evenodd"/><path id="Outline" d="M38,65.75C17.32,65.75.5,52.27.5,35.7c0-10,6.18-19.33,16.53-24.92,3-1.6,5.57-3.21,7.86-4.62,1.26-.78,2.45-1.51,3.6-2.19C32,1.89,35,.5,38,.5s5.62,1.2,8.9,3.14c1,.57,2,1.19,3.07,1.87,2.49,1.54,5.3,3.28,9,5.27C69.32,16.37,75.5,25.69,75.5,35.7,75.5,52.27,58.68,65.75,38,65.75ZM38,3c-2.42,0-5,1.25-8.25,3.13-1.13.66-2.3,1.39-3.54,2.15-2.33,1.44-5,3.07-8,4.7C8.69,18.13,3,26.62,3,35.7,3,50.89,18.7,63.25,38,63.25S73,50.89,73,35.7C73,26.62,67.31,18.13,57.78,13,54,11,51.05,9.12,48.66,7.64c-1.09-.67-2.09-1.29-3-1.84C42.63,4,40.42,3,38,3Z"/></g><g id="Mouth"><g id="Background-2" data-name="Background"><path d="M45.05,43a8.93,8.93,0,0,1-2.92,4.71,6.81,6.81,0,0,1-4,1.88A6.84,6.84,0,0,1,34,47.71,8.93,8.93,0,0,1,31.12,43a.72.72,0,0,1,.8-.81H44.26A.72.72,0,0,1,45.05,43Z" style="fill:#b71422"/></g><g id="Tongue"><path id="Background-3" data-name="Background" d="M34,47.79a6.91,6.91,0,0,0,4.12,1.9,6.91,6.91,0,0,0,4.11-1.9,10.63,10.63,0,0,0,1-1.07,6.83,6.83,0,0,0-4.9-2.31,6.15,6.15,0,0,0-5,2.78C33.56,47.4,33.76,47.6,34,47.79Z" style="fill:#ff6164"/><path id="Outline-2" data-name="Outline" d="M34.16,47a5.36,5.36,0,0,1,4.19-2.08,6,6,0,0,1,4,1.69c.23-.25.45-.51.66-.77a7,7,0,0,0-4.71-1.93,6.36,6.36,0,0,0-4.89,2.36A9.53,9.53,0,0,0,34.16,47Z"/></g><path id="Outline-3" data-name="Outline" d="M38.09,50.19a7.42,7.42,0,0,1-4.45-2,9.52,9.52,0,0,1-3.11-5.05,1.2,1.2,0,0,1,.26-1,1.41,1.41,0,0,1,1.13-.51H44.26a1.44,1.44,0,0,1,1.13.51,1.19,1.19,0,0,1,.25,1h0a9.52,9.52,0,0,1-3.11,5.05A7.42,7.42,0,0,1,38.09,50.19Zm-6.17-7.4c-.16,0-.2.07-.21.09a8.29,8.29,0,0,0,2.73,4.37A6.23,6.23,0,0,0,38.09,49a6.28,6.28,0,0,0,3.65-1.73,8.3,8.3,0,0,0,2.72-4.37.21.21,0,0,0-.2-.09Z"/></g><g id="Face"><ellipse id="Right_Blush" data-name="Right Blush" cx="53.22" cy="40.18" rx="5.85" ry="3.44" style="fill:#febbd0"/><ellipse id="Left_Bluch" data-name="Left Bluch" cx="22.95" cy="40.18" rx="5.85" ry="3.44" style="fill:#febbd0"/><path id="Eyes" d="M25.7,38.8a5.51,5.51,0,1,0-5.5-5.51A5.51,5.51,0,0,0,25.7,38.8Zm24.77,0A5.51,5.51,0,1,0,45,33.29,5.5,5.5,0,0,0,50.47,38.8Z" style="fill-rule:evenodd"/><path id="Iris" d="M24,33.64a2.07,2.07,0,1,0-2.06-2.07A2.07,2.07,0,0,0,24,33.64Zm24.77,0a2.07,2.07,0,1,0-2.06-2.07A2.07,2.07,0,0,0,48.75,33.64Z" style="fill:#fff;fill-rule:evenodd"/></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.8 KiB |
@@ -0,0 +1,10 @@
|
|||||||
|
import { StrictMode } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import './index.css'
|
||||||
|
import App from './App.tsx'
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-11.5 -10.23174 23 20.46348">
|
|
||||||
<circle cx="0" cy="0" r="2.05" fill="#61dafb"/>
|
|
||||||
<g stroke="#61dafb" stroke-width="1" fill="none">
|
|
||||||
<ellipse rx="11" ry="4.2"/>
|
|
||||||
<ellipse rx="11" ry="4.2" transform="rotate(60)"/>
|
|
||||||
<ellipse rx="11" ry="4.2" transform="rotate(120)"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 338 B |
@@ -1,120 +0,0 @@
|
|||||||
@import "tailwindcss";
|
|
||||||
@import "tw-animate-css";
|
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
|
||||||
|
|
||||||
@theme inline {
|
|
||||||
--color-background: var(--background);
|
|
||||||
--color-foreground: var(--foreground);
|
|
||||||
--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);
|
|
||||||
--radius-sm: calc(var(--radius) - 4px);
|
|
||||||
--radius-md: calc(var(--radius) - 2px);
|
|
||||||
--radius-lg: var(--radius);
|
|
||||||
--radius-xl: calc(var(--radius) + 4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--radius: 0.625rem;
|
|
||||||
--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.577 0.245 27.325);
|
|
||||||
--border: oklch(0.922 0 0);
|
|
||||||
--input: oklch(0.922 0 0);
|
|
||||||
--ring: oklch(0.708 0 0);
|
|
||||||
--chart-1: oklch(0.646 0.222 41.116);
|
|
||||||
--chart-2: oklch(0.6 0.118 184.704);
|
|
||||||
--chart-3: oklch(0.398 0.07 227.392);
|
|
||||||
--chart-4: oklch(0.828 0.189 84.429);
|
|
||||||
--chart-5: oklch(0.769 0.188 70.08);
|
|
||||||
--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.922 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.269 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.488 0.243 264.376);
|
|
||||||
--chart-2: oklch(0.696 0.17 162.48);
|
|
||||||
--chart-3: oklch(0.769 0.188 70.08);
|
|
||||||
--chart-4: oklch(0.627 0.265 303.9);
|
|
||||||
--chart-5: oklch(0.645 0.246 16.439);
|
|
||||||
--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);
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer base {
|
|
||||||
* {
|
|
||||||
@apply border-border outline-ring/50;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
@apply bg-background text-foreground;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "es2023",
|
||||||
|
"lib": ["ES2023", "DOM"],
|
||||||
|
"module": "esnext",
|
||||||
|
"types": ["vite/client"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
+7
-30
@@ -1,36 +1,13 @@
|
|||||||
{
|
{
|
||||||
|
"files": [],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
// Environment setup & latest features
|
"baseUrl": ".",
|
||||||
"lib": ["ESNext", "DOM"],
|
|
||||||
"target": "ESNext",
|
|
||||||
"module": "Preserve",
|
|
||||||
"moduleDetection": "force",
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
"allowJs": true,
|
|
||||||
"types": ["bun", "react"],
|
|
||||||
|
|
||||||
// Bundler mode
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"verbatimModuleSyntax": true,
|
|
||||||
"noEmit": true,
|
|
||||||
|
|
||||||
// Best practices
|
|
||||||
"strict": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"noUncheckedIndexedAccess": true,
|
|
||||||
"noImplicitOverride": true,
|
|
||||||
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
},
|
}
|
||||||
|
|
||||||
// Some stricter flags (disabled by default)
|
|
||||||
"noUnusedLocals": false,
|
|
||||||
"noUnusedParameters": false,
|
|
||||||
"noPropertyAccessFromIndexSignature": false
|
|
||||||
},
|
},
|
||||||
|
"references": [
|
||||||
"exclude": ["dist", "node_modules"]
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "es2023",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "esnext",
|
||||||
|
"types": ["node"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react, { reactCompilerPreset } from "@vitejs/plugin-react";
|
||||||
|
import babel from "@rolldown/plugin-babel";
|
||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { dirname, resolve } from "node:path";
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
babel({ presets: [reactCompilerPreset()] }),
|
||||||
|
tailwindcss(),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": resolve(__dirname, "src"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user