Compare commits

..

1 commit

Author SHA1 Message Date
0aa83f7a86 Update src/components/navbar/index.tsx
All checks were successful
Code quality checks / biome (push) Successful in 9s
2025-05-01 22:38:49 +02:00
37 changed files with 2513 additions and 683 deletions

48
.gitignore vendored
View file

@ -1,34 +1,24 @@
# dependencies (bun install) # Logs
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs logs
_.log *.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# dotenv environment variable files node_modules
.env dist
.env.development.local dist-ssr
.env.test.local *.local
.env.production.local
.env.local
# caches # Editor directories and files
.eslintcache .vscode/*
.cache !.vscode/extensions.json
*.tsbuildinfo
# IntelliJ based IDEs
.idea .idea
# Finder (MacOS) folder config
.DS_Store .DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View file

@ -1,15 +1,2 @@
# ipv4.army # ipv4.army
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run index.ts
```
This project was created using `bun init` in bun v1.2.11. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.

35
biome.json Normal file
View file

@ -0,0 +1,35 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": false
},
"files": {
"ignoreUnknown": true,
"ignore": []
},
"formatter": {
"enabled": true,
"indentStyle": "tab",
"lineEnding": "lf"
},
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"javascript": {
"formatter": {
"quoteStyle": "double",
"indentStyle": "tab",
"lineEnding": "lf",
"jsxQuoteStyle": "double",
"semicolons": "always"
}
}
}

2169
bun.lock

File diff suppressed because it is too large Load diff

View file

@ -1,2 +0,0 @@
[loader]
".woff2" = "file"

15
index.html Normal file
View file

@ -0,0 +1,15 @@
<!doctype html>
<html lang="en" data-bs-theme="dark" data-bs-core="default">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="dark">
<meta name="description" content="A Dedicated Backend Developer." />
<title>Seth @ IPv4 dot Army</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.tsx" defer></script>
</body>
</html>

247
index.ts
View file

@ -1,247 +0,0 @@
import { build, serve, gzipSync, file, type BunRequest, gc } from "bun";
import pkg from "./package.json";
const development = process.env.NODE_ENV === "development";
let heartrate = 0;
let lanyard = {};
require("node:fs/promises").rm("./dist", { recursive: true, force: true }).catch(() => {
// ignore
});
const buildWeb = async () => {
return await build({
entrypoints: ['./src/index.html'],
outdir: './dist',
minify: !development,
sourcemap: (development ? "inline" : "none"),
splitting: true,
publicPath: "/assets/",
loader: {
".woff2": "file"
},
})
}
if (!development) {
await buildWeb()
}
const server = serve({
routes: {
"/": async () => {
if (development) {
await buildWeb()
}
return new Response(gzipSync(await file("./dist/index.html").arrayBuffer()), {
headers: {
"Content-Type": "text/html",
"Cache-Control": "no-cache",
"Content-Encoding": "gzip",
}
})
},
"/assets/:file": async (req: BunRequest<"/assets/:file">) => {
const reqFile = file(`./dist/${req.params.file}`)
return new Response(gzipSync(await reqFile.arrayBuffer()), {
headers: {
"Content-Type": reqFile.type,
"Cache-Control": "public, max-age=31536000",
"Content-Encoding": "gzip",
}
})
},
"/public/:file": async (req: BunRequest<"/public/:file">) => {
const reqFile = file(`./public/${req.params.file}`)
let fileRes = await reqFile.text()
fileRes = fileRes.replace("{{LANYARD}}", `${JSON.stringify({ example: "lanyard data" })}`)
fileRes = fileRes.replace("{{HYPERATE}}", `${JSON.stringify({ example: "hyperate data" })}`)
return new Response(gzipSync(fileRes), {
headers: {
"Content-Type": reqFile.type,
"Cache-Control": "public, max-age=31536000",
"Content-Encoding": "gzip",
}
})
},
"/public/font/:file": async (req: BunRequest<"/public/font/:file">) => {
const reqFile = file(`./public/font/${req.params.file}`)
return new Response(gzipSync(await reqFile.arrayBuffer()), {
headers: {
"Content-Type": reqFile.type,
"Cache-Control": "public, max-age=31536000",
"Content-Encoding": "gzip",
}
})
},
"/api/server": () => {
const string = JSON.stringify(process)
const json = JSON.parse(string)
// clear possibly data that could be sensitive
json.argv = {}
json.debugPort = 0
json.env = {}
json.execArgv = []
json.execPath = ""
json.stderr = {}
json.stdin = {}
json.stdout = {}
json.title = ""
json.availableMemory = process.availableMemory()
json.constrainedMemory = process.constrainedMemory()
json.cpuUsage = process.cpuUsage()
json.memoryUsage = process.memoryUsage()
json.uptime = process.uptime()
json.package = pkg
return new Response(gzipSync(JSON.stringify({ data: json })), {
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-cache",
"Content-Encoding": "gzip",
}
})
},
"/api/health": () => {
return new Response(gzipSync(JSON.stringify({ data: "ok" })), {
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-cache",
"Content-Encoding": "gzip",
}
})
},
"/api/ws": (req, server) => {
if (server.upgrade(req)) {
return;
}
return new Response("Upgrade failed", { status: 500 });
},
"/api/gc": () => {
gc(true)
return new Response(gzipSync(JSON.stringify({ data: "triggered" })), {
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-cache",
"Content-Encoding": "gzip",
}
})
},
},
websocket: {
open: async (ws) => {
ws.subscribe("lanyard");
ws.send(JSON.stringify({ type: "lanyard", data: lanyard }), true);
ws.subscribe("hyperate");
ws.send(JSON.stringify({ type: "hyperate", data: { hr: heartrate } }), true);
},
message: async (ws, message) => {
ws.send(JSON.stringify({ type: "echo", data: message }), true)
},
close: async (ws) => {
console.log("WebSocket closed", ws.id);
},
drain: async (ws) => {
console.log("WebSocket drain", ws.id);
},
},
development,
port: 3000,
});
const lanyardSocket = new WebSocket("wss://lanyard.creations.works/socket");
const setLanyard = (data: object) => {
lanyard = data;
return server.publish("lanyard", JSON.stringify({ type: "lanyard", data }), true);
}
lanyardSocket.onmessage = ({ data }) => {
data = JSON.parse(data);
switch (data.op) {
case 0: {
setLanyard(data.d)
break;
}
case 1: {
lanyardSocket.send(JSON.stringify({
op: 2,
d: {
subscribe_to_id: "1273447359417942128"
}
}))
break;
}
}
}
const hyperate = new WebSocket(
"wss://app.hyperate.io/socket/websocket?token=wv39nM6iyrNJulvpmMQrimYPIXy2dVrYRjkuHpbRapKT2VSh65ngDGHdCdCtmEN9",
);
let hrTimeout: ReturnType<typeof setTimeout>;
const setHeartrate = async (hr: number) => {
heartrate = hr;
return server.publish("hyperate", JSON.stringify({ type: "hyperate", data: { hr } }), true);
}
const setHrInterval = () => {
hrTimeout = setTimeout(() => {
setHeartrate(0);
}, 6000);
};
hyperate.onopen = () => {
hyperate.send(
JSON.stringify({
topic: "hr:0BCA",
event: "phx_join",
payload: {},
ref: 0,
}),
);
setInterval(() => {
hyperate.send(
JSON.stringify({
topic: "phoenix",
event: "heartbeat",
payload: {},
ref: 0,
}),
);
}, 10000);
return setHrInterval();
};
hyperate.onmessage = ({ data }) => {
const { event, payload } = JSON.parse(data);
switch (event) {
case "hr_update": {
clearTimeout(hrTimeout);
setHrInterval();
setHeartrate(payload.hr);
break;
}
default: {
break;
}
}
};

View file

@ -1,21 +1,25 @@
{ {
"name": "ipv4.army", "name": "ipv4.army-vite",
"module": "index.ts", "private": true,
"scripts": { "version": "0.0.0",
"dev": "NODE_ENV=development bun run --hot . --watch", "type": "module",
"start": "bun run ." "scripts": {
}, "dev": "bunx --bun vite",
"devDependencies": { "lint": "bunx biome check --fix --unsafe",
"@biomejs/biome": "1.9.4", "build": "bunx --bun tsc -b && bunx --bun vite build",
"@types/bun": "latest" "preview": "bunx --bun vite preview"
}, },
"peerDependencies": { "dependencies": {
"typescript": "^5.8.3" "halfmoon": "^2.0.2",
}, "lucide-preact": "^0.487.0",
"private": true, "preact": "^10.26.2"
"type": "module", },
"dependencies": { "devDependencies": {
"microlight": "^0.0.7", "@biomejs/biome": "1.9.4",
"tsx-dom": "^3.1.0" "@fullhuman/postcss-purgecss": "^7.0.2",
} "@preact/preset-vite": "^2.10.1",
} "lightningcss": "^1.29.3",
"typescript": "~5.7.2",
"vite": "^6.2.0"
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,17 +0,0 @@
import { Glob } from "bun";
const woff2 = new Glob("./*.woff2");
for await (const file of woff2.scan(".")) {
const font = Bun.file(file);
const name = font.name?.split("-")[1];
await Bun.write(`./${name}`, await font.arrayBuffer());
console.log(`Renamed ${file} to ${name}`);
await font.delete();
console.log(`Deleted original font file: ${file}`);
}
console.log("Done")

View file

@ -1,59 +0,0 @@
.scanlines {
overflow: hidden;
}
.scanlines:before,
.scanlines:after {
display: block;
pointer-events: none;
content: "";
position: absolute;
}
.scanlines:before {
width: 100%;
height: 2px;
z-index: 2147483649;
background: rgba(0, 0, 0, 0.3);
opacity: 0.75;
animation: scanline 6s linear infinite;
}
.scanlines:after {
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 2147483648;
background: linear-gradient(to bottom,
transparent 50%,
rgba(0, 0, 0, 0.3) 51%);
background-size: 100% 4px;
animation: scanlines 1s steps(60) infinite;
}
/* ANIMATE UNIQUE SCANLINE */
@keyframes scanline {
0% {
transform: translate3d(0, 200000%, 0);
}
}
@keyframes scanlines {
0% {
background-position: 0 50%;
}
}
div {
margin: 0;
padding: 0;
}
div.scanlines {
position: absolute;
}
.microlight>span:nth-child(6) {
color: var(--status-color);
}

