Compare commits

...

10 commits

Author SHA1 Message Date
Fi3w0
592d19fea6 style: restore local ticker icons 2026-08-11 17:34:51 +02:00
Fi3w0
2ecac616d5 style: restore profile card outline 2026-08-11 17:30:50 +02:00
Fi3w0
cc30d0d44a style: remove avatar card borders 2026-08-11 17:29:28 +02:00
Fi3w0
cd591d31fe fix: show avatar transparency 2026-08-11 17:27:23 +02:00
Fi3w0
25ae07d364 chore: prepare homepage repository for publishing 2026-08-11 17:25:11 +02:00
Fi3w0
f8ac123283 security: harden static site deployment 2026-08-11 17:16:48 +02:00
Fi3w0
57636e230a style: reveal liquid behind terminal 2026-08-11 17:03:33 +02:00
Fi3w0
a817460fa7 feat: polish Moonlit Shell showcase 2026-08-11 16:59:43 +02:00
Fi3w0
ccdd2d6e40 feat: polish featured project cards 2026-08-11 16:54:39 +02:00
Fi3w0
98beb0cbf4 feat: refresh projects from GitHub 2026-08-11 16:48:12 +02:00
37 changed files with 1815 additions and 1672 deletions

View file

@ -6,5 +6,10 @@ dist
node_modules node_modules
npm-debug.log* npm-debug.log*
.DS_Store .DS_Store
.env .env*
DEPLOY.md !.env.example
.github
.openai
.agents
.codex
*.md

5
.gitignore vendored
View file

@ -1,7 +1,10 @@
node_modules node_modules
dist dist
.cache .cache
.npm
.idea/ .idea/
*.iml *.iml
*.log
.DS_Store .DS_Store
.env .env*
!.env.example

View file

@ -6,9 +6,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>404 - Fi3w0</title> <title>404 - Fi3w0</title>
<meta name="robots" content="noindex"> <meta name="robots" content="noindex">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Archivo+Black&amp;family=IBM+Plex+Mono:wght@400;500;600&amp;display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/style.css?v=4"> <link rel="stylesheet" href="/css/style.css?v=4">
<link rel="stylesheet" href="/css/editorial.css?v=4"> <link rel="stylesheet" href="/css/editorial.css?v=4">
<link rel="icon" href="/icon.svg" type="image/svg+xml"> <link rel="icon" href="/icon.svg" type="image/svg+xml">

View file

@ -1,50 +0,0 @@
# Server deployment
The production image builds the Webpack site with Node and serves the generated `dist/` directory with Nginx. The container does not publish a host port; Traefik reaches it over an external Docker network.
## Defaults
| Setting | Default |
| --- | --- |
| Domain | `fiwlabs.dev` |
| Traefik network | `proxy` |
| HTTPS entrypoint | `websecure` |
| Certificate resolver | `porkbun` |
Compose reads overrides from `.env`. The available keys and defaults are documented in `.env.example`.
## Prerequisites
1. Point the `A` and optional `AAAA` records for `fiwlabs.dev` to the server.
2. Ensure Traefik has its Docker provider enabled.
3. Attach Traefik to the same external network configured by `TRAEFIK_NETWORK`.
4. Ensure the configured certificate resolver exists in Traefik.
Create the default shared network once if the Traefik stack has not already created it:
```sh
docker network create proxy
```
## Deploy
Validate the resolved Compose configuration:
```sh
docker compose -f docker-compose.yml config
```
Build and start the site:
```sh
docker compose -f docker-compose.yml up --build -d
```
Check container health and logs:
```sh
docker compose -f docker-compose.yml ps
docker compose -f docker-compose.yml logs -f fiws-page
```
The container exposes `/healthz` internally for Docker health checks. Traefik serves the site at `https://fiwlabs.dev`; the server's `web` entrypoint redirects HTTP requests to HTTPS globally.

View file

@ -1,21 +1,29 @@
# syntax=docker/dockerfile:1.7 # syntax=docker/dockerfile:1.7
FROM node:22-alpine AS build FROM node:24-alpine AS build
WORKDIR /app WORKDIR /app
COPY package.json package-lock.json ./ COPY package.json package-lock.json ./
RUN npm ci --ignore-scripts RUN npm ci --ignore-scripts --no-audit --no-fund
COPY . . COPY webpack.common.js webpack.config.prod.js ./
COPY index.html 404.html favicon.ico icon.png icon.svg robots.txt site.webmanifest ./
COPY css ./css
COPY img ./img
COPY js ./js
RUN npm run build RUN npm run build
FROM nginx:1.27-alpine AS runtime FROM nginxinc/nginx-unprivileged:1.29.4-alpine AS runtime
COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf COPY --chown=101:101 deploy/nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html COPY --chown=101:101 --from=build /app/dist /usr/share/nginx/html
EXPOSE 80 USER 101:101
EXPOSE 8080
STOPSIGNAL SIGQUIT
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget -q -O /dev/null http://127.0.0.1/healthz || exit 1 CMD wget -q -O /dev/null http://127.0.0.1:8080/healthz || exit 1

View file

@ -1,4 +1,7 @@
Copyright (c) HTML5 Boilerplate MIT License
Copyright (c) 2026 Alex (Fi3w0)
Portions copyright (c) HTML5 Boilerplate
Permission is hereby granted, free of charge, to any person obtaining a copy of Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in this software and associated documentation files (the "Software"), to deal in

93
README.md Normal file
View file

