" - ? "
" + type + description.slice(3) - : type + description) + - "
diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 0983094d3e..a64f745516 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -22,3 +22,19 @@ jobs: - name: Run Prettier Check run: pnpm format:check + + # per the docs: "caching browser binaries is not recommended, + # since the amount of time it takes to restore the cache is + # comparable to the time it takes to download the binaries" + - name: Install Playwright Browsers + run: npx playwright install --with-deps + + - name: Run Playwright tests + run: npx playwright test + + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index 2d422a0b51..2c2af0a81e 100644 --- a/.gitignore +++ b/.gitignore @@ -63,3 +63,5 @@ public/sitemap.xml out/ tsconfig.tsbuildinfo + +playwright-report/ diff --git a/package.json b/package.json index f8fad50692..19372b5c1f 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "type": "module", "repository": "graphql/graphql.github.io website", "private": true, - "packageManager": "pnpm@9.15.9", + "packageManager": "pnpm@10.15.0", "scripts": { "analyze": "ANALYZE=true next build", "build": "next build && next-image-export-optimizer", @@ -22,9 +22,17 @@ "validate:snippets": "node scripts/validate-snippets.js" }, "dependencies": { + "@codemirror/autocomplete": "^6.18.6", + "@codemirror/commands": "^6.3.3", + "@codemirror/lang-json": "^6.0.1", + "@codemirror/language": "^6.10.0", + "@codemirror/lint": "^6.8.5", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.24.0", "@graphql-tools/schema": "10.0.25", "@headlessui/react": "^2.2.4", "@igorkowalczyk/is-browser": "^5.1.0", + "@lezer/highlight": "^1.2.1", "@next/bundle-analyzer": "^15.4.5", "@radix-ui/react-radio-group": "^1.2.2", "@sparticuz/chromium": "^138.0.2", @@ -34,8 +42,7 @@ "autoprefixer": "^10.4.20", "calendar-link": "^2.10.0", "clsx": "^2.1.1", - "codemirror": "^5.65.19", - "codemirror-graphql": "1.3.2", + "cm6-graphql": "^0.2.1", "date-fns": "^2.30.0", "fast-glob": "^3.3.3", "github-slugger": "2.0.0", @@ -46,9 +53,8 @@ "leaflet": "^1.9.4", "lucide-react": "^0.469.0", "markdown-to-jsx": "^7.7.2", - "marked": "5.1.2", "motion": "^12.11.0", - "next": "^14.2.22", + "next": "^14.2.32", "next-image-export-optimizer": "^1.18.0", "next-query-params": "^5.0.1", "next-sitemap": "^4.2.3", @@ -88,7 +94,7 @@ "@types/codemirror": "5.60.16", "@types/hast": "3.0.4", "@types/node": "^22.10.5", - "@types/react": "^18.3.18", + "@types/react": "^18.3.23", "@types/rss": "0.0.32", "@types/string-similarity": "^4.0.2", "@typescript-eslint/eslint-plugin": "7.18.0", @@ -106,7 +112,7 @@ "remark-lint-first-heading-level": "3.1.2", "remark-lint-heading-increment": "3.1.2", "tsx": "^4.19.4", - "typescript": "^5.8.3" + "typescript": "^5.9.2" }, "browserslist": [ "chrome >0 and last 2.5 years", @@ -122,6 +128,9 @@ "nextra": "patches/nextra.patch", "nextra-theme-docs": "patches/nextra-theme-docs.patch", "mermaid-isomorphic": "patches/mermaid-isomorphic.patch" - } + }, + "onlyBuiltDependencies": [ + "esbuild" + ] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8d0939e77a..58f507aaba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,19 +6,40 @@ settings: patchedDependencies: mermaid-isomorphic: - hash: tt5abewtk4kmqtpqfe2f44hnpa + hash: fccadc7038719bcf9dc12a573655719edaf7ea8246bd144c660191d05b38c637 path: patches/mermaid-isomorphic.patch nextra: - hash: 6ssvmycplmexou7j5jtwzrnhmy + hash: ea67bba83484b83abd3764c3d267a635474008733ff1efe84d2a16d02fddc42d path: patches/nextra.patch nextra-theme-docs: - hash: 3vafyhtna4g6ghudqadsxldexi + hash: db05bf9d86002253cd072795bad24938011273dded5221e22840e1c2e439b3e5 path: patches/nextra-theme-docs.patch importers: .: dependencies: + '@codemirror/autocomplete': + specifier: ^6.18.6 + version: 6.18.6 + '@codemirror/commands': + specifier: ^6.3.3 + version: 6.8.1 + '@codemirror/lang-json': + specifier: ^6.0.1 + version: 6.0.2 + '@codemirror/language': + specifier: ^6.10.0 + version: 6.11.3 + '@codemirror/lint': + specifier: ^6.8.5 + version: 6.8.5 + '@codemirror/state': + specifier: ^6.4.0 + version: 6.5.2 + '@codemirror/view': + specifier: ^6.24.0 + version: 6.38.1 '@graphql-tools/schema': specifier: 10.0.25 version: 10.0.25(graphql@16.10.0) @@ -28,6 +49,9 @@ importers: '@igorkowalczyk/is-browser': specifier: ^5.1.0 version: 5.1.0(tailwindcss@3.4.17) + '@lezer/highlight': + specifier: ^1.2.1 + version: 1.2.1 '@next/bundle-analyzer': specifier: ^15.4.5 version: 15.5.0 @@ -55,12 +79,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 - codemirror: - specifier: ^5.65.19 - version: 5.65.20 - codemirror-graphql: - specifier: 1.3.2 - version: 1.3.2(@codemirror/language@0.20.2)(codemirror@5.65.20)(graphql@16.10.0) + cm6-graphql: + specifier: ^0.2.1 + version: 0.2.1(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.3)(@codemirror/lint@6.8.5)(@codemirror/state@6.5.2)(@codemirror/view@6.38.1)(@lezer/highlight@1.2.1)(graphql@16.10.0) date-fns: specifier: ^2.30.0 version: 2.30.0 @@ -91,14 +112,11 @@ importers: markdown-to-jsx: specifier: ^7.7.2 version: 7.7.13(react@18.3.1) - marked: - specifier: 5.1.2 - version: 5.1.2 motion: specifier: ^12.11.0 version: 12.23.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next: - specifier: ^14.2.22 + specifier: ^14.2.32 version: 14.2.32(@babel/core@7.28.3)(@playwright/test@1.55.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-image-export-optimizer: specifier: ^1.18.0 @@ -114,10 +132,10 @@ importers: version: 3.0.1(less-loader@12.3.0(less@4.4.1))(less@4.4.1)(next@14.2.32(@babel/core@7.28.3)(@playwright/test@1.55.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) nextra: specifier: 3.3.1 - version: 3.3.1(patch_hash=6ssvmycplmexou7j5jtwzrnhmy)(@types/react@18.3.24)(acorn@8.15.0)(next@14.2.32(@babel/core@7.28.3)(@playwright/test@1.55.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + version: 3.3.1(patch_hash=ea67bba83484b83abd3764c3d267a635474008733ff1efe84d2a16d02fddc42d)(@types/react@18.3.24)(acorn@8.15.0)(next@14.2.32(@babel/core@7.28.3)(@playwright/test@1.55.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) nextra-theme-docs: specifier: 3.3.1 - version: 3.3.1(patch_hash=3vafyhtna4g6ghudqadsxldexi)(next@14.2.32(@babel/core@7.28.3)(@playwright/test@1.55.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@3.3.1(patch_hash=6ssvmycplmexou7j5jtwzrnhmy)(@types/react@18.3.24)(acorn@8.15.0)(next@14.2.32(@babel/core@7.28.3)(@playwright/test@1.55.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 3.3.1(patch_hash=db05bf9d86002253cd072795bad24938011273dded5221e22840e1c2e439b3e5)(next@14.2.32(@babel/core@7.28.3)(@playwright/test@1.55.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@3.3.1(patch_hash=ea67bba83484b83abd3764c3d267a635474008733ff1efe84d2a16d02fddc42d)(@types/react@18.3.24)(acorn@8.15.0)(next@14.2.32(@babel/core@7.28.3)(@playwright/test@1.55.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) numbro: specifier: 2.5.0 version: 2.5.0 @@ -181,10 +199,6 @@ importers: use-query-params: specifier: ^2.2.1 version: 2.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - optionalDependencies: - playwright: - specifier: ^1.54.2 - version: 1.55.0 devDependencies: '@graphql-eslint/eslint-plugin': specifier: 4.4.0 @@ -208,7 +222,7 @@ importers: specifier: ^22.10.5 version: 22.17.2 '@types/react': - specifier: ^18.3.18 + specifier: ^18.3.23 version: 18.3.24 '@types/rss': specifier: 0.0.32 @@ -262,8 +276,12 @@ importers: specifier: ^4.19.4 version: 4.20.4 typescript: - specifier: ^5.8.3 + specifier: ^5.9.2 version: 5.9.2 + optionalDependencies: + playwright: + specifier: ^1.54.2 + version: 1.55.0 packages: @@ -867,14 +885,26 @@ packages: '@chevrotain/utils@11.0.3': resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==} - '@codemirror/language@0.20.2': - resolution: {integrity: sha512-WB3Bnuusw0xhVvhBocieYKwJm04SOk5bPoOEYksVHKHcGHFOaYaw+eZVxR4gIqMMcGzOIUil0FsCmFk8yrhHpw==} + '@codemirror/autocomplete@6.18.6': + resolution: {integrity: sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==} + + '@codemirror/commands@6.8.1': + resolution: {integrity: sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==} - '@codemirror/state@0.20.1': - resolution: {integrity: sha512-ms0tlV5A02OK0pFvTtSUGMLkoarzh1F8mr6jy1cD7ucSC2X/VLHtQCxfhdSEGqTYlQF2hoZtmLv+amqhdgbwjQ==} + '@codemirror/lang-json@6.0.2': + resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==} - '@codemirror/view@0.20.7': - resolution: {integrity: sha512-pqEPCb9QFTOtHgAH5XU/oVy9UR/Anj6r+tG5CRmkNVcqSKEPmBU05WtN/jxJCFZBXf6HumzWC9ydE4qstO3TxQ==} + '@codemirror/language@6.11.3': + resolution: {integrity: sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==} + + '@codemirror/lint@6.8.5': + resolution: {integrity: sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==} + + '@codemirror/state@6.5.2': + resolution: {integrity: sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==} + + '@codemirror/view@6.38.1': + resolution: {integrity: sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ==} '@corex/deepmerge@4.0.43': resolution: {integrity: sha512-N8uEMrMPL0cu/bdboEWpQYb/0i2K5Qn8eCsxzOmxSggJbbQte7ljMRoXm917AbntqTGOzdTu+vP3KOOzoC70HQ==} @@ -1394,14 +1424,20 @@ packages: '@jridgewell/trace-mapping@0.3.30': resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} - '@lezer/common@0.16.1': - resolution: {integrity: sha512-qPmG7YTZ6lATyTOAWf8vXE+iRrt1NJd4cm2nJHK+v7X9TsOF6+HtuU/ctaZy2RCrluxDb89hI6KWQ5LfQGQWuA==} + '@lezer/common@1.2.3': + resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==} + + '@lezer/highlight@1.2.1': + resolution: {integrity: sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==} - '@lezer/highlight@0.16.0': - resolution: {integrity: sha512-iE5f4flHlJ1g1clOStvXNLbORJoiW4Kytso6ubfYzHnaNo/eo5SKhxs4wv/rtvwZQeZrK3we8S9SyA7OGOoRKQ==} + '@lezer/json@1.0.3': + resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==} - '@lezer/lr@0.16.3': - resolution: {integrity: sha512-pau7um4eAw94BEuuShUIeQDTf3k4Wt6oIUOYxMmkZgDHdqtIcxWND4LRxi8nI9KuT4I1bXQv67BCapkxt7Ywqw==} + '@lezer/lr@1.4.2': + resolution: {integrity: sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==} + + '@marijn/find-cluster-break@1.0.2': + resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} '@mdx-js/mdx@3.1.0': resolution: {integrity: sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw==} @@ -2537,15 +2573,16 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} - codemirror-graphql@1.3.2: - resolution: {integrity: sha512-glwFsEVlH5TvxjSKGymZ1sNy37f3Mes58CB4fXOd0zy9+JzDL08Wti1b5ycy4vFZYghMDK1/Or/zRSjMAGtC2w==} + cm6-graphql@0.2.1: + resolution: {integrity: sha512-FIAFHn6qyiXChTz3Pml0NgTM8LyyXs8QfP2iPG7MLA8Xi83WuVlkGG5PDs+DDeEVabHkLIZmcyNngQlxLXKk6A==} peerDependencies: - '@codemirror/language': ^0.20.0 - codemirror: ^5.65.3 - graphql: ^15.5.0 || ^16.0.0 - - codemirror@5.65.20: - resolution: {integrity: sha512-i5dLDDxwkFCbhjvL2pNjShsojoL3XHyDwsGv1jqETUoW+lzpBKKqNTUWgQwVAOa0tUm4BwekT455ujafi8payA==} + '@codemirror/autocomplete': ^6.0.0 + '@codemirror/language': ^6.0.0 + '@codemirror/lint': ^6.0.0 + '@codemirror/state': ^6.0.0 + '@codemirror/view': ^6.0.0 + '@lezer/highlight': ^1.0.0 + graphql: ^15.5.0 || ^16.0.0 || ^17.0.0 codsen-utils@1.6.8: resolution: {integrity: sha512-8GESpsIuEsprHKuTWen1KO221gxyA+PDUXSZq0ywo5G5IcqmPtqTOw5BAljQUz2GdOe8TcWcH896qYb8gdxHFA==} @@ -2630,6 +2667,9 @@ packages: typescript: optional: true + crelt@1.0.6: + resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + cross-inspect@1.0.1: resolution: {integrity: sha512-Pcw1JTvZLSJH83iiGWt6fRcT+BjZlCDRVwYLbUcHzv/CRpB7r0MlSrGbIyQvVSNyGnbt7G4AXuyCiDR3POvZ1A==} engines: {node: '>=16.0.0'} @@ -4006,11 +4046,6 @@ packages: engines: {node: '>= 20'} hasBin: true - marked@5.1.2: - resolution: {integrity: sha512-ahRPGXJpjMjwSOlBoTMZAK7ATXkli5qCPxZ21TG44rx1KEo44bii4ekgTDQPNRQ4Kh7JMb9Ub1PVk1NxRSsorg==} - engines: {node: '>= 16'} - hasBin: true - math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -6606,20 +6641,48 @@ snapshots: '@chevrotain/utils@11.0.3': {} - '@codemirror/language@0.20.2': + '@codemirror/autocomplete@6.18.6': dependencies: - '@codemirror/state': 0.20.1 - '@codemirror/view': 0.20.7 - '@lezer/common': 0.16.1 - '@lezer/highlight': 0.16.0 - '@lezer/lr': 0.16.3 + '@codemirror/language': 6.11.3 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.38.1 + '@lezer/common': 1.2.3 + + '@codemirror/commands@6.8.1': + dependencies: + '@codemirror/language': 6.11.3 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.38.1 + '@lezer/common': 1.2.3 + + '@codemirror/lang-json@6.0.2': + dependencies: + '@codemirror/language': 6.11.3 + '@lezer/json': 1.0.3 + + '@codemirror/language@6.11.3': + dependencies: + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.38.1 + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.1 + '@lezer/lr': 1.4.2 style-mod: 4.1.2 - '@codemirror/state@0.20.1': {} + '@codemirror/lint@6.8.5': + dependencies: + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.38.1 + crelt: 1.0.6 - '@codemirror/view@0.20.7': + '@codemirror/state@6.5.2': dependencies: - '@codemirror/state': 0.20.1 + '@marijn/find-cluster-break': 1.0.2 + + '@codemirror/view@6.38.1': + dependencies: + '@codemirror/state': 6.5.2 + crelt: 1.0.6 style-mod: 4.1.2 w3c-keyname: 2.2.8 @@ -7153,15 +7216,23 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@lezer/common@0.16.1': {} + '@lezer/common@1.2.3': {} + + '@lezer/highlight@1.2.1': + dependencies: + '@lezer/common': 1.2.3 - '@lezer/highlight@0.16.0': + '@lezer/json@1.0.3': dependencies: - '@lezer/common': 0.16.1 + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.1 + '@lezer/lr': 1.4.2 - '@lezer/lr@0.16.3': + '@lezer/lr@1.4.2': dependencies: - '@lezer/common': 0.16.1 + '@lezer/common': 1.2.3 + + '@marijn/find-cluster-break@1.0.2': {} '@mdx-js/mdx@3.1.0(acorn@8.15.0)': dependencies: @@ -8394,15 +8465,17 @@ snapshots: clsx@2.1.1: {} - codemirror-graphql@1.3.2(@codemirror/language@0.20.2)(codemirror@5.65.20)(graphql@16.10.0): + cm6-graphql@0.2.1(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.3)(@codemirror/lint@6.8.5)(@codemirror/state@6.5.2)(@codemirror/view@6.38.1)(@lezer/highlight@1.2.1)(graphql@16.10.0): dependencies: - '@codemirror/language': 0.20.2 - codemirror: 5.65.20 + '@codemirror/autocomplete': 6.18.6 + '@codemirror/language': 6.11.3 + '@codemirror/lint': 6.8.5 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.38.1 + '@lezer/highlight': 1.2.1 graphql: 16.10.0 graphql-language-service: 5.5.0(graphql@16.10.0) - codemirror@5.65.20: {} - codsen-utils@1.6.8: dependencies: rfdc: 1.4.1 @@ -8483,6 +8556,8 @@ snapshots: optionalDependencies: typescript: 5.9.2 + crelt@1.0.6: {} + cross-inspect@1.0.1: dependencies: tslib: 2.8.1 @@ -10097,8 +10172,6 @@ snapshots: marked@16.2.0: {} - marked@5.1.2: {} - math-intrinsics@1.1.0: {} mathjax-full@3.2.2: @@ -10302,7 +10375,7 @@ snapshots: merge2@1.4.1: {} - mermaid-isomorphic@3.0.4(patch_hash=tt5abewtk4kmqtpqfe2f44hnpa)(playwright@1.55.0): + mermaid-isomorphic@3.0.4(patch_hash=fccadc7038719bcf9dc12a573655719edaf7ea8246bd144c660191d05b38c637)(playwright@1.55.0): dependencies: '@fortawesome/fontawesome-free': 6.7.2 mermaid: 11.10.0 @@ -10765,7 +10838,7 @@ snapshots: - '@babel/core' - babel-plugin-macros - nextra-theme-docs@3.3.1(patch_hash=3vafyhtna4g6ghudqadsxldexi)(next@14.2.32(@babel/core@7.28.3)(@playwright/test@1.55.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@3.3.1(patch_hash=6ssvmycplmexou7j5jtwzrnhmy)(@types/react@18.3.24)(acorn@8.15.0)(next@14.2.32(@babel/core@7.28.3)(@playwright/test@1.55.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + nextra-theme-docs@3.3.1(patch_hash=db05bf9d86002253cd072795bad24938011273dded5221e22840e1c2e439b3e5)(next@14.2.32(@babel/core@7.28.3)(@playwright/test@1.55.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@3.3.1(patch_hash=ea67bba83484b83abd3764c3d267a635474008733ff1efe84d2a16d02fddc42d)(@types/react@18.3.24)(acorn@8.15.0)(next@14.2.32(@babel/core@7.28.3)(@playwright/test@1.55.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@headlessui/react': 2.2.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) clsx: 2.1.1 @@ -10773,13 +10846,13 @@ snapshots: flexsearch: 0.7.43 next: 14.2.32(@babel/core@7.28.3)(@playwright/test@1.55.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-themes: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - nextra: 3.3.1(patch_hash=6ssvmycplmexou7j5jtwzrnhmy)(@types/react@18.3.24)(acorn@8.15.0)(next@14.2.32(@babel/core@7.28.3)(@playwright/test@1.55.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + nextra: 3.3.1(patch_hash=ea67bba83484b83abd3764c3d267a635474008733ff1efe84d2a16d02fddc42d)(@types/react@18.3.24)(acorn@8.15.0)(next@14.2.32(@babel/core@7.28.3)(@playwright/test@1.55.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) scroll-into-view-if-needed: 3.1.0 zod: 3.25.76 - nextra@3.3.1(patch_hash=6ssvmycplmexou7j5jtwzrnhmy)(@types/react@18.3.24)(acorn@8.15.0)(next@14.2.32(@babel/core@7.28.3)(@playwright/test@1.55.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2): + nextra@3.3.1(patch_hash=ea67bba83484b83abd3764c3d267a635474008733ff1efe84d2a16d02fddc42d)(@types/react@18.3.24)(acorn@8.15.0)(next@14.2.32(@babel/core@7.28.3)(@playwright/test@1.55.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2): dependencies: '@formatjs/intl-localematcher': 0.5.10 '@headlessui/react': 2.2.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -11372,7 +11445,7 @@ snapshots: '@types/hast': 3.0.4 hast-util-from-html-isomorphic: 2.0.0 hast-util-to-text: 4.0.2 - mermaid-isomorphic: 3.0.4(patch_hash=tt5abewtk4kmqtpqfe2f44hnpa)(playwright@1.55.0) + mermaid-isomorphic: 3.0.4(patch_hash=fccadc7038719bcf9dc12a573655719edaf7ea8246bd144c660191d05b38c637)(playwright@1.55.0) mini-svg-data-uri: 1.4.4 space-separated-tokens: 2.0.2 unified: 11.0.5 diff --git a/scripts/sync-sched/speakers.json b/scripts/sync-sched/speakers.json index e5021cd76c..42ecdf690b 100644 --- a/scripts/sync-sched/speakers.json +++ b/scripts/sync-sched/speakers.json @@ -156,7 +156,7 @@ "_years": [ 2025 ], - "~syncedDetailsAt": 1756282673957 + "~syncedDetailsAt": 1756487485564 }, { "username": "ajhingran", @@ -1573,7 +1573,7 @@ "_years": [ 2025 ], - "~syncedDetailsAt": 1756282610669 + "~syncedDetailsAt": 1756487482717 }, { "username": "jens63", @@ -1719,7 +1719,7 @@ "_years": [ 2025 ], - "~syncedDetailsAt": 1756282610669 + "~syncedDetailsAt": 1756487485564 }, { "username": "kamilkisiela", @@ -1749,7 +1749,7 @@ 2024, 2025 ], - "~syncedDetailsAt": 1756282610669 + "~syncedDetailsAt": 1756487485564 }, { "username": "kbahl", @@ -2116,7 +2116,7 @@ "_years": [ 2025 ], - "~syncedDetailsAt": 1756282610669 + "~syncedDetailsAt": 1756487485564 }, { "username": "marion84", @@ -2246,7 +2246,7 @@ 2024, 2025 ], - "~syncedDetailsAt": 1756282610669 + "~syncedDetailsAt": 1756487485564 }, { "username": "matteo.collina1", @@ -2266,7 +2266,7 @@ "_years": [ 2025 ], - "~syncedDetailsAt": 1756282610669 + "~syncedDetailsAt": 1756487485564 }, { "username": "mauricio.montalvo.guzman", @@ -2282,7 +2282,7 @@ 2024, 2025 ], - "~syncedDetailsAt": 1756282583283 + "~syncedDetailsAt": 1756487482717 }, { "username": "meenakshi.dhanani1", @@ -2357,7 +2357,7 @@ 2024, 2025 ], - "~syncedDetailsAt": 1756282583283 + "~syncedDetailsAt": 1756487482717 }, { "username": "michael.astle", @@ -2442,7 +2442,7 @@ 2024, 2025 ], - "~syncedDetailsAt": 1756282583283 + "~syncedDetailsAt": 1756487482717 }, { "username": "patrick.arminio", @@ -2550,7 +2550,7 @@ 2024, 2025 ], - "~syncedDetailsAt": 1756282610669 + "~syncedDetailsAt": 1756487485564 }, { "username": "rachit_sengupta", @@ -2665,7 +2665,7 @@ 2024, 2025 ], - "~syncedDetailsAt": 1756282583284 + "~syncedDetailsAt": 1756487482717 }, { "username": "robrichard87", @@ -2681,7 +2681,7 @@ 2024, 2025 ], - "~syncedDetailsAt": 1756282583284 + "~syncedDetailsAt": 1756487482717 }, { "username": "ruben.cagnie", @@ -2712,7 +2712,7 @@ 2024, 2025 ], - "~syncedDetailsAt": 1756282583284 + "~syncedDetailsAt": 1756487482717 }, { "username": "saihaj", @@ -2802,7 +2802,7 @@ 2024, 2025 ], - "~syncedDetailsAt": 1756282610669 + "~syncedDetailsAt": 1756487485564 }, { "username": "sasha177", @@ -2965,7 +2965,7 @@ "_years": [ 2025 ], - "~syncedDetailsAt": 1756282610669 + "~syncedDetailsAt": 1756487485564 }, { "username": "spencer211", @@ -3002,7 +3002,7 @@ 2024, 2025 ], - "~syncedDetailsAt": 1756282583284 + "~syncedDetailsAt": 1756487482717 }, { "username": "stefan239", @@ -3288,7 +3288,7 @@ 2024, 2025 ], - "~syncedDetailsAt": 1756282583284 + "~syncedDetailsAt": 1756487482717 }, { "username": "vincent.desmares", @@ -3338,7 +3338,7 @@ "_years": [ 2025 ], - "~syncedDetailsAt": 1756282583284 + "~syncedDetailsAt": 1756487482717 }, { "username": "watson17", @@ -3354,7 +3354,7 @@ 2024, 2025 ], - "~syncedDetailsAt": 1756282610669 + "~syncedDetailsAt": 1756487485564 }, { "username": "x65han", diff --git a/src/_design-system/mdx-components/index.tsx b/src/_design-system/mdx-components/index.tsx index bec4faee2c..b3876d35be 100644 --- a/src/_design-system/mdx-components/index.tsx +++ b/src/_design-system/mdx-components/index.tsx @@ -1,7 +1,19 @@ +import { Pre, PreProps } from "../../components/pre" import { getMdxHeadings } from "./get-mdx-headings" import { MdxLink } from "./mdx-link" +const MdxPre = (props: PreProps) => { + return ( +
+ ) +} + export const mdxComponents = { a: MdxLink, + pre: MdxPre, ...getMdxHeadings(), } diff --git a/src/_design-system/syntax/dark.json b/src/_design-system/syntax/dark.json index 6897a3fe5b..a3d613278a 100644 --- a/src/_design-system/syntax/dark.json +++ b/src/_design-system/syntax/dark.json @@ -1,5 +1,5 @@ { - "name": "k-colorable dark, based on GitHub Dark", + "name": "k-colorable-dark", "type": "dark", "semanticHighlighting": true, "colors": { @@ -191,7 +191,7 @@ { "scope": ["comment", "punctuation.definition.comment", "string.comment"], "settings": { - "foreground": "#6a737d" + "foreground": "#737373" } }, { diff --git a/src/_design-system/syntax/light.json b/src/_design-system/syntax/light.json index 781f2ea182..a6b8f371ff 100644 --- a/src/_design-system/syntax/light.json +++ b/src/_design-system/syntax/light.json @@ -1,5 +1,5 @@ { - "name": "k-colorable light, based on Min Light", + "name": "k-colorable-light", "type": "light", "semanticHighlighting": true, "colors": { @@ -159,12 +159,6 @@ "foreground": "#2b5581" } }, - { - "scope": ["comment", "string.quoted.docstring.multi"], - "settings": { - "foreground": "#c2c3c5" - } - }, { "scope": [ "constant.numeric", @@ -322,6 +316,46 @@ "settings": { "foreground": "#990069" } + }, + { + "scope": ["meta.diff.header.git"], + "settings": { + "fontStyle": "underline bold" + } + }, + { + "scope": ["meta.diff.index.git", "meta.diff.range"], + "settings": { + "foreground": "#6E7557", + "fontStyle": "italic" + } + }, + { + "scope": ["meta.diff.header.from-file"], + "settings": { "foreground": "#BD0026", "fontStyle": "italic" } + }, + { + "scope": ["meta.diff.header.to-file"], + "settings": { "foreground": "#558900", "fontStyle": "italic" } + }, + { + "scope": ["markup.deleted.diff"], + "settings": { "foreground": "#B31D28" } + }, + { + "scope": ["markup.inserted.diff"], + "settings": { "foreground": "#558900" } + }, + { + "scope": [ + "comment", + "string.quoted.docstring.multi", + "punctuation.definition.comment" + ], + "settings": { + "foreground": "#9FA88A", + "fontStyle": "italic" + } } ] } diff --git a/src/codemirror.less b/src/codemirror.less deleted file mode 100644 index f1cdee39d9..0000000000 --- a/src/codemirror.less +++ /dev/null @@ -1,896 +0,0 @@ -@import "variables.less"; - -/* Code Mirror */ - -/* BASICS */ - -.CodeMirror { - /* Set height, width, borders, and global font properties here */ - height: 300px; - .code-font(); -} - -/* PADDING */ - -.CodeMirror-lines { - padding: 4px 0; /* Vertical padding around content */ -} -.CodeMirror pre { - padding: 0 4px; /* Horizontal padding of content */ -} - -.CodeMirror-scrollbar-filler, -.CodeMirror-gutter-filler { - background-color: white; /* The little square between H and V scrollbars */ -} - -/* GUTTER */ - -.CodeMirror-gutters { - border-right: 1px solid #ddd; - background-color: #f7f7f7; - white-space: nowrap; -} -.CodeMirror-linenumbers { -} -.CodeMirror-linenumber { - padding: 0 3px 0 5px; - min-width: 20px; - text-align: right; - color: #999; - white-space: nowrap; -} - -.CodeMirror-guttermarker { - color: black; -} -.CodeMirror-guttermarker-subtle { - color: #999; -} - -/* CURSOR */ - -.CodeMirror-cursor { - border-left: 1px solid black; - border-right: none; - width: 0; -} -/* Shown when moving in bi-directional text */ -.CodeMirror div.CodeMirror-secondarycursor { - border-left: 1px solid silver; -} -.cm-fat-cursor .CodeMirror-cursor { - width: auto; - border: 0; - background: #7e7; -} -.cm-fat-cursor div.CodeMirror-cursors { - z-index: 1; -} - -.cm-animate-fat-cursor { - width: auto; - border: 0; - -webkit-animation: blink 1.06s steps(1) infinite; - -moz-animation: blink 1.06s steps(1) infinite; - animation: blink 1.06s steps(1) infinite; - background-color: #7e7; -} -@-moz-keyframes blink { - 0% { - } - 50% { - background-color: transparent; - } - 100% { - } -} -@-webkit-keyframes blink { - 0% { - } - 50% { - background-color: transparent; - } - 100% { - } -} -@keyframes blink { - 0% { - } - 50% { - background-color: transparent; - } - 100% { - } -} - -/* Can style cursor different in overwrite (non-insert) mode */ -.CodeMirror-overwrite .CodeMirror-cursor { -} - -.cm-tab { - display: inline-block; - text-decoration: inherit; -} - -.CodeMirror-ruler { - border-left: 1px solid #ccc; - position: absolute; -} - -/* DEFAULT THEME */ - -.cm-s-default .cm-header { - color: blue; -} -.cm-s-default .cm-quote { - color: #090; -} -.cm-negative { - color: #d44; -} -.cm-positive { - color: #292; -} -.cm-header, -.cm-strong { - font-weight: bold; -} -.cm-em { - font-style: italic; -} -.cm-link { - text-decoration: underline; -} -.cm-strikethrough { - text-decoration: line-through; -} - -.cm-s-default .cm-keyword { - color: #708; -} -.cm-s-default .cm-atom { - color: #219; -} -.cm-s-default .cm-number { - color: #164; -} -.cm-s-default .cm-def { - color: #00f; -} -.cm-s-default .cm-variable, -.cm-s-default .cm-punctuation, -.cm-s-default .cm-property, -.cm-s-default .cm-operator { -} -.cm-s-default .cm-variable-2 { - color: #05a; -} -.cm-s-default .cm-variable-3 { - color: #085; -} -.cm-s-default .cm-comment { - color: #a50; -} -.cm-s-default .cm-string { - color: #a11; -} -.cm-s-default .cm-string-2 { - color: #f50; -} -.cm-s-default .cm-meta { - color: #555; -} -.cm-s-default .cm-qualifier { - color: #555; -} -.cm-s-default .cm-builtin { - color: #30a; -} -.cm-s-default .cm-bracket { - color: #997; -} -.cm-s-default .cm-tag { - color: #170; -} -.cm-s-default .cm-attribute { - color: #00c; -} -.cm-s-default .cm-hr { - color: #999; -} -.cm-s-default .cm-link { - color: #00c; -} - -.cm-s-default .cm-error { - color: #f00; -} -.cm-invalidchar { - color: #f00; -} - -.CodeMirror-composing { - border-bottom: 2px solid; -} - -/* Default styles for common addons */ - -div.CodeMirror span.CodeMirror-matchingbracket { - color: #0f0; -} -div.CodeMirror span.CodeMirror-nonmatchingbracket { - color: #f22; -} -.CodeMirror-matchingtag { - background: rgba(255, 150, 0, 0.3); -} -.CodeMirror-activeline-background { - background: #e8f2ff; -} - -/* STOP */ - -/* The rest of this file contains styles related to the mechanics of - the editor. You probably shouldn't touch them. */ - -.CodeMirror { - position: relative; - overflow: hidden; - background: white; -} - -.CodeMirror-scroll { - overflow: scroll !important; /* Things will break if this is overridden */ - /* 30px is the magic margin used to hide the element's real scrollbars */ - /* See overflow: hidden in .CodeMirror */ - margin-bottom: -30px; - margin-right: -30px; - padding-bottom: 30px; - height: 100%; - outline: none; /* Prevent dragging from highlighting the element */ - position: relative; -} -.CodeMirror-sizer { - position: relative; - border-right: 30px solid transparent; -} - -/* The fake, visible scrollbars. Used to force redraw during scrolling - before actuall scrolling happens, thus preventing shaking and - flickering artifacts. */ -.CodeMirror-vscrollbar, -.CodeMirror-hscrollbar, -.CodeMirror-scrollbar-filler, -.CodeMirror-gutter-filler { - position: absolute; - z-index: 6; - display: none; -} -.CodeMirror-scroll { - overflow: hidden; -} -.CodeMirror-scrollbar-filler { - right: 0; - bottom: 0; -} -.CodeMirror-gutter-filler { - left: 0; - bottom: 0; -} - -.CodeMirror-gutters { - position: absolute; - left: 0; - top: 0; - z-index: 3; -} -.CodeMirror-gutter { - white-space: normal; - height: 100%; - display: inline-block; - margin-bottom: -30px; - /* Hack to make IE7 behave */ - *zoom: 1; - *display: inline; -} -.CodeMirror-gutter-wrapper { - position: absolute; - z-index: 4; - background: none !important; - border: none !important; -} -.CodeMirror-gutter-background { - position: absolute; - top: 0; - bottom: 0; - z-index: 4; -} -.CodeMirror-gutter-elt { - position: absolute; - cursor: default; - z-index: 4; -} -.CodeMirror-gutter-wrapper { - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; -} - -.CodeMirror-lines { - cursor: text; - min-height: 1px; /* prevents collapsing before first draw */ -} -.CodeMirror pre { - /* Reset some styles that the rest of the page might have set */ - -moz-border-radius: 0; - -webkit-border-radius: 0; - border-radius: 0; - border-width: 0; - background: transparent; - font-family: inherit; - font-size: inherit; - margin: 0; - white-space: pre; - word-wrap: normal; - line-height: inherit; - color: inherit; - z-index: 2; - position: relative; - overflow: visible; - -webkit-tap-highlight-color: transparent; -} -.CodeMirror-wrap pre { - word-wrap: break-word; - white-space: pre-wrap; - word-break: normal; -} - -.CodeMirror-linebackground { - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: 0; -} - -.CodeMirror-linewidget { - position: relative; - z-index: 2; - overflow: auto; -} - -.CodeMirror-widget { -} - -.CodeMirror-code { - outline: none; -} - -/* Force content-box sizing for the elements where we expect it */ -.CodeMirror-scroll, -.CodeMirror-sizer, -.CodeMirror-gutter, -.CodeMirror-gutters, -.CodeMirror-linenumber { - -moz-box-sizing: content-box; - box-sizing: content-box; -} - -.CodeMirror-measure { - position: absolute; - width: 100%; - height: 0; - overflow: hidden; - visibility: hidden; -} - -.CodeMirror-cursor { - position: absolute; -} -.CodeMirror-measure pre { - position: static; -} - -div.CodeMirror-cursors { - visibility: hidden; - position: relative; - z-index: 3; -} -div.CodeMirror-dragcursors { - visibility: visible; -} - -.CodeMirror-focused div.CodeMirror-cursors { - visibility: visible; -} - -.CodeMirror-selected { - background: #d9d9d9; -} -.CodeMirror-focused .CodeMirror-selected { - background: #d7d4f0; -} -.CodeMirror-crosshair { - cursor: crosshair; -} -.CodeMirror-line::selection, -.CodeMirror-line > span::selection, -.CodeMirror-line > span > span::selection { - background: #d7d4f0; -} -.CodeMirror-line::-moz-selection, -.CodeMirror-line > span::-moz-selection, -.CodeMirror-line > span > span::-moz-selection { - background: #d7d4f0; -} - -.cm-searching { - background: #ffa; - background: rgba(255, 255, 0, 0.4); -} - -/* IE7 hack to prevent it from returning funny offsetTops on the spans */ -.CodeMirror span { - *vertical-align: text-bottom; -} - -/* Used to force a border model for a node */ -.cm-force-border { - padding-right: 0.1px; -} - -@media print { - /* Hide the cursor when printing */ - .CodeMirror div.CodeMirror-cursors { - visibility: hidden; - } -} - -/* See issue #2901 */ -.cm-tab-wrap-hack:after { - content: ""; -} - -/* Help users use markselection to safely style text background */ -span.CodeMirror-selectedtext { - background: none; -} - -/* Fold */ - -.CodeMirror-foldmarker { - color: blue; - text-shadow: - #b9f 1px 1px 2px, - #b9f -1px -1px 2px, - #b9f 1px -1px 2px, - #b9f -1px 1px 2px; - font-family: arial; - line-height: 0.3; - cursor: pointer; -} -.CodeMirror-foldgutter { - width: 0.7em; -} -.CodeMirror-foldgutter-open, -.CodeMirror-foldgutter-folded { - cursor: pointer; -} -.CodeMirror-foldgutter-open:after { - content: "\25BE"; -} -.CodeMirror-foldgutter-folded:after { - content: "\25B8"; -} - -/* Fold override */ - -.CodeMirror-foldmarker { - border-radius: 4px; - background: #08f; - background: -webkit-linear-gradient(#43a8ff, #0f83e8); - background: linear-gradient(#43a8ff, #0f83e8); - - color: white; - -webkit-box-shadow: - 0 1px 1px rgba(0, 0, 0, 0.2), - inset 0 0 0 1px rgba(0, 0, 0, 0.1); - -moz-box-shadow: - 0 1px 1px rgba(0, 0, 0, 0.2), - inset 0 0 0 1px rgba(0, 0, 0, 0.1); - box-shadow: - 0 1px 1px rgba(0, 0, 0, 0.2), - inset 0 0 0 1px rgba(0, 0, 0, 0.1); - font-family: arial; - line-height: 0; - padding: 0px 4px 1px; - font-size: 12px; - margin: 0 3px; - text-shadow: 0 -1px rgba(0, 0, 0, 0.1); -} - -/* Lint */ - -/* The lint marker gutter */ -.CodeMirror-lint-markers { - width: 16px; -} - -.CodeMirror-lint-tooltip { - background-color: infobackground; - border: 1px solid black; - border-radius: 4px 4px 4px 4px; - .code-font(); - overflow: hidden; - padding: 2px 5px; - position: fixed; - white-space: pre; - white-space: pre-wrap; - z-index: 100; - max-width: 600px; - opacity: 0; - transition: opacity 0.4s; - -moz-transition: opacity 0.4s; - -webkit-transition: opacity 0.4s; - -o-transition: opacity 0.4s; - -ms-transition: opacity 0.4s; -} - -.CodeMirror-lint-mark-error, -.CodeMirror-lint-mark-warning { - background-position: left bottom; - background-repeat: repeat-x; -} - -.CodeMirror-lint-mark-error { - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg=="); -} - -.CodeMirror-lint-mark-warning { - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII="); -} - -.CodeMirror-lint-marker-error, -.CodeMirror-lint-marker-warning { - background-position: center center; - background-repeat: no-repeat; - cursor: pointer; - display: inline-block; - height: 16px; - width: 16px; - vertical-align: middle; - position: relative; -} - -.CodeMirror-lint-message-error, -.CodeMirror-lint-message-warning { - padding-left: 18px; - background-position: top left; - background-repeat: no-repeat; -} - -.CodeMirror-lint-marker-error, -.CodeMirror-lint-message-error { - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII="); -} - -.CodeMirror-lint-marker-warning, -.CodeMirror-lint-message-warning { - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII="); -} - -.CodeMirror-lint-marker-multiple { - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC"); - background-repeat: no-repeat; - background-position: right bottom; - width: 100%; - height: 100%; -} - -/* Hint */ - -.CodeMirror-hints { - background: white; - -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); - -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); - .code-font(); - list-style: none; - margin: 0; - margin-left: -6px; - max-height: 14.5em; - overflow-y: auto; - overflow: hidden; - padding: 0; - position: absolute; - z-index: 10; -} - -.CodeMirror-hints-wrapper { - background: white; - -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); - -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); - margin-left: -6px; - position: absolute; - z-index: 10; -} - -.CodeMirror-hints-wrapper .CodeMirror-hints { - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; - position: relative; - margin-left: 0; - z-index: 0; -} - -.CodeMirror-hint { - border-top: solid 1px #f7f7f7; - color: #333; - cursor: pointer; - margin: 0; - max-width: 300px; - overflow: hidden; - padding: 2px 6px; - white-space: pre; -} - -li.CodeMirror-hint-active { - background-color: #08f; - border-top-color: white; - color: white; -} - -.CodeMirror-hint-information { - border-top: solid 1px #c0c0c0; - max-width: 300px; - padding: 4px 6px; - position: relative; - z-index: 1; -} - -.CodeMirror-hint-information:first-child { - border-bottom: solid 1px #c0c0c0; - border-top: none; - margin-bottom: -1px; -} - -/* Custom typeahead */ - -.CodeMirror-hint-information .content { - -webkit-box-orient: vertical; - display: -webkit-box; - .body-font(@size: 15px); - -webkit-line-clamp: 3; - max-height: 48px; - overflow: hidden; - text-overflow: -o-ellipsis-lastline; -} - -.CodeMirror-hint-information .content p:first-child { - margin-top: 0; -} - -.CodeMirror-hint-information .content p:last-child { - margin-bottom: 0; -} - -.CodeMirror-hint-information .infoType { - color: #30a; - margin-right: 0.5em; - display: inline; -} - -/* Lint override */ - -div.CodeMirror-lint-tooltip { - background-color: white; - color: #333; - border: 0; - border-radius: 2px; - -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); - -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); - .body-font(@size: 15px); - padding: 6px 10px; - opacity: 0; - transition: opacity 0.15s; - -moz-transition: opacity 0.15s; - -webkit-transition: opacity 0.15s; - -o-transition: opacity 0.15s; - -ms-transition: opacity 0.15s; -} - -div.CodeMirror-lint-message-error, -div.CodeMirror-lint-message-warning { - padding-left: 23px; -} - -/* Brackets override */ - -div.CodeMirror span.CodeMirror-matchingbracket { - color: #555; - text-decoration: underline; -} - -div.CodeMirror span.CodeMirror-nonmatchingbracket { - color: #f00; -} - -/* Theme */ - -/* COLORS */ - -/* Comment */ -.cm-comment { - color: #999; -} - -/* Punctuation */ -.cm-punctuation { - color: #555; -} - -/* Keyword */ -.cm-keyword { - color: #b11a04; -} - -/* OperationName, FragmentName */ -.cm-def { - color: #d2054e; -} - -/* FieldName */ -.cm-property { - color: #1f61a0; -} - -/* FieldAlias */ -.cm-qualifier { - color: #1c92a9; -} - -/* ArgumentName and ObjectFieldName */ -.cm-attribute { - color: #8b2bb9; -} - -/* Number */ -.cm-number { - color: #2882f9; -} - -/* String */ -.cm-string { - color: #d64292; -} - -/* Boolean */ -.cm-builtin { - color: #d47509; -} - -/* EnumValue */ -.cm-string-2 { - color: #0b7fc7; -} - -/* Variable */ -.cm-variable { - color: #397d13; -} - -/* Directive */ -.cm-meta { - color: #b33086; -} - -/* Type */ -.cm-atom { - color: #ca9800; -} - -/* CM override */ - -.CodeMirror { - .code-font(); -} - -.miniGraphiQL { - margin: 28px 0; - color: #333; - width: 100%; - display: -webkit-flex; - display: flex; - -webkit-flex-direction: row; - flex-direction: row; - position: relative; - - background: white; - box-shadow: - inset 0 0 0 1px #eee, - inset 4px 0 0 #eee; - border-radius: 3px; - margin-left: -4px; - - .editor-name { - position: sticky; - display: block; - padding: 0.5rem 0.75rem; - border-bottom: 1px solid rgb(209, 213, 219); - background-color: rgb(243, 244, 246); - color: rgb(55, 65, 81); - font-size: 0.8rem; - font-weight: 600; - - .dark & { - border-bottom-color: rgb(23, 23, 23); - background-color: rgb(8, 8, 8); - color: rgb(209, 213, 219); - font-weight: 400; - } - } -} - -.query-editor .CodeMirror { - height: auto; - min-height: 100px; - margin: 0px 7px 35px; - background: none; -} - -.query-editor { - width: 50%; -} - -.hasVariables { - width: 50%; - - .query-editor { - width: auto; - } - - .query-editor .CodeMirror { - margin-bottom: 21px; - } - - .variable-editor .CodeMirror { - height: auto; - min-height: 30px; - margin: 0 7px; - background: none; - } -} - -.result-window { - /*background: #fbfafa;*/ - box-shadow: inset 5px 0px 4px -3px rgba(0, 0, 0, 0.2); - - position: absolute; - left: 50%; - top: 0; - bottom: 0; - height: 100%; - right: 0; - - box-shadow: inset 0 0 0 1px #eee; - border-radius: 3px; -} - -.result-window .CodeMirror { - background: none; - height: 100%; - padding-bottom: 45px; - margin: 0 7px; - box-sizing: border-box; -} - -.query-editor, -.variable-editor, -.result-window { - overflow: hidden; -} diff --git a/src/components/index-page/graphql-advantages/precision.tsx b/src/components/index-page/graphql-advantages/precision.tsx index fdbe24d1bb..214f507d5b 100644 --- a/src/components/index-page/graphql-advantages/precision.tsx +++ b/src/components/index-page/graphql-advantages/precision.tsx @@ -96,19 +96,19 @@ export function PrecisionFigure() { className="nextra-codeblocks flex w-full max-w-[100vw] bg-gradient-to-b from-transparent to-sec-lighter px-[14px] py-[30px] *:w-1/2 dark:to-sec-darker/25 max-[380px]:px-0 sm:max-w-[calc(100vw-32px)] xl:px-[46px] max-[380px]:[&_:is(.rounded-t-md,pre)]:rounded-none [&_pre]:!h-48" aria-hidden > -+{"{"} {"\n "} - {"hero"} + {"hero"} {" {"} - + {"\n name"} {"\n height\n mass".split("").map((char, i) => ( {char === "\n" ?
: char} diff --git a/src/components/index-page/how-it-works.tsx b/src/components/index-page/how-it-works.tsx index e20aea999f..20842ac2e1 100644 --- a/src/components/index-page/how-it-works.tsx +++ b/src/components/index-page/how-it-works.tsx @@ -12,6 +12,13 @@ export function HowItWorks() {A GraphQL Query
diff --git a/src/components/interactive-code-block/codemirror-theme.tsx b/src/components/interactive-code-block/codemirror-theme.tsx new file mode 100644 index 0000000000..a6708358d7 --- /dev/null +++ b/src/components/interactive-code-block/codemirror-theme.tsx @@ -0,0 +1,137 @@ +import { EditorView } from "@codemirror/view" +import { Extension } from "@codemirror/state" +import { HighlightStyle, syntaxHighlighting } from "@codemirror/language" +import { tags as t } from "@lezer/highlight" + +export const editorTheme = EditorView.theme({ + "&": { + color: "var(--cm-foreground)", + backgroundColor: "var(--cm-background)", + }, + + ".cm-content": { + caretColor: "var(--cm-cursor)", + }, + + ".cm-cursor, .cm-dropCursor": { borderLeftColor: "var(--cm-cursor)" }, + "&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": + { backgroundColor: "var(--cm-selection)" }, + + ".cm-panels": { + backgroundColor: "var(--cm-background)", + color: "var(--cm-foreground)", + }, + ".cm-panels.cm-panels-top": { + borderBottom: "2px solid var(--cm-gutter-border)", + }, + ".cm-panels.cm-panels-bottom": { + borderTop: "2px solid var(--cm-gutter-border)", + }, + + ".cm-searchMatch": { + backgroundColor: "#72a1ff59", + outline: "1px solid #457dff", + }, + ".cm-searchMatch.cm-searchMatch-selected": { + backgroundColor: "#6199ff2f", + }, + + ".cm-activeLine": { backgroundColor: "rgba(255, 255, 255, 0.05)" }, + ".cm-selectionMatch": { backgroundColor: "#aafe661a" }, + + "&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket": { + backgroundColor: "#bad0f847", + }, + + ".cm-gutters": { + backgroundColor: "var(--cm-gutter-background)", + color: "var(--cm-line-number)", + border: "none", + borderRight: "1px solid var(--cm-gutter-border)", + }, + + ".cm-activeLineGutter": { + backgroundColor: "var(--cm-gutter-background)", + }, + + ".cm-foldPlaceholder": { + backgroundColor: "transparent", + border: "none", + color: "#ddd", + }, + + ".cm-tooltip": { + border: "none", + backgroundColor: "var(--cm-hints-background)", + color: "var(--cm-hints-foreground)", + }, + ".cm-tooltip .cm-tooltip-arrow:before": { + borderTopColor: "transparent", + borderBottomColor: "transparent", + }, + ".cm-tooltip .cm-tooltip-arrow:after": { + borderTopColor: "var(--cm-hints-background)", + borderBottomColor: "var(--cm-hints-background)", + }, + ".cm-tooltip-autocomplete": { + "& > ul > li[aria-selected]": { + backgroundColor: "var(--cm-hints-active-background)", + color: "var(--cm-hints-active-foreground)", + }, + }, +}) + +export const syntaxTheme = HighlightStyle.define([ + { tag: t.keyword, class: "cm-keyword" }, + { + tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName], + class: "cm-def", + }, + { tag: [t.function(t.variableName), t.labelName], class: "cm-variable" }, + { tag: [t.color, t.constant(t.name), t.standard(t.name)], class: "cm-atom" }, + { tag: [t.definition(t.name), t.separator], class: "cm-def" }, + { + tag: [ + t.typeName, + t.className, + t.number, + t.changed, + t.annotation, + t.modifier, + t.self, + t.namespace, + ], + class: "cm-atom", + }, + { + tag: [ + t.operator, + t.operatorKeyword, + t.url, + t.escape, + t.regexp, + t.link, + t.special(t.string), + t.separator, + ], + class: "cm-punctuation", + }, + { tag: [t.meta, t.comment], class: "cm-comment" }, + { tag: t.strong, fontWeight: "bold" }, + { tag: t.emphasis, fontStyle: "italic" }, + { tag: t.strikethrough, textDecoration: "line-through" }, + { tag: t.link, class: "cm-comment", textDecoration: "underline" }, + { tag: t.heading, fontWeight: "bold", class: "cm-def" }, + { tag: [t.atom, t.bool, t.special(t.variableName)], class: "cm-atom" }, + { tag: [t.processingInstruction, t.string, t.inserted], class: "cm-string" }, + { tag: t.invalid, class: "cm-invalidchar" }, + { + tag: t.punctuation, + class: "cm-punctuation", + }, +]) + +export const codeMirrorThemeExtension: Extension = [ + editorTheme, + syntaxHighlighting(syntaxTheme), +] diff --git a/src/components/interactive-code-block/get-variable-to-type.tsx b/src/components/interactive-code-block/get-variable-to-type.tsx new file mode 100644 index 0000000000..dc2924395d --- /dev/null +++ b/src/components/interactive-code-block/get-variable-to-type.tsx @@ -0,0 +1,30 @@ +import { GraphQLSchema, parse, typeFromAST } from "graphql" + +export function getVariableToType(schema: GraphQLSchema, documentStr: string) { + if (!documentStr || !schema) { + return {} + } + + try { + const documentAST = parse(documentStr) + const variableToType = Object.create(null) + documentAST.definitions.forEach(definition => { + if (definition.kind === "OperationDefinition") { + const variableDefinitions = definition.variableDefinitions + if (variableDefinitions) { + variableDefinitions.forEach(({ variable, type }) => { + const inputType = typeFromAST(schema, type) + if (inputType) { + variableToType[variable.name.value] = inputType + } + }) + } + } + }) + return variableToType + } catch (e) { + // ignore + } + + return {} +} diff --git a/src/components/interactive-code-block/index.tsx b/src/components/interactive-code-block/index.tsx new file mode 100644 index 0000000000..e856fdcd0b --- /dev/null +++ b/src/components/interactive-code-block/index.tsx @@ -0,0 +1,5 @@ +import dynamic from "next/dynamic" + +export const InteractiveCodeBlock = dynamic(() => import("./mini-graphiQL"), { + ssr: true, +}) diff --git a/src/components/interactive-code-block/mini-graphiQL.tsx b/src/components/interactive-code-block/mini-graphiQL.tsx new file mode 100644 index 0000000000..ac8004aa89 --- /dev/null +++ b/src/components/interactive-code-block/mini-graphiQL.tsx @@ -0,0 +1,190 @@ +/** + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { Component } from "react" + +import { graphql, GraphQLSchema } from "graphql" + +import { QueryEditor } from "./query-editor" +import { VariableEditor } from "./variable-editor" +import { ResultViewer } from "./result-viewer" +import { getVariableToType } from "./get-variable-to-type" +import { StarWarsSchema } from "./swapi-schema" +import { UsersSchema } from "./users-schema" +import { CodeBlockLabel } from "@/components/pre/code-block-label" + +export type MiniGraphiQLProps = { + children: string +} + +interface MiniGraphiQLState { + query: string + variables: string + response: string | null + variableToType: Record
} /> + + {/* TODO: Instead of importing CodeB and CodeC, we'll refactor MiniGraphiQL and dynamically import it here. + Required changes: + - [ ] Move VariableEditor and QueryEditor to separate files. + - [ ] Import them here with the raw code snippets. + + */} } /> } /> +} + +const SCHEMA_MAP = { + StarWars: StarWarsSchema, + Users: UsersSchema, +} as const + +type SchemaKey = keyof typeof SCHEMA_MAP + +type Metadata = { + graphiql?: boolean + variables?: unknown + schema?: SchemaKey +} + +export default class MiniGraphiQL extends Component< + MiniGraphiQLProps, + MiniGraphiQLState +> { + // Lifecycle + + _editorQueryID = 0 + + schema: GraphQLSchema + + constructor(props: MiniGraphiQLProps) { + super(props) + + const codeMatch = this.props.children.match(/```graphql\s*\n([\s\S]*?)```/) + const blockContent = codeMatch?.[1] + const [firstLine, ...rest] = (blockContent || "").split("\n") + + const metaMatch = firstLine.match(/^\s*#\s*({.*})\s*$/)?.[1] ?? "{}" + const meta = JSON.parse(metaMatch) as Metadata + + const query = rest.join("\n").replace(/^\s+/, "") + const variables = meta.variables + ? JSON.stringify(meta.variables, null, 2) + : "" + this.schema = SCHEMA_MAP[meta.schema ?? "StarWars"] + + this.state = { + query: query, + variables: variables, + response: null, + variableToType: getVariableToType(this.schema, query), + } + } + + render() { + const editor = ( + ++ ) + + return ( ++ + + {Object.keys(this.state.variableToType).length > 0 ? ( ++ ) + } + + componentDidMount() { + this._runQueryFromEditor() + } + + // Private methods + + _runQueryFromEditor() { + this.setState({ + variableToType: getVariableToType(this.schema, this.state.query), + }) + this._runQuery({ manual: true }) + } + + async _runQuery(options: { manual: boolean }) { + this._editorQueryID++ + const queryID = this._editorQueryID + try { + const result = await graphql({ + schema: this.schema, + source: this.state.query, + variableValues: JSON.parse(this.state.variables || "{}"), + }) + + let resultToSerialize: any = result + if (result.errors) { + if (!options.manual) { + // if the query was ran on edit, we display errors on the left side + // so we can just return instead of showing the resulting error + return + } + + // Convert errors to serializable format + const serializedErrors = result.errors.map(error => ({ + message: error.message, + locations: error.locations, + path: error.path, + })) + // Replace errors with serialized version for JSON.stringify + resultToSerialize = { ...result, errors: serializedErrors } + } + + if (queryID === this._editorQueryID) { + this.setState({ response: JSON.stringify(resultToSerialize, null, 2) }) + } + } catch (error) { + if (queryID === this._editorQueryID) { + this.setState({ response: JSON.stringify(error, null, 2) }) + } + } + } + + _handleEditQuery(value: string) { + this.setState({ query: value }, () => { + void this._runQuery({ manual: false }) + }) + } + + _handleEditVariables(value: string) { + this.setState({ variables: value }, () => { + void this._runQuery({ manual: false }) + }) + } +} diff --git a/src/components/interactive-code-block/on-has-completion.tsx b/src/components/interactive-code-block/on-has-completion.tsx new file mode 100644 index 0000000000..6bcbf066d4 --- /dev/null +++ b/src/components/interactive-code-block/on-has-completion.tsx @@ -0,0 +1,14 @@ +/** + * Note: This file is retained for compatibility but is no longer used with CodeMirror 6. + * CodeMirror 6 and cm6-graphql handle completions differently and don't require this custom UI. + */ + +export function onHasCompletion( + cm: any, + data: any, + onHintInformationRender?: any, +) { + // This function is no longer used with CodeMirror 6 + // Left as a stub for backwards compatibility + return +} diff --git a/src/components/interactive-code-block/query-editor.tsx b/src/components/interactive-code-block/query-editor.tsx new file mode 100644 index 0000000000..b5f975497c --- /dev/null +++ b/src/components/interactive-code-block/query-editor.tsx @@ -0,0 +1,150 @@ +import { Component } from "react" +import { EditorView, keymap } from "@codemirror/view" +import { EditorState } from "@codemirror/state" +import { history, historyKeymap, defaultKeymap } from "@codemirror/commands" +import { bracketMatching } from "@codemirror/language" +import { + autocompletion, + closeBrackets, + completionKeymap, +} from "@codemirror/autocomplete" +import { graphql, updateSchema } from "cm6-graphql" +import { GraphQLSchema } from "graphql" +import { codeMirrorThemeExtension } from "./codemirror-theme" +import "./syntax-highlighting.css" + +interface QueryEditorProps { + schema?: GraphQLSchema + value?: string + onEdit?: (value: string) => void + runQuery?: () => void + onHintInformationRender?: (el: HTMLElement) => void +} + +/** + * QueryEditor + * + * Maintains an instance of CodeMirror 6 responsible for editing a GraphQL query. + * + * Props: + * + * - schema: A GraphQLSchema instance enabling editor linting and hinting. + * - value: The text of the editor. + * - onEdit: A function called when the editor changes, given the edited text. + * + */ +export class QueryEditor extends Component+ {editor} ++ ) : ( + editor + )} ++++ void this._runQuery.bind(this)} + /> + +++ + { + private view: EditorView | null = null + private domNode: HTMLDivElement | null = null + private cachedValue: string + private ignoreChangeEvent = false + + constructor(props: QueryEditorProps) { + super(props) + + // Keep a cached version of the value, this cache will be updated when the + // editor is updated, which can later be used to protect the editor from + // unnecessary updates during the update lifecycle. + this.cachedValue = props.value || "" + } + + /** + * Public API for retrieving the CodeMirror instance from this + * React component. + */ + getCodeMirror() { + return this.view + } + + componentDidMount() { + if (!this.domNode) return + + const runQueryBinding = keymap.of([ + { + key: "Cmd-Enter", + run: () => (this.props.runQuery?.(), true), + }, + { + key: "Ctrl-Enter", + run: () => (this.props.runQuery?.(), true), + }, + ]) + + const state = EditorState.create({ + doc: this.props.value || "", + extensions: [ + history(), + closeBrackets(), + bracketMatching(), + keymap.of([...historyKeymap, ...completionKeymap, ...defaultKeymap]), + runQueryBinding, + codeMirrorThemeExtension, + graphql(this.props.schema, {}), + autocompletion({ + icons: false, + }), + EditorView.updateListener.of(update => { + if (update.docChanged && !this.ignoreChangeEvent) { + this.cachedValue = update.state.doc.toString() + if (this.props.onEdit) { + this.props.onEdit(this.cachedValue) + } + } + }), + ], + }) + + // Create editor view + this.view = new EditorView({ + state, + parent: this.domNode, + }) + } + + componentWillUnmount() { + if (this.view) { + this.view.destroy() + this.view = null + } + } + + componentDidUpdate(prevProps: QueryEditorProps) { + if (!this.view) return + + // Ensure the changes caused by this update are not interpreted as + // user-input changes which could otherwise result in an infinite + // event loop. + this.ignoreChangeEvent = true + + if (this.props.schema !== prevProps.schema && this.props.schema) { + updateSchema(this.view, this.props.schema) + } + + if ( + this.props.value !== prevProps.value && + this.props.value !== this.cachedValue + ) { + this.cachedValue = this.props.value || "" + this.view.dispatch({ + changes: { + from: 0, + to: this.view.state.doc.length, + insert: this.props.value || "", + }, + }) + } + + this.ignoreChangeEvent = false + } + + render() { + return ( + { + this.domNode = e + }} + /> + ) + } +} diff --git a/src/components/interactive-code-block/result-viewer.tsx b/src/components/interactive-code-block/result-viewer.tsx new file mode 100644 index 0000000000..032424c44b --- /dev/null +++ b/src/components/interactive-code-block/result-viewer.tsx @@ -0,0 +1,81 @@ +import { Component } from "react" +import { EditorView } from "@codemirror/view" +import { EditorState } from "@codemirror/state" +// todo: perhaps custom grammar to match the shiki highlighting? +import { json } from "@codemirror/lang-json" +import { codeMirrorThemeExtension } from "./codemirror-theme" + +import "./syntax-highlighting.css" + +interface ResultViewerProps { + value?: string +} + +/** + * ResultViewer + * + * Maintains an instance of CodeMirror 6 for viewing a GraphQL response. + * + * Props: + * + * - value: The text of the editor. + * + */ +export class ResultViewer extends Component{ + private view: EditorView | null = null + private domNode: HTMLDivElement | null = null + + componentDidMount() { + if (!this.domNode) return + + // Create read-only editor state for JSON results + const state = EditorState.create({ + doc: this.props.value || "", + extensions: [ + EditorState.readOnly.of(true), + json(), + codeMirrorThemeExtension, + ], + }) + + // Create editor view + this.view = new EditorView({ + state, + parent: this.domNode, + }) + } + + componentWillUnmount() { + if (this.view) { + this.view.destroy() + this.view = null + } + } + + shouldComponentUpdate(nextProps: ResultViewerProps) { + return this.props.value !== nextProps.value + } + + componentDidUpdate() { + if (!this.view) return + + this.view.dispatch({ + changes: { + from: 0, + to: this.view.state.doc.length, + insert: this.props.value || "", + }, + }) + } + + render() { + return ( + { + this.domNode = e + }} + /> + ) + } +} diff --git a/src/components/marked/swapi-schema.tsx b/src/components/interactive-code-block/swapi-schema.tsx similarity index 100% rename from src/components/marked/swapi-schema.tsx rename to src/components/interactive-code-block/swapi-schema.tsx diff --git a/src/components/interactive-code-block/syntax-highlighting.css b/src/components/interactive-code-block/syntax-highlighting.css new file mode 100644 index 0000000000..62580fb102 --- /dev/null +++ b/src/components/interactive-code-block/syntax-highlighting.css @@ -0,0 +1,314 @@ +:root { + --cm-comment: #6e7557; + --cm-punctuation: #6e7557; + --cm-keyword: #990069; + --cm-def: hsl(var(--color-pri-base)); + --cm-property: #0e0f0b; + --cm-qualifier: #1c92a9; + --cm-attribute: #8b2bb9; + --cm-number: #1976d2; + --cm-string: #609a06; + --cm-builtin: #1976d2; + --cm-string-2: #0b7fc7; + --cm-variable: #e10198; + --cm-meta: #b33086; + --cm-atom: #1976d2; + + --cm-background: #ffffff; + --cm-foreground: #4f533f; + --cm-gutter-background: #f7f7f7; + --cm-gutter-border: #ddd; + --cm-line-number: #999; + --cm-cursor: #000000; + --cm-selection: #d7d4f0; + --cm-hints-background: hsl(var(--color-neu-0) / 0.8); + --cm-hints-foreground: #080707; + --cm-hints-active-background: hsl(var(--color-pri-base) / 0.1); + --cm-hints-active-foreground: #ffffff; +} + +.dark { + /* Dark theme colors (matching Shiki dark.json) */ + --cm-comment: #737373; + --cm-punctuation: #6e7557; + --cm-keyword: #c2f653; + --cm-def: #dbf6a2; + --cm-property: #dbf6a2; + --cm-qualifier: #1c92a9; + --cm-attribute: #8b2bb9; + --cm-number: #79b8ff; + --cm-string: #9ecbff; + --cm-builtin: #79b8ff; + --cm-string-2: #0b7fc7; + --cm-variable: #dbf6a2; + --cm-meta: #ff99e0; + --cm-atom: #79b8ff; + + /* Editor UI colors - dark theme */ + --cm-background: hsl(var(--color-neu-800) / 0.025); + --cm-foreground: #cfd3c5; + --cm-gutter-background: #1f2425; + --cm-gutter-border: #1b1f20; + --cm-line-number: #737373; + --cm-cursor: #c8e1ff; + --cm-selection: #3392ff44; + --cm-hints-foreground: #e1e4e8; + --cm-hints-active-background: hsl(var(--color-sec-base) / 0.1); + --cm-hints-active-foreground: #ffffff; +} + +.cm-editor { + font-family: inherit; + font-size: inherit; + background: var(--cm-background); + color: var(--cm-foreground); + position: relative; + + & .cm-content { + padding: 16px 0; + } + + & .cm-line { + padding: 0 0 0 16px; + } + + & .cm-scroller { + line-height: 1.5; + font-family: var(--font-mono); + } +} + +.cm-focused { + outline: none; +} + +/* Syntax Highlighting Classes */ +.cm-comment { + color: var(--cm-comment); +} + +.cm-punctuation { + color: var(--cm-punctuation); +} + +.cm-keyword { + color: var(--cm-keyword); +} + +.cm-def { + color: var(--cm-def); +} + +.cm-property { + color: var(--cm-property); +} + +.cm-qualifier { + color: var(--cm-qualifier); +} + +.cm-attribute { + color: var(--cm-attribute); +} + +.cm-number { + color: var(--cm-number); +} + +.cm-string { + color: var(--cm-string); +} + +.cm-builtin { + color: var(--cm-builtin); +} + +.cm-string-2 { + color: var(--cm-string-2); +} + +.cm-variable { + color: var(--cm-variable); +} + +.cm-meta { + color: var(--cm-meta); +} + +.cm-atom { + color: var(--cm-atom); +} + +/* Editor UI Elements */ +.cm-gutters { + border-right: 1px solid var(--cm-gutter-border); + background-color: var(--cm-gutter-background); + white-space: nowrap; +} + +.cm-gutter { + white-space: normal; + height: 100%; + display: inline-block; + margin-bottom: -30px; +} + +.cm-lineNumbers .cm-gutterElement { + padding: 0 3px 0 5px; + min-width: 20px; + text-align: right; + color: var(--cm-line-number); + white-space: nowrap; +} + +.cm-cursor { + border-left: 1px solid var(--cm-cursor); + border-right: none; + width: 0; +} + +.cm-selectionBackground { + background: var(--cm-selection); +} + +.cm-focused.cm-focused { + outline: none; +} + +.cm-focused .cm-selectionBackground { + background: var(--cm-selection); +} + +/* Autocomplete/Hints */ +.cm-editor .cm-tooltip { + @apply shadow-md backdrop-blur-[6.4px] dark:border-neu-50; + + border: 1px solid hsl(var(--color-neu-100)); + background: var(--cm-hints-background); + color: var(--cm-hints-foreground); + font-family: inherit; + font-size: inherit; + + &.cm-completionInfo-right { + @apply bg-neu-0 shadow-lg; + } +} + +.cm-tooltip-autocomplete { + background: var(--cm-hints-background); + margin-left: -6px; + position: absolute; + z-index: 10; +} + +.cm-completionLabel { + color: var(--cm-hints-foreground); +} + +.cm-editor .cm-completionDetail { + color: var(--cm-def); + font-style: normal; + font-size: 0.8em; +} + +.cm-completionMatchedText { + text-decoration: underline; +} + +.cm-completionIcon { + width: 1em; + height: 1em; + vertical-align: middle; + margin-right: 0.5em; +} + +.cm-tooltip.cm-tooltip-autocomplete > ul > li { + color: var(--cm-hints-foreground); + cursor: pointer; + margin: 0; + max-width: 300px; + overflow: hidden; + white-space: pre; + font-size: 14px; + line-height: 1.5 !important; +} + +.cm-tooltip-autocomplete li:hover { + background-color: var(--cm-hints-active-background); + color: var(--cm-hints-active-foreground); +} + +/* Lint Markers */ +.cm-lintRange { + background-position: left bottom; + background-repeat: repeat-x; +} + +.cm-lintRange-error { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg=="); +} + +.cm-lintRange-warning { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII="); +} + +.cm-diagnostic { + background-position: center center; + background-repeat: no-repeat; + cursor: pointer; + display: inline-block; + height: 16px; + width: 16px; + vertical-align: middle; + position: relative; +} + +.cm-diagnostic-error { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII="); +} + +.cm-diagnostic-warning { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII="); +} + +.cm-tooltip-lint { + background-color: var(--cm-hints-background); + color: var(--cm-hints-foreground); + border: none; + border-radius: 2px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); + font-family: inherit; + font-size: inherit; + padding: 6px 10px; + opacity: 0; + transition: opacity 0.15s; +} + +/* Matching brackets */ +.cm-matchingBracket { + color: #555; + text-decoration: underline; +} + +.cm-nonmatchingBracket { + color: #f00; +} + +/* Active line */ +.cm-activeLine { + background-color: rgba(0, 0, 0, 0.05); +} + +.dark .cm-activeLine { + background-color: rgba(255, 255, 255, 0.05); +} + +.cm-searchMatch { + background: #ffa; + background: rgba(255, 255, 0, 0.4); +} + +.cm-searchMatch-selected { + background: #ff8; + background: rgba(255, 255, 0, 0.6); +} diff --git a/src/components/marked/users-schema.ts b/src/components/interactive-code-block/users-schema.ts similarity index 100% rename from src/components/marked/users-schema.ts rename to src/components/interactive-code-block/users-schema.ts diff --git a/src/components/interactive-code-block/variable-editor.tsx b/src/components/interactive-code-block/variable-editor.tsx new file mode 100644 index 0000000000..73fe6a5e7c --- /dev/null +++ b/src/components/interactive-code-block/variable-editor.tsx @@ -0,0 +1,114 @@ +import { Component } from "react" +import { EditorView } from "@codemirror/view" +import { EditorState } from "@codemirror/state" +import { json } from "@codemirror/lang-json" +import { history } from "@codemirror/commands" +import { syntaxHighlighting } from "@codemirror/language" +import { codeMirrorThemeExtension } from "./codemirror-theme" + +interface VariableEditorProps { + value: string + variableToType?: any + onEdit?: (value: string) => void + onRunQuery?: () => void + onHintInformationRender?: (el: HTMLElement) => void +} + +/** + * VariableEditor + * + * An instance of CodeMirror 6 for editing variables defined in QueryEditor. + * + * Props: + * + * - variableToType: A mapping of variable name to GraphQLType. + * - value: The text of the editor. + * - onEdit: A function called when the editor changes, given the edited text. + * + */ +export class VariableEditor extends Component{ + private view: EditorView | null = null + private domNode: HTMLDivElement | null = null + private cachedValue: string + private ignoreChangeEvent = false + + constructor(props: VariableEditorProps) { + super(props) + + // Keep a cached version of the value, this cache will be updated when the + // editor is updated, which can later be used to protect the editor from + // unnecessary updates during the update lifecycle. + this.cachedValue = props.value || "" + } + + componentDidMount() { + if (!this.domNode) return + + // Create editor state for JSON (variables are JSON) + const state = EditorState.create({ + doc: this.props.value || "", + extensions: [ + history(), + json(), + codeMirrorThemeExtension, + EditorView.updateListener.of(update => { + if (update.docChanged && !this.ignoreChangeEvent) { + this.cachedValue = update.state.doc.toString() + if (this.props.onEdit) { + this.props.onEdit(this.cachedValue) + } + } + }), + ], + }) + + // Create editor view + this.view = new EditorView({ + state, + parent: this.domNode, + }) + } + + componentDidUpdate(prevProps: VariableEditorProps) { + if (!this.view) return + + // Ensure the changes caused by this update are not interpreted as + // user-input changes which could otherwise result in an infinite + // event loop. + this.ignoreChangeEvent = true + + if ( + this.props.value !== prevProps.value && + this.props.value !== this.cachedValue + ) { + this.cachedValue = this.props.value + this.view.dispatch({ + changes: { + from: 0, + to: this.view.state.doc.length, + insert: this.props.value || "", + }, + }) + } + + this.ignoreChangeEvent = false + } + + componentWillUnmount() { + if (this.view) { + this.view.destroy() + this.view = null + } + } + + render() { + return ( + { + this.domNode = e + }} + /> + ) + } +} diff --git a/src/components/marked/index.tsx b/src/components/marked/index.tsx deleted file mode 100644 index 39730051bc..0000000000 --- a/src/components/marked/index.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import dynamic from "next/dynamic" -import { StarWarsSchema } from "./swapi-schema" -import { UsersSchema } from "./users-schema" - -const SCHEMA_MAP = { - StarWars: StarWarsSchema, - Users: UsersSchema, -} as const - -type SchemaKey = keyof typeof SCHEMA_MAP - -type Metadata = { - graphiql?: boolean - variables?: unknown - schema?: SchemaKey -} - -const MiniGraphiQL = dynamic(() => import("./mini-graphiQL"), { ssr: true }) - -export function Marked({ children }: { children: string }) { - const codeMatch = children.match(/```graphql\s*\n([\s\S]*?)```/) - const blockContent = codeMatch?.[1] - const [firstLine, ...rest] = (blockContent || "").split("\n") - // First line must contain the metadata JSON comment: # { … } - const metaMatch = firstLine.match(/^\s*#\s*({.*})\s*$/) - - if (!metaMatch) { - throw new Error( - `Invalid GraphiQL metadata JSON: ${firstLine}. MiniGraphQL shouldn't be used here.`, - ) - } - - let meta: Metadata - try { - meta = JSON.parse(metaMatch[1]) as Metadata - } catch { - throw new Error(`Invalid GraphiQL metadata JSON: ${metaMatch[1]}`) - } - - const query = rest.join("\n") - const variables = meta.variables - ? JSON.stringify(meta.variables, null, 2) - : "" - const schema = SCHEMA_MAP[meta.schema ?? "StarWars"] - - return-} diff --git a/src/components/marked/mini-graphiQL.tsx b/src/components/marked/mini-graphiQL.tsx deleted file mode 100644 index 37c1d3e8db..0000000000 --- a/src/components/marked/mini-graphiQL.tsx +++ /dev/null @@ -1,608 +0,0 @@ -// @ts-nocheck -/** - * Copyright (c) 2015, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the license found in the - * LICENSE file in the root directory of this source tree. - */ - -import { Component } from "react" -import { marked } from "marked" - -import { graphql, formatError, parse, typeFromAST } from "graphql" - -export type MiniGraphiQLProps = { - schema: GraphQLSchema - query: string - variables: string -} - -export default class MiniGraphiQL extends Component { - // Lifecycle - - constructor(props) { - super() - const query = props.query.replace(/^\s+/, "") - - // Initialize state - this.state = { - query: query, - variables: props.variables, - response: null, - variableToType: getVariableToType(props.schema, query), - } - - this._editorQueryID = 0 - } - - render() { - const editor = ( - - ) - - return ( - - {Object.keys(this.state.variableToType).length > 0 ? ( -- ) - } - - componentDidMount() { - this._runQueryFromEditor() - } - - // Private methods - - _runQueryFromEditor() { - this.setState({ - variableToType: getVariableToType(this.props.schema, this.state.query), - }) - this._runQuery() - } - - async _runQuery() { - this._editorQueryID++ - const queryID = this._editorQueryID - try { - const result = await graphql({ - schema: this.props.schema, - source: this.state.query, - variableValues: JSON.parse(this.state.variables || "{}"), - rootValue: this.props.rootValue, - }) - - if (result.errors) { - result.errors = result.errors.map(formatError) - } - - if (queryID === this._editorQueryID) { - this.setState({ response: JSON.stringify(result, null, 2) }) - } - } catch (error) { - if (queryID === this._editorQueryID) { - this.setState({ response: JSON.stringify(error, null, 2) }) - } - } - } - - _handleEditQuery(value) { - this.setState({ query: value }) - } - - _handleEditVariables(value) { - this.setState({ variables: value }) - } -} - -/** - * QueryEditor - * - * Maintains an instance of CodeMirror responsible for editing a GraphQL query. - * - * Props: - * - * - schema: A GraphQLSchema instance enabling editor linting and hinting. - * - value: The text of the editor. - * - onEdit: A function called when the editor changes, given the edited text. - * - */ -class QueryEditor extends Component { - constructor(props) { - super() - - // Keep a cached version of the value, this cache will be updated when the - // editor is updated, which can later be used to protect the editor from - // unnecessary updates during the update lifecycle. - this.cachedValue = props.value || "" - } - - /** - * Public API for retrieving the CodeMirror instance from this - * React component. - */ - getCodeMirror() { - return this.editor - } - - componentDidMount() { - const CodeMirror = require("codemirror") - require("codemirror/addon/hint/show-hint") - require("codemirror/addon/comment/comment") - require("codemirror/addon/edit/matchbrackets") - require("codemirror/addon/edit/closebrackets") - require("codemirror/addon/lint/lint") - require("codemirror/keymap/sublime") - require("codemirror-graphql/hint") - require("codemirror-graphql/lint") - require("codemirror-graphql/mode") - - this.editor = CodeMirror(this.domNode, { - value: this.props.value || "", - viewportMargin: Infinity, - tabSize: 2, - mode: "graphql", - theme: "graphiql", - keyMap: "sublime", - autoCloseBrackets: true, - matchBrackets: true, - showCursorWhenSelecting: true, - lint: { - schema: this.props.schema, - onUpdateLinting: this._didLint.bind(this), - }, - hintOptions: { - schema: this.props.schema, - closeOnUnfocus: true, - completeSingle: false, - }, - extraKeys: { - "Cmd-Space": () => this.editor.showHint({ completeSingle: false }), - "Ctrl-Space": () => this.editor.showHint({ completeSingle: false }), - "Alt-Space": () => this.editor.showHint({ completeSingle: false }), - "Shift-Space": () => this.editor.showHint({ completeSingle: false }), - - "Cmd-Enter": () => { - if (this.props.onRunQuery) { - this.props.onRunQuery() - } - }, - "Ctrl-Enter": () => { - if (this.props.onRunQuery) { - this.props.onRunQuery() - } - }, - - // Editor improvements - "Ctrl-Left": "goSubwordLeft", - "Ctrl-Right": "goSubwordRight", - "Alt-Left": "goGroupLeft", - "Alt-Right": "goGroupRight", - }, - }) - - this.editor.on("change", this._onEdit.bind(this)) - this.editor.on("keyup", this._onKeyUp.bind(this)) - this.editor.on("hasCompletion", this._onHasCompletion.bind(this)) - } - - componentWillUnmount() { - this.editor = null - } - - componentDidUpdate(prevProps) { - // Ensure the changes caused by this update are not interpreted as - // user-input changes which could otherwise result in an infinite - // event loop. - this.ignoreChangeEvent = true - if (this.props.schema !== prevProps.schema) { - this.editor.options.lint.schema = this.props.schema - this.editor.options.hintOptions.schema = this.props.schema - CodeMirror.signal(this.editor, "change", this.editor) - } - if ( - this.props.value !== prevProps.value && - this.props.value !== this.cachedValue - ) { - this.cachedValue = this.props.value - this.editor.setValue(this.props.value) - } - this.ignoreChangeEvent = false - } - - _didLint(annotations) { - if (annotations.length === 0) { - this.props.runQuery() - } - } - - _onKeyUp(cm, event) { - const code = event.keyCode - if ( - (code >= 65 && code <= 90) || // letters - (!event.shiftKey && code >= 48 && code <= 57) || // numbers - (event.shiftKey && code === 189) || // underscore - (event.shiftKey && code === 50) || // @ - (event.shiftKey && code === 57) // ( - ) { - this.editor.execCommand("autocomplete") - } - } - - _onEdit() { - if (!this.ignoreChangeEvent) { - this.cachedValue = this.editor.getValue() - if (this.props.onEdit) { - this.props.onEdit(this.cachedValue) - } - } - } - - _onHasCompletion(cm, data) { - onHasCompletion(cm, data, this.props.onHintInformationRender) - } - - render() { - return ( -- {editor} -- ) : ( - editor - )} -- - (this.domNode = e)}> - Operation -- ) - } -} - -/** - * ResultViewer - * - * Maintains an instance of CodeMirror for viewing a GraphQL response. - * - * Props: - * - * - value: The text of the editor. - * - */ -class ResultViewer extends Component { - componentDidMount() { - const CodeMirror = require("codemirror") - require("codemirror-graphql/results/mode") - - this.viewer = CodeMirror(this.domNode, { - value: this.props.value || "", - viewportMargin: Infinity, - readOnly: true, - theme: "graphiql", - mode: "graphql-results", - keyMap: "sublime", - extraKeys: { - // Editor improvements - "Ctrl-Left": "goSubwordLeft", - "Ctrl-Right": "goSubwordRight", - "Alt-Left": "goGroupLeft", - "Alt-Right": "goGroupRight", - }, - }) - } - - componentWillUnmount() { - this.viewer = null - } - - shouldComponentUpdate(nextProps) { - return this.props.value !== nextProps.value - } - - componentDidUpdate() { - this.viewer.setValue(this.props.value || "") - } - - render() { - return ( -(this.domNode = e)}> - Response -- ) - } -} - -/** - * VariableEditor - * - * An instance of CodeMirror for editing variables defined in QueryEditor. - * - * Props: - * - * - variableToType: A mapping of variable name to GraphQLType. - * - value: The text of the editor. - * - onEdit: A function called when the editor changes, given the edited text. - * - */ -class VariableEditor extends Component { - constructor(props) { - super() - - // Keep a cached version of the value, this cache will be updated when the - // editor is updated, which can later be used to protect the editor from - // unnecessary updates during the update lifecycle. - this.cachedValue = props.value || "" - this._onKeyUp = this.onKeyUp.bind(this) - this._onEdit = this.onEdit.bind(this) - this._onHasCompletion = this.onHasCompletion.bind(this) - } - - componentDidMount() { - // Lazily require to ensure requiring GraphiQL outside of a Browser context - // does not produce an error. - const CodeMirror = require("codemirror") - require("codemirror/addon/hint/show-hint") - require("codemirror/addon/edit/matchbrackets") - require("codemirror/addon/edit/closebrackets") - require("codemirror/addon/lint/lint") - require("codemirror/keymap/sublime") - require("codemirror-graphql/variables/hint") - require("codemirror-graphql/variables/lint") - require("codemirror-graphql/variables/mode") - - this.editor = CodeMirror(this.domNode, { - value: this.props.value || "", - viewportMargin: Infinity, - tabSize: 2, - mode: "graphql-variables", - theme: "graphiql", - keyMap: "sublime", - autoCloseBrackets: true, - matchBrackets: true, - showCursorWhenSelecting: true, - lint: { - variableToType: this.props.variableToType, - onUpdateLinting: this._didLint.bind(this), - }, - hintOptions: { - variableToType: this.props.variableToType, - }, - extraKeys: { - "Cmd-Space": () => this.editor.showHint({ completeSingle: false }), - "Ctrl-Space": () => this.editor.showHint({ completeSingle: false }), - "Alt-Space": () => this.editor.showHint({ completeSingle: false }), - "Shift-Space": () => this.editor.showHint({ completeSingle: false }), - - "Cmd-Enter"() { - if (this.props.onRunQuery) { - this.props.onRunQuery() - } - }, - "Ctrl-Enter"() { - if (this.props.onRunQuery) { - this.props.onRunQuery() - } - }, - - // Editor improvements - "Ctrl-Left": "goSubwordLeft", - "Ctrl-Right": "goSubwordRight", - "Alt-Left": "goGroupLeft", - "Alt-Right": "goGroupRight", - }, - }) - - this.editor.on("change", this._onEdit) - this.editor.on("keyup", this._onKeyUp) - this.editor.on("hasCompletion", this._onHasCompletion) - } - - componentDidUpdate(prevProps) { - const CodeMirror = require("codemirror") - - // Ensure the changes caused by this update are not interpreted as - // user-input changes which could otherwise result in an infinite - // event loop. - this.ignoreChangeEvent = true - if (this.props.variableToType !== prevProps.variableToType) { - this.editor.options.lint.variableToType = this.props.variableToType - this.editor.options.hintOptions.variableToType = this.props.variableToType - CodeMirror.signal(this.editor, "change", this.editor) - } - if ( - this.props.value !== prevProps.value && - this.props.value !== this.cachedValue - ) { - this.cachedValue = this.props.value - this.editor.setValue(this.props.value) - } - this.ignoreChangeEvent = false - } - - componentWillUnmount() { - this.editor.off("change", this._onEdit) - this.editor.off("keyup", this._onKeyUp) - this.editor.off("hasCompletion", this._onHasCompletion) - this.editor = null - } - - render() { - return ( -(this.domNode = e)}> - Variables -- ) - } - - _didLint(annotations) { - if (annotations.length === 0) { - this.props.onRunQuery() - } - } - - onKeyUp(cm, event) { - const code = event.keyCode - if ( - (code >= 65 && code <= 90) || // letters - (!event.shiftKey && code >= 48 && code <= 57) || // numbers - (event.shiftKey && code === 189) || // underscore - (event.shiftKey && code === 222) // " - ) { - this.editor.execCommand("autocomplete") - } - } - - onEdit() { - if (!this.ignoreChangeEvent) { - this.cachedValue = this.editor.getValue() - if (this.props.onEdit) { - this.props.onEdit(this.cachedValue) - } - } - } - - onHasCompletion(cm, data) { - onHasCompletion(cm, data, this.props.onHintInformationRender) - } -} - -/** - * Render a custom UI for CodeMirror's hint which includes additional info - * about the type and description for the selected context. - */ -function onHasCompletion(cm, data, onHintInformationRender) { - const CodeMirror = require("codemirror") - let wrapper - let information - - // When a hint result is selected, we touch the UI. - CodeMirror.on(data, "select", (ctx, el) => { - // Only the first time (usually when the hint UI is first displayed) - // do we create the wrapping node. - if (!wrapper) { - // Wrap the existing hint UI, so we have a place to put information. - const hintsUl = el.parentNode - const container = hintsUl.parentNode - wrapper = document.createElement("div") - container.appendChild(wrapper) - - // CodeMirror vertically inverts the hint UI if there is not enough - // space below the cursor. Since this modified UI appends to the bottom - // of CodeMirror's existing UI, it could cover the cursor. This adjusts - // the positioning of the hint UI to accommodate. - let top = hintsUl.style.top - let bottom = "" - const cursorTop = cm.cursorCoords().top - if (parseInt(top, 10) < cursorTop) { - top = "" - bottom = window.innerHeight - cursorTop + 3 + "px" - } - - // Style the wrapper, remove positioning from hints. Note that usage - // of this option will need to specify CSS to remove some styles from - // the existing hint UI. - wrapper.className = "CodeMirror-hints-wrapper" - wrapper.style.left = hintsUl.style.left - wrapper.style.top = top - wrapper.style.bottom = bottom - hintsUl.style.left = "" - hintsUl.style.top = "" - - // This "information" node will contain the additional info about the - // highlighted typeahead option. - information = document.createElement("div") - information.className = "CodeMirror-hint-information" - if (bottom) { - wrapper.appendChild(information) - wrapper.appendChild(hintsUl) - } else { - wrapper.appendChild(hintsUl) - wrapper.appendChild(information) - } - - // When CodeMirror attempts to remove the hint UI, we detect that it was - // removed from our wrapper and in turn remove the wrapper from the - // original container. - let onRemoveFn - const observer = new MutationObserver(mutationsList => { - for (const mutation of mutationsList) { - // Check if the hintsUl element was removed - if (mutation.removedNodes) { - mutation.removedNodes.forEach(node => { - if (node === hintsUl) { - // Cleanup logic - observer.disconnect() // Stop observing - wrapper.parentNode.removeChild(wrapper) - wrapper = null - information = null - onRemoveFn = null - } - }) - } - } - }) - - // Start observing the wrapper for child node removals - observer.observe(wrapper, { childList: true, subtree: false }) - } - - // Now that the UI has been set up, add info to information. - const description = ctx.description - ? marked(ctx.description, { smartypants: true }) - : "Self descriptive." - const type = ctx.type - ? '' + String(ctx.type) + "" - : "" - information.innerHTML = - '' + - (description.slice(0, 3) === "" - - // Additional rendering? - if (onHintInformationRender) { - onHintInformationRender(information) - } - }) -} - -function getVariableToType(schema, documentStr) { - if (!documentStr || !schema) { - return {} - } - - try { - const documentAST = parse(documentStr) - const variableToType = Object.create(null) - documentAST.definitions.forEach(definition => { - if (definition.kind === "OperationDefinition") { - const variableDefinitions = definition.variableDefinitions - if (variableDefinitions) { - variableDefinitions.forEach(({ variable, type }) => { - const inputType = typeFromAST(schema, type) - if (inputType) { - variableToType[variable.name.value] = inputType - } - }) - } - } - }) - return variableToType - } catch (e) { - // ignore - } - - return {} -} diff --git a/src/components/pre/code-block-label.tsx b/src/components/pre/code-block-label.tsx new file mode 100644 index 0000000000..2de78c266e --- /dev/null +++ b/src/components/pre/code-block-label.tsx @@ -0,0 +1,32 @@ +import cn from "clsx" +import type { FC, ReactElement, ReactNode } from "react" + +import classes from "./pre.module.css" + +interface CodeBlockLabelProps { + text: ReactNode + icon?: FC<{ className?: string }> + button?: ReactElement | false + className?: string +} + +export function CodeBlockLabel({ + text, + icon: Icon, + button, + className, +}: CodeBlockLabelProps): ReactElement { + return ( +" - ? "
" + type + description.slice(3) - : type + description) + - "
+ {Icon &&+ ) +} diff --git a/src/components/pre/index.tsx b/src/components/pre/index.tsx index 25bf431ff8..949dab6ddc 100644 --- a/src/components/pre/index.tsx +++ b/src/components/pre/index.tsx @@ -5,8 +5,9 @@ import { WordWrapIcon } from "nextra/icons" import { Button, CopyToClipboard } from "nextra/components" import classes from "./pre.module.css" +import { CodeBlockLabel } from "./code-block-label" -interface PreProps extends ComponentPropsWithoutRef<"pre"> { +export interface PreProps extends ComponentPropsWithoutRef<"pre"> { "data-filename"?: string "data-copy"?: "" "data-language"?: string @@ -39,20 +40,16 @@ export function Pre({ return (} + {text} + {button} + {filename && ( -- {Icon &&+} - {filename} - {copyButton} - )} -The examples in this guide are based on [a modified version of the SWAPI GraphQL schema](https://github.com/graphql/graphql.github.io/blob/source/src/components/marked/swapi-schema.tsx). Because these queries are designed for illustrative purposes, they will not run on the full version of the SWAPI GraphQL API due to differences between the two schemas. [You can try the full version of the API here.](https://graphql.org/swapi-graphql/) +The examples in this guide are based on [a modified version of the SWAPI GraphQL schema](https://github.com/graphql/graphql.github.io/blob/source/src/components/interactive-code-block/swapi-schema.tsx). Because these queries are designed for illustrative purposes, they will not run on the full version of the SWAPI GraphQL API due to differences between the two schemas. [You can try the full version of the API here.](https://graphql.org/swapi-graphql/) ## Next steps diff --git a/src/remark-graphiql-comment.js b/src/remark-graphiql-comment.js index 1cce6af551..05d7ed56f2 100644 --- a/src/remark-graphiql-comment.js +++ b/src/remark-graphiql-comment.js @@ -1,14 +1,19 @@ import { visit } from "unist-util-visit" +const MINI_GRAPHIQL_COMPONENT = "InteractiveCodeBlock" +const MINI_GRAPHIQL_PATH = "@/components/interactive-code-block" + export const remarkGraphiQLComment = () => ast => { const nodes = [] - const MINI_GRAPHIQL_COMPONENT = "Marked" - visit(ast, { type: "code", lang: "graphql" }, node => { + if ((node.meta || "").split(" ").includes("graphiql")) { + nodes.push(node) + return + } + const [firstLine] = node.value.split("\n") - const isGraphiQLComment = /graphiql["']?: ?true/.test(firstLine) - if (isGraphiQLComment) { + if (/graphiql["']?: ?true/.test(firstLine)) { nodes.push(node) } }) @@ -21,7 +26,7 @@ export const remarkGraphiQLComment = () => ast => { body: [ { type: "ImportDeclaration", - source: { type: "Literal", value: "@/components/marked" }, + source: { type: "Literal", value: MINI_GRAPHIQL_PATH }, specifiers: [ { type: "ImportSpecifier", diff --git a/test/e2e/graphql-interactive.spec.ts b/test/e2e/graphql-interactive.spec.ts index 990db07467..84946b81cd 100644 --- a/test/e2e/graphql-interactive.spec.ts +++ b/test/e2e/graphql-interactive.spec.ts @@ -5,9 +5,9 @@ test.describe("interactive examples", () => { page, }) => { await page.goto("/learn") - await page.waitForSelector(".CodeMirror", { timeout: 10000 }) + await page.waitForSelector(".cm-editor", { timeout: 10000 }) - const editors = page.locator(".miniGraphiQL") + const editors = page.locator(".cm-editor") let heroEditor: Locator | null = null for (let i = 0; i < (await editors.count()); i++) { @@ -23,12 +23,7 @@ test.describe("interactive examples", () => { throw new Error("Could not find hero GraphQL editor") } - const codeMirrorEditor = heroEditor.locator(".CodeMirror").first() - await expect(codeMirrorEditor).toBeVisible() - - await codeMirrorEditor.click() - - const codeLines = codeMirrorEditor.locator(".CodeMirror-line") + const codeLines = heroEditor.locator(".cm-line") // Find the line containing "name" and click after it for (let i = 0; i < (await codeLines.count()); i++) { @@ -47,20 +42,24 @@ test.describe("interactive examples", () => { await page.keyboard.type("ap") await page.keyboard.press("Control+Space") - const autoCompleteMenu = page.locator(".CodeMirror-hints") + const autoCompleteMenu = page.locator(".cm-tooltip-autocomplete") await expect(autoCompleteMenu).toBeVisible({ timeout: 5000 }) const appearsInSuggestion = page - .locator(".CodeMirror-hints li") + .locator(".cm-completionLabel") .filter({ hasText: "appearsIn" }) + expect(page.locator(".cm-completionDetail").first()).toHaveText( + "[Episode]!", + ) + if (await appearsInSuggestion.isVisible()) { await appearsInSuggestion.click() } else { await page.keyboard.press("Enter") } - const resultViewer = heroEditor.locator(".result-window") + const resultViewer = page.locator(".result-window").first() await expect(resultViewer).toBeVisible() await expect @@ -91,7 +90,7 @@ test.describe("interactive examples", () => { await page.waitForLoadState("networkidle") // Find the mutation example that has GraphiQL enabled - const editors = page.locator(".miniGraphiQL") + const editors = page.locator(".cm-editor") let mutationEditor: Locator | null = null for (let i = 0; i < (await editors.count()); i++) {