diff --git a/.changeset/modern-moons-judge.md b/.changeset/modern-moons-judge.md new file mode 100644 index 0000000000..1eaf52fd9b --- /dev/null +++ b/.changeset/modern-moons-judge.md @@ -0,0 +1,5 @@ +--- +'gitbook': patch +--- + +Update design for hint block: use semantic colors (info, warning, danger, success) and add alternative styling for hints with headings diff --git a/bun.lock b/bun.lock index 7f6e7b7aea..7304ff0d4f 100644 --- a/bun.lock +++ b/bun.lock @@ -24,7 +24,7 @@ }, "packages/colors": { "name": "@gitbook/colors", - "version": "0.1.0", + "version": "0.2.0", "devDependencies": { "typescript": "^5.5.3", }, @@ -38,9 +38,9 @@ }, "packages/gitbook": { "name": "gitbook", - "version": "0.6.1", + "version": "0.6.2", "dependencies": { - "@gitbook/api": "^0.94.0", + "@gitbook/api": "^0.95.0", "@gitbook/cache-do": "workspace:*", "@gitbook/colors": "workspace:*", "@gitbook/emoji-codepoints": "workspace:*", @@ -155,7 +155,7 @@ }, "packages/openapi-parser": { "name": "@gitbook/openapi-parser", - "version": "1.0.0", + "version": "1.0.1", "dependencies": { "@scalar/openapi-parser": "^0.10.4", "@scalar/openapi-types": "^0.1.6", @@ -211,7 +211,7 @@ }, "packages/react-openapi": { "name": "@gitbook/react-openapi", - "version": "1.0.1", + "version": "1.0.2", "dependencies": { "@gitbook/openapi-parser": "workspace:*", "@scalar/api-client-react": "1.0.87", @@ -4651,7 +4651,7 @@ "gaxios/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], - "gitbook/@gitbook/api": ["@gitbook/api@0.94.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-jOvqUSdyXeuPpBiujkQLb14uVQA5A0XL+P89MmC/53hV7v/8gR8WlJN9RJVDrP0LX51dsLT+/zYN8xWp19nPwA=="], + "gitbook/@gitbook/api": ["@gitbook/api@0.95.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-9KAbt27Ile6cqAch7QEbiJHALQHojYlhsPzilgdQ5wpHgLwsrd7Smd58A3/8bWBKq4KV0vP4rh3oYhIw+LlWFw=="], "gitbook-v2/next": ["next@15.2.0-canary.45", "", { "dependencies": { "@next/env": "15.2.0-canary.45", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.2.0-canary.45", "@next/swc-darwin-x64": "15.2.0-canary.45", "@next/swc-linux-arm64-gnu": "15.2.0-canary.45", "@next/swc-linux-arm64-musl": "15.2.0-canary.45", "@next/swc-linux-x64-gnu": "15.2.0-canary.45", "@next/swc-linux-x64-musl": "15.2.0-canary.45", "@next/swc-win32-arm64-msvc": "15.2.0-canary.45", "@next/swc-win32-x64-msvc": "15.2.0-canary.45", "sharp": "^0.33.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-UsneTQn9tntbiAaXpvoXhhsTBb58Q2XIs2Dfka+qWA8motBz0ZvW297YHLxhdur4xN0IJvknnZKl5Bs7wAGlOg=="], diff --git a/packages/gitbook/e2e/util.ts b/packages/gitbook/e2e/util.ts index 8ddd2ddbbd..f0c503079e 100644 --- a/packages/gitbook/e2e/util.ts +++ b/packages/gitbook/e2e/util.ts @@ -14,7 +14,7 @@ import { CustomizationThemeMode, SiteCustomizationSettings, } from '@gitbook/api'; -import { test, expect, Page, BrowserContext } from '@playwright/test'; +import { BrowserContext, expect, Page, test } from '@playwright/test'; import deepMerge from 'deepmerge'; import rison from 'rison'; import { DeepPartial } from 'ts-essentials'; @@ -222,6 +222,10 @@ export function getCustomizationURL(partial: DeepPartial) { >
) { ) { const hintStyle = HINT_STYLES[block.data.style] ?? HINT_STYLES.info; const firstLine = getBlockTextStyle(block.nodes[0]); + const firstNode = block.nodes[0]; + const hasHeading = ['heading-1', 'heading-2', 'heading-3'].includes(block.nodes[0].type); + return (
-
+
- + {hasHeading ? ( + -
+ ) : null} +
); } @@ -55,77 +87,69 @@ export function Hint(props: BlockProps) { const HINT_STYLES: { [style in DocumentBlockHint['data']['style']]: { icon: IconName; - iconColor: ClassValue; - bodyColor: ClassValue; - style: ClassValue; + iconColor?: ClassValue; + body?: ClassValue; + header?: ClassValue; + container?: ClassValue; + containerWithHeader?: ClassValue; }; } = { info: { icon: 'circle-info', - iconColor: ['text-primary'], - bodyColor: ['[&_a]:text-primary', '[&_a:hover]:text-primary-strong'], - style: [ - 'bg-tint', - 'print-mode:!bg-tint', - 'theme-muted:bg-tint-base', - 'theme-bold-tint:bg-tint-base', - 'theme-gradient:bg-tint-12/1', - 'border-tint', - '[&_.can-override-bg]:bg-tint-active', - '[&_.can-override-text]:text-tint-strong', + iconColor: 'text-info-subtle contrast-more:text-info', + header: 'bg-info-active', + body: [ + 'text-neutral-strong', + '[&_.can-override-bg]:bg-neutral-active', + '[&_.can-override-text]:text-neutral-strong', ], + container: + 'bg-neutral theme-muted:bg-neutral-base theme-bold-tint:bg-neutral-base theme-gradient:bg-neutral-12/1 border-neutral', + containerWithHeader: 'border-info-solid bg-neutral-subtle theme-gradient:bg-neutral-12/1', }, warning: { icon: 'circle-exclamation', - iconColor: ['text-amber-500', 'dark:text-orange-400'], // Darker shades of orange-* mismatch with lighter shades, so in light mode we use amber text on top of orange bg. - bodyColor: [ - 'text-orange-950', - 'dark:text-orange-50', - '[&_a]:text-orange-800', - '[&_a:hover]:text-orange-900', - 'dark:[&_a]:text-orange-400', - 'dark:[&_a:hover]:text-orange-300', - '[&_.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', + iconColor: 'text-warning-subtle contrast-more:text-warning', + header: 'bg-warning-active', + body: [ + 'text-neutral-strong', + '[&_a]:text-warning', + '[&_a:hover]:text-warning-strong', + '[&_.can-override-bg]:bg-warning-active', + '[&_.can-override-text]:text-warning-strong', + 'decoration-warning/6', ], - style: ['bg-orange-500/2', 'border-orange-500/4'], + container: 'bg-warning border-warning', + containerWithHeader: 'border-warning-solid bg-warning-subtle', }, danger: { icon: 'triangle-exclamation', - iconColor: ['text-red-500', 'dark:text-red-400'], - bodyColor: [ - 'text-red-950', - 'dark:text-red-50', - '[&_a]:text-red-800', - '[&_a:hover]:text-red-900', - 'dark:[&_a]:text-red-400', - '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', + iconColor: 'text-danger-subtle contrast-more:text-danger', + header: 'bg-danger-active', + body: [ + 'text-neutral-strong', + '[&_a]:text-danger', + '[&_a:hover]:text-danger-strong', + '[&_.can-override-bg]:bg-danger-active', + '[&_.can-override-text]:text-danger-strong', + 'decoration-danger/6', ], - style: ['bg-red-500/2', 'border-red-500/4'], + container: 'bg-danger border-danger', + containerWithHeader: 'border-danger-solid bg-danger-subtle', }, success: { icon: 'circle-check', - iconColor: ['text-green-500', 'dark:text-green-400'], - bodyColor: [ - 'text-green-950', - 'dark:text-green-50', - '[&_a]:text-green-800', - '[&_a:hover]:text-green-900', - 'dark:[&_a]:text-green-400', - 'dark:[&_a:hover]:text-green-300', - '[&_.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', + iconColor: 'text-success-subtle contrast-more:text-success', + header: 'bg-success-active', + body: [ + 'text-neutral-strong', + '[&_a]:text-success', + '[&_a:hover]:text-success-strong', + '[&_.can-override-bg]:bg-success-active', + '[&_.can-override-text]:text-success-strong', + 'decoration-success/6', ], - style: ['bg-green-500/2', 'border-green-500/4'], + container: 'bg-success border-success', + containerWithHeader: 'border-success-solid bg-success-subtle', }, }; diff --git a/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx b/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx index 8eed4f5863..c1915b2c12 100644 --- a/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx +++ b/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx @@ -14,6 +14,10 @@ import { colorScale, type ColorScaleOptions, DEFAULT_TINT_COLOR, + DEFAULT_HINT_INFO_COLOR, + DEFAULT_HINT_SUCCESS_COLOR, + DEFAULT_HINT_DANGER_COLOR, + DEFAULT_HINT_WARNING_COLOR, hexToRgb, } from '@gitbook/colors'; import { IconsProvider, IconStyle } from '@gitbook/icons'; @@ -42,6 +46,7 @@ export async function CustomizationRootLayout(props: { const tintColor = getTintColor(customization); const mixColor = getTintMixColor(customization.styling.primaryColor, tintColor); const sidebarStyles = getSidebarStyles(customization); + const { infoColor, successColor, warningColor, dangerColor } = getSemanticColors(customization); return ( @@ -198,6 +213,45 @@ function getSidebarStyles( }; } +/** + * Get the semnatic color customization settings. + * If it is a space customization, it will return the default styles. + */ +function getSemanticColors( + customization: CustomizationSettings | SiteCustomizationSettings, +): Pick< + SiteCustomizationSettings['styling'], + 'infoColor' | 'successColor' | 'warningColor' | 'dangerColor' +> { + if ('infoColor' in customization.styling) { + return { + infoColor: customization.styling.infoColor, + successColor: customization.styling.successColor, + warningColor: customization.styling.warningColor, + dangerColor: customization.styling.dangerColor, + }; + } + + return { + infoColor: { + light: DEFAULT_HINT_INFO_COLOR, + dark: DEFAULT_HINT_INFO_COLOR, + }, + successColor: { + light: DEFAULT_HINT_SUCCESS_COLOR, + dark: DEFAULT_HINT_SUCCESS_COLOR, + }, + warningColor: { + light: DEFAULT_HINT_WARNING_COLOR, + dark: DEFAULT_HINT_WARNING_COLOR, + }, + dangerColor: { + light: DEFAULT_HINT_DANGER_COLOR, + dark: DEFAULT_HINT_DANGER_COLOR, + }, + }; +} + type ColorInput = string; function generateColorVariable( name: string, diff --git a/packages/gitbook/src/lib/api.ts b/packages/gitbook/src/lib/api.ts index 9f4019d583..16ad9f6e5a 100644 --- a/packages/gitbook/src/lib/api.ts +++ b/packages/gitbook/src/lib/api.ts @@ -208,42 +208,6 @@ export const getUserById = cache({ }, }); -/** - * Get the latest version of an OpenAPI spec by its slug. - */ -export const getLatestOpenAPISpecVersion = cache({ - name: 'api.getLatestOpenApiSpecVersion', - tag: (organization, openAPISpec) => - getAPICacheTag({ - tag: 'openapi', - organization, - openAPISpec, - }), - get: async (organizationId: string, slug: string, options: CacheFunctionOptions) => { - try { - const apiCtx = await api(); - const response = await apiCtx.client.orgs.getLatestOpenApiSpecVersion( - organizationId, - slug, - { - ...noCacheFetchOptions, - signal: options.signal, - }, - ); - return cacheResponse(response, { revalidateBefore: 60 * 60 }); - } catch (error) { - if (checkHasErrorCode(error, 404)) { - return { - revalidateBefore: 5, - data: null, - }; - } - - throw error; - } - }, -}); - /** * Get the latest version of an OpenAPI spec by its slug. */ diff --git a/packages/gitbook/src/lib/references.tsx b/packages/gitbook/src/lib/references.tsx index b6b76fd907..3f1d0749f0 100644 --- a/packages/gitbook/src/lib/references.tsx +++ b/packages/gitbook/src/lib/references.tsx @@ -18,7 +18,6 @@ import { SpaceContentPointer, getCollection, getDocument, - getLatestOpenAPISpecVersion, getLatestOpenAPISpecVersionContent, getPageDocument, getPublishedContentSite, @@ -282,19 +281,19 @@ export async function resolveContentRef( return null; } const { organizationId } = siteContext; - const [openAPISpecVersion, openAPISpecVersionContent] = await Promise.all([ - getLatestOpenAPISpecVersion(organizationId, contentRef.spec), - getLatestOpenAPISpecVersionContent(organizationId, contentRef.spec), - ]); + const openAPISpecVersionContent = await getLatestOpenAPISpecVersionContent( + organizationId, + contentRef.spec, + ); - if (!openAPISpecVersion || !openAPISpecVersionContent) { + if (!openAPISpecVersionContent) { return null; } return { - href: openAPISpecVersion.url, + href: openAPISpecVersionContent.url, text: contentRef.spec, active: false, - openAPIFilesystem: openAPISpecVersionContent as Filesystem, + openAPIFilesystem: openAPISpecVersionContent.filesystem as Filesystem, }; } diff --git a/packages/gitbook/tailwind.config.ts b/packages/gitbook/tailwind.config.ts index 1a56691f3a..d5a90a520c 100644 --- a/packages/gitbook/tailwind.config.ts +++ b/packages/gitbook/tailwind.config.ts @@ -8,6 +8,8 @@ import { ColorCategory, hexToRgb, scale, shadesOfColor } from '@gitbook/colors'; export const shades = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900]; export const opacities = [0, 4, 8, 12, 16, 24, 40, 64, 72, 88, 96, 100]; +export const semanticColors = ['info', 'warning', 'danger', 'success']; + /** * Generate a Tailwind color shades from a variable. */ @@ -100,6 +102,18 @@ const config: Config = { 'header-background': 'rgb(var(--header-background))', 'header-link': 'rgb(var(--header-link))', + + // Add each semantic color + ...Object.fromEntries( + semanticColors.map((color) => [color, generateVarShades(color)]), + ), + ...Object.fromEntries( + semanticColors.map((color) => [ + `contrast-${color}`, + generateVarShades(`contrast-${color}`), + ]), + ), + yellow: generateShades('#f4e28d'), teal: generateShades('#3f89a1'), pomegranate: generateShades('#f25b3a'), @@ -127,6 +141,17 @@ const config: Config = { ColorCategory.components, ColorCategory.accents, ]), + // Semantic colors + ...Object.fromEntries( + semanticColors.map((color) => [ + color, + generateVarShades(color, [ + ColorCategory.backgrounds, + ColorCategory.components, + ColorCategory.accents, + ]), + ]), + ), }, gradientColorStops: { primary: generateVarShades('primary', [ @@ -144,26 +169,71 @@ const config: Config = { ColorCategory.components, ColorCategory.accents, ]), + // Semantic colors + ...Object.fromEntries( + semanticColors.map((color) => [ + color, + generateVarShades(color, [ + ColorCategory.backgrounds, + ColorCategory.components, + ColorCategory.accents, + ]), + ]), + ), }, borderColor: { - primary: generateVarShades('primary', [ColorCategory.borders]), - tint: generateVarShades('tint', [ColorCategory.borders]), - neutral: generateVarShades('neutral', [ColorCategory.borders]), + primary: generateVarShades('primary', [ + ColorCategory.borders, + ColorCategory.accents, + ]), + tint: generateVarShades('tint', [ColorCategory.borders, ColorCategory.accents]), + neutral: generateVarShades('neutral', [ + ColorCategory.borders, + ColorCategory.accents, + ]), + // Semantic colors + ...Object.fromEntries( + semanticColors.map((color) => [ + color, + generateVarShades(color, [ColorCategory.borders, ColorCategory.accents]), + ]), + ), }, ringColor: { primary: generateVarShades('primary', [ColorCategory.borders]), tint: generateVarShades('tint', [ColorCategory.borders]), neutral: generateVarShades('neutral', [ColorCategory.borders]), + // Semantic colors + ...Object.fromEntries( + semanticColors.map((color) => [ + color, + generateVarShades(color, [ColorCategory.borders]), + ]), + ), }, outlineColor: { primary: generateVarShades('primary', [ColorCategory.borders]), tint: generateVarShades('tint', [ColorCategory.borders]), neutral: generateVarShades('neutral', [ColorCategory.borders]), + // Semantic colors + ...Object.fromEntries( + semanticColors.map((color) => [ + color, + generateVarShades(color, [ColorCategory.borders]), + ]), + ), }, boxShadowColor: { primary: generateVarShades('primary', [ColorCategory.borders]), tint: generateVarShades('tint', [ColorCategory.borders]), neutral: generateVarShades('neutral', [ColorCategory.borders]), + // Semantic colors + ...Object.fromEntries( + semanticColors.map((color) => [ + color, + generateVarShades(color, [ColorCategory.borders]), + ]), + ), }, textColor: { primary: generateVarShades('primary', [ColorCategory.text]), @@ -181,6 +251,18 @@ const config: Config = { ColorCategory.backgrounds, ColorCategory.accents, ]), + ...Object.fromEntries( + semanticColors.flatMap((color) => [ + [color, generateVarShades(color, [ColorCategory.text])], + [ + `contrast-${color}`, + generateVarShades(`contrast-${color}`, [ + ColorCategory.backgrounds, + ColorCategory.accents, + ]), + ], + ]), + ), }, textDecorationColor: { primary: generateVarShades('primary', [ColorCategory.text]), @@ -198,6 +280,18 @@ const config: Config = { ColorCategory.backgrounds, ColorCategory.accents, ]), + ...Object.fromEntries( + semanticColors.flatMap((color) => [ + [color, generateVarShades(color, [ColorCategory.text])], + [ + `contrast-${color}`, + generateVarShades(`contrast-${color}`, [ + ColorCategory.backgrounds, + ColorCategory.accents, + ]), + ], + ]), + ), }, animation: { present: 'present .5s ease-out both',