View file

@ -1,15 +0,0 @@
import Lanyard from './components/Lanyard';
import Hyperate from './components/Hyperate';
export default () => {
return <div>
<p>seth&gt; cat ./about.txt</p>
<p>A Dedicated Backend Developer, with a passion for high-fidelity audio, gaming, and web development.</p>
<p>seth&gt; curl /tmp/discord-ipc</p>
<p><Lanyard /></p>
<p>seth&gt; cat /tmp/heartrate</p>
<p><Hyperate /></p>
</div>
}

View file

@ -1,38 +0,0 @@
const { protocol, host } = window.location;
class Socket extends EventTarget {
private _socket: WebSocket;
constructor(url: string) {
super();
this._socket = new WebSocket(url);
this._socket.onmessage = (event) => {
const { type, data } = JSON.parse(event.data);
switch (type) {
case "lanyard": {
this.emitLanyard(data);
break;
}
case "hyperate": {
this.emitHyperate(data.hr);
break;
}
}
};
setInterval(() => {
this._socket.send("ping");
}, 10000);
}
emitLanyard(lanyard: object) {
this.dispatchEvent(new CustomEvent('lanyard', { detail: lanyard }));
}
emitHyperate(heartRate: number) {
this.dispatchEvent(new CustomEvent('hyperate', { detail: heartRate }));
}
}
export default new Socket(`${protocol.replace("http", "ws")}//${host}/api/ws`);

