mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-17 08:05:19 +00:00
Docs Embed: Better support light/dark mode overrides (#4181)
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
"@gitbook/embed": patch
|
||||
---
|
||||
|
||||
Docs Embed: Better support light/dark mode overrides
|
||||
@@ -72,6 +72,7 @@ const gitbook = createGitBook({
|
||||
// Create an iframe and get its URL
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.src = gitbook.getFrameURL({
|
||||
colorScheme: 'dark', // Optional: force the embed to render in dark mode
|
||||
visitor: {
|
||||
token: 'your-jwt-token', // Optional: for Adaptive Content or Authenticated Access
|
||||
unsignedClaims: { // Optional: custom claims for dynamic expressions
|
||||
@@ -122,6 +123,7 @@ import { GitBookProvider, GitBookFrame } from '@gitbook/embed/react';
|
||||
|
||||
<GitBookProvider siteURL="https://docs.company.com">
|
||||
<GitBookFrame
|
||||
colorScheme="dark"
|
||||
visitor={{
|
||||
token: 'your-jwt-token', // Optional: for Adaptive Content or Authenticated Access
|
||||
unsignedClaims: { userId: '123' } // Optional: custom claims for dynamic expressions
|
||||
@@ -150,7 +152,7 @@ import { useGitBook } from '@gitbook/embed/react';
|
||||
|
||||
function MyComponent() {
|
||||
const gitbook = useGitBook();
|
||||
const frameURL = gitbook.getFrameURL({ visitor: { token: '...' } });
|
||||
const frameURL = gitbook.getFrameURL({ colorScheme: 'dark', visitor: { token: '...' } });
|
||||
// ...
|
||||
}
|
||||
```
|
||||
@@ -178,7 +180,7 @@ function MyComponent() {
|
||||
|
||||
### Standalone Script
|
||||
|
||||
- `GitBook('init', options: { siteURL: string }, frameOptions?: { visitor?: {...} })` - Initialize widget
|
||||
- `GitBook('init', options: { siteURL: string }, frameOptions?: { colorScheme?: 'light' | 'dark', visitor?: {...} })` - Initialize widget
|
||||
- `GitBook('show')` - Show widget button
|
||||
- `GitBook('hide')` - Hide widget button
|
||||
- `GitBook('open')` - Open widget window
|
||||
@@ -195,7 +197,7 @@ function MyComponent() {
|
||||
|
||||
**Client Factory:**
|
||||
- `createGitBook(options: { siteURL: string })` → `GitBookClient`
|
||||
- `client.getFrameURL(options?: { visitor?: {...} })` → `string`
|
||||
- `client.getFrameURL(options?: { colorScheme?: 'light' | 'dark', visitor?: {...} })` → `string`
|
||||
- `client.createFrame(iframe: HTMLIFrameElement)` → `GitBookFrameClient`
|
||||
|
||||
**Frame Client:**
|
||||
@@ -457,6 +459,24 @@ visitor: {
|
||||
}
|
||||
```
|
||||
|
||||
### `colorScheme`
|
||||
|
||||
Available in: Standalone script (via `init`), NPM package (via `getFrameURL()`), React components (as prop)
|
||||
|
||||
Override the embed's color scheme. When omitted, the embed follows the iframe's CSS `color-scheme`, which lets it inherit the parent page or browser preference.
|
||||
|
||||
**Note**: This is not a configuration option but rather a parameter when initializing the frame or creating the frame URL.
|
||||
|
||||
**Standalone script**: Pass as the second argument to `GitBook('init', options, frameOptions)`
|
||||
**NPM package**: Pass to `getFrameURL({ colorScheme: 'dark' })`
|
||||
**React components**: Pass as the `colorScheme` prop on `<GitBookFrame>`
|
||||
|
||||
- **Type**: `'light' | 'dark'`
|
||||
|
||||
```javascript
|
||||
colorScheme: 'dark'
|
||||
```
|
||||
|
||||
### `button`
|
||||
|
||||
Available in: Standalone script only
|
||||
|
||||
@@ -35,4 +35,17 @@ describe('createGitBook.getFrameURL', () => {
|
||||
expect(url.searchParams.get('visitor.count')).toBe('3');
|
||||
expect(url.searchParams.get('visitor.enabled')).toBe('false');
|
||||
});
|
||||
|
||||
it('adds an explicit color scheme override when requested', () => {
|
||||
const client = createGitBook({ siteURL: 'https://example.com/docs/' });
|
||||
|
||||
const url = new URL(
|
||||
client.getFrameURL({
|
||||
colorScheme: 'dark',
|
||||
})
|
||||
);
|
||||
|
||||
expect(url.pathname).toBe('/docs/~gitbook/embed');
|
||||
expect(url.searchParams.get('theme')).toBe('dark');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,12 @@ export type CreateGitBookOptions = {
|
||||
};
|
||||
|
||||
export type GetFrameURLOptions = {
|
||||
/**
|
||||
* Override the color scheme used by the embedded docs.
|
||||
* When omitted, the embed follows the iframe's CSS `color-scheme`.
|
||||
*/
|
||||
colorScheme?: 'light' | 'dark';
|
||||
|
||||
/**
|
||||
* Authentication to use for the frame.
|
||||
*/
|
||||
@@ -42,6 +48,10 @@ export function createGitBook(options: CreateGitBookOptions) {
|
||||
const url = new URL(options.siteURL);
|
||||
url.pathname = `${url.pathname.endsWith('/') ? url.pathname : `${url.pathname}/`}~gitbook/embed`;
|
||||
|
||||
if (frameOptions.colorScheme) {
|
||||
url.searchParams.set('theme', frameOptions.colorScheme);
|
||||
}
|
||||
|
||||
if (frameOptions.visitor?.token) {
|
||||
url.searchParams.set('jwt_token', frameOptions.visitor.token);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export type GitBookFrameProps = {
|
||||
export function GitBookFrame(props: GitBookFrameProps) {
|
||||
const {
|
||||
className,
|
||||
colorScheme,
|
||||
visitor,
|
||||
actions = [],
|
||||
greeting,
|
||||
@@ -34,7 +35,10 @@ export function GitBookFrame(props: GitBookFrameProps) {
|
||||
const gitbook = useGitBook();
|
||||
const [gitbookFrame, setGitbookFrame] = useState<GitBookFrameClient | null>(null);
|
||||
|
||||
const frameURL = useMemo(() => gitbook.getFrameURL({ visitor }), [gitbook, visitor]);
|
||||
const frameURL = useMemo(
|
||||
() => gitbook.getFrameURL({ visitor, colorScheme }),
|
||||
[gitbook, visitor, colorScheme]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (frameRef.current) {
|
||||
@@ -73,6 +77,7 @@ export function GitBookFrame(props: GitBookFrameProps) {
|
||||
width="100%"
|
||||
height="100%"
|
||||
className={className}
|
||||
style={colorScheme ? { colorScheme } : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -101,6 +101,9 @@ function getIframe() {
|
||||
widgetIframe?.remove();
|
||||
widgetIframe = document.createElement('iframe');
|
||||
widgetIframe.id = 'gitbook-widget-iframe';
|
||||
if (frameOptions?.colorScheme) {
|
||||
widgetIframe.style.colorScheme = frameOptions.colorScheme;
|
||||
}
|
||||
widgetIframe.src = client.getFrameURL({
|
||||
...frameOptions,
|
||||
});
|
||||
|
||||
+3
@@ -5,6 +5,7 @@ import {
|
||||
generateEmbeddableViewport,
|
||||
} from '@/components/Embeddable';
|
||||
import { getEmbeddableStaticContext } from '@/lib/embeddable';
|
||||
import { getThemeFromMiddleware } from '@/lib/middleware';
|
||||
import { shouldTrackEvents } from '@/lib/tracking';
|
||||
import { headers } from 'next/headers';
|
||||
|
||||
@@ -18,12 +19,14 @@ export default async function RootLayout({
|
||||
}: React.PropsWithChildren<SiteStaticLayoutProps>) {
|
||||
const { context, visitorAuthClaims } = await getEmbeddableStaticContext(await params);
|
||||
const withTracking = shouldTrackEvents(await headers());
|
||||
const forcedTheme = await getThemeFromMiddleware();
|
||||
|
||||
return (
|
||||
<EmbeddableRootLayout
|
||||
context={context}
|
||||
withTracking={withTracking}
|
||||
visitorAuthClaims={visitorAuthClaims}
|
||||
forcedTheme={forcedTheme}
|
||||
>
|
||||
{children}
|
||||
</EmbeddableRootLayout>
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
} from '@/components/SiteLayout';
|
||||
import type { VisitorAuthClaims } from '@/lib/adaptive';
|
||||
import type { GitBookSiteContext } from '@/lib/context';
|
||||
import { resolveEmbeddableTheme } from '@/lib/embeddable';
|
||||
import type { CustomizationDefaultThemeMode } from '@gitbook/api';
|
||||
import { SiteInsightsTrademarkPlacement } from '@gitbook/api';
|
||||
import { SpaceLayoutServerContext } from '../SpaceLayout';
|
||||
import { Trademark } from '../TableOfContents/Trademark';
|
||||
@@ -18,6 +20,7 @@ type EmbeddableRootLayoutProps = {
|
||||
context: GitBookSiteContext;
|
||||
withTracking: boolean;
|
||||
visitorAuthClaims: VisitorAuthClaims;
|
||||
forcedTheme?: CustomizationDefaultThemeMode | null;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -27,17 +30,22 @@ export async function EmbeddableRootLayout({
|
||||
context,
|
||||
withTracking,
|
||||
visitorAuthClaims,
|
||||
forcedTheme,
|
||||
children,
|
||||
}: React.PropsWithChildren<EmbeddableRootLayoutProps>) {
|
||||
const theme = resolveEmbeddableTheme(context.customization, forcedTheme);
|
||||
|
||||
return (
|
||||
<CustomizationRootLayout context={context} htmlClassName="embed">
|
||||
<CustomizationRootLayout
|
||||
context={context}
|
||||
htmlClassName="embed"
|
||||
forcedTheme={theme.htmlTheme}
|
||||
>
|
||||
<SiteLayoutClientContexts
|
||||
forcedTheme={
|
||||
context.customization.themes.toggeable
|
||||
? undefined
|
||||
: context.customization.themes.default
|
||||
}
|
||||
defaultTheme={context.customization.themes.default}
|
||||
forcedTheme={theme.forcedTheme}
|
||||
defaultTheme={theme.defaultTheme}
|
||||
// Keep embed theme separate from site so it does not reuse the full site's saved theme and vice versa.
|
||||
themeStorageKey={`gitbook-theme-embed:${context.site.id}`}
|
||||
externalLinksTarget={context.customization.externalLinks.target}
|
||||
contextId={context.contextId}
|
||||
proxyOrigin={context.site.proxy?.origin}
|
||||
|
||||
@@ -17,13 +17,21 @@ import { isExternalLink } from '../utils/link';
|
||||
export function SiteLayoutClientContexts(props: {
|
||||
forcedTheme: CustomizationDefaultThemeMode | undefined;
|
||||
defaultTheme: CustomizationDefaultThemeMode | undefined;
|
||||
themeStorageKey?: string;
|
||||
externalLinksTarget: SiteExternalLinksTarget;
|
||||
contextId: string | undefined;
|
||||
proxyOrigin: string | undefined;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { children, forcedTheme, defaultTheme, externalLinksTarget, contextId, proxyOrigin } =
|
||||
props;
|
||||
const {
|
||||
children,
|
||||
forcedTheme,
|
||||
defaultTheme,
|
||||
themeStorageKey,
|
||||
externalLinksTarget,
|
||||
contextId,
|
||||
proxyOrigin,
|
||||
} = props;
|
||||
|
||||
useClearRouterCache(contextId);
|
||||
|
||||
@@ -51,6 +59,7 @@ export function SiteLayoutClientContexts(props: {
|
||||
enableSystem
|
||||
forcedTheme={forcedTheme}
|
||||
defaultTheme={defaultTheme}
|
||||
storageKey={themeStorageKey}
|
||||
>
|
||||
<NuqsAdapter>
|
||||
<LinkContext.Provider value={linkContext}>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { getEmbeddableLinker } from './embeddable';
|
||||
import { CustomizationDefaultThemeMode, type SiteCustomizationSettings } from '@gitbook/api';
|
||||
import { getEmbeddableLinker, resolveEmbeddableTheme } from './embeddable';
|
||||
import { createLinker } from './links';
|
||||
|
||||
describe('getEmbeddableLinker', () => {
|
||||
@@ -21,3 +22,58 @@ describe('getEmbeddableLinker', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveEmbeddableTheme', () => {
|
||||
function createCustomization(
|
||||
themes: SiteCustomizationSettings['themes']
|
||||
): Pick<SiteCustomizationSettings, 'themes'> {
|
||||
return { themes };
|
||||
}
|
||||
|
||||
it('follows the frame color scheme for multi-theme sites by default', () => {
|
||||
expect(
|
||||
resolveEmbeddableTheme(
|
||||
createCustomization({
|
||||
toggeable: true,
|
||||
default: CustomizationDefaultThemeMode.Dark,
|
||||
})
|
||||
)
|
||||
).toEqual({
|
||||
htmlTheme: CustomizationDefaultThemeMode.System,
|
||||
defaultTheme: CustomizationDefaultThemeMode.System,
|
||||
forcedTheme: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts an explicit override for multi-theme sites', () => {
|
||||
expect(
|
||||
resolveEmbeddableTheme(
|
||||
createCustomization({
|
||||
toggeable: true,
|
||||
default: CustomizationDefaultThemeMode.Light,
|
||||
}),
|
||||
CustomizationDefaultThemeMode.Dark
|
||||
)
|
||||
).toEqual({
|
||||
htmlTheme: CustomizationDefaultThemeMode.Dark,
|
||||
defaultTheme: CustomizationDefaultThemeMode.Dark,
|
||||
forcedTheme: CustomizationDefaultThemeMode.Dark,
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps the site theme for single-theme sites', () => {
|
||||
expect(
|
||||
resolveEmbeddableTheme(
|
||||
createCustomization({
|
||||
toggeable: false,
|
||||
default: CustomizationDefaultThemeMode.Light,
|
||||
}),
|
||||
CustomizationDefaultThemeMode.Dark
|
||||
)
|
||||
).toEqual({
|
||||
htmlTheme: CustomizationDefaultThemeMode.Light,
|
||||
defaultTheme: CustomizationDefaultThemeMode.Light,
|
||||
forcedTheme: CustomizationDefaultThemeMode.Light,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { GitBookSiteContext } from '@/lib/context';
|
||||
import type { GitBookLinker } from '@/lib/links';
|
||||
import { getPagePath } from '@/lib/pages';
|
||||
import { joinPath } from '@/lib/paths';
|
||||
import { CustomizationDefaultThemeMode, type SiteCustomizationSettings } from '@gitbook/api';
|
||||
|
||||
/**
|
||||
* Get the context for the embeddable static routes.
|
||||
@@ -68,3 +69,35 @@ export function getEmbeddableLinker(linker: GitBookLinker): GitBookLinker {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve theme behavior for docs embeds.
|
||||
* Embeds should follow the parent frame's color-scheme by default,
|
||||
* while still allowing an explicit override for multi-theme sites.
|
||||
*/
|
||||
export function resolveEmbeddableTheme(
|
||||
customization: Pick<SiteCustomizationSettings, 'themes'>,
|
||||
forcedTheme?: CustomizationDefaultThemeMode | null
|
||||
) {
|
||||
if (!customization.themes.toggeable) {
|
||||
return {
|
||||
htmlTheme: customization.themes.default,
|
||||
defaultTheme: customization.themes.default,
|
||||
forcedTheme: customization.themes.default,
|
||||
};
|
||||
}
|
||||
|
||||
if (forcedTheme) {
|
||||
return {
|
||||
htmlTheme: forcedTheme,
|
||||
defaultTheme: forcedTheme,
|
||||
forcedTheme,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
htmlTheme: CustomizationDefaultThemeMode.System,
|
||||
defaultTheme: CustomizationDefaultThemeMode.System,
|
||||
forcedTheme: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user