"Datalists",
"Deque",
"discoverability",
- "docsearch",
"docsref",
"dropend",
"dropleft",
versioning-strategy: increase
rebase-strategy: disabled
ignore:
- - dependency-name: "@docsearch/js"
- update-types:
- - "version-update:semver-major"
- - "version-update:semver-minor"
- dependency-name: "karma-browserstack-launcher"
update-types:
- "version-update:semver-major"
{
"reject": [
- "@docsearch/js",
"karma-browserstack-launcher",
"karma-rollup-preprocessor",
"stylelint"
Bootstrap’s documentation, included in this repo in the root directory, is built with [Astro](https://astro.build/) and publicly hosted on GitHub Pages at <https://getbootstrap.com/>. The docs may also be run locally.
-Documentation search is powered by [Algolia's DocSearch](https://docsearch.algolia.com/).
+Documentation search is powered by [Pagefind](https://pagefind.app/).
### Running documentation locally
analytics:
fathom_site: "ITUSEYJG"
-algolia:
- app_id: "AK7KMZKZHQ"
- api_key: "3151f502c7b9e9dafd5e6372b691a24e"
- index_name: "bootstrap"
-
download:
source: "https://github.com/twbs/bootstrap/archive/v6.0.0-alpha1.zip"
dist: "https://github.com/twbs/bootstrap/releases/download/v6.0.0-alpha1/bootstrap-6.0.0-alpha1-dist.zip"
"@babel/cli": "^7.28.6",
"@babel/core": "^7.29.0",
"@babel/preset-env": "^7.29.2",
- "@docsearch/js": "3.9.0",
"@floating-ui/dom": "^1.7.6",
+ "@pagefind/component-ui": "^1.5.2",
"@rollup/plugin-babel": "^7.0.0",
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-replace": "^6.0.3",
"mime": "^4.1.0",
"nodemon": "^3.1.14",
"npm-run-all2": "^8.0.4",
+ "pagefind": "^1.5.0",
"playwright": "^1.59.1",
"postcss": "^8.5.10",
"postcss-cli": "^11.0.1",
"vanilla-calendar-pro": "^3.1.0"
}
},
- "node_modules/@algolia/abtesting": {
- "version": "1.12.2",
- "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.12.2.tgz",
- "integrity": "sha512-oWknd6wpfNrmRcH0vzed3UPX0i17o4kYLM5OMITyMVM2xLgaRbIafoxL0e8mcrNNb0iORCJA0evnNDKRYth5WQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@algolia/client-common": "5.46.2",
- "@algolia/requester-browser-xhr": "5.46.2",
- "@algolia/requester-fetch": "5.46.2",
- "@algolia/requester-node-http": "5.46.2"
- },
- "engines": {
- "node": ">= 14.0.0"
- }
- },
- "node_modules/@algolia/autocomplete-core": {
- "version": "1.17.9",
- "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.17.9.tgz",
- "integrity": "sha512-O7BxrpLDPJWWHv/DLA9DRFWs+iY1uOJZkqUwjS5HSZAGcl0hIVCQ97LTLewiZmZ402JYUrun+8NqFP+hCknlbQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@algolia/autocomplete-plugin-algolia-insights": "1.17.9",
- "@algolia/autocomplete-shared": "1.17.9"
- }
- },
- "node_modules/@algolia/autocomplete-plugin-algolia-insights": {
- "version": "1.17.9",
- "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.9.tgz",
- "integrity": "sha512-u1fEHkCbWF92DBeB/KHeMacsjsoI0wFhjZtlCq2ddZbAehshbZST6Hs0Avkc0s+4UyBGbMDnSuXHLuvRWK5iDQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@algolia/autocomplete-shared": "1.17.9"
- },
- "peerDependencies": {
- "search-insights": ">= 1 < 3"
- }
- },
- "node_modules/@algolia/autocomplete-preset-algolia": {
- "version": "1.17.9",
- "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.9.tgz",
- "integrity": "sha512-Na1OuceSJeg8j7ZWn5ssMu/Ax3amtOwk76u4h5J4eK2Nx2KB5qt0Z4cOapCsxot9VcEN11ADV5aUSlQF4RhGjQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@algolia/autocomplete-shared": "1.17.9"
- },
- "peerDependencies": {
- "@algolia/client-search": ">= 4.9.1 < 6",
- "algoliasearch": ">= 4.9.1 < 6"
- }
- },
- "node_modules/@algolia/autocomplete-shared": {
- "version": "1.17.9",
- "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.9.tgz",
- "integrity": "sha512-iDf05JDQ7I0b7JEA/9IektxN/80a2MZ1ToohfmNS3rfeuQnIKI3IJlIafD0xu4StbtQTghx9T3Maa97ytkXenQ==",
- "dev": true,
- "license": "MIT",
- "peerDependencies": {
- "@algolia/client-search": ">= 4.9.1 < 6",
- "algoliasearch": ">= 4.9.1 < 6"
- }
- },
- "node_modules/@algolia/client-abtesting": {
- "version": "5.46.2",
- "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.46.2.tgz",
- "integrity": "sha512-oRSUHbylGIuxrlzdPA8FPJuwrLLRavOhAmFGgdAvMcX47XsyM+IOGa9tc7/K5SPvBqn4nhppOCEz7BrzOPWc4A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@algolia/client-common": "5.46.2",
- "@algolia/requester-browser-xhr": "5.46.2",
- "@algolia/requester-fetch": "5.46.2",
- "@algolia/requester-node-http": "5.46.2"
- },
- "engines": {
- "node": ">= 14.0.0"
- }
- },
- "node_modules/@algolia/client-analytics": {
- "version": "5.46.2",
- "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.46.2.tgz",
- "integrity": "sha512-EPBN2Oruw0maWOF4OgGPfioTvd+gmiNwx0HmD9IgmlS+l75DatcBkKOPNJN+0z3wBQWUO5oq602ATxIfmTQ8bA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@algolia/client-common": "5.46.2",
- "@algolia/requester-browser-xhr": "5.46.2",
- "@algolia/requester-fetch": "5.46.2",
- "@algolia/requester-node-http": "5.46.2"
- },
- "engines": {
- "node": ">= 14.0.0"
- }
- },
- "node_modules/@algolia/client-common": {
- "version": "5.46.2",
- "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.46.2.tgz",
- "integrity": "sha512-Hj8gswSJNKZ0oyd0wWissqyasm+wTz1oIsv5ZmLarzOZAp3vFEda8bpDQ8PUhO+DfkbiLyVnAxsPe4cGzWtqkg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 14.0.0"
- }
- },
- "node_modules/@algolia/client-insights": {
- "version": "5.46.2",
- "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.46.2.tgz",
- "integrity": "sha512-6dBZko2jt8FmQcHCbmNLB0kCV079Mx/DJcySTL3wirgDBUH7xhY1pOuUTLMiGkqM5D8moVZTvTdRKZUJRkrwBA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@algolia/client-common": "5.46.2",
- "@algolia/requester-browser-xhr": "5.46.2",
- "@algolia/requester-fetch": "5.46.2",
- "@algolia/requester-node-http": "5.46.2"
- },
- "engines": {
- "node": ">= 14.0.0"
- }
- },
- "node_modules/@algolia/client-personalization": {
- "version": "5.46.2",
- "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.46.2.tgz",
- "integrity": "sha512-1waE2Uqh/PHNeDXGn/PM/WrmYOBiUGSVxAWqiJIj73jqPqvfzZgzdakHscIVaDl6Cp+j5dwjsZ5LCgaUr6DtmA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@algolia/client-common": "5.46.2",
- "@algolia/requester-browser-xhr": "5.46.2",
- "@algolia/requester-fetch": "5.46.2",
- "@algolia/requester-node-http": "5.46.2"
- },
- "engines": {
- "node": ">= 14.0.0"
- }
- },
- "node_modules/@algolia/client-query-suggestions": {
- "version": "5.46.2",
- "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.46.2.tgz",
- "integrity": "sha512-EgOzTZkyDcNL6DV0V/24+oBJ+hKo0wNgyrOX/mePBM9bc9huHxIY2352sXmoZ648JXXY2x//V1kropF/Spx83w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@algolia/client-common": "5.46.2",
- "@algolia/requester-browser-xhr": "5.46.2",
- "@algolia/requester-fetch": "5.46.2",
- "@algolia/requester-node-http": "5.46.2"
- },
- "engines": {
- "node": ">= 14.0.0"
- }
- },
- "node_modules/@algolia/client-search": {
- "version": "5.46.2",
- "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.46.2.tgz",
- "integrity": "sha512-ZsOJqu4HOG5BlvIFnMU0YKjQ9ZI6r3C31dg2jk5kMWPSdhJpYL9xa5hEe7aieE+707dXeMI4ej3diy6mXdZpgA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@algolia/client-common": "5.46.2",
- "@algolia/requester-browser-xhr": "5.46.2",
- "@algolia/requester-fetch": "5.46.2",
- "@algolia/requester-node-http": "5.46.2"
- },
- "engines": {
- "node": ">= 14.0.0"
- }
- },
- "node_modules/@algolia/ingestion": {
- "version": "1.46.2",
- "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.46.2.tgz",
- "integrity": "sha512-1Uw2OslTWiOFDtt83y0bGiErJYy5MizadV0nHnOoHFWMoDqWW0kQoMFI65pXqRSkVvit5zjXSLik2xMiyQJDWQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@algolia/client-common": "5.46.2",
- "@algolia/requester-browser-xhr": "5.46.2",
- "@algolia/requester-fetch": "5.46.2",
- "@algolia/requester-node-http": "5.46.2"
- },
- "engines": {
- "node": ">= 14.0.0"
- }
- },
- "node_modules/@algolia/monitoring": {
- "version": "1.46.2",
- "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.46.2.tgz",
- "integrity": "sha512-xk9f+DPtNcddWN6E7n1hyNNsATBCHIqAvVGG2EAGHJc4AFYL18uM/kMTiOKXE/LKDPyy1JhIerrh9oYb7RBrgw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@algolia/client-common": "5.46.2",
- "@algolia/requester-browser-xhr": "5.46.2",
- "@algolia/requester-fetch": "5.46.2",
- "@algolia/requester-node-http": "5.46.2"
- },
- "engines": {
- "node": ">= 14.0.0"
- }
- },
- "node_modules/@algolia/recommend": {
- "version": "5.46.2",
- "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.46.2.tgz",
- "integrity": "sha512-NApbTPj9LxGzNw4dYnZmj2BoXiAc8NmbbH6qBNzQgXklGklt/xldTvu+FACN6ltFsTzoNU6j2mWNlHQTKGC5+Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@algolia/client-common": "5.46.2",
- "@algolia/requester-browser-xhr": "5.46.2",
- "@algolia/requester-fetch": "5.46.2",
- "@algolia/requester-node-http": "5.46.2"
- },
- "engines": {
- "node": ">= 14.0.0"
- }
- },
- "node_modules/@algolia/requester-browser-xhr": {
- "version": "5.46.2",
- "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.46.2.tgz",
- "integrity": "sha512-ekotpCwpSp033DIIrsTpYlGUCF6momkgupRV/FA3m62SreTSZUKjgK6VTNyG7TtYfq9YFm/pnh65bATP/ZWJEg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@algolia/client-common": "5.46.2"
- },
- "engines": {
- "node": ">= 14.0.0"
- }
- },
- "node_modules/@algolia/requester-fetch": {
- "version": "5.46.2",
- "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.46.2.tgz",
- "integrity": "sha512-gKE+ZFi/6y7saTr34wS0SqYFDcjHW4Wminv8PDZEi0/mE99+hSrbKgJWxo2ztb5eqGirQTgIh1AMVacGGWM1iw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@algolia/client-common": "5.46.2"
- },
- "engines": {
- "node": ">= 14.0.0"
- }
- },
- "node_modules/@algolia/requester-node-http": {
- "version": "5.46.2",
- "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.46.2.tgz",
- "integrity": "sha512-ciPihkletp7ttweJ8Zt+GukSVLp2ANJHU+9ttiSxsJZThXc4Y2yJ8HGVWesW5jN1zrsZsezN71KrMx/iZsOYpg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@algolia/client-common": "5.46.2"
- },
- "engines": {
- "node": ">= 14.0.0"
- }
- },
"node_modules/@astrojs/check": {
"version": "0.9.8",
"resolved": "https://registry.npmjs.org/@astrojs/check/-/check-0.9.8.tgz",
"postcss-selector-parser": "^7.0.0"
}
},
- "node_modules/@docsearch/css": {
- "version": "3.9.0",
- "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.9.0.tgz",
- "integrity": "sha512-cQbnVbq0rrBwNAKegIac/t6a8nWoUAn8frnkLFW6YARaRmAQr5/Eoe6Ln2fqkUCZ40KpdrKbpSAmgrkviOxuWA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@docsearch/js": {
- "version": "3.9.0",
- "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-3.9.0.tgz",
- "integrity": "sha512-4bKHcye6EkLgRE8ze0vcdshmEqxeiJM77M0JXjef7lrYZfSlMunrDOCqyLjiZyo1+c0BhUqA2QpFartIjuHIjw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@docsearch/react": "3.9.0",
- "preact": "^10.0.0"
- }
- },
- "node_modules/@docsearch/react": {
- "version": "3.9.0",
- "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.9.0.tgz",
- "integrity": "sha512-mb5FOZYZIkRQ6s/NWnM98k879vu5pscWqTLubLFBO87igYYT4VzVazh4h5o/zCvTIZgEt3PvsCOMOswOUo9yHQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@algolia/autocomplete-core": "1.17.9",
- "@algolia/autocomplete-preset-algolia": "1.17.9",
- "@docsearch/css": "3.9.0",
- "algoliasearch": "^5.14.2"
- },
- "peerDependencies": {
- "@types/react": ">= 16.8.0 < 20.0.0",
- "react": ">= 16.8.0 < 20.0.0",
- "react-dom": ">= 16.8.0 < 20.0.0",
- "search-insights": ">= 1 < 3"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "react": {
- "optional": true
- },
- "react-dom": {
- "optional": true
- },
- "search-insights": {
- "optional": true
- }
- }
- },
"node_modules/@dual-bundle/import-meta-resolve": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.2.1.tgz",
"dev": true,
"license": "MIT"
},
+ "node_modules/@pagefind/component-ui": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/@pagefind/component-ui/-/component-ui-1.5.2.tgz",
+ "integrity": "sha512-t8/aE0tan4JiKa6cyhhSt/5qrEVwAK/qlYBHFpnRoq+qaFFVrhmXFFMY+r6n4GJtVIFCN2A5nUpeLN68cYjEjw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "adequate-little-templates": "^1.0.2",
+ "bcp-47": "^2.1.0"
+ }
+ },
+ "node_modules/@pagefind/darwin-arm64": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/@pagefind/darwin-arm64/-/darwin-arm64-1.5.2.tgz",
+ "integrity": "sha512-MXpI+7HsAdPkvJ0gk9xj9g541BCqBZOBbdwj9g6lB5LCj6kSV6nqDSjzcAJwvOsfu0fjwvC8hQU+ecfhp+MpiQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@pagefind/darwin-x64": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/@pagefind/darwin-x64/-/darwin-x64-1.5.2.tgz",
+ "integrity": "sha512-IojxFWMEJe0RQ7PQ3KXQsPIImNsbpPYpoZ+QUDrL8fAl/O27IX+LVLs74/UzEZy5uA2LD8Nz1AiwKr72vrkZQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@pagefind/freebsd-x64": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/@pagefind/freebsd-x64/-/freebsd-x64-1.5.2.tgz",
+ "integrity": "sha512-7EVzo9+0w+2cbe671BtMj10UlNo83I+HrLVLfRxO731svHRJKUfJ/mo05gU14pe9PCfpKNQT8FS3Xc/oDN6pOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@pagefind/linux-arm64": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/@pagefind/linux-arm64/-/linux-arm64-1.5.2.tgz",
+ "integrity": "sha512-Ovt9+K35sqzn8H3ZMXGwls4TD/wMJuvRtShHIsmUQREmaxjrDEX7gHckRCrwYJ4XE1H1p6HkLz3wukrAnsfXQw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@pagefind/linux-x64": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/@pagefind/linux-x64/-/linux-x64-1.5.2.tgz",
+ "integrity": "sha512-V+tFqHKXhQKq/WqPBD67AFy7scn1/aZID00ws4fSDd+1daSi5UHR9VVlRrOUYKxn3VuFQYRD7lYXdZK1WED1YA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@pagefind/windows-arm64": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/@pagefind/windows-arm64/-/windows-arm64-1.5.2.tgz",
+ "integrity": "sha512-hN9Nh90fNW61nNRCW9ZyQrAj/mD0eRvmJ8NlTUzkbuW8kIzGJUi3cxjFkEcMZ5h/8FsKWD/VcouZl4yo1F7B6g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@pagefind/windows-x64": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/@pagefind/windows-x64/-/windows-x64-1.5.2.tgz",
+ "integrity": "sha512-Fa2Iyw7kaDRzGMfNYNUXNW2zbL5FQVDgSOcbDHdzBrDEdpqOqg8TcZ68F22ol6NJ9IGzvUdmeyZypLW5dyhqsg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
"node_modules/@parcel/watcher": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
+ "node_modules/adequate-little-templates": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/adequate-little-templates/-/adequate-little-templates-1.0.2.tgz",
+ "integrity": "sha512-d0tFFG538l3Y4CElRKtticzrMr/OGohs32/nf3Ewn8rbTMBYwkVNe8mPdXWrDnMlz+ZVUFQjsTmsBD5zCtU4wg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/agent-base": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz",
}
}
},
- "node_modules/algoliasearch": {
- "version": "5.46.2",
- "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.46.2.tgz",
- "integrity": "sha512-qqAXW9QvKf2tTyhpDA4qXv1IfBwD2eduSW6tUEBFIfCeE9gn9HQ9I5+MaKoenRuHrzk5sQoNh1/iof8mY7uD6Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@algolia/abtesting": "1.12.2",
- "@algolia/client-abtesting": "5.46.2",
- "@algolia/client-analytics": "5.46.2",
- "@algolia/client-common": "5.46.2",
- "@algolia/client-insights": "5.46.2",
- "@algolia/client-personalization": "5.46.2",
- "@algolia/client-query-suggestions": "5.46.2",
- "@algolia/client-search": "5.46.2",
- "@algolia/ingestion": "1.46.2",
- "@algolia/monitoring": "1.46.2",
- "@algolia/recommend": "5.46.2",
- "@algolia/requester-browser-xhr": "5.46.2",
- "@algolia/requester-fetch": "5.46.2",
- "@algolia/requester-node-http": "5.46.2"
- },
- "engines": {
- "node": ">= 14.0.0"
- }
- },
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"node": ">=6.0.0"
}
},
+ "node_modules/bcp-47": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.0.tgz",
+ "integrity": "sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-alphabetical": "^2.0.0",
+ "is-alphanumerical": "^2.0.0",
+ "is-decimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/bcp-47/node_modules/is-alphabetical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
+ "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/bcp-47/node_modules/is-alphanumerical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
+ "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-alphabetical": "^2.0.0",
+ "is-decimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/bcp-47/node_modules/is-decimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
+ "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/big-integer": {
"version": "1.6.52",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
"dev": true,
"license": "MIT"
},
+ "node_modules/pagefind": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/pagefind/-/pagefind-1.5.2.tgz",
+ "integrity": "sha512-XTUaK0hXMCu2jszWE584JGQT7y284TmMV9l/HX3rnG5uo3rHI/uHU56XTyyyPFjeWEBxECbAi0CaFDJOONtG0Q==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "pagefind": "lib/runner/bin.cjs"
+ },
+ "optionalDependencies": {
+ "@pagefind/darwin-arm64": "1.5.2",
+ "@pagefind/darwin-x64": "1.5.2",
+ "@pagefind/freebsd-x64": "1.5.2",
+ "@pagefind/linux-arm64": "1.5.2",
+ "@pagefind/linux-x64": "1.5.2",
+ "@pagefind/windows-arm64": "1.5.2",
+ "@pagefind/windows-x64": "1.5.2"
+ }
+ },
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"license": "MIT"
},
- "node_modules/preact": {
- "version": "10.29.1",
- "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.1.tgz",
- "integrity": "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/preact"
- }
- },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
"node": ">=11.0.0"
}
},
- "node_modules/search-insights": {
- "version": "2.17.3",
- "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz",
- "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==",
- "dev": true,
- "license": "MIT",
- "peer": true
- },
"node_modules/select": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
"watch-js-docs": "nodemon --watch site/src/assets/ --ext js --exec \"npm run js-lint\"",
"astro-clean": "rm -rf site/.astro site/node_modules/.astro",
"astro-dev": "astro dev --root site --port 9001",
- "astro-build": "astro build --root site && rm -rf _site && cp -r site/dist _site",
+ "astro-build": "astro build --root site && rm -rf _site && cp -r site/dist _site && pagefind --site _site",
"astro-preview": "astro preview --root site --port 9001",
"spellcheck": "cspell --config .cspell.json \"**/*.{md,mdx}\""
},
"@babel/cli": "^7.28.6",
"@babel/core": "^7.29.0",
"@babel/preset-env": "^7.29.2",
- "@docsearch/js": "3.9.0",
"@floating-ui/dom": "^1.7.6",
+ "@pagefind/component-ui": "^1.5.2",
"@rollup/plugin-babel": "^7.0.0",
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-replace": "^6.0.3",
"mime": "^4.1.0",
"nodemon": "^3.1.14",
"npm-run-all2": "^8.0.4",
+ "pagefind": "^1.5.0",
"playwright": "^1.59.1",
"postcss": "^8.5.10",
"postcss-cli": "^11.0.1",
--- /dev/null
+# Pagefind CLI configuration
+# https://pagefind.app/docs/config-options/
+
+# Index special characters that show up in CSS / Sass / HTML docs so
+# users can search for things like `$primary`, `--bs-primary`,
+# `&::before`, or `<head>`. Without this they're stripped at index time.
+include_characters: "$&-_<>:."
// scss-docs-start badge-variants
$badge-variants: (
"subtle": (
- "color": "text",
+ "color": "fg",
"bg": "bg-subtle",
"border-color": "transparent"
),
"outline": (
- "color": "text",
+ "color": "fg",
"bg": "transparent",
"border-color": "border"
)
"outline": (
"base": (
"bg": "transparent",
- "color": "text",
+ "color": "fg",
"border-color": "border"
),
"hover": (
"subtle": (
"base": (
"bg": "bg-subtle",
- "color": "text",
+ "color": "fg",
"border-color": "transparent"
),
"hover": (
"bg": ("bg-muted", "bg-subtle"),
- "color": "text-emphasis"
+ "color": "fg-emphasis"
),
"active": (
"bg": "bg-subtle",
- "color": "text-emphasis"
+ "color": "fg-emphasis"
)
),
"text": (
"base": (
- "color": "text",
+ "color": "fg",
"bg": "transparent",
"border-color": "transparent"
),
"hover": (
- "color": "text",
+ "color": "fg",
"bg": "bg-subtle"
),
"active": (
- "color": "text",
+ "color": "fg",
"bg": "bg-subtle"
)
)
import { bootstrap } from './src/libs/astro'
import { getConfig } from './src/libs/config'
-import { algoliaPlugin } from './src/plugins/algolia-plugin'
import { stackblitzPlugin } from './src/plugins/stackblitz-plugin'
// Resolve `@bootstrap` to the same on-disk Bootstrap bundle the docs ship, so
},
site,
vite: {
- plugins: [algoliaPlugin(), stackblitzPlugin()],
+ plugins: [stackblitzPlugin()],
resolve: {
alias: {
'@bootstrap': bootstrapBundlePath
plugins: [
require('postcss-prefix-custom-properties')({
prefix: 'bs-',
- ignore: [/^--bs-/, /^--bd-/, /^--shell-/, /^--shiki-/]
+ ignore: [/^--bs-/, /^--bd-/, /^--pf-/, /^--shell-/, /^--shiki-/]
}),
require('autoprefixer')
]
// NOTICE: Internal docs helpers — not shipped in Bootstrap; not for reuse.
/*!
- * JavaScript for Bootstrap's docs (https://getbootstrap.com/)
+ * JavaScript for Bootstrap's docs search (https://getbootstrap.com/)
* Copyright 2024-2026 The Bootstrap Authors
* Licensed under the Creative Commons Attribution 3.0 Unported License.
* For details, see https://creativecommons.org/licenses/by/3.0/.
*/
-import docsearch from '@docsearch/js'
+// Custom Pagefind integration. We use `@pagefind/component-ui` only for its
+// instance manager / search engine; the trigger, dialog, input, and results
+// list are all built from Bootstrap primitives (Dialog, .form-control,
+// .list-group). No `<pagefind-*>` element is ever rendered — those custom
+// elements still self-register when the package loads, but stay unused.
-export default () => {
- // These values will be replaced by Astro's Vite plugin
- const CONFIG = {
- apiKey: '__API_KEY__',
- indexName: '__INDEX_NAME__',
- appId: '__APP_ID__'
+import { getInstanceManager } from '@pagefind/component-ui'
+import { Dialog } from '../../../dist/js/bootstrap.bundle.js'
+
+const DIALOG_SELECTOR = '#bdSearchDialog'
+const SUB_RESULTS_LIMIT = 3
+// How long a search may take before we replace the previous results with the
+// loading skeleton. Most queries resolve in well under this, so the skeleton
+// stays hidden and the UI doesn't flash on every keystroke.
+const LOADING_SKELETON_DELAY_MS = 200
+const RECENT_STORAGE_KEY = 'bd:search:recent'
+const RECENT_LIMIT = 5
+
+const instance = getInstanceManager().getInstance('default')
+let searchInitialized = false
+
+// Recent visits — opt-out via Do Not Track and quietly no-op when storage is
+// unavailable (private mode, quota, disabled cookies/storage).
+const isDoNotTrackEnabled = () => {
+ if (typeof navigator === 'undefined') {
+ return false
+ }
+
+ return navigator.doNotTrack === '1' ||
+ globalThis.doNotTrack === '1' ||
+ navigator.msDoNotTrack === '1'
+}
+
+const getRecentVisits = () => {
+ if (isDoNotTrackEnabled()) {
+ return []
}
- const searchElement = document.getElementById('docsearch')
+ try {
+ const raw = localStorage.getItem(RECENT_STORAGE_KEY)
+ const parsed = raw ? JSON.parse(raw) : []
+ return Array.isArray(parsed) ? parsed.filter(item => item && item.url) : []
+ } catch {
+ return []
+ }
+}
- if (!searchElement) {
+const saveRecentVisit = visit => {
+ if (isDoNotTrackEnabled() || !visit?.url) {
return
}
- const siteDocsVersion = searchElement.getAttribute('data-bd-docs-version')
-
- docsearch({
- apiKey: CONFIG.apiKey,
- indexName: CONFIG.indexName,
- appId: CONFIG.appId,
- container: searchElement,
- searchParameters: {
- facetFilters: [`version:${siteDocsVersion}`]
- },
- transformItems(items) {
- return items.map(item => {
- const liveUrl = 'https://getbootstrap.com/'
-
- item.url = window.location.origin.startsWith(liveUrl) ?
- // On production, return the result as is
- item.url :
- // On development or Netlify, replace `item.url` with a trailing slash,
- // so that the result link is relative to the server root
- item.url.replace(liveUrl, '/')
-
- // Prevent jumping to first header
- if (item.anchor === 'content') {
- item.url = item.url.replace(/#content$/, '')
- item.anchor = null
+ try {
+ const existing = getRecentVisits().filter(item => item.url !== visit.url)
+ const next = [visit, ...existing].slice(0, RECENT_LIMIT)
+ localStorage.setItem(RECENT_STORAGE_KEY, JSON.stringify(next))
+ } catch {
+ // Ignore — storage is best-effort.
+ }
+}
+
+const clearRecentVisits = () => {
+ try {
+ localStorage.removeItem(RECENT_STORAGE_KEY)
+ } catch {
+ // Ignore — storage is best-effort.
+ }
+}
+
+const HTML_ESCAPES = {
+ '&': '&',
+ '<': '<',
+ '>': '>',
+ '"': '"',
+ '\'': '''
+}
+
+const escapeHtml = value => String(value ?? '').replace(/[&<>"']/g, char => HTML_ESCAPES[char])
+
+// Single template for every clickable row in the results list — top-level
+// results, nested subresults, and recently visited items. The two variants
+// only differ by icon and a `-sub` modifier that nudges the font size down.
+// `title` and `excerpt` are inlined as-is so callers can either escape them
+// (recents, plain text) or pass through Pagefind's pre-marked HTML excerpts.
+const renderItem = ({ url, title, excerpt = '', icon, sub = false }) => `
+ <a class="list-group-item list-group-item-action bd-search-item${sub ? ' bd-search-item-sub' : ''}" href="${escapeHtml(url)}">
+ <svg class="bi bd-search-item-icon" width="16" height="16" aria-hidden="true">
+ <use href="#${icon}"></use>
+ </svg>
+ <span class="bd-search-item-title">${title}</span>
+ ${excerpt ? `<span class="bd-search-item-excerpt">${excerpt}</span>` : ''}
+ </a>
+`
+
+class BdSearchInput extends HTMLElement {
+ connectedCallback() {
+ this.input = this.querySelector('input')
+ if (!this.input) {
+ return
+ }
+
+ this.inputEl = this.input
+ instance.registerInput(this, { keyboardNavigation: true })
+
+ this._onInput = this._onInput.bind(this)
+ this._onKeydown = this._onKeydown.bind(this)
+ this.input.addEventListener('input', this._onInput)
+ this.input.addEventListener('keydown', this._onKeydown)
+
+ this.input.addEventListener('focus', () => {
+ instance.triggerLoad()
+ }, { once: true })
+ }
+
+ disconnectedCallback() {
+ this.input?.removeEventListener('input', this._onInput)
+ this.input?.removeEventListener('keydown', this._onKeydown)
+ }
+
+ _onInput(event) {
+ instance.triggerSearch(event.target.value)
+ }
+
+ _onKeydown(event) {
+ // ArrowDown jumps focus to the first link in the next results component.
+ if (event.key === 'ArrowDown' && instance.focusNextResults(this)) {
+ event.preventDefault()
+ return
+ }
+
+ // Enter follows the first result link — this matches the command-palette
+ // pattern where the top result is always the implicit Enter target.
+ // Always preventDefault so the surrounding `<form role="search">` never
+ // implicit-submits and reloads the page when no results are present
+ // (loading / empty / error / zero-results states).
+ if (event.key === 'Enter') {
+ event.preventDefault()
+ const dialogEl = this.closest('dialog')
+ dialogEl?.querySelector('.bd-search-item')?.click()
+ }
+ }
+
+ focus() {
+ this.input?.focus()
+ }
+}
+
+class BdSearchResults extends HTMLElement {
+ connectedCallback() {
+ instance.registerResults(this, { keyboardNavigation: true })
+
+ instance.on('loading', () => this._renderLoading(), this)
+ instance.on('results', result => this._renderResults(result), this)
+ instance.on('error', error => this._renderError(error), this)
+
+ this._onKeydown = this._onKeydown.bind(this)
+ this._onClick = this._onClick.bind(this)
+ this.addEventListener('keydown', this._onKeydown)
+ this.addEventListener('click', this._onClick)
+
+ this._renderEmpty()
+ }
+
+ disconnectedCallback() {
+ this.removeEventListener('keydown', this._onKeydown)
+ this.removeEventListener('click', this._onClick)
+ this._clearLoadingTimer()
+ }
+
+ _onClick(event) {
+ if (event.target.closest('[data-bd-search-clear-recent]')) {
+ event.preventDefault()
+ clearRecentVisits()
+ this._renderEmpty()
+ instance.focusInputAndType(this, '')
+ }
+ }
+
+ _getLinks() {
+ return [...this.querySelectorAll('.bd-search-item')]
+ }
+
+ _onKeydown(event) {
+ const link = event.target.closest('.bd-search-item')
+ if (!link) {
+ return
+ }
+
+ const links = this._getLinks()
+ const index = links.indexOf(link)
+
+ switch (event.key) {
+ case 'ArrowDown': {
+ const next = links[index + 1]
+ if (next) {
+ event.preventDefault()
+ next.focus()
+ }
+
+ break
+ }
+
+ case 'ArrowUp': {
+ event.preventDefault()
+ if (index === 0) {
+ instance.focusPreviousInput(this)
+ } else {
+ links[index - 1].focus()
}
- return item
- })
+ break
+ }
+
+ case 'Home': {
+ if (links[0]) {
+ event.preventDefault()
+ links[0].focus()
+ }
+
+ break
+ }
+
+ case 'End': {
+ const last = links.at(-1)
+ if (last) {
+ event.preventDefault()
+ last.focus()
+ }
+
+ break
+ }
+
+ case 'Backspace': {
+ event.preventDefault()
+ instance.focusInputAndDelete(this)
+ break
+ }
+
+ default: {
+ // Single printable character — redirect to the input and keep typing there.
+ if (event.key.length === 1 && !event.metaKey && !event.ctrlKey && !event.altKey) {
+ event.preventDefault()
+ instance.focusInputAndType(this, event.key)
+ }
+ }
+ }
+ }
+
+ _clearLoadingTimer() {
+ if (this._loadingTimer) {
+ clearTimeout(this._loadingTimer)
+ this._loadingTimer = null
+ }
+ }
+
+ _renderEmpty() {
+ this._clearLoadingTimer()
+
+ if (instance.searchTerm) {
+ return
+ }
+
+ const recents = getRecentVisits()
+ if (recents.length === 0) {
+ this.innerHTML = `
+ <div class="bd-search-empty">Start typing to search…</div>
+ `
+ return
+ }
+
+ this.innerHTML = `
+ <div class="bd-search-recent">
+ <div class="bd-search-recent-header">
+ <span class="bd-search-recent-label">Recently visited</span>
+ <button type="button" class="bd-search-recent-clear" data-bd-search-clear-recent>
+ Clear
+ </button>
+ </div>
+ <ol class="list-group list-group-flush bd-search-results-list">
+ ${recents.map(visit => {
+ // URL fragment → in-page link (Pagefind subresult). Render with
+ // the hash icon and smaller title to match the live results UI.
+ const isSubLink = visit.url.includes('#')
+ return `
+ <li class="bd-search-result">
+ ${renderItem({
+ url: visit.url,
+ title: escapeHtml(visit.title),
+ excerpt: escapeHtml(visit.excerpt),
+ icon: isSubLink ? 'hash' : 'file-earmark-richtext',
+ sub: isSubLink
+ })}
+ </li>
+ `
+ }).join('')}
+ </ol>
+ </div>
+ `
+ }
+
+ _renderLoading() {
+ // Defer the skeleton paint — most searches finish well before this fires,
+ // so the previous results stay on screen instead of flashing to skeletons
+ // on every keystroke.
+ this._clearLoadingTimer()
+ this._loadingTimer = setTimeout(() => {
+ this._loadingTimer = null
+ this._paintLoading()
+ }, LOADING_SKELETON_DELAY_MS)
+ }
+
+ _paintLoading() {
+ this.innerHTML = `
+ <div class="bd-search-loading placeholder-glow" role="status" aria-live="polite">
+ <span class="visually-hidden">${escapeHtml(instance.translate('searching') || 'Searching…')}</span>
+ ${Array.from({ length: 3 }, () => `
+ <div class="bd-search-skeleton">
+ <span class="placeholder col-4"></span>
+ <span class="placeholder col-12"></span>
+ <span class="placeholder col-10"></span>
+ </div>
+ `).join('')}
+ </div>
+ `
+ }
+
+ _renderError(error) {
+ this._clearLoadingTimer()
+
+ const message = error?.message || instance.translate('error_text') || 'Something went wrong with search.'
+ this.innerHTML = `
+ <div class="bd-search-error" role="alert">
+ ${escapeHtml(message)}
+ </div>
+ `
+ }
+
+ async _renderResults(searchResult) {
+ this._clearLoadingTimer()
+
+ const term = instance.searchTerm
+
+ if (!term) {
+ this._renderEmpty()
+ return
+ }
+
+ const rawResults = searchResult?.results ?? []
+
+ if (rawResults.length === 0) {
+ const zero = instance.translate('zero_results', { SEARCH_TERM: term }) || `No results for "${term}"`
+ this.innerHTML = `
+ <div class="bd-search-empty">${escapeHtml(zero)}</div>
+ `
+ return
+ }
+
+ const top = rawResults.slice(0, 5)
+ const settled = await Promise.all(top.map(raw => raw.data().catch(() => null)))
+ const data = settled.filter(Boolean)
+
+ if (instance.searchTerm !== term) {
+ // A newer search has started while we were resolving — drop these.
+ return
+ }
+
+ this.innerHTML = `
+ <ol class="list-group list-group-flush bd-search-results-list">
+ ${data.map(result => this._renderResult(result)).join('')}
+ </ol>
+ `
+ }
+
+ _renderResult(result) {
+ const title = result.meta?.title || result.url
+ const subResults = instance.getDisplaySubResults(result, SUB_RESULTS_LIMIT)
+
+ const subResultsHtml = subResults.length === 0 ?
+ '' :
+ `
+ <ul class="bd-search-subresults list-unstyled">
+ ${subResults.map(sub => `
+ <li>
+ ${renderItem({
+ url: sub.url,
+ title: escapeHtml(sub.title),
+ excerpt: sub.excerpt,
+ icon: 'hash',
+ sub: true
+ })}
+ </li>
+ `).join('')}
+ </ul>
+ `
+
+ return `
+ <li class="bd-search-result">
+ ${renderItem({
+ url: result.url,
+ title: escapeHtml(title),
+ excerpt: result.excerpt,
+ icon: 'file-earmark-richtext'
+ })}
+ ${subResultsHtml}
+ </li>
+ `
+ }
+}
+
+const defineSearchCustomElements = () => {
+ if (!customElements.get('bd-search-input')) {
+ customElements.define('bd-search-input', BdSearchInput)
+ }
+
+ if (!customElements.get('bd-search-results')) {
+ customElements.define('bd-search-results', BdSearchResults)
+ }
+}
+
+const isMac = () => {
+ if (typeof navigator === 'undefined') {
+ return false
+ }
+
+ const platform = navigator.userAgentData?.platform || navigator.platform || ''
+ return /mac|iphone|ipad|ipod/i.test(platform)
+}
+
+// Populate every `.bd-search-trigger-shortcut` slot with the platform-correct
+// shortcut and reveal it. Rendered empty/hidden by `SearchTrigger.astro` so
+// non-JS visitors never see a misleading hint and JS visitors never see a
+// flash of the wrong key.
+const setupTriggerShortcuts = () => {
+ const mac = isMac()
+ const modifier = mac ? '⌘' : '⌃'
+ const ariaKeyshortcut = mac ? 'Meta+K' : 'Control+K'
+
+ for (const slot of document.querySelectorAll('.bd-search-trigger-shortcut')) {
+ slot.innerHTML = `<kbd class="bd-search-trigger-key">${modifier}K</kbd>`
+ slot.hidden = false
+ slot.closest('.bd-search-trigger')?.setAttribute('aria-keyshortcuts', ariaKeyshortcut)
+ }
+}
+
+const isEditableTarget = target => {
+ if (!target) {
+ return false
+ }
+
+ if (target.isContentEditable) {
+ return true
+ }
+
+ const tag = target.tagName
+ return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT'
+}
+
+const openSearchDialog = () => {
+ const dialogEl = document.querySelector(DIALOG_SELECTOR)
+ if (!dialogEl) {
+ return
+ }
+
+ const dialog = Dialog.getOrCreateInstance(dialogEl)
+ dialog.show()
+ // Focus the input on the next frame so the dialog is in the top layer.
+ requestAnimationFrame(() => {
+ dialogEl.querySelector('bd-search-input')?.focus()
+ })
+}
+
+const registerGlobalShortcuts = () => {
+ // Global Cmd/Ctrl + K shortcut to open the search dialog.
+ document.addEventListener('keydown', event => {
+ if ((event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === 'k') {
+ event.preventDefault()
+ openSearchDialog()
+ return
+ }
+
+ // `/` opens search when not typing in another field — common docs shortcut.
+ if (event.key === '/' && !event.metaKey && !event.ctrlKey && !event.altKey && !isEditableTarget(event.target)) {
+ event.preventDefault()
+ openSearchDialog()
}
})
}
+
+const setupDialogResetOnClose = () => {
+ const dialogEl = document.querySelector(DIALOG_SELECTOR)
+ if (!dialogEl) {
+ return
+ }
+
+ dialogEl.addEventListener('hidden.bs.dialog', () => {
+ const inputEl = dialogEl.querySelector('bd-search-input input')
+ if (inputEl) {
+ inputEl.value = ''
+ }
+
+ instance.triggerSearch('')
+ })
+
+ // Re-render empty state on open so a freshly-saved visit shows up in the
+ // "Recently visited" list without waiting for the user to type and clear.
+ dialogEl.addEventListener('shown.bs.dialog', () => {
+ if (!instance.searchTerm) {
+ dialogEl.querySelector('bd-search-results')?._renderEmpty?.()
+ }
+ })
+}
+
+const onDomReady = () => {
+ setupTriggerShortcuts()
+ setupDialogResetOnClose()
+}
+
+const registerResultLinkHandler = () => {
+ // Result links should close the dialog when clicked (so the user lands on the
+ // destination page with the modal already gone) and have the visit recorded
+ // so it can resurface in the empty-state "Recently visited" list.
+ document.addEventListener('click', event => {
+ const link = event.target.closest('.bd-search-item')
+ if (!link) {
+ return
+ }
+
+ const dialogEl = link.closest(DIALOG_SELECTOR)
+ if (!dialogEl) {
+ return
+ }
+
+ const titleEl = link.querySelector('.bd-search-item-title')
+ const excerptEl = link.querySelector('.bd-search-item-excerpt')
+ saveRecentVisit({
+ url: link.getAttribute('href') || '',
+ title: titleEl?.textContent.trim() || link.textContent.trim(),
+ excerpt: excerptEl?.textContent.trim() || ''
+ })
+
+ Dialog.getInstance(dialogEl)?.hide()
+ })
+}
+
+const initSearch = () => {
+ if (searchInitialized) {
+ return
+ }
+
+ searchInitialized = true
+ defineSearchCustomElements()
+ registerGlobalShortcuts()
+ registerResultLinkHandler()
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', onDomReady, { once: true })
+ } else {
+ onDomReady()
+ }
+}
+
+export default initSearch
---
<script>
+ import initSearch from '../assets/search.js'
import stackblitz from '../assets/stackblitz.js'
+ initSearch()
stackblitz()
</script>
const { version } = Astro.props
---
-<span class="badge bg-3 ms-auto fg-3 fw-normal me--1" data-bs-toggle="tooltip" data-bs-title={`Added in v${version}`}>
+<span
+ class="badge badge-subtle theme-primary ms-auto fw-normal me--1"
+ data-bs-toggle="tooltip"
+ data-bs-title={`Added in v${version}`}
+>
New
<span class="visually-hidden">{` (Added in v${version})`}</span>
</span>
<script>
import application from '../assets/application.js'
- import search from '../assets/search.js'
application()
- search()
</script>
-{layout === 'docs' && <DocsScripts />}
+<DocsScripts />
<meta name="description" content={description} />
<meta name="author" content={getConfig().authors} />
+
<meta name="generator" content={Astro.generator} />
-<meta name="docsearch:language" content="en" />
-<meta name="docsearch:version" content={getConfig().docs_version} />
<link rel="canonical" href={canonicalUrl} />
-<link rel="preconnect" href=`https://${getConfig().algolia.app_id}-dsn.algolia.net` crossorigin />
-
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
import Versions from '@components/header/Versions.astro'
import ThemeToggler from '@layouts/partials/ThemeToggler.astro'
import CloseButton from '@components/shortcodes/CloseButton.astro'
+import SearchTrigger from '@components/header/SearchTrigger.astro'
+import SearchDialog from '@components/header/SearchDialog.astro'
interface Props {
addedIn?: CollectionEntry<'docs'>['data']['added']
</div>
<ul class="nav navbar-nav flex-row ms-auto">
- <li class="nav-item nav-link px-1" id="docsearch" data-bd-docs-version={getConfig().docs_version}></li>
+ <div class="bd-search">
+ <SearchTrigger placeholder="Search docs…" />
+ </div>
<LinkItem class="px-1 d-none lg:d-flex" href={getConfig().github_org} target="_blank" rel="noopener">
<GitHubIcon class="navbar-nav-svg" height={16} width={16} />
}
</nav>
</header>
+
+<SearchDialog placeholder="Search docs…" />
--- /dev/null
+---
+import CloseButton from '@components/shortcodes/CloseButton.astro'
+
+interface Props {
+ id?: string
+ placeholder?: string
+}
+
+const { id = 'bdSearchDialog', placeholder = 'Search docs…' } = Astro.props
+---
+
+<dialog class="dialog dialog-lg dialog-search dialog-scrollable" id={id} aria-labelledby={`${id}-label`}>
+ <form class="dialog-header bd-search-form" role="search" autocomplete="off">
+ <label class="visually-hidden" id={`${id}-label`} for={`${id}-input`}>
+ {placeholder}
+ </label>
+ <div class="form-control form-adorn">
+ <div class="form-adorn-icon">
+ <svg class="bi bd-search-form-icon" width="16" height="16" aria-hidden="true">
+ <use href="#search"></use>
+ </svg>
+ </div>
+ <bd-search-input>
+ <input
+ id={`${id}-input`}
+ type="search"
+ class="form-ghost bd-search-input"
+ placeholder={placeholder}
+ autocomplete="off"
+ spellcheck="false"
+ enterkeyhint="search"
+ />
+ </bd-search-input>
+ </div>
+ <CloseButton dismiss="dialog" class="bd-search-close" />
+ </form>
+
+ <div class="dialog-body bd-search-body">
+ <bd-search-results></bd-search-results>
+ </div>
+</dialog>
--- /dev/null
+---
+interface Props {
+ class?: string
+ placeholder?: string
+ target?: string
+}
+
+const { class: className, placeholder = 'Search docs…', target = '#bdSearchDialog' } = Astro.props
+---
+
+<button
+ type="button"
+ class:list={['bd-search-trigger', className]}
+ data-bs-toggle="dialog"
+ data-bs-target={target}
+ aria-haspopup="dialog"
+ aria-label={placeholder}
+>
+ <svg class="bi bd-search-trigger-icon" width="16" height="16" aria-hidden="true">
+ <use href="#search"></use>
+ </svg>
+ <span class="bd-search-trigger-label">{placeholder}</span>
+ {
+ /* Shortcut hint is empty + hidden until search.js detects the platform and
+ fills in either ⌘K (Mac) or Ctrl K (other). Without JS the shortcut
+ itself doesn't work, so showing nothing is more honest than guessing. */
+ }
+ <span class="bd-search-trigger-shortcut" aria-hidden="true" hidden></span>
+</button>
d="M1 2.5A1.5 1.5 0 0 1 2.5 1h3A1.5 1.5 0 0 1 7 2.5v3A1.5 1.5 0 0 1 5.5 7h-3A1.5 1.5 0 0 1 1 5.5v-3zm8 0A1.5 1.5 0 0 1 10.5 1h3A1.5 1.5 0 0 1 15 2.5v3A1.5 1.5 0 0 1 13.5 7h-3A1.5 1.5 0 0 1 9 5.5v-3zm-8 8A1.5 1.5 0 0 1 2.5 9h3A1.5 1.5 0 0 1 7 10.5v3A1.5 1.5 0 0 1 5.5 15h-3A1.5 1.5 0 0 1 1 13.5v-3zm8 0A1.5 1.5 0 0 1 10.5 9h3a1.5 1.5 0 0 1 1.5 1.5v3a1.5 1.5 0 0 1-1.5 1.5h-3A1.5 1.5 0 0 1 9 13.5v-3z"
></path>
</symbol>
+ <symbol id="hash" viewBox="0 0 16 16">
+ <path
+ d="M8.39 12.648a1 1 0 0 0-.015.18c0 .305.21.508.5.508.266 0 .492-.172.555-.477l.554-2.703h1.204c.421 0 .617-.234.617-.547 0-.312-.188-.53-.617-.53h-.985l.516-2.524h1.265c.43 0 .618-.227.618-.547 0-.313-.188-.524-.618-.524h-1.046l.476-2.304a1 1 0 0 0 .016-.164.51.51 0 0 0-.516-.516.54.54 0 0 0-.539.43l-.523 2.554H7.617l.477-2.304c.008-.04.015-.118.015-.164a.51.51 0 0 0-.523-.516.54.54 0 0 0-.531.43L6.53 5.484H5.414c-.43 0-.617.22-.617.532s.187.539.617.539h.906l-.515 2.523H4.609c-.421 0-.609.219-.609.531s.188.547.61.547h.976l-.516 2.492c-.008.04-.015.125-.015.18 0 .305.21.508.5.508.265 0 .492-.172.554-.477l.555-2.703h2.242zm-1-6.109h2.266l-.515 2.563H6.859l.532-2.563z"
+ ></path>
+ </symbol>
<symbol id="lightning-charge-fill" viewBox="0 0 16 16">
<path
d="M11.251.068a.5.5 0 0 1 .227.58L9.677 6.5H13a.5.5 0 0 1 .364.843l-8 8.5a.5.5 0 0 1-.842-.49L6.323 9.5H3a.5.5 0 0 1-.364-.843l8-8.5a.5.5 0 0 1 .615-.09z"
)}
</>
) : (
- <div class:list={['bd-code-snippet', containerClass]}>
+ <div class:list={['bd-code-snippet', containerClass]} data-pagefind-ignore>
{!noToolbar && (
<div class="hstack highlight-toolbar align-items-center">
{highlightedTabs ? (
)
---
-<div class="bd-example-snippet bd-code-snippet not-prose">
+<div class="bd-example-snippet bd-code-snippet not-prose" data-pagefind-ignore>
{
showPreview && (
<div id={id} class:list={['bd-example', className]}>
)
---
-<div class="bd-example-snippet bd-code-snippet">
+<div class="bd-example-snippet bd-code-snippet" data-pagefind-ignore>
<div class="bd-example bd-example-resizable p-2">
<div
class:list={['bd-resizable-container', containerClass]}
<Ads />
</div>
- <div class="bd-content prose lg:ps-2">
+ <div class="bd-content prose lg:ps-2 lg:pt-4" data-pagefind-body>
{
frontmatter.sections && (
<div class="grid grid-cols-1 lg:grid-cols-3 mb-5">
copyBootstrap()
copyStatic()
aliasStatic()
+ copyPagefindIndex()
},
'astro:build:done': ({ dir }) => {
validateVersionedDocsPaths(dir)
fs.rmSync(getDocsPublicFsPath(), { force: true, recursive: true })
}
+// Copy the previously-generated Pagefind search index from `_site/pagefind/`
+// into `site/public/pagefind/` so `astro dev` can serve it at `/pagefind/`.
+// The index is regenerated by `npm run docs-build`; this step is a no-op if
+// no build has been run yet, in which case dev simply has no search results.
+function copyPagefindIndex() {
+ const source = path.join(process.cwd(), '_site', 'pagefind')
+
+ if (!fs.existsSync(source)) {
+ return
+ }
+
+ const destination = path.join(getDocsPublicFsPath(), 'pagefind')
+
+ fs.mkdirSync(destination, { recursive: true })
+ fs.cpSync(source, destination, { recursive: true })
+}
+
// Copy the `dist` folder from the root of the repo containing the latest version of Bootstrap to make it available from
// the `/docs/${docs_version}/dist` URL.
function copyBootstrap() {
// The config schema used to validate the config file content and ensure all values required by the site are valid.
const configSchema = z.object({
- algolia: z.object({
- api_key: z.string(),
- app_id: z.string(),
- index_name: z.string()
- }),
analytics: z.object({
fathom_site: z.string()
}),
+++ /dev/null
-import { getConfig } from '../libs/config.ts'
-
-/**
- * Vite plugin to replace placeholder values in search.js with actual configuration values
- */
-export function algoliaPlugin() {
- const config = getConfig()
-
- return {
- name: 'algolia-config-replacer',
- transform(code, id) {
- if (id.includes('search.js')) {
- return code
- .replace(/__API_KEY__/g, config.algolia.api_key)
- .replace(/__INDEX_NAME__/g, config.algolia.index_name)
- .replace(/__APP_ID__/g, config.algolia.app_id)
- }
-
- return code
- }
- }
-}
--link-hover-color: var(--bd-callout-color);
--code-color: var(--bd-callout-code-color);
- padding: 1.25rem;
+ container-type: inline-size;
+ padding: 1rem;
font-size: .875rem;
line-height: 1.5;
background-color: var(--bd-callout-bg, var(--bs-gray-100));
+ .bd-callout {
margin-top: -.25rem;
}
+
+ @container (width >= 768px) {
+ padding: 1.25rem;
+ }
}
// Variations
@layer custom {
.bd-content {
+ --bs-content-font-size: 15px;
+ --bs-content-line-height: calc(22 / 15);
+
@media (width >= 1024px) {
+ --bs-content-line-height: 1.625;
font-size: var(--font-size-md);
}
"content";
grid-template-rows: auto auto 1fr;
gap: inherit;
+ padding-top: .75rem;
}
@include media-breakpoint-up(lg) {
@use "../../../scss/layout/breakpoints" as *;
-@use "../../../scss/mixins/color-mode" as *;
@use "../../../scss/mixins/border-radius" as *;
+@use "../../../scss/mixins/focus-ring" as *;
+@use "../../../scss/mixins/transition" as *;
-// stylelint-disable selector-class-pattern
-
-:root {
- --docsearch-primary-color: var(--bs-indigo-500);
- --docsearch-logo-color: var(--bs-indigo-500);
-}
-
-@include color-mode(dark, true) {
- // From here, the values are copied from https://cdn.jsdelivr.net/npm/@docsearch/css@3
- // in html[data-theme="dark"] selector
- // and are slightly modified for formatting purpose
- // --docsearch-text-color: #f5f6f7;
- --docsearch-container-background: rgba(9, 10, 17, .8);
- --docsearch-modal-background: #15172a;
- --docsearch-modal-shadow: inset 1px 1px 0 0 #2c2e40, 0 3px 8px 0 #000309;
- // --docsearch-searchbox-background: #090a11;
- // --docsearch-searchbox-focus-background: #000;
- --docsearch-hit-color: #bec3c9;
- --docsearch-hit-shadow: none;
- --docsearch-hit-background: #090a11;
- --docsearch-key-gradient: linear-gradient(-26.5deg, #565872, #31355b);
- --docsearch-key-shadow: inset 0 -2px 0 0 #282d55, inset 0 0 1px 1px #51577d, 0 2px 2px 0 rgba(3, 4, 9, .3);
- --docsearch-footer-background: #1e2136;
- --docsearch-footer-shadow: inset 0 1px 0 0 rgba(73, 76, 106, .5), 0 -4px 8px 0 rgba(0, 0, 0, .2);
- // --docsearch-muted-color: #7f8497;
-}
-
-.DocSearch-Button {
- width: auto;
- height: auto;
- padding: var(--bs-nav-link-padding-y) 0;
- margin-left: 0;
- color: var(--bs-nav-link-color);
- background-color: var(--bs-nav-link-bg);
+//
+// Custom Pagefind search UI w/ Bootstrap components
+//
+
+.bd-search {
+ display: flex;
+ flex-shrink: 0;
+ align-items: center;
+
+ // Below `md` the trigger is a borderless icon button
+ @include media-breakpoint-up(md) {
+ position: absolute;
+ top: .875rem;
+ left: 50%;
+ width: 240px;
+ margin-inline-start: -100px;
+ }
+
+ @include media-breakpoint-up(xl) {
+ width: 280px;
+ margin-inline-start: -140px;
+ }
+}
+
+// Trigger button
+//
+// Mobile (< md): icon-only button, no chrome.
+// md+: expands to a faux input with label + ⌘K shortcut.
+
+.bd-search-trigger {
+ display: inline-flex;
+ gap: .5rem;
+ align-items: center;
+ padding: .375rem;
+ font-size: var(--bs-font-size-sm);
+ color: var(--bs-fg-3, var(--bs-secondary-color));
+ text-align: start;
+ cursor: pointer;
+ background-color: transparent;
+ border: 0;
+ @include border-radius(var(--bs-border-radius));
+ @include transition(color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out);
&:hover {
- color: var(--bs-nav-link-hover-color);
- background-color: var(--bs-nav-link-hover-bg);
- box-shadow: none;
+ color: var(--bs-fg-body);
+ }
+
+ &:focus-visible {
+ @include focus-ring(true);
}
- @include media-breakpoint-up(lg) {
- padding-inline: calc(var(--bs-spacer) * .5);
+ @include media-breakpoint-up(md) {
+ width: 100%;
+ padding: .375rem .5rem .375rem .75rem;
+ background-color: var(--bs-bg-1);
+ border: var(--bs-border-width) solid var(--bs-border-color);
+
+ &:hover {
+ background-color: var(--bs-bg-2);
+ border-color: var(--bs-border-color);
+ }
}
}
-.DocSearch-Search-Icon {
- width: 16px;
- height: 16px;
- color: inherit !important; // stylelint-disable-line declaration-no-important
+.bd-search-trigger-icon {
+ flex-shrink: 0;
+ color: var(--bs-fg-3);
}
-.DocSearch-Button-Placeholder {
+.bd-search-trigger-label,
+.bd-search-trigger-shortcut {
display: none;
- font-size: var(--bs-font-size-sm);
- font-weight: var(--bs-font-weight-normal);
- color: var(--bs-nav-link-color);
+
+ @include media-breakpoint-up(md) {
+ display: inline-flex;
+ }
}
-.DocSearch-Button-Keys {
- display: none;
+.bd-search-trigger-label {
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.bd-search-trigger-shortcut {
+ flex-shrink: 0;
+ gap: .125rem;
+}
+
+.bd-search-trigger-key {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 1.25rem;
+ padding: 0 .25rem;
+ font-family: var(--bs-font-monospace);
+ font-size: .75rem;
+ color: var(--bs-fg-3, var(--bs-secondary-color));
+ background-color: var(--bs-bg-body);
+ border: var(--bs-border-width) solid var(--bs-border-color);
+ @include border-radius(var(--bs-border-radius-sm));
}
-.DocSearch-Container {
- --docsearch-muted-color: var(--bs-fg-3);
- --docsearch-hit-shadow: none;
+// Search dialog shell
+//
+// Top-aligned overlay that grows up to the viewport. Uses the
+// existing `.dialog` token surface; we only override sizing and
+// position so the dialog feels like a docs-style command palette.
+
+.dialog-search {
+ --dialog-margin: 1.5rem;
+ --dialog-padding: .75rem;
+ --dialog-header-padding: .75rem;
+ --dialog-footer-padding: .5rem .75rem;
+ --dialog-border-radius: var(--border-radius-xl);
- position: fixed;
- z-index: 2000; // Make sure to be over all components showcased in the documentation
- cursor: auto; // Needed because of [role="button"] in Algolia search modal. Remove once https://github.com/algolia/docsearch/issues/1370 is tackled.
+ align-self: flex-start;
+ width: 640px;
+ max-height: min(80dvh, 720px);
+ margin-block-start: 3rem;
+ overflow: hidden;
- @include media-breakpoint-up(lg) {
- padding-block-start: 4rem;
+ @include media-breakpoint-down(sm) {
+ max-height: calc(100dvh - 4rem);
}
}
-.DocSearch-Form {
- @include border-radius(var(--bs-border-radius));
+//
+// Header / search form row
+//
+
+.bd-search-form {
+ display: flex;
+ flex-wrap: nowrap;
+ gap: .75rem;
+ align-items: center;
+ // padding: var(--dialog-header-padding);
+ padding: var(--spacer-6) var(--spacer-6) var(--spacer-4);
+ border-bottom: 0;
+}
+
+.bd-search-form-icon {
+ flex-shrink: 0;
+ color: var(--bs-fg-3);
+}
+
+bd-search-input {
+ width: 100%;
+}
+
+.bd-search-input {
+ flex: 1;
+ width: 100%;
+ min-width: 0;
+ background-color: transparent;
+ border-color: transparent;
+ box-shadow: none;
+
+ &:focus {
+ background-color: transparent;
+ border-color: transparent;
+ box-shadow: none;
+ }
+}
+
+.bd-search-close {
+ flex-shrink: 0;
+ margin-inline-start: 0;
+}
+
+//
+// Body / results
+//
+
+.bd-search-body {
+ padding: 0 var(--spacer-6) var(--spacer-6);
+}
+
+bd-search-results {
+ display: block;
+ min-height: 6rem;
+}
+
+.bd-search-results-list {
+ --bs-list-group-border-width: 0;
+ --bs-list-group-border-radius: 0;
+
+ gap: var(--spacer-2);
+ padding-block-start: var(--spacer-1);
+ list-style: none;
}
-.DocSearch-Hits {
+.bd-search-result {
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacer-1);
+ padding: 0;
+}
+
+// Default-active state: while focus is still in the search input (i.e. the
+// results list isn't `:focus-within`), the first result is visually
+// highlighted as the "Enter target", giving the dialog a command-palette
+// feel. Once the user arrow-keys into the list, `:focus-within` flips on,
+// the highlight drops, and the focus ring on the actually-focused row takes
+// over instead.
+bd-search-results:not(:focus-within) .bd-search-result:first-child > .bd-search-item {
+ @include focus-ring(true, var(--bs-accent-focus-ring));
+ --focus-ring-offset: -2px;
+ background-color: var(--bs-bg-1);
+}
+
+// Single template for every clickable row: top-level results, nested
+// subresults, and recently visited items. Variants only differ by icon
+// (`#file-earmark-richtext` vs `#hash`) and a `-sub` modifier that nudges
+// the title down to `font-size-sm`.
+.bd-search-item {
+ display: grid;
+ grid-template-columns: auto 1fr;
+ column-gap: .5rem;
+ padding: .75rem;
+ text-decoration: none;
+ @include border-radius(var(--bs-border-radius-lg));
+
+ &:hover {
+ background-color: var(--bs-bg-1);
+ }
+
+ &:focus-visible {
+ --focus-ring-offset: -2px;
+
+ z-index: 1;
+ @include focus-ring(true, var(--bs-accent-focus-ring));
+ }
+
+ &:not(.bd-search-item-sub) {
+ border: var(--border-width) solid var(--bs-border-color);
+ }
+
mark {
padding: 0;
+ font-weight: 600;
+ color: light-dark(var(--bs-indigo-600), var(--bs-indigo-300));
+ text-decoration: underline;
+ text-decoration-thickness: 1px;
+ text-decoration-color: light-dark(var(--bs-indigo-500), var(--bs-indigo-400));
+ text-underline-offset: 2px;
+ background-color: transparent;
}
}
-.DocSearch-Hit {
- padding-block-end: 0;
- @include border-radius(0);
+.bd-search-item-icon {
+ margin-block-start: .15rem;
+ color: var(--bs-fg-3);
+}
+
+.bd-search-item-title {
+ grid-column: 2;
+ font-weight: 600;
+ color: var(--bs-fg-body);
+}
+
+.bd-search-item-excerpt {
+ display: -webkit-box;
+ grid-column: 2;
+ overflow: hidden;
+ font-size: var(--bs-font-size-sm);
+ line-height: 1.4;
+ color: var(--bs-fg-2);
+ -webkit-line-clamp: 1;
+ -webkit-box-orient: vertical;
+}
+
+.bd-search-item-sub {
+ padding-inline-start: var(--spacer-3);
+ background-color: color-mix(in lab, var(--bs-bg-1), var(--bs-bg-body));
- a {
- @include border-radius(0);
- border: solid var(--bs-border-color);
- border-width: 0 1px 1px;
+ .bd-search-item-title {
+ font-size: var(--bs-font-size-sm);
+ font-weight: 500;
}
+}
+
+// Sub-results are nested inside their parent result row.
+.bd-search-subresults {
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacer-1);
+ padding-inline-start: var(--spacer-6);
+}
- &:first-child a {
- @include border-top-radius(var(--bs-border-radius));
- border-top-width: 1px;
+//
+// Recently visited
+//
+
+.bd-search-recent-header {
+ display: flex;
+ gap: .5rem;
+ align-items: center;
+ justify-content: space-between;
+ padding-block: .5rem;
+}
+
+.bd-search-recent-label {
+ font-size: var(--bs-font-size-xs);
+ font-weight: 600;
+ color: var(--bs-fg-3);
+ text-transform: uppercase;
+ letter-spacing: .04em;
+}
+
+.bd-search-recent-clear {
+ padding: .125rem .375rem;
+ font-size: var(--bs-font-size-xs);
+ color: var(--bs-fg-3);
+ cursor: pointer;
+ background-color: transparent;
+ border: 0;
+ @include border-radius(var(--bs-border-radius-sm));
+ @include transition(color .15s ease-in-out, background-color .15s ease-in-out);
+
+ &:hover {
+ color: var(--bs-fg-body);
+ background-color: var(--bs-bg-1);
}
- &:last-child a {
- @include border-bottom-radius(var(--bs-border-radius));
+
+ &:focus-visible {
+ @include focus-ring(true);
}
}
-.DocSearch-Hit-icon {
+//
+// Empty / loading / error states
+//
+
+.bd-search-empty,
+.bd-search-error {
+ padding: 2rem 1rem;
+ font-size: var(--bs-font-size-sm);
+ color: var(--bs-fg-3);
+ text-align: center;
+}
+
+.bd-search-error {
+ color: var(--bs-danger-text-emphasis);
+}
+
+.bd-search-loading {
display: flex;
- align-items: center;
+ flex-direction: column;
+ gap: .25rem;
+ padding: 0;
}
-// Fix --docsearch-logo-color that doesn't do anything
-.DocSearch-Logo svg .cls-1,
-.DocSearch-Logo svg .cls-2 {
- fill: var(--docsearch-logo-color);
+.bd-search-skeleton {
+ display: flex;
+ flex-direction: column;
+ gap: .375rem;
+ padding: .75rem 1rem;
+
+ & + & {
+ border-block-start: var(--bs-border-width) solid var(--bs-border-color);
+ }
+
+ .placeholder {
+ display: block;
+ height: .65rem;
+ @include border-radius(var(--bs-border-radius-sm));
+ }
+
+ .placeholder.col-4 {
+ height: .85rem;
+ }
}
* For details, see https://creativecommons.org/licenses/by/3.0/.
*/
-@use "@docsearch/css/dist/style";
+// Custom search UI styling. We no longer ship Pagefind's vendored
+// component CSS — the search experience is built from Bootstrap
+// primitives (.dialog, .form-control, .list-group) and styled in
+// `_search.scss`.
@use "search";
return storedTheme
}
- return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
+ return 'auto'
}
const setTheme = theme => {