mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-30 22:34:01 -05:00
Improved ActivityPub reader view (#21877)
ref https://linear.app/ghost/issue/AP-633/reader-view-customization-options, https://linear.app/ghost/issue/AP-631/estimated-reading-time-in-reader-view - Improved typography, spacing and alignment. - Improved selection of font sizes from the reader view customization popover. - Added a button to reset reader view customization settings to default, in case user ends up in a state they’re not happy with and want to go back to a sensible default. - Added preview to typeface selection component so it’s easier to see what you’re selecting. - Disabled background clicks on all AP modals for a more consistent experience and to avoid accidental clicks. - Changed the reading progress indicator increments from 1 to 5 to attract less attention while the user is reading.
This commit is contained in:
parent
c1c7828499
commit
6595697864
5 changed files with 95 additions and 48 deletions
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tryghost/admin-x-activitypub",
|
||||
"version": "0.3.37",
|
||||
"version": "0.3.38",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
@ -312,14 +312,14 @@ button.gh-form-input {
|
|||
.gh-article-title {
|
||||
font-weight: 700;
|
||||
text-wrap: pretty;
|
||||
font-size: calc(3rem * var(--content-spacing-factor, 1));
|
||||
letter-spacing: -0.021em;
|
||||
line-height: 1.4;
|
||||
font-size: calc(3.6rem * var(--content-spacing-factor, 1));
|
||||
letter-spacing: -0.025em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.gh-article-excerpt {
|
||||
margin-top: 16px;
|
||||
font-size: calc(2rem * var(--content-spacing-factor, 1));
|
||||
font-size: calc(1.8rem * var(--content-spacing-factor, 1));
|
||||
line-height: 1.4;
|
||||
letter-spacing: -0.017em;
|
||||
text-wrap: pretty;
|
||||
|
@ -344,7 +344,7 @@ headings, text, images and lists. We deal with cards lower down. */
|
|||
.gh-content {
|
||||
font-size: var(--font-size);
|
||||
overflow-x: hidden;
|
||||
letter-spacing: -0.013em;
|
||||
letter-spacing: var(--letter-spacing);
|
||||
line-height: var(--line-height);
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
@ -363,7 +363,7 @@ headings, text, images and lists. We deal with cards lower down. */
|
|||
/* Add back a top margin to all headings,
|
||||
unless a heading is the very first element in the post content */
|
||||
.gh-content > [id]:not(:first-child) {
|
||||
margin-top: calc(56px * var(--content-spacing-factor, 1));
|
||||
margin-top: calc(40px * var(--content-spacing-factor, 1));
|
||||
}
|
||||
|
||||
/* Add drop cap setting */
|
||||
|
|
|
@ -4,10 +4,11 @@ import NiceModal from '@ebay/nice-modal-react';
|
|||
import React, {useEffect, useRef, useState} from 'react';
|
||||
import articleBodyStyles from '../articleBodyStyles';
|
||||
import getUsername from '../../utils/get-username';
|
||||
import {OptionProps, SingleValueProps, components} from 'react-select';
|
||||
|
||||
import {type Activity} from '../activities/ActivityItem';
|
||||
import {ActorProperties, ObjectProperties} from '@tryghost/admin-x-framework/api/activitypub';
|
||||
import {Button, LoadingIndicator, Modal, Popover, Select, SelectOption} from '@tryghost/admin-x-design-system';
|
||||
import {Button, Icon, LoadingIndicator, Modal, Popover, Select, SelectOption} from '@tryghost/admin-x-design-system';
|
||||
import {renderTimestamp} from '../../utils/render-timestamp';
|
||||
import {useBrowseSite} from '@tryghost/admin-x-framework/api/site';
|
||||
import {useModal} from '@ebay/nice-modal-react';
|
||||
|
@ -24,7 +25,6 @@ interface ArticleModalProps {
|
|||
focusReply: boolean;
|
||||
focusReplies: boolean;
|
||||
width?: 'narrow' | 'wide';
|
||||
backDrop?: boolean;
|
||||
updateActivity: (id: string, updated: Partial<Activity>) => void;
|
||||
history: {
|
||||
activityId: string;
|
||||
|
@ -63,6 +63,7 @@ const ArticleBody: React.FC<{heading: string, image: string|undefined, excerpt:
|
|||
--font-size: ${fontSize};
|
||||
--line-height: ${lineHeight};
|
||||
--font-family: ${fontFamily.value};
|
||||
--letter-spacing: ${fontFamily.label === 'Clean sans-serif' ? '-0.013em' : '0'};
|
||||
--content-spacing-factor: ${SPACING_FACTORS[FONT_SIZES.indexOf(fontSize)]};
|
||||
}
|
||||
body {
|
||||
|
@ -184,6 +185,7 @@ const ArticleBody: React.FC<{heading: string, image: string|undefined, excerpt:
|
|||
root.style.setProperty('--font-size', fontSize);
|
||||
root.style.setProperty('--line-height', lineHeight);
|
||||
root.style.setProperty('--font-family', fontFamily.value);
|
||||
root.style.setProperty('--letter-spacing', fontFamily.label === 'Clean sans-serif' ? '-0.013em' : '0');
|
||||
root.style.setProperty('--content-spacing-factor', SPACING_FACTORS[FONT_SIZES.indexOf(fontSize)]);
|
||||
|
||||
const iframeWindow = iframe.contentWindow as IframeWindow;
|
||||
|
@ -226,28 +228,55 @@ const FeedItemDivider: React.FC = () => (
|
|||
<div className="h-px bg-grey-200"></div>
|
||||
);
|
||||
|
||||
const FONT_SIZES = ['1.4rem', '1.7rem', '1.9rem', '2.1rem', '2.4rem'] as const;
|
||||
const LINE_HEIGHTS = ['1.3', '1.4', '1.5', '1.6', '1.7', '1.8'] as const;
|
||||
const SPACING_FACTORS = ['0.7', '1', '1.1', '1.2', '1.3'] as const;
|
||||
const FONT_SIZES = ['1.5rem', '1.6rem', '1.7rem', '1.8rem', '2rem'] as const;
|
||||
const LINE_HEIGHTS = ['1.3', '1.4', '1.5', '1.6', '1.7'] as const;
|
||||
const SPACING_FACTORS = ['0.85', '1', '1.1', '1.2', '1.3'] as const;
|
||||
|
||||
type FontSize = typeof FONT_SIZES[number];
|
||||
|
||||
// Add constants for localStorage keys
|
||||
const STORAGE_KEYS = {
|
||||
FONT_SIZE: 'ghost-ap-font-size',
|
||||
LINE_HEIGHT: 'ghost-ap-line-height',
|
||||
FONT_FAMILY: 'ghost-ap-font-family'
|
||||
} as const;
|
||||
|
||||
// Add this constant near your other constants
|
||||
const MAX_WIDTHS = {
|
||||
'1.4rem': '544px',
|
||||
'1.7rem': '644px',
|
||||
'1.9rem': '684px',
|
||||
'2.1rem': '724px',
|
||||
'2.4rem': '764px'
|
||||
'1.5rem': '544px',
|
||||
'1.6rem': '644px',
|
||||
'1.7rem': '684px',
|
||||
'1.8rem': '724px',
|
||||
'2rem': '764px'
|
||||
} as const;
|
||||
|
||||
const SingleValue: React.FC<SingleValueProps<FontSelectOption, false>> = ({children, ...props}) => (
|
||||
<components.SingleValue {...props}>
|
||||
<div className='group' data-testid="select-current-option" data-value={props.data.value}>
|
||||
<div className='flex items-center gap-2.5'>
|
||||
<div className={`${props.data.className} flex h-8 w-8 items-center justify-center rounded-md bg-white text-[1.5rem] font-semibold dark:bg-black`}>Aa</div>
|
||||
<span className={`text-md ${props.data.className}`}>{children}</span>
|
||||
</div>
|
||||
</div>
|
||||
</components.SingleValue>
|
||||
);
|
||||
|
||||
const Option: React.FC<OptionProps<FontSelectOption, false>> = ({children, ...props}) => (
|
||||
<components.Option {...props}>
|
||||
<div className={props.isSelected ? 'relative flex w-full items-center justify-between gap-2' : 'group'} data-testid="select-option" data-value={props.data.value}>
|
||||
<div className='flex items-center gap-2.5'>
|
||||
<div className='flex h-8 w-8 items-center justify-center rounded-md bg-grey-150 text-[1.5rem] font-semibold group-hover:bg-grey-250 dark:bg-grey-900 dark:group-hover:bg-grey-800'>Aa</div>
|
||||
<span className={`text-md ${props.data.className}`}>{children}</span>
|
||||
</div>
|
||||
{props.isSelected && <span><Icon name='check' size='xs' /></span>}
|
||||
</div>
|
||||
</components.Option>
|
||||
);
|
||||
|
||||
interface FontSelectOption {
|
||||
value: string;
|
||||
label: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ArticleModal: React.FC<ArticleModalProps> = ({
|
||||
activityId,
|
||||
object,
|
||||
|
@ -255,7 +284,6 @@ const ArticleModal: React.FC<ArticleModalProps> = ({
|
|||
focusReply,
|
||||
focusReplies,
|
||||
width = 'narrow',
|
||||
backDrop = false,
|
||||
updateActivity = () => {},
|
||||
history = []
|
||||
}) => {
|
||||
|
@ -367,7 +395,7 @@ const ArticleModal: React.FC<ArticleModalProps> = ({
|
|||
|
||||
const [currentLineHeightIndex, setCurrentLineHeightIndex] = useState(() => {
|
||||
const saved = localStorage.getItem(STORAGE_KEYS.LINE_HEIGHT);
|
||||
return saved ? parseInt(saved) : 3;
|
||||
return saved ? parseInt(saved) : 1;
|
||||
});
|
||||
|
||||
const [fontFamily, setFontFamily] = useState<SelectOption>(() => {
|
||||
|
@ -415,6 +443,7 @@ const ArticleModal: React.FC<ArticleModalProps> = ({
|
|||
iframeDocument.documentElement.style.setProperty('--font-size', FONT_SIZES[currentFontSizeIndex]);
|
||||
iframeDocument.documentElement.style.setProperty('--line-height', LINE_HEIGHTS[currentLineHeightIndex]);
|
||||
iframeDocument.documentElement.style.setProperty('--font-family', fontFamily.value);
|
||||
iframeDocument.documentElement.style.setProperty('--letter-spacing', fontFamily.label === 'Clean sans-serif' ? '-0.013em' : '0');
|
||||
iframeDocument.documentElement.style.setProperty('--content-spacing-factor', SPACING_FACTORS[FONT_SIZES.indexOf(FONT_SIZES[currentFontSizeIndex])]);
|
||||
}
|
||||
}
|
||||
|
@ -427,7 +456,6 @@ const ArticleModal: React.FC<ArticleModalProps> = ({
|
|||
|
||||
const [readingProgress, setReadingProgress] = useState(0);
|
||||
|
||||
// Add the scroll handler
|
||||
useEffect(() => {
|
||||
const container = document.querySelector('.overflow-y-auto');
|
||||
const article = document.getElementById('object-content');
|
||||
|
@ -439,14 +467,11 @@ const ArticleModal: React.FC<ArticleModalProps> = ({
|
|||
|
||||
const articleRect = article.getBoundingClientRect();
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const totalHeight = article.offsetHeight;
|
||||
|
||||
// Calculate how much of the article has been scrolled past the viewport
|
||||
const scrolledPast = Math.max(0, containerRect.top - articleRect.top);
|
||||
const totalHeight = (article as HTMLElement).offsetHeight - (container as HTMLElement).offsetHeight;
|
||||
|
||||
// Only add the visible portion if we've started scrolling
|
||||
const visibleHeight = scrolledPast > 0 ? Math.min(containerRect.height, articleRect.height) : 0;
|
||||
const progress = Math.round(Math.min(Math.max(((scrolledPast + visibleHeight) / totalHeight) * 100, 0), 100));
|
||||
const rawProgress = Math.min(Math.max((scrolledPast / totalHeight) * 100, 0), 100);
|
||||
const progress = Math.round(rawProgress / 5) * 5;
|
||||
|
||||
setReadingProgress(progress);
|
||||
};
|
||||
|
@ -458,9 +483,9 @@ const ArticleModal: React.FC<ArticleModalProps> = ({
|
|||
return (
|
||||
<Modal
|
||||
align='right'
|
||||
allowBackgroundInteraction={true}
|
||||
allowBackgroundInteraction={false}
|
||||
animate={true}
|
||||
backDrop={backDrop}
|
||||
backDrop={false}
|
||||
backDropClick={true}
|
||||
footer={<></>}
|
||||
height={'full'}
|
||||
|
@ -470,9 +495,9 @@ const ArticleModal: React.FC<ArticleModalProps> = ({
|
|||
width={modalSize === MODAL_SIZE_LG ? 'toSidebar' : modalSize}
|
||||
>
|
||||
<div className='flex h-full flex-col'>
|
||||
<div className='sticky top-0 z-50 border-b border-grey-200 bg-white py-8'>
|
||||
<div className='sticky top-0 z-50 flex h-[97px] items-center justify-center border-b border-grey-200 bg-white'>
|
||||
<div
|
||||
className={`flex h-8 ${modalSize === MODAL_SIZE_LG ? 'grid px-8' : 'justify-between gap-2 px-8'}`}
|
||||
className={`w-full ${modalSize === MODAL_SIZE_LG ? 'grid px-8' : 'flex justify-between gap-2 px-8'}`}
|
||||
style={modalSize === MODAL_SIZE_LG ? {
|
||||
gridTemplateColumns: `1fr minmax(0,${currentGridWidth}) 1fr`
|
||||
} : undefined}
|
||||
|
@ -495,26 +520,37 @@ const ArticleModal: React.FC<ArticleModalProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
</div>)}
|
||||
<div className='col-[3/4] flex items-center justify-end space-x-6'>
|
||||
<div className='col-[3/4] flex items-center justify-end gap-2'>
|
||||
{modalSize === MODAL_SIZE_LG && object.type === 'Article' && <Popover position='end' trigger={ <Button className='transition-color flex h-10 w-10 items-center justify-center rounded-full bg-white hover:bg-grey-100' icon='typography' size='sm' unstyled onClick={() => {}}/>
|
||||
}>
|
||||
<div className='flex min-w-[240px] flex-col p-5'>
|
||||
<div className='flex min-w-[300px] flex-col p-5'>
|
||||
<Select
|
||||
className='mb-3'
|
||||
components={{Option, SingleValue}}
|
||||
controlClasses={{control: '!min-h-[40px] !py-0 !pl-1', option: '!pl-1 !py-[4px]'}}
|
||||
options={[
|
||||
{value: 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif', label: 'Clean sans-serif'},
|
||||
{value: 'Georgia, Times, serif', label: 'Elegant serif'}
|
||||
{
|
||||
value: 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
|
||||
label: 'Clean sans-serif',
|
||||
className: 'font-sans'
|
||||
},
|
||||
{
|
||||
value: 'Georgia, Times, serif',
|
||||
label: 'Elegant serif',
|
||||
className: 'font-serif'
|
||||
}
|
||||
]}
|
||||
title='Typeface'
|
||||
value={fontFamily}
|
||||
onSelect={option => setFontFamily(option || {
|
||||
value: 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
|
||||
label: 'Clean sans-serif'
|
||||
label: 'Clean sans-serif',
|
||||
className: 'font-sans'
|
||||
})}
|
||||
/>
|
||||
<div className='mb-2 flex items-center justify-between'>
|
||||
<span className='text-sm font-medium'>Font size</span>
|
||||
<div className='flex items-center gap-2'>
|
||||
<span className='text-sm font-medium text-grey-900'>Font size</span>
|
||||
<div className='flex items-center'>
|
||||
<Button
|
||||
className={`transition-color flex h-8 w-8 items-center justify-center rounded-full bg-white ${currentFontSizeIndex === 0 ? 'opacity-20 hover:bg-white' : 'hover:bg-grey-100'}`}
|
||||
disabled={currentFontSizeIndex === 0}
|
||||
|
@ -525,7 +561,6 @@ const ArticleModal: React.FC<ArticleModalProps> = ({
|
|||
unstyled={true}
|
||||
onClick={decreaseFontSize}
|
||||
/>
|
||||
{/* <span className='text-grey-700'>{FONT_SIZES[currentFontSizeIndex]}</span> */}
|
||||
<Button
|
||||
className={`transition-color flex h-8 w-8 items-center justify-center rounded-full bg-white hover:bg-grey-100 ${currentFontSizeIndex === FONT_SIZES.length - 1 ? 'opacity-20 hover:bg-white' : 'hover:bg-grey-100'}`}
|
||||
disabled={currentFontSizeIndex === FONT_SIZES.length - 1}
|
||||
|
@ -538,9 +573,9 @@ const ArticleModal: React.FC<ArticleModalProps> = ({
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center justify-between'>
|
||||
<span className='text-sm font-medium'>Line spacing</span>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='mb-5 flex items-center justify-between'>
|
||||
<span className='text-sm font-medium text-grey-900'>Line spacing</span>
|
||||
<div className='flex items-center'>
|
||||
<Button
|
||||
className={`transition-color flex h-8 w-8 items-center justify-center rounded-full bg-white hover:bg-grey-100 ${currentLineHeightIndex === 0 ? 'opacity-20 hover:bg-white' : 'hover:bg-grey-100'}`}
|
||||
disabled={currentLineHeightIndex === 0}
|
||||
|
@ -551,7 +586,6 @@ const ArticleModal: React.FC<ArticleModalProps> = ({
|
|||
unstyled={true}
|
||||
onClick={decreaseLineHeight}
|
||||
/>
|
||||
{/* <span className='text-grey-700'>{LINE_HEIGHTS[currentLineHeightIndex]}</span> */}
|
||||
<Button
|
||||
className={`transition-color flex h-8 w-8 items-center justify-center rounded-full bg-white hover:bg-grey-100 ${currentLineHeightIndex === LINE_HEIGHTS.length - 1 ? 'opacity-20 hover:bg-white' : 'hover:bg-grey-100'}`}
|
||||
disabled={currentLineHeightIndex === LINE_HEIGHTS.length - 1}
|
||||
|
@ -564,6 +598,19 @@ const ArticleModal: React.FC<ArticleModalProps> = ({
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
className="text-sm text-grey-600 hover:text-grey-700"
|
||||
label="Reset to default"
|
||||
link={true}
|
||||
onClick={() => {
|
||||
setCurrentFontSizeIndex(1); // Default font size
|
||||
setCurrentLineHeightIndex(1); // Default line height
|
||||
setFontFamily({
|
||||
value: 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
|
||||
label: 'Clean sans-serif'
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Popover>}
|
||||
<Button className='transition-color flex h-10 w-10 items-center justify-center rounded-full bg-white hover:bg-grey-100' icon='close' size='sm' unstyled onClick={() => modal.remove()}/>
|
||||
|
@ -679,11 +726,11 @@ const ArticleModal: React.FC<ArticleModalProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
{modalSize === MODAL_SIZE_LG && object.type === 'Article' && (
|
||||
<div className='pointer-events-none sticky bottom-0 flex items-end justify-between p-8'>
|
||||
<div className='pointer-events-none sticky bottom-0 flex items-end justify-between px-10 pb-[42px]'>
|
||||
<div className='pointer-events-auto text-grey-600'>
|
||||
{getReadingTime(object.content)}
|
||||
</div>
|
||||
<div className='pointer-events-auto text-grey-600'>
|
||||
<div className='pointer-events-auto text-grey-600 transition-all duration-200 ease-out'>
|
||||
{readingProgress}%
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -293,6 +293,7 @@ const ViewProfileModal: React.FC<ViewProfileModalProps> = ({
|
|||
<Modal
|
||||
align='right'
|
||||
animate={true}
|
||||
backDrop={false}
|
||||
footer={<></>}
|
||||
height={'full'}
|
||||
padding={false}
|
||||
|
@ -302,7 +303,7 @@ const ViewProfileModal: React.FC<ViewProfileModalProps> = ({
|
|||
<div className='sticky top-0 z-50 border-grey-200 bg-white py-3'>
|
||||
<div className='grid h-8 grid-cols-3'>
|
||||
<div className='col-[3/4] flex items-center justify-end space-x-6 px-8'>
|
||||
<Button icon='close' size='sm' unstyled onClick={() => modal.remove()}/>
|
||||
<Button className='transition-color flex h-10 w-10 items-center justify-center rounded-full bg-white hover:bg-grey-100' icon='close' size='sm' unstyled onClick={() => modal.remove()}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -13,7 +13,6 @@ export const handleViewContent = (
|
|||
object: activity.object,
|
||||
actor: authorActor,
|
||||
focusReply,
|
||||
backDrop: activity.object.type === 'Article' ? false : false,
|
||||
width: activity.object.type === 'Article' ? 'wide' : 'narrow',
|
||||
updateActivity
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue