mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-20 17:43:24 +00:00
Improve tabs link support (#3211)
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
import { type ClassValue, tcls } from '@/lib/tailwind';
|
||||
import type { DocumentBlockHeading, DocumentBlockTabs } from '@gitbook/api';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import { getBlockTextStyle } from './spacing';
|
||||
|
||||
/**
|
||||
* A hash icon which adds the block or active block item's ID in the URL hash.
|
||||
* The button needs to be wrapped in a container with `hashLinkButtonWrapperStyles`.
|
||||
*/
|
||||
export const hashLinkButtonWrapperStyles = tcls('relative', 'group/hash');
|
||||
|
||||
export function HashLinkButton(props: {
|
||||
id: string;
|
||||
block: DocumentBlockTabs | DocumentBlockHeading;
|
||||
label?: string;
|
||||
className?: ClassValue;
|
||||
iconClassName?: ClassValue;
|
||||
}) {
|
||||
const { id, block, className, iconClassName, label = 'Direct link to block' } = props;
|
||||
const textStyle = getBlockTextStyle(block);
|
||||
return (
|
||||
<div
|
||||
className={tcls(
|
||||
'relative',
|
||||
'hash',
|
||||
'grid',
|
||||
'grid-area-1-1',
|
||||
'h-[1em]',
|
||||
'border-0',
|
||||
'opacity-0',
|
||||
'group-hover/hash:opacity-[0]',
|
||||
'group-focus/hash:opacity-[0]',
|
||||
'md:group-hover/hash:md:opacity-[1]',
|
||||
'md:group-focus/hash:md:opacity-[1]',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<a
|
||||
href={`#${id}`}
|
||||
aria-label={label}
|
||||
className={tcls('inline-flex', 'h-full', 'items-start', textStyle.lineHeight)}
|
||||
>
|
||||
<Icon
|
||||
icon="hashtag"
|
||||
className={tcls(
|
||||
'size-3',
|
||||
'self-center',
|
||||
'transition-colors',
|
||||
'text-transparent',
|
||||
'group-hover/hash:text-tint-subtle',
|
||||
'contrast-more:group-hover/hash:text-tint-strong',
|
||||
iconClassName
|
||||
)}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { DocumentBlockHeading } from '@gitbook/api';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import type { BlockProps } from './Block';
|
||||
import { HashLinkButton, hashLinkButtonWrapperStyles } from './HashLinkButton';
|
||||
import { Inlines } from './Inlines';
|
||||
import { getBlockTextStyle } from './spacing';
|
||||
|
||||
@@ -23,50 +23,20 @@ export function Heading(props: BlockProps<DocumentBlockHeading>) {
|
||||
className={tcls(
|
||||
textStyle.textSize,
|
||||
'heading',
|
||||
'group',
|
||||
'relative',
|
||||
'grid',
|
||||
'scroll-m-12',
|
||||
hashLinkButtonWrapperStyles,
|
||||
style
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={tcls(
|
||||
'hash',
|
||||
'grid',
|
||||
'grid-area-1-1',
|
||||
'relative',
|
||||
'-ml-6',
|
||||
'w-7',
|
||||
'border-0',
|
||||
'opacity-0',
|
||||
'group-hover:opacity-[0]',
|
||||
'group-focus:opacity-[0]',
|
||||
'md:group-hover:md:opacity-[1]',
|
||||
'md:group-focus:md:opacity-[1]',
|
||||
textStyle.marginTop
|
||||
)}
|
||||
>
|
||||
<a
|
||||
href={`#${id}`}
|
||||
aria-label="Direct link to heading"
|
||||
className={tcls('inline-flex', 'h-full', 'items-start', textStyle.lineHeight)}
|
||||
>
|
||||
<Icon
|
||||
icon="hashtag"
|
||||
className={tcls(
|
||||
'w-3.5',
|
||||
'h-[1em]',
|
||||
'mt-0.5',
|
||||
'transition-colors',
|
||||
'text-transparent',
|
||||
'group-hover:text-tint-subtle',
|
||||
'contrast-more:group-hover:text-tint-strong',
|
||||
'lg:w-4'
|
||||
)}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<HashLinkButton
|
||||
id={id}
|
||||
block={block}
|
||||
className={tcls('-ml-6', textStyle.anchorButtonMarginTop)}
|
||||
iconClassName={tcls('size-4')}
|
||||
label="Direct link to heading"
|
||||
/>
|
||||
|
||||
<div
|
||||
className={tcls(
|
||||
'grid-area-1-1',
|
||||
|
||||
@@ -5,6 +5,8 @@ import React, { useCallback, useMemo } from 'react';
|
||||
import { useHash, useIsMounted } from '@/components/hooks';
|
||||
import * as storage from '@/lib/local-storage';
|
||||
import { type ClassValue, tcls } from '@/lib/tailwind';
|
||||
import type { DocumentBlockTabs } from '@gitbook/api';
|
||||
import { HashLinkButton, hashLinkButtonWrapperStyles } from '../HashLinkButton';
|
||||
|
||||
interface TabsState {
|
||||
activeIds: {
|
||||
@@ -68,9 +70,10 @@ export function DynamicTabs(
|
||||
props: TabsInput & {
|
||||
tabsBody: React.ReactNode[];
|
||||
style: ClassValue;
|
||||
block: DocumentBlockTabs;
|
||||
}
|
||||
) {
|
||||
const { id, tabs, tabsBody, style } = props;
|
||||
const { id, block, tabs, tabsBody, style } = props;
|
||||
|
||||
const hash = useHash();
|
||||
const [tabsState, setTabsState] = useTabsState();
|
||||
@@ -146,8 +149,8 @@ export function DynamicTabs(
|
||||
'ring-inset',
|
||||
'ring-tint-subtle',
|
||||
'flex',
|
||||
'overflow-hidden',
|
||||
'flex-col',
|
||||
'overflow-hidden',
|
||||
style
|
||||
)}
|
||||
>
|
||||
@@ -165,16 +168,14 @@ export function DynamicTabs(
|
||||
)}
|
||||
>
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
<div
|
||||
key={tab.id}
|
||||
role="tab"
|
||||
aria-selected={active.id === tab.id}
|
||||
aria-controls={getTabPanelId(tab.id)}
|
||||
id={getTabButtonId(tab.id)}
|
||||
onClick={() => {
|
||||
onSelectTab(tab);
|
||||
}}
|
||||
className={tcls(
|
||||
hashLinkButtonWrapperStyles,
|
||||
'flex',
|
||||
'items-center',
|
||||
'gap-3.5',
|
||||
|
||||
//prev from active-tab
|
||||
'[&:has(+_.active-tab)]:rounded-br-md',
|
||||
|
||||
@@ -184,14 +185,6 @@ export function DynamicTabs(
|
||||
//next from active-tab
|
||||
'[.active-tab_+_:after]:rounded-br-md',
|
||||
|
||||
'inline-block',
|
||||
'text-sm',
|
||||
'px-3.5',
|
||||
'py-2',
|
||||
'transition-[color]',
|
||||
'font-[500]',
|
||||
'relative',
|
||||
|
||||
'after:transition-colors',
|
||||
'after:border-r',
|
||||
'after:absolute',
|
||||
@@ -202,14 +195,16 @@ export function DynamicTabs(
|
||||
'after:h-[70%]',
|
||||
'after:w-[1px]',
|
||||
|
||||
'px-3.5',
|
||||
'py-2',
|
||||
|
||||
'last:after:border-transparent',
|
||||
|
||||
'text-tint',
|
||||
'bg-tint-12/1',
|
||||
'hover:text-tint-strong',
|
||||
|
||||
'truncate',
|
||||
'max-w-full',
|
||||
'truncate',
|
||||
|
||||
active.id === tab.id
|
||||
? [
|
||||
@@ -224,8 +219,34 @@ export function DynamicTabs(
|
||||
: null
|
||||
)}
|
||||
>
|
||||
{tab.title}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={active.id === tab.id}
|
||||
aria-controls={getTabPanelId(tab.id)}
|
||||
id={getTabButtonId(tab.id)}
|
||||
onClick={() => {
|
||||
onSelectTab(tab);
|
||||
}}
|
||||
className={tcls(
|
||||
'inline-block',
|
||||
'text-sm',
|
||||
'transition-[color]',
|
||||
'font-[500]',
|
||||
'relative',
|
||||
'max-w-full',
|
||||
'truncate'
|
||||
)}
|
||||
>
|
||||
{tab.title}
|
||||
</button>
|
||||
|
||||
<HashLinkButton
|
||||
id={getTabButtonId(tab.id)}
|
||||
block={block}
|
||||
label="Direct link to tab"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{tabs.map((tab, index) => (
|
||||
|
||||
@@ -39,6 +39,7 @@ export function Tabs(props: BlockProps<DocumentBlockTabs>) {
|
||||
<DynamicTabs
|
||||
key={tab.id}
|
||||
id={block.key!}
|
||||
block={block}
|
||||
tabs={[tab]}
|
||||
tabsBody={[tabsBody[index]]}
|
||||
style={style}
|
||||
@@ -48,5 +49,7 @@ export function Tabs(props: BlockProps<DocumentBlockTabs>) {
|
||||
);
|
||||
}
|
||||
|
||||
return <DynamicTabs id={block.key!} tabs={tabs} tabsBody={tabsBody} style={style} />;
|
||||
return (
|
||||
<DynamicTabs id={block.key!} block={block} tabs={tabs} tabsBody={tabsBody} style={style} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ export function getBlockTextStyle(block: DocumentBlock): {
|
||||
lineHeight: string;
|
||||
/** Tailwind class for the margin top (mt-*) */
|
||||
marginTop?: string;
|
||||
/** Tailwind class for the margin top to apply on the anchor link button */
|
||||
anchorButtonMarginTop?: string;
|
||||
} {
|
||||
switch (block.type) {
|
||||
case 'paragraph':
|
||||
@@ -22,18 +24,21 @@ export function getBlockTextStyle(block: DocumentBlock): {
|
||||
textSize: 'text-3xl font-semibold',
|
||||
lineHeight: 'leading-tight',
|
||||
marginTop: 'mt-[1em]',
|
||||
anchorButtonMarginTop: 'mt-[1.05em]',
|
||||
};
|
||||
case 'heading-2':
|
||||
return {
|
||||
textSize: 'text-2xl font-semibold',
|
||||
lineHeight: 'leading-snug',
|
||||
marginTop: 'mt-[0.75em]',
|
||||
anchorButtonMarginTop: 'mt-[0.9em]',
|
||||
};
|
||||
case 'heading-3':
|
||||
return {
|
||||
textSize: 'text-xl font-semibold',
|
||||
lineHeight: 'leading-snug',
|
||||
marginTop: 'mt-[0.5em]',
|
||||
anchorButtonMarginTop: 'mt-[0.65em]',
|
||||
};
|
||||
case 'divider':
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user