Compare commits

...

11 Commits

Author SHA1 Message Date
Amrit e2f0e676f9 chore: update scalar 2024-12-05 17:10:22 -08:00
Samy Pessé 1005ee5d52 Prevent resizing the url of an image already resized (#2600) 2024-12-05 16:34:31 +01:00
Valentino Hudhra d9bb9f9a07 Fix cookie banner button being non responsive (#2599) 2024-12-05 14:04:03 +01:00
Greg Bergé 75a6fabe61 Support dropdown without link (#2596) 2024-12-04 12:05:17 +01:00
Greg Bergé 75e29b69cc Allow to work locally (#2597) 2024-12-03 14:49:13 +01:00
Samy Pessé ffd3937538 Fix security issue with images resizing (#2595) 2024-12-03 08:51:46 +01:00
Valentino Hudhra 575587e4c5 Improve footer link styling (#2591) 2024-12-01 07:54:45 +01:00
Brett Jephson 2ce59d7c92 Fix whitespace added with image (#2594) 2024-11-29 17:10:29 +00:00
Brett Jephson 3b3d6e2a73 Add an icon to site sections (#2593) 2024-11-29 16:09:06 +00:00
Zeno Kapitein f2a467fdbf Fix hint block decoration color (#2590) 2024-11-29 16:40:10 +01:00
vibhanshub 867481c66d Support Integers in Response sample (#2592) 2024-11-28 14:19:37 +00:00
22 changed files with 328 additions and 178 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Fix an issue with the cookie banner buttons being non responsive
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/react-openapi': patch
---
Support Integers in Response example
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Fix security issue with image resizing that could be used for phishing
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Fix - whitespace added to site section tabs with icons.
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Add icons to sections
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -16,7 +16,7 @@
"clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static"
},
"dependencies": {
"@gitbook/api": "^0.77.0",
"@gitbook/api": "^0.78.0",
"@gitbook/cache-do": "workspace:*",
"@gitbook/emoji-codepoints": "workspace:*",
"@gitbook/icons": "workspace:*",
@@ -4,6 +4,7 @@ import {
verifyImageSignature,
resizeImage,
CloudflareImageOptions,
imagesResizingSignVersion,
checkIsSizableImageURL,
} from '@/lib/images';
import { parseImageAPIURL } from '@/lib/urls';
@@ -18,28 +19,29 @@ export const runtime = 'edge';
export async function GET(request: NextRequest) {
let urlParam = request.nextUrl.searchParams.get('url');
const signature = request.nextUrl.searchParams.get('sign');
// The current signature algorithm sets version as 1, but we need to support the older version as well
// for previously generated content. In this case, we default to version 0.
const signatureVersion = (request.nextUrl.searchParams.get('sv') as '1') || '0';
// The current signature algorithm sets version as 2, but we need to support the older version as well
const signatureVersion = request.nextUrl.searchParams.get('sv') as string | undefined;
if (!urlParam || !signature) {
return new Response('Missing url/sign parameters', { status: 400 });
}
const url = parseImageAPIURL(urlParam);
// Prevent infinite loops
if (url.includes('/~gitbook/image')) {
return new Response('Invalid url parameter', { status: 400 });
}
// Check again if the image can be sized, even though we checked when rendering the Image component
// Otherwise, it's possible to pass just any link to this endpoint and trigger HTML injection on the domain
// Also prevent infinite redirects.
if (!checkIsSizableImageURL(url)) {
return new Response('Invalid url parameter', { status: 400 });
}
// For older signatures, we redirect to the url.
if (signatureVersion !== imagesResizingSignVersion) {
return Response.redirect(url, 302);
}
// Verify the signature
const verified = await verifyImageSignature(url, { signature, version: signatureVersion });
const verified = await verifyImageSignature(url, { signature });
if (!verified) {
return new Response(`Invalid signature "${signature ?? ''}" for "${url}"`, { status: 400 });
}
@@ -91,6 +91,8 @@ const HINT_STYLES: {
'[&_.can-override-bg]:bg-orange-500/3',
'[&_.can-override-text]:text-orange-800',
'dark:[&_.can-override-text]:text-orange-400',
'decoration-orange-800/6',
'dark:decoration-orange-400/6',
],
style: ['bg-orange-500/2', 'border-orange-500/4'],
},
@@ -104,6 +106,8 @@ const HINT_STYLES: {
'dark:[&_a:hover]:text-red-300',
'[&_.can-override-bg]:bg-red-500/3',
'[&_.can-override-text]:text-red-400',
'decoration-red-800/6',
'dark:decoration-red-400/6',
],
style: ['bg-red-500/2', 'border-red-500/4'],
},
@@ -118,6 +122,8 @@ const HINT_STYLES: {
'[&_.can-override-bg]:bg-green-500/3',
'[&_.can-override-text]:text-green-800',
'dark:[&_.can-override-text]:text-green-400',
'decoration-green-800/6',
'dark:decoration-green-400/6',
],
style: ['bg-green-500/2', 'border-green-500/4'],
},
@@ -13,7 +13,7 @@ export function FooterLinksGroup(props: {
return (
<div className={tcls('flex', 'flex-col', 'gap-3')}>
<p className={tcls('text-base', 'font-medium')}>{group.title}</p>
<p className={tcls('text-base', 'font-semibold')}>{group.title}</p>
{group.links.map((link, index) => {
return <FooterLink key={index} link={link} context={context} />;
})}
@@ -32,7 +32,14 @@ async function FooterLink(props: { link: CustomizationContentLink; context: Cont
return (
<Link
href={resolved.href}
className={tcls('text-sm', 'text-primary-400', 'hover:text-primary-500')}
className={tcls(
'text-sm',
'font-normal',
'text-dark/8',
'hover:text-dark/9',
'dark:text-light/8',
'dark:hover:text-light/9',
)}
>
{link.title}
</Link>
@@ -82,11 +82,12 @@ export function Dropdown<E extends HTMLElement>(props: {
/**
* Animated chevron to display in the dropdown button.
*/
export function DropdownChevron(props: {}) {
export function DropdownChevron() {
return (
<Icon
icon="chevron-down"
className={tcls(
'shrink-0',
'opacity-6',
'size-3',
'ms-1',
@@ -1,15 +1,20 @@
import {
CustomizationContentLink,
CustomizationHeaderLink,
CustomizationSettings,
CustomizationHeaderPreset,
SiteCustomizationSettings,
CustomizationHeaderLink,
} from '@gitbook/api';
import assertNever from 'assert-never';
import { ContentRefContext, resolveContentRef } from '@/lib/references';
import { tcls } from '@/lib/tailwind';
// @TODO replace by api.CustomizationHeaderItem when available
type CustomizationHeaderItem = Omit<CustomizationHeaderLink, 'to'> & {
to: CustomizationHeaderLink['to'] | null;
};
import {
Dropdown,
DropdownButtonProps,
@@ -21,93 +26,40 @@ import { Button, Link } from '../primitives';
export async function HeaderLink(props: {
context: ContentRefContext;
link: CustomizationHeaderLink;
link: CustomizationHeaderItem;
customization: CustomizationSettings | SiteCustomizationSettings;
}) {
const { context, link, customization } = props;
const target = await resolveContentRef(link.to, context);
if (!target) {
return null;
}
const target = link.to ? await resolveContentRef(link.to, context) : null;
const headerPreset = customization.header.preset;
const renderLink = (linkProps: DropdownButtonProps<HTMLAnchorElement>) => {
const linkStyle = link.style ?? 'link';
switch (linkStyle) {
case 'button-secondary':
case 'button-primary': {
const variant = (() => {
switch (linkStyle) {
case 'button-secondary':
return 'secondary';
case 'button-primary':
return 'primary';
default:
assertNever(linkStyle);
}
})();
return (
<Button
href={target.href}
variant={variant}
size="medium"
className={tcls(
{
'button-primary':
headerPreset === CustomizationHeaderPreset.Custom ||
headerPreset === CustomizationHeaderPreset.Bold
? tcls(
'bg-header-link-500 hover:bg-text-header-link-300 text-header-button-text',
'dark:bg-header-link-500 dark:hover:bg-text-header-link-300 dark:text-header-button-text',
)
: null,
'button-secondary': tcls(
'bg:transparent hover:bg-transparent',
'dark:bg-transparent dark:hover:bg-transparent',
'ring-header-link-500 hover:ring-header-link-300 text-header-link-500',
'dark:ring-header-link-500 dark:hover:ring-header-link-300 dark:text-header-link-500',
),
}[linkStyle],
)}
>
{link.title}
</Button>
);
}
case 'link': {
return (
<Link
{...linkProps}
href={target.href}
className={tcls(
'overflow-hidden',
'text-sm lg:text-base',
'flex flex-row items-center',
'whitespace-nowrap',
'hover:text-header-link-400 dark:hover:text-light',
headerPreset === CustomizationHeaderPreset.Default
? ['text-dark/8', 'dark:text-light/8']
: ['text-header-link-500 hover:text-header-link-400'],
)}
>
<span className={tcls('truncate')}>{link.title}</span>
{link.links && link.links.length > 0 ? <DropdownChevron /> : null}
</Link>
);
}
default:
assertNever(linkStyle);
}
};
const linkStyle = link.style ?? 'link';
if (link.links && link.links.length > 0) {
return (
<Dropdown button={renderLink}>
<Dropdown
button={(buttonProps) => {
if (!target) {
return (
<HeaderItemDropdown
{...buttonProps}
headerPreset={headerPreset}
title={link.title}
/>
);
}
return (
<HeaderLinkNavItem
{...buttonProps}
linkStyle={linkStyle}
headerPreset={headerPreset}
title={link.title}
isDropdown
href={target?.href}
/>
);
}}
>
<DropdownMenu>
{link.links.map((subLink, index) => (
<SubHeaderLink key={index} {...props} link={subLink} />
@@ -117,7 +69,128 @@ export async function HeaderLink(props: {
);
}
return renderLink({});
if (!target) {
return null;
}
return (
<HeaderLinkNavItem
linkStyle={linkStyle}
headerPreset={headerPreset}
title={link.title}
isDropdown={false}
href={target.href}
/>
);
}
export type HeaderLinkNavItemProps = {
linkStyle: NonNullable<CustomizationHeaderLink['style']>;
headerPreset: CustomizationHeaderPreset;
title: string;
href: string;
isDropdown: boolean;
} & DropdownButtonProps<HTMLElement>;
function HeaderLinkNavItem(props: HeaderLinkNavItemProps) {
switch (props.linkStyle) {
case 'button-secondary':
case 'button-primary':
return <HeaderItemButton {...props} linkStyle={props.linkStyle} />;
case 'link':
return <HeaderItemLink {...props} />;
default:
assertNever(props.linkStyle);
}
}
function HeaderItemButton(
props: Omit<HeaderLinkNavItemProps, 'linkStyle'> & {
linkStyle: 'button-secondary' | 'button-primary';
},
) {
const { linkStyle, headerPreset, title, href, ...rest } = props;
const variant = (() => {
switch (linkStyle) {
case 'button-secondary':
return 'secondary';
case 'button-primary':
return 'primary';
default:
assertNever(linkStyle);
}
})();
return (
<Button
href={href}
variant={variant}
size="medium"
className={tcls(
{
'button-primary':
headerPreset === CustomizationHeaderPreset.Custom ||
headerPreset === CustomizationHeaderPreset.Bold
? tcls(
'bg-header-link-500 hover:bg-text-header-link-300 text-header-button-text',
'dark:bg-header-link-500 dark:hover:bg-text-header-link-300 dark:text-header-button-text',
)
: null,
'button-secondary': tcls(
'bg:transparent hover:bg-transparent',
'dark:bg-transparent dark:hover:bg-transparent',
'ring-header-link-500 hover:ring-header-link-300 text-header-link-500',
'dark:ring-header-link-500 dark:hover:ring-header-link-300 dark:text-header-link-500',
),
}[linkStyle],
)}
{...rest}
>
{title}
</Button>
);
}
function getHeaderLinkClassName(props: { headerPreset: CustomizationHeaderPreset }) {
return tcls(
'overflow-hidden',
'text-sm lg:text-base',
'flex flex-row items-center',
'whitespace-nowrap',
'hover:text-header-link-400 dark:hover:text-light',
'truncate',
props.headerPreset === CustomizationHeaderPreset.Default
? ['text-dark/8', 'dark:text-light/8']
: ['text-header-link-500 hover:text-header-link-400'],
);
}
function HeaderItemLink(props: HeaderLinkNavItemProps) {
const { headerPreset, title, isDropdown, href, ...rest } = props;
return (
<Link href={href} className={getHeaderLinkClassName({ headerPreset })} {...rest}>
{title}
{isDropdown ? <DropdownChevron /> : null}
</Link>
);
}
function HeaderItemDropdown(
props: {
headerPreset: CustomizationHeaderPreset;
title: string;
} & DropdownButtonProps<HTMLElement>,
) {
const { headerPreset, title, ...rest } = props;
return (
<span
className={tcls(getHeaderLinkClassName({ headerPreset }), 'cursor-default')}
{...rest}
>
{title}
<DropdownChevron />
</span>
);
}
async function SubHeaderLink(props: {
@@ -13,12 +13,17 @@ import { tcls } from '@/lib/tailwind';
import { Dropdown, DropdownMenu, DropdownMenuItem } from './Dropdown';
import styles from './headerLinks.module.css';
// @TODO replace by api.CustomizationHeaderItem when available
type CustomizationHeaderItem = Omit<CustomizationHeaderLink, 'to'> & {
to: CustomizationHeaderLink['to'] | null;
};
/**
* Dropdown menu for header links hidden at small screen size.
*/
export function HeaderLinkMore(props: {
label: React.ReactNode;
links: CustomizationHeaderLink[];
links: CustomizationHeaderItem[];
context: ContentRefContext;
customization: CustomizationSettings | SiteCustomizationSettings;
}) {
@@ -55,10 +60,10 @@ export function HeaderLinkMore(props: {
);
}
async function MoreMenuLink(props: { context: ContentRefContext; link: CustomizationHeaderLink }) {
async function MoreMenuLink(props: { context: ContentRefContext; link: CustomizationHeaderItem }) {
const { context, link } = props;
const target = await resolveContentRef(link.to, context);
const target = link.to ? await resolveContentRef(link.to, context) : null;
if (!target) {
return null;
@@ -0,0 +1,21 @@
import type { SiteSection } from '@gitbook/api';
import { Icon, type IconName } from '@gitbook/icons';
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;
return (
<Icon
icon={icon}
className={tcls(
'size-[1em] text-inherit opacity-8',
isActive && 'text-inherit opacity-10',
)}
/>
);
}
@@ -1,10 +1,13 @@
'use client';
import { SiteSection } from '@gitbook/api';
import type { SiteSection } from '@gitbook/api';
import type { IconName } from '@gitbook/icons';
import React from 'react';
import { tcls } from '@/lib/tailwind';
import { Link } from '../primitives';
import { SectionIcon } from './SectionIcon';
/**
* A set of navigational tabs representing site sections for multi-section sites
@@ -14,13 +17,7 @@ export function SiteSectionTabs(props: {
section: SiteSection;
index: number;
}) {
const { list: sections, section: currentSection, index: currentIndex } = props;
const tabs = sections.map((section) => ({
id: section.id,
label: section.title,
path: section.urls.published ?? '',
}));
const { list: sections, index: currentIndex } = props;
const currentTabRef = React.useRef<HTMLAnchorElement>(null);
const navRef = React.useRef<HTMLDivElement>(null);
@@ -43,7 +40,9 @@ export function SiteSectionTabs(props: {
}, []);
React.useEffect(() => {
updateTabDimensions();
if (currentIndex >= 0) {
updateTabDimensions();
}
}, [currentIndex, updateTabDimensions]);
React.useLayoutEffect(() => {
@@ -55,11 +54,11 @@ export function SiteSectionTabs(props: {
};
}, [updateTabDimensions]);
const opacity = Boolean(tabDimensions) ? 1 : 0.0;
const opacity = tabDimensions ? 1 : 0.0;
const scale = (tabDimensions?.width ?? 0) * 0.01;
const startPos = `${tabDimensions?.left ?? 0}px`;
return tabs.length > 0 ? (
return sections.length > 0 ? (
<nav
aria-label="Sections"
ref={navRef}
@@ -84,15 +83,24 @@ export function SiteSectionTabs(props: {
'md:px-5',
)}
>
{tabs.map((tab, index) => (
<Tab
active={currentIndex === index}
key={index + tab.path}
label={tab.label}
href={tab.path}
ref={currentIndex === index ? currentTabRef : null}
/>
))}
{sections.map((section, index) => {
const { id, urls, title, icon } = section;
const isActive = index === currentIndex;
return (
<Tab
active={isActive}
key={id}
label={title}
href={urls.published ?? ''}
ref={isActive ? currentTabRef : null}
icon={
icon ? (
<SectionIcon isActive={isActive} icon={icon as IconName} />
) : null
}
/>
);
})}
</div>
{/* A container for a pseudo element for active tab indicator. A container is needed so we can set
a relative position without breaking the z-index of other parts of the header. */}
@@ -117,7 +125,7 @@ export function SiteSectionTabs(props: {
'after:bg-primary',
'dark:after:bg-primary-400',
)}
></div>
/>
</div>
</nav>
) : null;
@@ -126,24 +134,26 @@ export function SiteSectionTabs(props: {
/**
* The tab item - a link to a site section
*/
const Tab = React.forwardRef<HTMLSpanElement, { active: boolean; href: string; label: string }>(
function Tab(props, ref) {
const { active, href, label } = props;
return (
<Link
className={tcls(
'px-3 py-1 my-2 rounded straight-corners:rounded-none transition-colors',
active && 'text-primary dark:text-primary-400',
!active &&
'text-dark/8 hover:bg-dark/1 hover:text-dark/9 dark:text-light/8 dark:hover:bg-light/2 dark:hover:text-light/9',
)}
role="tab"
href={href}
>
<span ref={ref} className={tcls('inline-flex w-full truncate')}>
{label}
</span>
</Link>
);
},
);
const Tab = React.forwardRef<
HTMLSpanElement,
{ active: boolean; href: string; icon?: React.ReactNode; label: string }
>(function Tab(props, ref) {
const { active, href, icon, label } = props;
return (
<Link
className={tcls(
'group/tab px-3 py-1 my-2 rounded straight-corners:rounded-none transition-colors',
active && 'text-primary dark:text-primary-400',
!active &&
'text-dark/8 hover:bg-dark/1 hover:text-dark/9 dark:text-light/8 dark:hover:bg-light/2 dark:hover:text-light/9',
)}
role="tab"
href={href}
>
<span ref={ref} className={tcls('flex gap-2 items-center w-full truncate')}>
{icon}
{label}
</span>
</Link>
);
});
@@ -1,25 +1,25 @@
'use client';
import type { ComponentPropsWithoutRef, HTMLAttributes } from 'react';
import { tcls, ClassValue } from '@/lib/tailwind';
import { Link } from './Link';
type ButtonProps = {
href?: string;
onClick?: () => void;
children: React.ReactNode;
variant?: 'primary' | 'secondary';
size?: 'default' | 'medium' | 'small';
className?: ClassValue;
};
} & HTMLAttributes<HTMLElement>;
export function Button({
href,
onClick,
children,
variant = 'primary',
size = 'default',
className,
...rest
}: ButtonProps) {
const variantClasses =
variant === 'primary'
@@ -69,14 +69,14 @@ export function Button({
if (href) {
return (
<Link href={href} className={domClassName}>
<Link href={href} className={domClassName} {...rest}>
{children}
</Link>
);
}
return (
<button type="button" onClick={onClick} className={domClassName}>
<button type="button" className={domClassName} {...rest}>
{children}
</button>
);
+1 -1
View File
@@ -54,7 +54,7 @@ export function getContentSecurityPolicy(scripts: SpaceIntegrationScript[], nonc
object-src 'none';
base-uri 'self' ${assetsDomain};
form-action 'self' ${assetsDomain};
frame-ancestors https:;
frame-ancestors https: ${process.env.NODE_ENV !== 'production' ? 'http:' : ''};
`;
const result = scripts
+28 -28
View File
@@ -4,7 +4,7 @@ import fnv1a from '@sindresorhus/fnv1a';
import { noCacheFetchOptions } from '@/lib/cache/http';
import { rootUrl } from './links';
import { host, rootUrl } from './links';
import { getImageAPIUrl } from './urls';
export interface CloudflareImageJsonFormat {
@@ -31,6 +31,8 @@ export interface CloudflareImageOptions {
quality?: number;
}
export const imagesResizingSignVersion = '2';
/**
* Return true if images resizing is enabled.
*/
@@ -53,17 +55,26 @@ export function checkIsHttpURL(input: string | URL): boolean {
* Check if an image URL is resizable.
* Skip it for non-http(s) URLs (data, etc).
* Skip it for SVGs.
* Skip it for GitBook images (to avoid recursion).
*/
export function checkIsSizableImageURL(input: string): boolean {
if (!URL.canParse(input)) {
return false;
}
if (input.includes('/~gitbook/image')) {
return false;
}
const parsed = new URL(input);
if (parsed.pathname.endsWith('.svg')) {
return false;
}
return checkIsHttpURL(parsed);
if (!checkIsHttpURL(parsed)) {
return false;
}
return true;
}
interface ResizeImageOptions {
@@ -83,7 +94,7 @@ export function getResizedImageURLFactory(
return null;
}
const signature = generateSignatureV1(input);
const signature = generateSignature(input);
return (options) => {
const url = new URL('/~gitbook/image', rootUrl());
@@ -103,7 +114,7 @@ export function getResizedImageURLFactory(
}
url.searchParams.set('sign', signature);
url.searchParams.set('sv', '1');
url.searchParams.set('sv', imagesResizingSignVersion);
return url.toString();
};
@@ -123,11 +134,9 @@ export function getResizedImageURL(input: string, options: ResizeImageOptions):
*/
export async function verifyImageSignature(
input: string,
{ signature, version }: { signature: string; version: '1' | '0' },
{ signature }: { signature: string },
): Promise<boolean> {
const expectedSignature =
version === '1' ? generateSignatureV1(input) : await generateSignatureV0(input);
return expectedSignature === signature;
return generateSignature(input) === signature;
}
/**
@@ -229,26 +238,17 @@ function stringifyOptions(options: CloudflareImageOptions): string {
const fnv1aUtf8Buffer = new Uint8Array(512);
/**
* New and faster algorithm to generate a signature for an image.
* When setting it in a URL, we use version '1' for the 'sv' querystring parameneter
* to know that it was the algorithm that was used.
* Generate a signature for an image.
* The signature is relative to the current site being rendered to avoid serving images from other sites on the same domain.
*/
function generateSignatureV1(input: string): string {
const all = [input, process.env.GITBOOK_IMAGE_RESIZE_SIGNING_KEY].filter(Boolean).join(':');
function generateSignature(input: string) {
const hostName = host();
const all = [
input,
hostName, // The hostname is used to avoid serving images from other sites on the same domain
process.env.GITBOOK_IMAGE_RESIZE_SIGNING_KEY,
]
.filter(Boolean)
.join(':');
return fnv1a(all, { utf8Buffer: fnv1aUtf8Buffer }).toString(16);
}
/**
* Initial algorithm used to generate a signature for an image. It didn't use any versioning in the URL.
* We still need it to validate older signatures that were generated without versioning
* but still exist in previously generated and cached content.
*/
async function generateSignatureV0(input: string): Promise<string> {
const all = [input, process.env.GITBOOK_IMAGE_RESIZE_SIGNING_KEY].filter(Boolean).join(':');
const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(all));
// Convert ArrayBuffer to hex string
const hashArray = Array.from(new Uint8Array(hash));
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
return hashHex;
}
+1 -1
View File
@@ -501,7 +501,7 @@ async function lookupSiteOrSpaceInMultiIdMode(
httpOnly: true,
maxAge: 60 * 30,
secure: process.env.NODE_ENV === 'production',
sameSite: 'none',
sameSite: process.env.NODE_ENV === 'production' ? 'none' : undefined,
},
},
};
+1 -1
View File
@@ -10,7 +10,7 @@
},
"version": "0.7.0",
"dependencies": {
"@scalar/api-client-react": "1.0.87",
"@scalar/api-client-react": "1.0.98",
"classnames": "^2.5.1",
"flatted": "^3.2.9",
"openapi-types": "^12.1.3",
@@ -13,7 +13,7 @@ export function ScalarApiButton({ method, path }: { method: string; path: string
<div className="scalar scalar-activate">
<button
className="scalar-activate-button"
onClick={() => client?.open({ method, path })}
onClick={() => client?.open({ method, path, _source: 'gitbook' })}
>
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="12" fill="none">
<path
@@ -79,7 +79,7 @@ export function generateSchemaExample(
return 'text';
}
if (schema.type === 'number') {
if (schema.type === 'number' || schema.type === 'integer') {
return schema.default || 0;
}