├── .env.example ├── .eslintrc.json ├── .github └── workflows │ ├── on-leafygreen-release.yml │ ├── on-pr.yml │ └── on-staging-push.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── README.md ├── environment.d.ts ├── eslint.config.mjs ├── next-env.d.ts ├── next.config.mjs ├── package.json ├── pnpm-lock.yaml ├── public └── examples │ ├── marketing-center-light.svg │ ├── marketing-fill-dark.jpg │ └── marketing-fill-light.jpg ├── scripts └── update.mjs ├── src ├── app │ ├── [contentPageGroup] │ │ └── [contentPage] │ │ │ ├── page.tsx │ │ │ └── template.tsx │ ├── api │ │ └── auth │ │ │ └── [...nextauth] │ │ │ └── route.ts │ ├── component │ │ └── [component] │ │ │ ├── code-docs │ │ │ ├── client.tsx │ │ │ ├── codeDocs.styles.ts │ │ │ ├── loading.tsx │ │ │ ├── page.tsx │ │ │ ├── server.ts │ │ │ └── utils.ts │ │ │ ├── design-docs │ │ │ ├── client.tsx │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── live-example │ │ │ ├── page.tsx │ │ │ ├── server.ts │ │ │ └── utils.ts │ ├── error.tsx │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ ├── manifest.ts │ ├── not-found.tsx │ ├── page.tsx │ ├── private │ │ ├── layout.tsx │ │ └── page.tsx │ └── template.tsx ├── auth │ ├── auth.ts │ ├── getSession.tsx │ ├── index.ts │ ├── login.tsx │ ├── logout.tsx │ └── types.ts ├── components │ ├── code-docs │ │ ├── InstallCard.tsx │ │ ├── PropsTable.tsx │ │ ├── VersionCard.tsx │ │ ├── index.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── content-page │ │ └── index.tsx │ ├── content-stack │ │ ├── AnnotatedImageBlock │ │ │ ├── AnnotatedImageBlock.tsx │ │ │ ├── ImageContainer.tsx │ │ │ ├── StyledList.tsx │ │ │ ├── StyledListItem.tsx │ │ │ └── index.tsx │ │ ├── BasicUsageBlock │ │ │ ├── BasicUsageBlock.tsx │ │ │ └── index.tsx │ │ ├── ContentstackChildren.tsx │ │ ├── ContentstackEntry.tsx │ │ ├── ContentstackImage.tsx │ │ ├── ContentstackReference.tsx │ │ ├── ContentstackRichText.tsx │ │ ├── ContentstackText.tsx │ │ ├── ExampleCardBlock │ │ │ ├── ExampleCardBlock.tsx │ │ │ ├── ImageContainer.tsx │ │ │ ├── constants.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── HeaderContent.tsx │ │ ├── HorizontalLayout.tsx │ │ ├── TwoColumnExampleCard.tsx │ │ ├── componentMap.tsx │ │ ├── index.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── foundations │ │ ├── Palette.tsx │ │ └── index.ts │ ├── global │ │ ├── DarkModeToggle.tsx │ │ ├── Footer.tsx │ │ ├── LogIn.tsx │ │ ├── NotFound.tsx │ │ ├── PrivateContentWall.tsx │ │ ├── RootStyleRegistry.tsx │ │ ├── Search │ │ │ ├── Search.styles.ts │ │ │ ├── Search.tsx │ │ │ └── index.ts │ │ ├── SideNavigation │ │ │ ├── Drawer.tsx │ │ │ ├── SideNavItem.tsx │ │ │ ├── SideNavLabel.tsx │ │ │ ├── SideNavList.tsx │ │ │ ├── SideNavigation.tsx │ │ │ └── index.ts │ │ ├── UserMenu.tsx │ │ └── index.ts │ ├── glyphs │ │ ├── CodeSandbox.tsx │ │ ├── ComingSoon.tsx │ │ ├── Components.tsx │ │ ├── Display.tsx │ │ ├── Error.tsx │ │ ├── Figma.tsx │ │ ├── FormElements.tsx │ │ ├── Foundations.tsx │ │ ├── Github.tsx │ │ ├── Logo.tsx │ │ ├── Modals.tsx │ │ ├── Navigation.tsx │ │ ├── NotFound.tsx │ │ ├── Notifications.tsx │ │ ├── Patterns.tsx │ │ ├── Resources.tsx │ │ ├── Security.tsx │ │ └── index.ts │ ├── home │ │ ├── ComponentCard.tsx │ │ ├── HomeCard.tsx │ │ └── index.ts │ ├── layout-wrapper │ │ └── index.tsx │ └── live-example │ │ ├── Knob │ │ ├── Knob.tsx │ │ └── types.ts │ │ ├── KnobRow.tsx │ │ ├── Knobs.tsx │ │ ├── index.ts │ │ └── types.ts ├── constants.ts ├── contexts │ ├── AppContext.tsx │ └── ContentStackContext.tsx ├── hooks │ ├── index.ts │ ├── useCallbackRef.ts │ ├── useComponentFields.ts │ ├── useMediaQuery.ts │ └── useSession.ts └── utils │ ├── ContentStack │ ├── getContentstackResources.ts │ └── types.ts │ ├── components.ts │ ├── findComponent.ts │ ├── foundations.ts │ ├── getGithubLink.tsx │ ├── getMappedComponentName.ts │ ├── getScopeFromPkgName.ts │ ├── index.ts │ ├── mergeObjects.ts │ ├── patterns.ts │ ├── shouldAddColonToTitle.ts │ ├── titleCase.ts │ └── types.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_CONTENTSTACK_API_KEY='apikey' 2 | NEXT_PUBLIC_CONTENTSTACK_DELIVERY_TOKEN='token' 3 | NEXT_PUBLIC_GOOGLE_ANALYTICS='' 4 | NEXT_PUBLIC_ENVIRONMENT='staging' 5 | 6 | OKTA_OAUTH2_CLIENT_ID='' 7 | OKTA_OAUTH2_CLIENT_SECRET='' 8 | OKTA_OAUTH2_ISSUER='` 9 | NEXTAUTH_URL='' 10 | SECRET='' -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "./node_modules/standard/eslintrc.json"], 3 | "rules": { 4 | "space-before-function-paren": "off" 5 | } 6 | } -------------------------------------------------------------------------------- /.github/workflows/on-leafygreen-release.yml: -------------------------------------------------------------------------------- 1 | name: Repository Dispatch 2 | on: 3 | repository_dispatch: 4 | types: [release-leafygreen-ui] 5 | jobs: 6 | update-packages: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | with: 11 | ref: post-release/update-packages 12 | - name: Use Node 18 13 | uses: actions/setup-node@v3 14 | if: ${{ steps.build-cache.outputs.cache-hit != 'true' }} 15 | with: 16 | node-version: '18' 17 | cache: npm 18 | cache-dependency-path: package-lock.json 19 | - run: pnpm install 20 | - run: pnpm run update-lg '${{ github.event.client_payload.packages }}' 21 | - name: add and commit 22 | env: 23 | MY_EMAIL: s.park@mongodb.com 24 | MY_NAME: spark33 25 | run: | 26 | git config --global user.email $MY_EMAIL 27 | git config --global user.name $MY_NAME 28 | git add . 29 | git commit -m "update LGUI packages" 30 | git push origin post-release/update-packages 31 | - name: pull-request 32 | run: gh pr create -B staging -H post-release/update-packages --title 'Automated PR - Update packages' --body 'Automated PR to update LGUI packages' 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | -------------------------------------------------------------------------------- /.github/workflows/on-pr.yml: -------------------------------------------------------------------------------- 1 | name: on-pr 2 | on: 3 | pull_request: 4 | types: ['opened', 'edited', 'reopened', 'synchronize'] 5 | 6 | env: 7 | CONTENTFUL_ACCESS_TOKEN: ${{ secrets.CONTENTFUL_ACCESS_TOKEN }} 8 | NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }} 9 | 10 | jobs: 11 | lint: 12 | runs-on: ubuntu-latest 13 | name: Lint 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: pnpm 18 | uses: pnpm/action-setup@v4 19 | with: 20 | version: 9.15.6 21 | 22 | - name: Use Node 18 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: '18' 26 | cache: pnpm 27 | cache-dependency-path: 'pnpm-lock.yaml' 28 | - name: Set .npmrc 29 | run: | 30 | echo "@lg-private:registry=https://artifactory.corp.mongodb.com/artifactory/api/npm/leafygreen-ui/" >> .npmrc 31 | echo "//artifactory.corp.mongodb.com/artifactory/api/npm/leafygreen-ui/:_auth=${JFROG_AUTH}" >> .npmrc 32 | echo "always-auth=true" >> .npmrc 33 | env: 34 | JFROG_AUTH: ${{ secrets.JFROG_AUTH }} 35 | 36 | - name: Install Dependencies 37 | run: pnpm install 38 | - name: Lint 39 | run: pnpm lint 40 | types: 41 | runs-on: ubuntu-latest 42 | name: Types 43 | steps: 44 | - uses: actions/checkout@v2 45 | 46 | - name: pnpm 47 | uses: pnpm/action-setup@v4 48 | with: 49 | version: 9.15.6 50 | 51 | - name: Use Node 18 52 | uses: actions/setup-node@v3 53 | with: 54 | node-version: '18' 55 | cache: pnpm 56 | cache-dependency-path: 'pnpm-lock.yaml' 57 | 58 | - name: Set .npmrc 59 | run: | 60 | echo "@lg-private:registry=https://artifactory.corp.mongodb.com/artifactory/api/npm/leafygreen-ui/" >> .npmrc 61 | echo "//artifactory.corp.mongodb.com/artifactory/api/npm/leafygreen-ui/:_auth=${JFROG_AUTH}" >> .npmrc 62 | echo "always-auth=true" >> .npmrc 63 | env: 64 | JFROG_AUTH: ${{ secrets.JFROG_AUTH }} 65 | 66 | - name: Install Dependencies 67 | run: pnpm install 68 | build: 69 | runs-on: ubuntu-latest 70 | name: Build 71 | steps: 72 | - uses: actions/checkout@v2 73 | 74 | - name: pnpm 75 | uses: pnpm/action-setup@v4 76 | with: 77 | version: 9.15.6 78 | 79 | - name: Use Node 18 80 | uses: actions/setup-node@v3 81 | with: 82 | node-version: '18' 83 | cache: pnpm 84 | cache-dependency-path: 'pnpm-lock.yaml' 85 | 86 | - name: Set .npmrc 87 | run: | 88 | echo "@lg-private:registry=https://artifactory.corp.mongodb.com/artifactory/api/npm/leafygreen-ui/" >> .npmrc 89 | echo "//artifactory.corp.mongodb.com/artifactory/api/npm/leafygreen-ui/:_auth=${JFROG_AUTH}" >> .npmrc 90 | echo "always-auth=true" >> .npmrc 91 | env: 92 | JFROG_AUTH: ${{ secrets.JFROG_AUTH }} 93 | - name: Install Dependencies 94 | run: pnpm install 95 | - name: Build NextJS 96 | run: NEXT_PUBLIC_ENVIRONMENT=main pnpm build 97 | -------------------------------------------------------------------------------- /.github/workflows/on-staging-push.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - staging 5 | 6 | jobs: 7 | create-pr-to-main: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: pull-request 12 | uses: repo-sync/pull-request@v2 13 | with: 14 | destination_branch: "main" 15 | pr_title: "Automated PR: staging to main" 16 | github_token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.js 5 | 6 | # testing 7 | /coverage 8 | 9 | # next.js 10 | /.next/ 11 | /out/ 12 | /hosting 13 | 14 | # production 15 | /build 16 | 17 | # misc 18 | .DS_Store 19 | **/.DS_Store 20 | *.pem 21 | *.crt 22 | *.key 23 | 24 | # debug 25 | pnpm-debug.log* 26 | 27 | # env files 28 | .env 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | # vercel 35 | .vercel 36 | 37 | tsconfig.tsbuildinfo 38 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .next/ 4 | deprecated/ 5 | package.json 6 | 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "trailingComma": "all", 6 | "bracketSpacing": true, 7 | "jsxBracketSameLine": false, 8 | "arrowParens": "avoid", 9 | "endOfLine": "lf" 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "github.copilot.chat.codeGeneration.instructions": [ 4 | { 5 | "file": "node_modules/@lg-tools/prompt-kit/src/prompts/codeGeneration.md" 6 | } 7 | ], 8 | "github.copilot.chat.commitMessageGeneration.instructions": [ 9 | { 10 | "file": "node_modules/@lg-tools/prompt-kit/src/prompts/commitMessageGeneration.md" 11 | } 12 | ], 13 | "github.copilot.chat.testGeneration.instructions": [ 14 | { 15 | "file": "node_modules/@lg-tools/prompt-kit/src/prompts/testGeneration.md" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LeafyDocs 2 | 3 | ```bash 4 | pnpm install && pnpm dev 5 | ``` 6 | 7 | ## 401 Errors 8 | 9 | If you encounter 401 errors during `pnpm install`, check the following: 10 | 11 | - Ensure you are logged into Artifactory on your local machine. 12 | - Verify that your `~/.npmrc` file includes the correct credentials and permissions. 13 | 14 | For detailed guidance, refer to the [permissions setup instructions](https://github.com/10gen/leafygreen-ui-private/blob/main/README.md#permissions). 15 | -------------------------------------------------------------------------------- /environment.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | namespace NodeJS { 3 | interface ProcessEnv { 4 | AUTH_SECRET: string; 5 | NEXT_PUBLIC_CONTENTSTACK_API_KEY: string; 6 | NEXT_PUBLIC_CONTENTSTACK_DELIVERY_TOKEN: string; 7 | NEXT_PUBLIC_ENVIRONMENT: 'dev' | 'main' | 'production' | 'staging'; 8 | OKTA_CLIENT_ID: string; 9 | OKTA_CLIENT_SECRET: string; 10 | OKTA_ISSUER: string; 11 | } 12 | } 13 | } 14 | 15 | // If this file has no import/export statements (i.e. is a script) 16 | // convert it into a module by adding an empty export statement. 17 | export { } 18 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import path from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | import js from "@eslint/js"; 5 | import { FlatCompat } from "@eslint/eslintrc"; 6 | import { fixupConfigRules } from "@eslint/compat"; 7 | 8 | const __filename = fileURLToPath(import.meta.url); 9 | const __dirname = path.dirname(__filename); 10 | const compat = new FlatCompat({ 11 | baseDirectory: __dirname, 12 | recommendedConfig: js.configs.recommended, 13 | allConfig: js.configs.all, 14 | }); 15 | 16 | const patchedConfig = fixupConfigRules([...compat.extends("next/core-web-vitals")]); 17 | 18 | const config = [ 19 | ...patchedConfig, 20 | // Add more flat configs here 21 | { ignores: [".next/*"] }, 22 | ]; 23 | 24 | export default config; -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | domains: ['images.contentstack.io'], 5 | }, 6 | eslint: { 7 | ignoreDuringBuilds: true, 8 | }, 9 | }; 10 | 11 | export default nextConfig; 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leafygreen-docs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "engines": { 6 | "node": ">=18.20.0", 7 | "pnpm": ">=9.15.0" 8 | }, 9 | "packageManager": "pnpm@9.15.6", 10 | "scripts": { 11 | "preinstall": "only-allow pnpm", 12 | "dev": "next dev", 13 | "build": "next build", 14 | "start": "next start", 15 | "lint": "eslint .", 16 | "lg": "lg", 17 | "update-lg": "node scripts/update.mjs" 18 | }, 19 | "dependencies": { 20 | "@auth/core": "^0.38.0", 21 | "@contentstack/utils": "^1.3.20", 22 | "@emotion/cache": "^11.14.0", 23 | "@emotion/css": "^11.11.2", 24 | "@emotion/react": "^11.11.4", 25 | "@faker-js/faker": "^8.4.1", 26 | "@leafygreen-ui/avatar": "^2.0.10", 27 | "@leafygreen-ui/badge": "^9.0.10", 28 | "@leafygreen-ui/banner": "^9.0.13", 29 | "@leafygreen-ui/button": "^23.1.6", 30 | "@leafygreen-ui/callout": "^11.0.12", 31 | "@leafygreen-ui/card": "^12.0.9", 32 | "@leafygreen-ui/code": "^18.0.5", 33 | "@leafygreen-ui/combobox": "^11.0.19", 34 | "@leafygreen-ui/confirmation-modal": "^7.0.11", 35 | "@leafygreen-ui/copyable": "^10.0.14", 36 | "@leafygreen-ui/date-picker": "^3.0.16", 37 | "@leafygreen-ui/drawer": "^2.0.5", 38 | "@leafygreen-ui/emotion": "^4.1.1", 39 | "@leafygreen-ui/empty-state": "^2.0.15", 40 | "@leafygreen-ui/expandable-card": "^4.0.13", 41 | "@leafygreen-ui/form-footer": "^6.0.9", 42 | "@leafygreen-ui/guide-cue": "^7.0.15", 43 | "@leafygreen-ui/icon": "^13.4.0", 44 | "@leafygreen-ui/icon-button": "^16.0.12", 45 | "@leafygreen-ui/info-sprinkle": "^4.0.14", 46 | "@leafygreen-ui/leafygreen-provider": "^4.0.7", 47 | "@leafygreen-ui/lib": "^14.2.0", 48 | "@leafygreen-ui/loading-indicator": "^3.0.12", 49 | "@leafygreen-ui/logo": "^10.0.6", 50 | "@leafygreen-ui/marketing-modal": "^5.0.15", 51 | "@leafygreen-ui/menu": "^29.0.5", 52 | "@leafygreen-ui/modal": "^17.1.7", 53 | "@leafygreen-ui/number-input": "^4.1.7", 54 | "@leafygreen-ui/ordered-list": "^2.0.13", 55 | "@leafygreen-ui/pagination": "^3.0.15", 56 | "@leafygreen-ui/palette": "^4.1.4", 57 | "@leafygreen-ui/password-input": "^3.0.13", 58 | "@leafygreen-ui/pipeline": "^7.0.14", 59 | "@leafygreen-ui/polymorphic": "^2.0.9", 60 | "@leafygreen-ui/radio-box-group": "^14.0.10", 61 | "@leafygreen-ui/radio-group": "^12.0.12", 62 | "@leafygreen-ui/search-input": "^5.0.14", 63 | "@leafygreen-ui/segmented-control": "^10.0.13", 64 | "@leafygreen-ui/select": "^14.1.8", 65 | "@leafygreen-ui/side-nav": "^16.0.14", 66 | "@leafygreen-ui/skeleton-loader": "^2.0.12", 67 | "@leafygreen-ui/split-button": "^4.1.15", 68 | "@leafygreen-ui/stepper": "^5.0.13", 69 | "@leafygreen-ui/table": "^13.1.10", 70 | "@leafygreen-ui/tabs": "^14.2.5", 71 | "@leafygreen-ui/text-area": "^10.0.12", 72 | "@leafygreen-ui/text-input": "^14.0.13", 73 | "@leafygreen-ui/toast": "^7.0.14", 74 | "@leafygreen-ui/toggle": "^11.0.10", 75 | "@leafygreen-ui/tokens": "^2.12.2", 76 | "@leafygreen-ui/tooltip": "^13.0.13", 77 | "@leafygreen-ui/typography": "^20.1.9", 78 | "@lg-private/canvas-header": "^3.0.1", 79 | "@lg-private/cloud-nav": "^1.0.2", 80 | "@lg-private/feature-walls": "^4.1.1", 81 | "@lg-private/vertical-stepper": "^3.0.0", 82 | "@next/third-parties": "^14.2.3", 83 | "@storybook/react": "^8.5.3", 84 | "@storybook/test": "^8.5.3", 85 | "@storybook/testing-library": "^0.2.2", 86 | "commander": "^12.0.0", 87 | "contentstack": "^3.19.2", 88 | "fuse.js": "^7.0.0", 89 | "lodash": "^4.17.21", 90 | "marked": "^12.0.2", 91 | "next": "14.2.25", 92 | "next-auth": "^5.0.0-beta.18", 93 | "polished": "^4.3.1", 94 | "react": "^18", 95 | "react-dom": "^18" 96 | }, 97 | "devDependencies": { 98 | "@eslint/compat": "^1.2.6", 99 | "@eslint/js": "^9.19.0", 100 | "@lg-tools/cli": "^0.9.2", 101 | "@lg-tools/codemods": "^0.1.6", 102 | "@lg-tools/prompt-kit": "^0.2.0", 103 | "@lg-tools/storybook-utils": "^0.1.3", 104 | "@types/lodash": "^4.17.0", 105 | "@types/node": "^20", 106 | "@types/react": "^18", 107 | "@types/react-dom": "^18", 108 | "eslint": "^9.19.0", 109 | "eslint-config-next": "15.1.6", 110 | "eslint-plugin-react": "^7.37.4", 111 | "eslint-plugin-react-hooks": "^5.1.0", 112 | "globals": "^15.14.0", 113 | "only-allow": "^1.2.1", 114 | "typescript": "^5", 115 | "typescript-eslint": "^8.23.0" 116 | }, 117 | "overrides": { 118 | "eslint": "$eslint" 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /public/examples/marketing-fill-dark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mongodb/design/657eacc389f21a2d97ffa3d7b6f5935b0bd5b853/public/examples/marketing-fill-dark.jpg -------------------------------------------------------------------------------- /public/examples/marketing-fill-light.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mongodb/design/657eacc389f21a2d97ffa3d7b6f5935b0bd5b853/public/examples/marketing-fill-light.jpg -------------------------------------------------------------------------------- /scripts/update.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { execSync } from 'child_process'; 3 | 4 | // Read the package.json file 5 | fs.readFile('package.json', 'utf8', (err, data) => { 6 | if (err) { 7 | console.error('Error reading package.json:', err); 8 | return; 9 | } 10 | 11 | try { 12 | const packageJson = JSON.parse(data); 13 | const dependencies = packageJson.dependencies || {}; 14 | const devDependencies = packageJson.devDependencies || {}; 15 | const leafyGreenPackages = Object.keys(dependencies) 16 | .concat(Object.keys(devDependencies)) 17 | .filter( 18 | pkg => 19 | pkg.startsWith('@leafygreen-ui') || pkg.startsWith('@lg-private'), 20 | ); 21 | 22 | leafyGreenPackages.forEach(pkg => { 23 | console.log(`Updating ${pkg}...`); 24 | execSync(`pnpm install ${pkg}@latest`, { stdio: 'inherit' }); 25 | }); 26 | } catch (e) { 27 | console.error('Error updating packages:', e); 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /src/app/[contentPageGroup]/[contentPage]/page.tsx: -------------------------------------------------------------------------------- 1 | import { getContentPage } from '@/utils/ContentStack/getContentstackResources'; 2 | 3 | import startCase from 'lodash/startCase'; 4 | import { auth } from '@/auth'; 5 | import { PrivateContentWall } from '@/components/global'; 6 | import { ContentPage } from '@/components/content-page'; 7 | 8 | export default async function Page({ 9 | params: { contentPage: contentPageTitle }, 10 | }: { 11 | params: { contentPage: string }; 12 | }) { 13 | const [session, contentPage] = await Promise.all([ 14 | auth(), 15 | getContentPage(startCase(contentPageTitle)), 16 | ]); 17 | const isLoggedIn = !!session?.user; 18 | const isContentPrivate = contentPage?.is_private; 19 | const shouldRenderPrivateContentWall = Boolean( 20 | isContentPrivate && !isLoggedIn, 21 | ); 22 | 23 | return ( 24 |
25 | {shouldRenderPrivateContentWall ? ( 26 | 27 | ) : ( 28 | 29 | )} 30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/app/[contentPageGroup]/[contentPage]/template.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { css } from '@emotion/css'; 4 | 5 | export default function Layout({ children }: { children: React.ReactNode }) { 6 | return ( 7 |
13 | {children} 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import { handlers } from "@/auth/auth"; 2 | export const { GET, POST } = handlers; 3 | -------------------------------------------------------------------------------- /src/app/component/[component]/code-docs/client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { 3 | InstallCard, 4 | PropTableState, 5 | PropsTable, 6 | VersionCard, 7 | } from '@/components/code-docs'; 8 | import { codeDocsMetaCardsStyles, codeDocsPageStyles } from './codeDocs.styles'; 9 | import { SubPath } from '@/utils'; 10 | 11 | interface CodeDocsContentProps { 12 | componentName: SubPath; 13 | componentProps?: Array; 14 | changelog: string | null; 15 | } 16 | 17 | export const CodeDocsContent = ({ 18 | componentName, 19 | componentProps, 20 | changelog, 21 | }: CodeDocsContentProps) => { 22 | return ( 23 |
24 |
25 | 26 | 27 |
28 | 29 | {componentProps?.map(({ name, props }) => { 30 | return ; 31 | })} 32 |
33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /src/app/component/[component]/code-docs/codeDocs.styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css'; 2 | import { breakpoints, spacing } from '@leafygreen-ui/tokens'; 3 | 4 | export const codeDocsPageStyles = css` 5 | display: flex; 6 | flex-direction: column; 7 | gap: ${spacing[800]}px; 8 | `; 9 | 10 | export const codeDocsMetaCardsStyles = css` 11 | display: grid; 12 | gap: ${spacing[800]}px; 13 | grid-template-columns: 2fr 1fr; 14 | max-width: 100%; 15 | 16 | @media (max-width: ${breakpoints.Tablet}px) { 17 | grid-template-columns: 1fr; 18 | } 19 | `; 20 | -------------------------------------------------------------------------------- /src/app/component/[component]/code-docs/loading.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { CardSkeleton } from '@leafygreen-ui/skeleton-loader'; 3 | import { codeDocsMetaCardsStyles, codeDocsPageStyles } from './codeDocs.styles'; 4 | 5 | export default function Loading() { 6 | return ( 7 |
8 |
9 | 10 | 11 |
12 | 13 | 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/app/component/[component]/code-docs/page.tsx: -------------------------------------------------------------------------------- 1 | import { fetchTSDocs, fetchChangelog } from './server'; 2 | import { CodeDocsContent } from './client'; 3 | import { parseComponentPropsFromTSDocs } from './utils'; 4 | import { getMappedComponentName, type SubPath } from '@/utils'; 5 | import { auth } from '@/auth'; 6 | 7 | export default async function Page({ 8 | params, 9 | }: { 10 | params: { component: SubPath }; 11 | }) { 12 | const session = await auth(); 13 | const isLoggedIn = !!session?.user; 14 | const componentName = params.component; 15 | const mappedComponentName = 16 | getMappedComponentName[componentName] ?? componentName; 17 | 18 | const [tsDocs, changelog] = await Promise.all([ 19 | fetchTSDocs(mappedComponentName), 20 | isLoggedIn ? fetchChangelog(componentName) : Promise.resolve(null), 21 | ]); 22 | 23 | const componentProps = parseComponentPropsFromTSDocs(tsDocs, componentName); 24 | 25 | return ( 26 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/app/component/[component]/code-docs/server.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { TSDocResponse } from '@/components/code-docs'; 4 | import { SubPath } from '@/utils'; 5 | import { getScopeFromPkgName } from '@/utils/getScopeFromPkgName'; 6 | import { marked } from 'marked'; 7 | 8 | export async function fetchTSDocs( 9 | componentName: SubPath | string, 10 | ): Promise | null> { 11 | if (typeof componentName !== 'string') return null; 12 | 13 | try { 14 | return await import( 15 | `/node_modules/${getScopeFromPkgName( 16 | componentName, 17 | )}/${componentName}/tsdoc.json` 18 | ).then(response => { 19 | return response.default; 20 | }); 21 | } catch (error) { 22 | console.warn(error); 23 | return null; 24 | } 25 | } 26 | 27 | export async function fetchChangelog( 28 | componentName: string, 29 | ): Promise { 30 | try { 31 | const response = await fetch( 32 | `https://cdn.jsdelivr.net/npm/@leafygreen-ui/${componentName}/CHANGELOG.md`, 33 | ); 34 | if (!response.ok) { 35 | throw new Error('Failed to fetch Markdown file'); 36 | } 37 | const markdown = await response.text(); 38 | const parsedMarkdown = marked(markdown); 39 | 40 | return parsedMarkdown; 41 | } catch (error) { 42 | console.warn(error); 43 | return null; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/app/component/[component]/code-docs/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PropTableState, 3 | TSDocResponse, 4 | mergeProps, 5 | } from '@/components/code-docs'; 6 | import { SubPath, findComponent } from '@/utils'; 7 | import { kebabCase } from 'lodash'; 8 | 9 | export function parseComponentPropsFromTSDocs( 10 | tsDocs: Array | null, 11 | componentName: SubPath, 12 | ): Array | undefined { 13 | if (!tsDocs) return; 14 | 15 | const componentMeta = findComponent(componentName); 16 | const subComponents = componentMeta?.subComponents; 17 | 18 | if (!!subComponents) { 19 | const propTables = tsDocs.filter(tsdoc => 20 | subComponents.includes(tsdoc.displayName), 21 | ); 22 | 23 | const reducedPropTables: Array = propTables.reduce( 24 | (acc: Array, value: TSDocResponse) => { 25 | const mergedProps = mergeProps(value.props); 26 | return [...acc, { name: value.displayName, props: mergedProps }]; 27 | }, 28 | [], 29 | ); 30 | 31 | return reducedPropTables; 32 | } else { 33 | const centralProps = tsDocs.find(tsdoc => { 34 | return kebabCase(tsdoc.displayName).includes(kebabCase(componentName)); 35 | }); 36 | const mergedProps = mergeProps(centralProps?.props); 37 | 38 | return [{ name: componentName, props: mergedProps }]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/component/[component]/design-docs/client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { css } from '@emotion/css'; 4 | import { ContentstackRichText } from '@/components/content-stack'; 5 | import { CSNode } from '@/components/content-stack/types'; 6 | 7 | interface DesignDocsContentProps { 8 | content?: CSNode; 9 | } 10 | 11 | export const DesignDocsContent = ({ content }: DesignDocsContentProps) => { 12 | return ( 13 |
18 | 19 |
20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/app/component/[component]/design-docs/loading.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { ParagraphSkeleton } from '@leafygreen-ui/skeleton-loader'; 3 | 4 | export default function Loading() { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/app/component/[component]/design-docs/page.tsx: -------------------------------------------------------------------------------- 1 | import { fetchComponent } from '@/utils/ContentStack/getContentstackResources'; 2 | import { DesignDocsContent } from './client'; 3 | 4 | export default async function Page({ 5 | params: { component: componentName }, 6 | }: { 7 | params: { component: string }; 8 | }) { 9 | const component = await fetchComponent(componentName, { 10 | includeContent: true, 11 | }); 12 | 13 | return ( 14 |
15 | 16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/app/component/[component]/layout.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { css } from '@emotion/css'; 4 | import { useRouter, usePathname } from 'next/navigation'; 5 | import React from 'react'; 6 | 7 | import IconButton from '@leafygreen-ui/icon-button'; 8 | import { CodeSandbox, Figma, Github } from '@/components/glyphs'; 9 | import { Tabs, Tab } from '@leafygreen-ui/tabs'; 10 | import { spacing } from '@leafygreen-ui/tokens'; 11 | import { H2 } from '@leafygreen-ui/typography'; 12 | 13 | import { useSession } from '@/hooks'; 14 | import { SubPath, getGithubLink } from '@/utils'; 15 | import { useContentStackContext } from '@/contexts/ContentStackContext'; 16 | 17 | import { titleCase } from '@/utils/titleCase'; 18 | import { PrivateContentWall } from '@/components/global'; 19 | 20 | const liveExamplePath = 'live-example'; 21 | const designDocsPath = 'design-docs'; 22 | const codeDocsPath = 'code-docs'; 23 | 24 | export default function ComponentLayout({ 25 | children, 26 | }: { 27 | children: React.ReactNode; 28 | }) { 29 | const { isLoggedIn } = useSession(); 30 | const router = useRouter(); 31 | const pathname = usePathname(); 32 | const currentComponent = pathname.split('/')[2]; 33 | const { components: componentsFromContext } = useContentStackContext(); 34 | // canvas-header => Canvas Header 35 | const componentTitle = titleCase(currentComponent.split('-').join(' ')); 36 | // Find component in context. This will include the data from Contentstack 37 | const component = componentsFromContext.find( 38 | component => component.title === componentTitle, 39 | ); 40 | 41 | const isComponentPrivate = component?.private; 42 | const shouldRenderPrivateContentWall = Boolean( 43 | isComponentPrivate && !isLoggedIn, 44 | ); 45 | 46 | const getSelected = () => { 47 | const suffix = pathname.split('/')[3]; 48 | if (suffix === liveExamplePath) { 49 | return 0; 50 | } 51 | 52 | if (suffix === designDocsPath) { 53 | return 1; 54 | } 55 | 56 | if (suffix === codeDocsPath) { 57 | return 2; 58 | } 59 | }; 60 | 61 | const externalLinks = [ 62 | { 63 | 'aria-label': 'View Figma file', 64 | href: component?.figmaurl, 65 | icon: , 66 | isPrivate: true, 67 | }, 68 | { 69 | 'aria-label': 'View GitHub package', 70 | href: getGithubLink( 71 | component?.private ?? false, 72 | currentComponent as SubPath, 73 | ), 74 | icon: , 75 | }, 76 | { 77 | 'aria-label': 'Edit in CodeSandbox', 78 | href: component?.codesandbox_url?.href, 79 | icon: , 80 | }, 81 | ]; 82 | 83 | return ( 84 |
89 | {shouldRenderPrivateContentWall ? ( 90 | 91 | ) : ( 92 | <> 93 |

99 | {currentComponent.split('-').join(' ')} 100 |

101 | 102 | 110 | {externalLinks.map( 111 | ( 112 | { 'aria-label': ariaLabel, href, icon, isPrivate }, 113 | index, 114 | ) => { 115 | if (isPrivate && !isLoggedIn) { 116 | return null; 117 | } 118 | 119 | if (!href) { 120 | return null; 121 | } 122 | 123 | return ( 124 | 132 | {icon} 133 | 134 | ); 135 | }, 136 | )} 137 | 138 | } 139 | > 140 | 142 | router.push(`/component/${currentComponent}/${liveExamplePath}`) 143 | } 144 | name="Live Example" 145 | > 146 | <> 147 | 148 | 150 | router.push(`/component/${currentComponent}/${designDocsPath}`) 151 | } 152 | name="Design Documentation" 153 | > 154 | <> 155 | 156 | 158 | router.push(`/component/${currentComponent}/${codeDocsPath}`) 159 | } 160 | name="Code Documentation" 161 | > 162 | <> 163 | 164 | 165 | 166 |
{children}
167 | 168 | )} 169 |
170 | ); 171 | } 172 | -------------------------------------------------------------------------------- /src/app/component/[component]/live-example/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useEffect, useState } from 'react'; 4 | import { css } from '@emotion/css'; 5 | import Card from '@leafygreen-ui/card'; 6 | import { borderRadius, color, spacing } from '@leafygreen-ui/tokens'; 7 | import { 8 | StoryData, 9 | KnobProps, 10 | ComponentProps, 11 | } from '@/components/live-example/types'; 12 | import { loadStories } from './server'; 13 | import { Knobs } from '@/components/live-example/Knobs'; 14 | import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; 15 | import { createDefaultProps } from './utils'; 16 | import { SubPath } from '@/utils'; 17 | 18 | export default function Page({ params }: { params: { component: SubPath } }) { 19 | const { darkMode } = useDarkMode(); 20 | const [data, setData] = useState(); 21 | const [knobProps, setKnobProps] = useState({}); 22 | const [componentProps, setComponentProps] = useState({}); 23 | 24 | // When the component changes, re-fetch the LiveExample data 25 | useEffect(() => { 26 | loadStories(params.component).then(response => { 27 | if (response) { 28 | setData(response); 29 | } 30 | }); 31 | }, [params.component]); 32 | 33 | // When the story data changes, update the knobs 34 | useEffect(() => { 35 | if (data) { 36 | const normalizedProps = createDefaultProps(data.meta, darkMode); 37 | setKnobProps(normalizedProps); 38 | 39 | const propsWithValue: ComponentProps = {}; 40 | // creates an object with all the prop names and the values 41 | for (let key in normalizedProps) { 42 | propsWithValue[key] = normalizedProps[key].value ?? undefined; 43 | } 44 | 45 | setComponentProps(propsWithValue); 46 | } 47 | // eslint-disable-next-line react-hooks/exhaustive-deps 48 | }, [data]); 49 | 50 | // If the Context DarkMode changes, update the knobs 51 | useEffect(() => { 52 | updateKnobValue('darkMode', darkMode); 53 | }, [darkMode]); 54 | 55 | const updateKnobValue = (propName: string, newValue: any) => { 56 | setKnobProps(props => { 57 | return { 58 | ...props, 59 | [propName]: { 60 | ...props[propName], 61 | value: newValue, 62 | }, 63 | }; 64 | }); 65 | 66 | setComponentProps(props => { 67 | return { 68 | ...props, 69 | [propName]: newValue, 70 | }; 71 | }); 72 | }; 73 | 74 | const LiveExample = data?.LiveExample; 75 | 76 | return LiveExample ? ( 77 | 82 |
94 |
div { 100 | // temp workaround for cloudNav because the width of cloudNav is set to 100vw 101 | &[data-lgid='lg-cloud_nav'] { 102 | width: auto; 103 | margin: 0 auto; 104 | } 105 | } 106 | `} 107 | > 108 | {/* @ts-expect-error */} 109 | 110 |
111 |
112 |
117 | 118 |
119 |
120 | ) : ( 121 | 126 | ); 127 | } 128 | -------------------------------------------------------------------------------- /src/app/component/[component]/live-example/server.ts: -------------------------------------------------------------------------------- 1 | import { StoryData } from '@/components/live-example/types'; 2 | import { composeStories } from '@storybook/react'; 3 | 4 | import { getScopeFromPkgName } from '../../../../utils/getScopeFromPkgName'; 5 | import { SubPath, getMappedComponentName } from '@/utils'; 6 | 7 | export async function loadStories(componentName: SubPath) { 8 | const mappedComponentName = 9 | getMappedComponentName[componentName] ?? componentName; 10 | 11 | try { 12 | // We have to use node_modules because it is static and can be analyzed at build time 13 | const stories = await import( 14 | `/node_modules/${getScopeFromPkgName( 15 | mappedComponentName, 16 | )}/${mappedComponentName}/stories` 17 | ); 18 | const { LiveExample, default: extractMeta } = composeStories(stories); 19 | const meta = extractMeta ?? stories.default; 20 | 21 | return { LiveExample, meta } as StoryData; 22 | } catch (error) { 23 | console.log('ERROR LOADING STORIES'); 24 | console.error(error); 25 | return; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/component/[component]/live-example/utils.ts: -------------------------------------------------------------------------------- 1 | import { mergeObjects } from '@/utils'; 2 | import { Meta } from '@storybook/react'; 3 | 4 | export const OMIT_PROPS = [ 5 | 'aria-controls', 6 | 'as', 7 | 'baseFontSize', 8 | 'children', 9 | 'className', 10 | 'contentClassName', 11 | 'defaultOpen', 12 | 'href', 13 | 'id', 14 | 'inputValue', 15 | 'loadingIndicator', 16 | 'menuItems', 17 | 'name', 18 | 'onCurrentPageOptionChange', 19 | 'onDismiss', 20 | 'open', 21 | 'primaryButton', 22 | 'refButtonPosition', 23 | 'shouldTooltipUsePortal', 24 | 'stateNotifications', 25 | 'timeout', 26 | 'usePortal', 27 | 'value', 28 | 'trigger', 29 | ]; 30 | 31 | export function constructArgValues(argValues: Record) { 32 | let returnObj: Record = {}; 33 | 34 | for (let key in argValues) { 35 | if (typeof argValues[key] !== 'object') { 36 | returnObj[key] = { value: argValues[key] }; 37 | } else { 38 | returnObj[key] = argValues[key]; 39 | } 40 | } 41 | 42 | return returnObj; 43 | } 44 | 45 | export function removeProps(object: Record) { 46 | return Object.fromEntries( 47 | Object.entries(object).filter(([key]) => !OMIT_PROPS.includes(key)), 48 | ); 49 | } 50 | 51 | export function createDefaultProps(meta: Meta, darkMode: boolean) { 52 | const combinedProps = 53 | meta && meta.args && meta.argTypes 54 | ? mergeObjects(constructArgValues(meta.args), meta.argTypes) 55 | : {}; 56 | const filteredProps = removeProps(combinedProps); 57 | 58 | filteredProps.darkMode = { value: darkMode, control: 'boolean' }; 59 | 60 | return filteredProps; 61 | } 62 | -------------------------------------------------------------------------------- /src/app/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | import React from "react"; 5 | 6 | import Button from "@leafygreen-ui/button"; 7 | import { BasicEmptyState } from "@leafygreen-ui/empty-state"; 8 | // @ts-expect-error 9 | import ArrowLeftIcon from "@leafygreen-ui/icon/dist/ArrowLeft"; 10 | import { Error } from "@/components/glyphs"; 11 | 12 | export default function NotFoundComponent() { 13 | const router = useRouter(); 14 | 15 | return ( 16 |
17 | } 21 | primaryButton={ 22 | 28 | } 29 | /> 30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mongodb/design/657eacc389f21a2d97ffa3d7b6f5935b0bd5b853/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | /* Semibold */ 2 | @font-face { 3 | font-family: 'Euclid Circular A'; 4 | src: url('https://d2va9gm4j17fy9.cloudfront.net/fonts/euclid-circular/EuclidCircularA-Semibold-WebXL.woff') format('woff'), 5 | url('https://d2va9gm4j17fy9.cloudfront.net/fonts/euclid-circular/EuclidCircularA-Semibold-WebXL.woff2') format('woff2'), 6 | url('https://d2va9gm4j17fy9.cloudfront.net/fonts/euclid-circular/EuclidCircularA-Semibold.ttf') format('truetype'); 7 | font-weight: 700; 8 | font-style: normal; 9 | } 10 | 11 | /* Semibold Italic */ 12 | @font-face { 13 | font-family: 'Euclid Circular A'; 14 | src: url('https://d2va9gm4j17fy9.cloudfront.net/fonts/euclid-circular/EuclidCircularA-SemiboldItalic-WebXL.woff') format('woff'), 15 | url('https://d2va9gm4j17fy9.cloudfront.net/fonts/euclid-circular/EuclidCircularA-SemiboldItalic-WebXL.woff2') format('woff2'), 16 | url('https://d2va9gm4j17fy9.cloudfront.net/fonts/euclid-circular/EuclidCircularA-SemiboldItalic.ttf') format('truetype'); 17 | font-weight: 700; 18 | font-style: italic; 19 | } 20 | 21 | /* Medium */ 22 | @font-face { 23 | font-family: 'Euclid Circular A'; 24 | src: url('https://d2va9gm4j17fy9.cloudfront.net/fonts/euclid-circular/EuclidCircularA-Medium-WebXL.woff') format('woff'), 25 | url('https://d2va9gm4j17fy9.cloudfront.net/fonts/euclid-circular/EuclidCircularA-Medium-WebXL.woff2') format('woff2'), 26 | url('https://d2va9gm4j17fy9.cloudfront.net/fonts/euclid-circular/EuclidCircularA-Medium.ttf') format('truetype'); 27 | font-weight: 500; 28 | font-style: normal; 29 | } 30 | 31 | /* Medium Italic */ 32 | @font-face { 33 | font-family: 'Euclid Circular A'; 34 | src: url('https://d2va9gm4j17fy9.cloudfront.net/fonts/euclid-circular/EuclidCircularA-MediumItalic-WebXL.woff') format('woff'), 35 | url('https://d2va9gm4j17fy9.cloudfront.net/fonts/euclid-circular/EuclidCircularA-MediumItalic-WebXL.woff2') format('woff2'), 36 | url('https://d2va9gm4j17fy9.cloudfront.net/fonts/euclid-circular/EuclidCircularA-MediumItalic.ttf') format('truetype'); 37 | font-weight: 500; 38 | font-style: italic; 39 | } 40 | 41 | /* Normal */ 42 | @font-face { 43 | font-family: 'Euclid Circular A'; 44 | src: url('https://d2va9gm4j17fy9.cloudfront.net/fonts/euclid-circular/EuclidCircularA-Regular-WebXL.woff') format('woff'), 45 | url('https://d2va9gm4j17fy9.cloudfront.net/fonts/euclid-circular/EuclidCircularA-Regular-WebXL.woff2') format('woff2'), 46 | url('https://d2va9gm4j17fy9.cloudfront.net/fonts/euclid-circular/EuclidCircularA-Regular.ttf') format('truetype'); 47 | font-weight: 400; 48 | font-style: normal; 49 | } 50 | 51 | /* Italic */ 52 | @font-face { 53 | font-family: 'Euclid Circular A'; 54 | src: url('https://d2va9gm4j17fy9.cloudfront.net/fonts/euclid-circular/EuclidCircularA-RegularItalic-WebXL.woff') format('woff'), 55 | url('https://d2va9gm4j17fy9.cloudfront.net/fonts/euclid-circular/EuclidCircularA-RegularItalic-WebXL.woff2') format('woff2'), 56 | url('https://d2va9gm4j17fy9.cloudfront.net/fonts/euclid-circular/EuclidCircularA-RegularItalic.ttf') format('truetype'); 57 | font-weight: 400; 58 | font-style: italic; 59 | } 60 | 61 | /** 62 | * Value Serif 63 | */ 64 | 65 | /* Bold */ 66 | @font-face { 67 | font-family: 'MongoDB Value Serif'; 68 | font-weight: 700; 69 | src: url('https://d2va9gm4j17fy9.cloudfront.net/fonts/value-serif/MongoDBValueSerif-Bold.woff') format('woff'), 70 | url('https://d2va9gm4j17fy9.cloudfront.net/fonts/value-serif/MongoDBValueSerif-Bold.woff2') format('woff2'), 71 | url('https://d2va9gm4j17fy9.cloudfront.net/fonts/value-serif/MongoDBValueSerif-Bold.ttf') format('truetype'); 72 | } 73 | 74 | /* Medium */ 75 | @font-face { 76 | font-family: 'MongoDB Value Serif'; 77 | font-weight: 500; 78 | src: url('https://d2va9gm4j17fy9.cloudfront.net/fonts/value-serif/MongoDBValueSerif-Medium.woff') format('woff'), 79 | url('https://d2va9gm4j17fy9.cloudfront.net/fonts/value-serif/MongoDBValueSerif-Medium.woff2') format('woff2'), 80 | url('https://d2va9gm4j17fy9.cloudfront.net/fonts/value-serif/MongoDBValueSerif-Medium.ttf') format('truetype'); 81 | } 82 | 83 | /* Normal */ 84 | @font-face { 85 | font-family: 'MongoDB Value Serif'; 86 | font-weight: 400; 87 | src: url('https://d2va9gm4j17fy9.cloudfront.net/fonts/value-serif/MongoDBValueSerif-Regular.woff') format('woff'), 88 | url('https://d2va9gm4j17fy9.cloudfront.net/fonts/value-serif/MongoDBValueSerif-Regular.woff2') format('woff2'), 89 | url('https://d2va9gm4j17fy9.cloudfront.net/fonts/value-serif/MongoDBValueSerif-Regular.ttf') format('truetype'); 90 | } 91 | 92 | html { 93 | font-family: 'Euclid Circular A', 'Helvetica Neue', Helvetica, 94 | Arial, sans-serif; 95 | font-weight: normal; 96 | font-style: normal; 97 | } 98 | 99 | body { 100 | margin: 0; 101 | 102 | } 103 | 104 | *, 105 | *:before, 106 | *:after { 107 | box-sizing: border-box; 108 | } -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import './globals.css'; 4 | import { auth } from '@/auth'; 5 | import { SessionProvider } from 'next-auth/react'; 6 | import LayoutWrapper from '@/components/layout-wrapper'; 7 | import { getComponents } from '@/utils/ContentStack/getContentstackResources'; 8 | 9 | export default async function RootLayout({ 10 | children, 11 | }: Readonly<{ 12 | children: React.ReactNode; 13 | }>) { 14 | const [session, components] = await Promise.all([ 15 | auth(), 16 | getComponents({ includeContent: false }), 17 | ]); 18 | 19 | return ( 20 | // Provide the session to the entire app 21 | 22 | {children} 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/app/manifest.ts: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | 3 | export default function manifest(): Metadata { 4 | return { 5 | title: 'LeafyGreen Docs', 6 | description: 'LeafyGreen Docs', 7 | icons: [ 8 | { 9 | url: '/favicon.ico', 10 | sizes: 'any', 11 | type: 'image/x-icon', 12 | }, 13 | ], 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { NotFound } from "@/components/global/NotFound"; 4 | 5 | export default function NotFoundComponent() { 6 | return ; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { css } from '@emotion/css'; 4 | import { Body, H1 } from '@leafygreen-ui/typography'; 5 | import { spacing } from '@leafygreen-ui/tokens'; 6 | import { ComponentCard, HomeCard } from '@/components/home'; 7 | 8 | export default function Home() { 9 | return ( 10 |
15 |
20 |

LeafyGreen Design System

21 | 22 | MongoDB’s open-source design system for building intuitive, and 23 | beautiful experiences 24 | 25 |
26 | 27 |
35 | 36 | 37 | 44 | 45 | 51 |
52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /src/app/private/layout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { css } from "@emotion/css"; 5 | import { spacing } from "@leafygreen-ui/tokens"; 6 | 7 | export default function PrivateLayout({ 8 | children, 9 | }: { 10 | children: React.ReactNode; 11 | }) { 12 | return ( 13 |
20 | {children} 21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/app/private/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useRouter } from 'next/navigation'; 4 | import Button from '@leafygreen-ui/button'; 5 | import { BasicEmptyState } from '@leafygreen-ui/empty-state'; 6 | // @ts-expect-error 7 | import ArrowLeftIcon from '@leafygreen-ui/icon/dist/ArrowLeft'; 8 | // @ts-expect-error 9 | import LogInIcon from '@leafygreen-ui/icon/dist/LogIn'; 10 | import { login } from '@/auth'; 11 | import { ComingSoon, Security } from '@/components/glyphs'; 12 | import { useSession } from '@/hooks'; 13 | 14 | export default function Private() { 15 | const router = useRouter(); 16 | const { isLoggedIn } = useSession(); 17 | 18 | return isLoggedIn ? ( 19 | } onClick={() => router.push('/')}> 24 | Return to home 25 | 26 | } 27 | graphic={} 28 | /> 29 | ) : ( 30 | login()} 37 | leftGlyph={} 38 | > 39 | Log In 40 | 41 | } 42 | graphic={} 43 | /> 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/app/template.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { css, cx } from '@emotion/css'; 4 | import React from 'react'; 5 | import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; 6 | import { color, spacing } from '@leafygreen-ui/tokens'; 7 | 8 | import { 9 | DarkModeToggle, 10 | Footer, 11 | UserMenu, 12 | SideNavigation, 13 | } from '@/components/global'; 14 | import { useMediaQuery } from '@/hooks'; 15 | import { SIDE_NAV_WIDTH } from '@/constants'; 16 | 17 | export default function Template({ children }: { children: React.ReactNode }) { 18 | const { darkMode } = useDarkMode(); 19 | const [isMobile] = useMediaQuery(['(max-width: 640px)'], { 20 | fallback: [false], 21 | }); 22 | 23 | return ( 24 |
34 | 35 | 36 |
49 | 50 | 51 |
52 | 53 |
66 | {children} 67 |
68 |
69 |
70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /src/auth/auth.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from 'next-auth'; 2 | import Okta from '@auth/core/providers/okta'; 3 | 4 | const AUTH_OPTIONS = { 5 | providers: [ 6 | Okta({ 7 | clientId: process.env.NEXT_PUBLIC_OKTA_CLIENT_ID, 8 | clientSecret: process.env.NEXT_PUBLIC_OKTA_CLIENT_SECRET, 9 | issuer: process.env.NEXT_PUBLIC_OKTA_ISSUER, 10 | }), 11 | ], 12 | trustHost: true, 13 | secret: process.env.NEXT_PUBLIC_AUTH_SECRET, 14 | }; 15 | 16 | export const { handlers, signIn, signOut, auth } = NextAuth(AUTH_OPTIONS); 17 | -------------------------------------------------------------------------------- /src/auth/getSession.tsx: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { auth } from '@/auth/auth'; 4 | 5 | export async function getSession() { 6 | 'use server'; 7 | const session = await auth(); // server side 8 | return session; 9 | } 10 | -------------------------------------------------------------------------------- /src/auth/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./auth"; 2 | export { getSession } from "./getSession"; 3 | export { login } from "./login"; 4 | export { logout } from "./logout"; 5 | export * from "./types"; 6 | -------------------------------------------------------------------------------- /src/auth/login.tsx: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { signIn } from '@/auth/auth'; 4 | 5 | export async function login() { 6 | 'use server'; 7 | await signIn('okta'); 8 | } 9 | -------------------------------------------------------------------------------- /src/auth/logout.tsx: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { signOut } from '@/auth/auth'; 4 | 5 | export async function logout() { 6 | 'use server'; 7 | await signOut({ redirect: false }); 8 | } 9 | -------------------------------------------------------------------------------- /src/auth/types.ts: -------------------------------------------------------------------------------- 1 | // Copied from @auth/core since they are not exported 2 | 3 | type ISODateString = string; 4 | 5 | export interface User { 6 | id?: string; 7 | name?: string | null; 8 | email?: string | null; 9 | image?: string | null; 10 | } 11 | 12 | export interface Session { 13 | user?: User; 14 | expires: ISODateString; 15 | } 16 | -------------------------------------------------------------------------------- /src/components/code-docs/InstallCard.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useState } from 'react'; 4 | import { css } from '@emotion/css'; 5 | import Card from '@leafygreen-ui/card'; 6 | import Copyable from '@leafygreen-ui/copyable'; 7 | import { 8 | SegmentedControl, 9 | SegmentedControlOption, 10 | } from '@leafygreen-ui/segmented-control'; 11 | import { spacing } from '@leafygreen-ui/tokens'; 12 | import { Subtitle } from '@leafygreen-ui/typography'; 13 | 14 | import { getScopeFromPkgName } from '@/utils/getScopeFromPkgName'; 15 | 16 | export const InstallCard = ({ component }: { component: string }) => { 17 | const [packageManager, setPackageManager] = useState('yarn'); 18 | 19 | return ( 20 | 21 | 27 | Installation 28 | 29 |
36 | 41 | yarn 42 | npm 43 | 44 | 45 | 52 | {packageManager === 'yarn' 53 | ? `yarn add ${getScopeFromPkgName(component)}/${component}` 54 | : `npm i ${getScopeFromPkgName(component)}/${component}`} 55 | 56 |
57 |
58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /src/components/code-docs/PropsTable.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { css, cx } from "@emotion/css"; 5 | import ExpandableCard from "@leafygreen-ui/expandable-card"; 6 | import { 7 | Table, 8 | TableHead, 9 | HeaderRow, 10 | HeaderCell, 11 | TableBody, 12 | Row, 13 | Cell, 14 | } from "@leafygreen-ui/table"; 15 | import { Body, InlineCode, Link } from "@leafygreen-ui/typography"; 16 | import { getHTMLAttributesLink, formatType } from "./utils"; 17 | import { PropTableState } from "./types"; 18 | 19 | const COLUMNS = ["name", "default", "description", "type"]; 20 | 21 | export const PropsTable = ({ props, name }: PropTableState) => { 22 | const { componentProps, inheritedProps } = props; 23 | 24 | return ( 25 | 33 | {name.split("-").join(" ")} Props 34 | 35 | } 36 | contentClassName={css` 37 | padding-left: 0; 38 | padding-right: 0; 39 | `} 40 | > 41 | 45 | 46 | 47 | {COLUMNS.map((columnName: string) => ( 48 | 59 | {columnName} 60 | 61 | ))} 62 | 63 | 64 | 65 | {componentProps && 66 | Object.keys(componentProps) 67 | .sort() 68 | .map((row: string) => { 69 | const { 70 | name, 71 | required, 72 | defaultValue = false, 73 | description, 74 | type, 75 | } = componentProps[row]; 76 | return ( 77 | 78 | 79 | <> 80 | {name} 81 | 86 | {required ? "*" : ""} 87 | 88 | 89 | 90 | 91 | {defaultValue?.value ?? `'-'`} 92 | 93 | 94 | {description} 95 | 96 | 97 | {formatType(type)} 98 | 99 | 100 | ); 101 | })} 102 | {inheritedProps && ( 103 | 104 | 105 | ...rest 106 | 107 | 108 | 109 | Native attributes inherited from   110 | {inheritedProps.map(({ groupName }) => ( 111 | 116 | {groupName} 117 | 118 | ))} 119 | 120 | 121 | )} 122 | 123 |
124 |
125 | ); 126 | }; 127 | -------------------------------------------------------------------------------- /src/components/code-docs/VersionCard.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { ReactNode, useEffect, useState } from 'react'; 4 | import { css } from '@emotion/css'; 5 | import Button from '@leafygreen-ui/button'; 6 | import Card from '@leafygreen-ui/card'; 7 | // @ts-expect-error 8 | import ActivityFeed from '@leafygreen-ui/icon/dist/ActivityFeed'; 9 | import Modal from '@leafygreen-ui/modal'; 10 | import { spacing } from '@leafygreen-ui/tokens'; 11 | import { Subtitle } from '@leafygreen-ui/typography'; 12 | import { color } from '@leafygreen-ui/tokens'; 13 | import { findComponent, SubPath, getMappedComponentName } from '@/utils'; 14 | 15 | interface VersionCardProps { 16 | changelog: string | null; 17 | component: SubPath; 18 | } 19 | 20 | export const VersionCard = ({ changelog, component }: VersionCardProps) => { 21 | const [isModalOpen, setIsModalOpen] = useState(false); 22 | const [version, setVersion] = useState(null); 23 | 24 | useEffect(() => { 25 | setVersion(changelog?.split('h2')[1]?.replace(/[>/<]+/g, '') ?? null); 26 | }, [changelog]); 27 | 28 | const isPrivate = findComponent(component)?.isPrivate; 29 | const mappedComponentName = getMappedComponentName[component] ?? component; 30 | const privateChangelog = `https://github.com/10gen/leafygreen-ui-private/blob/main/packages/${mappedComponentName}/CHANGELOG.md`; 31 | 32 | return ( 33 | 34 | {version && ( 35 | 40 | Version {version} 41 | 42 | )} 43 | {changelog && ( 44 | <> 45 | 51 | 52 |
60 | 61 | 62 | )} 63 | {isPrivate && ( 64 | <> 65 | 72 | 73 | )} 74 | 75 | ); 76 | }; 77 | -------------------------------------------------------------------------------- /src/components/code-docs/index.ts: -------------------------------------------------------------------------------- 1 | export { InstallCard } from "./InstallCard"; 2 | export { PropsTable } from "./PropsTable"; 3 | export { VersionCard } from "./VersionCard"; 4 | 5 | export * from "./types"; 6 | export * from "./utils"; 7 | -------------------------------------------------------------------------------- /src/components/code-docs/types.ts: -------------------------------------------------------------------------------- 1 | export interface TSDocResponse { 2 | description: string; 3 | displayName: string; 4 | methods: Array; 5 | props: Record; 6 | tags: Record; 7 | } 8 | 9 | export interface PropTableState { 10 | name: string; 11 | props: { 12 | componentProps?: TSDocResponse["props"]; 13 | inheritedProps?: Array>; 14 | }; 15 | } 16 | 17 | /** PropGroup names that are inherited from elsewhere */ 18 | export const InheritablePropGroup = [ 19 | "HTMLAttributes", 20 | "DOMAttributes", 21 | "AriaAttributes", 22 | "SVGAttributes", 23 | "String", 24 | ] as const; 25 | 26 | export type InheritablePropGroup = keyof typeof InheritablePropGroup; 27 | -------------------------------------------------------------------------------- /src/components/code-docs/utils.ts: -------------------------------------------------------------------------------- 1 | import { pickBy } from "lodash"; 2 | import { TSDocResponse } from "./types"; 3 | 4 | const InheritablePropGroup = [ 5 | "HTMLAttributes", 6 | "DOMAttributes", 7 | "AriaAttributes", 8 | "SVGAttributes", 9 | "String", 10 | ] as const; 11 | 12 | export const isInheritableGroup = (_: never, key: any) => 13 | InheritablePropGroup.includes(key) || key.endsWith("HTMLAttributes"); 14 | 15 | function getInheritedProps(componentProps: TSDocResponse["props"] | undefined) { 16 | return Object.entries(pickBy(componentProps, isInheritableGroup)).map( 17 | ([groupName]: [string, TSDocResponse["props"]]) => ({ 18 | groupName, 19 | }) 20 | ); 21 | } 22 | 23 | export function mergeProps(componentProps: TSDocResponse["props"] | undefined) { 24 | if (!componentProps) { 25 | return {}; 26 | } 27 | 28 | let mergedProps = {}; 29 | 30 | Object.keys(componentProps).forEach((key) => { 31 | if (typeof componentProps[key] === "object") { 32 | mergedProps = { ...mergedProps, ...componentProps[key] }; 33 | } 34 | }); 35 | 36 | return { 37 | componentProps: mergedProps, 38 | inheritedProps: getInheritedProps(componentProps), 39 | }; 40 | } 41 | 42 | export function getHTMLAttributesLink(groupName: string) { 43 | switch (groupName) { 44 | case "SVGAttributes": 45 | return "https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute"; 46 | case "HTMLAttributes": 47 | return "https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes"; 48 | 49 | default: { 50 | let tag = groupName 51 | .slice(0, groupName.indexOf("HTMLAttributes")) 52 | .toLowerCase(); 53 | 54 | tag = tag == "anchor" ? "a" : tag; 55 | return `https://developer.mozilla.org/en-US/docs/Web/HTML/Element/${tag}`; 56 | } 57 | } 58 | } 59 | 60 | export function formatType(type: { raw?: string; name?: string; value?: any }) { 61 | if (!type) { 62 | return; 63 | } 64 | 65 | if (type.raw === "boolean" || type.raw === "ReactNode") { 66 | return type.raw; 67 | } 68 | 69 | if (type.value && Array.isArray(type.value)) { 70 | return type.value.map((obj) => obj.value).join(", "); 71 | } 72 | 73 | return type.name; 74 | } 75 | -------------------------------------------------------------------------------- /src/components/content-page/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | import { ContentstackRichText } from '@/components/content-stack'; 5 | 6 | import { css } from '@emotion/css'; 7 | 8 | import { ContentPage as ContentPageType } from '@/utils/ContentStack/types'; 9 | 10 | export function ContentPage({ 11 | contentPage, 12 | }: { 13 | contentPage?: ContentPageType; 14 | }) { 15 | return ( 16 |
21 | 22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/components/content-stack/AnnotatedImageBlock/AnnotatedImageBlock.tsx: -------------------------------------------------------------------------------- 1 | import { css } from "@emotion/css"; 2 | import { useDarkMode } from "@leafygreen-ui/leafygreen-provider"; 3 | import { color, spacing } from "@leafygreen-ui/tokens"; 4 | import { Body } from "@leafygreen-ui/typography"; 5 | import { ImageContainer } from "./ImageContainer"; 6 | import { StyledList } from "./StyledList"; 7 | import { StyledListItem } from "./StyledListItem"; 8 | 9 | export const AnnotatedImageBlock = ({ entry }: { entry: any }) => { 10 | const { theme } = useDarkMode(); 11 | 12 | return ( 13 |
19 | 20 | {entry.title} 21 | 22 | 23 | {entry.steps.map((obj: any, index: number) => ( 24 | 25 | 31 | 36 | {obj.step.title} 37 | {obj.step.description ? ":" : ""}  38 | 39 | 45 | {obj.step.description} 46 | 47 | 48 | 49 | ))} 50 | 51 |
52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /src/components/content-stack/AnnotatedImageBlock/ImageContainer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { css } from '@emotion/css'; 3 | import { borderRadius, color, spacing } from '@leafygreen-ui/tokens'; 4 | 5 | const bottomBorderHeight = 6; 6 | 7 | export const ImageContainer = ({ children }: { children: React.ReactNode }) => { 8 | return ( 9 |
img { 22 | max-width: 100%; 23 | } 24 | 25 | &::after { 26 | content: ''; 27 | position: absolute; 28 | height: ${spacing[800]}px; 29 | width: 100%; 30 | bottom: 0px; 31 | left: 0; 32 | border-radius: 0 0 16px 16px; 33 | background: linear-gradient( 34 | to bottom, 35 | transparent ${spacing[800] - bottomBorderHeight}px, 36 | ${color.light.background.secondary.default} 8px 37 | ); 38 | } 39 | `} 40 | > 41 | {children} 42 |
43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /src/components/content-stack/AnnotatedImageBlock/StyledList.tsx: -------------------------------------------------------------------------------- 1 | import { css } from "@emotion/css"; 2 | 3 | export function StyledList({ children }: { children: React.ReactNode }) { 4 | return ( 5 |
    11 | {children} 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/components/content-stack/AnnotatedImageBlock/StyledListItem.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css'; 2 | import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; 3 | import { color, spacing } from '@leafygreen-ui/tokens'; 4 | 5 | export const StyledListItem = ({ children }: { children: React.ReactNode }) => { 6 | const { theme } = useDarkMode(); 7 | 8 | return ( 9 |
  • p { 37 | font-size: 16px; 38 | line-height: 28px; 39 | } 40 | `} 41 | > 42 | {children} 43 |
  • 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /src/components/content-stack/AnnotatedImageBlock/index.tsx: -------------------------------------------------------------------------------- 1 | export { AnnotatedImageBlock } from "./AnnotatedImageBlock"; 2 | export { ImageContainer } from "./ImageContainer"; 3 | export { StyledList } from "./StyledList"; 4 | export { StyledListItem } from "./StyledListItem"; 5 | -------------------------------------------------------------------------------- /src/components/content-stack/BasicUsageBlock/BasicUsageBlock.tsx: -------------------------------------------------------------------------------- 1 | import { css } from "@emotion/css"; 2 | // @ts-expect-error 3 | import CheckmarkWithCircleIcon from "@leafygreen-ui/icon/dist/CheckmarkWithCircle"; 4 | // @ts-expect-error 5 | import XWithCircleIcon from "@leafygreen-ui/icon/dist/XWithCircle"; 6 | import { useDarkMode } from "@leafygreen-ui/leafygreen-provider"; 7 | import { palette } from "@leafygreen-ui/palette"; 8 | import { spacing } from "@leafygreen-ui/tokens"; 9 | import { Body } from "@leafygreen-ui/typography"; 10 | 11 | import { ContentstackRichText } from ".."; 12 | 13 | export const BasicUsageBlock = ({ entry }: { entry: any }) => { 14 | const { darkMode } = useDarkMode(); 15 | 16 | return ( 17 |
    23 |
    29 | 34 | 41 | Do 42 | 43 |
    44 | 45 |
    51 | 56 | 62 | Don't 63 | 64 |
    65 | 66 |
    67 | ); 68 | }; 69 | -------------------------------------------------------------------------------- /src/components/content-stack/BasicUsageBlock/index.tsx: -------------------------------------------------------------------------------- 1 | export { BasicUsageBlock } from "./BasicUsageBlock"; 2 | -------------------------------------------------------------------------------- /src/components/content-stack/ContentstackChildren.tsx: -------------------------------------------------------------------------------- 1 | import { CSNode } from './types'; 2 | import { ContentstackRichText } from '.'; 3 | 4 | /** 5 | * Renders a node's children 6 | */ 7 | export const ContentstackChildren = ({ 8 | nodeChildren, 9 | 10 | ...props 11 | }: { 12 | nodeChildren: CSNode['children']; 13 | [key: string]: any; 14 | }): JSX.Element => { 15 | return ( 16 | <> 17 | {nodeChildren.map(childNode => ( 18 | 23 | ))} 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/components/content-stack/ContentstackEntry.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { css, cx } from '@emotion/css'; 3 | import { getEntryById } from '@/utils/ContentStack/getContentstackResources'; 4 | import Badge from '@leafygreen-ui/badge'; 5 | import Button from '@leafygreen-ui/button'; 6 | import Callout, { Variant } from '@leafygreen-ui/callout'; 7 | import Card from '@leafygreen-ui/card'; 8 | import ExpandableCard from '@leafygreen-ui/expandable-card'; 9 | // @ts-expect-error 10 | import ArrowRight from '@leafygreen-ui/icon/dist/ArrowRight'; 11 | import { CardSkeleton } from '@leafygreen-ui/skeleton-loader'; 12 | import { spacing } from '@leafygreen-ui/tokens'; 13 | 14 | import { AnnotatedImageBlock } from './AnnotatedImageBlock'; 15 | import { BasicUsageBlock } from './BasicUsageBlock'; 16 | import { ExampleCardBlock } from './ExampleCardBlock'; 17 | import { HorizontalLayout } from './HorizontalLayout'; 18 | import { TwoColumnExampleCard } from './TwoColumnExampleCard'; 19 | import { BlockPropsMap, ContentTypeUID } from './types'; 20 | import { ContentstackRichText } from '.'; 21 | 22 | /** 23 | * An object that maps keys of each `contentTypeUid` 24 | * to new function component. 25 | * 26 | * The new component accepts the relevant interface as props 27 | * (as defined by the key `contentTypeUid` in {@link BlockPropsMap}) 28 | * and returns a JSX.Element. 29 | * 30 | * With this approach we can ensure that the value of `props` we're passing into 31 | * each component matches the expected interface 32 | */ 33 | const blockToElementMap: { 34 | [K in ContentTypeUID]: (props: BlockPropsMap[K]) => JSX.Element; 35 | } = { 36 | annotated_image_block: props => , 37 | badge_block: props => ( 38 | 44 | {props.title} 45 | 46 | ), 47 | basic_usage_block: props => , 48 | button_block: props => ( 49 | 59 | ), 60 | callout_block: props => ( 61 | 70 | 71 | 72 | ), 73 | card_block: props => ( 74 | 83 | 84 | 85 | ), 86 | example_card_block: props => , 87 | example_card_block_2_column_: props => , 88 | expandable_card_block: props => ( 89 | 94 | 95 | 96 | ), 97 | horizontal_layout: props => , 98 | } as const; 99 | 100 | const ContentstackEntry = ({ 101 | contentTypeUid, 102 | entryUid, 103 | }: { 104 | contentTypeUid: T; 105 | entryUid: string; 106 | }) => { 107 | // Note: not using `useMemo` here, since `getEntryById` is async 108 | const [entry, setEntry] = useState(); 109 | useEffect(() => { 110 | getEntryById(contentTypeUid, entryUid).then(res => { 111 | if (res) setEntry(res); 112 | }); 113 | }, [contentTypeUid, entryUid]); 114 | 115 | if (!entry) { 116 | return ; 117 | } 118 | 119 | return blockToElementMap[contentTypeUid](entry); 120 | }; 121 | 122 | export { ContentstackEntry }; 123 | -------------------------------------------------------------------------------- /src/components/content-stack/ContentstackImage.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { cx, css } from '@emotion/css'; 3 | import Image from 'next/image'; 4 | import { spacing } from '@leafygreen-ui/tokens'; 5 | import { CSNode } from './types'; 6 | 7 | // TODO: restrict the type of `content` more (should assert it has certain attrs) 8 | export const ContentstackImage = ({ 9 | content, 10 | ...props 11 | }: { 12 | content: CSNode; 13 | [key: string]: any; 14 | }) => { 15 | const attrs = content.attrs; 16 | 17 | return ( 18 |
    28 | {attrs['asset-name']} 35 |
    36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /src/components/content-stack/ContentstackReference.tsx: -------------------------------------------------------------------------------- 1 | import { ContentstackEntry } from "./ContentstackEntry"; 2 | import { ContentstackImage } from "./ContentstackImage"; 3 | import { CSNode } from "./types"; 4 | 5 | export const ContentstackReference = ({ 6 | content, 7 | ...props 8 | }: { 9 | content: CSNode; 10 | [key: string]: any; 11 | }) => { 12 | const { 13 | type, 14 | "content-type-uid": contentTypeUid, 15 | "entry-uid": entryUid, 16 | } = content.attrs; 17 | 18 | if (type === "asset") { 19 | return ; 20 | } else if (type === "entry") { 21 | return ( 22 | 27 | ); 28 | } else { 29 | console.warn(`Unknown reference type: ${type}.`); 30 | return <>Unknown reference type: {type}. ; 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /src/components/content-stack/ContentstackRichText.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { NotFound } from '@/components/global/NotFound'; 4 | import { nodeTypeToElementMap } from './componentMap'; 5 | import { ContentstackText } from './ContentstackText'; 6 | import { CSNode } from './types'; 7 | import { getCSNodeTextContent, isTextNode, nodeHasAssets } from './utils'; 8 | import { ErrorBoundary } from 'next/dist/client/components/error-boundary'; 9 | 10 | interface CSRichTextProps 11 | extends Omit { 12 | content?: CSNode; 13 | isNested?: boolean; 14 | [key: string]: any; 15 | } 16 | 17 | /** 18 | * Renders a ContentStack Node 19 | */ 20 | export const ContentStackRichText = ({ 21 | content, 22 | ...rest 23 | }: CSRichTextProps): JSX.Element => { 24 | return ( 25 | { 27 | console.error( 28 | 'The above error occurred mapping the following content to an element', 29 | content, 30 | ); 31 | return <>; 32 | }} 33 | > 34 | 35 | 36 | ); 37 | }; 38 | 39 | const ContentStackRichTextElement = ({ 40 | content, 41 | ...rest 42 | }: CSRichTextProps): JSX.Element => { 43 | if (!content) return ; 44 | 45 | if (isTextNode(content) && getCSNodeTextContent(content)) { 46 | return ; 47 | } else { 48 | const textContent = getCSNodeTextContent(content); 49 | 50 | if (textContent || nodeHasAssets(content)) { 51 | /* @ts-expect-error */ 52 | return nodeTypeToElementMap[content.type]?.(content, rest); 53 | } else { 54 | return <>; 55 | } 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /src/components/content-stack/ContentstackText.tsx: -------------------------------------------------------------------------------- 1 | import { css, cx } from '@emotion/css'; 2 | import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; 3 | import { color } from '@leafygreen-ui/tokens'; 4 | import { CSTextNode } from './types'; 5 | 6 | type CSRichTextProps = JSX.IntrinsicElements['span'] & { 7 | node: CSTextNode; 8 | }; 9 | 10 | export const ContentstackText = ({ 11 | node, 12 | className, 13 | ...rest 14 | }: CSRichTextProps) => { 15 | const { theme } = useDarkMode(); 16 | const Component = node.bold ? 'b' : 'span'; 17 | 18 | return ( 19 | 28 | {node.text} 29 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/components/content-stack/ExampleCardBlock/ExampleCardBlock.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css'; 2 | import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; 3 | import { color, spacing } from '@leafygreen-ui/tokens'; 4 | import { Body } from '@leafygreen-ui/typography'; 5 | import { ExampleCardBlockProps } from '../types'; 6 | import { 7 | BorderColors, 8 | IconColors, 9 | Icons, 10 | TextColors, 11 | VariantHeaders, 12 | } from './constants'; 13 | import { ImageContainer } from './ImageContainer'; 14 | 15 | export const ExampleCardBlock = ({ 16 | entry, 17 | }: { 18 | entry: ExampleCardBlockProps; 19 | }) => { 20 | const { theme } = useDarkMode(); 21 | const IconComponent = Icons[entry.variant]; 22 | 23 | return ( 24 |
    29 | 30 | {/** 31 | * TODO: fix this 32 | * Contentstack doesn't send image sizes, 33 | * so we can't appropriately size a Next/Image component. 34 | * 35 | * Also can't use `ContentstackImage`, since the `entry.image` object 36 | * is not a `CSNode` type 37 | */} 38 | {/* eslint-disable-next-line @next/next/no-img-element */} 39 | {entry.title} 40 | 41 |
    48 | 56 |
    57 | 63 | {VariantHeaders[entry.variant]} 64 | 65 | 71 | {entry.subtext} 72 | 73 |
    74 |
    75 |
    76 | ); 77 | }; 78 | -------------------------------------------------------------------------------- /src/components/content-stack/ExampleCardBlock/ImageContainer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { css } from '@emotion/css'; 3 | import { color } from '@leafygreen-ui/tokens'; 4 | 5 | export const ImageContainer = ({ 6 | children, 7 | gradient, 8 | }: { 9 | children: React.ReactNode; 10 | gradient: string; 11 | }) => { 12 | return ( 13 |
    img { 25 | max-width: 100%; 26 | } 27 | &::after { 28 | content: ''; 29 | position: absolute; 30 | height: 32px; 31 | width: 100%; 32 | bottom: 0px; 33 | left: 0; 34 | background: linear-gradient( 35 | to bottom, 36 | transparent 26px, 37 | ${gradient} 8px 38 | ); 39 | } 40 | `} 41 | > 42 | {children} 43 |
    44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /src/components/content-stack/ExampleCardBlock/constants.ts: -------------------------------------------------------------------------------- 1 | // @ts-expect-error 2 | import CheckmarkWithCircleIcon from '@leafygreen-ui/icon/dist/CheckmarkWithCircle'; 3 | // @ts-expect-error 4 | import ImportantWithCircleIcon from '@leafygreen-ui/icon/dist/ImportantWithCircle'; 5 | // @ts-expect-error 6 | import InfoWithCircleIcon from '@leafygreen-ui/icon/dist/InfoWithCircle'; 7 | // @ts-expect-error 8 | import XWithCircle from '@leafygreen-ui/icon/dist/XWithCircle'; 9 | import { palette } from '@leafygreen-ui/palette'; 10 | import { color } from '@leafygreen-ui/tokens'; 11 | import { Variant } from './types'; 12 | 13 | export const Icons: Record> = { 14 | [Variant.Info]: InfoWithCircleIcon, 15 | [Variant.Caution]: ImportantWithCircleIcon, 16 | [Variant.Dont]: XWithCircle, 17 | [Variant.Do]: CheckmarkWithCircleIcon, 18 | }; 19 | 20 | export const BorderColors = (theme: 'light' | 'dark') => { 21 | return { 22 | [Variant.Info]: theme === 'dark' ? palette.blue.light2 : palette.blue.base, 23 | [Variant.Caution]: 24 | theme === 'dark' ? palette.yellow.light2 : palette.yellow.base, 25 | [Variant.Dont]: color[theme].border.error.default, 26 | [Variant.Do]: color[theme].border.success.default, 27 | }; 28 | }; 29 | 30 | export const IconColors = (theme: 'dark' | 'light') => { 31 | return { 32 | [Variant.Info]: theme === 'dark' ? palette.blue.light2 : palette.blue.base, 33 | [Variant.Caution]: color[theme].icon.warning.default, 34 | [Variant.Dont]: color[theme].icon.error.default, 35 | [Variant.Do]: color[theme].icon.success.default, 36 | }; 37 | }; 38 | 39 | export const TextColors = (theme: 'dark' | 'light') => { 40 | if (theme === 'dark') { 41 | return { 42 | [Variant.Info]: palette.blue.light2, 43 | [Variant.Caution]: palette.yellow.light2, 44 | [Variant.Dont]: color[theme].icon.error.default, 45 | [Variant.Do]: palette.green.base, 46 | }; 47 | } else { 48 | return { 49 | [Variant.Info]: palette.blue.dark2, 50 | [Variant.Caution]: palette.yellow.dark2, 51 | [Variant.Dont]: color[theme].icon.error.default, 52 | [Variant.Do]: palette.green.dark2, 53 | }; 54 | } 55 | }; 56 | 57 | export const VariantHeaders: Record = { 58 | [Variant.Info]: 'Info', 59 | [Variant.Caution]: 'Use with caution', 60 | [Variant.Dont]: "Don't", 61 | [Variant.Do]: 'Do', 62 | }; 63 | -------------------------------------------------------------------------------- /src/components/content-stack/ExampleCardBlock/index.ts: -------------------------------------------------------------------------------- 1 | export { ExampleCardBlock } from "./ExampleCardBlock"; 2 | -------------------------------------------------------------------------------- /src/components/content-stack/ExampleCardBlock/types.ts: -------------------------------------------------------------------------------- 1 | // Below is taken from the Banner component 2 | 3 | const Variant = { 4 | Info: 'info', 5 | Caution: 'caution', 6 | Dont: 'dont', 7 | Do: 'do', 8 | } as const; 9 | 10 | type Variant = (typeof Variant)[keyof typeof Variant]; 11 | 12 | export { Variant }; 13 | -------------------------------------------------------------------------------- /src/components/content-stack/HeaderContent.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css'; 2 | import { kebabCase } from 'lodash'; 3 | import Link from 'next/link'; 4 | // @ts-expect-error 5 | import LinkIcon from '@leafygreen-ui/icon/dist/Link'; 6 | import { color, spacing } from '@leafygreen-ui/tokens'; 7 | import { ContentstackChildren } from './ContentstackChildren'; 8 | import { CSNode } from './types'; 9 | import { getCSNodeTextContent } from './utils'; 10 | import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; 11 | 12 | /** 13 | * Content of headers in rich text markup need to be wrapped in links and anchors for hashed links. 14 | */ 15 | export const HeaderContent = ({ node }: { node: CSNode }) => { 16 | const { theme } = useDarkMode(); 17 | const headerId = kebabCase(getCSNodeTextContent(node)); 18 | 19 | return ( 20 | 29 |
    56 | 57 |
    58 | 69 | 70 | ); 71 | }; 72 | -------------------------------------------------------------------------------- /src/components/content-stack/HorizontalLayout.tsx: -------------------------------------------------------------------------------- 1 | import { css, cx } from '@emotion/css'; 2 | import { Polymorph } from '@leafygreen-ui/polymorphic'; 3 | import { spacing } from '@leafygreen-ui/tokens'; 4 | import { ContentStackRichText } from './ContentstackRichText'; 5 | import { HorizontalLayoutBlockProps } from './types'; 6 | 7 | /// Note: can't use `css` from `@emotion/react` with `cx` 8 | const flexColumnStyles = css` 9 | display: flex; 10 | flex-direction: column; 11 | align-items: start; 12 | justify-content: flex-start; 13 | max-width: 100%; 14 | 15 | p + p { 16 | margin-top: ${spacing[200]}px; 17 | } 18 | `; 19 | 20 | export const HorizontalLayout = ({ 21 | column_1, 22 | column_2, 23 | vertical_align = 'start', 24 | flex_ratio, 25 | }: HorizontalLayoutBlockProps) => { 26 | const [flex1, flex2] = flex_ratio?.match(/[0-9]+/g) ?? [1, 1]; 27 | return ( 28 |
    35 | 51 | 52 | 67 |
    68 | ); 69 | }; 70 | -------------------------------------------------------------------------------- /src/components/content-stack/TwoColumnExampleCard.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css'; 2 | import { spacing } from '@leafygreen-ui/tokens'; 3 | import { ExampleCardBlock } from './ExampleCardBlock'; 4 | import { TwoColumnExampleCardBlockProps } from './types'; 5 | 6 | export const TwoColumnExampleCard = ({ 7 | entry, 8 | }: { 9 | entry: TwoColumnExampleCardBlockProps; 10 | }) => { 11 | return ( 12 |
    19 |
    29 | 37 |
    38 |
    48 | 56 |
    57 |
    58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /src/components/content-stack/index.ts: -------------------------------------------------------------------------------- 1 | export { ContentStackRichText as ContentstackRichText } from './ContentstackRichText'; 2 | -------------------------------------------------------------------------------- /src/components/content-stack/types.ts: -------------------------------------------------------------------------------- 1 | import { EntryEmbedable, Node, TextNode } from '@contentstack/utils'; 2 | 3 | import { BadgeProps } from '@leafygreen-ui/badge'; 4 | import { ButtonProps } from '@leafygreen-ui/button'; 5 | import { CalloutProps } from '@leafygreen-ui/callout'; 6 | 7 | type AnyNode = CSNode | CSTextNode; 8 | 9 | /** Contentstack is missing props in their type definitions */ 10 | export interface CSNode extends Node { 11 | uid: string; 12 | children: Array; 13 | } 14 | 15 | export interface CSTextNode extends TextNode, CSNode { 16 | children: Array; 17 | } 18 | 19 | export interface CSEntry extends EntryEmbedable {} 20 | 21 | // TODO: consider extending `@contentstack/utils.NodeType` 22 | /** 23 | * Map of all Contentstack block types. Blocks contain inline or block nodes. 24 | */ 25 | export enum CSNodeType { 26 | DOCUMENT = 'doc', 27 | FRAGMENT = 'fragment', 28 | PARAGRAPH = 'p', 29 | ANCHOR = 'a', 30 | REFERENCE = 'reference', 31 | HEADING_1 = 'h1', 32 | HEADING_2 = 'h2', 33 | HEADING_3 = 'h3', 34 | HEADING_4 = 'h4', 35 | HEADING_5 = 'h5', 36 | HEADING_6 = 'h6', 37 | ORDERED_LIST = 'ol', 38 | UNORDERED_LIST = 'ul', 39 | LIST_ITEM = 'li', 40 | ENTRY = 'entry', 41 | SPAN = 'span', 42 | HR = 'hr', 43 | QUOTE = 'blockquote', 44 | EMBEDDED_ENTRY = 'embedded-entry-block', 45 | EMBEDDED_ASSET = 'embedded-asset-block', 46 | TABLE = 'table', 47 | TABLE_HEAD = 'thead', 48 | TABLE_BODY = 'tbody', 49 | TABLE_ROW = 'tr', 50 | TABLE_CELL = 'td', 51 | TABLE_HEADER_CELL = 'th', 52 | } 53 | 54 | /** 55 | * Define custom Contentstack block interfaces 56 | */ 57 | 58 | interface CSImage { 59 | uid: string; 60 | content_type: string; 61 | url: string; 62 | file_size: string; 63 | file_name: string; 64 | } 65 | 66 | /** https://app.contentstack.com/#!/stack/bltee845ee8bbd3fe1a/content-type/annotated_image_block/content-type-builder?branch=main */ 67 | export interface AnnotatedImageBlockProps { 68 | title: string; 69 | image: CSImage; 70 | steps: Array; 71 | } 72 | interface AnnotatedImageBlockStep { 73 | title: string; 74 | description?: string; 75 | } 76 | 77 | /** https://app.contentstack.com/#/stack/bltee845ee8bbd3fe1a/content-type/badge_block/content-type-builder?branch=main */ 78 | export interface BadgeBlockProps { 79 | title: string; 80 | variant?: BadgeProps['variant']; 81 | } 82 | 83 | /** */ 84 | export interface BasicUsageBlockProps { 85 | title: string; 86 | dos?: CSNode; 87 | donts?: CSNode; 88 | } 89 | 90 | /** */ 91 | export interface ButtonBlockProps { 92 | button_block: string; 93 | url: string; 94 | content?: string; 95 | variant?: ButtonProps['variant']; 96 | link?: string; 97 | } 98 | 99 | /** */ 100 | export interface CalloutBock { 101 | url: string; 102 | title?: string; 103 | content?: CSNode; 104 | variant?: CalloutProps['variant']; 105 | } 106 | 107 | /** */ 108 | export interface CardBlockProps { 109 | url: string; 110 | title?: string; 111 | content?: CSNode; 112 | } 113 | 114 | /** */ 115 | export interface ExampleCardBlockProps { 116 | title: string; 117 | subtext?: string; 118 | variant: 'info' | 'caution' | 'do' | 'dont'; 119 | image: CSImage; 120 | } 121 | 122 | /** */ 123 | export interface ExpandableCardBlockProps { 124 | url: string; 125 | title: string; 126 | description?: string; 127 | content?: CSNode; 128 | } 129 | 130 | /** https://app.contentstack.com/#!/stack/bltee845ee8bbd3fe1a/content-type/horizontal_layout/content-type-builder?branch=main */ 131 | export interface HorizontalLayoutBlockProps { 132 | url: string; 133 | title: string; 134 | column_1: CSNode; // richText 135 | column_2: CSNode; // richText, 136 | vertical_align: 'start' | 'center' | 'end' | 'baseline'; 137 | flex_ratio: `${number}:${number}`; 138 | } 139 | 140 | export interface TwoColumnExampleCardBlockProps { 141 | title: string; 142 | column_1_subtext: string; 143 | column_1_variant: 'info' | 'caution' | 'do' | 'dont'; 144 | column_1_image: CSImage; 145 | column_2_subtext: string; 146 | column_2_variant: 'info' | 'caution' | 'do' | 'dont'; 147 | column_2_image: CSImage; 148 | } 149 | 150 | export interface BlockPropsMap { 151 | annotated_image_block: AnnotatedImageBlockProps; 152 | badge_block: BadgeBlockProps; 153 | basic_usage_block: BasicUsageBlockProps; 154 | button_block: ButtonBlockProps; 155 | callout_block: CalloutBock; 156 | card_block: CardBlockProps; 157 | example_card_block: ExampleCardBlockProps; 158 | expandable_card_block: ExpandableCardBlockProps; 159 | horizontal_layout: HorizontalLayoutBlockProps; 160 | example_card_block_2_column_: TwoColumnExampleCardBlockProps; 161 | } 162 | 163 | export type ContentTypeUID = keyof BlockPropsMap; 164 | -------------------------------------------------------------------------------- /src/components/content-stack/utils.ts: -------------------------------------------------------------------------------- 1 | import { CSNode, CSTextNode } from './types'; 2 | 3 | export const isTextNode = (node: CSNode): node is CSTextNode => { 4 | return node && Object.hasOwn(node, 'text'); 5 | }; 6 | 7 | /** 8 | * Loop through a node's children until we have all its text content 9 | */ 10 | export const getCSNodeTextContent = (node?: CSNode): string => { 11 | if (!node) return ''; 12 | 13 | if (isTextNode(node)) { 14 | return node.text; 15 | } else { 16 | return node.children 17 | .map(node => getCSNodeTextContent(node)) 18 | .join(' ') 19 | .trim(); 20 | } 21 | }; 22 | 23 | export const nodeHasAssets = (node: CSNode): boolean => { 24 | if (['asset', 'entry', 'reference'].includes(node.type)) { 25 | return true; 26 | } else { 27 | return node.children && node.children.some(child => nodeHasAssets(child)); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /src/components/foundations/Palette.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { MouseEventHandler, useState } from 'react'; 4 | import { css, cx } from '@emotion/css'; 5 | import { darken, lighten, readableColor, transparentize } from 'polished'; 6 | 7 | import { HTMLElementProps } from '@leafygreen-ui/lib'; 8 | import { palette } from '@leafygreen-ui/palette'; 9 | import { spacing } from '@leafygreen-ui/tokens'; 10 | import { H2 } from '@leafygreen-ui/typography'; 11 | 12 | type HueName = keyof typeof palette; 13 | const baseHues: Array = ['white', 'black', 'transparent']; 14 | 15 | const isBaseHue = (hue: HueName): hue is 'white' | 'black' | 'transparent' => { 16 | return baseHues.includes(hue); 17 | }; 18 | 19 | const ShadeNames = [ 20 | 'dark4', 21 | 'dark3', 22 | 'dark2', 23 | 'dark1', 24 | 'base', 25 | 'light1', 26 | 'light2', 27 | 'light3', 28 | ] as const; 29 | type ShadeName = (typeof ShadeNames)[number]; 30 | 31 | interface ColorBlockProps extends HTMLElementProps<'div'> { 32 | hue: HueName; 33 | name: string; 34 | shade?: ShadeName; 35 | } 36 | 37 | const BLOCK_WIDTH = 88; 38 | 39 | const copiedOverlayStyle = css` 40 | position: absolute; 41 | top: 0; 42 | left: 0; 43 | width: 100%; 44 | height: 100%; 45 | display: flex; 46 | align-items: center; 47 | justify-content: center; 48 | font-family: 'Euclid Circular A', 'Helvetica Neue', Helvetica, Arial, 49 | sans-serif; 50 | border-radius: inherit; 51 | `; 52 | 53 | const colorBlockWrapper = css` 54 | display: inline-block; 55 | position: relative; 56 | width: ${BLOCK_WIDTH}px; 57 | padding-bottom: 16px; 58 | `; 59 | 60 | const colorBlock = css` 61 | position: relative; 62 | outline: none; 63 | border: none; 64 | border-top-color: transparent; 65 | width: 100%; 66 | padding-bottom: 100%; 67 | border-radius: 8px; 68 | cursor: pointer; 69 | vertical-align: top; 70 | 71 | &:focus-visible { 72 | box-shadow: 0 0 0 2px white, 0 0 0 4px #0498ec; 73 | } 74 | `; 75 | 76 | const hexLabelStyle = css` 77 | width: calc(100% - 1em); 78 | position: absolute; 79 | left: 50%; 80 | margin: auto; 81 | font-size: 13px; 82 | text-align: center; 83 | padding: 3px 0.3rem; 84 | border-radius: 4px; 85 | transform: translate(-50%, -125%); 86 | pointer-events: none; 87 | `; 88 | 89 | const nameLabelStyle = css` 90 | text-align: center; 91 | color: ${palette.gray.dark1}; 92 | margin: auto; 93 | padding-block: 0.3em; 94 | `; 95 | 96 | const colorRowStyle = css` 97 | grid-template-columns: repeat(${ShadeNames.length}, ${BLOCK_WIDTH}px); 98 | display: grid; 99 | gap: 24px; 100 | `; 101 | 102 | function ColorBlock({ hue, shade, ...rest }: ColorBlockProps) { 103 | const [wasCopied, setWasCopied] = useState(); 104 | const name = `${hue} ${shade ?? ''}`; 105 | 106 | let color: string; 107 | 108 | if (isBaseHue(hue)) { 109 | color = palette[hue]; 110 | } else { 111 | shade = shade ?? 'base'; 112 | color = (palette[hue] as Record)[shade]; 113 | } 114 | 115 | const colorBlockWrapperDynamic = css` 116 | grid-column: ${shade ? ShadeNames.indexOf(shade) + 1 : '0'}; 117 | `; 118 | 119 | const colorBlockColor = css` 120 | transition: all 0.3s ease; 121 | background-color: ${color}; 122 | box-shadow: 0 8px 6px -8px ${transparentize(0.7, darken(0.2, color))}, 123 | 0 2px 3px ${transparentize(0.8, darken(0.5, color))}; 124 | 125 | &:hover { 126 | transform: scale(1.05); 127 | box-shadow: 0 8px 6px -8px ${transparentize(0.7, darken(0.4, color))}, 128 | 0 2px 3px ${transparentize(0.5, darken(0.3, color))}; 129 | } 130 | `; 131 | 132 | const hexLabelColor = css` 133 | color: ${readableColor(lighten(0.2, color))}; 134 | background-color: ${lighten(0.2, color)}; 135 | `; 136 | 137 | const handleClick: MouseEventHandler = () => { 138 | navigator.clipboard.writeText(color); 139 | setWasCopied(true); 140 | setTimeout(() => { 141 | setWasCopied(false); 142 | }, 2000); 143 | }; 144 | 145 | return ( 146 |
    147 | 157 | {!wasCopied && ( 158 |
    {color}
    159 | )} 160 |
    {name}
    161 |
    162 | ); 163 | } 164 | 165 | export function Palette() { 166 | const allColors = Object.keys(palette); 167 | const hues = (allColors as Array).slice( 168 | baseHues.length, 169 | allColors.length, 170 | ); // remove black and white 171 | 172 | return ( 173 |
    174 |

    179 | Palette Hex Map 180 |

    181 |
    182 | 183 | 184 | 185 |
    186 | {hues.map(hue => { 187 | const hueValues = palette[hue]; 188 | 189 | return ( 190 |
    191 | {(Object.keys(hueValues) as Array).map( 192 | shade => ( 193 | 199 | ), 200 | )} 201 |
    202 | ); 203 | })} 204 |
    205 | ); 206 | } 207 | -------------------------------------------------------------------------------- /src/components/foundations/index.ts: -------------------------------------------------------------------------------- 1 | export { Palette } from './Palette'; 2 | -------------------------------------------------------------------------------- /src/components/global/DarkModeToggle.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | 5 | import Icon from '@leafygreen-ui/icon'; 6 | import IconButton from '@leafygreen-ui/icon-button/'; 7 | import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; 8 | 9 | export function DarkModeToggle({}: {}) { 10 | const { darkMode, setDarkMode } = useDarkMode(); 11 | return ( 12 | setDarkMode(!darkMode)} 15 | darkMode={darkMode} 16 | > 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/components/global/Footer.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { css, cx } from "@emotion/css"; 4 | 5 | import { useDarkMode } from "@leafygreen-ui/leafygreen-provider"; 6 | import { MongoDBLogo, SupportedColors } from '@leafygreen-ui/logo'; 7 | import { color, fontWeights, Mode, spacing, typeScales } from "@leafygreen-ui/tokens"; 8 | import { Body, Link } from "@leafygreen-ui/typography"; 9 | 10 | const MONGODB_URL = "https://www.mongodb.com"; 11 | 12 | const FOOTER_LINKS = [ 13 | { href: `${MONGODB_URL}/blog/post/meet-our-product-design-team-part-1`, text: "About design at MongoDB" }, 14 | { href: `${MONGODB_URL}/blog`, text: "Blog" }, 15 | { href: `${MONGODB_URL}/blog/channel/events`, text: "Events" }, 16 | { href: "https://github.com/mongodb/leafygreen-ui", text: "GitHub" }, 17 | { href: `${MONGODB_URL}/careers`, text: "Careers" }, 18 | ]; 19 | 20 | const footerContainerStyle = (theme: Mode) => css` 21 | display: flex; 22 | width: 100%; 23 | padding: ${spacing[1600]}px 0; 24 | `; 25 | 26 | const linksContainer = css` 27 | display: flex; 28 | flex-direction: column; 29 | gap: ${spacing[400]}px; 30 | padding-top: 5px; // Used for vertical alignment with logo 31 | padding-left: ${spacing[1200]}px; 32 | padding-right: ${spacing[1200]}px; 33 | padding-bottom: ${spacing[600]}px; 34 | `; 35 | 36 | const linkStyles = (theme: Mode) => css` 37 | margin: 0; 38 | font-size: ${typeScales.body1.fontSize}px; 39 | font-weight: ${fontWeights.regular}; 40 | line-height: ${typeScales.body1.lineHeight}px; 41 | text-decoration: none; 42 | color: ${color[theme].text.primary.default}; 43 | 44 | &:hover { 45 | text-decoration: none; 46 | color: ${color[theme].text.primary.hover}; 47 | } 48 | `; 49 | 50 | const trademarkStyle = (theme: Mode) => css` 51 | color: ${color[theme].text.secondary.default}; 52 | `; 53 | 54 | export function Footer() { 55 | const { theme } = useDarkMode(); 56 | 57 | return ( 58 |
    62 | 63 | 69 |
    74 | {FOOTER_LINKS.map(({ href, text }) => ( 75 | 83 | {text} 84 | 85 | ))} 86 | 87 | © {new Date().getFullYear()} MongoDB, Inc. 88 | 89 |
    90 |
    91 | ); 92 | } 93 | -------------------------------------------------------------------------------- /src/components/global/LogIn.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { css } from '@emotion/css'; 4 | import Button from '@leafygreen-ui/button'; 5 | import { login } from '@/auth/login'; 6 | 7 | export function LogIn() { 8 | return ( 9 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/components/global/NotFound.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | import React from "react"; 5 | 6 | import Button from "@leafygreen-ui/button"; 7 | import { BasicEmptyState } from "@leafygreen-ui/empty-state"; 8 | // @ts-expect-error 9 | import ArrowLeftIcon from "@leafygreen-ui/icon/dist/ArrowLeft"; 10 | import { NotFound as NotFoundGraphic } from "@/components/glyphs"; 11 | 12 | export function NotFound() { 13 | const router = useRouter(); 14 | 15 | return ( 16 |
    17 | } 21 | primaryButton={ 22 | 28 | } 29 | /> 30 |
    31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/components/global/PrivateContentWall.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | 5 | import Button from '@leafygreen-ui/button'; 6 | import { BasicEmptyState } from '@leafygreen-ui/empty-state'; 7 | // @ts-expect-error 8 | import LogInIcon from '@leafygreen-ui/icon/dist/LogIn'; 9 | 10 | import { login } from '@/auth'; 11 | 12 | import { Security } from '@/components/glyphs'; 13 | 14 | export function PrivateContentWall() { 15 | return ( 16 | login()} 23 | leftGlyph={} 24 | > 25 | Log In 26 | 27 | } 28 | graphic={} 29 | /> 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/components/global/RootStyleRegistry.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { CacheProvider } from "@emotion/react"; 4 | import createCache from "@emotion/cache"; 5 | import { useServerInsertedHTML } from "next/navigation"; 6 | import { useState } from "react"; 7 | 8 | export function RootStyleRegistry({ children }: { children: JSX.Element }) { 9 | const [{ cache, flush }] = useState(() => { 10 | const cache = createCache({ key: "my" }); 11 | cache.compat = true; 12 | const prevInsert = cache.insert; 13 | let inserted: string[] = []; 14 | cache.insert = (...args) => { 15 | const serialized = args[1]; 16 | if (cache.inserted[serialized.name] === undefined) { 17 | inserted.push(serialized.name); 18 | } 19 | return prevInsert(...args); 20 | }; 21 | const flush = () => { 22 | const prevInserted = inserted; 23 | inserted = []; 24 | return prevInserted; 25 | }; 26 | return { cache, flush }; 27 | }); 28 | 29 | useServerInsertedHTML(() => { 30 | const names = flush(); 31 | if (names.length === 0) return null; 32 | let styles = ""; 33 | for (const name of names) { 34 | styles += cache.inserted[name]; 35 | } 36 | return ( 37 |