View file

@ -1,19 +0,0 @@
import { createRef } from "tsx-dom";
import microlight from "microlight";
import socket from "../../Socket";
export default () => {
const paragraph = createRef<HTMLParagraphElement>();
socket.addEventListener('hyperate', (event: Event) => {
const heartRate = (event as CustomEvent).detail;
if (paragraph.current) {
paragraph.current.innerText = `${heartRate} BPM`;
}
microlight.reset();
});
return <div>
<p class="microlight" ref={paragraph}>0 BPM</p>
</div>;
}

View file

@ -1,40 +0,0 @@
import { createRef } from "tsx-dom";
import microlight from "microlight";
import socket from "../../Socket";
const statusTypes: { [key: string]: string } = {
online: "green",
idle: "yellow",
dnd: "red",
invisible: "inherent",
offline: "inherent",
}
const activityTypes: { [key: number]: string } = {
0: "Playing",
1: "Streaming",
2: "Listening to",
3: "Watching",
4: "Custom",
5: "Competing in",
}
export default () => {
const paragraph = createRef<HTMLParagraphElement>();
socket.addEventListener('lanyard', (event: Event) => {
const lanyard = (event as CustomEvent).detail;
if (paragraph.current) {
paragraph.current.style = `--status-color: ${statusTypes[lanyard.discord_status]};`;
paragraph.current.innerText = JSON.stringify({
status: lanyard.discord_status,
activities: lanyard.activities.map((act: { type: number, name: string }) => { return `${activityTypes[act.type]} ${act.name}` }),
}, null, 1);
}
microlight.reset();
});
return <div>
<p class="microlight" ref={paragraph}>{JSON.stringify({})}</p>
</div>;
}

