+34
.gitignore
+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
+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
+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
+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
+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
+3
bunfig.toml
+21
components.json
+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
+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
+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
+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
+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
+
}
+25
src/components/SocialLink.tsx
+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
+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
+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
+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
+
}
+193
src/components/sections/Header.tsx
+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
+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
+
}
+56
src/components/ui/card.tsx
+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
+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
+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
+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
+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
+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
+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
+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
src/index.css
···
1
+
@import "../styles/globals.css";
+49
src/index.html
+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
+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
+6
src/lib/utils.ts
+166
styles/globals.css
+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
+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
+
}