Compare commits

..

2 Commits

Author SHA1 Message Date
6ba7287889 Some adjustments
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
2026-02-06 19:33:43 -03:00
7a7dbf635d Update snapshots and .well-known handling
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
2026-02-02 10:44:23 -03:00
25 changed files with 51 additions and 1059 deletions

View File

@@ -1,4 +1,4 @@
# CI workflow: one clone, one workspace — install → lint → build → unit test - e2e test. # CI workflow: one clone, one workspace — install → lint → build → test.
# Runs on pull requests, push/tag/manual on main, or manual from any branch. # Runs on pull requests, push/tag/manual on main, or manual from any branch.
# Deploy workflow depends on this (ci) and runs only on main. # Deploy workflow depends on this (ci) and runs only on main.
when: when:
@@ -19,86 +19,22 @@ steps:
image: node:20-alpine image: node:20-alpine
commands: commands:
- corepack enable && corepack prepare pnpm@10.28.2 --activate - corepack enable && corepack prepare pnpm@10.28.2 --activate
- pnpm install --frozen-lockfile || pnpm install
- pnpm run lint - pnpm run lint
- pnpm run lint:css - pnpm run lint:css
depends_on:
- install
- name: Send Lint Status Notification (failure)
image: curlimages/curl
environment:
MATTERMOST_BOT_ACCESS_TOKEN:
from_secret: mattermost_bot_access_token
MATTERMOST_CHANNEL_ID:
from_secret: mattermost_tests_channel_id
MATTERMOST_POST_API_URL:
from_secret: mattermost_post_api_url
commands:
- |
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Lint failure 💩"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL
depends_on:
- lint
when:
- status: [failure]
- name: unit test
image: node:20-alpine
commands:
- corepack enable && corepack prepare pnpm@10.28.2 --activate
- pnpm install --frozen-lockfile || pnpm install
- pnpm exec svelte-kit sync
- pnpm test
depends_on:
- lint
- name: Send Unit Test Status Notification (failure)
image: curlimages/curl
environment:
MATTERMOST_BOT_ACCESS_TOKEN:
from_secret: mattermost_bot_access_token
MATTERMOST_CHANNEL_ID:
from_secret: mattermost_tests_channel_id
MATTERMOST_POST_API_URL:
from_secret: mattermost_post_api_url
commands:
- |
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Unit test failure 💩"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL
depends_on:
- unit test
when:
- status: [failure]
- name: build - name: build
image: node:20-alpine image: node:20-alpine
commands: commands:
- corepack enable && corepack prepare pnpm@10.28.2 --activate - corepack enable && corepack prepare pnpm@10.28.2 --activate
- pnpm install --frozen-lockfile || pnpm install
- pnpm run build - pnpm run build
depends_on:
- unit test
- name: Send Test Build Status Notification (failure) - name: test
image: curlimages/curl image: node:20-alpine
environment:
MATTERMOST_BOT_ACCESS_TOKEN:
from_secret: mattermost_bot_access_token
MATTERMOST_CHANNEL_ID:
from_secret: mattermost_tests_channel_id
MATTERMOST_POST_API_URL:
from_secret: mattermost_post_api_url
commands: commands:
- | - corepack enable && corepack prepare pnpm@10.28.2 --activate
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Test build failure 💩"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER") - pnpm test
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL
depends_on:
- build
when:
- status: [failure]
- name: e2e test - name: test e2e
image: mcr.microsoft.com/playwright:v1.58.0-noble image: mcr.microsoft.com/playwright:v1.58.0-noble
commands: commands:
- corepack enable && corepack prepare pnpm@10.28.2 --activate - corepack enable && corepack prepare pnpm@10.28.2 --activate
@@ -107,45 +43,3 @@ steps:
- npx serve dist -p 4173 & - npx serve dist -p 4173 &
- sleep 2 - sleep 2
- CI=1 pnpm run test:e2e - CI=1 pnpm run test:e2e
depends_on:
- build
- name: Send E2E Test Status Notification (failure)
image: curlimages/curl
environment:
MATTERMOST_BOT_ACCESS_TOKEN:
from_secret: mattermost_bot_access_token
MATTERMOST_CHANNEL_ID:
from_secret: mattermost_tests_channel_id
MATTERMOST_POST_API_URL:
from_secret: mattermost_post_api_url
commands:
- |
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] E2E test failure 💩"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL
depends_on:
- e2e test
when:
- status: [failure]
- name: Send CI Pipeline Status Notification (success)
image: curlimages/curl
environment:
MATTERMOST_BOT_ACCESS_TOKEN:
from_secret: mattermost_bot_access_token
MATTERMOST_CHANNEL_ID:
from_secret: mattermost_tests_channel_id
MATTERMOST_POST_API_URL:
from_secret: mattermost_post_api_url
commands:
- |
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] CI pipeline success 🎉"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL
depends_on:
- install
- lint
- unit test
- build
- e2e test
when:
- status: [success]

View File

@@ -2,10 +2,8 @@
# Runs on push to main, tag, or manual (only when on main). # Runs on push to main, tag, or manual (only when on main).
# Waits for ci workflow (install → lint → build → test) to succeed first. # Waits for ci workflow (install → lint → build → test) to succeed first.
when: when:
- branch: main branch: main
event: [push, tag, manual] event: [push, tag, manual]
- event: deployment
evaluate: 'CI_PIPELINE_DEPLOY_TARGET == "production"'
depends_on: depends_on:
- ci - ci
@@ -14,7 +12,6 @@ steps:
- name: 'Docker image build' - name: 'Docker image build'
image: docker:latest image: docker:latest
environment: environment:
DOCKER_API_VERSION: '1.43'
REGISTRY_REPO: git.mifi.dev/mifi-ventures/landing REGISTRY_REPO: git.mifi.dev/mifi-ventures/landing
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
@@ -32,46 +29,9 @@ steps:
. .
- echo "✓ Docker image built successfully" - echo "✓ Docker image built successfully"
- name: Send Build Status Notification (success)
image: curlimages/curl
environment:
MATTERMOST_BOT_ACCESS_TOKEN:
from_secret: mattermost_bot_access_token
MATTERMOST_CHANNEL_ID:
from_secret: mattermost_pushes_channel_id
MATTERMOST_POST_API_URL:
from_secret: mattermost_post_api_url
commands:
- |
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Build success 🎉"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL
depends_on:
- 'Docker image build'
when:
- status: [success]
- name: Send Build Status Notification (failure)
image: curlimages/curl
environment:
MATTERMOST_BOT_ACCESS_TOKEN:
from_secret: mattermost_bot_access_token
MATTERMOST_CHANNEL_ID:
from_secret: mattermost_pushes_channel_id
MATTERMOST_POST_API_URL:
from_secret: mattermost_post_api_url
commands:
- |
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Build failure 💩"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL
depends_on:
- Docker image build
when:
- status: [failure]
- name: 'Push to registry' - name: 'Push to registry'
image: docker:latest image: docker:latest
environment: environment:
DOCKER_API_VERSION: '1.43'
REGISTRY_URL: git.mifi.dev REGISTRY_URL: git.mifi.dev
REGISTRY_REPO: git.mifi.dev/mifi-ventures/landing REGISTRY_REPO: git.mifi.dev/mifi-ventures/landing
REGISTRY_USERNAME: REGISTRY_USERNAME:
@@ -95,42 +55,6 @@ steps:
depends_on: depends_on:
- 'Docker image build' - 'Docker image build'
- name: Send Push Status Notification (success)
image: curlimages/curl
environment:
MATTERMOST_BOT_ACCESS_TOKEN:
from_secret: mattermost_bot_access_token
MATTERMOST_CHANNEL_ID:
from_secret: mattermost_pushes_channel_id
MATTERMOST_POST_API_URL:
from_secret: mattermost_post_api_url
commands:
- |
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Push to registry success 🎉"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL
depends_on:
- 'Push to registry'
when:
- status: [success]
- name: Send Push Status Notification (failure)
image: curlimages/curl
environment:
MATTERMOST_BOT_ACCESS_TOKEN:
from_secret: mattermost_bot_access_token
MATTERMOST_CHANNEL_ID:
from_secret: mattermost_pushes_channel_id
MATTERMOST_POST_API_URL:
from_secret: mattermost_post_api_url
commands:
- |
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Push to registry failure 💩"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL
depends_on:
- 'Push to registry'
when:
- status: [failure]
- name: 'Trigger Portainer stack redeploy' - name: 'Trigger Portainer stack redeploy'
image: curlimages/curl:latest image: curlimages/curl:latest
environment: environment:
@@ -150,39 +74,3 @@ steps:
echo "✓ Portainer redeploy triggered (HTTP $code)" echo "✓ Portainer redeploy triggered (HTTP $code)"
depends_on: depends_on:
- 'Push to registry' - 'Push to registry'
- name: Send Deploy Status Notification (success)
image: curlimages/curl
environment:
MATTERMOST_BOT_ACCESS_TOKEN:
from_secret: mattermost_bot_access_token
MATTERMOST_CHANNEL_ID:
from_secret: mattermost_pushes_channel_id
MATTERMOST_POST_API_URL:
from_secret: mattermost_post_api_url
commands:
- |
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Production Deploy success 🎉"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL
depends_on:
- 'Trigger Portainer stack redeploy'
when:
- status: [success]
- name: Send Deploy Status Notification (failure)
image: curlimages/curl
environment:
MATTERMOST_BOT_ACCESS_TOKEN:
from_secret: mattermost_bot_access_token
MATTERMOST_CHANNEL_ID:
from_secret: mattermost_pushes_channel_id
MATTERMOST_POST_API_URL:
from_secret: mattermost_post_api_url
commands:
- |
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Production Deploy failure 💩"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL
depends_on:
- Trigger Portainer stack redeploy
when:
- status: [failure]

