diff --git a/.gitignore b/.gitignore index b2d6de3..f7420de 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +.vscode diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 1606369..88ae64a 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -1,8 +1,7 @@ -import {themes as prismThemes} from 'prism-react-renderer'; -import type {Config} from '@docusaurus/types'; -import type * as Preset from '@docusaurus/preset-classic'; -import remarkMath from 'remark-math'; -import rehypeKatex from 'rehype-katex'; +import type * as Preset from '@docusaurus/preset-classic' +import type { Config } from '@docusaurus/types' +import rehypeKatex from 'rehype-katex' +import remarkMath from 'remark-math' // This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) @@ -37,7 +36,7 @@ const config: Config = { mermaid: true, }, themes: ['@docusaurus/theme-mermaid'], - + plugins: ['docusaurus-plugin-sass'], presets: [ [ 'classic', @@ -49,48 +48,32 @@ const config: Config = { rehypePlugins: [rehypeKatex], // Please change this to your repo. // Remove this to remove the "edit this page" links. - editUrl: - 'https://github.com/optimainetwork/docs/tree/main/', + editUrl: 'https://github.com/optimainetwork/docs/tree/main/', }, - // blog: { - // showReadingTime: true, - // feedOptions: { - // type: ['rss', 'atom'], - // xslt: true, - // }, - // // Please change this to your repo. - // // Remove this to remove the "edit this page" links. - // editUrl: - // 'https://github.com/optimainetwork/docs/tree/main/', - // // Useful options to enforce blogging best practices - // onInlineTags: 'warn', - // onInlineAuthors: 'warn', - // onUntruncatedBlogPosts: 'warn', - // }, theme: { - customCss: './src/css/custom.css', + customCss: './src/styles/index.scss', }, } satisfies Preset.Options, ], ], - themeConfig: { + themeConfig: { image: 'img/social-card.jpeg', - algolia: { - appId: '21H8NIK27E', + algolia: { + appId: '21H8NIK27E', apiKey: '2764b00b42701493a032fe50aab6c8f4', indexName: 'optimai', - contextualSearch: true, - externalUrlRegex: 'external\\.com|domain\\.com', + contextualSearch: true, + externalUrlRegex: 'external\\.com|domain\\.com', // replaceSearchResultPathname: { // from: '/docs/', // or as RegExp: /\/docs\// // to: '/', // }, // Optional: Algolia search parameters - searchParameters: {}, - searchPagePath: 'search', + searchParameters: {}, + searchPagePath: 'search', insights: false, }, @@ -99,29 +82,28 @@ const config: Config = { disableSwitch: true, respectPrefersColorScheme: false, }, + navbar: { - title: 'OptimAI Network', logo: { alt: 'OptimAI Network Logo', - src: 'img/optimai-logo.svg', + src: 'img/branding/optimai-documentation-logo.svg', }, items: [ - { - type: 'docSidebar', - sidebarId: 'tutorialSidebar', - position: 'left', - label: 'Documentation', - }, - // {to: '/blog', label: 'Whitepaper', position: 'left'}, { href: 'https://github.com/optimainetwork/docs', label: 'GitHub', position: 'right', + className: 'navbar__item--github', }, ], }, footer: { style: 'dark', + logo: { + src: 'img/branding/optimai-documentation-logo.svg', + alt: 'OptimAI Network Logo', + href: 'https://optimai.network', + }, links: [ { title: 'Docs', @@ -161,14 +143,7 @@ const config: Config = { ], copyright: `Copyright © ${new Date().getFullYear()} OptimAI Network.`, }, - // prism: { - // theme: prismThemes.github, - // darkTheme: prismThemes.dracula, - // }, - // mermaid: { - // theme: {light: 'neutral', dark: 'forest'}, - // }, } satisfies Preset.ThemeConfig, -}; +} -export default config; +export default config diff --git a/package-lock.json b/package-lock.json index f91d525..f418aa4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,12 +14,14 @@ "@docusaurus/theme-search-algolia": "^3.7.0", "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", + "docusaurus-plugin-sass": "^0.2.6", "prism-react-renderer": "^2.3.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-player": "^2.16.0", "rehype-katex": "^7.0.1", - "remark-math": "^6.0.0" + "remark-math": "^6.0.0", + "sass": "^1.85.1" }, "devDependencies": { "@docusaurus/module-type-aliases": "3.7.0", @@ -4142,6 +4144,302 @@ "node": ">= 8" } }, + "node_modules/@parcel/watcher": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/@pnpm/config.env-replace": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", @@ -8006,6 +8304,19 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "license": "Apache-2.0", + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/detect-node": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", @@ -8098,6 +8409,19 @@ "node": ">=6" } }, + "node_modules/docusaurus-plugin-sass": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/docusaurus-plugin-sass/-/docusaurus-plugin-sass-0.2.6.tgz", + "integrity": "sha512-2hKQQDkrufMong9upKoG/kSHJhuwd+FA3iAe/qzS/BmWpbIpe7XKmq5wlz4J5CJaOPu4x+iDJbgAxZqcoQf0kg==", + "license": "MIT", + "dependencies": { + "sass-loader": "^16.0.2" + }, + "peerDependencies": { + "@docusaurus/core": "^2.0.0-beta || ^3.0.0-alpha", + "sass": "^1.30.0" + } + }, "node_modules/dom-converter": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", @@ -10307,6 +10631,12 @@ "url": "https://opencollective.com/immer" } }, + "node_modules/immutable": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz", + "integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==", + "license": "MIT" + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -13782,6 +14112,13 @@ "tslib": "^2.0.3" } }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT", + "optional": true + }, "node_modules/node-emoji": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", @@ -17216,6 +17553,94 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/sass": { + "version": "1.85.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.85.1.tgz", + "integrity": "sha512-Uk8WpxM5v+0cMR0XjX9KfRIacmSG86RH4DCCZjLU2rFh5tyutt9siAXJ7G+YfxQ99Q6wrRMbMlVl6KqUms71ag==", + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass-loader": { + "version": "16.0.5", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.5.tgz", + "integrity": "sha512-oL+CMBXrj6BZ/zOq4os+UECPL+bWqt6OAC6DWS8Ln8GZRcMDjlJ4JC3FBDuHJdYaFWIdKNIBYmtZtK2MaMkNIw==", + "license": "MIT", + "dependencies": { + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/sass/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sass/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/sax": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", diff --git a/package.json b/package.json index 9f4282b..eedcd78 100644 --- a/package.json +++ b/package.json @@ -21,12 +21,14 @@ "@docusaurus/theme-search-algolia": "^3.7.0", "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", + "docusaurus-plugin-sass": "^0.2.6", "prism-react-renderer": "^2.3.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-player": "^2.16.0", "rehype-katex": "^7.0.1", - "remark-math": "^6.0.0" + "remark-math": "^6.0.0", + "sass": "^1.85.1" }, "devDependencies": { "@docusaurus/module-type-aliases": "3.7.0", diff --git a/src/css/custom.css b/src/css/custom.css deleted file mode 100644 index fb75861..0000000 --- a/src/css/custom.css +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Any CSS included here will be global. The classic template - * bundles Infima by default. Infima is a CSS framework designed to - * work well for content-centric websites. - */ - - @import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700&display=swap'); - -/* You can override the default Infima variables here. */ -:root { - --ifm-color-primary: #2e8555; - --ifm-color-primary-dark: #29784c; - --ifm-color-primary-darker: #277148; - --ifm-color-primary-darkest: #205d3b; - --ifm-color-primary-light: #33925d; - --ifm-color-primary-lighter: #359962; - --ifm-color-primary-lightest: #3cad6e; - --ifm-code-font-size: 95%; - --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); - --ifm-font-family-base: 'Plus Jakarta Sans', sans-serif; - --ifm-font-size-base: 18px; -} - -/* For readability concerns, you should choose a lighter palette in dark mode. */ -[data-theme='dark'] { - --ifm-color-primary: #25c2a0; - --ifm-color-primary-dark: #21af90; - --ifm-color-primary-darker: #1fa588; - --ifm-color-primary-darkest: #1a8870; - --ifm-color-primary-light: #29d5b0; - --ifm-color-primary-lighter: #32d8b4; - --ifm-color-primary-lightest: #4fddbf; - --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); -} - -@font-face { - font-family: "Actual"; - src: url("/fonts/actual.otf") format("otf"); -} - -.actual-font { - font-family: "Actual", sans-serif; -} - -.navbar__title { - font-family: "Actual", sans-serif; -} - -/* .menu__link { - font-family: "Actual", sans-serif; -} */ - -.footer { - border-top: 1px solid rgba(255, 255, 255, 0.2); - padding-top: 20px; - background-color: var(--ifm-background-color); -} \ No newline at end of file diff --git a/src/hooks/useIsomorphicLayoutEffect.ts b/src/hooks/useIsomorphicLayoutEffect.ts new file mode 100644 index 0000000..5d7dc3c --- /dev/null +++ b/src/hooks/useIsomorphicLayoutEffect.ts @@ -0,0 +1,3 @@ +import { useEffect, useLayoutEffect } from 'react' + +export const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect diff --git a/src/hooks/useMediaQuery.ts b/src/hooks/useMediaQuery.ts new file mode 100644 index 0000000..312d956 --- /dev/null +++ b/src/hooks/useMediaQuery.ts @@ -0,0 +1,57 @@ +import { useIsomorphicLayoutEffect } from '@site/src/hooks/useIsomorphicLayoutEffect' +import { useState } from 'react' + +type UseMediaQueryOptions = { + defaultValue?: boolean + initializeWithValue?: boolean +} + +const IS_SERVER = typeof window === 'undefined' + +export function useMediaQuery( + query: string, + { defaultValue = false, initializeWithValue = true }: UseMediaQueryOptions = {} +): boolean { + const getMatches = (query: string): boolean => { + if (IS_SERVER) { + return defaultValue + } + return window.matchMedia(query).matches + } + + const [matches, setMatches] = useState(() => { + if (initializeWithValue) { + return getMatches(query) + } + return defaultValue + }) + + // Handles the change event of the media query. + function handleChange() { + setMatches(getMatches(query)) + } + + useIsomorphicLayoutEffect(() => { + const matchMedia = window.matchMedia(query) + + // Triggered at the first client-side load and if query changes + handleChange() + + // Use deprecated `addListener` and `removeListener` to support Safari < 14 (#135) + if (matchMedia.addListener) { + matchMedia.addListener(handleChange) + } else { + matchMedia.addEventListener('change', handleChange) + } + + return () => { + if (matchMedia.removeListener) { + matchMedia.removeListener(handleChange) + } else { + matchMedia.removeEventListener('change', handleChange) + } + } + }, [query]) + + return matches +} diff --git a/src/styles/admonition.scss b/src/styles/admonition.scss new file mode 100644 index 0000000..fe82a1b --- /dev/null +++ b/src/styles/admonition.scss @@ -0,0 +1,51 @@ +@use '@site/src/styles/common/color.scss' as *; +@use '@site/src/styles/common/media.scss' as *; + +.alert { + position: relative; + padding: 2rem 2rem 2rem 2.4rem; + border: none; + border-radius: 1.2rem; + overflow: hidden; + color: rgba(white, 0.8); + --border-color: rgba(255, 255, 255, 0.3); + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 0.4rem; + height: 100%; + background: var(--border-color); + } + + strong { + color: white; + } +} + +.alert--secondary { + --border-color: rgba(255, 255, 255, 0.3); + background-color: rgba(255, 255, 255, 0.1); +} + +.alert--success { + background-color: rgba($accent, 0.8); + --border-color: linear-gradient(90deg, #f6f655 0%, #5eed87 100%); +} + +.alert--info { + background-color: rgba($background, 0.8); + --border-color: white; +} + +.alert--warning { + background-color: rgba(205, 177, 80, 0.05); + --border-color: #cdb150; +} + +.alert--danger { + background-color: rgba(241, 65, 88, 0.1); + --border-color: #f14158; +} diff --git a/src/styles/breadcrumb.scss b/src/styles/breadcrumb.scss new file mode 100644 index 0000000..b48ed6f --- /dev/null +++ b/src/styles/breadcrumb.scss @@ -0,0 +1,66 @@ +@use './common/color.scss' as *; +@use './common/media.scss' as *; + +.theme-doc-breadcrumbs { + @include xl { + --margin-bottom: 3.2rem !important; + } +} + +.breadcrumbs { + display: flex; + align-items: center; + gap: 1.2rem; +} + +.breadcrumbs__item { + display: inline-flex; + align-items: center; + flex-shrink: 0; + gap: 1.2rem; +} + +.breadcrumbs__item:not(:last-child):after { + background: url('data:image/svg+xml,'); + width: 1.6rem; + height: 1.6rem; + filter: none; +} + +.breadcrumbs__item--home { + .breadcrumbs__link { + padding: 0; + background-color: transparent; + + &:hover { + background-color: transparent !important; + } + } +} + +.breadcrumbs__item--active { + .breadcrumbs__link { + border-color: rgba(white, 0.1); + background-color: rgba($secondary, 0.6); + color: white; + } +} + +.breadcrumbs__link { + border-radius: 5.6rem; + display: flex; + align-items: center; + justify-content: center; + height: 3.6rem; + padding: 0 1.2rem; + font-size: 1.4rem; + line-height: normal; + color: rgba(white, 0.5); + border: 1px solid transparent; + background-color: rgba($secondary, 0.6); + + &:hover { + --ifm-breadcrumb-item-background-active: #{rgba($secondary, 0.8)}; + color: white; + } +} diff --git a/src/styles/common/color.scss b/src/styles/common/color.scss new file mode 100644 index 0000000..a9d57f4 --- /dev/null +++ b/src/styles/common/color.scss @@ -0,0 +1,6 @@ +$background: #121614; +$secondary: #242926; +$accent: #313532; +$muted-background: #71717a; +$gradient: linear-gradient(90deg, #f6f655 0%, #5eed87 100%); +$radial-05: radial-gradient(119.09% 136.65% at 2.81% 0%, rgba(228, 228, 231, 0.05) 0%, rgba(115, 115, 115, 0.05) 100%); diff --git a/src/styles/common/index.scss b/src/styles/common/index.scss new file mode 100644 index 0000000..8e8d6bd --- /dev/null +++ b/src/styles/common/index.scss @@ -0,0 +1,2 @@ +@use './color.scss' as *; +@use './media.scss' as *; diff --git a/src/styles/common/media.scss b/src/styles/common/media.scss new file mode 100644 index 0000000..b240db3 --- /dev/null +++ b/src/styles/common/media.scss @@ -0,0 +1,65 @@ +@use 'sass:map'; +@use 'sass:string'; + +// Định nghĩa breakpoints với !default để có thể ghi đè nếu cần +$breakpoints: ( + md: 768px, + lg: 997px, + xl: 1280px, +) !default; + +// Hàm lấy giá trị từ breakpoints +@function get-breakpoint($key) { + @if map.has-key($breakpoints, $key) { + @return map.get($breakpoints, $key); + } + @return $key; // Nếu không tìm thấy, trả về giá trị raw (px, em, rem...) +} + +// Mixin media query nâng cao +@mixin media($queries...) { + $media-query: ''; + + @each $query in $queries { + $operator: string.slice($query, 1, 1); + $value: string.slice($query, 2); + + @if $media-query == '' { + // First condition doesn't need the "and" prefix + @if $operator == '>' { + $media-query: '(min-width: #{get-breakpoint($value)})'; + } @else if $operator == '<' { + $media-query: '(max-width: #{get-breakpoint($value) - 1px})'; + } + } @else { + // Additional conditions need "and" prefix + @if $operator == '>' { + $media-query: '#{$media-query} and (min-width: #{get-breakpoint($value)})'; + } @else if $operator == '<' { + $media-query: '#{$media-query} and (max-width: #{get-breakpoint($value) - 1px})'; + } + } + } + + @media #{$media-query} { + @content; + } +} + +@mixin md { + @include media('>md') { + @content; + } +} + +@mixin lg { + @include media('>lg') { + @content; + } +} + +@mixin xl { + @include media('>xl') { + @content; + } +} diff --git a/src/styles/container.scss b/src/styles/container.scss new file mode 100644 index 0000000..4b16adb --- /dev/null +++ b/src/styles/container.scss @@ -0,0 +1,10 @@ +@use './common/color.scss' as *; +@use './common/media.scss' as *; + +.container { + padding: 0rem 2rem; + + @include xl { + padding: 0rem 2.4rem; + } +} diff --git a/src/styles/fonts.scss b/src/styles/fonts.scss new file mode 100644 index 0000000..990f3bd --- /dev/null +++ b/src/styles/fonts.scss @@ -0,0 +1,6 @@ +@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700&display=swap'); + +@font-face { + font-family: 'Actual'; + src: url('/fonts/actual.otf') format('otf'); +} diff --git a/src/styles/footer.scss b/src/styles/footer.scss new file mode 100644 index 0000000..6273000 --- /dev/null +++ b/src/styles/footer.scss @@ -0,0 +1,101 @@ +@use './common/color.scss' as *; +@use './common/media.scss' as *; + +.footer { + background-color: $background; + padding: 2.4rem 2rem; + + @include xl { + padding: 3.2rem 4rem; + } +} + +.footer__inner { + display: grid; + gap: 2.4rem; + + @include md { + grid-template-columns: 1fr 1fr; + column-gap: 20rem; + } + + @include xl { + grid-template-columns: 1fr auto; + column-gap: 12rem; + } +} + +.footer__logo { + height: 3.2rem; + + @include xl { + height: 4rem; + } +} + +.footer__links { + display: grid; + gap: 4rem; + grid-row-start: span 2; + + @include xl { + grid-template-columns: repeat(3, 18.4rem); + column-gap: 12rem; + } +} + +.footer__col { + margin: 0; + display: flex; + flex-direction: column; + gap: 1.2rem; +} + +.footer__title { + margin: 0; + font-size: 1.6rem; + line-height: 1.5; + color: white; + font-weight: 700; + + @include xl { + font-size: 1.8rem; + } +} + +.footer__items { + display: flex; + flex-direction: column; + gap: 1.2rem; +} + +.footer__link-item { + font-size: 1.6rem; + line-height: 1.5; + font-weight: 500; + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + + &:hover { + color: #b9f167; + text-decoration: none; + } + + @include xl { + font-size: 1.8rem; + } +} + +.footer__copyright { + font-size: 1.4rem; + line-height: 1.5; + font-weight: 500; + color: white; + opacity: 0.5; + + @include md { + align-content: end; + } +} diff --git a/src/styles/index.scss b/src/styles/index.scss new file mode 100644 index 0000000..a29e65d --- /dev/null +++ b/src/styles/index.scss @@ -0,0 +1,13 @@ +@use './variables.scss'; +@use './fonts.scss'; +@use './reset.scss'; +@use './container.scss'; +@use './nav.scss'; +@use './sidebar.scss'; +@use './footer.scss'; +@use './search-modal.scss'; +@use './breadcrumb.scss'; +@use './toc.scss'; +@use './markdown.scss'; +@use './pagination.scss'; +@use './admonition.scss'; diff --git a/src/styles/markdown.scss b/src/styles/markdown.scss new file mode 100644 index 0000000..8f833dc --- /dev/null +++ b/src/styles/markdown.scss @@ -0,0 +1,89 @@ +@use '@site/src/styles/common/color.scss' as *; +@use '@site/src/styles/common/media.scss' as *; + +.markdown { + --ifm-heading-line-height: 1.5; + --ifm-leading-desktop: 1.5; + color: rgba(white, 0.8); + + h1, + h2, + h3, + h4, + h5 { + color: white; + } + + h1 { + font-size: 2.4rem; + } + + h2 { + font-size: 2rem; + } + + h3 { + font-size: 1.8rem; + } + + p { + font-size: 1.4rem; + } + code { + font-size: 1.4rem; + } + + li + li { + margin-top: 0.4rem; + } + + p:has(+ h2) { + margin-bottom: 0; + } + + table { + --ifm-table-border-color: rgba(255, 255, 255, 0.08); + border-radius: 0.8rem; + overflow: hidden; + color: white; + + thead { + tr { + background-color: rgba($secondary, 0.6); + } + } + tbody { + tr { + background-color: $accent; + } + } + } +} + +@include xl { + .markdown { + h1 { + font-size: 3.2rem; + } + + h2 { + font-size: 2.8rem; + } + + h3 { + font-size: 2.4rem; + } + + h4 { + font-size: 2rem; + } + + p { + font-size: 1.6rem; + } + + code { + font-size: 1.6rem; + } + } +} diff --git a/src/styles/nav.scss b/src/styles/nav.scss new file mode 100644 index 0000000..ec47afc --- /dev/null +++ b/src/styles/nav.scss @@ -0,0 +1,126 @@ +@use './common/color.scss' as *; +@use './common/media.scss' as *; + +.navbar { + height: var(--ifm-navbar-height); + padding: 0; + background-color: transparent; +} + +.navbar__inner { + height: 100%; + padding: 0 1.6rem; + align-items: center; + backdrop-filter: blur(50px); + background-color: rgba($background, 0.6); + + @include xl { + padding-right: 4rem; + padding-left: 4rem; + } +} + +.navbar__toggle { + margin-right: 1.6rem; + + svg { + width: 2.4rem; + height: 2.4rem; + } +} + +.navbar-sidebar__brand { + padding: 2rem 1.6rem 0.4rem; +} + +.navbar__logo { + height: 2.8rem; + + @include lg { + height: 4rem; + } +} + +.navbar__items--right { + gap: 0.8rem; +} + +.navbar__item { + &.navbar__item--github { + display: block; + width: 12.6rem; + height: 4.4rem; + background-image: $gradient; + color: black; + font-size: 1.6rem; + line-height: 2rem; + font-weight: 600; + border-radius: 0.8rem; + display: flex; + align-items: center; + justify-content: center; + gap: 0.8rem; + order: 1; + box-shadow: 0px -5px 10px 0px rgba(255, 255, 255, 0.3) inset; + + &:hover { + background-image: linear-gradient(0deg, rgba(255, 255, 255, 0.3) 0%, rgba(255, 255, 255, 0.3) 100%), + linear-gradient(90deg, #f6f655 0%, #5eed87 100%); + } + + @include xl { + width: 20rem; + } + } +} + +.navbarSearchContainer_Bca1 { + position: unset !important; + + .DocSearch-Button { + height: 4.4rem; + min-width: 4.4rem; + justify-content: center; + border: 1px solid rgba($accent, 0.8); + background-color: rgba($accent, 0.8); + border-radius: 0.8rem; + transition-property: background-color, border-color; + transition-duration: 0.2s; + transition-timing-function: ease-in-out; + + @include md { + min-width: 20rem; + justify-content: start; + gap: 1.2rem; + padding: 0 1.6rem; + } + + @include xl { + min-width: 40rem; + } + + &:active, + &:focus, + &:hover { + border-color: rgba(white, 0.2); + background-color: rgba($accent, 0.9); + box-shadow: none; + } + } + + .DocSearch-Search-Icon { + width: 2.4rem; + height: 2.4rem; + } + + .DocSearch-Button-Placeholder { + font-size: 1.6rem; + line-height: 1.63; + font-weight: 500; + color: $muted-background; + } + + .DocSearch-Button-Keys { + display: none; + } +} diff --git a/src/styles/pagination.scss b/src/styles/pagination.scss new file mode 100644 index 0000000..083eaea --- /dev/null +++ b/src/styles/pagination.scss @@ -0,0 +1,99 @@ +@use './common/color.scss' as *; +@use './common/media.scss' as *; + +.pagination-nav { + gap: 1.2rem; + margin-top: 2.4rem; + min-height: 4.4rem; +} + +.pagination-nav__link { + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.4rem; + border-radius: 0.8rem; + border: 0.1rem solid rgba(white, 0.1); + background-color: rgba($accent, 0.1); + box-shadow: 0px 0px 4px 0px rgba(255, 255, 255, 0.12) inset; + + &:hover { + border-color: rgba(white, 0.1); + background-color: rgba($accent, 0.3); + + .pagination-nav__sublabel { + opacity: 1; + } + } +} + +.pagination-nav__sublabel { + display: flex; + align-items: center; + justify-content: center; + gap: 0.4rem; + margin-bottom: 0; + color: white; + opacity: 0.8; + + span { + font-size: 1.4rem; + font-weight: 500; + line-height: 1.5; + } + + svg { + display: block; + transform: translateY(1px); + } +} + +.pagination-nav__label { + display: none; +} + +.pagination-nav__label { + &::after, + &::before { + display: none; + } + + span { + font-size: 1.6rem; + font-weight: 600; + line-height: 1.5; + } + + svg { + display: block; + transform: translateY(1px); + } +} + +@include md { + .pagination-nav__link { + padding: 1rem 1.6rem; + } + + .pagination-nav__link--prev { + align-items: start; + } + + .pagination-nav__link--next { + align-items: end; + } + + .pagination-nav__sublabel { + font-size: 1.4rem; + svg { + display: none; + } + } + + .pagination-nav__label { + display: flex; + align-items: center; + } +} diff --git a/src/styles/reset.scss b/src/styles/reset.scss new file mode 100644 index 0000000..1b9ba4c --- /dev/null +++ b/src/styles/reset.scss @@ -0,0 +1,9 @@ +html { + background-color: #0f0f0f; +} + +body { + font-size: 1.6rem; + line-height: 1.5; + background-color: #0f0f0f; +} diff --git a/src/styles/search-modal.scss b/src/styles/search-modal.scss new file mode 100644 index 0000000..af29d71 --- /dev/null +++ b/src/styles/search-modal.scss @@ -0,0 +1,124 @@ +@use './common/color.scss' as *; +@use './common/media.scss' as *; + +.DocSearch-Container { + --docsearch-modal-background: #{$background}; + --docsearch-muted-color: white; + --docsearch-highlight-color: #5eed87; + --docsearch-hit-height: 6rem; + + background-color: rgba(black, 0.8) !important; + + .DocSearch-SearchBar { + padding-bottom: 0.8rem; + background-color: $background; + border-top-right-radius: 0.8rem; + border-top-left-radius: 0.8rem; + } + + .DocSearch-Modal { + background-color: $background; + box-shadow: none; + } + + .DocSearch-Form { + height: 4.4rem; + background-color: rgba($accent, 0.8); + box-shadow: none; + padding: 0 1.6rem; + border-radius: 0.8rem; + } + + .DocSearch-MagnifierLabel { + color: white; + width: 2.4rem; + height: 2.4rem; + + svg { + width: 2rem; + height: 2rem; + } + } + + .DocSearch-Input { + font-size: 1.6rem; + line-height: 1.63; + font-weight: 500; + color: white; + + &::placeholder { + color: rgba(white, 0.5); + } + } + + .DocSearch-Help { + font-size: 1.4rem; + line-height: 1.63; + color: rgba(white, 0.5); + } + + .DocSearch-Hit { + scroll-margin-top: 6rem; + padding-bottom: 0.8rem; + + a { + border: 0.1rem solid rgba($secondary, 0.6); + background-color: rgba($secondary, 0.6); + border-radius: 0.6rem; + padding-left: 1.2rem; + } + + &[aria-selected='true'] { + a { + background-color: $secondary; + border-color: rgba(white, 0.12); + } + mark { + --docsearch-hit-active-color: #5eed87; + } + } + } + + .DocSearch-Hit-Container, + .DocSearch-Hit-action, + .DocSearch-Hit-icon, + .DocSearch-Hit-Tree { + color: white; + } + + .DocSearch-Hit-Tree { + opacity: 0.1; + } + + .DocSearch-Hit-source { + background-color: $background; + font-size: 1.8rem; + padding: 1.2rem 0 0.8rem; + color: white; + margin: 0; + } + + .DocSearch-Hit-title { + font-size: 1.6rem; + line-height: 1.5; + } + + .DocSearch-Hit-path { + font-size: 1.4rem; + line-height: 1.5; + opacity: 0.8; + } + + .DocSearch-HitsFooter { + a { + border: none; + font-weight: 600; + font-size: 1.6rem; + line-height: 150%; + } + } + + .DocSearch-Footer { + display: none; + } +} diff --git a/src/styles/search-page.scss b/src/styles/search-page.scss new file mode 100644 index 0000000..78b0749 --- /dev/null +++ b/src/styles/search-page.scss @@ -0,0 +1,26 @@ +@use '@site/src/styles/common/color.scss' as *; +@use '@site/src/styles/common/media.scss' as *; + +.searchQueryColumn_RTkw { + .searchQueryInput_u2C7 { + font-size: 1.6rem; + line-height: 1.63; + font-weight: 500; + color: white; + height: 4.4rem; + background-color: rgba($accent, 0.8); + box-shadow: none; + padding: 0 1.6rem; + border-radius: 0.8rem; + border-width: 1px; + margin-bottom: 0.8rem; + + &::placeholder { + color: rgba(white, 0.5); + } + + &:focus { + border-color: rgba(255, 255, 255, 0.4); + } + } +} diff --git a/src/styles/sidebar.scss b/src/styles/sidebar.scss new file mode 100644 index 0000000..566337e --- /dev/null +++ b/src/styles/sidebar.scss @@ -0,0 +1,99 @@ +@use './common/color.scss' as *; +@use './common/media.scss' as *; + +.menu { + padding: 2rem 1.6rem 2rem 1.6rem; + background-color: rgba($secondary, 0.6); + + @include lg { + padding: 2rem 1.6rem 2rem 2.4rem !important; + } +} + +.navbar-sidebar { + background-color: $secondary; +} + +.navbar-sidebar__close { + width: 2.4rem; + height: 2.4rem; + justify-content: center; + align-items: center; + + svg { + width: 1.8rem; + height: 1.8rem; + } +} + +.navbar-sidebar__back { + display: none; +} + +.menu__list-item { + &:not(:first-child) { + margin-top: 0.8rem; + } + + .menu__link { + font-size: 1.6rem; + line-height: 1.5; + font-weight: 500; + color: white !important; + } + + & > { + .menu__link, + .menu__list-item-collapsible { + padding: 0.8rem 1.6rem; + opacity: 0.5; + border-radius: 0.8rem; + border: 0.1rem solid transparent; + transition-property: opacity, background-color, border-color; + transition-duration: 0.3s; + transition-timing-function: ease-in-out; + + &:hover { + opacity: 1; + background-color: rgba($accent, 0.5); + border-color: rgba(white, 0.04); + } + } + + .menu__link--active, + .menu__list-item-collapsible--active { + opacity: 1; + background-color: $accent; + border-color: rgba(white, 0.1); + + &:hover { + background-color: $accent; + border-color: rgba(white, 0.1); + } + } + .menu__list-item-collapsible:has(.menu__link--active) { + opacity: 1; + background-color: $accent; + border-color: rgba(white, 0.1); + } + } + + .menu__list { + padding-left: 2.4rem; + .menu__link { + border: none; + background-color: transparent; + } + } +} + +.menu__link--sublist { + padding: 0; +} + +.menu__caret { + &:hover, + &:active { + background-color: transparent; + } +} diff --git a/src/styles/toc.scss b/src/styles/toc.scss new file mode 100644 index 0000000..e26a4d3 --- /dev/null +++ b/src/styles/toc.scss @@ -0,0 +1,64 @@ +@use './common/color.scss' as *; +@use './common/media.scss' as *; + +.theme-doc-toc-desktop { + .table-of-contents { + width: 100%; + padding: 2rem; + background: $radial-05; + border-radius: 1.2rem; + border: 0.1rem solid rgba($color: white, $alpha: 0.1); + + li { + margin: 0; + position: relative; + + &:not(:first-child) { + margin-top: 1.2rem; + } + + &::before { + content: ''; + position: absolute; + top: 0.6rem; + left: 0; + width: 0.4rem; + height: 1.2rem; + background: #f6f655; + border-radius: 0.4rem; + opacity: 0; + transition: opacity 0.2s ease-in-out; + } + + &:has(> .table-of-contents__link--active) { + &::before { + opacity: 1; + } + } + } + + a { + font-size: 1.6rem; + font-weight: normal; + line-height: normal; + color: white; + opacity: 0.5; + transition: opacity 0.2s ease-in-out, color 0.2s ease-in-out, transform 0.2s ease-in-out; + + &:hover { + opacity: 1; + } + } + + ul { + margin-top: 1.2rem; + padding-left: 1.6rem; + } + + .table-of-contents__link--active { + color: #f6f655; + opacity: 1; + transform: translateX(1.2rem); + } + } +} diff --git a/src/styles/variables.scss b/src/styles/variables.scss new file mode 100644 index 0000000..799e4ef --- /dev/null +++ b/src/styles/variables.scss @@ -0,0 +1,29 @@ +@use './common/color.scss' as *; +@use './common/media.scss' as *; + +:root { + --ifm-color-primary: white; + --ifm-color-primary-dark: #29784c; + --ifm-color-primary-darker: #277148; + --ifm-color-primary-darkest: #205d3b; + --ifm-color-primary-light: #33925d; + --ifm-color-primary-lighter: #359962; + --ifm-color-primary-lightest: #3cad6e; + --ifm-code-font-size: 160%; + --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); + --ifm-font-family-base: 'Plus Jakarta Sans', sans-serif; + --ifm-font-size-base: 10px; + --ifm-navbar-height: 6.4rem; + + --ifm-navbar-background-color: #{$secondary}; + --doc-sidebar-width: 36rem !important; + ---ifm-link-color: white; + --docsearch-highlight-color: #{$accent}; + --ifm-h1-font-size: 2.4rem; +} + +@include lg { + :root { + --ifm-navbar-height: 8rem; + } +} diff --git a/src/theme/Admonition/Icon/Danger.tsx b/src/theme/Admonition/Icon/Danger.tsx new file mode 100644 index 0000000..7dc2274 --- /dev/null +++ b/src/theme/Admonition/Icon/Danger.tsx @@ -0,0 +1,16 @@ +import type { Props } from '@theme/Admonition/Icon/Danger' +import { type ReactNode } from 'react' + +export default function AdmonitionIconDanger(props: Props): ReactNode { + return ( + + + + ) +} diff --git a/src/theme/Admonition/Icon/Info.tsx b/src/theme/Admonition/Icon/Info.tsx new file mode 100644 index 0000000..e4c20b1 --- /dev/null +++ b/src/theme/Admonition/Icon/Info.tsx @@ -0,0 +1,16 @@ +import type { Props } from '@theme/Admonition/Icon/Info' +import { type ReactNode } from 'react' + +export default function AdmonitionIconInfo(props: Props): ReactNode { + return ( + + + + ) +} diff --git a/src/theme/Admonition/Icon/Note.tsx b/src/theme/Admonition/Icon/Note.tsx new file mode 100644 index 0000000..0f3e208 --- /dev/null +++ b/src/theme/Admonition/Icon/Note.tsx @@ -0,0 +1,16 @@ +import type { Props } from '@theme/Admonition/Icon/Note' +import { type ReactNode } from 'react' + +export default function AdmonitionIconNote(props: Props): ReactNode { + return ( + + + + ) +} diff --git a/src/theme/Admonition/Icon/Tip.tsx b/src/theme/Admonition/Icon/Tip.tsx new file mode 100644 index 0000000..8c7bd7a --- /dev/null +++ b/src/theme/Admonition/Icon/Tip.tsx @@ -0,0 +1,16 @@ +import type { Props } from '@theme/Admonition/Icon/Tip' +import { type ReactNode } from 'react' + +export default function AdmonitionIconTip(props: Props): ReactNode { + return ( + + + + ) +} diff --git a/src/theme/Admonition/Icon/Warning.tsx b/src/theme/Admonition/Icon/Warning.tsx new file mode 100644 index 0000000..86087f3 --- /dev/null +++ b/src/theme/Admonition/Icon/Warning.tsx @@ -0,0 +1,16 @@ +import type { Props } from '@theme/Admonition/Icon/Warning' +import { type ReactNode } from 'react' + +export default function AdmonitionIconCaution(props: Props): ReactNode { + return ( + + + + ) +} diff --git a/src/theme/Admonition/Layout/index.tsx b/src/theme/Admonition/Layout/index.tsx new file mode 100644 index 0000000..b9848d8 --- /dev/null +++ b/src/theme/Admonition/Layout/index.tsx @@ -0,0 +1,49 @@ +import { ThemeClassNames } from '@docusaurus/theme-common' +import clsx from 'clsx' +import { type ReactNode } from 'react' + +import type { Props } from '@theme/Admonition/Layout' + +import styles from './styles.module.scss' + +function AdmonitionContainer({ + type, + className, + children, +}: Pick & { children: ReactNode }) { + return ( +
+ {children} +
+ ) +} + +function AdmonitionHeading({ icon, title }: Pick) { + return ( +
+ {icon} + {title} +
+ ) +} + +function AdmonitionContent({ children }: Pick) { + return children ?
{children}
: null +} + +export default function AdmonitionLayout(props: Props): ReactNode { + const { type, icon, title, children, className } = props + return ( + + {title || icon ? : null} + {children} + + ) +} diff --git a/src/theme/Admonition/Layout/styles.module.scss b/src/theme/Admonition/Layout/styles.module.scss new file mode 100644 index 0000000..4100e11 --- /dev/null +++ b/src/theme/Admonition/Layout/styles.module.scss @@ -0,0 +1,32 @@ +.admonition { + margin-bottom: 1.6rem; +} + +.admonitionHeading { + font-size: 1.6rem; + line-height: 150%; + font-weight: 600; + display: flex; + align-items: center; + gap: 0.4rem; + color: white; + // text-transform: uppercase; +} + +.admonitionHeading:not(:last-child) { + margin-bottom: 1.6rem; +} + +.admonitionHeading code { + text-transform: none; +} + +.admonitionIcon svg { + display: block; + height: 2.4rem; + width: 2.4rem; +} + +.admonitionContent > :last-child { + margin-bottom: 0; +} diff --git a/src/theme/Admonition/Type/Caution.tsx b/src/theme/Admonition/Type/Caution.tsx new file mode 100644 index 0000000..b570a37 --- /dev/null +++ b/src/theme/Admonition/Type/Caution.tsx @@ -0,0 +1,32 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import Translate from '@docusaurus/Translate'; +import type {Props} from '@theme/Admonition/Type/Caution'; +import AdmonitionLayout from '@theme/Admonition/Layout'; +import IconWarning from '@theme/Admonition/Icon/Warning'; + +const infimaClassName = 'alert alert--warning'; + +const defaultProps = { + icon: , + title: ( + + caution + + ), +}; + +// TODO remove before v4: Caution replaced by Warning +// see https://github.com/facebook/docusaurus/issues/7558 +export default function AdmonitionTypeCaution(props: Props): ReactNode { + return ( + + {props.children} + + ); +} diff --git a/src/theme/Admonition/Type/Danger.tsx b/src/theme/Admonition/Type/Danger.tsx new file mode 100644 index 0000000..49901fa --- /dev/null +++ b/src/theme/Admonition/Type/Danger.tsx @@ -0,0 +1,30 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import Translate from '@docusaurus/Translate'; +import type {Props} from '@theme/Admonition/Type/Danger'; +import AdmonitionLayout from '@theme/Admonition/Layout'; +import IconDanger from '@theme/Admonition/Icon/Danger'; + +const infimaClassName = 'alert alert--danger'; + +const defaultProps = { + icon: , + title: ( + + danger + + ), +}; + +export default function AdmonitionTypeDanger(props: Props): ReactNode { + return ( + + {props.children} + + ); +} diff --git a/src/theme/Admonition/Type/Info.tsx b/src/theme/Admonition/Type/Info.tsx new file mode 100644 index 0000000..018e0a1 --- /dev/null +++ b/src/theme/Admonition/Type/Info.tsx @@ -0,0 +1,30 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import Translate from '@docusaurus/Translate'; +import type {Props} from '@theme/Admonition/Type/Info'; +import AdmonitionLayout from '@theme/Admonition/Layout'; +import IconInfo from '@theme/Admonition/Icon/Info'; + +const infimaClassName = 'alert alert--info'; + +const defaultProps = { + icon: , + title: ( + + info + + ), +}; + +export default function AdmonitionTypeInfo(props: Props): ReactNode { + return ( + + {props.children} + + ); +} diff --git a/src/theme/Admonition/Type/Note.tsx b/src/theme/Admonition/Type/Note.tsx new file mode 100644 index 0000000..c99e038 --- /dev/null +++ b/src/theme/Admonition/Type/Note.tsx @@ -0,0 +1,30 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import Translate from '@docusaurus/Translate'; +import type {Props} from '@theme/Admonition/Type/Note'; +import AdmonitionLayout from '@theme/Admonition/Layout'; +import IconNote from '@theme/Admonition/Icon/Note'; + +const infimaClassName = 'alert alert--secondary'; + +const defaultProps = { + icon: , + title: ( + + note + + ), +}; + +export default function AdmonitionTypeNote(props: Props): ReactNode { + return ( + + {props.children} + + ); +} diff --git a/src/theme/Admonition/Type/Tip.tsx b/src/theme/Admonition/Type/Tip.tsx new file mode 100644 index 0000000..18604a5 --- /dev/null +++ b/src/theme/Admonition/Type/Tip.tsx @@ -0,0 +1,30 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import Translate from '@docusaurus/Translate'; +import type {Props} from '@theme/Admonition/Type/Tip'; +import AdmonitionLayout from '@theme/Admonition/Layout'; +import IconTip from '@theme/Admonition/Icon/Tip'; + +const infimaClassName = 'alert alert--success'; + +const defaultProps = { + icon: , + title: ( + + tip + + ), +}; + +export default function AdmonitionTypeTip(props: Props): ReactNode { + return ( + + {props.children} + + ); +} diff --git a/src/theme/Admonition/Type/Warning.tsx b/src/theme/Admonition/Type/Warning.tsx new file mode 100644 index 0000000..61d9597 --- /dev/null +++ b/src/theme/Admonition/Type/Warning.tsx @@ -0,0 +1,30 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import Translate from '@docusaurus/Translate'; +import type {Props} from '@theme/Admonition/Type/Warning'; +import AdmonitionLayout from '@theme/Admonition/Layout'; +import IconWarning from '@theme/Admonition/Icon/Warning'; + +const infimaClassName = 'alert alert--warning'; + +const defaultProps = { + icon: , + title: ( + + warning + + ), +}; + +export default function AdmonitionTypeWarning(props: Props): ReactNode { + return ( + + {props.children} + + ); +} diff --git a/src/theme/Admonition/Types.tsx b/src/theme/Admonition/Types.tsx new file mode 100644 index 0000000..2a10019 --- /dev/null +++ b/src/theme/Admonition/Types.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import AdmonitionTypeNote from '@theme/Admonition/Type/Note'; +import AdmonitionTypeTip from '@theme/Admonition/Type/Tip'; +import AdmonitionTypeInfo from '@theme/Admonition/Type/Info'; +import AdmonitionTypeWarning from '@theme/Admonition/Type/Warning'; +import AdmonitionTypeDanger from '@theme/Admonition/Type/Danger'; +import AdmonitionTypeCaution from '@theme/Admonition/Type/Caution'; +import type AdmonitionTypes from '@theme/Admonition/Types'; + +const admonitionTypes: typeof AdmonitionTypes = { + note: AdmonitionTypeNote, + tip: AdmonitionTypeTip, + info: AdmonitionTypeInfo, + warning: AdmonitionTypeWarning, + danger: AdmonitionTypeDanger, +}; + +// Undocumented legacy admonition type aliases +// Provide hardcoded/untranslated retrocompatible label +// See also https://github.com/facebook/docusaurus/issues/7767 +const admonitionAliases: typeof AdmonitionTypes = { + secondary: (props) => , + important: (props) => , + success: (props) => , + caution: AdmonitionTypeCaution, +}; + +export default { + ...admonitionTypes, + ...admonitionAliases, +}; diff --git a/src/theme/Admonition/index.tsx b/src/theme/Admonition/index.tsx new file mode 100644 index 0000000..8f4225d --- /dev/null +++ b/src/theme/Admonition/index.tsx @@ -0,0 +1,21 @@ +import React, {type ComponentType, type ReactNode} from 'react'; +import {processAdmonitionProps} from '@docusaurus/theme-common'; +import type {Props} from '@theme/Admonition'; +import AdmonitionTypes from '@theme/Admonition/Types'; + +function getAdmonitionTypeComponent(type: string): ComponentType { + const component = AdmonitionTypes[type]; + if (component) { + return component; + } + console.warn( + `No admonition component found for admonition type "${type}". Using Info as fallback.`, + ); + return AdmonitionTypes.info!; +} + +export default function Admonition(unprocessedProps: Props): ReactNode { + const props = processAdmonitionProps(unprocessedProps); + const AdmonitionTypeComponent = getAdmonitionTypeComponent(props.type); + return ; +} diff --git a/src/theme/Details/index.tsx b/src/theme/Details/index.tsx new file mode 100644 index 0000000..5bfcb89 --- /dev/null +++ b/src/theme/Details/index.tsx @@ -0,0 +1,14 @@ +import { Details as DetailsGeneric } from '@docusaurus/theme-common/Details' +import type { Props } from '@theme/Details' +import clsx from 'clsx' +import { type ReactNode } from 'react' + +import styles from './styles.module.scss' + +// Should we have a custom details/summary comp in Infima instead of reusing +// alert classes? +const InfimaClasses = 'alert alert--info' + +export default function Details({ ...props }: Props): ReactNode { + return +} diff --git a/src/theme/Details/styles.module.scss b/src/theme/Details/styles.module.scss new file mode 100644 index 0000000..2a870be --- /dev/null +++ b/src/theme/Details/styles.module.scss @@ -0,0 +1,59 @@ +@use '@site/src/styles/common/color.scss' as *; +@use '@site/src/styles/common/media.scss' as *; + +.details { + border-radius: 1.2rem; + overflow: hidden; + background-color: rgba(36, 41, 38, 0.6); + border: 0.1rem solid rgba(36, 41, 38, 0.6); + margin-bottom: 1.6rem; + transition: all 0.3s ease; + + &[data-collapsed='false'] { + border-color: rgba(white, 0.1); + background-color: $secondary; + + summary { + &:after { + transform: rotate(0deg); + } + } + } + + summary { + &::before { + display: none; + } + + padding: 2rem; + font-size: 1.6rem; + line-height: 1.5; + font-weight: 500; + color: white; + display: flex; + justify-content: space-between; + align-items: center; + gap: 1.6rem; + + &:after { + content: ''; + display: block; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='lucide lucide-chevron-down'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E"); + width: 2.4rem; + height: 2.4rem; + transform: rotate(180deg); + transition: transform ease 0.3s; + } + } + + summary + div { + div { + margin-top: 0; + padding: 2rem 2rem 1.2rem; + color: white; + p { + font-size: 1.6rem; + } + } + } +} diff --git a/src/theme/DocBreadcrumbs/Items/Home/index.tsx b/src/theme/DocBreadcrumbs/Items/Home/index.tsx new file mode 100644 index 0000000..8a7bc64 --- /dev/null +++ b/src/theme/DocBreadcrumbs/Items/Home/index.tsx @@ -0,0 +1,27 @@ +import Link from '@docusaurus/Link' +import { translate } from '@docusaurus/Translate' +import useBaseUrl from '@docusaurus/useBaseUrl' +import IconHome from '@theme/Icon/Home' +import { type ReactNode } from 'react' + +import styles from './styles.module.css' + +export default function HomeBreadcrumbItem(): ReactNode { + const homeHref = useBaseUrl('/') + + return ( +
  • + + + +
  • + ) +} diff --git a/src/theme/DocBreadcrumbs/Items/Home/styles.module.css b/src/theme/DocBreadcrumbs/Items/Home/styles.module.css new file mode 100644 index 0000000..e847fcb --- /dev/null +++ b/src/theme/DocBreadcrumbs/Items/Home/styles.module.css @@ -0,0 +1,4 @@ +.breadcrumbHomeIcon { + width: 2.4rem; + height: 2.4rem; +} diff --git a/src/theme/DocBreadcrumbs/index.tsx b/src/theme/DocBreadcrumbs/index.tsx new file mode 100644 index 0000000..b8ba327 --- /dev/null +++ b/src/theme/DocBreadcrumbs/index.tsx @@ -0,0 +1,117 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import {ThemeClassNames} from '@docusaurus/theme-common'; +import {useSidebarBreadcrumbs} from '@docusaurus/plugin-content-docs/client'; +import {useHomePageRoute} from '@docusaurus/theme-common/internal'; +import Link from '@docusaurus/Link'; +import {translate} from '@docusaurus/Translate'; +import HomeBreadcrumbItem from '@theme/DocBreadcrumbs/Items/Home'; + +import styles from './styles.module.css'; + +// TODO move to design system folder +function BreadcrumbsItemLink({ + children, + href, + isLast, +}: { + children: ReactNode; + href: string | undefined; + isLast: boolean; +}): ReactNode { + const className = 'breadcrumbs__link'; + if (isLast) { + return ( + + {children} + + ); + } + return href ? ( + + {children} + + ) : ( + // TODO Google search console doesn't like breadcrumb items without href. + // The schema doesn't seem to require `id` for each `item`, although Google + // insist to infer one, even if it's invalid. Removing `itemProp="item + // name"` for now, since I don't know how to properly fix it. + // See https://github.com/facebook/docusaurus/issues/7241 + {children} + ); +} + +// TODO move to design system folder +function BreadcrumbsItem({ + children, + active, + index, + addMicrodata, +}: { + children: ReactNode; + active?: boolean; + index: number; + addMicrodata: boolean; +}): ReactNode { + return ( +
  • + {children} + +
  • + ); +} + +export default function DocBreadcrumbs(): ReactNode { + const breadcrumbs = useSidebarBreadcrumbs(); + const homePageRoute = useHomePageRoute(); + + if (!breadcrumbs) { + return null; + } + + return ( + + ); +} diff --git a/src/theme/DocBreadcrumbs/styles.module.css b/src/theme/DocBreadcrumbs/styles.module.css new file mode 100644 index 0000000..f16c21a --- /dev/null +++ b/src/theme/DocBreadcrumbs/styles.module.css @@ -0,0 +1,5 @@ +.breadcrumbsContainer { + --ifm-breadcrumb-size-multiplier: 0.8; + --margin-bottom: 2rem; + margin-bottom: var(--margin-bottom); +} diff --git a/src/theme/DocCard/index.tsx b/src/theme/DocCard/index.tsx new file mode 100644 index 0000000..c967f9e --- /dev/null +++ b/src/theme/DocCard/index.tsx @@ -0,0 +1,101 @@ +import isInternalUrl from '@docusaurus/isInternalUrl' +import Link from '@docusaurus/Link' +import { findFirstSidebarItemLink, useDocById } from '@docusaurus/plugin-content-docs/client' +import { usePluralForm } from '@docusaurus/theme-common' +import { translate } from '@docusaurus/Translate' +import clsx from 'clsx' +import { type ReactNode } from 'react' + +import type { PropSidebarItemCategory, PropSidebarItemLink } from '@docusaurus/plugin-content-docs' +import type { Props } from '@theme/DocCard' +import Heading from '@theme/Heading' + +import styles from './styles.module.scss' + +function useCategoryItemsPlural() { + const { selectMessage } = usePluralForm() + return (count: number) => + selectMessage( + count, + translate( + { + message: '1 item|{count} items', + id: 'theme.docs.DocCard.categoryDescription.plurals', + description: + 'The default description for a category card in the generated index about how many items this category includes', + }, + { count } + ) + ) +} + +function CardContainer({ href, children }: { href: string; children: ReactNode }): ReactNode { + return ( + + {children} + + ) +} + +function CardLayout({ + href, + icon, + title, + description, +}: { + href: string + icon: ReactNode + title: string + description?: string +}): ReactNode { + return ( + + + {icon} {title} + + {description && ( +

    + {description} +

    + )} +
    + ) +} + +function CardCategory({ item }: { item: PropSidebarItemCategory }): ReactNode { + const href = findFirstSidebarItemLink(item) + const categoryItemsPlural = useCategoryItemsPlural() + + // Unexpected: categories that don't have a link have been filtered upfront + if (!href) { + return null + } + + return ( + + ) +} + +function CardLink({ item }: { item: PropSidebarItemLink }): ReactNode { + const icon = isInternalUrl(item.href) ? '📄️' : '🔗' + const doc = useDocById(item.docId ?? undefined) + return ( + + ) +} + +export default function DocCard({ item }: Props): ReactNode { + switch (item.type) { + case 'link': + return + case 'category': + return + default: + throw new Error(`unknown item type ${JSON.stringify(item)}`) + } +} diff --git a/src/theme/DocCard/styles.module.scss b/src/theme/DocCard/styles.module.scss new file mode 100644 index 0000000..df22c3e --- /dev/null +++ b/src/theme/DocCard/styles.module.scss @@ -0,0 +1,39 @@ +@use '@site/src/styles/common/media.scss' as *; +@use '@site/src/styles/common/color.scss' as *; + +.cardContainer { + display: block; + width: 100%; + height: 100%; + border-radius: 1.2rem; + border: 0.1rem solid #{rgba(white, 0.1)}; + transition: all 0.3s ease; + padding: 1.6rem; + background-color: $secondary; + + &:hover { + border-color: rgba(white, 0.5); + text-decoration: none; + } +} + +.cardContainer *:last-child { + margin-bottom: 0; +} + +.cardTitle { + font-size: 1.6rem; + line-height: 1.5; + margin-bottom: 0.4rem; +} + +.cardDescription { + font-size: 1.4rem; + line-height: 1.5; +} + +@include xl { + .container { + padding: 2rem; + } +} diff --git a/src/theme/DocCardList/index.tsx b/src/theme/DocCardList/index.tsx new file mode 100644 index 0000000..fd7dfce --- /dev/null +++ b/src/theme/DocCardList/index.tsx @@ -0,0 +1,28 @@ +import { filterDocCardListItems, useCurrentSidebarCategory } from '@docusaurus/plugin-content-docs/client' +import DocCard from '@theme/DocCard' +import type { Props } from '@theme/DocCardList' +import clsx from 'clsx' +import { type ReactNode } from 'react' +import styles from './styles.module.scss' + +function DocCardListForCurrentSidebarCategory({ className }: Props) { + const category = useCurrentSidebarCategory() + return +} + +export default function DocCardList(props: Props): ReactNode { + const { items, className } = props + if (!items) { + return + } + const filteredItems = filterDocCardListItems(items) + return ( +
    + {filteredItems.map((item, index) => ( +
    + +
    + ))} +
    + ) +} diff --git a/src/theme/DocCardList/styles.module.scss b/src/theme/DocCardList/styles.module.scss new file mode 100644 index 0000000..cabf620 --- /dev/null +++ b/src/theme/DocCardList/styles.module.scss @@ -0,0 +1,23 @@ +@use '@site/src/styles/common/media.scss' as *; +.container { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: 1.2rem; +} + +.article { + width: 100%; + height: auto; +} + +@include md { + .container { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@include xl { + .container { + gap: 1.6rem; + } +} diff --git a/src/theme/DocCategoryGeneratedIndexPage/index.tsx b/src/theme/DocCategoryGeneratedIndexPage/index.tsx new file mode 100644 index 0000000..9452b39 --- /dev/null +++ b/src/theme/DocCategoryGeneratedIndexPage/index.tsx @@ -0,0 +1,60 @@ +import { useCurrentSidebarCategory } from '@docusaurus/plugin-content-docs/client' +import { PageMetadata } from '@docusaurus/theme-common' +import useBaseUrl from '@docusaurus/useBaseUrl' +import DocBreadcrumbs from '@theme/DocBreadcrumbs' +import DocCardList from '@theme/DocCardList' +import type { Props } from '@theme/DocCategoryGeneratedIndexPage' +import DocPaginator from '@theme/DocPaginator' +import DocVersionBadge from '@theme/DocVersionBadge' +import DocVersionBanner from '@theme/DocVersionBanner' +import Heading from '@theme/Heading' +import { type ReactNode } from 'react' + +import styles from './styles.module.css' + +function DocCategoryGeneratedIndexPageMetadata({ categoryGeneratedIndex }: Props): ReactNode { + return ( + + ) +} + +function DocCategoryGeneratedIndexPageContent({ categoryGeneratedIndex }: Props): ReactNode { + const category = useCurrentSidebarCategory() + return ( +
    + + + +
    + + {categoryGeneratedIndex.title} + + {categoryGeneratedIndex.description &&

    {categoryGeneratedIndex.description}

    } +
    +
    + +
    +
    + +
    +
    + ) +} + +export default function DocCategoryGeneratedIndexPage(props: Props): ReactNode { + return ( + <> + + + + ) +} diff --git a/src/theme/DocCategoryGeneratedIndexPage/styles.module.css b/src/theme/DocCategoryGeneratedIndexPage/styles.module.css new file mode 100644 index 0000000..c50fc4e --- /dev/null +++ b/src/theme/DocCategoryGeneratedIndexPage/styles.module.css @@ -0,0 +1,36 @@ +@media (min-width: 997px) { + .generatedIndexPage { + max-width: 75% !important; + } + + .list article:nth-last-child(-n + 2) { + margin-bottom: 0 !important; + } +} + +/* Duplicated from .markdown h1 */ +.title { + margin-bottom: 0.8rem; + font-size: 2.4rem; + color: white; + + + p { + font-size: 1.6rem; + color: rgba(white, 0.8); + } +} + +.article, +.footer { + margin-top: 2rem; +} + +.list article:last-child { + margin-bottom: 0 !important; +} + +@include xl { + .title { + font-size: 3.2rem; + } +} diff --git a/src/theme/DocItem/Layout/index.tsx b/src/theme/DocItem/Layout/index.tsx new file mode 100644 index 0000000..6910e76 --- /dev/null +++ b/src/theme/DocItem/Layout/index.tsx @@ -0,0 +1,61 @@ +import { useDoc } from '@docusaurus/plugin-content-docs/client' +import { useWindowSize } from '@docusaurus/theme-common' +import ContentVisibility from '@theme/ContentVisibility' +import DocBreadcrumbs from '@theme/DocBreadcrumbs' +import DocItemContent from '@theme/DocItem/Content' +import DocItemFooter from '@theme/DocItem/Footer' +import type { Props } from '@theme/DocItem/Layout' +import DocItemPaginator from '@theme/DocItem/Paginator' +import DocItemTOCDesktop from '@theme/DocItem/TOC/Desktop' +import DocItemTOCMobile from '@theme/DocItem/TOC/Mobile' +import DocVersionBadge from '@theme/DocVersionBadge' +import DocVersionBanner from '@theme/DocVersionBanner' +import clsx from 'clsx' +import { type ReactNode } from 'react' + +import styles from './styles.module.scss' + +/** + * Decide if the toc should be rendered, on mobile or desktop viewports + */ +function useDocTOC() { + const { frontMatter, toc } = useDoc() + const windowSize = useWindowSize({ desktopBreakpoint: 1280 }) + + const hidden = frontMatter.hide_table_of_contents + const canRender = !hidden && toc.length > 0 + + const mobile = canRender ? : undefined + + const desktop = canRender && (windowSize === 'desktop' || windowSize === 'ssr') ? : undefined + + return { + hidden, + mobile, + desktop, + } +} + +export default function DocItemLayout({ children }: Props): ReactNode { + const docTOC = useDocTOC() + const { metadata } = useDoc() + return ( +
    +
    + + +
    +
    + + + {docTOC.mobile} + {children} + +
    + +
    +
    + {docTOC.desktop &&
    {docTOC.desktop}
    } +
    + ) +} diff --git a/src/theme/DocItem/Layout/styles.module.scss b/src/theme/DocItem/Layout/styles.module.scss new file mode 100644 index 0000000..d6b6b2c --- /dev/null +++ b/src/theme/DocItem/Layout/styles.module.scss @@ -0,0 +1,33 @@ +@use '@site/src/styles/common/media.scss' as *; + +.docItemWrapper { + display: grid; +} + +.docItemMain { + width: 100%; +} + +.docItemContainer header + *, +.docItemContainer article > *:first-child { + margin-top: 0; +} + +@media (min-width: 1281px) { + .docItemWrapper { + grid-template-columns: 1fr 360px; + gap: 3.2rem; + } +} + +@media (min-width: 1440px) { + .docItemWrapper { + gap: 4rem; + } +} + +@media (min-width: 1700px) { + .docItemWrapper { + gap: 5.2rem; + } +} diff --git a/src/theme/DocItem/TOC/Mobile/index.tsx b/src/theme/DocItem/TOC/Mobile/index.tsx new file mode 100644 index 0000000..0123caa --- /dev/null +++ b/src/theme/DocItem/TOC/Mobile/index.tsx @@ -0,0 +1,20 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import {ThemeClassNames} from '@docusaurus/theme-common'; +import {useDoc} from '@docusaurus/plugin-content-docs/client'; + +import TOCCollapsible from '@theme/TOCCollapsible'; + +import styles from './styles.module.css'; + +export default function DocItemTOCMobile(): ReactNode { + const {toc, frontMatter} = useDoc(); + return ( + + ); +} diff --git a/src/theme/DocItem/TOC/Mobile/styles.module.css b/src/theme/DocItem/TOC/Mobile/styles.module.css new file mode 100644 index 0000000..c015361 --- /dev/null +++ b/src/theme/DocItem/TOC/Mobile/styles.module.css @@ -0,0 +1,12 @@ +@media (min-width: 1281px) { + /* Prevent hydration FOUC, as the mobile TOC needs to be server-rendered */ + .tocMobile { + display: none; + } +} + +@media print { + .tocMobile { + display: none; + } +} diff --git a/src/theme/DocPaginator/index.tsx b/src/theme/DocPaginator/index.tsx new file mode 100644 index 0000000..a45e587 --- /dev/null +++ b/src/theme/DocPaginator/index.tsx @@ -0,0 +1,40 @@ +import Translate, { translate } from '@docusaurus/Translate' +import type { Props } from '@theme/DocPaginator' +import PaginatorNavLink from '@theme/PaginatorNavLink' +import { type ReactNode } from 'react' + +export default function DocPaginator(props: Props): ReactNode { + const { previous, next } = props + return ( + + ) +} diff --git a/src/theme/DocRoot/Layout/Main/index.tsx b/src/theme/DocRoot/Layout/Main/index.tsx new file mode 100644 index 0000000..da0f8a2 --- /dev/null +++ b/src/theme/DocRoot/Layout/Main/index.tsx @@ -0,0 +1,17 @@ +import { useDocsSidebar } from '@docusaurus/plugin-content-docs/client' +import type { Props } from '@theme/DocRoot/Layout/Main' +import clsx from 'clsx' +import { type ReactNode } from 'react' + +import styles from './styles.module.scss' + +export default function DocRootLayoutMain({ hiddenSidebarContainer, children }: Props): ReactNode { + const sidebar = useDocsSidebar() + return ( +
    +
    {children}
    +
    + ) +} diff --git a/src/theme/DocRoot/Layout/Main/styles.module.scss b/src/theme/DocRoot/Layout/Main/styles.module.scss new file mode 100644 index 0000000..5d244d1 --- /dev/null +++ b/src/theme/DocRoot/Layout/Main/styles.module.scss @@ -0,0 +1,34 @@ +@use '@site/src/styles/common/media' as *; + +.docMainContainer { + display: flex; + width: 100%; +} + +.docItemWrapper { + width: 100%; + max-width: 1440px; + margin: 0 auto; + padding: 1.6rem; +} + +@media (min-width: 997px) { + .docMainContainer { + flex-grow: 1; + max-width: calc(100% - var(--doc-sidebar-width)); + } + + .docMainContainerEnhanced { + max-width: calc(100% - var(--doc-sidebar-hidden-width)); + } + + .docItemWrapper { + padding: 1.6rem 2.4rem; + } +} + +@media (min-width: 1281px) { + .docItemWrapper { + padding: 2.4rem 4rem; + } +} diff --git a/src/theme/Footer/Copyright/index.tsx b/src/theme/Footer/Copyright/index.tsx new file mode 100644 index 0000000..5acc50d --- /dev/null +++ b/src/theme/Footer/Copyright/index.tsx @@ -0,0 +1,12 @@ +import type { Props } from '@theme/Footer/Copyright' +import { type ReactNode } from 'react' + +export default function FooterCopyright({ copyright }: Props): ReactNode { + return ( +
    + ) +} diff --git a/src/theme/Footer/Layout/index.tsx b/src/theme/Footer/Layout/index.tsx new file mode 100644 index 0000000..a79591e --- /dev/null +++ b/src/theme/Footer/Layout/index.tsx @@ -0,0 +1,14 @@ +import type { Props } from '@theme/Footer/Layout' +import { type ReactNode } from 'react' + +export default function FooterLayout({ style, links, logo, copyright }: Props): ReactNode { + return ( +
    +
    +
    {logo}
    + {links} + {copyright} +
    +
    + ) +} diff --git a/src/theme/Footer/LinkItem/index.tsx b/src/theme/Footer/LinkItem/index.tsx new file mode 100644 index 0000000..59efcba --- /dev/null +++ b/src/theme/Footer/LinkItem/index.tsx @@ -0,0 +1,29 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import Link from '@docusaurus/Link'; +import useBaseUrl from '@docusaurus/useBaseUrl'; +import isInternalUrl from '@docusaurus/isInternalUrl'; +import IconExternalLink from '@theme/Icon/ExternalLink'; +import type {Props} from '@theme/Footer/LinkItem'; + +export default function FooterLinkItem({item}: Props): ReactNode { + const {to, href, label, prependBaseUrlToHref, className, ...props} = item; + const toUrl = useBaseUrl(to); + const normalizedHref = useBaseUrl(href, {forcePrependBaseUrl: true}); + + return ( + + {label} + {href && !isInternalUrl(href) && } + + ); +} diff --git a/src/theme/Footer/Links/MultiColumn/index.tsx b/src/theme/Footer/Links/MultiColumn/index.tsx new file mode 100644 index 0000000..45e0051 --- /dev/null +++ b/src/theme/Footer/Links/MultiColumn/index.tsx @@ -0,0 +1,45 @@ +import LinkItem from '@theme/Footer/LinkItem' +import type { Props } from '@theme/Footer/Links/MultiColumn' +import clsx from 'clsx' +import { type ReactNode } from 'react' + +type ColumnType = Props['columns'][number] +type ColumnItemType = ColumnType['items'][number] + +function ColumnLinkItem({ item }: { item: ColumnItemType }) { + return item.html ? ( +
  • + ) : ( +
  • + +
  • + ) +} + +function Column({ column }: { column: ColumnType }) { + return ( +
    +
    {column.title}
    +
      + {column.items.map((item, i) => ( + + ))} +
    +
    + ) +} + +export default function FooterLinksMultiColumn({ columns }: Props): ReactNode { + return ( +
    + {columns.map((column, i) => ( + + ))} +
    + ) +} diff --git a/src/theme/Footer/Links/Simple/index.tsx b/src/theme/Footer/Links/Simple/index.tsx new file mode 100644 index 0000000..dc6cf9d --- /dev/null +++ b/src/theme/Footer/Links/Simple/index.tsx @@ -0,0 +1,36 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import LinkItem from '@theme/Footer/LinkItem'; +import type {Props} from '@theme/Footer/Links/Simple'; + +function Separator() { + return ·; +} + +function SimpleLinkItem({item}: {item: Props['links'][number]}) { + return item.html ? ( + + ) : ( + + ); +} + +export default function FooterLinksSimple({links}: Props): ReactNode { + return ( +
    +
    + {links.map((item, i) => ( + + + {links.length !== i + 1 && } + + ))} +
    +
    + ); +} diff --git a/src/theme/Footer/Links/index.tsx b/src/theme/Footer/Links/index.tsx new file mode 100644 index 0000000..2dd3d84 --- /dev/null +++ b/src/theme/Footer/Links/index.tsx @@ -0,0 +1,14 @@ +import React, {type ReactNode} from 'react'; + +import {isMultiColumnFooterLinks} from '@docusaurus/theme-common'; +import FooterLinksMultiColumn from '@theme/Footer/Links/MultiColumn'; +import FooterLinksSimple from '@theme/Footer/Links/Simple'; +import type {Props} from '@theme/Footer/Links'; + +export default function FooterLinks({links}: Props): ReactNode { + return isMultiColumnFooterLinks(links) ? ( + + ) : ( + + ); +} diff --git a/src/theme/Footer/Logo/index.tsx b/src/theme/Footer/Logo/index.tsx new file mode 100644 index 0000000..07759f3 --- /dev/null +++ b/src/theme/Footer/Logo/index.tsx @@ -0,0 +1,36 @@ +import Link from '@docusaurus/Link' +import { useBaseUrlUtils } from '@docusaurus/useBaseUrl' +import type { Props } from '@theme/Footer/Logo' +import ThemedImage from '@theme/ThemedImage' +import clsx from 'clsx' +import { type ReactNode } from 'react' + +import styles from './styles.module.css' + +function LogoImage({ logo }: Props) { + const { withBaseUrl } = useBaseUrlUtils() + const sources = { + light: withBaseUrl(logo.src), + dark: withBaseUrl(logo.srcDark ?? logo.src), + } + return ( + + ) +} + +export default function FooterLogo({ logo }: Props): ReactNode { + return logo.href ? ( + + + + ) : ( + + ) +} diff --git a/src/theme/Footer/Logo/styles.module.css b/src/theme/Footer/Logo/styles.module.css new file mode 100644 index 0000000..5e6c557 --- /dev/null +++ b/src/theme/Footer/Logo/styles.module.css @@ -0,0 +1,2 @@ +.footerLogoLink { +} diff --git a/src/theme/Footer/Logo/styles.module.scss b/src/theme/Footer/Logo/styles.module.scss new file mode 100644 index 0000000..5e6c557 --- /dev/null +++ b/src/theme/Footer/Logo/styles.module.scss @@ -0,0 +1,2 @@ +.footerLogoLink { +} diff --git a/src/theme/Footer/index.tsx b/src/theme/Footer/index.tsx new file mode 100644 index 0000000..95c0f91 --- /dev/null +++ b/src/theme/Footer/index.tsx @@ -0,0 +1,26 @@ +import React, { type ReactNode } from 'react' + +import { useThemeConfig } from '@docusaurus/theme-common' +import FooterCopyright from '@theme/Footer/Copyright' +import FooterLayout from '@theme/Footer/Layout' +import FooterLinks from '@theme/Footer/Links' +import FooterLogo from '@theme/Footer/Logo' + +function Footer(): ReactNode { + const { footer } = useThemeConfig() + if (!footer) { + return null + } + const { copyright, links, logo, style } = footer + + return ( + 0 && } + logo={logo && } + copyright={copyright && } + /> + ) +} + +export default React.memo(Footer) diff --git a/src/theme/Icon/ExternalLink/index.tsx b/src/theme/Icon/ExternalLink/index.tsx new file mode 100644 index 0000000..bd0d01a --- /dev/null +++ b/src/theme/Icon/ExternalLink/index.tsx @@ -0,0 +1,32 @@ +import type { Props } from '@theme/Icon/ExternalLink' +import { type ReactNode } from 'react' + +import styles from './styles.module.css' + +export default function IconExternalLink({ width = 18, height = 18 }: Props): ReactNode { + return ( + + ) +} diff --git a/src/theme/Icon/ExternalLink/styles.module.css b/src/theme/Icon/ExternalLink/styles.module.css new file mode 100644 index 0000000..9468b19 --- /dev/null +++ b/src/theme/Icon/ExternalLink/styles.module.css @@ -0,0 +1,3 @@ +.iconExternalLink { + margin-left: 0.3rem; +} diff --git a/src/theme/Icon/Home/index.tsx b/src/theme/Icon/Home/index.tsx new file mode 100644 index 0000000..3432d9a --- /dev/null +++ b/src/theme/Icon/Home/index.tsx @@ -0,0 +1,16 @@ +import type { Props } from '@theme/Icon/Home' +import { type ReactNode } from 'react' + +export default function IconHome(props: Props): ReactNode { + return ( + + + + ) +} diff --git a/src/theme/LayoutHead.tsx b/src/theme/LayoutHead.tsx deleted file mode 100644 index 417c670..0000000 --- a/src/theme/LayoutHead.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from "react"; -import Head from "@docusaurus/Head"; - -export default function LayoutHead() { - return ( - - - - ); -} diff --git a/src/theme/Logo/index.tsx b/src/theme/Logo/index.tsx new file mode 100644 index 0000000..4e0410d --- /dev/null +++ b/src/theme/Logo/index.tsx @@ -0,0 +1,31 @@ +import Link from '@docusaurus/Link' +import { useThemeConfig } from '@docusaurus/theme-common' +import useBaseUrl from '@docusaurus/useBaseUrl' +import { useMediaQuery } from '@site/src/hooks/useMediaQuery' +import type { Props } from '@theme/Logo' +import { type ReactNode } from 'react' + +function LogoThemedImage() { + const isMobile = useMediaQuery('(max-width: 768px)') + + const mobileLogo = useBaseUrl('/img/branding/pi.svg') + const desktopLogo = useBaseUrl('/img/branding/optimai-documentation-logo.svg') + const source = isMobile ? mobileLogo : desktopLogo + + return OptimAI Documentation Logo +} + +export default function Logo(props: Props): ReactNode { + const { + navbar: { logo }, + } = useThemeConfig() + + const { imageClassName, titleClassName, ...propsRest } = props + const logoLink = useBaseUrl(logo?.href || '/') + + return ( + + + + ) +} diff --git a/src/theme/PaginatorNavLink/index.tsx b/src/theme/PaginatorNavLink/index.tsx new file mode 100644 index 0000000..dc8b22a --- /dev/null +++ b/src/theme/PaginatorNavLink/index.tsx @@ -0,0 +1,87 @@ +import Link from '@docusaurus/Link' +import type { Props } from '@theme/PaginatorNavLink' +import clsx from 'clsx' +import { type ReactNode } from 'react' + +export default function PaginatorNavLink(props: Props): ReactNode { + const { permalink, title, subLabel, isNext } = props + return ( + + {subLabel && ( +
    + {!isNext && ( + + + + + )} + {subLabel} + {isNext && ( + + + + + )} +
    + )} +
    + {!isNext && ( + + + + + )} + {title} + {isNext && ( + + + + + )} +
    + + ) +} diff --git a/src/theme/SearchPage/index.tsx b/src/theme/SearchPage/index.tsx new file mode 100644 index 0000000..dfe36eb --- /dev/null +++ b/src/theme/SearchPage/index.tsx @@ -0,0 +1,469 @@ +/* eslint-disable jsx-a11y/no-autofocus */ + +import clsx from 'clsx' +import { type ReactNode, useEffect, useReducer, useRef, useState } from 'react' + +import algoliaSearchHelper from 'algoliasearch-helper' +import { liteClient } from 'algoliasearch/lite' + +import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment' +import Head from '@docusaurus/Head' +import Link from '@docusaurus/Link' +import { useAllDocsData } from '@docusaurus/plugin-content-docs/client' +import { HtmlClassNameProvider, useEvent, usePluralForm, useSearchQueryString } from '@docusaurus/theme-common' +import { useTitleFormatter } from '@docusaurus/theme-common/internal' +import { useAlgoliaThemeConfig, useSearchResultUrlProcessor } from '@docusaurus/theme-search-algolia/client' +import Translate, { translate } from '@docusaurus/Translate' +import useDocusaurusContext from '@docusaurus/useDocusaurusContext' +import Heading from '@theme/Heading' +import Layout from '@theme/Layout' +import styles from './styles.module.scss' + +// Very simple pluralization: probably good enough for now +function useDocumentsFoundPlural() { + const { selectMessage } = usePluralForm() + return (count: number) => + selectMessage( + count, + translate( + { + id: 'theme.SearchPage.documentsFound.plurals', + description: + 'Pluralized label for "{count} documents found". Use as much plural forms (separated by "|") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)', + message: 'One document found|{count} documents found', + }, + { count } + ) + ) +} + +function useDocsSearchVersionsHelpers() { + const allDocsData = useAllDocsData() + + // State of the version select menus / algolia facet filters + // docsPluginId -> versionName map + const [searchVersions, setSearchVersions] = useState<{ + [pluginId: string]: string + }>(() => + Object.entries(allDocsData).reduce( + (acc, [pluginId, pluginData]) => ({ + ...acc, + [pluginId]: pluginData.versions[0]!.name, + }), + {} + ) + ) + + // Set the value of a single select menu + const setSearchVersion = (pluginId: string, searchVersion: string) => + setSearchVersions((s) => ({ ...s, [pluginId]: searchVersion })) + + const versioningEnabled = Object.values(allDocsData).some((docsData) => docsData.versions.length > 1) + + return { + allDocsData, + versioningEnabled, + searchVersions, + setSearchVersion, + } +} + +// We want to display one select per versioned docs plugin instance +function SearchVersionSelectList({ + docsSearchVersionsHelpers, +}: { + docsSearchVersionsHelpers: ReturnType +}) { + const versionedPluginEntries = Object.entries(docsSearchVersionsHelpers.allDocsData) + // Do not show a version select for unversioned docs plugin instances + .filter(([, docsData]) => docsData.versions.length > 1) + + return ( +
    + {versionedPluginEntries.map(([pluginId, docsData]) => { + const labelPrefix = versionedPluginEntries.length > 1 ? `${pluginId}: ` : '' + return ( + + ) + })} +
    + ) +} + +type ResultDispatcherState = { + items: { + title: string + url: string + summary: string + breadcrumbs: string[] + }[] + query: string | null + totalResults: number | null + totalPages: number | null + lastPage: number | null + hasMore: boolean | null + loading: boolean | null +} + +type ResultDispatcher = + | { type: 'reset'; value?: undefined } + | { type: 'loading'; value?: undefined } + | { type: 'update'; value: ResultDispatcherState } + | { type: 'advance'; value?: undefined } + +function SearchPageContent(): ReactNode { + const { + i18n: { currentLocale }, + } = useDocusaurusContext() + const { + algolia: { appId, apiKey, indexName, contextualSearch }, + } = useAlgoliaThemeConfig() + + const processSearchResultUrl = useSearchResultUrlProcessor() + const documentsFoundPlural = useDocumentsFoundPlural() + + const docsSearchVersionsHelpers = useDocsSearchVersionsHelpers() + const [searchQuery, setSearchQuery] = useSearchQueryString() + const initialSearchResultState: ResultDispatcherState = { + items: [], + query: null, + totalResults: null, + totalPages: null, + lastPage: null, + hasMore: null, + loading: null, + } + const [searchResultState, searchResultStateDispatcher] = useReducer( + (prevState: ResultDispatcherState, data: ResultDispatcher) => { + switch (data.type) { + case 'reset': { + return initialSearchResultState + } + case 'loading': { + return { ...prevState, loading: true } + } + case 'update': { + if (searchQuery !== data.value.query) { + return prevState + } + + return { + ...data.value, + items: data.value.lastPage === 0 ? data.value.items : prevState.items.concat(data.value.items), + } + } + case 'advance': { + const hasMore = prevState.totalPages! > prevState.lastPage! + 1 + + return { + ...prevState, + lastPage: hasMore ? prevState.lastPage! + 1 : prevState.lastPage, + hasMore, + } + } + default: + return prevState + } + }, + initialSearchResultState + ) + + // respect settings from the theme config for facets + const disjunctiveFacets = contextualSearch ? ['language', 'docusaurus_tag'] : [] + + const algoliaClient = liteClient(appId, apiKey) + const algoliaHelper = algoliaSearchHelper(algoliaClient, indexName, { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore: why errors happens after upgrading to TS 5.5 ? + hitsPerPage: 15, + advancedSyntax: true, + disjunctiveFacets, + }) + + algoliaHelper.on('result', ({ results: { query, hits, page, nbHits, nbPages } }) => { + if (query === '' || !Array.isArray(hits)) { + searchResultStateDispatcher({ type: 'reset' }) + return + } + + const sanitizeValue = (value: string) => + value.replace(/algolia-docsearch-suggestion--highlight/g, 'search-result-match') + + const items = hits.map( + ({ + url, + _highlightResult: { hierarchy }, + _snippetResult: snippet = {}, + }: { + url: string + _highlightResult: { hierarchy: { [key: string]: { value: string } } } + _snippetResult: { content?: { value: string } } + }) => { + const titles = Object.keys(hierarchy).map((key) => sanitizeValue(hierarchy[key]!.value)) + return { + title: titles.pop()!, + url: processSearchResultUrl(url), + summary: snippet.content ? `${sanitizeValue(snippet.content.value)}...` : '', + breadcrumbs: titles, + } + } + ) + + searchResultStateDispatcher({ + type: 'update', + value: { + items, + query, + totalResults: nbHits, + totalPages: nbPages, + lastPage: page, + hasMore: nbPages > page + 1, + loading: false, + }, + }) + }) + + const [loaderRef, setLoaderRef] = useState(null) + const prevY = useRef(0) + const observer = useRef( + ExecutionEnvironment.canUseIntersectionObserver && + new IntersectionObserver( + (entries) => { + const { + isIntersecting, + boundingClientRect: { y: currentY }, + } = entries[0]! + + if (isIntersecting && prevY.current > currentY) { + searchResultStateDispatcher({ type: 'advance' }) + } + + prevY.current = currentY + }, + { threshold: 1 } + ) + ) + + const getTitle = () => + searchQuery + ? translate( + { + id: 'theme.SearchPage.existingResultsTitle', + message: 'Search results for "{query}"', + description: 'The search page title for non-empty query', + }, + { + query: searchQuery, + } + ) + : translate({ + id: 'theme.SearchPage.emptyResultsTitle', + message: 'Search the documentation', + description: 'The search page title for empty query', + }) + + const makeSearch = useEvent((page: number = 0) => { + if (contextualSearch) { + algoliaHelper.addDisjunctiveFacetRefinement('docusaurus_tag', 'default') + algoliaHelper.addDisjunctiveFacetRefinement('language', currentLocale) + + Object.entries(docsSearchVersionsHelpers.searchVersions).forEach(([pluginId, searchVersion]) => { + algoliaHelper.addDisjunctiveFacetRefinement('docusaurus_tag', `docs-${pluginId}-${searchVersion}`) + }) + } + + algoliaHelper.setQuery(searchQuery).setPage(page).search() + }) + + useEffect(() => { + if (!loaderRef) { + return undefined + } + const currentObserver = observer.current + if (currentObserver) { + currentObserver.observe(loaderRef) + return () => currentObserver.unobserve(loaderRef) + } + return () => true + }, [loaderRef]) + + useEffect(() => { + searchResultStateDispatcher({ type: 'reset' }) + + if (searchQuery) { + searchResultStateDispatcher({ type: 'loading' }) + + setTimeout(() => { + makeSearch() + }, 300) + } + }, [searchQuery, docsSearchVersionsHelpers.searchVersions, makeSearch]) + + useEffect(() => { + if (!searchResultState.lastPage || searchResultState.lastPage === 0) { + return + } + + makeSearch(searchResultState.lastPage) + }, [makeSearch, searchResultState.lastPage]) + + return ( + + + {useTitleFormatter(getTitle())} + {/* + We should not index search pages + See https://github.com/facebook/docusaurus/pull/3233 + */} + + + +
    + {getTitle()} + +
    e.preventDefault()}> +
    + setSearchQuery(e.target.value)} + value={searchQuery} + autoComplete="off" + autoFocus + /> +
    + + {contextualSearch && docsSearchVersionsHelpers.versioningEnabled && ( + + )} + + +
    +
    + {!!searchResultState.totalResults && documentsFoundPlural(searchResultState.totalResults)} +
    + +
    + + + + + + + + + +
    +
    + + {searchResultState.items.length > 0 ? ( +
    + {searchResultState.items.map(({ title, url, summary, breadcrumbs }, i) => ( +
    + + + + + {breadcrumbs.length > 0 && ( + + )} + + {summary && ( +

    + )} +

    + ))} +
    + ) : ( + [ + searchQuery && !searchResultState.loading && ( +

    + + No results were found + +

    + ), + !!searchResultState.loading &&
    , + ] + )} + + {searchResultState.hasMore && ( +
    + + Fetching new results... + +
    + )} +
    + + ) +} + +export default function SearchPage(): ReactNode { + return ( + + + + ) +} diff --git a/src/theme/SearchPage/styles.module.scss b/src/theme/SearchPage/styles.module.scss new file mode 100644 index 0000000..ff2810e --- /dev/null +++ b/src/theme/SearchPage/styles.module.scss @@ -0,0 +1,153 @@ +@use '@site/src/styles/common/color.scss' as *; +@use '@site/src/styles/common/media.scss' as *; + +.searchQueryInput, +.searchVersionInput { + width: 100%; + font-size: 1.6rem; + line-height: 1.63; + font-weight: 500; + color: white; + height: 4.4rem; + background-color: rgba($accent, 0.8); + box-shadow: none; + padding: 0 1.6rem; + border-radius: 0.8rem; + border: 1px solid rgba(255, 255, 255, 0.1); + margin-bottom: 0.8rem; +} + +.searchQueryInput:focus, +.searchVersionInput:focus { + border-color: rgba(255, 255, 255, 0.4); + outline: none; +} + +.searchQueryInput::placeholder { + color: rgba(white, 0.5); +} + +.searchResultsColumn { + font-size: 1.6rem; + color: rgba(white, 0.5); + margin-bottom: 2.4rem; +} + +.algoliaLogo { + max-width: 150px; +} + +.algoliaLogoPathFill { + fill: var(--ifm-font-color-base); +} +.searchLogoColumn { + display: none; +} + +.searchResultItem { + width: 100%; + overflow: hidden; + padding: 2rem 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + + ul { + gap: 0.4rem !important; + } + + li { + color: rgba(white, 0.5); + font-size: 1.6rem; + line-height: 1.5; + font-weight: 400; + gap: 0.4rem; + &:after { + margin: 0 !important; + transform: translateY(1px); + } + } +} + +.searchResultItemHeading { + font-weight: 600; + font-size: 2rem; + line-height: 1.5; + margin-bottom: 0; + + a { + &:hover { + // text-decoration: none; + text-underline-offset: 2px; + } + } +} + +.searchResultItemPath { + font-size: 0.8rem; + color: var(--ifm-color-content-secondary); + --ifm-breadcrumb-separator-size-multiplier: 1; +} + +.searchResultItemSummary { + margin: 0.5rem 0 0; + color: rgba(white, 0.5); + font-style: italic; +} + +@media only screen and (max-width: 996px) { + .searchQueryColumn { + max-width: 60% !important; + } + + .searchVersionColumn { + max-width: 40% !important; + } + + .searchResultsColumn { + max-width: 60% !important; + } + + .searchLogoColumn { + display: none; + max-width: 40% !important; + padding-left: 0 !important; + } +} + +@media screen and (max-width: 576px) { + .searchQueryColumn { + max-width: 100% !important; + } + + .searchVersionColumn { + max-width: 100% !important; + padding-left: var(--ifm-spacing-horizontal) !important; + } +} + +.loadingSpinner { + width: 3rem; + height: 3rem; + border: 0.4em solid #eee; + border-top-color: var(--ifm-color-primary); + border-radius: 50%; + animation: loading-spin 1s linear infinite; + margin: 0 auto; +} + +@keyframes loading-spin { + 100% { + transform: rotate(360deg); + } +} + +.loader { + margin-top: 2rem; +} + +:global(.search-result-match) { + // background: var(--Gradient, linear-gradient(90deg, #f6f655 0%, #5eed87 100%)); + // background-clip: text; + // -webkit-background-clip: text; + // -webkit-text-fill-color: transparent; + color: #5eed87; +} diff --git a/src/theme/TOC/index.tsx b/src/theme/TOC/index.tsx new file mode 100644 index 0000000..828648e --- /dev/null +++ b/src/theme/TOC/index.tsx @@ -0,0 +1,23 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import TOCItems from '@theme/TOCItems'; +import type {Props} from '@theme/TOC'; + +import styles from './styles.module.css'; + +// Using a custom className +// This prevents TOCInline/TOCCollapsible getting highlighted by mistake +const LINK_CLASS_NAME = 'table-of-contents__link toc-highlight'; +const LINK_ACTIVE_CLASS_NAME = 'table-of-contents__link--active'; + +export default function TOC({className, ...props}: Props): ReactNode { + return ( +
    + +
    + ); +} diff --git a/src/theme/TOC/styles.module.css b/src/theme/TOC/styles.module.css new file mode 100644 index 0000000..44f73d6 --- /dev/null +++ b/src/theme/TOC/styles.module.css @@ -0,0 +1,16 @@ +.tableOfContents { + max-height: calc(100vh - (var(--ifm-navbar-height) + 4.8rem)); + overflow-y: auto; + position: sticky; + top: calc(var(--ifm-navbar-height) + 2.4rem); +} + +@media (max-width: 996px) { + .tableOfContents { + display: none; + } + + .docItemContainer { + padding: 0 0.3rem; + } +} diff --git a/src/theme/TOCCollapsible/CollapseButton/index.tsx b/src/theme/TOCCollapsible/CollapseButton/index.tsx new file mode 100644 index 0000000..68b99f4 --- /dev/null +++ b/src/theme/TOCCollapsible/CollapseButton/index.tsx @@ -0,0 +1,44 @@ +import Translate from '@docusaurus/Translate' +import type { Props } from '@theme/TOCCollapsible/CollapseButton' +import clsx from 'clsx' +import { type ReactNode } from 'react' + +import styles from './styles.module.scss' + +export default function TOCCollapsibleCollapseButton({ collapsed, ...props }: Props): ReactNode { + return ( + + ) +} diff --git a/src/theme/TOCCollapsible/CollapseButton/styles.module.scss b/src/theme/TOCCollapsible/CollapseButton/styles.module.scss new file mode 100644 index 0000000..8385896 --- /dev/null +++ b/src/theme/TOCCollapsible/CollapseButton/styles.module.scss @@ -0,0 +1,24 @@ +.tocCollapsibleButton { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 1rem 1.2rem; + width: 100%; + + span { + font-size: 1.6rem; + line-height: normal; + font-weight: 600; + } + + svg { + transition: transform 0.2s ease-in-out; + } +} + +.tocCollapsibleButtonExpanded { + svg { + transform: rotate(180deg); + } +} diff --git a/src/theme/TOCCollapsible/index.tsx b/src/theme/TOCCollapsible/index.tsx new file mode 100644 index 0000000..1e0775b --- /dev/null +++ b/src/theme/TOCCollapsible/index.tsx @@ -0,0 +1,22 @@ +import { Collapsible, useCollapsible } from '@docusaurus/theme-common' +import type { Props } from '@theme/TOCCollapsible' +import CollapseButton from '@theme/TOCCollapsible/CollapseButton' +import TOCItems from '@theme/TOCItems' +import clsx from 'clsx' +import { type ReactNode } from 'react' + +import styles from './styles.module.scss' + +export default function TOCCollapsible({ toc, className, minHeadingLevel, maxHeadingLevel }: Props): ReactNode { + const { collapsed, toggleCollapsed } = useCollapsible({ + initialState: true, + }) + return ( +
    + + + + +
    + ) +} diff --git a/src/theme/TOCCollapsible/styles.module.scss b/src/theme/TOCCollapsible/styles.module.scss new file mode 100644 index 0000000..574f6f0 --- /dev/null +++ b/src/theme/TOCCollapsible/styles.module.scss @@ -0,0 +1,44 @@ +@use '@site/src/styles/common/color.scss' as *; + +.tocCollapsible { + background-color: rgba($secondary, 0.6); + border: 1px solid rgba($secondary, 0.6); + border-radius: 0.8rem; + overflow: hidden; + margin-bottom: 2rem; +} + +.tocCollapsibleExpanded { + border-color: rgba(white, 0.1); +} + +.tocCollapsibleContent > ul { + border-left: none; + border-top: 1px solid rgba(white, 0.1); + padding: 1.2rem 1.6rem 1rem; + display: flex; + flex-direction: column; + gap: 1.2rem; +} + +.tocCollapsibleContent ul li { + margin: 0; + font-size: 1.6rem; + line-height: normal; + color: white; + + ul { + margin-top: 1.2rem; + display: flex; + flex-direction: column; + gap: 1.2rem; + padding-left: 1.6rem; + } +} + +.tocCollapsibleContent a { + display: block; + &:hover { + color: #f6f655; + } +} diff --git a/static/img/branding/optimai-documentation-logo.svg b/static/img/branding/optimai-documentation-logo.svg new file mode 100644 index 0000000..2318162 --- /dev/null +++ b/static/img/branding/optimai-documentation-logo.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/static/img/branding/pi.svg b/static/img/branding/pi.svg new file mode 100644 index 0000000..4a31230 --- /dev/null +++ b/static/img/branding/pi.svg @@ -0,0 +1,3 @@ + + + diff --git a/tsconfig.json b/tsconfig.json index 920d7a6..d2ec0c4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,8 @@ // This file is not used in compilation. It is here just for a nice editor experience. "extends": "@docusaurus/tsconfig", "compilerOptions": { - "baseUrl": "." + "baseUrl": ".", + "types": ["docusaurus-plugin-sass"] }, "exclude": [".docusaurus", "build"] }