redirecter for ao3 that adds opengraph metadata

Compare changes

Choose any two refs to compare.

+7
.env.example
···
+
SITENAME=fixAO3
+
DESCRIPTION="Unofficial AO3 embed prettifier for social media"
+
DOMAIN=localhost:3000
+
ARCHIVE=archiveofourown.org
+
DEFAULT_THEME=ao3
+
DEFAULT_BASE_FONT=bricolagegrotesque
+
DEFAULT_TITLE_FONT=stacksansnotch
+4 -1
.gitignore
···
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
-
.env*
+
.env
+
.env.development
+
.env.production
+
.env.staging
# vercel
.vercel
+11 -3
README.md
···
you too can self-host if you want to change the visuals or just have a different redirection url or something. the main things you need installed are:
-
- ImageMagick (at least 6.9), built from source with pkg-config, pangocairo, libpng-dev, libwebp-dev. this is kind of a pain in the ass but it is in fact essential
- Bun
- Probably some process manager like PM2 (you will also need Node.js for PM2)
- A web server that allows reverse proxying (I use Caddy)
How to run:
-
- `bun main.jsx` will run the application. if you're running it through pm2, do `pm2 start --name=fixao3 bun -- main.jsx`
+
- copy .env.example to .env, and update the variables in there to be whatever you want them to be. the lists of existing themes and fonts can be found in the src/lib folder, for setting defaults
+
- `bun --bun next start` will run the application. if you're running it through pm2, do `pm2 start --name=fixao3 bun -- --bun next start`
- set up a reverse proxy on your domain or subdomain to localhost:3000
-
- it should, theoretically work (installing imagemagick from source is the hard part. wheeze)
+
- it should, theoretically work (installing imagemagick from source is the hard part. wheeze)
+
+
How to customize:
+
+
- stuff like site name, meta description, etc. are set in site variables. right now the ARCHIVE var is only used for what to display on the bottom of cards because I'm not sure how AO3.js supports other archives apart from that it does.
+
- basically all the styles are in src/app/globals.css
+
- add fonts to src/lib/baseFonts.js and src/lib/titleFonts.js, and themes to src/lib/themes.js (titleFonts already grabs all the baseFonts as long as you don't remove `...baseFonts` from the array)
+
- icons are svg components stored in src/icons if you don't like my icon choices
+
- add font files to the fonts folder. they have to be either ttf or otf (sorry, no woff/woff2, next/og hates those as it turns out)
+345 -34
bun.lock
···
"@fontsource-variable/bricolage-grotesque": "^5.2.10",
"@fontsource/stack-sans-notch": "^5.2.1",
"@fujocoded/ao3.js": "^0.22.1",
-
"@hono/vite-build": "^1.7.0",
+
"@hono/vite-build": "^1.8.0",
"@hono/vite-dev-server": "^0.23.0",
-
"bun": "^1.3.2",
"fauxdom": "^1.2.2",
-
"hono": "^4.10.4",
-
"next": "16.0.2",
+
"hono": "^4.10.8",
+
"next": "16.0.7",
"react": "19.2.0",
"react-dom": "19.2.0",
},
"devDependencies": {
"@biomejs/biome": "2.2.0",
"@types/bun": "latest",
+
"baseline-browser-mapping": "^2.9.5",
+
"cypress": "^15.7.1",
},
},
},
···
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.2.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Nawu5nHjP/zPKTIryh2AavzTc/KEg4um/MxWdXW0A6P/RZOyIpa7+QSjeXwAwX/utJGaCoXRPWtF3m5U/bB3Ww=="],
+
"@cypress/request": ["@cypress/request@3.0.9", "", { "dependencies": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", "caseless": "~0.12.0", "combined-stream": "~1.0.6", "extend": "~3.0.2", "forever-agent": "~0.6.1", "form-data": "~4.0.4", "http-signature": "~1.4.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "performance-now": "^2.1.0", "qs": "6.14.0", "safe-buffer": "^5.1.2", "tough-cookie": "^5.0.0", "tunnel-agent": "^0.6.0", "uuid": "^8.3.2" } }, "sha512-I3l7FdGRXluAS44/0NguwWlO83J18p0vlr2FYHrJkWdNYhgVoiYo61IXPqaOsL+vNxU1ZqMACzItGK3/KKDsdw=="],
+
+
"@cypress/xvfb": ["@cypress/xvfb@1.2.4", "", { "dependencies": { "debug": "^3.1.0", "lodash.once": "^4.1.1" } }, "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q=="],
+
"@emnapi/runtime": ["@emnapi/runtime@1.7.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q=="],
"@fontsource-variable/bricolage-grotesque": ["@fontsource-variable/bricolage-grotesque@5.2.10", "", {}, "sha512-5EDsCqgGpKVcJWE4sg9ydli+t5WM97mISYw5lla/Ev4z71FwXh1oN0YUU8xjkRW9+wBCGD9R+ntAvI8G4bUFJg=="],
···
"@hono/node-server": ["@hono/node-server@1.19.6", "", { "peerDependencies": { "hono": "^4" } }, "sha512-Shz/KjlIeAhfiuE93NDKVdZ7HdBVLQAfdbaXEaoAVO3ic9ibRSLGIQGkcBbFyuLr+7/1D5ZCINM8B+6IvXeMtw=="],
-
"@hono/vite-build": ["@hono/vite-build@1.7.0", "", { "peerDependencies": { "hono": "*" } }, "sha512-L73WBed5teC7DHTzXYkho83POYYluD2rTkbT76FJuqpfXPgTW/PsbIa8O0YcCETE3VUoh584fe6vuLAM5ctjww=="],
+
"@hono/vite-build": ["@hono/vite-build@1.8.0", "", { "peerDependencies": { "hono": "*" } }, "sha512-T/w0Tuv8VV6FgjNnw2aW6LXtRfc5tOhaO54tKCRv4Zqc1tP3w6JyqTFtzJFFDvmY0xTxHS+Pj+ZPLhzFsdiFzQ=="],
"@hono/vite-dev-server": ["@hono/vite-dev-server@0.23.0", "", { "dependencies": { "@hono/node-server": "^1.14.2", "minimatch": "^9.0.3" }, "peerDependencies": { "hono": "*", "miniflare": "*", "wrangler": "*" }, "optionalPeers": ["miniflare", "wrangler"] }, "sha512-tHV86xToed9Up0j/dubQW2PDP4aYNFDSfQrjcV6Ra7bqCGrxhtg/zZBmbgSco3aTxKOEPzDXKK+6seAAfsbIXw=="],
···
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
-
"@next/env": ["@next/env@16.0.2", "", {}, "sha512-V2e9ITU6Ts9kxtTBX60qtWlKV+AeBNlz/hgAt0gkGA8aPgX27cRLjp7OEUMzYq4cY0QzOkOQ4CI/8IJh6kW/iw=="],
+
"@next/env": ["@next/env@16.0.7", "", {}, "sha512-gpaNgUh5nftFKRkRQGnVi5dpcYSKGcZZkQffZ172OrG/XkrnS7UBTQ648YY+8ME92cC4IojpI2LqTC8sTDhAaw=="],
-
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-E6rxUdkZX5sZjLduXphiMuRJAmvsxWi5IivD0kRLLX5cjNLOs2PjlSyda+dtT3iqE6vxaRGV3oQMnQiJU8F+Ig=="],
+
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.0.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LlDtCYOEj/rfSnEn/Idi+j1QKHxY9BJFmxx7108A6D8K0SB+bNgfYQATPk/4LqOl4C0Wo3LACg2ie6s7xqMpJg=="],
-
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-QNXdjXVFtb35vImDJtXqYlhq8A2mHLroqD8q4WCwO+IVnVoQshhcEVWJlP9UB/dOC6Wh782BbTHqGzKQwlCSkQ=="],
+
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.0.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rtZ7BhnVvO1ICf3QzfW9H3aPz7GhBrnSIMZyr4Qy6boXF0b5E3QLs+cvJmg3PsTCG2M1PBoC+DANUi4wCOKXpA=="],
-
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-dM9yEB35GZAW3r+w88iGEz7OkJjSYSd4pKyl4KwSXx8cLWMpWaX1WW42dCAKXCWWQhVUXUZAEx38yfpEZ1/IJg=="],
+
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.0.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-mloD5WcPIeIeeZqAIP5c2kdaTa6StwP4/2EGy1mUw8HiexSHGK/jcM7lFuS3u3i2zn+xH9+wXJs6njO7VrAqww=="],
-
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hiNysPK1VeK5MGNmuKLnj3Y4lkaffvAlXin404QpxYkNCBms/Bk0msZHey5lUNq8FV50PY6I9CgY+c/NK+xeLg=="],
+
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.0.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-+ksWNrZrthisXuo9gd1XnjHRowCbMtl/YgMpbRvFeDEqEBd523YHPWpBuDjomod88U8Xliw5DHhekBC3EOOd9g=="],
-
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-hAhhobw4tHOCzZ5sm5W/EsQPxS3NbZl6rqzmA0GTV9etE8sPHmsV6OopP12TeeoXA/NjXKD2mcz8hcVWLe4jkg=="],
+
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.0.7", "", { "os": "linux", "cpu": "x64" }, "sha512-4WtJU5cRDxpEE44Ana2Xro1284hnyVpBb62lIpU5k85D8xXxatT+rXxBgPkc7C1XwkZMWpK5rXLXTh9PFipWsA=="],
-
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-s0LUsoeRky95aTS6IfYnJOn6F5kbs+gjiVUQK0JmsJ/ZCXaply20kDoJ8/zHwMz5cyOVg7GrQJdMvyO9FLD9Bw=="],
+
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.0.7", "", { "os": "linux", "cpu": "x64" }, "sha512-HYlhqIP6kBPXalW2dbMTSuB4+8fe+j9juyxwfMwCe9kQPPeiyFn7NMjNfoFOfJ2eXkeQsoUGXg+O2SE3m4Qg2w=="],
-
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-TMWE1h44d0WRyq0yQI/0W5A7nZUoiwE2Sdg43wt2Q1IoadU5Ky00G3cJ2mSnbetwL7+eFyM7BQgx+Fonpz6T8w=="],
+
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.0.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-EviG+43iOoBRZg9deGauXExjRphhuYmIOJ12b9sAPy0eQ6iwcPxfED2asb/s2/yiLYOdm37kPaiZu8uXSYPs0Q=="],
-
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-+8SqzDhau/PNsWdcagnoz6ltOM9IcsqagdTFsEELNOty0+lNh5hwO5oUFForPOywTbM+d3tPLo5m20VdEBDf3Q=="],
+
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.0.7", "", { "os": "win32", "cpu": "x64" }, "sha512-gniPjy55zp5Eg0896qSrf3yB1dw4F/3s8VK1ephdsZZ129j2n6e1WqCbE2YgcKhW9hPB9TVZENugquWJD5x0ug=="],
-
"@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-licBDIbbLP5L5/S0+bwtJynso94XD3KyqSP48K59Sq7Mude6C7dR5ZujZm4Ut4BwZqUFfNOfYNMWBU5nlL7t1A=="],
+
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
-
"@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-hn8lLzsYyyh6ULo2E8v2SqtrWOkdQKJwapeVy1rDw7juTTeHY3KDudGWf4mVYteC9riZU6HD88Fn3nGwyX0eIg=="],
+
"@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="],
-
"@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-UHxdtbyxdtNJUNcXtIrjx3Lmq8ji3KywlXtIHV/0vn9A8W5mulqOcryqUWMFVH9JTIIzmNn6Q/qVmXHTME63Ww=="],
+
"@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
+
+
"@types/sinonjs__fake-timers": ["@types/sinonjs__fake-timers@8.1.1", "", {}, "sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g=="],
+
+
"@types/sizzle": ["@types/sizzle@2.3.10", "", {}, "sha512-TC0dmN0K8YcWEAEfiPi5gJP14eJe30TTGjkvek3iM/1NdHHsdCA/Td6GvNndMOo/iSnIsZ4HuuhrYPDAmbxzww=="],
+
+
"@types/tmp": ["@types/tmp@0.2.6", "", {}, "sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA=="],
+
+
"@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
+
+
"aggregate-error": ["aggregate-error@3.1.0", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="],
-
"@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5uZzxzvHU/z+3cZwN/A0H8G+enQ+9FkeJVZkE2fwK2XhiJZFUGAuWajCpy7GepvOWlqV7VjPaKi2+Qmr4IX7nQ=="],
+
"ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="],
-
"@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-OD9DYkjes7WXieBn4zQZGXWhRVZhIEWMDGCetZ3H4vxIuweZ++iul/CNX5jdpNXaJ17myb1ROMvmRbrqW44j3w=="],
+
"ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="],
-
"@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-EoEuRP9bxAxVKuvi6tZ0ZENjueP4lvjz0mKsMzdG0kwg/2apGKiirH1l0RIcdmvfDGGuDmNiv/XBpkoXq1x8ug=="],
+
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
-
"@oven/bun-linux-x64-baseline": ["@oven/bun-linux-x64-baseline@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-m9Ov9YH8KjRLui87eNtQQFKVnjGsNk3xgbrR9c8d2FS3NfZSxmVjSeBvEsDjzNf1TXLDriHb/NYOlpiMf/QzDg=="],
+
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
-
"@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-3TuOsRVoG8K+soQWRo+Cp5ACpRs6rTFSu5tAqc/6WrqwbNWmqjov/eWJPTgz3gPXnC7uNKVG7RxxAmV8r2EYTQ=="],
+
"arch": ["arch@2.2.0", "", {}, "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ=="],
-
"@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-q8Hto8hcpofPJjvuvjuwyYvhOaAzPw1F5vRUUeOJDmDwZ4lZhANFM0rUwchMzfWUJCD6jg8/EVQ8MiixnZWU0A=="],
+
"asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="],
-
"@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.3.2", "", { "os": "win32", "cpu": "x64" }, "sha512-nZJUa5NprPYQ4Ii4cMwtP9PzlJJTp1XhxJ+A9eSn1Jfr6YygVWyN2KLjenyI93IcuBouBAaepDAVZZjH2lFBhg=="],
+
"assert-plus": ["assert-plus@1.0.0", "", {}, "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="],
-
"@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.2", "", { "os": "win32", "cpu": "x64" }, "sha512-s00T99MjB+xLOWq+t+wVaVBrry+oBOZNiTJijt+bmkp/MJptYS3FGvs7a+nkjLNzoNDoWQcXgKew6AaHES37Bg=="],
+
"astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="],
-
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
+
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
-
"@types/bun": ["@types/bun@1.3.2", "", { "dependencies": { "bun-types": "1.3.2" } }, "sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg=="],
+
"at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="],
-
"@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
+
"aws-sign2": ["aws-sign2@0.7.0", "", {}, "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA=="],
-
"@types/react": ["@types/react@19.2.4", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-tBFxBp9Nfyy5rsmefN+WXc1JeW/j2BpBHFdLZbEVfs9wn3E3NRFxwV0pJg8M1qQAexFpvz73hJXFofV0ZAu92A=="],
+
"aws4": ["aws4@1.13.2", "", {}, "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
+
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
+
+
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.5", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-D5vIoztZOq1XM54LUdttJVc96ggEsIfju2JBvht06pSzpckp3C7HReun67Bghzrtdsq9XdMGbSSB3v3GhMNmAA=="],
+
+
"bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="],
+
+
"blob-util": ["blob-util@2.0.2", "", {}, "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ=="],
+
+
"bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="],
+
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
"brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
-
"bun": ["bun@1.3.2", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.2", "@oven/bun-darwin-x64": "1.3.2", "@oven/bun-darwin-x64-baseline": "1.3.2", "@oven/bun-linux-aarch64": "1.3.2", "@oven/bun-linux-aarch64-musl": "1.3.2", "@oven/bun-linux-x64": "1.3.2", "@oven/bun-linux-x64-baseline": "1.3.2", "@oven/bun-linux-x64-musl": "1.3.2", "@oven/bun-linux-x64-musl-baseline": "1.3.2", "@oven/bun-windows-x64": "1.3.2", "@oven/bun-windows-x64-baseline": "1.3.2" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-x75mPJiEfhO1j4Tfc65+PtW6ZyrAB6yTZInydnjDZXF9u9PRAnr6OK3v0Q9dpDl0dxRHkXlYvJ8tteJxc8t4Sw=="],
+
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
+
+
"buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
+
+
"bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="],
+
+
"cachedir": ["cachedir@2.4.0", "", {}, "sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ=="],
+
+
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
-
"bun-types": ["bun-types@1.3.2", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg=="],
+
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
"caniuse-lite": ["caniuse-lite@1.0.30001754", "", {}, "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg=="],
+
"caseless": ["caseless@0.12.0", "", {}, "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw=="],
+
+
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
+
"cheerio": ["cheerio@1.1.2", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "encoding-sniffer": "^0.2.1", "htmlparser2": "^10.0.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", "undici": "^7.12.0", "whatwg-mimetype": "^4.0.0" } }, "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg=="],
"cheerio-select": ["cheerio-select@2.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-select": "^5.1.0", "css-what": "^6.1.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1" } }, "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g=="],
+
"ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="],
+
+
"clean-stack": ["clean-stack@2.2.0", "", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="],
+
+
"cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="],
+
+
"cli-table3": ["cli-table3@0.6.1", "", { "dependencies": { "string-width": "^4.2.0" }, "optionalDependencies": { "colors": "1.4.0" } }, "sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA=="],
+
+
"cli-truncate": ["cli-truncate@2.1.0", "", { "dependencies": { "slice-ansi": "^3.0.0", "string-width": "^4.2.0" } }, "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg=="],
+
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
+
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
+
+
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
+
+
"colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="],
+
+
"colors": ["colors@1.4.0", "", {}, "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA=="],
+
+
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
+
+
"commander": ["commander@6.2.1", "", {}, "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA=="],
+
+
"common-tags": ["common-tags@1.8.2", "", {}, "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA=="],
+
+
"core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="],
+
+
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
+
"css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="],
"css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="],
-
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
+
"cypress": ["cypress@15.7.1", "", { "dependencies": { "@cypress/request": "^3.0.9", "@cypress/xvfb": "^1.2.4", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", "@types/tmp": "^0.2.3", "arch": "^2.2.0", "blob-util": "^2.0.2", "bluebird": "^3.7.2", "buffer": "^5.7.1", "cachedir": "^2.3.0", "chalk": "^4.1.0", "ci-info": "^4.1.0", "cli-cursor": "^3.1.0", "cli-table3": "0.6.1", "commander": "^6.2.1", "common-tags": "^1.8.0", "dayjs": "^1.10.4", "debug": "^4.3.4", "enquirer": "^2.3.6", "eventemitter2": "6.4.7", "execa": "4.1.0", "executable": "^4.1.1", "extract-zip": "2.0.1", "figures": "^3.2.0", "fs-extra": "^9.1.0", "hasha": "5.2.2", "is-installed-globally": "~0.4.0", "listr2": "^3.8.3", "lodash": "^4.17.21", "log-symbols": "^4.0.0", "minimist": "^1.2.8", "ospath": "^1.2.2", "pretty-bytes": "^5.6.0", "process": "^0.11.10", "proxy-from-env": "1.0.0", "request-progress": "^3.0.0", "supports-color": "^8.1.1", "systeminformation": "5.27.7", "tmp": "~0.2.4", "tree-kill": "1.2.2", "untildify": "^4.0.0", "yauzl": "^2.10.0" }, "bin": { "cypress": "bin/cypress" } }, "sha512-U3sYnJ+Cnpgr6IPycxsznTg//mGVXfPGeGV+om7VQCyp5XyVkhG4oPr3X3hTq1+OB0Om0O5DxusYmt7cbvwqMQ=="],
+
+
"dashdash": ["dashdash@1.14.1", "", { "dependencies": { "assert-plus": "^1.0.0" } }, "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g=="],
+
+
"dayjs": ["dayjs@1.11.19", "", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+
+
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
···
"domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
+
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
+
+
"ecc-jsbn": ["ecc-jsbn@0.1.2", "", { "dependencies": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" } }, "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw=="],
+
+
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
+
"encoding-sniffer": ["encoding-sniffer@0.2.1", "", { "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" } }, "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw=="],
+
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
+
+
"enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="],
+
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
+
+
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
+
+
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
+
+
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
+
+
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
+
+
"escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
+
+
"eventemitter2": ["eventemitter2@6.4.7", "", {}, "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg=="],
+
+
"execa": ["execa@4.1.0", "", { "dependencies": { "cross-spawn": "^7.0.0", "get-stream": "^5.0.0", "human-signals": "^1.1.1", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.0", "onetime": "^5.1.0", "signal-exit": "^3.0.2", "strip-final-newline": "^2.0.0" } }, "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA=="],
+
+
"executable": ["executable@4.1.1", "", { "dependencies": { "pify": "^2.2.0" } }, "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg=="],
+
+
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
+
+
"extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="],
+
+
"extsprintf": ["extsprintf@1.3.0", "", {}, "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g=="],
"fauxdom": ["fauxdom@1.2.2", "", {}, "sha512-Xoj9VPhIx9p9wU1CncWaKqgm5e+lnTqi5nYsQIfALEUKu++99Pj/BMkggH6mYjWaBeVWBxeV8xZTJauTXGiUDw=="],
+
"fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],
+
+
"figures": ["figures@3.2.0", "", { "dependencies": { "escape-string-regexp": "^1.0.5" } }, "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg=="],
+
+
"forever-agent": ["forever-agent@0.6.1", "", {}, "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw=="],
+
+
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
+
+
"fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
+
+
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
+
+
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
+
+
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
+
+
"get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],
+
+
"getpass": ["getpass@0.1.7", "", { "dependencies": { "assert-plus": "^1.0.0" } }, "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng=="],
+
+
"global-dirs": ["global-dirs@3.0.1", "", { "dependencies": { "ini": "2.0.0" } }, "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA=="],
+
"globrex": ["globrex@0.1.2", "", {}, "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg=="],
-
"hono": ["hono@4.10.5", "", {}, "sha512-h/MXuTkoAK8NG1EfDp0jI1YLf6yGdDnfkebRO2pwEh5+hE3RAJFXkCsnD0vamSiARK4ZrB6MY+o3E/hCnOyHrQ=="],
+
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
+
+
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
+
+
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
+
+
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
+
+
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
+
+
"hasha": ["hasha@5.2.2", "", { "dependencies": { "is-stream": "^2.0.0", "type-fest": "^0.8.0" } }, "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ=="],
+
+
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
+
+
"hono": ["hono@4.10.8", "", {}, "sha512-DDT0A0r6wzhe8zCGoYOmMeuGu3dyTAE40HHjwUsWFTEy5WxK1x2WDSsBPlEXgPbRIFY6miDualuUDbasPogIww=="],
"htmlparser2": ["htmlparser2@10.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.1", "entities": "^6.0.0" } }, "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g=="],
+
"http-signature": ["http-signature@1.4.0", "", { "dependencies": { "assert-plus": "^1.0.0", "jsprim": "^2.0.2", "sshpk": "^1.18.0" } }, "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg=="],
+
+
"human-signals": ["human-signals@1.1.1", "", {}, "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw=="],
+
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
+
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
+
+
"indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="],
+
+
"ini": ["ini@2.0.0", "", {}, "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA=="],
+
+
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
+
+
"is-installed-globally": ["is-installed-globally@0.4.0", "", { "dependencies": { "global-dirs": "^3.0.0", "is-path-inside": "^3.0.2" } }, "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ=="],
+
+
"is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="],
+
+
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
+
+
"is-typedarray": ["is-typedarray@1.0.0", "", {}, "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="],
+
+
"is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="],
+
+
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
+
+
"isstream": ["isstream@0.1.2", "", {}, "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g=="],
+
+
"jsbn": ["jsbn@0.1.1", "", {}, "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg=="],
+
+
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
+
+
"json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="],
+
+
"jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="],
+
+
"jsprim": ["jsprim@2.0.2", "", { "dependencies": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", "json-schema": "0.4.0", "verror": "1.10.0" } }, "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ=="],
+
+
"listr2": ["listr2@3.14.0", "", { "dependencies": { "cli-truncate": "^2.1.0", "colorette": "^2.0.16", "log-update": "^4.0.0", "p-map": "^4.0.0", "rfdc": "^1.3.0", "rxjs": "^7.5.1", "through": "^2.3.8", "wrap-ansi": "^7.0.0" }, "peerDependencies": { "enquirer": ">= 2.3.0 < 3" }, "optionalPeers": ["enquirer"] }, "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g=="],
+
+
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
+
+
"lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="],
+
+
"log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="],
+
+
"log-update": ["log-update@4.0.0", "", { "dependencies": { "ansi-escapes": "^4.3.0", "cli-cursor": "^3.1.0", "slice-ansi": "^4.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg=="],
+
+
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
+
+
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
+
+
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
+
+
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
+
+
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
+
"minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
+
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
+
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
-
"next": ["next@16.0.2", "", { "dependencies": { "@next/env": "16.0.2", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.0.2", "@next/swc-darwin-x64": "16.0.2", "@next/swc-linux-arm64-gnu": "16.0.2", "@next/swc-linux-arm64-musl": "16.0.2", "@next/swc-linux-x64-gnu": "16.0.2", "@next/swc-linux-x64-musl": "16.0.2", "@next/swc-win32-arm64-msvc": "16.0.2", "@next/swc-win32-x64-msvc": "16.0.2", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-zL8+UBf+xUIm8zF0vYGJYJMYDqwaBrRRe7S0Kob6zo9Kf+BdqFLEECMI+B6cNIcoQ+el9XM2fvUExwhdDnXjtw=="],
+
"next": ["next@16.0.7", "", { "dependencies": { "@next/env": "16.0.7", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.0.7", "@next/swc-darwin-x64": "16.0.7", "@next/swc-linux-arm64-gnu": "16.0.7", "@next/swc-linux-arm64-musl": "16.0.7", "@next/swc-linux-x64-gnu": "16.0.7", "@next/swc-linux-x64-musl": "16.0.7", "@next/swc-win32-arm64-msvc": "16.0.7", "@next/swc-win32-x64-msvc": "16.0.7", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-3mBRJyPxT4LOxAJI6IsXeFtKfiJUbjCLgvXO02fV8Wy/lIhPvP94Fe7dGhUgHXcQy4sSuYwQNcOLhIfOm0rL0A=="],
+
+
"npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="],
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
+
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
+
+
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
+
+
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
+
+
"ospath": ["ospath@1.2.2", "", {}, "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA=="],
+
+
"p-map": ["p-map@4.0.0", "", { "dependencies": { "aggregate-error": "^3.0.0" } }, "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ=="],
+
"parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
"parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@7.1.0", "", { "dependencies": { "domhandler": "^5.0.3", "parse5": "^7.0.0" } }, "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g=="],
"parse5-parser-stream": ["parse5-parser-stream@7.1.2", "", { "dependencies": { "parse5": "^7.0.0" } }, "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow=="],
+
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
+
+
"pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="],
+
+
"performance-now": ["performance-now@2.1.0", "", {}, "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="],
+
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
+
+
"pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="],
"postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
+
"pretty-bytes": ["pretty-bytes@5.6.0", "", {}, "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="],
+
+
"process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="],
+
+
"proxy-from-env": ["proxy-from-env@1.0.0", "", {}, "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A=="],
+
+
"pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="],
+
+
"qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
+
"react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="],
"react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="],
+
"request-progress": ["request-progress@3.0.0", "", { "dependencies": { "throttleit": "^1.0.0" } }, "sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg=="],
+
+
"restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="],
+
+
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
+
+
"rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
+
+
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
+
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
···
"sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
+
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
+
+
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
+
+
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
+
+
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
+
+
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
+
+
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
+
+
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
+
+
"slice-ansi": ["slice-ansi@3.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ=="],
+
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+
"sshpk": ["sshpk@1.18.0", "", { "dependencies": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", "bcrypt-pbkdf": "^1.0.0", "dashdash": "^1.12.0", "ecc-jsbn": "~0.1.1", "getpass": "^0.1.1", "jsbn": "~0.1.0", "safer-buffer": "^2.0.2", "tweetnacl": "~0.14.0" }, "bin": { "sshpk-conv": "bin/sshpk-conv", "sshpk-sign": "bin/sshpk-sign", "sshpk-verify": "bin/sshpk-verify" } }, "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ=="],
+
+
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
+
+
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
+
"strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="],
+
"styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
+
"supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
+
+
"systeminformation": ["systeminformation@5.27.7", "", { "os": "!aix", "bin": { "systeminformation": "lib/cli.js" } }, "sha512-saaqOoVEEFaux4v0K8Q7caiauRwjXC4XbD2eH60dxHXbpKxQ8kH9Rf7Jh+nryKpOUSEFxtCdBlSUx0/lO6rwRg=="],
+
+
"throttleit": ["throttleit@1.0.1", "", {}, "sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ=="],
+
+
"through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="],
+
+
"tldts": ["tldts@6.1.86", "", { "dependencies": { "tldts-core": "^6.1.86" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ=="],
+
+
"tldts-core": ["tldts-core@6.1.86", "", {}, "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA=="],
+
+
"tmp": ["tmp@0.2.5", "", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="],
+
+
"tough-cookie": ["tough-cookie@5.1.2", "", { "dependencies": { "tldts": "^6.1.32" } }, "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A=="],
+
+
"tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
+
"tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
+
+
"tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="],
+
+
"type-fest": ["type-fest@0.8.1", "", {}, "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA=="],
+
"undici": ["undici@7.16.0", "", {}, "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
+
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
+
+
"untildify": ["untildify@4.0.0", "", {}, "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw=="],
+
+
"uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
+
+
"verror": ["verror@1.10.0", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw=="],
+
"vite-tsconfig-paths": ["vite-tsconfig-paths@5.1.4", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" }, "optionalPeers": ["vite"] }, "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w=="],
"whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="],
"whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="],
+
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
+
+
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
+
+
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
+
+
"yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],
+
+
"@cypress/xvfb/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
+
+
"ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],
+
+
"chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
+
"htmlparser2/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
+
+
"log-update/slice-ansi": ["slice-ansi@4.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ=="],
+
+
"log-update/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
"parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
}
+56
cypress/component/generator.cy.js
···
+
import baseFonts from '../../src/lib/baseFonts'
+
import titleFonts from '../../src/lib/titleFonts'
+
import themes from '../../src/lib/themes'
+
import ao3CanonicalUrls from "../../src/lib/ao3Canonical"
+
import Page from "../../src/app/generator/page"
+
import styles from "../../src/app/generator/page.module.css"
+
+
describe('<Page />', () => {
+
it('displays an alphabetized list of base font options using the list in the baseFont lib', () => {
+
cy.mount('<Page />')
+
cy.get('select#baseFont > option').should('have.length', Object.keys(baseFonts).length)
+
const sortedFontKeys = Object.keys(baseFonts).sort()
+
const sortedFontValues = Object.values(baseFonts).sort((a, b) => {
+
if (a.displayName > b.displayName) {
+
return 1;
+
}
+
return -1;
+
})
+
cy.get('select#baseFont > option').each(($opt, i) => {
+
expect($opt).to.contain(sortedFontValues[i].displayName)
+
expect($opt).to.have.value(sortedFontKeys[i])
+
})
+
})
+
+
it('displays an alphabetized list of title font options using the list in the titleFont lib', () => {
+
cy.mount('<Page />')
+
cy.get('select#titleFont > option').should('have.length', Object.keys(titleFonts).length)
+
const sortedTitleFontKeys = Object.keys(titleFonts).sort()
+
const sortedTitleFontValues = Object.values(titleFonts).sort((a, b) => {
+
if (a.displayName > b.displayName) {
+
return 1
+
}
+
return -1
+
})
+
cy.get('select#titleFont > option').each(($opt, i) => {
+
expect($opt).to.contain(sortedTitleFontValues[i].displayName)
+
expect($opt).to.have.value(sortedTitleFontKeys[i])
+
})
+
})
+
+
it('displays an alphabetized list of themes using the list in the themes lib', () => {
+
cy.mount('<Page />')
+
cy.get('select#theme > option').should('have.length', Object.keys(themes).length)
+
const sortedThemeKeys = Object.keys(themes).sort()
+
const sortedThemeValues = Object.values(themes).sort((a, b) => {
+
if (a.name > b.name) {
+
return 1
+
}
+
return -1
+
})
+
cy.get('select#theme > option').each(($opt, i) => {
+
expect($opt).to.contain(sortedThemeValues[i].name)
+
expect($opt).to.have.value(sortedThemeKeys[i])
+
})
+
})
+
})
cypress/e2e/app.cy.js

This is a binary file and will not be displayed.

+16
cypress/e2e/navigation.cy.js
···
+
describe('Navigation', () => {
+
it('should navigate to the generator page', () => {
+
// Start from the index page
+
cy.visit('http://localhost:3001')
+
+
// Find a link with an href attribute containing "generator" and click it
+
cy.get('a[href*="/generator"]').click()
+
+
// The new url should include "/generator"
+
cy.url().should('include', '/generator')
+
+
// The new page should contain an form with the ID "geerator"
+
cy.get('form').should('have.id', 'generator')
+
+
})
+
})
+14
cypress/support/component-index.html
···
+
<html lang="en">
+
<head>
+
</head>
+
<body>
+
<div id="page">
+
<h1>
+
<a href="/">fixAO3</a>
+
<a href="/generator" id="generator-link">โœจ generator โœจ</a>
+
</h1>
+
</div>
+
<script data-goatcounter="https://fixao3.goatcounter.com/count"
+
async src="//gc.zgo.at/count.js"></script>
+
</body>
+
</html>
+3
cypress/support/component.js
···
+
import '@fontsource-variable/bricolage-grotesque'
+
import '@fontsource/stack-sans-notch'
+
import "../../src/app/globals.css"
cypress/support/e2e.cy.js

This is a binary file and will not be displayed.

+15
cypress.config.js
···
+
import { defineConfig } from 'cypress'
+
+
export default defineConfig({
+
e2e: {
+
setupNodeEvents(on, config) {},
+
supportFile: false
+
},
+
component: {
+
supportFile: false,
+
devServer: {
+
framework: 'next',
+
bundler: 'webpack',
+
},
+
},
+
})
-3
next.config.mjs
···
/** @type {import('next').NextConfig} */
const nextConfig = {
/* config options here */
-
turbopack: {
-
root: './'
-
}
};
export default nextConfig;
+8 -5
package.json
···
"build": "--bun next build",
"start": "--bun next start",
"lint": "--bun biome check",
-
"format": "--bun biome format --write"
+
"format": "--bun biome format --write",
+
"cypress:open": "cypress open"
},
"dependencies": {
"@fontsource-variable/bricolage-grotesque": "^5.2.10",
"@fontsource/stack-sans-notch": "^5.2.1",
"@fujocoded/ao3.js": "^0.22.1",
-
"@hono/vite-build": "^1.7.0",
+
"@hono/vite-build": "^1.8.0",
"@hono/vite-dev-server": "^0.23.0",
"fauxdom": "^1.2.2",
-
"hono": "^4.10.4",
-
"next": "16.0.2",
+
"hono": "^4.10.8",
+
"next": "16.0.7",
"react": "19.2.0",
"react-dom": "19.2.0"
},
"devDependencies": {
"@biomejs/biome": "2.2.0",
-
"@types/bun": "latest"
+
"@types/bun": "latest",
+
"baseline-browser-mapping": "^2.9.5",
+
"cypress": "^15.7.1"
}
}
src/app/android-chrome-192x192.png

This is a binary file and will not be displayed.

src/app/android-chrome-512x512.png

This is a binary file and will not be displayed.

src/app/api/series/[seriesId]/preview/route.js

This is a binary file and will not be displayed.

+12 -1
src/app/api/series/[seriesId]/route.js
···
import { getSeries } from "@fujocoded/ao3.js"
+
import { setArchiveBaseUrl, resetArchiveBaseUrl } from "@fujocoded/ao3.js/urls"
-
export async function GET(_req, ctx) {
+
export const dynamic = 'force-static'
+
+
export async function GET(req, ctx) {
const { seriesId } = await ctx.params
+
const { archive } = await req.nextUrl.searchParams
+
console.log(seriesId)
+
console.log(archive)
+
const domain = await req.nextUrl.hostname
+
const subdomain = domain.split(".").length > 2 ? domain.split(".")[0] : null
+
if (subdomain) setArchiveBaseUrl('https://'+subdomain)
+
if (archive) setArchiveBaseUrl(archive)
const series = await getSeries({seriesId: seriesId})
+
if (archive || subdomain) resetArchiveBaseUrl()
return Response.json(series)
}
src/app/api/works/[workId]/chapters/[chapterId]/preview/route.js

This is a binary file and will not be displayed.

+12 -1
src/app/api/works/[workId]/chapters/[chapterId]/route.js
···
import { getWork } from "@fujocoded/ao3.js"
+
import { setArchiveBaseUrl, resetArchiveBaseUrl } from "@fujocoded/ao3.js/urls"
+
import siteMap from "@/lib/siteMap.js"
+
+
export const dynamic = 'force-static'
-
export async function GET(_req, ctx) {
+
export async function GET(req, ctx) {
const { workId, chapterId } = await ctx.params
+
const params = await req.nextUrl.searchParams
+
const domain = await req.nextUrl.hostname
+
const subdomain = domain.split(".").length > 2 ? domain.split(".")[0] : null
+
const archive = params ? params.get('archive') : null
+
if (subdomain) setArchiveBaseUrl('https://'+siteMap[subdomain])
+
if (archive) setArchiveBaseUrl(archive)
const work = await getWork({workId: workId, chapterId: chapterId})
+
if (archive || subdomain) resetArchiveBaseUrl()
return Response.json(work)
}
+9 -1
src/app/api/works/[workId]/route.js
···
import { getWork } from "@fujocoded/ao3.js"
+
import { setArchiveBaseUrl, getArchiveBaseUrl, resetArchiveBaseUrl } from "@fujocoded/ao3.js/urls"
-
export async function GET(_req, ctx) {
+
export async function GET(req, ctx) {
const { workId } = await ctx.params
+
const params = await req.nextUrl.searchParams
+
const domain = await req.nextUrl.hostname
+
const subdomain = (domain.split(".").length > 2) ? domain.split(".")[0] : null
+
const archive = params ? params.get('archive') : null
+
if (subdomain) setArchiveBaseUrl('https://'+siteMap[subdomain])
+
if (archive) setArchiveBaseUrl(archive)
const work = await getWork({workId: workId})
+
if (archive || subdomain) resetArchiveBaseUrl()
return Response.json(work)
}
src/app/apple-touch-icon.png

This is a binary file and will not be displayed.

-498
src/app/baseFonts.js
···
-
const baseFonts = {
-
opensans: {
-
displayName: 'Open Sans',
-
defs: [
-
{
-
path: '/fonts/OpenSans-Regular.ttf',
-
style: 'normal',
-
weight: 400
-
},
-
{
-
path: '/fonts/OpenSans-Italic.ttf',
-
style: 'italic',
-
weight: 400
-
},
-
{
-
path: '/fonts/OpenSans-Bold.ttf',
-
style: 'normal',
-
weight: 700
-
},
-
{
-
path: '/fonts/OpenSans-BoldItalic.ttf',
-
style: 'italic',
-
weight: 700
-
}
-
]
-
},
-
bricolagegrotesque: {
-
displayName: 'Bricolage Grotesque',
-
defs: [
-
{
-
path: '/fonts/BricolageGrotesque-Regular.ttf',
-
style: 'normal',
-
weight: 400
-
},
-
{
-
path: '/fonts/BricolageGrotesque-Bold.ttf',
-
style: 'normal',
-
weight: 700
-
}
-
]
-
},
-
spacemono: {
-
displayName: 'Space Mono',
-
defs: [
-
{
-
path: '/fonts/SpaceMono-Regular.ttf',
-
style: 'normal',
-
weight: 400
-
},
-
{
-
path: '/fonts/SpaceMono-Italic.ttf',
-
style: 'italic',
-
weight: 400
-
},
-
{
-
path: '/fonts/SpaceMono-Bold.ttf',
-
style: 'normal',
-
weight: 700
-
},
-
{
-
path: '/fonts/SpaceMono-BoldItalic.ttf',
-
style: 'italic',
-
weight: 700
-
}
-
]
-
},
-
inconsolata: {
-
displayName: 'Inconsolata',
-
defs: [
-
{
-
path: '/fonts/Inconsolata.otf',
-
style: 'normal'
-
}
-
]
-
},
-
bitter: {
-
displayName: 'Bitter',
-
defs: [
-
{
-
path: '/fonts/Bitter-Regular.otf',
-
style: 'normal',
-
weight: 400
-
},
-
{
-
path: '/fonts/Bitter-Italic.otf',
-
style: 'italic',
-
weight: 400
-
},
-
{
-
path: '/fonts/Bitter-Bold.otf',
-
style: 'normal',
-
weight: 700
-
},
-
{
-
path: '/fonts/Bitter-BoldItalic.otf',
-
style: 'italic',
-
weight: 700
-
}
-
]
-
},
-
archivo: {
-
displayName: 'Archivo',
-
defs: [
-
{
-
path: '/fonts/Archivo-Regular.ttf',
-
style: 'normal',
-
weight: 400
-
},
-
{
-
path: '/fonts/Archivo-Italic.ttf',
-
style: 'italic',
-
weight: 400
-
},
-
{
-
path: '/fonts/Archivo-Bold.ttf',
-
style: 'normal',
-
weight: 700
-
},
-
{
-
path: '/fonts/Archivo-BoldItalic.ttf',
-
style: 'italic',
-
weight: 700
-
}
-
]
-
},
-
outfit: {
-
displayName: 'Outfit',
-
defs: [
-
{
-
path: '/fonts/outfit-regular-webfont.woff2',
-
style: 'normal',
-
weight: 400
-
},
-
{
-
path: '/fonts/outfit-italic-webfont.woff2',
-
style: 'italic',
-
weight: 400
-
}
-
]
-
},
-
notosans: {
-
displayName: 'Noto Sans',
-
defs: [
-
{
-
path: '/fonts/NotoSans-Regular.ttf',
-
style: 'normal',
-
weight: 400
-
},
-
{
-
path: '/fonts/NotoSans-Italic.ttf',
-
style: 'italic',
-
weight: 400
-
},
-
{
-
path: '/fonts/NotoSans-Bold.ttf',
-
style: 'normal',
-
weight: 700
-
},
-
{
-
path: '/fonts/NotoSans-BoldItalic.ttf',
-
style: 'italic',
-
weight: 700
-
}
-
]
-
},
-
alegreya: {
-
displayName: 'Alegreya',
-
defs: [
-
{
-
path: '/fonts/Alegreya-Regular.otf',
-
style: 'normal',
-
weight: 400
-
},
-
{
-
path: '/fonts/Alegreya-Italic.otf',
-
style: 'italic',
-
weight: 400
-
},
-
{
-
path: '/fonts/Alegreya-Bold.otf',
-
style: 'normal',
-
weight: 700
-
},
-
{
-
path: '/fonts/Alegreya-BoldItalic.otf',
-
style: 'italic',
-
weight: 700
-
}
-
]
-
},
-
alegreyasans: {
-
displayName: 'Alegreya Sans',
-
defs: [
-
{
-
path: '/fonts/AlegreyaSans-Regular.otf',
-
style: 'normal',
-
weight: 400
-
},
-
{
-
path: '/fonts/AlegreyaSans-Italic.otf',
-
style: 'italic',
-
weight: 400
-
},
-
{
-
path: '/fonts/AlegreyaSans-Bold.otf',
-
style: 'normal',
-
weight: 700
-
},
-
{
-
path: '/fonts/AlegreyaSans-BoldItalic.otf',
-
style: 'italic',
-
weight: 700
-
}
-
]
-
},
-
stacksanstext: {
-
displayName: 'Stack Sans Text',
-
defs: [
-
{
-
path: '/fonts/StackSansText-Regular.ttf',
-
style: 'normal',
-
weight: 400
-
},
-
{
-
path: '/fonts/StackSansText-Bold.ttf',
-
style: 'normal',
-
weight: 700
-
}
-
],
-
},
-
momotrustsans: {
-
displayName: 'Momo Trust Sans',
-
defs: [
-
{
-
path: '/fonts/MomoTrustSans-Regular.ttf',
-
style: 'normal',
-
weight: 400
-
},
-
{
-
path: '/fonts/MomoTrustSans-Bold.ttf',
-
style: 'normal',
-
weight: 700
-
}
-
]
-
},
-
montserrat: {
-
displayName: 'Montserrat',
-
defs: [
-
{
-
path: '/fonts/Montserrat-Regular.otf',
-
style: 'normal',
-
weight: 400
-
},
-
{
-
path: '/fonts/Montserrat-Italic.otf',
-
style: 'italic',
-
weight: 400
-
},
-
{
-
path: '/fonts/Montserrat-Bold.otf',
-
style: 'normal',
-
weight: 700
-
},
-
{
-
path: '/fonts/Montserrat-BoldItalic.otf',
-
style: 'italic',
-
weight: 700
-
}
-
]
-
},
-
robotoslab: {
-
displayName: 'Roboto Slab',
-
defs: [
-
{
-
path: '/fonts/RobotoSlab-Regular.ttf',
-
style: 'normal',
-
weight: 400
-
},
-
{
-
path: '/fonts/RobotoSlab-Bold.ttf',
-
style: 'normal',
-
weight: 700
-
}
-
]
-
},
-
quicksand: {
-
displayName: 'Quicksand',
-
defs: [
-
{
-
path: '/fonts/Quicksand-Regular.otf',
-
style: 'normal',
-
weight: 400
-
},
-
{
-
path: '/fonts/Quicksand-Italic.otf',
-
style: 'italic',
-
weight: 400
-
},
-
{
-
path: '/fonts/Quicksand-Bold.otf',
-
style: 'normal',
-
weight: 700
-
},
-
{
-
path: '/fonts/Quicksand-BoldItalic.otf',
-
style: 'italic',
-
weight: 700
-
}
-
]
-
},
-
worksans: {
-
displayName: 'Work Sans',
-
defs: [
-
{
-
path: '/fonts/WorkSans-Regular.ttf',
-
style: 'normal',
-
weight: 400
-
},
-
{
-
path: '/fonts/WorkSans-Italic.ttf',
-
style: 'italic',
-
weight: 400
-
},
-
{
-
path: '/fonts/WorkSans-Bold.ttf',
-
style: 'normal',
-
weight: 700
-
},
-
{
-
path: '/fonts/WorkSans-BoldItalic.ttf',
-
style: 'italic',
-
weight: 700
-
}
-
]
-
},
-
notosans: {
-
displayName: 'Noto Sans',
-
defs: [
-
{
-
path: '/fonts/NotoSans-Regular.ttf',
-
style: 'normal',
-
weight: 400
-
},
-
{
-
path: '/fonts/NotoSans-Italic.ttf',
-
style: 'italic',
-
weight: 400
-
},
-
{
-
path: '/fonts/NotoSans-Bold.ttf',
-
style: 'normal',
-
weight: 700
-
},
-
{
-
path: '/fonts/NotoSans-BoldItalic.ttf',
-
style: 'italic',
-
weight: 700
-
}
-
]
-
},
-
notoserif: {
-
displayName: 'Noto Serif',
-
defs: [
-
{
-
path: '/fonts/NotoSerif-Regular.ttf',
-
style: 'normal',
-
weight: 400
-
},
-
{
-
path: '/fonts/NotoSerif-Italic.ttf',
-
style: 'italic',
-
weight: 400
-
},
-
{
-
path: '/fonts/NotoSerif-Bold.ttf',
-
style: 'normal',
-
weight: 700
-
},
-
{
-
path: '/fonts/NotoSerif-BoldItalic.ttf',
-
style: 'italic',
-
weight: 700
-
}
-
]
-
},
-
librebaskerville: {
-
displayName: 'Libre Baskerville',
-
defs: [
-
{
-
path: '/fonts/LibreBaskerville-Regular.otf',
-
style: 'normal',
-
weight: 400
-
},
-
{
-
path: '/fonts/LibreBaskerville-Italic.otf',
-
style: 'italic',
-
weight: 400
-
},
-
{
-
path: '/fonts/LibreBaskerville-Bold.otf',
-
style: 'normal',
-
weight: 700
-
}
-
]
-
},
-
ubuntu: {
-
displayName: 'Ubuntu',
-
defs: [
-
{
-
path: '/fonts/Ubuntu-Regular.ttf',
-
style: 'normal',
-
weight: 400
-
},
-
{
-
path: '/fonts/Ubuntu-Italic.ttf',
-
style: 'italic',
-
weight: 400
-
},
-
{
-
path: '/fonts/Ubuntu-Bold.ttf',
-
style: 'normal',
-
weight: 700
-
},
-
{
-
path: '/fonts/Ubuntu-BoldItalic.ttf',
-
style: 'italic',
-
weight: 700
-
}
-
]
-
},
-
parkinsans: {
-
displayName: 'Parkinsans',
-
defs: [
-
{
-
path: '/fonts/Parkinsans-Regular.ttf',
-
style: 'normal',
-
weight: 400
-
},
-
{
-
path: '/fonts/Parkinsans-Bold.ttf',
-
style: 'normal',
-
weight: 700
-
}
-
]
-
},
-
lora: {
-
displayName: 'Lora',
-
defs: [
-
{
-
path: '/fonts/Lora-Regular.ttf',
-
style: 'normal',
-
weight: 400
-
},
-
{
-
path: '/fonts/Lora-Italic.ttf',
-
style: 'italic',
-
weight: 400
-
},
-
{
-
path: '/fonts/Lora-Bold.ttf',
-
style: 'normal',
-
weight: 700
-
},
-
{
-
path: '/fonts/Lora-BoldItalic.ttf',
-
style: 'italic',
-
weight: 700
-
}
-
]
-
},
-
josefinsans: {
-
displayName: 'Josefin Sans',
-
defs: [
-
{
-
path: '/fonts/JosefinSans-Regular.ttf',
-
style: 'normal',
-
weight: 400
-
},
-
{
-
path: '/fonts/JosefinSans-Italic.ttf',
-
style: 'italic',
-
weight: 400
-
},
-
{
-
path: '/fonts/JosefinSans-Bold.ttf',
-
style: 'normal',
-
weight: 700
-
},
-
{
-
path: '/fonts/JosefinSans-BoldItalic.ttf',
-
style: 'italic',
-
weight: 700
-
}
-
]
-
}
-
}
-
-
export default baseFonts
src/app/favicon-16x16.png

This is a binary file and will not be displayed.

src/app/favicon-32x32.png

This is a binary file and will not be displayed.

src/app/favicon.ico

This is a binary file and will not be displayed.

+100 -79
src/app/generator/page.js
···
"use client"
import { useEffect, useState } from "react"
-
import themes from "./themes.js"
-
import baseFonts from "../baseFonts.js"
-
import titleFonts from "../titleFonts.js"
-
import styles from "./page.module.css"
+
import { setArchiveBaseUrl, resetArchiveBaseUrl } from "@fujocoded/ao3.js/urls"
+
import themes from "@/lib/themes.js"
+
import baseFonts from "@/lib/baseFonts.js"
+
import titleFonts from "@/lib/titleFonts.js"
+
import defaults from "@/lib/ogdefaults.js"
+
import ao3CanonicalUrls from "@/lib/ao3Canonical.js"
export default function Generator() {
const [url, setUrl] = useState('')
const [workData, setWorkData] = useState(null)
const [addr, setAddr] = useState('')
const [imgData, setImgData] = useState(null)
-
const [props, setProps] = useState({
-
theme: 'ao3',
-
baseFont: 'bricolagegrotesque',
-
titleFont: 'stacksansnotch',
-
category: true,
-
rating: true,
-
warnings: false,
-
charTags: false,
-
relTags: false,
-
freeTags: false,
-
summary: true,
-
wordcount: true,
-
chapters: true,
-
summaryType: 'basic',
-
customSummary: ''
-
})
+
const [props, setProps] = useState(defaults)
+
const [domain, setDomain] = useState('')
const updateProp = (name, value) => {
const newProps = props
···
}
const updateData = async () => {
+
if (url === '') return
const workMatch = /\/works\/(?<workId>[0-9]+)(?:\/chapters\/(?<chapterId>[0-9]+))?$/
const seriesMatch = /\/series\/(?<seriesId>[0-9]+)$/
+
const baseurl = /(?<domain>https:\/\/[a-z0-9\-\.]+)\//
+
const domainMatch = url.match(baseurl)
+
if (!domainMatch) return
+
setDomain(domainMatch.groups.domain)
+
const domainParam = domain && !ao3CanonicalUrls.includes(domain) ? `?archive=${domain}` : ''
if (workMatch.test(url)) {
const match = url.match(workMatch)
-
const resp = match.groups.chapterId ? await fetch(`/api/works/${match.groups.workId}/chapters/${match.groups.chapterId}`) : await fetch(`/api/works/${match.groups.workId}`)
+
const resp = match.groups.chapterId ? await fetch(`/api/works/${match.groups.workId}/chapters/${match.groups.chapterId}${domainParam}`) : await fetch(`/api/works/${match.groups.workId}${domainParam}`)
+
if (!resp) return
const data = await resp.json()
setAddr(match.groups.chapterId ? `works/${match.groups.workId}/chapters/${match.groups.chapterId}` : `works/${match.groups.workId}`)
setWorkData(data)
} else if (seriesMatch.test(url)) {
const match = url.match(seriesMatch)
-
const resp = await fetch(`/api/series/${match.groups.seriesId}`)
+
const resp = await fetch(`/api/series/${match.groups.seriesId}${domainParam}`)
+
if (!resp) return
const data = await resp.json()
setAddr(`series/${match.groups.seriesId}`)
setWorkData(data)
···
useEffect(() => {
const fn = async () => {
if (!addr) return;
+
if (workData.locked) {
+
const image = await fetch('/locked')
+
if (image.status !== 200) return;
+
const imageBlob = await image.blob()
+
const reader = new FileReader()
+
reader.onloadend = () => {
+
setImgData(reader.result)
+
}
+
return
+
}
const params = new URLSearchParams(props)
-
const image = await fetch(`/${addr}/preview?${params.toString()}`)
+
const image = await fetch(`/${addr}/preview?${params.toString()}&archive=${domain}`)
if (image.status !== 200) return;
const imageBlob = await image.blob()
const reader = new FileReader()
···
<input type="text" name="url" id="url" onChange={e => setUrl(e.target.value)} />
</div>
<details><summary>Style Settings</summary>
-
<div className="input-field">
-
<label htmlFor="theme">Theme</label>
-
<select name="theme" id="theme" defaultValue={props.theme} onChange={e => updateProp("theme", e.target.value)}>
-
{Object.entries(themes).map((th, i) => {
-
return (
-
<option key={i} value={th[0]}>{th[1].name}</option>
-
)
-
})}
-
</select>
-
</div>
-
<div className="input-field">
-
<label htmlFor="baseFont">Base Font</label>
-
<select name="baseFont" id="baseFont" defaultValue={props.baseFont} onChange={e => updateProp("baseFont", e.target.value)}>
-
{Object.entries(baseFonts).sort().map((bf, i) => {
+
<div className="cols">
+
<div className="col">
+
<div className="input-field">
+
<label htmlFor="theme">Theme</label>
+
<select name="theme" id="theme" defaultValue={props.theme} onChange={e => updateProp("theme", e.target.value)}>
+
{Object.entries(themes).sort().map((th, i) => {
+
return (
+
<option key={i} value={th[0]}>{th[1].name}</option>
+
)
+
})}
+
</select>
+
</div>
+
<div className="input-field">
+
<label htmlFor="baseFont">Base Font</label>
+
<select name="baseFont" id="baseFont" defaultValue={props.baseFont} onChange={e => updateProp("baseFont", e.target.value)}>
+
{Object.entries(baseFonts).sort().map((bf, i) => {
-
return (
-
<option key={i} value={bf[0]}>{bf[1].displayName}</option>
-
)
-
})}
-
</select>
-
</div>
-
<div className="input-field">
-
<label htmlFor="titleFont">Title Font</label>
-
<select name="titleFont" id="titleFont" defaultValue={props.titleFont} onChange={e => updateProp("titleFont", e.target.value)}>
-
{Object.entries({...titleFonts, ...baseFonts}).sort().map((tf, i) => {
-
return (
-
<option key={i} value={tf[0]}>{tf[1].displayName}</option>
-
)
-
})}
-
</select>
-
</div>
-
<div className="input-field">
-
<label htmlFor="features">Features:</label>
-
<ul>
-
<li><label><input type="checkbox" name="features[]" value="category" onChange={e => updateProp(e.target.value, e.target.checked)} /> Category</label></li>
-
<li><label><input type="checkbox" name="features[]" value="rating" onChange={e => updateProp(e.target.value, e.target.checked)} /> Rating</label></li>
-
<li><label><input type="checkbox" name="features[]" value="warnings" onChange={e => updateProp(e.target.value, e.target.checked)} /> Archive Warnings</label></li>
-
<li><label><input type="checkbox" name="features[]" value="chartags" onChange={e => updateProp(e.target.value, e.target.checked)} /> Character Tags</label></li>
-
<li><label><input type="checkbox" name="features[]" value="reltags" onChange={e => updateProp(e.target.value, e.target.checked)} /> Relationship Tags</label></li>
-
<li><label><input type="checkbox" name="features[]" value="freetags" onChange={e => updateProp(e.target.value, e.target.checked)} /> Free Tags</label></li>
-
<li><label><input type="checkbox" name="features[]" value="summary" onChange={e => updateProp(e.target.value, e.target.checked)} /> Summary</label></li>
-
<li><label><input type="checkbox" name="features[]" value="wordcount" onChange={e => updateProp(e.target.value, e.target.checked)} /> Wordcount</label></li>
-
<li><label><input type="checkbox" name="features[]" value="chapters" onChange={e => updateProp(e.target.value, e.target.checked)} /> Chapters</label></li>
-
</ul>
-
</div>
-
<div className="input-field">
-
<label htmlFor="summaryOptions">Summary Type</label>
-
<ul>
-
<li><label><input type="radio" name="summaryType" value="basic" onChange={e => updateProp(e.target.name, e.target.value)} /> Story Summary</label></li>
-
<li><label><input type="radio" name="summaryType" value="chapter" onChange={e => updateProp(e.target.name, e.target.value)} /> Chapter Summary (if available)</label></li>
-
<li><label><input type="radio" name="summaryType" value="custom" onChange={e => updateProp(e.target.name, e.target.value)} /> Custom Summary</label></li>
-
</ul>
-
</div>
-
{props.summaryType === 'custom' && (
+
return (
+
<option key={i} value={bf[0]}>{bf[1].displayName}</option>
+
)
+
})}
+
</select>
+
</div>
+
<div className="input-field">
+
<label htmlFor="titleFont">Title Font</label>
+
<select name="titleFont" id="titleFont" defaultValue={props.titleFont} onChange={e => updateProp("titleFont", e.target.value)}>
+
{Object.entries({...titleFonts, ...baseFonts}).sort().map((tf, i) => {
+
return (
+
<option key={i} value={tf[0]}>{tf[1].displayName}</option>
+
)
+
})}
+
</select>
+
</div>
+
</div>
<div className="input-field">
-
<label htmlFor="customSummary">Custom Summary</label>
-
<textarea name="customSummary" id="customSummary" onChange={e => updateProp(e.target.name, e.target.value)}></textarea>
+
<label htmlFor="features">Features:</label>
+
<ul>
+
<li><label><input type="checkbox" name="features[]" value="category" defaultChecked={props.category} onChange={e => updateProp(e.target.value, e.target.checked)} /> Category</label></li>
+
<li><label><input type="checkbox" name="features[]" value="rating" defaultChecked={props.rating} onChange={e => updateProp(e.target.value, e.target.checked)} /> Rating</label></li>
+
<li><label><input type="checkbox" name="features[]" value="warnings" defaultChecked={props.warnings} onChange={e => updateProp(e.target.value, e.target.checked)} /> Archive Warnings</label></li>
+
<li><label><input type="checkbox" name="features[]" value="charTags" defaultChecked={props.charTags} onChange={e => updateProp(e.target.value, e.target.checked)} /> Character Tags</label></li>
+
<li><label><input type="checkbox" name="features[]" value="relTags" defaultChecked={props.relTags} onChange={e => updateProp(e.target.value, e.target.checked)} /> Relationship Tags</label></li>
+
<li><label><input type="checkbox" name="features[]" value="freeTags" defaultChecked={props.freeTags} onChange={e => updateProp(e.target.value, e.target.checked)} /> Free Tags</label></li>
+
<li><label><input type="checkbox" name="features[]" value="summary" defaultChecked={props.summary} onChange={e => updateProp(e.target.value, e.target.checked)} /> Summary</label></li>
+
<li><label><input type="checkbox" name="features[]" value="wordcount" defaultChecked={props.wordcount} onChange={e => updateProp(e.target.value, e.target.checked)} /> Wordcount</label></li>
+
<li><label><input type="checkbox" name="features[]" value="chapters" defaultChecked={props.chapters} onChange={e => updateProp(e.target.value, e.target.checked)} /> Chapters</label></li>
+
<li><label><input type="checkbox" name="features[]" value="postedAt" defaultChecked={props.postedAt} onChange={e => updateProp(e.target.value, e.target.checked)} /> Posted Date</label></li>
+
<li><label><input type="checkbox" name="features[]" value="updatedAt" defaultChecked={props.updatedAt} onChange={e => updateProp(e.target.value, e.target.checked)} /> Updated Date</label></li>
+
</ul>
</div>
-
)}
+
<div className="col">
+
<div className="input-field">
+
<label htmlFor="displayOptions">Display Options</label>
+
<ul>
+
<li><label><input type="checkbox" name="uppercaseTitle" value="uppercaseTitle" defaultChecked={props.uppercaseTitle} onChange={e => updateProp(e.target.value, e.target.checked)} /> Uppercase Title?</label></li>
+
<li><label><input type="checkbox" name="uppercaseChapterName" value="uppercaseChapterName" defaultChecked={props.uppercaseChapterName} onChange={e => updateProp(e.target.value, e.target.checked)} /> Uppercase Chapter Name?</label></li>
+
</ul>
+
</div>
+
<div className="input-field">
+
<label htmlFor="summaryOptions">Summary Type</label>
+
<ul>
+
<li><label><input type="radio" name="summaryType" value="basic" defaultChecked={props.summaryType === 'basic'} onChange={e => updateProp(e.target.name, e.target.value)} /> Story Summary</label></li>
+
<li><label><input type="radio" name="summaryType" defaultChecked={props.summaryType === 'chapter'} value="chapter" onChange={e => updateProp(e.target.name, e.target.value)} /> Chapter Summary (if available)</label></li>
+
<li><label><input type="radio" name="summaryType" defaultChecked={props.summaryType === 'custom'} value="custom" onChange={e => updateProp(e.target.name, e.target.value)} /> Custom Summary</label></li>
+
</ul>
+
{props.summaryType === 'custom' && (
+
<div className="input-field">
+
<label htmlFor="customSummary">Custom Summary</label>
+
<textarea name="customSummary" id="customSummary" onBlur={e => updateProp(e.target.name, e.target.value)}></textarea>
+
</div>
+
)}
+
</div>
+
</div>
+
</div>
</details>
</form>
{imgData && imgData !== '' && (
+28 -3
src/app/globals.css
···
border-bottom: 1px white solid;
padding: 20px;
margin: 0;
+
display: flex;
+
justify-content: space-between;
+
align-items: center;
+
}
+
+
h1 a {
+
text-decoration: none;
+
}
+
+
h1 #generator-link {
+
font-size: 16px;
}
h2 {
···
}
textarea {
-
width: 500px;
+
width: 100%;
max-width: 100%;
-
min-height: 200px;
+
min-height: 100px;
}
.input-field {
···
form ul {
list-style: none;
-
margin: 0;
+
margin: 0 0 1em;
padding: 0;
}
···
details {
margin-bottom: 1em;
+
}
+
+
details .cols {
+
margin-top: 1em;
+
display: grid;
+
grid-template-columns: repeat(3, 1fr);
+
gap: 20px;
+
}
+
+
@media only screen and (max-width: 767px) {
+
details .cols {
+
grid-template-columns: auto;
+
gap: 0;
+
}
}
#output {
+11 -3
src/app/layout.js
···
import "./globals.css"
export const metadata = {
-
title: "fixAO3",
-
description: "fixes yr ao3",
+
title: process.env.SITENAME,
+
description: process.env.DESCRIPTION,
+
metadataBase: new URL('https://'+process.env.DOMAIN),
};
+
+
export const viewport = "width=device-width, initial-scale=1.0"
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<div id="page">
-
<h1>fixAO3</h1>
+
<h1>
+
<a href="/">{process.env.SITENAME}</a>
+
<a href="/generator" id="generator-link">โœจ generator โœจ</a>
+
</h1>
{children}
</div>
+
<script data-goatcounter="https://fixao3.goatcounter.com/count"
+
async src="//gc.zgo.at/count.js"></script>
</body>
</html>
)
+13
src/app/locked/route.js
···
+
import OGImageLocked from "@/lib/ogimagelocked.js"
+
+
export const size = {
+
width: 1600,
+
height: 900,
+
}
+
+
export const contentType = 'image/png'
+
+
export async function GET(req, _ctx) {
+
const props = await req.nextUrl.searchParams
+
return OGImageLocked({theme: props.has('theme') ? props.get('theme') : process.env.DEFAULT_THEME})
+
}
+54
src/app/opengraph-image.jsx
···
+
import { ImageResponse } from "next/og"
+
import { readFile } from 'node:fs/promises'
+
import { join } from 'node:path'
+
+
export const size = {
+
width: 1600,
+
height: 900,
+
}
+
export const alt = 'fixAO3'
+
+
export const contentType = 'image/webp'
+
+
export default async function Image() {
+
return new ImageResponse(
+
(
+
<div
+
style={{
+
backgroundColor: '#990000',
+
color: '#FFFFFF',
+
display: 'flex',
+
alignItems: 'center',
+
justifyContent: 'center',
+
width: '100%',
+
height: '100%',
+
fontFamily: 'Stack Sans Notch',
+
fontSize: 288,
+
fontWeight: 700
+
}}
+
>
+
{process.env.SITENAME}
+
</div>
+
),
+
{
+
fonts: [
+
{
+
name: 'Stack Sans Notch',
+
data: await readFile(
+
join(process.cwd(), '/fonts/StackSansNotch-Regular.ttf')
+
),
+
style: 'normal',
+
weight: 400
+
},
+
{
+
name: 'Stack Sans Notch',
+
data: await readFile(
+
join(process.cwd(), '/fonts/StackSansNotch-Bold.ttf')
+
),
+
style: 'normal',
+
weight: 700
+
}
+
]
+
}
+
)
+
}
+7 -3
src/app/page.js
···
<h2>How do I use it?</h2>
<p>
All you have to do is change the "ao3.org" or
-
"archiveofourown.org" part of your fic's URL to "fixao3.val.run".
+
"archiveofourown.org" part of your fic's URL to "{process.env.DOMAIN}".
It'll automatically pull in your fic's metadata and set up a
redirect. Easy as that!
</p>
+
<h2>How is my data used?</h2>
+
<p>No data is stored on my servers; it's all just fetched from AO3 in realtime. If this starts eating all my bandwidth I might look into caching resources but also I don't want to deal with a database. Data is only fetched when requested (either by linking a url somewhere that uses opengraph embeds, or by using the card generator).</p>
+
<p>A limited amount of non-identifying data is collected for statistical purposes so I know, like, how much use the site is getting, where people are hearing about it from, etc. If you don't want your data collected, just block goatcounter.com with your ad/scriptblocker of choice and you will be functionally invisible to me. Spooky...</p>
<h2>Support fixAO3</h2>
<p>
If you found this useful and would like to support further
development, you can tip me on{" "}
<a href="https://ko-fi.com/veryroundbird" target="_blank">
Ko-fi
-
</a>! This work would also be much, much harder and more annoying
+
</a>! The server/domain costs are maybe like, $40/yr at the moment which is not much, but also I'm an underemployed freelancer doing this for fun, so I appreciate people chipping in if you find it useful!</p>
+
<p>This work would also be much, much harder and more annoying
without being able to build off of Fujocoded's{" "}
<a href="https://github.com/fujocoded/ao3.js" target="_blank">
AO3.js
-
</a>. For questions, comments, job offers, and other inquiries,
+
</a> (give them your support too!). For questions, comments, job offers, and other inquiries,
I'm{" "}
<a
href="https://bsky.app/profile/veryroundbird.house"
+27
src/app/series/[seriesId]/opengraph-image.jsx
···
+
import { getSeries } from "@fujocoded/ao3.js"
+
import sanitizeData from "@/lib/sanitizeData.js"
+
import OGImage from "@/lib/ogimage.js"
+
import OGImageLocked from "@/lib/ogimagelocked.js"
+
import baseFonts from "@/lib/baseFonts.js"
+
import titleFonts from "@/lib/titleFonts.js"
+
import defaults from "@/lib/ogdefaults.js"
+
+
export const size = {
+
width: 1600,
+
height: 900,
+
}
+
export const alt = 'fixAO3'
+
export const contentType = 'image/webp'
+
+
export default async function Image({params, searchParams}) {
+
const { seriesId } = await params
+
const addr = `series/${seriesId}`
+
const data = await getSeries({seriesId: seriesId})
+
if (data.locked) return OGImageLocked({theme: theme})
+
const imageParams = await sanitizeData({type: 'series', data: data, props: new URLSearchParams(defaults)})
+
const theme = imageParams.theme
+
const baseFont = baseFonts[imageParams.baseFont].displayName
+
const titleFont = titleFonts[imageParams.titleFont].displayName
+
const opts = imageParams.opts
+
return OGImage({theme: theme, baseFont: baseFont, titleFont: titleFont, image: imageParams, addr: addr, opts: opts})
+
}
+7 -5
src/app/series/[seriesId]/page.js
···
import { getSeries } from "@fujocoded/ao3.js"
+
import DOM from "fauxdom"
export async function generateMetadata({ params, _searchParams }, _parent) {
// read route params
···
const fandoms = series.works.map(w => w.fandoms).reduce((a,b) => { return a.concat(b) }).filter((f, i, arr) => arr.indexOf(f) === i)
const fandomString = fandoms.length > 4 ? fandoms.slice(0, 4).join(", ")+" (+"+(fandoms.length - 4)+")" : (fandoms.length > 1 ? fandoms.slice(0, -1).join(", ")+" & "+fandoms.slice(-1)[0] : fandoms[0])
const title = `${series.name} by ${authorString} - ${fandomString} (${series.workCount} works)`
-
const description = `${series.description.replace("<br />", "\n").replace(/<[^>]>/g, "")}`
+
const summaryDOM = new DOM(summary, {decodeEntities: true});
+
const description = summaryDOM.innerHTML.replace(/\<br(?: \/)?\>/g, "\n").replace(
+
/(<([^>]+)>)/ig,
+
"",
+
)
const addr = `series/${seriesId}`
return {
title: title,
description: description,
-
openGraph: {
-
description: description,
-
images: [`/${addr}/preview`]
-
}
+
metadataBase: new URL('https://'+process.env.DOMAIN)
}
}
+31
src/app/series/[seriesId]/preview/route.js
···
+
import { getSeries } from "@fujocoded/ao3.js"
+
import { setArchiveBaseUrl, resetArchiveBaseUrl } from "@fujocoded/ao3.js/urls"
+
import querystring from 'node:querystring'
+
import sanitizeData from "@/lib/sanitizeData.js"
+
import OGImage from "@/lib/ogimage.js"
+
import baseFonts from "@/lib/baseFonts.js"
+
import titleFonts from "@/lib/titleFonts.js"
+
import ao3CanonicalUrls from "@/lib/ao3Canonical.js"
+
+
export const size = {
+
width: 1600,
+
height: 900,
+
}
+
+
export const contentType = 'image/png'
+
+
export async function GET(req, ctx) {
+
const { seriesId } = await ctx.params
+
const p = await req.nextUrl.searchParams
+
const props = querystring.parse(p.toString())
+
const addr = `series/${seriesId}`
+
const domainParam = p && p.has('archive') && !ao3CanonicalUrls.includes(p.get('archive')) ? `?archive=${p.get('archive')}` : ''
+
const work = await fetch(`http://${process.env.DOMAIN}/api/series/${seriesId}${domainParam}`)
+
const data = await work.json()
+
const imageParams = await sanitizeData({type: 'series', data: data, props: p})
+
const theme = imageParams.theme
+
const baseFont = baseFonts[imageParams.baseFont].displayName
+
const titleFont = titleFonts[imageParams.titleFont].displayName
+
const opts = imageParams.opts
+
return OGImage({theme: theme, baseFont: baseFont, titleFont: titleFont, image: imageParams, addr: addr, opts: opts})
+
}
-94
src/app/themes.js
···
-
const themes = {
-
ao3: {
-
name: 'AO3',
-
background: '#990000',
-
color: '#FFFFFF',
-
descBackground: '#FFFFFF',
-
descColor: '#000000',
-
accent: '#FFFFFF',
-
accent2: '#990000'
-
},
-
softEra: {
-
name: 'Soft Era',
-
background: '#F9F5F5',
-
color: '#C8B3B3',
-
descBackground: '#F9F5F5',
-
descColor: '#414141',
-
accent: '#DB90A7',
-
accent2: '#EEAABE'
-
},
-
wildCherry: {
-
name: 'Wild Cherry',
-
background: '#2B1F32',
-
color: '#FFFFFF',
-
descBackground: '#FFFFFF',
-
descColor: '#2B1F32',
-
accent: '#E15D97',
-
accent2: '#0AACC5'
-
},
-
rosePine: {
-
name: 'Rosรฉ Pine',
-
background: '#191724',
-
color: '#e0def4',
-
descBackground: '#1f1d2e',
-
descColor: '#e0def4',
-
accent: '#eb6f92',
-
accent2: '#31748f'
-
},
-
rosePineDawn: {
-
name: 'Rosรฉ Pine Dawn',
-
background: '#faf4ed',
-
color: '#575279',
-
descBackground: '#fffaf3',
-
descColor: '#575279',
-
accent: '#eb6f92',
-
accent2: '#286983'
-
},
-
rosePineMoon: {
-
name: 'Rosรฉ Pine Moon',
-
background: '#232136',
-
color: '#e0def4',
-
descBackground: '#2a273f',
-
descColor: '#e0def4',
-
accent: '#b4637a',
-
accent2: '#3e8fb0'
-
},
-
solarizedLight: {
-
name: 'Solarized Light',
-
background: '#fdf6e3',
-
color: '#b58900',
-
descBackground: '#eee8d5',
-
descColor: '#002b36',
-
accent: '#d33682',
-
accent2: '#2aa198'
-
},
-
solarizedDark: {
-
name: 'Solarized Dark',
-
background: '#002b36',
-
color: '#b58900',
-
descBackground: '#073642',
-
descColor: '#fdf6e3',
-
accent: '#d33682',
-
accent2: '#2aa198'
-
},
-
squidgeworld: {
-
name: 'Squidgeworld',
-
background: '#b8860b',
-
color: '#f5f5dc',
-
descBackground: '#f5f5dc',
-
color: '#2a2a2a',
-
accent: '#fece3f',
-
accent2: '#818D4C'
-
},
-
superlove: {
-
name: 'Superlove',
-
background: '#df6191',
-
color: '#ffffff',
-
descBackground: '#FFFFFF',
-
color: '#2a2a2a',
-
accent: '#F9E4E6',
-
accent2: '#a33961'
-
}
-
}
-
-
export default themes
-226
src/app/titleFonts.js
···
-
import baseFonts from "./baseFonts.js"
-
-
const titleFonts = {
-
...baseFonts,
-
playfairdisplay: {
-
displayName: 'Playfair Display',
-
defs: [
-
{
-
path: '/fonts/Playfair-Regular.ttf',
-
style: 'normal',
-
weight: 400
-
},
-
{
-
path: '/fonts/Playfair-Italic.ttf',
-
style: 'italic',
-
weight: 400
-
},
-
{
-
path: '/fonts/Playfair-Bold.ttf',
-
style: 'normal',
-
weight: 700
-
},
-
{
-
path: '/fonts/Playfair-BoldItalic.ttf',
-
style: 'italic',
-
weight: 700
-
}
-
]
-
},
-
ultra: {
-
displayName: 'Ultra',
-
defs: [
-
{
-
path: '/fonts/Ultra-Regular.ttf',
-
style: 'normal',
-
weight: 400
-
}
-
]
-
},
-
stacksansheadline: {
-
displayName: 'Stack Sans Headline',
-
defs: [
-
{
-
path: '/fonts/StackSansHeadline-Regular.ttf',
-
style: 'normal',
-
weight: 400
-
},
-
{
-
path: '/fonts/StackSansHeadline-Bold.ttf',
-
style: 'normal',
-
weight: 700
-
}
-
]
-
},
-
stacksansnotch: {
-
displayName: 'Stack Sans Notch',
-
defs: [
-
{
-
path: '/fonts/StackSansNotch-Regular.ttf',
-
style: 'normal',
-
weight: 400
-
},
-
{
-
path: '/fonts/StackSansNotch-Bold.ttf',
-
style: 'normal',
-
weight: 700
-
}
-
]
-
},
-
titanone: {
-
displayName: 'Titan One',
-
defs: []
-
},
-
momotrustdisplay: {
-
displayName: 'Momo Trust Display',
-
defs: [
-
{
-
path: '/fonts/MomoTrustDisplay-Regular.ttf',
-
style: 'normal',
-
weight: 400
-
},
-
{
-
path: '/fonts/MomoTrustDisplay-Bold.ttf',
-
style: 'normal',
-
weight: 700
-
}
-
]
-
},
-
momosignature: {
-
displayName: 'Momo Signature',
-
defs: [
-
{
-
path: '/fonts/MomoSignature-Regular.ttf',
-
style: 'normal',
-
weight: 400
-
}
-
]
-
},
-
londrinasketch: {
-
displayName: 'Londrina Sketch',
-
defs: [
-
{
-
path: '/fonts/LondrinaSketch-Regular.ttf',
-
style: 'normal',
-
weight: 400
-
}
-
]
-
},
-
londrinashadow: {
-
displayName: 'Londrina Shadow',
-
defs: [
-
{
-
path: '/fonts/LondrinaShadow-Regular.ttf',
-
style: 'normal',
-
weight: 400
-
}
-
]
-
},
-
londrinasolid: {
-
displayName: 'Londrina Solid',
-
defs: [
-
{
-
path: '/fonts/LondrinaSolid-Regular.ttf',
-
style: 'normal',
-
weight: 400
-
},
-
{
-
path: '/fonts/LondrinaSolid-Black.ttf',
-
style: 'normal',
-
weight: 700
-
}
-
]
-
},
-
bebasneue: {
-
displayName: 'Bebas Neue',
-
defs: [
-
{
-
path: '/fonts/BebasNeue-Regular.ttf',
-
style: 'normal',
-
weight: 400
-
}
-
]
-
},
-
oswald: {
-
displayName: 'Oswald',
-
defs: [
-
{
-
path: '/fonts/Oswald-Regular.ttf',
-
style: 'normal',
-
weight: 400
-
},
-
{
-
path: '/fonts/Oswald-Bold.ttf',
-
style: 'normal',
-
weight: 700
-
}
-
]
-
},
-
archivoblack: {
-
displayName: 'Archivo Black',
-
defs: [
-
{
-
path: '/fonts/ArchivoBlack.otf',
-
style: 'normal',
-
weight: 400
-
}
-
]
-
},
-
alfaslabone: {
-
displayName: 'Alfa Slab One',
-
defs: [
-
{
-
path: '/fonts/AlfaSlabOne-Regular.ttf',
-
style: 'normal',
-
weight: 400
-
}
-
]
-
},
-
sixtyfour: {
-
displayName: 'SixtyFour',
-
defs: [
-
{
-
path: '/fonts/Sixtyfour-Regular.ttf',
-
style: 'normal',
-
weight: 400
-
},
-
{
-
path: '/fonts/Sixtyfour-Regular.ttf',
-
style: 'normal',
-
weight: 700
-
}
-
]
-
},
-
datalegreyathin: {
-
displayName: 'Datalegreya Thin',
-
defs: [
-
{
-
path: '/fonts/Datalegreya-Thin.otf',
-
style: 'normal',
-
weight: 400
-
}
-
]
-
},
-
datalegreyadot: {
-
displayName: 'Datalegreya Dot',
-
defs: [
-
{
-
path: '/fonts/Datalegreya-Dot.otf',
-
style: 'normal',
-
weight: 400
-
}
-
]
-
},
-
datalegreyagradient: {
-
displayName: 'Datalegreya Gradient',
-
defs: [
-
{
-
path: '/fonts/Datalegreya-Gradient.otf',
-
style: 'normal',
-
weight: 400
-
}
-
]
-
}
-
}
-
-
export default titleFonts
+27
src/app/works/[workId]/chapters/[chapterId]/opengraph-image.jsx
···
+
import { getWork } from "@fujocoded/ao3.js"
+
import sanitizeData from "@/lib/sanitizeData.js"
+
import OGImage from "@/lib/ogimage.js"
+
import OGImageLocked from "@/lib/ogimagelocked.js"
+
import baseFonts from "@/lib/baseFonts.js"
+
import titleFonts from "@/lib/titleFonts.js"
+
import defaults from "@/lib/ogdefaults.js"
+
+
export const size = {
+
width: 1600,
+
height: 900,
+
}
+
export const alt = 'fixAO3'
+
export const contentType = 'image/webp'
+
+
export default async function Image({params, searchParams}) {
+
const { workId, chapterId } = await params
+
const addr = `works/${workId}/chapters/${chapterId}`
+
const data = await getWork({workId: workId, chapterId: chapterId})
+
if (data.locked) return OGImageLocked({theme: process.env.DEFAULT_THEME})
+
const imageParams = await sanitizeData({type: 'work', data: data, props: new URLSearchParams(defaults)})
+
const theme = imageParams.theme
+
const baseFont = baseFonts[imageParams.baseFont].displayName
+
const titleFont = titleFonts[imageParams.titleFont].displayName
+
const opts = imageParams.opts
+
return OGImage({theme: theme, baseFont: baseFont, titleFont: titleFont, image: imageParams, addr: addr, opts: opts})
+
}
+27 -9
src/app/works/[workId]/chapters/[chapterId]/page.js
···
import { getWork } from "@fujocoded/ao3.js"
+
import DOM from "fauxdom"
+
import siteMap from "@/lib/siteMap.js"
-
export async function generateMetadata({ params, _searchParams }, _parent) {
+
export async function generateMetadata({ params, _searchParams }, parent) {
// read route params
const { workId, chapterId } = await params
-
const work = await getWork({workId: workId, chapterId: chapterId})
+
const parentData = await parent;
+
const base = parentData.metadataBase.replace('https://', '').replace('/', '')
+
const subdomain = base.split(".").length > 0 ? base.split(".")[0] : null
+
const archive = subdomain && Object.keys(siteMap).includes(subdomain) ? siteMap[subdomain] : null
+
const domainParam = archive ? `?archive=https://${archive}` : ''
+
const data = await fetch(`http://${process.env.DOMAIN}/api/works/${workId}/chapters/${chapterId}${domainParam}`)
+
const work = await data.json()
+
if (work.locked) {
+
return {
+
title: 'Locked Work',
+
description: 'This work is locked to the public. Log in to see it!',
+
metadataBase: new URL('https://'+archive)
+
}
+
}
+
const parentWorkData = await fetch(`http://${process.env.DOMAIN}/api/works/${workId}`)
+
const parentWork = await parentWorkData.json()
const authors = work.authors.map((a) => {
if (a.anonymous) {
return "Anonymous"
···
const fandoms = work.fandoms
const fandomString = fandoms.length > 4 ? fandoms.slice(0, 4).join(", ")+" (+"+(fandoms.length - 4)+")" : (fandoms.length > 1 ? fandoms.slice(0, -1).join(", ")+" & "+fandoms.slice(-1)[0] : fandoms[0])
const title = `${work.title} by ${authorString}, Chapter ${work.chapterInfo.index}${work.chapterInfo.name ? ": "+work.chapterInfo.name : ''} - ${fandomString}`
-
const summary = work.chapterInfo && work.chapterInfo.summary ? work.chapterInfo.summary : work.summary
-
const description = `${summary.replace("<br />", "\n").replace(/<[^>]>/g, "")}`
+
const summary = work.chapterInfo && work.chapterInfo.summary ? work.chapterInfo.summary : parentWork.summary
+
const summaryDOM = new DOM(summary, {decodeEntities: true});
+
const description = summaryDOM.innerHTML.replace(/\<br(?: \/)?\>/g, "\n").replace(
+
/(<([^>]+)>)/ig,
+
"",
+
)
const addr = `works/${workId}/chapters/${chapterId}`
return {
title: title,
description: description,
-
openGraph: {
-
description: description,
-
images: [`/${addr}/preview`]
-
}
+
metadataBase: new URL('https://'+base)
}
}
···
const { workId, chapterId } = await params
return (
<div dangerouslySetInnerHTML={{__html: `<script type="text/javascript">
-
window.location.replace("https://archiveofourown.org/works/${workId}/chapters/${chapterId}");
+
window.location.replace("https://archiveofourown.org/works/${workId}");
</script>`}}></div>
)
}
+30
src/app/works/[workId]/chapters/[chapterId]/preview/route.js
···
+
import querystring from 'node:querystring'
+
import sanitizeData from "@/lib/sanitizeData.js"
+
import OGImage from "@/lib/ogimage.js"
+
import baseFonts from "@/lib/baseFonts.js"
+
import titleFonts from "@/lib/titleFonts.js"
+
import siteMap from "@/lib/siteMap.js"
+
import ao3CanonicalUrls from "@/lib/ao3Canonical.js"
+
+
export const size = {
+
width: 1600,
+
height: 900,
+
}
+
+
export const contentType = 'image/png'
+
+
export async function GET(req, ctx) {
+
const { workId, chapterId } = await ctx.params
+
const props = await req.nextUrl.searchParams
+
const addr = `${props && props.has('archive') ? props.get('archive').replace("https://", "")+"/" : 'archiveofourown.org/'}works/${workId}/chapters/${chapterId}`
+
const domainParam = props && props.has('archive') && !ao3CanonicalUrls.includes(props.get('archive')) ? `?archive=${props.get('archive')}` : ''
+
const subdomain = props && props.has('archive') && Object.values(siteMap).includes(props.get('archive')) ? Object.keys(siteMap)[Object.values(siteMap).indexOf(props.get('archive'))]+'.' : ''
+
const work = await fetch(`http://${subdomain}${process.env.DOMAIN}/api/works/${workId}${domainParam}`)
+
const data = await work.json()
+
const imageParams = await sanitizeData({type: 'work', data: data, props: props})
+
const theme = imageParams.theme
+
const baseFont = baseFonts[imageParams.baseFont].displayName
+
const titleFont = titleFonts[imageParams.titleFont].displayName
+
const opts = imageParams.opts
+
return OGImage({theme: theme, baseFont: baseFont, titleFont: titleFont, image: imageParams, addr: addr, opts: opts})
+
}
+27
src/app/works/[workId]/opengraph-image.jsx
···
+
import { getWork } from "@fujocoded/ao3.js"
+
import sanitizeData from "@/lib/sanitizeData.js"
+
import OGImage from "@/lib/ogimage.js"
+
import OGImageLocked from "@/lib/ogimagelocked.js"
+
import baseFonts from "@/lib/baseFonts.js"
+
import titleFonts from "@/lib/titleFonts.js"
+
import defaults from "@/lib/ogdefaults.js"
+
+
export const size = {
+
width: 1600,
+
height: 900,
+
}
+
export const alt = 'fixAO3'
+
export const contentType = 'image/webp'
+
+
export default async function Image({params, searchParams}) {
+
const { workId } = await params
+
const addr = `works/${workId}`
+
const data = await getWork({workId: workId})
+
if (data.locked) return OGImageLocked({theme: process.env.DEFAULT_THEME})
+
const imageParams = await sanitizeData({type: 'work', data: data, props: new URLSearchParams(defaults)})
+
const theme = imageParams.theme
+
const baseFont = baseFonts[imageParams.baseFont].displayName
+
const titleFont = titleFonts[imageParams.titleFont].displayName
+
const opts = imageParams.opts
+
return OGImage({theme: theme, baseFont: baseFont, titleFont: titleFont, image: imageParams, addr: addr, opts: opts})
+
}
+22 -9
src/app/works/[workId]/page.js
···
import { getWork } from "@fujocoded/ao3.js"
+
import DOM from "fauxdom"
-
export async function generateMetadata({ params, _searchParams }, _parent) {
+
export async function generateMetadata({ params, _searchParams }, parent) {
// read route params
const { workId, chapterId } = await params
+
const p = await parent
+
console.log(p)
const work = await getWork({workId: workId, chapterId: chapterId})
+
if (work.locked) {
+
return {
+
title: 'Locked Work',
+
description: 'This work is locked to the public. Log in to see it!',
+
metadataBase: new URL('https://'+process.env.DOMAIN)
+
}
+
}
const authors = work.authors.map((a) => {
if (a.anonymous) {
return "Anonymous"
···
const fandomString = fandoms.length > 4 ? fandoms.slice(0, 4).join(", ")+" (+"+(fandoms.length - 4)+")" : (fandoms.length > 1 ? fandoms.slice(0, -1).join(", ")+" & "+fandoms.slice(-1)[0] : fandoms[0])
const title = `${work.title} by ${authorString} - ${fandomString}`
const summary = work.summary
-
const description = `${summary.replace("<br />", "\n").replace(/<[^>]>/g, "")}`
+
const summaryDOM = new DOM(summary, {decodeEntities: true});
+
const description = summaryDOM.innerHTML.replace(/\<br(?: \/)?\>/g, "\n").replace(
+
/(<([^>]+)>)/ig,
+
"",
+
)
const addr = `works/${workId}`
+
const parentData = await parent;
+
const base = parentData.metadataBase.replace('https://', '').replace('/', '')
return {
-
title: title,
-
description: description,
-
openGraph: {
-
description: description,
-
images: [`/${addr}/preview`]
-
}
+
title: title ? title : 'Locked Work',
+
description: description ? description : 'This but this work is locked to the public. Log in to see it!',
+
metadataBase: new URL('https://'+base)
}
}
···
const { workId, chapterId } = await params
return (
<div dangerouslySetInnerHTML={{__html: `<script type="text/javascript">
-
window.location.replace("https://archiveofourown.org/works/${workId}");
+
window.location.replace("https://archiveofourown.org/works/${workId}/chapters/${chapterId}");
</script>`}}></div>
)
}
+16 -174
src/app/works/[workId]/preview/route.js
···
-
import { getWork } from "@fujocoded/ao3.js"
-
import DOM from "fauxdom"
-
import { ImageResponse } from "next/og"
-
import { readFile } from 'node:fs/promises'
-
import { join } from 'node:path'
-
import themes from '../../../themes.js'
-
import baseFonts from '../../../baseFonts.js'
-
import titleFonts from '../../../titleFonts.js'
+
import sanitizeData from "@/lib/sanitizeData.js"
+
import OGImage from "@/lib/ogimage.js"
+
import baseFonts from "@/lib/baseFonts.js"
+
import titleFonts from "@/lib/titleFonts.js"
export const size = {
width: 1600,
height: 900,
}
-
export const contentType = 'image/webp'
+
export const contentType = 'image/png'
export async function GET(req, ctx) {
const { workId } = await ctx.params
const props = await req.nextUrl.searchParams
-
const addr = `works/${workId}`
-
const data = await getWork({workId: workId})
-
const baseFontData = baseFonts[props.has('baseFont') ? props.get('baseFont') : 'bricolagegrotesque']
-
const titleFontData = titleFonts[props.has('titleFont') ? props.get('titleFont') : 'stacksansnotch']
-
const themeData = props.has('theme') ? themes[props.get('theme')] : themes['ao3']
-
const bfs = await Promise.all(baseFontData.defs.map(async (bf) => {
-
return {
-
name: baseFontData.displayName,
-
data: await readFile(
-
join(process.cwd(), bf.path)
-
),
-
style: bf.style,
-
weight: bf.weight
-
}
-
})).then(x => x)
-
const tfs = await Promise.all(titleFontData.defs.map(async (tf) => {
-
return {
-
name: titleFontData.displayName,
-
data: await readFile(
-
join(process.cwd(), tf.path)
-
),
-
style: tf.style,
-
weight: tf.weight
-
}
-
})).then(x => x)
-
const authorsFormatted = data.authors
-
? data.authors.map((a) => {
-
if (a.anonymous) return "Anonymous"
-
if (a.pseud !== a.username) return `${a.pseud} (${a.username})`
-
return a.username
-
})
-
: []
-
const authorString = authorsFormatted.length > 1
-
? authorsFormatted.slice(0, -1).join(", ") + " & " +
-
authorsFormatted.slice(-1)[0]
-
: authorsFormatted[0]
-
const summaryDOM = new DOM(props.get('summaryType') === 'chapter' && data.chapterInfo && data.chapterInfo.summary ? data.chapterInfo.summary : (props.get('summaryType') === 'custom' && props.has('customSummary') ? props.get('customSummary') : data.summary), {decodeEntities: true});
-
const summaryFormatted = summaryDOM.innerHTML.replace("<br />", "\n").replace(
-
/(<([^>]+)>)/ig,
-
"",
-
).split("\n")
-
const titleString = `<b>${data.title}</b> by ${authorString}`
-
const chapterString = data.chapterInfo ? (data.chapterInfo.name
-
? data.chapterInfo.name
-
: "Chapter " + data.chapterInfo.index) : ''
-
const chapterCountString = data.chapters
-
? ' | <b>Chapters:</b> '+data.chapters.published+' / '+(
-
data.chapters.total
-
? data.chapters.total
-
: '?'
-
)
-
: ''
-
const fandomString = (data.fandoms.length > 1 ? (data.fandoms.length <= 5 ? data.fandoms.slice(0, -1).join(", ")+" & "+data.fandoms.slice(-1) : data.fandoms.join(", ")+" (+"+(data.fandoms.length - 4)+")") : data.fandoms[0]).toUpperCase()
-
const headingString = `<span size='16pt'>${fandomString}</span>\n${titleString}${chapterString !== '' ? "\n<span size='36pt'><i>"+chapterString+"</i></span></span>" : ''}`
-
const opts = {
-
fonts: bfs.concat(tfs)
-
}
-
console.log(themeData)
-
console.log(baseFontData)
-
console.log(titleFontData)
-
return new ImageResponse(
-
(
-
<div
-
style={{
-
display: "flex",
-
flexDirection: "column",
-
color: themeData.color,
-
backgroundColor: themeData.background,
-
fontFamily: baseFontData.displayName,
-
fontSize: 24,
-
padding: 40,
-
width: "100%",
-
height: "100%",
-
}}
-
>
-
<div
-
style={{
-
display: "flex",
-
flexDirection: "column",
-
marginBottom: 20
-
}}
-
>
-
<div
-
style={{
-
textTransform: "uppercase",
-
display: "flex",
-
justifyContent: "center",
-
color: themeData.accent
-
}}
-
>
-
{fandomString}
-
</div>
-
<div
-
style={{
-
fontSize: 54,
-
justifyContent: "center",
-
fontFamily: titleFontData.displayName,
-
fontWeight: "bold"
-
}}
-
>
-
{data.title}
-
</div>
-
<div
-
style={{
-
fontSize: 42,
-
justifyContent: "center",
-
fontFamily: titleFontData.displayName
-
}}
-
>
-
{`by ${authorString}`}
-
</div>
-
<div
-
style={{
-
fontStyle: "italic",
-
fontSize: 36,
-
fontFamily: titleFontData.displayName
-
}}
-
>
-
{chapterString}
-
</div>
-
</div>
-
<div
-
style={{
-
backgroundColor: themeData.descBackground,
-
padding: 20,
-
display: "flex",
-
flexDirection: "column",
-
flexGrow: 1,
-
color: themeData.descColor,
-
alignItems: "flex-end"
-
}}
-
>
-
<div
-
style={{
-
display: "flex",
-
flexDirection: "column",
-
flexGrow: 1,
-
width: '100%'
-
}}
-
>
-
{summaryFormatted.map(l => (
-
<div
-
style={{
-
width: "100%",
-
marginBottom: 10
-
}}
-
>
-
{l}
-
</div>
-
))}
-
</div>
-
<div
-
style={{
-
textAlign: "right",
-
fontSize: 18,
-
color: themeData.accent2
-
}}
-
>
-
{`https://archiveofourown.org/${addr}`}
-
</div>
-
</div>
-
</div>
-
),
-
opts
-
)
+
const addr = `${props && props.has('archive') ? props.get('archive').replace("https://", "")+"/" : 'archiveofourown.org/'}works/${workId}`
+
const domainParam = props && props.has('archive') ? `?archive=${props.get('archive')}` : ''
+
const work = await fetch(`http://${process.env.DOMAIN}/api/works/${workId}${domainParam}`)
+
const data = await work.json()
+
const imageParams = await sanitizeData({type: 'work', data: data, props: props})
+
console.log(imageParams)
+
const theme = imageParams.theme
+
const baseFont = baseFonts[imageParams.baseFont].displayName
+
const titleFont = titleFonts[imageParams.titleFont].displayName
+
const opts = imageParams.opts
+
return OGImage({theme: theme, baseFont: baseFont, titleFont: titleFont, image: imageParams, addr: addr, opts: opts})
}
+8
src/icons/chosenottowarn.js
···
+
export default function ChoseNotToWarn ({bg, fg, width, height}) {
+
return (
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" width={width} height={height}>
+
<ellipse cx="320" cy="320" rx="320" ry="320" fill={bg} />
+
<path d="M320 128C96 128 32 224 32 336C32 448 112 512 208 512L216.4 512C240.6 512 262.8 498.3 273.6 476.6L296.8 430.3C301.2 421.5 310.1 416 320 416C329.9 416 338.8 421.5 343.2 430.3L366.4 476.6C377.2 498.3 399.4 512 423.6 512L432 512C528 512 608 448 608 336C608 224 544 128 320 128zM128 320C128 284.7 156.7 256 192 256C227.3 256 256 284.7 256 320C256 355.3 227.3 384 192 384C156.7 384 128 355.3 128 320zM448 256C483.3 256 512 284.7 512 320C512 355.3 483.3 384 448 384C412.7 384 384 355.3 384 320C384 284.7 412.7 256 448 256z" fill={fg}/>
+
</svg>
+
)
+
}
+8
src/icons/explicit.js
···
+
export default function Explicit ({bg, fg, width, height}) {
+
return (
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" width={width} height={height}>
+
<ellipse cx="320" cy="320" rx="320" ry="320" fill={bg} />
+
<path d="M192 96C174.3 96 160 110.3 160 128L160 512C160 529.7 174.3 544 192 544L448 544C465.7 544 480 529.7 480 512C480 494.3 465.7 480 448 480L224 480L224 352L384 352C401.7 352 416 337.7 416 320C416 302.3 401.7 288 384 288L224 288L224 160L448 160C465.7 160 480 145.7 480 128C480 110.3 465.7 96 448 96L192 96z" fill={fg} />
+
</svg>
+
)
+
}
+8
src/icons/gen.js
···
+
export default function Yaoi ({bg, fg, width, height}) {
+
return (
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" width={width} height={height}>
+
<ellipse cx="320" cy="320" rx="320" ry="320" fill={bg} />
+
<path d="M320 576C178.6 576 64 461.4 64 320C64 178.6 178.6 64 320 64C461.4 64 576 178.6 576 320C576 461.4 461.4 576 320 576zM320 112C205.1 112 112 205.1 112 320C112 434.9 205.1 528 320 528C434.9 528 528 434.9 528 320C528 205.1 434.9 112 320 112zM320 416C267 416 224 373 224 320C224 267 267 224 320 224C373 224 416 267 416 320C416 373 373 416 320 416z" fill={fg}/>
+
</svg>
+
)
+
}
+8
src/icons/general.js
···
+
export default function General ({bg, fg, width, height}) {
+
return (
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" width={width} height={height}>
+
<ellipse cx="320" cy="320" rx="320" ry="320" fill={bg} />
+
<path d="M320 160C231.6 160 160 231.6 160 320C160 408.4 231.6 480 320 480C397.4 480 462 425 476.8 352L352 352C334.3 352 320 337.7 320 320C320 302.3 334.3 288 352 288L496 288C521.8 288 545.6 309.4 543.2 338.6C533.7 453.6 437.4 544 320 544C196.3 544 96 443.7 96 320C96 196.3 196.3 96 320 96C377.4 96 429.7 117.6 469.3 153C482.5 164.8 483.6 185 471.8 198.2C460 211.4 439.8 212.5 426.6 200.7C398.3 175.4 361 160 320 160z" fill={fg} />
+
</svg>
+
)
+
}
+8
src/icons/het.js
···
+
export default function Het ({bg, fg, width, height}) {
+
return (
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" width={width} height={height}>
+
<ellipse cx="320" cy="320" rx="320" ry="320" fill={bg} />
+
<path d="M480 0C462.3 0 448 14.3 448 32C448 49.7 462.3 64 480 64L530.7 64L474 120.7C447.7 105 416.9 96 384 96C348.6 96 315.6 106.5 288 124.5C260.4 106.5 227.4 96 192 96C94.8 96 16 174.8 16 272C16 358.3 78.1 430.1 160 445.1L160 480L128 480C110.3 480 96 494.3 96 512C96 529.7 110.3 544 128 544L160 544L160 576C160 593.7 174.3 608 192 608C209.7 608 224 593.7 224 576L224 544L256 544C273.7 544 288 529.7 288 512C288 494.3 273.7 480 256 480L224 480L224 445.1C247.3 440.8 268.9 432 288 419.5C315.6 437.5 348.6 448 384 448C481.2 448 560 369.2 560 272C560 230.9 545.9 193 522.2 163L576 109.3L576 160C576 177.7 590.3 192 608 192C625.7 192 640 177.7 640 160L640 32C640 14.3 625.7 0 608 0L480 0zM336 373.2C356.2 344.6 368 309.7 368 272C368 234.3 356.2 199.4 336 170.8C350.6 163.9 366.8 160 384 160C445.9 160 496 210.1 496 272C496 333.9 445.9 384 384 384C366.8 384 350.5 380.1 336 373.2zM288 214.3C298.2 231.2 304 250.9 304 272C304 293.1 298.2 312.9 288 329.7C277.8 312.8 272 293.1 272 272C272 250.9 277.8 231.1 288 214.3zM240 170.8C219.8 199.4 208 234.3 208 272C208 309.7 219.8 344.6 240 373.2C225.5 380.1 209.2 384 192 384C130.1 384 80 333.9 80 272C80 210.1 130.1 160 192 160C209.2 160 225.5 163.9 240 170.8z" fill={fg} />
+
</svg>
+
)
+
}
+8
src/icons/lock.js
···
+
export default function Lock ({bg, fg, width, height}) {
+
return (
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" width={width} height={height}>
+
<ellipse cx="320" cy="320" rx="320" ry="320" fill={bg} />
+
<path d="M256 160L256 224L384 224L384 160C384 124.7 355.3 96 320 96C284.7 96 256 124.7 256 160zM192 224L192 160C192 89.3 249.3 32 320 32C390.7 32 448 89.3 448 160L448 224C483.3 224 512 252.7 512 288L512 512C512 547.3 483.3 576 448 576L192 576C156.7 576 128 547.3 128 512L128 288C128 252.7 156.7 224 192 224z" fill={fg} />
+
</svg>
+
)
+
}
+8
src/icons/mature.js
···
+
export default function Mature ({bg, fg, width, height}) {
+
return (
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" width={width} height={height}>
+
<ellipse cx="320" cy="320" rx="320" ry="320" fill={bg} />
+
<path d="M118.7 97.4C132.2 93.3 146.8 98.5 154.6 110.3L320 358.3L485.4 110.3C493.2 98.6 507.8 93.3 521.3 97.4C534.8 101.5 544 113.9 544 128L544 512C544 529.7 529.7 544 512 544C494.3 544 480 529.7 480 512L480 233.7L346.6 433.8C340.7 442.7 330.7 448 320 448C309.3 448 299.3 442.7 293.4 433.8L160 233.7L160 512C160 529.7 145.7 544 128 544C110.3 544 96 529.7 96 512L96 128C96 113.9 105.2 101.5 118.7 97.4z" fill={fg} />
+
</svg>
+
)
+
}
+8
src/icons/multi.js
···
+
export default function MultiShip ({bg, fg, width, height}) {
+
return (
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" width={width} height={height}>
+
<ellipse cx="320" cy="320" rx="320" ry="320" fill={bg} />
+
<path d="M320 576C461.4 576 576 461.4 576 320C576 178.6 461.4 64 320 64C178.6 64 64 178.6 64 320C64 461.4 178.6 576 320 576zM296 408L296 344L232 344C218.7 344 208 333.3 208 320C208 306.7 218.7 296 232 296L296 296L296 232C296 218.7 306.7 208 320 208C333.3 208 344 218.7 344 232L344 296L408 296C421.3 296 432 306.7 432 320C432 333.3 421.3 344 408 344L344 344L344 408C344 421.3 333.3 432 320 432C306.7 432 296 421.3 296 408z" fill={fg} />
+
</svg>
+
)
+
}
+8
src/icons/notrated.js
···
+
export default function NotRated ({bg, fg, width, height}) {
+
return (
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" width={width} height={height}>
+
<ellipse cx="320" cy="320" rx="320" ry="320" fill={bg} />
+
<path d="M224 224C224 171 267 128 320 128C373 128 416 171 416 224C416 266.7 388.1 302.9 349.5 315.4C321.1 324.6 288 350.7 288 392L288 416C288 433.7 302.3 448 320 448C337.7 448 352 433.7 352 416L352 392C352 390.3 352.6 387.9 355.5 384.7C358.5 381.4 363.4 378.2 369.2 376.3C433.5 355.6 480 295.3 480 224C480 135.6 408.4 64 320 64C231.6 64 160 135.6 160 224C160 241.7 174.3 256 192 256C209.7 256 224 241.7 224 224zM320 576C342.1 576 360 558.1 360 536C360 513.9 342.1 496 320 496C297.9 496 280 513.9 280 536C280 558.1 297.9 576 320 576z" fill={fg}/>
+
</svg>
+
)
+
}
+8
src/icons/nowarnings.js
···
+
export default function NoWarnings ({bg, fg, width, height}) {
+
return (
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" width={width} height={height}>
+
<ellipse cx="320" cy="320" rx="320" ry="320" fill={bg} />
+
<path d="M288 509.3L288 387.5L193.7 464.6C219.8 487.4 252.2 503.3 288 509.3zM153.2 415.1L288 304.8L288 130.7C197.2 145.9 128 224.9 128 320C128 354.6 137.2 387.1 153.2 415.1zM446.3 464.6L352 387.5L352 509.3C387.7 503.3 420.1 487.4 446.3 464.6zM486.9 415.1C502.9 387.1 512.1 354.6 512.1 320C512.1 224.9 442.9 145.9 352.1 130.7L352.1 304.9L486.9 415.2zM64 320C64 178.6 178.6 64 320 64C461.4 64 576 178.6 576 320C576 461.4 461.4 576 320 576C178.6 576 64 461.4 64 320z" fill={fg} />
+
</svg>
+
)
+
}
+8
src/icons/other.js
···
+
export default function OtherShip ({bg, fg, width, height}) {
+
return (
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" width={width} height={height}>
+
<ellipse cx="320" cy="320" rx="320" ry="320" fill={bg} />
+
<path d="M320 608C222.8 608 144 529.2 144 432C144 345.7 206.1 274 288 259L288 211.8L238.3 236.6L235.3 237.9C220.1 243.6 202.8 237.1 195.4 222.2C188 207.4 193.2 189.6 206.9 180.9L209.7 179.3L248.5 159.9L209.7 140.5C193.9 132.6 187.5 113.4 195.4 97.6C202.8 82.8 220.2 76.2 235.4 82L238.4 83.3L288.1 108.1L288.1 63.9C288.1 46.2 302.4 31.9 320.1 31.9C337.8 31.9 352.1 46.2 352.1 63.9L352.1 108.1L401.8 83.3L404.8 82C420 76.2 437.3 82.8 444.7 97.6C452.1 112.4 446.9 130.3 433.2 138.9L430.4 140.5L391.7 159.9L430.4 179.2C446.2 187.1 452.6 206.3 444.7 222.1C437.3 236.9 420 243.5 404.8 237.7L401.8 236.4L352.1 211.6L352.1 258.8C434 273.9 496.1 345.6 496.1 431.8C496.1 529 417.3 607.8 320.1 607.8zM320 544C381.9 544 432 493.9 432 432C432 370.1 381.9 320 320 320C258.1 320 208 370.1 208 432C208 493.9 258.1 544 320 544z" fill={fg} />
+
</svg>
+
)
+
}
+8
src/icons/teen.js
···
+
export default function Teen ({bg, fg, width, height}) {
+
return (
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" width={width} height={height}>
+
<ellipse cx="320" cy="320" rx="320" ry="320" fill={bg} />
+
<path d="M160 96C142.3 96 128 110.3 128 128C128 145.7 142.3 160 160 160L288 160L288 512C288 529.7 302.3 544 320 544C337.7 544 352 529.7 352 512L352 160L480 160C497.7 160 512 145.7 512 128C512 110.3 497.7 96 480 96L160 96z" fill={fg} />
+
</svg>
+
)
+
}
+8
src/icons/warnings.js
···
+
export default function Warnings ({bg, fg, width, height}) {
+
return (
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" width={width} height={height}>
+
<ellipse cx="320" cy="320" rx="320" ry="320" fill={bg} />
+
<path d="M320 64C334.7 64 348.2 72.1 355.2 85L571.2 485C577.9 497.4 577.6 512.4 570.4 524.5C563.2 536.6 550.1 544 536 544L104 544C89.9 544 76.8 536.6 69.6 524.5C62.4 512.4 62.1 497.4 68.8 485L284.8 85C291.8 72.1 305.3 64 320 64zM320 416C302.3 416 288 430.3 288 448C288 465.7 302.3 480 320 480C337.7 480 352 465.7 352 448C352 430.3 337.7 416 320 416zM320 224C301.8 224 287.3 239.5 288.6 257.7L296 361.7C296.9 374.2 307.4 384 319.9 384C332.5 384 342.9 374.3 343.8 361.7L351.2 257.7C352.5 239.5 338.1 224 319.8 224z" fill={fg} />
+
</svg>
+
)
+
}
+8
src/icons/yaoi.js
···
+
export default function Yaoi ({bg, fg, width, height}) {
+
return (
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" width={width} height={height}>
+
<ellipse cx="320" cy="320" rx="320" ry="320" fill={bg} />
+
<path d="M288 96C288 78.3 302.3 64 320 64L448 64C465.7 64 480 78.3 480 96L480 224C480 241.7 465.7 256 448 256C430.3 256 416 241.7 416 224L416 173.3L321 268.3C340.5 296.7 352 331 352 368.1C352 465.3 273.2 544.1 176 544.1C78.8 544.1 0 465.2 0 368C0 270.8 78.8 192 176 192C213 192 247.4 203.4 275.8 223L370.8 128L320.1 128C302.4 128 288.1 113.7 288.1 96zM176 480C237.9 480 288 429.9 288 368C288 306.1 237.9 256 176 256C114.1 256 64 306.1 64 368C64 429.9 114.1 480 176 480zM336 544C329.2 544 322.6 543.6 316 542.9C339.6 524 359.3 500.4 373.6 473.5C416.9 458 448 416.6 448 368C448 342.8 439.7 319.5 425.6 300.8C432.7 302.9 440.2 304 448 304C465 304 480.7 298.7 493.7 289.7C505.4 313.3 512 339.9 512 368C512 465.2 433.2 544 336 544zM528 221.3L528 96C528 84.6 525.6 73.8 521.3 64L608 64C625.7 64 640 78.3 640 96L640 224C640 241.7 625.7 256 608 256C590.3 256 576 241.7 576 224L576 173.3L528 221.3z" fill={fg}/>
+
</svg>
+
)
+
}
+8
src/icons/yuri.js
···
+
export default function Yuri ({bg, fg, width, height}) {
+
return (
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" width={width} height={height}>
+
<ellipse cx="320" cy="320" rx="320" ry="320" fill={bg} />
+
<path d="M192 352C253.9 352 304 301.9 304 240C304 178.1 253.9 128 192 128C130.1 128 80 178.1 80 240C80 301.9 130.1 352 192 352zM368 240C368 326.3 305.9 398.1 223.9 413.1C224 414.1 224 415 224 416L224 480L256 480C273.7 480 288 494.3 288 512C288 529.7 273.7 544 256 544L224 544L224 576C224 593.7 209.7 608 192 608C174.3 608 160 593.7 160 576L160 544L128 544C110.3 544 96 529.7 96 512C96 494.3 110.3 480 128 480L160 480L160 416C160 415 160 414.1 160.1 413.1C78.1 398.1 16 326.3 16 240C16 142.8 94.8 64 192 64C289.2 64 368 142.8 368 240zM357.5 391C371.9 375.2 384.1 357.3 393.6 337.9C409.7 346.9 428.3 352 448.1 352C510 352 560.1 301.9 560.1 240C560.1 178.1 509.9 128 448 128C428.2 128 409.7 133.1 393.5 142.1C384 122.7 371.9 104.8 357.4 89C383.8 73.1 414.8 64 447.9 64C545.1 64 623.9 142.8 623.9 240C623.9 326.3 561.8 398.1 479.8 413.1C479.9 414 479.9 415 479.9 416L479.9 480L511.9 480C529.6 480 543.9 494.3 543.9 512C543.9 529.7 529.6 544 511.9 544L479.9 544L479.9 576C479.9 593.7 465.6 608 447.9 608C430.2 608 415.9 593.7 415.9 576L415.9 544L383.9 544C366.2 544 351.9 529.7 351.9 512C351.9 494.3 366.2 480 383.9 480L415.9 480L415.9 416C415.9 415 415.9 414.1 416 413.1C394.9 409.2 375.1 401.6 357.4 390.9z" fill={fg} />
+
</svg>
+
)
+
}
+7
src/lib/ao3Canonical.js
···
+
const ao3CanonicalUrls = [
+
"https://archiveofourown.org",
+
"https://ao3.org",
+
"https://archive.transformativeworks.org"
+
]
+
+
export default ao3CanonicalUrls
+498
src/lib/baseFonts.js
···
+
const baseFonts = {
+
opensans: {
+
displayName: 'Open Sans',
+
defs: [
+
{
+
path: '/fonts/OpenSans-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/OpenSans-Italic.ttf',
+
style: 'italic',
+
weight: 400
+
},
+
{
+
path: '/fonts/OpenSans-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
},
+
{
+
path: '/fonts/OpenSans-BoldItalic.ttf',
+
style: 'italic',
+
weight: 700
+
}
+
]
+
},
+
bricolagegrotesque: {
+
displayName: 'Bricolage Grotesque',
+
defs: [
+
{
+
path: '/fonts/BricolageGrotesque-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/BricolageGrotesque-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
}
+
]
+
},
+
spacemono: {
+
displayName: 'Space Mono',
+
defs: [
+
{
+
path: '/fonts/SpaceMono-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/SpaceMono-Italic.ttf',
+
style: 'italic',
+
weight: 400
+
},
+
{
+
path: '/fonts/SpaceMono-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
},
+
{
+
path: '/fonts/SpaceMono-BoldItalic.ttf',
+
style: 'italic',
+
weight: 700
+
}
+
]
+
},
+
inconsolata: {
+
displayName: 'Inconsolata',
+
defs: [
+
{
+
path: '/fonts/Inconsolata.otf',
+
style: 'normal'
+
}
+
]
+
},
+
bitter: {
+
displayName: 'Bitter',
+
defs: [
+
{
+
path: '/fonts/Bitter-Regular.otf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/Bitter-Italic.otf',
+
style: 'italic',
+
weight: 400
+
},
+
{
+
path: '/fonts/Bitter-Bold.otf',
+
style: 'normal',
+
weight: 700
+
},
+
{
+
path: '/fonts/Bitter-BoldItalic.otf',
+
style: 'italic',
+
weight: 700
+
}
+
]
+
},
+
archivo: {
+
displayName: 'Archivo',
+
defs: [
+
{
+
path: '/fonts/Archivo-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/Archivo-Italic.ttf',
+
style: 'italic',
+
weight: 400
+
},
+
{
+
path: '/fonts/Archivo-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
},
+
{
+
path: '/fonts/Archivo-BoldItalic.ttf',
+
style: 'italic',
+
weight: 700
+
}
+
]
+
},
+
outfit: {
+
displayName: 'Outfit',
+
defs: [
+
{
+
path: '/fonts/outfit-regular-webfont.woff2',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/outfit-italic-webfont.woff2',
+
style: 'italic',
+
weight: 400
+
}
+
]
+
},
+
notosans: {
+
displayName: 'Noto Sans',
+
defs: [
+
{
+
path: '/fonts/NotoSans-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/NotoSans-Italic.ttf',
+
style: 'italic',
+
weight: 400
+
},
+
{
+
path: '/fonts/NotoSans-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
},
+
{
+
path: '/fonts/NotoSans-BoldItalic.ttf',
+
style: 'italic',
+
weight: 700
+
}
+
]
+
},
+
alegreya: {
+
displayName: 'Alegreya',
+
defs: [
+
{
+
path: '/fonts/Alegreya-Regular.otf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/Alegreya-Italic.otf',
+
style: 'italic',
+
weight: 400
+
},
+
{
+
path: '/fonts/Alegreya-Bold.otf',
+
style: 'normal',
+
weight: 700
+
},
+
{
+
path: '/fonts/Alegreya-BoldItalic.otf',
+
style: 'italic',
+
weight: 700
+
}
+
]
+
},
+
alegreyasans: {
+
displayName: 'Alegreya Sans',
+
defs: [
+
{
+
path: '/fonts/AlegreyaSans-Regular.otf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/AlegreyaSans-Italic.otf',
+
style: 'italic',
+
weight: 400
+
},
+
{
+
path: '/fonts/AlegreyaSans-Bold.otf',
+
style: 'normal',
+
weight: 700
+
},
+
{
+
path: '/fonts/AlegreyaSans-BoldItalic.otf',
+
style: 'italic',
+
weight: 700
+
}
+
]
+
},
+
stacksanstext: {
+
displayName: 'Stack Sans Text',
+
defs: [
+
{
+
path: '/fonts/StackSansText-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/StackSansText-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
}
+
],
+
},
+
momotrustsans: {
+
displayName: 'Momo Trust Sans',
+
defs: [
+
{
+
path: '/fonts/MomoTrustSans-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/MomoTrustSans-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
}
+
]
+
},
+
montserrat: {
+
displayName: 'Montserrat',
+
defs: [
+
{
+
path: '/fonts/Montserrat-Regular.otf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/Montserrat-Italic.otf',
+
style: 'italic',
+
weight: 400
+
},
+
{
+
path: '/fonts/Montserrat-Bold.otf',
+
style: 'normal',
+
weight: 700
+
},
+
{
+
path: '/fonts/Montserrat-BoldItalic.otf',
+
style: 'italic',
+
weight: 700
+
}
+
]
+
},
+
robotoslab: {
+
displayName: 'Roboto Slab',
+
defs: [
+
{
+
path: '/fonts/RobotoSlab-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/RobotoSlab-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
}
+
]
+
},
+
quicksand: {
+
displayName: 'Quicksand',
+
defs: [
+
{
+
path: '/fonts/Quicksand-Regular.otf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/Quicksand-Italic.otf',
+
style: 'italic',
+
weight: 400
+
},
+
{
+
path: '/fonts/Quicksand-Bold.otf',
+
style: 'normal',
+
weight: 700
+
},
+
{
+
path: '/fonts/Quicksand-BoldItalic.otf',
+
style: 'italic',
+
weight: 700
+
}
+
]
+
},
+
worksans: {
+
displayName: 'Work Sans',
+
defs: [
+
{
+
path: '/fonts/WorkSans-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/WorkSans-Italic.ttf',
+
style: 'italic',
+
weight: 400
+
},
+
{
+
path: '/fonts/WorkSans-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
},
+
{
+
path: '/fonts/WorkSans-BoldItalic.ttf',
+
style: 'italic',
+
weight: 700
+
}
+
]
+
},
+
notosans: {
+
displayName: 'Noto Sans',
+
defs: [
+
{
+
path: '/fonts/NotoSans-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/NotoSans-Italic.ttf',
+
style: 'italic',
+
weight: 400
+
},
+
{
+
path: '/fonts/NotoSans-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
},
+
{
+
path: '/fonts/NotoSans-BoldItalic.ttf',
+
style: 'italic',
+
weight: 700
+
}
+
]
+
},
+
notoserif: {
+
displayName: 'Noto Serif',
+
defs: [
+
{
+
path: '/fonts/NotoSerif-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/NotoSerif-Italic.ttf',
+
style: 'italic',
+
weight: 400
+
},
+
{
+
path: '/fonts/NotoSerif-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
},
+
{
+
path: '/fonts/NotoSerif-BoldItalic.ttf',
+
style: 'italic',
+
weight: 700
+
}
+
]
+
},
+
librebaskerville: {
+
displayName: 'Libre Baskerville',
+
defs: [
+
{
+
path: '/fonts/LibreBaskerville-Regular.otf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/LibreBaskerville-Italic.otf',
+
style: 'italic',
+
weight: 400
+
},
+
{
+
path: '/fonts/LibreBaskerville-Bold.otf',
+
style: 'normal',
+
weight: 700
+
}
+
]
+
},
+
ubuntu: {
+
displayName: 'Ubuntu',
+
defs: [
+
{
+
path: '/fonts/Ubuntu-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/Ubuntu-Italic.ttf',
+
style: 'italic',
+
weight: 400
+
},
+
{
+
path: '/fonts/Ubuntu-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
},
+
{
+
path: '/fonts/Ubuntu-BoldItalic.ttf',
+
style: 'italic',
+
weight: 700
+
}
+
]
+
},
+
parkinsans: {
+
displayName: 'Parkinsans',
+
defs: [
+
{
+
path: '/fonts/Parkinsans-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/Parkinsans-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
}
+
]
+
},
+
lora: {
+
displayName: 'Lora',
+
defs: [
+
{
+
path: '/fonts/Lora-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/Lora-Italic.ttf',
+
style: 'italic',
+
weight: 400
+
},
+
{
+
path: '/fonts/Lora-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
},
+
{
+
path: '/fonts/Lora-BoldItalic.ttf',
+
style: 'italic',
+
weight: 700
+
}
+
]
+
},
+
josefinsans: {
+
displayName: 'Josefin Sans',
+
defs: [
+
{
+
path: '/fonts/JosefinSans-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/JosefinSans-Italic.ttf',
+
style: 'italic',
+
weight: 400
+
},
+
{
+
path: '/fonts/JosefinSans-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
},
+
{
+
path: '/fonts/JosefinSans-BoldItalic.ttf',
+
style: 'italic',
+
weight: 700
+
}
+
]
+
}
+
}
+
+
export default baseFonts
+22
src/lib/ogdefaults.js
···
+
const defaults = {
+
theme: 'ao3',
+
baseFont: 'bricolagegrotesque',
+
titleFont: 'stacksansnotch',
+
category: true,
+
rating: true,
+
warnings: false,
+
charTags: false,
+
relTags: false,
+
freeTags: false,
+
summary: true,
+
wordcount: true,
+
chapters: true,
+
postedAt: true,
+
updatedAt: false,
+
uppercaseTitle: false,
+
uppercaseChapterName: false,
+
summaryType: 'basic',
+
customSummary: ''
+
}
+
+
export default defaults
+236
src/lib/ogimage.js
···
+
import { ImageResponse } from "next/og"
+
import General from "@/icons/general.js"
+
import Teen from "@/icons/teen.js"
+
import Mature from "@/icons/mature.js"
+
import Explicit from "@/icons/explicit.js"
+
import NotRated from "@/icons/notrated.js"
+
import Gen from "@/icons/gen.js"
+
import Yaoi from "@/icons/yaoi.js"
+
import Yuri from "@/icons/yuri.js"
+
import Het from "@/icons/het.js"
+
import OtherShip from "@/icons/other.js"
+
import MultiShip from "@/icons/multi.js"
+
import NoWarnings from "@/icons/nowarnings.js"
+
import Warnings from "@/icons/warnings.js"
+
import ChoseNotToWarn from "@/icons/chosenottowarn.js"
+
import { checkItem, checkToggle } from '@/lib/propUtils.js'
+
+
export default async function OGImage ({ theme, baseFont, titleFont, image, addr, opts }) {
+
console.log(image)
+
return new ImageResponse(
+
(
+
<div
+
style={{
+
display: "flex",
+
flexDirection: "column",
+
color: theme.color,
+
backgroundColor: theme.background,
+
fontFamily: baseFont,
+
fontSize: 24,
+
padding: 20,
+
width: "100%",
+
height: "100%",
+
}}
+
>
+
<div
+
style={{
+
display: "flex",
+
flexDirection: "column",
+
marginBottom: 20
+
}}
+
>
+
<div
+
style={{
+
textTransform: "uppercase",
+
display: "flex",
+
justifyContent: "center",
+
color: theme.accent,
+
alignItems: "center",
+
display: "flex",
+
justifyContent: "center",
+
alignItems: "center",
+
textAlign: "center"
+
}}
+
>
+
{image.topLine}
+
</div>
+
<div
+
style={{
+
display: "flex",
+
justifyContent: "center",
+
alignItems: "center",
+
gap: 10
+
}}
+
>
+
{checkToggle('rating', image.props) && image.rating === 'E' && (<Explicit fg={theme.accentColor} bg={theme.accent} width={28} height={28} />)}
+
{checkToggle('rating', image.props) && image.rating === 'M' && (<Mature fg={theme.accentColor} bg={theme.accent} width={28} height={28} />)}
+
{checkToggle('rating', image.props) && image.rating === 'T' && (<Teen fg={theme.accentColor} bg={theme.accent} width={28} height={28} />)}
+
{checkToggle('rating', image.props) && image.rating === 'G' && (<General fg={theme.accentColor} bg={theme.accent} width={28} height={28} />)}
+
{checkToggle('rating', image.props) && image.rating === 'NR' && (<NotRated fg={theme.accentColor} bg={theme.accent} width={28} height={28} />)}
+
+
{checkToggle('warnings', image.props) && image.warning === 'NW' && (<NoWarnings fg={theme.accent2Color} bg={theme.accent2} width={28} height={28} />)}
+
{checkToggle('warnings', image.props) && image.warning === 'CNTW' && (<ChoseNotToWarn fg={theme.accent2Color} bg={theme.accent2} width={28} height={28} />)}
+
{checkToggle('warnings', image.props) && image.warning === 'W' && (<Warnings fg={theme.accent2Color} bg={theme.accent2} width={28} height={28} />)}
+
+
{checkToggle('category', image.props) && image.category === 'F' && (<Yuri fg={theme.accent3Color} bg={theme.accent3} width={28} height={28} />)}
+
{checkToggle('category', image.props)&& image.category === 'M' && (<Yaoi fg={theme.accent3Color} bg={theme.accent3} width={28} height={28} />)}
+
{checkToggle('category', image.props) && image.category === 'FM' && (<Het fg={theme.accent3Color} bg={theme.accent3} width={28} height={28} />)}
+
{checkToggle('category', image.props) && image.category === 'G' && (<Gen fg={theme.accent3Color} bg={theme.accent3} width={28} height={28} />)}
+
{checkToggle('category', image.props) && image.category === 'MX' && (<MultiShip fg={theme.accent3Color} bg={theme.accent3} width={28} height={28} />)}
+
{checkToggle('category', image.props) && image.category === 'O' && (<OtherShip fg={theme.accent3Color} bg={theme.accent3} width={28} height={28} />)}
+
</div>
+
<div
+
style={{
+
fontSize: 54,
+
justifyContent: "center",
+
textAlign: "center",
+
fontFamily: titleFont,
+
fontWeight: "bold",
+
color: theme.color,
+
textTransform: (image.props.uppercaseTitle ? 'uppercase' : 'none')
+
}}
+
>
+
{image.titleLine}
+
</div>
+
<div
+
style={{
+
fontSize: 42,
+
display: "flex",
+
justifyContent: "center",
+
fontFamily: titleFont,
+
color: theme.color
+
}}
+
>
+
{`by ${image.authorLine}`}
+
</div>
+
{image.chapterLine !== '' && (<div
+
style={{
+
fontStyle: "italic",
+
fontSize: 36,
+
fontFamily: titleFont,
+
display: "flex",
+
justifyContent: "center",
+
color: theme.color,
+
textTransform: (image.props.uppercaseChapterName ? 'uppercase' : 'none')
+
}}
+
>
+
{image.chapterLine}
+
</div>)}
+
</div>
+
<div
+
style={{
+
backgroundColor: theme.descBackground,
+
padding: 20,
+
display: "flex",
+
flexDirection: "column",
+
flexGrow: 1,
+
color: theme.descColor,
+
alignItems: "flex-end"
+
}}
+
>
+
{checkToggle('charTags', image.props) && (<div
+
style={{
+
display: "flex",
+
flexWrap: "wrap",
+
gap: 5,
+
fontSize: 18,
+
width: "100%",
+
marginBottom: 5
+
}}
+
>
+
{image.charTags.map(c => (
+
<span
+
style={{
+
backgroundColor: theme.accent2,
+
color: theme.accent2Color,
+
padding: "3px 5px",
+
borderRadius: 5
+
}}
+
>
+
{c}
+
</span>
+
))}
+
</div>)}
+
{checkToggle('relTags', image.props) && (<div
+
style={{
+
display: "flex",
+
flexWrap: "wrap",
+
gap: 5,
+
fontSize: 18,
+
width: "100%",
+
marginBottom: 5
+
}}
+
>
+
{image.relTags.map(r => (
+
<span
+
style={{
+
backgroundColor: theme.accent3,
+
color: theme.accent3Color,
+
padding: "3px 5px",
+
borderRadius: 5
+
}}
+
>
+
{r}
+
</span>
+
))}
+
</div>)}
+
{checkToggle('freeTags', image.props) && (<div
+
style={{
+
display: "flex",
+
flexWrap: "wrap",
+
gap: 5,
+
fontSize: 18,
+
width: "100%",
+
marginBottom: 5
+
}}
+
>
+
{image.freeTags.map(f => (
+
<span
+
style={{
+
backgroundColor: theme.accent4,
+
color: theme.accent4Color,
+
padding: "3px 5px",
+
borderRadius: 5
+
}}
+
>
+
{f}
+
</span>
+
))}
+
</div>)}
+
{checkToggle('summary', image.props) && (<div
+
style={{
+
display: "flex",
+
flexDirection: "column",
+
flexGrow: 1,
+
width: '100%'
+
}}
+
>
+
{image.summary.map(l => (
+
<div
+
style={{
+
width: "100%",
+
marginBottom: 10
+
}}
+
>
+
{l}
+
</div>
+
))}
+
</div>)}
+
<div
+
style={{
+
textAlign: "right",
+
fontSize: 18,
+
display: "flex",
+
justifyContent: "flex-end",
+
alignItems: "center",
+
color: theme.accent2
+
}}
+
>
+
{checkToggle('wordcount', image.props) && `${image.words} words โ€ข `}{(checkToggle('chapters', image.props) && image.chapterCount !== null) && `${image.chapterCount} chapters โ€ข `}{checkToggle('postedAt', image.props) && `posted on ${image.postedAt} โ€ข `}{checkToggle('updatedAt', image.props) && `updated on ${image.updatedAt} โ€ข `}{addr}
+
</div>
+
</div>
+
</div>
+
),
+
opts
+
)
+
}
+27
src/lib/ogimagelocked.js
···
+
import { ImageResponse } from "next/og"
+
import themes from "@/lib/themes.js"
+
import Lock from "@/icons/lock.js"
+
+
export default async function OGImageLocked ({ theme }) {
+
const themeData = themes[theme]
+
return new ImageResponse(
+
(
+
<div
+
style={{
+
display: "flex",
+
flexDirection: "column",
+
justifyContent: "center",
+
alignItems: "center",
+
color: themeData.color,
+
backgroundColor: themeData.background,
+
fontSize: 24,
+
padding: 20,
+
width: "100%",
+
height: "100%",
+
}}
+
>
+
<Lock bg={themeData.background} fg={themeData.color} width={480} height={480} />
+
</div>
+
)
+
)
+
}
+7
src/lib/propUtils.js
···
+
export function checkItem (key, props) {
+
return props.has(key) ? props.get(key) !== '' : false
+
}
+
+
export function checkToggle (key, props) {
+
return props.has(key) ? props.get(key) === 'true' : false
+
}
+192
src/lib/sanitizeData.js
···
+
import DOM from "fauxdom"
+
import { readFile } from 'node:fs/promises'
+
import querystring from 'node:querystring'
+
import { join } from 'node:path'
+
import themes from '@/lib/themes.js'
+
import baseFonts from '@/lib/baseFonts.js'
+
import titleFonts from '@/lib/titleFonts.js'
+
import { checkItem, checkToggle } from '@/lib/propUtils.js'
+
+
const getWork = async (workId, archive = null) => {
+
const domainParam = (archive && archive !== process.env.ARCHIVE) ? `?archive=${archive}` : ''
+
const data = await fetch(`http://${process.env.DOMAIN}/api/works/${workId}${domainParam}`)
+
const work = await data.json()
+
return work
+
}
+
+
const getHighestRating = async (works, archive = null) => {
+
const ratings = await Promise.all(works.map(async (w) => {
+
const work = await getWork(w.id, archive)
+
return work.rating
+
}))
+
if (ratings.includes("Not Rated")) {
+
return "NR"
+
} else if (ratings.includes("Explicit")) {
+
return "E"
+
} else if (ratings.includes("Mature")) {
+
return "M"
+
} else if (ratings.includes("Teen")) {
+
return "T"
+
}
+
return "G"
+
}
+
+
const getHighestWarning = async (works, archive = null) => {
+
const warnings = await Promise.all(works.map(async (w) => {
+
const work = await getWork(w.id, archive)
+
return work.tags.warnings
+
}))
+
const warningsUnique = warnings.reduce((a, b) => { return a.concat(b) }).filter((w, i) => { return i === warnings.indexOf(w) })
+
if (warningsUnique.length === 1 && warningsUnique[0] === "Creator Chose Not To Use Archive Warnings") {
+
return "CNTW"
+
} else if (warningsUnique.length === 1 && warningsUnique[0] === "No Archive Warnings Apply") {
+
return "NW"
+
}
+
return "W"
+
}
+
+
const getCategory = async (works, archive = null) => {
+
const categories = await Promise.all(works.map(async (w) => {
+
const work = await getWork(w.id, archive)
+
return work.category
+
}))
+
const categoriesJoined = categories.reduce((a, b) => { return a.concat(b) })
+
const categoriesUnique = categoriesJoined.filter((w, i) => { return i === categoriesJoined.indexOf(w) })
+
+
if (categoriesUnique.length === 1) {
+
if (categoriesUnique[0] === "F/F") return "F"
+
if (categoriesUnique[0] === "M/M") return "M"
+
if (categoriesUnique[0] === "F/M") return "FM"
+
if (categoriesUnique[0] === "Gen") return "G"
+
if (categoriesUnique[0] === "Multi") return "MX"
+
if (categoriesUnique[0] === "Other") return "O"
+
}
+
return "MX"
+
}
+
+
const sanitizeProps = (props) => {
+
let propsParsed = {}
+
Object.keys(props).forEach((pr) => {
+
if (props[pr] === 'true') {
+
propsParsed[pr] = true
+
return
+
} else if (props[pr] === 'false') {
+
propsParsed[pr] = false
+
return
+
} else if (typeof parseInt(props[pr]) === 'Number') {
+
propsParsed[pr] = parseInt(props[pr])
+
return
+
}
+
propsParsed[pr] = props[pr]
+
})
+
return propsParsed
+
}
+
+
export default async function sanitizeData ({ type, data, props}) {
+
const archive = props && checkItem('archive', props) ? props.get('archive') : process.env.ARCHIVE
+
console.log(props)
+
const baseFont = checkItem('baseFont', props) ? props.get('baseFont') : process.env.DEFAULT_BASE_FONT
+
const baseFontData = baseFonts[baseFont]
+
const titleFont = checkItem('titleFont', props) ? props.get('titleFont') : process.env.DEFAULT_TITLE_FONT
+
const titleFontData = titleFonts[titleFont]
+
const archClean = checkItem('archive', props) ? props.get('archive').replace("https://", '').replace('/', '') : null
+
const theme = checkItem('theme', props) ? props.get('theme') : (checkItem('archive', props) && !["ao3.org", "archiveofourown.org", "archive.transformativeworks.org"].includes(archClean) && Object.values(siteMap).includes(archClean) ? Object.keys(siteMap)[Object.values(siteMap).indexOf(archClean)] : process.env.DEFAULT_THEME)
+
const themeData = themes[theme]
+
const parentWork = type === 'work' && data.chapterInfo ? await getWork(data.id, archive) : null
+
const bfs = await Promise.all(baseFontData.defs.map(async (bf) => {
+
return {
+
name: baseFontData.displayName,
+
data: await readFile(
+
join(process.cwd(), bf.path)
+
),
+
style: bf.style,
+
weight: bf.weight
+
}
+
})).then(x => x)
+
const tfs = await Promise.all(titleFontData.defs.map(async (tf) => {
+
return {
+
name: titleFontData.displayName,
+
data: await readFile(
+
join(process.cwd(), tf.path)
+
),
+
style: tf.style,
+
weight: tf.weight
+
}
+
})).then(x => x)
+
const authorsFormatted = data.authors
+
? data.authors.map((a) => {
+
if (a.anonymous) return "Anonymous"
+
if (a.pseud !== a.username) return `${a.pseud} (${a.username})`
+
return a.username
+
})
+
: []
+
const rating = type === 'work' ? data.rating : await getHighestRating(data.works, archive)
+
const warning = type === 'work' ? await getHighestWarning([data], archive) : await getHighestWarning(data.works, archive)
+
const category = type === 'work' ? await getCategory([data], archive) : await getCategory(data.works, archive)
+
const authorString = (authorsFormatted.length > 1
+
? authorsFormatted.slice(0, -1).join(", ") + " & " +
+
authorsFormatted.slice(-1)[0]
+
: authorsFormatted[0])
+
const summaryContent = type === 'work'
+
? (props.get('summaryType') === 'chapter' && data.chapterInfo && data.chapterInfo.summary ? data.chapterInfo.summary : (props.get('summaryType') === 'custom' && props.get('customSummary') !== '' ? props.get('customSummary') : (data.summary ? data.summary : (parentWork ? parentWork.summary : ''))))
+
: (props.get('summaryType') === 'custom' && props.get('customSummary') !== '' ? props.get('customSummary') : data.notes)
+
const formatter = new Intl.NumberFormat('en-US')
+
const words = formatter.format(data.words)
+
const summaryDOM = new DOM(summaryContent, {decodeEntities: true})
+
const summaryFormatted = summaryDOM.innerHTML.replace(/\<br(?: \/)?\>/g, "\n").replace(
+
/(<([^>]+)>)/ig,
+
"",
+
).split("\n")
+
const titleString = type === 'work' ? data.title : data.name
+
const chapterString = data.chapterInfo ? (data.chapterInfo.name
+
? data.chapterInfo.name
+
: "Chapter " + data.chapterInfo.index) : null
+
const chapterCountString = type === 'work' ? (data.chapters
+
? data.chapters.published+'/'+(
+
data.chapters.total
+
? data.chapters.total
+
: '?'
+
)
+
: '') : null
+
const fandomString = type === 'work' ? (
+
data.fandoms.length > 1
+
? (
+
data.fandoms.length <= 2
+
? data.fandoms.slice(0, -1).join(", ")+" & "+data.fandoms.slice(-1)
+
: data.fandoms.join(", ")+" (+"+(data.fandoms.length - 2)+")"
+
)
+
: data.fandoms[0]
+
) : (
+
''
+
)
+
const charTags = type === 'work' ? data.tags.characters : data.works.map(w => w.tags.characters).reduce((a, b) => { return b ? (a ? a.concat(b) : []) : (a ? a : []) }).filter((w, i) => { return i === data.works.indexOf(w) })
+
const relTags = type === 'work' ? data.tags.relationships : data.works.map(w => w.tags.relationships).reduce((a, b) => { return b ? (a ? a.concat(b) : []) : (a ? a : []) }).filter((w, i) => { return i === data.works.indexOf(w) })
+
const freeTags = type === 'work' ? data.tags.additional : data.works.map(w => w.tags.additional).reduce((a, b) => { return b ? (a ? a.concat(b) : []) : (a ? a : []) }).filter((w, i) => { return i === data.works.indexOf(w) })
+
const warnings = type === 'work' ? data.tags.warnings : data.works.map(w => w.tags.warnings).reduce((a, b) => { return b ? (a ? a.concat(b) : []) : (a ? a : []) }).filter((w, i) => { return i === data.works.indexOf(w) })
+
+
const ret = {
+
topLine: fandomString,
+
titleLine: titleString,
+
authorLine: authorString,
+
chapterLine: chapterString,
+
chapterCount: chapterCountString,
+
words: words,
+
rating: rating,
+
warning: warning,
+
category: category,
+
summary: summaryFormatted,
+
theme: themeData,
+
charTags: charTags,
+
relTags: relTags,
+
freeTags: freeTags,
+
postedAt: type === 'work' ? data.publishedAt : data.startedAt,
+
updatedAt: data.updatedAt,
+
baseFont: baseFont,
+
titleFont: titleFont,
+
props: props,
+
opts: {
+
fonts: bfs.concat(tfs)
+
}
+
}
+
return ret;
+
}
+9
src/lib/siteMap.js
···
+
const siteMap = {
+
superlove: 'superlove.sayitditto.net',
+
sunset: 'sunset.femslash.club',
+
squidgeworld: 'squidgeworld.org',
+
cfaarchive: 'cfaarchive.org',
+
adastra: 'adastrafanfic.com'
+
}
+
+
export default siteMap
+934
src/lib/themes.js
···
+
const themes = {
+
ao3: {
+
name: 'AO3',
+
background: '#990000',
+
color: '#FFFFFF',
+
descBackground: '#FFFFFF',
+
descColor: '#000000',
+
accent: '#FFFFFF',
+
accentColor: '#990000',
+
accent2: '#CC9999',
+
accent2Color: '#990000',
+
accent3: '#EEBBBB',
+
accent3Color: '#990000',
+
accent4: '#FFDDDD',
+
accent4Color: '#990000'
+
},
+
softEra: {
+
name: 'Soft Era',
+
background: '#F9F5F5',
+
color: '#C8B3B3',
+
descBackground: '#F9F5F5',
+
descColor: '#414141',
+
accent: '#DB90A7',
+
accentColor: '#F9F5F5',
+
accent2: '#EEAABE',
+
accent2Color: '#F9F5F5',
+
accent3: '#82B4E3',
+
accent3Color: '#F9F5F5',
+
accent4: '#a29acb',
+
accent4Color: '#F9F5F5',
+
},
+
wildCherry: {
+
name: 'Wild Cherry',
+
background: '#2B1F32',
+
color: '#FFFFFF',
+
descBackground: '#FFFFFF',
+
descColor: '#2B1F32',
+
accent: '#E15D97',
+
accentColor: '#FFFFFF',
+
accent2: '#0AACC5',
+
accent2Color: '#FFFFFF',
+
accent3: '#FFB86C',
+
accent3Color: '#2B1F32',
+
accent4: '#35BA66',
+
accent4Color: '#FFFFFF',
+
},
+
rosePine: {
+
name: 'Rosรฉ Pine',
+
background: '#191724',
+
color: '#e0def4',
+
descBackground: '#1f1d2e',
+
descColor: '#e0def4',
+
accent: '#eb6f92',
+
accentColor: '#191724',
+
accent2: '#9ccfd8',
+
accent2Color: '#191724',
+
accent3: '#f6c177',
+
accent3Color: '#191724',
+
accent4: '#c4a7e7',
+
accent4Color: '#191724',
+
},
+
rosePineDawn: {
+
name: 'Rosรฉ Pine Dawn',
+
background: '#faf4ed',
+
color: '#575279',
+
descBackground: '#fffaf3',
+
descColor: '#575279',
+
accent: '#eb6f92',
+
accentColor: '#faf4ed',
+
accent2: '#286983',
+
accent2Color: '#faf4ed',
+
accent3: '#ea9d34',
+
accent3Color: '#faf4ed',
+
accent4: '#907aa9',
+
accent4Color: '#faf4ed',
+
},
+
rosePineMoon: {
+
name: 'Rosรฉ Pine Moon',
+
background: '#232136',
+
color: '#e0def4',
+
descBackground: '#2a273f',
+
descColor: '#e0def4',
+
accent: '#b4637a',
+
accentColor: '#232136',
+
accent2: '#9ccfd8',
+
accent2Color: '#232136',
+
accent3: '#f6c177',
+
accent3Color: '#232136',
+
accent4: '#c4a7e7',
+
accent4Color: '#232136',
+
},
+
solarizedLight: {
+
name: 'Solarized Light',
+
background: '#fdf6e3',
+
color: '#b58900',
+
descBackground: '#eee8d5',
+
descColor: '#002b36',
+
accent: '#d33682',
+
accentColor: '#fdf6e3',
+
accent2: '#2aa198',
+
accent2Color: '#fdf6e3',
+
accent3: '#859900',
+
accent3Color: '#fdf6e3',
+
accent4: '#6c71c4',
+
accent4Color: '#fdf6e3',
+
},
+
solarizedDark: {
+
name: 'Solarized Dark',
+
background: '#002b36',
+
color: '#b58900',
+
descBackground: '#073642',
+
descColor: '#fdf6e3',
+
accent: '#d33682',
+
accentColor: '#002b36',
+
accent2: '#2aa198',
+
accent2Color: '#002b36',
+
accent3: '#859900',
+
accent3Color: '#002b36',
+
accent4: '#6c71c4',
+
accent4Color: '#002b36',
+
},
+
squidgeworld: {
+
name: 'Squidgeworld',
+
background: '#b8860b',
+
color: '#f5f5dc',
+
descBackground: '#f5f5dc',
+
descColor: '#2a2a2a',
+
accent: '#FFC825',
+
accentColor: '#f5f5dc',
+
accent2: '#818D4C',
+
accent2Color: '#f5f5dc',
+
accent3: '#6D7A34',
+
accent3Color: '#f5f5dc',
+
accent4: '#556121',
+
accent4Color: '#f5f5dc',
+
},
+
superlove: {
+
name: 'Superlove',
+
background: '#df6191',
+
color: '#ffffff',
+
descBackground: '#FFFFFF',
+
descColor: '#2a2a2a',
+
accent: '#F4C4C5',
+
accentColor: '#ffffff',
+
accent2: '#a33961',
+
accent2Color: '#ffffff',
+
accent3: '#87254A',
+
accent3Color: '#ffffff',
+
accent4: '#6A1133',
+
accent4Color: '#ffffff',
+
},
+
catppuccinMocha: {
+
name: 'Catppuccin Mocha',
+
background: '#1e1e2e',
+
color: '#cdd6f4',
+
descBackground: '#313244',
+
descColor: '#bac2de',
+
accent: '#f5e0dc',
+
accentColor: '#1e1e2e',
+
accent2: '#cba6f7',
+
accent2Color: '#1e1e2e',
+
accent3: '#fab387',
+
accent3Color: '#1e1e2e',
+
accent4: '#89dceb',
+
accent4Color: '#1e1e2e',
+
},
+
catppuccinLatte: {
+
name: 'Catppuccin Latte',
+
background: '#eff1f5',
+
color: '#4c4f69',
+
descBackground: '#ccd0da',
+
descColor: '#5c5f77',
+
accent: '#dc8a78',
+
accentColor: '#FFFFFF',
+
accent2: '#8839ef',
+
accent2Color: '#eff1f5',
+
accent3: '#fe640b',
+
accent3Color: '#eff1f5',
+
accent4: '#04a5e5',
+
accent4Color: '#eff1f5',
+
},
+
atelierCave: {
+
name: 'Atelier Cave',
+
background: '#19171c',
+
color: '#655F6D',
+
descBackground: '#19171c',
+
descColor: '#E2DFE7',
+
accent: '#BE4678',
+
accentColor: '#E2DFE7',
+
accent2: '#2A9292',
+
accent2Color: '#E2DFE7',
+
accent3: '#AA573C',
+
accent3Color: '#E2DFE7',
+
accent4: '#A06E3B',
+
accent4Color: '#E2DFE7'
+
},
+
atelierDune: {
+
name: 'Atelier Dune',
+
background: '#20201d',
+
color: '#7D7A68',
+
descBackground: '#20201d',
+
descColor: '#7D7A68',
+
accent: '#D73737',
+
accentColor: '#FEFBEC',
+
accent2: '#60AC39',
+
accent2Color: '#FEFBEC',
+
accent3: '#AE9513',
+
accent3Color: '#FEFBEC',
+
accent4: '#6684E1',
+
accent4Color: '#FEFBEC'
+
},
+
atelierEstuary: {
+
name: 'Atelier Estuary',
+
background: '#22221b',
+
color: '#6C6B5A',
+
descBackground: '#22221b',
+
descColor: '#E7E6DF',
+
accent: '#BA6236',
+
accentColor: '#F4F3EC',
+
accent2: '#7D9726',
+
accent2Color: '#F4F3EC',
+
accent3: '#AE7313',
+
accent3Color: '#F4F3EC',
+
accent4: '#36A166',
+
accent4Color: '#F4F3EC'
+
},
+
atelierForest: {
+
name: 'Atelier Forest',
+
background: '#1b1918',
+
color: '#766E6B',
+
descBackground: '#1b1918',
+
descColor: '#F1EFEE',
+
accent: '#F22C40',
+
accentColor: '#F1EFEE',
+
accent2: '#7B9726',
+
accent2Color: '#F1EFEE',
+
accent3: '#DF5320',
+
accent3Color: '#F1EFEE',
+
accent4: '#407EE7',
+
accent4Color: '#F1EFEE'
+
},
+
atelierHeath: {
+
name: 'Atelier Heath',
+
background: '#1b1918',
+
color: '#776977',
+
descBackground: '#1b1918',
+
descColor: '#F7F3F7',
+
accent: '#CA402B',
+
accentColor: '#F7F3F7',
+
accent2: '#918B3B',
+
accent2Color: '#F7F3F7',
+
accent3: '#BB8A35',
+
accent3Color: '#F7F3F7',
+
accent4: '#516AEC',
+
accent4Color: '#F7F3F7'
+
},
+
atelierLakeside: {
+
name: 'Atelier Lakeside',
+
background: '#161b1d',
+
color: '#5A7B8C',
+
descBackground: '#161b1d',
+
descColor: '#EBF8FF',
+
accent: '#D22D72',
+
accentColor: '#EBF8FF',
+
accent2: '#568C3B',
+
accent2Color: '#EBF8FF',
+
accent3: '#935C25',
+
accent3Color: '#EBF8FF',
+
accent4: '#257FAD',
+
accent4Color: '#EBF8FF'
+
},
+
atelierPlateau: {
+
name: 'Atelier Plateau',
+
background: '#1b1818',
+
color: '#655D5D',
+
descBackground: '#1b1818',
+
descColor: '#F4ECEC',
+
accent: '#CA4949',
+
accentColor: '#F4ECEC',
+
accent2: '#4B8B8B',
+
accent2Color: '#F4ECEC',
+
accent3: '#A06E3B',
+
accent3Color: '#F4ECEC',
+
accent4: '#7272CA',
+
accent4Color: '#F4ECEC'
+
},
+
atelierSavanna: {
+
name: 'Atelier Savanna',
+
background: '#171c19',
+
color: '#5F6D64',
+
descBackground: '#1b1818',
+
descColor: '#ECF4EE',
+
accent: '#B16139',
+
accentColor: '#ECF4EE',
+
accent2: '#489963',
+
accent2Color: '#ECF4EE',
+
accent3: '#A07E3B',
+
accent3Color: '#ECF4EE',
+
accent4: '#478C90',
+
accent4Color: '#ECF4EE'
+
},
+
atelierSeaside: {
+
name: 'Atelier Seaside',
+
background: '#131513',
+
color: '#687D68',
+
descBackground: '#131513',
+
descColor: '#CFE8CF',
+
accent: '#E6193C',
+
accentColor: '#CFE8CF',
+
accent2: '#29A329',
+
accent2Color: '#CFE8CF',
+
accent3: '#98981B',
+
accent3Color: '#CFE8CF',
+
accent4: '#3D62F5',
+
accent4Color: '#CFE8CF'
+
},
+
atelierSulphurpool: {
+
name: 'Atelier Sulphurpool',
+
background: '#202746',
+
color: '#6B7394',
+
descBackground: '#202746',
+
descColor: '#DFE2F1',
+
accent: '#C94922',
+
accentColor: '#DFE2F1',
+
accent2: '#AC9739',
+
accent2Color: '#DFE2F1',
+
accent3: '#C08B30',
+
accent3Color: '#DFE2F1',
+
accent4: '#3D8FD1',
+
accent4Color: '#DFE2F1'
+
},
+
ayaka: {
+
name: 'Ayaka',
+
background: '#36283d',
+
color: '#FFFEFE',
+
descBackground: '#36283d',
+
descColor: '#FFFEFE',
+
accent: '#71ADE9',
+
accentColor: '#FFFEFE',
+
accent2: '#AB8CAE',
+
accent2Color: '#FFFEFE',
+
accent3: '#E59DB1',
+
accent3Color: '#FFFEFE',
+
accent4: '#8BB8E9',
+
accent4Color: '#FFFEFE'
+
},
+
ayuMirage: {
+
name: 'Ayu Mirage',
+
background: '#1F2430',
+
color: '#CBCCC6',
+
descBackground: '#1F2430',
+
descColor: '#CBCCC6',
+
accent: '#FF3333',
+
accentColor: '#1F2430',
+
accent2: '#BAE67E',
+
accent2Color: '#1F2430',
+
accent3: '#FFA759',
+
accent3Color: '#1F2430',
+
accent4: '#73D0FF',
+
accent4Color: '#1F2430'
+
},
+
base2ToneCave: {
+
name: 'Base2Tone Cave',
+
background: '#222021',
+
color: '#9f999b',
+
descBackground: '#2f2d2e',
+
descColor: '#ffebf2',
+
accent: '#936c7a',
+
accentColor: '#ffebf2',
+
accent2: '#cca133',
+
accent2Color: '#ffebf2',
+
accent3: '#d27998',
+
accent3Color: '#ffebf2',
+
accent4: '#706b6d',
+
accent4Color: '#ffebf2'
+
},
+
base2ToneDesert: {
+
name: 'Base2Tone Desert',
+
background: '#292724',
+
color: '#ada594',
+
descBackground: '#3d3a34',
+
descColor: '#f2ead9',
+
accent: '#816f4b',
+
accentColor: '#f2ead9',
+
accent2: '#ec9255',
+
accent2Color: '#f2ead9',
+
accent3: '#957e50',
+
accent3Color: '#f2ead9',
+
accent4: '#615c51',
+
accent4Color: '#f2ead9'
+
},
+
base2ToneDrawbridge: {
+
name: 'Base2Tone Drawbridge',
+
background: '#1b1f32',
+
color: '#9094a7',
+
descBackground: '#252a41',
+
descColor: '#9094a7',
+
accent: '#627af4',
+
accentColor: '#e1e6ff',
+
accent2: '#5cbcd6',
+
accent2Color: '#e1e6ff',
+
accent3: '#8b9efd',
+
accent3Color: '#e1e6ff',
+
accent4: '#444b6f',
+
accent4Color: '#e1e6ff'
+
},
+
base2ToneEarth: {
+
name: 'Base2Tone Earth',
+
background: '#322d29',
+
color: '#b5a9a1',
+
descBackground: '#3f3a37',
+
descColor: '#fff3eb',
+
accent: '#e6b84d',
+
accentColor: '#fff3eb',
+
accent2: '#d9b154',
+
accent2Color: '#fff3eb',
+
accent3: '#816d5f',
+
accent3Color: '#fff3eb',
+
accent4: '#b5a9a1',
+
accent4Color: '#fff3eb'
+
},
+
base2ToneEvening: {
+
name: 'Base2Tone Evening',
+
background: '#2a2734',
+
color: '#a4a1b5',
+
descBackground: '#363342',
+
descColor: '#eeebff',
+
accent: '#8a75f5',
+
accentColor: '#eeebff',
+
accent2: '#ffad5c',
+
accent2Color: '#eeebff',
+
accent3: '#afa0fe',
+
accent3Color: '#eeebff',
+
accent4: '#a4a1b5',
+
accent4Color: '#eeebff'
+
},
+
base2ToneField: {
+
name: 'Base2Tone Field',
+
background: '#18201e',
+
color: '#8ea4a0',
+
descBackground: '#242e2c',
+
descColor: '#a8fff1',
+
accent: '#0fbda0',
+
accentColor: '#242e2c',
+
accent2: '#3be381',
+
accent2Color: '#242e2c',
+
accent3: '#40ddc3',
+
accent3Color: '#242e2c',
+
accent4: '#8ea4a0',
+
accent4Color: '#242e2c'
+
},
+
base2ToneForest: {
+
name: 'Base2Tone Forest',
+
background: '#2a2d2a',
+
color: '#a1b5a1',
+
descBackground: '#353b35',
+
descColor: '#f0fff0',
+
accent: '#5c705c',
+
accentColor: '#f0fff0',
+
accent2: '#b1c44f',
+
accent2Color: '#f0fff0',
+
accent3: '#687d68',
+
accent3Color: '#f0fff0',
+
accent4: '#8fae8f',
+
accent4Color: '#f0fff0'
+
},
+
base2ToneLavender: {
+
name: 'Base2Tone Lavender',
+
background: '#201d2a',
+
color: '#9992b0',
+
descBackground: '#2c2839',
+
descColor: '#efebff',
+
accent: '#9375f5',
+
accentColor: '#efebff',
+
accent2: '#d294ff',
+
accent2Color: '#efebff',
+
accent3: '#b5a0fe',
+
accent3Color: '#efebff',
+
accent4: '#9992b0',
+
accent4Color: '#efebff'
+
},
+
base2ToneMall: {
+
name: 'Base2Tone Mall',
+
background: '#1e1e1f',
+
color: '#97959d',
+
descBackground: '#2b2b2c',
+
descColor: '#f4f0ff',
+
accent: '#a17efc',
+
accentColor: '#f4f0ff',
+
accent2: '#75bfff',
+
accent2Color: '#f4f0ff',
+
accent3: '#c5adff',
+
accent3Color: '#f4f0ff',
+
accent4: '#97959d',
+
accent4Color: '#f4f0ff'
+
},
+
base2ToneMotel: {
+
name: 'Base2Tone Motel',
+
background: '#242323',
+
color: '#a5979a',
+
descBackground: '#373434',
+
descColor: '#f0dbdf',
+
accent: '#956f76',
+
accentColor: '#f0dbdf',
+
accent2: '#f8917c',
+
accent2Color: '#f0dbdf',
+
accent3: '#a7868b',
+
accent3Color: '#f0dbdf',
+
accent4: '#a5979a',
+
accent4Color: '#f0dbdf'
+
},
+
base2TonePool: {
+
name: 'Base2Tone Pool',
+
background: '#2a2433',
+
color: '#9a90a7',
+
descBackground: '#372f42',
+
descColor: '#f3ebff',
+
accent: '#aa75f5',
+
accentColor: '#f3ebff',
+
accent2: '#f87972',
+
accent2Color: '#f3ebff',
+
accent3: '#c7a0fe',
+
accent3Color: '#f3ebff',
+
accent4: '#9a90a7',
+
accent4Color: '#f3ebff'
+
},
+
base2TonePorch: {
+
name: 'Base2Tone Porch',
+
background: '#221e24',
+
color: '#9f95a3',
+
descBackground: '#302a32',
+
descColor: '#f2e3f7',
+
accent: '#9466a3',
+
accentColor: '#f2e3f7',
+
accent2: '#f39b68',
+
accent2Color: '#f2e3f7',
+
accent3: '#a77cb6',
+
accent3Color: '#f2e3f7',
+
accent4: '#9f95a3',
+
accent4Color: '#f2e3f7'
+
},
+
base2ToneSpace: {
+
name: 'Base2Tone Space',
+
background: '#24242e',
+
color: '#a1a1b5',
+
descBackground: '#333342',
+
descColor: '#cecee3',
+
accent: '#7676f4',
+
accentColor: '#ebebff',
+
accent2: '#f37b3f',
+
accent2Color: '#ebebff',
+
accent3: '#fe8c52',
+
accent3Color: '#ebebff',
+
accent4: '#737391',
+
accent4Color: '#ebebff'
+
},
+
base2ToneSuburb: {
+
name: 'Base2Tone Suburb',
+
background: '#1e202f',
+
color: '#4f5472',
+
descBackground: '#292c3d',
+
descColor: '#ebedff',
+
accent: '#7586f5',
+
accentColor: '#ebedff',
+
accent2: '#fe81b5',
+
accent2Color: '#ebedff',
+
accent3: '#fb6fa9',
+
accent3Color: '#ebedff',
+
accent4: '#5b6080',
+
accent4Color: '#ebedff'
+
},
+
blueDolphin: {
+
name: "Blue Dolphin",
+
background: '#006984',
+
color: '#A3F7FF',
+
descBackground: '#006984',
+
descColor: '#FFFFFF',
+
accent: '#FF8288',
+
accentColor: '#292D3E',
+
accent2: '#B4E88D',
+
accent2Color: '#292D3E',
+
accent3: '#F4D69F',
+
accent3Color: '#292D3E',
+
accent4: '#82AAFF',
+
accent4Color: '#292D3E'
+
},
+
borland: {
+
name: "Borland",
+
background: '#0000a4',
+
color: '#FFFFB6',
+
descBackground: '#0000a4',
+
descColor: '#FFFFB6',
+
accent: '#FF6C60',
+
accentColor: '#292D3E',
+
accent2: '#B4E88D',
+
accent2Color: '#292D3E',
+
accent3: '#F4D69F',
+
accent3Color: '#292D3E',
+
accent4: '#82AAFF',
+
accent4Color: '#292D3E'
+
},
+
butrin: {
+
name: 'Butrin',
+
background: '#4b3b3c',
+
color: '#F2F2F2',
+
descBackground: '#4b3b3c',
+
descColor: '#F2F2F2',
+
accent: '#F2B1B1',
+
accentColor: '#4b3b3c',
+
accent2: '#B2D8B2',
+
accent2Color: '#4b3b3c',
+
accent3: '#87CEFA',
+
accent3Color: '#4b3b3c',
+
accent4: '#D8BFD8',
+
accent4Color: '#4b3b3c'
+
},
+
gooey: {
+
name: "Gooey",
+
background: '#000009',
+
color: '#FFFFFF',
+
descBackground: '#1F222D',
+
descColor: '#FFFFFF',
+
accent: '#BB4F6C',
+
accentColor: '#FFFFFF',
+
accent2: '#C65E3D',
+
accent2Color: '#FFFFFF',
+
accent3: '#72CCAE',
+
accent3Color: '#FFFFFF',
+
accent4: '#58B6CA',
+
accent4Color: '#FFFFFF'
+
},
+
gruvbox: {
+
name: 'Gruvbox',
+
background: '#FBF1C7',
+
color: '#4f5472',
+
descBackground: '#FBF1C7',
+
descColor: '#3C3836',
+
accent: '#9D0006',
+
accentColor: '#4F4F4F',
+
accent2: '#A8FF60',
+
accent2Color: '#4F4F4F',
+
accent3: '#96CBFE',
+
accent3Color: '#4F4F4F',
+
accent4: '#FF73FD',
+
accent4Color: '#4F4F4F'
+
},
+
gruvboxDark: {
+
name: 'Gruvbox Dark',
+
background: '#282828',
+
color: '#7C6F64',
+
descBackground: '#282828',
+
descColor: '#ebedff',
+
accent: '#FB4934',
+
accentColor: '#282828',
+
accent2: '#B8BB26',
+
accent2Color: '#282828',
+
accent3: '#FABD2F',
+
accent3Color: '#282828',
+
accent4: '#83A598',
+
accent4Color: '#282828'
+
},
+
monoAmber: {
+
name: "Mono Amber",
+
background: '#2b1900',
+
color: '#FF9400',
+
descBackground: '#2b1900',
+
descColor: '#FF9400',
+
accent: '#FF9400',
+
accentColor: '#402500',
+
accent2: '#FF9400',
+
accent2Color: '#402500',
+
accent3: '#FF9400',
+
accent3Color: '#402500',
+
accent4: '#FF9400',
+
accent4Color: '#402500'
+
},
+
monoCyan: {
+
name: "Mono Cyan",
+
background: '#00222b',
+
color: '#00CCFF',
+
descBackground: '#00222b',
+
descColor: '#00CCFF',
+
accent: '#00CCFF',
+
accentColor: '#003340',
+
accent2: '#00CCFF',
+
accent2Color: '#003340',
+
accent3: '#00CCFF',
+
accent3Color: '#003340',
+
accent4: '#00CCFF',
+
accent4Color: '#003340'
+
},
+
monoGreen: {
+
name: "Mono Green",
+
background: '#022b00',
+
color: '#0BFF00',
+
descBackground: '#022b00',
+
descColor: '#0BFF00',
+
accent: '#0BFF00',
+
accentColor: '#034000',
+
accent2: '#0BFF00',
+
accent2Color: '#034000',
+
accent3: '#0BFF00',
+
accent3Color: '#034000',
+
accent4: '#0BFF00',
+
accent4Color: '#034000'
+
},
+
monoRed: {
+
name: "Mono Red",
+
background: '#2b0c00',
+
color: '#FF3600',
+
descBackground: '#2b0c00',
+
descColor: '#FF3600',
+
accent: '#FF3600',
+
accentColor: '#401200',
+
accent2: '#FF3600',
+
accent2Color: '#401200',
+
accent3: '#FF3600',
+
accent3Color: '#401200',
+
accent4: '#FF3600',
+
accent4Color: '#401200'
+
},
+
monoWhite: {
+
name: "Mono White",
+
background: '#262626',
+
color: '#FAFAFA',
+
descBackground: '#262626',
+
descColor: '#FAFAFA',
+
accent: '#FAFAFA',
+
accentColor: '#3B3B3B',
+
accent2: '#FAFAFA',
+
accent2Color: '#3B3B3B',
+
accent3: '#FAFAFA',
+
accent3Color: '#3B3B3B',
+
accent4: '#FAFAFA',
+
accent4Color: '#3B3B3B'
+
},
+
monoYellow: {
+
name: "Mono Yellow",
+
background: '#2b2400',
+
color: '#FFD300',
+
descBackground: '#2b2400',
+
descColor: '#FFD300',
+
accent: '#FFD300',
+
accentColor: '#403500',
+
accent2: '#FFD300',
+
accent2Color: '#403500',
+
accent3: '#FFD300',
+
accent3Color: '#403500',
+
accent4: '#FFD300',
+
accent4Color: '#403500'
+
},
+
seaShells: {
+
name: "Sea Shells",
+
background: '#09141b',
+
color: '#DEB88D',
+
descBackground: '#09141b',
+
descColor: '#FEE4CE',
+
accent: '#D15123',
+
accentColor: '#FEE4CE',
+
accent2: '#027C9B',
+
accent2Color: '#FEE4CE',
+
accent3: '#1E4950',
+
accent3Color: '#FEE4CE',
+
accent4: '#434B53',
+
accent4Color: '#FEE4CE'
+
},
+
seafoamPastel: {
+
name: "Seafoam Pastel",
+
background: '#243435',
+
color: '#E0E0E0',
+
descBackground: '#243435',
+
descColor: '#E0E0E0',
+
accent: '#825D4D',
+
accentColor: '#FEE4CE',
+
accent2: '#728C62',
+
accent2Color: '#FEE4CE',
+
accent3: '#ADA16D',
+
accent3Color: '#FEE4CE',
+
accent4: '#4D7B82',
+
accent4Color: '#FEE4CE'
+
},
+
seoul256: {
+
name: "Seoul 256",
+
background: '#3a3a3a',
+
color: '#e4e4e4',
+
descBackground: '#4e4e4e',
+
descColor: '#d0d0d0',
+
accent: '#d68787',
+
accentColor: '#4e4e4e',
+
accent2: '#87af87',
+
accent2Color: '#4e4e4e',
+
accent3: '#add4fb',
+
accent3Color: '#4e4e4e',
+
accent4: '#d7afaf',
+
accent4Color: '#4e4e4e'
+
},
+
seoul256Light: {
+
name: "Seoul 256 Light",
+
background: '#dadada',
+
color: '#4e4e4e',
+
descBackground: '#e4e4e4',
+
descColor: '#3a3a3a',
+
accent: '#870100',
+
accentColor: '#eeeeee',
+
accent2: '#d8865f',
+
accent2Color: '#eeeeee',
+
accent3: '#87025f',
+
accent3Color: '#eeeeee',
+
accent4: '#008787',
+
accent4Color: '#eeeeee'
+
},
+
shel: {
+
name: "Shel",
+
background: '#2C2423',
+
color: '#8FBAEC',
+
descBackground: '#2C2423',
+
descColor: '#F5EEEC',
+
accent: '#F588B9',
+
accentColor: '#F5EEEC',
+
accent2: '#AB6423',
+
accent2Color: '#F5EEEC',
+
accent3: '#2C64A2',
+
accent3Color: '#F5EEEC',
+
accent4: '#6C24A2',
+
accent4Color: '#F5EEEC'
+
},
+
slate: {
+
name: "Slate",
+
background: '#222222',
+
color: '#8CDFE0',
+
descBackground: '#222222',
+
descColor: '#E0E0E0',
+
accent: '#E2A8BF',
+
accentColor: '#222222',
+
accent2: '#81D778',
+
accent2Color: '#222222',
+
accent3: '#C4C9C0',
+
accent3Color: '#222222',
+
accent4: '#A481D3',
+
accent4Color: '#222222'
+
},
+
vaughn: {
+
name: "Vaughn",
+
background: '#25234F',
+
color: '#FFFFFF',
+
descBackground: '#25234F',
+
descColor: '#FFFFFF',
+
accent: '#60B48A',
+
accentColor: '#25234F',
+
accent2: '#DFAF8F',
+
accent2Color: '#25234F',
+
accent3: '#F08CC3',
+
accent3Color: '#25234F',
+
accent4: '#8CD0D3',
+
accent4Color: '#25234F'
+
},
+
warmNeon: {
+
name: "Warm Neon",
+
background: '#404040',
+
color: '#9CC090',
+
descBackground: '#404040',
+
descColor: '#FEFCFC',
+
accent: '#2ABBD4',
+
accentColor: '#FEFCFC',
+
accent2: '#39B13A',
+
accent2Color: '#FEFCFC',
+
accent3: '#4261C5',
+
accent3Color: '#FEFCFC',
+
accent4: '#F920FB',
+
accent4Color: '#FEFCFC'
+
},
+
website: {
+
name: "Website",
+
background: '#132f35',
+
color: '#ffd48f',
+
descBackground: '#183c44',
+
descColor: '#ffd48f',
+
accent: '#ff5757',
+
accentColor: '#235662',
+
accent2: '#ecff14',
+
accent2Color: '#235662',
+
accent3: '#4cbfff',
+
accent3Color: '#235662',
+
accent4: '#ff4cc2',
+
accent4Color: '#235662'
+
},
+
wryan: {
+
name: "Wryan",
+
background: '#101010',
+
color: '#899CA1',
+
descBackground: '#101010',
+
descColor: '#f0efd0',
+
accent: '#8C4665',
+
accentColor: '#C0C0C0',
+
accent2: '#287373',
+
accent2Color: '#C0C0C0',
+
accent3: '#7C7C99',
+
accent3Color: '#C0C0C0',
+
accent4: '#395573',
+
accent4Color: '#C0C0C0'
+
},
+
zenburn: {
+
name: "Zenburn",
+
background: '#3a3a3a',
+
color: '#f0efd0',
+
descBackground: '#333333',
+
descColor: '#f0efd0',
+
accent: '#cc9393',
+
accentColor: '#333333',
+
accent2: '#dfaf87',
+
accent2Color: '#333333',
+
accent3: '#efef87',
+
accent3Color: '#333333',
+
accent4: '#bca3a3',
+
accent4Color: '#333333'
+
},
+
sunset: {
+
name: "Sunset",
+
background: '#c757ab',
+
color: '#FFFFFF',
+
descBackground: '#FFFFFF',
+
descColor: '#2a2a2a',
+
accent: '#ff8c00',
+
accentColor: '#FFFFFF',
+
accent2: '#c5b95c',
+
accent2Color: '#FFFFFF',
+
accent3: '#ff69b4',
+
accent3Color: '#FFFFFF',
+
accent4: '#000069',
+
accent4Color: '#FFFFFF'
+
}
+
}
+
+
export default themes
+196
src/lib/titleFonts.js
···
+
import baseFonts from "./baseFonts.js"
+
+
const titleFonts = {
+
...baseFonts,
+
playfairdisplay: {
+
displayName: 'Playfair Display',
+
defs: [
+
{
+
path: '/fonts/Playfair-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/Playfair-Italic.ttf',
+
style: 'italic',
+
weight: 400
+
},
+
{
+
path: '/fonts/Playfair-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
},
+
{
+
path: '/fonts/Playfair-BoldItalic.ttf',
+
style: 'italic',
+
weight: 700
+
}
+
]
+
},
+
ultra: {
+
displayName: 'Ultra',
+
defs: [
+
{
+
path: '/fonts/Ultra-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
}
+
]
+
},
+
stacksansheadline: {
+
displayName: 'Stack Sans Headline',
+
defs: [
+
{
+
path: '/fonts/StackSansHeadline-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/StackSansHeadline-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
}
+
]
+
},
+
stacksansnotch: {
+
displayName: 'Stack Sans Notch',
+
defs: [
+
{
+
path: '/fonts/StackSansNotch-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/StackSansNotch-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
}
+
]
+
},
+
titanone: {
+
displayName: 'Titan One',
+
defs: []
+
},
+
momotrustdisplay: {
+
displayName: 'Momo Trust Display',
+
defs: [
+
{
+
path: '/fonts/MomoTrustDisplay-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/MomoTrustDisplay-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
}
+
]
+
},
+
momosignature: {
+
displayName: 'Momo Signature',
+
defs: [
+
{
+
path: '/fonts/MomoSignature-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
}
+
]
+
},
+
londrinasketch: {
+
displayName: 'Londrina Sketch',
+
defs: [
+
{
+
path: '/fonts/LondrinaSketch-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
}
+
]
+
},
+
londrinashadow: {
+
displayName: 'Londrina Shadow',
+
defs: [
+
{
+
path: '/fonts/LondrinaShadow-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
}
+
]
+
},
+
londrinasolid: {
+
displayName: 'Londrina Solid',
+
defs: [
+
{
+
path: '/fonts/LondrinaSolid-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/LondrinaSolid-Black.ttf',
+
style: 'normal',
+
weight: 700
+
}
+
]
+
},
+
bebasneue: {
+
displayName: 'Bebas Neue',
+
defs: [
+
{
+
path: '/fonts/BebasNeue-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
}
+
]
+
},
+
oswald: {
+
displayName: 'Oswald',
+
defs: [
+
{
+
path: '/fonts/Oswald-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/Oswald-Bold.ttf',
+
style: 'normal',
+
weight: 700
+
}
+
]
+
},
+
archivoblack: {
+
displayName: 'Archivo Black',
+
defs: [
+
{
+
path: '/fonts/ArchivoBlack.otf',
+
style: 'normal',
+
weight: 400
+
}
+
]
+
},
+
alfaslabone: {
+
displayName: 'Alfa Slab One',
+
defs: [
+
{
+
path: '/fonts/AlfaSlabOne-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
}
+
]
+
},
+
sixtyfour: {
+
displayName: 'SixtyFour',
+
defs: [
+
{
+
path: '/fonts/Sixtyfour-Regular.ttf',
+
style: 'normal',
+
weight: 400
+
},
+
{
+
path: '/fonts/Sixtyfour-Regular.ttf',
+
style: 'normal',
+
weight: 700
+
}
+
]
+
}
+
}
+
+
export default titleFonts