Compare commits

..

12 Commits

Author SHA1 Message Date
Zeno Kapitein 4ab2e4c862 Format 2025-03-19 14:21:29 +01:00
Zeno Kapitein fb3819ab00 Disable animations on tests 2025-03-19 14:15:54 +01:00
Greg Bergé 7212973f34 Update Scalar client (#2993) 2025-03-18 16:02:40 +01:00
Greg Bergé af44e0436b fix typo (#2992) 2025-03-18 14:20:23 +01:00
Greg Bergé 3f9c35d505 Fix emoji display on other platforms than Mac OS (#2990) 2025-03-17 17:49:13 +01:00
Zeno Kapitein 2c13f8b95c Fix horizontal overscroll issue on large screens (#2989) 2025-03-17 15:38:44 +01:00
Zeno Kapitein 4dab1c56e5 Fix alignment of prominent search bar on full-width pages (#2987) 2025-03-17 15:02:23 +01:00
Zeno Kapitein 4723f03ef1 Restyle section group dropdown (#2985) 2025-03-17 13:22:54 +01:00
Samy Pessé 278689e0a0 Version Packages (#2984)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-03-17 11:06:18 +01:00
Nolann B. 886e204e68 Update OpenAPI operation path design (#2972) 2025-03-17 10:18:24 +01:00
Greg Bergé c59947ab39 Remove Sentry (#2986) 2025-03-17 09:00:58 +01:00
Zeno Kapitein 4f0a772a21 Override tint lightness if supplied color is out of bounds (#2983) 2025-03-14 18:24:25 +01:00
42 changed files with 523 additions and 905 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Fix alignment of `prominent` search bar on full-width pages
+6
View File
@@ -0,0 +1,6 @@
---
"@gitbook/openapi-parser": patch
"@gitbook/react-openapi": patch
---
Update scalar
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Restyle section group dropdown
-10
View File
@@ -35,21 +35,11 @@ jobs:
- name: Sets env vars for production
if: github.ref == 'refs/heads/main'
run: |
echo "SENTRY_ENVIRONMENT=production" >> $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
run: bun run turbo gitbook#build:cloudflare
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
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
name: Deploy to Cloudflare
uses: cloudflare/wrangler-action@v3.14.0
+40 -497
View File
File diff suppressed because it is too large Load Diff
+6
View File
@@ -1,5 +1,11 @@
# @gitbook/colors
## 0.3.0
### Minor Changes
- 4f0a772: Override tint lightness if supplied color is out of bounds
## 0.2.0
### Minor Changes
+1 -1
View File
@@ -8,7 +8,7 @@
"default": "./dist/index.js"
}
},
"version": "0.2.0",
"version": "0.3.0",
"devDependencies": {
"typescript": "^5.5.3"
},
+15 -1
View File
@@ -184,6 +184,7 @@ export function colorScale(
const mixColor = mix?.color ? rgbToOklch(hexToRgbArray(mix.color)) : null;
const foregroundColor = rgbToOklch(hexToRgbArray(foreground));
const backgroundColor = rgbToOklch(hexToRgbArray(background));
let mapping = darkMode ? colorMixMapping.dark : colorMixMapping.light;
if (mixColor && mix?.ratio && mix.ratio > 0) {
// 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;
}
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 = [];
-3
View File
@@ -23,8 +23,5 @@
## Caching
# GITBOOK_OUTPUT_CACHE=true
### Sentry ###
# SENTRY_DSN=xxx
### Silent logs
# SILENT=true
-3
View File
@@ -23,9 +23,6 @@ next-env.d.ts
# visual tests
screenshots/
# Sentry Config File
.sentryclirc
/test-results/
/playwright-report/
/blob-report/
+10
View File
@@ -1,5 +1,15 @@
# 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
### Minor Changes
+1
View File
@@ -188,6 +188,7 @@ export function runTestCases(testCases: TestsCase[]) {
const screenshotOptions = testEntry.screenshot;
if (screenshotOptions !== false) {
await argosScreenshot(page, `${testCase.name} - ${testEntry.name}`, {
animations: 'disabled',
viewports: ['macbook-16', 'macbook-13', 'ipad-2', 'iphone-x'],
argosCSS: `
/* Hide Intercom */
+3 -37
View File
@@ -1,19 +1,13 @@
const { withSentryConfig } = require('@sentry/nextjs');
module.exports = withSentryConfig(
{
module.exports = {
env: {
BUILD_VERSION: (process.env.GITHUB_SHA ?? '').slice(0, 7),
SENTRY_RELEASE: process.env.SENTRY_RELEASE ?? '',
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 }) {
webpack(config) {
config.resolve.fallback = {
...config.resolve.fallback,
@@ -23,21 +17,6 @@ module.exports = withSentryConfig(
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;
},
@@ -67,17 +46,4 @@ module.exports = withSentryConfig(
},
],
},
},
{
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)
widenClientFileUpload: true,
// Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers (increases server load)
tunnelRoute: '/~gitbook/monitoring',
disableLogger: true,
}
);
};
+1 -2
View File
@@ -1,6 +1,6 @@
{
"name": "gitbook",
"version": "0.8.0",
"version": "0.8.1",
"private": true,
"scripts": {
"dev": "env-cmd --silent -f ../../.env.local next dev",
@@ -29,7 +29,6 @@
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-navigation-menu": "^1.2.3",
"@radix-ui/react-popover": "^1.0.7",
"@sentry/nextjs": "8.35.0",
"@sindresorhus/fnv1a": "^3.1.0",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/typography": "^0.5.16",
-18
View File
@@ -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 -7
View File
@@ -1,14 +1,8 @@
'use client';
import { captureException } from '@sentry/nextjs';
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 (
<html lang="en">
<body>
@@ -1,8 +1,5 @@
'use client';
import { captureException, withScope } from '@sentry/nextjs';
import React from 'react';
import { Button } from '@/components/primitives/Button';
import { t, useLanguage } from '@/intl/client';
import { tcls } from '@/lib/tailwind';
@@ -11,18 +8,9 @@ export default function ErrorPage(props: {
error: Error & { digest?: string };
reset: () => void;
}) {
const { error, reset } = props;
const { reset } = props;
const language = useLanguage();
React.useEffect(() => {
withScope((scope) => {
if ('_componentStack' in error && error._componentStack) {
scope.setExtra('componentStack', error._componentStack);
}
captureException(error);
});
}, [error]);
return (
<div
className={tcls(
@@ -264,14 +264,18 @@
gap: 6px;
}
.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 ring-1 ring-tint hover:ring-tint-hover;
@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 hover:scale-105 active:scale-100 transition-all;
@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 {
@@ -1,6 +1,6 @@
/* Layout Components */
.openapi-operation {
@apply flex-1 flex flex-col gap-4 mb-14;
@apply flex-1 flex flex-col gap-8 mb-14;
}
.openapi-schemas {
@@ -17,7 +17,7 @@
}
.openapi-summary {
@apply flex flex-col items-start justify-start gap-2;
@apply flex flex-col items-start justify-start gap-3;
}
.openapi-deprecated {
@@ -391,17 +391,30 @@
@apply flex flex-row items-center h-fit;
}
.openapi-codesample-footer {
@apply flex w-full justify-end;
}
/* 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 {
@apply text-[0.813rem] m-0 px-1;
@apply text-[0.813rem] m-0 h-full items-center flex px-2;
}
.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;
-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;
}
.openapi-tabs-footer .openapi-markdown {
@apply text-[0.813rem] text-tint;
}
/* Disclosure group */
.openapi-disclosure-group {
@apply border-b border-tint-subtle relative;
@@ -631,3 +648,37 @@
.openapi-section-schemas > .openapi-section-body > .openapi-schema-properties > .openapi-schema {
@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)
'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-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',
'order-last',
'md:order-[unset]',
@@ -179,6 +180,7 @@ export function Header(props: { context: GitBookSiteContext; withTopHeader?: boo
</div>
{sections || siteSpaces.length > 1 ? (
<div className="scroll-nojump">
<div
className={tcls(
'w-full',
@@ -219,6 +221,7 @@ export function Header(props: { context: GitBookSiteContext; withTopHeader?: boo
)}
</div>
</div>
</div>
) : null}
</header>
);
@@ -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
'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
'page-api-block:max-w-[1654px]',
'page-api-block:max-w-screen-2xl',
'page-api-block:mx-auto'
// page.layout.tableOfContents ? null : 'xl:ml-56'
@@ -143,6 +143,10 @@
margin-right: 0;
width: calc(100% - var(--scrollbar-width));
}
body:has(.page-full-width) .scroll-nojump {
margin-left: 0;
width: 100%;
}
}
}
@@ -1,6 +1,5 @@
'use client';
import { captureException } from '@sentry/nextjs';
import { readStreamableValue } from 'ai/rsc';
import assertNever from 'assert-never';
import React from 'react';
@@ -132,12 +131,6 @@ export const SearchResults = React.forwardRef(function SearchResults(
}
if (!results) {
captureException(
new Error(
`corrupt-cache: ${global ? 'searchAllSiteContent' : 'searchSiteSpaceContent'} is ${results}`
),
{ extra: { results } }
);
setResultsState({ results: [], fetching: false });
return;
}
@@ -1,19 +1,20 @@
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.
*/
export function SectionIcon(props: { icon: IconName; isActive: boolean }) {
const { icon, isActive } = props;
export function SectionIcon(props: { icon: IconName; isActive: boolean; className?: ClassValue }) {
const { icon, isActive, className } = props;
return (
<Icon
icon={icon}
className={tcls(
'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
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
>
<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.List>
</div>
@@ -127,7 +127,7 @@ export function SiteSectionTabs(props: { sections: ClientSiteSections }) {
}}
>
<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={{
translate:
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 (
<ul
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'
)}
>
@@ -246,17 +246,23 @@ function SectionGroupTile(props: { section: ClientSiteSection; isActive: boolean
<Link
href={url}
className={tcls(
'flex min-h-12 w-full select-none flex-col gap-2 rounded p-3 transition-colors hover:bg-tint-hover',
isActive
? 'text-primary hover:text-primary-strong focus:text-primary-strong'
: 'text-tint hover:text-tint-strong focus:text-tint-strong'
'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 ? 'text-primary' : 'text-tint-strong'
)}
>
<div className="flex w-full items-center gap-2 font-medium light:text-dark dark:text-light">
{icon ? <SectionIcon isActive={false} icon={icon as IconName} /> : null}
<span className="min-w-0 truncate">{title}</span>
<div className="flex w-full gap-2">
{icon ? (
<SectionIcon
className="mt-[3px]"
isActive={false}
icon={icon as IconName}
/>
) : null}
{title}
</div>
{section.description ? (
<p className="text-tint-subtle">{section.description}</p>
) : null}
</Link>
</li>
);
@@ -62,6 +62,7 @@ export function SpaceLayout(props: {
spaceId={context.space.id}
>
<Header withTopHeader={withTopHeader} context={context} />
<div className="scroll-nojump">
<div
className={tcls(
'flex',
@@ -136,6 +137,7 @@ export function SpaceLayout(props: {
/>
<div className={tcls('flex-1', 'flex', 'flex-col')}>{children}</div>
</div>
</div>
{withFooter ? <Footer context={context} /> : null}
-7
View File
@@ -1,6 +1,5 @@
import hash from 'object-hash';
import { captureException } from '../../sentry';
import { race, singletonMap } from '../async';
import { type TraceSpan, trace } from '../tracing';
import { assertIsNotV2 } from '../v2';
@@ -159,17 +158,11 @@ export function cache<Args extends any[], Result>(
}
// Detect empty cache entries to avoid returning them.
// Also log in Sentry to investigate what cache is returning empty entries.
if (
entry.data &&
typeof entry.data === 'object' &&
Object.keys(entry.data).length === 0
) {
captureException(
new Error(
`Cache entry ${key} from ${backendName} is an empty object`
)
);
return null;
}
+1 -1
View File
@@ -56,7 +56,7 @@ const fetchFilesystemV1 = cache({
get: async (url: string, options: CacheFunctionOptions) => {
const richFilesystem = await fetchFilesystemUncached(url, options);
return {
// Cache for 4 hours
// Cache for 24 hours
ttl: 24 * 60 * 60,
// Revalidate every 2 hours
revalidateBefore: 22 * 60 * 60,
-17
View File
@@ -1,5 +1,4 @@
import { type ContentAPITokenPayload, CustomizationThemeMode, GitBookAPI } from '@gitbook/api';
import { setContext, setTag } from '@sentry/nextjs';
import { getURLLookupAlternatives, normalizeURL } from '@v2/lib/data';
import assertNever from 'assert-never';
import jwt from 'jsonwebtoken';
@@ -96,14 +95,6 @@ export async function middleware(request: NextRequest) {
const { url, mode } = getInputURL(request);
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
const normalized = normalizeURL(url);
if (normalized.toString() !== url.toString()) {
@@ -148,14 +139,6 @@ export async function middleware(request: NextRequest) {
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.
const rewritePathname = normalizePathname(encodePathname(resolved.pathname));
-20
View File
@@ -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';
+1 -1
View File
@@ -77,7 +77,7 @@ const config: Config = {
emoji: [
'Apple Color Emoji',
'Noto Color Emoji',
'--font-noto-color-emoji',
'var(--font-noto-color-emoji)',
'sans-serif',
],
var: ['var(--font-family)'],
+1 -1
View File
@@ -12,7 +12,7 @@
"version": "2.1.0",
"sideEffects": false,
"dependencies": {
"@scalar/openapi-parser": "^0.10.9",
"@scalar/openapi-parser": "^0.10.10",
"@scalar/openapi-types": "^0.1.9"
},
"devDependencies": {
+6
View File
@@ -1,5 +1,11 @@
# @gitbook/react-openapi
## 1.1.5
### Patch Changes
- 886e204: Update OpenAPI operation path design
## 1.1.4
### Patch Changes
+3 -3
View File
@@ -8,12 +8,12 @@
"default": "./dist/index.js"
}
},
"version": "1.1.4",
"version": "1.1.5",
"sideEffects": false,
"dependencies": {
"@gitbook/openapi-parser": "workspace:*",
"@scalar/api-client-react": "^1.1.36",
"@scalar/oas-utils": "^0.2.110",
"@scalar/api-client-react": "^1.2.5",
"@scalar/oas-utils": "^0.2.120",
"clsx": "^2.1.1",
"flatted": "^3.2.9",
"json-xml-parse": "^1.3.0",
@@ -1,4 +1,6 @@
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
import { ScalarApiButton } from './ScalarApiButton';
import { StaticSection } from './StaticSection';
import { type CodeSampleInput, codeSampleGenerators } from './code-samples';
import { generateMediaTypeExample, generateSchemaExample } from './generateSchemaExample';
@@ -79,6 +81,7 @@ export function OpenAPICodeSample(props: {
code: generator.generate(input),
syntax: generator.syntax,
}),
footer: <OpenAPICodeSampleFooter data={data} context={context} />,
}));
// Use custom samples if defined
@@ -105,6 +108,7 @@ export function OpenAPICodeSample(props: {
code: sample.source,
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']): {
[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,
})
: null}
<OpenAPIPath data={data} context={context} />
{operation.deprecated && <div className="openapi-deprecated">Deprecated</div>}
</div>
<div className="openapi-columns">
@@ -49,7 +50,6 @@ export function OpenAPIOperation(props: {
</div>
) : null}
<OpenAPIOperationDescription operation={operation} context={context} />
<OpenAPIPath data={data} context={context} />
<OpenAPISpec data={data} context={clientContext} />
</div>
<div className="openapi-column-preview">
+37 -39
View File
@@ -1,7 +1,6 @@
import type { OpenAPIV3_1 } from '@gitbook/openapi-parser';
import type React from 'react';
import { ScalarApiButton } from './ScalarApiButton';
import { OpenAPICopyButton } from './OpenAPICopyButton';
import type { OpenAPIContextProps, OpenAPIOperationData } from './types';
import { getDefaultServerURL } from './util/server';
/**
* Display the path of an operation.
@@ -10,63 +9,62 @@ export function OpenAPIPath(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'];
const { data } = props;
const { method, path, operation } = data;
const server = getDefaultServerURL(data.servers);
const formattedPath = formatPath(path);
return (
<div className="openapi-path">
<div className={`openapi-method openapi-method-${method}`}>{method}</div>
<div className="openapi-path-title" data-deprecated={data.operation.deprecated}>
<p>{formatPath(path)}</p>
</div>
{!hideTryItPanel && validateHttpMethod(method) && (
<ScalarApiButton method={method} path={path} specUrl={specUrl} />
)}
<OpenAPICopyButton
value={server + path}
className="openapi-path-title"
data-deprecated={operation.deprecated}
>
<span className="openapi-path-server">{server}</span>
{formattedPath}
</OpenAPICopyButton>
</div>
);
}
function validateHttpMethod(method: string): method is OpenAPIV3_1.HttpMethods {
return ['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace'].includes(method);
}
// Format the path to highlight placeholders
/**
* Format the path by wrapping placeholders in <span> tags.
*/
function formatPath(path: string) {
// Matches placeholders like {id}, {userId}, etc.
const regex = /\{(\w+)\}/g;
const regex = /\{\s*(\w+)\s*\}|:\w+/g;
const parts: (string | React.JSX.Element)[] = [];
let lastIndex = 0;
// Replace placeholders with <em> tags
path.replace(regex, (match, key, offset) => {
//Wrap the variables in <span> tags and maintain either {variable} or :variable
path.replace(regex, (match, _, offset) => {
if (offset > lastIndex) {
parts.push(path.slice(lastIndex, offset));
parts.push(<em key={key}>{`{${key}}`}</em>);
}
parts.push(
<span key={offset} className="openapi-path-variable">
{match}
</span>
);
lastIndex = offset + match.length;
return match;
});
// Push remaining text after the last placeholder
if (lastIndex < path.length) {
parts.push(path.slice(lastIndex));
// Join parts with separators wrapped in <span>
const formattedPath = parts.reduce(
(acc, part, index) => {
if (typeof part === 'string' && index > 0 && part === '/') {
acc.push(
<span className="openapi-path-separator" key={`sep-${index}`}>
/
</span>
);
}
acc.push(part);
return acc;
},
[] as (string | React.JSX.Element)[]
);
const formattedPath = parts.map((part, index) => {
if (typeof part === 'string') {
return <span key={index}>{part}</span>;
}
return part;
});
return <span>{formattedPath}</span>;
return formattedPath;
}
@@ -1,4 +1,5 @@
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
import { Markdown } from './Markdown';
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
import { StaticSection } from './StaticSection';
import { generateSchemaExample } from './generateSchemaExample';
@@ -39,15 +40,13 @@ export function OpenAPIResponseExample(props: {
return Number(a) - Number(b);
});
const tabs = responses
.map(([key, responseObject]) => {
const tabs = responses.map(([key, responseObject]) => {
const description = resolveDescription(responseObject);
if (checkIsReference(responseObject)) {
return {
key: key,
label: key,
description,
body: (
<OpenAPIExample
example={getExampleFromReference(responseObject)}
@@ -55,6 +54,7 @@ export function OpenAPIResponseExample(props: {
syntax="json"
/>
),
footer: description ? <Markdown source={description} /> : undefined,
};
}
@@ -62,21 +62,18 @@ export function OpenAPIResponseExample(props: {
return {
key: key,
label: key,
description,
body: <OpenAPIEmptyResponseExample />,
footer: description ? <Markdown source={description} /> : undefined,
};
}
return {
key: key,
label: key,
description: resolveDescription(responseObject),
body: <OpenAPIResponse context={context} content={responseObject.content} />,
footer: description ? <Markdown source={description} /> : undefined,
};
})
.filter((val): val is { key: string; label: string; body: any; description: string } =>
Boolean(val)
);
});
if (tabs.length === 0) {
return null;
+13 -4
View File
@@ -3,14 +3,13 @@
import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { type Key, Tab, TabList, TabPanel, Tabs, type TabsProps } from 'react-aria-components';
import { useEventCallback } from 'usehooks-ts';
import { Markdown } from './Markdown';
import { getOrCreateTabStoreByKey } from './useSyncedTabsGlobalState';
export type TabItem = {
key: Key;
label: string;
body: React.ReactNode;
description?: string;
footer?: React.ReactNode;
};
type OpenAPITabsContextData = {
@@ -140,9 +139,19 @@ export function OpenAPITabsPanels() {
return (
<TabPanel key={key} id={key} className="openapi-tabs-panel">
{selectedTab.body}
{selectedTab.description ? (
<Markdown source={selectedTab.description} className="openapi-tabs-footer" />
{selectedTab.footer ? (
<OpenAPITabsPanelFooter>{selectedTab.footer}</OpenAPITabsPanelFooter>
) : null}
</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);
}}
>
<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
stroke="currentColor"
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"
/>
</svg>
Test it
</button>
{isOpen &&
-1
View File
@@ -7,7 +7,6 @@
"ARGOS_*",
"GITHUB_*",
"GITBOOK_*",
"SENTRY_*",
"NEXT_SERVER_ACTIONS_ENCRYPTION_KEY"
],
"tasks": {