0
src/components/app.css Normal file
View file

13
src/components/app.tsx Normal file
View file

@ -0,0 +1,13 @@
import Container from "./container";
import Navbar from "./navbar";
import "./app.css";
export default () => {
return (
<>
<Navbar />
<Container />
</>
);
};

View file

@ -0,0 +1,49 @@
import { useState } from "preact/hooks";
import Heart from "../heart";
const statusMap = {
online: "border-success-subtle",
idle: "border-warning-subtle",
dnd: "border-danger-subtle",
offline: "border-light-subtle",
};
export default () => {
const [status, setStatus] = useState<keyof typeof statusMap>("offline");
fetch("https://lanyard.creations.works/v1/users/1273447359417942128")
.then((req) => req.json())
.then((res) => {
if (res.data.discord_status) {
setStatus(res.data.discord_status);
} else {
setStatus("offline");
}
});
return (
<>
<div class="container bg-body-tertiary shadow text-center position-absolute top-50 start-50 translate-middle mx-auto py-4">
<img
src="favicon.svg"
class={`img-thumbnail rounded-circle border border-4 ${statusMap[status]}`}
alt="..."
width="100px"
height="100px"
/>
<br />
<h1>Seth</h1>
<h2 class="lead">
Dedicated Backend Developer
<br />
<br />
<small class="text-body-secondary">
With a passsion for high-fidelity audio, gaming, and web development
</small>
</h2>
<Heart />
</div>
</>
);
};

View file

@ -0,0 +1,18 @@
:root {
--bpm: 0;
}
@keyframes pulse {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}
.heart {
animation: pulse calc(60s / var(--bpm)) infinite ease;
}

View file

@ -0,0 +1,71 @@
import { useState } from "preact/hooks";
import "./index.css";
export default () => {
const [heartrate, setHeartrate] = useState(0);
const ws = new WebSocket(
"wss://app.hyperate.io/socket/websocket?token=wv39nM6iyrNJulvpmMQrimYPIXy2dVrYRjkuHpbRapKT2VSh65ngDGHdCdCtmEN9",
);
let hrTimeout: ReturnType<typeof setTimeout>;
const setHrInterval = () => {
hrTimeout = setTimeout(() => {
return setHeartrate(0);
}, 6000);
};
ws.onopen = () => {
ws.send(
JSON.stringify({
topic: "hr:0BCA",
event: "phx_join",
payload: {},
ref: 0,
}),
);
setInterval(() => {
ws.send(
JSON.stringify({
topic: "phoenix",
event: "heartbeat",
payload: {},
ref: 0,
}),
);
}, 10000);
return setHrInterval();
};
ws.onmessage = ({ data }) => {
const { event, payload } = JSON.parse(data);
switch (event) {
case "hr_update": {
clearTimeout(hrTimeout);
setHrInterval();
setHeartrate(payload.hr);
break;
}
default: {
break;
}
}
};
return (
<>
<div
style={heartrate === 0 ? "display:none" : `--bpm: ${heartrate};`}
class="heart"
>
<br />
<span>{heartrate} BPM</span>
</div>
</>
);
};

View file

@ -0,0 +1,46 @@
import { Minimize, Minus, X } from "lucide-preact";
const close = () => {
window.self.close();
window.history.back();
};
export default () => {
return (
<>
<nav
class="navbar shadow fixed-top"
style="background-color: var(--bs-content-bg); border-bottom: var(--bs-border-width) solid var(--bs-content-border-color);"
>
<div class="container-fluid">
<div class="navbar-brand">
<img
src="favicon.svg"
alt="Logo"
width="24"
height="24"
class="d-inline-block align-text-top"
/>
Seth
</div>
<span class="navbar-text">IPv4 dot Army</span>
<div class="d-flex hstack gap-2">
<button type="button" class="btn btn-outline-success btn-sm">
<Minus size={20} />
</button>
<button type="button" class="btn btn-outline-warning btn-sm">
<Minimize size={20} />
</button>
<button
type="button"
class="btn btn-outline-danger btn-sm"
onClick={close}
>
<X size={20} />
</button>
</div>
</div>
</nav>
</>
);
};

View file

