mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-19 09:06:03 +00:00
New implementation
This commit is contained in:
+38
-10
@@ -32,16 +32,6 @@ window.GitBook('show');
|
||||
|
||||
The standalone script provides a global `GitBook` function. See the [API Reference](#api-reference) section for all available methods.
|
||||
|
||||
### Color mode behavior and override
|
||||
|
||||
- If the site's Customization settings enforce a single mode (`light` or `dark`), the embed always uses that mode.
|
||||
- If the site supports both light and dark, the embed uses the page's `color-scheme` when defined or the browser/OS preference as a fallback.
|
||||
- You can also force the standalone widget to a specific mode (if the site supports it) by setting `data-color-scheme` on the button and/or window elements.
|
||||
|
||||
Supported values:
|
||||
- `light`
|
||||
- `dark`
|
||||
|
||||
### Example: Configuring the widget
|
||||
|
||||
```javascript
|
||||
@@ -295,6 +285,44 @@ Display GitBook branding in the embed. Defaults to true.
|
||||
trademark: true
|
||||
```
|
||||
|
||||
### Theming and `color-scheme` (CSS-first)
|
||||
|
||||
The embed supports both site-controlled theming and CSS-driven theming.
|
||||
|
||||
Precedence (highest → lowest):
|
||||
|
||||
- **Site mode / forced theme**: if the GitBook site does not support multiple themes, the embed is forced to the site’s default theme.
|
||||
- **Visitor preference**: if the site supports multiple themes and the visitor has previously selected a theme on the site, that preference is remembered.
|
||||
- **Browser/OS default**: otherwise the embed follows the browser/OS preference (`prefers-color-scheme`).
|
||||
|
||||
You can also drive the embed theme from CSS by setting `color-scheme` on the iframe element (or a parent it inherits from). When the iframe resolves to an explicit `color-scheme: light` or `color-scheme: dark`, that value is propagated into the embedded content so it renders consistently.
|
||||
|
||||
Standalone widget example:
|
||||
|
||||
```css
|
||||
/* Force the GitBook widget iframe to render in dark mode */
|
||||
#gitbook-widget-iframe {
|
||||
color-scheme: dark;
|
||||
}
|
||||
```
|
||||
|
||||
### `colorScheme`
|
||||
|
||||
Available in: Standalone script, NPM package, React components
|
||||
|
||||
Force the embed to render in a specific color scheme.
|
||||
|
||||
- **Type**: `'light' | 'dark'`
|
||||
- **Default**: `undefined` (follow site/visitor preference/system)
|
||||
|
||||
```javascript
|
||||
GitBook('configure', {
|
||||
colorScheme: 'dark'
|
||||
});
|
||||
```
|
||||
|
||||
To clear an override, omit `colorScheme` (or set it to `undefined` in JS) in a subsequent `configure` call.
|
||||
|
||||
### `actions`
|
||||
|
||||
Available in: Standalone script, NPM package, React components
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"scripts": {
|
||||
"build": "bun run build-lib && bun run build-standalone",
|
||||
"build-lib": "tsdown",
|
||||
"build-standalone": "bun build src/standalone/index.ts --bundle --minify --outdir=standalone",
|
||||
"build-standalone": "bun run ./scripts/build-standalone.ts",
|
||||
"clean": "rm -rf ./dist",
|
||||
"unit": "bun test",
|
||||
"typecheck": "tsc --noEmit",
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import { Features, transform } from 'lightningcss';
|
||||
|
||||
/**
|
||||
* Build the standalone embed script.
|
||||
* Bun's default CSS transpiler (which is a port of LightningCSS) strips out the native light-dark() function in favour of a polyfill.
|
||||
* Light-dark() is widely supported now, and the polyfill requires you to set a data attribute on the element instead of relying on plain CSS.
|
||||
* This script's purpose is to pass a feature flag to the CSS transpiler to keep the native light-dark() behavior.
|
||||
*/
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const child = spawn(
|
||||
'bun',
|
||||
['build', 'src/standalone/index.ts', '--bundle', '--minify', '--outdir=standalone'],
|
||||
{ stdio: 'inherit' }
|
||||
);
|
||||
child.on('error', reject);
|
||||
child.on('exit', (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
reject(new Error(`bun build failed with exit code ${code ?? 'unknown'}`));
|
||||
});
|
||||
});
|
||||
|
||||
const sourceCSS = await readFile('src/standalone/style.css');
|
||||
const transformedCSS = transform({
|
||||
filename: 'src/standalone/style.css',
|
||||
code: sourceCSS,
|
||||
minify: true,
|
||||
// Keep native light-dark() behavior scoped to element color-scheme.
|
||||
exclude: Features.LightDark,
|
||||
});
|
||||
|
||||
await writeFile('standalone/index.css', transformedCSS.code);
|
||||
@@ -83,6 +83,11 @@ export type GitBookEmbeddableConfiguration = {
|
||||
* Display a close button inside the assistant.
|
||||
*/
|
||||
closeButton?: boolean;
|
||||
|
||||
/**
|
||||
* Force the embed to render in a specific color-scheme.
|
||||
*/
|
||||
colorScheme?: 'light' | 'dark';
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -55,6 +55,7 @@ let widgetIframe: HTMLIFrameElement | undefined;
|
||||
let _client: GitBookClient | undefined;
|
||||
let _frame: GitBookFrameClient | undefined;
|
||||
let frameOptions: GetFrameURLOptions | undefined;
|
||||
let lastPushedColorScheme: 'light' | 'dark' | undefined;
|
||||
let frameConfiguration: GitBookEmbeddableConfiguration & StandaloneConfiguration = {
|
||||
button: {
|
||||
label: 'Ask',
|
||||
@@ -85,6 +86,52 @@ widgetWindow.classList.add('hidden');
|
||||
document.body.appendChild(widgetButton);
|
||||
document.body.appendChild(widgetWindow);
|
||||
|
||||
function pushColorSchemeToFrame() {
|
||||
if (!_frame) return;
|
||||
|
||||
// Manual override via configure always wins.
|
||||
const desired =
|
||||
frameConfiguration.colorScheme ??
|
||||
(widgetIframe
|
||||
? ((() => {
|
||||
const declared = getComputedStyle(widgetIframe).colorScheme.trim().toLowerCase();
|
||||
if (declared === 'dark') return 'dark';
|
||||
if (declared === 'light') return 'light';
|
||||
// If the iframe element doesn't have an explicit scheme, don't force anything.
|
||||
// The embedded content can rely on `forcedTheme` or system theme.
|
||||
return undefined;
|
||||
})() as 'light' | 'dark' | undefined)
|
||||
: undefined);
|
||||
|
||||
if (desired === lastPushedColorScheme) {
|
||||
return;
|
||||
}
|
||||
lastPushedColorScheme = desired;
|
||||
|
||||
_frame.configure({
|
||||
...frameConfiguration,
|
||||
colorScheme: desired,
|
||||
});
|
||||
}
|
||||
|
||||
// Watch for host theme changes and push them down into the iframe.
|
||||
// This does not mutate the host/widget styling; it only re-configures the frame content.
|
||||
if (typeof MutationObserver !== 'undefined') {
|
||||
const observer = new MutationObserver(() => pushColorSchemeToFrame());
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class', 'style'],
|
||||
});
|
||||
// Safari needs an explicit watch on the widget container when `color-scheme`
|
||||
// is overridden there (e.g. `#gitbook-widget-window { color-scheme: dark; }`).
|
||||
observer.observe(widgetWindow, { attributes: true, attributeFilter: ['class', 'style'] });
|
||||
}
|
||||
{
|
||||
const mql = window.matchMedia?.('(prefers-color-scheme: dark)');
|
||||
const handler = () => pushColorSchemeToFrame();
|
||||
mql?.addEventListener?.('change', handler);
|
||||
}
|
||||
|
||||
function getClient() {
|
||||
if (!_client) {
|
||||
throw new Error(
|
||||
@@ -99,6 +146,7 @@ function getIframe() {
|
||||
const client = getClient();
|
||||
|
||||
widgetIframe?.remove();
|
||||
lastPushedColorScheme = undefined;
|
||||
widgetIframe = document.createElement('iframe');
|
||||
widgetIframe.id = 'gitbook-widget-iframe';
|
||||
widgetIframe.src = client.getFrameURL({
|
||||
@@ -111,6 +159,8 @@ function getIframe() {
|
||||
widgetWindow.classList.add('hidden');
|
||||
widgetButton.classList.remove('open');
|
||||
});
|
||||
|
||||
pushColorSchemeToFrame();
|
||||
}
|
||||
return { iframe: widgetIframe, frame: _frame };
|
||||
}
|
||||
@@ -191,6 +241,7 @@ const GitBook = (...args: StandaloneCalls) => {
|
||||
getIframe().frame.configure({
|
||||
...frameConfiguration,
|
||||
});
|
||||
pushColorSchemeToFrame();
|
||||
break;
|
||||
}
|
||||
case 'clearChat':
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import React, { useEffect, useRef } from 'react';
|
||||
|
||||
import { useAI, useAIChatController } from '@/components/AI';
|
||||
import { CustomizationAIMode } from '@gitbook/api';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { createStore, useStore } from 'zustand';
|
||||
import { integrationsAssistantTools } from '../Integrations';
|
||||
@@ -36,11 +37,12 @@ export function EmbeddableIframeAPI(props: {
|
||||
|
||||
const router = useRouter();
|
||||
const chatController = useAIChatController();
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
// Live ref to avoid adding them as dependencies
|
||||
const refs = useRef({ router, chatController, baseURL });
|
||||
const refs = useRef({ router, chatController, baseURL, setTheme });
|
||||
useEffect(() => {
|
||||
refs.current = { router, chatController, baseURL };
|
||||
refs.current = { router, chatController, baseURL, setTheme };
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -57,7 +59,7 @@ export function EmbeddableIframeAPI(props: {
|
||||
}
|
||||
|
||||
channel.receive((payload) => {
|
||||
const { baseURL, router, chatController } = refs.current;
|
||||
const { baseURL, router, chatController, setTheme } = refs.current;
|
||||
const message = payload as ParentToFrameMessage;
|
||||
|
||||
log('[gitbook] received message', message);
|
||||
@@ -78,6 +80,15 @@ export function EmbeddableIframeAPI(props: {
|
||||
integrationsAssistantTools.setState({
|
||||
tools: message.settings.tools,
|
||||
});
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(message.settings, 'colorScheme')) {
|
||||
const colorScheme = message.settings.colorScheme;
|
||||
if (!colorScheme) {
|
||||
setTheme('system');
|
||||
} else {
|
||||
setTheme(colorScheme);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'navigateToPage': {
|
||||
@@ -99,7 +110,6 @@ export function EmbeddableIframeAPI(props: {
|
||||
* Hook to get the configuration from the parent window.
|
||||
*/
|
||||
export function useEmbeddableConfiguration<T = GitBookEmbeddableConfiguration>(
|
||||
// @ts-expect-error - This is a workaround to allow the function to be optional.
|
||||
fn: (state: GitBookEmbeddableConfiguration) => T = (state) => state
|
||||
) {
|
||||
return useStore(embeddableConfiguration, fn);
|
||||
|
||||
Reference in New Issue
Block a user