Compare commits

...

8 Commits

Author SHA1 Message Date
Nolann Biron 7b87f9f3ff Store the generated font manifest by family instead of by face
The unicode-ranges were repeated identically on every weight of a family, and
sources.json held 220 full URLs where 24 per-family prefixes cover them.

226 KB -> 106 KB committed, and the runtime JSON that ends up in the server
bundle drops from 190 KB to 90 KB. The emitted faces are unchanged: same 491
faces, same fallback faces, same CSS variable values.
2026-08-19 19:51:30 +02:00
Nolann Biron 0af8f181e9 changeset 2026-08-19 19:02:32 +02:00
Nolann Biron 02d370a558 Self-host default fonts and inline only the ones a site uses (RND-12524)
`src/fonts/default.ts` instantiated 23 `next/font` families in one module, so the
507 `@font-face` rules for all of them shipped to every page across two or three
render-blocking stylesheets (195 KB) — for the two families a site actually uses.

Resolve the faces from the Google Fonts CSS API at generate time, vendor the
woff2 files under `~gitbook/static/fonts`, and inline the picked families'
rules in the head, reusing the path custom fonts already took. The generated
data reproduces next/font's 491 faces and all 16 metric-adjusted fallback faces
exactly, so rendering and CLS are unchanged.

Published pages are now down to a single render-blocking stylesheet, plus ~3 KB
compressed of inline font rules. Builds also no longer depend on Google Fonts.
2026-08-19 18:51:56 +02:00
Nolann Biron d10b9643fa changeset 2026-08-19 18:01:25 +02:00
Nolann Biron c3d8f08a0c Keep block, toolbar and print CSS out of the render-blocking path (RND-12524)
Published pages loaded 8 render-blocking stylesheets (620 KB raw / 71.6 KB
compressed), four of them for features the page did not use: the OpenAPI block
CSS (185 KB), ContentKit (33 KB), the admin toolbar CSS modules, and the PDF
print styles.

Build the OpenAPI and ContentKit stylesheets as standalone assets and link them
from the blocks themselves via React's stylesheet hoisting, so only pages that
render those blocks pay for them. Move the admin toolbar behind a lazy chunk,
inline the print styles in the PDF layout, and fold the two remaining CSS
modules into globals.css so they stop splitting off their own chunks.

