redesign

waveringana 2f90eead

+34
.gitignore
··· 1 + # dependencies (bun install) 2 + node_modules 3 + 4 + # output 5 + out 6 + dist 7 + *.tgz 8 + 9 + # code coverage 10 + coverage 11 + *.lcov 12 + 13 + # logs 14 + logs 15 + _.log 16 + report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 17 + 18 + # dotenv environment variable files 19 + .env 20 + .env.development.local 21 + .env.test.local 22 + .env.production.local 23 + .env.local 24 + 25 + # caches 26 + .eslintcache 27 + .cache 28 + *.tsbuildinfo 29 + 30 + # IntelliJ based IDEs 31 + .idea 32 + 33 + # Finder (MacOS) folder config 34 + .DS_Store
+21
README.md
··· 1 + # bun-react-tailwind-shadcn-template 2 + 3 + To install dependencies: 4 + 5 + ```bash 6 + bun install 7 + ``` 8 + 9 + To start a development server: 10 + 11 + ```bash 12 + bun dev 13 + ``` 14 + 15 + To run for production: 16 + 17 + ```bash 18 + bun start 19 + ``` 20 + 21 + 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
··· 1 + #!/usr/bin/env bun 2 + import plugin from "bun-plugin-tailwind"; 3 + import { existsSync } from "fs"; 4 + import { rm } from "fs/promises"; 5 + import path from "path"; 6 + 7 + if (process.argv.includes("--help") || process.argv.includes("-h")) { 8 + console.log(` 9 + 🏗️ Bun Build Script 10 + 11 + Usage: bun run build.ts [options] 12 + 13 + Common Options: 14 + --outdir <path> Output directory (default: "dist") 15 + --minify Enable minification (or --minify.whitespace, --minify.syntax, etc) 16 + --sourcemap <type> Sourcemap type: none|linked|inline|external 17 + --target <target> Build target: browser|bun|node 18 + --format <format> Output format: esm|cjs|iife 19 + --splitting Enable code splitting 20 + --packages <type> Package handling: bundle|external 21 + --public-path <path> Public path for assets 22 + --env <mode> Environment handling: inline|disable|prefix* 23 + --conditions <list> Package.json export conditions (comma separated) 24 + --external <list> External packages (comma separated) 25 + --banner <text> Add banner text to output 26 + --footer <text> Add footer text to output 27 + --define <obj> Define global constants (e.g. --define.VERSION=1.0.0) 28 + --help, -h Show this help message 29 + 30 + Example: 31 + bun run build.ts --outdir=dist --minify --sourcemap=linked --external=react,react-dom 32 + `); 33 + process.exit(0); 34 + } 35 + 36 + const toCamelCase = (str: string): string => str.replace(/-([a-z])/g, g => g[1].toUpperCase()); 37 + 38 + const parseValue = (value: string): any => { 39 + if (value === "true") return true; 40 + if (value === "false") return false; 41 + 42 + if (/^\d+$/.test(value)) return parseInt(value, 10); 43 + if (/^\d*\.\d+$/.test(value)) return parseFloat(value); 44 + 45 + if (value.includes(",")) return value.split(",").map(v => v.trim()); 46 + 47 + return value; 48 + }; 49 + 50 + function parseArgs(): Partial<Bun.BuildConfig> { 51 + const config: Partial<Bun.BuildConfig> = {}; 52 + const args = process.argv.slice(2); 53 + 54 + for (let i = 0; i < args.length; i++) { 55 + const arg = args[i]; 56 + if (arg === undefined) continue; 57 + if (!arg.startsWith("--")) continue; 58 + 59 + if (arg.startsWith("--no-")) { 60 + const key = toCamelCase(arg.slice(5)); 61 + config[key] = false; 62 + continue; 63 + } 64 + 65 + if (!arg.includes("=") && (i === args.length - 1 || args[i + 1]?.startsWith("--"))) { 66 + const key = toCamelCase(arg.slice(2)); 67 + config[key] = true; 68 + continue; 69 + } 70 + 71 + let key: string; 72 + let value: string; 73 + 74 + if (arg.includes("=")) { 75 + [key, value] = arg.slice(2).split("=", 2) as [string, string]; 76 + } else { 77 + key = arg.slice(2); 78 + value = args[++i] ?? ""; 79 + } 80 + 81 + key = toCamelCase(key); 82 + 83 + if (key.includes(".")) { 84 + const [parentKey, childKey] = key.split("."); 85 + config[parentKey] = config[parentKey] || {}; 86 + config[parentKey][childKey] = parseValue(value); 87 + } else { 88 + config[key] = parseValue(value); 89 + } 90 + } 91 + 92 + return config; 93 + } 94 + 95 + const formatFileSize = (bytes: number): string => { 96 + const units = ["B", "KB", "MB", "GB"]; 97 + let size = bytes; 98 + let unitIndex = 0; 99 + 100 + while (size >= 1024 && unitIndex < units.length - 1) { 101 + size /= 1024; 102 + unitIndex++; 103 + } 104 + 105 + return `${size.toFixed(2)} ${units[unitIndex]}`; 106 + }; 107 + 108 + console.log("\n🚀 Starting build process...\n"); 109 + 110 + const cliConfig = parseArgs(); 111 + const outdir = cliConfig.outdir || path.join(process.cwd(), "dist"); 112 + 113 + if (existsSync(outdir)) { 114 + console.log(`🗑️ Cleaning previous build at ${outdir}`); 115 + await rm(outdir, { recursive: true, force: true }); 116 + } 117 + 118 + const start = performance.now(); 119 + 120 + const entrypoints = [...new Bun.Glob("**.html").scanSync("src")] 121 + .map(a => path.resolve("src", a)) 122 + .filter(dir => !dir.includes("node_modules")); 123 + console.log(`📄 Found ${entrypoints.length} HTML ${entrypoints.length === 1 ? "file" : "files"} to process\n`); 124 + 125 + const result = await Bun.build({ 126 + entrypoints, 127 + outdir, 128 + plugins: [plugin], 129 + minify: true, 130 + target: "browser", 131 + sourcemap: "linked", 132 + define: { 133 + "process.env.NODE_ENV": JSON.stringify("production"), 134 + }, 135 + ...cliConfig, 136 + }); 137 + 138 + const end = performance.now(); 139 + 140 + const outputTable = result.outputs.map(output => ({ 141 + File: path.relative(process.cwd(), output.path), 142 + Type: output.kind, 143 + Size: formatFileSize(output.size), 144 + })); 145 + 146 + console.table(outputTable); 147 + const buildTime = (end - start).toFixed(2); 148 + 149 + console.log(`\n✅ Build completed in ${buildTime}ms\n`);
+17
bun-env.d.ts
··· 1 + // Generated by `bun init` 2 + 3 + declare module "*.svg" { 4 + /** 5 + * A path to the SVG file 6 + */ 7 + const path: `${string}.svg`; 8 + export = path; 9 + } 10 + 11 + declare module "*.module.css" { 12 + /** 13 + * A record of class names to their corresponding CSS module classes 14 + */ 15 + const classes: { readonly [key: string]: string }; 16 + export = classes; 17 + }
+191
bun.lock
··· 1 + { 2 + "lockfileVersion": 1, 3 + "workspaces": { 4 + "": { 5 + "name": "bun-react-template", 6 + "dependencies": { 7 + "@radix-ui/react-label": "^2.1.7", 8 + "@radix-ui/react-select": "^2.2.6", 9 + "@radix-ui/react-slot": "^1.2.3", 10 + "atproto-ui": "^0.7.2", 11 + "bun-plugin-tailwind": "^0.1.2", 12 + "class-variance-authority": "^0.7.1", 13 + "clsx": "^2.1.1", 14 + "lucide-react": "^0.545.0", 15 + "react": "^19", 16 + "react-dom": "^19", 17 + "tailwind-merge": "^3.3.1", 18 + }, 19 + "devDependencies": { 20 + "@types/bun": "latest", 21 + "@types/react": "^19", 22 + "@types/react-dom": "^19", 23 + "tailwindcss": "^4.1.11", 24 + "tw-animate-css": "^1.4.0", 25 + }, 26 + }, 27 + }, 28 + "packages": { 29 + "@atcute/atproto": ["@atcute/atproto@3.1.8", "", { "dependencies": { "@atcute/lexicons": "^1.2.2" } }, "sha512-Miu+S7RSgAYbmQWtHJKfSFUN5Kliqoo4YH0rILPmBtfmlZieORJgXNj9oO/Uive0/ulWkiRse07ATIcK8JxMnw=="], 30 + 31 + "@atcute/bluesky": ["@atcute/bluesky@3.2.8", "", { "dependencies": { "@atcute/atproto": "^3.1.8", "@atcute/lexicons": "^1.2.2" } }, "sha512-wxEnSOvX7nLH4sVzX9YFCkaNEWIDrTv3pTs6/x4NgJ3AJ3XJio0OYPM8tR7wAgsklY6BHvlAgt3yoCDK0cl1CA=="], 32 + 33 + "@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=="], 34 + 35 + "@atcute/identity": ["@atcute/identity@1.1.1", "", { "dependencies": { "@atcute/lexicons": "^1.2.2", "@badrap/valita": "^0.4.6" } }, "sha512-zax42n693VEhnC+5tndvO2KLDTMkHOz8UExwmklvJv7R9VujfEwiSWhcv6Jgwb3ellaG8wjiQ1lMOIjLLvwh0Q=="], 36 + 37 + "@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=="], 38 + 39 + "@atcute/lexicons": ["@atcute/lexicons@1.2.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-bgEhJq5Z70/0TbK5sx+tAkrR8FsCODNiL2gUEvS5PuJfPxmFmRYNWaMGehxSPaXWpU2+Oa9ckceHiYbrItDTkA=="], 40 + 41 + "@atcute/tangled": ["@atcute/tangled@1.0.10", "", { "dependencies": { "@atcute/atproto": "^3.1.8", "@atcute/lexicons": "^1.2.2" } }, "sha512-DGconZIN5TpLBah+aHGbWI1tMsL7XzyVEbr/fW4CbcLWYKICU6SAUZ0YnZ+5GvltjlORWHUy7hfftvoh4zodIA=="], 42 + 43 + "@atcute/util-fetch": ["@atcute/util-fetch@1.0.3", "", { "dependencies": { "@badrap/valita": "^0.4.6" } }, "sha512-f8zzTb/xlKIwv2OQ31DhShPUNCmIIleX6p7qIXwWwEUjX6x8skUtpdISSjnImq01LXpltGV5y8yhV4/Mlb7CRQ=="], 44 + 45 + "@badrap/valita": ["@badrap/valita@0.4.6", "", {}, "sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg=="], 46 + 47 + "@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="], 48 + 49 + "@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=="], 50 + 51 + "@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=="], 52 + 53 + "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], 54 + 55 + "@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-7Rap1BHNWqgnexc4wLjjdZeVRQKtk534iGuJ7qZ42i/q1B+cxJZ6zSnrFsYmo+zreH7dUyUXL3AHuXGrl2772Q=="], 56 + 57 + "@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-wpqmgT/8w+tEr5YMGt1u1sEAMRHhyA2SKZddC6GCPasHxSqkCWOPQvYIHIApnTsoSsxhxP0x6Cpe93+4c7hq/w=="], 58 + 59 + "@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-mJo715WvwEHmJ6khNymWyxi0QrFzU94wolsUmxolViNHrk+2ugzIkVIJhTnxf7pHnarxxHwyJ/kgatuV//QILQ=="], 60 + 61 + "@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-ACn038SZL8del+sFnqCjf+haGB02//j2Ez491IMmPTvbv4a/D0iiNz9xiIB3ICbQd3EwQzi+Ut/om3Ba/KoHbQ=="], 62 + 63 + "@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gKU3Wv3BTG5VMjqMMnRwqU6tipCveE9oyYNt62efy6cQK3Vo1DOBwY2SmjbFw+yzj+Um20YoFOLGxghfQET4Ng=="], 64 + 65 + "@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-cAUeM3I5CIYlu5Ur52eCOGg9yfqibQd4lzt9G1/rA0ajqcnCBaTuekhUDZETJJf5H9QV+Gm46CqQg2DpdJzJsw=="], 66 + 67 + "@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=="], 68 + 69 + "@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-8AgEAHyuJ5Jm9MUo1L53K1SRYu0bNGqV0E0L5rB5DjkteO4GXrnWGBT8qsuwuy7WMuCMY3bj64/pFjlRkZuiXw=="], 70 + 71 + "@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-tP0WWcAqrMayvkggOHBGBoyyoK+QHAqgRUyj1F6x5/udiqc9vCXmIt1tlydxYV/NvyvUAmJ7MWT0af44Xm2kJw=="], 72 + 73 + "@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.3.1", "", { "os": "win32", "cpu": "x64" }, "sha512-xdUjOZRq6PwPbbz4/F2QEMLBZwintGp7AS50cWxgkHnyp7Omz5eJfV6/vWtN4qwZIyR3V3DT/2oXsY1+7p3rtg=="], 74 + 75 + "@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.1", "", { "os": "win32", "cpu": "x64" }, "sha512-dcA+Kj7hGFrY3G8NWyYf3Lj3/GMViknpttWUf5pI6p6RphltZaoDu0lY5Lr71PkMdRZTwL2NnZopa/x/NWCdKA=="], 76 + 77 + "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], 78 + 79 + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], 80 + 81 + "@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=="], 82 + 83 + "@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=="], 84 + 85 + "@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=="], 86 + 87 + "@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=="], 88 + 89 + "@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=="], 90 + 91 + "@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=="], 92 + 93 + "@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=="], 94 + 95 + "@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=="], 96 + 97 + "@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=="], 98 + 99 + "@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=="], 100 + 101 + "@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=="], 102 + 103 + "@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=="], 104 + 105 + "@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=="], 106 + 107 + "@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=="], 108 + 109 + "@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=="], 110 + 111 + "@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=="], 112 + 113 + "@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=="], 114 + 115 + "@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=="], 116 + 117 + "@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=="], 118 + 119 + "@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=="], 120 + 121 + "@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=="], 122 + 123 + "@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=="], 124 + 125 + "@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=="], 126 + 127 + "@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=="], 128 + 129 + "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], 130 + 131 + "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], 132 + 133 + "@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="], 134 + 135 + "@types/node": ["@types/node@24.9.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg=="], 136 + 137 + "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], 138 + 139 + "@types/react-dom": ["@types/react-dom@19.2.2", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw=="], 140 + 141 + "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], 142 + 143 + "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=="], 144 + 145 + "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=="], 146 + 147 + "bun-plugin-tailwind": ["bun-plugin-tailwind@0.1.2", "", { "peerDependencies": { "bun": ">=1.0.0" } }, "sha512-41jNC1tZRSK3s1o7pTNrLuQG8kL/0vR/JgiTmZAJ1eHwe0w5j6HFPKeqEk0WAD13jfrUC7+ULuewFBBCoADPpg=="], 148 + 149 + "bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="], 150 + 151 + "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], 152 + 153 + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], 154 + 155 + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], 156 + 157 + "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], 158 + 159 + "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], 160 + 161 + "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], 162 + 163 + "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=="], 164 + 165 + "react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="], 166 + 167 + "react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="], 168 + 169 + "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=="], 170 + 171 + "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=="], 172 + 173 + "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=="], 174 + 175 + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], 176 + 177 + "tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="], 178 + 179 + "tailwindcss": ["tailwindcss@4.1.16", "", {}, "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA=="], 180 + 181 + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 182 + 183 + "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], 184 + 185 + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], 186 + 187 + "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=="], 188 + 189 + "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=="], 190 + } 191 + }
+3
bunfig.toml
··· 1 + [serve.static] 2 + plugins = ["bun-plugin-tailwind"] 3 + env = "BUN_PUBLIC_*"
+21
components.json
··· 1 + { 2 + "$schema": "https://ui.shadcn.com/schema.json", 3 + "style": "new-york", 4 + "rsc": false, 5 + "tsx": true, 6 + "tailwind": { 7 + "config": "", 8 + "css": "styles/globals.css", 9 + "baseColor": "neutral", 10 + "cssVariables": true, 11 + "prefix": "" 12 + }, 13 + "aliases": { 14 + "components": "@/components", 15 + "utils": "@/lib/utils", 16 + "ui": "@/components/ui", 17 + "lib": "@/lib", 18 + "hooks": "@/hooks" 19 + }, 20 + "iconLibrary": "lucide" 21 + }
+31
package.json
··· 1 + { 2 + "name": "bun-react-template", 3 + "version": "0.1.0", 4 + "private": true, 5 + "type": "module", 6 + "scripts": { 7 + "dev": "bun --hot src/index.ts", 8 + "start": "NODE_ENV=production bun src/index.ts", 9 + "build": "bun run build.ts" 10 + }, 11 + "dependencies": { 12 + "@radix-ui/react-label": "^2.1.7", 13 + "@radix-ui/react-select": "^2.2.6", 14 + "@radix-ui/react-slot": "^1.2.3", 15 + "atproto-ui": "^0.7.2", 16 + "bun-plugin-tailwind": "^0.1.2", 17 + "class-variance-authority": "^0.7.1", 18 + "clsx": "^2.1.1", 19 + "lucide-react": "^0.545.0", 20 + "react": "^19", 21 + "react-dom": "^19", 22 + "tailwind-merge": "^3.3.1" 23 + }, 24 + "devDependencies": { 25 + "@types/react": "^19", 26 + "@types/react-dom": "^19", 27 + "@types/bun": "latest", 28 + "tailwindcss": "^4.1.11", 29 + "tw-animate-css": "^1.4.0" 30 + } 31 + }
+50
src/App.tsx
··· 1 + import "./index.css" 2 + import { useEffect, useRef, useState } from "react" 3 + import { SectionNav } from "./components/SectionNav" 4 + import { Header } from "./components/sections/Header" 5 + import { Work } from "./components/sections/Work" 6 + import { Connect } from "./components/sections/Connect" 7 + import { sections } from "./data/portfolio" 8 + 9 + export function App() { 10 + const [activeSection, setActiveSection] = useState("") 11 + const sectionsRef = useRef<(HTMLElement | null)[]>([]) 12 + 13 + useEffect(() => { 14 + const observer = new IntersectionObserver( 15 + (entries) => { 16 + entries.forEach((entry) => { 17 + if (entry.isIntersecting) { 18 + entry.target.classList.add("animate-fade-in-up") 19 + setActiveSection(entry.target.id) 20 + } 21 + }) 22 + }, 23 + { threshold: 0.1, rootMargin: "0px 0px -5% 0px" }, 24 + ) 25 + 26 + sectionsRef.current.forEach((section) => { 27 + if (section) observer.observe(section) 28 + }) 29 + 30 + return () => observer.disconnect() 31 + }, []) 32 + 33 + 34 + 35 + return ( 36 + <div className="min-h-screen dark:bg-background text-foreground relative"> 37 + <SectionNav sections={sections} activeSection={activeSection} /> 38 + 39 + <main> 40 + <div className="max-w-4xl mx-auto px-6 sm:px-8 lg:px-16"> 41 + <Header sectionRef={(el) => (sectionsRef.current[0] = el)} /> 42 + </div> 43 + <Work sectionRef={(el) => (sectionsRef.current[1] = el)} /> 44 + <Connect sectionRef={(el) => (sectionsRef.current[2] = el)} /> 45 + </main> 46 + </div> 47 + ) 48 + } 49 + 50 + export default App
+37
src/components/BlogCard.tsx
··· 1 + interface BlogCardProps { 2 + title: string 3 + excerpt: string 4 + date: string 5 + readTime: string 6 + } 7 + 8 + export function BlogCard({ title, excerpt, date, readTime }: BlogCardProps) { 9 + return ( 10 + <article className="group glass glass-hover p-6 sm:p-8 rounded-lg transition-all duration-500 cursor-pointer"> 11 + <div className="space-y-4"> 12 + <div className="flex items-center justify-between text-xs text-muted-foreground font-mono"> 13 + <span>{date}</span> 14 + <span>{readTime}</span> 15 + </div> 16 + 17 + <h3 className="text-lg sm:text-xl font-medium group-hover:text-muted-foreground transition-colors duration-300"> 18 + {title} 19 + </h3> 20 + 21 + <p className="text-muted-foreground leading-relaxed">{excerpt}</p> 22 + 23 + <div className="flex items-center gap-2 text-sm text-muted-foreground group-hover:text-foreground transition-colors duration-300"> 24 + <span>Read more</span> 25 + <svg 26 + className="w-4 h-4 transform group-hover:translate-x-1 transition-transform duration-300" 27 + fill="none" 28 + stroke="currentColor" 29 + viewBox="0 0 24 24" 30 + > 31 + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" /> 32 + </svg> 33 + </div> 34 + </div> 35 + </article> 36 + ) 37 + }
+81
src/components/ProjectCard.tsx
··· 1 + interface ProjectCardProps { 2 + title: string 3 + description: string 4 + year: string 5 + tech: string[] 6 + links?: { 7 + live?: string 8 + github?: string 9 + } 10 + } 11 + 12 + export function ProjectCard({ title, description, year, tech, links }: ProjectCardProps) { 13 + return ( 14 + <div className="group p-6 sm:p-8 rounded-lg transition-all duration-500"> 15 + <div className="space-y-4"> 16 + <div className="flex items-start justify-between gap-4"> 17 + <div className="space-y-2 flex-1"> 18 + <h3 className="text-lg sm:text-xl font-medium group-hover:text-muted-foreground transition-colors duration-300"> 19 + {title} 20 + </h3> 21 + <div className="text-xs text-muted-foreground font-mono">{year}</div> 22 + </div> 23 + 24 + {links && ( 25 + <div className="flex gap-2"> 26 + {links.github && ( 27 + <a 28 + href={links.github} 29 + target="_blank" 30 + rel="noopener noreferrer" 31 + className="p-2 rounded-lg transition-all duration-300 hover:bg-muted" 32 + aria-label="View on GitHub" 33 + > 34 + <svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"> 35 + <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" /> 36 + </svg> 37 + </a> 38 + )} 39 + {links.live && ( 40 + <a 41 + href={links.live} 42 + target="_blank" 43 + rel="noopener noreferrer" 44 + className="p-2 rounded-lg transition-all duration-300 hover:bg-muted" 45 + aria-label="View live site" 46 + > 47 + <svg 48 + className="w-4 h-4" 49 + fill="none" 50 + stroke="currentColor" 51 + viewBox="0 0 24 24" 52 + strokeWidth={2} 53 + > 54 + <path 55 + strokeLinecap="round" 56 + strokeLinejoin="round" 57 + d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" 58 + /> 59 + </svg> 60 + </a> 61 + )} 62 + </div> 63 + )} 64 + </div> 65 + 66 + <p className="text-muted-foreground leading-relaxed">{description}</p> 67 + 68 + <div className="flex flex-wrap gap-2"> 69 + {tech.map((techItem) => ( 70 + <span 71 + key={techItem} 72 + className="bg-muted px-3 py-1 text-xs rounded-full transition-colors duration-300" 73 + > 74 + {techItem} 75 + </span> 76 + ))} 77 + </div> 78 + </div> 79 + </div> 80 + ) 81 + }
+30
src/components/SectionNav.tsx
··· 1 + import type { Section } from "../data/portfolio" 2 + import { useSystemTheme } from "../hooks/useSystemTheme" 3 + 4 + interface SectionNavProps { 5 + sections: readonly Section[] 6 + activeSection: string 7 + } 8 + 9 + export function SectionNav({ sections, activeSection }: SectionNavProps) { 10 + const scrollToSection = (section: string) => { 11 + document.getElementById(section)?.scrollIntoView({ behavior: "smooth" }) 12 + } 13 + 14 + return ( 15 + <nav className="fixed left-8 top-1/2 -translate-y-1/2 z-10 hidden lg:block"> 16 + <div className="flex flex-col gap-4"> 17 + {sections.map((section) => ( 18 + <button 19 + key={section} 20 + onClick={() => scrollToSection(section)} 21 + className={`w-2 h-8 rounded-full transition-all duration-500 ${ 22 + activeSection === section ? "bg-foreground" : "glass glass-hover" 23 + }`} 24 + aria-label={`Navigate to ${section}`} 25 + /> 26 + ))} 27 + </div> 28 + </nav> 29 + ) 30 + }
+25
src/components/SocialLink.tsx
··· 1 + interface SocialLinkProps { 2 + name: string 3 + handle: string 4 + url: string 5 + } 6 + 7 + export function SocialLink({ name, handle, url }: SocialLinkProps) { 8 + return ( 9 + <a 10 + href={url} 11 + 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" 12 + target="_blank" 13 + rel="noopener noreferrer" 14 + > 15 + <div className="space-y-2"> 16 + <div className="text-[oklch(0.2_0.02_255)] dark:text-foreground dark:group-hover:text-muted-foreground transition-colors duration-300"> 17 + {name} 18 + </div> 19 + <div className="text-sm text-[oklch(0.48_0.015_255)] dark:text-muted-foreground"> 20 + {handle} 21 + </div> 22 + </div> 23 + </a> 24 + ) 25 + }
+36
src/components/ThemeToggle.tsx
··· 1 + interface ThemeToggleProps { 2 + isDark: boolean 3 + onToggle: () => void 4 + } 5 + 6 + export function ThemeToggle({ isDark, onToggle }: ThemeToggleProps) { 7 + return ( 8 + <button 9 + onClick={onToggle} 10 + className="group glass glass-hover p-3 rounded-lg transition-all duration-300" 11 + aria-label="Toggle theme" 12 + > 13 + {isDark ? ( 14 + <svg 15 + className="w-4 h-4 text-muted-foreground group-hover:text-foreground transition-colors duration-300" 16 + fill="currentColor" 17 + viewBox="0 0 20 20" 18 + > 19 + <path 20 + fillRule="evenodd" 21 + 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" 22 + clipRule="evenodd" 23 + /> 24 + </svg> 25 + ) : ( 26 + <svg 27 + className="w-4 h-4 text-muted-foreground group-hover:text-foreground transition-colors duration-300" 28 + fill="currentColor" 29 + viewBox="0 0 20 20" 30 + > 31 + <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" /> 32 + </svg> 33 + )} 34 + </button> 35 + ) 36 + }
+121
src/components/WorkExperienceCard.tsx
··· 1 + interface Project { 2 + title: string 3 + description: string 4 + tech: string[] 5 + links?: { 6 + live?: string 7 + github?: string 8 + } 9 + } 10 + 11 + interface WorkExperienceCardProps { 12 + year: string 13 + role: string 14 + company: string 15 + description: string 16 + tech: string[] 17 + projects?: Project[] 18 + } 19 + 20 + export function WorkExperienceCard({ year, role, company, description, tech, projects }: WorkExperienceCardProps) { 21 + return ( 22 + <div className="group py-6 sm:py-8 border-b border-border/50 hover:border-border transition-colors duration-500"> 23 + <div className="grid lg:grid-cols-12 gap-4 sm:gap-8"> 24 + <div className="lg:col-span-2"> 25 + <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"> 26 + {year} 27 + </div> 28 + </div> 29 + 30 + <div className="lg:col-span-10 space-y-3"> 31 + <div className="flex items-start justify-between gap-4"> 32 + <div> 33 + <h3 className="text-lg sm:text-xl font-medium text-[oklch(0.2_0.02_255)] dark:text-foreground">{role}</h3> 34 + <div className="text-[oklch(0.48_0.015_255)] dark:text-muted-foreground"> 35 + {company} 36 + </div> 37 + </div> 38 + <div className="flex flex-wrap gap-2 justify-end items-start"> 39 + {tech.map((techItem) => ( 40 + <span 41 + key={techItem} 42 + className="px-2 py-1 text-xs rounded text-[oklch(0.48_0.015_255)] dark:text-muted-foreground transition-colors duration-500" 43 + > 44 + {techItem} 45 + </span> 46 + ))} 47 + </div> 48 + </div> 49 + <p className="leading-relaxed text-[oklch(0.48_0.015_255)] dark:text-muted-foreground"> 50 + {description} 51 + </p> 52 + </div> 53 + </div> 54 + 55 + {projects && projects.length > 0 && ( 56 + <div className="mt-6 lg:ml-[calc((2/12)*100%+2rem)] space-y-4"> 57 + <div className="text-xs font-mono tracking-wider text-[oklch(0.48_0.015_255)] dark:text-muted-foreground"> 58 + PROJECTS 59 + </div> 60 + <div className="space-y-4"> 61 + {projects.map((project, index) => ( 62 + <div 63 + key={index} 64 + className="glass glass-hover p-4 rounded-lg transition-colors duration-300" 65 + > 66 + <div className="space-y-3"> 67 + <div className="flex items-start justify-between gap-4"> 68 + <h4 className="font-medium text-md text-[oklch(0.2_0.02_255)] dark:text-foreground">{project.title}</h4> 69 + {project.links && ( 70 + <div className="flex gap-2"> 71 + {project.links.github && ( 72 + <a 73 + href={project.links.github} 74 + target="_blank" 75 + rel="noopener noreferrer" 76 + className="transition-colors text-[oklch(0.48_0.015_255)] dark:text-muted-foreground dark:hover:text-foreground" 77 + aria-label="View on GitHub" 78 + > 79 + <svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"> 80 + <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" /> 81 + </svg> 82 + </a> 83 + )} 84 + {project.links.live && ( 85 + <a 86 + href={project.links.live} 87 + target="_blank" 88 + rel="noopener noreferrer" 89 + className="transition-colors text-[oklch(0.48_0.015_255)] dark:text-muted-foreground dark:hover:text-foreground" 90 + aria-label="View live site" 91 + > 92 + <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}> 93 + <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" /> 94 + </svg> 95 + </a> 96 + )} 97 + </div> 98 + )} 99 + </div> 100 + <p className="text-md leading-relaxed text-[oklch(0.48_0.015_255)] dark:text-muted-foreground"> 101 + {project.description} 102 + </p> 103 + <div className="flex flex-wrap gap-2"> 104 + {project.tech.map((techItem) => ( 105 + <span 106 + key={techItem} 107 + className="glass glass-hover px-2 py-1 text-xs rounded text-[oklch(0.48_0.015_255)] dark:text-muted-foreground" 108 + > 109 + {techItem} 110 + </span> 111 + ))} 112 + </div> 113 + </div> 114 + </div> 115 + ))} 116 + </div> 117 + </div> 118 + )} 119 + </div> 120 + ) 121 + }
+131
src/components/sections/Connect.tsx
··· 1 + import { SocialLink } from "../SocialLink"; 2 + import { personalInfo, socialLinks } from "../../data/portfolio"; 3 + import { useSystemTheme } from "../../hooks/useSystemTheme"; 4 + import { BlueskyProfile } from "atproto-ui"; 5 + import { type AtProtoStyles } from "atproto-ui"; 6 + 7 + interface ConnectProps { 8 + sectionRef: (el: HTMLElement | null) => void; 9 + } 10 + 11 + export function Connect({ sectionRef }: ConnectProps) { 12 + const isLightMode = useSystemTheme(); 13 + 14 + return ( 15 + <section 16 + id="connect" 17 + ref={sectionRef} 18 + className="py-20 sm:py-32 opacity-0" 19 + style={ 20 + isLightMode 21 + ? { 22 + backgroundColor: "#e2e2e2", 23 + color: "oklch(0.2 0.02 255)", 24 + } 25 + : {} 26 + } 27 + > 28 + <div className="max-w-4xl mx-auto px-6 sm:px-8 lg:px-16"> 29 + <div className="grid lg:grid-cols-2 gap-12 sm:gap-16"> 30 + <div className="space-y-6 sm:space-y-8"> 31 + <h2 className="text-3xl sm:text-4xl font-light"> 32 + Let's Connect 33 + </h2> 34 + 35 + <div className="space-y-6"> 36 + <p 37 + className="text-lg sm:text-xl leading-relaxed" 38 + style={ 39 + isLightMode 40 + ? { 41 + color: "oklch(0.48 0.015 255)", 42 + } 43 + : {} 44 + } 45 + > 46 + Always interested in new opportunities, 47 + collaborations, and conversations about technology 48 + and design. 49 + </p> 50 + 51 + <div className="space-y-4"> 52 + <a 53 + href={`mailto:${personalInfo.contact.email}`} 54 + className="group flex items-center gap-3 transition-colors duration-300" 55 + style={ 56 + isLightMode 57 + ? { 58 + color: "oklch(0.2 0.02 255)", 59 + } 60 + : {} 61 + } 62 + onMouseEnter={(e) => { 63 + if (isLightMode) { 64 + e.currentTarget.style.color = 65 + "oklch(0.48 0.015 255)"; 66 + } 67 + }} 68 + onMouseLeave={(e) => { 69 + if (isLightMode) { 70 + e.currentTarget.style.color = 71 + "oklch(0.2 0.02 255)"; 72 + } 73 + }} 74 + > 75 + <span className="text-base sm:text-lg"> 76 + {personalInfo.contact.email} 77 + </span> 78 + <svg 79 + className="w-5 h-5 transform group-hover:translate-x-1 transition-transform duration-300" 80 + fill="none" 81 + stroke="currentColor" 82 + viewBox="0 0 24 24" 83 + > 84 + <path 85 + strokeLinecap="round" 86 + strokeLinejoin="round" 87 + strokeWidth={2} 88 + d="M17 8l4 4m0 0l-4 4m4-4H3" 89 + /> 90 + </svg> 91 + </a> 92 + </div> 93 + </div> 94 + </div> 95 + 96 + <div className="space-y-6 sm:space-y-8"> 97 + <div 98 + className="text-sm font-mono" 99 + style={ 100 + isLightMode 101 + ? { 102 + color: "oklch(0.48 0.015 255)", 103 + } 104 + : {} 105 + } 106 + > 107 + ELSEWHERE 108 + </div> 109 + 110 + <div className="grid grid-cols-1 lg:grid-cols-2 gap-4"> 111 + {socialLinks.map((social) => ( 112 + <SocialLink key={social.name} {...social} /> 113 + ))} 114 + </div> 115 + </div> 116 + </div> 117 + <div style={{ 118 + '--atproto-color-bg': isLightMode ? '#f2f2f2' : '#1f1f1f', 119 + } as AtProtoStyles } 120 + className="pt-8 grid lg:grid-cols-2 gap-12 sm:gap-4" 121 + > 122 + <BlueskyProfile did="nekomimi.pet" /> 123 + <BlueskyProfile did="art.nekomimi.pet" /> 124 + <p className="text-sm sm:text-base"> 125 + ^ This is <a className="text-cyan-600" href="https://tangled.org/@nekomimi.pet/atproto-ui" target="_blank" rel="noopener noreferrer">atproto-ui</a> btw. :) 126 + </p> 127 + </div> 128 + </div> 129 + </section> 130 + ); 131 + }
+64
src/components/sections/Footer.tsx
··· 1 + import { useSystemTheme } from "../../hooks/useSystemTheme" 2 + 3 + export function Footer() { 4 + const isLightMode = useSystemTheme() 5 + 6 + return ( 7 + <footer 8 + className="py-12 sm:py-16 border-t" 9 + style={isLightMode ? { 10 + backgroundColor: '#e2e2e2', 11 + color: 'oklch(0.2 0.02 255)', 12 + borderColor: 'oklch(0.88 0.01 255)' 13 + } : {}} 14 + > 15 + <div className="flex flex-col lg:flex-row justify-between items-start lg:items-center gap-6 sm:gap-8"> 16 + <div className="flex items-center gap-4"> 17 + <button 18 + className="group p-3 rounded-lg border transition-all duration-300" 19 + style={isLightMode ? { 20 + borderColor: 'oklch(0.88 0.01 255)' 21 + } : {}} 22 + onMouseEnter={(e) => { 23 + if (isLightMode) { 24 + e.currentTarget.style.borderColor = 'oklch(0.48 0.015 255)' 25 + } 26 + }} 27 + onMouseLeave={(e) => { 28 + if (isLightMode) { 29 + e.currentTarget.style.borderColor = 'oklch(0.88 0.01 255)' 30 + } 31 + }} 32 + > 33 + <svg 34 + className="w-4 h-4 transition-colors duration-300" 35 + style={isLightMode ? { 36 + color: 'oklch(0.48 0.015 255)' 37 + } : {}} 38 + fill="none" 39 + stroke="currentColor" 40 + viewBox="0 0 24 24" 41 + onMouseEnter={(e) => { 42 + if (isLightMode) { 43 + e.currentTarget.style.color = 'oklch(0.2 0.02 255)' 44 + } 45 + }} 46 + onMouseLeave={(e) => { 47 + if (isLightMode) { 48 + e.currentTarget.style.color = 'oklch(0.48 0.015 255)' 49 + } 50 + }} 51 + > 52 + <path 53 + strokeLinecap="round" 54 + strokeLinejoin="round" 55 + strokeWidth={2} 56 + 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" 57 + /> 58 + </svg> 59 + </button> 60 + </div> 61 + </div> 62 + </footer> 63 + ) 64 + }
+193
src/components/sections/Header.tsx
··· 1 + import type { RefObject } from "react" 2 + import { personalInfo, currentRole, skills } from "../../data/portfolio" 3 + 4 + interface HeaderProps { 5 + sectionRef: (el: HTMLElement | null) => void 6 + } 7 + 8 + export function Header({ sectionRef }: HeaderProps) { 9 + const scrollToWork = () => { 10 + document.getElementById('work')?.scrollIntoView({ behavior: 'smooth' }) 11 + } 12 + 13 + return ( 14 + <header id="intro" ref={sectionRef} className="min-h-screen flex items-center opacity-0 relative"> 15 + {/* Background Image - Full Width */} 16 + <div 17 + className="absolute top-0 bottom-0 left-1/2 right-1/2 -ml-[50vw] -mr-[50vw] w-screen z-0" 18 + style={{ 19 + backgroundImage: 'url(https://cdn.donmai.us/original/31/2f/__kazusa_blue_archive_drawn_by_astelia__312fc11a21c5d4ce06dc3aa8bfbb7221.jpg)', 20 + backgroundSize: 'cover', 21 + backgroundPosition: 'center', 22 + backgroundRepeat: 'no-repeat', 23 + }} 24 + > 25 + {/* Overlay for better text readability */} 26 + <div className="absolute inset-0 bg-background/70"></div> 27 + </div> 28 + 29 + <div className="grid lg:grid-cols-5 gap-12 sm:gap-16 w-full relative z-10"> 30 + <div className="lg:col-span-3 space-y-6 sm:space-y-8"> 31 + <div className="space-y-3 sm:space-y-2"> 32 + <div className="text-sm text-gray-300 font-mono tracking-wider">PORTFOLIO / 2025</div> 33 + <h1 className="text-md sm:text-md lg:text-4xl font-light tracking-tight text-cyan-400"> 34 + {personalInfo.name.first} 35 + <br /> 36 + <span className=" text-gray-400">{personalInfo.name.last}</span> 37 + </h1> 38 + </div> 39 + 40 + <div className="space-y-6 max-w-md"> 41 + <p className="text-lg sm:text-xl text-stone-200 leading-relaxed"> 42 + {personalInfo.description.map((part, i) => { 43 + if (part.url) { 44 + return ( 45 + <a 46 + key={i} 47 + href={part.url} 48 + target="_blank" 49 + rel="noopener noreferrer" 50 + 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" 51 + > 52 + {part.text} 53 + </a> 54 + ) 55 + } 56 + 57 + if (part.bold) { 58 + return ( 59 + <span key={i} className="text-white font-medium"> 60 + {part.text} 61 + </span> 62 + ) 63 + } 64 + 65 + return part.text 66 + })} 67 + </p> 68 + 69 + <div className="space-y-4"> 70 + <div className="flex items-center gap-2 text-sm text-gray-300"> 71 + <div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div> 72 + {personalInfo.availability.status} 73 + </div> 74 + <div className="flex items-center gap-4"> 75 + <a 76 + href={`mailto:${personalInfo.contact.email}`} 77 + 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" 78 + > 79 + <svg 80 + className="w-4 h-4" 81 + fill="none" 82 + stroke="currentColor" 83 + viewBox="0 0 24 24" 84 + strokeWidth={2} 85 + > 86 + <path 87 + strokeLinecap="round" 88 + strokeLinejoin="round" 89 + 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" 90 + /> 91 + </svg> 92 + Email me 93 + </a> 94 + <a 95 + href="https://nekomimi.leaflet.pub" 96 + target="_blank" 97 + rel="noopener noreferrer" 98 + 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" 99 + > 100 + <svg 101 + className="w-4 h-4" 102 + fill="none" 103 + stroke="currentColor" 104 + viewBox="0 0 24 24" 105 + strokeWidth={2} 106 + > 107 + <path 108 + strokeLinecap="round" 109 + strokeLinejoin="round" 110 + 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" 111 + /> 112 + </svg> 113 + Read my blog 114 + </a> 115 + </div> 116 + </div> 117 + </div> 118 + </div> 119 + 120 + <div className="lg:col-span-2 flex flex-col justify-end space-y-6 sm:space-y-8 mt-8 lg:mt-0"> 121 + <div className="space-y-4"> 122 + <div className="text-sm text-gray-300 font-mono">CURRENTLY</div> 123 + <div className="space-y-2"> 124 + <div className="text-white">{currentRole.title}</div> 125 + <div className="text-sm text-gray-300">{personalInfo.availability.location}</div> 126 + <div className="text-gray-300">@ {currentRole.company}</div> 127 + <div className="text-xs text-gray-100">{currentRole.period}</div> 128 + </div> 129 + </div> 130 + 131 + <div className="space-y-4"> 132 + <div className="text-sm text-gray-300 font-mono">FOCUS</div> 133 + <div className="flex flex-wrap gap-2"> 134 + {skills.map((skill) => ( 135 + <span 136 + key={skill} 137 + className="glass glass-hover px-3 py-1 text-xs rounded-full transition-colors duration-300" 138 + > 139 + {skill} 140 + </span> 141 + ))} 142 + </div> 143 + </div> 144 + </div> 145 + </div> 146 + 147 + {/* Image Source Link */} 148 + <a 149 + href="https://danbooru.donmai.us/posts/9959832" 150 + target="_blank" 151 + rel="noopener noreferrer" 152 + 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" 153 + aria-label="View image source" 154 + > 155 + <svg 156 + className="w-4 h-4 inline-block mr-1" 157 + fill="none" 158 + stroke="currentColor" 159 + viewBox="0 0 24 24" 160 + strokeWidth={2} 161 + > 162 + <path 163 + strokeLinecap="round" 164 + strokeLinejoin="round" 165 + 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" 166 + /> 167 + </svg> 168 + Source 169 + </a> 170 + 171 + {/* Scroll Down Arrow */} 172 + <button 173 + onClick={scrollToWork} 174 + 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" 175 + aria-label="Scroll to work section" 176 + > 177 + <svg 178 + className="w-5 h-5 text-gray-300" 179 + fill="none" 180 + stroke="currentColor" 181 + viewBox="0 0 24 24" 182 + strokeWidth={2} 183 + > 184 + <path 185 + strokeLinecap="round" 186 + strokeLinejoin="round" 187 + d="M19 14l-7 7m0 0l-7-7m7 7V3" 188 + /> 189 + </svg> 190 + </button> 191 + </header> 192 + ) 193 + }
+45
src/components/sections/Work.tsx
··· 1 + import { WorkExperienceCard } from "../WorkExperienceCard" 2 + import { workExperience } from "../../data/portfolio" 3 + import { useSystemTheme } from "../../hooks/useSystemTheme" 4 + 5 + interface WorkProps { 6 + sectionRef: (el: HTMLElement | null) => void 7 + } 8 + 9 + export function Work({ sectionRef }: WorkProps) { 10 + const isLightMode = useSystemTheme() 11 + 12 + return ( 13 + <section 14 + id="work" 15 + ref={sectionRef} 16 + className="min-h-screen py-20 sm:py-32 opacity-0" 17 + style={isLightMode ? { 18 + backgroundColor: '#e2e2e2', 19 + color: 'oklch(0.2 0.02 255)' 20 + } : {}} 21 + > 22 + <div className="max-w-4xl mx-auto px-6 sm:px-8 lg:px-16"> 23 + <div className="space-y-12 sm:space-y-16"> 24 + <div className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-4"> 25 + <h2 className="text-3xl sm:text-4xl font-light">Selected Work</h2> 26 + <div 27 + className="text-sm font-mono" 28 + style={isLightMode ? { 29 + color: 'oklch(0.48 0.015 255)' 30 + } : {}} 31 + > 32 + 2016 — 2025 33 + </div> 34 + </div> 35 + 36 + <div className="space-y-8 sm:space-y-12"> 37 + {workExperience.map((job, index) => ( 38 + <WorkExperienceCard key={index} {...job} /> 39 + ))} 40 + </div> 41 + </div> 42 + </div> 43 + </section> 44 + ) 45 + }
+52
src/components/ui/button.tsx
··· 1 + import { Slot } from "@radix-ui/react-slot"; 2 + import { cva, type VariantProps } from "class-variance-authority"; 3 + import * as React from "react"; 4 + 5 + import { cn } from "@/lib/utils"; 6 + 7 + const buttonVariants = cva( 8 + "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", 9 + { 10 + variants: { 11 + variant: { 12 + default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 + destructive: 14 + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 15 + outline: 16 + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", 17 + secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", 18 + ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", 19 + link: "text-primary underline-offset-4 hover:underline", 20 + }, 21 + size: { 22 + default: "h-9 px-4 py-2 has-[>svg]:px-3", 23 + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", 24 + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 25 + icon: "size-9", 26 + "icon-sm": "size-8", 27 + "icon-lg": "size-10", 28 + }, 29 + }, 30 + defaultVariants: { 31 + variant: "default", 32 + size: "default", 33 + }, 34 + }, 35 + ); 36 + 37 + function Button({ 38 + className, 39 + variant, 40 + size, 41 + asChild = false, 42 + ...props 43 + }: React.ComponentProps<"button"> & 44 + VariantProps<typeof buttonVariants> & { 45 + asChild?: boolean; 46 + }) { 47 + const Comp = asChild ? Slot : "button"; 48 + 49 + return <Comp data-slot="button" className={cn(buttonVariants({ variant, size, className }))} {...props} />; 50 + } 51 + 52 + export { Button, buttonVariants };
+56
src/components/ui/card.tsx
··· 1 + import * as React from "react"; 2 + 3 + import { cn } from "@/lib/utils"; 4 + 5 + function Card({ className, ...props }: React.ComponentProps<"div">) { 6 + return ( 7 + <div 8 + data-slot="card" 9 + className={cn("glass glass-hover text-card-foreground flex flex-col gap-6 rounded-xl py-6", className)} 10 + {...props} 11 + /> 12 + ); 13 + } 14 + 15 + function CardHeader({ className, ...props }: React.ComponentProps<"div">) { 16 + return ( 17 + <div 18 + data-slot="card-header" 19 + className={cn( 20 + "@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", 21 + className, 22 + )} 23 + {...props} 24 + /> 25 + ); 26 + } 27 + 28 + function CardTitle({ className, ...props }: React.ComponentProps<"div">) { 29 + return <div data-slot="card-title" className={cn("leading-none font-semibold", className)} {...props} />; 30 + } 31 + 32 + function CardDescription({ className, ...props }: React.ComponentProps<"div">) { 33 + return <div data-slot="card-description" className={cn("text-muted-foreground text-sm", className)} {...props} />; 34 + } 35 + 36 + function CardAction({ className, ...props }: React.ComponentProps<"div">) { 37 + return ( 38 + <div 39 + data-slot="card-action" 40 + className={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)} 41 + {...props} 42 + /> 43 + ); 44 + } 45 + 46 + function CardContent({ className, ...props }: React.ComponentProps<"div">) { 47 + return <div data-slot="card-content" className={cn("px-6", className)} {...props} />; 48 + } 49 + 50 + function CardFooter({ className, ...props }: React.ComponentProps<"div">) { 51 + return ( 52 + <div data-slot="card-footer" className={cn("flex items-center px-6 [.border-t]:pt-6", className)} {...props} /> 53 + ); 54 + } 55 + 56 + export { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle };
+21
src/components/ui/input.tsx
··· 1 + import * as React from "react"; 2 + 3 + import { cn } from "@/lib/utils"; 4 + 5 + function Input({ className, type, ...props }: React.ComponentProps<"input">) { 6 + return ( 7 + <input 8 + type={type} 9 + data-slot="input" 10 + className={cn( 11 + "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", 12 + "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", 13 + "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 14 + className, 15 + )} 16 + {...props} 17 + /> 18 + ); 19 + } 20 + 21 + export { Input };
+21
src/components/ui/label.tsx
··· 1 + "use client"; 2 + 3 + import * as LabelPrimitive from "@radix-ui/react-label"; 4 + import * as React from "react"; 5 + 6 + import { cn } from "@/lib/utils"; 7 + 8 + function Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) { 9 + return ( 10 + <LabelPrimitive.Root 11 + data-slot="label" 12 + className={cn( 13 + "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", 14 + className, 15 + )} 16 + {...props} 17 + /> 18 + ); 19 + } 20 + 21 + export { Label };
+162
src/components/ui/select.tsx
··· 1 + "use client"; 2 + 3 + import * as SelectPrimitive from "@radix-ui/react-select"; 4 + import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"; 5 + import * as React from "react"; 6 + 7 + import { cn } from "@/lib/utils"; 8 + 9 + function Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>) { 10 + return <SelectPrimitive.Root data-slot="select" {...props} />; 11 + } 12 + 13 + function SelectGroup({ ...props }: React.ComponentProps<typeof SelectPrimitive.Group>) { 14 + return <SelectPrimitive.Group data-slot="select-group" {...props} />; 15 + } 16 + 17 + function SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.Value>) { 18 + return <SelectPrimitive.Value data-slot="select-value" {...props} />; 19 + } 20 + 21 + function SelectTrigger({ 22 + className, 23 + size = "default", 24 + children, 25 + ...props 26 + }: React.ComponentProps<typeof SelectPrimitive.Trigger> & { 27 + size?: "sm" | "default"; 28 + }) { 29 + return ( 30 + <SelectPrimitive.Trigger 31 + data-slot="select-trigger" 32 + data-size={size} 33 + className={cn( 34 + "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", 35 + className, 36 + )} 37 + {...props} 38 + > 39 + {children} 40 + <SelectPrimitive.Icon asChild> 41 + <ChevronDownIcon className="size-4 opacity-50" /> 42 + </SelectPrimitive.Icon> 43 + </SelectPrimitive.Trigger> 44 + ); 45 + } 46 + 47 + function SelectContent({ 48 + className, 49 + children, 50 + position = "popper", 51 + align = "center", 52 + ...props 53 + }: React.ComponentProps<typeof SelectPrimitive.Content>) { 54 + return ( 55 + <SelectPrimitive.Portal> 56 + <SelectPrimitive.Content 57 + data-slot="select-content" 58 + className={cn( 59 + "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", 60 + position === "popper" && 61 + "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", 62 + className, 63 + )} 64 + position={position} 65 + align={align} 66 + {...props} 67 + > 68 + <SelectScrollUpButton /> 69 + <SelectPrimitive.Viewport 70 + className={cn( 71 + "p-1", 72 + position === "popper" && 73 + "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1", 74 + )} 75 + > 76 + {children} 77 + </SelectPrimitive.Viewport> 78 + <SelectScrollDownButton /> 79 + </SelectPrimitive.Content> 80 + </SelectPrimitive.Portal> 81 + ); 82 + } 83 + 84 + function SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>) { 85 + return ( 86 + <SelectPrimitive.Label 87 + data-slot="select-label" 88 + className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)} 89 + {...props} 90 + /> 91 + ); 92 + } 93 + 94 + function SelectItem({ className, children, ...props }: React.ComponentProps<typeof SelectPrimitive.Item>) { 95 + return ( 96 + <SelectPrimitive.Item 97 + data-slot="select-item" 98 + className={cn( 99 + "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", 100 + className, 101 + )} 102 + {...props} 103 + > 104 + <span className="absolute right-2 flex size-3.5 items-center justify-center"> 105 + <SelectPrimitive.ItemIndicator> 106 + <CheckIcon className="size-4" /> 107 + </SelectPrimitive.ItemIndicator> 108 + </span> 109 + <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> 110 + </SelectPrimitive.Item> 111 + ); 112 + } 113 + 114 + function SelectSeparator({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Separator>) { 115 + return ( 116 + <SelectPrimitive.Separator 117 + data-slot="select-separator" 118 + className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)} 119 + {...props} 120 + /> 121 + ); 122 + } 123 + 124 + function SelectScrollUpButton({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) { 125 + return ( 126 + <SelectPrimitive.ScrollUpButton 127 + data-slot="select-scroll-up-button" 128 + className={cn("flex cursor-default items-center justify-center py-1", className)} 129 + {...props} 130 + > 131 + <ChevronUpIcon className="size-4" /> 132 + </SelectPrimitive.ScrollUpButton> 133 + ); 134 + } 135 + 136 + function SelectScrollDownButton({ 137 + className, 138 + ...props 139 + }: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) { 140 + return ( 141 + <SelectPrimitive.ScrollDownButton 142 + data-slot="select-scroll-down-button" 143 + className={cn("flex cursor-default items-center justify-center py-1", className)} 144 + {...props} 145 + > 146 + <ChevronDownIcon className="size-4" /> 147 + </SelectPrimitive.ScrollDownButton> 148 + ); 149 + } 150 + 151 + export { 152 + Select, 153 + SelectContent, 154 + SelectGroup, 155 + SelectItem, 156 + SelectLabel, 157 + SelectScrollDownButton, 158 + SelectScrollUpButton, 159 + SelectSeparator, 160 + SelectTrigger, 161 + SelectValue, 162 + };
+18
src/components/ui/textarea.tsx
··· 1 + import * as React from "react"; 2 + 3 + import { cn } from "@/lib/utils"; 4 + 5 + function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { 6 + return ( 7 + <textarea 8 + data-slot="textarea" 9 + className={cn( 10 + "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", 11 + className, 12 + )} 13 + {...props} 14 + /> 15 + ); 16 + } 17 + 18 + export { Textarea };
+118
src/data/portfolio.ts
··· 1 + type DescriptionPart = { 2 + text: string 3 + bold?: boolean 4 + url?: string 5 + } 6 + 7 + export const personalInfo = { 8 + name: { 9 + first: "at://nekomimi.pet", 10 + last: "", 11 + }, 12 + title: "Developer, cat", 13 + description: [ 14 + { text: "A cat working on " }, 15 + { text: "useful", bold: true }, 16 + { text: ", " }, 17 + { text: "genuine", bold: true }, 18 + { text: " " }, 19 + { text: "decentralized", bold: true, url: "https://atproto.com" }, 20 + { text: " experiences. Not Web3. Also likes to " }, 21 + { text: "draw", bold: true, url: "https://bsky.app/profile/art.nekomimi.pet" }, 22 + { text: "." }, 23 + ], 24 + availability: { 25 + status: "Available for work", 26 + location: "Richmond, VA, USA", 27 + }, 28 + contact: { 29 + email: "ana@nekomimi.pet", 30 + }, 31 + } 32 + 33 + export const currentRole = { 34 + title: "Freelance", 35 + company: "maybe your company :)", 36 + period: "2021 — Present", 37 + } 38 + 39 + export const skills = ["React", "TypeScript", "Go", "Devops/Infra", "Atproto", "Cryptocurrencies"] 40 + 41 + export const workExperience = [ 42 + { 43 + year: "2020-2025", 44 + role: "Fullstack Engineer, Infra/DevOps, Security Reviews", 45 + company: "Freelance", 46 + 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.", 47 + tech: ["React", "TypeScript", "Bun", "SQL"], 48 + projects: [ 49 + { 50 + title: "atproto-ui", 51 + description: "A React component for rendering common UI elements across AT applications like Bluesky, Leaflet.pub, and more.", 52 + tech: ["React", "TypeScript"], 53 + links: { 54 + live: "https://atproto-ui.netlify.app/", 55 + github: "https://tangled.org/@nekomimi.pet/atproto-ui", 56 + }, 57 + }, 58 + { 59 + title: "wisp.place", 60 + description: "A static site hoster built on the AT protocol. Users retain control over site data and content while Wisp acts as a CDN.", 61 + tech: ["React", "TypeScript", "Bun", "ElysiaJS", "Hono", "Docker"], 62 + links: { 63 + live: "https://wisp.place", 64 + github: "#", 65 + }, 66 + }, 67 + { 68 + title: "Confidential", 69 + description: "NixOS consulting. Maintaining automated deployments, provisioning, and configuration management.", 70 + tech: ["Nix", "Terraform"], 71 + }, 72 + { 73 + title: "Embedder", 74 + 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.", 75 + tech: ["HTMX", "Express", "TypeScript"], 76 + links: { 77 + github: "https://github.com/waveringana/embedder", 78 + }, 79 + } 80 + ], 81 + }, 82 + { 83 + year: "2016-2019", 84 + role: "Software Engineer, DevOps", 85 + company: "Horizen.io, later freelance", 86 + description: "Helped launch Horizen, built various necessary blockchain infrastructure like explorers and pools. Managed CI/CD pipelines, infrastructure, and community support.", 87 + tech: ["Node.js", "Jenkins", "C++"], 88 + projects: [ 89 + { 90 + title: "Z-NOMP", 91 + description: "A port of NOMP for Equihash Coins, built using Node.js and Redis.", 92 + tech: ["Node.js", "Redis"], 93 + links: { 94 + github: "https://github.com/z-classic/z-nomp", 95 + }, 96 + }, 97 + { 98 + title: "Equihash-Solomining", 99 + description: "Pool software dedicated for Solomining, initially for equihash but for private clients, worked to adapt to PoW of their needs.", 100 + tech: ["Node.js", "Redis", "d3.js"], 101 + links: { 102 + github: "https://github.com/waveringana/equihash-solomining", 103 + }, 104 + } 105 + ] 106 + }, 107 + ] 108 + 109 + export const socialLinks = [ 110 + { name: "GitHub", handle: "@waveringana", url: "https://github.com/waveringana" }, 111 + { name: "Bluesky", handle: "@nekomimi.pet", url: "https://bsky.app/profile/nekomimi.pet" }, 112 + { name: "Tangled", handle: "@nekomimi.pet", url: "https://tangled.org/@nekomimi.pet" }, 113 + { name: "Vgen", handle: "@ananekomimi", url: "https://vgen.co/ananekomimi" } 114 + ] 115 + 116 + export const sections = ["intro", "work", "connect"] as const 117 + 118 + export type Section = (typeof sections)[number]
+29
src/frontend.tsx
··· 1 + /** 2 + * This file is the entry point for the React app, it sets up the root 3 + * element and renders the App component to the DOM. 4 + * 5 + * It is included in `src/index.html`. 6 + */ 7 + 8 + import { StrictMode } from "react"; 9 + import { createRoot } from "react-dom/client"; 10 + import { App } from "./App"; 11 + import { AtProtoProvider } from "atproto-ui" 12 + 13 + const elem = document.getElementById("root")!; 14 + const app = ( 15 + <StrictMode> 16 + <AtProtoProvider> 17 + <App /> 18 + </AtProtoProvider> 19 + </StrictMode> 20 + ); 21 + 22 + if (import.meta.hot) { 23 + // With hot module reloading, `import.meta.hot.data` is persisted. 24 + const root = (import.meta.hot.data.root ??= createRoot(elem)); 25 + root.render(app); 26 + } else { 27 + // The hot module reloading API is not available in production. 28 + createRoot(elem).render(app); 29 + }
+37
src/hooks/useSystemTheme.ts
··· 1 + import { useState, useEffect } from "react" 2 + 3 + export function useSystemTheme() { 4 + const [isLightMode, setIsLightMode] = useState(false) 5 + 6 + useEffect(() => { 7 + const mediaQuery = window.matchMedia("(prefers-color-scheme: light)") 8 + 9 + const handleChange = (e: MediaQueryListEvent) => { 10 + const isLight = e.matches 11 + setIsLightMode(isLight) 12 + 13 + // Apply or remove the 'dark' class on the document element 14 + if (isLight) { 15 + document.documentElement.classList.remove('dark') 16 + } else { 17 + document.documentElement.classList.add('dark') 18 + } 19 + } 20 + 21 + const isLight = mediaQuery.matches 22 + setIsLightMode(isLight) 23 + 24 + // Apply or remove the 'dark' class on initial load 25 + if (isLight) { 26 + document.documentElement.classList.remove('dark') 27 + } else { 28 + document.documentElement.classList.add('dark') 29 + } 30 + 31 + mediaQuery.addEventListener("change", handleChange) 32 + 33 + return () => mediaQuery.removeEventListener("change", handleChange) 34 + }, []) 35 + 36 + return isLightMode 37 + }
+1
src/index.css
··· 1 + @import "../styles/globals.css";
+49
src/index.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 + <title>Bun + React</title> 7 + <link rel="preconnect" href="https://fonts.googleapis.com" /> 8 + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> 9 + <link 10 + href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@300..700&display=swap" 11 + rel="stylesheet" 12 + /> 13 + <script type="module" src="./frontend.tsx" async></script> 14 + </head> 15 + <body> 16 + <div id="root"></div> 17 + 18 + <svg 19 + xmlns="http://www.w3.org/2000/svg" 20 + width="0" 21 + height="0" 22 + style="position: absolute; overflow: hidden" 23 + > 24 + <defs> 25 + <filter id="frosted" x="0%" y="0%" width="100%" height="100%"> 26 + <feTurbulence 27 + type="fractalNoise" 28 + baseFrequency="0.008 0.008" 29 + numOctaves="2" 30 + seed="92" 31 + result="noise" 32 + /> 33 + <feGaussianBlur 34 + in="noise" 35 + stdDeviation="2" 36 + result="blurred" 37 + /> 38 + <feDisplacementMap 39 + in="SourceGraphic" 40 + in2="blurred" 41 + scale="77" 42 + xChannelSelector="R" 43 + yChannelSelector="G" 44 + /> 45 + </filter> 46 + </defs> 47 + </svg> 48 + </body> 49 + </html>
+49
src/index.ts
··· 1 + import { serve } from "bun"; 2 + import index from "./index.html"; 3 + 4 + const server = serve({ 5 + routes: { 6 + "/.well-known/atproto-did": async () => { 7 + return new Response("did:plc:ttdrpj45ibqunmfhdsb4zdwq", { 8 + headers: { 9 + "Content-Type": "text/plain", 10 + }, 11 + }); 12 + }, 13 + 14 + // Serve index.html for all unmatched routes. 15 + "/*": index, 16 + 17 + "/api/hello": { 18 + async GET(req) { 19 + return Response.json({ 20 + message: "Hello, world!", 21 + method: "GET", 22 + }); 23 + }, 24 + async PUT(req) { 25 + return Response.json({ 26 + message: "Hello, world!", 27 + method: "PUT", 28 + }); 29 + }, 30 + }, 31 + 32 + "/api/hello/:name": async req => { 33 + const name = req.params.name; 34 + return Response.json({ 35 + message: `Hello, ${name}!`, 36 + }); 37 + }, 38 + }, 39 + 40 + development: process.env.NODE_ENV !== "production" && { 41 + // Enable browser hot reloading in development 42 + hmr: true, 43 + 44 + // Echo console logs from the browser to the server 45 + console: true, 46 + }, 47 + }); 48 + 49 + console.log(`🚀 Server running at ${server.url}`);
+6
src/lib/utils.ts
··· 1 + import { type ClassValue, clsx } from "clsx"; 2 + import { twMerge } from "tailwind-merge"; 3 + 4 + export function cn(...inputs: ClassValue[]) { 5 + return twMerge(clsx(inputs)); 6 + }
+166
styles/globals.css
··· 1 + @import "tailwindcss"; 2 + @import "tw-animate-css"; 3 + 4 + @custom-variant dark (&:is(.dark *)); 5 + 6 + @theme inline { 7 + --color-background: var(--background); 8 + --color-foreground: var(--foreground); 9 + --color-sidebar-ring: var(--sidebar-ring); 10 + --color-sidebar-border: var(--sidebar-border); 11 + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 12 + --color-sidebar-accent: var(--sidebar-accent); 13 + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 14 + --color-sidebar-primary: var(--sidebar-primary); 15 + --color-sidebar-foreground: var(--sidebar-foreground); 16 + --color-sidebar: var(--sidebar); 17 + --color-chart-5: var(--chart-5); 18 + --color-chart-4: var(--chart-4); 19 + --color-chart-3: var(--chart-3); 20 + --color-chart-2: var(--chart-2); 21 + --color-chart-1: var(--chart-1); 22 + --color-ring: var(--ring); 23 + --color-input: var(--input); 24 + --color-border: var(--border); 25 + --color-destructive: var(--destructive); 26 + --color-accent-foreground: var(--accent-foreground); 27 + --color-accent: var(--accent); 28 + --color-muted-foreground: var(--muted-foreground); 29 + --color-muted: var(--muted); 30 + --color-secondary-foreground: var(--secondary-foreground); 31 + --color-secondary: var(--secondary); 32 + --color-primary-foreground: var(--primary-foreground); 33 + --color-primary: var(--primary); 34 + --color-popover-foreground: var(--popover-foreground); 35 + --color-popover: var(--popover); 36 + --color-card-foreground: var(--card-foreground); 37 + --color-card: var(--card); 38 + --radius-sm: calc(var(--radius) - 4px); 39 + --radius-md: calc(var(--radius) - 2px); 40 + --radius-lg: var(--radius); 41 + --radius-xl: calc(var(--radius) + 4px); 42 + --font-sans: "Fira Code", monospace; 43 + } 44 + 45 + :root { 46 + --radius: 0.625rem; 47 + /* Dark mode - #1a1a1a primary, #2a2a2a secondary */ 48 + --background: #1a1a1a; 49 + --foreground: oklch(0.92 0.005 255); 50 + --card: #2a2a2a; 51 + --card-foreground: oklch(0.92 0.005 255); 52 + --popover: #2a2a2a; 53 + --popover-foreground: oklch(0.92 0.005 255); 54 + --primary: oklch(0.88 0.008 255); 55 + --primary-foreground: #1a1a1a; 56 + --secondary: #2a2a2a; 57 + --secondary-foreground: oklch(0.92 0.005 255); 58 + --muted: #2a2a2a; 59 + --muted-foreground: oklch(0.65 0.012 255); 60 + --accent: #2a2a2a; 61 + --accent-foreground: oklch(0.92 0.005 255); 62 + --destructive: oklch(0.704 0.191 22.216); 63 + --border: #333333; 64 + --input: #333333; 65 + --ring: oklch(0.5 0.015 255); 66 + --chart-1: oklch(0.488 0.243 264.376); 67 + --chart-2: oklch(0.696 0.17 162.48); 68 + --chart-3: oklch(0.769 0.188 70.08); 69 + --chart-4: oklch(0.627 0.265 303.9); 70 + --chart-5: oklch(0.645 0.246 16.439); 71 + --sidebar: #2a2a2a; 72 + --sidebar-foreground: oklch(0.92 0.005 255); 73 + --sidebar-primary: oklch(0.488 0.243 264.376); 74 + --sidebar-primary-foreground: oklch(0.95 0.005 255); 75 + --sidebar-accent: #2a2a2a; 76 + --sidebar-accent-foreground: oklch(0.92 0.005 255); 77 + --sidebar-border: #333333; 78 + --sidebar-ring: oklch(0.5 0.015 255); 79 + 80 + /* Glassmorphism variables - Dark mode */ 81 + --glass-bg: rgba(255, 255, 255, 0.05); 82 + --glass-border: rgba(255, 255, 255, 0.1); 83 + --glass-shadow: rgba(0, 0, 0, 0.3); 84 + } 85 + 86 + @layer base { 87 + * { 88 + @apply border-border outline-ring/50; 89 + } 90 + body { 91 + @apply bg-background text-foreground; 92 + } 93 + } 94 + 95 + @keyframes fade-in-up { 96 + from { 97 + opacity: 0; 98 + transform: translateY(20px); 99 + } 100 + to { 101 + opacity: 1; 102 + transform: translateY(0); 103 + } 104 + } 105 + 106 + .animate-fade-in-up { 107 + animation: fade-in-up 0.8s ease-out forwards; 108 + } 109 + 110 + @keyframes bounce-slow { 111 + 0%, 112 + 100% { 113 + transform: translateY(0); 114 + } 115 + 50% { 116 + transform: translateY(10px); 117 + } 118 + } 119 + 120 + .animate-bounce-slow { 121 + animation: bounce-slow 2s ease-in-out infinite; 122 + } 123 + 124 + /* Glassmorphism utilities */ 125 + @layer utilities { 126 + .glass { 127 + --shadow-offset: 0; 128 + --shadow-blur: 20px; 129 + --shadow-spread: -5px; 130 + --shadow-color: rgba(255, 255, 255, 0.7); 131 + 132 + /* Painted glass */ 133 + --tint-color: rgba(255, 255, 255, 0.08); 134 + --tint-opacity: 0.4; 135 + 136 + /* Background frost */ 137 + --frost-blur: 2px; 138 + 139 + /* SVG noise/distortion */ 140 + --noise-frequency: 0.008; 141 + --distortion-strength: 77; 142 + 143 + /* Outer shadow blur */ 144 + --outer-shadow-blur: 24px; 145 + 146 + /*background: rgba(255, 255, 255, 0.08);*/ 147 + box-shadow: inset var(--shadow-offset) var(--shadow-offset) 148 + var(--shadow-blur) var(--shadow-spread) var(--shadow-color); 149 + background-color: rgba(var(--tint-color), var(--tint-opacity)); 150 + backdrop-filter: url(#frosted); 151 + -webkit-backdrop-filter: url(#frosted); 152 + -webkit-backdrop-filter: blur(var(--frost-blur)); 153 + /*border: 2px solid transparent;*/ 154 + } 155 + 156 + .glass-hover { 157 + transition: all 0.3s ease; 158 + } 159 + 160 + .glass-hover:hover { 161 + background: rgba(255, 255, 255, 0.12); 162 + box-shadow: 163 + 0 0 0 2px rgba(255, 255, 255, 0.7), 164 + 0 20px 40px rgba(0, 0, 0, 0.16); 165 + } 166 + }
+36
tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + // Environment setup & latest features 4 + "lib": ["ESNext", "DOM"], 5 + "target": "ESNext", 6 + "module": "Preserve", 7 + "moduleDetection": "force", 8 + "jsx": "react-jsx", 9 + "allowJs": true, 10 + 11 + // Bundler mode 12 + "moduleResolution": "bundler", 13 + "allowImportingTsExtensions": true, 14 + "verbatimModuleSyntax": true, 15 + "noEmit": true, 16 + 17 + // Best practices 18 + "strict": true, 19 + "skipLibCheck": true, 20 + "noFallthroughCasesInSwitch": true, 21 + "noUncheckedIndexedAccess": true, 22 + "noImplicitOverride": true, 23 + 24 + "baseUrl": ".", 25 + "paths": { 26 + "@/*": ["./src/*"] 27 + }, 28 + 29 + // Some stricter flags (disabled by default) 30 + "noUnusedLocals": false, 31 + "noUnusedParameters": false, 32 + "noPropertyAccessFromIndexSignature": false 33 + }, 34 + 35 + "exclude": ["dist", "node_modules"] 36 + }