View File

@@ -1,75 +0,0 @@
# Agent guide: mifi Ventures landing
This file helps LLM agents work in this repo without introducing anti-patterns. Follow the architecture and conventions below.
## Purpose
- **Audience**: LLM agents (e.g. Cursor, Codex) making code or content changes.
- **Goal**: Preserve a minimal static site: SvelteKit prerender, no client-side app JS, shared theming, critical CSS inlining, and clear separation between app routes and static error pages.
## Stack and architecture
- **Framework**: SvelteKit with **adapter-static**. All routes are prerendered; there is no client-side router or hydration (`csr: false` in `src/routes/+layout.ts`).
- **Build**: `pnpm run build` = `vite build``node scripts/critters.mjs``node scripts/minify-static-js.mjs``node scripts/copy-410-paths.mjs`. Output is `dist/` (static files only).
- **Runtime**: nginx serves `dist/` (mounted as `/usr/share/nginx/html` in the container). No Node at runtime.
- **Theming**: CSS only. Light/dark follows **system preference** via `@media (prefers-color-scheme: dark)` in `src/app.css`. There is no JS theme toggle or `data-theme`; do not add one unless explicitly requested.
- **Fonts**: **Local only.** Inter and Fraunces are served from `static/assets/fonts/` (e.g. `inter-v20-latin-*.woff2`, `fraunces-v38-latin-*.woff2`). Preloads are in `src/routes/+layout.svelte`. Do not add Google Fonts or other external font URLs for the main site or error pages.
## Key paths
| Path | Role |
|------|------|
| `src/app.css` | Single global stylesheet: CSS variables (light/dark), base styles, components. Source of truth for theme tokens. |
| `src/app.html` | SvelteKit HTML shell. Rarely edited. |
| `src/routes/+layout.svelte` | Root layout: head (meta, fonts, favicon, scripts), skip link, slot. Imports `app.css`. |
| `src/routes/+layout.ts` | Exports `prerender = true`, `ssr = true`, `csr = false`. Do not enable CSR. |
| `src/routes/+page.svelte` | Home page; composes sections from `src/lib/components/`. |
| `src/lib/data/*.ts` | Content and meta (home-meta, content, experience, engagements, json-ld). Edit here for copy or SEO. |
| `src/lib/seo.ts` | SEO defaults (baseUrl, theme colors, etc.) and `mergeMeta()`. |
| `static/` | Copied as-is into `dist/` by SvelteKit. Favicon, robots.txt, fonts, images, **404.html**, **410.html**, and **assets/error-pages.css** live here. |
| `scripts/critters.mjs` | Post-build: inlines critical CSS into **every** `dist/*.html` (including 404 and 410). Resolves stylesheet URLs relative to `dist/`. |
| `scripts/minify-static-js.mjs` | Post-build: minifies JS in `dist/assets/`. |
| `scripts/copy-410-paths.mjs` | Post-build: copies `410.html` to each 410 URL path as `index.html` so static preview (e.g. `serve dist`) shows the 410 page at those URLs; nginx still returns 410 via explicit location blocks. |
| `nginx.conf` | Serves static files; `try_files $uri $uri/ /index.html` for SPA-style fallback; `error_page 404 /404.html` and `error_page 410 /410.html` for custom error pages. |
## Static error pages (404, 410)
- **Files**: `static/404.html`, `static/410.html`. They are **standalone HTML** (not Svelte). Do not convert them to Svelte routes.
- **Styling**: Both link to **one** shared stylesheet: `<link rel="stylesheet" href="/assets/error-pages.css">`. All error-page CSS lives in **`static/assets/error-pages.css`** (theme variables for light/dark, local `@font-face` for Inter/Fraunces, layout for `.error-page`). Do not duplicate theme tokens or add inline `<style>` in the HTML; keep a single source in `error-pages.css`.
- **Critical CSS**: The build runs Critters on all `dist/*.html`. Critters inlines critical CSS from linked stylesheets (including `/assets/error-pages.css`) into 404.html and 410.html. So:
- **Do** keep the `<link rel="stylesheet" href="/assets/error-pages.css">` in 404.html and 410.html; Critters will inline it at build time.
- **Do not** add error-page-only CSS in `src/app.css`; the app bundle is not loaded on error pages.
- **Theme alignment**: When changing light/dark colors or typography in `src/app.css`, update **`static/assets/error-pages.css`** so 404/410 stay visually consistent (same `--ep-*` tokens and, if needed, `@media (prefers-color-scheme: dark)`).
- **Local fonts**: Error pages use the same font paths as the main site (`/assets/fonts/...`) via `@font-face` in `error-pages.css`. Do not use Google Fonts or other external font URLs on error pages.
- **Preview vs production**: In preview (`serve dist`), the 410 URLs (e.g. `/pt/`, `/feed/`) are served by copying `410.html` to each path as `index.html` (see `scripts/copy-410-paths.mjs`). In production, nginx returns HTTP 410 for those paths and serves the same content via `error_page 410 /410.html`. If you add or remove 410 paths, update both `nginx.conf` and the `PATHS` array in `scripts/copy-410-paths.mjs`.
## Anti-patterns to avoid
1. **Enabling client-side rendering**
Do not set `csr: true` or add a client-side router. The site is intentionally static and JS-minimal.
2. **Adding app JavaScript for the main shell**
The only scripts are small, purposeful ones (e.g. `copyright-year.js`, `ga-init.js`, `mobile-menu-helper.js`) in `static/assets/js/`. Do not introduce a Svelte hydration bundle or large runtime for the main pages.
3. **External fonts**
Do not add `<link>` to Google Fonts (or similar) in layout or error pages. Use local fonts in `static/assets/fonts/` and reference them via preload (layout) or `@font-face` (error-pages.css).
4. **Skipping Critters for new HTML**
Any new `.html` in `static/` is copied to `dist/` and **must** be processed by Critters (the script already runs on all `dist/*.html`). Do not add static HTML that bypasses the build or that uses only inline styles without a linked stylesheet (linked styles get inlined by Critters).
5. **Diverging error page theme**
Do not change 404/410 styling in a way that ignores `static/assets/error-pages.css` or that duplicates theme tokens from `src/app.css` in ad-hoc form. Keep one error-page stylesheet and align its variables with `app.css` when you change the main theme.
6. **Breaking static export**
Do not add routes or behavior that require server-side rendering at request time (e.g. dynamic routes without prerender). The app is fully prerendered and served as static files.
7. **Scattering SEO or theme defaults**
Keep SEO defaults and theme-color values in `src/lib/seo.ts` and in layout/error pages that need them. Do not duplicate or hardcode them in many places.
## Quick reference
- **Change copy or structure (home)**: `src/lib/data/*.ts`, `src/lib/components/*.svelte`, `src/routes/+page.svelte`.
- **Change global styles or theme**: `src/app.css`. Then sync **`static/assets/error-pages.css`** if tokens or dark mode change.
- **Change error page copy or structure**: `static/404.html`, `static/410.html`. Style changes: **`static/assets/error-pages.css`** only.
- **Add a new static HTML page**: Add it under `static/`, link to `/assets/error-pages.css` (or a dedicated stylesheet that Critters can inline). Ensure `scripts/critters.mjs` runs over all `dist/*.html` (it already does).
- **Change nginx behavior**: `nginx.conf` (e.g. cache headers, `error_page`, `try_files`).