@ -0,0 +1,93 @@
# FIW Labs Homepage
The source for [fiwlabs.dev](https://fiwlabs.dev), the personal portfolio of Alex (`Fi3w0`). It presents systems and desktop projects including TideWM, Scorium, Flux, Moonlit Shell, and the FIW Minecraft tooling collection.
The site is intentionally framework-light: semantic HTML, hand-written CSS, and vanilla JavaScript are compiled into a static production bundle. There is no analytics, telemetry, backend, database, or runtime dependency on third-party fonts, icons, or scripts.
## Highlights
- Responsive editorial interface with accessible navigation and reduced-motion support.
- Interactive liquid ASCII artwork with graceful text fallbacks.
- Self-contained assets and strict browser security headers.
- Reproducible Webpack production build.
- Unprivileged, read-only Nginx container designed to sit behind Traefik.
## Technology
| Area | Choice |
| --- | --- |
| Interface | HTML, CSS, vanilla JavaScript |
| Build | Node.js 24, Webpack 5 |
| Runtime | Unprivileged Nginx on port `8080` |
| Deployment | Docker Compose and Traefik |
| Privacy | No analytics or third-party runtime requests |
## Local development
Requirements: Node.js 24 and npm.
```sh
npm ci
npm start
```
Create the production bundle with:
```sh
npm run build
```
Webpack writes the static site to `dist/`.
## Production deployment
The Compose stack expects an existing external Docker network shared with Traefik. Copy the example environment file and adjust it for the target host:
```sh
cp .env.example .env
docker network create proxy
docker compose config
docker compose up --build -d
```
The network only needs to be created once. If Traefik already owns it, skip that command.
| Variable | Default | Purpose |
| --- | --- | --- |
| `DOMAIN` | `fiwlabs.dev` | Traefik host rule |
| `TRAEFIK_NETWORK` | `proxy` | Shared external network |
| `TRAEFIK_HTTPS_ENTRYPOINT` | `websecure` | HTTPS entrypoint |
| `TRAEFIK_CERT_RESOLVER` | `porkbun` | Configured certificate resolver |
| `IMAGE_NAME` | `fiws-page` | Local/container registry image name |
| `IMAGE_TAG` | `latest` | Image tag to deploy |
Check the deployment with:
```sh
docker compose ps
docker compose logs --tail=100 fiws-page
```
Docker monitors `/healthz` inside the container. The service is exposed only to the shared Docker network; it does not publish a host port.
## Security posture
The production container runs as an unprivileged user with a read-only filesystem, all Linux capabilities dropped, privilege escalation disabled, and bounded CPU, memory, process, temporary-storage, and log usage. Nginx serves only static files and applies CSP, HSTS, referrer, permissions, framing, and content-type protections.
Keep secrets out of the frontend and out of Docker build arguments. Everything delivered to a browser is public by definition.
## Repository layout
```text
css/ Site styles
deploy/nginx.conf Static server and security headers
img/ Optimized project artwork
js/app.js Interaction and visual effects
Dockerfile Multi-stage production image
docker-compose.yml Traefik-connected runtime
webpack.*.js Development and production builds
```
## License
The website source is available under the [MIT License](LICENSE.txt). Product names, screenshots, logos, and third-party marks retain their respective rights.

View file

@ -16,8 +16,8 @@
--cyan: #8fd4d0; --cyan: #8fd4d0;
--lime: #c8ee80; --lime: #c8ee80;
--yellow: #f3d277; --yellow: #f3d277;
--display: "Archivo Black", "Arial Black", sans-serif; --display: "Arial Black", "Helvetica Neue", system-ui, sans-serif;
--mono: "IBM Plex Mono", "Courier New", monospace; --mono: ui-monospace, "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", monospace;
--page-pad: clamp(1.25rem, 4vw, 4rem); --page-pad: clamp(1.25rem, 4vw, 4rem);
--max-width: 92rem; --max-width: 92rem;
--radius-sm: 0; --radius-sm: 0;
@ -315,7 +315,7 @@ code {
overflow: visible; overflow: visible;
border: 2px solid rgba(143, 212, 208, 0.35); border: 2px solid rgba(143, 212, 208, 0.35);
border-radius: 0; border-radius: 0;
background: var(--bg-soft); background: rgba(23, 37, 46, 0.8);
box-shadow: none; box-shadow: none;
transform: rotate(1deg); transform: rotate(1deg);
transition: transform 220ms ease, border-color 220ms ease; transition: transform 220ms ease, border-color 220ms ease;
@ -391,7 +391,7 @@ code {
.profile-chip img { .profile-chip img {
width: 4rem; width: 4rem;
height: 4rem; height: 4rem;
border: 1px solid var(--line); border: 0;
border-radius: 0; border-radius: 0;
} }
@ -732,101 +732,6 @@ code {
transform: translateX(1.2rem); transform: translateX(1.2rem);
} }
.current-project.has-tide-ascii .current-watermark {
opacity: 0;
}
/* ASCIIText, ported from React Bits to vanilla JS/three.js. Lazily mounted
(see app.js) to replace the flat "TIDE" watermark with a rippling,
wave-distorted ASCII render a nod to the compositor's water theme.
Falls back to the plain watermark above if WebGL/three.js is unavailable. */
.tide-ascii {
position: absolute;
z-index: 1;
bottom: -9%;
left: -6%;
width: min(82rem, 126%);
height: 55%;
opacity: 0;
pointer-events: none;
transition: opacity 900ms ease;
}
.tide-ascii.is-ready {
opacity: 0.6;
}
.tide-ascii-inner {
position: absolute;
inset: 0;
}
.tide-ascii-canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0.7;
filter: saturate(1.4);
image-rendering: pixelated;
}
.tide-ascii-pre {
position: absolute;
top: 0;
left: 0;
z-index: 2;
margin: 0;
padding: 0;
overflow: hidden;
color: rgba(17, 23, 26, 0.86);
line-height: 1em;
mix-blend-mode: multiply;
white-space: pre;
user-select: none;
}
@media (max-width: 68rem) {
.tide-ascii {
display: none;
}
.current-project.has-tide-ascii .current-watermark {
opacity: 1;
}
}
.project-visual-flux.has-flux-ascii .flux-watermark {
opacity: 0;
}
.flux-ascii {
position: absolute;
z-index: 1;
bottom: 34%;
left: -14%;
width: min(58rem, 118%);
height: 88%;
opacity: 0;
pointer-events: none;
transition: opacity 900ms ease;
}
.flux-ascii.is-ready {
opacity: 0.85;
}
@media (max-width: 64rem) {
.flux-ascii {
display: none;
}
.project-visual-flux.has-flux-ascii .flux-watermark {
opacity: 1;
}
}
.project-feature { .project-feature {
min-height: 38rem; min-height: 38rem;
margin-bottom: 4rem; margin-bottom: 4rem;
@ -885,6 +790,210 @@ code {
font-size: 0.86rem; font-size: 0.86rem;
} }
/* Scorium is code-native, so its project visual is a small editorial
language specimen rather than a fake product screenshot. */
.project-visual-scorium {
display: block;
overflow: hidden;
background:
linear-gradient(rgba(17, 23, 26, 0.08) 1px, transparent 1px),
linear-gradient(90deg, rgba(17, 23, 26, 0.08) 1px, transparent 1px),
var(--paper-deep);
background-size: 2.25rem 2.25rem;
color: var(--text);
}
.project-visual-scorium::before {
position: absolute;
z-index: 0;
top: -16%;
right: -5%;
width: 55%;
height: 72%;
transform: rotate(12deg);
background: var(--coral);
content: "";
clip-path: polygon(26% 0, 100% 0, 100% 78%, 0 100%);
}
.scorium-window {
position: absolute;
z-index: 2;
top: 8%;
right: 8%;
bottom: 21%;
left: 7%;
display: grid;
overflow: hidden;
border: 3px solid var(--ink);
background: #111b20;
box-shadow: 0.9rem 0.9rem 0 rgba(17, 23, 26, 0.2);
grid-template-rows: auto 1fr auto;
}
.scorium-titlebar {
display: grid;
min-height: 3.5rem;
padding: 0 1rem;
border-bottom: 2px solid rgba(237, 241, 233, 0.22);
color: var(--muted);
font-family: var(--mono);
font-size: 0.62rem;
letter-spacing: 0.04em;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 1rem;
}
.scorium-titlebar b {
color: var(--coral-bright);
letter-spacing: 0.13em;
}
.scorium-titlebar span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.scorium-titlebar i {
color: var(--lime);
font-style: normal;
}
.scorium-code {
align-self: center;
min-width: 0;
margin: 0;
padding: clamp(1.2rem, 3vw, 2.2rem);
overflow: hidden;
border: 0;
background: transparent;
color: var(--text);
font: 500 clamp(0.67rem, 1.05vw, 0.93rem)/1.65 var(--mono);
white-space: pre;
}
.scorium-code .scorium-dim {
display: inline-block;
width: 2.25em;
color: #60737b;
font-weight: 400;
}
.scorium-line {
display: block;
min-height: 1.65em;
}
.scorium-indent {
display: inline-block;
width: 2.2em;
}
.scorium-indent-double {
width: 4.4em;
}
.scorium-code b {
color: var(--cyan);
font-weight: 600;
}
.scorium-code em {
color: var(--coral-bright);
font-style: normal;
}
.scorium-code strong {
color: var(--lime);
font-weight: 500;
}
.scorium-code i {
color: var(--yellow);
font-style: normal;
}
.scorium-diagnostic {
display: flex;
min-height: 2.9rem;
padding: 0 1rem;
border-top: 2px solid rgba(237, 241, 233, 0.16);
color: var(--muted);
font-family: var(--mono);
font-size: 0.61rem;
letter-spacing: 0.03em;
align-items: center;
gap: 0.65rem;
}
.scorium-diagnostic i {
width: 0.55rem;
height: 0.55rem;
flex: 0 0 auto;
background: var(--lime);
}
.scorium-pipeline {
position: absolute;
z-index: 2;
right: 2rem;
bottom: 2rem;
left: 2rem;
display: flex;
padding-top: 0.8rem;
border-top: 3px solid var(--ink);
color: var(--ink);
font-family: var(--mono);
font-size: clamp(0.54rem, 0.8vw, 0.68rem);
letter-spacing: 0.05em;
text-transform: uppercase;
align-items: center;
justify-content: space-between;
gap: 0.6rem;
}
.scorium-pipeline b {
font-weight: 600;
}
.scorium-pipeline i {
color: #765b4d;
font-style: normal;
}
.scorium-watermark {
position: absolute;
z-index: 1;
right: -0.04em;
bottom: -0.2em;
color: rgba(17, 23, 26, 0.1);
font-family: var(--display);
font-size: clamp(7rem, 15vw, 14rem);
letter-spacing: -0.08em;
line-height: 0.8;
pointer-events: none;
}
.project-visual-scorium .image-label {
z-index: 3;
right: auto;
bottom: auto;
left: 0;
top: 0;
transform: translate(-2px, -100%);
}
.project-feature-scorium .project-feature-copy {
border-top-color: var(--coral);
}
.scorium-tags {
margin: -0.2rem 0 1.4rem;
color: var(--muted);
}
.project-facts { .project-facts {
border-top-width: 3px; border-top-width: 3px;
border-bottom-width: 2px; border-bottom-width: 2px;
@ -1764,6 +1873,45 @@ code {
min-height: 20rem; min-height: 20rem;
} }
.project-visual-scorium {
min-height: 28rem;
}
.scorium-window {
top: 6%;
right: 1.1rem;
bottom: 19%;
left: 1.1rem;
box-shadow: 0.55rem 0.55rem 0 rgba(17, 23, 26, 0.2);
}
.scorium-titlebar {
min-height: 3rem;
}
.scorium-titlebar span {
display: none;
}
.scorium-titlebar {
grid-template-columns: 1fr auto;
}
.scorium-code {
padding: 1rem;
font-size: clamp(0.56rem, 2.35vw, 0.72rem);
}
.scorium-pipeline {
right: 1rem;
bottom: 1.35rem;
left: 1rem;
}
.scorium-pipeline i {
display: none;
}
.project-card-cool { .project-card-cool {
width: 94%; width: 94%;
} }
@ -1899,6 +2047,617 @@ code {
} }
} }
/* Product-led project cards: quieter surfaces, tighter hierarchy, and real
product evidence in place of the earlier oversized ASCII treatments. */
.current-project-tide {
min-height: 36rem;
border: 1px solid rgba(143, 212, 208, 0.55);
background:
radial-gradient(circle at 8% 100%, rgba(92, 196, 192, 0.16), transparent 28rem),
linear-gradient(135deg, #102832 0%, #101d26 58%, #0c171e 100%);
color: var(--text);
grid-template-columns: minmax(0, 1.4fr) minmax(21rem, 0.72fr);
box-shadow: 0 1.8rem 5rem rgba(3, 9, 13, 0.24);
}
.current-project-tide:hover {
transform: translateY(-0.2rem);
border-color: var(--cyan);
}
.current-project-tide::after {
position: absolute;
z-index: 0;
right: 24%;
bottom: -14rem;
width: 44rem;
height: 25rem;
border: 1px solid rgba(143, 212, 208, 0.12);
border-radius: 50%;
box-shadow:
0 -2.5rem 0 rgba(143, 212, 208, 0.025),
0 -5rem 0 rgba(143, 212, 208, 0.025),
0 -7.5rem 0 rgba(143, 212, 208, 0.025);
content: "";
pointer-events: none;
}
.current-project-tide .current-project-main {
padding: clamp(2rem, 4vw, 3.8rem);
}
.current-project-tide .current-meta {
border-bottom-color: rgba(143, 212, 208, 0.3);
color: var(--muted);
}
.current-project-tide .current-meta > span:last-child {
color: var(--cyan);
}
.current-project-tide .current-meta i {
border-radius: 50%;
background: var(--lime);
}
.current-project-tide .current-kicker {
margin: clamp(3.2rem, 7vw, 6rem) 0 0.9rem;
color: var(--cyan);
}
.current-project-tide .tide-heading {
gap: clamp(0.8rem, 1.8vw, 1.4rem);
}
.current-project-tide .tide-heading h3 {
color: var(--paper);
font-size: clamp(4.2rem, 7.4vw, 7.4rem);
line-height: 0.88;
}
.current-project-tide .tide-mark {
width: clamp(3rem, 4.5vw, 4.2rem);
height: clamp(3rem, 4.5vw, 4.2rem);
filter: drop-shadow(0 0.7rem 1rem rgba(60, 166, 182, 0.22));
}
.current-project-tide .current-project-main > p:not(.current-kicker) {
max-width: 50rem;
margin: 1.8rem 0 0;
color: #c7d4d2;
font-size: 0.9rem;
line-height: 1.72;
}
.tide-proof {
position: relative;
z-index: 2;
display: grid;
max-width: 48rem;
margin: 2rem 0 1.7rem;
border-top: 1px solid rgba(143, 212, 208, 0.32);
border-bottom: 1px solid rgba(143, 212, 208, 0.2);
grid-template-columns: repeat(3, 1fr);
}
.tide-proof div {
padding: 1rem 0.9rem 1rem 0;
}
.tide-proof div + div {
padding-left: 1rem;
border-left: 1px solid rgba(143, 212, 208, 0.2);
}
.tide-proof strong,
.tide-proof span {
display: block;
}
.tide-proof strong {
color: var(--lime);
font-family: var(--display);
font-size: clamp(1.2rem, 2vw, 1.8rem);
font-weight: 400;
letter-spacing: -0.04em;
}
.tide-proof span {
margin-top: 0.2rem;
color: var(--muted);
font-size: 0.57rem;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.current-project-tide .current-tags {
color: var(--muted);
}
.current-project-tide .current-repo-link {
color: var(--text);
}
.current-project-tide .current-roadmap {
z-index: 2;
padding: clamp(2rem, 3.2vw, 3rem);
border-left: 1px solid rgba(143, 212, 208, 0.26);
background: rgba(7, 18, 24, 0.42);
color: var(--text);
}
.current-project-tide .current-roadmap > p {
color: var(--cyan);
}
.current-project-tide .current-roadmap ol {
border-top-color: rgba(143, 212, 208, 0.55);
}
.current-project-tide .current-roadmap li {
min-height: 6.2rem;
border-bottom-color: rgba(143, 212, 208, 0.2);
grid-template-columns: 1.7rem minmax(0, 1fr) auto;
}
.current-project-tide .current-roadmap li > span {
color: #6ca3a7;
}
.current-project-tide .current-roadmap strong {
color: var(--text);
}
.current-project-tide .current-roadmap small {
max-width: 28rem;
color: #9eb1b2;
line-height: 1.45;
overflow-wrap: anywhere;
}
.current-project-tide .current-roadmap li > b {
background: rgba(200, 238, 128, 0.12);
color: var(--lime);
}
.current-project-tide .current-roadmap li:last-child > b {
background: rgba(240, 153, 110, 0.12);
color: var(--coral-bright);
}
.current-project-tide .current-watermark {
right: auto;
bottom: -0.24em;
left: -0.04em;
color: rgba(143, 212, 208, 0.045);
font-size: clamp(10rem, 18vw, 17rem);
}
.project-visual-flux {
min-height: 36rem;
padding: 0;
border-color: rgba(186, 177, 238, 0.55);
background:
radial-gradient(circle at 88% 12%, rgba(184, 146, 255, 0.2), transparent 24rem),
linear-gradient(145deg, #1a1d2a, #111821 72%);
}
.project-visual-flux::after {
display: none;
}
.flux-product-frame {
position: absolute;
top: 2.2rem;
right: 2.2rem;
bottom: 6.5rem;
left: 2.2rem;
display: grid;
min-width: 0;
overflow: hidden;
border: 1px solid rgba(237, 241, 233, 0.28);
background: #0d1015;
box-shadow: 1rem 1rem 0 rgba(111, 92, 170, 0.14);
grid-template-rows: auto minmax(0, 1fr);
}
.flux-product-bar {
display: grid;
min-height: 2.9rem;
padding: 0 1rem;
border-bottom: 1px solid rgba(237, 241, 233, 0.16);
color: var(--muted);
font-family: var(--mono);
font-size: 0.58rem;
letter-spacing: 0.06em;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 0.9rem;
text-transform: uppercase;
}
.flux-product-bar > span {
display: flex;
gap: 0.35rem;
}
.flux-product-bar i {
width: 0.55rem;
height: 0.55rem;
border-radius: 50%;
background: var(--coral);
}
.flux-product-bar i:nth-child(2) {
background: var(--yellow);
}
.flux-product-bar i:nth-child(3) {
background: var(--lime);
}
.flux-product-bar b {
color: var(--text);
font-weight: 500;
}
.flux-product-bar em {
color: var(--cyan);
font-style: normal;
}
.project-visual-flux .flux-product-shot {
position: relative;
width: 100%;
height: 100%;
min-height: 0;
border: 0;
filter: saturate(0.84) contrast(1.04);
object-fit: cover;
object-position: left center;
transform: none;
transition: filter 250ms ease, transform 500ms ease;
}
.project-visual-flux:hover .flux-product-shot {
filter: saturate(1) contrast(1.03);
transform: scale(1.012);
}
.flux-product-caption {
position: absolute;
z-index: 3;
right: 2.2rem;
bottom: 1.5rem;
display: flex;
min-width: 18rem;
padding: 0.65rem 0.8rem;
border: 1px solid rgba(237, 241, 233, 0.25);
background: #171d27;
color: var(--text);
align-items: center;
gap: 0.8rem;
}
.project-visual-flux .flux-product-caption img {
width: 2.8rem;
height: 2.8rem;
border-radius: 22%;
filter: none;
object-fit: contain;
transform: none;
}
.flux-product-caption span,
.flux-product-caption strong,
.flux-product-caption small {
display: block;
}
.flux-product-caption strong {
font-family: var(--mono);
font-size: 0.68rem;
font-weight: 600;
}
.flux-product-caption small {
margin-top: 0.15rem;
color: var(--muted);
font-size: 0.55rem;
}
.project-visual-flux .image-label {
right: auto;
bottom: 1.8rem;
left: 2.2rem;
border-color: rgba(237, 241, 233, 0.22);
}
.project-visual-moonlit {
min-height: 36rem;
overflow: hidden;
border-color: rgba(113, 177, 179, 0.58);
background:
radial-gradient(circle at 12% 90%, rgba(65, 147, 157, 0.18), transparent 24rem),
linear-gradient(145deg, #0c2028, #0a141b 72%);
}
.project-visual-moonlit::after {
display: none;
}
.moonlit-screen {
position: absolute;
top: 2.2rem;
right: 2.2rem;
bottom: 6.6rem;
left: 2.2rem;
display: block;
overflow: hidden;
border: 1px solid rgba(143, 212, 208, 0.42);
background: #071219;
box-shadow: 1rem 1rem 0 rgba(31, 103, 112, 0.15);
}
.moonlit-camera {
position: absolute;
z-index: 3;
top: 0.45rem;
left: 50%;
width: 0.35rem;
height: 0.35rem;
transform: translateX(-50%);
border: 1px solid rgba(237, 241, 233, 0.22);
border-radius: 50%;
background: #03090d;
}
.project-visual-moonlit .moonlit-screen > img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
filter: saturate(0.92) contrast(1.04) brightness(0.98);
object-fit: contain;
object-position: center;
transform: none;
transition: filter 300ms ease, transform 650ms cubic-bezier(0.2, 0.7, 0.2, 1);
}
.project-visual-moonlit:hover .moonlit-screen > img {
filter: saturate(1) contrast(1.03) brightness(1.03);
transform: scale(1.008);
}
.moonlit-session {
position: absolute;
z-index: 3;
right: 1rem;
bottom: 1rem;
left: 1rem;
display: grid;
min-height: 3.4rem;
padding: 0.7rem 0.85rem;
border: 1px solid rgba(143, 212, 208, 0.28);
background: rgba(5, 17, 24, 0.84);
color: var(--text);
font-family: var(--mono);
grid-template-columns: 1fr auto;
align-content: center;
backdrop-filter: blur(0.5rem);
}
.moonlit-session b,
.moonlit-session span,
.moonlit-session i {
display: block;
}
.moonlit-session b {
font-size: 0.68rem;
font-weight: 600;
}
.moonlit-session span {
margin-top: 0.12rem;
color: #9eb7b8;
font-size: 0.54rem;
}
.moonlit-session i {
color: var(--lime);
font-size: 0.56rem;
font-style: normal;
letter-spacing: 0.08em;
grid-column: 2;
grid-row: 1 / 3;
align-self: center;
}
.moonlit-specs {
position: absolute;
z-index: 3;
right: 2.2rem;
bottom: 1.5rem;
display: flex;
color: #aebfc0;
font-family: var(--mono);
font-size: 0.54rem;
letter-spacing: 0.03em;
gap: 1.1rem;
}
.moonlit-specs span + span::before {
margin-right: 1.1rem;
color: #4c6e72;
content: "/";
}
.moonlit-specs b {
color: var(--cyan);
font-weight: 600;
text-transform: uppercase;
}
.project-visual-moonlit .image-label {
right: auto;
bottom: 1.35rem;
left: 2.2rem;
border-color: rgba(143, 212, 208, 0.24);
}
article[data-repo="Moonlit-shell"] .project-feature-copy {
border-top-color: var(--cyan);
}
.contact-links {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
@media (max-width: 68rem) {
.current-project-tide {
grid-template-columns: 1fr;
}
.current-project-tide .current-roadmap {
border-top: 1px solid rgba(143, 212, 208, 0.26);
border-left: 0;
}
.contact-links {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.contact-links > *:nth-child(odd) {
padding-left: 0;
border-left: 0;
}
.contact-links > *:nth-child(n + 3) {
border-top: 2px solid var(--ink);
}
}
@media (max-width: 52rem) {
.current-project-tide .current-kicker {
margin-top: 3rem;
}
.current-project-tide .tide-heading h3 {
font-size: clamp(3.2rem, 15vw, 5rem);
}
.tide-proof {
grid-template-columns: 1fr;
}
.tide-proof div,
.tide-proof div + div {
padding: 0.75rem 0;
border-top: 1px solid rgba(143, 212, 208, 0.18);
border-left: 0;
}
.tide-proof div:first-child {
border-top: 0;
}
.project-visual-flux {
min-height: 27rem;
}
.project-visual-moonlit {
min-height: 27rem;
}
.flux-product-frame {
top: 1rem;
right: 1rem;
bottom: 5.8rem;
left: 1rem;
box-shadow: 0.55rem 0.55rem 0 rgba(111, 92, 170, 0.14);
}
.flux-product-bar {
padding: 0 0.7rem;
}
.flux-product-caption {
right: 1rem;
bottom: 0.9rem;
min-width: 0;
}
.project-visual-flux .image-label {
bottom: 1.2rem;
left: 1rem;
}
.moonlit-screen {
top: 1rem;
right: 1rem;
bottom: 5.8rem;
left: 1rem;
box-shadow: 0.55rem 0.55rem 0 rgba(31, 103, 112, 0.15);
}
.moonlit-session {
right: 0.6rem;
bottom: 0.6rem;
left: 0.6rem;
}
.moonlit-specs {
right: 1rem;
bottom: 1.25rem;
gap: 0.55rem;
}
.moonlit-specs span + span::before {
margin-right: 0.55rem;
}
.project-visual-moonlit .image-label {
bottom: 1.1rem;
left: 1rem;
}
.contact-links {
grid-template-columns: 1fr;
}
.contact-links > * + *,
.contact-links > *:nth-child(n + 3) {
padding-left: 0;
border-top: 0;
border-left: 0;
}
}
@media (max-width: 36rem) {
.current-project-tide .current-project-main,
.current-project-tide .current-roadmap {
padding: 1.35rem;
}
.flux-product-caption {
display: none;
}
.moonlit-session span,
.moonlit-specs {
display: none;
}
.moonlit-session {
grid-template-columns: 1fr auto;
}
.project-visual-flux .image-label {
right: 1rem;
}
}
@media print { @media print {
.site-header, .site-header,
.signal-strip { .signal-strip {

View file

@ -16,8 +16,8 @@
--cyan: #8fd4d0; --cyan: #8fd4d0;
--lime: #c8ee80; --lime: #c8ee80;
--yellow: #f3d277; --yellow: #f3d277;
--display: "Syne", "Avenir Next", sans-serif; --display: "Arial Black", "Helvetica Neue", system-ui, sans-serif;
--mono: "IBM Plex Mono", "Courier New", monospace; --mono: ui-monospace, "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", monospace;
--page-pad: clamp(1.25rem, 4vw, 4.5rem); --page-pad: clamp(1.25rem, 4vw, 4.5rem);
--max-width: 96rem; --max-width: 96rem;
--radius-sm: 0.5rem; --radius-sm: 0.5rem;
@ -681,7 +681,7 @@ code {
width: 4.1rem; width: 4.1rem;
height: 4.1rem; height: 4.1rem;
border-radius: 0.65rem; border-radius: 0.65rem;
background: var(--paper); background: transparent;
object-fit: contain; object-fit: contain;
} }

View file

@ -1,18 +1,28 @@
server { server {
listen 80 default_server; listen 8080 default_server;
listen [::]:80 default_server; listen [::]:8080 default_server;
server_name _; server_name _;
root /usr/share/nginx/html; root /usr/share/nginx/html;
index index.html; index index.html;
charset utf-8; charset utf-8;
server_tokens off; server_tokens off;
autoindex off;
access_log off;
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Content-Type-Options "nosniff" always; add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always; add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header X-Permitted-Cross-Domain-Policies "none" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; add_header Referrer-Policy "no-referrer" always;
add_header Content-Security-Policy "default-src 'self'; base-uri 'self'; form-action 'none'; frame-ancestors 'none'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data:; connect-src 'self'" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=(), browsing-topics=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Resource-Policy "same-origin" always;
add_header Content-Security-Policy "default-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; object-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self'; img-src 'self' data:; connect-src 'self'; media-src 'self'; manifest-src 'self'; worker-src 'none'; upgrade-insecure-requests" always;
if ($request_method !~ ^(GET|HEAD)$) {
return 405;
}
gzip on; gzip on;
gzip_vary on; gzip_vary on;

View file

@ -2,30 +2,39 @@ name: fiws-page
services: services:
fiws-page: fiws-page:
image: fiws-page:latest image: "${IMAGE_NAME:-fiws-page}:${IMAGE_TAG:-latest}"
container_name: fiws-page
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
target: runtime target: runtime
restart: unless-stopped restart: unless-stopped
init: true init: true
user: "101:101"
read_only: true read_only: true
cap_drop:
- ALL
security_opt: security_opt:
- no-new-privileges:true - no-new-privileges:true
tmpfs: tmpfs:
- /var/cache/nginx - /tmp:rw,noexec,nosuid,nodev,size=16m,uid=101,gid=101,mode=1770
- /var/run pids_limit: 64
- /tmp mem_limit: 128m
cpus: 0.50
stop_grace_period: 10s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
expose: expose:
- "80" - "8080"
networks: networks:
- proxy - proxy
labels: labels:
traefik.enable: "true" traefik.enable: "true"
traefik.docker.network: "${TRAEFIK_NETWORK:-proxy}" traefik.docker.network: "${TRAEFIK_NETWORK:-proxy}"
traefik.http.services.fiws-page.loadbalancer.server.port: "80" traefik.http.services.fiws-page.loadbalancer.server.port: "8080"
traefik.http.routers.fiws-page.rule: "Host(`${DOMAIN:-fiwlabs.dev}`)" traefik.http.routers.fiws-page.rule: "Host(`${DOMAIN:-fiwlabs.dev}`)"
traefik.http.routers.fiws-page.entrypoints: "${TRAEFIK_HTTPS_ENTRYPOINT:-websecure}" traefik.http.routers.fiws-page.entrypoints: "${TRAEFIK_HTTPS_ENTRYPOINT:-websecure}"

BIN
icon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 131 KiB

1
img/icons/archlinux.svg Normal file
View file

@ -0,0 +1 @@
<svg fill="#11171a" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Arch Linux</title><path d="M11.39.605C10.376 3.092 9.764 4.72 8.635 7.132c.693.734 1.543 1.589 2.923 2.554-1.484-.61-2.496-1.224-3.252-1.86C6.86 10.842 4.596 15.138 0 23.395c3.612-2.085 6.412-3.37 9.021-3.862a6.61 6.61 0 01-.171-1.547l.003-.115c.058-2.315 1.261-4.095 2.687-3.973 1.426.12 2.534 2.096 2.478 4.409a6.52 6.52 0 01-.146 1.243c2.58.505 5.352 1.787 8.914 3.844-.702-1.293-1.33-2.459-1.929-3.57-.943-.73-1.926-1.682-3.933-2.713 1.38.359 2.367.772 3.137 1.234-6.09-11.334-6.582-12.84-8.67-17.74zM22.898 21.36v-.623h-.234v-.084h.562v.084h-.234v.623h.331v-.707h.142l.167.5.034.107a2.26 2.26 0 01.038-.114l.17-.493H24v.707h-.091v-.593l-.206.593h-.084l-.205-.602v.602h-.091"/></svg>

After

Width:  |  Height:  |  Size: 780 B

1
img/icons/docker.svg Normal file
View file

@ -0,0 +1 @@
<svg fill="#11171a" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Docker</title><path d="M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.184-.186h-2.12a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z"/></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

1
img/icons/ffmpeg.svg Normal file
View file

@ -0,0 +1 @@
<svg fill="#11171a" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>FFmpeg</title><path d="M21.72 17.91V6.5l-.53-.49L9.05 18.52l-1.29-.06L24 1.53l-.33-.95-11.93 1-5.75 6.6v-.23l4.7-5.39-1.38-.77-9.11.77v2.85l1.91.46v.01l.19-.01-.56.66v10.6c.609-.126 1.22-.241 1.83-.36L14.12 5.22l.83-.04L0 21.44l9.67.82 1.35-.77 6.82-6.74v2.15l-5.72 5.57 11.26.95.35-.94v-3.16l-3.29-.18c.434-.403.858-.816 1.28-1.23z"/></svg>

After

Width:  |  Height:  |  Size: 434 B

View file

@ -0,0 +1 @@
<svg fill="#11171a" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub Actions</title><path d="M10.984 13.836a.5.5 0 0 1-.353-.146l-.745-.743a.5.5 0 1 1 .706-.708l.392.391 1.181-1.18a.5.5 0 0 1 .708.707l-1.535 1.533a.504.504 0 0 1-.354.146zm9.353-.147l1.534-1.532a.5.5 0 0 0-.707-.707l-1.181 1.18-.392-.391a.5.5 0 1 0-.706.708l.746.743a.497.497 0 0 0 .706-.001zM4.527 7.452l2.557-1.585A1 1 0 0 0 7.09 4.17L4.533 2.56A1 1 0 0 0 3 3.406v3.196a1.001 1.001 0 0 0 1.527.85zm2.03-2.436L4 6.602V3.406l2.557 1.61zM24 12.5c0 1.93-1.57 3.5-3.5 3.5a3.503 3.503 0 0 1-3.46-3h-2.08a3.503 3.503 0 0 1-3.46 3 3.502 3.502 0 0 1-3.46-3h-.558c-.972 0-1.85-.399-2.482-1.042V17c0 1.654 1.346 3 3 3h.04c.244-1.693 1.7-3 3.46-3 1.93 0 3.5 1.57 3.5 3.5S13.43 24 11.5 24a3.502 3.502 0 0 1-3.46-3H8c-2.206 0-4-1.794-4-4V9.899A5.008 5.008 0 0 1 0 5c0-2.757 2.243-5 5-5s5 2.243 5 5a5.005 5.005 0 0 1-4.952 4.998A2.482 2.482 0 0 0 7.482 12h.558c.244-1.693 1.7-3 3.46-3a3.502 3.502 0 0 1 3.46 3h2.08a3.503 3.503 0 0 1 3.46-3c1.93 0 3.5 1.57 3.5 3.5zm-15 8c0 1.378 1.122 2.5 2.5 2.5s2.5-1.122 2.5-2.5-1.122-2.5-2.5-2.5S9 19.122 9 20.5zM5 9c2.206 0 4-1.794 4-4S7.206 1 5 1 1 2.794 1 5s1.794 4 4 4zm9 3.5c0-1.378-1.122-2.5-2.5-2.5S9 11.122 9 12.5s1.122 2.5 2.5 2.5 2.5-1.122 2.5-2.5zm9 0c0-1.378-1.122-2.5-2.5-2.5S18 11.122 18 12.5s1.122 2.5 2.5 2.5 2.5-1.122 2.5-2.5zm-13 8a.5.5 0 1 0 1 0 .5.5 0 0 0-1 0zm2 0a.5.5 0 1 0 1 0 .5.5 0 0 0-1 0zm12 0c0 1.93-1.57 3.5-3.5 3.5a3.503 3.503 0 0 1-3.46-3.002c-.007.001-.013.005-.021.005l-.506.017h-.017a.5.5 0 0 1-.016-.999l.506-.017c.018-.002.035.006.052.007A3.503 3.503 0 0 1 20.5 17c1.93 0 3.5 1.57 3.5 3.5zm-1 0c0-1.378-1.122-2.5-2.5-2.5S18 19.122 18 20.5s1.122 2.5 2.5 2.5 2.5-1.122 2.5-2.5z"/></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

1
img/icons/gnubash.svg Normal file
View file

@ -0,0 +1 @@
<svg fill="#11171a" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GNU Bash</title><path d="M21.038,4.9l-7.577-4.498C13.009,0.134,12.505,0,12,0c-0.505,0-1.009,0.134-1.462,0.403L2.961,4.9 C2.057,5.437,1.5,6.429,1.5,7.503v8.995c0,1.073,0.557,2.066,1.462,2.603l7.577,4.497C10.991,23.866,11.495,24,12,24 c0.505,0,1.009-0.134,1.461-0.402l7.577-4.497c0.904-0.537,1.462-1.529,1.462-2.603V7.503C22.5,6.429,21.943,5.437,21.038,4.9z M15.17,18.946l0.013,0.646c0.001,0.078-0.05,0.167-0.111,0.198l-0.383,0.22c-0.061,0.031-0.111-0.007-0.112-0.085L14.57,19.29 c-0.328,0.136-0.66,0.169-0.872,0.084c-0.04-0.016-0.057-0.075-0.041-0.142l0.139-0.584c0.011-0.046,0.036-0.092,0.069-0.121 c0.012-0.011,0.024-0.02,0.036-0.026c0.022-0.011,0.043-0.014,0.062-0.006c0.229,0.077,0.521,0.041,0.802-0.101 c0.357-0.181,0.596-0.545,0.592-0.907c-0.003-0.328-0.181-0.465-0.613-0.468c-0.55,0.001-1.064-0.107-1.072-0.917 c-0.007-0.667,0.34-1.361,0.889-1.8l-0.007-0.652c-0.001-0.08,0.048-0.168,0.111-0.2l0.37-0.236 c0.061-0.031,0.111,0.007,0.112,0.087l0.006,0.653c0.273-0.109,0.511-0.138,0.726-0.088c0.047,0.012,0.067,0.076,0.048,0.151 l-0.144,0.578c-0.011,0.044-0.036,0.088-0.065,0.116c-0.012,0.012-0.025,0.021-0.038,0.028c-0.019,0.01-0.038,0.013-0.057,0.009 c-0.098-0.022-0.332-0.073-0.699,0.113c-0.385,0.195-0.52,0.53-0.517,0.778c0.003,0.297,0.155,0.387,0.681,0.396 c0.7,0.012,1.003,0.318,1.01,1.023C16.105,17.747,15.736,18.491,15.17,18.946z M19.143,17.859c0,0.06-0.008,0.116-0.058,0.145 l-1.916,1.164c-0.05,0.029-0.09,0.004-0.09-0.056v-0.494c0-0.06,0.037-0.093,0.087-0.122l1.887-1.129 c0.05-0.029,0.09-0.004,0.09,0.056V17.859z M20.459,6.797l-7.168,4.427c-0.894,0.523-1.553,1.109-1.553,2.187v8.833 c0,0.645,0.26,1.063,0.66,1.184c-0.131,0.023-0.264,0.039-0.398,0.039c-0.42,0-0.833-0.114-1.197-0.33L3.226,18.64 c-0.741-0.44-1.201-1.261-1.201-2.142V7.503c0-0.881,0.46-1.702,1.201-2.142l7.577-4.498c0.363-0.216,0.777-0.33,1.197-0.33 c0.419,0,0.833,0.114,1.197,0.33l7.577,4.498c0.624,0.371,1.046,1.013,1.164,1.732C21.686,6.557,21.12,6.411,20.459,6.797z"/></svg>

After

Width:  |  Height:  |  Size: 2 KiB

1
img/icons/go.svg Normal file
View file

@ -0,0 +1 @@
<svg fill="#11171a" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Go</title><path d="M1.811 10.231c-.047 0-.058-.023-.035-.059l.246-.315c.023-.035.081-.058.128-.058h4.172c.046 0 .058.035.035.07l-.199.303c-.023.036-.082.07-.117.07zM.047 11.306c-.047 0-.059-.023-.035-.058l.245-.316c.023-.035.082-.058.129-.058h5.328c.047 0 .07.035.058.07l-.093.28c-.012.047-.058.07-.105.07zm2.828 1.075c-.047 0-.059-.035-.035-.07l.163-.292c.023-.035.07-.07.117-.07h2.337c.047 0 .07.035.07.082l-.023.28c0 .047-.047.082-.082.082zm12.129-2.36c-.736.187-1.239.327-1.963.514-.176.046-.187.058-.34-.117-.174-.199-.303-.327-.548-.444-.737-.362-1.45-.257-2.115.175-.795.514-1.204 1.274-1.192 2.22.011.935.654 1.706 1.577 1.835.795.105 1.46-.175 1.987-.77.105-.13.198-.27.315-.434H10.47c-.245 0-.304-.152-.222-.35.152-.362.432-.97.596-1.274a.315.315 0 01.292-.187h4.253c-.023.316-.023.631-.07.947a4.983 4.983 0 01-.958 2.29c-.841 1.11-1.94 1.8-3.33 1.986-1.145.152-2.209-.07-3.143-.77-.865-.655-1.356-1.52-1.484-2.595-.152-1.274.222-2.419.993-3.424.83-1.086 1.928-1.776 3.272-2.02 1.098-.2 2.15-.07 3.096.571.62.41 1.063.97 1.356 1.648.07.105.023.164-.117.2m3.868 6.461c-1.064-.024-2.034-.328-2.852-1.029a3.665 3.665 0 01-1.262-2.255c-.21-1.32.152-2.489.947-3.529.853-1.122 1.881-1.706 3.272-1.95 1.192-.21 2.314-.095 3.33.595.923.63 1.496 1.484 1.648 2.605.198 1.578-.257 2.863-1.344 3.962-.771.783-1.718 1.273-2.805 1.495-.315.06-.63.07-.934.106zm2.78-4.72c-.011-.153-.011-.27-.034-.387-.21-1.157-1.274-1.81-2.384-1.554-1.087.245-1.788.935-2.045 2.033-.21.912.234 1.835 1.075 2.21.643.28 1.285.244 1.905-.07.923-.48 1.425-1.228 1.484-2.233z"/></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

1
img/icons/json.svg Normal file
View file

@ -0,0 +1 @@
<svg fill="#11171a" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>JSON</title><path d="M12.043 23.968c.479-.004.953-.029 1.426-.094a11.805 11.805 0 003.146-.863 12.404 12.404 0 003.793-2.542 11.977 11.977 0 002.44-3.427 11.794 11.794 0 001.02-3.476c.149-1.16.135-2.346-.045-3.499a11.96 11.96 0 00-.793-2.788 11.197 11.197 0 00-.854-1.617c-1.168-1.837-2.861-3.314-4.81-4.3a12.835 12.835 0 00-2.172-.87h-.005c.119.063.24.132.345.201.12.074.239.146.351.225a8.93 8.93 0 011.559 1.33c1.063 1.145 1.797 2.548 2.218 4.041.284.982.434 1.998.495 3.017.044.743.044 1.491-.047 2.229-.149 1.27-.554 2.51-1.228 3.596a7.475 7.475 0 01-1.903 2.084c-1.244.928-2.877 1.482-4.436 1.114a3.916 3.916 0 01-.748-.258 4.692 4.692 0 01-.779-.45 6.08 6.08 0 01-1.244-1.105 6.507 6.507 0 01-1.049-1.747 7.366 7.366 0 01-.494-2.54c-.03-1.273.225-2.553.854-3.67a6.43 6.43 0 011.663-1.918c.225-.178.464-.333.704-.479l.016-.007a5.121 5.121 0 00-1.441-.12 4.963 4.963 0 00-1.228.24c-.359.12-.704.27-1.019.45a6.146 6.146 0 00-.733.494c-.211.18-.42.36-.615.555-1.123 1.153-1.768 2.682-2.022 4.256-.15.973-.15 1.96-.091 2.95.105 1.395.391 2.787.945 4.062a8.518 8.518 0 001.348 2.173 8.14 8.14 0 003.132 2.23 7.934 7.934 0 002.113.54c.074.015.149.015.209.015zm-2.934-.398a4.102 4.102 0 01-.45-.228 8.5 8.5 0 01-2.038-1.534c-1.094-1.137-1.827-2.566-2.247-4.08a15.184 15.184 0 01-.495-3.172 12.14 12.14 0 01.046-2.082c.135-1.257.495-2.501 1.124-3.58a6.889 6.889 0 011.783-2.053 6.23 6.23 0 011.633-.9 5.363 5.363 0 013.522-.045c.029 0 .029 0 .045.03.015.015.045.015.06.03.045.016.104.045.165.074.239.12.479.271.704.42a6.294 6.294 0 012.097 2.502c.42.914.615 1.934.631 2.938.014 1.079-.18 2.157-.645 3.146a6.42 6.42 0 01-2.638 2.832c.09.03.18.045.271.075.225.044.449.074.688.074 1.468.045 2.892-.66 3.94-1.647.195-.18.375-.375.54-.585.225-.27.435-.54.614-.823.239-.375.435-.75.614-1.154a8.112 8.112 0 00.509-1.664c.196-1.004.211-2.022.149-3.026-.135-2.022-.673-4.045-1.842-5.724a9.054 9.054 0 00-.555-.719 9.868 9.868 0 00-1.063-1.034 8.477 8.477 0 00-1.363-.915 9.927 9.927 0 00-1.692-.598l-.3-.06c-.209-.03-.42-.044-.634-.06a8.453 8.453 0 00-1.015.016c-.704.045-1.412.16-2.112.337C5.799 1.227 2.863 3.566 1.3 6.67A11.834 11.834 0 00.238 9.801a11.81 11.81 0 00-.104 3.775c.12 1.02.374 2.023.778 2.977.227.57.511 1.124.825 1.648 1.094 1.783 2.683 3.236 4.51 4.24.688.39 1.408.69 2.157.944.226.074.45.15.689.21z"/></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

1
img/icons/kotlin.svg Normal file
View file

@ -0,0 +1 @@
<svg fill="#11171a" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Kotlin</title><path d="M24 24H0V0h24L12 12Z"/></svg>

After

Width:  |  Height:  |  Size: 145 B

1
img/icons/lua.svg Normal file
View file

@ -0,0 +1 @@
<svg fill="#11171a" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Lua</title><path d="M.38 10.377l-.272-.037c-.048.344-.082.695-.101 1.041l.275.016c.018-.34.051-.682.098-1.02zM4.136 3.289l-.184-.205c-.258.232-.509.48-.746.734l.202.188c.231-.248.476-.49.728-.717zM5.769 2.059l-.146-.235c-.296.186-.586.385-.863.594l.166.219c.27-.203.554-.399.843-.578zM1.824 18.369c.185.297.384.586.593.863l.22-.164c-.205-.271-.399-.555-.58-.844l-.233.145zM1.127 16.402l-.255.104c.129.318.274.635.431.943l.005.01.245-.125-.005-.01c-.153-.301-.295-.611-.421-.922zM.298 9.309l.269.063c.076-.332.168-.664.272-.986l-.261-.087c-.108.332-.202.672-.28 1.01zM.274 12.42l-.275.01c.012.348.04.699.083 1.043l.273-.033c-.042-.336-.069-.68-.081-1.02zM.256 14.506c.073.34.162.682.264 1.014l.263-.08c-.1-.326-.187-.658-.258-.99l-.269.056zM11.573.275L11.563 0c-.348.012-.699.039-1.044.082l.034.273c.338-.041.68-.068 1.02-.08zM23.221 8.566c.1.326.186.66.256.992l.27-.059c-.072-.34-.16-.682-.262-1.014l-.264.081zM17.621 1.389c-.309-.164-.627-.314-.947-.449l-.107.252c.314.133.625.281.926.439l.128-.242zM15.693.572c-.332-.105-.67-.199-1.01-.277l-.063.268c.332.076.664.168.988.273l.085-.264zM6.674 1.545c.298-.15.606-.291.916-.418L7.486.873c-.317.127-.632.272-.937.428l-.015.008.125.244.015-.008zM23.727 11.588l.275-.01a11.797 11.797 0 0 0-.082-1.045l-.273.033c.041.338.068.682.08 1.022zM13.654.105c-.346-.047-.696-.08-1.043-.098l-.014.273c.339.018.683.051 1.019.098l.038-.273zM9.544.527l-.058-.27c-.34.072-.681.16-1.014.264l.081.262c.325-.099.659-.185.991-.256zM1.921 5.469l.231.15c.185-.285.384-.566.592-.834l-.217-.17c-.213.276-.417.563-.606.854zM.943 7.318l.253.107c.132-.313.28-.625.439-.924l-.243-.128c-.163.307-.314.625-.449.945zM18.223 21.943l.145.234c.295-.186.586-.385.863-.594l-.164-.219c-.272.204-.557.4-.844.579zM21.248 19.219l.217.17c.215-.273.418-.561.607-.854l-.23-.148c-.186.285-.385.564-.594.832zM19.855 20.715l.184.203c.258-.23.51-.479.746-.732l-.201-.188c-.23.248-.477.488-.729.717zM22.359 17.504l.244.129c.162-.307.314-.625.449-.945l-.254-.107a11.27 11.27 0 0 1-.439.923zM23.617 13.629l.273.039c.049-.346.082-.695.102-1.043l-.275-.014c-.018.338-.051.682-.1 1.018zM23.156 15.621l.264.086c.107-.332.201-.67.279-1.01l-.268-.063c-.077.333-.169.665-.275.987zM22.453 6.672c.154.303.297.617.424.932l.256-.104c-.131-.322-.277-.643-.436-.953l-.244.125zM8.296 23.418c.331.107.67.201 1.009.279l.062-.268c-.331-.076-.663-.168-.986-.273l-.085.262zM10.335 23.889c.345.049.696.082 1.043.102l.014-.275c-.339-.018-.682-.051-1.019-.098l-.038.271zM17.326 22.449c-.303.154-.613.297-.926.424l.104.256c.318-.131.639-.275.947-.434l.004-.002-.123-.246-.006.002zM4.613 21.467c.274.213.562.418.854.605l.149-.23c-.285-.184-.565-.385-.833-.592l-.17.217zM12.417 23.725l.009.275c.348-.014.699-.041 1.045-.084l-.035-.271c-.336.041-.68.068-1.019.08zM6.37 22.604c.307.162.625.314.946.449l.107-.254c-.313-.133-.624-.279-.924-.439l-.129.244zM3.083 20.041c.233.258.48.51.734.746l.188-.201c-.249-.23-.49-.477-.717-.729l-.205.184zM14.445 23.475l.059.27c.34-.074.68-.162 1.014-.266l-.082-.262c-.325.099-.659.185-.991.258zM21.18.129A2.689 2.689 0 1 0 21.18 5.507 2.689 2.689 0 1 0 21.18.129zM15.324 15.447c0 .471.314.66.852.66.67 0 1.297-.396 1.297-1.016v-.645c-.23.107-.379.141-1.107.24-.735.109-1.042.306-1.042.761zM12 2.818c-5.07 0-9.18 4.109-9.18 9.18 0 5.068 4.11 9.18 9.18 9.18 5.07 0 9.18-4.111 9.18-9.18 0-5.07-4.11-9.18-9.18-9.18zm-2.487 13.77H5.771v-6.023h.769v5.346h2.974v.677zm4.13 0h-.619v-.67c-.405.57-.811.793-1.446.793-.843 0-1.38-.463-1.38-1.182v-3.271h.686v3c0 .52.347.85.893.85.719 0 1.181-.578 1.181-1.461v-2.389h.686v4.33zm-.53-8.393c0-1.484 1.205-2.689 2.689-2.689s2.688 1.205 2.688 2.689-1.203 2.688-2.688 2.688-2.689-1.203-2.689-2.688zm5.567 7.856v.52c-.223.059-.33.074-.471.074-.34 0-.637-.238-.711-.57-.381.406-.918.637-1.471.637-.877 0-1.422-.463-1.422-1.248 0-.527.256-.916.76-1.123.266-.107.414-.141 1.389-.264.545-.066.719-.191.719-.48v-.182c0-.412-.348-.645-.967-.645-.645 0-.957.24-1.016.77h-.693c.041-1 .686-1.404 1.734-1.404 1.066 0 1.627.412 1.627 1.182v2.412c0 .215.133.338.373.338.041-.002.074-.002.149-.017z"/></svg>

After

Width:  |  Height:  |  Size: 4 KiB

1
img/icons/macos.svg Normal file
View file

@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>macOS</title><circle cx="12" cy="12" r="9.6" fill="none" stroke="#11171a" stroke-width="1.4"/><path d="M12 2.4a9.6 9.6 0 0 1 0 19.2Z" fill="#11171a"/><circle cx="8" cy="9.6" r="1.15" fill="#11171a"/><path d="M6.4 13.8q1.9 2.8 4.2.7" fill="none" stroke="#11171a" stroke-linecap="round" stroke-width="1.4"/></svg>

After

Width:  |  Height:  |  Size: 390 B

1
img/icons/modrinth.svg Normal file
View file

@ -0,0 +1 @@
<svg fill="#11171a" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Modrinth</title><path d="M12.252.004a11.78 11.768 0 0 0-8.92 3.73 11 10.999 0 0 0-2.17 3.11 11.37 11.359 0 0 0-1.16 5.169c0 1.42.17 2.5.6 3.77.24.759.77 1.899 1.17 2.529a12.3 12.298 0 0 0 8.85 5.639c.44.05 2.54.07 2.76.02.2-.04.22.1-.26-1.7l-.36-1.37-1.01-.06a8.5 8.489 0 0 1-5.18-1.8 5.34 5.34 0 0 1-1.3-1.26c0-.05.34-.28.74-.5a37.572 37.545 0 0 1 2.88-1.629c.03 0 .5.45 1.06.98l1 .97 2.07-.43 2.06-.43 1.47-1.47c.8-.8 1.48-1.5 1.48-1.52 0-.09-.42-1.63-.46-1.7-.04-.06-.2-.03-1.02.18-.53.13-1.2.3-1.45.4l-.48.15-.53.53-.53.53-.93.1-.93.07-.52-.5a2.7 2.7 0 0 1-.96-1.7l-.13-.6.43-.57c.68-.9.68-.9 1.46-1.1.4-.1.65-.2.83-.33.13-.099.65-.579 1.14-1.069l.9-.9-.7-.7-.7-.7-1.95.54c-1.07.3-1.96.53-1.97.53-.03 0-2.23 2.48-2.63 2.97l-.29.35.28 1.03c.16.56.3 1.16.31 1.34l.03.3-.34.23c-.37.23-2.22 1.3-2.84 1.63-.36.2-.37.2-.44.1-.08-.1-.23-.6-.32-1.03-.18-.86-.17-2.75.02-3.73a8.84 8.839 0 0 1 7.9-6.93c.43-.03.77-.08.78-.1.06-.17.5-2.999.47-3.039-.01-.02-.1-.02-.2-.03Zm3.68.67c-.2 0-.3.1-.37.38-.06.23-.46 2.42-.46 2.52 0 .04.1.11.22.16a8.51 8.499 0 0 1 2.99 2 8.38 8.379 0 0 1 2.16 3.449 6.9 6.9 0 0 1 .4 2.8c0 1.07 0 1.27-.1 1.73a9.37 9.369 0 0 1-1.76 3.769c-.32.4-.98 1.06-1.37 1.38-.38.32-1.54 1.1-1.7 1.14-.1.03-.1.06-.07.26.03.18.64 2.56.7 2.78l.06.06a12.07 12.058 0 0 0 7.27-9.4c.13-.77.13-2.58 0-3.4a11.96 11.948 0 0 0-5.73-8.578c-.7-.42-2.05-1.06-2.25-1.06Z"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

1
img/icons/rust.svg Normal file
View file

@ -0,0 +1 @@
<svg fill="#11171a" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Rust</title><path d="M23.8346 11.7033l-1.0073-.6236a13.7268 13.7268 0 00-.0283-.2936l.8656-.8069a.3483.3483 0 00-.1154-.578l-1.1066-.414a8.4958 8.4958 0 00-.087-.2856l.6904-.9587a.3462.3462 0 00-.2257-.5446l-1.1663-.1894a9.3574 9.3574 0 00-.1407-.2622l.49-1.0761a.3437.3437 0 00-.0274-.3361.3486.3486 0 00-.3006-.154l-1.1845.0416a6.7444 6.7444 0 00-.1873-.2268l.2723-1.153a.3472.3472 0 00-.417-.4172l-1.1532.2724a14.0183 14.0183 0 00-.2278-.1873l.0415-1.1845a.3442.3442 0 00-.49-.328l-1.076.491c-.0872-.0476-.1742-.0952-.2623-.1407l-.1903-1.1673A.3483.3483 0 0016.256.955l-.9597.6905a8.4867 8.4867 0 00-.2855-.086l-.414-1.1066a.3483.3483 0 00-.5781-.1154l-.8069.8666a9.2936 9.2936 0 00-.2936-.0284L12.2946.1683a.3462.3462 0 00-.5892 0l-.6236 1.0073a13.7383 13.7383 0 00-.2936.0284L9.9803.3374a.3462.3462 0 00-.578.1154l-.4141 1.1065c-.0962.0274-.1903.0567-.2855.086L7.744.955a.3483.3483 0 00-.5447.2258L7.009 2.348a9.3574 9.3574 0 00-.2622.1407l-1.0762-.491a.3462.3462 0 00-.49.328l.0416 1.1845a7.9826 7.9826 0 00-.2278.1873L3.8413 3.425a.3472.3472 0 00-.4171.4171l.2713 1.1531c-.0628.075-.1255.1509-.1863.2268l-1.1845-.0415a.3462.3462 0 00-.328.49l.491 1.0761a9.167 9.167 0 00-.1407.2622l-1.1662.1894a.3483.3483 0 00-.2258.5446l.6904.9587a13.303 13.303 0 00-.087.2855l-1.1065.414a.3483.3483 0 00-.1155.5781l.8656.807a9.2936 9.2936 0 00-.0283.2935l-1.0073.6236a.3442.3442 0 000 .5892l1.0073.6236c.008.0982.0182.1964.0283.2936l-.8656.8079a.3462.3462 0 00.1155.578l1.1065.4141c.0273.0962.0567.1914.087.2855l-.6904.9587a.3452.3452 0 00.2268.5447l1.1662.1893c.0456.088.0922.1751.1408.2622l-.491 1.0762a.3462.3462 0 00.328.49l1.1834-.0415c.0618.0769.1235.1528.1873.2277l-.2713 1.1541a.3462.3462 0 00.4171.4161l1.153-.2713c.075.0638.151.1255.2279.1863l-.0415 1.1845a.3442.3442 0 00.49.327l1.0761-.49c.087.0486.1741.0951.2622.1407l.1903 1.1662a.3483.3483 0 00.5447.2268l.9587-.6904a9.299 9.299 0 00.2855.087l.414 1.1066a.3452.3452 0 00.5781.1154l.8079-.8656c.0972.0111.1954.0203.2936.0294l.6236 1.0073a.3472.3472 0 00.5892 0l.6236-1.0073c.0982-.0091.1964-.0183.2936-.0294l.8069.8656a.3483.3483 0 00.578-.1154l.4141-1.1066a8.4626 8.4626 0 00.2855-.087l.9587.6904a.3452.3452 0 00.5447-.2268l.1903-1.1662c.088-.0456.1751-.0931.2622-.1407l1.0762.49a.3472.3472 0 00.49-.327l-.0415-1.1845a6.7267 6.7267 0 00.2267-.1863l1.1531.2713a.3472.3472 0 00.4171-.416l-.2713-1.1542c.0628-.0749.1255-.1508.1863-.2278l1.1845.0415a.3442.3442 0 00.328-.49l-.49-1.076c.0475-.0872.0951-.1742.1407-.2623l1.1662-.1893a.3483.3483 0 00.2258-.5447l-.6904-.9587.087-.2855 1.1066-.414a.3462.3462 0 00.1154-.5781l-.8656-.8079c.0101-.0972.0202-.1954.0283-.2936l1.0073-.6236a.3442.3442 0 000-.5892zm-6.7413 8.3551a.7138.7138 0 01.2986-1.396.714.714 0 11-.2997 1.396zm-.3422-2.3142a.649.649 0 00-.7715.5l-.3573 1.6685c-1.1035.501-2.3285.7795-3.6193.7795a8.7368 8.7368 0 01-3.6951-.814l-.3574-1.6684a.648.648 0 00-.7714-.499l-1.473.3158a8.7216 8.7216 0 01-.7613-.898h7.1676c.081 0 .1356-.0141.1356-.088v-2.536c0-.074-.0536-.0881-.1356-.0881h-2.0966v-1.6077h2.2677c.2065 0 1.1065.0587 1.394 1.2088.0901.3533.2875 1.5044.4232 1.8729.1346.413.6833 1.2381 1.2685 1.2381h3.5716a.7492.7492 0 00.1296-.0131 8.7874 8.7874 0 01-.8119.9526zM6.8369 20.024a.714.714 0 11-.2997-1.396.714.714 0 01.2997 1.396zM4.1177 8.9972a.7137.7137 0 11-1.304.5791.7137.7137 0 011.304-.579zm-.8352 1.9813l1.5347-.6824a.65.65 0 00.33-.8585l-.3158-.7147h1.2432v5.6025H3.5669a8.7753 8.7753 0 01-.2834-3.348zm6.7343-.5437V8.7836h2.9601c.153 0 1.0792.1772 1.0792.8697 0 .575-.7107.7815-1.2948.7815zm10.7574 1.4862c0 .2187-.008.4363-.0243.651h-.9c-.09 0-.1265.0586-.1265.1477v.413c0 .973-.5487 1.1846-1.0296 1.2382-.4576.0517-.9648-.1913-1.0275-.4717-.2704-1.5186-.7198-1.8436-1.4305-2.4034.8817-.5599 1.799-1.386 1.799-2.4915 0-1.1936-.819-1.9458-1.3769-2.3153-.7825-.5163-1.6491-.6195-1.883-.6195H5.4682a8.7651 8.7651 0 014.907-2.7699l1.0974 1.151a.648.648 0 00.9182.0213l1.227-1.1743a8.7753 8.7753 0 016.0044 4.2762l-.8403 1.8982a.652.652 0 00.33.8585l1.6178.7188c.0283.2875.0425.577.0425.8717zm-9.3006-9.5993a.7128.7128 0 11.984 1.0316.7137.7137 0 01-.984-1.0316zm8.3389 6.71a.7107.7107 0 01.9395-.3625.7137.7137 0 11-.9405.3635z"/></svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -0,0 +1 @@
<svg fill="#11171a" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Self-hosting</title><path d="M12 2C7.03 2 3 3.34 3 5s4.03 3 9 3 9-1.34 9-3-4.03-3-9-3Z"/><path d="M3 8.2v3.3c0 1.66 4.03 3 9 3s9-1.34 9-3V8.2c-1.6 1.28-5.1 2.3-9 2.3S4.6 9.48 3 8.2Z"/><path d="M3 13.7V17c0 1.66 4.03 3 9 3s9-1.34 9-3v-3.3c-1.6 1.28-5.1 2.3-9 2.3s-7.4-1.02-9-2.3Z"/></svg>

After

Width:  |  Height:  |  Size: 381 B

1
img/icons/typescript.svg Normal file
View file

@ -0,0 +1 @@
<svg fill="#11171a" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>TypeScript</title><path d="M1.125 0C.502 0 0 .502 0 1.125v21.75C0 23.498.502 24 1.125 24h21.75c.623 0 1.125-.502 1.125-1.125V1.125C24 .502 23.498 0 22.875 0zm17.363 9.75c.612 0 1.154.037 1.627.111a6.38 6.38 0 0 1 1.306.34v2.458a3.95 3.95 0 0 0-.643-.361 5.093 5.093 0 0 0-.717-.26 5.453 5.453 0 0 0-1.426-.2c-.3 0-.573.028-.819.086a2.1 2.1 0 0 0-.623.242c-.17.104-.3.229-.393.374a.888.888 0 0 0-.14.49c0 .196.053.373.156.529.104.156.252.304.443.444s.423.276.696.41c.273.135.582.274.926.416.47.197.892.407 1.266.628.374.222.695.473.963.753.268.279.472.598.614.957.142.359.214.776.214 1.253 0 .657-.125 1.21-.373 1.656a3.033 3.033 0 0 1-1.012 1.085 4.38 4.38 0 0 1-1.487.596c-.566.12-1.163.18-1.79.18a9.916 9.916 0 0 1-1.84-.164 5.544 5.544 0 0 1-1.512-.493v-2.63a5.033 5.033 0 0 0 3.237 1.2c.333 0 .624-.03.872-.09.249-.06.456-.144.623-.25.166-.108.29-.234.373-.38a1.023 1.023 0 0 0-.074-1.089 2.12 2.12 0 0 0-.537-.5 5.597 5.597 0 0 0-.807-.444 27.72 27.72 0 0 0-1.007-.436c-.918-.383-1.602-.852-2.053-1.405-.45-.553-.676-1.222-.676-2.005 0-.614.123-1.141.369-1.582.246-.441.58-.804 1.004-1.089a4.494 4.494 0 0 1 1.47-.629 7.536 7.536 0 0 1 1.77-.201zm-15.113.188h9.563v2.166H9.506v9.646H6.789v-9.646H3.375z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

1
img/icons/ubuntu.svg Normal file
View file

@ -0,0 +1 @@
<svg fill="#11171a" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Ubuntu</title><path d="M17.61.455a3.41 3.41 0 0 0-3.41 3.41 3.41 3.41 0 0 0 3.41 3.41 3.41 3.41 0 0 0 3.41-3.41 3.41 3.41 0 0 0-3.41-3.41zM12.92.8C8.923.777 5.137 2.941 3.148 6.451a4.5 4.5 0 0 1 .26-.007 4.92 4.92 0 0 1 2.585.737A8.316 8.316 0 0 1 12.688 3.6 4.944 4.944 0 0 1 13.723.834 11.008 11.008 0 0 0 12.92.8zm9.226 4.994a4.915 4.915 0 0 1-1.918 2.246 8.36 8.36 0 0 1-.273 8.303 4.89 4.89 0 0 1 1.632 2.54 11.156 11.156 0 0 0 .559-13.089zM3.41 7.932A3.41 3.41 0 0 0 0 11.342a3.41 3.41 0 0 0 3.41 3.409 3.41 3.41 0 0 0 3.41-3.41 3.41 3.41 0 0 0-3.41-3.41zm2.027 7.866a4.908 4.908 0 0 1-2.915.358 11.1 11.1 0 0 0 7.991 6.698 11.234 11.234 0 0 0 2.422.249 4.879 4.879 0 0 1-.999-2.85 8.484 8.484 0 0 1-.836-.136 8.304 8.304 0 0 1-5.663-4.32zm11.405.928a3.41 3.41 0 0 0-3.41 3.41 3.41 3.41 0 0 0 3.41 3.41 3.41 3.41 0 0 0 3.41-3.41 3.41 3.41 0 0 0-3.41-3.41z"/></svg>

After

Width:  |  Height:  |  Size: 963 B

1
img/icons/wayland.svg Normal file
View file

@ -0,0 +1 @@
<svg fill="#11171a" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Wayland</title><path d="M23.982 12c0 9.224-9.985 14.989-17.973 10.377A11.982 11.982 0 0 1 .018 12c0-3.386 1.346-6.306 3.444-8.418v.675l.553 3.413v1.07l.077 1.365c-.23 0-.344.053-.344.159v.09l.344-.09h.076v2.366c.178 1.578.509 2.981.992 4.21.05.864.146 1.297.286 1.297.229.864.394 1.297.496 1.297a5.494 5.494 0 0 0 2.422 1.865c.344 0 .63-.432.859-1.296.292-1.866.528-3.004.706-3.414.165.167.451.281.858.342v-.091c0-.152-.26-.311-.782-.478.14-.728.33-1.35.572-1.866.038-.88.49-2.1 1.354-3.663l.344.09c.05.714.146 1.305.286 1.776.521 1.061.782 2.063.782 3.003 0 .364.407 1.585 1.22 3.663.204 1.366.465 2.17.783 2.412h.152c.42 0 .916-.538 1.488-1.615.56-1.214 1.011-1.889 1.354-2.025 0-.106-.12-.16-.362-.16v-.09c.19 0 .286-.054.286-.16v-.068l-.076-.091.801-.319c0-.273-.095-.41-.286-.41v-.09h.572v-.16c-.292 0-.483-.159-.572-.477h.133v-.319l-.076-.091c.432 0 .649-.083.649-.25v-.069h-.134v-.09c.153 0 .267-.16.343-.478-.42-.137-.629-.273-.629-.41l.553-1.615c0-.82.146-1.715.439-2.685.33-.94.566-1.965.706-3.072.304-1.064.617-1.787.938-2.17A11.979 11.979 0 0 1 23.982 12Zm-5.99-10.377c.587.34 1.138.724 1.65 1.148a33.45 33.45 0 0 0-.157 1.736c-.458 1.077-.96 2.594-1.507 4.55h-1.411l-.725.07v.181h.362l.286-.09 1.202.158v.092c-.343 1.183-.553 1.774-.63 1.774-.445 2.215-.8 3.322-1.068 3.322h-.229c-.318-.288-.674-.88-1.068-1.774a.861.861 0 0 0-.21-.57L13.42 9.855c-.216-1-.502-1.752-.858-2.252-.115-.334-.305-.5-.572-.5h-.134c-.42 0-.826.295-1.22.887l.076.25c-.178.607-.465 1.312-.859 2.116-.61 1.775-1.01 2.662-1.201 2.662h-.153l-.286-.227c-.483-1.305-.814-3.224-.992-5.757h-.134v-.16h.058a1.33 1.33 0 0 1 .515-.75v-.069l-.077-.09-.076.09-.076-.09-.344.09h-.152c-.09 0-.23-.348-.42-1.046v-.183h.496v-.16h-.648c-.139-.698-.545-1.53-1.218-2.494.296-.206.601-.398.915-.577h.016l.077.068.438-.068h.783c.228 0 .464.083.705.25.115-.152.33-.235.649-.25v-.16h-2.38c3.11-1.667 6.947-1.99 10.51-.392l-.443.142v.16c.24 0 .4-.095.482-.284.368.166.734.354 1.095.562Zm3.477 3.203h-.134l.035-.295.179.23-.08.065ZM9.719 1.595h.572l.133.159.363-.16.42.069.438-.068v-.16H9.72v.16Zm3.7 1.297v.159l.21.09v-.09l.076-.091c.14 0 .21-.053.21-.16v-.068h-.21l-.286.16Zm-1.144.887h-.058l-.076-.068-.42.227v.16c.61-.137.916-.266.916-.387V3.62l-.362.159Zm-2.842.978c0 .167-.051.25-.153.25v.16c.598-.197 1.03-.417 1.297-.66V4.44l-1.144.318Zm4.482.57v.068l.153.182h.076l.134-.182v-.069l-.077-.09-.286.09Zm2.785.159v.09h.076l.134-.181v-.069h-.076l-.134.16Zm-1.354 1.206.152.25c.23 0 .37-.084.42-.25l-.286-.228c-.19.076-.286.152-.286.228Zm1.85.978v.091h.153l.057-.091v-.068l-.057-.091-.153.159Zm-3.128.978v.091h.21l.362-.34v-.07c-.382.077-.572.183-.572.32ZM2.24 12.13v.341h.153c.14-.106.305-.41.496-.91v-.068h-.076l-.573.637Zm9.328 3.413v.159l.076.091.572-.341v-.16h-.076l-.572.25Zm-7.267.887.21.82h.152l-.229-.888h-.133v.068Zm7.63 1.07.133.227h.076l.076-.068-.228-.25h-.058v.09Z"/></svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View file

@ -5,10 +5,10 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fi3w0 / Alex - Systems Builder</title> <title>Fi3w0 / Alex - Systems Builder</title>
<meta name="description" content="Alex, also known as Fi3w0, builds Linux and macOS desktop tools, homelab infrastructure, server tooling, and configurable Minecraft frameworks."> <meta name="description" content="Alex, also known as Fi3w0, builds TideWM, Scorium, Linux and macOS desktop tools, homelab infrastructure, and configurable server tooling.">
<meta property="og:title" content="Fi3w0 / Alex - Systems Builder"> <meta property="og:title" content="Fi3w0 / Alex - Systems Builder">
<meta property="og:description" content="Linux, homelabs, server tooling, and the systems holding everything up."> <meta property="og:description" content="TideWM, Scorium, Linux desktops, homelabs, and the systems holding everything up.">
<meta property="og:type" content="website"> <meta property="og:type" content="website">
<meta property="og:url" content="https://fiwlabs.dev/"> <meta property="og:url" content="https://fiwlabs.dev/">
<meta property="og:image" content="https://fiwlabs.dev/img/moonlit-shell.jpg"> <meta property="og:image" content="https://fiwlabs.dev/img/moonlit-shell.jpg">
@ -16,11 +16,8 @@
<meta name="twitter:card" content="summary_large_image"> <meta name="twitter:card" content="summary_large_image">
<link rel="canonical" href="https://fiwlabs.dev/"> <link rel="canonical" href="https://fiwlabs.dev/">
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="stylesheet" href="css/style.css?v=10">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="stylesheet" href="css/editorial.css?v=11">
<link href="https://fonts.googleapis.com/css2?family=Archivo+Black&amp;family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,600;1,400&amp;display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/style.css?v=5">
<link rel="stylesheet" href="css/editorial.css?v=5">
<link rel="icon" href="/favicon.ico" sizes="any"> <link rel="icon" href="/favicon.ico" sizes="any">
<link rel="icon" href="/icon.svg" type="image/svg+xml"> <link rel="icon" href="/icon.svg" type="image/svg+xml">
@ -68,7 +65,7 @@
<p class="eyebrow"><span class="status-dot" aria-hidden="true"></span> alex@fiw:~ $ cat about.txt</p> <p class="eyebrow"><span class="status-dot" aria-hidden="true"></span> alex@fiw:~ $ cat about.txt</p>
<h1 id="hero-title">Alex <span id="glitch-name" data-text="/ Fi3w0">/ Fi3w0</span></h1> <h1 id="hero-title">Alex <span id="glitch-name" data-text="/ Fi3w0">/ Fi3w0</span></h1>
<p class="hero-intro"> <p class="hero-intro">
I make Linux and macOS desktop tools, run a homelab, and publish Minecraft server mods. I build a Wayland desktop, a programmable configuration language, macOS tools, and Minecraft server mods.
Most of my projects start as something I wanted for my own machine or server, Most of my projects start as something I wanted for my own machine or server,
then grow until they are useful to somebody else too. then grow until they are useful to somebody else too.
</p> </p>
@ -92,15 +89,15 @@
</div> </div>
<div class="terminal-body"> <div class="terminal-body">
<p><span class="prompt">$</span> git status --short</p> <p><span class="prompt">$</span> git status --short</p>
<p class="terminal-output"><span>M</span> homelab/<br><span>M</span> minecraft-mods/<br><span>??</span> desktop-experiments/</p> <p class="terminal-output"><span>M</span> TideWM/<br><span>M</span> Scorium/<br><span>??</span> next-system/</p>
<p><span class="prompt">$</span> cat NOTES</p> <p><span class="prompt">$</span> cat NOTES</p>
<p class="terminal-output">linux + macos / servers / modding<br>automation / config / self-hosting</p> <p class="terminal-output">wayland + rust / config languages<br>automation / servers / self-hosting</p>
<p class="terminal-note"># there is always something unfinished</p> <p class="terminal-note"># there is always something unfinished</p>
</div> </div>
<div class="profile-chip"> <div class="profile-chip">
<img src="img/avatar.png" alt="Fi3w0's GitHub avatar" width="460" height="460"> <img src="img/avatar.png" alt="Fi3w0's GitHub avatar" width="460" height="460">
<div><strong>Alex / Fi3w0</strong><span>Linux, servers, mods</span></div> <div><strong>Alex / Fi3w0</strong><span>Systems over categories</span></div>
</div> </div>
</aside> </aside>
</section> </section>
@ -108,40 +105,40 @@
<div class="signal-strip" aria-label="Core interests"> <div class="signal-strip" aria-label="Core interests">
<div class="signal-track"> <div class="signal-track">
<div class="signal-set"> <div class="signal-set">
<span class="signal-item"><img class="signal-icon" src="https://cdn.simpleicons.org/archlinux/11171a" alt="" loading="lazy" onerror="this.remove()">Arch Linux</span><i></i> <span class="signal-item"><img class="signal-icon" src="img/icons/archlinux.svg" alt="" loading="lazy">Arch Linux</span><i></i>
<span class="signal-item"><img class="signal-icon" src="https://cdn.simpleicons.org/ubuntu/11171a" alt="" loading="lazy" onerror="this.remove()">Ubuntu Server</span><i></i> <span class="signal-item"><img class="signal-icon" src="img/icons/ubuntu.svg" alt="" loading="lazy">Ubuntu Server</span><i></i>
<span class="signal-item"><img class="signal-icon" src="https://cdn.simpleicons.org/docker/11171a" alt="" loading="lazy" onerror="this.remove()">Docker</span><i></i> <span class="signal-item"><img class="signal-icon" src="img/icons/docker.svg" alt="" loading="lazy">Docker</span><i></i>
<span class="signal-item"><img class="signal-icon" src="https://cdn.simpleicons.org/modrinth/11171a" alt="" loading="lazy" onerror="this.remove()">Minecraft tooling</span><i></i> <span class="signal-item"><img class="signal-icon" src="img/icons/modrinth.svg" alt="" loading="lazy">Minecraft tooling</span><i></i>
<span class="signal-item"><img class="signal-icon" src="https://cdn.simpleicons.org/wayland/11171a" alt="" loading="lazy" onerror="this.remove()">Wayland</span><i></i> <span class="signal-item"><img class="signal-icon" src="img/icons/wayland.svg" alt="" loading="lazy">Wayland</span><i></i>
<span class="signal-item"><img class="signal-icon" src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22%2311171a%22%3E%3Cpath%20d%3D%22M12%202C7.03%202%203%203.34%203%205s4.03%203%209%203%209-1.34%209-3-4.03-3-9-3z%22%2F%3E%3Cpath%20d%3D%22M3%208.2v3.3c0%201.66%204.03%203%209%203s9-1.34%209-3V8.2c-1.6%201.28-5.1%202.3-9%202.3s-7.4-1.02-9-2.3z%22%2F%3E%3Cpath%20d%3D%22M3%2013.7V17c0%201.66%204.03%203%209%203s9-1.34%209-3v-3.3c-1.6%201.28-5.1%202.3-9%202.3s-7.4-1.02-9-2.3z%22%2F%3E%3C%2Fsvg%3E" alt="">Self-hosting</span><i></i> <span class="signal-item"><img class="signal-icon" src="img/icons/selfhosting.svg" alt="" loading="lazy">Self-hosting</span><i></i>
<span class="signal-item"><img class="signal-icon" src="https://cdn.simpleicons.org/json/11171a" alt="" loading="lazy" onerror="this.remove()">Things configured with JSON</span><i></i> <span class="signal-item"><img class="signal-icon" src="img/icons/json.svg" alt="" loading="lazy">Things configured with JSON</span><i></i>
<span class="signal-item"><img class="signal-icon" src="https://cdn.simpleicons.org/gnubash/11171a" alt="" loading="lazy" onerror="this.remove()">Bash</span><i></i> <span class="signal-item"><img class="signal-icon" src="img/icons/gnubash.svg" alt="" loading="lazy">Bash</span><i></i>
<span class="signal-item"><img class="signal-icon" src="https://cdn.simpleicons.org/rust/11171a" alt="" loading="lazy" onerror="this.remove()">Rust</span><i></i> <span class="signal-item"><img class="signal-icon" src="img/icons/rust.svg" alt="" loading="lazy">Rust</span><i></i>
<span class="signal-item"><img class="signal-icon" src="https://cdn.simpleicons.org/go/11171a" alt="" loading="lazy" onerror="this.remove()">Go</span><i></i> <span class="signal-item"><img class="signal-icon" src="img/icons/go.svg" alt="" loading="lazy">Go</span><i></i>
<span class="signal-item"><img class="signal-icon" src="https://cdn.simpleicons.org/lua/11171a" alt="" loading="lazy" onerror="this.remove()">Lua</span><i></i> <span class="signal-item"><img class="signal-icon" src="img/icons/lua.svg" alt="" loading="lazy">Lua</span><i></i>
<span class="signal-item"><img class="signal-icon" src="https://cdn.simpleicons.org/kotlin/11171a" alt="" loading="lazy" onerror="this.remove()">Kotlin</span><i></i> <span class="signal-item"><img class="signal-icon" src="img/icons/kotlin.svg" alt="" loading="lazy">Kotlin</span><i></i>
<span class="signal-item"><img class="signal-icon" src="https://cdn.simpleicons.org/typescript/11171a" alt="" loading="lazy" onerror="this.remove()">TypeScript</span><i></i> <span class="signal-item"><img class="signal-icon" src="img/icons/typescript.svg" alt="" loading="lazy">TypeScript</span><i></i>
<span class="signal-item"><img class="signal-icon" src="https://cdn.simpleicons.org/githubactions/11171a" alt="" loading="lazy" onerror="this.remove()">GitHub Actions</span><i></i> <span class="signal-item"><img class="signal-icon" src="img/icons/githubactions.svg" alt="" loading="lazy">GitHub Actions</span><i></i>
<span class="signal-item"><img class="signal-icon" src="https://cdn.simpleicons.org/ffmpeg/11171a" alt="" loading="lazy" onerror="this.remove()">FFmpeg</span><i></i> <span class="signal-item"><img class="signal-icon" src="img/icons/ffmpeg.svg" alt="" loading="lazy">FFmpeg</span><i></i>
<span class="signal-item"><img class="signal-icon" src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%3E%3Ccircle%20cx%3D%2212%22%20cy%3D%2212%22%20r%3D%229.6%22%20fill%3D%22none%22%20stroke%3D%22%2311171a%22%20stroke-width%3D%221.4%22%2F%3E%3Cpath%20d%3D%22M12%2C2.4%20A9.6%2C9.6%200%200%2C1%2012%2C21.6%20Z%22%20fill%3D%22%2311171a%22%2F%3E%3Ccircle%20cx%3D%228%22%20cy%3D%229.6%22%20r%3D%221.15%22%20fill%3D%22%2311171a%22%2F%3E%3Cpath%20d%3D%22M6.4%2013.8%20Q8.3%2016.6%2010.6%2014.5%22%20fill%3D%22none%22%20stroke%3D%22%2311171a%22%20stroke-width%3D%221.4%22%20stroke-linecap%3D%22round%22%2F%3E%3C%2Fsvg%3E" alt="">macOS</span><i></i> <span class="signal-item"><img class="signal-icon" src="img/icons/macos.svg" alt="" loading="lazy">macOS</span><i></i>
</div> </div>
<div class="signal-set" aria-hidden="true"> <div class="signal-set" aria-hidden="true">
<span class="signal-item"><img class="signal-icon" src="https://cdn.simpleicons.org/archlinux/11171a" alt="" loading="lazy" onerror="this.remove()">Arch Linux</span><i></i> <span class="signal-item"><img class="signal-icon" src="img/icons/archlinux.svg" alt="" loading="lazy">Arch Linux</span><i></i>
<span class="signal-item"><img class="signal-icon" src="https://cdn.simpleicons.org/ubuntu/11171a" alt="" loading="lazy" onerror="this.remove()">Ubuntu Server</span><i></i> <span class="signal-item"><img class="signal-icon" src="img/icons/ubuntu.svg" alt="" loading="lazy">Ubuntu Server</span><i></i>
<span class="signal-item"><img class="signal-icon" src="https://cdn.simpleicons.org/docker/11171a" alt="" loading="lazy" onerror="this.remove()">Docker</span><i></i> <span class="signal-item"><img class="signal-icon" src="img/icons/docker.svg" alt="" loading="lazy">Docker</span><i></i>
<span class="signal-item"><img class="signal-icon" src="https://cdn.simpleicons.org/modrinth/11171a" alt="" loading="lazy" onerror="this.remove()">Minecraft tooling</span><i></i> <span class="signal-item"><img class="signal-icon" src="img/icons/modrinth.svg" alt="" loading="lazy">Minecraft tooling</span><i></i>
<span class="signal-item"><img class="signal-icon" src="https://cdn.simpleicons.org/wayland/11171a" alt="" loading="lazy" onerror="this.remove()">Wayland</span><i></i> <span class="signal-item"><img class="signal-icon" src="img/icons/wayland.svg" alt="" loading="lazy">Wayland</span><i></i>
<span class="signal-item"><img class="signal-icon" src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22%2311171a%22%3E%3Cpath%20d%3D%22M12%202C7.03%202%203%203.34%203%205s4.03%203%209%203%209-1.34%209-3-4.03-3-9-3z%22%2F%3E%3Cpath%20d%3D%22M3%208.2v3.3c0%201.66%204.03%203%209%203s9-1.34%209-3V8.2c-1.6%201.28-5.1%202.3-9%202.3s-7.4-1.02-9-2.3z%22%2F%3E%3Cpath%20d%3D%22M3%2013.7V17c0%201.66%204.03%203%209%203s9-1.34%209-3v-3.3c-1.6%201.28-5.1%202.3-9%202.3s-7.4-1.02-9-2.3z%22%2F%3E%3C%2Fsvg%3E" alt="">Self-hosting</span><i></i> <span class="signal-item"><img class="signal-icon" src="img/icons/selfhosting.svg" alt="" loading="lazy">Self-hosting</span><i></i>
<span class="signal-item"><img class="signal-icon" src="https://cdn.simpleicons.org/json/11171a" alt="" loading="lazy" onerror="this.remove()">Things configured with JSON</span><i></i> <span class="signal-item"><img class="signal-icon" src="img/icons/json.svg" alt="" loading="lazy">Things configured with JSON</span><i></i>
<span class="signal-item"><img class="signal-icon" src="https://cdn.simpleicons.org/gnubash/11171a" alt="" loading="lazy" onerror="this.remove()">Bash</span><i></i> <span class="signal-item"><img class="signal-icon" src="img/icons/gnubash.svg" alt="" loading="lazy">Bash</span><i></i>
<span class="signal-item"><img class="signal-icon" src="https://cdn.simpleicons.org/rust/11171a" alt="" loading="lazy" onerror="this.remove()">Rust</span><i></i> <span class="signal-item"><img class="signal-icon" src="img/icons/rust.svg" alt="" loading="lazy">Rust</span><i></i>
<span class="signal-item"><img class="signal-icon" src="https://cdn.simpleicons.org/go/11171a" alt="" loading="lazy" onerror="this.remove()">Go</span><i></i> <span class="signal-item"><img class="signal-icon" src="img/icons/go.svg" alt="" loading="lazy">Go</span><i></i>
<span class="signal-item"><img class="signal-icon" src="https://cdn.simpleicons.org/lua/11171a" alt="" loading="lazy" onerror="this.remove()">Lua</span><i></i> <span class="signal-item"><img class="signal-icon" src="img/icons/lua.svg" alt="" loading="lazy">Lua</span><i></i>
<span class="signal-item"><img class="signal-icon" src="https://cdn.simpleicons.org/kotlin/11171a" alt="" loading="lazy" onerror="this.remove()">Kotlin</span><i></i> <span class="signal-item"><img class="signal-icon" src="img/icons/kotlin.svg" alt="" loading="lazy">Kotlin</span><i></i>
<span class="signal-item"><img class="signal-icon" src="https://cdn.simpleicons.org/typescript/11171a" alt="" loading="lazy" onerror="this.remove()">TypeScript</span><i></i> <span class="signal-item"><img class="signal-icon" src="img/icons/typescript.svg" alt="" loading="lazy">TypeScript</span><i></i>
<span class="signal-item"><img class="signal-icon" src="https://cdn.simpleicons.org/githubactions/11171a" alt="" loading="lazy" onerror="this.remove()">GitHub Actions</span><i></i> <span class="signal-item"><img class="signal-icon" src="img/icons/githubactions.svg" alt="" loading="lazy">GitHub Actions</span><i></i>
<span class="signal-item"><img class="signal-icon" src="https://cdn.simpleicons.org/ffmpeg/11171a" alt="" loading="lazy" onerror="this.remove()">FFmpeg</span><i></i> <span class="signal-item"><img class="signal-icon" src="img/icons/ffmpeg.svg" alt="" loading="lazy">FFmpeg</span><i></i>
<span class="signal-item"><img class="signal-icon" src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%3E%3Ccircle%20cx%3D%2212%22%20cy%3D%2212%22%20r%3D%229.6%22%20fill%3D%22none%22%20stroke%3D%22%2311171a%22%20stroke-width%3D%221.4%22%2F%3E%3Cpath%20d%3D%22M12%2C2.4%20A9.6%2C9.6%200%200%2C1%2012%2C21.6%20Z%22%20fill%3D%22%2311171a%22%2F%3E%3Ccircle%20cx%3D%228%22%20cy%3D%229.6%22%20r%3D%221.15%22%20fill%3D%22%2311171a%22%2F%3E%3Cpath%20d%3D%22M6.4%2013.8%20Q8.3%2016.6%2010.6%2014.5%22%20fill%3D%22none%22%20stroke%3D%22%2311171a%22%20stroke-width%3D%221.4%22%20stroke-linecap%3D%22round%22%2F%3E%3C%2Fsvg%3E" alt="">macOS</span><i></i> <span class="signal-item"><img class="signal-icon" src="img/icons/macos.svg" alt="" loading="lazy">macOS</span><i></i>
</div> </div>
</div> </div>
</div> </div>
@ -173,8 +170,8 @@
hot reload, readable config files, updates that can fail safely, and logs that say what went wrong. hot reload, readable config files, updates that can fail safely, and logs that say what went wrong.
</p> </p>
<p> <p>
I write Bash, Go, Lua, and modify Java directly. For Rust, Swift, and larger Kotlin, TypeScript, Python, I write Bash, Go, Lua, and Python directly, and modify Java. For heavier Rust, Swift, Kotlin,
and QML builds, I use AI agents heavily, then review, test, and integrate the result myself. TypeScript, and QML builds, I direct AI agents, then own the design, review, testing, and integration.
</p> </p>
<div class="language-line"> <div class="language-line">
<span>Ukrainian + Russian</span><span>Spanish</span><span>English C1</span> <span>Ukrainian + Russian</span><span>Spanish</span><span>English C1</span>
@ -201,50 +198,74 @@
<p>Public repositories, installable mods, and rougher ideas that started on my own machines.</p> <p>Public repositories, installable mods, and rougher ideas that started on my own machines.</p>
</header> </header>
<article class="current-project reveal" aria-labelledby="tidewm-title"> <article class="current-project current-project-tide reveal" aria-labelledby="tidewm-title">
<div class="current-project-main"> <div class="current-project-main">
<div class="current-meta"><span><i aria-hidden="true"></i> Daily-driver compositor</span><span>AI-assisted / evolving</span></div> <div class="current-meta"><span><i aria-hidden="true"></i> Functional pre-release</span><span>v0.90 / daily-driver path</span></div>
<p class="current-kicker">Wayland compositor / Rust</p> <p class="current-kicker">Wayland compositor / Rust</p>
<div class="tide-heading"> <div class="tide-heading">
<img class="tide-mark" src="img/tidewm-logo.png" alt="" width="48" height="48" loading="lazy"> <img class="tide-mark" src="img/tidewm-logo.png" alt="" width="48" height="48" loading="lazy">
<h3 id="tidewm-title">TideWM</h3> <h3 id="tidewm-title">TideWM</h3>
</div> </div>
<p>A water-themed Wayland compositor built on Smithay, made because I wanted to see how far I could take it. Tiling (BSP and master/stack), multi-monitor, XWayland, workspaces, and IPC already run as my daily driver; the signature aqua render effects are next.</p> <p>A Wayland desktop that feels like water, built in Rust on Smithay. Under the ripples, wave transitions, glass, shadows, and window sway is a real tiling compositor with multi-monitor, XWayland, IPC, screencasting, and two spatial engines you can switch live.</p>
<div class="tag-row current-tags"><span>AI-assisted</span><span>Smithay</span><span>Rust</span><span>Wayland</span></div> <div class="tide-proof" aria-label="TideWM project facts">
<div><strong>2</strong><span>spatial engines</span></div>
<div><strong>~63 MB</strong><span>nine glass windows</span></div>
<div><strong>live</strong><span>config reload</span></div>
</div>
<div class="tag-row current-tags"><span>Smithay</span><span>Rust</span><span>Wayland</span><span>GPL-3.0</span></div>
<a class="text-link current-repo-link" href="https://github.com/Fi3w0/TideWM" target="_blank" rel="noreferrer">View repository <span aria-hidden="true">&nearr;</span></a> <a class="text-link current-repo-link" href="https://github.com/Fi3w0/TideWM" target="_blank" rel="noreferrer">View repository <span aria-hidden="true">&nearr;</span></a>
</div> </div>
<div class="current-roadmap"> <div class="current-roadmap">
<p>Project shape</p> <p>Core systems</p>
<ol> <ol>
<li><span>01</span><div><strong>Foundation</strong><small>Rust compositor built directly on Smithay</small></div><b>base</b></li> <li><span>01</span><div><strong>Classic</strong><small>BSP, master/stack, cascade, workspaces, Depth Deck</small></div><b>done</b></li>
<li><span>02</span><div><strong>Tiling &amp; compat</strong><small>BSP/master-stack layouts, multi-monitor, XWayland</small></div><b>done</b></li> <li><span>02</span><div><strong>Ocean</strong><small>One continuous 2D world with reefs, cameras, and bookmarks</small></div><b>done</b></li>
<li><span>03</span><div><strong>Visual direction</strong><small>Water/aqua render effects, still pending</small></div><b>next</b></li> <li><span>03</span><div><strong>Water stack</strong><small>Ripples, waves, glass, caustics, sway, depth, and buoyancy</small></div><b>done</b></li>
<li><span>04</span><div><strong>Development method</strong><small>AI-assisted implementation, tested and reviewed by me</small></div><b>method</b></li> <li><span>04</span><div><strong>Next pass</strong><small>More hardware coverage, feel tuning, Nvidia native, AUR</small></div><b>testing</b></li>
</ol> </ol>
</div> </div>
<span class="current-watermark" aria-hidden="true">TIDE</span> <span class="current-watermark" aria-hidden="true">TIDE</span>
<div class="tide-ascii" id="tide-ascii" aria-hidden="true"></div> </article>
<article class="project-feature project-feature-scorium reveal" data-repo="Scorium">
<a class="project-visual project-visual-scorium" href="https://github.com/Fi3w0/Scorium" target="_blank" rel="noreferrer" aria-label="View Scorium on GitHub">
<span class="scorium-window" aria-hidden="true">
<span class="scorium-titlebar"><b>SCORIUM</b><span>examples/services.scor</span><i>v0.1.0</i></span>
<code class="scorium-code"><span class="scorium-line"><span class="scorium-dim">01</span> <em>@base_port</em> = <strong>8000</strong></span><span class="scorium-line"><span class="scorium-dim">02</span></span><span class="scorium-line"><span class="scorium-dim">03</span> <b>server</b> {</span><span class="scorium-line"><span class="scorium-dim">04</span><span class="scorium-indent"></span>host = <i>localhost</i></span><span class="scorium-line"><span class="scorium-dim">05</span><span class="scorium-indent"></span>port = <em>base_port</em> + <strong>80</strong></span><span class="scorium-line"><span class="scorium-dim">06</span><span class="scorium-indent"></span>timeout = <strong>5s</strong></span><span class="scorium-line"><span class="scorium-dim">07</span> }</span><span class="scorium-line"><span class="scorium-dim">08</span></span><span class="scorium-line"><span class="scorium-dim">09</span> <b>for</b> i = <strong>1</strong>, <strong>3</strong> <b>do</b></span><span class="scorium-line"><span class="scorium-dim">10</span><span class="scorium-indent"></span>worker {</span><span class="scorium-line"><span class="scorium-dim">11</span><span class="scorium-indent scorium-indent-double"></span>name = worker-$i</span><span class="scorium-line"><span class="scorium-dim">12</span><span class="scorium-indent"></span>}</span><span class="scorium-line"><span class="scorium-dim">13</span> <b>end</b></span></code>
<span class="scorium-diagnostic"><i></i> parsed · sandboxed · schema valid</span>
</span>
<span class="scorium-pipeline" aria-hidden="true"><b>source</b><i>&rarr;</i><b>parse</b><i>&rarr;</i><b>evaluate</b><i>&rarr;</i><b>validate</b><i>&rarr;</i><b>apply</b></span>
<span class="scorium-watermark" aria-hidden="true">.SCOR</span>
<span class="image-label">Readable config / programmable core</span>
</a>
<div class="project-feature-copy">
<div class="project-meta"><span>Newest release</span><span>Rust / Lua 5.4 / pre-1.0</span></div>
<h3>Scorium</h3>
<p>A readable, embeddable configuration framework: declarative for simple files, programmable when repetition or conditions appear. It combines typed values, precise diagnostics, built-in schema validation, a canonical formatter, and sandboxed Lua—with no filesystem, network, or process access by default.</p>
<ul class="project-facts">
<li><strong>5</strong><span>focused Rust crates</span></li>
<li><strong>5</strong><span>CLI workflows</span></li>
<li><strong>0</strong><span>ambient system access</span></li>
</ul>
<div class="tag-row scorium-tags"><span>Source-available</span><span>PolyForm Strict</span><span>Sandboxed</span></div>
<a class="text-link" href="https://github.com/Fi3w0/Scorium" target="_blank" rel="noreferrer">Read the repository <span aria-hidden="true">&nearr;</span></a>
</div>
</article> </article>
<article class="project-feature reveal" data-repo="Flux"> <article class="project-feature reveal" data-repo="Flux">
<a class="project-visual project-visual-flux" href="https://github.com/Fi3w0/Flux" target="_blank" rel="noreferrer" aria-label="View Flux on GitHub"> <a class="project-visual project-visual-flux" href="https://github.com/Fi3w0/Flux" target="_blank" rel="noreferrer" aria-label="View Flux on GitHub">
<span class="flux-chrome" aria-hidden="true"><i></i><i></i><i></i></span> <span class="flux-product-frame">
<img class="flux-icon" src="img/flux-icon.png" alt="" width="256" height="256" loading="lazy"> <span class="flux-product-bar" aria-hidden="true"><span><i></i><i></i><i></i></span><b>Flux / Workbench</b><em>LOCAL</em></span>
<span class="flux-tags" aria-hidden="true"><span>SwiftUI</span><span>AppKit</span><span>Local-first</span></span> <img class="flux-product-shot" src="img/flux-overview.jpg" alt="Flux media workbench showing a download ready to add to the processing queue" width="1600" height="922" loading="lazy">
<span class="flux-stack" aria-hidden="true">
<b>Tool stack</b>
<span class="flux-stack-row"><span>01</span><span>FFmpeg<small>transcode + inspect</small></span></span>
<span class="flux-stack-row"><span>02</span><span>yt-dlp<small>downloads</small></span></span>
<span class="flux-stack-row"><span>03</span><span>Apple Vision<small>subject isolation</small></span></span>
<span class="flux-stack-row"><span>04</span><span>Sparkle<small>auto updates</small></span></span>
</span> </span>
<span class="flux-status" aria-hidden="true"><i></i> pre-flight checks passed</span> <span class="flux-product-caption" aria-hidden="true">
<span class="flux-watermark" aria-hidden="true">FLUX</span> <img src="img/flux-icon.png" alt="" width="256" height="256" loading="lazy">
<div class="flux-ascii" id="flux-ascii" aria-hidden="true"></div> <span><strong>Native by design</strong><small>SwiftUI · AppKit · local processing</small></span>
<span class="image-label">macOS Tahoe / Apple silicon</span> </span>
<span class="image-label">Current interface / macOS</span>
</a> </a>
<div class="project-feature-copy"> <div class="project-feature-copy">
<div class="project-meta"><span>Latest project</span><span>SwiftUI / AppKit / macOS</span></div> <div class="project-meta"><span>Native media workbench</span><span>SwiftUI / AppKit / macOS</span></div>
<h3>Flux</h3> <h3>Flux</h3>
<p>A native macOS media workbench: convert, download, edit, and compose media in one focused app. FFmpeg, yt-dlp, and Apple Vision underneath, pre-flight checks that explain failures before a job ever starts, and everything processed locally.</p> <p>A native macOS media workbench: convert, download, edit, and compose media in one focused app. FFmpeg, yt-dlp, and Apple Vision underneath, pre-flight checks that explain failures before a job ever starts, and everything processed locally.</p>
<ul class="project-facts"> <ul class="project-facts">
@ -257,14 +278,19 @@
</article> </article>
<article class="project-feature reveal" data-repo="Moonlit-shell"> <article class="project-feature reveal" data-repo="Moonlit-shell">
<a class="project-visual" href="https://github.com/Fi3w0/Moonlit-shell" target="_blank" rel="noreferrer" aria-label="View Moonlit Shell on GitHub"> <a class="project-visual project-visual-moonlit" href="https://github.com/Fi3w0/Moonlit-shell" target="_blank" rel="noreferrer" aria-label="View Moonlit Shell on GitHub">
<img src="img/moonlit-shell.jpg" alt="Moonlit Shell desktop with its top bar over a nocturnal landscape" width="1600" height="900"> <span class="moonlit-screen">
<span class="image-label">Daily driver / ThinkPad T14</span> <span class="moonlit-camera" aria-hidden="true"></span>
<img src="img/moonlit-shell.jpg" alt="Moonlit Shell running on Arch Linux with its custom top panels over a nocturnal landscape" width="1600" height="900" loading="lazy">
<span class="moonlit-session" aria-hidden="true"><b>Moonlit Shell</b><span>ThinkPad T14 · live session</span><i>ACTIVE</i></span>
</span>
<span class="moonlit-specs" aria-hidden="true"><span><b>12</b> native panels</span><span><b>/proc</b> direct telemetry</span><span><b>safe</b> dotfile updates</span></span>
<span class="image-label">Arch · Hyprland · Quickshell</span>
</a> </a>
<div class="project-feature-copy"> <div class="project-feature-copy">
<div class="project-meta"><span>Featured system</span><span>QML / Go / Linux</span></div> <div class="project-meta"><span>Daily system</span><span>QML / Go / Linux</span></div>
<h3>Moonlit Shell</h3> <h3>Moonlit Shell</h3>
<p>This is the desktop I actually use: Arch, Hyprland, and Quickshell with twelve panels, a live settings app, safe dotfile updates, and no monitoring daemon between the UI and <code>/proc</code>.</p> <p>My daily Arch desktop, built around Hyprland and Quickshell. Twelve native panels, one live settings surface, recoverable dotfile updates, and direct system telemetry without another monitoring daemon in the middle.</p>
<ul class="project-facts"> <ul class="project-facts">
<li><strong>12</strong><span>native panels</span></li> <li><strong>12</strong><span>native panels</span></li>
<li><strong>1</strong><span>live config</span></li> <li><strong>1</strong><span>live config</span></li>
@ -370,7 +396,7 @@
<header class="section-heading reveal"> <header class="section-heading reveal">
<p class="section-index">04 / Tools</p> <p class="section-index">04 / Tools</p>
<h2 id="stack-title">What is usually open in my terminal.</h2> <h2 id="stack-title">What is usually open in my terminal.</h2>
<p class="stack-intro">Kitty as the daily emulator, running on a MacBook (macOS) — my main device — SSH'd out to the Arch desktop and the Ubuntu server for everything else.</p> <p class="stack-intro">Kitty on a MacBook Air M4 is the main console, SSH'd out to the Arch desktop, ThinkPad T14, and Ubuntu server for everything else.</p>
</header> </header>
<div class="stack-layout"> <div class="stack-layout">
@ -403,6 +429,7 @@
<h2 id="contact-title" class="reveal">Say hello.</h2> <h2 id="contact-title" class="reveal">Say hello.</h2>
<p class="contact-copy reveal">Linux, infrastructure, self-hosting, Minecraft tooling, or a strange project you cannot stop thinking about are all good reasons to message me.</p> <p class="contact-copy reveal">Linux, infrastructure, self-hosting, Minecraft tooling, or a strange project you cannot stop thinking about are all good reasons to message me.</p>
<div class="contact-links reveal"> <div class="contact-links reveal">
<a href="mailto:alex@fiwlabs.dev"><span>Email</span><small>alex@fiwlabs.dev</small><b>&rarr;</b></a>
<a href="https://github.com/Fi3w0" target="_blank" rel="noreferrer"><span>GitHub</span><small>@Fi3w0</small><b>&nearr;</b></a> <a href="https://github.com/Fi3w0" target="_blank" rel="noreferrer"><span>GitHub</span><small>@Fi3w0</small><b>&nearr;</b></a>
<a href="https://t.me/fi3w0" target="_blank" rel="noreferrer"><span>Telegram</span><small>@fi3w0</small><b>&nearr;</b></a> <a href="https://t.me/fi3w0" target="_blank" rel="noreferrer"><span>Telegram</span><small>@fi3w0</small><b>&nearr;</b></a>
<button type="button" id="copy-discord"><span>Discord</span><small id="discord-label">fi3w0 - click to copy</small><b aria-hidden="true">+</b></button> <button type="button" id="copy-discord"><span>Discord</span><small id="discord-label">fi3w0 - click to copy</small><b aria-hidden="true">+</b></button>
@ -418,7 +445,7 @@
<p>&copy; <span id="year">2026</span> Alex / Fi3w0</p> <p>&copy; <span id="year">2026</span> Alex / Fi3w0</p>
</footer> </footer>
<script src="js/app.js?v=5" defer></script> <script src="js/app.js?v=9" defer></script>
</body> </body>
</html> </html>

379
js/app.js
View file

@ -241,383 +241,6 @@ if (gooeyNav && gooeyLinks.length && !reducedMotion) {
}); });
} }
// --- ASCIIText, ported from React Bits (vanilla JS, three.js loaded on
// demand). Replaces the flat "TIDE" watermark on the TideWM card with a
// rippling, wave-distorted ASCII render once the card scrolls into view.
// The plain-text watermark stays in the DOM as the fallback if WebGL or the
// three.js CDN load ever fails. ---
(() => {
const targets = [
{
host: document.querySelector("#tide-ascii"),
ancestorSelector: ".current-project",
readyClass: "has-tide-ascii",
text: "TIDE",
enableWaves: true,
asciiFontSize: 11,
textFontSize: 220,
},
{
// Flux is a media-editing tool, not a water compositor — keep its
// watermark shape still and crisp (no mesh ripple), but let the
// glyphs themselves flicker/decode for a "processing feed" feel.
host: document.querySelector("#flux-ascii"),
ancestorSelector: ".project-visual-flux",
readyClass: "has-flux-ascii",
text: "FLUX",
enableWaves: false,
charJitter: true,
asciiFontSize: 8,
textFontSize: 240,
},
]
.map((t) => ({ ...t, ancestor: t.host?.closest(t.ancestorSelector) }))
.filter((t) => t.host && t.ancestor);
if (!targets.length || reducedMotion) return;
let hasWebGL = false;
try {
const testCanvas = document.createElement("canvas");
hasWebGL = Boolean(testCanvas.getContext("webgl") || testCanvas.getContext("experimental-webgl"));
} catch {
hasWebGL = false;
}
if (!hasWebGL) return;
const THREE_CDN_URL = "https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js";
let threeLoadPromise = null;
const loadThree = () => {
if (window.THREE) return Promise.resolve();
if (!threeLoadPromise) {
threeLoadPromise = new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = THREE_CDN_URL;
script.onload = () => resolve();
script.onerror = () => reject(new Error("three.js failed to load"));
document.head.append(script);
});
}
return threeLoadPromise;
};
const mapRange = (n, start, stop, start2, stop2) => ((n - start) / (stop - start)) * (stop2 - start2) + start2;
const vertexShader = `
varying vec2 vUv;
uniform float uTime;
uniform float uEnableWaves;
void main() {
vUv = uv;
float time = uTime * 5.0;
float waveFactor = uEnableWaves;
vec3 transformed = position;
transformed.x += sin(time + position.y) * 0.5 * waveFactor;
transformed.y += cos(time + position.z) * 0.15 * waveFactor;
transformed.z += sin(time + position.x) * waveFactor;
gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);
}
`;
const fragmentShader = `
varying vec2 vUv;
uniform float uTime;
uniform float uEnableWaves;
uniform sampler2D uTexture;
void main() {
float time = uTime;
vec2 pos = vUv;
float amount = mix(0.006, 0.01, uEnableWaves);
float r = texture2D(uTexture, pos + cos(time * 2.0 - time + pos.x) * amount).r;
float g = texture2D(uTexture, pos + tan(time * 0.5 + pos.x - time) * amount).g;
float b = texture2D(uTexture, pos - cos(time * 2.0 + time + pos.y) * amount).b;
float a = texture2D(uTexture, pos).a;
gl_FragColor = vec4(r, g, b, a);
}
`;
class AsciiFilter {
constructor(renderer, { fontSize, fontFamily, charset, charJitter } = {}) {
this.renderer = renderer;
this.domElement = document.createElement("div");
this.domElement.className = "tide-ascii-inner";
this.pre = document.createElement("pre");
this.pre.className = "tide-ascii-pre";
this.domElement.append(this.pre);
this.canvas = document.createElement("canvas");
this.canvas.className = "tide-ascii-canvas";
this.context = this.canvas.getContext("2d");
this.domElement.append(this.canvas);
this.fontSize = fontSize ?? 8;
this.fontFamily = fontFamily ?? "'IBM Plex Mono', monospace";
this.charset = charset ?? " .'`^\",:;Il!i~+_-?][}{1)(|/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$";
// Optional per-cell character flicker — a subtle "decoding" glitch
// where a handful of glyphs jump to a denser/lighter char and settle
// back, independent of any mesh/wave motion.
this.charJitter = charJitter ?? false;
this.context.imageSmoothingEnabled = false;
}
setSize(width, height) {
if (!width || !height) return;
this.width = width;
this.height = height;
this.renderer.setSize(width, height);
this.reset();
}
reset() {
this.context.font = `${this.fontSize}px ${this.fontFamily}`;
const charWidth = this.context.measureText("A").width;
this.cols = Math.max(1, Math.floor(this.width / (this.fontSize * (charWidth / this.fontSize))));
this.rows = Math.max(1, Math.floor(this.height / this.fontSize));
this.canvas.width = this.cols;
this.canvas.height = this.rows;
this.pre.style.fontFamily = this.fontFamily;
this.pre.style.fontSize = `${this.fontSize}px`;
}
render(scene, camera, time) {
this.renderer.render(scene, camera);
const w = this.canvas.width;
const h = this.canvas.height;
this.context.clearRect(0, 0, w, h);
if (this.context && w && h) this.context.drawImage(this.renderer.domElement, 0, 0, w, h);
this.asciify(this.context, w, h, time ?? 0);
}
asciify(ctx, w, h, time = 0) {
if (!w || !h) return;
const imgData = ctx.getImageData(0, 0, w, h).data;
const jitter = this.charJitter;
const frameBucket = Math.floor(time * 7);
const last = this.charset.length - 1;
let str = "";
for (let y = 0; y < h; y += 1) {
for (let x = 0; x < w; x += 1) {
const i = x * 4 + y * 4 * w;
const [r, g, b, a] = [imgData[i], imgData[i + 1], imgData[i + 2], imgData[i + 3]];
if (a === 0) {
str += " ";
continue;
}
const gray = (0.3 * r + 0.6 * g + 0.1 * b) / 255;
let idx = this.charset.length - Math.floor((1 - gray) * last) - 1;
if (jitter) {
const seed = Math.sin(x * 12.9898 + y * 78.233 + frameBucket * 37.719) * 43758.5453;
const rnd = seed - Math.floor(seed);
if (rnd > 0.85) {
const swing = Math.floor(((rnd - 0.85) / 0.15) * 8) - 4;
idx = Math.min(last, Math.max(0, idx + swing));
}
}
str += this.charset[idx];
}
str += "\n";
}
this.pre.textContent = str;
}
dispose() {}
}
class CanvasTxt {
constructor(txt, { fontSize = 200, fontFamily = "Arial", color = "#fdf9f3" } = {}) {
this.canvas = document.createElement("canvas");
this.context = this.canvas.getContext("2d");
this.txt = txt;
this.font = `600 ${fontSize}px ${fontFamily}`;
this.color = color;
}
resize() {
this.context.font = this.font;
const metrics = this.context.measureText(this.txt);
this.canvas.width = Math.ceil(metrics.width) + 20;
this.canvas.height = Math.ceil((metrics.actualBoundingBoxAscent || 0) + (metrics.actualBoundingBoxDescent || 0)) + 20;
}
render() {
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.context.fillStyle = this.color;
this.context.font = this.font;
const metrics = this.context.measureText(this.txt);
this.context.fillText(this.txt, 10, 10 + metrics.actualBoundingBoxAscent);
}
get width() {
return this.canvas.width;
}
get height() {
return this.canvas.height;
}
get texture() {
return this.canvas;
}
}
class CanvAscii {
constructor({ text, asciiFontSize, textFontSize, textColor, planeBaseHeight, enableWaves, charJitter }, container, width, height) {
this.textString = text;
this.asciiFontSize = asciiFontSize;
this.textFontSize = textFontSize;
this.textColor = textColor;
this.planeBaseHeight = planeBaseHeight;
this.container = container;
this.width = width;
this.height = height;
this.enableWaves = enableWaves;
this.charJitter = charJitter;
this.camera = new window.THREE.PerspectiveCamera(45, this.width / this.height, 1, 1000);
this.camera.position.z = 30;
this.scene = new window.THREE.Scene();
}
async init() {
try {
await document.fonts.load(`600 200px "IBM Plex Mono"`);
} catch {
// fall back to default font metrics if the webfont isn't ready yet
}
this.setMesh();
this.setRenderer();
}
setMesh() {
const THREE = window.THREE;
this.textCanvas = new CanvasTxt(this.textString, {
fontSize: this.textFontSize,
fontFamily: "IBM Plex Mono",
color: this.textColor,
});
this.textCanvas.resize();
this.textCanvas.render();
this.texture = new THREE.CanvasTexture(this.textCanvas.texture);
this.texture.minFilter = THREE.NearestFilter;
const textAspect = this.textCanvas.width / this.textCanvas.height;
const planeW = this.planeBaseHeight * textAspect;
this.geometry = new THREE.PlaneGeometry(planeW, this.planeBaseHeight, 36, 36);
this.material = new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
transparent: true,
uniforms: {
uTime: { value: 0 },
uTexture: { value: this.texture },
uEnableWaves: { value: this.enableWaves ? 1 : 0 },
},
});
this.mesh = new THREE.Mesh(this.geometry, this.material);
this.scene.add(this.mesh);
}
setRenderer() {
const THREE = window.THREE;
this.renderer = new THREE.WebGLRenderer({ antialias: false, alpha: true });
this.renderer.setPixelRatio(1);
this.renderer.setClearColor(0x000000, 0);
this.filter = new AsciiFilter(this.renderer, { fontSize: this.asciiFontSize, charJitter: this.charJitter });
this.container.append(this.filter.domElement);
this.setSize(this.width, this.height);
}
setSize(w, h) {
if (!w || !h) return;
this.width = w;
this.height = h;
this.camera.aspect = w / h;
this.camera.updateProjectionMatrix();
this.filter.setSize(w, h);
}
animate() {
const frame = () => {
this.animationFrameId = requestAnimationFrame(frame);
this.renderFrame();
};
frame();
}
renderFrame() {
const time = Date.now() * 0.001;
this.mesh.material.uniforms.uTime.value = Math.sin(time);
this.filter.render(this.scene, this.camera, time);
}
dispose() {
if (this.animationFrameId) cancelAnimationFrame(this.animationFrameId);
this.filter?.dispose();
this.filter?.domElement?.remove();
this.renderer?.dispose();
}
}
const mount = async ({ host, ancestor, readyClass, text, enableWaves, charJitter, asciiFontSize, textFontSize }) => {
try {
await loadThree();
} catch {
return;
}
if (!window.THREE) return;
const rect = host.getBoundingClientRect();
if (rect.width < 20 || rect.height < 20) return;
let instance;
try {
instance = new CanvAscii(
{
text,
asciiFontSize: asciiFontSize ?? 11,
textFontSize: textFontSize ?? 220,
textColor: "#eaf9f7",
planeBaseHeight: 8,
enableWaves: enableWaves ?? true,
charJitter: charJitter ?? false,
},
host,
rect.width,
rect.height,
);
await instance.init();
instance.animate();
} catch {
instance?.dispose();
return;
}
host.classList.add("is-ready");
ancestor.classList.add(readyClass);
if ("ResizeObserver" in window) {
const resizeObserver = new ResizeObserver((entries) => {
const entry = entries[0];
if (!entry) return;
const { width, height } = entry.contentRect;
if (width > 20 && height > 20) instance.setSize(width, height);
});
resizeObserver.observe(host);
}
};
targets.forEach((target) => {
const observer = new IntersectionObserver(
(entries, obs) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
obs.disconnect();
mount(target);
});
},
{ rootMargin: "200px" },
);
observer.observe(target.host);
});
})();
// --- Liquid ASCII: a real FLIP/PIC fluid simulation (grid + particles, // --- Liquid ASCII: a real FLIP/PIC fluid simulation (grid + particles,
// incompressible pressure solve, particle-particle separation) rendered as // incompressible pressure solve, particle-particle separation) rendered as
// ASCII. No React, no registry, no CDN. Ported by hand from // ASCII. No React, no registry, no CDN. Ported by hand from
@ -1159,7 +782,7 @@ if (gooeyNav && gooeyLinks.length && !reducedMotion) {
let renderTick = 0; let renderTick = 0;
// Monospace glyph *advance width* is narrower than the font-size (for // Monospace glyph *advance width* is narrower than the font-size (for
// IBM Plex Mono, roughly 0.6x) — assuming 1 character == CELL px wide // A typical monospace font is roughly 0.6x as wide as it is tall — assuming 1 character == CELL px wide
// under-filled the row by the same margin, which is why the water // under-filled the row by the same margin, which is why the water
// stopped a good way short of the right edge even though the box itself // stopped a good way short of the right edge even though the box itself
// was full width. Measure the real rendered glyph box instead of // was full width. Measure the real rendered glyph box instead of

1703
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
{ {
"name": "fi3w0-portfolio", "name": "fiwlabs-homepage",
"version": "1.0.0", "version": "1.0.0",
"description": "Personal systems and development portfolio for Alex (Fi3w0).", "description": "Personal systems and development portfolio for Alex (Fi3w0).",
"private": true, "private": true,
@ -10,17 +10,22 @@
"homelab" "homelab"
], ],
"license": "MIT", "license": "MIT",
"author": "Alex (Fi3w0)", "author": "Alex (Fi3w0) <alex@fiwlabs.dev>",
"homepage": "https://fiwlabs.dev",
"engines": {
"node": ">=24",
"npm": ">=11"
},
"scripts": { "scripts": {
"start": "webpack serve --open --config webpack.config.dev.js", "start": "webpack serve --open --config webpack.config.dev.js",
"build": "webpack --config webpack.config.prod.js" "build": "webpack --config webpack.config.prod.js"
}, },
"devDependencies": { "devDependencies": {
"copy-webpack-plugin": "^11.0.0", "copy-webpack-plugin": "^14.0.0",
"html-webpack-plugin": "^5.6.0", "html-webpack-plugin": "^5.6.0",
"webpack": "^5.91.0", "webpack": "^5.91.0",
"webpack-cli": "^5.1.4", "webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.0.4", "webpack-dev-server": "^6.0.0",
"webpack-merge": "^5.10.0" "webpack-merge": "^5.10.0"
} }
} }