Compare commits

..

2 Commits

Author SHA1 Message Date
Viktor Renkema bddd458446 Add buttons to hide toolbar 2025-10-03 11:53:39 +02:00
Viktor Renkema 174e7f43a2 wip 2025-10-02 10:50:23 +02:00
128 changed files with 2221 additions and 4855 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Fix missing geo data in site insights because of incorrect proxying of events.
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Add stable scroll gutter to search container
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Fix robots.txt preventing favicon from being indexed.
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Make search accessible
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Improve default site icon
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Use sitespace title instead of space title in search
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Support nested site section groups
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": minor
---
Show a toolbar for authenticated users with access to the current site.
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Add support for inline expressions rendering with visitor data on GBO side
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Update to column width sizing
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Expose "Best match" when site has translations
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Improve screen reader accessibility for hints
+5
View File
@@ -0,0 +1,5 @@
---
"@gitbook/react-openapi": minor
---
Fix OpenAPISecurities and code sample not using operation security requirements
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Fix crash for card cover defined without objectFit
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Add scrollcontainer component
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Refactor section tabs
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Fix OpenAPI response dropdown & vertical overflow
+5
View File
@@ -0,0 +1,5 @@
---
"@gitbook/react-openapi": patch
---
Fix OpenAPI response showing as JSON instead of YAML when it should
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": minor
---
Scope search across sections and variants
-1
View File
@@ -37,7 +37,6 @@ jobs:
# https://github.com/orgs/community/discussions/26875#discussioncomment-3253761
GITHUB_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+1285 -2265
View File
File diff suppressed because it is too large Load Diff
+8 -10
View File
@@ -3,9 +3,9 @@
"version": "0.1.0",
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@changesets/cli": "^2.29.7",
"turbo": "^2.5.8",
"vercel": "^39.4.2"
"@changesets/cli": "^2.27.12",
"turbo": "^2.5.0",
"vercel": "^39.3.0"
},
"packageManager": "bun@1.2.15",
"overrides": {
@@ -16,7 +16,7 @@
},
"private": true,
"scripts": {
"dev": "turbo run dev --concurrency 20",
"dev": "turbo run dev",
"build": "turbo run build",
"clean-deps": "rm -rf node_modules && rm -rf packages/*/node_modules",
"typecheck": "turbo run typecheck",
@@ -26,18 +26,16 @@
"e2e": "turbo run e2e",
"e2e-customers": "turbo run e2e-customers",
"changeset": "changeset",
"changeset-version": "changeset version && bun run format && bun update",
"release": "turbo run release && bun run publish-all-packages",
"publish-all-packages": "for dir in packages/*; do (cd \"$dir\" && bun publish || true); done && changeset tag",
"changeset-version": "changeset version && bun run format",
"release": "turbo run release && changeset publish",
"download:env": "op read op://gitbook-x-dev/gitbook-open/.env.local >> .env.local",
"clean": "turbo run clean"
},
"workspaces": {
"packages": ["packages/*"],
"catalog": {
"@gitbook/api": "0.143.2",
"bidc": "^0.0.2",
"tsdown": "^0.15.6"
"@gitbook/api": "^0.142.0",
"bidc": "^0.0.2"
}
},
"patchedDependencies": {
-8
View File
@@ -1,13 +1,5 @@
# @gitbook/browser-types
## 0.1.1
### Patch Changes
- 295f03d: Republish packages
- Updated dependencies [295f03d]
- @gitbook/icons@0.3.2
## 0.1.0
### Minor Changes
+2 -6
View File
@@ -8,7 +8,7 @@
"default": "./dist/index.js"
}
},
"version": "0.1.1",
"version": "0.1.0",
"dependencies": {
"@gitbook/api": "catalog:",
"@gitbook/icons": "workspace:"
@@ -20,9 +20,5 @@
"build": "tsc",
"typecheck": "tsc --noEmit"
},
"files": ["dist", "README.md", "CHANGELOG.md"],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
}
"files": ["dist", "README.md", "CHANGELOG.md"]
}
-6
View File
@@ -1,11 +1,5 @@
# @gitbook/cache-tags
## 0.3.2
### Patch Changes
- 295f03d: Republish packages
## 0.3.1
### Patch Changes
+2 -6
View File
@@ -8,7 +8,7 @@
"default": "./dist/index.js"
}
},
"version": "0.3.2",
"version": "0.3.1",
"dependencies": {
"@gitbook/api": "catalog:",
"assert-never": "^1.2.1"
@@ -21,9 +21,5 @@
"typecheck": "tsc --noEmit",
"dev": "tsc -w"
},
"files": ["dist", "src", "README.md", "CHANGELOG.md"],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
}
"files": ["dist", "src", "README.md", "CHANGELOG.md"]
}
-6
View File
@@ -1,11 +1,5 @@
# @gitbook/colors
## 0.4.1
### Patch Changes
- 295f03d: Republish packages
## 0.4.0
### Minor Changes
+2 -6
View File
@@ -8,7 +8,7 @@
"default": "./dist/index.js"
}
},
"version": "0.4.1",
"version": "0.4.0",
"devDependencies": {
"typescript": "^5.5.3"
},
@@ -17,9 +17,5 @@
"typecheck": "tsc --noEmit",
"dev": "tsc -w"
},
"files": ["dist", "src", "README.md", "CHANGELOG.md"],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
}
"files": ["dist", "src", "README.md", "CHANGELOG.md"]
}
-14
View File
@@ -1,19 +1,5 @@
# @gitbook/embed
## 0.1.3
### Patch Changes
- 295f03d: Republish packages
- Updated dependencies [295f03d]
- @gitbook/icons@0.3.2
## 0.1.2
### Patch Changes
- 7508674: Fix bundling of packages
## 0.1.1
### Patch Changes
+7 -18
View File
@@ -5,14 +5,12 @@
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./react": {
"types": "./dist/react/index.d.ts",
"default": "./dist/react/index.js"
"default": "./dist/index.js",
"standalone": "./dist/standalone/index.js",
"react": "./dist/react/index.js"
}
},
"version": "0.1.3",
"version": "0.1.1",
"dependencies": {
"@gitbook/api": "catalog:",
"@gitbook/icons": "workspace:",
@@ -22,21 +20,12 @@
"react": "^18.0.0"
},
"devDependencies": {
"tsdown": "catalog:",
"typescript": "^5.5.3",
"react": "^19.0.0"
},
"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",
"clean": "rm -rf ./dist",
"typecheck": "tsc --noEmit",
"dev": "bun run build -- --watch ./src"
"build": "tsc && bun build src/standalone/index.ts --bundle --minify --outdir=standalone",
"typecheck": "tsc --noEmit"
},
"files": ["dist", "README.md", "CHANGELOG.md"],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
}
"files": ["dist", "README.md", "CHANGELOG.md", "standalone"]
}
-9
View File
@@ -1,9 +0,0 @@
import { defineConfig } from 'tsdown';
export default defineConfig([
{
entry: ['src/index.ts', 'src/react/index.ts'],
dts: true,
format: ['esm'],
},
]);
+2 -8
View File
@@ -1,19 +1,13 @@
# @gitbook/emoji-codepoints
## 0.2.1
### Patch Changes
- 295f03d: Republish packages
## 0.2.0
### Minor Changes
- 57adb3e: Second release to fix publishing with changeset
- 57adb3e: Second release to fix publishing with changeset
## 0.1.0
### Minor Changes
- 5f8a8fe: Initial release
- 5f8a8fe: Initial release
+1 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@gitbook/emoji-codepoints",
"description": "Optimized mapping of codepoints to the fully qualified emoji codepoints",
"version": "0.2.1",
"version": "0.2.0",
"private": true,
"exports": "./dist/index.ts",
"dependencies": {},
@@ -11,9 +11,5 @@
"scripts": {
"generate": "bun ./build.ts",
"clean": "rm -rf ./dist"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
}
}
-12
View File
@@ -1,17 +1,5 @@
# @gitbook/expr
## 1.2.2
### Patch Changes
- 295f03d: Republish packages
## 1.2.1
### Patch Changes
- a629900: Add dev script for @gitbook/expr
## 1.2.0
### Minor Changes
+5 -10
View File
@@ -1,7 +1,7 @@
{
"name": "@gitbook/expr",
"description": "Safely evaluate & parse user-defined GitBook expressions.",
"version": "1.2.2",
"version": "1.2.0",
"type": "module",
"exports": {
".": {
@@ -21,22 +21,17 @@
},
"devDependencies": {
"bun-types": "^1.1.20",
"tsdown": "catalog:",
"tsdown": "^0.15.0",
"@types/estree": "^1.0.6",
"@babel/types": "^7.26.0",
"@types/json-schema": "^7.0.15",
"@types/escodegen": "^0.0.10"
},
"scripts": {
"build": "tsdown",
"build": "tsdown --project tsconfig.build.json",
"typecheck": "tsc --noEmit",
"unit": "bun test",
"clean": "rm -rf ./dist",
"dev": "bun run build -- --watch ./src"
"clean": "rm -rf ./dist"
},
"files": ["dist", "README.md", "CHANGELOG.md"],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
}
"files": ["dist", "README.md", "CHANGELOG.md"]
}
+10
View File
@@ -0,0 +1,10 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": ["./tsconfig.json"],
"exclude": ["**/*.test.ts"],
"compilerOptions": {
"declaration": true,
"noEmit": false,
"outDir": "dist"
}
}
-6
View File
@@ -1,11 +1,5 @@
# @gitbook/fonts
## 0.1.1
### Patch Changes
- 295f03d: Republish packages
## 0.1.0
### Minor Changes
+1 -5
View File
@@ -8,7 +8,7 @@
"default": "./dist/index.js"
}
},
"version": "0.1.1",
"version": "0.1.0",
"dependencies": {
"@gitbook/api": "catalog:"
},
@@ -27,9 +27,5 @@
"files": ["dist", "src", "bin", "README.md", "CHANGELOG.md"],
"engines": {
"node": ">=20.0.0"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
}
}
-69
View File
@@ -1,74 +1,5 @@
# gitbook
## 0.19.1
### Patch Changes
- 295f03d: Republish packages
- Updated dependencies [bdde392]
- Updated dependencies [295f03d]
- @gitbook/react-contentkit@0.7.6
- @gitbook/browser-types@0.1.1
- @gitbook/cache-tags@0.3.2
- @gitbook/colors@0.4.1
- @gitbook/emoji-codepoints@0.2.1
- @gitbook/expr@1.2.2
- @gitbook/fonts@0.1.1
- @gitbook/icons@0.3.2
- @gitbook/openapi-parser@3.0.3
- @gitbook/react-math@0.6.2
- @gitbook/react-openapi@1.5.1
## 0.19.0
### Minor Changes
- ed7d47d: Show a toolbar for authenticated users with access to the current site.
- 50c1be3: Move inline expression evaluation from API to GBO
- 56e46ce: Add Italian translation
- b932e4e: Scope search across sections and variants
### Patch Changes
- 162cfe4: Hide sections if only one is present
- 1456251: Unify section display condition
- 9344431: Fix missing geo data in site insights because of incorrect proxying of events.
- eea8f1e: Enhance OpenAPI security scopes handling
- 2b8a2d2: Add stable scroll gutter to search container
- b498521: Fix robots.txt preventing favicon from being indexed.
- ceb32b1: Redo search scope UI
- 8e99871: Highlight discriminator properties in oneOf, allOf, anyOf objects
- 2fc2127: Make search accessible
- f3e4041: Adapt OpenAPI code samples to prefill API key using visitor data
- 6815dd1: Fix markdownPageURL for PageActions
- c06b3dd: Improve default site icon
- cba583a: Use sitespace title instead of space title in search
- e434442: Support nested site section groups
- 469b332: Add support for inline expressions rendering with visitor data on GBO side
- 0ef5dc8: Update to column width sizing
- 1e4e54e: Fix rounded outline items
- 379d486: Expose "Best match" when site has translations
- c21693f: Improve screen reader accessibility for hints
- badf3a9: Add alt text to inline image
- c23d4ef: Fix crash for card cover defined without objectFit
- d74f8a7: Improve wide page width layout
- b8388e5: Add scrollcontainer component
- b85eccd: Refactor section tabs
- 1e53f46: Fix button content truncate
- 7a504b7: Fix OpenAPI response dropdown & vertical overflow
- a4c3399: Fix OpenAPI alternatives not showing
- Updated dependencies [a629900]
- Updated dependencies [eea8f1e]
- Updated dependencies [8e99871]
- Updated dependencies [f3e4041]
- Updated dependencies [319a1e5]
- Updated dependencies [529f940]
- Updated dependencies [754cc11]
- Updated dependencies [d7948e3]
- Updated dependencies [a4c3399]
- @gitbook/expr@1.2.1
- @gitbook/react-openapi@1.5.0
## 0.18.0
### Minor Changes
+5 -9
View File
@@ -1,14 +1,14 @@
{
"name": "gitbook",
"version": "0.19.1",
"version": "0.18.0",
"private": true,
"dependencies": {
"@gitbook/api": "catalog:",
"@gitbook/expr": "workspace:*",
"@gitbook/browser-types": "workspace:*",
"@gitbook/cache-tags": "workspace:*",
"@gitbook/colors": "workspace:*",
"@gitbook/emoji-codepoints": "workspace:*",
"@gitbook/expr": "workspace:*",
"@gitbook/fonts": "workspace:*",
"@gitbook/icons": "workspace:*",
"@gitbook/openapi-parser": "workspace:*",
@@ -16,7 +16,7 @@
"@gitbook/react-math": "workspace:*",
"@gitbook/react-openapi": "workspace:*",
"@modelcontextprotocol/sdk": "^1.17.5",
"@opennextjs/cloudflare": "^1.9.2",
"@opennextjs/cloudflare": "^1.8.3",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.1.12",
"@radix-ui/react-hover-card": "^1.1.15",
@@ -51,7 +51,7 @@
"micromark-extension-gfm": "^3.0.0",
"motion": "^12.23.12",
"next": "15.4.0",
"next-themes": "^0.4.6",
"next-themes": "^0.2.1",
"nuqs": "^2.2.3",
"object-hash": "^3.0.0",
"object-identity": "^0.1.2",
@@ -121,9 +121,5 @@
},
"browserslist": [
">0.3%, chrome >= 64, edge >= 79, firefox >= 67, opera >= 51, safari >= 12 and not dead"
],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
}
]
}
@@ -1,6 +1,7 @@
'use client';
import { useLanguage } from '@/intl/client';
import { t } from '@/intl/translate';
import { tcls } from '@/lib/tailwind';
import type { Assistant } from '../AI';
import { Button } from '../primitives';
import { KeyboardShortcut } from '../primitives/KeyboardShortcut';
@@ -23,7 +24,7 @@ export function AIChatButton(props: {
iconOnly={!showLabel}
size="medium"
variant="header"
className="h-9 px-2.5 max-md:[&_.button-content]:hidden"
className={tcls('h-9 px-2.5')}
label={
<div className="flex items-center gap-2">
{t(language, 'ai_chat_ask', assistant.label)}
@@ -37,7 +38,7 @@ export function AIChatButton(props: {
}
onClick={() => assistant.open()}
>
{showLabel ? t(language, 'ask') : null}
{showLabel ? <span className="max-md:hidden">{t(language, 'ask')}</span> : null}
</Button>
);
}
@@ -1,10 +1,11 @@
'use client';
import { Icon } from '@gitbook/icons';
import { MotionConfig } from 'motion/react';
import React from 'react';
import { useCheckForContentUpdate } from '../AutoRefreshContent';
import { useVisitorSession } from '../Insights';
import { useCurrentPagePath } from '../hooks';
import { DateRelative } from '../primitives';
import { DateRelative, Tooltip } from '../primitives';
import { HideToolbarButton } from './HideToolbarButton';
import { IframeWrapper } from './IframeWrapper';
import { RefreshContentButton } from './RefreshContentButton';
@@ -18,98 +19,113 @@ import {
ToolbarSubtitle,
ToolbarTitle,
} from './Toolbar';
import {
type ToolbarControlsContextValue,
ToolbarControlsProvider,
} from './ToolbarControlsContext';
import { ToolbarControlsProvider } from './ToolbarControlsContext';
import type { AdminToolbarClientProps, AdminToolbarContext } from './types';
import { useToolbarVisibility } from './utils';
export function AdminToolbarClient(props: AdminToolbarClientProps) {
const { context, onPersistentClose, onSessionClose, onToggleMinify } = props;
const {
minified,
setMinified,
shouldAutoExpand,
hidden,
minimize,
closeSession,
closePersistent,
} = useToolbarVisibility({
onPersistentClose,
onSessionClose,
onToggleMinify,
});
const [minified, setMinified] = React.useState(true);
const visitorSession = useVisitorSession();
const [sessionClosed, setSessionClosed] = React.useState(false);
const [shouldHide, setShouldHide] = React.useState(false);
const toolbarControls: ToolbarControlsContextValue = {
minimize,
closeSession,
closePersistent,
shouldAutoExpand,
};
React.useEffect(() => {
const STORAGE_KEY = 'gitbook_toolbar_closed';
if (hidden) {
try {
const hidden = !!localStorage.getItem(STORAGE_KEY);
setShouldHide(hidden);
} catch {
setShouldHide(false);
}
}, []);
const handleSessionClose = React.useCallback(() => {
setSessionClosed(true);
onSessionClose?.();
}, [onSessionClose]);
const handlePersistentClose = React.useCallback(() => {
try {
localStorage.setItem('gitbook_toolbar_closed', '1');
} catch {
console.error('Failed to close toolbar using local storage');
}
setSessionClosed(true);
onPersistentClose?.();
}, [onPersistentClose]);
const handleMinifiedChange = React.useCallback(
(value: boolean) => {
setMinified(value);
onToggleMinify?.();
},
[onToggleMinify]
);
const toolbarControls = React.useMemo(
() => ({
minimize: () => handleMinifiedChange(true),
closeSession: handleSessionClose,
closePersistent: handlePersistentClose,
}),
[handleMinifiedChange, handleSessionClose, handlePersistentClose]
);
if (shouldHide || sessionClosed) {
return null;
}
// If there is a change request, show the change request toolbar
if (context.changeRequest) {
return (
<ToolbarControlsWrapper value={toolbarControls}>
<ChangeRequestToolbar
context={context}
minified={minified}
onMinifiedChange={setMinified}
/>
</ToolbarControlsWrapper>
<ToolbarControlsProvider value={toolbarControls}>
<IframeWrapper>
<MotionConfig reducedMotion="user">
<ChangeRequestToolbar
context={context}
minified={minified}
onMinifiedChange={handleMinifiedChange}
/>
</MotionConfig>
</IframeWrapper>
</ToolbarControlsProvider>
);
}
// If the revision is not the current revision, the user is looking at a previous version of the site, so show the revision toolbar
if (context.revisionId !== context.space.revision) {
return (
<ToolbarControlsWrapper value={toolbarControls}>
<RevisionToolbar
context={context}
minified={minified}
onMinifiedChange={setMinified}
/>
</ToolbarControlsWrapper>
<ToolbarControlsProvider value={toolbarControls}>
<IframeWrapper>
<MotionConfig reducedMotion="user">
<RevisionToolbar
context={context}
minified={minified}
onMinifiedChange={handleMinifiedChange}
/>
</MotionConfig>
</IframeWrapper>
</ToolbarControlsProvider>
);
}
// If the user is authenticated and part of the organization owning this site, show the authenticated user toolbar
if (visitorSession?.organizationId === context.organizationId) {
return (
<ToolbarControlsWrapper value={toolbarControls}>
<AuthenticatedUserToolbar
context={context}
minified={minified}
onMinifiedChange={setMinified}
/>
</ToolbarControlsWrapper>
<ToolbarControlsProvider value={toolbarControls}>
<IframeWrapper>
<MotionConfig reducedMotion="user">
<AuthenticatedUserToolbar
context={context}
minified={minified}
onMinifiedChange={handleMinifiedChange}
/>
</MotionConfig>
</IframeWrapper>
</ToolbarControlsProvider>
);
}
return null;
}
/**
* Reusable wrapper that provides tooling and containers that are used by all types of toolbar views.
*/
export function ToolbarControlsWrapper(
props: React.PropsWithChildren<{ value: ToolbarControlsContextValue | null }>
) {
const { children, value } = props;
return (
<ToolbarControlsProvider value={value}>
<IframeWrapper>
<MotionConfig reducedMotion="user">{children}</MotionConfig>
</IframeWrapper>
</ToolbarControlsProvider>
);
}
interface ToolbarViewProps {
@@ -132,7 +148,7 @@ function ChangeRequestToolbar(props: ToolbarViewProps) {
});
return (
<Toolbar minified={minified} onMinifiedChange={onMinifiedChange} label="Site preview">
<Toolbar minified={minified} onMinifiedChange={onMinifiedChange}>
<ToolbarBody>
<ToolbarTitle
prefix={`Change #${changeRequest.number}:`}
@@ -207,17 +223,19 @@ function RevisionToolbar(props: ToolbarViewProps) {
const gitProvider = isGitHub ? 'GitHub' : 'GitLab';
return (
<Toolbar minified={minified} onMinifiedChange={onMinifiedChange} label="Site preview">
<ToolbarBody>
<ToolbarTitle prefix="Site version" suffix={context.site.title} />
<ToolbarSubtitle
subtitle={
<>
Created <DateRelative value={revision.createdAt} />
</>
}
/>
</ToolbarBody>
<Toolbar minified={minified} onMinifiedChange={onMinifiedChange}>
<Tooltip label="Site preview">
<ToolbarBody>
<ToolbarTitle prefix="Site version" suffix={context.site.title} />
<ToolbarSubtitle
subtitle={
<>
Created <DateRelative value={revision.createdAt} />
</>
}
/>
</ToolbarBody>
</Tooltip>
<ToolbarSeparator />
<ToolbarActions>
{/* Open commit in Git client */}
@@ -280,21 +298,19 @@ function AuthenticatedUserToolbar(props: ToolbarViewProps) {
});
return (
<Toolbar
minified={minified}
onMinifiedChange={onMinifiedChange}
label="Only visible to your GitBook organization"
>
<ToolbarBody>
<ToolbarTitle suffix={context.site.title} />
<ToolbarSubtitle
subtitle={
<>
Updated <DateRelative value={revision.createdAt} />
</>
}
/>
</ToolbarBody>
<Toolbar minified={minified} onMinifiedChange={onMinifiedChange}>
<Tooltip label="Site preview">
<ToolbarBody>
<ToolbarTitle suffix={context.site.title} />
<ToolbarSubtitle
subtitle={
<>
Updated <DateRelative value={revision.createdAt} />
</>
}
/>
</ToolbarBody>
</Tooltip>
<ToolbarSeparator />
<ToolbarActions>
{/* Refresh to retrieve latest changes */}
@@ -1,9 +1,9 @@
.svgLogo {
--logo-fill: var(--color-neutral-100);
--logo-fill: var(--color-neutral-900);
--seg-A-color: #2782c4;
--seg-B-color: #43b7f2;
--seg-C-color: #8be2ff;
--trace-color: #46474c;
--trace-color: #dedfe3;
--T: 2s;
@@ -11,15 +11,9 @@
vector-effect: non-scaling-stroke;
}
.static .trace {
animation: none;
fill: var(--logo-fill);
stroke: none;
}
.static .seg {
animation: none;
opacity: 0;
:global(html.dark) .svgLogo {
--logo-fill: var(--color-neutral-100);
--trace-color: #46474c;
}
/* Base segment animation */
@@ -182,4 +176,4 @@
fill: var(--logo-fill);
stroke:none;
}
}
}
@@ -2,16 +2,10 @@ import { tcls } from '@/lib/tailwind';
import type React from 'react';
import styles from './AnimatedLogo.module.css';
interface AnimatedLogoProps {
shouldAnimate?: boolean;
}
export const AnimatedLogo: React.FC<AnimatedLogoProps> = (props) => {
const { shouldAnimate = true } = props;
export const AnimatedLogo: React.FC = () => {
return (
<svg
className={tcls(styles.svgLogo, shouldAnimate ? undefined : styles.static)}
className={styles.svgLogo}
width="28"
height="28"
viewBox="0 0 28 28"
@@ -29,7 +29,7 @@ export function HideToolbarButton(props: HideToolbarButtonProps) {
const buttonRef = useRef<HTMLDivElement>(null);
const handleClickOutsideArcMenu = (event: Event) => {
// Don't close the arc if we are clicking on the button itself
// Don't close the arc if we are clicking clicking on the button itself
if (buttonRef.current?.contains(event.target as Node)) {
return;
}
@@ -47,32 +47,36 @@ export function HideToolbarButton(props: HideToolbarButtonProps) {
return () => window.removeEventListener('scroll', handleScroll);
}, [open]);
const items = [
controls?.minimize
? {
id: 'minimize',
icon: 'minus',
label: 'Minimize',
onClick: controls.minimize,
}
: null,
controls?.closeSession
? {
id: 'session-close',
icon: 'xmark',
label: 'Close for one session',
onClick: controls.closeSession,
}
: null,
controls?.closePersistent
? {
id: 'persistent-close',
icon: 'ban',
label: "Don't show again",
onClick: controls.closePersistent,
}
: null,
].filter(Boolean) as Array<ArcMenuItem>;
const items = React.useMemo(
() =>
[
controls?.minimize
? {
id: 'minimize',
icon: 'minus',
label: 'Minimize',
onClick: controls.minimize,
}
: null,
controls?.closeSession
? {
id: 'session-close',
icon: 'xmark',
label: 'Close for one session',
onClick: controls.closeSession,
}
: null,
controls?.closePersistent
? {
id: 'persistent-close',
icon: 'ban',
label: "Don't show again",
onClick: controls.closePersistent,
}
: null,
].filter(Boolean) as Array<ArcMenuItem>,
[controls]
);
const sharedMotionStyle = motionValues
? {
@@ -204,10 +208,12 @@ export function ArcToolbarButton(props: ArcToolbarButtonProps) {
'text-tint-1 dark:text-tint-12',
'bg-[linear-gradient(110deg,rgba(51,53,57,1)_0%,rgba(50,52,56,1)_100%)]',
'dark:[background:linear-gradient(110deg,rgba(255,255,255,1)_0%,rgba(240,246,248,1)_100%)]',
'border border-solid dark:border-[rgba(256,_256,_256,_0.06)]'
'border border-solid dark:border-[rgb(255_255_255_/_40%)]'
)}
style={{
background: 'linear-gradient(rgb(51, 53, 57), rgb(50, 52, 56))',
border: '1px solid rgba(0, 0, 0, 0.06)',
boxShadow: 'rgba(255, 255, 255, 0.15) 0px 1px 1px 0px inset',
}}
>
<Icon
@@ -8,7 +8,6 @@ import {
} from 'motion/react';
import React from 'react';
import { AnimatedLogo } from './AnimatedLogo';
import { useToolbarControls } from './ToolbarControlsContext';
import { tcls } from '@/lib/tailwind';
import { Icon, type IconName, IconStyle } from '@gitbook/icons';
@@ -20,20 +19,14 @@ const DURATION_LOGO_APPEARANCE = 2000;
const DELAY_BETWEEN_LOGO_AND_CONTENT = 100;
interface ToolbarProps {
label: React.ReactNode;
children: React.ReactNode;
minified: boolean;
onMinifiedChange: (value: boolean) => void;
}
export function Toolbar(props: ToolbarProps) {
const { children, label, minified, onMinifiedChange } = props;
const controls = useToolbarControls();
const { children, minified, onMinifiedChange } = props;
const [isReady, setIsReady] = React.useState(false);
const autoExpandTriggeredRef = React.useRef(false);
const shouldAutoExpand = Boolean(controls?.shouldAutoExpand);
const [shouldAnimateLogo, setShouldAnimateLogo] = React.useState(shouldAutoExpand);
// Wait for page to be ready, then show the toolbar
React.useEffect(() => {
@@ -49,41 +42,18 @@ export function Toolbar(props: ToolbarProps) {
}
}, []);
// After toolbar appears, wait, then show the full content
// After toolbar appears, wait then show the full content
React.useEffect(() => {
if (!isReady || autoExpandTriggeredRef.current) {
if (!isReady) {
return;
}
if (!shouldAutoExpand) {
// When we already know the toolbar should stay expanded (e.g. the user previously
// opened it this session) we short-circuit the auto-expand animation and immediately
// render the expanded state without replaying the logo animation.
autoExpandTriggeredRef.current = true;
setShouldAnimateLogo(false);
return;
}
autoExpandTriggeredRef.current = true;
// On a fresh session we let the toolbar appear in its compact form, play the logo
// animation, and only then expand the toolbar. The timeout mirrors the duration of the
// logo animation so both transitions feel connected.
const expandAfterTimeout = setTimeout(() => {
setShouldAnimateLogo(false);
onMinifiedChange(false);
}, DURATION_LOGO_APPEARANCE + DELAY_BETWEEN_LOGO_AND_CONTENT);
return () => clearTimeout(expandAfterTimeout);
}, [isReady, onMinifiedChange, shouldAutoExpand]);
React.useEffect(() => {
if (!minified) {
// Any manual expansion should stop the logo animation so the icon stays in its
// “settled” state once the toolbar is open.
setShouldAnimateLogo(false);
}
}, [minified]);
}, [isReady, onMinifiedChange]);
// Don't render anything until page is ready
if (!isReady) {
@@ -91,47 +61,51 @@ export function Toolbar(props: ToolbarProps) {
}
return (
<Tooltip label={label}>
<motion.div className="-translate-x-1/2 fixed bottom-5 left-1/2 z-40 w-auto max-w-xl transform px-4">
<AnimatePresence mode="wait">
<motion.div
onClick={() => {
if (minified) {
setShouldAnimateLogo(false);
onMinifiedChange(false);
}
}}
layout
transition={toolbarEasings.spring}
className={tcls(
minified ? 'cursor-pointer px-2' : 'pr-2 pl-3.5',
'flex',
'items-center',
'justify-center',
'min-h-11',
'min-w-12',
'h-12',
'py-2',
'backdrop-blur-sm',
'origin-center',
'border-[0.5px] border-neutral-5 border-solid dark:border-neutral-8',
'bg-[linear-gradient(45deg,rgba(39,39,39,0.8)_100%,rgba(39,39,39,0.4)_80%)]',
'dark:bg-[linear-gradient(45deg,rgba(39,39,39,0.5)_100%,rgba(39,39,39,0.3)_80%)]'
)}
style={{
borderRadius: '100px', // This is set on `style` so Framer Motion can correct for distortions
}}
>
{/* Logo with stroke segments animation in blue-tints */}
<motion.div layout>
<AnimatedLogo shouldAnimate={shouldAnimateLogo} />
</motion.div>
{!minified ? children : null}
<motion.div className="-translate-x-1/2 fixed bottom-5 left-1/2 z-40 w-auto max-w-xl transform px-4">
<AnimatePresence mode="wait">
<motion.div
onClick={() => {
if (minified) {
onMinifiedChange(false);
}
}}
layout
transition={toolbarEasings.spring}
className={tcls(
minified ? 'cursor-pointer px-2' : 'pr-2 pl-3.5',
'flex',
'items-center',
'justify-center',
'min-h-11',
'min-w-12',
'h-12',
'py-2',
'backdrop-blur-sm',
'origin-center',
'border-[0.5px] border-neutral-5 border-solid dark:border-neutral-8',
'bg-[linear-gradient(45deg,rgba(255,255,255,0)_0%,rgba(255,255,255,0.2)_100%)]'
)}
initial={{
scale: 1,
opacity: 1,
}}
animate={{
scale: 1,
opacity: 1,
}}
style={{
borderRadius: '100px', // This is set on `style` so Framer Motion can correct for distortions
}}
>
{/* Logo with stroke segments animation in blue-tints */}
<motion.div layout>
<AnimatedLogo />
</motion.div>
</AnimatePresence>
</motion.div>
</Tooltip>
{!minified ? children : null}
</motion.div>
</AnimatePresence>
</motion.div>
);
}
@@ -216,6 +190,9 @@ export const ToolbarButton = React.forwardRef<HTMLDivElement, ToolbarButtonProps
transformOrigin: 'bottom center',
zIndex: motionValues?.scale ? 10 : 'auto',
...style,
background: 'linear-gradient(rgb(51, 53, 57), rgb(50, 52, 56))',
boxShadow: 'rgba(255, 255, 255, 0.15) 0px 1px 1px 0px inset',
border: '1px solid rgba(0, 0, 0, 0.06)',
}
}
transition={{
@@ -239,9 +216,7 @@ export const ToolbarButton = React.forwardRef<HTMLDivElement, ToolbarButtonProps
'cursor-pointer',
'transition-colors',
'size-8',
disabled ? 'cursor-not-allowed opacity-50' : '',
'border border-[rgba(256,_256,_256,_0.06)] border-solid',
'bg-[linear-gradient(45deg,rgba(51,53,57,1)_0%,rgba(50,52,56,1)_100%)]'
disabled ? 'cursor-not-allowed opacity-50' : ''
)}
>
<Icon
@@ -311,10 +286,7 @@ export function ToolbarTitle(props: { prefix?: string; suffix: string }) {
function ToolbarTitlePrefix(props: { title: string }) {
return (
<motion.span
{...getCopyVariants(0)}
className="truncate font-medium text-neutral-1 dark:text-neutral-12"
>
<motion.span {...getCopyVariants(0)} className="truncate font-medium text-neutral-12">
{props.title}
</motion.span>
);
@@ -322,10 +294,7 @@ function ToolbarTitlePrefix(props: { title: string }) {
function ToolbarTitleSuffix(props: { title: string }) {
return (
<motion.span
{...getCopyVariants(1)}
className="max-w-[20ch] truncate text-neutral-1 dark:text-neutral-12"
>
<motion.span {...getCopyVariants(1)} className="max-w-[20ch] truncate text-neutral-12">
{props.title}
</motion.span>
);
@@ -333,10 +302,7 @@ function ToolbarTitleSuffix(props: { title: string }) {
export function ToolbarSubtitle(props: { subtitle: React.ReactNode }) {
return (
<motion.span
{...getCopyVariants(1)}
className="text-neutral-1/80 text-xxs dark:text-neutral-12/80"
>
<motion.span {...getCopyVariants(1)} className="text-neutral-12/90 text-xxs">
{props.subtitle}
</motion.span>
);
@@ -5,21 +5,15 @@ export interface ToolbarControlsContextValue {
minimize: () => void;
closeSession?: () => void;
closePersistent?: () => void;
shouldAutoExpand?: boolean;
}
const ToolbarControlsContext = React.createContext<ToolbarControlsContextValue | null>(null);
/*
* Provides reusable state setters (mainly for hiding/showing the toolbar) for the toolbar controls propagated through to the children
*/
export function ToolbarControlsProvider(
props: React.PropsWithChildren<{ value: ToolbarControlsContextValue | null }>
) {
const { children, value } = props;
return (
<ToolbarControlsContext.Provider value={value}>{children}</ToolbarControlsContext.Provider>
);
return <ToolbarControlsContext.Provider value={value}>{children}</ToolbarControlsContext.Provider>;
}
export function useToolbarControls() {
@@ -3,4 +3,3 @@ export * from './AdminToolbarClient';
export * from './IframeWrapper';
export * from './Toolbar';
export * from './transitions';
export * from './utils';
@@ -212,7 +212,7 @@ export function useMagnificationEffect(props: {
);
});
// Update motion values
// Update motion values - springs in components will animate to these values
buttonEffects.forEach((effect, index) => {
const motionValue = buttonMotionValues[index];
if (motionValue) {
@@ -1,154 +0,0 @@
import React from 'react';
import {
getLocalStorageItem,
getSessionStorageItem,
removeSessionStorageItem,
setLocalStorageItem,
setSessionStorageItem,
} from '@/lib/browser/local-storage';
const STORAGE_KEY = 'gitbook_toolbar_closed';
const SESSION_STORAGE_KEY = 'gitbook_toolbar_session_closed';
const SESSION_MINIFIED_KEY = 'gitbook_toolbar_minified';
type SessionHideReason = 'session' | 'persistent';
/**
* Read the current session hide state. We store whether the toolbar was hidden “for session” or
* “persistently” so we can distinguish between a temporary dismissal and a user preference.
*/
export const getSessionHideState = (): { hidden: boolean; reason?: SessionHideReason } => {
const value = getSessionStorageItem<SessionHideReason | null>(SESSION_STORAGE_KEY, null);
if (value === 'session' || value === 'persistent') {
return { hidden: true, reason: value };
}
return { hidden: false, reason: undefined };
};
/**
* Persist the session hide state. Passing `false` clears the stored preference entirely.
*/
export const setSessionHidden = (value: boolean, reason?: SessionHideReason) => {
if (value && reason) {
setSessionStorageItem(SESSION_STORAGE_KEY, reason);
} else {
removeSessionStorageItem(SESSION_STORAGE_KEY);
}
};
/**
* Retrieve the last minified state from session storage. If no value was ever stored we return
* `undefined`, which signals that the toolbar should auto-expand once the logo animation finishes.
*/
export const getStoredMinified = (): boolean | undefined => {
const stored = getSessionStorageItem<boolean | null>(SESSION_MINIFIED_KEY, null);
if (stored === null || stored === undefined) {
removeSessionStorageItem(SESSION_MINIFIED_KEY);
return undefined;
}
return stored;
};
/**
* Persist the current minified state for the ongoing session.
*/
export const setStoredMinified = (value: boolean) => {
setSessionStorageItem(SESSION_MINIFIED_KEY, value);
};
interface UseToolbarVisibilityOptions {
onPersistentClose?: () => void;
onSessionClose?: () => void;
onToggleMinify?: () => void;
}
export function useToolbarVisibility(options: UseToolbarVisibilityOptions = {}) {
const { onPersistentClose, onSessionClose, onToggleMinify } = options;
const [initialStoredMinified] = React.useState<boolean | undefined>(() =>
typeof window !== 'undefined' ? getStoredMinified() : undefined
);
const shouldAutoExpand = initialStoredMinified === undefined;
const [minified, setMinifiedState] = React.useState<boolean>(() =>
initialStoredMinified !== undefined ? initialStoredMinified : true
);
const [persistentHidden, setPersistentHidden] = React.useState(() =>
getLocalStorageItem(STORAGE_KEY, false)
);
const [sessionReason, setSessionReason] = React.useState<SessionHideReason | undefined>(() => {
const state = getSessionHideState();
return state.hidden ? state.reason : undefined;
});
React.useEffect(() => {
if (typeof window === 'undefined') {
return;
}
const handleStorage = () => {
setPersistentHidden(getLocalStorageItem(STORAGE_KEY, false));
const state = getSessionHideState();
setSessionReason(state.hidden ? state.reason : undefined);
};
window.addEventListener('storage', handleStorage);
return () => window.removeEventListener('storage', handleStorage);
}, []);
React.useEffect(() => {
if (persistentHidden) {
if (sessionReason !== 'persistent') {
// Sync the session flag so we honour the persistent preference and avoid flashes when
// opening new tabs in the same browsing session.
setSessionHidden(true, 'persistent');
setSessionReason('persistent');
}
return;
}
if (sessionReason === 'persistent') {
// If the persistent preference was cleared we also clear the session flag so the toolbar
// can reappear immediately without requiring a full reload.
setSessionHidden(false);
setSessionReason(undefined);
}
}, [persistentHidden, sessionReason]);
const setMinified = (value: boolean) => {
setMinifiedState(value);
setStoredMinified(value);
onToggleMinify?.();
};
const minimize = () => setMinified(true);
const closeSession = () => {
setSessionHidden(true, 'session');
setSessionReason('session');
onSessionClose?.();
};
const closePersistent = () => {
setLocalStorageItem(STORAGE_KEY, true);
setPersistentHidden(true);
setSessionHidden(true, 'persistent');
setSessionReason('persistent');
onPersistentClose?.();
};
const hidden = persistentHidden || sessionReason === 'session';
return {
minified,
setMinified,
shouldAutoExpand,
hidden,
minimize,
closeSession,
closePersistent,
};
}
@@ -89,7 +89,7 @@ export function UnwrappedBlocks<TBlock extends DocumentBlock>(props: UnwrappedBl
node.data && 'fullWidth' in node.data && node.data.fullWidth
? 'max-w-screen-xl'
: 'max-w-3xl',
FULL_WIDTH_BLOCKS.includes(node.type) && 'page-width-wide:max-w-full',
FULL_WIDTH_BLOCKS.includes(node.type) && 'page-width-wide:max-w-screen-2xl',
blockStyle,
]}
isEstimatedOffscreen={isOffscreen}
@@ -24,7 +24,6 @@ export function Caption(
wrapperStyle?: ClassValue;
block: DocumentBlockImage | DocumentBlockDrawing | DocumentBlockEmbed | DocumentBlockFile;
withBorder?: boolean;
withFrame?: boolean;
} & DocumentContextProps
) {
const {
@@ -34,7 +33,6 @@ export function Caption(
context,
fit = false,
withBorder = false,
withFrame = false,
wrapperStyle = [
'relative',
'overflow-hidden',
@@ -46,7 +44,6 @@ export function Caption(
withBorder
? 'rounded-corners:rounded-sm circular-corners:rounded-2xl after:border-tint-subtle after:border after:rounded circular-corners:after:rounded-2xl rounded-corners:after:rounded-sm dark:after:mix-blend-plus-lighter after:pointer-events-none'
: null,
withFrame && 'p-2',
],
style,
} = props;
@@ -65,14 +62,7 @@ export function Caption(
return (
<picture className={tcls('relative', style)}>
<div className={tcls(wrapperStyle, 'mx-auto')}>{children}</div>
<figcaption
className={tcls(
'text-xs',
'text-center',
'text-tint',
withFrame ? 'mt-0.5 mb-2.5' : 'mt-2'
)}
>
<figcaption className={tcls('text-sm', 'text-center', 'mt-2', 'text-tint')}>
<Inlines
nodes={captionParagraph.nodes}
document={document}
@@ -1,8 +1,7 @@
import type { JSONDocument } from '@gitbook/api';
import type { DocumentBlockCode, JSONDocument } from '@gitbook/api';
import { useId } from 'react';
import { CodeBlock } from './CodeBlock';
import { convertCodeStringToBlock } from './utils';
/**
* Plain code block with syntax highlighting.
@@ -12,7 +11,31 @@ export function PlainCodeBlock(props: { code: string; syntax: string }) {
const { code, syntax } = props;
const id = useId();
const block = convertCodeStringToBlock({ key: id, code, syntax });
const block: DocumentBlockCode = {
key: id,
object: 'block',
type: 'code',
data: {
syntax,
},
nodes: code.split('\n').map((line) => ({
object: 'block',
type: 'code-line',
data: {},
nodes: [
{
object: 'text',
leaves: [
{
object: 'leaf',
text: line,
marks: [],
},
],
},
],
})),
};
const document: JSONDocument = {
object: 'document',
@@ -1,219 +0,0 @@
import { describe, expect, it } from 'bun:test';
import { convertCodeStringToBlock } from './utils';
describe('convertCodeStringToBlock', () => {
it('converts plain code string without placeholders', () => {
const result = convertCodeStringToBlock({
key: 'test1',
code: 'console.log("hello");',
syntax: 'javascript',
});
expect(result).toEqual({
key: 'test1',
object: 'block',
type: 'code',
data: { syntax: 'javascript' },
nodes: [
{
object: 'block',
type: 'code-line',
data: {},
nodes: [
{
object: 'text',
leaves: [{ object: 'leaf', text: 'console.log("hello");', marks: [] }],
},
],
},
],
});
});
it('converts a single placeholder into an expression node', () => {
const result = convertCodeStringToBlock({
key: 'test2',
code: 'const config = { API_KEY: "$$__X-GITBOOK-PREFILL[visitor.claims.apiKey ?? "YOUR_API_KEY"]__$$" }',
syntax: 'javascript',
});
expect(result).toEqual({
key: 'test2',
object: 'block',
type: 'code',
data: { syntax: 'javascript' },
nodes: [
{
object: 'block',
type: 'code-line',
data: {},
nodes: [
{
object: 'text',
leaves: [
{ object: 'leaf', text: 'const config = { API_KEY: "', marks: [] },
],
},
{
object: 'inline',
type: 'expression',
data: { expression: 'visitor.claims.apiKey ?? "YOUR_API_KEY"' },
isVoid: true,
},
{
object: 'text',
leaves: [{ object: 'leaf', text: '" }', marks: [] }],
},
],
},
],
});
});
it('handles multiple placeholders in one line', () => {
const result = convertCodeStringToBlock({
key: 'test3',
code: 'let a = $$__X-GITBOOK-PREFILL[valA]__$$, b = $$__X-GITBOOK-PREFILL[valB]__$$;',
syntax: 'javascript',
});
expect(result).toEqual({
key: 'test3',
object: 'block',
type: 'code',
data: { syntax: 'javascript' },
nodes: [
{
object: 'block',
type: 'code-line',
data: {},
nodes: [
{
object: 'text',
leaves: [{ object: 'leaf', text: 'let a = ', marks: [] }],
},
{
object: 'inline',
type: 'expression',
data: { expression: 'valA' },
isVoid: true,
},
{
object: 'text',
leaves: [{ object: 'leaf', text: ', b = ', marks: [] }],
},
{
object: 'inline',
type: 'expression',
data: { expression: 'valB' },
isVoid: true,
},
{
object: 'text',
leaves: [{ object: 'leaf', text: ';', marks: [] }],
},
],
},
],
});
});
it('handles multiple placeholders across different lines', () => {
const result = convertCodeStringToBlock({
key: 'test4',
code: [
'const name = "$$__X-GITBOOK-PREFILL[visitor.claims.userName]__$$";',
'const age = $$__X-GITBOOK-PREFILL[visitor.claims.userAge]__$$;',
'console.log(name, age);',
].join('\n'),
syntax: 'javascript',
});
expect(result).toEqual({
key: 'test4',
object: 'block',
type: 'code',
data: { syntax: 'javascript' },
nodes: [
{
object: 'block',
type: 'code-line',
data: {},
nodes: [
{
object: 'text',
leaves: [{ object: 'leaf', text: 'const name = "', marks: [] }],
},
{
object: 'inline',
type: 'expression',
data: { expression: 'visitor.claims.userName' },
isVoid: true,
},
{
object: 'text',
leaves: [{ object: 'leaf', text: '";', marks: [] }],
},
],
},
{
object: 'block',
type: 'code-line',
data: {},
nodes: [
{
object: 'text',
leaves: [{ object: 'leaf', text: 'const age = ', marks: [] }],
},
{
object: 'inline',
type: 'expression',
data: { expression: 'visitor.claims.userAge' },
isVoid: true,
},
{
object: 'text',
leaves: [{ object: 'leaf', text: ';', marks: [] }],
},
],
},
{
object: 'block',
type: 'code-line',
data: {},
nodes: [
{
object: 'text',
leaves: [
{ object: 'leaf', text: 'console.log(name, age);', marks: [] },
],
},
],
},
],
});
});
it('returns an empty code block for empty string', () => {
const result = convertCodeStringToBlock({
key: 'test5',
code: '',
syntax: 'javascript',
});
expect(result).toEqual({
key: 'test5',
object: 'block',
type: 'code',
data: { syntax: 'javascript' },
nodes: [
{
object: 'block',
type: 'code-line',
data: {},
nodes: [],
},
],
});
});
});
@@ -1,67 +0,0 @@
import type { DocumentBlockCode, DocumentBlockCodeLine } from '@gitbook/api';
const PREFILL_WITH_EXPR_REGEX = /\$\$__X-GITBOOK-PREFILL\[(.+?)\]__\$\$/g;
/**
* Convert a raw code string into a `DocumentBlockCode` object representation.
*
* Any placeholder of the form `$$__X-GITBOOK-PREFILL[<expression>]__$$` inside the code
* string is transformed into a DocumentInlineExpression node with its `data.expression` set to the
* extracted `<expression>`.
*/
export function convertCodeStringToBlock(args: {
key: string;
code: string;
syntax: string;
}): DocumentBlockCode {
const { key, code, syntax } = args;
const lines = code.split('\n').map<DocumentBlockCodeLine>((line) => {
const nodes: DocumentBlockCodeLine['nodes'] = [];
let lastIndex = 0;
for (const match of line.matchAll(PREFILL_WITH_EXPR_REGEX)) {
const [placeholder, expression] = match;
const start = match.index ?? 0;
if (start > lastIndex) {
nodes.push({
object: 'text',
leaves: [{ object: 'leaf', text: line.slice(lastIndex, start), marks: [] }],
});
}
if (expression) {
nodes.push({
object: 'inline',
type: 'expression',
data: { expression },
isVoid: true,
});
}
lastIndex = start + placeholder.length;
}
if (lastIndex < line.length) {
nodes.push({
object: 'text',
leaves: [{ object: 'leaf', text: line.slice(lastIndex), marks: [] }],
});
}
return {
object: 'block',
type: 'code-line',
data: {},
nodes,
};
});
return {
key,
object: 'block',
type: 'code',
data: { syntax },
nodes: lines,
};
}
@@ -11,8 +11,8 @@ import type { DocumentContext } from './DocumentView';
export function Images(props: BlockProps<DocumentBlockImages>) {
const { document, block, style, context, isEstimatedOffscreen } = props;
const hasMultipleImages = block.nodes.length > 1;
const { align = 'center', withFrame } = block.data;
const isMultipleImages = block.nodes.length > 1;
const { align = 'center' } = block.data;
return (
<div
@@ -24,15 +24,7 @@ export function Images(props: BlockProps<DocumentBlockImages>) {
align === 'center' && 'justify-center',
align === 'right' && 'justify-end',
align === 'left' && 'justify-start',
hasMultipleImages && ['grid', 'grid-flow-col'],
withFrame && [
'rounded-2xl',
'border',
'border-[rgb(234,235,238)]',
'dark:border-[rgb(45,50,58)]',
'relative',
'overflow-hidden',
]
isMultipleImages && ['grid', 'grid-flow-col']
)}
>
{block.nodes.map((node: any, _i: number) => (
@@ -44,7 +36,6 @@ export function Images(props: BlockProps<DocumentBlockImages>) {
siblings={block.nodes.length}
context={context}
isEstimatedOffscreen={isEstimatedOffscreen}
withFrame={withFrame}
/>
))}
</div>
@@ -71,9 +62,8 @@ async function ImageBlock(props: {
context: DocumentContext;
siblings: number;
isEstimatedOffscreen: boolean;
withFrame?: boolean;
}) {
const { block, context, isEstimatedOffscreen, withFrame } = props;
const { block, context, isEstimatedOffscreen } = props;
const [src, darkSrc] = await Promise.all([
context.contentContext ? resolveContentRef(block.data.ref, context.contentContext) : null,
@@ -87,66 +77,33 @@ async function ImageBlock(props: {
}
return (
<div className={tcls('relative', 'overflow-hidden')}>
{/* Frame grid */}
{withFrame && (
<div
className={tcls(
'absolute',
'-top-0.5',
'-left-0.5',
'right-px',
'bottom-px',
'opacity-40',
'dark:opacity-[0.1]',
'bg-[length:24px_24px,24px_24px]',
'bg-[linear-gradient(to_right,_rgb(234,235,238)_1px,_transparent_1px),linear-gradient(to_bottom,_rgb(234,235,238)_1px,_transparent_1px)]',
'dark:bg-[linear-gradient(to_right,_rgb(122,128,139)_1px,_transparent_1px),linear-gradient(to_bottom,_rgb(122,128,139)_1px,_transparent_1px)]',
'bg-repeat'
)}
/>
)}
{/* Shadow overlay */}
{withFrame && (
<div
className={tcls(
'pointer-events-none absolute inset-0 rounded-2xl',
'shadow-[inset_0_0_10px_10px_rgba(255,255,255,0.9)]',
'dark:shadow-[inset_0_0_10px_10px_rgb(29,29,29)]'
)}
/>
)}
<Caption {...props} fit>
<Image
alt={block.data.alt ?? ''}
sizes={imageBlockSizes}
resize={context.contentContext?.imageResizer}
sources={{
light: {
src: src.href,
size: src.file?.dimensions,
},
dark: darkSrc
? {
src: darkSrc.href,
size: darkSrc.file?.dimensions,
}
: null,
}}
priority={isEstimatedOffscreen ? 'lazy' : 'high'}
preload
zoom
inlineStyle={{
maxWidth: '100%',
width: getImageDimension(block.data.width, undefined),
height: getImageDimension(block.data.height, 'auto'),
}}
style={withFrame ? 'rounded-xl' : undefined}
/>
</Caption>
</div>
<Caption {...props} fit>
<Image
alt={block.data.alt ?? ''}
sizes={imageBlockSizes}
resize={context.contentContext?.imageResizer}
sources={{
light: {
src: src.href,
size: src.file?.dimensions,
},
dark: darkSrc
? {
src: darkSrc.href,
size: darkSrc.file?.dimensions,
}
: null,
}}
priority={isEstimatedOffscreen ? 'lazy' : 'high'}
preload
zoom
inlineStyle={{
maxWidth: '100%',
width: getImageDimension(block.data.width, undefined),
height: getImageDimension(block.data.height, 'auto'),
}}
/>
</Caption>
);
}
@@ -1,23 +1,10 @@
import type { DocumentInlineIcon, DocumentMarkColor } from '@gitbook/api';
import type { DocumentInlineIcon } from '@gitbook/api';
import { tcls } from '@/lib/tailwind';
import { Icon, type IconName } from '@gitbook/icons';
import type { InlineProps } from './Inline';
import { textColorToStyle } from './utils/colors';
export async function InlineIcon(props: InlineProps<DocumentInlineIcon>) {
const { inline } = props;
const icon = inline.data.icon as IconName;
// @ts-expect-error remove this comment once API is updated
const color = inline.data.color
? // @ts-expect-error remove "as DocumentMarkColor['data']['text']" once API is updated
(inline.data.color as DocumentMarkColor['data']['text'])
: undefined;
return (
<Icon
icon={icon}
className={tcls('inline size-[1em]', color ? textColorToStyle[color] : null)}
/>
);
return <Icon icon={inline.data.icon as IconName} className="inline size-[1em]" />;
}
@@ -34,7 +34,7 @@ export async function InlineImage(props: InlineProps<DocumentInlineImage>) {
)}
>
<Image
alt={inline.data.alt ?? inline.data.caption ?? ''}
alt={inline.data.caption ?? ''}
sizes={sizes}
resize={context.contentContext?.imageResizer}
sources={{
@@ -192,7 +192,7 @@
.openapi-schema-name {
/* To make double click on the property name select only the name,
we disable selection on the parent and re-enable it on the children. */
@apply select-none text-sm text-balance *:whitespace-nowrap flex flex-wrap gap-y-1.5 gap-x-2.5 items-center;
@apply select-none text-sm text-balance *:whitespace-nowrap flex flex-wrap gap-y-1.5 gap-x-2.5;
}
.openapi-schema-name .openapi-deprecated {
@@ -207,10 +207,6 @@
@apply line-through opacity-9;
}
.openapi-schema-discriminator {
@apply text-primary-subtle/9 text-[0.813rem] lowercase;
}
.openapi-schema-required {
@apply text-warning-subtle text-[0.813rem] lowercase;
}
@@ -317,11 +313,10 @@
}
.openapi-securities-oauth-flows {
@apply flex flex-col gap-3;
@apply flex flex-col gap-2 divide-y divide-tint-subtle;
}
.openapi-securities-oauth-content,
.openapi-securities-scopes {
.openapi-securities-oauth-content {
@apply prose *:!prose-sm *:text-tint;
}
@@ -329,7 +324,7 @@
@apply text-xs;
}
.openapi-securities-scopes ul {
.openapi-securities-oauth-content ul {
@apply !my-0;
}
@@ -841,7 +836,7 @@ body:has(.openapi-select-popover) {
}
.openapi-disclosure-trigger-label {
@apply absolute right-3 mr-px px-2 h-5 justify-end shrink-0 ring-tint-subtle truncate text-tint duration-300 transition-all rounded straight-corners:rounded-none circular-corners:rounded-xl flex flex-row gap-1 items-center text-xs;
@apply absolute right-3 px-2 h-5 justify-end shrink-0 ring-tint-subtle truncate text-tint duration-300 transition-all rounded straight-corners:rounded-none circular-corners:rounded-xl flex flex-row gap-1 items-center text-xs;
}
.openapi-disclosure-trigger-label span {
@@ -12,8 +12,7 @@ import type {
} from '@gitbook/api';
import React from 'react';
import { tcls } from '@/lib/tailwind';
import { backgroundColorToStyle, textColorToStyle } from './utils/colors';
import { type ClassValue, tcls } from '@/lib/tailwind';
export function Text(props: { text: DocumentText }) {
const { text } = props;
@@ -122,3 +121,51 @@ function Color(props: MarkedLeafProps<DocumentMarkColor>) {
</span>
);
}
/**
* @TODO replace by DocumentMarkColor['data']['text'] and DocumentMarkColor['data']['background']
* once the API is updated.
*/
type DocumentMarkColorValue =
| 'default'
| 'green'
| 'blue'
| 'red'
| 'orange'
| 'yellow'
| 'purple'
| '$primary'
| '$info'
| '$success'
| '$warning'
| '$danger';
const textColorToStyle: { [color in DocumentMarkColorValue]: ClassValue } = {
default: [],
blue: ['text-blue-500'],
red: ['text-red-500'],
green: ['text-green-500'],
yellow: ['text-yellow-600'],
purple: ['text-purple-500'],
orange: ['text-orange-500'],
$primary: ['text-primary'],
$info: ['text-info'],
$success: ['text-success'],
$warning: ['text-warning'],
$danger: ['text-danger'],
};
const backgroundColorToStyle: { [color in DocumentMarkColorValue]: ClassValue } = {
default: [],
blue: ['bg-mark-blue'],
red: ['bg-mark-red'],
green: ['bg-mark-green'],
yellow: ['bg-mark-yellow'],
purple: ['bg-mark-purple'],
orange: ['bg-mark-orange'],
$primary: ['bg-primary'],
$info: ['bg-info'],
$success: ['bg-success'],
$warning: ['bg-warning'],
$danger: ['bg-danger'],
};
@@ -1,34 +0,0 @@
import type { ClassValue } from '@/lib/tailwind';
import type { DocumentMarkColor } from '@gitbook/api';
export const textColorToStyle: { [color in DocumentMarkColor['data']['text']]: ClassValue } = {
default: [],
blue: ['text-blue-500'],
red: ['text-red-500'],
green: ['text-green-500'],
yellow: ['text-yellow-600'],
purple: ['text-purple-500'],
orange: ['text-orange-500'],
$primary: ['text-primary'],
$info: ['text-info'],
$success: ['text-success'],
$warning: ['text-warning'],
$danger: ['text-danger'],
};
export const backgroundColorToStyle: {
[color in DocumentMarkColor['data']['background']]: ClassValue;
} = {
default: [],
blue: ['bg-mark-blue'],
red: ['bg-mark-red'],
green: ['bg-mark-green'],
yellow: ['bg-mark-yellow'],
purple: ['bg-mark-purple'],
orange: ['bg-mark-orange'],
$primary: ['bg-primary'],
$info: ['bg-info'],
$success: ['bg-success'],
$warning: ['bg-warning'],
$danger: ['bg-danger'],
};
@@ -23,12 +23,6 @@ export function Header(props: {
const { context, withTopHeader, withVariants } = props;
const { siteSpace, siteSpaces, sections, customization } = context;
const withSections = Boolean(
sections &&
(sections.list.length > 1 || // Show section tabs if there are at least 2 sections or at least 1 section group
sections.list.some((s) => s.object === 'site-section-group'))
);
return (
<header
id="site-header"
@@ -150,7 +144,7 @@ export function Header(props: {
</div>
{customization.header.links.length > 0 ||
(!withSections && withVariants === 'translations') ? (
(!sections && withVariants === 'translations') ? (
<HeaderLinks>
{customization.header.links.length > 0 ? (
<>
@@ -170,7 +164,7 @@ export function Header(props: {
/>
</>
) : null}
{!withSections && withVariants === 'translations' ? (
{!sections && withVariants === 'translations' ? (
<TranslationsDropdown
context={context}
siteSpace={siteSpace}
@@ -184,7 +178,7 @@ export function Header(props: {
</div>
</div>
{sections && withSections ? (
{sections ? (
<div className="transition-[padding] duration-300 lg:chat-open:pr-80 xl:chat-open:pr-96">
<SiteSectionTabs sections={encodeClientSiteSections(context, sections)}>
{withVariants === 'translations' ? (
@@ -18,22 +18,17 @@ import {
ActionViewAsPDF,
} from './PageActions';
export type PageActionsDropdownURLs = {
html: string;
markdown: string;
mcp?: string;
pdf?: string;
interface PageActionsDropdownProps {
siteTitle: string;
markdownPageURL: string;
mcpURL?: string;
pdfURL?: string;
className?: string;
actions: SiteCustomizationSettings['pageActions'];
editOnGit?: {
provider: GitSyncState['installationProvider'];
url: string;
};
};
interface PageActionsDropdownProps {
siteTitle: string;
urls: PageActionsDropdownURLs;
className?: string;
actions: SiteCustomizationSettings['pageActions'];
}
/**
@@ -80,7 +75,7 @@ export function PageActionsDropdown(props: PageActionsDropdownProps) {
* Return the list of actions to show in the dropdown menu.
*/
function getPageDropdownActions(props: PageActionsDropdownProps): React.ReactNode[] {
const { siteTitle, urls, actions } = props;
const { siteTitle, markdownPageURL, mcpURL, actions } = props;
const assistants = useAI().assistants.filter(
(assistant) => assistant.ui === true && assistant.pageAction
);
@@ -99,45 +94,55 @@ function getPageDropdownActions(props: PageActionsDropdownProps): React.ReactNod
<DropdownMenuSeparator className="first:hidden" />
<ActionCopyMarkdown
isDefaultAction={!assistants.length}
markdownPageURL={urls.markdown}
markdownPageURL={markdownPageURL}
type="dropdown-menu-item"
/>
<ActionViewAsMarkdown markdownPageURL={urls.markdown} type="dropdown-menu-item" />
<ActionViewAsMarkdown markdownPageURL={markdownPageURL} type="dropdown-menu-item" />
</React.Fragment>
) : null,
actions.externalAI ? (
<React.Fragment key="externalAI">
<DropdownMenuSeparator className="first:hidden" />
<ActionOpenInLLM provider="chatgpt" url={urls.html} type="dropdown-menu-item" />
<ActionOpenInLLM provider="claude" url={urls.html} type="dropdown-menu-item" />
<ActionOpenInLLM
provider="chatgpt"
url={markdownPageURL}
type="dropdown-menu-item"
/>
<ActionOpenInLLM
provider="claude"
url={markdownPageURL}
type="dropdown-menu-item"
/>
</React.Fragment>
) : null,
actions.mcp && urls.mcp ? (
actions.mcp && mcpURL ? (
<React.Fragment key="mcp">
<DropdownMenuSeparator className="first:hidden" />
<ActionCopyMCPURL mcpURL={urls.mcp} type="dropdown-menu-item" />
<ActionCopyMCPURL mcpURL={mcpURL} type="dropdown-menu-item" />
<ActionOpenMCP
provider="vscode"
mcpURL={urls.mcp}
mcpURL={mcpURL}
siteTitle={siteTitle}
type="dropdown-menu-item"
/>
</React.Fragment>
) : null,
urls.editOnGit || urls.pdf ? (
props.editOnGit || props.pdfURL ? (
<React.Fragment key="editOnGit">
<DropdownMenuSeparator className="first:hidden" />
{urls.editOnGit ? (
{props.editOnGit ? (
<ActionOpenEditOnGit
type="dropdown-menu-item"
provider={urls.editOnGit.provider}
url={urls.editOnGit.url}
provider={props.editOnGit.provider}
url={props.editOnGit.url}
/>
) : null}
{urls.pdf ? <ActionViewAsPDF url={urls.pdf} type="dropdown-menu-item" /> : null}
{props.pdfURL ? (
<ActionViewAsPDF url={props.pdfURL} type="dropdown-menu-item" />
) : null}
</React.Fragment>
) : null,
].filter(Boolean);
@@ -147,7 +152,7 @@ function getPageDropdownActions(props: PageActionsDropdownProps): React.ReactNod
* A default action shown as a quick-access button beside the dropdown menu
*/
function getPageDefaultAction(props: PageActionsDropdownProps) {
const { urls, actions } = props;
const { markdownPageURL, actions } = props;
const assistants = useAI().assistants.filter(
(assistant) => assistant.ui === true && assistant.pageAction
);
@@ -157,12 +162,12 @@ function getPageDefaultAction(props: PageActionsDropdownProps) {
return <ActionOpenAssistant assistant={assistant} type="button" />;
}
if (urls.editOnGit) {
if (props.editOnGit) {
return (
<ActionOpenEditOnGit
type="button"
provider={urls.editOnGit.provider}
url={urls.editOnGit.url}
provider={props.editOnGit.provider}
url={props.editOnGit.url}
/>
);
}
@@ -171,7 +176,7 @@ function getPageDefaultAction(props: PageActionsDropdownProps) {
return (
<ActionCopyMarkdown
isDefaultAction={!assistant}
markdownPageURL={urls.markdown}
markdownPageURL={markdownPageURL}
type="button"
/>
);
@@ -35,7 +35,6 @@ export function PageAside(props: {
<aside
className={tcls(
'group/aside',
'order-last',
'hidden',
'pt-8',
'pb-4',
@@ -110,7 +110,7 @@ export function ScrollSectionsList({ sections }: { sections: DocumentSection[] }
'opacity-8',
'contrast-more:opacity-11',
'sidebar-list-default:rounded-l-none!',
'sidebar-list-default:rounded-l-none',
'sidebar-list-default:border-l',
'sidebar-list-default:border-tint',
],
@@ -51,10 +51,10 @@ export function PageBody(props: {
<main
className={tcls(
'relative min-w-0 flex-1',
'max-w-screen-2xl py-8',
'mx-auto max-w-screen-2xl py-8',
// Allow words to break if they are too long.
'break-anywhere',
pageWidthWide ? 'page-width-wide 3xl:px-8' : 'page-width-default',
pageWidthWide ? 'page-width-wide 2xl:px-8' : 'page-width-default',
siteWidthWide ? 'site-width-wide' : 'site-width-default',
page.layout.tableOfContents ? 'page-has-toc' : 'page-no-toc'
)}
@@ -5,10 +5,7 @@ import { type RevisionPageDocument, SiteVisibility } from '@gitbook/api';
import { Icon } from '@gitbook/icons';
import urlJoin from 'url-join';
import { getPDFURLSearchParams } from '../PDF';
import {
PageActionsDropdown,
type PageActionsDropdownURLs,
} from '../PageActions/PageActionsDropdown';
import { PageActionsDropdown } from '../PageActions/PageActionsDropdown';
import { PageIcon } from '../PageIcon';
import { StyledLink } from '../primitives';
@@ -43,7 +40,35 @@ export async function PageHeader(props: {
// Show page actions if *any* of the actions are enabled
<PageActionsDropdown
siteTitle={context.site.title}
urls={getPageActionsURLs(context, page)}
markdownPageURL={`${context.linker.toAbsoluteURL(context.linker.toPathInSpace(page.path))}.md`}
editOnGit={
context.customization.git.showEditLink &&
context.space.gitSync?.url &&
page.git
? {
provider: context.space?.gitSync?.installationProvider,
url: urlJoin(context.space.gitSync.url, page.git.path),
}
: undefined
}
pdfURL={
context.customization.pdf.enabled
? context.linker.toPathInSpace(
`~gitbook/pdf?${getPDFURLSearchParams({
page: page.id,
only: true,
limit: 100,
}).toString()}`
)
: undefined
}
mcpURL={
context.site.visibility !== SiteVisibility.VisitorAuth
? context.linker.toAbsoluteURL(
context.linker.toPathInSpace('~gitbook/mcp')
)
: undefined
}
actions={context.customization.pageActions}
className={tcls(
'float-right ml-4 xl:max-2xl:page-api-block:mr-62',
@@ -121,38 +146,3 @@ export async function PageHeader(props: {
</header>
);
}
/**
* Return the URLs for the page actions.
*/
function getPageActionsURLs(
context: GitBookSiteContext,
page: RevisionPageDocument
): PageActionsDropdownURLs {
return {
html: context.linker.toAbsoluteURL(
context.linker.toPathForPage({ pages: context.revision.pages, page })
),
markdown: `${context.linker.toAbsoluteURL(context.linker.toPathInSpace(page.path))}.md`,
editOnGit:
context.customization.git.showEditLink && context.space.gitSync?.url && page.git
? {
provider: context.space?.gitSync?.installationProvider,
url: urlJoin(context.space.gitSync.url, page.git.path),
}
: undefined,
pdf: context.customization.pdf.enabled
? context.linker.toPathInSpace(
`~gitbook/pdf?${getPDFURLSearchParams({
page: page.id,
only: true,
limit: 100,
}).toString()}`
)
: undefined,
mcp:
context.site.visibility !== SiteVisibility.VisitorAuth
? context.linker.toAbsoluteURL(context.linker.toPathInSpace('~gitbook/mcp'))
: undefined,
};
}
@@ -240,7 +240,7 @@ export function SearchContainer(props: SearchContainerProps) {
}
rootProps={{
open: Boolean(visible && (state?.open ?? false)),
modal: false,
modal: isMobile,
}}
contentProps={{
onOpenAutoFocus: (event) => event.preventDefault(),
@@ -72,7 +72,7 @@ export const SearchInput = React.forwardRef<HTMLDivElement, SearchInputProps>(
'has-[input:focus]:-translate-y-px h-9 grow cursor-pointer px-2.5 has-[input:focus]:bg-tint-base has-[input:focus]:depth-subtle:shadow-lg has-[input:focus]:depth-subtle:shadow-primary-subtle has-[input:focus-visible]:ring-2 has-[input:focus-visible]:ring-primary-hover md:cursor-text',
'theme-bold:border-header-link/3 has-[input:focus-visible]:theme-bold:border-header-link/5 has-[input:focus-visible]:theme-bold:bg-header-link/3 has-[input:focus-visible]:theme-bold:ring-header-link/5',
'theme-bold:before:absolute theme-bold:before:inset-0 theme-bold:before:bg-header-background/7 theme-bold:before:backdrop-blur-xl ', // Special overlay to make the transparent colors of theme-bold visible.
'relative z-30 max-w-none shrink grow justify-start max-md:absolute max-md:right-0',
'relative z-30 shrink grow justify-start max-md:absolute max-md:right-0',
isOpen ? 'max-md:w-56' : 'max-md:w-[38px]'
)}
>
@@ -2,7 +2,6 @@
import { tString, useLanguage } from '@/intl/client';
import type { SiteSection } from '@gitbook/api';
import { Button, Checkbox, DropdownMenu, DropdownMenuItem } from '../primitives';
import { SegmentedControl, SegmentedControlItem } from '../primitives/SegmentedControl';
import { useSearch } from './useSearch';
@@ -25,106 +24,80 @@ export function SearchScopeToggle(props: {
return null;
}
// Whether to include all variants in the search
const scopeIsExtended = ['extended', 'all'].includes(state.scope);
return (
<>
{withSections ? (
<div className="mb-2 flex items-center gap-2">
<SegmentedControl className="grow animate-scale-in">
<SegmentedControl className="animate-scale-in">
{/* `Default` scope = current section's current variant + best match in other sections */}
<SegmentedControlItem
active={
withSiteVariants
? state.scope === 'default'
: ['default', 'all'].includes(state.scope)
}
label={
withSiteVariants
? tString(language, 'search_scope_default')
: tString(language, 'search_scope_all')
}
className={withSiteVariants ? '@max-md:basis-full' : ''}
icon={withSiteVariants ? 'bullseye-arrow' : 'infinity'}
onClick={() => setSearchState({ ...state, scope: 'default' })}
/>
{/* `Current` scope = current section's current variant (with further variant scope selection if necessary) */}
<SegmentedControlItem
active={state.scope === 'current' || state.scope === 'extended'}
icon={section?.icon ?? 'crosshairs'}
label={tString(language, 'search_scope_current', section?.title)}
onClick={() => setSearchState({ ...state, scope: 'current' })}
/>
{/* `All` scope = all content on the site. Only visible if site has variants, otherwise it's the same as default */}
{withSiteVariants ? (
<SegmentedControlItem
active={['default', 'all'].includes(state.scope)}
active={state.scope === 'all'}
label={tString(language, 'search_scope_all')}
icon="infinity"
onClick={() =>
setSearchState({
...state,
scope: scopeIsExtended ? 'all' : 'default',
})
}
onClick={() => setSearchState({ ...state, scope: 'all' })}
/>
<SegmentedControlItem
active={['current', 'extended'].includes(state.scope)}
icon={section?.icon}
label={tString(language, 'search_scope_current', section?.title)}
onClick={() =>
setSearchState({
...state,
scope: scopeIsExtended ? 'extended' : 'current',
})
}
/>
</SegmentedControl>
{withSiteVariants ? (
<DropdownMenu
button={
<Button
icon="ellipsis"
iconOnly
size="default"
variant="blank"
label={tString(language, 'more')}
className="shrink-0"
active={scopeIsExtended}
/>
}
>
<DropdownMenuItem
onClick={() =>
setSearchState({
...state,
scope: (() => {
switch (state.scope) {
case 'extended':
return 'current';
case 'current':
return 'extended';
case 'all':
return 'default';
default:
return 'all';
}
})(),
})
}
active={scopeIsExtended}
>
<div className="flex items-center gap-3">
<Checkbox checked={scopeIsExtended} />
<div className="flex flex-col items-start">
Include all variants
<div className="text-tint-subtle text-xs">
Search content in every language/version
</div>
</div>
</div>
</DropdownMenuItem>
</DropdownMenu>
) : null}
</div>
) : withVariants ? (
<SegmentedControl className="mb-2 animate-scale-in">
</SegmentedControl>
) : null}
{withVariants &&
(!withSections || state.scope === 'current' || state.scope === 'extended') ? (
<SegmentedControl className="animate-scale-in">
{/* `Current` scope = current section's current variant. `Default` on sites without sections. */}
<SegmentedControlItem
size={withSections ? 'small' : 'medium'}
active={!scopeIsExtended}
active={
withSections
? state.scope === 'current'
: ['default', 'current'].includes(state.scope)
}
className="py-1"
label={tString(language, 'search_scope_current', spaceTitle)}
onClick={() =>
setSearchState({
...state,
scope: 'default',
scope: withSections ? 'current' : 'default',
})
}
/>
{/* `Extended` scope = all variants of the current section. `All` on sites without sections. */}
<SegmentedControlItem
size={withSections ? 'small' : 'medium'}
active={scopeIsExtended}
active={
withSections
? state.scope === 'extended'
: ['extended', 'all'].includes(state.scope)
}
className="py-1"
icon="infinity"
label={tString(language, 'search_scope_all')}
onClick={() => setSearchState({ ...state, scope: 'all' })}
label={tString(language, 'search_scope_extended')}
onClick={() =>
setSearchState({ ...state, scope: withSections ? 'extended' : 'all' })
}
/>
</SegmentedControl>
) : null}
@@ -22,17 +22,12 @@ export function SiteLayoutClientContexts(props: {
useClearRouterCache(contextId);
return (
<ThemeProvider
attribute="class"
disableTransitionOnChange
enableSystem
forcedTheme={forcedTheme}
>
<NuqsAdapter>
<NuqsAdapter>
<ThemeProvider attribute="class" enableSystem forcedTheme={forcedTheme}>
<LinkSettingsContext.Provider value={{ externalLinksTarget }}>
<SearchContextProvider>{children}</SearchContextProvider>
</LinkSettingsContext.Provider>
</NuqsAdapter>
</ThemeProvider>
</ThemeProvider>
</NuqsAdapter>
);
}
@@ -43,42 +43,38 @@ export async function SitePage(props: SitePageProps) {
return (
<PageContextProvider pageId={page.id} spaceId={context.space.id} title={page.title}>
{/* Using `contents` makes the children of this div according to its parent — which keeps them in a single flex row with the TOC by default.
If there's a page cover, we use `flex flex-col` to lay out the PageCover above the PageBody + PageAside instead. */}
<div className={withFullPageCover && page.cover ? 'flex grow flex-col' : 'contents'}>
{withFullPageCover && page.cover ? (
<PageCover as="full" page={page} cover={page.cover} context={context} />
) : null}
<div
className={tcls(
withFullPageCover && page.cover ? 'flex grow flex-row' : 'contents',
withSections
? '[--content-scroll-margin:calc(var(--spacing)*27)]'
: '[--content-scroll-margin:calc(var(--spacing)*16)]'
)}
>
<PageAside
page={page}
document={document}
withHeaderOffset={headerOffset}
withFullPageCover={withFullPageCover}
withPageFeedback={withPageFeedback}
context={context}
/>
<PageBody
context={context}
page={page}
ancestors={ancestors}
document={document}
withPageFeedback={withPageFeedback}
insightsDisplayContext={SiteInsightsDisplayContext.Site}
/>
</div>
<React.Suspense fallback={null}>
<PageClientLayout />
</React.Suspense>
{withFullPageCover && page.cover ? (
<PageCover as="full" page={page} cover={page.cover} context={context} />
) : null}
{/* We use a flex row reverse to render the aside first because the page is streamed. */}
<div
className={tcls(
'flex grow flex-row-reverse justify-end',
withSections
? '[--content-scroll-margin:calc(var(--spacing)*27)]'
: '[--content-scroll-margin:calc(var(--spacing)*16)]'
)}
>
<PageAside
page={page}
document={document}
withHeaderOffset={headerOffset}
withFullPageCover={withFullPageCover}
withPageFeedback={withPageFeedback}
context={context}
/>
<PageBody
context={context}
page={page}
ancestors={ancestors}
document={document}
withPageFeedback={withPageFeedback}
insightsDisplayContext={SiteInsightsDisplayContext.Site}
/>
</div>
<React.Suspense fallback={null}>
<PageClientLayout />
</React.Suspense>
</PageContextProvider>
);
}
@@ -137,7 +137,6 @@ export function SpaceLayout(props: SpaceLayoutProps) {
'flex',
'flex-col',
'lg:flex-row',
'lg:justify-center',
CONTAINER_STYLE,
'site-width-wide:max-w-full',
@@ -224,7 +223,7 @@ export function SpaceLayout(props: SpaceLayoutProps) {
</>
}
/>
{children}
<div className="flex min-w-0 flex-1 flex-col">{children}</div>
</div>
</div>
@@ -59,11 +59,6 @@ export const NavigationStatusProvider: React.FC<React.PropsWithChildren> = ({ ch
}, []);
const onNavigationClick = React.useCallback((href: string) => {
// We need to skip it for search like params (i.e. ?ask= or ?q=) because they don't really trigger a navigation
// Search may trigger a navigation whenn clicking on the ask ai for example, this is not something we want to track here
if (href.startsWith('?') || href.startsWith('#')) {
return;
}
const url = new URL(
href,
typeof window !== 'undefined' ? window.location.origin : 'http://localhost'
@@ -145,9 +145,7 @@ export const Button = React.forwardRef<
icon
)
) : null}
{iconOnly || (!children && !label) ? null : (
<span className="button-content truncate">{children ?? label}</span>
)}
{iconOnly ? null : (children ?? label)}
</>
);
@@ -82,7 +82,7 @@ export function DropdownMenu(props: {
onMouseLeave={() => setHovered(false)}
align={align}
side={side}
className="z-50 animate-scale-in border-tint pt-2"
className="z-40 animate-scale-in border-tint pt-2"
>
<div
className={tcls(
@@ -214,7 +214,7 @@ export function DropdownSubMenu(props: { children: React.ReactNode; label: React
<RadixDropdownMenu.SubContent
hideWhenDetached
collisionPadding={8}
className="z-50 animate-present"
className="z-40 animate-present"
>
<div className="flex max-h-96 min-w-40 max-w-[40vw] flex-col gap-1 overflow-auto rounded-lg straight-corners:rounded-sm bg-tint-base p-2 shadow-lg ring-1 ring-tint-subtle sm:min-w-52 sm:max-w-80">
{children}
@@ -80,8 +80,7 @@ export const Link = React.forwardRef(function Link(
const onClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
const isExternalWithOrigin = isExternalLink(href, window.location.origin);
// Only trigger navigation context for internal links without modifier keys (i.e. open in new tab).
if (!isExternal && !event.ctrlKey && !event.metaKey) {
if (!isExternal) {
onNavigationClick(href);
}
@@ -9,7 +9,7 @@ export function SegmentedControl(props: { children: React.ReactNode; className?:
role="toolbar"
aria-orientation="horizontal"
className={tcls(
'flex flex-wrap gap-1 circular-corners:rounded-3xl rounded-corners:rounded-lg bg-tint p-1',
'mb-2 flex flex-wrap gap-1 circular-corners:rounded-3xl rounded-corners:rounded-lg bg-tint p-1',
className
)}
>
@@ -35,7 +35,6 @@ export const ButtonStyles = [
'grow-0',
'shrink-0',
'truncate',
'max-w-full',
'disabled:cursor-not-allowed',
'disabled:translate-y-0!',
+3 -3
View File
@@ -17,9 +17,9 @@ export const de = {
search_section_result_content: 'Abschnitt mit Inhalt ${1}',
search_section_result_default: 'Abschnitt',
search_scope_current: '${1}',
search_scope_all: 'Überall',
search_scope_include_all_title: 'Alle Varianten einschließen',
search_scope_include_all_description: 'Inhalt in jeder Sprache/Version durchsuchen',
search_scope_extended: 'Alle Inhalte',
search_scope_default: 'Beste Match',
search_scope_all: 'Alles',
ask: 'Fragen',
search_ask: 'Fragen "${1}"',
search_ask_description: 'Finden Sie die Antwort mit ${1}',
+3 -3
View File
@@ -17,9 +17,9 @@ export const en = {
search_section_result_content: 'Section with content ${1}',
search_section_result_default: 'Section',
search_scope_current: '${1}',
search_scope_all: 'Everywhere',
search_scope_include_all_title: 'Include all variants',
search_scope_include_all_description: 'Search content in every language/version',
search_scope_extended: 'All content',
search_scope_default: 'Best match',
search_scope_all: 'Everything',
ask: 'Ask',
search_ask: 'Ask "${1}"',
search_ask_description: 'Find the answer with ${1}',
+3 -3
View File
@@ -19,9 +19,9 @@ export const es: TranslationLanguage = {
search_section_result_content: 'Sección con contenido ${1}',
search_section_result_default: 'Sección',
search_scope_current: '${1}',
search_scope_all: 'En todas partes',
search_scope_include_all_title: 'Incluir todas las variantes',
search_scope_include_all_description: 'Buscar contenido en todos los idiomas/versiones',
search_scope_extended: 'Todo el contenido',
search_scope_default: 'Mejor match',
search_scope_all: 'Todo',
ask: 'Preguntar',
search_ask: 'Preguntar "${1}"',
search_ask_description: 'Encuentra la respuesta con ${1}',
+3 -3
View File
@@ -17,9 +17,9 @@ export const fr = {
search_section_result_content: 'Section avec contenu ${1}',
search_section_result_default: 'Section',
search_scope_current: '${1}',
search_scope_all: 'Partout',
search_scope_include_all_title: 'Inclure toutes les variantes',
search_scope_include_all_description: 'Rechercher le contenu dans toutes les langues/versions',
search_scope_extended: 'Tous les contenus',
search_scope_default: 'Meilleur match',
search_scope_all: 'Tout',
ask: 'Poser une question',
search_ask: 'Demander « ${1} »',
search_ask_description: 'Trouvez la réponse avec ${1}',
@@ -2,7 +2,6 @@ import { de } from './de';
import { en } from './en';
import { es } from './es';
import { fr } from './fr';
import { it } from './it';
import { ja } from './ja';
import { nl } from './nl';
import { no } from './no';
@@ -16,9 +15,8 @@ export * from './types';
export const languages = {
de,
en,
es,
fr,
it,
es,
zh,
ja,
nl,
@@ -1,133 +0,0 @@
import type { TranslationLanguage } from './types';
export const it: TranslationLanguage = {
locale: 'it',
powered_by_gitbook: 'Offerto da GitBook',
sponsored_via_gitbook: 'Sponsorizzato tramite GitBook',
switch_to_dark_theme: 'Passa al tema scuro',
switch_to_light_theme: 'Passa al tema chiaro',
switch_to_system_theme: 'Passa al tema di sistema',
search: 'Cerca',
search_or_ask: 'Chiedi o cerca',
search_input_placeholder: 'Cerca contenuti',
search_ask_input_placeholder: 'Cerca contenuti o fai una domanda',
search_no_results_for: 'Nessun risultato per "${1}".',
search_no_results: 'Nessun risultato',
search_results_count: '${1} risultati',
search_page_result_title: 'Pagina con titolo ${1}',
search_section_result_title: 'Sezione con titolo ${1}',
search_section_result_content: 'Sezione con contenuto ${1}',
search_section_result_default: 'Sezione',
search_scope_current: '${1}',
search_scope_all: 'Ovunque',
search_scope_include_all_title: 'Includi tutte le varianti',
search_scope_include_all_description: 'Cerca contenuti in ogni lingua/versione',
ask: 'Chiedi',
search_ask: 'Chiedi "${1}"',
search_ask_description: 'Trova la risposta con ${1}',
search_ask_sources: 'Fonti',
search_ask_sources_no_answer: 'Pagine correlate',
search_ask_no_answer:
'Non è stato possibile trovare una risposta alla tua domanda. Prova a riformularla o a essere più specifico.',
search_ask_error: 'Qualcosa è andato storto. Riprova più tardi.',
on_this_page: 'In questa pagina',
next_page: 'Successivo',
previous_page: 'Precedente',
page_last_modified: 'Ultimo aggiornamento ${1}',
was_this_helpful: 'È stato utile?',
was_this_helpful_positive: 'Sì, lo è stato!',
was_this_helpful_neutral: 'Non sono sicuro',
was_this_helpful_negative: 'No',
was_this_helpful_thank_you: 'Grazie!',
was_this_helpful_comment: "Vuoi aggiungere qualcos'altro?",
was_this_helpful_positive_label: 'Utile',
was_this_helpful_negative_label: 'Non utile',
submit: 'Invia',
annotation_button_label: 'Apri annotazione',
code_copied: 'Copiato!',
code_copy: 'Copia',
table_of_contents_button_label: 'Apri indice dei contenuti',
cookies_title: 'Cookie',
cookies_prompt:
'Questo sito utilizza i cookie per fornire il servizio e analizzare il traffico. Navigando su questo sito accetti la ${1}.',
cookies_prompt_privacy: 'informativa sulla privacy',
cookies_accept: 'Accetta',
cookies_reject: 'Rifiuta',
close: 'Chiudi',
view: 'Visualizza',
edit_on_git: 'Modifica su ${1}',
edit: 'Modifica',
notfound_title: 'Pagina non trovata',
notfound_link: 'Questo link punta a una pagina che è stata rimossa o non esiste più.',
notfound: 'La pagina che cerchi non esiste.',
unexpected_error_title: 'Si è verificato un errore',
unexpected_error: 'Spiacenti, si è verificato un errore imprevisto. Riprova più tardi.',
unexpected_error_retry: 'Riprova',
pdf_download: 'Esporta in PDF',
pdf_goback: 'Torna al contenuto',
pdf_print: 'Stampa o salva come PDF',
pdf_page_of: '${1} di ${2}',
pdf_mode_only_page: 'Solo questa pagina',
pdf_mode_all: 'Tutte le pagine',
pdf_limit_reached:
'Impossibile generare il PDF per ${1} pagine, la generazione si è fermata a ${2}.',
pdf_limit_reached_continue: 'Estendi con altre ${1} pagine.',
more: 'Altro',
link_tooltip_external_link: 'Link esterno a',
link_tooltip_email: "Invia un'email a",
link_tooltip_page_anchor: 'Vai alla sezione',
open_in_new_tab: 'Apri in una nuova scheda',
ai_chat_assistant_name: 'Assistente GitBook',
ai_chat_assistant_name_unbranded: 'Assistente AI',
ai_chat_assistant_description: 'Sono qui per aiutarti con la documentazione.',
ai_chat_assistant_greeting_morning: 'Buongiorno',
ai_chat_assistant_greeting_afternoon: 'Buon pomeriggio',
ai_chat_assistant_greeting_evening: 'Buonasera',
ai_chat_assistant_greeting_night: 'Buonanotte',
ai_chat_clear_conversation: 'Cancella conversazione',
ai_chat_thinking: 'Sto pensando...',
ai_chat_working: 'Sto lavorando...',
ai_chat_context_badge: 'IA',
ai_chat_error: 'Qualcosa è andato storto.',
ai_chat_hold_message_1: 'Ci sto pensando…',
ai_chat_hold_message_2: 'Un momento…',
ai_chat_context_title: 'Basato sul tuo contesto',
ai_chat_context_description:
"L'assistente usa l'IA e il tuo contesto per generare risposte ed eseguire azioni.",
ai_chat_context_pages_youve_read: 'Pagine che hai letto',
ai_chat_context_info_provided_by_the_site: 'Informazioni fornite dal sito',
ai_chat_context_previous_messages: 'Messaggi precedenti',
ai_chat_context_disclaimer: "Le risposte dell'IA possono contenere errori.",
ai_chat_input_placeholder: 'Chiedi, cerca o spiega...',
send: 'Invia',
actions: 'Azioni',
ai_chat_suggested_questions_title: 'Domande suggerite',
ai_chat_suggested_questions_about_this_page: 'Di cosa parla questa pagina?',
ai_chat_suggested_questions_read_next: 'Cosa dovrei leggere dopo?',
ai_chat_suggested_questions_example: 'Puoi fare un esempio?',
searched_for: 'Ricerca per ${1}',
ai_chat_tools_listed_pages: 'Ha esplorato la documentazione',
ai_chat_tools_read_page: 'Ha letto la pagina ${1}',
ai_chat_tools_mcp_tool: 'Ha chiamato ${1}',
ai_chat_ask: 'Chiedi a ${1}',
ai_chat_ask_about_page: 'Chiedi a ${1} di questa pagina',
copy_for_llms: 'Copia per LLM',
copy_page_markdown: 'Copia la pagina come Markdown per LLM',
copy_page: 'Copia pagina',
view_page_markdown: 'Visualizza come Markdown',
view_page_plaintext: 'Visualizza questa pagina come testo semplice',
open_in: 'Apri in ${1}',
open_in_llms_pre_prompt: 'Leggi ${1} e rispondi alle domande sul contenuto.',
connect_mcp_to: 'Connetti a ${1}',
install_mcp_on: 'Installa il server MCP su ${1}',
connect_with_mcp: 'Connettiti con MCP',
copy_mcp_url: "Copia l'URL del server MCP",
press_to_confirm: 'Premi ${1} per confermare',
tool_call_skipped: 'Saltato "${1}"',
scroll_back: 'Scorri indietro',
scroll_further: 'Scorri ancora',
hint_info: 'Informazioni',
hint_warning: 'Avviso',
hint_danger: 'Pericolo',
hint_success: 'Successo',
};
+3 -3
View File
@@ -19,9 +19,9 @@ export const ja: TranslationLanguage = {
search_section_result_content: 'コンテンツ「${1}」のセクション',
search_section_result_default: 'セクション',
search_scope_current: '${1}',
search_scope_all: 'どこでも',
search_scope_include_all_title: 'すべてのバリエーションを含める',
search_scope_include_all_description: 'すべての言語/バージョンでコンテンツを検索',
search_scope_extended: '全てのコンテンツ',
search_scope_default: '最適なマッチ',
search_scope_all: 'すべて',
ask: '質問する',
search_ask: '"${1}" を質問する',
search_ask_description: '${1}で答えを見つける',
+3 -3
View File
@@ -19,9 +19,9 @@ export const nl: TranslationLanguage = {
search_section_result_content: 'Sectie met inhoud ${1}',
search_section_result_default: 'Sectie',
search_scope_current: '${1}',
search_scope_all: 'Overal',
search_scope_include_all_title: 'Alle varianten opnemen',
search_scope_include_all_description: 'Zoek inhoud in elke taal/versie',
search_scope_extended: 'Alle inhoud',
search_scope_default: 'Beste match',
search_scope_all: 'Alles',
ask: 'Vragen',
search_ask: 'Vraag "${1}"',
search_ask_description: 'Vind het antwoord met ${1}',
+3 -3
View File
@@ -19,9 +19,9 @@ export const no: TranslationLanguage = {
search_section_result_content: 'Seksjon med innhold ${1}',
search_section_result_default: 'Seksjon',
search_scope_current: '${1}',
search_scope_all: 'Overalt',
search_scope_include_all_title: 'Inkluder alle varianter',
search_scope_include_all_description: 'Søk innhold på alle språk/versjoner',
search_scope_extended: 'Alt innhold',
search_scope_default: 'Beste treff',
search_scope_all: 'Alt',
ask: 'Spør',
search_ask: 'Spør "${1}"',
search_ask_description: 'Finn svaret med ${1}',
@@ -17,9 +17,9 @@ export const pt_br = {
search_section_result_content: 'Seção com conteúdo ${1}',
search_section_result_default: 'Seção',
search_scope_current: '${1}',
search_scope_all: 'Em todos os lugares',
search_scope_include_all_title: 'Incluir todas as variantes',
search_scope_include_all_description: 'Buscar conteúdo em todos os idiomas/versões',
search_scope_extended: 'Todo o conteúdo',
search_scope_default: 'Melhor match',
search_scope_all: 'Tudo',
ask: 'Perguntar',
search_ask: 'Perguntar "${1}"',
search_ask_description: 'Encontre a resposta com ${1}',
+3 -3
View File
@@ -17,9 +17,9 @@ export const ru = {
search_section_result_content: 'Раздел с контентом ${1}',
search_section_result_default: 'Раздел',
search_scope_current: '${1}',
search_scope_all: 'Везде',
search_scope_include_all_title: 'Включить все варианты',
search_scope_include_all_description: 'Искать контент на всех языках/версиях',
search_scope_extended: 'Все материалы',
search_scope_default: 'Лучший матч',
search_scope_all: 'Всё',
ask: 'Спросить',
search_ask: 'Спросить "${1}"',
search_ask_description: 'Найти ответ с помощью ${1}',
+3 -3
View File
@@ -19,9 +19,9 @@ export const zh: TranslationLanguage = {
search_section_result_content: '内容为"${1}"的章节',
search_section_result_default: '章节',
search_scope_current: '${1}',
search_scope_all: '任何地方',
search_scope_include_all_title: '包含所有变体',
search_scope_include_all_description: '在所有语言/版本中搜索内容',
search_scope_extended: '所有内容',
search_scope_default: '最佳匹配',
search_scope_all: '全部',
ask: '询问',
search_ask: '询问"${1}"',
search_ask_description: '利用${1}找到答案',
@@ -1,13 +1,13 @@
import { checkIsSecurityError } from './security-error';
/**
* Reusable function to read from any kind of Storage
* Get an item from local storage safely.
*/
function readStorage<T>(storage: Storage | undefined, key: string, defaultValue: T): T {
export function getLocalStorageItem<T>(key: string, defaultValue: T): T {
try {
if (storage && 'getItem' in storage) {
const stored = storage.getItem(key);
return stored !== null ? (JSON.parse(stored) as T) : defaultValue;
if (typeof localStorage !== 'undefined' && localStorage && 'getItem' in localStorage) {
const stored = localStorage.getItem(key);
return stored ? (JSON.parse(stored) as T) : defaultValue;
}
return defaultValue;
} catch (error) {
@@ -18,74 +18,18 @@ function readStorage<T>(storage: Storage | undefined, key: string, defaultValue:
}
}
/**
* Reusable function to write to any kind of Storage
*/
function writeStorage(storage: Storage | undefined, key: string, value: unknown) {
try {
if (storage && 'setItem' in storage) {
storage.setItem(key, JSON.stringify(value));
}
} catch (error) {
if (checkIsSecurityError(error)) {
return;
}
throw error;
}
}
function removeStorage(storage: Storage | undefined, key: string) {
try {
if (storage && 'removeItem' in storage) {
storage.removeItem(key);
}
} catch (error) {
if (checkIsSecurityError(error)) {
return;
}
throw error;
}
}
/**
* Get an item from local storage safely.
*/
export function getLocalStorageItem<T>(key: string, defaultValue: T): T {
return readStorage(
typeof localStorage !== 'undefined' ? localStorage : undefined,
key,
defaultValue
);
}
/**
* Set an item in local storage safely.
*/
export function setLocalStorageItem(key: string, value: unknown) {
writeStorage(typeof localStorage !== 'undefined' ? localStorage : undefined, key, value);
}
/**
* Get an item from session storage safely.
*/
export function getSessionStorageItem<T>(key: string, defaultValue: T): T {
return readStorage(
typeof sessionStorage !== 'undefined' ? sessionStorage : undefined,
key,
defaultValue
);
}
/**
* Set an item in session storage safely.
*/
export function setSessionStorageItem(key: string, value: unknown) {
writeStorage(typeof sessionStorage !== 'undefined' ? sessionStorage : undefined, key, value);
}
/**
* Remove an item from session storage safely.
*/
export function removeSessionStorageItem(key: string) {
removeStorage(typeof sessionStorage !== 'undefined' ? sessionStorage : undefined, key);
try {
if (typeof localStorage !== 'undefined' && localStorage && 'setItem' in localStorage) {
localStorage.setItem(key, JSON.stringify(value));
}
} catch (error) {
if (checkIsSecurityError(error)) {
return;
}
throw error;
}
}
+2 -2
View File
@@ -344,7 +344,7 @@ const getRevisionPageDocument = cache(
params.revisionId,
params.pageId,
{
evaluated: 'deterministic-only',
evaluated: true,
},
{
...noCacheFetchOptions,
@@ -377,7 +377,7 @@ const getRevisionReusableContentDocument = cache(
params.revisionId,
params.reusableContentId,
{
evaluated: 'deterministic-only',
evaluated: true,
},
{
...noCacheFetchOptions,
+3 -41
View File
@@ -1,11 +1,9 @@
import type { GitBookAnyContext } from '@/lib/context';
import type { DocumentBlock, JSONDocument } from '@gitbook/api';
import { getDataOrNull } from './data';
import { getNodeText } from './document';
import { resolveOpenAPIOperationBlock } from './openapi/resolveOpenAPIOperationBlock';
import { resolveOpenAPISchemasBlock } from './openapi/resolveOpenAPISchemasBlock';
import { resolveContentRef } from './references';
export interface DocumentSection {
id: string;
@@ -30,11 +28,10 @@ export async function getDocumentSections(
*/
async function getSectionsFromNodes(
nodes: DocumentBlock[],
context: GitBookAnyContext,
initialDepth = 0
context: GitBookAnyContext
): Promise<DocumentSection[]> {
const sections: DocumentSection[] = [];
let depth = initialDepth;
let depth = 0;
for (const block of nodes) {
switch (block.type) {
@@ -67,9 +64,7 @@ async function getSectionsFromNodes(
}
case 'stepper': {
const stepNodes = await Promise.all(
block.nodes.map(async (step) =>
getSectionsFromNodes(step.nodes, context, depth)
)
block.nodes.map(async (step) => getSectionsFromNodes(step.nodes, context))
);
for (const stepSections of stepNodes) {
sections.push(...stepSections);
@@ -121,39 +116,6 @@ async function getSectionsFromNodes(
}
continue;
}
case 'reusable-content': {
const dataFetcher = block.meta?.token
? context.dataFetcher.withToken({ apiToken: block.meta.token })
: context.dataFetcher;
const resolved = await resolveContentRef(block.data.ref, {
...context,
dataFetcher,
});
if (!resolved) {
continue;
}
const { reusableContent } = resolved;
if (!reusableContent) {
continue;
}
const document = await getDataOrNull(
dataFetcher.getRevisionReusableContentDocument({
spaceId: reusableContent.context.space.id,
revisionId: reusableContent.context.revisionId,
reusableContentId: reusableContent.revisionReusableContent.id,
})
);
if (!document) {
continue;
}
const reusableContentSections = await getSectionsFromNodes(
document.nodes,
reusableContent.context,
depth
);
sections.push(...reusableContentSections);
}
}
}
+1 -2
View File
@@ -18,7 +18,6 @@ export type ResponseCookie = {
sameSite: boolean | 'lax' | 'strict' | 'none' | undefined;
secure: boolean;
maxAge: number;
path: string;
}>;
};
@@ -455,7 +454,7 @@ export function serveVisitorClaimsDataRequest(request: NextRequest, siteRequestU
});
if (!visitorToken && !Object.keys(unsignedClaims).length) {
return NextResponse.json({ visitor: { claims: { unsigned: {} } } });
return NextResponse.json({});
}
const visitorClaims = {
+2 -27
View File
@@ -296,41 +296,16 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
requestHeaders.set(MiddlewareHeaders.SiteURLData, JSON.stringify(stableSiteURLData));
// Preview of customization/theme
const customizationCookie = request.cookies.get(MiddlewareHeaders.Customization);
const customization =
siteRequestURL.searchParams.get('customization') ??
(customizationCookie ? decodeURIComponent(customizationCookie.value) : undefined);
const customization = siteRequestURL.searchParams.get('customization');
if (customization && validateSerializedCustomization(customization)) {
routeType = 'dynamic';
// We need to encode the customization headers, otherwise it will fail for some customization values containing non ASCII chars on vercel.
requestHeaders.set(MiddlewareHeaders.Customization, encodeURIComponent(customization));
cookies.push({
name: MiddlewareHeaders.Customization,
value: encodeURIComponent(customization),
options: {
httpOnly: true,
sameSite: 'lax',
maxAge: 10 * 60, // 10 minutes
path: '/url/preview', // Only send the cookie to preview routes
},
});
}
const theme =
siteRequestURL.searchParams.get('theme') ??
request.cookies.get(MiddlewareHeaders.Theme)?.value;
const theme = siteRequestURL.searchParams.get('theme');
if (theme === CustomizationThemeMode.Dark || theme === CustomizationThemeMode.Light) {
routeType = 'dynamic';
requestHeaders.set(MiddlewareHeaders.Theme, theme);
cookies.push({
name: MiddlewareHeaders.Theme,
value: theme,
options: {
httpOnly: true,
sameSite: 'lax',
maxAge: 10 * 60, // 10 minutes
path: '/url/preview', // Only send the cookie to preview routes
},
});
}
// We support forcing dynamic routes by setting a `gitbook-dynamic-route` cookie
-6
View File
@@ -1,11 +1,5 @@
# @gitbook/icons
## 0.3.2
### Patch Changes
- 295f03d: Republish packages
## 0.3.1
### Patch Changes
+1 -5
View File
@@ -16,7 +16,7 @@
"default": "./src/style.css"
}
},
"version": "0.3.2",
"version": "0.3.1",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.6.0",
"@fortawesome/fontawesome-free": "^6.6.0"
@@ -44,9 +44,5 @@
"files": ["dist", "src", "bin", "README.md", "CHANGELOG.md"],
"engines": {
"node": ">=20.0.0"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
}
}
-6
View File
@@ -1,11 +1,5 @@
# @gitbook/openapi-parser
## 3.0.3
### Patch Changes
- 295f03d: Republish packages
## 3.0.2
### Patch Changes
+2 -6
View File
@@ -9,7 +9,7 @@
"default": "./dist/index.js"
}
},
"version": "3.0.3",
"version": "3.0.2",
"sideEffects": false,
"dependencies": {
"@scalar/json-magic": "^0.3.1",
@@ -30,9 +30,5 @@
"dev": "bun run build -- --watch",
"clean": "rm -rf ./dist"
},
"files": ["dist", "src", "README.md", "CHANGELOG.md"],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
}
"files": ["dist", "src", "README.md", "CHANGELOG.md"]
}

Some files were not shown because too many files have changed in this diff Show More