View File

@@ -14,7 +14,6 @@ export default [
'site/**', 'site/**',
'static/**', 'static/**',
'build.mjs', 'build.mjs',
'playwright-report/**',
], ],
}, },
js.configs.recommended, js.configs.recommended,

View File

@@ -108,25 +108,6 @@ http {
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;
} }
# 410 Gone: permanently removed URLs (tells crawlers to deindex)
error_page 410 /410.html;
location = /410.html {
add_header Cache-Control "no-cache, must-revalidate";
add_header Content-Type "text/html; charset=utf-8";
}
location = /2024/02/18/hello-world/ { return 410; }
location = /2024/02/18/hello-world { return 410; }
location = /pt/ { return 410; }
location = /pt { return 410; }
location = /feed/ { return 410; }
location = /feed { return 410; }
location = /category/uncategorized/feed/ { return 410; }
location = /category/uncategorized/feed { return 410; }
location = /category/uncategorized/ { return 410; }
location = /category/uncategorized { return 410; }
location = /comments/feed/ { return 410; }
location = /comments/feed { return 410; }
# Allow .well-known (security.txt, ACME challenge, etc.) # Allow .well-known (security.txt, ACME challenge, etc.)
location ^~ /.well-known/ { location ^~ /.well-known/ {
add_header Cache-Control "public, max-age=86400"; add_header Cache-Control "public, max-age=86400";
@@ -139,11 +120,7 @@ http {
log_not_found off; log_not_found off;
} }
# Custom 404 page (for missing static assets; SPA routes still try index.html first via try_files) # 404 falls back to index.html for SPA-style routing
error_page 404 /404.html; error_page 404 /index.html;
location = /404.html {
add_header Cache-Control "no-cache, must-revalidate";
add_header Content-Type "text/html; charset=utf-8";
}
} }
} }

View File

@@ -3,11 +3,11 @@
"version": "2.0.0", "version": "2.0.0",
"private": true, "private": true,
"repository": "https://git.mifi.dev/mifi-ventures/landing.git", "repository": "https://git.mifi.dev/mifi-ventures/landing.git",
"packageManager": "pnpm@10.30.0+sha512.2b5753de015d480eeb88f5b5b61e0051f05b4301808a82ec8b840c9d2adf7748eb352c83f5c1593ca703ff1017295bc3fdd3119abb9686efc96b9fcb18200937", "packageManager": "pnpm@10.28.2",
"description": "mifi Ventures landing site — SvelteKit static build with critical CSS inlining", "description": "mifi Ventures landing site — SvelteKit static build with critical CSS inlining",
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "vite build && node scripts/critters.mjs && node scripts/minify-static-js.mjs && node scripts/copy-410-paths.mjs", "build": "vite build && node scripts/critters.mjs",
"dev": "vite dev", "dev": "vite dev",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
@@ -34,7 +34,6 @@
"@typescript-eslint/eslint-plugin": "^8.54.0", "@typescript-eslint/eslint-plugin": "^8.54.0",
"@typescript-eslint/parser": "^8.54.0", "@typescript-eslint/parser": "^8.54.0",
"critters": "^0.0.24", "critters": "^0.0.24",
"esbuild": "^0.24.0",
"eslint": "^9.39.2", "eslint": "^9.39.2",
"jsdom": "^25.0.0", "jsdom": "^25.0.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",

261
pnpm-lock.yaml generated
View File

