Compare commits
27 Commits
6ba7287889
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
66640fa535
|
|||
|
5e0e211f80
|
|||
|
d66e9f7cb8
|
|||
|
39ba54e254
|
|||
|
56b5740393
|
|||
|
3a94a50def
|
|||
|
1349488827
|
|||
|
c963e34766
|
|||
|
f91531b5fa
|
|||
|
b0146992c2
|
|||
|
7a01cbd2c9
|
|||
|
fe8cf26a29
|
|||
|
a519df1016
|
|||
|
ceeb76663b
|
|||
|
14edd403eb
|
|||
|
9f43bf7879
|
|||
|
2d0a4935a5
|
|||
|
e4929a4699
|
|||
|
af705efc17
|
|||
|
f73b7822a5
|
|||
| c094cc29ea | |||
|
4cfe2c5da0
|
|||
| 864c9a735c | |||
|
11ff3dcff3
|
|||
|
0f423f0677
|
|||
| aaea4169f9 | |||
| e76f0f79e8 |
@@ -1,4 +1,4 @@
|
|||||||
# CI workflow: one clone, one workspace — install → lint → build → test.
|
# CI workflow: one clone, one workspace — install → lint → build → unit test - e2e 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,22 +19,86 @@ 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: test
|
- name: Send Test Build Status Notification (failure)
|
||||||
image: node:20-alpine
|
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:
|
commands:
|
||||||
- corepack enable && corepack prepare pnpm@10.28.2 --activate
|
- |
|
||||||
- pnpm test
|
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Test 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:
|
||||||
|
- build
|
||||||
|
when:
|
||||||
|
- status: [failure]
|
||||||
|
|
||||||
- name: test e2e
|
- name: e2e test
|
||||||
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
|
||||||
@@ -43,3 +107,45 @@ 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]
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
# 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
|
||||||
@@ -12,6 +14,7 @@ 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
|
||||||
@@ -29,9 +32,46 @@ 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:
|
||||||
@@ -55,6 +95,42 @@ 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:
|
||||||
@@ -74,3 +150,39 @@ 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]
|
||||||
|
|||||||
75
AGENTS.md
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# 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`).
|
||||||
@@ -14,6 +14,7 @@ export default [
|
|||||||
'site/**',
|
'site/**',
|
||||||
'static/**',
|
'static/**',
|
||||||
'build.mjs',
|
'build.mjs',
|
||||||
|
'playwright-report/**',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
js.configs.recommended,
|
js.configs.recommended,
|
||||||
|
|||||||
BIN
logos/avatar.af
BIN
logos/favicon cutout block (new).af
Normal file
BIN
logos/favicon.af
34
nginx.conf
@@ -108,14 +108,42 @@ http {
|
|||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Deny access to hidden files (.git, .env, etc.)
|
# 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.)
|
||||||
|
location ^~ /.well-known/ {
|
||||||
|
add_header Cache-Control "public, max-age=86400";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Deny access to other hidden files (.git, .env, etc.)
|
||||||
location ~ /\. {
|
location ~ /\. {
|
||||||
deny all;
|
deny all;
|
||||||
access_log off;
|
access_log off;
|
||||||
log_not_found off;
|
log_not_found off;
|
||||||
}
|
}
|
||||||
|
|
||||||
# 404 falls back to index.html for SPA-style routing
|
# Custom 404 page (for missing static assets; SPA routes still try index.html first via try_files)
|
||||||
error_page 404 /index.html;
|
error_page 404 /404.html;
|
||||||
|
location = /404.html {
|
||||||
|
add_header Cache-Control "no-cache, must-revalidate";
|
||||||
|
add_header Content-Type "text/html; charset=utf-8";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.28.2",
|
"packageManager": "pnpm@10.30.0+sha512.2b5753de015d480eeb88f5b5b61e0051f05b4301808a82ec8b840c9d2adf7748eb352c83f5c1593ca703ff1017295bc3fdd3119abb9686efc96b9fcb18200937",
|
||||||
"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",
|
"build": "vite build && node scripts/critters.mjs && node scripts/minify-static-js.mjs && node scripts/copy-410-paths.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,6 +34,7 @@
|
|||||||
"@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",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { defineConfig, devices } from '@playwright/test';
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
testDir: 'tests',
|
testDir: 'tests',
|
||||||
|
testMatch: /.*\.spec\.(ts|js)/,
|
||||||
fullyParallel: true,
|
fullyParallel: true,
|
||||||
forbidOnly: !!process.env.CI,
|
forbidOnly: !!process.env.CI,
|
||||||
retries: process.env.CI ? 2 : 0,
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
|||||||
261
pnpm-lock.yaml
generated
@@ -39,6 +39,9 @@ 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
|
||||||
@@ -453,126 +456,252 @@ 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'}
|
||||||
@@ -585,24 +714,48 @@ 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'}
|
||||||
@@ -1376,6 +1529,11 @@ 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'}
|
||||||
@@ -3022,81 +3180,156 @@ 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
|
||||||
|
|
||||||
@@ -3826,6 +4059,34 @@ 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
|
||||||
|
|||||||
39
scripts/copy-410-paths.mjs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#!/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();
|
||||||
45
scripts/minify-static-js.mjs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
#!/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);
|
||||||
|
});
|
||||||
@@ -8,6 +8,7 @@ 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}"
|
||||||
@@ -31,6 +32,8 @@ 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
|
||||||
|
|||||||
@@ -8,12 +8,10 @@
|
|||||||
aria-labelledby="experience-heading"
|
aria-labelledby="experience-heading"
|
||||||
>
|
>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2 id="experience-heading" class="section-title">
|
<h2 id="experience-heading" class="section-title">Previously at:</h2>
|
||||||
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 as logo (logo.alt)}
|
{#each experienceLogos.filter((logo) => logo.showLogo) as logo (logo.alt)}
|
||||||
<div class="logo-item" role="listitem">
|
<div class="logo-item" role="listitem">
|
||||||
<img
|
<img
|
||||||
src={logo.src}
|
src={logo.src}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
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
|
||||||
@@ -27,6 +29,8 @@
|
|||||||
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
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
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} />
|
||||||
@@ -28,6 +30,8 @@
|
|||||||
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} />
|
||||||
|
|||||||
@@ -14,20 +14,27 @@
|
|||||||
<span class="mobile nav-header-logo">
|
<span class="mobile nav-header-logo">
|
||||||
<Wordmark />
|
<Wordmark />
|
||||||
</span>
|
</span>
|
||||||
<label
|
<button
|
||||||
id="nav-toggle-label"
|
type="button"
|
||||||
for="nav-toggle"
|
id="nav-toggle-button"
|
||||||
class="nav-toggle"
|
class="btn-clear"
|
||||||
aria-controls="nav-menu"
|
aria-controls="nav-menu"
|
||||||
aria-label="Toggle navigation"
|
aria-label="Toggle navigation"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
>
|
>
|
||||||
<span class="nav-toggle-inner">
|
<label
|
||||||
<span class="nav-toggle-line"></span>
|
id="nav-toggle-label"
|
||||||
<span class="nav-toggle-line"></span>
|
for="nav-toggle"
|
||||||
<span class="nav-toggle-line"></span>
|
class="nav-toggle"
|
||||||
</span>
|
role="presentation"
|
||||||
</label>
|
>
|
||||||
|
<span class="nav-toggle-inner">
|
||||||
|
<span class="nav-toggle-line"></span>
|
||||||
|
<span class="nav-toggle-line"></span>
|
||||||
|
<span class="nav-toggle-line"></span>
|
||||||
|
</span>
|
||||||
|
</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">
|
||||||
@@ -35,20 +42,45 @@
|
|||||||
</span>
|
</span>
|
||||||
<ul class="nav-list">
|
<ul class="nav-list">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a href="#what-we-do" class="nav-link">Services</a>
|
<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 href="#impact" class="nav-link">Impact</a>
|
<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 href="#how-we-work" class="nav-link">Process</a>
|
<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 href="#schedule" class="nav-link">Contact</a>
|
<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 href="#header" class="nav-link">Back to top</a>
|
<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>
|
||||||
@@ -149,6 +181,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
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} />
|
||||||
|
|||||||
@@ -1,25 +1,52 @@
|
|||||||
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;
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,12 @@
|
|||||||
<!-- 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 ?? ''} />
|
||||||
@@ -132,7 +138,7 @@
|
|||||||
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
<link rel="apple-touch-icon" href="/favicon.svg" />
|
<link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon.png" />
|
||||||
|
|
||||||
{#if jsonLdHtml}
|
{#if jsonLdHtml}
|
||||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -- static trusted JSON-LD -->
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -- static trusted JSON-LD -->
|
||||||
@@ -143,5 +149,15 @@
|
|||||||
<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">Skip to main content</a>
|
<a href="#main" class="skip-link" data-umami-event="skip to main content"
|
||||||
|
>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"
|
||||||
|
/>
|
||||||
|
|||||||
20
static/404.html
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<!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. We’re not sure it’s coming back.</p>
|
||||||
|
<p><a href="/">Back to mifi Ventures →</a></p>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
20
static/410.html
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<!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. We’ve moved on—and so should you.</p>
|
||||||
|
<p><a href="/">Back to mifi Ventures →</a></p>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 3.6 KiB |
BIN
static/assets/avatar-print.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
102
static/assets/error-pages.css
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
1
static/assets/favicon-new.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 2134 2134" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><path d="M2133.333,400l0,1333.333c0,220.766 -179.234,400 -400,400l-1333.333,0c-220.766,0 -400,-179.234 -400,-400l0,-1333.333c0,-220.766 179.234,-400 400,-400l1333.333,0c220.766,0 400,179.234 400,400Z" style="fill:#0b0b0f;"/><g><path d="M178.684,1755.333l0,-916.655l208.714,0l0,211.244l-24.034,-34.153c16.304,-66.339 49.965,-115.812 100.984,-148.419c51.019,-32.607 110.822,-48.911 179.41,-48.911c74.772,0 140.899,19.466 198.384,58.398c57.484,38.932 94.659,90.724 111.525,155.376l-63.247,5.481c28.391,-73.928 70.625,-128.953 126.704,-165.074c56.079,-36.121 120.801,-54.181 194.167,-54.181c64.933,0 122.98,14.617 174.139,43.851c51.16,29.234 91.567,69.852 121.223,121.855c29.656,52.003 44.483,112.157 44.483,180.464l0,590.724l-221.363,0l0,-538.018c0,-40.759 -7.309,-75.615 -21.926,-104.568c-14.617,-28.953 -34.996,-51.511 -61.138,-67.674c-26.142,-16.163 -57.344,-24.245 -93.605,-24.245c-35.137,0 -66.128,8.082 -92.973,24.245c-26.845,16.163 -47.646,38.791 -62.403,67.885c-14.758,29.093 -22.136,63.879 -22.136,104.357l0,538.018l-221.363,0l0,-538.018c0,-40.759 -7.309,-75.615 -21.926,-104.568c-14.617,-28.953 -34.996,-51.511 -61.138,-67.674c-26.142,-16.163 -57.344,-24.245 -93.605,-24.245c-35.418,0 -66.479,8.082 -93.183,24.245c-26.704,16.163 -47.435,38.791 -62.193,67.885c-14.758,29.093 -22.136,63.879 -22.136,104.357l0,538.018l-221.363,0Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M1733.286,1755.333l0,-916.655l221.363,0l0,916.655l-221.363,0Zm0,-1020.379l0,-236.121l221.363,0l0,236.121l-221.363,0Z" style="fill:#fff;fill-rule:nonzero;"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -1,8 +1,12 @@
|
|||||||
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 mobileMenuToggleLabel = document.getElementById('nav-toggle-label');
|
const mobileMenuToggleButton = document.getElementById('nav-toggle-button');
|
||||||
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');
|
||||||
|
|
||||||
@@ -10,35 +14,75 @@ const mobileMenuHelper = () => {
|
|||||||
|
|
||||||
const syncMenuAriaHidden = () => {
|
const syncMenuAriaHidden = () => {
|
||||||
if (isMobile()) {
|
if (isMobile()) {
|
||||||
mobileMenu.setAttribute('aria-hidden', mobileMenuToggle.checked ? 'false' : 'true');
|
const hidden = !mobileMenuToggle.checked;
|
||||||
|
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 syncLabelAriaExpanded = () => {
|
const setMenuFocusablesTabIndex = () => {
|
||||||
mobileMenuToggleLabel.setAttribute('aria-expanded', mobileMenuToggle.checked ? 'true' : 'false');
|
const focusables = getMenuFocusables(mobileMenu);
|
||||||
|
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;
|
||||||
syncLabelAriaExpanded();
|
syncButtonAriaExpanded();
|
||||||
syncMenuAriaHidden();
|
syncMenuAriaHidden();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
mobileMenuToggle.addEventListener('change', () => {
|
mobileMenuToggle.addEventListener('change', () => {
|
||||||
syncLabelAriaExpanded();
|
syncButtonAriaExpanded();
|
||||||
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();
|
||||||
syncLabelAriaExpanded();
|
syncButtonAriaExpanded();
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', mobileMenuHelper);
|
document.addEventListener('DOMContentLoaded', mobileMenuHelper);
|
||||||
|
|||||||
28
static/assets/js/umami-helper.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
// 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 });
|
||||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
@@ -1,15 +1,9 @@
|
|||||||
<svg width="100%" height="100%" viewBox="0 0 2134 2134" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
<svg width="100%" height="100%" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
<style>
|
<style>
|
||||||
.bg { fill: #0b0b0f; }
|
.block { fill: #0b0b0f; }
|
||||||
.fg { fill: #fff; }
|
@media (prefers-color-scheme: dark) {
|
||||||
@media (prefers-color-scheme: dark) {
|
.block { fill: #f2f2f2; }
|
||||||
.bg { fill: #f2f2f2; }
|
}
|
||||||
.fg { fill: #0b0b0f; }
|
</style>
|
||||||
}
|
<path class="block" d="M512,96l0,320c0,52.984 -43.016,96 -96,96l-320,0c-52.984,0 -96,-43.016 -96,-96l0,-320c0,-52.984 43.016,-96 96,-96l320,0c52.984,0 96,43.016 96,96Zm-96.011,80.389l53.127,0l0,-56.669l-53.127,0l0,56.669Zm-193.658,55.292c-4.819,-8.296 -11.558,-15.376 -20.217,-21.24c-13.796,-9.344 -29.667,-14.015 -47.612,-14.015c-16.461,0 -30.814,3.913 -43.058,11.739c-7.882,5.038 -14.038,11.753 -18.468,20.146l0,-27.027l-50.091,0l0,219.997l53.127,0l0,-129.124c0,-9.715 1.771,-18.063 5.313,-25.046c3.542,-6.982 8.517,-12.413 14.926,-16.292c6.409,-3.879 13.864,-5.819 22.364,-5.819c8.703,0 16.191,1.94 22.465,5.819c6.274,3.879 11.165,9.293 14.673,16.242c3.508,6.949 5.262,15.314 5.262,25.096l0,129.124l53.127,0l0,-129.124c0,-9.715 1.771,-18.063 5.313,-25.046c3.542,-6.982 8.534,-12.413 14.977,-16.292c6.443,-3.879 13.881,-5.819 22.313,-5.819c8.703,0 16.191,1.94 22.465,5.819c6.274,3.879 11.165,9.293 14.673,16.242c3.508,6.949 5.262,15.314 5.262,25.096l0,129.124l53.127,0l0,-141.774c0,-16.394 -3.559,-30.831 -10.676,-43.311c-7.117,-12.481 -16.815,-22.229 -29.093,-29.245c-12.278,-7.016 -26.209,-10.524 -41.793,-10.524c-17.608,0 -33.141,4.335 -46.6,13.004c-8.624,5.555 -15.884,12.972 -21.779,22.252Zm193.658,189.599l53.127,0l0,-219.997l-53.127,0l0,219.997Z" />
|
||||||
</style>
|
|
||||||
<path class="bg" d="M2133.333,400l0,1333.333c0,220.766 -179.234,400 -400,400l-1333.333,0c-220.766,0 -400,-179.234 -400,-400l0,-1333.333c0,-220.766 179.234,-400 400,-400l1333.333,0c220.766,0 400,179.234 400,400Z"/>
|
|
||||||
<g>
|
|
||||||
<path class="fg" d="M434.867,1513.667l0,-652.2l148.5,0l0,150.3l-17.1,-24.3c11.6,-47.2 35.55,-82.4 71.85,-105.6c36.3,-23.2 78.85,-34.8 127.65,-34.8c53.2,0 100.25,13.85 141.15,41.55c40.9,27.7 67.35,64.55 79.35,110.55l-45,3.9c20.2,-52.6 50.25,-91.75 90.15,-117.45c39.9,-25.7 85.95,-38.55 138.15,-38.55c46.2,0 87.5,10.4 123.9,31.2c36.4,20.8 65.15,49.7 86.25,86.7c21.1,37 31.65,79.8 31.65,128.4l0,420.3l-157.5,0l0,-382.8c0,-29 -5.2,-53.8 -15.6,-74.4c-10.4,-20.6 -24.9,-36.65 -43.5,-48.15c-18.6,-11.5 -40.8,-17.25 -66.6,-17.25c-25,0 -47.05,5.75 -66.15,17.25c-19.1,11.5 -33.9,27.6 -44.4,48.3c-10.5,20.7 -15.75,45.45 -15.75,74.25l0,382.8l-157.5,0l0,-382.8c0,-29 -5.2,-53.8 -15.6,-74.4c-10.4,-20.6 -24.9,-36.65 -43.5,-48.15c-18.6,-11.5 -40.8,-17.25 -66.6,-17.25c-25.2,0 -47.3,5.75 -66.3,17.25c-19,11.5 -33.75,27.6 -44.25,48.3c-10.5,20.7 -15.75,45.45 -15.75,74.25l0,382.8l-157.5,0Z" style="fill-rule:nonzero;"/>
|
|
||||||
<path class="fg" d="M1540.967,1513.667l0,-652.2l157.5,0l0,652.2l-157.5,0Zm0,-726l0,-168l157.5,0l0,168l-157.5,0Z" style="fill-rule:nonzero;"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
@@ -29,11 +29,17 @@ 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';
|
||||||
header.appendChild(label);
|
toggleButton.appendChild(label);
|
||||||
|
header.appendChild(toggleButton);
|
||||||
nav.appendChild(header);
|
nav.appendChild(header);
|
||||||
|
|
||||||
const menu = document.createElement('div');
|
const menu = document.createElement('div');
|
||||||
@@ -53,7 +59,7 @@ function createNavDOM() {
|
|||||||
nav.appendChild(menu);
|
nav.appendChild(menu);
|
||||||
|
|
||||||
document.body.appendChild(nav);
|
document.body.appendChild(nav);
|
||||||
return { nav, menu, label, checkbox, item };
|
return { nav, menu, toggleButton, label, checkbox, item, link };
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('mobile-menu-helper.js', () => {
|
describe('mobile-menu-helper.js', () => {
|
||||||
@@ -75,7 +81,12 @@ 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.label.getAttribute('aria-expanded')).toBe('false');
|
expect(dom.toggleButton.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', () => {
|
||||||
@@ -83,35 +94,55 @@ 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.label.getAttribute('aria-expanded')).toBe('true');
|
expect(dom.toggleButton.getAttribute('aria-expanded')).toBe('true');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('removes aria-hidden from menu when viewport is desktop', () => {
|
it('removes inert and link tabindex when menu is open on mobile', () => {
|
||||||
|
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 when resizing from desktop to mobile', () => {
|
it('adds aria-hidden and inert 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.label.getAttribute('aria-expanded')).toBe('true');
|
expect(dom.toggleButton.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.label.getAttribute('aria-expanded')).toBe('false');
|
expect(dom.toggleButton.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: jsdom’s 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.
|
||||||
});
|
});
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 583 KiB After Width: | Height: | Size: 577 KiB |
@@ -5,6 +5,12 @@ 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,
|
||||||
|
|||||||