redesign

waveringana 2f90eead

+34
.gitignore
···
···
+
# 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
+21
README.md
···
···
+
# bun-react-tailwind-shadcn-template
+
+
To install dependencies:
+
+
```bash
+
bun install
+
```
+
+
To start a development server:
+
+
```bash
+
bun dev
+
```
+
+
To run for production:
+
+
```bash
+
bun start
+
```
+
+
This project was created using `bun init` in bun v1.3.1. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
+149
build.ts
···
···
+
#!/usr/bin/env bun
+
import plugin from "bun-plugin-tailwind";
+
import { existsSync } from "fs";
+
import { rm } from "fs/promises";
+
import path from "path";
+
+
if (process.argv.includes("--help") || process.argv.includes("-h")) {
+
console.log(`
+
🏗️ Bun Build Script
+
+
Usage: bun run build.ts [options]
+
+
Common Options:
+
--outdir <path> Output directory (default: "dist")
+
--minify Enable minification (or --minify.whitespace, --minify.syntax, etc)
+
--sourcemap <type> Sourcemap type: none|linked|inline|external
+
--target <target> Build target: browser|bun|node
+
--format <format> Output format: esm|cjs|iife
+
--splitting Enable code splitting
+
--packages <type> Package handling: bundle|external
+
--public-path <path> Public path for assets
+
--env <mode> Environment handling: inline|disable|prefix*
+
--conditions <list> Package.json export conditions (comma separated)
+
--external <list> External packages (comma separated)
+
--banner <text> Add banner text to output
+
--footer <text> Add footer text to output
+
--define <obj> Define global constants (e.g. --define.VERSION=1.0.0)
+
--help, -h Show this help message
+
+
Example:
+
bun run build.ts --outdir=dist --minify --sourcemap=linked --external=react,react-dom
+
`);
+
process.exit(0);
+
}
+
+
const toCamelCase = (str: string): string => str.replace(/-([a-z])/g, g => g[1].toUpperCase());
+
+
const parseValue = (value: string): any => {
+
if (value === "true") return true;
+
if (value === "false") return false;
+
+
if (/^\d+$/.test(value)) return parseInt(value, 10);
+
if (/^\d*\.\d+$/.test(value)) return parseFloat(value);
+
+
if (value.includes(",")) return value.split(",").map(v => v.trim());
+
+
return value;
+
};
+
+
function parseArgs(): Partial<Bun.BuildConfig> {
+
const config: Partial<Bun.BuildConfig> = {};
+
const args = process.argv.slice(2);
+
+
for (let i = 0; i < args.length; i++) {
+
const arg = args[i];
+
if (arg === undefined) continue;
+
if (!arg.startsWith("--")) continue;
+
+
if (arg.startsWith("--no-")) {
+
const key = toCamelCase(arg.slice(5));
+
config[key] = false;
+
continue;
+
}
+
+
if (!arg.includes("=") && (i === args.length - 1 || args[i + 1]?.startsWith("--"))) {
+
const key = toCamelCase(arg.slice(2));
+
config[key] = true;
+
continue;
+
}
+
+
let key: string;
+
let value: string;
+
+
if (arg.includes("=")) {
+
[key, value] = arg.slice(2).split("=", 2) as [string, string];
+
} else {
+
key = arg.slice(2);
+
value = args[++i] ?? "";
+
}
+
+
key = toCamelCase(key);
+
+
if (key.includes(".")) {
+
const [parentKey, childKey] = key.split(".");
+
config[parentKey] = config[parentKey] || {};
+
config[parentKey][childKey] = parseValue(value);
+
} else {
+
config[key] = parseValue(value);
+
}
+
}
+
+
return config;
+
}
+
+
const formatFileSize = (bytes: number): string => {
+
const units = ["B", "KB", "MB", "GB"];
+
let size = bytes;
+
let unitIndex = 0;
+
+
while (size >= 1024 && unitIndex < units.length - 1) {
+
size /= 1024;
+
unitIndex++;
+
}
+
+
return `${size.toFixed(2)} ${units[unitIndex]}`;
+
};
+
+
console.log("\n🚀 Starting build process...\n");
+
+
const cliConfig = parseArgs();
+
const outdir = cliConfig.outdir || path.join(process.cwd(), "dist");
+
+
if (existsSync(outdir)) {
+
console.log(`🗑️ Cleaning previous build at ${outdir}`);
+
await rm(outdir, { recursive: true, force: true });
+
}
+
+
const start = performance.now();
+
+
const entrypoints = [...new Bun.Glob("**.html").scanSync("src")]
+
.map(a => path.resolve("src", a))
+
.filter(dir => !dir.includes("node_modules"));
+
console.log(`📄 Found ${entrypoints.length} HTML ${entrypoints.length === 1 ? "file" : "files"} to process\n`);
+
+
const result = await Bun.build({
+
entrypoints,
+
outdir,
+
plugins: [plugin],
+
minify: true,
+
target: "browser",
+
sourcemap: "linked",
+
define: {
+
"process.env.NODE_ENV": JSON.stringify("production"),
+
},
+
...cliConfig,
+
});
+
+
const end = performance.now();
+
+
const outputTable = result.outputs.map(output => ({
+
File: path.relative(process.cwd(), output.path),
+
Type: output.kind,
+
Size: formatFileSize(output.size),
+
}));
+
+
console.table(outputTable);
+
const buildTime = (end - start).toFixed(2);
+
+
console.log(`\n✅ Build completed in ${buildTime}ms\n`);
+17
bun-env.d.ts
···
···
+
// Generated by `bun init`
+
+
declare module "*.svg" {
+
/**
+
* A path to the SVG file
+
*/
+
const path: `${string}.svg`;
+
export = path;
+
}
+
+
declare module "*.module.css" {
+
/**
+
* A record of class names to their corresponding CSS module classes
+
*/
+
const classes: { readonly [key: string]: string };
+
export = classes;
+
}
+191
bun.lock
···
···
+
{
+
"lockfileVersion": 1,
+
"workspaces": {
+
"": {
+
"name": "bun-react-template",
+
"dependencies": {
+
"@radix-ui/react-label": "^2.1.7",
+
"@radix-ui/react-select": "^2.2.6",
+
"@radix-ui/react-slot": "^1.2.3",
+
"atproto-ui": "^0.7.2",
+
"bun-plugin-tailwind": "^0.1.2",
+
"class-variance-authority": "^0.7.1",
+
"clsx": "^2.1.1",
+
"lucide-react": "^0.545.0",
+
"react": "^19",
+
"react-dom": "^19",
+
"tailwind-merge": "^3.3.1",
+
},
+
"devDependencies": {
+
"@types/bun": "latest",
+
"@types/react": "^19",
+
"@types/react-dom": "^19",
+
"tailwindcss": "^4.1.11",
+
"tw-animate-css": "^1.4.0",
+
},
+
},
+
},
+
"packages": {
+
"@atcute/atproto": ["@atcute/atproto@3.1.8", "", { "dependencies": { "@atcute/lexicons": "^1.2.2" } }, "sha512-Miu+S7RSgAYbmQWtHJKfSFUN5Kliqoo4YH0rILPmBtfmlZieORJgXNj9oO/Uive0/ulWkiRse07ATIcK8JxMnw=="],
+
+
"@atcute/bluesky": ["@atcute/bluesky@3.2.8", "", { "dependencies": { "@atcute/atproto": "^3.1.8", "@atcute/lexicons": "^1.2.2" } }, "sha512-wxEnSOvX7nLH4sVzX9YFCkaNEWIDrTv3pTs6/x4NgJ3AJ3XJio0OYPM8tR7wAgsklY6BHvlAgt3yoCDK0cl1CA=="],
+
+
"@atcute/client": ["@atcute/client@4.0.5", "", { "dependencies": { "@atcute/identity": "^1.1.1", "@atcute/lexicons": "^1.2.2" } }, "sha512-R8Qen8goGmEkynYGg2m6XFlVmz0GTDvQ+9w+4QqOob+XMk8/WDpF4aImev7WKEde/rV2gjcqW7zM8E6W9NShDA=="],
+
+
"@atcute/identity": ["@atcute/identity@1.1.1", "", { "dependencies": { "@atcute/lexicons": "^1.2.2", "@badrap/valita": "^0.4.6" } }, "sha512-zax42n693VEhnC+5tndvO2KLDTMkHOz8UExwmklvJv7R9VujfEwiSWhcv6Jgwb3ellaG8wjiQ1lMOIjLLvwh0Q=="],
+
+
"@atcute/identity-resolver": ["@atcute/identity-resolver@1.1.4", "", { "dependencies": { "@atcute/lexicons": "^1.2.2", "@atcute/util-fetch": "^1.0.3", "@badrap/valita": "^0.4.6" }, "peerDependencies": { "@atcute/identity": "^1.0.0" } }, "sha512-/SVh8vf2cXFJenmBnGeYF2aY3WGQm3cJeew5NWTlkqoy3LvJ5wkvKq9PWu4Tv653VF40rPOp6LOdVr9Fa+q5rA=="],
+
+
"@atcute/lexicons": ["@atcute/lexicons@1.2.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-bgEhJq5Z70/0TbK5sx+tAkrR8FsCODNiL2gUEvS5PuJfPxmFmRYNWaMGehxSPaXWpU2+Oa9ckceHiYbrItDTkA=="],
+
+
"@atcute/tangled": ["@atcute/tangled@1.0.10", "", { "dependencies": { "@atcute/atproto": "^3.1.8", "@atcute/lexicons": "^1.2.2" } }, "sha512-DGconZIN5TpLBah+aHGbWI1tMsL7XzyVEbr/fW4CbcLWYKICU6SAUZ0YnZ+5GvltjlORWHUy7hfftvoh4zodIA=="],
+
+
"@atcute/util-fetch": ["@atcute/util-fetch@1.0.3", "", { "dependencies": { "@badrap/valita": "^0.4.6" } }, "sha512-f8zzTb/xlKIwv2OQ31DhShPUNCmIIleX6p7qIXwWwEUjX6x8skUtpdISSjnImq01LXpltGV5y8yhV4/Mlb7CRQ=="],
+
+
"@badrap/valita": ["@badrap/valita@0.4.6", "", {}, "sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg=="],
+
+
"@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="],
+
+
"@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="],
+
+
"@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.6", "", { "dependencies": { "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw=="],
+
+
"@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
+
+
"@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-7Rap1BHNWqgnexc4wLjjdZeVRQKtk534iGuJ7qZ42i/q1B+cxJZ6zSnrFsYmo+zreH7dUyUXL3AHuXGrl2772Q=="],
+
+
"@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-wpqmgT/8w+tEr5YMGt1u1sEAMRHhyA2SKZddC6GCPasHxSqkCWOPQvYIHIApnTsoSsxhxP0x6Cpe93+4c7hq/w=="],
+
+
"@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-mJo715WvwEHmJ6khNymWyxi0QrFzU94wolsUmxolViNHrk+2ugzIkVIJhTnxf7pHnarxxHwyJ/kgatuV//QILQ=="],
+
+
"@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-ACn038SZL8del+sFnqCjf+haGB02//j2Ez491IMmPTvbv4a/D0iiNz9xiIB3ICbQd3EwQzi+Ut/om3Ba/KoHbQ=="],
+
+
"@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gKU3Wv3BTG5VMjqMMnRwqU6tipCveE9oyYNt62efy6cQK3Vo1DOBwY2SmjbFw+yzj+Um20YoFOLGxghfQET4Ng=="],
+
+
"@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-cAUeM3I5CIYlu5Ur52eCOGg9yfqibQd4lzt9G1/rA0ajqcnCBaTuekhUDZETJJf5H9QV+Gm46CqQg2DpdJzJsw=="],
+
+
"@oven/bun-linux-x64-baseline": ["@oven/bun-linux-x64-baseline@1.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-7+2aCrL81mtltZQbKdiPB58UL+Gr3DAIuPyUAKm0Ib/KG/Z8t7nD/eSMRY/q6b+NsAjYnVPiPwqSjC3edpMmmQ=="],
+
+
"@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-8AgEAHyuJ5Jm9MUo1L53K1SRYu0bNGqV0E0L5rB5DjkteO4GXrnWGBT8qsuwuy7WMuCMY3bj64/pFjlRkZuiXw=="],
+
+
"@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-tP0WWcAqrMayvkggOHBGBoyyoK+QHAqgRUyj1F6x5/udiqc9vCXmIt1tlydxYV/NvyvUAmJ7MWT0af44Xm2kJw=="],
+
+
"@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.3.1", "", { "os": "win32", "cpu": "x64" }, "sha512-xdUjOZRq6PwPbbz4/F2QEMLBZwintGp7AS50cWxgkHnyp7Omz5eJfV6/vWtN4qwZIyR3V3DT/2oXsY1+7p3rtg=="],
+
+
"@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.1", "", { "os": "win32", "cpu": "x64" }, "sha512-dcA+Kj7hGFrY3G8NWyYf3Lj3/GMViknpttWUf5pI6p6RphltZaoDu0lY5Lr71PkMdRZTwL2NnZopa/x/NWCdKA=="],
+
+
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
+
+
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
+
+
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
+
+
"@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="],
+
+
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
+
+
"@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+
"@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
+
+
"@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="],
+
+
"@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="],
+
+
"@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
+
+
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
+
+
"@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="],
+
+
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="],
+
+
"@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="],
+
+
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+
"@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="],
+
+
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
+
+
"@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
+
+
"@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="],
+
+
"@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="],
+
+
"@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
+
+
"@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="],
+
+
"@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="],
+
+
"@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="],
+
+
"@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="],
+
+
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
+
+
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
+
+
"@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="],
+
+
"@types/node": ["@types/node@24.9.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg=="],
+
+
"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
+
+
"@types/react-dom": ["@types/react-dom@19.2.2", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw=="],
+
+
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
+
+
"atproto-ui": ["atproto-ui@0.7.2", "", { "dependencies": { "@atcute/atproto": "^3.1.7", "@atcute/bluesky": "^3.2.3", "@atcute/client": "^4.0.3", "@atcute/identity-resolver": "^1.1.3", "@atcute/tangled": "^1.0.6" }, "peerDependencies": { "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0" }, "optionalPeers": ["react-dom"] }, "sha512-bVHjur5Wh5g+47p8Zaq7iZkd5zpqw5A8xg0z5rsDWkmRvqO8E3kZbL9Svco0qWQM/jg4akG/97Vn1XecATovzg=="],
+
+
"bun": ["bun@1.3.1", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.1", "@oven/bun-darwin-x64": "1.3.1", "@oven/bun-darwin-x64-baseline": "1.3.1", "@oven/bun-linux-aarch64": "1.3.1", "@oven/bun-linux-aarch64-musl": "1.3.1", "@oven/bun-linux-x64": "1.3.1", "@oven/bun-linux-x64-baseline": "1.3.1", "@oven/bun-linux-x64-musl": "1.3.1", "@oven/bun-linux-x64-musl-baseline": "1.3.1", "@oven/bun-windows-x64": "1.3.1", "@oven/bun-windows-x64-baseline": "1.3.1" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-enqkEb0RhNOgDzHQwv7uvnIhX3uSzmKzz779dL7kdH8SauyTdQvCz4O1UT2rU0UldQp2K9OlrJNdyDHayPEIvw=="],
+
+
"bun-plugin-tailwind": ["bun-plugin-tailwind@0.1.2", "", { "peerDependencies": { "bun": ">=1.0.0" } }, "sha512-41jNC1tZRSK3s1o7pTNrLuQG8kL/0vR/JgiTmZAJ1eHwe0w5j6HFPKeqEk0WAD13jfrUC7+ULuewFBBCoADPpg=="],
+
+
"bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="],
+
+
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
+
+
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
+
+
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
+
+
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
+
+
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
+
+
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
+
+
"lucide-react": ["lucide-react@0.545.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-7r1/yUuflQDSt4f1bpn5ZAocyIxcTyVyBBChSVtBKn5M+392cPmI5YJMWOJKk/HUWGm5wg83chlAZtCcGbEZtw=="],
+
+
"react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="],
+
+
"react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="],
+
+
"react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA=="],
+
+
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
+
+
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
+
+
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
+
+
"tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="],
+
+
"tailwindcss": ["tailwindcss@4.1.16", "", {}, "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA=="],
+
+
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
+
+
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
+
+
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
+
+
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
+
}
+
}
+3
bunfig.toml
···
···
+
[serve.static]
+
plugins = ["bun-plugin-tailwind"]
+
env = "BUN_PUBLIC_*"
+21
components.json
···
···
+
{
+
"$schema": "https://ui.shadcn.com/schema.json",
+
"style": "new-york",
+
"rsc": false,
+
"tsx": true,
+
"tailwind": {
+
"config": "",
+
"css": "styles/globals.css",
+
"baseColor": "neutral",
+
"cssVariables": true,
+
"prefix": ""
+
},
+
"aliases": {
+
"components": "@/components",
+
"utils": "@/lib/utils",
+
"ui": "@/components/ui",
+
"lib": "@/lib",
+
"hooks": "@/hooks"
+
},
+
"iconLibrary": "lucide"
+
}
+31
package.json
···
···
+
{
+
"name": "bun-react-template",
+
"version": "0.1.0",
+
"private": true,
+
"type": "module",
+
"scripts": {
+
"dev": "bun --hot src/index.ts",
+
"start": "NODE_ENV=production bun src/index.ts",
+
"build": "bun run build.ts"
+
},
+
"dependencies": {
+
"@radix-ui/react-label": "^2.1.7",
+
"@radix-ui/react-select": "^2.2.6",
+
"@radix-ui/react-slot": "^1.2.3",
+
"atproto-ui": "^0.7.2",
+
"bun-plugin-tailwind": "^0.1.2",
+
"class-variance-authority": "^0.7.1",
+
"clsx": "^2.1.1",
+
"lucide-react": "^0.545.0",
+
"react": "^19",
+
"react-dom": "^19",
+
"tailwind-merge": "^3.3.1"
+
},
+
"devDependencies": {
+
"@types/react": "^19",
+
"@types/react-dom": "^19",
+
"@types/bun": "latest",
+
"tailwindcss": "^4.1.11",
+
"tw-animate-css": "^1.4.0"
+
}
+
}
+50
src/App.tsx
···
···
+
import "./index.css"
+
import { useEffect, useRef, useState } from "react"
+
import { SectionNav } from "./components/SectionNav"
+
import { Header } from "./components/sections/Header"
+
import { Work } from "./components/sections/Work"
+
import { Connect } from "./components/sections/Connect"
+
import { sections } from "./data/portfolio"
+
+
export function App() {
+
const [activeSection, setActiveSection] = useState("")
+
const sectionsRef = useRef<(HTMLElement | null)[]>([])
+
+
useEffect(() => {
+
const observer = new IntersectionObserver(
+
(entries) => {
+
entries.forEach((entry) => {
+
if (entry.isIntersecting) {
+
entry.target.classList.add("animate-fade-in-up")
+
setActiveSection(entry.target.id)
+
}
+
})
+
},
+
{ threshold: 0.1, rootMargin: "0px 0px -5% 0px" },
+
)
+
+
sectionsRef.current.forEach((section) => {
+
if (section) observer.observe(section)
+
})
+
+
return () => observer.disconnect()
+
}, [])
+
+
+
+
return (
+
<div className="min-h-screen dark:bg-background text-foreground relative">
+
<SectionNav sections={sections} activeSection={activeSection} />
+
+
<main>
+
<div className="max-w-4xl mx-auto px-6 sm:px-8 lg:px-16">
+
<Header sectionRef={(el) => (sectionsRef.current[0] = el)} />
+
</div>
+
<Work sectionRef={(el) => (sectionsRef.current[1] = el)} />
+
<Connect sectionRef={(el) => (sectionsRef.current[2] = el)} />
+
</main>
+
</div>
+
)
+
}
+
+
export default App
+37
src/components/BlogCard.tsx
···
···
+
interface BlogCardProps {
+
title: string
+
excerpt: string
+
date: string
+
readTime: string
+
}
+
+
export function BlogCard({ title, excerpt, date, readTime }: BlogCardProps) {
+
return (
+
<article className="group glass glass-hover p-6 sm:p-8 rounded-lg transition-all duration-500 cursor-pointer">
+
<div className="space-y-4">
+
<div className="flex items-center justify-between text-xs text-muted-foreground font-mono">
+
<span>{date}</span>
+
<span>{readTime}</span>
+
</div>
+
+
<h3 className="text-lg sm:text-xl font-medium group-hover:text-muted-foreground transition-colors duration-300">
+
{title}
+
</h3>
+
+
<p className="text-muted-foreground leading-relaxed">{excerpt}</p>
+
+
<div className="flex items-center gap-2 text-sm text-muted-foreground group-hover:text-foreground transition-colors duration-300">
+
<span>Read more</span>
+
<svg
+
className="w-4 h-4 transform group-hover:translate-x-1 transition-transform duration-300"
+
fill="none"
+
stroke="currentColor"
+
viewBox="0 0 24 24"
+
>
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
+
</svg>
+
</div>
+
</div>
+
</article>
+
)
+
}
+81
src/components/ProjectCard.tsx
···
···
+
interface ProjectCardProps {
+
title: string
+
description: string
+
year: string
+
tech: string[]
+
links?: {
+
live?: string
+
github?: string
+
}
+
}
+
+
export function ProjectCard({ title, description, year, tech, links }: ProjectCardProps) {
+
return (
+
<div className="group p-6 sm:p-8 rounded-lg transition-all duration-500">
+
<div className="space-y-4">
+
<div className="flex items-start justify-between gap-4">
+
<div className="space-y-2 flex-1">
+
<h3 className="text-lg sm:text-xl font-medium group-hover:text-muted-foreground transition-colors duration-300">
+
{title}
+
</h3>
+
<div className="text-xs text-muted-foreground font-mono">{year}</div>
+
</div>
+
+
{links && (
+
<div className="flex gap-2">
+
{links.github && (
+
<a
+
href={links.github}
+
target="_blank"
+
rel="noopener noreferrer"
+
className="p-2 rounded-lg transition-all duration-300 hover:bg-muted"
+
aria-label="View on GitHub"
+
>
+
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
+
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
+
</svg>
+
</a>
+
)}
+
{links.live && (
+
<a
+
href={links.live}
+
target="_blank"
+
rel="noopener noreferrer"
+
className="p-2 rounded-lg transition-all duration-300 hover:bg-muted"
+
aria-label="View live site"
+
>
+
<svg
+
className="w-4 h-4"
+
fill="none"
+
stroke="currentColor"
+
viewBox="0 0 24 24"
+
strokeWidth={2}
+
>
+
<path
+
strokeLinecap="round"
+
strokeLinejoin="round"
+
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
+
/>
+
</svg>
+
</a>
+
)}
+
</div>
+
)}
+
</div>
+
+
<p className="text-muted-foreground leading-relaxed">{description}</p>
+
+
<div className="flex flex-wrap gap-2">
+
{tech.map((techItem) => (
+
<span
+
key={techItem}
+
className="bg-muted px-3 py-1 text-xs rounded-full transition-colors duration-300"
+
>
+
{techItem}
+
</span>
+
))}
+
</div>
+
</div>
+
</div>
+
)
+
}
+30
src/components/SectionNav.tsx
···
···
+
import type { Section } from "../data/portfolio"
+
import { useSystemTheme } from "../hooks/useSystemTheme"
+
+
interface SectionNavProps {
+
sections: readonly Section[]
+
activeSection: string
+
}
+
+
export function SectionNav({ sections, activeSection }: SectionNavProps) {
+
const scrollToSection = (section: string) => {
+
document.getElementById(section)?.scrollIntoView({ behavior: "smooth" })
+
}
+
+
return (
+
<nav className="fixed left-8 top-1/2 -translate-y-1/2 z-10 hidden lg:block">
+
<div className="flex flex-col gap-4">
+
{sections.map((section) => (
+
<button
+
key={section}
+
onClick={() => scrollToSection(section)}
+
className={`w-2 h-8 rounded-full transition-all duration-500 ${
+
activeSection === section ? "bg-foreground" : "glass glass-hover"
+
}`}
+
aria-label={`Navigate to ${section}`}
+
/>
+
))}
+
</div>
+
</nav>
+
)
+
}
+25
src/components/SocialLink.tsx
···
···
+
interface SocialLinkProps {
+
name: string
+
handle: string
+
url: string
+
}
+
+
export function SocialLink({ name, handle, url }: SocialLinkProps) {
+
return (
+
<a
+
href={url}
+
className="group p-4 border border-[oklch(0.48_0.015_255)] dark:border-border rounded-lg hover:border-[oklch(0.3_0.015_255)] dark:hover:border-muted-foreground/50 transition-all duration-300 hover:shadow-sm block"
+
target="_blank"
+
rel="noopener noreferrer"
+
>
+
<div className="space-y-2">
+
<div className="text-[oklch(0.2_0.02_255)] dark:text-foreground dark:group-hover:text-muted-foreground transition-colors duration-300">
+
{name}
+
</div>
+
<div className="text-sm text-[oklch(0.48_0.015_255)] dark:text-muted-foreground">
+
{handle}
+
</div>
+
</div>
+
</a>
+
)
+
}
+36
src/components/ThemeToggle.tsx
···
···
+
interface ThemeToggleProps {
+
isDark: boolean
+
onToggle: () => void
+
}
+
+
export function ThemeToggle({ isDark, onToggle }: ThemeToggleProps) {
+
return (
+
<button
+
onClick={onToggle}
+
className="group glass glass-hover p-3 rounded-lg transition-all duration-300"
+
aria-label="Toggle theme"
+
>
+
{isDark ? (
+
<svg
+
className="w-4 h-4 text-muted-foreground group-hover:text-foreground transition-colors duration-300"
+
fill="currentColor"
+
viewBox="0 0 20 20"
+
>
+
<path
+
fillRule="evenodd"
+
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
+
clipRule="evenodd"
+
/>
+
</svg>
+
) : (
+
<svg
+
className="w-4 h-4 text-muted-foreground group-hover:text-foreground transition-colors duration-300"
+
fill="currentColor"
+
viewBox="0 0 20 20"
+
>
+
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
+
</svg>
+
)}
+
</button>
+
)
+
}
+121
src/components/WorkExperienceCard.tsx
···
···
+
interface Project {
+
title: string
+
description: string
+
tech: string[]
+
links?: {
+
live?: string
+
github?: string
+
}
+
}
+
+
interface WorkExperienceCardProps {
+
year: string
+
role: string
+
company: string
+
description: string
+
tech: string[]
+
projects?: Project[]
+
}
+
+
export function WorkExperienceCard({ year, role, company, description, tech, projects }: WorkExperienceCardProps) {
+
return (
+
<div className="group py-6 sm:py-8 border-b border-border/50 hover:border-border transition-colors duration-500">
+
<div className="grid lg:grid-cols-12 gap-4 sm:gap-8">
+
<div className="lg:col-span-2">
+
<div className="text-xl sm:text-2xl font-light text-[oklch(0.48_0.015_255)] dark:text-muted-foreground dark:group-hover:text-foreground transition-colors duration-500">
+
{year}
+
</div>
+
</div>
+
+
<div className="lg:col-span-10 space-y-3">
+
<div className="flex items-start justify-between gap-4">
+
<div>
+
<h3 className="text-lg sm:text-xl font-medium text-[oklch(0.2_0.02_255)] dark:text-foreground">{role}</h3>
+
<div className="text-[oklch(0.48_0.015_255)] dark:text-muted-foreground">
+
{company}
+
</div>
+
</div>
+
<div className="flex flex-wrap gap-2 justify-end items-start">
+
{tech.map((techItem) => (
+
<span
+
key={techItem}
+
className="px-2 py-1 text-xs rounded text-[oklch(0.48_0.015_255)] dark:text-muted-foreground transition-colors duration-500"
+
>
+
{techItem}
+
</span>
+
))}
+
</div>
+
</div>
+
<p className="leading-relaxed text-[oklch(0.48_0.015_255)] dark:text-muted-foreground">
+
{description}
+
</p>
+
</div>
+
</div>
+
+
{projects && projects.length > 0 && (
+
<div className="mt-6 lg:ml-[calc((2/12)*100%+2rem)] space-y-4">
+
<div className="text-xs font-mono tracking-wider text-[oklch(0.48_0.015_255)] dark:text-muted-foreground">
+
PROJECTS
+
</div>
+
<div className="space-y-4">
+
{projects.map((project, index) => (
+
<div
+
key={index}
+
className="glass glass-hover p-4 rounded-lg transition-colors duration-300"
+
>
+
<div className="space-y-3">
+
<div className="flex items-start justify-between gap-4">
+
<h4 className="font-medium text-md text-[oklch(0.2_0.02_255)] dark:text-foreground">{project.title}</h4>
+
{project.links && (
+
<div className="flex gap-2">
+
{project.links.github && (
+
<a
+
href={project.links.github}
+
target="_blank"
+
rel="noopener noreferrer"
+
className="transition-colors text-[oklch(0.48_0.015_255)] dark:text-muted-foreground dark:hover:text-foreground"
+
aria-label="View on GitHub"
+
>
+
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
+
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
+
</svg>
+
</a>
+
)}
+
{project.links.live && (
+
<a
+
href={project.links.live}
+
target="_blank"
+
rel="noopener noreferrer"
+
className="transition-colors text-[oklch(0.48_0.015_255)] dark:text-muted-foreground dark:hover:text-foreground"
+
aria-label="View live site"
+
>
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
+
<path strokeLinecap="round" strokeLinejoin="round" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
+
</svg>
+
</a>
+
)}
+
</div>
+
)}
+
</div>
+
<p className="text-md leading-relaxed text-[oklch(0.48_0.015_255)] dark:text-muted-foreground">
+
{project.description}
+
</p>
+
<div className="flex flex-wrap gap-2">
+
{project.tech.map((techItem) => (
+
<span
+
key={techItem}
+
className="glass glass-hover px-2 py-1 text-xs rounded text-[oklch(0.48_0.015_255)] dark:text-muted-foreground"
+
>
+
{techItem}
+
</span>
+
))}
+
</div>
+
</div>
+
</div>
+
))}
+
</div>
+
</div>
+
)}
+
</div>
+
)
+
}
+131
src/components/sections/Connect.tsx
···
···
+
import { SocialLink } from "../SocialLink";
+
import { personalInfo, socialLinks } from "../../data/portfolio";
+
import { useSystemTheme } from "../../hooks/useSystemTheme";
+
import { BlueskyProfile } from "atproto-ui";
+
import { type AtProtoStyles } from "atproto-ui";
+
+
interface ConnectProps {
+
sectionRef: (el: HTMLElement | null) => void;
+
}
+
+
export function Connect({ sectionRef }: ConnectProps) {
+
const isLightMode = useSystemTheme();
+
+
return (
+
<section
+
id="connect"
+
ref={sectionRef}
+
className="py-20 sm:py-32 opacity-0"
+
style={
+
isLightMode
+
? {
+
backgroundColor: "#e2e2e2",
+
color: "oklch(0.2 0.02 255)",
+
}
+
: {}
+
}
+
>
+
<div className="max-w-4xl mx-auto px-6 sm:px-8 lg:px-16">
+
<div className="grid lg:grid-cols-2 gap-12 sm:gap-16">
+
<div className="space-y-6 sm:space-y-8">
+
<h2 className="text-3xl sm:text-4xl font-light">
+
Let's Connect
+
</h2>
+
+
<div className="space-y-6">
+
<p
+
className="text-lg sm:text-xl leading-relaxed"
+
style={
+
isLightMode
+
? {
+
color: "oklch(0.48 0.015 255)",
+
}
+
: {}
+
}
+
>
+
Always interested in new opportunities,
+
collaborations, and conversations about technology
+
and design.
+
</p>
+
+
<div className="space-y-4">
+
<a
+
href={`mailto:${personalInfo.contact.email}`}
+
className="group flex items-center gap-3 transition-colors duration-300"
+
style={
+
isLightMode
+
? {
+
color: "oklch(0.2 0.02 255)",
+
}
+
: {}
+
}
+
onMouseEnter={(e) => {
+
if (isLightMode) {
+
e.currentTarget.style.color =
+
"oklch(0.48 0.015 255)";
+
}
+
}}
+
onMouseLeave={(e) => {
+
if (isLightMode) {
+
e.currentTarget.style.color =
+
"oklch(0.2 0.02 255)";
+
}
+
}}
+
>
+
<span className="text-base sm:text-lg">
+
{personalInfo.contact.email}
+
</span>
+
<svg
+
className="w-5 h-5 transform group-hover:translate-x-1 transition-transform duration-300"
+
fill="none"
+
stroke="currentColor"
+
viewBox="0 0 24 24"
+
>
+
<path
+
strokeLinecap="round"
+
strokeLinejoin="round"
+
strokeWidth={2}
+
d="M17 8l4 4m0 0l-4 4m4-4H3"
+
/>
+
</svg>
+
</a>
+
</div>
+
</div>
+
</div>
+
+
<div className="space-y-6 sm:space-y-8">
+
<div
+
className="text-sm font-mono"
+
style={
+
isLightMode
+
? {
+
color: "oklch(0.48 0.015 255)",
+
}
+
: {}
+
}
+
>
+
ELSEWHERE
+
</div>
+
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
+
{socialLinks.map((social) => (
+
<SocialLink key={social.name} {...social} />
+
))}
+
</div>
+
</div>
+
</div>
+
<div style={{
+
'--atproto-color-bg': isLightMode ? '#f2f2f2' : '#1f1f1f',
+
} as AtProtoStyles }
+
className="pt-8 grid lg:grid-cols-2 gap-12 sm:gap-4"
+
>
+
<BlueskyProfile did="nekomimi.pet" />
+
<BlueskyProfile did="art.nekomimi.pet" />
+
<p className="text-sm sm:text-base">
+
^ This is <a className="text-cyan-600" href="https://tangled.org/@nekomimi.pet/atproto-ui" target="_blank" rel="noopener noreferrer">atproto-ui</a> btw. :)
+
</p>
+
</div>
+
</div>
+
</section>
+
);
+
}
+64
src/components/sections/Footer.tsx
···
···
+
import { useSystemTheme } from "../../hooks/useSystemTheme"
+
+
export function Footer() {
+
const isLightMode = useSystemTheme()
+
+
return (
+
<footer
+
className="py-12 sm:py-16 border-t"
+
style={isLightMode ? {
+
backgroundColor: '#e2e2e2',
+
color: 'oklch(0.2 0.02 255)',
+
borderColor: 'oklch(0.88 0.01 255)'
+
} : {}}
+
>
+
<div className="flex flex-col lg:flex-row justify-between items-start lg:items-center gap-6 sm:gap-8">
+
<div className="flex items-center gap-4">
+
<button
+
className="group p-3 rounded-lg border transition-all duration-300"
+
style={isLightMode ? {
+
borderColor: 'oklch(0.88 0.01 255)'
+
} : {}}
+
onMouseEnter={(e) => {
+
if (isLightMode) {
+
e.currentTarget.style.borderColor = 'oklch(0.48 0.015 255)'
+
}
+
}}
+
onMouseLeave={(e) => {
+
if (isLightMode) {
+
e.currentTarget.style.borderColor = 'oklch(0.88 0.01 255)'
+
}
+
}}
+
>
+
<svg
+
className="w-4 h-4 transition-colors duration-300"
+
style={isLightMode ? {
+
color: 'oklch(0.48 0.015 255)'
+
} : {}}
+
fill="none"
+
stroke="currentColor"
+
viewBox="0 0 24 24"
+
onMouseEnter={(e) => {
+
if (isLightMode) {
+
e.currentTarget.style.color = 'oklch(0.2 0.02 255)'
+
}
+
}}
+
onMouseLeave={(e) => {
+
if (isLightMode) {
+
e.currentTarget.style.color = 'oklch(0.48 0.015 255)'
+
}
+
}}
+
>
+
<path
+
strokeLinecap="round"
+
strokeLinejoin="round"
+
strokeWidth={2}
+
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
+
/>
+
</svg>
+
</button>
+
</div>
+
</div>
+
</footer>
+
)
+
}
+193
src/components/sections/Header.tsx
···
···
+
import type { RefObject } from "react"
+
import { personalInfo, currentRole, skills } from "../../data/portfolio"
+
+
interface HeaderProps {
+
sectionRef: (el: HTMLElement | null) => void
+
}
+
+
export function Header({ sectionRef }: HeaderProps) {
+
const scrollToWork = () => {
+
document.getElementById('work')?.scrollIntoView({ behavior: 'smooth' })
+
}
+
+
return (
+
<header id="intro" ref={sectionRef} className="min-h-screen flex items-center opacity-0 relative">
+
{/* Background Image - Full Width */}
+
<div
+
className="absolute top-0 bottom-0 left-1/2 right-1/2 -ml-[50vw] -mr-[50vw] w-screen z-0"
+
style={{
+
backgroundImage: 'url(https://cdn.donmai.us/original/31/2f/__kazusa_blue_archive_drawn_by_astelia__312fc11a21c5d4ce06dc3aa8bfbb7221.jpg)',
+
backgroundSize: 'cover',
+
backgroundPosition: 'center',
+
backgroundRepeat: 'no-repeat',
+
}}
+
>
+
{/* Overlay for better text readability */}
+
<div className="absolute inset-0 bg-background/70"></div>
+
</div>
+
+
<div className="grid lg:grid-cols-5 gap-12 sm:gap-16 w-full relative z-10">
+
<div className="lg:col-span-3 space-y-6 sm:space-y-8">
+
<div className="space-y-3 sm:space-y-2">
+
<div className="text-sm text-gray-300 font-mono tracking-wider">PORTFOLIO / 2025</div>
+
<h1 className="text-md sm:text-md lg:text-4xl font-light tracking-tight text-cyan-400">
+
{personalInfo.name.first}
+
<br />
+
<span className=" text-gray-400">{personalInfo.name.last}</span>
+
</h1>
+
</div>
+
+
<div className="space-y-6 max-w-md">
+
<p className="text-lg sm:text-xl text-stone-200 leading-relaxed">
+
{personalInfo.description.map((part, i) => {
+
if (part.url) {
+
return (
+
<a
+
key={i}
+
href={part.url}
+
target="_blank"
+
rel="noopener noreferrer"
+
className="text-cyan-400/70 hover:text-cyan-300 font-medium transition-colors duration-300 underline decoration-cyan-400/30 hover:decoration-cyan-300/50"
+
>
+
{part.text}
+
</a>
+
)
+
}
+
+
if (part.bold) {
+
return (
+
<span key={i} className="text-white font-medium">
+
{part.text}
+
</span>
+
)
+
}
+
+
return part.text
+
})}
+
</p>
+
+
<div className="space-y-4">
+
<div className="flex items-center gap-2 text-sm text-gray-300">
+
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
+
{personalInfo.availability.status}
+
</div>
+
<div className="flex items-center gap-4">
+
<a
+
href={`mailto:${personalInfo.contact.email}`}
+
className="glass glass-hover px-6 py-3 rounded-lg transition-all duration-300 inline-flex items-center justify-center gap-2 text-sm text-gray-300 hover:text-white flex-1"
+
>
+
<svg
+
className="w-4 h-4"
+
fill="none"
+
stroke="currentColor"
+
viewBox="0 0 24 24"
+
strokeWidth={2}
+
>
+
<path
+
strokeLinecap="round"
+
strokeLinejoin="round"
+
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
+
/>
+
</svg>
+
Email me
+
</a>
+
<a
+
href="https://nekomimi.leaflet.pub"
+
target="_blank"
+
rel="noopener noreferrer"
+
className="glass glass-hover px-6 py-3 rounded-lg transition-all duration-300 inline-flex items-center justify-center gap-2 text-sm text-gray-300 hover:text-white flex-1"
+
>
+
<svg
+
className="w-4 h-4"
+
fill="none"
+
stroke="currentColor"
+
viewBox="0 0 24 24"
+
strokeWidth={2}
+
>
+
<path
+
strokeLinecap="round"
+
strokeLinejoin="round"
+
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
+
/>
+
</svg>
+
Read my blog
+
</a>
+
</div>
+
</div>
+
</div>
+
</div>
+
+
<div className="lg:col-span-2 flex flex-col justify-end space-y-6 sm:space-y-8 mt-8 lg:mt-0">
+
<div className="space-y-4">
+
<div className="text-sm text-gray-300 font-mono">CURRENTLY</div>
+
<div className="space-y-2">
+
<div className="text-white">{currentRole.title}</div>
+
<div className="text-sm text-gray-300">{personalInfo.availability.location}</div>
+
<div className="text-gray-300">@ {currentRole.company}</div>
+
<div className="text-xs text-gray-100">{currentRole.period}</div>
+
</div>
+
</div>
+
+
<div className="space-y-4">
+
<div className="text-sm text-gray-300 font-mono">FOCUS</div>
+
<div className="flex flex-wrap gap-2">
+
{skills.map((skill) => (
+
<span
+
key={skill}
+
className="glass glass-hover px-3 py-1 text-xs rounded-full transition-colors duration-300"
+
>
+
{skill}
+
</span>
+
))}
+
</div>
+
</div>
+
</div>
+
</div>
+
+
{/* Image Source Link */}
+
<a
+
href="https://danbooru.donmai.us/posts/9959832"
+
target="_blank"
+
rel="noopener noreferrer"
+
className="absolute bottom-8 right-8 glass glass-hover p-2 rounded-lg transition-all duration-300 z-20 text-xs dark:text-gray-400 text-gray-600 hover:dark:text-gray-200 hover:text-gray-800"
+
aria-label="View image source"
+
>
+
<svg
+
className="w-4 h-4 inline-block mr-1"
+
fill="none"
+
stroke="currentColor"
+
viewBox="0 0 24 24"
+
strokeWidth={2}
+
>
+
<path
+
strokeLinecap="round"
+
strokeLinejoin="round"
+
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
+
/>
+
</svg>
+
Source
+
</a>
+
+
{/* Scroll Down Arrow */}
+
<button
+
onClick={scrollToWork}
+
className="absolute bottom-8 left-1/2 -translate-x-1/2 glass glass-hover p-3 rounded-full animate-bounce-slow transition-all duration-300 z-20"
+
aria-label="Scroll to work section"
+
>
+
<svg
+
className="w-5 h-5 text-gray-300"
+
fill="none"
+
stroke="currentColor"
+
viewBox="0 0 24 24"
+
strokeWidth={2}
+
>
+
<path
+
strokeLinecap="round"
+
strokeLinejoin="round"
+
d="M19 14l-7 7m0 0l-7-7m7 7V3"
+
/>
+
</svg>
+
</button>
+
</header>
+
)
+
}
+45
src/components/sections/Work.tsx
···
···
+
import { WorkExperienceCard } from "../WorkExperienceCard"
+
import { workExperience } from "../../data/portfolio"
+
import { useSystemTheme } from "../../hooks/useSystemTheme"
+
+
interface WorkProps {
+
sectionRef: (el: HTMLElement | null) => void
+
}
+
+
export function Work({ sectionRef }: WorkProps) {
+
const isLightMode = useSystemTheme()
+
+
return (
+
<section
+
id="work"
+
ref={sectionRef}
+
className="min-h-screen py-20 sm:py-32 opacity-0"
+
style={isLightMode ? {
+
backgroundColor: '#e2e2e2',
+
color: 'oklch(0.2 0.02 255)'
+
} : {}}
+
>
+
<div className="max-w-4xl mx-auto px-6 sm:px-8 lg:px-16">
+
<div className="space-y-12 sm:space-y-16">
+
<div className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-4">
+
<h2 className="text-3xl sm:text-4xl font-light">Selected Work</h2>
+
<div
+
className="text-sm font-mono"
+
style={isLightMode ? {
+
color: 'oklch(0.48 0.015 255)'
+
} : {}}
+
>
+
2016 — 2025
+
</div>
+
</div>
+
+
<div className="space-y-8 sm:space-y-12">
+
{workExperience.map((job, index) => (
+
<WorkExperienceCard key={index} {...job} />
+
))}
+
</div>
+
</div>
+
</div>
+
</section>
+
)
+
}
+52
src/components/ui/button.tsx
···
···
+
import { Slot } from "@radix-ui/react-slot";
+
import { cva, type VariantProps } from "class-variance-authority";
+
import * as React from "react";
+
+
import { cn } from "@/lib/utils";
+
+
const buttonVariants = cva(
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-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",
+
{
+
variants: {
+
variant: {
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
+
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:
+
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
+
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
+
link: "text-primary underline-offset-4 hover:underline",
+
},
+
size: {
+
default: "h-9 px-4 py-2 has-[>svg]:px-3",
+
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
+
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
+
icon: "size-9",
+
"icon-sm": "size-8",
+
"icon-lg": "size-10",
+
},
+
},
+
defaultVariants: {
+
variant: "default",
+
size: "default",
+
},
+
},
+
);
+
+
function Button({
+
className,
+
variant,
+
size,
+
asChild = false,
+
...props
+
}: React.ComponentProps<"button"> &
+
VariantProps<typeof buttonVariants> & {
+
asChild?: boolean;
+
}) {
+
const Comp = asChild ? Slot : "button";
+
+
return <Comp data-slot="button" className={cn(buttonVariants({ variant, size, className }))} {...props} />;
+
}
+
+
export { Button, buttonVariants };
+56
src/components/ui/card.tsx
···
···
+
import * as React from "react";
+
+
import { cn } from "@/lib/utils";
+
+
function Card({ className, ...props }: React.ComponentProps<"div">) {
+
return (
+
<div
+
data-slot="card"
+
className={cn("glass glass-hover text-card-foreground flex flex-col gap-6 rounded-xl py-6", className)}
+
{...props}
+
/>
+
);
+
}
+
+
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
+
return (
+
<div
+
data-slot="card-header"
+
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",
+
className,
+
)}
+
{...props}
+
/>
+
);
+
}
+
+
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
+
return <div data-slot="card-title" className={cn("leading-none font-semibold", className)} {...props} />;
+
}
+
+
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
+
return <div data-slot="card-description" className={cn("text-muted-foreground text-sm", 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-6", className)} {...props} />;
+
}
+
+
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
+
return (
+
<div data-slot="card-footer" className={cn("flex items-center px-6 [.border-t]:pt-6", className)} {...props} />
+
);
+
}
+
+
export { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle };
+21
src/components/ui/input.tsx
···
···
+
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 };
+21
src/components/ui/label.tsx
···
···
+
"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 };
+162
src/components/ui/select.tsx
···
···
+
"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,
+
};
+18
src/components/ui/textarea.tsx
···
···
+
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 };
+118
src/data/portfolio.ts
···
···
+
type DescriptionPart = {
+
text: string
+
bold?: boolean
+
url?: string
+
}
+
+
export const personalInfo = {
+
name: {
+
first: "at://nekomimi.pet",
+
last: "",
+
},
+
title: "Developer, cat",
+
description: [
+
{ text: "A cat working on " },
+
{ text: "useful", bold: true },
+
{ text: ", " },
+
{ text: "genuine", bold: true },
+
{ text: " " },
+
{ text: "decentralized", bold: true, url: "https://atproto.com" },
+
{ text: " experiences. Not Web3. Also likes to " },
+
{ text: "draw", bold: true, url: "https://bsky.app/profile/art.nekomimi.pet" },
+
{ text: "." },
+
],
+
availability: {
+
status: "Available for work",
+
location: "Richmond, VA, USA",
+
},
+
contact: {
+
email: "ana@nekomimi.pet",
+
},
+
}
+
+
export const currentRole = {
+
title: "Freelance",
+
company: "maybe your company :)",
+
period: "2021 — Present",
+
}
+
+
export const skills = ["React", "TypeScript", "Go", "Devops/Infra", "Atproto", "Cryptocurrencies"]
+
+
export const workExperience = [
+
{
+
year: "2020-2025",
+
role: "Fullstack Engineer, Infra/DevOps, Security Reviews",
+
company: "Freelance",
+
description: "Partook in various freelance work while studying for my bachelor's. Took a strong interest in the AT Protocol and started building libraries to support it.",
+
tech: ["React", "TypeScript", "Bun", "SQL"],
+
projects: [
+
{
+
title: "atproto-ui",
+
description: "A React component for rendering common UI elements across AT applications like Bluesky, Leaflet.pub, and more.",
+
tech: ["React", "TypeScript"],
+
links: {
+
live: "https://atproto-ui.netlify.app/",
+
github: "https://tangled.org/@nekomimi.pet/atproto-ui",
+
},
+
},
+
{
+
title: "wisp.place",
+
description: "A static site hoster built on the AT protocol. Users retain control over site data and content while Wisp acts as a CDN.",
+
tech: ["React", "TypeScript", "Bun", "ElysiaJS", "Hono", "Docker"],
+
links: {
+
live: "https://wisp.place",
+
github: "#",
+
},
+
},
+
{
+
title: "Confidential",
+
description: "NixOS consulting. Maintaining automated deployments, provisioning, and configuration management.",
+
tech: ["Nix", "Terraform"],
+
},
+
{
+
title: "Embedder",
+
description: "An unbloated media self-hostable service specialized in great looking embeds for services like Discord. Autocompresses videos using FFmpeg. Loved by many gamers and has over 2k installs.",
+
tech: ["HTMX", "Express", "TypeScript"],
+
links: {
+
github: "https://github.com/waveringana/embedder",
+
},
+
}
+
],
+
},
+
{
+
year: "2016-2019",
+
role: "Software Engineer, DevOps",
+
company: "Horizen.io, later freelance",
+
description: "Helped launch Horizen, built various necessary blockchain infrastructure like explorers and pools. Managed CI/CD pipelines, infrastructure, and community support.",
+
tech: ["Node.js", "Jenkins", "C++"],
+
projects: [
+
{
+
title: "Z-NOMP",
+
description: "A port of NOMP for Equihash Coins, built using Node.js and Redis.",
+
tech: ["Node.js", "Redis"],
+
links: {
+
github: "https://github.com/z-classic/z-nomp",
+
},
+
},
+
{
+
title: "Equihash-Solomining",
+
description: "Pool software dedicated for Solomining, initially for equihash but for private clients, worked to adapt to PoW of their needs.",
+
tech: ["Node.js", "Redis", "d3.js"],
+
links: {
+
github: "https://github.com/waveringana/equihash-solomining",
+
},
+
}
+
]
+
},
+
]
+
+
export const socialLinks = [
+
{ name: "GitHub", handle: "@waveringana", url: "https://github.com/waveringana" },
+
{ name: "Bluesky", handle: "@nekomimi.pet", url: "https://bsky.app/profile/nekomimi.pet" },
+
{ name: "Tangled", handle: "@nekomimi.pet", url: "https://tangled.org/@nekomimi.pet" },
+
{ name: "Vgen", handle: "@ananekomimi", url: "https://vgen.co/ananekomimi" }
+
]
+
+
export const sections = ["intro", "work", "connect"] as const
+
+
export type Section = (typeof sections)[number]
+29
src/frontend.tsx
···
···
+
/**
+
* 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";
+
import { AtProtoProvider } from "atproto-ui"
+
+
const elem = document.getElementById("root")!;
+
const app = (
+
<StrictMode>
+
<AtProtoProvider>
+
<App />
+
</AtProtoProvider>
+
</StrictMode>
+
);
+
+
if (import.meta.hot) {
+
// With hot module reloading, `import.meta.hot.data` is persisted.
+
const root = (import.meta.hot.data.root ??= createRoot(elem));
+
root.render(app);
+
} else {
+
// The hot module reloading API is not available in production.
+
createRoot(elem).render(app);
+
}
+37
src/hooks/useSystemTheme.ts
···
···
+
import { useState, useEffect } from "react"
+
+
export function useSystemTheme() {
+
const [isLightMode, setIsLightMode] = useState(false)
+
+
useEffect(() => {
+
const mediaQuery = window.matchMedia("(prefers-color-scheme: light)")
+
+
const handleChange = (e: MediaQueryListEvent) => {
+
const isLight = e.matches
+
setIsLightMode(isLight)
+
+
// Apply or remove the 'dark' class on the document element
+
if (isLight) {
+
document.documentElement.classList.remove('dark')
+
} else {
+
document.documentElement.classList.add('dark')
+
}
+
}
+
+
const isLight = mediaQuery.matches
+
setIsLightMode(isLight)
+
+
// Apply or remove the 'dark' class on initial load
+
if (isLight) {
+
document.documentElement.classList.remove('dark')
+
} else {
+
document.documentElement.classList.add('dark')
+
}
+
+
mediaQuery.addEventListener("change", handleChange)
+
+
return () => mediaQuery.removeEventListener("change", handleChange)
+
}, [])
+
+
return isLightMode
+
}
+1
src/index.css
···
···
+
@import "../styles/globals.css";
+49
src/index.html
···
···
+
<!doctype html>
+
<html lang="en">
+
<head>
+
<meta charset="UTF-8" />
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+
<title>Bun + React</title>
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
+
<link
+
href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@300..700&display=swap"
+
rel="stylesheet"
+
/>
+
<script type="module" src="./frontend.tsx" async></script>
+
</head>
+
<body>
+
<div id="root"></div>
+
+
<svg
+
xmlns="http://www.w3.org/2000/svg"
+
width="0"
+
height="0"
+
style="position: absolute; overflow: hidden"
+
>
+
<defs>
+
<filter id="frosted" x="0%" y="0%" width="100%" height="100%">
+
<feTurbulence
+
type="fractalNoise"
+
baseFrequency="0.008 0.008"
+
numOctaves="2"
+
seed="92"
+
result="noise"
+
/>
+
<feGaussianBlur
+
in="noise"
+
stdDeviation="2"
+
result="blurred"
+
/>
+
<feDisplacementMap
+
in="SourceGraphic"
+
in2="blurred"
+
scale="77"
+
xChannelSelector="R"
+
yChannelSelector="G"
+
/>
+
</filter>
+
</defs>
+
</svg>
+
</body>
+
</html>
+49
src/index.ts
···
···
+
import { serve } from "bun";
+
import index from "./index.html";
+
+
const server = serve({
+
routes: {
+
"/.well-known/atproto-did": async () => {
+
return new Response("did:plc:ttdrpj45ibqunmfhdsb4zdwq", {
+
headers: {
+
"Content-Type": "text/plain",
+
},
+
});
+
},
+
+
// 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}`);
+6
src/lib/utils.ts
···
···
+
import { type ClassValue, clsx } from "clsx";
+
import { twMerge } from "tailwind-merge";
+
+
export function cn(...inputs: ClassValue[]) {
+
return twMerge(clsx(inputs));
+
}
+166
styles/globals.css
···
···
+
@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);
+
--font-sans: "Fira Code", monospace;
+
}
+
+
:root {
+
--radius: 0.625rem;
+
/* Dark mode - #1a1a1a primary, #2a2a2a secondary */
+
--background: #1a1a1a;
+
--foreground: oklch(0.92 0.005 255);
+
--card: #2a2a2a;
+
--card-foreground: oklch(0.92 0.005 255);
+
--popover: #2a2a2a;
+
--popover-foreground: oklch(0.92 0.005 255);
+
--primary: oklch(0.88 0.008 255);
+
--primary-foreground: #1a1a1a;
+
--secondary: #2a2a2a;
+
--secondary-foreground: oklch(0.92 0.005 255);
+
--muted: #2a2a2a;
+
--muted-foreground: oklch(0.65 0.012 255);
+
--accent: #2a2a2a;
+
--accent-foreground: oklch(0.92 0.005 255);
+
--destructive: oklch(0.704 0.191 22.216);
+
--border: #333333;
+
--input: #333333;
+
--ring: oklch(0.5 0.015 255);
+
--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: #2a2a2a;
+
--sidebar-foreground: oklch(0.92 0.005 255);
+
--sidebar-primary: oklch(0.488 0.243 264.376);
+
--sidebar-primary-foreground: oklch(0.95 0.005 255);
+
--sidebar-accent: #2a2a2a;
+
--sidebar-accent-foreground: oklch(0.92 0.005 255);
+
--sidebar-border: #333333;
+
--sidebar-ring: oklch(0.5 0.015 255);
+
+
/* Glassmorphism variables - Dark mode */
+
--glass-bg: rgba(255, 255, 255, 0.05);
+
--glass-border: rgba(255, 255, 255, 0.1);
+
--glass-shadow: rgba(0, 0, 0, 0.3);
+
}
+
+
@layer base {
+
* {
+
@apply border-border outline-ring/50;
+
}
+
body {
+
@apply bg-background text-foreground;
+
}
+
}
+
+
@keyframes fade-in-up {
+
from {
+
opacity: 0;
+
transform: translateY(20px);
+
}
+
to {
+
opacity: 1;
+
transform: translateY(0);
+
}
+
}
+
+
.animate-fade-in-up {
+
animation: fade-in-up 0.8s ease-out forwards;
+
}
+
+
@keyframes bounce-slow {
+
0%,
+
100% {
+
transform: translateY(0);
+
}
+
50% {
+
transform: translateY(10px);
+
}
+
}
+
+
.animate-bounce-slow {
+
animation: bounce-slow 2s ease-in-out infinite;
+
}
+
+
/* Glassmorphism utilities */
+
@layer utilities {
+
.glass {
+
--shadow-offset: 0;
+
--shadow-blur: 20px;
+
--shadow-spread: -5px;
+
--shadow-color: rgba(255, 255, 255, 0.7);
+
+
/* Painted glass */
+
--tint-color: rgba(255, 255, 255, 0.08);
+
--tint-opacity: 0.4;
+
+
/* Background frost */
+
--frost-blur: 2px;
+
+
/* SVG noise/distortion */
+
--noise-frequency: 0.008;
+
--distortion-strength: 77;
+
+
/* Outer shadow blur */
+
--outer-shadow-blur: 24px;
+
+
/*background: rgba(255, 255, 255, 0.08);*/
+
box-shadow: inset var(--shadow-offset) var(--shadow-offset)
+
var(--shadow-blur) var(--shadow-spread) var(--shadow-color);
+
background-color: rgba(var(--tint-color), var(--tint-opacity));
+
backdrop-filter: url(#frosted);
+
-webkit-backdrop-filter: url(#frosted);
+
-webkit-backdrop-filter: blur(var(--frost-blur));
+
/*border: 2px solid transparent;*/
+
}
+
+
.glass-hover {
+
transition: all 0.3s ease;
+
}
+
+
.glass-hover:hover {
+
background: rgba(255, 255, 255, 0.12);
+
box-shadow:
+
0 0 0 2px rgba(255, 255, 255, 0.7),
+
0 20px 40px rgba(0, 0, 0, 0.16);
+
}
+
}
+36
tsconfig.json
···
···
+
{
+
"compilerOptions": {
+
// Environment setup & latest features
+
"lib": ["ESNext", "DOM"],
+
"target": "ESNext",
+
"module": "Preserve",
+
"moduleDetection": "force",
+
"jsx": "react-jsx",
+
"allowJs": true,
+
+
// Bundler mode
+
"moduleResolution": "bundler",
+
"allowImportingTsExtensions": true,
+
"verbatimModuleSyntax": true,
+
"noEmit": true,
+
+
// Best practices
+
"strict": true,
+
"skipLibCheck": true,
+
"noFallthroughCasesInSwitch": true,
+
"noUncheckedIndexedAccess": true,
+
"noImplicitOverride": true,
+
+
"baseUrl": ".",
+
"paths": {
+
"@/*": ["./src/*"]
+
},
+
+
// Some stricter flags (disabled by default)
+
"noUnusedLocals": false,
+
"noUnusedParameters": false,
+
"noPropertyAccessFromIndexSignature": false
+
},
+
+
"exclude": ["dist", "node_modules"]
+
}