@@ -39,9 +39,6 @@ importers:
critters: critters:
specifier: ^0.0.24 specifier: ^0.0.24
version: 0.0.24 version: 0.0.24
esbuild:
specifier: ^0.24.0
version: 0.24.2
eslint: eslint:
specifier: ^9.39.2 specifier: ^9.39.2
version: 9.39.2 version: 9.39.2
@@ -456,252 +453,126 @@ packages:
peerDependencies: peerDependencies:
postcss: ^8.4 postcss: ^8.4
'@esbuild/aix-ppc64@0.24.2':
resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/aix-ppc64@0.25.12': '@esbuild/aix-ppc64@0.25.12':
resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [ppc64] cpu: [ppc64]
os: [aix] os: [aix]
'@esbuild/android-arm64@0.24.2':
resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm64@0.25.12': '@esbuild/android-arm64@0.25.12':
resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [arm64] cpu: [arm64]
os: [android] os: [android]
'@esbuild/android-arm@0.24.2':
resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-arm@0.25.12': '@esbuild/android-arm@0.25.12':
resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [arm] cpu: [arm]
os: [android] os: [android]
'@esbuild/android-x64@0.24.2':
resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/android-x64@0.25.12': '@esbuild/android-x64@0.25.12':
resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [x64] cpu: [x64]
os: [android] os: [android]
'@esbuild/darwin-arm64@0.24.2':
resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-arm64@0.25.12': '@esbuild/darwin-arm64@0.25.12':
resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@esbuild/darwin-x64@0.24.2':
resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/darwin-x64@0.25.12': '@esbuild/darwin-x64@0.25.12':
resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@esbuild/freebsd-arm64@0.24.2':
resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-arm64@0.25.12': '@esbuild/freebsd-arm64@0.25.12':
resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [arm64] cpu: [arm64]
os: [freebsd] os: [freebsd]
'@esbuild/freebsd-x64@0.24.2':
resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/freebsd-x64@0.25.12': '@esbuild/freebsd-x64@0.25.12':
resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [x64] cpu: [x64]
os: [freebsd] os: [freebsd]
'@esbuild/linux-arm64@0.24.2':
resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm64@0.25.12': '@esbuild/linux-arm64@0.25.12':
resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@esbuild/linux-arm@0.24.2':
resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-arm@0.25.12': '@esbuild/linux-arm@0.25.12':
resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
'@esbuild/linux-ia32@0.24.2':
resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-ia32@0.25.12': '@esbuild/linux-ia32@0.25.12':
resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [ia32] cpu: [ia32]
os: [linux] os: [linux]
'@esbuild/linux-loong64@0.24.2':
resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-loong64@0.25.12': '@esbuild/linux-loong64@0.25.12':
resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [loong64] cpu: [loong64]
os: [linux] os: [linux]
'@esbuild/linux-mips64el@0.24.2':
resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-mips64el@0.25.12': '@esbuild/linux-mips64el@0.25.12':
resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [mips64el] cpu: [mips64el]
os: [linux] os: [linux]
'@esbuild/linux-ppc64@0.24.2':
resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-ppc64@0.25.12': '@esbuild/linux-ppc64@0.25.12':
resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
'@esbuild/linux-riscv64@0.24.2':
resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-riscv64@0.25.12': '@esbuild/linux-riscv64@0.25.12':
resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
'@esbuild/linux-s390x@0.24.2':
resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-s390x@0.25.12': '@esbuild/linux-s390x@0.25.12':
resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
'@esbuild/linux-x64@0.24.2':
resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/linux-x64@0.25.12': '@esbuild/linux-x64@0.25.12':
resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@esbuild/netbsd-arm64@0.24.2':
resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-arm64@0.25.12': '@esbuild/netbsd-arm64@0.25.12':
resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [arm64] cpu: [arm64]
os: [netbsd] os: [netbsd]
'@esbuild/netbsd-x64@0.24.2':
resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/netbsd-x64@0.25.12': '@esbuild/netbsd-x64@0.25.12':
resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [x64] cpu: [x64]
os: [netbsd] os: [netbsd]
'@esbuild/openbsd-arm64@0.24.2':
resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-arm64@0.25.12': '@esbuild/openbsd-arm64@0.25.12':
resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [arm64] cpu: [arm64]
os: [openbsd] os: [openbsd]
'@esbuild/openbsd-x64@0.24.2':
resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/openbsd-x64@0.25.12': '@esbuild/openbsd-x64@0.25.12':
resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -714,48 +585,24 @@ packages:
cpu: [arm64] cpu: [arm64]
os: [openharmony] os: [openharmony]
'@esbuild/sunos-x64@0.24.2':
resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/sunos-x64@0.25.12': '@esbuild/sunos-x64@0.25.12':
resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [x64] cpu: [x64]
os: [sunos] os: [sunos]
'@esbuild/win32-arm64@0.24.2':
resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-arm64@0.25.12': '@esbuild/win32-arm64@0.25.12':
resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@esbuild/win32-ia32@0.24.2':
resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-ia32@0.25.12': '@esbuild/win32-ia32@0.25.12':
resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [ia32] cpu: [ia32]
os: [win32] os: [win32]
'@esbuild/win32-x64@0.24.2':
resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
'@esbuild/win32-x64@0.25.12': '@esbuild/win32-x64@0.25.12':
resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -1529,11 +1376,6 @@ packages:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
esbuild@0.24.2:
resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==}
engines: {node: '>=18'}
hasBin: true
esbuild@0.25.12: esbuild@0.25.12:
resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -3180,156 +3022,81 @@ snapshots:
dependencies: dependencies:
postcss: 8.5.6 postcss: 8.5.6
'@esbuild/aix-ppc64@0.24.2':
optional: true
'@esbuild/aix-ppc64@0.25.12': '@esbuild/aix-ppc64@0.25.12':
optional: true optional: true
'@esbuild/android-arm64@0.24.2':
optional: true
'@esbuild/android-arm64@0.25.12': '@esbuild/android-arm64@0.25.12':
optional: true optional: true
'@esbuild/android-arm@0.24.2':
optional: true
'@esbuild/android-arm@0.25.12': '@esbuild/android-arm@0.25.12':
optional: true optional: true
'@esbuild/android-x64@0.24.2':
optional: true
'@esbuild/android-x64@0.25.12': '@esbuild/android-x64@0.25.12':
optional: true optional: true
'@esbuild/darwin-arm64@0.24.2':
optional: true
'@esbuild/darwin-arm64@0.25.12': '@esbuild/darwin-arm64@0.25.12':
optional: true optional: true
'@esbuild/darwin-x64@0.24.2':
optional: true
'@esbuild/darwin-x64@0.25.12': '@esbuild/darwin-x64@0.25.12':
optional: true optional: true
'@esbuild/freebsd-arm64@0.24.2':
optional: true
'@esbuild/freebsd-arm64@0.25.12': '@esbuild/freebsd-arm64@0.25.12':
optional: true optional: true
'@esbuild/freebsd-x64@0.24.2':
optional: true
'@esbuild/freebsd-x64@0.25.12': '@esbuild/freebsd-x64@0.25.12':
optional: true optional: true
'@esbuild/linux-arm64@0.24.2':
optional: true
'@esbuild/linux-arm64@0.25.12': '@esbuild/linux-arm64@0.25.12':
optional: true optional: true
'@esbuild/linux-arm@0.24.2':
optional: true
'@esbuild/linux-arm@0.25.12': '@esbuild/linux-arm@0.25.12':
optional: true optional: true
'@esbuild/linux-ia32@0.24.2':
optional: true
'@esbuild/linux-ia32@0.25.12': '@esbuild/linux-ia32@0.25.12':
optional: true optional: true
'@esbuild/linux-loong64@0.24.2':
optional: true
'@esbuild/linux-loong64@0.25.12': '@esbuild/linux-loong64@0.25.12':
optional: true optional: true
'@esbuild/linux-mips64el@0.24.2':
optional: true
'@esbuild/linux-mips64el@0.25.12': '@esbuild/linux-mips64el@0.25.12':
optional: true optional: true
'@esbuild/linux-ppc64@0.24.2':
optional: true
'@esbuild/linux-ppc64@0.25.12': '@esbuild/linux-ppc64@0.25.12':
optional: true optional: true
'@esbuild/linux-riscv64@0.24.2':
optional: true
'@esbuild/linux-riscv64@0.25.12': '@esbuild/linux-riscv64@0.25.12':
optional: true optional: true
'@esbuild/linux-s390x@0.24.2':
optional: true
'@esbuild/linux-s390x@0.25.12': '@esbuild/linux-s390x@0.25.12':
optional: true optional: true
'@esbuild/linux-x64@0.24.2':
optional: true
'@esbuild/linux-x64@0.25.12': '@esbuild/linux-x64@0.25.12':
optional: true optional: true
'@esbuild/netbsd-arm64@0.24.2':
optional: true
'@esbuild/netbsd-arm64@0.25.12': '@esbuild/netbsd-arm64@0.25.12':
optional: true optional: true
'@esbuild/netbsd-x64@0.24.2':
optional: true
'@esbuild/netbsd-x64@0.25.12': '@esbuild/netbsd-x64@0.25.12':
optional: true optional: true
'@esbuild/openbsd-arm64@0.24.2':
optional: true
'@esbuild/openbsd-arm64@0.25.12': '@esbuild/openbsd-arm64@0.25.12':
optional: true optional: true
'@esbuild/openbsd-x64@0.24.2':
optional: true
'@esbuild/openbsd-x64@0.25.12': '@esbuild/openbsd-x64@0.25.12':
optional: true optional: true
'@esbuild/openharmony-arm64@0.25.12': '@esbuild/openharmony-arm64@0.25.12':
optional: true optional: true
'@esbuild/sunos-x64@0.24.2':
optional: true
'@esbuild/sunos-x64@0.25.12': '@esbuild/sunos-x64@0.25.12':
optional: true optional: true
'@esbuild/win32-arm64@0.24.2':
optional: true
'@esbuild/win32-arm64@0.25.12': '@esbuild/win32-arm64@0.25.12':
optional: true optional: true
'@esbuild/win32-ia32@0.24.2':
optional: true
'@esbuild/win32-ia32@0.25.12': '@esbuild/win32-ia32@0.25.12':
optional: true optional: true
'@esbuild/win32-x64@0.24.2':
optional: true
'@esbuild/win32-x64@0.25.12': '@esbuild/win32-x64@0.25.12':
optional: true optional: true
@@ -4059,34 +3826,6 @@ snapshots:
has-tostringtag: 1.0.2 has-tostringtag: 1.0.2
hasown: 2.0.2 hasown: 2.0.2
esbuild@0.24.2:
optionalDependencies:
'@esbuild/aix-ppc64': 0.24.2
'@esbuild/android-arm': 0.24.2
'@esbuild/android-arm64': 0.24.2
'@esbuild/android-x64': 0.24.2
'@esbuild/darwin-arm64': 0.24.2
'@esbuild/darwin-x64': 0.24.2
'@esbuild/freebsd-arm64': 0.24.2
'@esbuild/freebsd-x64': 0.24.2
'@esbuild/linux-arm': 0.24.2
'@esbuild/linux-arm64': 0.24.2
'@esbuild/linux-ia32': 0.24.2
'@esbuild/linux-loong64': 0.24.2
'@esbuild/linux-mips64el': 0.24.2
'@esbuild/linux-ppc64': 0.24.2
'@esbuild/linux-riscv64': 0.24.2
'@esbuild/linux-s390x': 0.24.2
'@esbuild/linux-x64': 0.24.2
'@esbuild/netbsd-arm64': 0.24.2
'@esbuild/netbsd-x64': 0.24.2
'@esbuild/openbsd-arm64': 0.24.2
'@esbuild/openbsd-x64': 0.24.2
'@esbuild/sunos-x64': 0.24.2
'@esbuild/win32-arm64': 0.24.2
'@esbuild/win32-ia32': 0.24.2
'@esbuild/win32-x64': 0.24.2
esbuild@0.25.12: esbuild@0.25.12:
optionalDependencies: optionalDependencies:
'@esbuild/aix-ppc64': 0.25.12 '@esbuild/aix-ppc64': 0.25.12

