diff --git a/package.json b/package.json
index 00293d1..ec0b27e 100644
--- a/package.json
+++ b/package.json
@@ -52,12 +52,15 @@
"eslint": "^8",
"eslint-config-next": "14.2.7",
"readium-css": "github:readium/readium-css#json",
- "typescript": "^5"
+ "typescript": "^5",
+ "vercel": "39.1.1"
},
"overrides": {
- "@babel/core": "7.25.2"
+ "@babel/core": "7.25.2",
+ "vercel": "$vercel"
},
"resolutions": {
- "@babel/core": "7.25.2"
+ "@babel/core": "7.25.2",
+ "vercel": "$vercel"
}
}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index b5322b5..8a45f05 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -13,6 +13,12 @@ export default function Home() {
Moby Dick (reflow)
+
+ The House of the Seven Gables (reflow advanced)
+
+
+ Les Diaboliques (reflow french)
+
Bella the Dragon (FXL)
diff --git a/src/components/ArrowButton.tsx b/src/components/ArrowButton.tsx
index 89abc5c..d7f1b96 100644
--- a/src/components/ArrowButton.tsx
+++ b/src/components/ArrowButton.tsx
@@ -14,6 +14,8 @@ import { Button, PressEvent, Tooltip, TooltipTrigger } from "react-aria-componen
import { useAppSelector } from "@/lib/hooks";
import classNames from "classnames";
+import { isActiveElement } from "@/helpers/focus";
+import { StaticBreakpoints } from "@/hooks/useBreakpoints";
export interface ReaderArrowProps {
direction: "left" | "right";
@@ -26,8 +28,10 @@ export const ArrowButton = (props: ReaderArrowProps) => {
const button = useRef(null);
const isImmersive = useAppSelector(state => state.reader.isImmersive);
const isFullscreen = useAppSelector(state => state.reader.isFullscreen);
- const hasReachedBreakpoint = useAppSelector(state => state.reader.hasReachedBreakpoint);
+ const hasReachedDynamicBreakpoint = useAppSelector(state => state.reader.hasReachedDynamicBreakpoint);
+ const staticBreakpoint = useAppSelector(state => state.reader.staticBreakpoint);
const isRTL = useAppSelector(state => state.publication.isRTL);
+ const isFXL = useAppSelector(state => state.publication.isFXL);
const [isHovering, setIsHovering] = useState(false);
@@ -35,7 +39,7 @@ export const ArrowButton = (props: ReaderArrowProps) => {
const handleClassNameFromState = () => {
let className = "";
- if (isImmersive && !hasReachedBreakpoint || isFullscreen) {
+ if (isImmersive && !hasReachedDynamicBreakpoint || isFullscreen) {
className = readerStateStyles.immersiveHidden;
} else if (isImmersive) {
className = readerStateStyles.immersive;
@@ -44,22 +48,34 @@ export const ArrowButton = (props: ReaderArrowProps) => {
};
const handleClassNameFromBreakpoint = () => {
- return hasReachedBreakpoint ? arrowStyles.viewportLarge : "";
+ if (isFXL && (staticBreakpoint === StaticBreakpoints.large || staticBreakpoint === StaticBreakpoints.xLarge)) {
+ return arrowStyles.viewportLarge;
+ } else if (!isFXL && hasReachedDynamicBreakpoint) {
+ return arrowStyles.viewportLarge;
+ } else {
+ return "";
+ }
};
useEffect(() => {
- if ((props.disabled || (isImmersive && !isHovering)) && document.activeElement === button.current) {
+ if ((props.disabled || (isImmersive && !isHovering)) && isActiveElement(button.current)) {
button.current!.blur();
}
});
+ const blurOnEsc = (event: React.KeyboardEvent) => {
+ if (isActiveElement(button.current) && event.code === "Escape") {
+ button.current!.blur();
+ }
+ };
+
// Unlike preventFocusOnPress, this gives a visual feedback
// the button has been pressed in immersive mode (esp. when hidden)
// CSS needs to take care of hover state though, as it will be applied
// on mobile depending on the length of the press
const handleNonKeyboardFocus = (event: PressEvent) => {
if (event.pointerType !== "keyboard") {
- if (document.activeElement === button.current) {
+ if (isActiveElement(button.current)) {
button.current!.blur()
}
}
@@ -74,8 +90,10 @@ export const ArrowButton = (props: ReaderArrowProps) => {
onPress={ props.onPressCallback }
onPressEnd={ handleNonKeyboardFocus }
onHoverChange={ (e) => setIsHovering(e) }
+ onKeyDown={ blurOnEsc }
className={ classNames(props.className, handleClassNameFromBreakpoint(), handleClassNameFromState()) }
- isDisabled={ props.disabled }>
+ isDisabled={ props.disabled }
+ >
{ props.direction === "left" ?
:
diff --git a/src/components/FullscreenAction.tsx b/src/components/FullscreenAction.tsx
index 6d6eeaa..793aacb 100644
--- a/src/components/FullscreenAction.tsx
+++ b/src/components/FullscreenAction.tsx
@@ -1,7 +1,8 @@
import React from "react";
import Locale from "../resources/locales/en.json";
-import { ActionKeys, RSPrefs } from "@/preferences";
+import { RSPrefs } from "@/preferences";
+import readerSharedUI from "./assets/styles/readerSharedUI.module.css";
import FullscreenCorners from "./assets/icons/fullscreen.svg";
import FullscreenExit from "./assets/icons/fullscreen_exit.svg";
@@ -10,13 +11,24 @@ import { OverflowMenuItem } from "./Templates/OverflowMenuItem";
import { ActionIcon } from "./Templates/ActionIcon";
import { useFullscreen } from "@/hooks/useFullscreen";
-import { ActionComponentVariant, IActionComponent } from "./Templates/ActionComponent";
+import { ActionComponentVariant, ActionKeys, IActionComponent } from "./Templates/ActionComponent";
+
+import { useAppDispatch } from "@/lib/hooks";
+import { setHovering } from "@/lib/readerReducer";
export const FullscreenAction: React.FC = ({ variant }) => {
// Note: Not using React Aria ToggleButton here as fullscreen is quite
// difficult to control in isolation due to collapsibility + shortcuts
const fs = useFullscreen();
+ const dispatch = useAppDispatch();
+
+ const handlePress = () => {
+ fs.handleFullscreen();
+ // Has to be dispatched manually, otherwise stays true…
+ dispatch(setHovering(false));
+ // TODO: fix hover state on exit, if even possible w/o a lot of getting around…
+ };
if (variant && variant === ActionComponentVariant.menu) {
return(
@@ -38,12 +50,13 @@ export const FullscreenAction: React.FC = ({ variant }) => {
<>
{ document.fullscreenEnabled
?
: <>>
}
diff --git a/src/components/JumpToPositionAction.tsx b/src/components/JumpToPositionAction.tsx
index dfdb28a..cd5d55d 100644
--- a/src/components/JumpToPositionAction.tsx
+++ b/src/components/JumpToPositionAction.tsx
@@ -1,13 +1,13 @@
import React from "react";
import Locale from "../resources/locales/en.json";
-import { ActionKeys, RSPrefs } from "@/preferences";
+import { RSPrefs } from "@/preferences";
import TargetIcon from "./assets/icons/point_scan.svg";
import { ActionIcon } from "./Templates/ActionIcon";
import { OverflowMenuItem } from "./Templates/OverflowMenuItem";
-import { ActionComponentVariant, IActionComponent } from "./Templates/ActionComponent";
+import { ActionComponentVariant, ActionKeys, IActionComponent } from "./Templates/ActionComponent";
export const JumpToPositionAction: React.FC = ({ variant }) => {
if (variant && variant === ActionComponentVariant.menu) {
diff --git a/src/components/OverflowMenu.tsx b/src/components/OverflowMenu.tsx
index 888ab1b..64f0390 100644
--- a/src/components/OverflowMenu.tsx
+++ b/src/components/OverflowMenu.tsx
@@ -1,7 +1,5 @@
import React, { ReactNode } from "react";
-import { ActionVisibility } from "@/preferences";
-
import Locale from "../resources/locales/en.json";
import overflowMenuStyles from "./assets/styles/overflowMenu.module.css";
@@ -9,10 +7,13 @@ import MenuIcon from "./assets/icons/more_vert.svg";
import { Key, Menu, MenuTrigger, Popover } from "react-aria-components";
import { ActionIcon } from "./Templates/ActionIcon";
-import { useAppDispatch } from "@/lib/hooks";
+import { useAppDispatch, useAppSelector } from "@/lib/hooks";
import { setOverflowMenuOpen, toggleImmersive } from "@/lib/readerReducer";
+import { ActionVisibility } from "./Templates/ActionComponent";
export const OverflowMenu = ({ children }: { children?: ReactNode }) => {
+ const isImmersive = useAppSelector(state => state.reader.isImmersive);
+ const isHovered = useAppSelector(state => state.reader.isHovering);
const dispatch = useAppDispatch();
const toggleMenuState = (value: boolean) => {
@@ -21,7 +22,7 @@ export const OverflowMenu = ({ children }: { children?: ReactNode }) => {
return(
<>
- { React.Children.toArray(children).length > 0 ?
+ { React.Children.toArray(children).length > 0 && (!isImmersive || isHovered) ?
<>
toggleMenuState(val) }>
state.reader.isImmersive);
const isImmersiveRef = useRef(isImmersive);
- const runningHead = useAppSelector(state => state.publication.runningHead);
const atPublicationStart = useAppSelector(state => state.publication.atPublicationStart);
const atPublicationEnd = useAppSelector(state => state.publication.atPublicationEnd);
const dispatch = useAppDispatch();
const fs = useFullscreen();
+ const staticBreakpoint = useBreakpoints();
const {
EpubNavigatorLoad,
@@ -272,27 +273,17 @@ export const Reader = ({ rawManifest, selfHref }: { rawManifest: object, selfHre
}
}, 250);
- const mq = "(min-width:"+ RSPrefs.breakpoint + "px)";
- const breakpointQuery = window.matchMedia(mq);
- const handleBreakpointChange = useCallback((event: MediaQueryListEvent) => {
- dispatch(setBreakpoint(event.matches))}, [dispatch]);
-
useEffect(() => {
dispatch(setPlatformModifier(getPlatformModifier()));
-
- // Initial setup
- dispatch(setBreakpoint(breakpointQuery.matches));
- breakpointQuery.addEventListener("change", handleBreakpointChange);
window.addEventListener("resize", handleResize);
window.addEventListener("orientationchange", handleResize);
return () => {
- breakpointQuery.removeEventListener("change", handleBreakpointChange);
window.removeEventListener("resize", handleResize);
window.removeEventListener("orientationchange", handleResize);
}
- }, [dispatch, breakpointQuery, handleBreakpointChange, handleResize]);
+ }, [dispatch, handleResize]);
useEffect(() => {
const fetcher: Fetcher = new HttpFetcher(undefined, selfHref);
@@ -302,44 +293,38 @@ export const Reader = ({ rawManifest, selfHref }: { rawManifest: object, selfHre
publication.current = new Publication({
manifest: manifest,
fetcher: fetcher,
- });
-
- let positionsList: Locator[] | undefined;
+ });
- dispatch(setRunningHead(publication.current.metadata.title.getTranslation("en")));
dispatch(setRTL(publication.current.metadata.effectiveReadingProgression === ReadingProgression.rtl));
dispatch(setFXL(publication.current.metadata.getPresentation()?.layout === EPUBLayout.fixed));
- dispatch(setProgression({ currentPublication: runningHead }));
+ const pubTitle = publication.current.metadata.title.getTranslation("en");
+
+ dispatch(setRunningHead(pubTitle));
+ dispatch(setProgression({ currentPublication: pubTitle }));
+
+ let positionsList: Locator[] | undefined;
+
const fetchPositions = async () => {
- const positionsJSON = publication.current?.manifest.links.findWithMediaType("application/vnd.readium.position-list+json");
- if (positionsJSON) {
- const fetcher = new HttpFetcher(undefined, selfHref);
- const fetched = fetcher.get(positionsJSON);
- try {
- const positionObj = await fetched.readAsJSON() as {total: number, positions: Locator[]};
- positionsList = positionObj.positions;
- dispatch(setProgression( { totalPositions: positionObj.total }));
- } catch(err) {
- console.error(err)
- }
- }
+ positionsList = await publication.current?.positionsFromManifest();
+ if (positionsList && positionsList.length > 0) dispatch(setProgression( { totalPositions: positionsList.length }));
};
fetchPositions()
- .catch(console.error);
-
- const initialPosition = localData.get(localDataKey.current);
-
- EpubNavigatorLoad({
- container: container.current,
- publication: publication.current,
- listeners: listeners,
- positionsList: positionsList,
- initialPosition: initialPosition,
- localDataKey: localDataKey.current,
- }, () => p.observe(window));
+ .catch(console.error)
+ .then(() => {
+ const initialPosition = localData.get(localDataKey.current);
+
+ EpubNavigatorLoad({
+ container: container.current,
+ publication: publication.current!,
+ listeners: listeners,
+ positionsList: positionsList,
+ initialPosition: initialPosition,
+ localDataKey: localDataKey.current,
+ }, () => p.observe(window));
+ });
return () => {
EpubNavigatorDestroy(() => p.destroy());
@@ -379,7 +364,7 @@ export const Reader = ({ rawManifest, selfHref }: { rawManifest: object, selfHre
<>>
}
- { isPaged ? : <>>}
+ { isPaged ? : <>> }
>
)};
\ No newline at end of file
diff --git a/src/components/ReaderHeader.tsx b/src/components/ReaderHeader.tsx
index b4854d7..2896d93 100644
--- a/src/components/ReaderHeader.tsx
+++ b/src/components/ReaderHeader.tsx
@@ -48,7 +48,7 @@ export const ReaderHeader = ({ toc }: { toc: Links }) => {
onMouseEnter={ setHover }
onMouseLeave={ removeHover }
>
-
+
{
>
{ Actions.ActionIcons }
- {/*
+ {/*
{ Actions.MenuItems }
- */}
+ */}
diff --git a/src/components/RunningHead.tsx b/src/components/RunningHead.tsx
index 9e64f08..ad1fcb7 100644
--- a/src/components/RunningHead.tsx
+++ b/src/components/RunningHead.tsx
@@ -4,12 +4,12 @@ import Locale from "../resources/locales/en.json";
import { useAppSelector } from "@/lib/hooks";
-export const RunningHead = () => {
+export const RunningHead = ({ syncDocTitle } : { syncDocTitle?: boolean }) => {
const runningHead = useAppSelector(state => state.publication.runningHead);
useEffect(() => {
- if (runningHead) document.title = runningHead;
- }, [runningHead])
+ if (syncDocTitle && runningHead) document.title = runningHead;
+ }, [syncDocTitle, runningHead])
return(
<>
diff --git a/src/components/SettingsAction.tsx b/src/components/SettingsAction.tsx
index 9d31e3c..aa65350 100644
--- a/src/components/SettingsAction.tsx
+++ b/src/components/SettingsAction.tsx
@@ -1,18 +1,18 @@
import React from "react";
-import { ActionKeys, RSPrefs } from "@/preferences";
+import { RSPrefs } from "@/preferences";
import Locale from "../resources/locales/en.json";
import settingsStyles from "./assets/styles/readerSettings.module.css";
import readerSharedUI from "./assets/styles/readerSharedUI.module.css";
-import TuneIcon from "./assets/icons/tune.svg";
+import TuneIcon from "./assets/icons/match_case.svg";
import CloseIcon from "./assets/icons/close.svg";
import { Button, Dialog, DialogTrigger, Heading, Popover, Separator } from "react-aria-components";
import { ActionIcon } from "./Templates/ActionIcon";
import { OverflowMenuItem } from "./Templates/OverflowMenuItem";
-import { ActionComponentVariant, IActionComponent } from "./Templates/ActionComponent";
+import { ActionComponentVariant, ActionKeys, IActionComponent } from "./Templates/ActionComponent";
import { ReadingDisplayCol } from "./ReadingDisplayCol";
import { ReadingDisplayLayout } from "./ReadingDisplayLayout";
@@ -70,7 +70,7 @@ export const SettingsAction: React.FC = ({ variant }) => {
>
- { Locale.reader.settings.heading }
+ { Locale.reader.settings.heading }
diff --git a/src/components/Templates/ActionComponent.ts b/src/components/Templates/ActionComponent.ts
index 9406084..19eaf6f 100644
--- a/src/components/Templates/ActionComponent.ts
+++ b/src/components/Templates/ActionComponent.ts
@@ -1,3 +1,16 @@
+export enum ActionKeys {
+ fullscreen = "fullscreen",
+ jumpToPosition = "jumpToPosition",
+ settings = "settings",
+ toc = "toc"
+}
+
+export enum ActionVisibility {
+ always = "always",
+ partially = "partially",
+ overflow = "overflow"
+}
+
export enum ActionComponentVariant {
button = "iconButton",
menu = "menuItem"
diff --git a/src/components/Templates/ActionIcon.tsx b/src/components/Templates/ActionIcon.tsx
index 5d1009c..22093eb 100644
--- a/src/components/Templates/ActionIcon.tsx
+++ b/src/components/Templates/ActionIcon.tsx
@@ -1,15 +1,19 @@
-import React, { ComponentType, SVGProps } from "react";
+import React, { ComponentType, SVGProps, useRef } from "react";
+
+import { RSPrefs } from "@/preferences";
import readerSharedUI from "../assets/styles/readerSharedUI.module.css";
import readerStateStyles from "../assets/styles/readerStates.module.css";
import { Button, Tooltip, TooltipTrigger, TooltipProps, PressEvent, ButtonProps } from "react-aria-components";
-import { ActionVisibility } from "@/preferences";
import { useAppDispatch, useAppSelector } from "@/lib/hooks";
-import classNames from "classnames";
import { setImmersive } from "@/lib/readerReducer";
+import classNames from "classnames";
+import { isActiveElement, isKeyboardTriggered } from "@/helpers/focus";
+import { ActionVisibility } from "./ActionComponent";
+
export interface IActionIconProps {
className?: string;
ariaLabel: string;
@@ -30,9 +34,8 @@ export const ActionIcon: React.FC & IAc
onPressCallback,
...props
}) => {
+ const triggerRef = useRef(null);
const isImmersive = useAppSelector(state => state.reader.isImmersive);
- const isFullscreen = useAppSelector(state => state.reader.isFullscreen);
- const overflowMenuOpen = useAppSelector(state => state.reader.overflowMenuOpen);
const isHovering = useAppSelector(state => state.reader.isHovering);
const dispatch = useAppDispatch();
@@ -40,22 +43,17 @@ export const ActionIcon: React.FC & IAc
const handleClassNameFromState = () => {
let className = "";
- const isSubdued = (isImmersive || isFullscreen);
- const isActive = (overflowMenuOpen || isHovering);
-
switch(visibility) {
case ActionVisibility.always:
- if (!isActive && isSubdued) {
+ if (!isHovering && isImmersive) {
className = readerStateStyles.subduedAlways;
} else {
className = visibility;
}
break;
case ActionVisibility.partially:
- if (!isActive && isSubdued) {
+ if (!isHovering && isImmersive) {
className = readerStateStyles.subduedPartially;
- } else if (isActive) {
- className = readerStateStyles.subduedPartiallyHovering;
} else {
className = visibility;
}
@@ -70,23 +68,41 @@ export const ActionIcon: React.FC & IAc
const defaultOnPressFunc = () => {
dispatch(setImmersive(false));
- }
+ };
+
+ const handleImmersive = (event: React.FocusEvent) => {
+ // Check whether the focus was triggered by keyboard…
+ // We don’t have access to type/modality, unlike onPress
+ if (isKeyboardTriggered(event.target)) {
+ dispatch(setImmersive(false));
+ }
+ };
+
+ const blurOnEsc = (event: React.KeyboardEvent) => {
+ // TODO: handle Tooltip cos first time you press esc, it’s the tooltip that is closed.
+ if (isActiveElement(triggerRef.current) && event.code === "Escape") {
+ triggerRef.current!.blur();
+ }
+ };
return (
<>
{ tooltipLabel }
diff --git a/src/components/Templates/OverflowMenuItem.tsx b/src/components/Templates/OverflowMenuItem.tsx
index 23b4504..7f73256 100644
--- a/src/components/Templates/OverflowMenuItem.tsx
+++ b/src/components/Templates/OverflowMenuItem.tsx
@@ -1,11 +1,10 @@
import React, { ComponentType, SVGProps } from "react";
-import { ActionKeys } from "@/preferences";
-
import overflowMenuStyles from "../assets/styles/overflowMenu.module.css";
import { MenuItem, Text } from "react-aria-components";
import { Shortcut } from "../Shortcut";
+import { ActionKeys } from "./ActionComponent";
export interface IOverflowMenuItemProp {
label: string;
diff --git a/src/components/TocAction.tsx b/src/components/TocAction.tsx
index 2a43100..fb109d4 100644
--- a/src/components/TocAction.tsx
+++ b/src/components/TocAction.tsx
@@ -1,23 +1,23 @@
import React from "react";
-import { ActionKeys, RSPrefs } from "@/preferences";
+import { RSPrefs } from "@/preferences";
import Locale from "../resources/locales/en.json";
import readerSharedUI from "./assets/styles/readerSharedUI.module.css";
import tocStyles from "./assets/styles/toc.module.css";
-import TocIcon from "./assets/icons/format_list_bulleted.svg";
+import TocIcon from "./assets/icons/toc.svg";
import CloseIcon from "./assets/icons/close.svg";
import { Links } from "@readium/shared";
import { ActionIcon } from "./Templates/ActionIcon";
-import { Button, Dialog, DialogTrigger, ListBox, ListBoxItem, Popover } from "react-aria-components";
+import { Button, Dialog, DialogTrigger, Heading, ListBox, ListBoxItem, Popover } from "react-aria-components";
import { useAppDispatch, useAppSelector } from "@/lib/hooks";
import { setTocOpen } from "@/lib/readerReducer";
import { OverflowMenuItem } from "./Templates/OverflowMenuItem";
-import { ActionComponentVariant, IActionComponent } from "./Templates/ActionComponent";
+import { ActionComponentVariant, ActionKeys, IActionComponent } from "./Templates/ActionComponent";
export const TocAction: React.FC = ({ variant, toc }) => {
const isOpen = useAppSelector(state => state.reader.tocOpen);
@@ -66,10 +66,11 @@ export const TocAction: React.FC = ({ variant
>
+ { Locale.reader.toc.heading }
{ toc.items.length > 0
?
- { item => { item.title } }
-
+ { item => { item.title } }
+
: { Locale.reader.toc.empty }
}
diff --git a/src/components/assets/icons/format_list_bulleted.svg b/src/components/assets/icons/format_list_bulleted.svg
deleted file mode 100644
index 1a40d7c..0000000
--- a/src/components/assets/icons/format_list_bulleted.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/components/assets/icons/match_case.svg b/src/components/assets/icons/match_case.svg
new file mode 100644
index 0000000..4cfd9ad
--- /dev/null
+++ b/src/components/assets/icons/match_case.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/assets/icons/toc.svg b/src/components/assets/icons/toc.svg
new file mode 100644
index 0000000..0903b54
--- /dev/null
+++ b/src/components/assets/icons/toc.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/assets/icons/tune.svg b/src/components/assets/icons/tune.svg
deleted file mode 100644
index 796e4bf..0000000
--- a/src/components/assets/icons/tune.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/components/assets/styles/overflowMenu.module.css b/src/components/assets/styles/overflowMenu.module.css
index 30e4681..0e79d5e 100644
--- a/src/components/assets/styles/overflowMenu.module.css
+++ b/src/components/assets/styles/overflowMenu.module.css
@@ -12,6 +12,15 @@
align-items: center;
gap: 10px;
padding: 10px;
+ outline: none;
+}
+
+.menuItem[data-hovered] {
+ background-color: var(--color-hover);
+}
+
+.menuItem[data-focus-visible] {
+ outline: auto;
}
.menuItem > svg {
diff --git a/src/components/assets/styles/reader.css b/src/components/assets/styles/reader.css
index ebf8e4a..d7be6ca 100644
--- a/src/components/assets/styles/reader.css
+++ b/src/components/assets/styles/reader.css
@@ -4,6 +4,8 @@
--color-secondary: white;
--color-disabled: #767676;
--color-subdued: #999999;
+ --color-hover: #eaeaea;
+ --color-selected: #eaeaea;
}
html,
@@ -22,14 +24,21 @@ body {
#bottom-bar,
#top-bar {
- height: calc(var(--icon-size, 24px) * 2.5);
+ box-sizing: border-box;
gap: 2px;
touch-action: manipulation;
background-color: #FFFFFF;
}
+/* Necessary to have all three so that there is no offset when switching paginated/scroll */
+#top-bar {
+ min-height: calc(var(--icon-size, 24px) * 2.5);
+ height: calc(var(--icon-size, 24px) * 2.5);
+ max-height: calc(var(--icon-size, 24px) * 2.5);
+}
+
#bottom-bar {
- height: calc(var(--icon-size, 24px) * 2);
+ height: calc(var(--icon-size, 24px) * 2.5);
display: flex;
justify-content: center;
align-items: center;
diff --git a/src/components/assets/styles/readerHeader.module.css b/src/components/assets/styles/readerHeader.module.css
index 9088c81..606fb27 100644
--- a/src/components/assets/styles/readerHeader.module.css
+++ b/src/components/assets/styles/readerHeader.module.css
@@ -10,7 +10,7 @@
justify-self: end;
display: flex;
align-items: center;
- gap: 10px;
+ gap: 2px;
}
.header h1 {
@@ -21,4 +21,9 @@
grid-area: header-center;
justify-self: center;
align-self: center;
+ /* Currently a layout issue
+ text-wrap: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ */
}
\ No newline at end of file
diff --git a/src/components/assets/styles/readerSettings.module.css b/src/components/assets/styles/readerSettings.module.css
index 6a530e7..4b21467 100644
--- a/src/components/assets/styles/readerSettings.module.css
+++ b/src/components/assets/styles/readerSettings.module.css
@@ -7,11 +7,6 @@
position: relative;
}
-.heading {
- font-size: 1.25rem;
- margin: 0 0 1rem 0;
-}
-
.readerSettingsLabel {
display: block;
font-weight: bold;
@@ -29,7 +24,7 @@
}
.readerSettingsRadio[data-selected] {
- background-color: #EAEAEA;
+ background-color: var(--color-selected);
border-radius: 5px;
}
diff --git a/src/components/assets/styles/readerSharedUI.module.css b/src/components/assets/styles/readerSharedUI.module.css
index 85ee50a..4bdcb76 100644
--- a/src/components/assets/styles/readerSharedUI.module.css
+++ b/src/components/assets/styles/readerSharedUI.module.css
@@ -1,13 +1,14 @@
.icon {
- width: var(--icon-size, 24px);
- height: var(--icon-size, 24px);
+ box-sizing: border-box;
+ padding: calc(var(--icon-size, 24px) * (1/4));
text-align: center;
+ border-radius: 5px;
}
.icon svg {
fill: var(--color-primary);
- width: 100%;
- height: 100%;
+ width: var(--icon-size, 24px);
+ height: var(--icon-size, 24px);
}
.tooltip {
@@ -23,6 +24,7 @@
position: absolute;
top: 10px;
right: 10px;
+ border-radius: 5px;
}
.closeButton svg {
@@ -32,7 +34,40 @@
}
.icon[data-hovered],
+.closeButton[data-hovered] {
+ background-color: var(--color-hover);
+}
+
.icon[data-focus-visible],
.closeButton[data-focus-visible] {
outline: auto;
+}
+
+/* Utils to improve icons’ consistency */
+.iconCompSm {
+ padding: calc(var(--icon-size, 24px) * (1/3));
+}
+
+.iconCompSm svg {
+ width: calc(var(--icon-size, 24px) * (3/4));
+ height: calc(var(--icon-size, 24px) * (3/4));
+ stroke: var(--color-primary);
+}
+
+.iconCompLg {
+ padding: calc(var(--icon-size, 24px) * (1/6));
+}
+
+.iconCompLg svg {
+ width: calc(var(--icon-size, 24px) * (4/3));
+ height: calc(var(--icon-size, 24px) * (4/3));
+}
+
+.iconApplyStroke svg {
+ stroke: var(--color-primary);
+}
+
+.popoverHeading {
+ font-size: 1.25rem;
+ margin: 0 0 1rem 0;
}
\ No newline at end of file
diff --git a/src/components/assets/styles/readerStates.module.css b/src/components/assets/styles/readerStates.module.css
index 1a1c156..991579a 100644
--- a/src/components/assets/styles/readerStates.module.css
+++ b/src/components/assets/styles/readerStates.module.css
@@ -17,10 +17,18 @@
opacity: 1;
}
+/* Opacity relative to .immersive */
.subduedPartially {
- visibility: hidden;
+ opacity: 0;
}
-.subduedPartiallyHovering {
- visibility: visible;
+/* Opacity relative to .immersive */
+.subduedPartially[data-hovered],
+.subduedPartially[data-focus-visible] {
+ opacity: 1;
+}
+
+/* TMP: To hide fullscreen icon that doesn’t update properly on exit */
+.immersive .subduedPartially[data-hovered] {
+ opacity: 0;
}
\ No newline at end of file
diff --git a/src/components/assets/styles/toc.module.css b/src/components/assets/styles/toc.module.css
index 1c6486a..8412fdc 100644
--- a/src/components/assets/styles/toc.module.css
+++ b/src/components/assets/styles/toc.module.css
@@ -9,9 +9,9 @@
}
.listBox {
- max-height: calc(100vh - (var(--icon-size, 32px) * 3));
- max-height: calc(100dvh - (var(--icon-size, 32px) * 3));
- overflow: scroll;
+ max-height: calc(100vh - (var(--icon-size, 24px) * 6));
+ max-height: calc(100dvh - (var(--icon-size, 24px) * 6));
+ overflow-y: scroll;
}
.listItem {
diff --git a/src/helpers/autoLayout/autoPaginate.ts b/src/helpers/autoLayout/autoPaginate.ts
index 148e55f..99fa1e3 100644
--- a/src/helpers/autoLayout/autoPaginate.ts
+++ b/src/helpers/autoLayout/autoPaginate.ts
@@ -1,6 +1,5 @@
-export const autoPaginate = (breakpoint: number, width: number, lineLength: number): number => {
+export const autoPaginate = (width: number, lineLength: number): number => {
const defaultFontSize = 16;
- breakpoint = breakpoint / defaultFontSize;
width = width / defaultFontSize;
- return (width >= breakpoint && width >= lineLength) ? Math.floor(width / lineLength) : 1;
+ return (width >= lineLength) ? Math.floor(width / lineLength) : 1;
}
\ No newline at end of file
diff --git a/src/helpers/autoLayout/optimalLineLength.ts b/src/helpers/autoLayout/optimalLineLength.ts
index 44c8239..52dcb28 100644
--- a/src/helpers/autoLayout/optimalLineLength.ts
+++ b/src/helpers/autoLayout/optimalLineLength.ts
@@ -4,7 +4,7 @@ export interface customFont {
}
export interface LineLengthTypography {
- minChars: number | null;
+ minChars: number | undefined | null;
optimalChars: number;
fontSize?: number;
sample?: string;
@@ -16,7 +16,7 @@ export interface LineLengthTypography {
}
export interface IOptimalLineLength {
- min: number;
+ min: number | null;
optimal: number;
fontSize: number;
}
@@ -31,7 +31,20 @@ export const getOptimalLineLength = (typo: LineLengthTypography): IOptimalLineLe
const letterSpacing = typo.letterSpacing || 0;
const wordSpacing = typo.wordSpacing || 0;
const fontSize = typo.fontSize || DEFAULT_FONT_SIZE;
- const divider = (typo.minChars && typo.minChars < typo.optimalChars) ? (typo.optimalChars / typo.minChars) : 1;
+
+ // Kept intentionally verbose to describe all cases for future implementation in Navigator
+ let divider: number | null = 1;
+ if (typo.minChars === null ) {
+ divider = null
+ } else if (typeof typo.minChars === "undefined") {
+ divider = 1;
+ } else {
+ if (typo.minChars < typo.optimalChars) {
+ divider = typo.optimalChars / typo.minChars;
+ } else {
+ divider = 1;
+ }
+ }
// It’s impractical or impossible to get the font in canvas
// so we assume it’s 0.5em wide by 1em tall
@@ -91,7 +104,7 @@ export const getOptimalLineLength = (typo: LineLengthTypography): IOptimalLineLe
}
return {
- min: Math.round((optimalLineLength / divider) + padding) / fontSize,
+ min: divider !== null ? Math.round((optimalLineLength / divider) + padding) / fontSize : null,
optimal: Math.round(optimalLineLength + padding) / fontSize,
fontSize: fontSize
}
diff --git a/src/helpers/focus.ts b/src/helpers/focus.ts
new file mode 100644
index 0000000..47a40c0
--- /dev/null
+++ b/src/helpers/focus.ts
@@ -0,0 +1,9 @@
+export const isActiveElement = (el: Element | undefined | null) => {
+ if (el) return document.activeElement === el;
+ return false;
+}
+
+export const isKeyboardTriggered = (el: Element | undefined | null) => {
+ if (el) return el.matches(":focus-visible");
+ return false;
+}
\ No newline at end of file
diff --git a/src/helpers/peripherals.ts b/src/helpers/peripherals.ts
index afdbd91..5bcfbe8 100644
--- a/src/helpers/peripherals.ts
+++ b/src/helpers/peripherals.ts
@@ -1,9 +1,10 @@
// Peripherals based on XBReader
-import { ActionKeys, RSPrefs } from "@/preferences";
+import { RSPrefs } from "@/preferences";
import { buildShortcut, PShortcut } from "./keyboard/buildShortcut";
import { useAppStore } from "@/lib/hooks";
import { isInteractiveElement } from "./isInteractiveElement";
+import { ActionKeys } from "@/components/Templates/ActionComponent";
export interface PCallbacks {
moveTo: (direction: "left" | "right" | "up" | "down" | "home" | "end") => void;
diff --git a/src/helpers/scrollAffordance.ts b/src/helpers/scrollAffordance.ts
index 1135bcb..924a9a7 100644
--- a/src/helpers/scrollAffordance.ts
+++ b/src/helpers/scrollAffordance.ts
@@ -1,6 +1,19 @@
-import { RSPrefs, ScrollAffordancePref } from "@/preferences";
+import { RSPrefs } from "@/preferences";
import Locale from "../resources/locales/en.json";
+export enum ScrollAffordancePref {
+ none = "none",
+ prev = "previous",
+ next = "next",
+ both = "both"
+}
+
+export enum ScrollBackTo {
+ top = "top",
+ bottom = "bottom",
+ untouched = "untouched"
+}
+
export interface IScrollAffordanceConfig {
pref: ScrollAffordancePref;
placement: "top" | "bottom";
@@ -47,6 +60,8 @@ export class ScrollAffordance {
display: flex;
width: 100%;
gap: 20px;
+ margin: 0;
+ padding: 0;
}
.playground-scroll-affordance-wrapper:focus-within {
/* to get around hidden overflow cutting off focus ring w/o being too noticeable */
@@ -71,6 +86,14 @@ export class ScrollAffordance {
font-weight: bold;
flex: 1 1 0;
text-align: left;
+ color: ${RSPrefs.theming.color.primary};
+ font-size: 1rem;
+ font-style: normal;
+ font-family: inherit;
+ }
+ .playground-scroll-affordance-wrapper > a:hover {
+ background-color: ${RSPrefs.theming.color.hover};
+ border: 1px solid ${RSPrefs.theming.color.primary}
}
.playground-scroll-affordance-wrapper > a:first-child:not(:last-child) {
text-align: right;
@@ -81,6 +104,10 @@ export class ScrollAffordance {
margin-right: 10px;
color: ${RSPrefs.theming.color.subdued};
}
+ .playground-scroll-affordance-wrapper > a.playground-scroll-affordance-button-prev:hover > span:before,
+ .playground-scroll-affordance-wrapper > a.playground-scroll-affordance-button-next:hover > span:after {
+ color: ${RSPrefs.theming.color.primary};
+ }
.playground-scroll-affordance-wrapper > a.playground-scroll-affordance-button-next > span:after {
content: "→";
float: right;
diff --git a/src/hooks/useBreakpoints.ts b/src/hooks/useBreakpoints.ts
new file mode 100644
index 0000000..f75262e
--- /dev/null
+++ b/src/hooks/useBreakpoints.ts
@@ -0,0 +1,117 @@
+import { useEffect, useLayoutEffect, useState } from "react";
+
+import { RSPrefs } from "@/preferences";
+import { setStaticBreakpoint } from "@/lib/readerReducer";
+import { useAppDispatch, useAppSelector } from "@/lib/hooks";
+import { useMediaQuery } from "./useMediaQuery";
+
+export enum StaticBreakpoints {
+ compact = "compact",
+ medium = "medium",
+ expanded = "expanded",
+ large = "large",
+ xLarge = "xLarge"
+}
+
+export type Breakpoints = { [key in StaticBreakpoints]: boolean | null } & { current: string | undefined } & { ranges: BreakpointRanges };
+
+type BreakpointRange = {
+ min: number | null,
+ max: number | null
+}
+
+type BreakpointRanges = { [key in StaticBreakpoints]: BreakpointRange | null; }
+
+export const useBreakpoints = () => {
+ const [isClient, setIsClient] = useState(false);
+ const staticBreakpoint = useAppSelector(state => state.reader.staticBreakpoint);
+ const dispatch = useAppDispatch();
+
+ const makeMediaString = (range: BreakpointRange | null) => {
+ if (!range || (!range.min && !range.max)) return null;
+
+ let mediaString = "screen"
+ if (range.min) {
+ mediaString += ` and (min-width: ${ range.min }px)`;
+ }
+ if (range.max) {
+ mediaString += ` and (max-width: ${ range.max }px)`
+ }
+ return mediaString;
+ };
+
+ const initRanges = () => {
+ const breakpointRanges: BreakpointRanges = {
+ [StaticBreakpoints.compact]: null,
+ [StaticBreakpoints.medium]: null,
+ [StaticBreakpoints.expanded]: null,
+ [StaticBreakpoints.large]: null,
+ [StaticBreakpoints.xLarge]: null
+ };
+
+ let prev: null | number = null;
+
+ Object.entries(RSPrefs.breakpoints).forEach(([ key, value ]) => {
+ if (value && !isNaN(value)) {
+ const max = value;
+ const min = prev ? prev + 1 : null;
+ Object.defineProperty(breakpointRanges, key, {
+ value: {
+ min: min,
+ max: max
+ }
+ });
+ prev = value;
+ } else if (!value && key === StaticBreakpoints.xLarge && prev) {
+ Object.defineProperty(breakpointRanges, key, {
+ value: {
+ min: prev + 1,
+ max: null
+ }
+ });
+ }
+ });
+
+ return breakpointRanges;
+ };
+
+ const ranges = initRanges();
+
+ const compactMedia = makeMediaString(ranges[StaticBreakpoints.compact]);
+ const mediumMedia = makeMediaString(ranges[StaticBreakpoints.medium]);
+ const expandedMedia = makeMediaString(ranges[StaticBreakpoints.expanded]);
+ const largeMedia = makeMediaString(ranges[StaticBreakpoints.large]);
+ const xLargeMedia = makeMediaString(ranges[StaticBreakpoints.xLarge]);
+
+ const breakpoints: Breakpoints = {
+ [StaticBreakpoints.compact]: useMediaQuery(compactMedia),
+ [StaticBreakpoints.medium]: useMediaQuery(mediumMedia),
+ [StaticBreakpoints.expanded]: useMediaQuery(expandedMedia),
+ [StaticBreakpoints.large]: useMediaQuery(largeMedia),
+ [StaticBreakpoints.xLarge]: useMediaQuery(xLargeMedia),
+ current: staticBreakpoint,
+ ranges: ranges
+ };
+
+ useLayoutEffect(() => {
+ if (typeof window !== "undefined") setIsClient(true);
+ }, []);
+
+ useEffect(() => {
+ if (isClient) {
+ if (breakpoints[StaticBreakpoints.compact]) {
+ dispatch(setStaticBreakpoint(StaticBreakpoints.compact));
+ } else if (breakpoints[StaticBreakpoints.medium]) {
+ dispatch(setStaticBreakpoint(StaticBreakpoints.medium));
+ } else if (breakpoints[StaticBreakpoints.expanded]) {
+ dispatch(setStaticBreakpoint(StaticBreakpoints.expanded))
+ } else if (breakpoints[StaticBreakpoints.large]) {
+ dispatch(setStaticBreakpoint(StaticBreakpoints.large));
+ } else if (breakpoints[StaticBreakpoints.xLarge]) {
+ dispatch(setStaticBreakpoint(StaticBreakpoints.xLarge))
+ };
+ }
+ });
+
+ return breakpoints;
+}
\ No newline at end of file
diff --git a/src/hooks/useCollapsibility.tsx b/src/hooks/useCollapsibility.tsx
index c815a3c..ae90c65 100644
--- a/src/hooks/useCollapsibility.tsx
+++ b/src/hooks/useCollapsibility.tsx
@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useState } from "react";
-import { ActionKeys, ActionVisibility, RSPrefs } from "@/preferences";
+import { RSPrefs } from "@/preferences";
import { Links } from "@readium/shared";
@@ -9,12 +9,13 @@ import { JumpToPositionAction } from "@/components/JumpToPositionAction";
import { SettingsAction } from "@/components/SettingsAction";
import { TocAction } from "@/components/TocAction";
import { useAppSelector } from "@/lib/hooks";
-import { ActionComponentVariant } from "@/components/Templates/ActionComponent";
+import { ActionComponentVariant, ActionKeys, ActionVisibility } from "@/components/Templates/ActionComponent";
+import { StaticBreakpoints } from "./useBreakpoints";
export const useCollapsibility = (toc: Links) => {
const [ActionIcons, setActionIcons] = useState([]);
const [MenuItems, setMenuItems] = useState([]);
- const hasReachedBreakpoint = useAppSelector(state => state.reader.hasReachedBreakpoint);
+ const staticBreakpoint = useAppSelector(state => state.reader.staticBreakpoint);
const triageActions = useCallback(() => {
const ActionIconsMap = {
@@ -32,32 +33,41 @@ export const useCollapsibility = (toc: Links) => {
};
const actionsOrder = RSPrefs.actions.displayOrder;
-
+
const actionIcons: React.JSX.Element[] = [];
const menuItems: React.JSX.Element[] = [];
- actionsOrder.map((key) => {
+ let countdown: number = 0;
+ if (staticBreakpoint === StaticBreakpoints.compact) {
+ countdown = actionsOrder.length - (actionsOrder.length - 2);
+ } else if (staticBreakpoint === StaticBreakpoints.medium) {
+ countdown = actionsOrder.length - (actionsOrder.length - 1);
+ }
+
+ // Creating a shallow copy so that actionsOrder doesn’t mutate between rerenders
+ [...actionsOrder].slice().reverse().map((key) => {
const actionPref = RSPrefs.actions[key];
if (actionPref.visibility === ActionVisibility.overflow) {
- menuItems.push(MenuItemsMap[key]);
- } else if (actionPref.collapsible) {
- if (hasReachedBreakpoint) {
- actionIcons.push(ActionIconsMap[key]);
- } else {
- menuItems.push(MenuItemsMap[key]);
- }
+ menuItems.unshift(MenuItemsMap[key]);
+ } else if (actionPref.visibility === ActionVisibility.partially) {
+ if (countdown > 0) {
+ menuItems.unshift(MenuItemsMap[key]);
+ --countdown;
+ } else {
+ actionIcons.unshift(ActionIconsMap[key]);
+ }
} else {
- actionIcons.push(ActionIconsMap[key]);
+ actionIcons.unshift(ActionIconsMap[key]);
}
});
setActionIcons(actionIcons);
setMenuItems(menuItems);
- }, [hasReachedBreakpoint, toc]);
+ }, [staticBreakpoint, toc]);
useEffect(() => {
triageActions();
- }, [hasReachedBreakpoint, triageActions]);
+ }, [staticBreakpoint, triageActions]);
return {
ActionIcons,
diff --git a/src/hooks/useEpubNavigator.ts b/src/hooks/useEpubNavigator.ts
index b2f33e3..6002221 100644
--- a/src/hooks/useEpubNavigator.ts
+++ b/src/hooks/useEpubNavigator.ts
@@ -1,7 +1,7 @@
import { useCallback, useMemo, useRef } from "react";
import Locale from "../resources/locales/en.json";
-import { RSPrefs, ScrollBackTo } from "@/preferences";
+import { RSPrefs } from "@/preferences";
import fontStacks from "readium-css/css/vars/fontStacks.json";
import { EPUBLayout, Link, Locator, Publication, ReadingProgression } from "@readium/shared";
@@ -9,11 +9,12 @@ import { EpubNavigator, EpubNavigatorListeners, FrameManager, FXLFrameManager }
import { useAppDispatch } from "@/lib/hooks";
-import { ScrollAffordance } from "@/helpers/scrollAffordance";
+import { ScrollAffordance, ScrollBackTo } from "@/helpers/scrollAffordance";
import { getOptimalLineLength, IOptimalLineLength } from "@/helpers/autoLayout/optimalLineLength";
import { autoPaginate } from "@/helpers/autoLayout/autoPaginate";
import { localData } from "@/helpers/localData";
import { setProgression } from "@/lib/publicationReducer";
+import { setDynamicBreakpoint } from "@/lib/readerReducer";
type cbb = (ok: boolean) => void;
@@ -73,28 +74,40 @@ export const useEpubNavigator = () => {
let RCSSColCount = 1;
if (colCount === "auto") {
- RCSSColCount = autoPaginate(RSPrefs.breakpoint, window.innerWidth, optimalLineLength.current.optimal);
+ RCSSColCount = autoPaginate(window.innerWidth, optimalLineLength.current.optimal);
} else if (colCount === "2") {
- const requiredWidth = ((2 * optimalLineLength.current.min) * optimalLineLength.current.fontSize);
- window.innerWidth > requiredWidth ? RCSSColCount = 2 : RCSSColCount = 1;
+ if (optimalLineLength.current.min !== null) {
+ const requiredWidth = ((2 * optimalLineLength.current.min) * optimalLineLength.current.fontSize);
+ window.innerWidth > requiredWidth ? RCSSColCount = 2 : RCSSColCount = 1;
+ } else {
+ RCSSColCount = 2;
+ }
} else {
RCSSColCount = Number(colCount);
}
- if (RSPrefs.breakpoint <= window.innerWidth) {
- const containerWithArrows = window.innerWidth - arrowsWidth.current;
- const containerWidth = RCSSColCount > 1 ? Math.min(((RCSSColCount * optimalLineLength.current.optimal) * optimalLineLength.current.fontSize), containerWithArrows) : containerWithArrows;
- container.current.style.width = `${containerWidth}px`;
+ const optimalLineLengthToPx = optimalLineLength.current.optimal * optimalLineLength.current.fontSize;
+ const containerWithArrows = window.innerWidth - arrowsWidth.current;
+ let containerWidth = window.innerWidth;
+ if (RCSSColCount > 1 && optimalLineLength.current.min !== null) {
+ containerWidth = Math.min((RCSSColCount * optimalLineLengthToPx), containerWithArrows);
+ dispatch(setDynamicBreakpoint(true));
} else {
- container.current.style.width = `${window.innerWidth}px`;
- }
+ if ((optimalLineLengthToPx + arrowsWidth.current) <= containerWithArrows) {
+ containerWidth = containerWithArrows;
+ dispatch(setDynamicBreakpoint(true));
+ } else {
+ dispatch(setDynamicBreakpoint(false));
+ }
+ };
+ container.current.style.width = `${containerWidth}px`;
applyReadiumCSSStyles({
"--USER__colCount": `${RCSSColCount}`,
"--RS__defaultLineLength": `${optimalLineLength.current.optimal}rem`
})
}
- }, [applyReadiumCSSStyles]);
+ }, [applyReadiumCSSStyles, dispatch]);
const handleScrollReflow = useCallback(() => {
if (container.current) {
@@ -107,18 +120,20 @@ export const useEpubNavigator = () => {
});
}
- if (RSPrefs.breakpoint <= window.innerWidth) {
- const containerWithArrows = window.innerWidth - arrowsWidth.current;
- container.current.style.width = `${containerWithArrows}px`;
+ container.current.style.width = `${window.innerWidth}px`;
+
+ const optimalLineLengthToPx = optimalLineLength.current.optimal * optimalLineLength.current.fontSize;
+ if (optimalLineLengthToPx <= window.innerWidth) {
+ dispatch(setDynamicBreakpoint(true));
} else {
- container.current.style.width = `${window.innerWidth}px`;
+ dispatch(setDynamicBreakpoint(false));
}
applyReadiumCSSStyles({
"--RS__defaultLineLength": `${optimalLineLength.current.optimal}rem`
})
}
- }, [applyReadiumCSSStyles]);
+ }, [applyReadiumCSSStyles, dispatch]);
// Warning: this is using an internal member that will become private, do not rely on it
// See https://github.com/readium/playground/issues/25
@@ -166,7 +181,8 @@ export const useEpubNavigator = () => {
await nav.current?.setReadingProgression(ReadingProgression.ttb);
}
mountScroll();
- }, [applyReadiumCSSStyles, mountScroll]);
+ handleScrollReflow();
+ }, [applyReadiumCSSStyles, handleScrollReflow, mountScroll]);
// Warning: this is using an internal member that will become private, do not rely on it
// See https://github.com/readium/playground/issues/25
diff --git a/src/hooks/useMediaQuery.ts b/src/hooks/useMediaQuery.ts
new file mode 100644
index 0000000..3650733
--- /dev/null
+++ b/src/hooks/useMediaQuery.ts
@@ -0,0 +1,22 @@
+import { useEffect, useState } from "react";
+
+export const useMediaQuery = (query: string | null) => {
+ const [matches, setMatches] = useState(false);
+
+ useEffect(() => {
+ if (!query) return;
+
+ const media = window.matchMedia(query);
+
+ if (media.matches !== matches) {
+ setMatches(media.matches);
+ }
+
+ const handleMatch = () => setMatches(media.matches);
+ media.addEventListener("change", handleMatch);
+
+ return () => media.removeEventListener("change", handleMatch);
+ }, [matches, query]);
+
+ return matches;
+}
\ No newline at end of file
diff --git a/src/lib/publicationReducer.ts b/src/lib/publicationReducer.ts
index 77f4d4c..4abf2ea 100644
--- a/src/lib/publicationReducer.ts
+++ b/src/lib/publicationReducer.ts
@@ -4,7 +4,7 @@ import { IProgression } from '@/components/ProgressionOf';
import { createSlice } from "@reduxjs/toolkit";
interface IPublicationState {
- runningHead: string;
+ runningHead?: string;
isFXL: boolean;
isRTL: boolean;
progression: IProgression;
@@ -13,7 +13,7 @@ interface IPublicationState {
}
const initialState: IPublicationState = {
- runningHead: Locale.reader.app.header.runningHead,
+ runningHead: undefined,
isFXL: false,
isRTL: false,
progression: {},
diff --git a/src/lib/readerReducer.ts b/src/lib/readerReducer.ts
index 6271732..3360f08 100644
--- a/src/lib/readerReducer.ts
+++ b/src/lib/readerReducer.ts
@@ -1,4 +1,5 @@
import { defaultPlatformModifier, IPlatformModifier } from "@/helpers/keyboard/getMetaKeys";
+import { StaticBreakpoints } from "@/hooks/useBreakpoints";
import { createSlice } from "@reduxjs/toolkit";
interface IReaderState {
@@ -7,7 +8,8 @@ interface IReaderState {
isFullscreen: boolean;
isPaged: boolean;
colCount: string;
- hasReachedBreakpoint: boolean;
+ hasReachedDynamicBreakpoint: boolean;
+ staticBreakpoint?: StaticBreakpoints;
settingsOpen: boolean;
tocOpen: boolean;
overflowMenuOpen: boolean;
@@ -20,7 +22,8 @@ const initialState: IReaderState = {
isFullscreen: false,
isPaged: true,
colCount: "auto",
- hasReachedBreakpoint: false,
+ hasReachedDynamicBreakpoint: false,
+ staticBreakpoint: undefined,
settingsOpen: false,
tocOpen: false,
overflowMenuOpen: false,
@@ -52,8 +55,11 @@ export const readerSlice = createSlice({
setColCount: (state, action) => {
state.colCount = action.payload
},
- setBreakpoint: (state, action) => {
- state.hasReachedBreakpoint = action.payload
+ setDynamicBreakpoint: (state, action) => {
+ state.hasReachedDynamicBreakpoint = action.payload
+ },
+ setStaticBreakpoint: (state, action) => {
+ state.staticBreakpoint = action.payload
},
setSettingsOpen: (state, action) => {
state.settingsOpen = action.payload
@@ -76,7 +82,8 @@ export const {
setFullscreen,
setPaged,
setColCount,
- setBreakpoint,
+ setDynamicBreakpoint,
+ setStaticBreakpoint,
setSettingsOpen,
setTocOpen,
setOverflowMenuOpen
diff --git a/src/preferences.ts b/src/preferences.ts
index 61503c1..e5b49c5 100644
--- a/src/preferences.ts
+++ b/src/preferences.ts
@@ -1,38 +1,23 @@
+import { StaticBreakpoints } from "./hooks/useBreakpoints";
+import { ScrollAffordancePref, ScrollBackTo } from "./helpers/scrollAffordance";
+import { ActionKeys, ActionVisibility } from "./components/Templates/ActionComponent";
import { ShortcutRepresentation } from "./components/Shortcut";
import { ShortcutMetaKeywords } from "./helpers/keyboard/getMetaKeys";
-export enum ScrollAffordancePref {
- none = "none",
- prev = "previous",
- next = "next",
- both = "both"
-}
-
-export enum ScrollBackTo {
- top = "top",
- bottom = "bottom",
- untouched = "untouched"
-}
-
-export enum ActionKeys {
- fullscreen = "fullscreen",
- jumpToPosition = "jumpToPosition",
- settings = "settings",
- toc = "toc"
-}
-
-export enum ActionVisibility {
- always = "always",
- partially = "partially",
- overflow = "overflow"
-}
-
export const RSPrefs = {
- breakpoint: 1024, // width in pixels
+ breakpoints: {
+ // See https://m3.material.io/foundations/layout/applying-layout/window-size-classes
+ [StaticBreakpoints.compact]: 600, // Phone in portrait
+ [StaticBreakpoints.medium]: 840, // Tablet in portrait, Foldable in portrait (unfolded)
+ [StaticBreakpoints.expanded]: 1200, // Phone in landscape, Tablet in landscape, Foldable in landscape (unfolded), Desktop
+ [StaticBreakpoints.large]: 1600, // Desktop
+ [StaticBreakpoints.xLarge]: null // Desktop Ultra-wide
+ },
typography: {
- minimalLineLength: 35, // number of characters. If 2 cols will switch to 1 based on this
+ minimalLineLength: 35, // undefined | null | number of characters. If 2 cols will switch to 1 based on this
optimalLineLength: 75, // number of characters. If auto layout, picks colCount based on this
pageGutter: 20 // body padding in px
+ // In the future we could have useDynamicBreakpoint: boolean so that devs can disable it and use breakpoints instead
},
scroll: {
topAffordance: ScrollAffordancePref.none,
@@ -48,10 +33,13 @@ export const RSPrefs = {
primary: "#4d4d4d",
secondary: "white",
disabled: "#767676",
- subdued: "#999999"
+ subdued: "#999999",
+ hover: "#eaeaea",
+ selected: "#eaeaea"
},
icon: {
size: 24, // Size of icons in px
+ tooltipOffset: 10 // offset of tooltip in px
}
},
shortcuts: {
@@ -61,28 +49,24 @@ export const RSPrefs = {
actions: {
displayOrder: [
ActionKeys.settings,
- // ActionKeys.fullscreen,
+ ActionKeys.fullscreen,
// ActionKeys.toc,
// ActionKeys.jumpToPosition
],
[ActionKeys.settings]: {
visibility: ActionVisibility.always,
- collapsible: false,
shortcut: `${ShortcutMetaKeywords.platform}+P`
},
[ActionKeys.fullscreen]: {
- visibility: ActionVisibility.partially,
- collapsible: true,
+ visibility: ActionVisibility.always,
shortcut: `${ShortcutMetaKeywords.platform}+F11`
},
[ActionKeys.toc]: {
visibility: ActionVisibility.partially,
- collapsible: true,
shortcut: `${ShortcutMetaKeywords.platform}+N`
},
[ActionKeys.jumpToPosition]: {
visibility: ActionVisibility.overflow,
- collapsible: false,
shortcut: `${ShortcutMetaKeywords.platform}+J`
}
}
diff --git a/src/resources/locales/en.json b/src/resources/locales/en.json
index 9ab9ad3..9598783 100644
--- a/src/resources/locales/en.json
+++ b/src/resources/locales/en.json
@@ -38,6 +38,7 @@
"tooltip": "Table of contents",
"trigger": "Toggle Table of Contents",
"close": "Close Table of Contents",
+ "heading": "Table of Contents",
"empty": "The Table of Contents was not provided for this publication."
},
"jumpToPosition": {