0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-27 22:49:56 -05:00

Extracted TableOfContents into a separate component (#22019)

ref https://linear.app/ghost/issue/AP-634/table-of-contents-in-reader-view

- `TableOfContents` is now a separate component to keep `ArticleModal` simpler
- Switched to using constants for styling different heading levels for better performance and maintainability
This commit is contained in:
Djordje Vlaisavljevic 2025-01-16 14:32:16 +00:00 committed by GitHub
parent 7bc1102cc6
commit 0f1d6167cf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 88 additions and 75 deletions

View file

@ -1,6 +1,6 @@
{ {
"name": "@tryghost/admin-x-activitypub", "name": "@tryghost/admin-x-activitypub",
"version": "0.3.47", "version": "0.3.48",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",

View file

@ -16,6 +16,7 @@ import {useThreadForUser} from '../../hooks/useActivityPubQueries';
import APAvatar from '../global/APAvatar'; import APAvatar from '../global/APAvatar';
import APReplyBox from '../global/APReplyBox'; import APReplyBox from '../global/APReplyBox';
import TableOfContents, {TOCItem} from './TableOfContents';
import getReadingTime from '../../utils/get-reading-time'; import getReadingTime from '../../utils/get-reading-time';
interface ArticleModalProps { interface ArticleModalProps {
@ -37,79 +38,6 @@ interface IframeWindow extends Window {
resizeIframe?: () => void; resizeIframe?: () => void;
} }
interface TOCItem {
id: string;
text: string;
level: number;
element?: HTMLElement;
}
const TableOfContents: React.FC<{
items: TOCItem[];
activeId: string | null;
onItemClick: (id: string) => void;
}> = ({items, onItemClick}) => {
if (items.length === 0) {
return null;
}
const getLineWidth = (level: number) => {
switch (level) {
case 1:
return 'w-3';
case 2:
return 'w-2';
default:
return 'w-1';
}
};
const getHeadingPadding = (level: number) => {
switch (level) {
case 1:
return 'pl-2';
case 2:
return 'pl-6';
default:
return 'pl-10';
}
};
return (
<div className="absolute right-2 top-1/2 -translate-y-1/2 text-base">
<Popover
position='center'
side='right'
trigger={
<div className="flex cursor-pointer flex-col items-end gap-2 rounded-md bg-white p-2 hover:bg-grey-75">
{items.map(item => (
<div
key={item.id}
className={`h-[2px] rounded-sm bg-grey-400 pr-1 transition-all ${getLineWidth(item.level)}`}
/>
))}
</div>
}
>
<div className="w-[220px] p-4">
<nav className="max-h-[60vh] overflow-y-auto">
{items.map(item => (
<button
key={item.id}
className={`block w-full cursor-pointer truncate rounded py-1 text-left text-grey-700 hover:bg-grey-75 hover:text-grey-900 ${getHeadingPadding(item.level)}`}
type='button'
onClick={() => onItemClick(item.id)}
>
{item.text}
</button>
))}
</nav>
</div>
</Popover>
</div>
);
};
const ArticleBody: React.FC<{ const ArticleBody: React.FC<{
heading: string; heading: string;
image: string|undefined; image: string|undefined;
@ -859,7 +787,6 @@ const ArticleModal: React.FC<ArticleModalProps> = ({
<div className="!visible absolute inset-y-0 right-7 z-40 hidden lg:!block"> <div className="!visible absolute inset-y-0 right-7 z-40 hidden lg:!block">
<div className="sticky top-1/2 -translate-y-1/2"> <div className="sticky top-1/2 -translate-y-1/2">
<TableOfContents <TableOfContents
activeId={activeHeadingId}
items={tocItems} items={tocItems}
onItemClick={scrollToHeading} onItemClick={scrollToHeading}
/> />

View file

@ -0,0 +1,86 @@
import React from 'react';
import {Popover} from '@tryghost/admin-x-design-system';
export interface TOCItem {
id: string;
text: string;
level: number;
element?: HTMLElement;
}
interface TableOfContentsProps {
items: TOCItem[];
onItemClick: (id: string) => void;
}
const LINE_WIDTHS = {
1: 'w-3',
2: 'w-2',
3: 'w-1'
} as const;
const HEADING_PADDINGS = {
1: 'pl-2',
2: 'pl-6',
3: 'pl-10'
} as const;
const TableOfContents: React.FC<TableOfContentsProps> = ({items, onItemClick}) => {
if (items.length === 0) {
return null;
}
const getNormalizedLevel = (level: number) => {
return Math.min(level, 3);
};
const getLineWidth = (level: number) => {
return LINE_WIDTHS[getNormalizedLevel(level) as keyof typeof LINE_WIDTHS];
};
const getHeadingPadding = (level: number) => {
return HEADING_PADDINGS[getNormalizedLevel(level) as keyof typeof HEADING_PADDINGS];
};
return (
<div className='absolute right-2 top-1/2 -translate-y-1/2 text-base'>
<Popover
aria-label='Table of Contents'
position='center'
side='right'
trigger={
<div className='flex cursor-pointer flex-col items-end gap-2 rounded-md bg-white p-2 hover:bg-grey-75'>
{items.map(item => (
<div
key={item.id}
className={`h-[2px] rounded-sm bg-grey-400 pr-1 transition-all ${getLineWidth(item.level)}`}
/>
))}
</div>
}
>
<div className='w-[220px] p-4'>
<nav
aria-label='Table of contents navigation'
className='max-h-[60vh] overflow-y-auto'
role='navigation'
>
{items.map(item => (
<button
key={item.id}
className={`block w-full cursor-pointer truncate rounded py-1 text-left text-grey-700 hover:bg-grey-75 hover:text-grey-900 ${getHeadingPadding(item.level)}`}
title={item.text}
type='button'
onClick={() => onItemClick(item.id)}
>
{item.text}
</button>
))}
</nav>
</div>
</Popover>
</div>
);
};
export default TableOfContents;