From 5fee3cb9653054a00a1f05f62cd68f4455990890 Mon Sep 17 00:00:00 2001 From: jackiequach Date: Wed, 28 Jan 2026 17:27:19 -0500 Subject: [PATCH 01/16] initial design changes --- README.md | 7 +- example/index.tsx | 2 +- example/use-html-reader.tsx | 8 +- example/use-pdf-reader.tsx | 10 +- src/HtmlReader/index.tsx | 12 ++ src/HtmlReader/reducer.ts | 27 +++ src/HtmlReader/types.ts | 1 + src/PdfReader/index.tsx | 7 + src/PdfReader/reducer.ts | 6 + src/PdfReader/types.ts | 1 + src/index.tsx | 12 +- src/types.ts | 7 +- src/ui/Button.tsx | 27 ++- src/ui/ErrorBoundary.tsx | 6 +- src/ui/Footer.tsx | 59 ------ src/ui/Header.tsx | 182 ++++++++++++------- src/ui/HtmlFontSizeControls.tsx | 32 ++++ src/ui/LoadingSkeleton.tsx | 2 - src/ui/PdfZoomControls.tsx | 28 +++ src/ui/SettingsButton.tsx | 2 +- src/ui/TableOfContent.tsx | 7 +- src/ui/icons/FitHeightWidth.tsx | 28 +++ src/ui/icons/PageDown.tsx | 16 ++ src/ui/icons/PageUp.tsx | 16 ++ src/ui/icons/ReaderSettings.tsx | 50 ++--- src/ui/icons/Reset.tsx | 23 +-- src/ui/icons/Search.tsx | 16 ++ src/ui/icons/TableOfContents.tsx | 30 +-- src/ui/icons/ToggleFullScreen.tsx | 30 +-- src/ui/icons/ToggleFullScreenExit.tsx | 30 +-- src/ui/icons/ZoomIn.tsx | 16 ++ src/ui/icons/ZoomOut.tsx | 16 ++ src/ui/icons/index.tsx | 12 ++ src/ui/manager.tsx | 21 +-- src/ui/nypl-base-theme/foundations/colors.ts | 3 + tests/Header.test.tsx | 31 ---- 36 files changed, 467 insertions(+), 316 deletions(-) delete mode 100644 src/ui/Footer.tsx create mode 100644 src/ui/HtmlFontSizeControls.tsx create mode 100644 src/ui/PdfZoomControls.tsx create mode 100644 src/ui/icons/FitHeightWidth.tsx create mode 100644 src/ui/icons/PageDown.tsx create mode 100644 src/ui/icons/PageUp.tsx create mode 100644 src/ui/icons/Search.tsx create mode 100644 src/ui/icons/ZoomIn.tsx create mode 100644 src/ui/icons/ZoomOut.tsx diff --git a/README.md b/README.md index 873bab9e..a4e2c7e4 100644 --- a/README.md +++ b/README.md @@ -41,12 +41,7 @@ Basic usage within a React app, using the default UI: import WebReader from '@nypl/web-reader'; const ReaderPage = ({ manifestUrl }) => { - return ( - Back to app} - /> - ); + return ; }; ``` diff --git a/example/index.tsx b/example/index.tsx index e852b32f..f55a25ee 100644 --- a/example/index.tsx +++ b/example/index.tsx @@ -137,7 +137,7 @@ const PdfReaders = () => { means it will not grow to fit content in scrolling mode. = ({ return (
-
Back} - containerRef={containerRef} - /> +
{reader.content} -
); }; diff --git a/example/use-pdf-reader.tsx b/example/use-pdf-reader.tsx index 72cd4c58..febb4e56 100644 --- a/example/use-pdf-reader.tsx +++ b/example/use-pdf-reader.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { usePdfReader } from '../src'; import { WebpubManifest } from '../src/types'; -import Footer from '../src/ui/Footer'; import Header from '../src/ui/Header'; type PDFReaderProps = { @@ -34,16 +33,11 @@ const UsePdfReader: React.FC = ({ return null; } - const { content, ...readerProps } = reader; + const { content } = reader; return (
-
Back} - containerRef={containerRef} - /> +
{content} -
); }; diff --git a/src/HtmlReader/index.tsx b/src/HtmlReader/index.tsx index d37b99ca..f63eb0db 100644 --- a/src/HtmlReader/index.tsx +++ b/src/HtmlReader/index.tsx @@ -164,6 +164,9 @@ export default function useHtmlReader(args: HtmlReaderArguments): ReaderReturn { goToPage(href) { dispatch({ type: 'GO_TO_HREF', href }); }, + goToPageNumber(page) { + dispatch({ type: 'GO_TO_PAGE', page }); + }, async goForward() { dispatch({ type: 'GO_FORWARD' }); }, @@ -241,6 +244,13 @@ export default function useHtmlReader(args: HtmlReaderArguments): ReaderReturn { const atStart = isFirstResource && isStartOfResource; const atEnd = isLastResource && isEndOfResource; + const totalPages = + state.iframe && state.settings + ? calcPosition(state.iframe, state.settings.isScrolling).totalPages + : 0; + + const currentPage = state.location?.locations?.position ?? 1; + // the reader is active return { type: 'HTML', @@ -280,5 +290,7 @@ export default function useHtmlReader(args: HtmlReaderArguments): ReaderReturn { }, manifest, navigator, + currentPage, + totalPages, }; } diff --git a/src/HtmlReader/reducer.ts b/src/HtmlReader/reducer.ts index 520ef329..56f61797 100644 --- a/src/HtmlReader/reducer.ts +++ b/src/HtmlReader/reducer.ts @@ -251,6 +251,33 @@ export default function makeHtmlReducer( return newState; } + case 'GO_TO_PAGE': { + if (state.state !== 'READY') { + return handleInvalidTransition(state, action); + } + const { totalPages } = calcPosition( + state.iframe, + state.settings.isScrolling + ); + const page = Math.max(1, Math.min(action.page, totalPages)); + const progression = (page - 1) / totalPages; + + const newState: NavigatingState = { + ...state, + state: 'NAVIGATING', + location: { + ...state.location, + locations: { + ...state.location.locations, + progression, + position: page, + remainingPositions: totalPages - page, + }, + }, + }; + return newState; + } + case 'GO_TO_LOCATION': { if (state.state !== 'READY') { return handleInvalidTransition(state, action); diff --git a/src/HtmlReader/types.ts b/src/HtmlReader/types.ts index c239c593..6a27a7dd 100644 --- a/src/HtmlReader/types.ts +++ b/src/HtmlReader/types.ts @@ -97,6 +97,7 @@ export type HtmlAction = | { type: 'NAV_PREVIOUS_RESOURCE' } | { type: 'NAV_NEXT_RESOURCE' } | { type: 'GO_TO_HREF'; href: string } + | { type: 'GO_TO_PAGE'; page: number } | { type: 'GO_TO_LOCATION'; location: Locator } | { type: 'GO_FORWARD' } | { type: 'GO_BACKWARD' } diff --git a/src/PdfReader/index.tsx b/src/PdfReader/index.tsx index 3c6497d6..671601b6 100644 --- a/src/PdfReader/index.tsx +++ b/src/PdfReader/index.tsx @@ -238,6 +238,10 @@ export default function usePdfReader(args: PdfReaderArguments): ReaderReturn { dispatch({ type: 'GO_TO_HREF', href }); }, []); + const goToPageNumber = React.useCallback((page: number) => { + dispatch({ type: 'GO_TO_PAGE', page: page }); + }, []); + // this format is inactive, return null if (!webpubManifestUrl || !manifest) return null; @@ -392,6 +396,9 @@ export default function usePdfReader(args: PdfReaderArguments): ReaderReturn { zoomIn, zoomOut, goToPage, + goToPageNumber, }, + currentPage: state.pageNumber, + totalPages: state.numPages ?? 0, }; } diff --git a/src/PdfReader/reducer.ts b/src/PdfReader/reducer.ts index 9e11d786..4f956fac 100644 --- a/src/PdfReader/reducer.ts +++ b/src/PdfReader/reducer.ts @@ -152,6 +152,12 @@ export function makePdfReducer( return goToLocation(resourceIndex, page); } + case 'GO_TO_PAGE': { + const numPages = state.numPages || 1; + const page = Math.max(1, Math.min(action.page, numPages)); + return goToLocation(state.resourceIndex, page); + } + case 'RESOURCE_FETCH_SUCCESS': return { ...state, diff --git a/src/PdfReader/types.ts b/src/PdfReader/types.ts index 33dc0cb6..b0b8c391 100644 --- a/src/PdfReader/types.ts +++ b/src/PdfReader/types.ts @@ -48,6 +48,7 @@ export type PdfReaderAction = | { type: 'GO_FORWARD' } | { type: 'GO_BACKWARD' } | { type: 'GO_TO_HREF'; href: string } + | { type: 'GO_TO_PAGE'; page: number } | { type: 'RESOURCE_FETCH_SUCCESS'; resource: { data: Uint8Array } } | { type: 'PDF_PARSED'; numPages: number } | { type: 'PDF_LOAD_ERROR'; error: Error } diff --git a/src/index.tsx b/src/index.tsx index 02bae619..fb078bb8 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,5 +1,5 @@ import React, { FC } from 'react'; -import { ReaderManagerArguments, UseWebReaderArguments } from './types'; +import { UseWebReaderArguments } from './types'; import ErrorBoundary from './ui/ErrorBoundary'; import ManagerUI from './ui/manager'; import useWebReader from './useWebReader'; @@ -8,14 +8,12 @@ import useWebReader from './useWebReader'; * The main React component export. */ -export type WebReaderProps = UseWebReaderArguments & - ReaderManagerArguments; +export type WebReaderProps = UseWebReaderArguments; export const WebReaderWithoutBoundary: FC = ({ webpubManifestUrl, proxyUrl, getContent, - headerLeft, ...props }) => { const webReader = useWebReader({ @@ -26,11 +24,7 @@ export const WebReaderWithoutBoundary: FC = ({ }); const { content } = webReader; - return ( - - {content} - - ); + return {content}; }; const WebReader: FC = (props) => { diff --git a/src/types.ts b/src/types.ts index d6ef458f..1c4f584e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -18,6 +18,7 @@ export type Navigator = { goBackward: () => void; setScroll: (val: 'scrolling' | 'paginated') => Promise; goToPage: (href: string) => void; + goToPageNumber: (pageNumber: number) => void; }; export type PdfNavigator = Navigator & { @@ -68,6 +69,8 @@ type CommonReader = { isLoading: false; content: JSX.Element; manifest: WebpubManifest; + currentPage: number; + totalPages: number; }; export type PDFActiveReader = CommonReader & { @@ -92,10 +95,6 @@ export type GetContent = ( proxyUrl?: string ) => Promise; -export type ReaderManagerArguments = { - headerLeft?: JSX.Element; // Top-left header section -}; - export type UseWebReaderArguments = { webpubManifestUrl: string; proxyUrl?: string; diff --git a/src/ui/Button.tsx b/src/ui/Button.tsx index 3b183d59..504de5fb 100644 --- a/src/ui/Button.tsx +++ b/src/ui/Button.tsx @@ -1,11 +1,36 @@ import React from 'react'; import { Button as ChakraButton } from '@chakra-ui/react'; +import useColorModeValue from './hooks/useColorModeValue'; export type ButtonProps = React.ComponentPropsWithRef; const Button = React.forwardRef( (props, ref) => { - return ; + const mainBgColor = useColorModeValue( + 'ui.gray.xx-dark', + 'ui.black', + 'ui.sepia' + ); + + return ( + + ); } ); diff --git a/src/ui/ErrorBoundary.tsx b/src/ui/ErrorBoundary.tsx index 69d2d056..fc02298d 100644 --- a/src/ui/ErrorBoundary.tsx +++ b/src/ui/ErrorBoundary.tsx @@ -8,7 +8,7 @@ import { } from '@chakra-ui/react'; import * as React from 'react'; import { WebReaderProps } from '..'; -import { DefaultHeaderLeft, HeaderWrapper } from './Header'; +import { HeaderWrapper } from './Header'; import { getTheme } from './theme'; type ErrorState = { error?: Error; info?: React.ErrorInfo }; @@ -38,9 +38,7 @@ class ErrorBoundary extends React.Component { if (error && info) { return ( - - {this.props.headerLeft ?? } - + ; - -const Footer: React.FC = ({ - state, - navigator, - ...rest -}): JSX.Element => { - const bgColor = useColorModeValue( - 'ui.gray.light-warm', - 'ui.black', - 'ui.sepia' - ); - const isAtStart = state?.atStart; - const isAtEnd = state?.atEnd; - return ( - - - - Previous - - - Next - - - ); -}; - -export default Footer; diff --git a/src/ui/Header.tsx b/src/ui/Header.tsx index 30bd50d7..335b9df6 100644 --- a/src/ui/Header.tsx +++ b/src/ui/Header.tsx @@ -1,93 +1,157 @@ import React, { ComponentProps } from 'react'; -import { Flex, Link, HStack, Text, Icon } from '@chakra-ui/react'; -import { ActiveReader, ReaderManagerArguments } from '../types'; +import { Flex, HStack, Text, Icon, Input } from '@chakra-ui/react'; +import { ActiveReader } from '../types'; import Button from './Button'; -import { HEADER_HEIGHT } from '../constants'; -import { Previous, ToggleFullScreen, ToggleFullScreenExit } from './icons'; +import { + PageDown, + PageUp, + Reset, + Search, + ToggleFullScreen, + ToggleFullScreenExit, +} from './icons'; import SettingsCard from './SettingsButton'; import TableOfContent from './TableOfContent'; import useColorModeValue from '../ui/hooks/useColorModeValue'; import useFullscreen from './hooks/useFullScreen'; import SkipNavigation from './SkipNavigation'; - -export const DefaultHeaderLeft = (): React.ReactElement => { - const linkColor = useColorModeValue('gray.700', 'gray.100', 'gray.700'); - const iconFill = useColorModeValue( - 'ui.gray.icon', - 'ui.white', - 'ui.gray.icon' - ); - const bgColorFocus = useColorModeValue( - 'ui.gray.active', - 'ui.gray.x-dark', - 'ui.gray.active' - ); - - return ( - - - Back - - ); -}; +import FitHeightWidth from './icons/FitHeightWidth'; +import PdfZoomControls from './PdfZoomControls'; +import HtmlFontSizeControls from './HtmlFontSizeControls'; export default function Header( - props: ActiveReader & - ReaderManagerArguments & { - containerRef: React.MutableRefObject; - } + props: ActiveReader & { + containerRef: React.MutableRefObject; + totalPages: number; + currentPage: number; + } ): React.ReactElement { const [isFullscreen, toggleFullScreen] = useFullscreen(); - const { headerLeft, navigator, manifest, containerRef } = props; + const { + navigator, + manifest, + type, + containerRef, + currentPage, + totalPages, + } = props; const iconFill = useColorModeValue( 'ui.gray.icon', 'ui.white', 'ui.gray.icon' ); const mainBgColor = useColorModeValue( - 'ui.gray.light-warm', + 'ui.gray.xx-dark', 'ui.black', 'ui.sepia' ); + const [inputValue, setInputValue] = React.useState(currentPage); + React.useEffect(() => { + setInputValue(currentPage); + }, [currentPage]); + + const goToPage = (page: number) => { + if (navigator) { + navigator.goToPageNumber(page); + } + }; + + const handleInputChange = (e: React.ChangeEvent) => { + const val = e.target.value.replace(/[^0-9]/g, ''); + setInputValue(val ? parseInt(val, 10) : 0); + }; + + const handleInputKeyDown = (e: React.KeyboardEvent) => { + if ( + e.key === 'Enter' && + inputValue && + inputValue >= 1 && + inputValue <= totalPages + ) { + goToPage(Number(inputValue)); + } + }; + + const handlePageUp = () => { + if (currentPage > 1) { + navigator.goBackward(); + } + }; + + const handlePageDown = () => { + if (currentPage < totalPages) { + navigator.goForward(); + } + }; + return ( - + - {headerLeft ?? } - + + {type === 'PDF' && } + {type === 'HTML' && ( + + )} + + + + + + + + / {totalPages} + + + + + - + ); @@ -116,11 +178,9 @@ export const HeaderWrapper = React.forwardRef< top={0} left={0} right={0} - height={`${HEADER_HEIGHT}px`} zIndex="sticky" alignContent="space-between" alignItems="center" - px={[0, 0, 8]} borderBottom="1px solid" borderColor="gray.100" {...rest} diff --git a/src/ui/HtmlFontSizeControls.tsx b/src/ui/HtmlFontSizeControls.tsx new file mode 100644 index 00000000..e430c16a --- /dev/null +++ b/src/ui/HtmlFontSizeControls.tsx @@ -0,0 +1,32 @@ +import * as React from 'react'; +import { ButtonGroup, Icon } from '@chakra-ui/react'; +import Button from './Button'; +import { HtmlNavigator, ReaderState } from '../types'; +import { ZoomIn, ZoomOut } from './icons'; + +export type HtmlFontSizeControlsProps = { + navigator: HtmlNavigator; + iconFill: string; + readerState: ReaderState; +}; + +export default function HtmlFontSizeControls( + props: HtmlFontSizeControlsProps +): React.ReactElement | null { + const { navigator, iconFill, readerState } = props; + + if (!readerState.settings) return null; + + const { decreaseFontSize, increaseFontSize } = navigator; + + return ( + + + + + ); +} diff --git a/src/ui/LoadingSkeleton.tsx b/src/ui/LoadingSkeleton.tsx index c535afb9..d8b86f08 100644 --- a/src/ui/LoadingSkeleton.tsx +++ b/src/ui/LoadingSkeleton.tsx @@ -8,7 +8,6 @@ import { import React from 'react'; import { HtmlState } from '../HtmlReader/types'; import { ReaderState } from '../types'; -import Footer from './Footer'; import { HeaderWrapper } from './Header'; import useColorModeValue from './hooks/useColorModeValue'; import { getTheme } from './theme'; @@ -37,7 +36,6 @@ const LoadingSkeletonContent = ({ -