@ -1,18 +0,0 @@
@import "./App.css";
html,
head,
body {
margin: 0;
padding: 0;
font-family: 'Circular Std', sans-serif;
height: 100vh;
width: 100vw;
}
body {
color: #DEDEDE;
font: 2vh Inconsolata, monospace;
text-shadow: 0 0 5px #C8C8C8;
background: radial-gradient(at bottom right, rgba(0, 150, 0, 0.1) 0%, rgba(0, 0, 0, 1) 100%);
}

View file

@ -1,23 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#1a1d1f">
<meta name="description"
content="A Dedicated Backend Developer, with a passion for high-fidelity audio, gaming, and web development.">
<meta name="keywords" content="Seth, IPv4, Army, Seth@IPv4, web development, audio, gaming">
<meta name="author" content="Seth">
<title>Seth @ IPv4 dot Army</title>
<link rel="icon" href="/public/favicon.svg" />
<meta name="color-scheme" content="dark" />
<link rel="stylesheet" href="index.css" />
<style id="font"></style>
</head>
<body class="scanlines">
<script src="index.tsx"></script>
</body>
</html>

View file

@ -1,74 +0,0 @@
import "tsx-dom";
import App from './App';
const font = document.getElementById("font")
if (font) {
font.innerText = `@font-face {
font-family: 'Circular Std';
src: url('/public/font/Black.woff2') format('woff2');
font-weight: black;
font-style: normal;
font-display: swap
}
@font-face {
font-family: 'Circular Std';
src: url('/public/font/BlackItalic.woff2') format('woff2');
font-weight: black;
font-style: italic;
font-display: swap
}
@font-face {
font-family: 'Circular Std';
src: url('/public/font/Bold.woff2') format('woff2');
font-weight: bold;
font-style: normal;
font-display: swap
}
@font-face {
font-family: 'Circular Std';
src: url('/public/font/BoldItalic.woff2') format('woff2');
font-weight: bold;
font-style: italic;
font-display: swap
}
@font-face {
font-family: 'Circular Std';
src: url('/public/font/Book.woff2') format('woff2');
font-weight: normal;
font-style: normal;
font-display: swap
}
@font-face {
font-family: 'Circular Std';
src: url('/public/font/BookItalic.woff2') format('woff2');
font-weight: normal;
font-style: italic;
font-display: swap
}
@font-face {
font-family: 'Circular Std';
src: url('/public/font/Medium.woff2') format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap
}
@font-face {
font-family: 'Circular Std';
src: url('/public/font/MediumItalic.woff2') format('woff2');
font-weight: 500;
font-style: italic;
font-display: swap
}`
}
document.body.appendChild(<App />);
// You're garbage, let me collect you.
fetch("/api/gc")

9
src/main.tsx Normal file
View file

@ -0,0 +1,9 @@
import { render } from "preact";
import "./index.css";
import "halfmoon/css/halfmoon.min.css";
import App from "./components/app.tsx";
render(
<App />,
(document.getElementById("app") as HTMLElement) || document.body,
);

1
src/vite-env.d.ts vendored Normal file
View file

@ -0,0 +1 @@
/// <reference types="vite/client" />

27
tsconfig.app.json Normal file
View file

@ -0,0 +1,27 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"paths": {
"react": ["./node_modules/preact/compat/"],
"react-dom": ["./node_modules/preact/compat/"]
},
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"jsxImportSource": "preact",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

View file

@ -1,29 +1,11 @@
{ {
"compilerOptions": { "files": [],
// Environment setup & latest features "references": [
"lib": [ {
"ESNext", "path": "./tsconfig.app.json"
"DOM", },
], {
"target": "ESNext", "path": "./tsconfig.node.json"
"module": "ESNext", }
"moduleDetection": "force", ]
"jsx": "react-jsx", }
"jsxImportSource": "tsx-dom",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}

20
tsconfig.node.json Normal file
View file

@ -0,0 +1,20 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

28
vite.config.ts Normal file
View file

@ -0,0 +1,28 @@
/// <reference types="vite/client" />
import preact from "@preact/preset-vite";
import { defineConfig } from "vite";
import postCSSPurgeCSS from "@fullhuman/postcss-purgecss";
// https://vite.dev/config/
export default defineConfig({
css: {
...(import.meta.env.NODE_ENV === "production"
? {
transformer: "postcss",
postcss: {
plugins: [
postCSSPurgeCSS({
content: ["./index.html", "./src/**/*.{ts,tsx}"],
}),
],
},
}
: { transformer: "lightningcss" }),
},
build: {
cssMinify: "lightningcss",
},
plugins: [preact()],
});