summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorElizabeth Hunt <me@liz.coffee>2025-12-14 16:14:29 -0800
committerElizabeth Hunt <me@liz.coffee>2025-12-14 16:17:29 -0800
commit8ec712c8c884110600954860c21f58107455cfdc (patch)
tree5e5b16ec8b0a1d15d58beae5bc8a7fd5285c6d0e
parentdb0d9b80b4412a46cae0e58997f4baa7213948e3 (diff)
downloadadelie-8ec712c8c884110600954860c21f58107455cfdc.tar.gz
adelie-8ec712c8c884110600954860c21f58107455cfdc.zip
Move to typescript
-rw-r--r--.claude/settings.local.json12
-rw-r--r--.gitignore1
-rw-r--r--Dockerfile2
-rw-r--r--biome.json26
-rw-r--r--docker-entrypoint.sh9
-rw-r--r--esbuild.config.js64
-rw-r--r--nginx.conf8
-rw-r--r--package-lock.json576
-rw-r--r--package.json18
-rw-r--r--rolldown.config.js36
-rw-r--r--src/css/style.css1395
-rw-r--r--src/index.html1167
-rw-r--r--src/js/oneko.js284
-rw-r--r--src/js/oneko.ts277
-rw-r--r--src/js/script.js87
-rw-r--r--src/js/script.ts78
-rw-r--r--src/types/global.d.ts7
-rw-r--r--tsconfig.json17
18 files changed, 1999 insertions, 2065 deletions
diff --git a/.claude/settings.local.json b/.claude/settings.local.json
deleted file mode 100644
index 282bb4b..0000000
--- a/.claude/settings.local.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "permissions": {
- "allow": [
- "Bash(tree:*)",
- "Bash(find:*)",
- "Bash(file:*)",
- "Bash(npm run dev:*)",
- "Bash(npm run build:*)",
- "Bash(HOST=\"https://cdn.example.com\" npm run build:*)"
- ]
- }
-}
diff --git a/.gitignore b/.gitignore
index f66f2eb..890d843 100644
--- a/.gitignore
+++ b/.gitignore
@@ -35,3 +35,4 @@ yarn-error.log
# Temporary files
*.tmp
*.log
+
diff --git a/Dockerfile b/Dockerfile
index 26c8c2a..18c3134 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -5,7 +5,7 @@ COPY package*.json ./
RUN npm ci
COPY src/ ./src/
-COPY esbuild.config.js ./
+COPY rolldown.config.js ./
RUN npm run build
FROM nginx:alpine as adelie
diff --git a/biome.json b/biome.json
new file mode 100644
index 0000000..bcf844c
--- /dev/null
+++ b/biome.json
@@ -0,0 +1,26 @@
+{
+ "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
+ "formatter": {
+ "enabled": true,
+ "indentStyle": "space",
+ "indentWidth": 4,
+ "lineWidth": 100
+ },
+ "linter": {
+ "enabled": true,
+ "rules": {
+ "recommended": true
+ }
+ },
+ "javascript": {
+ "formatter": {
+ "quoteStyle": "single",
+ "trailingCommas": "es5"
+ }
+ },
+ "json": {
+ "formatter": {
+ "indentWidth": 4
+ }
+ }
+}
diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh
index 123725b..f2eed53 100644
--- a/docker-entrypoint.sh
+++ b/docker-entrypoint.sh
@@ -1,12 +1,9 @@
#!/bin/sh
-# Rewrite asset URLs in HTML to use the production domain
if [ -n "$HOST" ]; then
- sed -i "s|href=\"/bundle\.|href=\"$HOST/bundle.|g" /usr/share/nginx/html/index.html
- sed -i "s|src=\"/bundle\.|src=\"$HOST/bundle.|g" /usr/share/nginx/html/index.html
- sed -i "s|href=\"/img/|href=\"$HOST/img/|g" /usr/share/nginx/html/index.html
- sed -i "s|src=\"/oneko/|src=\"$HOST/oneko/|g" /usr/share/nginx/html/index.html
+ sed -i "s|ASSET_BASE_PLACEHOLDER|$HOST|g" /usr/share/nginx/html/index.html
+else
+ sed -i "s|\"ASSET_BASE_PLACEHOLDER\"|\"\""|g" /usr/share/nginx/html/index.html
fi
-# Start nginx
exec nginx -g 'daemon off;'
diff --git a/esbuild.config.js b/esbuild.config.js
deleted file mode 100644
index 88d726a..0000000
--- a/esbuild.config.js
+++ /dev/null
@@ -1,64 +0,0 @@
-const esbuild = require('esbuild');
-const fs = require('fs-extra');
-const path = require('path');
-
-const production = process.env.NODE_ENV === 'production';
-
-async function buildJS() {
- await esbuild.build({
- entryPoints: ['src/js/script.js'],
- bundle: true,
- minify: production,
- sourcemap: true,
- target: 'es2020',
- outfile: 'dist/bundle.js',
- });
-}
-
-async function buildCSS() {
- await esbuild.build({
- entryPoints: ['src/css/style.css'],
- bundle: true,
- minify: production,
- sourcemap: true,
- loader: {
- '.css': 'css',
- '.woff2': 'file',
- '.png': 'file',
- '.svg': 'file',
- },
- outfile: 'dist/bundle.css',
- });
-}
-
-async function copyAssets() {
- await fs.copy('src/assets', 'dist', {
- overwrite: true,
- });
-}
-
-async function processHTML() {
- let html = await fs.readFile('src/index.html', 'utf8');
- await fs.writeFile('dist/index.html', html);
-}
-
-async function clean() {
- await fs.remove('dist');
- await fs.ensureDir('dist');
-}
-
-async function build() {
- try {
- await clean();
- await Promise.all([
- buildJS(),
- buildCSS(),
- copyAssets(),
- ]);
- await processHTML();
- } catch (err) {
- process.exit(1);
- }
-}
-
-build();
diff --git a/nginx.conf b/nginx.conf
index 9797947..12fcf04 100644
--- a/nginx.conf
+++ b/nginx.conf
@@ -24,7 +24,6 @@ http {
types_hash_max_size 2048;
client_max_body_size 20M;
- # Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
@@ -37,42 +36,35 @@ http {
server_name _;
root /usr/share/nginx/html;
- # Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
- # CORS headers - allow requests from any origin
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "GET, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type" always;
- # Handle preflight requests
if ($request_method = 'OPTIONS') {
return 204;
}
- # CSS and JS - long cache, versioning handled by filenames
location ~* \.(css|js)$ {
expires 1y;
add_header Cache-Control "public, immutable, max-age=31536000" always;
}
- # Fonts - long cache
location ~* \.(woff|woff2|ttf|otf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable, max-age=31536000" always;
add_header Access-Control-Allow-Origin "*" always;
}
- # Images - moderate cache
location ~* \.(jpg|jpeg|png|gif|svg|ico|webp)$ {
expires 30d;
add_header Cache-Control "public, max-age=2592000" always;
}
- # Everything else - short cache
location / {
expires 1h;
add_header Cache-Control "public, max-age=3600" always;
diff --git a/package-lock.json b/package-lock.json
index aa28f56..5791614 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,314 +1,309 @@
{
- "name": "adelie-css",
- "version": "1.0.0",
+ "name": "adelie",
+ "version": "0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "adelie-css",
- "version": "1.0.0",
+ "name": "adelie",
+ "version": "0.0.1",
"dependencies": {
"prismjs": "^1.29.0"
},
"devDependencies": {
- "esbuild": "^0.25.5",
+ "@biomejs/biome": "^1.9.4",
+ "@types/node": "^22.10.2",
"fs-extra": "^11.2.0",
- "http-server": "^14.1.1"
+ "http-server": "^14.1.1",
+ "rolldown": "^0.12.1",
+ "typescript": "^5.9.3"
}
},
- "node_modules/@esbuild/aix-ppc64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
- "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "node_modules/@biomejs/biome": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.4.tgz",
+ "integrity": "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT OR Apache-2.0",
+ "bin": {
+ "biome": "bin/biome"
+ },
+ "engines": {
+ "node": ">=14.21.3"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/biome"
+ },
+ "optionalDependencies": {
+ "@biomejs/cli-darwin-arm64": "1.9.4",
+ "@biomejs/cli-darwin-x64": "1.9.4",
+ "@biomejs/cli-linux-arm64": "1.9.4",
+ "@biomejs/cli-linux-arm64-musl": "1.9.4",
+ "@biomejs/cli-linux-x64": "1.9.4",
+ "@biomejs/cli-linux-x64-musl": "1.9.4",
+ "@biomejs/cli-win32-arm64": "1.9.4",
+ "@biomejs/cli-win32-x64": "1.9.4"
+ }
+ },
+ "node_modules/@biomejs/cli-darwin-arm64": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz",
+ "integrity": "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==",
"cpu": [
- "ppc64"
+ "arm64"
],
"dev": true,
- "license": "MIT",
+ "license": "MIT OR Apache-2.0",
"optional": true,
"os": [
- "aix"
+ "darwin"
],
"engines": {
- "node": ">=18"
+ "node": ">=14.21.3"
}
},
- "node_modules/@esbuild/android-arm": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
- "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "node_modules/@biomejs/cli-darwin-x64": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz",
+ "integrity": "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==",
"cpu": [
- "arm"
+ "x64"
],
"dev": true,
- "license": "MIT",
+ "license": "MIT OR Apache-2.0",
"optional": true,
"os": [
- "android"
+ "darwin"
],
"engines": {
- "node": ">=18"
+ "node": ">=14.21.3"
}
},
- "node_modules/@esbuild/android-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
- "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "node_modules/@biomejs/cli-linux-arm64": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz",
+ "integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==",
"cpu": [
"arm64"
],
"dev": true,
- "license": "MIT",
+ "license": "MIT OR Apache-2.0",
"optional": true,
"os": [
- "android"
+ "linux"
],
"engines": {
- "node": ">=18"
+ "node": ">=14.21.3"
}
},
- "node_modules/@esbuild/android-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
- "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "node_modules/@biomejs/cli-linux-arm64-musl": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz",
+ "integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==",
"cpu": [
- "x64"
+ "arm64"
],
"dev": true,
- "license": "MIT",
+ "license": "MIT OR Apache-2.0",
"optional": true,
"os": [
- "android"
+ "linux"
],
"engines": {
- "node": ">=18"
+ "node": ">=14.21.3"
}
},
- "node_modules/@esbuild/darwin-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
- "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "node_modules/@biomejs/cli-linux-x64": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz",
+ "integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==",
"cpu": [
- "arm64"
+ "x64"
],
"dev": true,
- "license": "MIT",
+ "license": "MIT OR Apache-2.0",
"optional": true,
"os": [
- "darwin"
+ "linux"
],
"engines": {
- "node": ">=18"
+ "node": ">=14.21.3"
}
},
- "node_modules/@esbuild/darwin-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
- "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "node_modules/@biomejs/cli-linux-x64-musl": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz",
+ "integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==",
"cpu": [
"x64"
],
"dev": true,
- "license": "MIT",
+ "license": "MIT OR Apache-2.0",
"optional": true,
"os": [
- "darwin"
+ "linux"
],
"engines": {
- "node": ">=18"
+ "node": ">=14.21.3"
}
},
- "node_modules/@esbuild/freebsd-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
- "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "node_modules/@biomejs/cli-win32-arm64": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz",
+ "integrity": "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==",
"cpu": [
"arm64"
],
"dev": true,
- "license": "MIT",
+ "license": "MIT OR Apache-2.0",
"optional": true,
"os": [
- "freebsd"
+ "win32"
],
"engines": {
- "node": ">=18"
+ "node": ">=14.21.3"
}
},
- "node_modules/@esbuild/freebsd-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
- "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "node_modules/@biomejs/cli-win32-x64": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz",
+ "integrity": "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==",
"cpu": [
"x64"
],
"dev": true,
- "license": "MIT",
+ "license": "MIT OR Apache-2.0",
"optional": true,
"os": [
- "freebsd"
+ "win32"
],
"engines": {
- "node": ">=18"
+ "node": ">=14.21.3"
}
},
- "node_modules/@esbuild/linux-arm": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
- "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
- "cpu": [
- "arm"
- ],
+ "node_modules/@emnapi/core": {
+ "version": "1.7.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz",
+ "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==",
"dev": true,
"license": "MIT",
"optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.1.0",
+ "tslib": "^2.4.0"
}
},
- "node_modules/@esbuild/linux-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
- "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
- "cpu": [
- "arm64"
- ],
+ "node_modules/@emnapi/runtime": {
+ "version": "1.7.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz",
+ "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==",
"dev": true,
"license": "MIT",
"optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
+ "dependencies": {
+ "tslib": "^2.4.0"
}
},
- "node_modules/@esbuild/linux-ia32": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
- "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
- "cpu": [
- "ia32"
- ],
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz",
+ "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==",
"dev": true,
"license": "MIT",
"optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
+ "dependencies": {
+ "tslib": "^2.4.0"
}
},
- "node_modules/@esbuild/linux-loong64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
- "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
- "cpu": [
- "loong64"
- ],
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "0.2.12",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
+ "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==",
"dev": true,
"license": "MIT",
"optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
+ "dependencies": {
+ "@emnapi/core": "^1.4.3",
+ "@emnapi/runtime": "^1.4.3",
+ "@tybys/wasm-util": "^0.10.0"
}
},
- "node_modules/@esbuild/linux-mips64el": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
- "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "0.12.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-0.12.2.tgz",
+ "integrity": "sha512-Y3Ajye63Z5KymGUwTLaK7Q6YMvycXqNiXtosecgVzjAwMITCmXdzgnWgzzx5UlWHMrDYL4m5zIeXGB5slLwoMA==",
"cpu": [
- "mips64el"
+ "arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
+ "darwin"
+ ]
},
- "node_modules/@esbuild/linux-ppc64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
- "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "0.12.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-0.12.2.tgz",
+ "integrity": "sha512-ZcVuVFEFBXhp00TUNn+EDYs7SGGLQCznvCeuW1XkM8EC2/LfewU/o4WuJK7CBC4iCSktuFGpw7+zLe8D6iinzg==",
"cpu": [
- "ppc64"
+ "x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
+ "darwin"
+ ]
},
- "node_modules/@esbuild/linux-riscv64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
- "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "0.12.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-0.12.2.tgz",
+ "integrity": "sha512-4fjuQHpm3q/Ly4fcqb8Qn49OQc2EQR2scUbQaOzXr7mIn9Zy8NfdRrsVG4/wpYvihIlTEtVx+ku0IZwcUzzZGg==",
"cpu": [
- "riscv64"
+ "x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
+ "freebsd"
+ ]
},
- "node_modules/@esbuild/linux-s390x": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
- "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "0.12.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.12.2.tgz",
+ "integrity": "sha512-MGEDaYLzTQ1kpvt13PzOwnd6O668S1mPM/vgi4O9vCfqJNTXZX8SeAg8Z2dQZbMSUyFDBVKGkz23GRTHqkGIsQ==",
"cpu": [
- "s390x"
+ "arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
- ],
- "engines": {
- "node": ">=18"
- }
+ ]
},
- "node_modules/@esbuild/linux-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
- "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "0.12.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.12.2.tgz",
+ "integrity": "sha512-8uiaMe39twyIAw0do1Gc3O2SpQmyL1A/BucFncEB8eU5jtb3BWIM/X+F+eKDU6c+XZ+S1T025dhR1cGg8y20Xw==",
"cpu": [
- "x64"
+ "arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
- ],
- "engines": {
- "node": ">=18"
- }
+ ]
},
- "node_modules/@esbuild/netbsd-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
- "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "0.12.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.12.2.tgz",
+ "integrity": "sha512-GOFSKaMJueaSSODcqI+0Hu79buHYtGV7h2tydIDkSDD20mQuvwOF7jIVd27yNdlXWS9wLObwEu3BNgHmIj1M6A==",
"cpu": [
"arm64"
],
@@ -316,16 +311,13 @@
"license": "MIT",
"optional": true,
"os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
+ "linux"
+ ]
},
- "node_modules/@esbuild/netbsd-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
- "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "0.12.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.12.2.tgz",
+ "integrity": "sha512-+hlRURUSiVP0+PFtjoTxUsiy/2NQpbf3DyUyMyl8Nv5+1BxjB6452VY1iFI+RzG4iLNJslcVcI6d6lJQ5zZYfw==",
"cpu": [
"x64"
],
@@ -333,50 +325,44 @@
"license": "MIT",
"optional": true,
"os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
+ "linux"
+ ]
},
- "node_modules/@esbuild/openbsd-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
- "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "0.12.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-0.12.2.tgz",
+ "integrity": "sha512-9WKIaQSZZ0i9F5msW4I5kEj6ov+TZLteuTqCzI7nYWDBDm7m/hYkOkdIBf41GC8iKFsgVIQO0kRVAT966U8RxQ==",
"cpu": [
- "arm64"
+ "x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
+ "linux"
+ ]
},
- "node_modules/@esbuild/openbsd-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
- "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "0.12.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-0.12.2.tgz",
+ "integrity": "sha512-KY7bNrR3Jk6O0ne8LAaAnq9yI6xuKwhr/L2d1lBwrraCCyLHk0UEv4g6PfJkyUx6GfN0gVYuSxFgo9OggycldA==",
"cpu": [
- "x64"
+ "wasm32"
],
"dev": true,
"license": "MIT",
"optional": true,
- "os": [
- "openbsd"
- ],
+ "dependencies": {
+ "@napi-rs/wasm-runtime": "^0.2.4"
+ },
"engines": {
- "node": ">=18"
+ "node": ">=14.21.3"
}
},
- "node_modules/@esbuild/openharmony-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
- "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "0.12.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.12.2.tgz",
+ "integrity": "sha512-XYBXifPk5iNcPUTyV/yB4tlj5nI+fYoe/8CLHjyUG8GS0l8rCD+jKaAv3Jvz2T2erlkjEPqJXmzhUfBscOUo6g==",
"cpu": [
"arm64"
],
@@ -384,78 +370,56 @@
"license": "MIT",
"optional": true,
"os": [
- "openharmony"
- ],
- "engines": {
- "node": ">=18"
- }
+ "win32"
+ ]
},
- "node_modules/@esbuild/sunos-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
- "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "node_modules/@rolldown/binding-win32-ia32-msvc": {
+ "version": "0.12.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.12.2.tgz",
+ "integrity": "sha512-SV9LqvEc0d4gCLbkezH+UJ2uj2pTw3mwSKhFJEm6ir1lb05W6y9++m67NRs17fhudkFAz8iKlyPxKtZBKzAKOw==",
"cpu": [
- "x64"
+ "ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
- "sunos"
- ],
- "engines": {
- "node": ">=18"
- }
+ "win32"
+ ]
},
- "node_modules/@esbuild/win32-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
- "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "0.12.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.12.2.tgz",
+ "integrity": "sha512-XnRqHx182tyk1M12OMXqLajIj9DZrOUEKSPYrQUSaMCmHHAhULYP6ki240CV16PBWZW4Q1pwQ7YVKYFevRtvKg==",
"cpu": [
- "arm64"
+ "x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
- ],
- "engines": {
- "node": ">=18"
- }
+ ]
},
- "node_modules/@esbuild/win32-ia32": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
- "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
- "cpu": [
- "ia32"
- ],
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.1",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
+ "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
"dev": true,
"license": "MIT",
"optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
+ "dependencies": {
+ "tslib": "^2.4.0"
}
},
- "node_modules/@esbuild/win32-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
- "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
- "cpu": [
- "x64"
- ],
+ "node_modules/@types/node": {
+ "version": "22.19.3",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz",
+ "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==",
"dev": true,
"license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
+ "dependencies": {
+ "undici-types": "~6.21.0"
}
},
"node_modules/ansi-styles": {
@@ -638,48 +602,6 @@
"node": ">= 0.4"
}
},
- "node_modules/esbuild": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
- "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "bin": {
- "esbuild": "bin/esbuild"
- },
- "engines": {
- "node": ">=18"
- },
- "optionalDependencies": {
- "@esbuild/aix-ppc64": "0.25.12",
- "@esbuild/android-arm": "0.25.12",
- "@esbuild/android-arm64": "0.25.12",
- "@esbuild/android-x64": "0.25.12",
- "@esbuild/darwin-arm64": "0.25.12",
- "@esbuild/darwin-x64": "0.25.12",
- "@esbuild/freebsd-arm64": "0.25.12",
- "@esbuild/freebsd-x64": "0.25.12",
- "@esbuild/linux-arm": "0.25.12",
- "@esbuild/linux-arm64": "0.25.12",
- "@esbuild/linux-ia32": "0.25.12",
- "@esbuild/linux-loong64": "0.25.12",
- "@esbuild/linux-mips64el": "0.25.12",
- "@esbuild/linux-ppc64": "0.25.12",
- "@esbuild/linux-riscv64": "0.25.12",
- "@esbuild/linux-s390x": "0.25.12",
- "@esbuild/linux-x64": "0.25.12",
- "@esbuild/netbsd-arm64": "0.25.12",
- "@esbuild/netbsd-x64": "0.25.12",
- "@esbuild/openbsd-arm64": "0.25.12",
- "@esbuild/openbsd-x64": "0.25.12",
- "@esbuild/openharmony-arm64": "0.25.12",
- "@esbuild/sunos-x64": "0.25.12",
- "@esbuild/win32-arm64": "0.25.12",
- "@esbuild/win32-ia32": "0.25.12",
- "@esbuild/win32-x64": "0.25.12"
- }
- },
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
@@ -1029,6 +951,33 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/rolldown": {
+ "version": "0.12.2",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-0.12.2.tgz",
+ "integrity": "sha512-YJYKiYt2O9XytiQ3Na4Kk29avfIXhvK7udB3wAaVaF4kiSsFKE1167tElO/0eD6tjfJXCvwNxwsyYkBJRtsLmQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "zod": "^3.23.8"
+ },
+ "bin": {
+ "rolldown": "bin/cli.js"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-darwin-arm64": "0.12.2",
+ "@rolldown/binding-darwin-x64": "0.12.2",
+ "@rolldown/binding-freebsd-x64": "0.12.2",
+ "@rolldown/binding-linux-arm-gnueabihf": "0.12.2",
+ "@rolldown/binding-linux-arm64-gnu": "0.12.2",
+ "@rolldown/binding-linux-arm64-musl": "0.12.2",
+ "@rolldown/binding-linux-x64-gnu": "0.12.2",
+ "@rolldown/binding-linux-x64-musl": "0.12.2",
+ "@rolldown/binding-wasm32-wasi": "0.12.2",
+ "@rolldown/binding-win32-arm64-msvc": "0.12.2",
+ "@rolldown/binding-win32-ia32-msvc": "0.12.2",
+ "@rolldown/binding-win32-x64-msvc": "0.12.2"
+ }
+ },
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@@ -1139,6 +1088,35 @@
"node": ">=8"
}
},
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD",
+ "optional": true
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/union": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz",
@@ -1180,6 +1158,16 @@
"engines": {
"node": ">=12"
}
+ },
+ "node_modules/zod": {
+ "version": "3.25.76",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
}
}
}
diff --git a/package.json b/package.json
index 9414767..e8f5dac 100644
--- a/package.json
+++ b/package.json
@@ -1,20 +1,28 @@
{
"name": "adelie",
"version": "0.0.1",
+ "type": "module",
"private": true,
"scripts": {
- "build": "NODE_ENV=production node esbuild.config.js",
- "dev": "node esbuild.config.js",
+ "build": "NODE_ENV=production node rolldown.config.js",
+ "dev": "node rolldown.config.js",
"clean": "rm -rf dist",
"serve": "npx http-server dist -p 8080 -o",
- "dev:serve": "npm run dev && npm run serve"
+ "dev:serve": "npm run dev && npm run serve",
+ "lint": "biome lint src",
+ "format": "biome format --write src",
+ "format:check": "biome format src",
+ "check": "biome check src"
},
"dependencies": {
"prismjs": "^1.29.0"
},
"devDependencies": {
- "esbuild": "^0.25.5",
+ "@biomejs/biome": "^1.9.4",
+ "@types/node": "^22.10.2",
"fs-extra": "^11.2.0",
- "http-server": "^14.1.1"
+ "http-server": "^14.1.1",
+ "rolldown": "^0.12.1",
+ "typescript": "^5.9.3"
}
}
diff --git a/rolldown.config.js b/rolldown.config.js
new file mode 100644
index 0000000..6cc7765
--- /dev/null
+++ b/rolldown.config.js
@@ -0,0 +1,36 @@
+import { rolldown } from 'rolldown';
+import fs from 'fs-extra';
+import path from 'path';
+
+const production = process.env.NODE_ENV === 'production';
+
+async function build() {
+ try {
+ await fs.remove('dist');
+ await fs.ensureDir('dist');
+
+ const jsBundle = await rolldown({
+ input: 'src/js/script.ts',
+ output: {
+ file: 'dist/bundle.js',
+ format: 'es',
+ sourcemap: !production,
+ },
+ });
+
+ await jsBundle.write();
+
+ await fs.copy('src/css/style.css', 'dist/bundle.css');
+ await fs.copy('src/assets', 'dist', {
+ overwrite: true,
+ });
+
+ let html = await fs.readFile('src/index.html', 'utf8');
+ await fs.writeFile('dist/index.html', html);
+ } catch (err) {
+ console.error('Build failed:', err);
+ process.exit(1);
+ }
+}
+
+build();
diff --git a/src/css/style.css b/src/css/style.css
index 50c6f48..97b14f3 100644
--- a/src/css/style.css
+++ b/src/css/style.css
@@ -1,271 +1,259 @@
:root {
- /* Win95 "strawberry latte" tokens */
-
- /* Desktop (rarely used, but handy for full-page demos) */
- --desktop: #d7a5b7;
-
- /* Page + surfaces */
- --bg: #f4ece7; /* page behind windows */
- --bg-pattern: #efe5df;
-
- /* Background texture (dither/checker overlay) */
- --dither-ink: rgba(59, 46, 44, 0.22);
- --dither-opacity: 0.18;
- --surface: #e9dcd5; /* window face */
- --surface-alt: #fff7f2; /* milk highlight */
-
- /* Text */
- --fg: #2a1f1d;
- --muted: #6a5550;
-
- /* 3D border system */
- --border: #3b2e2c;
- --border-light: #fff7f2;
- --border-dark: #8e7670;
-
- /* Girly coffee accents */
- --primary: #e56aa6; /* strawberry */
- --primary-light: #f08dbe; /* strawberry milk */
- --primary-dark: #c84d86; /* rose */
-
- --secondary: #b69cff; /* lilac */
- --secondary-light: #d7c8ff;
- --secondary-dark: #8f78d6;
-
- --accent-brown: #b88a68; /* latte */
- --accent-brown-light: #d4a574;
- --accent-brown-dark: #7a5245;
-
- /* Title bar (Win95-ish window chrome) */
- --titlebar-a: #d85b9b;
- --titlebar-b: #7a3e8e;
- --titlebar-fg: #fff7f2;
-
- /* Links */
- --link: #c84d86;
- --link-visited: #6f2f80;
- --link-active: #b5173d;
-
- /* Keep existing names as aliases */
- --accent-pink: var(--primary);
- --accent-lavender: var(--secondary);
-
- /* Status colors (slightly softened) */
- --success: #2e8b57;
- --error: #b3261e;
- --warning: #b7791f;
- --info: #2b6cb0;
-
- /* Spacing */
- --space-xs: 0.5rem;
- --space-sm: 1rem;
- --space-md: 1.5rem;
- --space-lg: 2rem;
- --space-xl: 3rem;
-
- /* Borders */
- --border-width: 2px;
- --border-style: solid;
-
- /* Type */
- --font-mono: "Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
- --line-height: 1.35;
-
- /* Shadows */
- --shadow-sm: 1px 1px 0;
- --shadow-md: 2px 2px 0;
- --shadow-lg: 3px 3px 0;
- --shadow-color: rgba(0, 0, 0, 0.22);
- --shadow-box: 2px 2px 0 rgba(0, 0, 0, 0.12);
- --shadow-button: 1px 1px 0 rgba(0, 0, 0, 0.18);
-
- --highlight: rgba(255, 247, 242, 0.9);
- --lowlight: rgba(59, 46, 44, 0.55);
-
- --content-max-width: 1000px;
+ /* Win95 "strawberry latte" tokens */
+
+ /* Desktop (rarely used, but handy for full-page demos) */
+ --desktop: #d7a5b7;
+
+ /* Page + surfaces */
+ --bg: #f4ece7; /* page behind windows */
+ --bg-pattern: #efe5df;
+
+ /* Background texture (dither/checker overlay) */
+ --dither-ink: rgba(59, 46, 44, 0.22);
+ --dither-opacity: 0.18;
+ --surface: #e9dcd5; /* window face */
+ --surface-alt: #fff7f2; /* milk highlight */
+
+ /* Text */
+ --fg: #2a1f1d;
+ --muted: #6a5550;
+
+ /* 3D border system */
+ --border: #3b2e2c;
+ --border-light: #fff7f2;
+ --border-dark: #8e7670;
+
+ /* Girly coffee accents */
+ --primary: #e56aa6; /* strawberry */
+ --primary-light: #f08dbe; /* strawberry milk */
+ --primary-dark: #c84d86; /* rose */
+
+ --secondary: #b69cff; /* lilac */
+ --secondary-light: #d7c8ff;
+ --secondary-dark: #8f78d6;
+
+ --accent-brown: #b88a68; /* latte */
+ --accent-brown-light: #d4a574;
+ --accent-brown-dark: #7a5245;
+
+ /* Title bar (Win95-ish window chrome) */
+ --titlebar-a: #d85b9b;
+ --titlebar-b: #7a3e8e;
+ --titlebar-fg: #fff7f2;
+
+ /* Links */
+ --link: #c84d86;
+ --link-visited: #6f2f80;
+ --link-active: #b5173d;
+
+ /* Keep existing names as aliases */
+ --accent-pink: var(--primary);
+ --accent-lavender: var(--secondary);
+
+ /* Status colors (slightly softened) */
+ --success: #2e8b57;
+ --error: #b3261e;
+ --warning: #b7791f;
+ --info: #2b6cb0;
+
+ /* Spacing */
+ --space-xs: 0.5rem;
+ --space-sm: 1rem;
+ --space-md: 1.5rem;
+ --space-lg: 2rem;
+ --space-xl: 3rem;
+
+ /* Borders */
+ --border-width: 2px;
+ --border-style: solid;
+
+ /* Type */
+ --font-mono: "Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
+ "Courier New", monospace;
+ --line-height: 1.35;
+
+ /* Shadows */
+ --shadow-sm: 1px 1px 0;
+ --shadow-md: 2px 2px 0;
+ --shadow-lg: 3px 3px 0;
+ --shadow-color: rgba(0, 0, 0, 0.22);
+ --shadow-box: 2px 2px 0 rgba(0, 0, 0, 0.12);
+ --shadow-button: 1px 1px 0 rgba(0, 0, 0, 0.18);
+
+ --highlight: rgba(255, 247, 242, 0.9);
+ --lowlight: rgba(59, 46, 44, 0.55);
+
+ --content-max-width: 1000px;
}
[data-theme="dark"] {
- /* Espresso-night variant */
- --bg: #2d2523;
- --bg-pattern: #241e1c;
+ /* Espresso-night variant */
+ --bg: #2d2523;
+ --bg-pattern: #241e1c;
- --dither-ink: rgba(255, 247, 242, 0.12);
- --dither-opacity: 0.14;
- --surface: #3b312f;
- --surface-alt: #463a37;
+ --dither-ink: rgba(255, 247, 242, 0.12);
+ --dither-opacity: 0.14;
+ --surface: #3b312f;
+ --surface-alt: #463a37;
- --fg: #f9efea;
- --muted: #cbb8b1;
+ --fg: #f9efea;
+ --muted: #cbb8b1;
- --border: #f1e6e0;
- --border-light: #6e5b57;
- --border-dark: #120d0c;
+ --border: #f1e6e0;
+ --border-light: #6e5b57;
+ --border-dark: #120d0c;
- --primary: #f06aa6;
- --primary-light: #ff97c8;
- --primary-dark: #d94b8e;
+ --primary: #f06aa6;
+ --primary-light: #ff97c8;
+ --primary-dark: #d94b8e;
- --secondary: #b69cff;
- --secondary-light: #d7c8ff;
- --secondary-dark: #8f78d6;
+ --secondary: #b69cff;
+ --secondary-light: #d7c8ff;
+ --secondary-dark: #8f78d6;
- --accent-brown: #a57353;
- --accent-brown-light: #c9936a;
- --accent-brown-dark: #6b4a34;
+ --accent-brown: #a57353;
+ --accent-brown-light: #c9936a;
+ --accent-brown-dark: #6b4a34;
- --titlebar-a: #b04a80;
- --titlebar-b: #4f245e;
- --titlebar-fg: #fff7f2;
+ --titlebar-a: #b04a80;
+ --titlebar-b: #4f245e;
+ --titlebar-fg: #fff7f2;
- --link: #ff97c8;
- --link-visited: #d7c8ff;
- --link-active: #fff0f7;
+ --link: #ff97c8;
+ --link-visited: #d7c8ff;
+ --link-active: #fff0f7;
- /* Status colors (brightened for dark mode) */
- --success: #4caf50;
- --error: #ff6b6b;
- --warning: #ffa500;
- --info: #64b5f6;
+ /* Status colors (brightened for dark mode) */
+ --success: #4caf50;
+ --error: #ff6b6b;
+ --warning: #ffa500;
+ --info: #64b5f6;
- --shadow-color: rgba(0, 0, 0, 0.75);
- --highlight: rgba(255, 255, 255, 0.2);
- --lowlight: rgba(0, 0, 0, 0.85);
+ --shadow-color: rgba(0, 0, 0, 0.75);
+ --highlight: rgba(255, 255, 255, 0.2);
+ --lowlight: rgba(0, 0, 0, 0.85);
}
@font-face {
- font-family: "Mono";
- src: url("../assets/fonts/Maple.woff2") format("woff2");
- font-display: swap;
- font-weight: normal;
+ font-family: "Mono";
+ src: url("../assets/fonts/Maple.woff2") format("woff2");
+ font-display: swap;
+ font-weight: normal;
}
@supports (font-synthesis: none) {
- @font-face {
- font-family: "Mono";
- src: url("../assets/fonts/Maple.woff2") format("woff2");
- font-synthesis: none;
- }
+ @font-face {
+ font-family: "Mono";
+ src: url("../assets/fonts/Maple.woff2") format("woff2");
+ font-synthesis: none;
+ }
}
/* base */
* {
- box-sizing: border-box;
- margin: 0;
- padding: 0;
- cursor:
- url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 24" width="32" height="24"><path d="M1 3h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v2H9v1h1v2h1v2h-1v1H8v-1H7v-2H6v-2H5v1H4v1H3v1H1" fill="%23ff69b4" stroke="%23b19cd9" stroke-width="0.5"/><path d="M2 5h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1H8v2h1v2h1v2H8v-2H7v-2H6v-1H5v1H4v1H3v1H2" fill="white" stroke="%23ff69b4" stroke-width="0.5"/></svg>')
- 0 0,
- auto !important;
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+ cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 24" width="32" height="24"><path d="M1 3h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v2H9v1h1v2h1v2h-1v1H8v-1H7v-2H6v-2H5v1H4v1H3v1H1" fill="%23ff69b4" stroke="%23b19cd9" stroke-width="0.5"/><path d="M2 5h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1H8v2h1v2h1v2H8v-2H7v-2H6v-1H5v1H4v1H3v1H2" fill="white" stroke="%23ff69b4" stroke-width="0.5"/></svg>')
+ 0 0, auto !important;
}
::selection {
- background: var(--primary);
- color: var(--surface-alt);
+ background: var(--primary);
+ color: var(--surface-alt);
}
:focus-visible {
- outline: 1px dotted var(--fg);
- outline-offset: 2px;
+ outline: 1px dotted var(--fg);
+ outline-offset: 2px;
}
html {
- font-size: 16px;
- background-color: var(--bg);
- position: relative;
- line-height: 1;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- height: 100vh;
+ font-size: 16px;
+ background-color: var(--bg);
+ position: relative;
+ line-height: 1;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ height: 100vh;
}
html::before {
- content: "";
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background-image: linear-gradient(
- 45deg,
- var(--dither-ink) 25%,
- transparent 25%,
- transparent 75%,
- var(--dither-ink) 75%
- ),
- linear-gradient(
- 45deg,
- var(--dither-ink) 25%,
- transparent 25%,
- transparent 75%,
- var(--dither-ink) 75%
- ),
- url("../assets/img/bg.png");
- background-size:
- 10px 10px,
- 10px 10px,
- auto;
- background-position:
- 0 0,
- 5px 5px,
- 0 0;
- background-repeat: repeat;
- opacity: var(--dither-opacity);
- z-index: -1;
- pointer-events: none;
+ content: "";
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-image: linear-gradient(
+ 45deg,
+ var(--dither-ink) 25%,
+ transparent 25%,
+ transparent 75%,
+ var(--dither-ink) 75%
+ ),
+ linear-gradient(
+ 45deg,
+ var(--dither-ink) 25%,
+ transparent 25%,
+ transparent 75%,
+ var(--dither-ink) 75%
+ ), url("../assets/img/bg.png");
+ background-size: 10px 10px, 10px 10px, auto;
+ background-position: 0 0, 5px 5px, 0 0;
+ background-repeat: repeat;
+ opacity: var(--dither-opacity);
+ z-index: -1;
+ pointer-events: none;
}
body {
- background: transparent;
- color: var(--fg);
- line-height: var(--line-height);
- padding: var(--space-md);
- padding-top: calc(var(--space-md) + 3rem);
- max-width: var(--content-max-width);
- margin: 0 auto;
- font-family: var(--font-mono);
- scrollbar-color: var(--accent-lavender) var(--bg);
- scrollbar-width: thin;
- display: flex;
- flex-direction: column;
- height: 100vh;
+ background: transparent;
+ color: var(--fg);
+ line-height: var(--line-height);
+ padding: var(--space-md);
+ padding-top: calc(var(--space-md) + 3rem);
+ max-width: var(--content-max-width);
+ margin: 0 auto;
+ font-family: var(--font-mono);
+ scrollbar-color: var(--accent-lavender) var(--bg);
+ scrollbar-width: thin;
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
}
/* webkit scrollbar (chrome, safari, edge) */
::-webkit-scrollbar {
- width: 12px;
+ width: 12px;
}
::-webkit-scrollbar-track {
- background: var(--bg);
- border: 2px solid var(--border);
- box-shadow:
- inset 1px 1px 0 var(--border-light),
- inset -1px -1px 0 var(--border-dark);
+ background: var(--bg);
+ border: 2px solid var(--border);
+ box-shadow: inset 1px 1px 0 var(--border-light), inset -1px -1px 0 var(--border-dark);
}
::-webkit-scrollbar-thumb {
- background: var(--accent-lavender);
- border: 2px outset var(--border-light);
- box-shadow:
- inset 1px 1px 0 var(--highlight),
- inset -1px -1px 0 var(--lowlight);
+ background: var(--accent-lavender);
+ border: 2px outset var(--border-light);
+ box-shadow: inset 1px 1px 0 var(--highlight), inset -1px -1px 0 var(--lowlight);
}
::-webkit-scrollbar-thumb:hover {
- background: var(--accent-pink);
+ background: var(--accent-pink);
}
.div-centered {
- max-width: var(--content-max-width);
- margin: auto;
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- min-height: calc(100vh - 4.5rem);
- text-align: justify;
+ max-width: var(--content-max-width);
+ margin: auto;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ min-height: calc(100vh - 4.5rem);
+ text-align: justify;
}
/* typography */
@@ -276,169 +264,163 @@ h3,
h4,
h5,
h6 {
- font-family: var(--font-mono);
- font-weight: 700;
- line-height: 1.15;
- margin-bottom: var(--space-md);
- color: var(--fg);
+ font-family: var(--font-mono);
+ font-weight: 700;
+ line-height: 1.15;
+ margin-bottom: var(--space-md);
+ color: var(--fg);
}
h1 {
- font-size: 2rem;
- margin-bottom: var(--space-lg);
- background: linear-gradient(90deg, var(--titlebar-a), var(--titlebar-b));
+ font-size: 2rem;
+ margin-bottom: var(--space-lg);
+ background: linear-gradient(90deg, var(--titlebar-a), var(--titlebar-b));
}
/* "Titlebar" headings: retro, but still cute */
h1,
h2,
h3 {
- padding: 0.55rem 0.75rem;
- border: var(--border-width) solid var(--border);
- box-shadow:
- inset 1px 1px 0 var(--border-light),
- inset -1px -1px 0 var(--border-dark);
- color: var(--titlebar-fg);
- text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.25);
+ padding: 0.55rem 0.75rem;
+ border: var(--border-width) solid var(--border);
+ box-shadow: inset 1px 1px 0 var(--border-light), inset -1px -1px 0 var(--border-dark);
+ color: var(--titlebar-fg);
+ text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.25);
}
h2 {
- font-size: 1.5rem;
- background: linear-gradient(90deg, var(--titlebar-a), var(--titlebar-b));
+ font-size: 1.5rem;
+ background: linear-gradient(90deg, var(--titlebar-a), var(--titlebar-b));
}
h3 {
- font-size: 1.25rem;
- background: linear-gradient(90deg, var(--accent-brown-dark), var(--accent-brown));
+ font-size: 1.25rem;
+ background: linear-gradient(90deg, var(--accent-brown-dark), var(--accent-brown));
}
h4 {
- font-size: 1.1rem;
+ font-size: 1.1rem;
}
h5 {
- font-size: 1rem;
+ font-size: 1rem;
}
h6 {
- font-size: 0.9rem;
+ font-size: 0.9rem;
}
p {
- margin-bottom: var(--space-sm);
+ margin-bottom: var(--space-sm);
}
a {
- color: var(--link);
- text-decoration: underline;
- text-decoration-thickness: 2px;
- text-underline-offset: 2px;
+ color: var(--link);
+ text-decoration: underline;
+ text-decoration-thickness: 2px;
+ text-underline-offset: 2px;
}
a:visited {
- color: var(--link-visited);
+ color: var(--link-visited);
}
a:hover {
- background: var(--primary-light);
- color: var(--fg);
+ background: var(--primary-light);
+ color: var(--fg);
}
a:active {
- color: var(--link-active);
+ color: var(--link-active);
}
small {
- color: var(--muted);
- font-size: 0.875rem;
+ color: var(--muted);
+ font-size: 0.875rem;
}
blockquote {
- border-left: 3px solid var(--accent-brown);
- padding-left: var(--space-md);
- margin: var(--space-md) 0;
- font-style: italic;
- color: var(--muted);
+ border-left: 3px solid var(--accent-brown);
+ padding-left: var(--space-md);
+ margin: var(--space-md) 0;
+ font-style: italic;
+ color: var(--muted);
}
/* lists */
ul,
ol {
- margin: var(--space-sm) 0;
- padding-left: var(--space-lg);
+ margin: var(--space-sm) 0;
+ padding-left: var(--space-lg);
}
li {
- margin-bottom: var(--space-xs);
+ margin-bottom: var(--space-xs);
}
/* tables */
table {
- width: 100%;
- border-collapse: collapse;
- margin: var(--space-md) 0;
- border: var(--border-width) solid var(--border);
- background: var(--surface);
- box-shadow:
- inset 1px 1px 0 var(--border-light),
- inset -1px -1px 0 var(--border-dark),
- 3px 3px 0 rgba(0, 0, 0, 0.12);
+ width: 100%;
+ border-collapse: collapse;
+ margin: var(--space-md) 0;
+ border: var(--border-width) solid var(--border);
+ background: var(--surface);
+ box-shadow: inset 1px 1px 0 var(--border-light), inset -1px -1px 0 var(--border-dark), 3px 3px 0
+ rgba(0, 0, 0, 0.12);
}
thead {
- background: linear-gradient(90deg, var(--titlebar-a), var(--titlebar-b));
- color: var(--titlebar-fg);
- font-weight: 700;
+ background: linear-gradient(90deg, var(--titlebar-a), var(--titlebar-b));
+ color: var(--titlebar-fg);
+ font-weight: 700;
}
th,
td {
- padding: var(--space-xs) var(--space-sm);
- border: 1px solid var(--border-dark);
- text-align: left;
+ padding: var(--space-xs) var(--space-sm);
+ border: 1px solid var(--border-dark);
+ text-align: left;
}
tbody tr:nth-child(odd) {
- background: var(--surface-alt);
+ background: var(--surface-alt);
}
tbody tr:nth-child(even) {
- background: var(--surface);
+ background: var(--surface);
}
tbody tr:hover {
- background: color-mix(in srgb, var(--primary) 12%, transparent);
+ background: color-mix(in srgb, var(--primary) 12%, transparent);
}
/* code */
code {
- background: var(--surface-alt);
- padding: 0.25rem 0.5rem;
- border: 1px solid var(--border-dark);
- font-size: 0.875rem;
- font-family: var(--font-mono);
+ background: var(--surface-alt);
+ padding: 0.25rem 0.5rem;
+ border: 1px solid var(--border-dark);
+ font-size: 0.875rem;
+ font-family: var(--font-mono);
}
pre {
- background: var(--surface-alt);
- border: var(--border-width) solid var(--border);
- padding: var(--space-md);
- margin: var(--space-md) 0;
- overflow-x: auto;
- box-shadow:
- inset 1px 1px 0 var(--border-light),
- inset -1px -1px 0 var(--border-dark),
- 3px 3px 0 rgba(0, 0, 0, 0.12);
+ background: var(--surface-alt);
+ border: var(--border-width) solid var(--border);
+ padding: var(--space-md);
+ margin: var(--space-md) 0;
+ overflow-x: auto;
+ box-shadow: inset 1px 1px 0 var(--border-light), inset -1px -1px 0 var(--border-dark), 3px 3px 0
+ rgba(0, 0, 0, 0.12);
}
pre code {
- background: transparent;
- border: none;
- padding: 0;
- font-size: 0.875rem;
+ background: transparent;
+ border: none;
+ padding: 0;
+ font-size: 0.875rem;
}
/* Prism.js syntax highlighting */
@@ -447,12 +429,12 @@ pre code {
.token.prolog,
.token.doctype,
.token.cdata {
- color: var(--muted);
- font-style: italic;
+ color: var(--muted);
+ font-style: italic;
}
.token.punctuation {
- color: var(--fg);
+ color: var(--fg);
}
.token.property,
@@ -461,7 +443,7 @@ pre code {
.token.number,
.token.constant,
.token.symbol {
- color: var(--secondary-dark);
+ color: var(--secondary-dark);
}
.token.selector,
@@ -469,7 +451,7 @@ pre code {
.token.string,
.token.char,
.token.builtin {
- color: var(--accent-brown-dark);
+ color: var(--accent-brown-dark);
}
.token.operator,
@@ -477,183 +459,179 @@ pre code {
.token.url,
.language-css .token.string,
.style .token.string {
- color: var(--primary-dark);
+ color: var(--primary-dark);
}
.token.atrule,
.token.attr-value,
.token.keyword {
- color: var(--primary-dark);
- font-weight: 700;
+ color: var(--primary-dark);
+ font-weight: 700;
}
.token.function,
.token.class-name {
- color: var(--primary);
- font-weight: 700;
+ color: var(--primary);
+ font-weight: 700;
}
.token.regex,
.token.important,
.token.variable {
- color: var(--accent-brown);
+ color: var(--accent-brown);
}
.token.important,
.token.bold {
- font-weight: bold;
+ font-weight: bold;
}
.token.italic {
- font-style: italic;
+ font-style: italic;
}
/* Dark theme variants */
[data-theme="dark"] .token.keyword,
[data-theme="dark"] .token.operator {
- color: var(--primary-light);
+ color: var(--primary-light);
}
[data-theme="dark"] .token.string,
[data-theme="dark"] .token.selector {
- color: var(--accent-brown-light);
+ color: var(--accent-brown-light);
}
[data-theme="dark"] .token.number,
[data-theme="dark"] .token.constant {
- color: var(--secondary-light);
+ color: var(--secondary-light);
}
/* dividers */
hr {
- border: none;
- border-top: 2px solid var(--border-dark);
- margin: var(--space-lg) 0;
- height: 0;
+ border: none;
+ border-top: 2px solid var(--border-dark);
+ margin: var(--space-lg) 0;
+ height: 0;
}
/* layout */
header {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- z-index: 1000;
- border-bottom: 2px solid var(--border);
- padding: 0.45rem var(--space-md);
- margin-bottom: 0;
- background: linear-gradient(90deg, var(--titlebar-a), var(--titlebar-b));
- color: var(--titlebar-fg);
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: var(--space-md);
- box-shadow:
- inset 1px 1px 0 var(--border-light),
- inset -1px -1px 0 var(--border-dark),
- 0 2px 0 rgba(0, 0, 0, 0.08);
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: 1000;
+ border-bottom: 2px solid var(--border);
+ padding: 0.45rem var(--space-md);
+ margin-bottom: 0;
+ background: linear-gradient(90deg, var(--titlebar-a), var(--titlebar-b));
+ color: var(--titlebar-fg);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--space-md);
+ box-shadow: inset 1px 1px 0 var(--border-light), inset -1px -1px 0 var(--border-dark), 0 2px 0
+ rgba(0, 0, 0, 0.08);
}
header h1 {
- display: none;
+ display: none;
}
header::before {
- content: "";
- display: inline-block;
- width: 1.5rem;
- height: 1.5rem;
- background-image: url("../assets/img/coffee.svg");
- background-size: 1.1rem 1.1rem;
- background-position: center;
- background-repeat: no-repeat;
- margin-right: 0.5rem;
- background-color: var(--surface);
- border: 2px outset var(--border-light);
- box-shadow:
- inset 1px 1px 0 var(--highlight),
- inset -1px -1px 0 var(--lowlight);
+ content: "";
+ display: inline-block;
+ width: 1.5rem;
+ height: 1.5rem;
+ background-image: url("../assets/img/coffee.svg");
+ background-size: 1.1rem 1.1rem;
+ background-position: center;
+ background-repeat: no-repeat;
+ margin-right: 0.5rem;
+ background-color: var(--surface);
+ border: 2px outset var(--border-light);
+ box-shadow: inset 1px 1px 0 var(--highlight), inset -1px -1px 0 var(--lowlight);
}
nav {
- display: flex;
- gap: var(--space-md);
- flex-wrap: wrap;
- align-items: center;
- margin: 0;
- flex: 1;
+ display: flex;
+ gap: var(--space-md);
+ flex-wrap: wrap;
+ align-items: center;
+ margin: 0;
+ flex: 1;
}
nav a {
- font-size: 0.85rem;
- padding: 0.1rem 0.15rem;
- color: var(--titlebar-fg);
- text-decoration: none;
- border-bottom: 2px solid color-mix(in srgb, var(--titlebar-fg) 55%, transparent);
+ font-size: 0.85rem;
+ padding: 0.1rem 0.15rem;
+ color: var(--titlebar-fg);
+ text-decoration: none;
+ border-bottom: 2px solid color-mix(in srgb, var(--titlebar-fg) 55%, transparent);
}
nav a:visited {
- color: var(--titlebar-fg);
+ color: var(--titlebar-fg);
}
nav a:hover {
- background: color-mix(in srgb, var(--surface-alt) 18%, transparent);
- border-bottom-color: var(--primary-light);
+ background: color-mix(in srgb, var(--surface-alt) 18%, transparent);
+ border-bottom-color: var(--primary-light);
}
nav a:active {
- background: color-mix(in srgb, var(--surface-alt) 28%, transparent);
- border-bottom-color: var(--primary);
+ background: color-mix(in srgb, var(--surface-alt) 28%, transparent);
+ border-bottom-color: var(--primary);
}
main {
- flex: 1;
+ flex: 1;
}
article {
- border: var(--border-width) solid var(--border);
- padding: var(--space-md);
- margin-bottom: var(--space-lg);
- background: var(--surface);
- box-shadow:
- inset 1px 1px 0 var(--border-light),
- inset -1px -1px 0 var(--border-dark),
- 4px 4px 0 rgba(0, 0, 0, 0.14),
- 5px 5px 0 rgba(0, 0, 0, 0.06);
+ border: var(--border-width) solid var(--border);
+ padding: var(--space-md);
+ margin-bottom: var(--space-lg);
+ background: var(--surface);
+ box-shadow:
+ inset 1px 1px 0 var(--border-light),
+ inset -1px -1px 0 var(--border-dark),
+ 4px 4px 0 rgba(0, 0, 0, 0.14),
+ 5px 5px 0 rgba(0, 0, 0, 0.06);
}
article > :last-child {
- margin-bottom: 0;
+ margin-bottom: 0;
}
article h2 {
- margin: calc(var(--space-md) * -1) calc(var(--space-md) * -1) var(--space-md);
- border-left: none;
- border-right: none;
- border-top: none;
- border-bottom: var(--border-width) solid var(--border);
- box-shadow: none;
- text-shadow: none;
+ margin: calc(var(--space-md) * -1) calc(var(--space-md) * -1) var(--space-md);
+ border-left: none;
+ border-right: none;
+ border-top: none;
+ border-bottom: var(--border-width) solid var(--border);
+ box-shadow: none;
+ text-shadow: none;
}
footer {
- border: var(--border-width) solid var(--border);
- padding: var(--space-md);
- text-align: center;
- background: var(--surface);
- font-size: 0.875rem;
- box-shadow:
- inset 1px 1px 0 var(--border-light),
- inset -1px -1px 0 var(--border-dark),
- 4px 4px 0 rgba(0, 0, 0, 0.14),
- 5px 5px 0 rgba(0, 0, 0, 0.06);
+ border: var(--border-width) solid var(--border);
+ padding: var(--space-md);
+ text-align: center;
+ background: var(--surface);
+ font-size: 0.875rem;
+ box-shadow:
+ inset 1px 1px 0 var(--border-light),
+ inset -1px -1px 0 var(--border-dark),
+ 4px 4px 0 rgba(0, 0, 0, 0.14),
+ 5px 5px 0 rgba(0, 0, 0, 0.06);
}
footer > :last-child {
- margin-bottom: 0;
+ margin-bottom: 0;
}
/* components */
@@ -663,615 +641,608 @@ footer > :last-child {
input,
textarea,
select {
- font-family: var(--font-mono);
- padding: var(--space-xs) var(--space-sm);
- border: 2px inset var(--border-light);
- background: var(--surface-alt);
- color: var(--fg);
- font-size: 0.875rem;
- box-shadow:
- inset 1px 1px 0 var(--lowlight),
- inset -1px -1px 0 var(--highlight);
- transition: border-color 0.15s, box-shadow 0.15s;
- width: 100%;
- max-width: 100%;
+ font-family: var(--font-mono);
+ padding: var(--space-xs) var(--space-sm);
+ border: 2px inset var(--border-light);
+ background: var(--surface-alt);
+ color: var(--fg);
+ font-size: 0.875rem;
+ box-shadow: inset 1px 1px 0 var(--lowlight), inset -1px -1px 0 var(--highlight);
+ transition: border-color 0.15s, box-shadow 0.15s;
+ width: 100%;
+ max-width: 100%;
}
input::placeholder,
textarea::placeholder,
select::placeholder {
- color: var(--muted);
- opacity: 0.8;
+ color: var(--muted);
+ opacity: 0.8;
}
[data-theme="dark"] input::placeholder,
[data-theme="dark"] textarea::placeholder,
[data-theme="dark"] select::placeholder {
- color: #b8b0a0;
- opacity: 1;
+ color: #b8b0a0;
+ opacity: 1;
}
input:focus,
textarea:focus,
select:focus {
- outline: 1px dotted var(--fg);
- outline-offset: 2px;
- border-color: var(--primary);
- box-shadow:
- inset 1px 1px 0 var(--lowlight),
- inset -1px -1px 0 var(--highlight),
- 0 0 0 2px var(--primary);
+ outline: 1px dotted var(--fg);
+ outline-offset: 2px;
+ border-color: var(--primary);
+ box-shadow: inset 1px 1px 0 var(--lowlight), inset -1px -1px 0 var(--highlight), 0 0 0 2px
+ var(--primary);
}
input:invalid,
textarea:invalid {
- border-color: var(--error);
+ border-color: var(--error);
}
input:invalid:focus,
textarea:invalid:focus {
- box-shadow:
- inset 1px 1px 0 var(--highlight),
- inset -1px -1px 0 var(--lowlight),
- 0 0 0 2px var(--error);
+ box-shadow: inset 1px 1px 0 var(--highlight), inset -1px -1px 0 var(--lowlight), 0 0 0 2px
+ var(--error);
}
textarea {
- resize: vertical;
- min-height: 4rem;
- width: 100%;
+ resize: vertical;
+ min-height: 4rem;
+ width: 100%;
}
select {
- cursor: pointer;
- appearance: none;
- background-image: url("data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%232a1f1d' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
- background-repeat: no-repeat;
- background-position: right 0.5rem center;
- background-size: 1.2em;
- padding-right: 2.5rem;
+ cursor: pointer;
+ appearance: none;
+ background-image: url("data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%232a1f1d' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: right 0.5rem center;
+ background-size: 1.2em;
+ padding-right: 2.5rem;
}
[data-theme="dark"] select {
- background-image: url("data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23f9efea' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
+ background-image: url("data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23f9efea' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
}
option {
- background: var(--bg);
- color: var(--fg);
- padding: var(--space-sm);
+ background: var(--bg);
+ color: var(--fg);
+ padding: var(--space-sm);
}
option:checked {
- background: linear-gradient(var(--primary), var(--primary));
- color: var(--bg);
+ background: linear-gradient(var(--primary), var(--primary));
+ color: var(--bg);
}
input[type="checkbox"],
input[type="radio"] {
- appearance: none;
- width: 1.3rem;
- height: 1.3rem;
- min-width: 1.3rem;
- min-height: 1.3rem;
- padding: 0;
- cursor: pointer;
- margin: 0 0.5rem 0 0;
- vertical-align: middle;
- border: 2px inset var(--border-light);
- box-shadow:
- inset 1px 1px 0 var(--lowlight),
- inset -1px -1px 0 var(--highlight);
- background: var(--surface-alt);
- display: inline-flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- font-weight: normal;
+ appearance: none;
+ width: 1.3rem;
+ height: 1.3rem;
+ min-width: 1.3rem;
+ min-height: 1.3rem;
+ padding: 0;
+ cursor: pointer;
+ margin: 0 0.5rem 0 0;
+ vertical-align: middle;
+ border: 2px inset var(--border-light);
+ box-shadow: inset 1px 1px 0 var(--lowlight), inset -1px -1px 0 var(--highlight);
+ background: var(--surface-alt);
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ font-weight: normal;
}
input[type="radio"] {
- border-radius: 50%;
+ border-radius: 50%;
}
input[type="checkbox"]:checked,
input[type="radio"]:checked {
- background: var(--primary);
- border-style: inset;
+ background: var(--primary);
+ border-style: inset;
}
label {
- display: block;
- margin-bottom: var(--space-xs);
- font-weight: bold;
- color: var(--fg);
+ display: block;
+ margin-bottom: var(--space-xs);
+ font-weight: bold;
+ color: var(--fg);
}
input[type="checkbox"] + label,
input[type="radio"] + label {
- display: inline;
- margin: 0;
- font-weight: normal;
+ display: inline;
+ margin: 0;
+ font-weight: normal;
}
/* toggle switches */
input[type="checkbox"].toggle {
- appearance: none;
- width: 2.8rem;
- height: 1.6rem;
- padding: 0;
- margin: 0 0.5rem 0 0;
- border: 2px outset var(--border-light);
- background: var(--muted);
- cursor: pointer;
- position: relative;
- vertical-align: middle;
- box-shadow:
- inset 1px 1px 0 rgba(255, 255, 255, 0.3),
- inset -1px -1px 0 rgba(0, 0, 0, 0.6);
- transition: background 0.2s;
+ appearance: none;
+ width: 2.8rem;
+ height: 1.6rem;
+ padding: 0;
+ margin: 0 0.5rem 0 0;
+ border: 2px outset var(--border-light);
+ background: var(--muted);
+ cursor: pointer;
+ position: relative;
+ vertical-align: middle;
+ box-shadow: inset 1px 1px 0 rgba(255, 255, 255, 0.3), inset -1px -1px 0 rgba(0, 0, 0, 0.6);
+ transition: background 0.2s;
}
input[type="checkbox"].toggle:checked {
- background: var(--primary);
+ background: var(--primary);
}
input[type="checkbox"].toggle::after {
- content: "";
- position: absolute;
- width: 0.6rem;
- height: 0.6rem;
- background: var(--surface);
- border: 2px outset var(--border-light);
- top: 50%;
- transform: translateY(-50%);
- left: 0.2rem;
- transition: left 0.2s;
- box-shadow:
- inset 1px 1px 0 var(--highlight),
- inset -1px -1px 0 var(--lowlight);
+ content: "";
+ position: absolute;
+ width: 0.6rem;
+ height: 0.6rem;
+ background: var(--surface);
+ border: 2px outset var(--border-light);
+ top: 50%;
+ transform: translateY(-50%);
+ left: 0.2rem;
+ transition: left 0.2s;
+ box-shadow: inset 1px 1px 0 var(--highlight), inset -1px -1px 0 var(--lowlight);
}
input[type="checkbox"].toggle:checked::after {
- left: 1.5rem;
+ left: 1.5rem;
}
[data-theme="dark"] input[type="checkbox"].toggle {
- background: var(--secondary);
+ background: var(--secondary);
}
[data-theme="dark"] input[type="checkbox"].toggle:checked {
- background: var(--primary);
+ background: var(--primary);
}
[data-theme="dark"] input[type="checkbox"].toggle::after {
- background: var(--accent-brown-light);
+ background: var(--accent-brown-light);
}
#theme-toggle::before {
- content: "☾";
- position: absolute;
- right: 0.4rem;
- top: 50%;
- transform: translateY(-50%);
- font-size: 0.9rem;
- line-height: 1;
- color: var(--titlebar-fg);
- transition: opacity 0.2s;
+ content: "☾";
+ position: absolute;
+ right: 0.4rem;
+ top: 50%;
+ transform: translateY(-50%);
+ font-size: 0.9rem;
+ line-height: 1;
+ color: var(--titlebar-fg);
+ transition: opacity 0.2s;
}
#theme-toggle:checked::before {
- content: "☀";
- right: auto;
- left: 0.4rem;
+ content: "☀";
+ right: auto;
+ left: 0.4rem;
}
/* range sliders */
input[type="range"] {
- width: 100%;
- height: 2.2rem;
- background: transparent;
- cursor: pointer;
- appearance: none;
- vertical-align: middle;
+ width: 100%;
+ height: 2.2rem;
+ background: transparent;
+ cursor: pointer;
+ appearance: none;
+ vertical-align: middle;
}
input[type="range"]::-webkit-slider-thumb {
- appearance: none;
- width: 1.5rem;
- height: 1rem;
- margin-top: -0.25rem;
- background: linear-gradient(135deg, var(--primary-light) 0%, var(--primary) 50%, var(--primary-dark) 100%);
- border: 2px outset var(--border-light);
- cursor: pointer;
- box-shadow:
- inset 1px 1px 0 var(--highlight),
- inset -1px -1px 0 var(--lowlight),
- 2px 2px 0 rgba(0, 0, 0, 0.3),
- 3px 3px 0 rgba(0, 0, 0, 0.1);
+ appearance: none;
+ width: 1.5rem;
+ height: 1rem;
+ margin-top: -0.25rem;
+ background: linear-gradient(
+ 135deg,
+ var(--primary-light) 0%,
+ var(--primary) 50%,
+ var(--primary-dark) 100%
+ );
+ border: 2px outset var(--border-light);
+ cursor: pointer;
+ box-shadow:
+ inset 1px 1px 0 var(--highlight),
+ inset -1px -1px 0 var(--lowlight),
+ 2px 2px 0 rgba(0, 0, 0, 0.3),
+ 3px 3px 0 rgba(0, 0, 0, 0.1);
}
input[type="range"]::-webkit-slider-thumb:active {
- box-shadow:
- inset 1px 1px 0 var(--lowlight),
- inset -1px -1px 0 var(--highlight),
- 1px 1px 0 rgba(0, 0, 0, 0.2);
+ box-shadow: inset 1px 1px 0 var(--lowlight), inset -1px -1px 0 var(--highlight), 1px 1px 0
+ rgba(0, 0, 0, 0.2);
}
input[type="range"]::-moz-range-thumb {
- width: 1.5rem;
- height: 0.8rem;
- margin-top: -0.15rem;
- background: linear-gradient(135deg, var(--primary-light) 0%, var(--primary) 50%, var(--primary-dark) 100%);
- border: 2px outset var(--border-light);
- cursor: pointer;
- box-shadow:
- inset 1px 1px 0 var(--highlight),
- inset -1px -1px 0 var(--lowlight),
- 2px 2px 0 rgba(0, 0, 0, 0.3),
- 3px 3px 0 rgba(0, 0, 0, 0.1);
- border-radius: 0.3rem;
+ width: 1.5rem;
+ height: 0.8rem;
+ margin-top: -0.15rem;
+ background: linear-gradient(
+ 135deg,
+ var(--primary-light) 0%,
+ var(--primary) 50%,
+ var(--primary-dark) 100%
+ );
+ border: 2px outset var(--border-light);
+ cursor: pointer;
+ box-shadow:
+ inset 1px 1px 0 var(--highlight),
+ inset -1px -1px 0 var(--lowlight),
+ 2px 2px 0 rgba(0, 0, 0, 0.3),
+ 3px 3px 0 rgba(0, 0, 0, 0.1);
+ border-radius: 0.3rem;
}
input[type="range"]::-moz-range-thumb:active {
- box-shadow:
- inset 1px 1px 0 var(--lowlight),
- inset -1px -1px 0 var(--highlight),
- 1px 1px 0 rgba(0, 0, 0, 0.2);
+ box-shadow: inset 1px 1px 0 var(--lowlight), inset -1px -1px 0 var(--highlight), 1px 1px 0
+ rgba(0, 0, 0, 0.2);
}
input[type="range"]::-webkit-slider-runnable-track {
- background: linear-gradient(180deg,
- #c0c0c0 0%,
- #e8e8e8 1px,
- #dfdfdf 2px,
- #d0d0d0 100%);
- border: 2px inset var(--border-light);
- height: 0.8rem;
- box-shadow:
- inset 2px 2px 0 rgba(255, 255, 255, 0.8),
- inset -2px -2px 0 rgba(0, 0, 0, 0.4),
- inset 1px 1px 0 rgba(0, 0, 0, 0.1);
+ background: linear-gradient(180deg, #c0c0c0 0%, #e8e8e8 1px, #dfdfdf 2px, #d0d0d0 100%);
+ border: 2px inset var(--border-light);
+ height: 0.8rem;
+ box-shadow: inset 2px 2px 0 rgba(255, 255, 255, 0.8), inset -2px -2px 0 rgba(0, 0, 0, 0.4),
+ inset 1px 1px 0 rgba(0, 0, 0, 0.1);
}
input[type="range"]::-moz-range-track {
- background: transparent;
- border: none;
+ background: transparent;
+ border: none;
}
input[type="range"]::-moz-range-progress {
- background: linear-gradient(180deg,
- var(--secondary-light) 0%,
- var(--secondary) 50%,
- var(--secondary-dark) 100%);
- height: 0.8rem;
- border: 2px inset var(--border-light);
- box-shadow:
- inset 2px 2px 0 rgba(255, 255, 255, 0.6),
- inset -2px -2px 0 rgba(0, 0, 0, 0.3);
+ background: linear-gradient(
+ 180deg,
+ var(--secondary-light) 0%,
+ var(--secondary) 50%,
+ var(--secondary-dark) 100%
+ );
+ height: 0.8rem;
+ border: 2px inset var(--border-light);
+ box-shadow: inset 2px 2px 0 rgba(255, 255, 255, 0.6), inset -2px -2px 0 rgba(0, 0, 0, 0.3);
}
fieldset {
- border: 2px solid var(--border-light);
- padding: var(--space-md);
- margin: var(--space-md) 0;
- background: var(--surface);
- box-shadow:
- inset 1px 1px 0 rgba(255, 255, 255, 0.3),
- inset -1px -1px 0 rgba(0, 0, 0, 0.6);
+ border: 2px solid var(--border-light);
+ padding: var(--space-md);
+ margin: var(--space-md) 0;
+ background: var(--surface);
+ box-shadow: inset 1px 1px 0 rgba(255, 255, 255, 0.3), inset -1px -1px 0 rgba(0, 0, 0, 0.6);
}
fieldset > :last-child {
- margin-bottom: 0;
+ margin-bottom: 0;
}
legend {
- padding: 0 var(--space-xs);
- margin-left: calc(var(--space-xs) * -1);
- font-weight: bold;
- color: var(--fg);
- background: var(--surface);
+ padding: 0 var(--space-xs);
+ margin-left: calc(var(--space-xs) * -1);
+ font-weight: bold;
+ color: var(--fg);
+ background: var(--surface);
}
/* buttons */
button {
- font-family: var(--font-mono);
- font-weight: 700;
- padding: var(--space-xs) var(--space-sm);
- border: 2px outset var(--border-light);
- background: var(--surface);
- color: var(--fg);
- font-size: 0.875rem;
- box-shadow:
- inset 1px 1px 0 var(--highlight),
- inset -1px -1px 0 var(--lowlight),
- 2px 2px 0 rgba(0, 0, 0, 0.18);
- transition:
- transform 0.05s,
- box-shadow 0.05s,
- background 0.1s;
- cursor: pointer;
+ font-family: var(--font-mono);
+ font-weight: 700;
+ padding: var(--space-xs) var(--space-sm);
+ border: 2px outset var(--border-light);
+ background: var(--surface);
+ color: var(--fg);
+ font-size: 0.875rem;
+ box-shadow: inset 1px 1px 0 var(--highlight), inset -1px -1px 0 var(--lowlight), 2px 2px 0
+ rgba(0, 0, 0, 0.18);
+ transition: transform 0.05s, box-shadow 0.05s, background 0.1s;
+ cursor: pointer;
}
button:hover {
- background: var(--surface-alt);
+ background: var(--surface-alt);
}
button:active {
- transform: translate(1px, 1px);
- border-style: inset;
- box-shadow:
- inset 1px 1px 0 var(--lowlight),
- inset -1px -1px 0 var(--highlight);
+ transform: translate(1px, 1px);
+ border-style: inset;
+ box-shadow: inset 1px 1px 0 var(--lowlight), inset -1px -1px 0 var(--highlight);
}
/* button variants */
button.primary {
- background: var(--primary);
- color: var(--surface-alt);
+ background: var(--primary);
+ color: var(--surface-alt);
}
button.primary:hover {
- background: var(--primary-light);
+ background: var(--primary-light);
}
button.primary:active {
- background: var(--primary-dark);
+ background: var(--primary-dark);
}
button.secondary {
- background: var(--secondary);
- color: var(--fg);
+ background: var(--secondary);
+ color: var(--fg);
}
button.secondary:hover {
- background: var(--secondary-light);
+ background: var(--secondary-light);
}
button.secondary:active {
- background: var(--secondary-dark);
+ background: var(--secondary-dark);
}
button.success,
button.error,
button.warning,
button.info {
- color: var(--surface-alt);
+ color: var(--surface-alt);
}
button.success {
- background: var(--success);
+ background: var(--success);
}
button.error {
- background: var(--error);
+ background: var(--error);
}
button.warning {
- background: var(--warning);
+ background: var(--warning);
}
button.info {
- background: var(--info);
+ background: var(--info);
}
button.success:hover,
button.error:hover,
button.warning:hover,
button.info:hover {
- filter: brightness(1.06);
+ filter: brightness(1.06);
}
button.contrast {
- background: var(--fg);
- color: var(--surface-alt);
+ background: var(--fg);
+ color: var(--surface-alt);
}
button.contrast:hover {
- background: var(--muted);
+ background: var(--muted);
}
button:disabled {
- opacity: 0.6;
- cursor: not-allowed;
- transform: none !important;
+ opacity: 0.6;
+ cursor: not-allowed;
+ transform: none !important;
}
/* button groups */
.button-group {
- display: inline-flex;
- border: var(--border-width) solid var(--border);
- box-shadow:
- inset 1px 1px 0 var(--border-light),
- inset -1px -1px 0 var(--border-dark),
- 3px 3px 0 rgba(0, 0, 0, 0.12);
+ display: inline-flex;
+ border: var(--border-width) solid var(--border);
+ box-shadow: inset 1px 1px 0 var(--border-light), inset -1px -1px 0 var(--border-dark), 3px 3px 0
+ rgba(0, 0, 0, 0.12);
}
.button-group button {
- border: none;
- margin: 0;
- border-radius: 0;
- box-shadow: none;
+ border: none;
+ margin: 0;
+ border-radius: 0;
+ box-shadow: none;
}
.button-group button:not(:last-child) {
- border-right: 1px solid var(--border-dark);
+ border-right: 1px solid var(--border-dark);
}
.button-group button:hover {
- box-shadow: none;
- transform: none;
+ box-shadow: none;
+ transform: none;
}
.button-group button:active {
- box-shadow: none;
- transform: none;
+ box-shadow: none;
+ transform: none;
}
/* alerts & messages */
.alert {
- padding: var(--space-md);
- margin: var(--space-md) 0;
- border: var(--border-width) solid var(--border);
- border-left: 4px solid var(--info);
- background: var(--surface);
- box-shadow:
- inset 1px 1px 0 var(--border-light),
- inset -1px -1px 0 var(--border-dark),
- 3px 3px 0 rgba(0, 0, 0, 0.12);
+ padding: var(--space-md);
+ margin: var(--space-md) 0;
+ border: var(--border-width) solid var(--border);
+ border-left: 4px solid var(--info);
+ background: var(--surface);
+ box-shadow: inset 1px 1px 0 var(--border-light), inset -1px -1px 0 var(--border-dark), 3px 3px 0
+ rgba(0, 0, 0, 0.12);
}
.alert.success {
- border-left-color: var(--success);
- background-color: color-mix(in srgb, var(--success) 10%, transparent);
+ border-left-color: var(--success);
+ background-color: color-mix(in srgb, var(--success) 10%, transparent);
}
.alert.error {
- border-left-color: var(--error);
- background-color: color-mix(in srgb, var(--error) 10%, transparent);
+ border-left-color: var(--error);
+ background-color: color-mix(in srgb, var(--error) 10%, transparent);
}
.alert.warning {
- border-left-color: var(--warning);
- background-color: color-mix(in srgb, var(--warning) 10%, transparent);
+ border-left-color: var(--warning);
+ background-color: color-mix(in srgb, var(--warning) 10%, transparent);
}
.alert.info {
- border-left-color: var(--info);
- background-color: color-mix(in srgb, var(--info) 10%, transparent);
+ border-left-color: var(--info);
+ background-color: color-mix(in srgb, var(--info) 10%, transparent);
}
/* badges & pills */
.badge {
- display: inline-block;
- padding: 0.25rem 0.5rem;
- margin: 0 0.25rem;
- background: var(--surface-alt);
- color: var(--fg);
- font-size: 0.75rem;
- font-weight: 700;
- border: 1px solid var(--border-dark);
- box-shadow:
- inset 1px 1px 0 var(--border-light),
- inset -1px -1px 0 var(--border-dark);
+ display: inline-block;
+ padding: 0.25rem 0.5rem;
+ margin: 0 0.25rem;
+ background: var(--surface-alt);
+ color: var(--fg);
+ font-size: 0.75rem;
+ font-weight: 700;
+ border: 1px solid var(--border-dark);
+ box-shadow: inset 1px 1px 0 var(--border-light), inset -1px -1px 0 var(--border-dark);
}
.badge.primary {
- background: var(--primary);
- color: var(--surface-alt);
+ background: var(--primary);
+ color: var(--surface-alt);
}
.badge.success {
- background: var(--success);
- color: var(--surface-alt);
+ background: var(--success);
+ color: var(--surface-alt);
}
.badge.error {
- background: var(--error);
- color: var(--surface-alt);
+ background: var(--error);
+ color: var(--surface-alt);
}
.badge.warning {
- background: var(--warning);
- color: var(--surface-alt);
+ background: var(--warning);
+ color: var(--surface-alt);
}
.badge.info {
- background: var(--info);
- color: var(--surface-alt);
+ background: var(--info);
+ color: var(--surface-alt);
}
.badge.contrast {
- background: var(--fg);
- color: var(--surface-alt);
+ background: var(--fg);
+ color: var(--surface-alt);
}
/* helpers */
.text-success {
- color: var(--success);
- font-weight: bold;
+ color: var(--success);
+ font-weight: bold;
}
.text-error {
- color: var(--error);
- font-weight: bold;
+ color: var(--error);
+ font-weight: bold;
}
.text-warning {
- color: var(--warning);
- font-weight: bold;
+ color: var(--warning);
+ font-weight: bold;
}
.text-info {
- color: var(--info);
- font-weight: bold;
+ color: var(--info);
+ font-weight: bold;
}
.text-muted {
- color: var(--muted);
+ color: var(--muted);
}
.text-contrast {
- color: var(--fg);
- font-weight: bold;
+ color: var(--fg);
+ font-weight: bold;
}
.text-center {
- text-align: center;
+ text-align: center;
}
.text-right {
- text-align: right;
+ text-align: right;
}
.text-left {
- text-align: left;
+ text-align: left;
}
-.mt-xs { margin-top: var(--space-xs); }
-.mt-sm { margin-top: var(--space-sm); }
-.mt-md { margin-top: var(--space-md); }
-.mt-lg { margin-top: var(--space-lg); }
-.mt-xl { margin-top: var(--space-xl); }
+.mt-xs {
+ margin-top: var(--space-xs);
+}
+.mt-sm {
+ margin-top: var(--space-sm);
+}
+.mt-md {
+ margin-top: var(--space-md);
+}
+.mt-lg {
+ margin-top: var(--space-lg);
+}
+.mt-xl {
+ margin-top: var(--space-xl);
+}
-.mb-xs { margin-bottom: var(--space-xs); }
-.mb-sm { margin-bottom: var(--space-sm); }
-.mb-md { margin-bottom: var(--space-md); }
-.mb-lg { margin-bottom: var(--space-lg); }
-.mb-xl { margin-bottom: var(--space-xl); }
+.mb-xs {
+ margin-bottom: var(--space-xs);
+}
+.mb-sm {
+ margin-bottom: var(--space-sm);
+}
+.mb-md {
+ margin-bottom: var(--space-md);
+}
+.mb-lg {
+ margin-bottom: var(--space-lg);
+}
+.mb-xl {
+ margin-bottom: var(--space-xl);
+}
.mx-auto {
- margin-left: auto;
- margin-right: auto;
+ margin-left: auto;
+ margin-right: auto;
}
.py-md {
- padding-top: var(--space-md);
- padding-bottom: var(--space-md);
+ padding-top: var(--space-md);
+ padding-bottom: var(--space-md);
}
#theme-toggle {
- margin-left: auto;
- margin-bottom: 0;
+ margin-left: auto;
+ margin-bottom: 0;
}
/* fairy dust */
@keyframes fairy-float {
- 0% {
- opacity: 1;
- transform: translateY(0) scale(1);
- }
- 100% {
- opacity: 0;
- transform: translateY(40px) scale(0.5);
- }
+ 0% {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+ 100% {
+ opacity: 0;
+ transform: translateY(40px) scale(0.5);
+ }
}
diff --git a/src/index.html b/src/index.html
index c5f83a2..b20e703 100644
--- a/src/index.html
+++ b/src/index.html
@@ -1,599 +1,582 @@
<!doctype html>
<html lang="en">
- <head>
- <meta charset="UTF-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
- <title>Liz CSS - Framework Demo</title>
- <link rel="stylesheet" href="/bundle.css" />
- <link rel="icon" href="/img/favicon.ico" />
- <style>
- .demo-section {
- margin-bottom: var(--space-xl);
- }
-
- .component-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
- gap: var(--space-md);
- margin: var(--space-md) 0;
- }
-
- .component-box {
- border: var(--border-width) solid var(--border);
- padding: var(--space-md);
- background: var(--surface);
- box-shadow:
- inset 1px 1px 0 var(--border-light),
- inset -1px -1px 0 var(--border-dark),
- var(--shadow-box);
- }
-
- .component-box > :last-child {
- margin-bottom: 0;
- }
-
- .component-label {
- display: block;
- font-size: 0.75rem;
- color: var(--muted);
- margin-bottom: var(--space-xs);
- text-transform: uppercase;
- letter-spacing: 0.05em;
- }
-
- .button-row {
- display: flex;
- gap: var(--space-sm);
- flex-wrap: wrap;
- margin: var(--space-md) 0;
- }
-
- .form-group {
- margin-bottom: var(--space-md);
- }
-
- code {
- background: var(--bg);
- padding: 0.25rem 0.5rem;
- border: 1px solid var(--border-light);
- font-size: 0.875rem;
- font-family: var(--font-mono);
- }
-
- .color-swatch {
- display: inline-block;
- width: 40px;
- height: 40px;
- border: var(--border-width) solid var(--border);
- margin-right: var(--space-sm);
- vertical-align: middle;
- box-shadow:
- inset 1px 1px 0 var(--border-light),
- inset -1px -1px 0 var(--border-dark),
- var(--shadow-sm);
- }
- </style>
- </head>
- <body>
- <header>
- <nav>
- <a href="/">Home</a>
- <a href="/demo.html">Components</a>
- </nav>
- <input type="checkbox" id="theme-toggle" class="toggle" aria-label="Toggle dark mode" />
- </header>
-
- <main>
- <!-- Color Palette -->
- <article>
- <h2>Color Palette</h2>
- <p>A retro-themed minimal CSS framework with carefully selected colors for light and dark modes.</p>
-
- <h3>Primary Colors</h3>
- <div class="component-grid">
- <div class="component-box">
- <span class="component-label">Primary</span>
- <div style="display: flex; align-items: center;">
- <div
- class="color-swatch"
- style="background: var(--primary);"
- ></div>
- <code>#e56aa6</code>
- </div>
- </div>
- <div class="component-box">
- <span class="component-label">Primary Light</span>
- <div style="display: flex; align-items: center;">
- <div
- class="color-swatch"
- style="background: var(--primary-light);"
- ></div>
- <code>#f08dbe</code>
- </div>
- </div>
- <div class="component-box">
- <span class="component-label">Primary Dark</span>
- <div style="display: flex; align-items: center;">
- <div
- class="color-swatch"
- style="background: var(--primary-dark);"
- ></div>
- <code>#c84d86</code>
- </div>
- </div>
- </div>
-
- <h3>Secondary Colors</h3>
- <div class="component-grid">
- <div class="component-box">
- <span class="component-label">Secondary</span>
- <div style="display: flex; align-items: center;">
- <div
- class="color-swatch"
- style="background: var(--secondary);"
- ></div>
- <code>#b69cff</code>
- </div>
- </div>
- <div class="component-box">
- <span class="component-label">Secondary Light</span>
- <div style="display: flex; align-items: center;">
- <div
- class="color-swatch"
- style="background: var(--secondary-light);"
- ></div>
- <code>#d7c8ff</code>
- </div>
- </div>
- <div class="component-box">
- <span class="component-label">Secondary Dark</span>
- <div style="display: flex; align-items: center;">
- <div
- class="color-swatch"
- style="background: var(--secondary-dark);"
- ></div>
- <code>#8f78d6</code>
- </div>
- </div>
- </div>
-
- <h3>Status Colors</h3>
- <div class="component-grid">
- <div class="component-box">
- <span class="component-label">Success</span>
- <div style="display: flex; align-items: center;">
- <div
- class="color-swatch"
- style="background: var(--success);"
- ></div>
- <code>#2e8b57</code>
- </div>
- </div>
- <div class="component-box">
- <span class="component-label">Error</span>
- <div style="display: flex; align-items: center;">
- <div class="color-swatch" style="background: var(--error);"></div>
- <code>#b3261e</code>
- </div>
- </div>
- <div class="component-box">
- <span class="component-label">Warning</span>
- <div style="display: flex; align-items: center;">
- <div
- class="color-swatch"
- style="background: var(--warning);"
- ></div>
- <code>#b7791f</code>
- </div>
- </div>
- <div class="component-box">
- <span class="component-label">Info</span>
- <div style="display: flex; align-items: center;">
- <div class="color-swatch" style="background: var(--info);"></div>
- <code>#2b6cb0</code>
- </div>
- </div>
- </div>
- </article>
-
- <!-- Buttons -->
- <article>
- <h2>Buttons</h2>
- <p>Retro-styled buttons with multiple variants and states.</p>
-
- <h3>Basic Buttons</h3>
- <div class="button-row">
- <button>Default (Secondary)</button>
- <button class="primary">Primary</button>
- <button class="secondary">Secondary</button>
- <button class="contrast">Contrast</button>
- </div>
-
- <h3>Status Buttons</h3>
- <div class="button-row">
- <button class="success">Success</button>
- <button class="error">Error</button>
- <button class="warning">Warning</button>
- <button class="info">Info</button>
- </div>
-
- <h3>Button States</h3>
- <div class="button-row">
- <button>Normal</button>
- <button disabled>Disabled</button>
- </div>
- </article>
-
- <!-- Forms -->
- <article>
- <h2>Forms</h2>
- <p>Complete form styling with retro aesthetics.</p>
-
- <form>
- <div class="form-group">
- <label for="text-input">Text Input</label>
- <input
- type="text"
- id="text-input"
- placeholder="Enter some text..."
- />
- </div>
-
- <div class="form-group">
- <label for="email-input">Email Input</label>
- <input
- type="email"
- id="email-input"
- placeholder="you@example.com"
- />
- </div>
-
- <div class="form-group">
- <label for="number-input">Number Input</label>
- <input type="number" id="number-input" placeholder="42" />
- </div>
-
- <div class="form-group">
- <label for="textarea">Textarea</label>
- <textarea
- id="textarea"
- placeholder="Write your message here..."
- ></textarea>
- </div>
-
- <div class="form-group">
- <label for="select">Select Dropdown</label>
- <select id="select">
- <option>Choose an option</option>
- <option>Option 1</option>
- <option>Option 2</option>
- <option>Option 3</option>
- </select>
- </div>
-
- <fieldset>
- <legend>Checkboxes</legend>
- <div class="form-group">
- <input type="checkbox" id="check1" />
- <label for="check1" style="display: inline; margin: 0;">
- Checkbox 1
- </label>
- </div>
- <div class="form-group">
- <input type="checkbox" id="check2" />
- <label for="check2" style="display: inline; margin: 0;">
- Checkbox 2
- </label>
- </div>
- </fieldset>
-
- <fieldset>
- <legend>Radio Buttons</legend>
- <div class="form-group">
- <input type="radio" id="radio1" name="radio-group" />
- <label for="radio1" style="display: inline; margin: 0;">
- Option A
- </label>
- </div>
- <div class="form-group">
- <input type="radio" id="radio2" name="radio-group" />
- <label for="radio2" style="display: inline; margin: 0;">
- Option B
- </label>
- </div>
- </fieldset>
-
- <div class="button-row mt-md">
- <button type="submit" class="primary">Submit</button>
- <button type="reset" class="secondary">Reset</button>
- <button type="button" class="contrast">Cancel</button>
- </div>
- </form>
- </article>
-
- <!-- Alerts -->
- <article>
- <h2>Alerts</h2>
- <p>Status messages and notifications with semantic colors.</p>
-
- <div class="alert info">
- <strong>ℹ️ Info:</strong> This is an informational message.
- </div>
-
- <div class="alert success">
- <strong>✓ Success:</strong> Operation completed successfully!
- </div>
-
- <div class="alert warning">
- <strong>⚠️ Warning:</strong> Please review this before proceeding.
- </div>
-
- <div class="alert error">
- <strong>✗ Error:</strong> Something went wrong. Please try again.
- </div>
- </article>
-
- <!-- Badges -->
- <article>
- <h2>Badges & Pills</h2>
- <p>Small inline elements for labels, tags, and status indicators.</p>
-
- <h3>Default Badges</h3>
- <p>
- <span class="badge">Default</span>
- <span class="badge primary">Primary</span>
- <span class="badge secondary">Secondary</span>
- <span class="badge contrast">Contrast</span>
- </p>
-
- <h3>Status Badges</h3>
- <p>
- <span class="badge success">✓ Success</span>
- <span class="badge error">✗ Error</span>
- <span class="badge warning">⚠️ Warning</span>
- <span class="badge info">ℹ️ Info</span>
- </p>
- </article>
-
- <!-- Typography & Helpers -->
- <article>
- <h2>Typography & Helper Classes</h2>
- <p>Text utilities and spacing helpers for quick styling.</p>
-
- <h3>Text Colors</h3>
- <p>
- Default text <span class="text-muted">(muted)</span>
- <span class="text-success">success text</span>
- <span class="text-error">error text</span>
- <span class="text-warning">warning text</span>
- <span class="text-info">info text</span>
- <span class="text-contrast">contrast text</span>
- </p>
-
- <h3>Text Alignment</h3>
- <p class="text-left">Left aligned</p>
- <p class="text-center">Center aligned</p>
- <p class="text-right">Right aligned</p>
-
- <h3>Spacing Helpers</h3>
- <p>
- Use <code>.mt-xs</code>, <code>.mt-sm</code>, <code>.mt-md</code>,
- <code>.mt-lg</code>, <code>.mt-xl</code> for margin-top.
- </p>
- <p>
- Use <code>.mb-xs</code>, <code>.mb-sm</code>, <code>.mb-md</code>,
- <code>.mb-lg</code>, <code>.mb-xl</code> for margin-bottom.
- </p>
- <p>
- Use <code>.mx-auto</code> for horizontal centering and
- <code>.py-md</code> for vertical padding.
- </p>
-
- <div class="component-box mt-md mb-lg mx-auto" style="max-width: 400px;">
- <p class="text-center">Example: Component with margin and padding utilities</p>
- </div>
- </article>
-
- <!-- Headings -->
- <article>
- <h2>Heading Styles</h2>
- <h1>Heading 1</h1>
- <h2>Heading 2</h2>
- <h3>Heading 3</h3>
- <h4>Heading 4</h4>
- <h5>Heading 5</h5>
- <h6>Heading 6</h6>
- </article>
-
- <!-- Lists -->
- <article>
- <h2>Lists</h2>
-
- <h3>Unordered List</h3>
- <ul>
- <li>Item one with some text</li>
- <li>Item two with more text</li>
- <li>Item three with even more text</li>
- <li>Nested lists:
- <ul>
- <li>Sub-item one</li>
- <li>Sub-item two</li>
- </ul>
- </li>
- </ul>
-
- <h3>Ordered List</h3>
- <ol>
- <li>First step in the process</li>
- <li>Second step follows naturally</li>
- <li>Third step completes the sequence</li>
- </ol>
- </article>
-
- <!-- Blockquotes -->
- <article>
- <h2>Blockquotes</h2>
- <blockquote>
- "The best time to plant a tree was 20 years ago. The second best time is now."
- <br />
- <small>— Chinese Proverb</small>
- </blockquote>
- </article>
-
- <!-- Links -->
- <article>
- <h2>Links</h2>
- <p>
- This is a <a href="#demo">hyperlink</a> styled with the framework. Links
- have a bottom border and change color on hover. You can use them
- <a href="#inline">inline within text</a> or as standalone elements.
- </p>
- </article>
-
- <!-- Tables -->
- <article>
- <h2>Tables</h2>
- <p>Tables with retro styling, alternating row colors, and hover effects.</p>
- <table>
- <thead>
- <tr>
- <th>Feature</th>
- <th>Light Mode</th>
- <th>Dark Mode</th>
- </tr>
- </thead>
- <tbody>
- <tr>
- <td>Background</td>
- <td>Warm cream</td>
- <td>Warm taupe</td>
- </tr>
- <tr>
- <td>Buttons</td>
- <td>Lavender primary</td>
- <td>Rose primary</td>
- </tr>
- <tr>
- <td>Borders</td>
- <td>Retro 90s beveled</td>
- <td>Retro 90s beveled</td>
- </tr>
- <tr>
- <td>Accents</td>
- <td>Pink & brown</td>
- <td>Pink & brown</td>
- </tr>
- </tbody>
- </table>
- </article>
-
- <!-- Code -->
- <article>
- <h2>Code</h2>
- <p>Inline <code>const x = 42;</code> looks different from block code:</p>
- <pre><code class="language-javascript">function retro() {
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Liz CSS - Framework Demo</title>
+ <script>
+ window.ASSET_BASE = 'ASSET_BASE_PLACEHOLDER';
+ </script>
+ <link rel="stylesheet" href="/bundle.css" />
+ <link rel="icon" href="/img/favicon.ico" />
+ <style>
+ .demo-section {
+ margin-bottom: var(--space-xl);
+ }
+
+ .component-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: var(--space-md);
+ margin: var(--space-md) 0;
+ }
+
+ .component-box {
+ border: var(--border-width) solid var(--border);
+ padding: var(--space-md);
+ background: var(--surface);
+ box-shadow:
+ inset 1px 1px 0 var(--border-light),
+ inset -1px -1px 0 var(--border-dark),
+ var(--shadow-box);
+ }
+
+ .component-box > :last-child {
+ margin-bottom: 0;
+ }
+
+ .component-label {
+ display: block;
+ font-size: 0.75rem;
+ color: var(--muted);
+ margin-bottom: var(--space-xs);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ }
+
+ .button-row {
+ display: flex;
+ gap: var(--space-sm);
+ flex-wrap: wrap;
+ margin: var(--space-md) 0;
+ }
+
+ .form-group {
+ margin-bottom: var(--space-md);
+ }
+
+ code {
+ background: var(--bg);
+ padding: 0.25rem 0.5rem;
+ border: 1px solid var(--border-light);
+ font-size: 0.875rem;
+ font-family: var(--font-mono);
+ }
+
+ .color-swatch {
+ display: inline-block;
+ width: 40px;
+ height: 40px;
+ border: var(--border-width) solid var(--border);
+ margin-right: var(--space-sm);
+ vertical-align: middle;
+ box-shadow:
+ inset 1px 1px 0 var(--border-light),
+ inset -1px -1px 0 var(--border-dark),
+ var(--shadow-sm);
+ }
+ </style>
+ </head>
+ <body>
+ <header>
+ <nav>
+ <a href="/">Home</a>
+ <a href="/demo.html">Components</a>
+ </nav>
+ <input type="checkbox" id="theme-toggle" class="toggle" aria-label="Toggle dark mode" />
+ </header>
+
+ <main>
+ <!-- Color Palette -->
+ <article>
+ <h2>Color Palette</h2>
+ <p>
+ A retro-themed minimal CSS framework with carefully selected colors for light
+ and dark modes.
+ </p>
+
+ <h3>Primary Colors</h3>
+ <div class="component-grid">
+ <div class="component-box">
+ <span class="component-label">Primary</span>
+ <div style="display: flex; align-items: center">
+ <div class="color-swatch" style="background: var(--primary)"></div>
+ <code>#e56aa6</code>
+ </div>
+ </div>
+ <div class="component-box">
+ <span class="component-label">Primary Light</span>
+ <div style="display: flex; align-items: center">
+ <div
+ class="color-swatch"
+ style="background: var(--primary-light)"
+ ></div>
+ <code>#f08dbe</code>
+ </div>
+ </div>
+ <div class="component-box">
+ <span class="component-label">Primary Dark</span>
+ <div style="display: flex; align-items: center">
+ <div class="color-swatch" style="background: var(--primary-dark)"></div>
+ <code>#c84d86</code>
+ </div>
+ </div>
+ </div>
+
+ <h3>Secondary Colors</h3>
+ <div class="component-grid">
+ <div class="component-box">
+ <span class="component-label">Secondary</span>
+ <div style="display: flex; align-items: center">
+ <div class="color-swatch" style="background: var(--secondary)"></div>
+ <code>#b69cff</code>
+ </div>
+ </div>
+ <div class="component-box">
+ <span class="component-label">Secondary Light</span>
+ <div style="display: flex; align-items: center">
+ <div
+ class="color-swatch"
+ style="background: var(--secondary-light)"
+ ></div>
+ <code>#d7c8ff</code>
+ </div>
+ </div>
+ <div class="component-box">
+ <span class="component-label">Secondary Dark</span>
+ <div style="display: flex; align-items: center">
+ <div
+ class="color-swatch"
+ style="background: var(--secondary-dark)"
+ ></div>
+ <code>#8f78d6</code>
+ </div>
+ </div>
+ </div>
+
+ <h3>Status Colors</h3>
+ <div class="component-grid">
+ <div class="component-box">
+ <span class="component-label">Success</span>
+ <div style="display: flex; align-items: center">
+ <div class="color-swatch" style="background: var(--success)"></div>
+ <code>#2e8b57</code>
+ </div>
+ </div>
+ <div class="component-box">
+ <span class="component-label">Error</span>
+ <div style="display: flex; align-items: center">
+ <div class="color-swatch" style="background: var(--error)"></div>
+ <code>#b3261e</code>
+ </div>
+ </div>
+ <div class="component-box">
+ <span class="component-label">Warning</span>
+ <div style="display: flex; align-items: center">
+ <div class="color-swatch" style="background: var(--warning)"></div>
+ <code>#b7791f</code>
+ </div>
+ </div>
+ <div class="component-box">
+ <span class="component-label">Info</span>
+ <div style="display: flex; align-items: center">
+ <div class="color-swatch" style="background: var(--info)"></div>
+ <code>#2b6cb0</code>
+ </div>
+ </div>
+ </div>
+ </article>
+
+ <!-- Buttons -->
+ <article>
+ <h2>Buttons</h2>
+ <p>Retro-styled buttons with multiple variants and states.</p>
+
+ <h3>Basic Buttons</h3>
+ <div class="button-row">
+ <button>Default (Secondary)</button>
+ <button class="primary">Primary</button>
+ <button class="secondary">Secondary</button>
+ <button class="contrast">Contrast</button>
+ </div>
+
+ <h3>Status Buttons</h3>
+ <div class="button-row">
+ <button class="success">Success</button>
+ <button class="error">Error</button>
+ <button class="warning">Warning</button>
+ <button class="info">Info</button>
+ </div>
+
+ <h3>Button States</h3>
+ <div class="button-row">
+ <button>Normal</button>
+ <button disabled>Disabled</button>
+ </div>
+ </article>
+
+ <!-- Forms -->
+ <article>
+ <h2>Forms</h2>
+ <p>Complete form styling with retro aesthetics.</p>
+
+ <form>
+ <div class="form-group">
+ <label for="text-input">Text Input</label>
+ <input type="text" id="text-input" placeholder="Enter some text..." />
+ </div>
+
+ <div class="form-group">
+ <label for="email-input">Email Input</label>
+ <input type="email" id="email-input" placeholder="you@example.com" />
+ </div>
+
+ <div class="form-group">
+ <label for="number-input">Number Input</label>
+ <input type="number" id="number-input" placeholder="42" />
+ </div>
+
+ <div class="form-group">
+ <label for="textarea">Textarea</label>
+ <textarea id="textarea" placeholder="Write your message here..."></textarea>
+ </div>
+
+ <div class="form-group">
+ <label for="select">Select Dropdown</label>
+ <select id="select">
+ <option>Choose an option</option>
+ <option>Option 1</option>
+ <option>Option 2</option>
+ <option>Option 3</option>
+ </select>
+ </div>
+
+ <fieldset>
+ <legend>Checkboxes</legend>
+ <div class="form-group">
+ <input type="checkbox" id="check1" />
+ <label for="check1" style="display: inline; margin: 0">
+ Checkbox 1
+ </label>
+ </div>
+ <div class="form-group">
+ <input type="checkbox" id="check2" />
+ <label for="check2" style="display: inline; margin: 0">
+ Checkbox 2
+ </label>
+ </div>
+ </fieldset>
+
+ <fieldset>
+ <legend>Radio Buttons</legend>
+ <div class="form-group">
+ <input type="radio" id="radio1" name="radio-group" />
+ <label for="radio1" style="display: inline; margin: 0">
+ Option A
+ </label>
+ </div>
+ <div class="form-group">
+ <input type="radio" id="radio2" name="radio-group" />
+ <label for="radio2" style="display: inline; margin: 0">
+ Option B
+ </label>
+ </div>
+ </fieldset>
+
+ <div class="button-row mt-md">
+ <button type="submit" class="primary">Submit</button>
+ <button type="reset" class="secondary">Reset</button>
+ <button type="button" class="contrast">Cancel</button>
+ </div>
+ </form>
+ </article>
+
+ <!-- Alerts -->
+ <article>
+ <h2>Alerts</h2>
+ <p>Status messages and notifications with semantic colors.</p>
+
+ <div class="alert info">
+ <strong>ℹ️ Info:</strong> This is an informational message.
+ </div>
+
+ <div class="alert success">
+ <strong>✓ Success:</strong> Operation completed successfully!
+ </div>
+
+ <div class="alert warning">
+ <strong>⚠️ Warning:</strong> Please review this before proceeding.
+ </div>
+
+ <div class="alert error">
+ <strong>✗ Error:</strong> Something went wrong. Please try again.
+ </div>
+ </article>
+
+ <!-- Badges -->
+ <article>
+ <h2>Badges & Pills</h2>
+ <p>Small inline elements for labels, tags, and status indicators.</p>
+
+ <h3>Default Badges</h3>
+ <p>
+ <span class="badge">Default</span>
+ <span class="badge primary">Primary</span>
+ <span class="badge secondary">Secondary</span>
+ <span class="badge contrast">Contrast</span>
+ </p>
+
+ <h3>Status Badges</h3>
+ <p>
+ <span class="badge success">✓ Success</span>
+ <span class="badge error">✗ Error</span>
+ <span class="badge warning">⚠️ Warning</span>
+ <span class="badge info">ℹ️ Info</span>
+ </p>
+ </article>
+
+ <!-- Typography & Helpers -->
+ <article>
+ <h2>Typography & Helper Classes</h2>
+ <p>Text utilities and spacing helpers for quick styling.</p>
+
+ <h3>Text Colors</h3>
+ <p>
+ Default text <span class="text-muted">(muted)</span>
+ <span class="text-success">success text</span>
+ <span class="text-error">error text</span>
+ <span class="text-warning">warning text</span>
+ <span class="text-info">info text</span>
+ <span class="text-contrast">contrast text</span>
+ </p>
+
+ <h3>Text Alignment</h3>
+ <p class="text-left">Left aligned</p>
+ <p class="text-center">Center aligned</p>
+ <p class="text-right">Right aligned</p>
+
+ <h3>Spacing Helpers</h3>
+ <p>
+ Use <code>.mt-xs</code>, <code>.mt-sm</code>, <code>.mt-md</code>,
+ <code>.mt-lg</code>, <code>.mt-xl</code> for margin-top.
+ </p>
+ <p>
+ Use <code>.mb-xs</code>, <code>.mb-sm</code>, <code>.mb-md</code>,
+ <code>.mb-lg</code>, <code>.mb-xl</code> for margin-bottom.
+ </p>
+ <p>
+ Use <code>.mx-auto</code> for horizontal centering and <code>.py-md</code> for
+ vertical padding.
+ </p>
+
+ <div class="component-box mt-md mb-lg mx-auto" style="max-width: 400px">
+ <p class="text-center">Example: Component with margin and padding utilities</p>
+ </div>
+ </article>
+
+ <!-- Headings -->
+ <article>
+ <h2>Heading Styles</h2>
+ <h1>Heading 1</h1>
+ <h2>Heading 2</h2>
+ <h3>Heading 3</h3>
+ <h4>Heading 4</h4>
+ <h5>Heading 5</h5>
+ <h6>Heading 6</h6>
+ </article>
+
+ <!-- Lists -->
+ <article>
+ <h2>Lists</h2>
+
+ <h3>Unordered List</h3>
+ <ul>
+ <li>Item one with some text</li>
+ <li>Item two with more text</li>
+ <li>Item three with even more text</li>
+ <li>
+ Nested lists:
+ <ul>
+ <li>Sub-item one</li>
+ <li>Sub-item two</li>
+ </ul>
+ </li>
+ </ul>
+
+ <h3>Ordered List</h3>
+ <ol>
+ <li>First step in the process</li>
+ <li>Second step follows naturally</li>
+ <li>Third step completes the sequence</li>
+ </ol>
+ </article>
+
+ <!-- Blockquotes -->
+ <article>
+ <h2>Blockquotes</h2>
+ <blockquote>
+ "The best time to plant a tree was 20 years ago. The second best time is now."
+ <br />
+ <small>— Chinese Proverb</small>
+ </blockquote>
+ </article>
+
+ <!-- Links -->
+ <article>
+ <h2>Links</h2>
+ <p>
+ This is a <a href="#demo">hyperlink</a> styled with the framework. Links have a
+ bottom border and change color on hover. You can use them
+ <a href="#inline">inline within text</a> or as standalone elements.
+ </p>
+ </article>
+
+ <!-- Tables -->
+ <article>
+ <h2>Tables</h2>
+ <p>Tables with retro styling, alternating row colors, and hover effects.</p>
+ <table>
+ <thead>
+ <tr>
+ <th>Feature</th>
+ <th>Light Mode</th>
+ <th>Dark Mode</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>Background</td>
+ <td>Warm cream</td>
+ <td>Warm taupe</td>
+ </tr>
+ <tr>
+ <td>Buttons</td>
+ <td>Lavender primary</td>
+ <td>Rose primary</td>
+ </tr>
+ <tr>
+ <td>Borders</td>
+ <td>Retro 90s beveled</td>
+ <td>Retro 90s beveled</td>
+ </tr>
+ <tr>
+ <td>Accents</td>
+ <td>Pink & brown</td>
+ <td>Pink & brown</td>
+ </tr>
+ </tbody>
+ </table>
+ </article>
+
+ <!-- Code -->
+ <article>
+ <h2>Code</h2>
+ <p>Inline <code>const x = 42;</code> looks different from block code:</p>
+ <pre><code class="language-javascript">function retro() {
return "That's totally rad!";
}
retro(); // "That's totally rad!"</code></pre>
- </article>
-
- <!-- Dividers -->
- <article>
- <h2>Dividers</h2>
- <p>Use <code>&lt;hr&gt;</code> to separate content sections:</p>
- <hr />
- <p>Content after the divider looks fresh and organized.</p>
- </article>
-
- <!-- Form Validation -->
- <article>
- <h2>Form Validation</h2>
- <p>Invalid inputs show a red border when they fail validation.</p>
- <form>
- <div class="form-group">
- <label for="valid-email">Valid Email</label>
- <input
- type="email"
- id="valid-email"
- placeholder="you@example.com"
- value="user@example.com"
- />
- </div>
- <div class="form-group">
- <label for="invalid-email">Invalid Email</label>
- <input
- type="email"
- id="invalid-email"
- placeholder="you@example.com"
- value="not-an-email"
- />
- </div>
- <div class="form-group">
- <label for="required-field">Required Field (empty = invalid)</label>
- <input
- type="text"
- id="required-field"
- placeholder="This field is required"
- required
- />
- </div>
- </form>
- </article>
-
- <!-- Button Groups -->
- <article>
- <h2>Button Groups</h2>
- <p>Combine buttons into a group with the <code>.button-group</code> class:</p>
- <div class="button-group mt-md">
- <button class="primary">First</button>
- <button class="secondary">Second</button>
- <button class="contrast">Third</button>
- </div>
- </article>
-
- <!-- Toggle Switches -->
- <article>
- <h2>Toggle Switches</h2>
- <p>Use checkboxes with the <code>.toggle</code> class to create stylish toggle switches:</p>
- <div class="form-group mt-md">
- <input type="checkbox" id="toggle1" class="toggle" />
- <label for="toggle1">Dark mode</label>
- </div>
- <div class="form-group">
- <input type="checkbox" id="toggle2" class="toggle" checked />
- <label for="toggle2">Notifications enabled</label>
- </div>
- <div class="form-group">
- <input type="checkbox" id="toggle3" class="toggle" />
- <label for="toggle3">Auto-save</label>
- </div>
- </article>
-
- <!-- Range Sliders -->
- <article>
- <h2>Range Sliders</h2>
- <p>Native range inputs styled with retro beveled appearance:</p>
- <div class="form-group mt-md">
- <label for="volume">Volume</label>
- <input type="range" id="volume" min="0" max="100" value="50" />
- </div>
- <div class="form-group">
- <label for="brightness">Brightness</label>
- <input type="range" id="brightness" min="0" max="100" value="75" />
- </div>
- <div class="form-group">
- <label for="saturation">Saturation</label>
- <input type="range" id="saturation" min="0" max="100" value="25" />
- </div>
- </article>
-
- </main>
-
- <footer>
- <p>&copy; 2025 Liz CSS Framework. Made with coffee and retro vibes.</p>
- </footer>
-
- <script src="/bundle.js"></script>
- </body>
+ </article>
+
+ <!-- Dividers -->
+ <article>
+ <h2>Dividers</h2>
+ <p>Use <code>&lt;hr&gt;</code> to separate content sections:</p>
+ <hr />
+ <p>Content after the divider looks fresh and organized.</p>
+ </article>
+
+ <!-- Form Validation -->
+ <article>
+ <h2>Form Validation</h2>
+ <p>Invalid inputs show a red border when they fail validation.</p>
+ <form>
+ <div class="form-group">
+ <label for="valid-email">Valid Email</label>
+ <input
+ type="email"
+ id="valid-email"
+ placeholder="you@example.com"
+ value="user@example.com"
+ />
+ </div>
+ <div class="form-group">
+ <label for="invalid-email">Invalid Email</label>
+ <input
+ type="email"
+ id="invalid-email"
+ placeholder="you@example.com"
+ value="not-an-email"
+ />
+ </div>
+ <div class="form-group">
+ <label for="required-field">Required Field (empty = invalid)</label>
+ <input
+ type="text"
+ id="required-field"
+ placeholder="This field is required"
+ required
+ />
+ </div>
+ </form>
+ </article>
+
+ <!-- Button Groups -->
+ <article>
+ <h2>Button Groups</h2>
+ <p>Combine buttons into a group with the <code>.button-group</code> class:</p>
+ <div class="button-group mt-md">
+ <button class="primary">First</button>
+ <button class="secondary">Second</button>
+ <button class="contrast">Third</button>
+ </div>
+ </article>
+
+ <!-- Toggle Switches -->
+ <article>
+ <h2>Toggle Switches</h2>
+ <p>
+ Use checkboxes with the <code>.toggle</code> class to create stylish toggle
+ switches:
+ </p>
+ <div class="form-group mt-md">
+ <input type="checkbox" id="toggle1" class="toggle" />
+ <label for="toggle1">Dark mode</label>
+ </div>
+ <div class="form-group">
+ <input type="checkbox" id="toggle2" class="toggle" checked />
+ <label for="toggle2">Notifications enabled</label>
+ </div>
+ <div class="form-group">
+ <input type="checkbox" id="toggle3" class="toggle" />
+ <label for="toggle3">Auto-save</label>
+ </div>
+ </article>
+
+ <!-- Range Sliders -->
+ <article>
+ <h2>Range Sliders</h2>
+ <p>Native range inputs styled with retro beveled appearance:</p>
+ <div class="form-group mt-md">
+ <label for="volume">Volume</label>
+ <input type="range" id="volume" min="0" max="100" value="50" />
+ </div>
+ <div class="form-group">
+ <label for="brightness">Brightness</label>
+ <input type="range" id="brightness" min="0" max="100" value="75" />
+ </div>
+ <div class="form-group">
+ <label for="saturation">Saturation</label>
+ <input type="range" id="saturation" min="0" max="100" value="25" />
+ </div>
+ </article>
+ </main>
+
+ <footer>
+ <p>&copy; 2025 Liz CSS Framework. Made with coffee and retro vibes.</p>
+ </footer>
+
+ <script src="/bundle.js"></script>
+ </body>
</html>
diff --git a/src/js/oneko.js b/src/js/oneko.js
deleted file mode 100644
index dcf927f..0000000
--- a/src/js/oneko.js
+++ /dev/null
@@ -1,284 +0,0 @@
-// oneko.js: https://github.com/adryd325/oneko.js
-
-export function initOneko() {
- const isReducedMotion =
- window.matchMedia(`(prefers-reduced-motion: reduce)`) === true ||
- window.matchMedia(`(prefers-reduced-motion: reduce)`).matches === true;
-
- if (isReducedMotion) return;
-
- const nekoEl = document.createElement("div");
- let persistPosition = true;
-
- let nekoPosX = 32;
- let nekoPosY = 32;
-
- let mousePosX = 0;
- let mousePosY = 0;
-
- let frameCount = 0;
- let idleTime = 0;
- let idleAnimation = null;
- let idleAnimationFrame = 0;
-
- const nekoSpeed = 10;
- const spriteSets = {
- idle: [[-3, -3]],
- alert: [[-7, -3]],
- scratchSelf: [
- [-5, 0],
- [-6, 0],
- [-7, 0],
- ],
- scratchWallN: [
- [0, 0],
- [0, -1],
- ],
- scratchWallS: [
- [-7, -1],
- [-6, -2],
- ],
- scratchWallE: [
- [-2, -2],
- [-2, -3],
- ],
- scratchWallW: [
- [-4, 0],
- [-4, -1],
- ],
- tired: [[-3, -2]],
- sleeping: [
- [-2, 0],
- [-2, -1],
- ],
- N: [
- [-1, -2],
- [-1, -3],
- ],
- NE: [
- [0, -2],
- [0, -3],
- ],
- E: [
- [-3, 0],
- [-3, -1],
- ],
- SE: [
- [-5, -1],
- [-5, -2],
- ],
- S: [
- [-6, -3],
- [-7, -2],
- ],
- SW: [
- [-5, -3],
- [-6, -1],
- ],
- W: [
- [-4, -2],
- [-4, -3],
- ],
- NW: [
- [-1, 0],
- [-1, -1],
- ],
- };
-
- function init() {
- let nekoFile = "/oneko/oneko.gif";
- const curScript = document.currentScript;
- if (curScript && curScript.dataset.cat) {
- nekoFile = curScript.dataset.cat;
- }
- if (curScript && curScript.dataset.persistPosition) {
- if (curScript.dataset.persistPosition === "") {
- persistPosition = true;
- } else {
- persistPosition = JSON.parse(
- curScript.dataset.persistPosition.toLowerCase(),
- );
- }
- }
-
- if (persistPosition) {
- let storedNeko = JSON.parse(window.localStorage.getItem("oneko"));
- if (storedNeko !== null) {
- nekoPosX = storedNeko.nekoPosX;
- nekoPosY = storedNeko.nekoPosY;
- mousePosX = storedNeko.mousePosX;
- mousePosY = storedNeko.mousePosY;
- frameCount = storedNeko.frameCount;
- idleTime = storedNeko.idleTime;
- idleAnimation = storedNeko.idleAnimation;
- idleAnimationFrame = storedNeko.idleAnimationFrame;
- nekoEl.style.backgroundPosition = storedNeko.bgPos;
- }
- }
-
- nekoEl.id = "oneko";
- nekoEl.ariaHidden = true;
- nekoEl.style.width = "32px";
- nekoEl.style.height = "32px";
- nekoEl.style.position = "fixed";
- nekoEl.style.pointerEvents = "none";
- nekoEl.style.imageRendering = "pixelated";
- nekoEl.style.left = `${nekoPosX - 16}px`;
- nekoEl.style.top = `${nekoPosY - 16}px`;
- nekoEl.style.zIndex = 2147483647;
-
- nekoEl.style.backgroundImage = `url(${nekoFile})`;
-
- document.body.appendChild(nekoEl);
-
- document.addEventListener("mousemove", function (event) {
- mousePosX = event.clientX;
- mousePosY = event.clientY;
- });
-
- if (persistPosition) {
- window.addEventListener("beforeunload", function (event) {
- window.localStorage.setItem(
- "oneko",
- JSON.stringify({
- nekoPosX: nekoPosX,
- nekoPosY: nekoPosY,
- mousePosX: mousePosX,
- mousePosY: mousePosY,
- frameCount: frameCount,
- idleTime: idleTime,
- idleAnimation: idleAnimation,
- idleAnimationFrame: idleAnimationFrame,
- bgPos: nekoEl.style.backgroundPosition,
- }),
- );
- });
- }
-
- window.requestAnimationFrame(onAnimationFrame);
- }
-
- let lastFrameTimestamp;
-
- function onAnimationFrame(timestamp) {
- // Stops execution if the neko element is removed from DOM
- if (!nekoEl.isConnected) {
- return;
- }
- if (!lastFrameTimestamp) {
- lastFrameTimestamp = timestamp;
- }
- if (timestamp - lastFrameTimestamp > 100) {
- lastFrameTimestamp = timestamp;
- frame();
- }
- window.requestAnimationFrame(onAnimationFrame);
- }
-
- function setSprite(name, frame) {
- const sprite = spriteSets[name][frame % spriteSets[name].length];
- nekoEl.style.backgroundPosition = `${sprite[0] * 32}px ${sprite[1] * 32}px`;
- }
-
- function resetIdleAnimation() {
- idleAnimation = null;
- idleAnimationFrame = 0;
- }
-
- function idle() {
- idleTime += 1;
-
- // every ~ 20 seconds
- if (
- idleTime > 10 &&
- Math.floor(Math.random() * 200) == 0 &&
- idleAnimation == null
- ) {
- let avalibleIdleAnimations = ["sleeping", "scratchSelf"];
- if (nekoPosX < 32) {
- avalibleIdleAnimations.push("scratchWallW");
- }
- if (nekoPosY < 32) {
- avalibleIdleAnimations.push("scratchWallN");
- }
- if (nekoPosX > window.innerWidth - 32) {
- avalibleIdleAnimations.push("scratchWallE");
- }
- if (nekoPosY > window.innerHeight - 32) {
- avalibleIdleAnimations.push("scratchWallS");
- }
- idleAnimation =
- avalibleIdleAnimations[
- Math.floor(Math.random() * avalibleIdleAnimations.length)
- ];
- }
-
- switch (idleAnimation) {
- case "sleeping":
- if (idleAnimationFrame < 8) {
- setSprite("tired", 0);
- break;
- }
- setSprite("sleeping", Math.floor(idleAnimationFrame / 4));
- if (idleAnimationFrame > 192) {
- resetIdleAnimation();
- }
- break;
- case "scratchWallN":
- case "scratchWallS":
- case "scratchWallE":
- case "scratchWallW":
- case "scratchSelf":
- setSprite(idleAnimation, idleAnimationFrame);
- if (idleAnimationFrame > 9) {
- resetIdleAnimation();
- }
- break;
- default:
- setSprite("idle", 0);
- return;
- }
- idleAnimationFrame += 1;
- }
-
- function frame() {
- frameCount += 1;
- const diffX = nekoPosX - mousePosX;
- const diffY = nekoPosY - mousePosY;
- const distance = Math.sqrt(diffX ** 2 + diffY ** 2);
-
- if (distance < nekoSpeed || distance < 48) {
- idle();
- return;
- }
-
- idleAnimation = null;
- idleAnimationFrame = 0;
-
- if (idleTime > 1) {
- setSprite("alert", 0);
- // count down after being alerted before moving
- idleTime = Math.min(idleTime, 7);
- idleTime -= 1;
- return;
- }
-
- let direction;
- direction = diffY / distance > 0.5 ? "N" : "";
- direction += diffY / distance < -0.5 ? "S" : "";
- direction += diffX / distance > 0.5 ? "W" : "";
- direction += diffX / distance < -0.5 ? "E" : "";
- setSprite(direction, frameCount);
-
- nekoPosX -= (diffX / distance) * nekoSpeed;
- nekoPosY -= (diffY / distance) * nekoSpeed;
-
- nekoPosX = Math.min(Math.max(16, nekoPosX), window.innerWidth - 16);
- nekoPosY = Math.min(Math.max(16, nekoPosY), window.innerHeight - 16);
-
- nekoEl.style.left = `${nekoPosX - 16}px`;
- nekoEl.style.top = `${nekoPosY - 16}px`;
- }
-
- init();
-}
diff --git a/src/js/oneko.ts b/src/js/oneko.ts
new file mode 100644
index 0000000..236c7cb
--- /dev/null
+++ b/src/js/oneko.ts
@@ -0,0 +1,277 @@
+// oneko.js: https://github.com/adryd325/oneko.js
+
+export function initOneko(): void {
+ const isReducedMotion =
+ window.matchMedia('(prefers-reduced-motion: reduce)') === true ||
+ window.matchMedia('(prefers-reduced-motion: reduce)').matches === true;
+
+ if (isReducedMotion) return;
+
+ const nekoEl = document.createElement('div');
+ let persistPosition = true;
+
+ let nekoPosX = 32;
+ let nekoPosY = 32;
+
+ let mousePosX = 0;
+ let mousePosY = 0;
+
+ let frameCount = 0;
+ let idleTime = 0;
+ let idleAnimation: string | null = null;
+ let idleAnimationFrame = 0;
+
+ const nekoSpeed = 10;
+ const spriteSets: Record<string, [number, number][]> = {
+ idle: [[-3, -3]],
+ alert: [[-7, -3]],
+ scratchSelf: [
+ [-5, 0],
+ [-6, 0],
+ [-7, 0],
+ ],
+ scratchWallN: [
+ [0, 0],
+ [0, -1],
+ ],
+ scratchWallS: [
+ [-7, -1],
+ [-6, -2],
+ ],
+ scratchWallE: [
+ [-2, -2],
+ [-2, -3],
+ ],
+ scratchWallW: [
+ [-4, 0],
+ [-4, -1],
+ ],
+ tired: [[-3, -2]],
+ sleeping: [
+ [-2, 0],
+ [-2, -1],
+ ],
+ N: [
+ [-1, -2],
+ [-1, -3],
+ ],
+ NE: [
+ [0, -2],
+ [0, -3],
+ ],
+ E: [
+ [-3, 0],
+ [-3, -1],
+ ],
+ SE: [
+ [-5, -1],
+ [-5, -2],
+ ],
+ S: [
+ [-6, -3],
+ [-7, -2],
+ ],
+ SW: [
+ [-5, -3],
+ [-6, -1],
+ ],
+ W: [
+ [-4, -2],
+ [-4, -3],
+ ],
+ NW: [
+ [-1, 0],
+ [-1, -1],
+ ],
+ };
+
+ function init(): void {
+ const assetBase = window.ASSET_BASE || '';
+ let nekoFile = `${assetBase}/oneko/oneko.gif`;
+ const curScript = document.currentScript as HTMLScriptElement;
+ if (curScript?.dataset.cat) {
+ nekoFile = curScript.dataset.cat;
+ }
+ if (curScript?.dataset.persistPosition) {
+ if (curScript.dataset.persistPosition === '') {
+ persistPosition = true;
+ } else {
+ persistPosition = JSON.parse(curScript.dataset.persistPosition.toLowerCase());
+ }
+ }
+
+ if (persistPosition) {
+ const storedNekoStr = window.localStorage.getItem('oneko');
+ const storedNeko = storedNekoStr ? JSON.parse(storedNekoStr) : null;
+ if (storedNeko !== null) {
+ nekoPosX = storedNeko.nekoPosX;
+ nekoPosY = storedNeko.nekoPosY;
+ mousePosX = storedNeko.mousePosX;
+ mousePosY = storedNeko.mousePosY;
+ frameCount = storedNeko.frameCount;
+ idleTime = storedNeko.idleTime;
+ idleAnimation = storedNeko.idleAnimation;
+ idleAnimationFrame = storedNeko.idleAnimationFrame;
+ nekoEl.style.backgroundPosition = storedNeko.bgPos;
+ }
+ }
+
+ nekoEl.id = 'oneko';
+ nekoEl.ariaHidden = 'true';
+ nekoEl.style.width = '32px';
+ nekoEl.style.height = '32px';
+ nekoEl.style.position = 'fixed';
+ nekoEl.style.pointerEvents = 'none';
+ nekoEl.style.imageRendering = 'pixelated';
+ nekoEl.style.left = `${nekoPosX - 16}px`;
+ nekoEl.style.top = `${nekoPosY - 16}px`;
+ nekoEl.style.zIndex = '2147483647';
+
+ nekoEl.style.backgroundImage = `url(${nekoFile})`;
+
+ document.body.appendChild(nekoEl);
+
+ document.addEventListener('mousemove', (event: MouseEvent) => {
+ mousePosX = event.clientX;
+ mousePosY = event.clientY;
+ });
+
+ if (persistPosition) {
+ window.addEventListener('beforeunload', () => {
+ window.localStorage.setItem(
+ 'oneko',
+ JSON.stringify({
+ nekoPosX: nekoPosX,
+ nekoPosY: nekoPosY,
+ mousePosX: mousePosX,
+ mousePosY: mousePosY,
+ frameCount: frameCount,
+ idleTime: idleTime,
+ idleAnimation: idleAnimation,
+ idleAnimationFrame: idleAnimationFrame,
+ bgPos: nekoEl.style.backgroundPosition,
+ })
+ );
+ });
+ }
+
+ window.requestAnimationFrame(onAnimationFrame);
+ }
+
+ let lastFrameTimestamp: number | undefined;
+
+ function onAnimationFrame(timestamp: number): void {
+ if (!nekoEl.isConnected) {
+ return;
+ }
+ if (!lastFrameTimestamp) {
+ lastFrameTimestamp = timestamp;
+ }
+ if (lastFrameTimestamp && timestamp - lastFrameTimestamp > 100) {
+ lastFrameTimestamp = timestamp;
+ frame();
+ }
+ window.requestAnimationFrame(onAnimationFrame);
+ }
+
+ function setSprite(name: string, frame: number): void {
+ const sprite = spriteSets[name][frame % spriteSets[name].length];
+ nekoEl.style.backgroundPosition = `${sprite[0] * 32}px ${sprite[1] * 32}px`;
+ }
+
+ function resetIdleAnimation(): void {
+ idleAnimation = null;
+ idleAnimationFrame = 0;
+ }
+
+ function idle(): void {
+ idleTime += 1;
+
+ // every ~ 20 seconds
+ if (idleTime > 10 && Math.floor(Math.random() * 200) === 0 && idleAnimation == null) {
+ const avalibleIdleAnimations: string[] = ['sleeping', 'scratchSelf'];
+ if (nekoPosX < 32) {
+ avalibleIdleAnimations.push('scratchWallW');
+ }
+ if (nekoPosY < 32) {
+ avalibleIdleAnimations.push('scratchWallN');
+ }
+ if (nekoPosX > window.innerWidth - 32) {
+ avalibleIdleAnimations.push('scratchWallE');
+ }
+ if (nekoPosY > window.innerHeight - 32) {
+ avalibleIdleAnimations.push('scratchWallS');
+ }
+ idleAnimation =
+ avalibleIdleAnimations[Math.floor(Math.random() * avalibleIdleAnimations.length)];
+ }
+
+ switch (idleAnimation) {
+ case 'sleeping':
+ if (idleAnimationFrame < 8) {
+ setSprite('tired', 0);
+ break;
+ }
+ setSprite('sleeping', Math.floor(idleAnimationFrame / 4));
+ if (idleAnimationFrame > 192) {
+ resetIdleAnimation();
+ }
+ break;
+ case 'scratchWallN':
+ case 'scratchWallS':
+ case 'scratchWallE':
+ case 'scratchWallW':
+ case 'scratchSelf':
+ setSprite(idleAnimation, idleAnimationFrame);
+ if (idleAnimationFrame > 9) {
+ resetIdleAnimation();
+ }
+ break;
+ default:
+ setSprite('idle', 0);
+ return;
+ }
+ idleAnimationFrame += 1;
+ }
+
+ function frame(): void {
+ frameCount += 1;
+ const diffX = nekoPosX - mousePosX;
+ const diffY = nekoPosY - mousePosY;
+ const distance = Math.sqrt(diffX ** 2 + diffY ** 2);
+
+ if (distance < nekoSpeed || distance < 48) {
+ idle();
+ return;
+ }
+
+ idleAnimation = null;
+ idleAnimationFrame = 0;
+
+ if (idleTime > 1) {
+ setSprite('alert', 0);
+ // count down after being alerted before moving
+ idleTime = Math.min(idleTime, 7);
+ idleTime -= 1;
+ return;
+ }
+
+ let direction = '';
+ direction += diffY / distance > 0.5 ? 'N' : '';
+ direction += diffY / distance < -0.5 ? 'S' : '';
+ direction += diffX / distance > 0.5 ? 'W' : '';
+ direction += diffX / distance < -0.5 ? 'E' : '';
+ setSprite(direction, frameCount);
+
+ nekoPosX -= (diffX / distance) * nekoSpeed;
+ nekoPosY -= (diffY / distance) * nekoSpeed;
+
+ nekoPosX = Math.min(Math.max(16, nekoPosX), window.innerWidth - 16);
+ nekoPosY = Math.min(Math.max(16, nekoPosY), window.innerHeight - 16);
+
+ nekoEl.style.left = `${nekoPosX - 16}px`;
+ nekoEl.style.top = `${nekoPosY - 16}px`;
+ }
+
+ init();
+}
diff --git a/src/js/script.js b/src/js/script.js
deleted file mode 100644
index 8ba3d82..0000000
--- a/src/js/script.js
+++ /dev/null
@@ -1,87 +0,0 @@
-import Prism from 'prismjs';
-import 'prismjs/components/prism-javascript';
-import 'prismjs/components/prism-css';
-import 'prismjs/components/prism-markup';
-import { initOneko } from './oneko.js';
-
-(() => {
- const toggleButton = document.getElementById("theme-toggle");
- const html = document.documentElement;
-
- const sessionTheme = sessionStorage.getItem("theme");
- const systemPrefersDark = window.matchMedia(
- "(prefers-color-scheme: dark)",
- ).matches;
-
- const initialTheme = sessionTheme || (systemPrefersDark ? "dark" : "light");
-
- if (initialTheme === "dark") {
- html.setAttribute("data-theme", "dark");
- toggleButton.checked = true;
- }
-
- toggleButton.addEventListener("change", () => {
- const theme = html.getAttribute("data-theme");
-
- if (theme === "dark") {
- html.removeAttribute("data-theme");
- sessionStorage.setItem("theme", "light");
- toggleButton.checked = false;
- } else {
- html.setAttribute("data-theme", "dark");
- sessionStorage.setItem("theme", "dark");
- toggleButton.checked = true;
- }
- });
-})();
-
-(() => {
- const colors = [
- "#ff69b4",
- "#b19cd9",
- "#8b6f47",
- "#ff85c0",
- "#c4b5fd",
- "#d4a574",
- ];
- const shapes = ["❀", "✿", "✽", "✾", "✻", "❊", "❋", "✼"];
-
- document.addEventListener("mousemove", (e) => {
- createParticle(e.clientX, e.clientY);
- });
-
- const createParticle = (x, y) => {
- const particle = document.createElement("div");
- particle.className = "fairy-dust";
-
- const shape = shapes[Math.floor(Math.random() * shapes.length)];
- const size = Math.random() * 8 + 6;
- const color = colors[Math.floor(Math.random() * colors.length)];
- const offsetX = (Math.random() - 0.5) * 20;
- const offsetY = (Math.random() - 0.5) * 20;
- const rotation = Math.random() * 360;
-
- particle.textContent = shape;
- particle.style.cssText = `
- position: fixed;
- left: ${x + offsetX}px;
- top: ${y + offsetY}px;
- font-size: ${size}px;
- color: ${color};
- opacity: 0.4;
- pointer-events: none;
- z-index: 9001; /* it's over 9000 */
- line-height: 1;
- transform: rotate(${rotation}deg);
- animation: fairy-float 0.8s ease-out forwards;
- `;
-
- document.body.appendChild(particle);
- setTimeout(() => particle.remove(), 800);
- };
-})();
-
-document.addEventListener('DOMContentLoaded', () => {
- Prism.highlightAll();
- initOneko();
-});
diff --git a/src/js/script.ts b/src/js/script.ts
new file mode 100644
index 0000000..e0b0b85
--- /dev/null
+++ b/src/js/script.ts
@@ -0,0 +1,78 @@
+import Prism from 'prismjs';
+import 'prismjs/components/prism-javascript';
+import 'prismjs/components/prism-css';
+import 'prismjs/components/prism-markup';
+import { initOneko } from './oneko';
+
+(() => {
+ const toggleButton = document.getElementById('theme-toggle') as HTMLInputElement;
+ const html = document.documentElement;
+
+ const sessionTheme = sessionStorage.getItem('theme');
+ const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
+
+ const initialTheme = sessionTheme || (systemPrefersDark ? 'dark' : 'light');
+
+ if (initialTheme === 'dark') {
+ html.setAttribute('data-theme', 'dark');
+ toggleButton.checked = true;
+ }
+
+ toggleButton.addEventListener('change', () => {
+ const theme = html.getAttribute('data-theme');
+
+ if (theme === 'dark') {
+ html.removeAttribute('data-theme');
+ sessionStorage.setItem('theme', 'light');
+ toggleButton.checked = false;
+ } else {
+ html.setAttribute('data-theme', 'dark');
+ sessionStorage.setItem('theme', 'dark');
+ toggleButton.checked = true;
+ }
+ });
+})();
+
+(() => {
+ const colors = ['#ff69b4', '#b19cd9', '#8b6f47', '#ff85c0', '#c4b5fd', '#d4a574'];
+ const shapes = ['❀', '✿', '✽', '✾', '✻', '❊', '❋', '✼'];
+
+ document.addEventListener('mousemove', (e: MouseEvent) => {
+ createParticle(e.clientX, e.clientY);
+ });
+
+ const createParticle = (x: number, y: number) => {
+ const particle = document.createElement('div');
+ particle.className = 'fairy-dust';
+
+ const shape = shapes[Math.floor(Math.random() * shapes.length)];
+ const size = Math.random() * 8 + 6;
+ const color = colors[Math.floor(Math.random() * colors.length)];
+ const offsetX = (Math.random() - 0.5) * 20;
+ const offsetY = (Math.random() - 0.5) * 20;
+ const rotation = Math.random() * 360;
+
+ particle.textContent = shape;
+ particle.style.cssText = `
+ position: fixed;
+ left: ${x + offsetX}px;
+ top: ${y + offsetY}px;
+ font-size: ${size}px;
+ color: ${color};
+ opacity: 0.4;
+ pointer-events: none;
+ z-index: 9001; /* it's over 9000 */
+ line-height: 1;
+ transform: rotate(${rotation}deg);
+ animation: fairy-float 0.8s ease-out forwards;
+ `;
+
+ document.body.appendChild(particle);
+ setTimeout(() => particle.remove(), 800);
+ };
+})();
+
+document.addEventListener('DOMContentLoaded', () => {
+ Prism.highlightAll();
+ initOneko();
+});
diff --git a/src/types/global.d.ts b/src/types/global.d.ts
new file mode 100644
index 0000000..21aec91
--- /dev/null
+++ b/src/types/global.d.ts
@@ -0,0 +1,7 @@
+declare global {
+ interface Window {
+ ASSET_BASE?: string;
+ }
+}
+
+export {};
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..cba458e
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "ESNext",
+ "lib": ["ES2020", "DOM"],
+ "declaration": false,
+ "sourceMap": true,
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "moduleResolution": "node",
+ "resolveJsonModule": true
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist"]
+}