Every route is down to 3 stylesheets / 380 KB raw.
2026-08-19 18:01:16 +02:00
Peter White 56c25587db Make keyboard shortcuts work on non-QWERTY layouts (#4516) 2026-08-19 15:03:34 +00:00
Zeno Kapitein cf4efc7213 Fix search input losing focus before hydration (RND-11849) (#4438)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-19 12:30:11 +02:00
Zeno Kapitein 1ef71609e7 Show the light/dark toggle on laptop-width screens (#4515)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 12:29:58 +02:00
43 changed files with 2737 additions and 463 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Show the theme toggle in the footer whenever the outline column that hosts the other toggle isn't pinned open, so it stays reachable on laptop-sized screens in wide layouts and while the AI chat is open.
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Upgrade react-hotkeys-hook to v5 and use its native `useKey` option so keyboard shortcuts match the produced key on non-QWERTY layouts.
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Load the OpenAPI, ContentKit, admin toolbar and PDF print stylesheets only on the pages that need them, cutting published pages from 8 render-blocking stylesheets to 3.
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Fix the search field losing focus if it was focused just before the page finished hydrating.
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Self-host the default fonts and inline only the families a site uses, instead of shipping the `@font-face` rules for all 23 supported fonts on every page.
+42 -7
View File
@@ -171,7 +171,7 @@
"quick-lru": "^7.0.1",
"react": "19.2.4",
"react-dom": "19.2.4",
"react-hotkeys-hook": "^4.4.1",
"react-hotkeys-hook": "^5.3.3",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"rehype-stringify": "^10.0.1",
@@ -198,6 +198,7 @@
"@argos-ci/playwright": "^7.2.0",
"@playwright/test": "^1.61.1",
"@scalar/api-client-react": "catalog:",
"@tailwindcss/cli": "4.1.11",
"@tailwindcss/postcss": "^4.1.11",
"@types/js-cookie": "^3.0.6",
"@types/jsontoxml": "^1.0.5",
@@ -1068,6 +1069,32 @@
"@panzoom/panzoom": ["@panzoom/panzoom@4.6.1", "", {}, "sha512-ogf/KhHHjj+DYAvHfaf3TXMQ8OE36pJtKpabLlx1OmpjcgtpCvkUoCiNONA8kuVRPGJdLiqMf0n8LRFXj1OyuA=="],
"@parcel/watcher": ["@parcel/watcher@2.6.0", "", { "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", "node-addon-api": "^7.0.0", "picomatch": "^4.0.4" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.6.0", "@parcel/watcher-darwin-arm64": "2.6.0", "@parcel/watcher-darwin-x64": "2.6.0", "@parcel/watcher-freebsd-x64": "2.6.0", "@parcel/watcher-linux-arm-glibc": "2.6.0", "@parcel/watcher-linux-arm-musl": "2.6.0", "@parcel/watcher-linux-arm64-glibc": "2.6.0", "@parcel/watcher-linux-arm64-musl": "2.6.0", "@parcel/watcher-linux-x64-glibc": "2.6.0", "@parcel/watcher-linux-x64-musl": "2.6.0", "@parcel/watcher-win32-arm64": "2.6.0", "@parcel/watcher-win32-x64": "2.6.0" } }, "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w=="],
"@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.6.0", "", { "os": "android", "cpu": "arm64" }, "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA=="],
"@parcel/watcher-darwin-arm64": ["@parcel/watcher-darwin-arm64@2.6.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw=="],
"@parcel/watcher-darwin-x64": ["@parcel/watcher-darwin-x64@2.6.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw=="],
"@parcel/watcher-freebsd-x64": ["@parcel/watcher-freebsd-x64@2.6.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ=="],
"@parcel/watcher-linux-arm-glibc": ["@parcel/watcher-linux-arm-glibc@2.6.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg=="],
"@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.6.0", "", { "os": "linux", "cpu": "arm" }, "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw=="],
"@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.6.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g=="],
"@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.6.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA=="],
"@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.6.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A=="],
"@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.6.0", "", { "os": "linux", "cpu": "x64" }, "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw=="],
"@parcel/watcher-win32-arm64": ["@parcel/watcher-win32-arm64@2.6.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ=="],
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.6.0", "", { "os": "win32", "cpu": "x64" }, "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A=="],
"@phosphor-icons/core": ["@phosphor-icons/core@2.1.1", "", {}, "sha512-v4ARvrip4qBCImOE5rmPUylOEK4iiED9ZyKjcvzuezqMaiRASCHKcRIuvvxL/twvLpkfnEODCOJp5dM4eZilxQ=="],
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
@@ -1316,6 +1343,8 @@
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
"@tailwindcss/cli": ["@tailwindcss/cli@4.1.11", "", { "dependencies": { "@parcel/watcher": "^2.5.1", "@tailwindcss/node": "4.1.11", "@tailwindcss/oxide": "4.1.11", "enhanced-resolve": "^5.18.1", "mri": "^1.2.0", "picocolors": "^1.1.1", "tailwindcss": "4.1.11" }, "bin": { "tailwindcss": "dist/index.mjs" } }, "sha512-7RAFOrVaXCFz5ooEG36Kbh+sMJiI2j4+Ozp71smgjnLfBRu7DTfoq8DsTvzse2/6nDeo2M3vS/FGaxfDgr3rtQ=="],
"@tailwindcss/container-queries": ["@tailwindcss/container-queries@0.1.1", "", { "peerDependencies": { "tailwindcss": ">=3.2.0" } }, "sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA=="],
"@tailwindcss/node": ["@tailwindcss/node@4.1.11", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.11" } }, "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q=="],
@@ -1948,7 +1977,7 @@
"detect-indent": ["detect-indent@6.1.0", "", {}, "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA=="],
"detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
@@ -2654,6 +2683,8 @@
"next-tick": ["next-tick@1.1.0", "", {}, "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="],
"node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
"node-fetch": ["node-fetch@2.6.7", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ=="],
@@ -2862,7 +2893,7 @@
"react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="],
"react-hotkeys-hook": ["react-hotkeys-hook@4.5.1", "", { "peerDependencies": { "react": ">=16.8.1", "react-dom": ">=16.8.1" } }, "sha512-scAEJOh3Irm0g95NIn6+tQVf/OICCjsQsC9NBHfQws/Vxw4sfq1tDQut5fhTEvPraXhu/sHxRd9lOtxzyYuNAg=="],
"react-hotkeys-hook": ["react-hotkeys-hook@5.3.3", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-aswgyWUnE25hmhzHTfKDmKzsaSE5DJ4LKaU/o6rQSXkDd/1Bh9TfAFQbHkf6fLy11HvlYkp+cDDarGdhmCDhoQ=="],
"read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="],
@@ -3460,6 +3491,8 @@
"@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
"@mapbox/node-pre-gyp/detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
"@mapbox/node-pre-gyp/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
"@mapbox/node-pre-gyp/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
@@ -3478,6 +3511,8 @@
"@oxc-transform/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
"@parcel/watcher/picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
"@playwright/test/playwright": ["playwright@1.61.1", "", { "dependencies": { "playwright-core": "1.61.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ=="],
"@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
@@ -3582,6 +3617,8 @@
"@tailwindcss/node/magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
"@tailwindcss/oxide/detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
"@tailwindcss/oxide/tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.5", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" }, "bundled": true }, "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q=="],
@@ -3800,6 +3837,8 @@
"jsonwebtoken/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
"lightningcss/detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
"lowlight/highlight.js": ["highlight.js@11.9.0", "", {}, "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw=="],
"mcp-handler/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
@@ -3890,8 +3929,6 @@
"send/fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
"sharp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"side-channel-list/object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"side-channel-map/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=="],
@@ -3988,8 +4025,6 @@
"@argos-ci/core/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.3", "", { "os": "win32", "cpu": "x64" }, "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA=="],
"@argos-ci/core/sharp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"@argos-ci/core/sharp/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
"@aws-crypto/crc32/@aws-sdk/types/@smithy/types": ["@smithy/types@4.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-enhjdwp4D7CXmwLtD6zbcDMbo6/T6WtuuKCY49Xxc6OMOmUWlBEBDREsxxgV2LIdeQPW756+f97GzcgAwp3iLw=="],
+8 -6
View File
@@ -62,7 +62,7 @@
"quick-lru": "^7.0.1",
"react": "19.2.4",
"react-dom": "19.2.4",
"react-hotkeys-hook": "^4.4.1",
"react-hotkeys-hook": "^5.3.3",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"rehype-stringify": "^10.0.1",
@@ -89,37 +89,39 @@
"@argos-ci/playwright": "^7.2.0",
"@playwright/test": "^1.61.1",
"@scalar/api-client-react": "catalog:",
"@tailwindcss/cli": "4.1.11",
"@tailwindcss/postcss": "^4.1.11",
"@types/js-cookie": "^3.0.6",
"@types/jsontoxml": "^1.0.5",
"@types/jsonwebtoken": "^9.0.6",
"@types/mdast": "^4.0.4",
"@types/negotiator": "^0.6.4",
"@types/node": "^20",
"@types/object-hash": "^3.0.6",
"@types/parse-cache-control": "^1.0.4",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"@types/rison": "^0.0.9",
"@types/negotiator": "^0.6.4",
"bun-types": "catalog:",
"deepmerge": "^4.3.1",
"diff": "^8.0.2",
"env-cmd": "^10.1.0",
"jsonwebtoken": "^9.0.2",
"postcss": "^8",
"rss-parser": "^3.13.0",
"stylelint": "^16.16.0",
"stylelint-no-unsupported-browser-features": "^8.1.1",
"tailwindcss": "^4.1.11",
"ts-essentials": "^10.0.1",
"typescript": "catalog:",
"vercel": "50.37.3",
"wrangler": "^4.79.0",
"rss-parser": "^3.13.0"
"wrangler": "^4.79.0"
},
"scripts": {
"generate": "./scripts/generate.sh",
"generate:assets": "bun ./scripts/generate-mermaid-runtime.ts && bun ./scripts/generate-scalar-runtime.ts",
"clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static/icons && rm -rf ./public/~gitbook/static/math && rm -rf ./public/~gitbook/static/mermaid && rm -rf ./public/~gitbook/static/scalar",
"generate:assets": "bun ./scripts/generate-mermaid-runtime.ts && bun ./scripts/generate-scalar-runtime.ts && bun ./scripts/generate-block-css.ts && bun ./scripts/download-fonts.ts",
"generate:fonts": "bun ./scripts/generate-font-faces.ts",
"clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static/icons && rm -rf ./public/~gitbook/static/math && rm -rf ./public/~gitbook/static/mermaid && rm -rf ./public/~gitbook/static/scalar && rm -rf ./public/~gitbook/static/css && rm -rf ./public/~gitbook/static/fonts",
"dev": "bun run generate:assets && env-cmd --silent -f ../../.env.local next --webpack",
"build": "bun run generate:assets && next build --webpack",
"build:local": "bun run generate:assets && GITBOOK_URL=http://localhost:3000 next build --webpack",
@@ -0,0 +1,73 @@
import { copyFile, mkdir, readdir, rm, writeFile } from 'node:fs/promises';
import { dirname, join, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
import sourcesData from '../src/fonts/generated/sources.json' with { type: 'json' };
import type { FontSourcesData } from '../src/fonts/types';
const CONCURRENCY = 8;
const ATTEMPTS = 3;
const scriptDir = dirname(fileURLToPath(import.meta.url));
const fontsDir = join(scriptDir, '../src/fonts');
const outputDir = join(scriptDir, '../public/~gitbook/static/fonts');
const { google, local } = sourcesData as FontSourcesData;
const sources = new Map<string, string>(Object.entries(local));
for (const [googleId, { prefix, files }] of Object.entries(google)) {
for (const file of files) {
sources.set(`${googleId}/${file}`, `${prefix}/${file}`);
}
}
// Faces move between releases; stale files would otherwise pile up in the deployed assets.
await mkdir(outputDir, { recursive: true });
for (const entry of await readdir(outputDir, { recursive: true, withFileTypes: true })) {
if (entry.isFile()) {
const file = relative(outputDir, join(entry.parentPath, entry.name));
if (!sources.has(file)) {
await rm(join(outputDir, file));
}
}
}
const pending = [...sources].filter(([file]) => !Bun.file(join(outputDir, file)).size);
if (pending.length > 0) {
console.log(`Downloading ${pending.length} font files…`);
}
const queue = pending.values();
await Promise.all(Array.from({ length: CONCURRENCY }, () => worker()));
async function worker() {
for (const [file, source] of queue) {
const target = join(outputDir, file);
await mkdir(dirname(target), { recursive: true });
if (source.startsWith('./')) {
await copyFile(join(fontsDir, source), target);
continue;
}
await writeFile(target, await fetchWithRetries(source));
}
}
// Google Fonts intermittently refuses connections when a build asks for hundreds of files at once,
// and a single miss fails the whole build.
async function fetchWithRetries(url: string): Promise<Buffer> {
for (let attempt = 1; ; attempt++) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`${response.status} ${response.statusText}`);
}
return Buffer.from(await response.arrayBuffer());
} catch (error) {
if (attempt >= ATTEMPTS) {
throw new Error(`Unable to download ${url}: ${error}`);
}
await new Promise((resolve) => setTimeout(resolve, 500 * 2 ** attempt));
}
}
}
@@ -0,0 +1,36 @@
import { mkdir, rm } from 'node:fs/promises';
import { createRequire } from 'node:module';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { BLOCK_STYLESHEETS } from '../src/components/DocumentView/blockStylesheets';
const scriptDir = dirname(fileURLToPath(import.meta.url));
const sourceDir = join(scriptDir, '../src');
const outputDir = join(scriptDir, '../public/~gitbook/static/css');
const tailwindCli = join(
dirname(createRequire(import.meta.url).resolve('@tailwindcss/cli/package.json')),
'dist/index.mjs'
);
await rm(outputDir, { force: true, recursive: true });
await mkdir(outputDir, { recursive: true });
for (const [name, source] of Object.entries(BLOCK_STYLESHEETS)) {
const result = Bun.spawnSync(
[
'node',
tailwindCli,
'--input',
join(sourceDir, source),
'--output',
join(outputDir, `${name}.css`),
'--minify',
],
{ stdout: 'pipe', stderr: 'pipe' }
);
if (!result.success) {
throw new Error(`Unable to build ${source}: ${result.stderr.toString()}`);
}
}
@@ -0,0 +1,186 @@
import { createHash } from 'node:crypto';
import { readFile, writeFile } from 'node:fs/promises';
import { createRequire } from 'node:module';
import { basename, dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { ABC_FAVORIT, FONT_DEFINITIONS, type FontDefinition } from '../src/fonts/definitions';
import type {
FontFacesData,
FontFallbackFaceData,
FontSourcesData,
FontVariantData,
} from '../src/fonts/types';
// Google Fonts picks the file format from the user agent — the same modern Chrome `next/font` sends,
// so we keep getting compact woff2 (and vector rather than bitmap emoji).
const USER_AGENT =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36';
// The precalculated metrics `next/font/google` uses, so the fallback faces stay identical.
const { calculateSizeAdjustValues } = createRequire(import.meta.url)(
'next/dist/server/font-utils'
) as {
calculateSizeAdjustValues: (family: string) => {
ascent: string;
descent: string;
lineGap: string;
fallbackFont: string;
sizeAdjust: string;
};
};
type ResolvedFace = {
weight: string;
style: string;
file: string;
source: string;
unicodeRange: string;
};
const scriptDir = dirname(fileURLToPath(import.meta.url));
const faces: FontFacesData = {};
const sources: FontSourcesData = { google: {}, local: {} };
for (const [name, definition] of Object.entries(FONT_DEFINITIONS)) {
const resolved = definition.googleId
? await getGoogleFaces(definition)
: await getABCFavoritFaces();
if (resolved.length === 0) {
throw new Error(`No font faces resolved for ${name}`);
}
recordSources(definition, resolved);
const subsets = [...new Set(resolved.map((face) => face.unicodeRange))];
const variants = new Map<string, FontVariantData>();
for (const face of resolved) {
const key = `${face.weight}|${face.style}`;
let variant = variants.get(key);
if (!variant) {
variant = { weight: face.weight, style: face.style, files: [] };
variants.set(key, variant);
}
variant.files[subsets.indexOf(face.unicodeRange)] = face.file;
}
faces[name] = {
family: definition.family,
variable: definition.variable,
fontFamilyValue: [
`"${definition.family}"`,
...(definition.adjustFallback ? [`"${definition.family} Fallback"`] : []),
...definition.fallback,
].join(','),
subsets,
variants: [...variants.values()],
fallbackFace: definition.adjustFallback ? getFallbackFace(name, definition.family) : null,
...(definition.googleId ? {} : { ascentOverride: ABC_FAVORIT.ascentOverride }),
};
}
const generatedDir = join(scriptDir, '../src/fonts/generated');
await writeFile(join(generatedDir, 'faces.json'), `${JSON.stringify(faces, null, 4)}\n`);
await writeFile(join(generatedDir, 'sources.json'), `${JSON.stringify(sources, null, 4)}\n`);
/** Google serves every file of a family from one versioned directory, so only the names differ. */
function recordSources(definition: FontDefinition, resolved: ResolvedFace[]) {
if (!definition.googleId) {
for (const face of resolved) {
sources.local[face.file] = face.source;
}
return;
}
const prefixes = new Set(
resolved.map((face) => face.source.slice(0, face.source.lastIndexOf('/')))
);
if (prefixes.size !== 1) {
throw new Error(`${definition.family} spans several Google Fonts directories`);
}
sources.google[definition.googleId] = {
prefix: [...prefixes][0] as string,
files: [...new Set(resolved.map((face) => basename(face.file)))],
};
}
async function getGoogleFaces(definition: FontDefinition): Promise<ResolvedFace[]> {
const { family, googleId, weights } = definition;
const url = `https://fonts.googleapis.com/css2?family=${family.replaceAll(' ', '+')}:wght@${weights.join(';')}&display=swap`;
const response = await fetch(url, { headers: { 'User-Agent': USER_AGENT } });
if (!response.ok) {
throw new Error(`Unable to fetch ${family} from Google Fonts: ${response.status} (${url})`);
}
const resolved = [...(await response.text()).matchAll(/@font-face\s*\{([^}]*)\}/g)].map(
(match) => {
const block = match[1] ?? '';
const source = read(block, 'src')?.match(/url\((https:[^)]+\.woff2)\)/)?.[1];
const weight = read(block, 'font-weight');
const unicodeRange = read(block, 'unicode-range');
if (!source || !weight || !unicodeRange) {
throw new Error(`Unexpected @font-face for ${family}: ${block}`);
}
return {
weight,
style: read(block, 'font-style') ?? 'normal',
// Google's filenames are content-addressed, so the asset can stay immutable.
file: `${googleId}/${basename(new URL(source).pathname)}`,
source,
unicodeRange: unicodeRange.toLowerCase(),
};
}
);
const missing = weights.filter((weight) => !resolved.some((face) => face.weight === weight));
if (missing.length > 0) {
throw new Error(`Google Fonts returned no ${missing.join('/')} weight for ${family}`);
}
return resolved;
}
async function getABCFavoritFaces(): Promise<ResolvedFace[]> {
return Promise.all(
ABC_FAVORIT.sources.map(async (source) => {
const path = join(scriptDir, '../src/fonts/ABCFavorit', source.file);
const digest = createHash('sha256')
.update(await readFile(path))
.digest('hex');
return {
weight: source.weight,
style: source.style,
file: `abcfavorit/${digest.slice(0, 16)}.woff2`,
source: `./ABCFavorit/${source.file}`,
unicodeRange: '',
};
})
);
}
function getFallbackFace(name: string, family: string): FontFallbackFaceData {
if (name === 'ABCFavorit') {
return { family: `${family} Fallback`, local: 'Arial', ...ABC_FAVORIT.fallbackMetrics };
}
const metrics = calculateSizeAdjustValues(family);
return {
family: `${family} Fallback`,
local: metrics.fallbackFont,
ascentOverride: `${metrics.ascent}%`,
descentOverride: `${metrics.descent}%`,
lineGapOverride: `${metrics.lineGap}%`,
sizeAdjust: `${metrics.sizeAdjust}%`,
};
}
function read(block: string, property: string): string | undefined {
return block.match(new RegExp(`${property}\\s*:\\s*([^;]+);`))?.[1]?.trim();
}
@@ -63,7 +63,9 @@ export function AIChat() {
() => {
chatController.close();
},
[]
{
useKey: true,
}
);
// Track the view of the AI chat
@@ -77,9 +77,9 @@ export function AIChatInput(props: {
},
{
enableOnFormTags: true,
// Match the logical character so Dvorak ⌘-C (physical "I" key) copies
// Match the produced key so Dvorak ⌘-C (physical "I" key) copies
// instead of focusing the Assistant input. RND-11340.
ignoreEventWhen: (e) => e.key.toLowerCase() !== 'i',
useKey: true,
}
);
@@ -1,4 +1,4 @@
import { AdminToolbarClient } from './AdminToolbarClient';
import { AdminToolbarLazy } from './AdminToolbarLazy';
import { getToolbarChangedPages } from './changedPages';
import type { AdminToolbarContext } from './types';
import type { GitBookSiteContext } from '@/lib/context';
@@ -62,5 +62,5 @@ export async function AdminToolbar(props: AdminToolbarProps) {
changedPages,
};
return <AdminToolbarClient context={minimalContext} />;
return <AdminToolbarLazy context={minimalContext} />;
}
@@ -0,0 +1,17 @@
'use client';
import dynamic from 'next/dynamic';
import type { AdminToolbarClientProps } from './types';
// The toolbar only ever appears after hydration (`useToolbarVisibility` reads cookies and local
// storage), so nothing is lost by deferring it — and it takes its CSS modules out of the route's
// render-blocking stylesheets, where every published page was paying for them.
const AdminToolbarClient = dynamic(
() => import('./AdminToolbarClient').then((mod) => mod.AdminToolbarClient),
{ ssr: false }
);
export function AdminToolbarLazy(props: AdminToolbarClientProps) {
return <AdminToolbarClient {...props} />;
}
@@ -1,6 +1,3 @@
// Only the server entrypoint: re-exporting the client modules would pull their CSS modules back
// into the route's render-blocking stylesheets, defeating the lazy boundary.
export * from './AdminToolbar';
export * from './AdminToolbarClient';
export * from './IframeWrapper';
export * from './Toolbar';
export * from './transitions';
export * from './utils';
@@ -0,0 +1,14 @@
import type { BlockStylesheetName } from './blockStylesheets';
import { getAssetURL } from '@/lib/assets';
import { buildVersion } from '@/lib/build';
export function getBlockStylesheetURL(name: BlockStylesheetName): string {
return getAssetURL(`css/${name}.css?v=${buildVersion()}`);
}
// `href` + `precedence` opt into React's stylesheet hoisting: the tag moves to `<head>`, dedupes
// across every block on the page, and holds paint until it loads, so the block never flashes
// unstyled. `blocks` sorts after Next's own `next` precedence, keeping the previous cascade order.
export function BlockStylesheet(props: { name: BlockStylesheetName }) {
return <link rel="stylesheet" href={getBlockStylesheetURL(props.name)} precedence="blocks" />;
}
@@ -2,9 +2,9 @@ import type { DocumentBlockIntegration, RenderIntegrationUI } from '@gitbook/api
import { ContentKit, ContentKitOutput } from '@gitbook/react-contentkit';
import type { BlockProps } from '../Block';
import { BlockStylesheet } from '../BlockStylesheet';
import { getWebframePageContext, integrationBlockContainsWebframe } from './adaptive';
import { contentKitServerContext } from './contentkit';
import './contentkit.css';
import {
ContentKitWithClientContext,
type WebframeLinkerData,
@@ -105,6 +105,7 @@ export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegratio
return (
<div className={tcls(style)}>
<BlockStylesheet name="contentkit" />
{useClientContext ? (
<ContentKitWithClientContext
{...contentKitProps}
@@ -1,4 +1,5 @@
import type { BlockProps } from '../Block';
import { BlockStylesheet } from '../BlockStylesheet';
import { getOpenAPIBlockClientProps } from './context';
import { OpenAPIBlockLazy } from './OpenAPIBlockLazy';
import { resolveOpenAPIOperationBlock } from '@/lib/openapi/resolveOpenAPIOperationBlock';
@@ -11,9 +12,12 @@ import { tcls } from '@/lib/tailwind';
export async function OpenAPIOperation(props: BlockProps<AnyOpenAPIOperationsBlock>) {
const { style } = props;
return (
<div className={tcls('flex w-full min-w-0', style, 'max-w-full')}>
<OpenAPIOperationBody {...props} />
</div>
<>
<BlockStylesheet name="openapi" />
<div className={tcls('flex w-full min-w-0', style, 'max-w-full')}>
<OpenAPIOperationBody {...props} />
</div>
</>
);
}
@@ -1,4 +1,5 @@
import type { BlockProps } from '../Block';
import { BlockStylesheet } from '../BlockStylesheet';
import { getOpenAPIBlockClientProps } from './context';
import { OpenAPIBlockLazy } from './OpenAPIBlockLazy';
import { resolveOpenAPISchemasBlock } from '@/lib/openapi/resolveOpenAPISchemasBlock';
@@ -11,9 +12,12 @@ import { tcls } from '@/lib/tailwind';
export async function OpenAPISchemas(props: BlockProps<OpenAPISchemasBlock>) {
const { style } = props;
return (
<div className={tcls('flex w-full', style, 'max-w-full')}>
<OpenAPISchemasBody {...props} />
</div>
<>
<BlockStylesheet name="openapi" />
<div className={tcls('flex w-full', style, 'max-w-full')}>
<OpenAPISchemasBody {...props} />
</div>
</>
);
}
@@ -1,4 +1,5 @@
import type { BlockProps } from '../Block';
import { BlockStylesheet } from '../BlockStylesheet';
import { getOpenAPIBlockClientProps } from './context';
import { OpenAPIBlockLazy } from './OpenAPIBlockLazy';
import { resolveOpenAPIWebhookBlock } from '@/lib/openapi/resolveOpenAPIWebhookBlock';
@@ -11,9 +12,12 @@ import { tcls } from '@/lib/tailwind';
export async function OpenAPIWebhook(props: BlockProps<OpenAPIWebhookBlock>) {
const { style } = props;
return (
<div className={tcls('flex w-full min-w-0', style, 'max-w-full')}>
<OpenAPIWebhookBody {...props} />
</div>
<>
<BlockStylesheet name="openapi" />
<div className={tcls('flex w-full min-w-0', style, 'max-w-full')}>
<OpenAPIWebhookBody {...props} />
</div>
</>
);
}
@@ -17,7 +17,6 @@ import { PlainCodeBlock } from '../CodeBlock';
import { DocumentView } from '../DocumentView';
import { Heading } from '../Heading';
import type { OpenAPIBlockClientContextProps } from './OpenAPIBlockClient';
import './style.css';
import { SCALAR_RUNTIME_PATH } from './scalar-runtime-path';
import { DEFAULT_LOCALE, getSpaceLocale } from '@/intl/server';
import { getAssetURL } from '@/lib/assets';
@@ -0,0 +1,9 @@
// Built as standalone assets by `scripts/generate-block-css.ts` rather than imported: an import
// would put them in the route's render-blocking CSS on every page, block or no block.
// Values are the sources to compile, relative to `src/`.
export const BLOCK_STYLESHEETS = {
openapi: 'components/DocumentView/OpenAPI/style.css',
contentkit: 'components/DocumentView/Integration/contentkit.css',
} as const;
export type BlockStylesheetName = keyof typeof BLOCK_STYLESHEETS;
@@ -30,8 +30,11 @@ export function Footer(props: { context: GitBookSiteContext }) {
data-gb-site-footer
className={tcls(
'border-tint-subtle border-t',
// If the footer only contains a mode toggle, we only show it on smaller screens
mobileOnly ? 'xl:hidden' : null
// If the footer only contains a mode toggle, we only show it where the outline
// column — which carries its own toggle — isn't pinned open.
mobileOnly
? 'layout-default:xl:chat-closed:hidden layout-default:3xl:hidden layout-wide:3xl:chat-closed:hidden page-api-block:page-has-outline:min-[96rem]:hidden'
: null
)}
>
<div className="lg:chat-open:pr-(--ai-chat-width) transition-[padding] duration-300 motion-reduce:transition-none">
@@ -96,7 +99,9 @@ export function Footer(props: { context: GitBookSiteContext }) {
{
// Theme Toggle
customization.themes.toggeable ? (
<div className="-col-start-2 row-start-1 flex items-start @xs:justify-end xl:hidden">
// Hidden where the outline column is pinned open (see PageAside),
// since that column carries its own toggle.
<div className="page-api-block:page-has-outline:min-[96rem]:hidden -col-start-2 row-start-1 flex items-start @xs:justify-end layout-default:xl:chat-closed:hidden layout-default:3xl:hidden layout-wide:3xl:chat-closed:hidden">
<React.Suspense fallback={null}>
<ThemeToggler />
</React.Suspense>
@@ -15,7 +15,6 @@ import {
} from '../primitives/DropdownMenu';
import { SiteAuthLoginDropdownMenuItem } from '../SiteAuth/SiteAuthLoginLink';
import { HeaderLinkMoreDropdown } from './HeaderLinkMoreClient';
import styles from './headerLinks.module.css';
import { isSiteAuthLoginHref } from '@/lib/auth-login-link';
import type { GitBookSiteContext } from '@/lib/context';
import { resolveContentRef } from '@/lib/references';
@@ -34,7 +33,7 @@ export function HeaderLinkMore(props: {
const { label, links, context, socialAccounts } = props;
return (
<div className={`${styles.linkEllipsis} z-20 items-center`}>
<div className="header-links-ellipsis z-20 items-center">
<HeaderLinkMoreDropdown
label={label}
dropdownClassName={tcls(
@@ -1,6 +1,5 @@
import type React from 'react';
import styles from './headerLinks.module.css';
import { type ClassValue, tcls } from '@/lib/tailwind';
interface HeaderLinksProps {
@@ -12,7 +11,7 @@ export async function HeaderLinks({ children, style }: HeaderLinksProps) {
return (
<div
className={tcls(
styles.containerHeaderlinks,
'header-links',
'@4xl:[&>.button+.button]:-ml-2 z-20 ml-auto flex min-w-9 shrink grow @7xl:grow-0 items-center justify-end @4xl:gap-x-6 gap-x-4',
style
)}
@@ -1,80 +0,0 @@
@container header (width < 1280px) {
.containerHeaderlinks {
container-type: inline-size;
container-name: headerlinks;
}
}
.linkEllipsis {
display: none;
& div > a {
display: none;
}
}
@container headerlinks (width < 150px) {
.containerHeaderlinks > :nth-child(n + 1) {
display: none;
}
.containerHeaderlinks > :nth-child(n + 1) ~ .linkEllipsis {
display: flex;
& div > *:nth-child(n + 1) {
display: flex;
}
}
}
@container headerlinks (width < 300px) {
.containerHeaderlinks > :nth-child(n + 2) {
display: none;
}
.containerHeaderlinks > :nth-child(n + 2) ~ .linkEllipsis {
display: flex;
& div > *:nth-child(n + 2) {
display: flex;
}
}
}
@container headerlinks (width < 450px) {
.containerHeaderlinks > :nth-child(n + 3) {
display: none;
}
.containerHeaderlinks > :nth-child(n + 3) ~ .linkEllipsis {
display: flex;
& div > *:nth-child(n + 3) {
display: flex;
}
}
}
@container headerlinks (width < 550px) {
.containerHeaderlinks > :nth-child(n + 4) {
display: none;
}
.containerHeaderlinks > :nth-child(n + 4) ~ .linkEllipsis {
display: flex;
& div > *:nth-child(n + 4) {
display: flex;
}
}
}
@container headerlinks (width < 650px) {
.containerHeaderlinks > :nth-child(n + 5) {
display: none;
}
.containerHeaderlinks > :nth-child(n + 5) ~ .linkEllipsis {
display: flex;
& div > *:nth-child(n + 5) {
display: flex;
}
}
}
@container headerlinks (width < 750px) {
.containerHeaderlinks > :nth-child(n + 6) {
display: none;
}
.containerHeaderlinks > :nth-child(n + 6) ~ .linkEllipsis {
display: flex;
& div > *:nth-child(n + 6) {
display: flex;
}
}
}
@@ -26,7 +26,6 @@ import { sanitizeGitBookAppURL } from '@/lib/app';
import type { GitBookSiteContext, GitBookSpaceContext } from '@/lib/context';
import { getPageDocument } from '@/lib/data';
import { type GitBookLinker, createLinker, linkerWithAbsoluteURLs } from '@/lib/links';
import './pdf.css';
import { resolvePageId } from '@/lib/pages';
import { getLinkerForSiteSpace } from '@/lib/sites';
import { tcls } from '@/lib/tailwind';
@@ -1,5 +1,6 @@
import { CustomizationDefaultThemeMode } from '@gitbook/api';
import { PRINT_STYLES } from './printStyles';
import { CustomizationRootLayout } from '@/components/RootLayout';
import type { GitBookSiteContext, GitBookSpaceContext } from '@/lib/context';
@@ -17,6 +18,9 @@ export async function PDFRootLayout(props: {
context={context}
forcedTheme={CustomizationDefaultThemeMode.Light}
>
<style href="pdf-print" precedence="blocks">
{PRINT_STYLES}
</style>
{children}
</CustomizationRootLayout>
);
@@ -1,3 +1,6 @@
// Inlined rather than imported as CSS: a `.css` import lands in the route's render-blocking
// stylesheets, and webpack was attaching this one to the content pages too.
export const PRINT_STYLES = `
@page {
size: A4;
margin: 60pt 60pt;
@@ -56,3 +59,4 @@
display: block !important;
}
}
`;
@@ -1,6 +1,9 @@
import * as ReactDOM from 'react-dom';
import type { JSONDocument, RevisionPageDocument, SiteInsightsDisplayContext } from '@gitbook/api';
import { DocumentView, DocumentViewSkeleton } from '../DocumentView';
import { getBlockStylesheetURL } from '../DocumentView/BlockStylesheet';
import { CurrentPageProvider } from '../hooks/useCurrentPage';
import { TrackPageViewEvent } from '../Insights';
import { CONTENT_STYLE } from '../layout';
@@ -69,6 +72,14 @@ export async function PageBody(props: {
// to API-reference pages only (matching the `page-api-block` styling), so other pages keep the
// header structure untouched.
const hasAPIBlocks = document ? hasAPIBlock(document) : false;
// The block renders its own stylesheet link, but that only reaches the browser once the block
// streams in — start the request from the shell instead. Top-level blocks only, which is the
// common case; nested ones still get the sheet from the link.
if (hasAPIBlocks) {
ReactDOM.preload(getBlockStylesheetURL('openapi'), { as: 'style' });
}
const wideLayout = wideContent || page.layout.width === 'wide';
const language = await getSpaceLanguage(context);
const updatedAt = page.updatedAt ?? page.createdAt;
@@ -26,8 +26,12 @@ import { AnnouncementDismissedScript } from '../Announcement';
import { SelectStateScript } from '../Select';
import { OperatingSystemClassScript } from './OperatingSystemClassScript';
import { RootLayoutClientContexts } from './RootLayoutClientContexts';
import { type FontData, getFontData } from '@/fonts';
import { fontNotoColorEmoji, fonts } from '@/fonts/default';
import {
DEFAULT_MONOSPACE_FONT,
type FontData,
generateEmojiFontFacesCSS,
getFontData,
} from '@/fonts';
import './globals.css';
import { getContentLocale, getSpaceLanguage } from '@/intl/server';
import { getAssetURL } from '@/lib/assets';
@@ -91,12 +95,10 @@ export async function CustomizationRootLayout(props: {
const fontData = getFontData(customization.styling.font, 'content');
// Temporarily add a if here while the cache is being warmed up.
// We can remove the condition after 14-07-2025.
const monospaceFontData = customization.styling.monospaceFont
? getFontData(customization.styling.monospaceFont, 'mono')
: {
type: 'default' as const,
variable: fonts.IBMPlexMono.variable,
};
const monospaceFontData = getFontData(
customization.styling.monospaceFont ?? DEFAULT_MONOSPACE_FONT,
'mono'
);
// Preconnect and preload custom fonts if needed
preloadFont(fontData);
@@ -127,10 +129,8 @@ export async function CustomizationRootLayout(props: {
sidebarStyles.list && `sidebar-list-${sidebarStyles.list}`,
'links' in customization.styling && `links-${customization.styling.links}`,
'depth' in customization.styling && `depth-${customization.styling.depth}`,
fontNotoColorEmoji.variable,
monospaceFontData.type === 'default' ? monospaceFontData.variable : null,
fontData.type === 'default'
? [fontData.variable, `font-${customization.styling.font}`]
typeof customization.styling.font === 'string'
? `font-${customization.styling.font}`
: null,
// Set the dark/light class statically to avoid flashing and make it work when JS is disabled
@@ -150,11 +150,10 @@ export async function CustomizationRootLayout(props: {
{/* Apply the visitor's content selection to <html> before first paint (no flash) */}
<SelectStateScript />
{/* Inject custom font @font-face rules */}
{fontData.type === 'custom' ? <style>{fontData.fontFaceRules}</style> : null}
{monospaceFontData.type === 'custom' ? (
<style>{monospaceFontData.fontFaceRules}</style>
) : null}
{/* Only the picked families, so the head never carries every font we support */}
<style>{generateEmojiFontFacesCSS()}</style>
<style>{fontData.fontFaceRules}</style>
<style>{monospaceFontData.fontFaceRules}</style>
{/* Inject a script to detect if the announcmeent banner has been dismissed */}
{'announcement' in customization && customization.announcement?.enabled ? (
@@ -552,3 +552,104 @@ html.dark .highlight-line.diff-deleted .highlight-line-content::before {
.button-group > :is(button, a):not(:first-child):has(~ :is(button, a)) {
@apply rounded-none!;
}
/* Zoomable images */
html:has(.zoom-modal) {
overflow: hidden;
}
.zoom-img {
cursor: zoom-in;
}
.zoom-image-active {
view-transition-name: zoom-image;
}
.zoom-modal img {
view-transition-name: zoom-image;
cursor: zoom-out;
}
/* Header links collapse into the "more" dropdown one by one as the header narrows. Each breakpoint
hides one more link and reveals the ellipsis holding the ones that no longer fit. */
@container header (width < 1280px) {
.header-links {
container-type: inline-size;
container-name: headerlinks;
}
}
.header-links-ellipsis {
display: none;
& div > a {
display: none;
}
}
@container headerlinks (width < 150px) {
.header-links > :nth-child(n + 1) {
display: none;
}
.header-links > :nth-child(n + 1) ~ .header-links-ellipsis {
display: flex;
& div > *:nth-child(n + 1) {
display: flex;
}
}
}
@container headerlinks (width < 300px) {
.header-links > :nth-child(n + 2) {
display: none;
}
.header-links > :nth-child(n + 2) ~ .header-links-ellipsis {
display: flex;
& div > *:nth-child(n + 2) {
display: flex;
}
}
}
@container headerlinks (width < 450px) {
.header-links > :nth-child(n + 3) {
display: none;
}
.header-links > :nth-child(n + 3) ~ .header-links-ellipsis {
display: flex;
& div > *:nth-child(n + 3) {
display: flex;
}
}
}
@container headerlinks (width < 550px) {
.header-links > :nth-child(n + 4) {
display: none;
}
.header-links > :nth-child(n + 4) ~ .header-links-ellipsis {
display: flex;
& div > *:nth-child(n + 4) {
display: flex;
}
}
}
@container headerlinks (width < 650px) {
.header-links > :nth-child(n + 5) {
display: none;
}
.header-links > :nth-child(n + 5) ~ .header-links-ellipsis {
display: flex;
& div > *:nth-child(n + 5) {
display: flex;
}
}
}
@container headerlinks (width < 750px) {
.header-links > :nth-child(n + 6) {
display: none;
}
.header-links > :nth-child(n + 6) ~ .header-links-ellipsis {
display: flex;
& div > *:nth-child(n + 6) {
display: flex;
}
}
}
@@ -75,10 +75,9 @@ export function SearchContainer({
},
{
enableOnFormTags: true,
// Match the logical character typed, not the physical key position, so
// non-QWERTY layouts don't trigger the shortcut by position (e.g. on
// Dvorak the physical "K"/"I" keys produce other characters). RND-11340.
ignoreEventWhen: (e) => e.key.toLowerCase() !== 'k',
// Match the produced key, not the physical key position, so non-QWERTY
// layouts don't trigger the shortcut by position. RND-11340.
useKey: true,
}
);
@@ -94,9 +93,9 @@ export function SearchContainer({
},
{
enableOnFormTags: true,
// Match the logical character so Dvorak ⌘-C (physical "I" key) copies
// Match the produced key so Dvorak ⌘-C (physical "I" key) copies
// instead of opening the Assistant. RND-11340.
ignoreEventWhen: (e) => e.key.toLowerCase() !== 'i',
useKey: true,
}
);
@@ -46,11 +46,22 @@ export const SearchInput = React.forwardRef<HTMLDivElement, SearchInputProps>(
} = props;
const inputRef = useRef<HTMLInputElement>(null);
const isFrame = mode === 'frame';
const isInitialMountRef = useRef(true);
const language = useLanguage();
useEffect(() => {
const isInitialMount = isInitialMountRef.current;
isInitialMountRef.current = false;
if (!isOpen) {
// A user can focus the SSR'd input natively before hydration attaches
// React's focus handler; syncing to "open" instead of blurring avoids
// stealing that focus back on mount.
if (isInitialMount && document.activeElement === inputRef.current) {
onFocus?.();
return;
}
inputRef.current?.blur();
return;
}
@@ -69,7 +80,7 @@ export const SearchInput = React.forwardRef<HTMLDivElement, SearchInputProps>(
const timeout = window.setTimeout(focusInput, 150);
return () => window.clearTimeout(timeout);
}, [isFrame, isOpen, value.length]);
}, [isFrame, isOpen, value.length, onFocus]);
return (
<div
@@ -1,20 +0,0 @@
html:has(.zoomModal) {
overflow: hidden;
}
.zoomImg {
cursor: zoom-in;
}
.zoomImageActive {
view-transition-name: zoom-image;
}
.zoomModal {
/** Unstyled */
}
.zoomModal img {
view-transition-name: zoom-image;
cursor: zoom-out;
}
@@ -6,7 +6,6 @@ import ReactDOM from 'react-dom';
import { Icon } from '@gitbook/icons';
import styles from './ZoomImage.module.css';
import { tcls } from '@/lib/tailwind';
const PADDING = 32; // Padding around the image in the modal, in pixels
@@ -159,8 +158,8 @@ export function ZoomImage(
}}
className={classNames(
props.className,
zoomable ? styles.zoomImg : null,
active ? styles.zoomImageActive : null
zoomable ? 'zoom-img' : null,
active ? 'zoom-image-active' : null
)}
/>
)}
@@ -200,7 +199,7 @@ function ZoomImageModal(props: {
<div
data-testid="zoom-image-modal"
className={classNames(
styles.zoomModal,
'zoom-modal',
tcls(
'fixed',
'inset-0',
+71 -285
View File
@@ -1,297 +1,83 @@
import {
DM_Mono,
Fira_Code,
Fira_Sans_Extra_Condensed,
IBM_Plex_Mono,
IBM_Plex_Serif,
Inconsolata,
Inter,
JetBrains_Mono,
Lato,
Merriweather,
Noto_Color_Emoji,
Noto_Sans,
Open_Sans,
Overpass,
Poppins,
Raleway,
Roboto,
Roboto_Mono,
Roboto_Slab,
Source_Code_Pro,
Source_Sans_3,
Space_Mono,
Ubuntu,
} from 'next/font/google';
import localFont from 'next/font/local';
import { CustomizationDefaultMonospaceFont } from '@gitbook/api';
import { CustomizationDefaultFont, CustomizationDefaultMonospaceFont } from '@gitbook/api';
import { EMOJI_FONT, type FontName } from './definitions';
import faces from './generated/faces.json';
import type { FontFacesData, FontFamilyData } from './types';
import { getAssetURL } from '@/lib/assets';
export const fontNotoColorEmoji = Noto_Color_Emoji({
variable: '--font-noto-color-emoji',
weight: ['400'],
preload: false,
display: 'swap',
});
const fontFaces = faces as FontFacesData;
/*
Fonts are downloaded and loaded by next/font.
// The rules only vary by font, and every page renders a few dozen of them.
const cache = new Map<FontName, string>();
We can't use "preload: true" as otherwise Next will preload all the fonts on the page
while spaces only use one font at a time.
*/
/** Used until the cache has warmed up for sites saved before the setting existed. */
export const DEFAULT_MONOSPACE_FONT = CustomizationDefaultMonospaceFont.IBMPlexMono;
const inter = Inter({
weight: ['400', '500', '600', '700'],
variable: '--font-content',
preload: false,
display: 'swap',
fallback: ['system-ui', 'arial'],
});
// Emitted per picked family and inlined in the head, so a page never carries the other 20 families
// the way a shared `next/font` stylesheet did.
export function generateDefaultFontFacesCSS(font: FontName): string {
const cached = cache.get(font);
if (cached !== undefined) {
return cached;
}
const firaSans = Fira_Sans_Extra_Condensed({
weight: ['400', '500', '600', '700'],
variable: '--font-content',
preload: false,
display: 'swap',
fallback: ['system-ui', 'arial'],
});
const family = fontFaces[font];
if (!family) {
throw new Error(`Missing generated font faces for ${font}`);
}
const ibmPlexSerif = IBM_Plex_Serif({
weight: ['400', '500', '600', '700'],
variable: '--font-content',
preload: false,
display: 'swap',
fallback: ['serif'],
});
const css = [
...family.variants.flatMap((variant) =>
variant.files.map((file, subset) =>
generateFace(family, variant.weight, variant.style, file, family.subsets[subset])
)
),
generateFallbackFace(family),
`:root { ${family.variable}: ${family.fontFamilyValue}; }`,
]
.filter(Boolean)
.join('\n');
const lato = Lato({
weight: ['400', '700', '900'],
variable: '--font-content',
preload: false,
display: 'swap',
fallback: ['system-ui', 'arial'],
});
cache.set(font, css);
return css;
}
const merriweather = Merriweather({
weight: ['400', '700', '900'],
variable: '--font-content',
preload: false,
display: 'swap',
fallback: ['serif'],
});
export function generateEmojiFontFacesCSS(): string {
return generateDefaultFontFacesCSS(EMOJI_FONT);
}
const notoSans = Noto_Sans({
weight: ['400', '500', '600', '700'],
variable: '--font-content',
preload: false,
display: 'swap',
fallback: ['system-ui', 'arial'],
});
function generateFace(
family: FontFamilyData,
weight: string,
style: string,
file: string,
unicodeRange: string | undefined
): string {
return `@font-face {
font-family: "${family.family}";
font-style: ${style};
font-weight: ${weight};
font-display: swap;${family.ascentOverride ? `\n ascent-override: ${family.ascentOverride};` : ''}
src: url(${getAssetURL(`fonts/${file}`)}) format("woff2");${
unicodeRange ? `\n unicode-range: ${unicodeRange};` : ''
}
}`;
}
const openSans = Open_Sans({
weight: ['400', '500', '600', '700'],
variable: '--font-content',
preload: false,
display: 'swap',
fallback: ['system-ui', 'arial'],
});
// A `local()` face with the real font's metrics: the page keeps the system font's glyphs but the
// picked font's proportions, so swapping it in barely shifts the layout.
function generateFallbackFace(family: FontFamilyData): string {
const fallback = family.fallbackFace;
if (!fallback) {
return '';
}
const overpass = Overpass({
weight: ['400', '500', '600', '700'],
variable: '--font-content',
preload: false,
display: 'swap',
fallback: ['system-ui', 'arial'],
});
const poppins = Poppins({
weight: ['400', '500', '600', '700'],
variable: '--font-content',
preload: false,
display: 'swap',
fallback: ['system-ui', 'arial'],
});
const raleway = Raleway({
weight: ['400', '500', '600', '700'],
variable: '--font-content',
preload: false,
display: 'swap',
fallback: ['system-ui', 'arial'],
});
const roboto = Roboto({
weight: ['400', '500', '600', '700'],
variable: '--font-content',
preload: false,
display: 'swap',
fallback: ['system-ui', 'arial'],
});
const robotoSlab = Roboto_Slab({
weight: ['400', '500', '600', '700'],
variable: '--font-content',
preload: false,
display: 'swap',
fallback: ['system-ui', 'arial'],
});
const sourceSansPro = Source_Sans_3({
weight: ['400', '500', '600', '700'],
variable: '--font-content',
preload: false,
display: 'swap',
fallback: ['system-ui', 'arial'],
});
const ubuntu = Ubuntu({
weight: ['400', '500', '700'],
variable: '--font-content',
preload: false,
display: 'swap',
fallback: ['system-ui', 'arial'],
});
const abcFavorit = localFont({
variable: '--font-content',
preload: false,
display: 'swap',
fallback: ['system-ui', 'arial'],
src: [
{
path: './ABCFavorit/ABCFavorit-Variable.woff2',
weight: '400 700',
style: 'normal',
},
{
path: './ABCFavorit/ABCFavorit-BoldItalic.woff2',
weight: '700',
style: 'italic',
},
{
path: './ABCFavorit/ABCFavorit-MediumItalic.woff2',
weight: '500',
style: 'italic',
},
{
path: './ABCFavorit/ABCFavorit-RegularItalic.woff2',
weight: '400',
style: 'italic',
},
],
declarations: [{ prop: 'ascent-override', value: '100%' }],
});
const ibmPlexMono = IBM_Plex_Mono({
weight: ['400', '500', '600', '700'],
variable: '--font-mono',
style: 'normal',
display: 'swap',
preload: false,
fallback: ['monospace'],
adjustFontFallback: false,
});
const dmMono = DM_Mono({
weight: ['400', '500'],
variable: '--font-mono',
style: 'normal',
display: 'swap',
preload: false,
fallback: ['monospace'],
adjustFontFallback: false,
});
const firaCode = Fira_Code({
weight: ['400', '500', '600', '700'],
variable: '--font-mono',
style: 'normal',
display: 'swap',
preload: false,
fallback: ['monospace'],
adjustFontFallback: false,
});
const inconsolata = Inconsolata({
weight: ['400', '500', '600', '700'],
variable: '--font-mono',
style: 'normal',
display: 'swap',
preload: false,
fallback: ['monospace'],
adjustFontFallback: false,
});
const jetBrainsMono = JetBrains_Mono({
weight: ['400', '500', '600', '700'],
variable: '--font-mono',
style: 'normal',
display: 'swap',
preload: false,
fallback: ['monospace'],
adjustFontFallback: false,
});
const robotoMono = Roboto_Mono({
weight: ['400', '500', '600', '700'],
variable: '--font-mono',
style: 'normal',
display: 'swap',
preload: false,
fallback: ['monospace'],
adjustFontFallback: false,
});
const sourceCodePro = Source_Code_Pro({
weight: ['400', '500', '600', '700'],
variable: '--font-mono',
style: 'normal',
display: 'swap',
preload: false,
fallback: ['monospace'],
adjustFontFallback: false,
});
const spaceMono = Space_Mono({
weight: ['400', '700'],
variable: '--font-mono',
style: 'normal',
display: 'swap',
preload: false,
fallback: ['monospace'],
adjustFontFallback: false,
});
/**
* Font definitions.
*/
export const fonts: {
[fontName in CustomizationDefaultFont | CustomizationDefaultMonospaceFont]: {
variable: string;
};
} = {
[CustomizationDefaultFont.Inter]: inter,
[CustomizationDefaultFont.FiraSans]: firaSans,
[CustomizationDefaultFont.IBMPlexSerif]: ibmPlexSerif,
[CustomizationDefaultFont.Lato]: lato,
[CustomizationDefaultFont.Merriweather]: merriweather,
[CustomizationDefaultFont.NotoSans]: notoSans,
[CustomizationDefaultFont.OpenSans]: openSans,
[CustomizationDefaultFont.Overpass]: overpass,
[CustomizationDefaultFont.Poppins]: poppins,
[CustomizationDefaultFont.Raleway]: raleway,
[CustomizationDefaultFont.Roboto]: roboto,
[CustomizationDefaultFont.RobotoSlab]: robotoSlab,
[CustomizationDefaultFont.SourceSansPro]: sourceSansPro,
[CustomizationDefaultFont.Ubuntu]: ubuntu,
[CustomizationDefaultFont.ABCFavorit]: abcFavorit,
[CustomizationDefaultMonospaceFont.IBMPlexMono]: ibmPlexMono,
[CustomizationDefaultMonospaceFont.DMMono]: dmMono,
[CustomizationDefaultMonospaceFont.FiraCode]: firaCode,
[CustomizationDefaultMonospaceFont.Inconsolata]: inconsolata,
[CustomizationDefaultMonospaceFont.JetBrainsMono]: jetBrainsMono,
[CustomizationDefaultMonospaceFont.RobotoMono]: robotoMono,
[CustomizationDefaultMonospaceFont.SourceCodePro]: sourceCodePro,
[CustomizationDefaultMonospaceFont.SpaceMono]: spaceMono,
};
return `@font-face {
font-family: "${fallback.family}";
src: local("${fallback.local}");
ascent-override: ${fallback.ascentOverride};
descent-override: ${fallback.descentOverride};
line-gap-override: ${fallback.lineGapOverride};
size-adjust: ${fallback.sizeAdjust};
}`;
}
+178
View File
@@ -0,0 +1,178 @@
import { CustomizationDefaultFont, CustomizationDefaultMonospaceFont } from '@gitbook/api';
/** The emoji font is always loaded, alongside whichever content and monospace fonts a site picked. */
export const EMOJI_FONT = 'NotoColorEmoji';
export type FontName =
| CustomizationDefaultFont
| CustomizationDefaultMonospaceFont
| typeof EMOJI_FONT;
export interface FontDefinition {
/** `font-family` to declare the faces under. */
family: string;
/** Family id on Google Fonts, or `null` for the fonts we ship ourselves. */
googleId: string | null;
weights: string[];
/** CSS variable the family is exposed as. */
variable: '--font-content' | '--font-mono' | '--font-noto-color-emoji';
/** Families appended after the (metric-adjusted) fallback. */
fallback: string[];
/** Declare a `<family> Fallback` face with metric overrides, so the swap shifts layout as
* little as possible. Monospace fonts opt out, as they did under `next/font`. */
adjustFallback: boolean;
}
const content = (
family: string,
googleId: string,
weights: string[],
fallback: string[] = ['system-ui', 'arial']
): FontDefinition => ({
family,
googleId,
weights,
variable: '--font-content',
fallback,
adjustFallback: true,
});
const mono = (family: string, googleId: string, weights: string[]): FontDefinition => ({
family,
googleId,
weights,
variable: '--font-mono',
fallback: ['monospace'],
adjustFallback: false,
});
export const FONT_DEFINITIONS: Record<FontName, FontDefinition> = {
[CustomizationDefaultFont.Inter]: content('Inter', 'inter', ['400', '500', '600', '700']),
[CustomizationDefaultFont.FiraSans]: content(
'Fira Sans Extra Condensed',
'fira-sans-extra-condensed',
['400', '500', '600', '700']
),
[CustomizationDefaultFont.IBMPlexSerif]: content(
'IBM Plex Serif',
'ibm-plex-serif',
['400', '500', '600', '700'],
['serif']
),
[CustomizationDefaultFont.Lato]: content('Lato', 'lato', ['400', '700', '900']),
[CustomizationDefaultFont.Merriweather]: content(
'Merriweather',
'merriweather',
['400', '700', '900'],
['serif']
),
[CustomizationDefaultFont.NotoSans]: content('Noto Sans', 'noto-sans', [
'400',
'500',
'600',
'700',
]),
[CustomizationDefaultFont.OpenSans]: content('Open Sans', 'open-sans', [
'400',
'500',
'600',
'700',
]),
[CustomizationDefaultFont.Overpass]: content('Overpass', 'overpass', [
'400',
'500',
'600',
'700',
]),
[CustomizationDefaultFont.Poppins]: content('Poppins', 'poppins', ['400', '500', '600', '700']),
[CustomizationDefaultFont.Raleway]: content('Raleway', 'raleway', ['400', '500', '600', '700']),
[CustomizationDefaultFont.Roboto]: content('Roboto', 'roboto', ['400', '500', '600', '700']),
[CustomizationDefaultFont.RobotoSlab]: content('Roboto Slab', 'roboto-slab', [
'400',
'500',
'600',
'700',
]),
[CustomizationDefaultFont.SourceSansPro]: content('Source Sans 3', 'source-sans-3', [
'400',
'500',
'600',
'700',
]),
[CustomizationDefaultFont.Ubuntu]: content('Ubuntu', 'ubuntu', ['400', '500', '700']),
[CustomizationDefaultFont.ABCFavorit]: {
family: 'abcFavorit',
googleId: null,
weights: [],
variable: '--font-content',
fallback: ['system-ui', 'arial'],
adjustFallback: true,
},
[CustomizationDefaultMonospaceFont.IBMPlexMono]: mono('IBM Plex Mono', 'ibm-plex-mono', [
'400',
'500',
'600',
'700',
]),
[CustomizationDefaultMonospaceFont.DMMono]: mono('DM Mono', 'dm-mono', ['400', '500']),
[CustomizationDefaultMonospaceFont.FiraCode]: mono('Fira Code', 'fira-code', [
'400',
'500',
'600',
'700',
]),
[CustomizationDefaultMonospaceFont.Inconsolata]: mono('Inconsolata', 'inconsolata', [
'400',
'500',
'600',
'700',
]),
[CustomizationDefaultMonospaceFont.JetBrainsMono]: mono('JetBrains Mono', 'jetbrains-mono', [
'400',
'500',
'600',
'700',
]),
[CustomizationDefaultMonospaceFont.RobotoMono]: mono('Roboto Mono', 'roboto-mono', [
'400',
'500',
'600',
'700',
]),
[CustomizationDefaultMonospaceFont.SourceCodePro]: mono('Source Code Pro', 'source-code-pro', [
'400',
'500',
'600',
'700',
]),
[CustomizationDefaultMonospaceFont.SpaceMono]: mono('Space Mono', 'space-mono', ['400', '700']),
[EMOJI_FONT]: {
family: 'Noto Color Emoji',
googleId: 'noto-color-emoji',
weights: ['400'],
variable: '--font-noto-color-emoji',
fallback: [],
adjustFallback: true,
},
};
// `abcFavorit` is licensed, so it ships in the repo instead of coming from Google Fonts. The
// fallback metrics are the ones `next/font/local` measured from these exact files with fontkit.
export const ABC_FAVORIT = {
sources: [
{ file: 'ABCFavorit-Variable.woff2', weight: '400 700', style: 'normal' },
{ file: 'ABCFavorit-BoldItalic.woff2', weight: '700', style: 'italic' },
{ file: 'ABCFavorit-MediumItalic.woff2', weight: '500', style: 'italic' },
{ file: 'ABCFavorit-RegularItalic.woff2', weight: '400', style: 'italic' },
],
/** The design sits low in its em box; without this the fallback swap jumps vertically. */
ascentOverride: '100%',
fallbackMetrics: {
ascentOverride: '90.97%',
descentOverride: '37.34%',
lineGapOverride: '0.00%',
sizeAdjust: '104.43%',
},
} as const;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,341 @@
{
"google": {
"inter": {
"prefix": "https://fonts.gstatic.com/s/inter/v20",
"files": [
"UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7W0Q5n-wU.woff2",
"UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7W0Q5n-wU.woff2",
"UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7W0Q5n-wU.woff2",
"UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7W0Q5n-wU.woff2",
"UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7W0Q5n-wU.woff2",
"UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7W0Q5n-wU.woff2",
"UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7W0Q5nw.woff2"
]
},
"fira-sans-extra-condensed": {
"prefix": "https://fonts.gstatic.com/s/firasansextracondensed/v11",
"files": [
"NaPKcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda1fKuulWcrE5Hcg.woff2",
"NaPKcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda1fuuulWcrE5Hcg.woff2",
"NaPKcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda1fOuulWcrE5Hcg.woff2",
"NaPKcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda1fyuulWcrE5Hcg.woff2",
"NaPKcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda1fCuulWcrE5Hcg.woff2",
"NaPKcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda1fGuulWcrE5Hcg.woff2",
"NaPKcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda1f-uulWcrE4.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3QyNr3W-oWR9e2WPJQ.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3QyNr3y-oWR9e2WPJQ.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3QyNr3S-oWR9e2WPJQ.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3QyNr3u-oWR9e2WPJQ.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3QyNr3e-oWR9e2WPJQ.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3QyNr3a-oWR9e2WPJQ.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3QyNr3i-oWR9e2U.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3SCKr3W-oWR9e2WPJQ.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3SCKr3y-oWR9e2WPJQ.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3SCKr3S-oWR9e2WPJQ.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3SCKr3u-oWR9e2WPJQ.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3SCKr3e-oWR9e2WPJQ.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3SCKr3a-oWR9e2WPJQ.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3SCKr3i-oWR9e2U.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3USLr3W-oWR9e2WPJQ.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3USLr3y-oWR9e2WPJQ.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3USLr3S-oWR9e2WPJQ.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3USLr3u-oWR9e2WPJQ.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3USLr3e-oWR9e2WPJQ.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3USLr3a-oWR9e2WPJQ.woff2",
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3USLr3i-oWR9e2U.woff2"
]
},
"ibm-plex-serif": {
"prefix": "https://fonts.gstatic.com/s/ibmplexserif/v20",
"files": [
"jizDREVNn1dOx-zrZ2X3pZvkTiUS2zcZiVbJsNo.woff2",
"jizDREVNn1dOx-zrZ2X3pZvkTiUb2zcZiVbJsNo.woff2",
"jizDREVNn1dOx-zrZ2X3pZvkTiUQ2zcZiVbJsNo.woff2",
"jizDREVNn1dOx-zrZ2X3pZvkTiUR2zcZiVbJsNo.woff2",
"jizDREVNn1dOx-zrZ2X3pZvkTiUf2zcZiVbJ.woff2",
"jizAREVNn1dOx-zrZ2X3pZvkTi3s-CI5q1vjitOh3oc.woff2",
"jizAREVNn1dOx-zrZ2X3pZvkTi3s-CIwq1vjitOh3oc.woff2",
"jizAREVNn1dOx-zrZ2X3pZvkTi3s-CI7q1vjitOh3oc.woff2",
"jizAREVNn1dOx-zrZ2X3pZvkTi3s-CI6q1vjitOh3oc.woff2",
"jizAREVNn1dOx-zrZ2X3pZvkTi3s-CI0q1vjitOh.woff2",
"jizAREVNn1dOx-zrZ2X3pZvkTi3A_yI5q1vjitOh3oc.woff2",
"jizAREVNn1dOx-zrZ2X3pZvkTi3A_yIwq1vjitOh3oc.woff2",
"jizAREVNn1dOx-zrZ2X3pZvkTi3A_yI7q1vjitOh3oc.woff2",
"jizAREVNn1dOx-zrZ2X3pZvkTi3A_yI6q1vjitOh3oc.woff2",
"jizAREVNn1dOx-zrZ2X3pZvkTi3A_yI0q1vjitOh.woff2",
"jizAREVNn1dOx-zrZ2X3pZvkTi2k_iI5q1vjitOh3oc.woff2",
"jizAREVNn1dOx-zrZ2X3pZvkTi2k_iIwq1vjitOh3oc.woff2",
"jizAREVNn1dOx-zrZ2X3pZvkTi2k_iI7q1vjitOh3oc.woff2",
"jizAREVNn1dOx-zrZ2X3pZvkTi2k_iI6q1vjitOh3oc.woff2",
"jizAREVNn1dOx-zrZ2X3pZvkTi2k_iI0q1vjitOh.woff2"
]
},
"lato": {
"prefix": "https://fonts.gstatic.com/s/lato/v25",
"files": [
"S6uyw4BMUTPHjxAwXiWtFCfQ7A.woff2",
"S6uyw4BMUTPHjx4wXiWtFCc.woff2",
"S6u9w4BMUTPHh6UVSwaPGQ3q5d0N7w.woff2",
"S6u9w4BMUTPHh6UVSwiPGQ3q5d0.woff2",
"S6u9w4BMUTPHh50XSwaPGQ3q5d0N7w.woff2",
"S6u9w4BMUTPHh50XSwiPGQ3q5d0.woff2"
]
},
"merriweather": {
"prefix": "https://fonts.gstatic.com/s/merriweather/v33",
"files": [
"u-4e0qyriQwlOrhSvowK_l5UcA6zuSYEqOzpPe3HOZJ5eX1WtLaQwmYiSeqnJ-mXq1Gi3iE.woff2",
"u-4e0qyriQwlOrhSvowK_l5UcA6zuSYEqOzpPe3HOZJ5eX1WtLaQwmYiSequJ-mXq1Gi3iE.woff2",
"u-4e0qyriQwlOrhSvowK_l5UcA6zuSYEqOzpPe3HOZJ5eX1WtLaQwmYiSeqlJ-mXq1Gi3iE.woff2",
"u-4e0qyriQwlOrhSvowK_l5UcA6zuSYEqOzpPe3HOZJ5eX1WtLaQwmYiSeqkJ-mXq1Gi3iE.woff2",
"u-4e0qyriQwlOrhSvowK_l5UcA6zuSYEqOzpPe3HOZJ5eX1WtLaQwmYiSeqqJ-mXq1Gi.woff2"
]
},
"noto-sans": {
"prefix": "https://fonts.gstatic.com/s/notosans/v42",
"files": [
"o-0bIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjc5aPdu3mhPy1Fig.woff2",
"o-0bIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjc5ardu3mhPy1Fig.woff2",
"o-0bIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjc5a_du3mhPy1Fig.woff2",
"o-0bIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjc5aLdu3mhPy1Fig.woff2",
"o-0bIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjc5a3du3mhPy1Fig.woff2",
"o-0bIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjc5aHdu3mhPy1Fig.woff2",
"o-0bIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjc5aDdu3mhPy1Fig.woff2",
"o-0bIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjc5a7du3mhPy0.woff2"
]
},
"open-sans": {
"prefix": "https://fonts.gstatic.com/s/opensans/v44",
"files": [
"memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSKmu0SC55K5gw.woff2",
"memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSumu0SC55K5gw.woff2",
"memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSOmu0SC55K5gw.woff2",
"memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSymu0SC55K5gw.woff2",
"memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS2mu0SC55K5gw.woff2",
"memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTVOmu0SC55K5gw.woff2",
"memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTUGmu0SC55K5gw.woff2",
"memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSCmu0SC55K5gw.woff2",
"memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSGmu0SC55K5gw.woff2",
"memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-mu0SC55I.woff2"
]
},
"overpass": {
"prefix": "https://fonts.gstatic.com/s/overpass/v19",
"files": [
"qFdH35WCmI96Ajtm81GoU9vgwBcIs1s.woff2",
"qFdH35WCmI96Ajtm81GhU9vgwBcIs1s.woff2",
"qFdH35WCmI96Ajtm81GqU9vgwBcIs1s.woff2",
"qFdH35WCmI96Ajtm81GrU9vgwBcIs1s.woff2",
"qFdH35WCmI96Ajtm81GlU9vgwBcI.woff2"
]
},
"poppins": {
"prefix": "https://fonts.gstatic.com/s/poppins/v24",
"files": [
"pxiEyp8kv8JHgFVrJJbecnFHGPezSQ.woff2",
"pxiEyp8kv8JHgFVrJJnecnFHGPezSQ.woff2",
"pxiEyp8kv8JHgFVrJJfecnFHGPc.woff2",
"pxiByp8kv8JHgFVrLGT9Z11lFd2JQEl8qw.woff2",
"pxiByp8kv8JHgFVrLGT9Z1JlFd2JQEl8qw.woff2",
"pxiByp8kv8JHgFVrLGT9Z1xlFd2JQEk.woff2",
"pxiByp8kv8JHgFVrLEj6Z11lFd2JQEl8qw.woff2",
"pxiByp8kv8JHgFVrLEj6Z1JlFd2JQEl8qw.woff2",
"pxiByp8kv8JHgFVrLEj6Z1xlFd2JQEk.woff2",
"pxiByp8kv8JHgFVrLCz7Z11lFd2JQEl8qw.woff2",
"pxiByp8kv8JHgFVrLCz7Z1JlFd2JQEl8qw.woff2",
"pxiByp8kv8JHgFVrLCz7Z1xlFd2JQEk.woff2"
]
},
"raleway": {
"prefix": "https://fonts.gstatic.com/s/raleway/v37",
"files": [
"1Ptug8zYS_SKggPNyCAIT4ttDfCmxA.woff2",
"1Ptug8zYS_SKggPNyCkIT4ttDfCmxA.woff2",
"1Ptug8zYS_SKggPNyCIIT4ttDfCmxA.woff2",
"1Ptug8zYS_SKggPNyCMIT4ttDfCmxA.woff2",
"1Ptug8zYS_SKggPNyC0IT4ttDfA.woff2"
]
},
"roboto": {
"prefix": "https://fonts.gstatic.com/s/roboto/v51",
"files": [
"KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3GUBHMdazTgWw.woff2",
"KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3iUBHMdazTgWw.woff2",
"KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3CUBHMdazTgWw.woff2",
"KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3-UBHMdazTgWw.woff2",
"KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMawCUBHMdazTgWw.woff2",
"KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMaxKUBHMdazTgWw.woff2",
"KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3OUBHMdazTgWw.woff2",
"KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3KUBHMdazTgWw.woff2",
"KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3yUBHMdazQ.woff2"
]
},
"roboto-slab": {
"prefix": "https://fonts.gstatic.com/s/robotoslab/v36",
"files": [
"BngMUXZYTXPIvIBgJJSb6ufA5qWr4xCCQ_k.woff2",
"BngMUXZYTXPIvIBgJJSb6ufJ5qWr4xCCQ_k.woff2",
"BngMUXZYTXPIvIBgJJSb6ufB5qWr4xCCQ_k.woff2",
"BngMUXZYTXPIvIBgJJSb6ufO5qWr4xCCQ_k.woff2",
"BngMUXZYTXPIvIBgJJSb6ufC5qWr4xCCQ_k.woff2",
"BngMUXZYTXPIvIBgJJSb6ufD5qWr4xCCQ_k.woff2",
"BngMUXZYTXPIvIBgJJSb6ufN5qWr4xCC.woff2"
]
},
"source-sans-3": {
"prefix": "https://fonts.gstatic.com/s/sourcesans3/v19",
"files": [
"nwpStKy2OAdR1K-IwhWudF-R3wIaZejf5HdF8Q.woff2",
"nwpStKy2OAdR1K-IwhWudF-R3wsaZejf5HdF8Q.woff2",
"nwpStKy2OAdR1K-IwhWudF-R3wMaZejf5HdF8Q.woff2",
"nwpStKy2OAdR1K-IwhWudF-R3wwaZejf5HdF8Q.woff2",
"nwpStKy2OAdR1K-IwhWudF-R3wAaZejf5HdF8Q.woff2",
"nwpStKy2OAdR1K-IwhWudF-R3wEaZejf5HdF8Q.woff2",
"nwpStKy2OAdR1K-IwhWudF-R3w8aZejf5Hc.woff2"
]
},
"ubuntu": {
"prefix": "https://fonts.gstatic.com/s/ubuntu/v21",
"files": [
"4iCs6KVjbNBYlgoKcg72nU6AF7xm.woff2",
"4iCs6KVjbNBYlgoKew72nU6AF7xm.woff2",
"4iCs6KVjbNBYlgoKcw72nU6AF7xm.woff2",
"4iCs6KVjbNBYlgoKfA72nU6AF7xm.woff2",
"4iCs6KVjbNBYlgoKcQ72nU6AF7xm.woff2",
"4iCs6KVjbNBYlgoKfw72nU6AFw.woff2",
"4iCv6KVjbNBYlgoCjC3jvWyNPYZvg7UI.woff2",
"4iCv6KVjbNBYlgoCjC3jtGyNPYZvg7UI.woff2",
"4iCv6KVjbNBYlgoCjC3jvGyNPYZvg7UI.woff2",
"4iCv6KVjbNBYlgoCjC3js2yNPYZvg7UI.woff2",
"4iCv6KVjbNBYlgoCjC3jvmyNPYZvg7UI.woff2",
"4iCv6KVjbNBYlgoCjC3jsGyNPYZvgw.woff2",
"4iCv6KVjbNBYlgoCxCvjvWyNPYZvg7UI.woff2",
"4iCv6KVjbNBYlgoCxCvjtGyNPYZvg7UI.woff2",
"4iCv6KVjbNBYlgoCxCvjvGyNPYZvg7UI.woff2",
"4iCv6KVjbNBYlgoCxCvjs2yNPYZvg7UI.woff2",
"4iCv6KVjbNBYlgoCxCvjvmyNPYZvg7UI.woff2",
"4iCv6KVjbNBYlgoCxCvjsGyNPYZvgw.woff2"
]
},
"ibm-plex-mono": {
"prefix": "https://fonts.gstatic.com/s/ibmplexmono/v20",
"files": [
"-F63fjptAgt5VM-kVkqdyU8n1iIq131nj-otFQ.woff2",
"-F63fjptAgt5VM-kVkqdyU8n1isq131nj-otFQ.woff2",
"-F63fjptAgt5VM-kVkqdyU8n1iAq131nj-otFQ.woff2",
"-F63fjptAgt5VM-kVkqdyU8n1iEq131nj-otFQ.woff2",
"-F63fjptAgt5VM-kVkqdyU8n1i8q131nj-o.woff2",
"-F6qfjptAgt5VM-kVkqdyU8n3twJwl1FgsAXHNlYzg.woff2",
"-F6qfjptAgt5VM-kVkqdyU8n3twJwlRFgsAXHNlYzg.woff2",
"-F6qfjptAgt5VM-kVkqdyU8n3twJwl9FgsAXHNlYzg.woff2",
"-F6qfjptAgt5VM-kVkqdyU8n3twJwl5FgsAXHNlYzg.woff2",
"-F6qfjptAgt5VM-kVkqdyU8n3twJwlBFgsAXHNk.woff2",
"-F6qfjptAgt5VM-kVkqdyU8n3vAOwl1FgsAXHNlYzg.woff2",
"-F6qfjptAgt5VM-kVkqdyU8n3vAOwlRFgsAXHNlYzg.woff2",
"-F6qfjptAgt5VM-kVkqdyU8n3vAOwl9FgsAXHNlYzg.woff2",
"-F6qfjptAgt5VM-kVkqdyU8n3vAOwl5FgsAXHNlYzg.woff2",
"-F6qfjptAgt5VM-kVkqdyU8n3vAOwlBFgsAXHNk.woff2",
"-F6qfjptAgt5VM-kVkqdyU8n3pQPwl1FgsAXHNlYzg.woff2",
"-F6qfjptAgt5VM-kVkqdyU8n3pQPwlRFgsAXHNlYzg.woff2",
"-F6qfjptAgt5VM-kVkqdyU8n3pQPwl9FgsAXHNlYzg.woff2",
"-F6qfjptAgt5VM-kVkqdyU8n3pQPwl5FgsAXHNlYzg.woff2",
"-F6qfjptAgt5VM-kVkqdyU8n3pQPwlBFgsAXHNk.woff2"
]
},
"dm-mono": {
"prefix": "https://fonts.gstatic.com/s/dmmono/v16",
"files": [
"aFTU7PB1QTsUX8KYthSQBK6PYK3EXw.woff2",
"aFTU7PB1QTsUX8KYthqQBK6PYK0.woff2",
"aFTR7PB1QTsUX8KYvumzEY2tbYf-Vlh3uA.woff2",
"aFTR7PB1QTsUX8KYvumzEYOtbYf-Vlg.woff2"
]
},
"fira-code": {
"prefix": "https://fonts.gstatic.com/s/firacode/v27",
"files": [
"uU9NCBsR6Z2vfE9aq3bh0NSDqFGedCMX.woff2",
"uU9NCBsR6Z2vfE9aq3bh2dSDqFGedCMX.woff2",
"uU9NCBsR6Z2vfE9aq3bh0dSDqFGedCMX.woff2",
"uU9NCBsR6Z2vfE9aq3bh3tSDqFGedCMX.woff2",
"uU9NCBsR6Z2vfE9aq3bhZ_Wmh3mUfBsu_Q.woff2",
"uU9NCBsR6Z2vfE9aq3bh09SDqFGedCMX.woff2",
"uU9NCBsR6Z2vfE9aq3bh3dSDqFGedA.woff2"
]
},
"inconsolata": {
"prefix": "https://fonts.gstatic.com/s/inconsolata/v37",
"files": [
"QlddNThLqRwH-OJ1UHjlKENVzkWGVkL3GZQmAwLyxq15IDhunJ_o.woff2",
"QlddNThLqRwH-OJ1UHjlKENVzkWGVkL3GZQmAwLyx615IDhunJ_o.woff2",
"QlddNThLqRwH-OJ1UHjlKENVzkWGVkL3GZQmAwLyya15IDhunA.woff2"
]
},
"jetbrains-mono": {
"prefix": "https://fonts.gstatic.com/s/jetbrainsmono/v24",
"files": [
"tDbv2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKwBNntkaToggR7BYRbKPx3cwgknk-6nFg.woff2",
"tDbv2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKwBNntkaToggR7BYRbKPxTcwgknk-6nFg.woff2",
"tDbv2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKwBNntkaToggR7BYRbKPxPcwgknk-6nFg.woff2",
"tDbv2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKwBNntkaToggR7BYRbKPx_cwgknk-6nFg.woff2",
"tDbv2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKwBNntkaToggR7BYRbKPx7cwgknk-6nFg.woff2",
"tDbv2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKwBNntkaToggR7BYRbKPxDcwgknk-4.woff2"
]
},
"roboto-mono": {
"prefix": "https://fonts.gstatic.com/s/robotomono/v31",
"files": [
"L0x5DF4xlVMF-BfR8bXMIjhGq3-cXbKDO1w.woff2",
"L0x5DF4xlVMF-BfR8bXMIjhPq3-cXbKDO1w.woff2",
"L0x5DF4xlVMF-BfR8bXMIjhIq3-cXbKDO1w.woff2",
"L0x5DF4xlVMF-BfR8bXMIjhEq3-cXbKDO1w.woff2",
"L0x5DF4xlVMF-BfR8bXMIjhFq3-cXbKDO1w.woff2",
"L0x5DF4xlVMF-BfR8bXMIjhLq3-cXbKD.woff2"
]
},
"source-code-pro": {
"prefix": "https://fonts.gstatic.com/s/sourcecodepro/v31",
"files": [
"HI_SiYsKILxRpg3hIP6sJ7fM7PqlMOvWnsUnxlC9.woff2",
"HI_SiYsKILxRpg3hIP6sJ7fM7PqlOevWnsUnxlC9.woff2",
"HI_SiYsKILxRpg3hIP6sJ7fM7PqlMevWnsUnxlC9.woff2",
"HI_SiYsKILxRpg3hIP6sJ7fM7PqlPuvWnsUnxlC9.woff2",
"HI_SiYsKILxRpg3hIP6sJ7fM7PqlMuvWnsUnxlC9.woff2",
"HI_SiYsKILxRpg3hIP6sJ7fM7PqlM-vWnsUnxlC9.woff2",
"HI_SiYsKILxRpg3hIP6sJ7fM7PqlPevWnsUnxg.woff2"
]
},
"space-mono": {
"prefix": "https://fonts.gstatic.com/s/spacemono/v17",
"files": [
"i7dPIFZifjKcF5UAWdDRYE58RXi4EwSsbg.woff2",
"i7dPIFZifjKcF5UAWdDRYE98RXi4EwSsbg.woff2",
"i7dPIFZifjKcF5UAWdDRYEF8RXi4EwQ.woff2",
"i7dMIFZifjKcF5UAWdDRaPpZUFqaHi6WZ3S_Yg.woff2",
"i7dMIFZifjKcF5UAWdDRaPpZUFuaHi6WZ3S_Yg.woff2",
"i7dMIFZifjKcF5UAWdDRaPpZUFWaHi6WZ3Q.woff2"
]
},
"noto-color-emoji": {
"prefix": "https://fonts.gstatic.com/s/notocoloremoji/v40",
"files": [
"Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabts6diysYTngZPnMC1MfLd4gw.0.woff2",
"Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabts6diysYTngZPnMC1MfLd4gw.1.woff2",
"Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabts6diysYTngZPnMC1MfLd4gw.2.woff2",
"Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabts6diysYTngZPnMC1MfLd4gw.3.woff2",
"Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabts6diysYTngZPnMC1MfLd4gw.4.woff2",
"Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabts6diysYTngZPnMC1MfLd4gw.5.woff2",
"Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabts6diysYTngZPnMC1MfLd4gw.6.woff2",
"Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabts6diysYTngZPnMC1MfLd4gw.7.woff2",
"Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabts6diysYTngZPnMC1MfLd4gw.8.woff2",
"Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabts6diysYTngZPnMC1MfLd4gw.9.woff2"
]
}
},
"local": {
"abcfavorit/7bb31c73cb8d820e.woff2": "./ABCFavorit/ABCFavorit-Variable.woff2",
"abcfavorit/aed849de6e333605.woff2": "./ABCFavorit/ABCFavorit-BoldItalic.woff2",
"abcfavorit/b435fb7330f5e171.woff2": "./ABCFavorit/ABCFavorit-MediumItalic.woff2",
"abcfavorit/aee27c224ccd5167.woff2": "./ABCFavorit/ABCFavorit-RegularItalic.woff2"
}
}
+12 -7
View File
@@ -5,27 +5,32 @@ import type {
} from '@gitbook/api';
import { generateFontFacesCSS, getFontSourcesToPreload } from './custom';
import { fonts } from './default';
import { generateDefaultFontFacesCSS } from './default';
export { DEFAULT_MONOSPACE_FONT, generateEmojiFontFacesCSS } from './default';
/**
* Represents font data for either a default font or a custom font
*/
export type FontData = DefaultFontData | CustomFontData;
interface BaseFontData {
/** `@font-face` rules and the CSS variable, to inline in the document head. */
fontFaceRules: string;
}
/**
* Font data for a default font, currently handle with next/font
* Font data for a default font, self-hosted from our own assets
*/
interface DefaultFontData {
interface DefaultFontData extends BaseFontData {
type: 'default';
variable: string;
}
/**
* Font data for a custom font with @font-face rules
*/
interface CustomFontData {
interface CustomFontData extends BaseFontData {
type: 'custom';
fontFaceRules: string;
preloadSources: CustomizationFontDefinition['fontFaces'];
}
@@ -39,7 +44,7 @@ export function getFontData(
if (typeof font === 'string') {
return {
type: 'default',
variable: fonts[font].variable,
fontFaceRules: generateDefaultFontFacesCSS(font),
};
}
+36
View File
@@ -0,0 +1,36 @@
export interface FontVariantData {
weight: string;
style: string;
/** Path under `~gitbook/static/fonts`, index-aligned with the family's `subsets`. */
files: string[];
}
export interface FontFallbackFaceData {
family: string;
local: string;
ascentOverride: string;
descentOverride: string;
lineGapOverride: string;
sizeAdjust: string;
}
export interface FontFamilyData {
family: string;
variable: string;
/** Full value for the CSS variable, fallbacks included. */
fontFamilyValue: string;
/** `unicode-range` per subset. Held once per family rather than repeated on every variant. */
subsets: string[];
variants: FontVariantData[];
fallbackFace: FontFallbackFaceData | null;
/** Applied to every face of the family. */
ascentOverride?: string;
}
export type FontFacesData = Record<string, FontFamilyData>;
/** Where `scripts/download-fonts.ts` fetches (or copies) each file from. */
export interface FontSourcesData {
google: Record<string, { prefix: string; files: string[] }>;
local: Record<string, string>;
}
+3 -1
View File
@@ -5,7 +5,9 @@
"outputs": [
"public/~gitbook/static/icons/**/*",
"public/~gitbook/static/math/**/*",
"public/~gitbook/static/embed/**/*"
"public/~gitbook/static/embed/**/*",
"public/~gitbook/static/css/**/*",
"public/~gitbook/static/fonts/**/*"
],
"dependsOn": ["^generate", "@gitbook/embed#build"]
},