View File

@@ -1,39 +0,0 @@
#!/usr/bin/env node
/**
* Post-build: copy 410.html to each 410-Gone URL path as index.html.
* So static preview (serve dist) shows the 410 page at those URLs.
* nginx still returns 410 for these paths via explicit location blocks.
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const DIST = path.join(__dirname, '..', 'dist');
const SOURCE = path.join(DIST, '410.html');
const PATHS = [
'2024/02/18/hello-world',
'pt',
'feed',
'category/uncategorized/feed',
'category/uncategorized',
'comments/feed',
];
function main() {
if (!fs.existsSync(SOURCE)) {
console.error('dist/410.html not found. Run build first.');
process.exit(1);
}
const content = fs.readFileSync(SOURCE, 'utf8');
for (const dir of PATHS) {
const dirPath = path.join(DIST, dir);
fs.mkdirSync(dirPath, { recursive: true });
fs.writeFileSync(path.join(dirPath, 'index.html'), content, 'utf8');
}
console.log('✓ 410 page copied to', PATHS.length, 'paths for preview.');
}
main();

View File

@@ -1,45 +0,0 @@
#!/usr/bin/env node
/**
* Post-build: minify all JS in dist/assets/js/ (static scripts copied from static/assets/js/).
* Runs after vite build (and optionally after critters). Uses esbuild for minification.
*/
import * as esbuild from 'esbuild';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.join(__dirname, '..');
const JS_DIR = path.join(ROOT, 'dist', 'assets', 'js');
async function main() {
if (!fs.existsSync(JS_DIR)) {
console.warn('dist/assets/js/ not found; skipping static JS minify.');
return;
}
const files = fs.readdirSync(JS_DIR).filter((f) => f.endsWith('.js'));
if (files.length === 0) {
console.warn('No .js files in dist/assets/js/; skipping.');
return;
}
for (const file of files) {
const filePath = path.join(JS_DIR, file);
const code = fs.readFileSync(filePath, 'utf8');
const result = await esbuild.transform(code, {
minify: true,
target: 'es2015',
});
fs.writeFileSync(filePath, result.code, 'utf8');
console.log('✓ Minified dist/assets/js/' + file);
}
console.log('Static JS minify complete.');
}
main().catch((err) => {
console.error(err);
process.exit(1);
});

View File

@@ -8,7 +8,6 @@ set -e
SCRIPT_DIR="${BASH_SOURCE%/*}" SCRIPT_DIR="${BASH_SOURCE%/*}"
PROJECT_ROOT="${SCRIPT_DIR}/.." PROJECT_ROOT="${SCRIPT_DIR}/.."
cd "$PROJECT_ROOT" cd "$PROJECT_ROOT"
PROJECT_ROOT="$(pwd)"
if command -v docker >/dev/null 2>&1; then if command -v docker >/dev/null 2>&1; then
PLAYWRIGHT_IMAGE="${PLAYWRIGHT_IMAGE:-mcr.microsoft.com/playwright:v1.58.0-noble}" PLAYWRIGHT_IMAGE="${PLAYWRIGHT_IMAGE:-mcr.microsoft.com/playwright:v1.58.0-noble}"
@@ -32,8 +31,6 @@ else
echo "Updating snapshots in the current environment (matches CI when using the devcontainer)." echo "Updating snapshots in the current environment (matches CI when using the devcontainer)."
echo "" echo ""
pnpm run build
# Unset CI so Playwright config starts the preview server # Unset CI so Playwright config starts the preview server
unset CI unset CI
pnpm exec playwright test --update-snapshots pnpm exec playwright test --update-snapshots

View File

