diff --git a/bun.lockb b/bun.lockb index 44298515d..3d289a09a 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/e2e/pages.spec.ts b/e2e/pages.spec.ts index 37ac08795..56d1f9998 100644 --- a/e2e/pages.spec.ts +++ b/e2e/pages.spec.ts @@ -81,94 +81,6 @@ const testCases: TestsCase[] = [ { name: 'Rocket.Chat', baseUrl: 'https://docs.rocket.chat', - tests: [ - { - name: 'Home', - url: '', - }, - { - name: 'PDF', - url: '~gitbook/pdf?limit=10', - }, - ], - }, - { - name: 'Commerce Layer', - baseUrl: 'https://docs.commercelayer.io/core/', - tests: [ - { - name: 'Home', - url: '', - }, - { - name: 'API Reference', - url: 'v/api-reference/', - }, - ], - }, - { - name: 'Naviga', - baseUrl: 'https://docs.navigaglobal.com/naviga-dashboard-overview/', - tests: [ - { - name: 'Home', - url: 'v/dashboard-5.4/', - }, - ], - }, - { - name: 'Mattermost', - baseUrl: 'https://handbook.mattermost.com/', - tests: [ - { - name: 'Home', - url: '', - }, - ], - }, - { - name: 'Tile DB', - baseUrl: 'https://docs.tiledb.com/main/', - tests: [ - { - name: 'Home', - url: '', - }, - ], - }, - { - name: 'Nimbleway', - baseUrl: 'https://docs.nimbleway.com/', - tests: [ - { - name: 'Home', - url: '', - }, - ], - }, - { - name: 'Parcellab', - baseUrl: 'https://how.parcellab.works/docs/', - tests: [ - { - name: 'Home', - url: '', - }, - ], - }, - { - name: 'CitrusAd', - baseUrl: 'https://help.citrusad.com/citrus-ads/', - tests: [ - { - name: 'Home', - url: '', - }, - ], - }, - { - name: 'ThousandEyes', - baseUrl: 'https://docs.thousandeyes.com/', tests: [ { name: 'Home', @@ -252,6 +164,10 @@ const testCases: TestsCase[] = [ name: 'Emojis', url: 'blocks/emojis', }, + { + name: 'Links', + url: 'blocks/links', + }, ], }, { diff --git a/package.json b/package.json index 474a5c918..660278c26 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "jsontoxml": "^1.0.1", "katex": "^0.16.9", "memoizee": "^0.4.15", - "next": "^14.1.0", + "next": "^14.1.3", "next-themes": "^0.2.1", "nuqs": "^1.15.4", "object-hash": "^3.0.0", diff --git a/src/app/(space)/(content)/[[...pathname]]/PageClientLayout.tsx b/src/app/(space)/(content)/[[...pathname]]/PageClientLayout.tsx new file mode 100644 index 000000000..02f5f1323 --- /dev/null +++ b/src/app/(space)/(content)/[[...pathname]]/PageClientLayout.tsx @@ -0,0 +1,13 @@ +'use client'; + +import { useScrollToHash } from '@/components/hooks'; + +/** + * Client component to initialize interactivity for a page. + */ +export function PageClientLayout(props: {}) { + // We use this hook in the page layout to ensure the elements for the blocks + // are rendered before we scroll to the hash. + useScrollToHash(); + return null; +} diff --git a/src/app/(space)/(content)/[[...pathname]]/page.tsx b/src/app/(space)/(content)/[[...pathname]]/page.tsx index 48b176b02..053e57b57 100644 --- a/src/app/(space)/(content)/[[...pathname]]/page.tsx +++ b/src/app/(space)/(content)/[[...pathname]]/page.tsx @@ -10,6 +10,7 @@ import { getPagePath } from '@/lib/pages'; import { ContentRefContext } from '@/lib/references'; import { tcls } from '@/lib/tailwind'; +import { PageClientLayout } from './PageClientLayout'; import { PagePathParams, fetchPageData, getPathnameParam } from '../../fetch'; export const runtime = 'edge'; @@ -83,6 +84,9 @@ export default async function Page(props: { params: PagePathParams }) { /> ) : null} + + + ); } diff --git a/src/components/DocumentView/Block.tsx b/src/components/DocumentView/Block.tsx index 7828fdce3..0c0a8e94e 100644 --- a/src/components/DocumentView/Block.tsx +++ b/src/components/DocumentView/Block.tsx @@ -111,15 +111,16 @@ export function Block(props: BlockProps) { function BlockPlaceholder(props: { block: DocumentBlock; style: ClassValue }) { const { block, style } = props; + const id = 'meta' in block && block.meta && 'id' in block.meta ? block.meta.id : undefined; switch (block.type) { case 'heading-1': case 'heading-2': case 'heading-3': case 'file': - return ; + return ; case 'paragraph': - return ; + return ; case 'list-ordered': case 'list-unordered': case 'list-tasks': @@ -129,7 +130,7 @@ function BlockPlaceholder(props: { block: DocumentBlock; style: ClassValue }) { case 'hint': case 'tabs': case 'synced-block': - return ; + return ; case 'expandable': case 'table': case 'swagger': @@ -137,17 +138,17 @@ function BlockPlaceholder(props: { block: DocumentBlock; style: ClassValue }) { case 'divider': case 'content-ref': case 'integration': - return ; + return ; case 'embed': case 'images': case 'drawing': - return ; + return ; case 'image': case 'code-line': case 'tabs-item': throw new Error('Blocks should be directly rendered by parent'); case 'integration': - return ; + return ; default: assertNever(block); } diff --git a/src/components/hooks/index.ts b/src/components/hooks/index.ts index 29fe0b0f9..11de0ee1a 100644 --- a/src/components/hooks/index.ts +++ b/src/components/hooks/index.ts @@ -1 +1,2 @@ export * from './useScrollActiveId'; +export * from './useScrollToHash'; diff --git a/src/components/hooks/useScrollToHash.ts b/src/components/hooks/useScrollToHash.ts new file mode 100644 index 000000000..f70f69fe7 --- /dev/null +++ b/src/components/hooks/useScrollToHash.ts @@ -0,0 +1,29 @@ +import { useParams } from 'next/navigation'; +import React from 'react'; + +/** + * Scroll to the current URL hash everytime the URL changes. + */ +export function useScrollToHash() { + const params = useParams(); + + const scrollToHash = React.useCallback(() => { + const hash = window.location.hash; + if (hash) { + const element = document.getElementById(hash.slice(1)); + if (element) { + element.scrollIntoView({ + block: 'start', + behavior: 'smooth', + }); + } + } + }, []); + + // With next.js, the hashchange event is not triggered when the hash changes + // Instead a hack is to use the `useParams` hook to listen to changes in the hash + // https://github.com/vercel/next.js/discussions/49465#discussioncomment-5845312 + React.useEffect(() => { + scrollToHash(); + }, [params, scrollToHash]); +} diff --git a/src/components/primitives/Skeleton.tsx b/src/components/primitives/Skeleton.tsx index cd9de2478..555eaa5c0 100644 --- a/src/components/primitives/Skeleton.tsx +++ b/src/components/primitives/Skeleton.tsx @@ -6,10 +6,10 @@ import { LoadingPane } from './LoadingPane'; * Placeholder to be used when a content is not yet loaded (in a React.Suspense boundary). * It's used when streaming the content of a page. */ -export function SkeletonParagraph(props: { style?: ClassValue }) { - const { style } = props; +export function SkeletonParagraph(props: { id?: string; style?: ClassValue }) { + const { id, style } = props; return ( -
+
+
+
+
@@ -75,10 +80,10 @@ export function SkeletonCard(props: { style?: ClassValue }) { /** * Placeholder when loading small elements */ -export function SkeletonSmall(props: { style?: ClassValue }) { - const { style } = props; +export function SkeletonSmall(props: { id?: string; style?: ClassValue }) { + const { id, style } = props; return ( -
+
= { export function streamResponse( createGenerator: (...args: P) => AsyncGenerator, ) { - return (...args: Parameters) => { + return async (...args: Parameters) => { const generator = createGenerator(...args); return streamChunk(generator); };