mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-11-01 17:59:52 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ab2e4c862 | |||
| fb3819ab00 | |||
| 7212973f34 | |||
| af44e0436b | |||
| 3f9c35d505 | |||
| 2c13f8b95c | |||
| 4dab1c56e5 | |||
| 4723f03ef1 | |||
| 278689e0a0 | |||
| 886e204e68 | |||
| c59947ab39 | |||
| 4f0a772a21 |
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"gitbook": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix alignment of `prominent` search bar on full-width pages
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"@gitbook/openapi-parser": patch
|
||||||
|
"@gitbook/react-openapi": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Update scalar
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"gitbook": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Restyle section group dropdown
|
||||||
@@ -35,21 +35,11 @@ jobs:
|
|||||||
- name: Sets env vars for production
|
- name: Sets env vars for production
|
||||||
if: github.ref == 'refs/heads/main'
|
if: github.ref == 'refs/heads/main'
|
||||||
run: |
|
run: |
|
||||||
echo "SENTRY_ENVIRONMENT=production" >> $GITHUB_ENV
|
|
||||||
echo "GITBOOK_ASSETS_PREFIX=https://static.gitbook.com" >> $GITHUB_ENV
|
echo "GITBOOK_ASSETS_PREFIX=https://static.gitbook.com" >> $GITHUB_ENV
|
||||||
- name: Sets env vars for preview
|
|
||||||
if: github.ref != 'refs/heads/main'
|
|
||||||
run: |
|
|
||||||
echo "SENTRY_ENVIRONMENT=preview" >> $GITHUB_ENV
|
|
||||||
- name: Build Next.js with next-on-pages
|
- name: Build Next.js with next-on-pages
|
||||||
run: bun run turbo gitbook#build:cloudflare
|
run: bun run turbo gitbook#build:cloudflare
|
||||||
env:
|
env:
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
|
||||||
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: ${{ secrets.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY }}
|
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: ${{ secrets.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY }}
|
||||||
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
|
|
||||||
SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }}
|
|
||||||
SENTRY_DSN: ${{ vars.SENTRY_DSN }}
|
|
||||||
SENTRY_RELEASE: ${{ github.sha }}
|
|
||||||
- id: deploy
|
- id: deploy
|
||||||
name: Deploy to Cloudflare
|
name: Deploy to Cloudflare
|
||||||
uses: cloudflare/wrangler-action@v3.14.0
|
uses: cloudflare/wrangler-action@v3.14.0
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
# @gitbook/colors
|
# @gitbook/colors
|
||||||
|
|
||||||
|
## 0.3.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 4f0a772: Override tint lightness if supplied color is out of bounds
|
||||||
|
|
||||||
## 0.2.0
|
## 0.2.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|
||||||
- 445baaa: Initial release
|
- 445baaa: Initial release
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"default": "./dist/index.js"
|
"default": "./dist/index.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.5.3"
|
"typescript": "^5.5.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -184,6 +184,7 @@ export function colorScale(
|
|||||||
const mixColor = mix?.color ? rgbToOklch(hexToRgbArray(mix.color)) : null;
|
const mixColor = mix?.color ? rgbToOklch(hexToRgbArray(mix.color)) : null;
|
||||||
const foregroundColor = rgbToOklch(hexToRgbArray(foreground));
|
const foregroundColor = rgbToOklch(hexToRgbArray(foreground));
|
||||||
const backgroundColor = rgbToOklch(hexToRgbArray(background));
|
const backgroundColor = rgbToOklch(hexToRgbArray(background));
|
||||||
|
let mapping = darkMode ? colorMixMapping.dark : colorMixMapping.light;
|
||||||
|
|
||||||
if (mixColor && mix?.ratio && mix.ratio > 0) {
|
if (mixColor && mix?.ratio && mix.ratio > 0) {
|
||||||
// If defined, we mix in a (tiny) bit of the mix color with the base color.
|
// If defined, we mix in a (tiny) bit of the mix color with the base color.
|
||||||
@@ -192,7 +193,20 @@ export function colorScale(
|
|||||||
baseColor.H = mix.color === DEFAULT_TINT_COLOR ? baseColor.H : mixColor.H;
|
baseColor.H = mix.color === DEFAULT_TINT_COLOR ? baseColor.H : mixColor.H;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapping = darkMode ? colorMixMapping.dark : colorMixMapping.light;
|
if (
|
||||||
|
(darkMode && baseColor.L < backgroundColor.L) ||
|
||||||
|
(!darkMode && baseColor.L > backgroundColor.L)
|
||||||
|
) {
|
||||||
|
// If the supplied color is outside of our lightness bounds, use the supplied color's lightness.
|
||||||
|
// This is mostly used to allow darker-than-dark backgrounds for brands that specifically want that look.
|
||||||
|
const difference = (backgroundColor.L - baseColor.L) / backgroundColor.L;
|
||||||
|
backgroundColor.L = baseColor.L;
|
||||||
|
// At the edges of the scale, the subtle lightness changes stop being perceptible. We need to amp up our mapping to still stand out.
|
||||||
|
const amplifier = 1;
|
||||||
|
mapping = mapping.map((step, index) =>
|
||||||
|
index < 9 ? step + step * amplifier * difference : step
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const result = [];
|
const result = [];
|
||||||
|
|
||||||
|
|||||||
@@ -23,8 +23,5 @@
|
|||||||
## Caching
|
## Caching
|
||||||
# GITBOOK_OUTPUT_CACHE=true
|
# GITBOOK_OUTPUT_CACHE=true
|
||||||
|
|
||||||
### Sentry ###
|
|
||||||
# SENTRY_DSN=xxx
|
|
||||||
|
|
||||||
### Silent logs
|
### Silent logs
|
||||||
# SILENT=true
|
# SILENT=true
|
||||||
@@ -23,9 +23,6 @@ next-env.d.ts
|
|||||||
# visual tests
|
# visual tests
|
||||||
screenshots/
|
screenshots/
|
||||||
|
|
||||||
# Sentry Config File
|
|
||||||
.sentryclirc
|
|
||||||
|
|
||||||
/test-results/
|
/test-results/
|
||||||
/playwright-report/
|
/playwright-report/
|
||||||
/blob-report/
|
/blob-report/
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
# gitbook
|
# gitbook
|
||||||
|
|
||||||
|
## 0.8.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 886e204: Update OpenAPI operation path design
|
||||||
|
- Updated dependencies [886e204]
|
||||||
|
- Updated dependencies [4f0a772]
|
||||||
|
- @gitbook/react-openapi@1.1.5
|
||||||
|
- @gitbook/colors@0.3.0
|
||||||
|
|
||||||
## 0.8.0
|
## 0.8.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
@@ -188,6 +188,7 @@ export function runTestCases(testCases: TestsCase[]) {
|
|||||||
const screenshotOptions = testEntry.screenshot;
|
const screenshotOptions = testEntry.screenshot;
|
||||||
if (screenshotOptions !== false) {
|
if (screenshotOptions !== false) {
|
||||||
await argosScreenshot(page, `${testCase.name} - ${testEntry.name}`, {
|
await argosScreenshot(page, `${testCase.name} - ${testEntry.name}`, {
|
||||||
|
animations: 'disabled',
|
||||||
viewports: ['macbook-16', 'macbook-13', 'ipad-2', 'iphone-x'],
|
viewports: ['macbook-16', 'macbook-13', 'ipad-2', 'iphone-x'],
|
||||||
argosCSS: `
|
argosCSS: `
|
||||||
/* Hide Intercom */
|
/* Hide Intercom */
|
||||||
|
|||||||
@@ -1,83 +1,49 @@
|
|||||||
const { withSentryConfig } = require('@sentry/nextjs');
|
module.exports = {
|
||||||
|
env: {
|
||||||
module.exports = withSentryConfig(
|
BUILD_VERSION: (process.env.GITHUB_SHA ?? '').slice(0, 7),
|
||||||
{
|
GITBOOK_ASSETS_PREFIX: process.env.GITBOOK_ASSETS_PREFIX,
|
||||||
env: {
|
GITBOOK_ICONS_URL: process.env.GITBOOK_ICONS_URL,
|
||||||
BUILD_VERSION: (process.env.GITHUB_SHA ?? '').slice(0, 7),
|
GITBOOK_ICONS_TOKEN: process.env.GITBOOK_ICONS_TOKEN,
|
||||||
SENTRY_RELEASE: process.env.SENTRY_RELEASE ?? '',
|
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY,
|
||||||
SENTRY_DSN: process.env.SENTRY_DSN ?? '',
|
|
||||||
SENTRY_ENVIRONMENT: process.env.SENTRY_ENVIRONMENT ?? 'development',
|
|
||||||
GITBOOK_ASSETS_PREFIX: process.env.GITBOOK_ASSETS_PREFIX,
|
|
||||||
GITBOOK_ICONS_URL: process.env.GITBOOK_ICONS_URL,
|
|
||||||
GITBOOK_ICONS_TOKEN: process.env.GITBOOK_ICONS_TOKEN,
|
|
||||||
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY,
|
|
||||||
},
|
|
||||||
|
|
||||||
webpack(config, { dev, webpack }) {
|
|
||||||
config.resolve.fallback = {
|
|
||||||
...config.resolve.fallback,
|
|
||||||
|
|
||||||
// Required for `swagger2openapi` to work:
|
|
||||||
fs: false,
|
|
||||||
path: false,
|
|
||||||
http: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Tree shake debug code for Sentry
|
|
||||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/tree-shaking/#tree-shaking-with-nextjs
|
|
||||||
if (!dev) {
|
|
||||||
config.plugins.push(
|
|
||||||
new webpack.DefinePlugin({
|
|
||||||
__SENTRY_DEBUG__: false,
|
|
||||||
// We always init Sentry with enableTracing: false for now, so this is useless
|
|
||||||
__SENTRY_TRACING__: false,
|
|
||||||
__RRWEB_EXCLUDE_IFRAME__: true,
|
|
||||||
__RRWEB_EXCLUDE_SHADOW_DOM__: true,
|
|
||||||
__SENTRY_EXCLUDE_REPLAY_WORKER__: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
|
|
||||||
async headers() {
|
|
||||||
return [
|
|
||||||
// Cache all static assets for 1 year
|
|
||||||
{
|
|
||||||
source: '/~gitbook/static/:path*',
|
|
||||||
headers: [
|
|
||||||
{
|
|
||||||
key: 'Cache-Control',
|
|
||||||
value: 'public, max-age=31536000, immutable',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
assetPrefix: process.env.GITBOOK_ASSETS_PREFIX,
|
|
||||||
poweredByHeader: false,
|
|
||||||
|
|
||||||
images: {
|
|
||||||
remotePatterns: [
|
|
||||||
{
|
|
||||||
protocol: 'https',
|
|
||||||
hostname: '*.gitbook.io',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
|
||||||
release: process.env.SENTRY_RELEASE,
|
|
||||||
org: process.env.SENTRY_ORG,
|
|
||||||
project: process.env.SENTRY_PROJECT,
|
|
||||||
authToken: process.env.SENTRY_AUTH_TOKEN,
|
|
||||||
|
|
||||||
// Upload a larger set of source maps for prettier stack traces (increases build time)
|
webpack(config) {
|
||||||
widenClientFileUpload: true,
|
config.resolve.fallback = {
|
||||||
// Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers (increases server load)
|
...config.resolve.fallback,
|
||||||
tunnelRoute: '/~gitbook/monitoring',
|
|
||||||
disableLogger: true,
|
// Required for `swagger2openapi` to work:
|
||||||
}
|
fs: false,
|
||||||
);
|
path: false,
|
||||||
|
http: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
|
||||||
|
async headers() {
|
||||||
|
return [
|
||||||
|
// Cache all static assets for 1 year
|
||||||
|
{
|
||||||
|
source: '/~gitbook/static/:path*',
|
||||||
|
headers: [
|
||||||
|
{
|
||||||
|
key: 'Cache-Control',
|
||||||
|
value: 'public, max-age=31536000, immutable',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
assetPrefix: process.env.GITBOOK_ASSETS_PREFIX,
|
||||||
|
poweredByHeader: false,
|
||||||
|
|
||||||
|
images: {
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
protocol: 'https',
|
||||||
|
hostname: '*.gitbook.io',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "gitbook",
|
"name": "gitbook",
|
||||||
"version": "0.8.0",
|
"version": "0.8.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "env-cmd --silent -f ../../.env.local next dev",
|
"dev": "env-cmd --silent -f ../../.env.local next dev",
|
||||||
@@ -29,7 +29,6 @@
|
|||||||
"@radix-ui/react-checkbox": "^1.0.4",
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
"@radix-ui/react-navigation-menu": "^1.2.3",
|
"@radix-ui/react-navigation-menu": "^1.2.3",
|
||||||
"@radix-ui/react-popover": "^1.0.7",
|
"@radix-ui/react-popover": "^1.0.7",
|
||||||
"@sentry/nextjs": "8.35.0",
|
|
||||||
"@sindresorhus/fnv1a": "^3.1.0",
|
"@sindresorhus/fnv1a": "^3.1.0",
|
||||||
"@tailwindcss/container-queries": "^0.1.1",
|
"@tailwindcss/container-queries": "^0.1.1",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
import * as Sentry from '@sentry/browser';
|
|
||||||
|
|
||||||
const dsn = process.env.SENTRY_DSN;
|
|
||||||
if (dsn) {
|
|
||||||
Sentry.init({
|
|
||||||
dsn,
|
|
||||||
release: process.env.SENTRY_RELEASE,
|
|
||||||
|
|
||||||
// Disable tracing as it creates additional requests in an env where subrequests are limited.
|
|
||||||
tracesSampleRate: 0,
|
|
||||||
|
|
||||||
// Disable transactions as it creates additional requests in an env where subrequests are limited.
|
|
||||||
// https://docs.sentry.io/platforms/node/configuration/filtering/#using--3
|
|
||||||
beforeSendTransaction: () => {
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { captureException } from '@sentry/nextjs';
|
|
||||||
import NextError from 'next/error';
|
import NextError from 'next/error';
|
||||||
import { useEffect } from 'react';
|
|
||||||
|
|
||||||
export default function GlobalError({ error }: { error: Error }) {
|
|
||||||
useEffect(() => {
|
|
||||||
captureException(error);
|
|
||||||
}, [error]);
|
|
||||||
|
|
||||||
|
export default function GlobalError() {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { captureException, withScope } from '@sentry/nextjs';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { Button } from '@/components/primitives/Button';
|
import { Button } from '@/components/primitives/Button';
|
||||||
import { t, useLanguage } from '@/intl/client';
|
import { t, useLanguage } from '@/intl/client';
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
@@ -11,18 +8,9 @@ export default function ErrorPage(props: {
|
|||||||
error: Error & { digest?: string };
|
error: Error & { digest?: string };
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
}) {
|
}) {
|
||||||
const { error, reset } = props;
|
const { reset } = props;
|
||||||
const language = useLanguage();
|
const language = useLanguage();
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
withScope((scope) => {
|
|
||||||
if ('_componentStack' in error && error._componentStack) {
|
|
||||||
scope.setExtra('componentStack', error._componentStack);
|
|
||||||
}
|
|
||||||
captureException(error);
|
|
||||||
});
|
|
||||||
}, [error]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={tcls(
|
className={tcls(
|
||||||
|
|||||||
@@ -264,14 +264,18 @@
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
.scalar-activate-button {
|
.scalar-activate-button {
|
||||||
@apply flex gap-1.5 items-center;
|
@apply flex gap-2 items-center;
|
||||||
@apply bg-primary-solid text-contrast-primary-solid hover:bg-primary-solid-hover hover:text-contrast-primary-solid-hover contrast-more:ring-1 rounded-md straight-corners:rounded-none place-self-start;
|
@apply bg-primary-solid text-contrast-primary-solid hover:bg-primary-solid-hover hover:text-contrast-primary-solid-hover contrast-more:ring-1 rounded-md straight-corners:rounded-none place-self-start;
|
||||||
@apply ring-1 ring-tint hover:ring-tint-hover;
|
@apply ring-1 ring-tint hover:ring-tint-hover;
|
||||||
@apply shadow-sm shadow-tint dark:shadow-tint-1 hover:shadow-md active:shadow-none;
|
@apply shadow-sm shadow-tint dark:shadow-tint-1 hover:shadow-md active:shadow-none;
|
||||||
@apply contrast-more:ring-tint-12 contrast-more:hover:ring-2 contrast-more:hover:ring-tint-12;
|
@apply contrast-more:ring-tint-12 contrast-more:hover:ring-2 contrast-more:hover:ring-tint-12;
|
||||||
@apply hover:scale-105 active:scale-100 transition-all;
|
@apply hover:scale-105 active:scale-100 transition-all;
|
||||||
@apply grow-0 shrink-0 truncate;
|
@apply grow-0 shrink-0 truncate;
|
||||||
@apply text-sm px-2.5 py-1;
|
@apply text-[13px] px-2 py-0.5 font-mono font-medium [word-spacing:-2px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.scalar-activate-button svg {
|
||||||
|
@apply size-2.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scalar-app-loading {
|
.scalar-app-loading {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* Layout Components */
|
/* Layout Components */
|
||||||
.openapi-operation {
|
.openapi-operation {
|
||||||
@apply flex-1 flex flex-col gap-4 mb-14;
|
@apply flex-1 flex flex-col gap-8 mb-14;
|
||||||
}
|
}
|
||||||
|
|
||||||
.openapi-schemas {
|
.openapi-schemas {
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.openapi-summary {
|
.openapi-summary {
|
||||||
@apply flex flex-col items-start justify-start gap-2;
|
@apply flex flex-col items-start justify-start gap-3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.openapi-deprecated {
|
.openapi-deprecated {
|
||||||
@@ -391,17 +391,30 @@
|
|||||||
@apply flex flex-row items-center h-fit;
|
@apply flex flex-row items-center h-fit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.openapi-codesample-footer {
|
||||||
|
@apply flex w-full justify-end;
|
||||||
|
}
|
||||||
|
|
||||||
/* Path */
|
/* Path */
|
||||||
.openapi-path {
|
.openapi-path {
|
||||||
@apply flex items-center bg-transparent text-sm gap-2 p-2 border rounded-md border-tint-subtle;
|
@apply flex items-center text-sm gap-2 h-fit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-path-variable {
|
||||||
|
@apply p-px min-w-[1.625rem] text-tint-strong font-normal w-fit justify-center items-center ring-1 ring-inset ring-tint bg-tint rounded text-sm leading-none before:!content-none after:!content-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-path-server {
|
||||||
|
@apply text-tint hidden md:inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.openapi-path .openapi-method {
|
.openapi-path .openapi-method {
|
||||||
@apply text-[0.813rem] m-0 px-1;
|
@apply text-[0.813rem] m-0 h-full items-center flex px-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.openapi-path-title {
|
.openapi-path-title {
|
||||||
@apply flex-1 relative font-normal whitespace-nowrap overflow-x-auto font-mono text-tint-strong;
|
@apply flex-1 relative font-normal whitespace-nowrap overflow-x-auto font-mono text-tint-strong/10;
|
||||||
|
@apply py-0.5 px-1 rounded hover:bg-tint cursor-pointer transition-colors;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
-ms-overflow-style: none;
|
-ms-overflow-style: none;
|
||||||
}
|
}
|
||||||
@@ -520,6 +533,10 @@
|
|||||||
@apply px-3 py-2 pt-2.5 border-t border-tint-subtle text-[0.813rem] text-tint;
|
@apply px-3 py-2 pt-2.5 border-t border-tint-subtle text-[0.813rem] text-tint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.openapi-tabs-footer .openapi-markdown {
|
||||||
|
@apply text-[0.813rem] text-tint;
|
||||||
|
}
|
||||||
|
|
||||||
/* Disclosure group */
|
/* Disclosure group */
|
||||||
.openapi-disclosure-group {
|
.openapi-disclosure-group {
|
||||||
@apply border-b border-tint-subtle relative;
|
@apply border-b border-tint-subtle relative;
|
||||||
@@ -631,3 +648,37 @@
|
|||||||
.openapi-section-schemas > .openapi-section-body > .openapi-schema-properties > .openapi-schema {
|
.openapi-section-schemas > .openapi-section-body > .openapi-schema-properties > .openapi-schema {
|
||||||
@apply p-2.5;
|
@apply p-2.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.openapi-tooltip {
|
||||||
|
@apply flex items-center gap-1 bg-tint-base border border-tint-subtle text-tint-strong rounded-md font-medium px-1.5 py-0.5 shadow-sm text-[13px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-tooltip svg {
|
||||||
|
@apply size-3 text-tint-strong;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-tooltip[data-entering] {
|
||||||
|
animation: tooltip-enter 0.2s ease-in-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-tooltip[data-exiting] {
|
||||||
|
animation: tooltip-leave 0.2s ease-in-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tooltip-enter {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tooltip-leave {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -106,7 +106,8 @@ export function Header(props: { context: GitBookSiteContext; withTopHeader?: boo
|
|||||||
'lg:ml-[max(calc((100%-18rem-48rem-3rem)/2),1.5rem)]', // container (100%) - sidebar (18rem) - content (48rem) - margin (3rem)
|
'lg:ml-[max(calc((100%-18rem-48rem-3rem)/2),1.5rem)]', // container (100%) - sidebar (18rem) - content (48rem) - margin (3rem)
|
||||||
'xl:ml-[max(calc((100%-18rem-48rem-14rem-3rem)/2),1.5rem)]', // container (100%) - sidebar (18rem) - content (48rem) - outline (14rem) - margin (3rem)
|
'xl:ml-[max(calc((100%-18rem-48rem-14rem-3rem)/2),1.5rem)]', // container (100%) - sidebar (18rem) - content (48rem) - outline (14rem) - margin (3rem)
|
||||||
'page-no-toc:lg:ml-[max(calc((100%-18rem-48rem-18rem-3rem)/2),0rem)]',
|
'page-no-toc:lg:ml-[max(calc((100%-18rem-48rem-18rem-3rem)/2),0rem)]',
|
||||||
'page-full-width:lg:ml-6',
|
'page-full-width:lg:ml-[max(calc((100%-18rem-103rem-3rem)/2),1.5rem)]',
|
||||||
|
'page-full-width:2xl:ml-[max(calc((100%-18rem-96rem-14rem+3rem)/2),1.5rem)]',
|
||||||
'lg:mr-auto',
|
'lg:mr-auto',
|
||||||
'order-last',
|
'order-last',
|
||||||
'md:order-[unset]',
|
'md:order-[unset]',
|
||||||
@@ -179,44 +180,46 @@ export function Header(props: { context: GitBookSiteContext; withTopHeader?: boo
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{sections || siteSpaces.length > 1 ? (
|
{sections || siteSpaces.length > 1 ? (
|
||||||
<div
|
<div className="scroll-nojump">
|
||||||
className={tcls(
|
|
||||||
'w-full',
|
|
||||||
'overflow-x-scroll',
|
|
||||||
'overflow-y-hidden',
|
|
||||||
'hide-scroll',
|
|
||||||
'-mb-4 pb-4', // Positive padding / negative margin allows the navigation menu indicator to show in a scroll viewƒ
|
|
||||||
!sections ? ['hidden', 'page-no-toc:flex'] : 'flex'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className={tcls(
|
className={tcls(
|
||||||
CONTAINER_STYLE,
|
'w-full',
|
||||||
'page-default-width:max-w-[unset]',
|
'overflow-x-scroll',
|
||||||
'grow',
|
'overflow-y-hidden',
|
||||||
'flex',
|
'hide-scroll',
|
||||||
'items-end',
|
'-mb-4 pb-4', // Positive padding / negative margin allows the navigation menu indicator to show in a scroll viewƒ
|
||||||
'page-default-width:2xl:px-[calc((100%-1536px+4rem)/2)]'
|
!sections ? ['hidden', 'page-no-toc:flex'] : 'flex'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{siteSpaces.length > 1 && (
|
<div
|
||||||
<div
|
className={tcls(
|
||||||
id="variants"
|
CONTAINER_STYLE,
|
||||||
className="my-2 mr-5 page-no-toc:flex hidden grow border-tint border-r pr-5 *:grow only:mr-0 only:border-none only:pr-0 sm:max-w-64"
|
'page-default-width:max-w-[unset]',
|
||||||
>
|
'grow',
|
||||||
<SpacesDropdown
|
'flex',
|
||||||
context={context}
|
'items-end',
|
||||||
siteSpace={siteSpace}
|
'page-default-width:2xl:px-[calc((100%-1536px+4rem)/2)]'
|
||||||
siteSpaces={siteSpaces}
|
)}
|
||||||
className="w-full grow py-1"
|
>
|
||||||
|
{siteSpaces.length > 1 && (
|
||||||
|
<div
|
||||||
|
id="variants"
|
||||||
|
className="my-2 mr-5 page-no-toc:flex hidden grow border-tint border-r pr-5 *:grow only:mr-0 only:border-none only:pr-0 sm:max-w-64"
|
||||||
|
>
|
||||||
|
<SpacesDropdown
|
||||||
|
context={context}
|
||||||
|
siteSpace={siteSpace}
|
||||||
|
siteSpaces={siteSpaces}
|
||||||
|
className="w-full grow py-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{sections && (
|
||||||
|
<SiteSectionTabs
|
||||||
|
sections={encodeClientSiteSections(context, sections)}
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
{sections && (
|
|
||||||
<SiteSectionTabs
|
|
||||||
sections={encodeClientSiteSections(context, sections)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export function PageBody(props: {
|
|||||||
// When in api page mode without the aside, we align with the border of the main content
|
// When in api page mode without the aside, we align with the border of the main content
|
||||||
'page-api-block:xl:max-2xl:pr-0',
|
'page-api-block:xl:max-2xl:pr-0',
|
||||||
// Max size to ensure one column in api is aligned with rest of content (2 x 3xl) + (gap-3 + 2) * px-12
|
// Max size to ensure one column in api is aligned with rest of content (2 x 3xl) + (gap-3 + 2) * px-12
|
||||||
'page-api-block:max-w-[1654px]',
|
'page-api-block:max-w-screen-2xl',
|
||||||
'page-api-block:mx-auto'
|
'page-api-block:mx-auto'
|
||||||
|
|
||||||
// page.layout.tableOfContents ? null : 'xl:ml-56'
|
// page.layout.tableOfContents ? null : 'xl:ml-56'
|
||||||
|
|||||||
@@ -143,6 +143,10 @@
|
|||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
width: calc(100% - var(--scrollbar-width));
|
width: calc(100% - var(--scrollbar-width));
|
||||||
}
|
}
|
||||||
|
body:has(.page-full-width) .scroll-nojump {
|
||||||
|
margin-left: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { captureException } from '@sentry/nextjs';
|
|
||||||
import { readStreamableValue } from 'ai/rsc';
|
import { readStreamableValue } from 'ai/rsc';
|
||||||
import assertNever from 'assert-never';
|
import assertNever from 'assert-never';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
@@ -132,12 +131,6 @@ export const SearchResults = React.forwardRef(function SearchResults(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!results) {
|
if (!results) {
|
||||||
captureException(
|
|
||||||
new Error(
|
|
||||||
`corrupt-cache: ${global ? 'searchAllSiteContent' : 'searchSiteSpaceContent'} is ${results}`
|
|
||||||
),
|
|
||||||
{ extra: { results } }
|
|
||||||
);
|
|
||||||
setResultsState({ results: [], fetching: false });
|
setResultsState({ results: [], fetching: false });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
import { Icon, type IconName } from '@gitbook/icons';
|
import { Icon, type IconName } from '@gitbook/icons';
|
||||||
|
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { type ClassValue, tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Icon shown beside a section in the site section tabs.
|
* Icon shown beside a section in the site section tabs.
|
||||||
*/
|
*/
|
||||||
export function SectionIcon(props: { icon: IconName; isActive: boolean }) {
|
export function SectionIcon(props: { icon: IconName; isActive: boolean; className?: ClassValue }) {
|
||||||
const { icon, isActive } = props;
|
const { icon, isActive, className } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Icon
|
<Icon
|
||||||
icon={icon}
|
icon={icon}
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'size-[1em] shrink-0 text-inherit opacity-8',
|
'size-[1em] shrink-0 text-inherit opacity-8',
|
||||||
isActive && 'text-inherit opacity-10'
|
isActive && 'text-inherit opacity-10',
|
||||||
|
className
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -112,10 +112,10 @@ export function SiteSectionTabs(props: { sections: ClientSiteSections }) {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<NavigationMenu.Indicator
|
<NavigationMenu.Indicator
|
||||||
className="top-full z-0 flex h-3 items-end justify-center duration-150 motion-safe:transition-[width,_transform] data-[state=hidden]:motion-safe:animate-fadeOut data-[state=visible]:motion-safe:animate-fadeIn"
|
className="fixed top-full z-50 flex h-3 items-end justify-center duration-150 motion-safe:transition-[width,_transform] data-[state=hidden]:motion-safe:animate-fadeOut data-[state=visible]:motion-safe:animate-fadeIn"
|
||||||
aria-hidden
|
aria-hidden
|
||||||
>
|
>
|
||||||
<div className="relative top-[70%] size-3 rotate-[225deg] rounded-tl-sm bg-tint shadow-1xs shadow-dark/1 dark:shadow-dark/4" />
|
<div className="relative top-1/2 size-3 rotate-45 rounded-tl-sm border-tint-subtle border-t border-l bg-tint-base" />
|
||||||
</NavigationMenu.Indicator>
|
</NavigationMenu.Indicator>
|
||||||
</NavigationMenu.List>
|
</NavigationMenu.List>
|
||||||
</div>
|
</div>
|
||||||
@@ -127,7 +127,7 @@ export function SiteSectionTabs(props: { sections: ClientSiteSections }) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<NavigationMenu.Viewport
|
<NavigationMenu.Viewport
|
||||||
className="relative mt-3 h-[var(--radix-navigation-menu-viewport-height)] w-[calc(100vw_-_2rem)] origin-[top_center] overflow-hidden rounded straight-corners:rounded-none bg-tint shadow-1xs shadow-dark/1 duration-250 data-[state=closed]:duration-150 motion-safe:transition-[width,_height,_transform] data-[state=closed]:motion-safe:animate-scaleOut data-[state=open]:motion-safe:animate-scaleIn md:mx-0 md:w-[var(--radix-navigation-menu-viewport-width)] dark:shadow-dark/4"
|
className="relative mt-3 h-[var(--radix-navigation-menu-viewport-height)] w-[calc(100vw_-_2rem)] origin-[top_center] overflow-hidden rounded-lg straight-corners:rounded-sm bg-tint-base shadow-lg shadow-tint-10/6 ring-1 ring-tint-subtle duration-250 data-[state=closed]:duration-150 motion-safe:transition-[width,_height,_transform] data-[state=closed]:motion-safe:animate-scaleOut data-[state=open]:motion-safe:animate-scaleIn md:mx-0 md:w-[var(--radix-navigation-menu-viewport-width)] dark:shadow-tint-1/6"
|
||||||
style={{
|
style={{
|
||||||
translate:
|
translate:
|
||||||
undefined /* don't move this to a Tailwind class as Radix renders viewport incorrectly for a few frames */,
|
undefined /* don't move this to a Tailwind class as Radix renders viewport incorrectly for a few frames */,
|
||||||
@@ -220,7 +220,7 @@ function SectionGroupTileList(props: {
|
|||||||
return (
|
return (
|
||||||
<ul
|
<ul
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'grid w-full p-2 sm:grid-cols-1 md:w-max',
|
'grid w-full gap-1 p-2 sm:grid-cols-1 md:w-max',
|
||||||
sections.length < MIN_ITEMS_FOR_COLS ? 'md:grid-cols-1' : 'md:grid-cols-2'
|
sections.length < MIN_ITEMS_FOR_COLS ? 'md:grid-cols-1' : 'md:grid-cols-2'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -246,17 +246,23 @@ function SectionGroupTile(props: { section: ClientSiteSection; isActive: boolean
|
|||||||
<Link
|
<Link
|
||||||
href={url}
|
href={url}
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'flex min-h-12 w-full select-none flex-col gap-2 rounded p-3 transition-colors hover:bg-tint-hover',
|
'flex w-full select-none flex-col gap-1 rounded straight-corners:rounded-none px-3 py-2 transition-colors hover:bg-tint-hover',
|
||||||
isActive
|
isActive ? 'text-primary' : 'text-tint-strong'
|
||||||
? 'text-primary hover:text-primary-strong focus:text-primary-strong'
|
|
||||||
: 'text-tint hover:text-tint-strong focus:text-tint-strong'
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex w-full items-center gap-2 font-medium light:text-dark dark:text-light">
|
<div className="flex w-full gap-2">
|
||||||
{icon ? <SectionIcon isActive={false} icon={icon as IconName} /> : null}
|
{icon ? (
|
||||||
<span className="min-w-0 truncate">{title}</span>
|
<SectionIcon
|
||||||
|
className="mt-[3px]"
|
||||||
|
isActive={false}
|
||||||
|
icon={icon as IconName}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{title}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-tint-subtle">{section.description}</p>
|
{section.description ? (
|
||||||
|
<p className="text-tint-subtle">{section.description}</p>
|
||||||
|
) : null}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -62,79 +62,81 @@ export function SpaceLayout(props: {
|
|||||||
spaceId={context.space.id}
|
spaceId={context.space.id}
|
||||||
>
|
>
|
||||||
<Header withTopHeader={withTopHeader} context={context} />
|
<Header withTopHeader={withTopHeader} context={context} />
|
||||||
<div
|
<div className="scroll-nojump">
|
||||||
className={tcls(
|
<div
|
||||||
'flex',
|
className={tcls(
|
||||||
'flex-col',
|
'flex',
|
||||||
'lg:flex-row',
|
'flex-col',
|
||||||
CONTAINER_STYLE,
|
'lg:flex-row',
|
||||||
|
CONTAINER_STYLE,
|
||||||
|
|
||||||
// Ensure the footer is display below the viewport even if the content is not enough
|
// Ensure the footer is display below the viewport even if the content is not enough
|
||||||
withFooter && 'min-h-[calc(100vh-64px)]',
|
withFooter && 'min-h-[calc(100vh-64px)]',
|
||||||
withTopHeader ? null : 'lg:min-h-screen'
|
withTopHeader ? null : 'lg:min-h-screen'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<TableOfContents
|
<TableOfContents
|
||||||
context={context}
|
context={context}
|
||||||
header={
|
header={
|
||||||
withTopHeader ? null : (
|
withTopHeader ? null : (
|
||||||
<div
|
<div
|
||||||
className={tcls(
|
|
||||||
'hidden',
|
|
||||||
'pr-4',
|
|
||||||
'lg:flex',
|
|
||||||
'grow-0',
|
|
||||||
'flex-wrap',
|
|
||||||
'dark:shadow-light/1',
|
|
||||||
'text-base/tight'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<HeaderLogo context={context} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
innerHeader={
|
|
||||||
// displays the search button and/or the space dropdown in the ToC according to the header/variant settings. E.g if there is no header, the search button will be displayed in the ToC.
|
|
||||||
<>
|
|
||||||
{!withTopHeader && (
|
|
||||||
<div className={tcls('hidden', 'lg:block')}>
|
|
||||||
<React.Suspense fallback={null}>
|
|
||||||
<SearchButton>
|
|
||||||
<span className={tcls('flex-1')}>
|
|
||||||
{t(
|
|
||||||
getSpaceLanguage(customization),
|
|
||||||
customization.aiSearch.enabled
|
|
||||||
? 'search_or_ask'
|
|
||||||
: 'search'
|
|
||||||
)}
|
|
||||||
...
|
|
||||||
</span>
|
|
||||||
</SearchButton>
|
|
||||||
</React.Suspense>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!withTopHeader && withSections && sections && (
|
|
||||||
<SiteSectionList
|
|
||||||
className={tcls('hidden', 'lg:block')}
|
|
||||||
sections={encodeClientSiteSections(context, sections)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isMultiVariants && (
|
|
||||||
<SpacesDropdown
|
|
||||||
context={context}
|
|
||||||
siteSpace={siteSpace}
|
|
||||||
siteSpaces={siteSpaces}
|
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'w-full',
|
'hidden',
|
||||||
'page-no-toc:hidden',
|
'pr-4',
|
||||||
'site-header-none:page-no-toc:flex'
|
'lg:flex',
|
||||||
|
'grow-0',
|
||||||
|
'flex-wrap',
|
||||||
|
'dark:shadow-light/1',
|
||||||
|
'text-base/tight'
|
||||||
)}
|
)}
|
||||||
/>
|
>
|
||||||
)}
|
<HeaderLogo context={context} />
|
||||||
</>
|
</div>
|
||||||
}
|
)
|
||||||
/>
|
}
|
||||||
<div className={tcls('flex-1', 'flex', 'flex-col')}>{children}</div>
|
innerHeader={
|
||||||
|
// displays the search button and/or the space dropdown in the ToC according to the header/variant settings. E.g if there is no header, the search button will be displayed in the ToC.
|
||||||
|
<>
|
||||||
|
{!withTopHeader && (
|
||||||
|
<div className={tcls('hidden', 'lg:block')}>
|
||||||
|
<React.Suspense fallback={null}>
|
||||||
|
<SearchButton>
|
||||||
|
<span className={tcls('flex-1')}>
|
||||||
|
{t(
|
||||||
|
getSpaceLanguage(customization),
|
||||||
|
customization.aiSearch.enabled
|
||||||
|
? 'search_or_ask'
|
||||||
|
: 'search'
|
||||||
|
)}
|
||||||
|
...
|
||||||
|
</span>
|
||||||
|
</SearchButton>
|
||||||
|
</React.Suspense>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!withTopHeader && withSections && sections && (
|
||||||
|
<SiteSectionList
|
||||||
|
className={tcls('hidden', 'lg:block')}
|
||||||
|
sections={encodeClientSiteSections(context, sections)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isMultiVariants && (
|
||||||
|
<SpacesDropdown
|
||||||
|
context={context}
|
||||||
|
siteSpace={siteSpace}
|
||||||
|
siteSpaces={siteSpaces}
|
||||||
|
className={tcls(
|
||||||
|
'w-full',
|
||||||
|
'page-no-toc:hidden',
|
||||||
|
'site-header-none:page-no-toc:flex'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className={tcls('flex-1', 'flex', 'flex-col')}>{children}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{withFooter ? <Footer context={context} /> : null}
|
{withFooter ? <Footer context={context} /> : null}
|
||||||
|
|||||||
-7
@@ -1,6 +1,5 @@
|
|||||||
import hash from 'object-hash';
|
import hash from 'object-hash';
|
||||||
|
|
||||||
import { captureException } from '../../sentry';
|
|
||||||
import { race, singletonMap } from '../async';
|
import { race, singletonMap } from '../async';
|
||||||
import { type TraceSpan, trace } from '../tracing';
|
import { type TraceSpan, trace } from '../tracing';
|
||||||
import { assertIsNotV2 } from '../v2';
|
import { assertIsNotV2 } from '../v2';
|
||||||
@@ -159,17 +158,11 @@ export function cache<Args extends any[], Result>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Detect empty cache entries to avoid returning them.
|
// Detect empty cache entries to avoid returning them.
|
||||||
// Also log in Sentry to investigate what cache is returning empty entries.
|
|
||||||
if (
|
if (
|
||||||
entry.data &&
|
entry.data &&
|
||||||
typeof entry.data === 'object' &&
|
typeof entry.data === 'object' &&
|
||||||
Object.keys(entry.data).length === 0
|
Object.keys(entry.data).length === 0
|
||||||
) {
|
) {
|
||||||
captureException(
|
|
||||||
new Error(
|
|
||||||
`Cache entry ${key} from ${backendName} is an empty object`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ const fetchFilesystemV1 = cache({
|
|||||||
get: async (url: string, options: CacheFunctionOptions) => {
|
get: async (url: string, options: CacheFunctionOptions) => {
|
||||||
const richFilesystem = await fetchFilesystemUncached(url, options);
|
const richFilesystem = await fetchFilesystemUncached(url, options);
|
||||||
return {
|
return {
|
||||||
// Cache for 4 hours
|
// Cache for 24 hours
|
||||||
ttl: 24 * 60 * 60,
|
ttl: 24 * 60 * 60,
|
||||||
// Revalidate every 2 hours
|
// Revalidate every 2 hours
|
||||||
revalidateBefore: 22 * 60 * 60,
|
revalidateBefore: 22 * 60 * 60,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { type ContentAPITokenPayload, CustomizationThemeMode, GitBookAPI } from '@gitbook/api';
|
import { type ContentAPITokenPayload, CustomizationThemeMode, GitBookAPI } from '@gitbook/api';
|
||||||
import { setContext, setTag } from '@sentry/nextjs';
|
|
||||||
import { getURLLookupAlternatives, normalizeURL } from '@v2/lib/data';
|
import { getURLLookupAlternatives, normalizeURL } from '@v2/lib/data';
|
||||||
import assertNever from 'assert-never';
|
import assertNever from 'assert-never';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
@@ -96,14 +95,6 @@ export async function middleware(request: NextRequest) {
|
|||||||
const { url, mode } = getInputURL(request);
|
const { url, mode } = getInputURL(request);
|
||||||
const isServerAction = request.headers.has('Next-Action');
|
const isServerAction = request.headers.has('Next-Action');
|
||||||
|
|
||||||
setTag('url', url.toString());
|
|
||||||
setContext('request', {
|
|
||||||
method: request.method,
|
|
||||||
url: url.toString(),
|
|
||||||
rawRequestURL: request.url,
|
|
||||||
userAgent: userAgent(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Redirect to normalize the URL
|
// Redirect to normalize the URL
|
||||||
const normalized = normalizeURL(url);
|
const normalized = normalizeURL(url);
|
||||||
if (normalized.toString() !== url.toString()) {
|
if (normalized.toString() !== url.toString()) {
|
||||||
@@ -148,14 +139,6 @@ export async function middleware(request: NextRequest) {
|
|||||||
return writeCookies(NextResponse.redirect(normalizedVA.toString()), resolved.cookies);
|
return writeCookies(NextResponse.redirect(normalizedVA.toString()), resolved.cookies);
|
||||||
}
|
}
|
||||||
|
|
||||||
setTag('space', resolved.space);
|
|
||||||
setContext('content', {
|
|
||||||
space: resolved.space,
|
|
||||||
changeRequest: resolved.changeRequest,
|
|
||||||
revision: resolved.revision,
|
|
||||||
...('site' in resolved ? { site: resolved.site, siteSpace: resolved.siteSpace } : {}),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Because of how Next will encode, we need to encode ourselves the pathname before rewriting to it.
|
// Because of how Next will encode, we need to encode ourselves the pathname before rewriting to it.
|
||||||
const rewritePathname = normalizePathname(encodePathname(resolved.pathname));
|
const rewritePathname = normalizePathname(encodePathname(resolved.pathname));
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import { init } from '@sentry/nextjs';
|
|
||||||
|
|
||||||
const dsn = process.env.SENTRY_DSN;
|
|
||||||
if (dsn) {
|
|
||||||
init({
|
|
||||||
dsn,
|
|
||||||
release: process.env.SENTRY_RELEASE,
|
|
||||||
|
|
||||||
// Disable tracing as it creates additional requests in an env where subrequests are limited.
|
|
||||||
tracesSampleRate: 0,
|
|
||||||
|
|
||||||
// Disable transactions as it creates additional requests in an env where subrequests are limited.
|
|
||||||
// https://docs.sentry.io/platforms/node/configuration/filtering/#using--3
|
|
||||||
beforeSendTransaction: () => {
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export * from '@sentry/nextjs';
|
|
||||||
@@ -77,7 +77,7 @@ const config: Config = {
|
|||||||
emoji: [
|
emoji: [
|
||||||
'Apple Color Emoji',
|
'Apple Color Emoji',
|
||||||
'Noto Color Emoji',
|
'Noto Color Emoji',
|
||||||
'--font-noto-color-emoji',
|
'var(--font-noto-color-emoji)',
|
||||||
'sans-serif',
|
'sans-serif',
|
||||||
],
|
],
|
||||||
var: ['var(--font-family)'],
|
var: ['var(--font-family)'],
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@scalar/openapi-parser": "^0.10.9",
|
"@scalar/openapi-parser": "^0.10.10",
|
||||||
"@scalar/openapi-types": "^0.1.9"
|
"@scalar/openapi-types": "^0.1.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
# @gitbook/react-openapi
|
# @gitbook/react-openapi
|
||||||
|
|
||||||
|
## 1.1.5
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 886e204: Update OpenAPI operation path design
|
||||||
|
|
||||||
## 1.1.4
|
## 1.1.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -8,12 +8,12 @@
|
|||||||
"default": "./dist/index.js"
|
"default": "./dist/index.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"version": "1.1.4",
|
"version": "1.1.5",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@gitbook/openapi-parser": "workspace:*",
|
"@gitbook/openapi-parser": "workspace:*",
|
||||||
"@scalar/api-client-react": "^1.1.36",
|
"@scalar/api-client-react": "^1.2.5",
|
||||||
"@scalar/oas-utils": "^0.2.110",
|
"@scalar/oas-utils": "^0.2.120",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"flatted": "^3.2.9",
|
"flatted": "^3.2.9",
|
||||||
"json-xml-parse": "^1.3.0",
|
"json-xml-parse": "^1.3.0",
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
||||||
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
|
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
|
||||||
|
import { ScalarApiButton } from './ScalarApiButton';
|
||||||
import { StaticSection } from './StaticSection';
|
import { StaticSection } from './StaticSection';
|
||||||
import { type CodeSampleInput, codeSampleGenerators } from './code-samples';
|
import { type CodeSampleInput, codeSampleGenerators } from './code-samples';
|
||||||
import { generateMediaTypeExample, generateSchemaExample } from './generateSchemaExample';
|
import { generateMediaTypeExample, generateSchemaExample } from './generateSchemaExample';
|
||||||
@@ -79,6 +81,7 @@ export function OpenAPICodeSample(props: {
|
|||||||
code: generator.generate(input),
|
code: generator.generate(input),
|
||||||
syntax: generator.syntax,
|
syntax: generator.syntax,
|
||||||
}),
|
}),
|
||||||
|
footer: <OpenAPICodeSampleFooter data={data} context={context} />,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Use custom samples if defined
|
// Use custom samples if defined
|
||||||
@@ -105,6 +108,7 @@ export function OpenAPICodeSample(props: {
|
|||||||
code: sample.source,
|
code: sample.source,
|
||||||
syntax: sample.lang,
|
syntax: sample.lang,
|
||||||
}),
|
}),
|
||||||
|
footer: <OpenAPICodeSampleFooter data={data} context={context} />,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -128,6 +132,30 @@ export function OpenAPICodeSample(props: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function OpenAPICodeSampleFooter(props: {
|
||||||
|
data: OpenAPIOperationData;
|
||||||
|
context: OpenAPIContextProps;
|
||||||
|
}) {
|
||||||
|
const { data, context } = props;
|
||||||
|
const { method, path } = data;
|
||||||
|
const { specUrl } = context;
|
||||||
|
const hideTryItPanel = data['x-hideTryItPanel'] || data.operation['x-hideTryItPanel'];
|
||||||
|
|
||||||
|
if (hideTryItPanel) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateHttpMethod(method)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="openapi-codesample-footer">
|
||||||
|
<ScalarApiButton method={method} path={path} specUrl={specUrl} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function getSecurityHeaders(securities: OpenAPIOperationData['securities']): {
|
function getSecurityHeaders(securities: OpenAPIOperationData['securities']): {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
} {
|
} {
|
||||||
@@ -169,3 +197,7 @@ function getSecurityHeaders(securities: OpenAPIOperationData['securities']): {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateHttpMethod(method: string): method is OpenAPIV3.HttpMethods {
|
||||||
|
return ['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace'].includes(method);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Button, type ButtonProps, Tooltip, TooltipTrigger } from 'react-aria-components';
|
||||||
|
|
||||||
|
export function OpenAPICopyButton(
|
||||||
|
props: ButtonProps & {
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { value } = props;
|
||||||
|
const { children, onPress, className } = props;
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleCopy = () => {
|
||||||
|
if (!value) return;
|
||||||
|
navigator.clipboard.writeText(value).then(() => {
|
||||||
|
setIsOpen(true);
|
||||||
|
setCopied(true);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setCopied(false);
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipTrigger isOpen={isOpen} onOpenChange={setIsOpen} closeDelay={200} delay={200}>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
preventFocusOnPress
|
||||||
|
onPress={(e) => {
|
||||||
|
handleCopy();
|
||||||
|
onPress?.(e);
|
||||||
|
}}
|
||||||
|
className={`openapi-copy-button ${className}`}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Tooltip
|
||||||
|
isOpen={isOpen}
|
||||||
|
onOpenChange={setIsOpen}
|
||||||
|
placement="top"
|
||||||
|
offset={4}
|
||||||
|
className="openapi-tooltip"
|
||||||
|
>
|
||||||
|
{copied ? 'Copied' : 'Copy to clipboard'}{' '}
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -35,6 +35,7 @@ export function OpenAPIOperation(props: {
|
|||||||
title: operation.summary,
|
title: operation.summary,
|
||||||
})
|
})
|
||||||
: null}
|
: null}
|
||||||
|
<OpenAPIPath data={data} context={context} />
|
||||||
{operation.deprecated && <div className="openapi-deprecated">Deprecated</div>}
|
{operation.deprecated && <div className="openapi-deprecated">Deprecated</div>}
|
||||||
</div>
|
</div>
|
||||||
<div className="openapi-columns">
|
<div className="openapi-columns">
|
||||||
@@ -49,7 +50,6 @@ export function OpenAPIOperation(props: {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<OpenAPIOperationDescription operation={operation} context={context} />
|
<OpenAPIOperationDescription operation={operation} context={context} />
|
||||||
<OpenAPIPath data={data} context={context} />
|
|
||||||
<OpenAPISpec data={data} context={clientContext} />
|
<OpenAPISpec data={data} context={clientContext} />
|
||||||
</div>
|
</div>
|
||||||
<div className="openapi-column-preview">
|
<div className="openapi-column-preview">
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { OpenAPIV3_1 } from '@gitbook/openapi-parser';
|
import { OpenAPICopyButton } from './OpenAPICopyButton';
|
||||||
import type React from 'react';
|
|
||||||
import { ScalarApiButton } from './ScalarApiButton';
|
|
||||||
import type { OpenAPIContextProps, OpenAPIOperationData } from './types';
|
import type { OpenAPIContextProps, OpenAPIOperationData } from './types';
|
||||||
|
import { getDefaultServerURL } from './util/server';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display the path of an operation.
|
* Display the path of an operation.
|
||||||
@@ -10,63 +9,62 @@ export function OpenAPIPath(props: {
|
|||||||
data: OpenAPIOperationData;
|
data: OpenAPIOperationData;
|
||||||
context: OpenAPIContextProps;
|
context: OpenAPIContextProps;
|
||||||
}) {
|
}) {
|
||||||
const { data, context } = props;
|
const { data } = props;
|
||||||
const { method, path } = data;
|
const { method, path, operation } = data;
|
||||||
const { specUrl } = context;
|
|
||||||
const hideTryItPanel = data['x-hideTryItPanel'] || data.operation['x-hideTryItPanel'];
|
const server = getDefaultServerURL(data.servers);
|
||||||
|
const formattedPath = formatPath(path);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="openapi-path">
|
<div className="openapi-path">
|
||||||
<div className={`openapi-method openapi-method-${method}`}>{method}</div>
|
<div className={`openapi-method openapi-method-${method}`}>{method}</div>
|
||||||
<div className="openapi-path-title" data-deprecated={data.operation.deprecated}>
|
|
||||||
<p>{formatPath(path)}</p>
|
<OpenAPICopyButton
|
||||||
</div>
|
value={server + path}
|
||||||
{!hideTryItPanel && validateHttpMethod(method) && (
|
className="openapi-path-title"
|
||||||
<ScalarApiButton method={method} path={path} specUrl={specUrl} />
|
data-deprecated={operation.deprecated}
|
||||||
)}
|
>
|
||||||
|
<span className="openapi-path-server">{server}</span>
|
||||||
|
{formattedPath}
|
||||||
|
</OpenAPICopyButton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateHttpMethod(method: string): method is OpenAPIV3_1.HttpMethods {
|
/**
|
||||||
return ['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace'].includes(method);
|
* Format the path by wrapping placeholders in <span> tags.
|
||||||
}
|
*/
|
||||||
|
|
||||||
// Format the path to highlight placeholders
|
|
||||||
function formatPath(path: string) {
|
function formatPath(path: string) {
|
||||||
// Matches placeholders like {id}, {userId}, etc.
|
// Matches placeholders like {id}, {userId}, etc.
|
||||||
const regex = /\{(\w+)\}/g;
|
const regex = /\{\s*(\w+)\s*\}|:\w+/g;
|
||||||
|
|
||||||
const parts: (string | React.JSX.Element)[] = [];
|
const parts: (string | React.JSX.Element)[] = [];
|
||||||
let lastIndex = 0;
|
let lastIndex = 0;
|
||||||
|
|
||||||
// Replace placeholders with <em> tags
|
//Wrap the variables in <span> tags and maintain either {variable} or :variable
|
||||||
path.replace(regex, (match, key, offset) => {
|
path.replace(regex, (match, _, offset) => {
|
||||||
parts.push(path.slice(lastIndex, offset));
|
if (offset > lastIndex) {
|
||||||
parts.push(<em key={key}>{`{${key}}`}</em>);
|
parts.push(path.slice(lastIndex, offset));
|
||||||
|
}
|
||||||
|
parts.push(
|
||||||
|
<span key={offset} className="openapi-path-variable">
|
||||||
|
{match}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
lastIndex = offset + match.length;
|
lastIndex = offset + match.length;
|
||||||
return match;
|
return match;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Push remaining text after the last placeholder
|
if (lastIndex < path.length) {
|
||||||
parts.push(path.slice(lastIndex));
|
parts.push(path.slice(lastIndex));
|
||||||
|
}
|
||||||
|
|
||||||
// Join parts with separators wrapped in <span>
|
const formattedPath = parts.map((part, index) => {
|
||||||
const formattedPath = parts.reduce(
|
if (typeof part === 'string') {
|
||||||
(acc, part, index) => {
|
return <span key={index}>{part}</span>;
|
||||||
if (typeof part === 'string' && index > 0 && part === '/') {
|
}
|
||||||
acc.push(
|
return part;
|
||||||
<span className="openapi-path-separator" key={`sep-${index}`}>
|
});
|
||||||
/
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
acc.push(part);
|
return formattedPath;
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
[] as (string | React.JSX.Element)[]
|
|
||||||
);
|
|
||||||
|
|
||||||
return <span>{formattedPath}</span>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
||||||
|
import { Markdown } from './Markdown';
|
||||||
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
|
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
|
||||||
import { StaticSection } from './StaticSection';
|
import { StaticSection } from './StaticSection';
|
||||||
import { generateSchemaExample } from './generateSchemaExample';
|
import { generateSchemaExample } from './generateSchemaExample';
|
||||||
@@ -39,44 +40,40 @@ export function OpenAPIResponseExample(props: {
|
|||||||
return Number(a) - Number(b);
|
return Number(a) - Number(b);
|
||||||
});
|
});
|
||||||
|
|
||||||
const tabs = responses
|
const tabs = responses.map(([key, responseObject]) => {
|
||||||
.map(([key, responseObject]) => {
|
const description = resolveDescription(responseObject);
|
||||||
const description = resolveDescription(responseObject);
|
|
||||||
|
|
||||||
if (checkIsReference(responseObject)) {
|
|
||||||
return {
|
|
||||||
key: key,
|
|
||||||
label: key,
|
|
||||||
description,
|
|
||||||
body: (
|
|
||||||
<OpenAPIExample
|
|
||||||
example={getExampleFromReference(responseObject)}
|
|
||||||
context={context}
|
|
||||||
syntax="json"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!responseObject.content || Object.keys(responseObject.content).length === 0) {
|
|
||||||
return {
|
|
||||||
key: key,
|
|
||||||
label: key,
|
|
||||||
description,
|
|
||||||
body: <OpenAPIEmptyResponseExample />,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (checkIsReference(responseObject)) {
|
||||||
return {
|
return {
|
||||||
key: key,
|
key: key,
|
||||||
label: key,
|
label: key,
|
||||||
description: resolveDescription(responseObject),
|
body: (
|
||||||
body: <OpenAPIResponse context={context} content={responseObject.content} />,
|
<OpenAPIExample
|
||||||
|
example={getExampleFromReference(responseObject)}
|
||||||
|
context={context}
|
||||||
|
syntax="json"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
footer: description ? <Markdown source={description} /> : undefined,
|
||||||
};
|
};
|
||||||
})
|
}
|
||||||
.filter((val): val is { key: string; label: string; body: any; description: string } =>
|
|
||||||
Boolean(val)
|
if (!responseObject.content || Object.keys(responseObject.content).length === 0) {
|
||||||
);
|
return {
|
||||||
|
key: key,
|
||||||
|
label: key,
|
||||||
|
body: <OpenAPIEmptyResponseExample />,
|
||||||
|
footer: description ? <Markdown source={description} /> : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: key,
|
||||||
|
label: key,
|
||||||
|
body: <OpenAPIResponse context={context} content={responseObject.content} />,
|
||||||
|
footer: description ? <Markdown source={description} /> : undefined,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
if (tabs.length === 0) {
|
if (tabs.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -3,14 +3,13 @@
|
|||||||
import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { type Key, Tab, TabList, TabPanel, Tabs, type TabsProps } from 'react-aria-components';
|
import { type Key, Tab, TabList, TabPanel, Tabs, type TabsProps } from 'react-aria-components';
|
||||||
import { useEventCallback } from 'usehooks-ts';
|
import { useEventCallback } from 'usehooks-ts';
|
||||||
import { Markdown } from './Markdown';
|
|
||||||
import { getOrCreateTabStoreByKey } from './useSyncedTabsGlobalState';
|
import { getOrCreateTabStoreByKey } from './useSyncedTabsGlobalState';
|
||||||
|
|
||||||
export type TabItem = {
|
export type TabItem = {
|
||||||
key: Key;
|
key: Key;
|
||||||
label: string;
|
label: string;
|
||||||
body: React.ReactNode;
|
body: React.ReactNode;
|
||||||
description?: string;
|
footer?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
type OpenAPITabsContextData = {
|
type OpenAPITabsContextData = {
|
||||||
@@ -140,9 +139,19 @@ export function OpenAPITabsPanels() {
|
|||||||
return (
|
return (
|
||||||
<TabPanel key={key} id={key} className="openapi-tabs-panel">
|
<TabPanel key={key} id={key} className="openapi-tabs-panel">
|
||||||
{selectedTab.body}
|
{selectedTab.body}
|
||||||
{selectedTab.description ? (
|
{selectedTab.footer ? (
|
||||||
<Markdown source={selectedTab.description} className="openapi-tabs-footer" />
|
<OpenAPITabsPanelFooter>{selectedTab.footer}</OpenAPITabsPanelFooter>
|
||||||
) : null}
|
) : null}
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The OpenAPI Tabs panel footer component.
|
||||||
|
* This component should be used as a child of the OpenAPITabs component.
|
||||||
|
*/
|
||||||
|
function OpenAPITabsPanelFooter(props: { children: React.ReactNode }) {
|
||||||
|
const { children } = props;
|
||||||
|
|
||||||
|
return <div className="openapi-tabs-footer">{children}</div>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,14 +27,14 @@ export function ScalarApiButton(props: {
|
|||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="12" fill="none">
|
Test it
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 12" fill="currentColor">
|
||||||
<path
|
<path
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth="1.5"
|
strokeWidth="1.5"
|
||||||
d="M1 10.05V1.43c0-.2.2-.31.37-.22l7.26 4.08c.17.1.17.33.01.43l-7.26 4.54a.25.25 0 0 1-.38-.21Z"
|
d="M1 10.05V1.43c0-.2.2-.31.37-.22l7.26 4.08c.17.1.17.33.01.43l-7.26 4.54a.25.25 0 0 1-.38-.21Z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
Test it
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{isOpen &&
|
{isOpen &&
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
"ARGOS_*",
|
"ARGOS_*",
|
||||||
"GITHUB_*",
|
"GITHUB_*",
|
||||||
"GITBOOK_*",
|
"GITBOOK_*",
|
||||||
"SENTRY_*",
|
|
||||||
"NEXT_SERVER_ACTIONS_ENCRYPTION_KEY"
|
"NEXT_SERVER_ACTIONS_ENCRYPTION_KEY"
|
||||||
],
|
],
|
||||||
"tasks": {
|
"tasks": {
|
||||||
|
|||||||
Reference in New Issue
Block a user