@@ -8,10 +8,12 @@
aria-labelledby="experience-heading" aria-labelledby="experience-heading"
> >
<div class="container"> <div class="container">
<h2 id="experience-heading" class="section-title">Previously at:</h2> <h2 id="experience-heading" class="section-title">
Experience includes teams at:
</h2>
<div class="logo-strip" role="list" aria-label="Company logos"> <div class="logo-strip" role="list" aria-label="Company logos">
{#each experienceLogos.filter((logo) => logo.showLogo) as logo (logo.alt)} {#each experienceLogos as logo (logo.alt)}
<div class="logo-item" role="listitem"> <div class="logo-item" role="listitem">
<img <img
src={logo.src} src={logo.src}

View File

@@ -16,8 +16,6 @@
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
aria-label="LinkedIn profile (opens in new tab)" aria-label="LinkedIn profile (opens in new tab)"
data-umami-event="footer link"
data-umami-event-label="linkedin"
> >
<LinkedInIcon size={15} /> <LinkedInIcon size={15} />
LinkedIn LinkedIn
@@ -29,8 +27,6 @@
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
aria-label="GitHub profile (opens in new tab)" aria-label="GitHub profile (opens in new tab)"
data-umami-event="footer link"
data-umami-event-label="github"
> >
<GithubIcon size={15} /> <GithubIcon size={15} />
GitHub GitHub

View File

@@ -19,8 +19,6 @@
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
aria-label="Schedule a 30-minute intro call (opens in new tab)" aria-label="Schedule a 30-minute intro call (opens in new tab)"
data-umami-event="schedule call"
data-umami-event-location="hero section"
> >
Schedule a 30-minute intro call Schedule a 30-minute intro call
<ExternalLinkIcon aria-label="Opens in new tab" size={17} /> <ExternalLinkIcon aria-label="Opens in new tab" size={17} />
@@ -30,8 +28,6 @@
class="btn btn-secondary icon-button" class="btn btn-secondary icon-button"
download download
aria-label="Download Mike Fitzpatrick's resume as PDF" aria-label="Download Mike Fitzpatrick's resume as PDF"
data-umami-event="download resume"
data-umami-event-location="hero section"
> >
Download resume Download resume
<FiletypePdfIcon aria-label="PDF format file" size={17} /> <FiletypePdfIcon aria-label="PDF format file" size={17} />

View File

@@ -14,19 +14,13 @@
<span class="mobile nav-header-logo"> <span class="mobile nav-header-logo">
<Wordmark /> <Wordmark />
</span> </span>
<button
type="button"
id="nav-toggle-button"
class="btn-clear"
aria-controls="nav-menu"
aria-label="Toggle navigation"
aria-expanded="false"
>
<label <label
id="nav-toggle-label" id="nav-toggle-label"
for="nav-toggle" for="nav-toggle"
class="nav-toggle" class="nav-toggle"
role="presentation" aria-controls="nav-menu"
aria-label="Toggle navigation"
aria-expanded="false"
> >
<span class="nav-toggle-inner"> <span class="nav-toggle-inner">
<span class="nav-toggle-line"></span> <span class="nav-toggle-line"></span>
@@ -34,7 +28,6 @@
<span class="nav-toggle-line"></span> <span class="nav-toggle-line"></span>
</span> </span>
</label> </label>
</button>
</div> </div>
<div id="nav-menu" class="nav-menu container"> <div id="nav-menu" class="nav-menu container">
<span class="nav-header-logo desktop"> <span class="nav-header-logo desktop">
@@ -42,45 +35,20 @@
</span> </span>
<ul class="nav-list"> <ul class="nav-list">
<li class="nav-item"> <li class="nav-item">
<a <a href="#what-we-do" class="nav-link">Services</a>
href="#what-we-do"
class="nav-link"
data-umami-event="navigation"
data-umami-event-label="services">Services</a
>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a <a href="#impact" class="nav-link">Impact</a>
href="#impact"
class="nav-link"
data-umami-event="navigation"
data-umami-event-label="impact">Impact</a
>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a <a href="#how-we-work" class="nav-link">Process</a>
href="#how-we-work"
class="nav-link"
data-umami-event="navigation"
data-umami-event-label="process">Process</a
>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a <a href="#schedule" class="nav-link">Contact</a>
href="#schedule"
class="nav-link"
data-umami-event="navigation"
data-umami-event-label="contact">Contact</a
>
</li> </li>
</ul> </ul>
<div class="nav-item nav-back-to-top"> <div class="nav-item nav-back-to-top">
<a <a href="#header" class="nav-link">Back to top</a>
href="#header"
class="nav-link"
data-umami-event="navigation"
data-umami-event-label="back to top">Back to top</a
>
</div> </div>
</div> </div>
</nav> </nav>
@@ -181,14 +149,6 @@
} }
} }
.btn-clear {
background: transparent;
border: none;
padding: 0;
margin: 0;
cursor: pointer;
}
.nav-toggle { .nav-toggle {
display: none; display: none;
align-items: center; align-items: center;

View File

@@ -16,8 +16,6 @@
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
aria-label="Schedule a 30-minute intro call (opens in new tab)" aria-label="Schedule a 30-minute intro call (opens in new tab)"
data-umami-event="schedule call"
data-umami-event-location="schedule section"
> >
Schedule a 30-minute intro call Schedule a 30-minute intro call
<ExternalLinkIcon aria-label="Opens in new tab" size={17} /> <ExternalLinkIcon aria-label="Opens in new tab" size={17} />

View File

@@ -1,52 +1,25 @@
export const experienceLogos = [ export const experienceLogos = [
{ { src: '/assets/logos/atlassian.svg', alt: 'Atlassian', width: 2500, height: 2500 },
src: '/assets/logos/atlassian.svg',
alt: 'Atlassian',
width: 2500,
height: 2500,
showLogo: true,
},
{ {
src: '/assets/logos/tjx.svg', src: '/assets/logos/tjx.svg',
alt: 'TJ Maxx (The TJX Companies)', alt: 'TJ Maxx (The TJX Companies)',
width: 2500, width: 2500,
height: 621, height: 621,
showLogo: false,
},
{
src: '/assets/logos/cargurus.svg',
alt: 'CarGurus',
width: 2500,
height: 398,
showLogo: true,
},
{
src: '/assets/logos/timberland.svg',
alt: 'Timberland',
width: 190,
height: 35,
showLogo: true,
},
{
src: '/assets/logos/vf.svg',
alt: 'VF Corporation',
width: 190,
height: 155,
showLogo: true,
}, },
{ src: '/assets/logos/cargurus.svg', alt: 'CarGurus', width: 2500, height: 398 },
{ src: '/assets/logos/timberland.svg', alt: 'Timberland', width: 190, height: 35 },
{ src: '/assets/logos/vf.svg', alt: 'VF Corporation', width: 190, height: 155 },
{ {
src: '/assets/logos/bottomline.svg', src: '/assets/logos/bottomline.svg',
alt: 'Bottomline Technologies', alt: 'Bottomline Technologies',
width: 2702, width: 2702,
height: 571, height: 571,
showLogo: true,
}, },
{ {
src: '/assets/logos/mfa-boston.svg', src: '/assets/logos/mfa-boston.svg',
alt: 'Museum of Fine Arts Boston', alt: 'Museum of Fine Arts Boston',
width: 572, width: 572,
height: 88, height: 88,
showLogo: true,
}, },
] as const; ] as const;

View File

@@ -30,12 +30,6 @@
<!-- Google tag (gtag.js) --> <!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-36F29PDKRT"></script> <script async src="https://www.googletagmanager.com/gtag/js?id=G-36F29PDKRT"></script>
<script defer src="/assets/js/ga-init.js"></script> <script defer src="/assets/js/ga-init.js"></script>
<script
defer
src="https://analytics.mifi.holdings/script.js"
data-website-id="72ac01ce-e7fc-4582-8593-703f15add8d5"
></script>
<script defer src="/assets/js/umami-helper.js"></script>
<title>{merged.title}</title> <title>{merged.title}</title>
<meta name="description" content={merged.description ?? ''} /> <meta name="description" content={merged.description ?? ''} />
@@ -149,15 +143,5 @@
<script src="/assets/js/mobile-menu-helper.js" defer></script> <script src="/assets/js/mobile-menu-helper.js" defer></script>
</svelte:head> </svelte:head>
<a href="#main" class="skip-link" data-umami-event="skip to main content" <a href="#main" class="skip-link">Skip to main content</a>
>Skip to main content</a
>
{@render children()} {@render children()}
<img
src="https://analytics.mifi.holdings/p/wQ9GYnLIg"
alt=""
width="1"
height="1"
role="presentation"
loading="eager"
/>

View File

@@ -1,20 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>404 Not Found — mifi Ventures</title>
<meta name="theme-color" content="#0052cc" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#4da6ff" media="(prefers-color-scheme: dark)">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="stylesheet" href="/assets/error-pages.css">
</head>
<body class="error-page">
<main>
<div class="emoji" aria-hidden="true">🔍</div>
<h1>404 Not Found</h1>
<p>This page went off to find itself. Were not sure its coming back.</p>
<p><a href="/">Back to mifi Ventures →</a></p>
</main>
</body>
</html>

View File

@@ -1,20 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>410 Gone — mifi Ventures</title>
<meta name="theme-color" content="#0052cc" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#4da6ff" media="(prefers-color-scheme: dark)">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="stylesheet" href="/assets/error-pages.css">
</head>
<body class="error-page">
<main>
<div class="emoji" aria-hidden="true">👋</div>
<h1>410 Gone</h1>
<p>This page has left the building. Weve moved on—and so should you.</p>
<p><a href="/">Back to mifi Ventures →</a></p>
</main>
</body>
</html>

View File

@@ -1,102 +0,0 @@
/**
* Shared styles for static error pages (404, 410).
* Uses same theme tokens and local fonts as the main site.
* Linked from 404.html and 410.html; Critters inlines critical CSS at build time.
*/
/* Theme: light (default) — matches src/app.css :root */
:root {
color-scheme: light dark;
--ep-bg: #ffffff;
--ep-bg-alt: #faf9ff;
--ep-text: #14121a;
--ep-text-secondary: #3f3a4a;
--ep-primary: #6d28d9;
--ep-primary-hover: #5b21b6;
--ep-font: 'Inter', system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
--ep-font-heading: 'Fraunces', ui-serif, Georgia, 'Times New Roman', serif;
--ep-size-base: 18px;
--ep-line-height: 1.75;
}
/* Theme: dark — matches src/app.css @media (prefers-color-scheme: dark) */
@media (prefers-color-scheme: dark) {
:root {
--ep-bg: #0b0b12;
--ep-bg-alt: #121226;
--ep-text: #f3f2ff;
--ep-text-secondary: #c9c6e4;
--ep-primary: #a78bfa;
--ep-primary-hover: #c4b5fd;
}
}
/* Local fonts — same paths as +layout.svelte preloads */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/assets/fonts/inter-v20-latin-regular.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('/assets/fonts/inter-v20-latin-500.woff2') format('woff2');
}
@font-face {
font-family: 'Fraunces';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/assets/fonts/fraunces-v38-latin-600.woff2') format('woff2');
}
/* Error page layout */
.error-page {
margin: 0;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
font-family: var(--ep-font);
font-size: var(--ep-size-base);
line-height: var(--ep-line-height);
color: var(--ep-text);
background-color: var(--ep-bg-alt);
}
.error-page main {
text-align: center;
padding: 2rem 1.5rem;
max-width: 28rem;
}
.error-page .emoji {
font-size: 4rem;
margin-bottom: 0.5rem;
}
.error-page h1 {
font-family: var(--ep-font-heading);
font-size: 2rem;
font-weight: 600;
margin: 0 0 0.75rem;
color: var(--ep-text);
}
.error-page p {
margin: 0 0 1.5rem;
color: var(--ep-text-secondary);
}
.error-page a {
color: var(--ep-primary);
font-weight: 500;
text-decoration: none;
}
.error-page a:hover {
color: var(--ep-primary-hover);
text-decoration: underline;
}
.error-page a:focus-visible {
outline: 2px solid var(--ep-primary);
outline-offset: 2px;
}

View File

@@ -1,12 +1,8 @@
const MOBILE_BREAKPOINT_PX = 768; const MOBILE_BREAKPOINT_PX = 768;
/** All focusable elements inside the menu (links). */
const getMenuFocusables = (menu) =>
menu.querySelectorAll('a[href]');
const mobileMenuHelper = () => { const mobileMenuHelper = () => {
const mobileMenu = document.getElementById('nav-menu'); const mobileMenu = document.getElementById('nav-menu');
const mobileMenuToggleButton = document.getElementById('nav-toggle-button'); const mobileMenuToggleLabel = document.getElementById('nav-toggle-label');
const mobileMenuToggle = document.querySelector('.nav-toggle-input'); const mobileMenuToggle = document.querySelector('.nav-toggle-input');
const menuItems = mobileMenu.querySelectorAll('.nav-item'); const menuItems = mobileMenu.querySelectorAll('.nav-item');
@@ -14,75 +10,35 @@ const mobileMenuHelper = () => {
const syncMenuAriaHidden = () => { const syncMenuAriaHidden = () => {
if (isMobile()) { if (isMobile()) {
const hidden = !mobileMenuToggle.checked; mobileMenu.setAttribute('aria-hidden', mobileMenuToggle.checked ? 'false' : 'true');
mobileMenu.setAttribute(
'aria-hidden',
hidden ? 'true' : 'false',
);
// inert removes the subtree from the a11y tree and makes descendants non-focusable
if (hidden) {
mobileMenu.setAttribute('inert', '');
} else {
mobileMenu.removeAttribute('inert');
}
setMenuFocusablesTabIndex();
} else { } else {
mobileMenu.removeAttribute('aria-hidden'); mobileMenu.removeAttribute('aria-hidden');
mobileMenu.removeAttribute('inert');
getMenuFocusables(mobileMenu).forEach((el) => {
el.removeAttribute('tabindex');
});
} }
}; };
const setMenuFocusablesTabIndex = () => { const syncLabelAriaExpanded = () => {
const focusables = getMenuFocusables(mobileMenu); mobileMenuToggleLabel.setAttribute('aria-expanded', mobileMenuToggle.checked ? 'true' : 'false');
const hidden = !mobileMenuToggle.checked;
focusables.forEach((el) => {
if (hidden) {
el.setAttribute('tabindex', '-1');
} else {
el.removeAttribute('tabindex');
}
});
};
const syncButtonAriaExpanded = () => {
mobileMenuToggleButton.setAttribute(
'aria-expanded',
mobileMenuToggle.checked ? 'true' : 'false',
);
}; };
menuItems.forEach((item) => { menuItems.forEach((item) => {
item.addEventListener('click', () => { item.addEventListener('click', () => {
mobileMenuToggle.checked = false; mobileMenuToggle.checked = false;
syncButtonAriaExpanded(); syncLabelAriaExpanded();
syncMenuAriaHidden(); syncMenuAriaHidden();
}); });
}); });
mobileMenuToggle.addEventListener('change', () => { mobileMenuToggle.addEventListener('change', () => {
syncButtonAriaExpanded(); syncLabelAriaExpanded();
syncMenuAriaHidden(); syncMenuAriaHidden();
}); });
mobileMenuToggleButton.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
mobileMenuToggle.checked = !mobileMenuToggle.checked;
// Programmatic .checked change does not fire 'change'; sync state so menu is focusable when open
syncButtonAriaExpanded();
syncMenuAriaHidden();
}
});
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
syncMenuAriaHidden(); syncMenuAriaHidden();
}); });
syncMenuAriaHidden(); syncMenuAriaHidden();
syncButtonAriaExpanded(); syncLabelAriaExpanded();
}; };
document.addEventListener('DOMContentLoaded', mobileMenuHelper); document.addEventListener('DOMContentLoaded', mobileMenuHelper);

View File

@@ -1,28 +0,0 @@
// Umami: safe track (no-op if script blocked or not loaded)
function umamiTrack(name, data) {
if (
typeof window.umami !== 'undefined' &&
typeof window.umami.track === 'function'
) {
if (data != null) window.umami.track(name, data);
else window.umami.track(name);
}
}
// Umami: scroll depth (25%, 50%, 75%, 100%) once per milestone
const scrollMilestones = new Set();
function onScroll() {
const doc = document.documentElement;
const scrollTop = doc.scrollTop || document.body.scrollTop;
const scrollHeight =
(doc.scrollHeight || document.body.scrollHeight) - window.innerHeight;
if (scrollHeight <= 0) return;
const pct = Math.round((scrollTop / scrollHeight) * 100);
for (const m of [25, 50, 75, 100]) {
if (pct >= m && !scrollMilestones.has(m)) {
scrollMilestones.add(m);
umamiTrack('scroll-depth', { depth: String(m) });
}
}
}
window.addEventListener('scroll', onScroll, { passive: true });

View File

@@ -29,17 +29,11 @@ function createNavDOM() {
const header = document.createElement('div'); const header = document.createElement('div');
header.className = 'mobile-nav-header'; header.className = 'mobile-nav-header';
const toggleButton = document.createElement('button');
toggleButton.type = 'button';
toggleButton.id = 'nav-toggle-button';
toggleButton.setAttribute('aria-controls', 'nav-menu');
toggleButton.setAttribute('aria-expanded', 'false');
const label = document.createElement('label'); const label = document.createElement('label');
label.id = 'nav-toggle-label'; label.id = 'nav-toggle-label';
label.htmlFor = 'nav-toggle'; label.htmlFor = 'nav-toggle';
label.className = 'nav-toggle'; label.className = 'nav-toggle';
toggleButton.appendChild(label); header.appendChild(label);
header.appendChild(toggleButton);
nav.appendChild(header); nav.appendChild(header);
const menu = document.createElement('div'); const menu = document.createElement('div');
@@ -59,7 +53,7 @@ function createNavDOM() {
nav.appendChild(menu); nav.appendChild(menu);
document.body.appendChild(nav); document.body.appendChild(nav);
return { nav, menu, toggleButton, label, checkbox, item, link }; return { nav, menu, label, checkbox, item };
} }
describe('mobile-menu-helper.js', () => { describe('mobile-menu-helper.js', () => {
@@ -81,12 +75,7 @@ describe('mobile-menu-helper.js', () => {
it('sets aria-hidden on menu when viewport is mobile and menu is closed', () => { it('sets aria-hidden on menu when viewport is mobile and menu is closed', () => {
expect(dom.menu.getAttribute('aria-hidden')).toBe('true'); expect(dom.menu.getAttribute('aria-hidden')).toBe('true');
expect(dom.toggleButton.getAttribute('aria-expanded')).toBe('false'); expect(dom.label.getAttribute('aria-expanded')).toBe('false');
});
it('sets inert and tabindex="-1" on links when menu is closed on mobile', () => {
expect(dom.menu.hasAttribute('inert')).toBe(true);
expect(dom.link.getAttribute('tabindex')).toBe('-1');
}); });
it('sets aria-hidden to false when menu is open on mobile', () => { it('sets aria-hidden to false when menu is open on mobile', () => {
@@ -94,55 +83,35 @@ describe('mobile-menu-helper.js', () => {
dom.checkbox.dispatchEvent(new Event('change', { bubbles: true })); dom.checkbox.dispatchEvent(new Event('change', { bubbles: true }));
expect(dom.menu.getAttribute('aria-hidden')).toBe('false'); expect(dom.menu.getAttribute('aria-hidden')).toBe('false');
expect(dom.toggleButton.getAttribute('aria-expanded')).toBe('true'); expect(dom.label.getAttribute('aria-expanded')).toBe('true');
}); });
it('removes inert and link tabindex when menu is open on mobile', () => { it('removes aria-hidden from menu when viewport is desktop', () => {
dom.checkbox.checked = true;
dom.checkbox.dispatchEvent(new Event('change', { bubbles: true }));
expect(dom.menu.hasAttribute('inert')).toBe(false);
expect(dom.link.hasAttribute('tabindex')).toBe(false);
});
it('removes aria-hidden and inert from menu when viewport is desktop', () => {
setViewportWidth(1024); setViewportWidth(1024);
window.dispatchEvent(new Event('resize')); window.dispatchEvent(new Event('resize'));
expect(dom.menu.hasAttribute('aria-hidden')).toBe(false); expect(dom.menu.hasAttribute('aria-hidden')).toBe(false);
expect(dom.menu.hasAttribute('inert')).toBe(false);
expect(dom.link.hasAttribute('tabindex')).toBe(false);
}); });
it('adds aria-hidden and inert when resizing from desktop to mobile', () => { it('adds aria-hidden when resizing from desktop to mobile', () => {
setViewportWidth(1024); setViewportWidth(1024);
window.dispatchEvent(new Event('resize')); window.dispatchEvent(new Event('resize'));
expect(dom.menu.hasAttribute('aria-hidden')).toBe(false); expect(dom.menu.hasAttribute('aria-hidden')).toBe(false);
expect(dom.menu.hasAttribute('inert')).toBe(false);
setViewportWidth(400); setViewportWidth(400);
window.dispatchEvent(new Event('resize')); window.dispatchEvent(new Event('resize'));
expect(dom.menu.getAttribute('aria-hidden')).toBe('true'); expect(dom.menu.getAttribute('aria-hidden')).toBe('true');
expect(dom.menu.hasAttribute('inert')).toBe(true);
expect(dom.link.getAttribute('tabindex')).toBe('-1');
}); });
it('closes menu and syncs aria when a menu item is clicked', () => { it('closes menu and syncs aria when a menu item is clicked', () => {
dom.checkbox.checked = true; dom.checkbox.checked = true;
dom.checkbox.dispatchEvent(new Event('change', { bubbles: true })); dom.checkbox.dispatchEvent(new Event('change', { bubbles: true }));
expect(dom.toggleButton.getAttribute('aria-expanded')).toBe('true'); expect(dom.label.getAttribute('aria-expanded')).toBe('true');
dom.item.click(); dom.item.click();
expect(dom.checkbox.checked).toBe(false); expect(dom.checkbox.checked).toBe(false);
expect(dom.toggleButton.getAttribute('aria-expanded')).toBe('false'); expect(dom.label.getAttribute('aria-expanded')).toBe('false');
expect(dom.menu.getAttribute('aria-hidden')).toBe('true'); expect(dom.menu.getAttribute('aria-hidden')).toBe('true');
expect(dom.menu.hasAttribute('inert')).toBe(true);
expect(dom.link.getAttribute('tabindex')).toBe('-1');
}); });
// Keyboard open (Enter/Space on toggle button) is not asserted here: jsdoms KeyboardEvent
// often does not set e.key, so the keydown handler may not run. Opening and sync are covered
// by “sets aria-hidden to false when menu is open” and “removes inert and link tabindex when
// menu is open”; keyboard open can be covered in e2e.
}); });

Binary file not shown.

Before

Width:  |  Height:  |  Size: 577 KiB

After

Width:  |  Height:  |  Size: 583 KiB

View File

@@ -5,12 +5,6 @@ export default defineConfig({
plugins: [sveltekit()], plugins: [sveltekit()],
server: { server: {
host: true, // listen on 0.0.0.0 so reachable from host (e.g. dev container, port forward) host: true, // listen on 0.0.0.0 so reachable from host (e.g. dev container, port forward)
port: 5173,
strictPort: false,
// Disable HMR WebSocket when using port forwarding (e.g. dev container); the tunnel
// often doesn't proxy WebSockets, causing repeated connection failures in the console.
// With csr: false we don't use client-side HMR anyway—refresh the page to see changes.
hmr: false,
}, },
preview: { preview: {
host: true, host: true,