feat(ui): restyle core views with Material You
This commit is contained in:
@@ -23,6 +23,7 @@ import { TypographyMaxLines } from '@/base/components/texts/TypographyMaxLines.t
|
|||||||
import { MANGA_COVER_ASPECT_RATIO } from '@/features/manga/Manga.constants.ts';
|
import { MANGA_COVER_ASPECT_RATIO } from '@/features/manga/Manga.constants.ts';
|
||||||
import { GridLayout } from '@/base/Base.types.ts';
|
import { GridLayout } from '@/base/Base.types.ts';
|
||||||
import { MediaQuery } from '@/base/utils/MediaQuery.tsx';
|
import { MediaQuery } from '@/base/utils/MediaQuery.tsx';
|
||||||
|
import { MATERIAL_YOU_VISUALS } from '@/features/theme/services/MaterialYouVisualTokens.ts';
|
||||||
|
|
||||||
const BottomGradient = styled('div')({
|
const BottomGradient = styled('div')({
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -78,10 +79,15 @@ export const MangaGridCard = memo(
|
|||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
m: 0.25,
|
m: 0.25,
|
||||||
outline: selected ? '4px solid' : undefined,
|
outline: selected ? '4px solid' : undefined,
|
||||||
borderRadius: selected ? '1px' : undefined,
|
borderRadius: MATERIAL_YOU_VISUALS.cardRadius,
|
||||||
outlineColor: (theme) => theme.palette.primary.main,
|
outlineColor: (theme) => theme.palette.primary.main,
|
||||||
backgroundColor: (theme) => (selected ? theme.palette.primary.main : undefined),
|
backgroundColor: (theme) => (selected ? theme.palette.primary.main : undefined),
|
||||||
|
transition: `transform ${MATERIAL_YOU_VISUALS.standardTransition}, filter ${MATERIAL_YOU_VISUALS.standardTransition}`,
|
||||||
'@media (hover: hover) and (pointer: fine)': {
|
'@media (hover: hover) and (pointer: fine)': {
|
||||||
|
'&:hover': {
|
||||||
|
transform: `translateY(${MATERIAL_YOU_VISUALS.cardHoverLift}px)`,
|
||||||
|
filter: 'drop-shadow(0 12px 18px rgba(0, 0, 0, 0.18))',
|
||||||
|
},
|
||||||
'&:hover .manga-option-button': {
|
'&:hover .manga-option-button': {
|
||||||
visibility: 'visible',
|
visibility: 'visible',
|
||||||
pointerEvents: 'all',
|
pointerEvents: 'all',
|
||||||
@@ -100,6 +106,7 @@ export const MangaGridCard = memo(
|
|||||||
// force standard aspect ratio of manga covers
|
// force standard aspect ratio of manga covers
|
||||||
aspectRatio: MANGA_COVER_ASPECT_RATIO,
|
aspectRatio: MANGA_COVER_ASPECT_RATIO,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
borderRadius: MATERIAL_YOU_VISUALS.cardRadius,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CardActionArea
|
<CardActionArea
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ import type {
|
|||||||
MangaTitleInfo,
|
MangaTitleInfo,
|
||||||
MangaTrackRecordInfo,
|
MangaTrackRecordInfo,
|
||||||
} from '@/features/manga/Manga.types.ts';
|
} from '@/features/manga/Manga.types.ts';
|
||||||
import { applyStyles } from '@/base/utils/ApplyStyles.ts';
|
|
||||||
import { CustomIconButton } from '@/base/components/buttons/CustomIconButton.tsx';
|
import { CustomIconButton } from '@/base/components/buttons/CustomIconButton.tsx';
|
||||||
import { Sources } from '@/features/source/services/Sources.ts';
|
import { Sources } from '@/features/source/services/Sources.ts';
|
||||||
import type { SourceIdInfo } from '@/features/source/Source.types.ts';
|
import type { SourceIdInfo } from '@/features/source/Source.types.ts';
|
||||||
@@ -56,12 +55,13 @@ import { IconBrowser } from '@/assets/icons/IconBrowser.tsx';
|
|||||||
import { IconWebView } from '@/assets/icons/IconWebView.tsx';
|
import { IconWebView } from '@/assets/icons/IconWebView.tsx';
|
||||||
import { MediaQuery } from '@/base/utils/MediaQuery.tsx';
|
import { MediaQuery } from '@/base/utils/MediaQuery.tsx';
|
||||||
import { copyToClipboard } from '@/lib/HelperFunctions.ts';
|
import { copyToClipboard } from '@/lib/HelperFunctions.ts';
|
||||||
|
import { MATERIAL_YOU_VISUALS } from '@/features/theme/services/MaterialYouVisualTokens.ts';
|
||||||
|
|
||||||
const DetailsWrapper = styled('div')(({ theme }) => ({
|
const DetailsWrapper = styled('div')(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: theme.spacing(2),
|
gap: theme.spacing(3),
|
||||||
padding: theme.spacing(1),
|
padding: theme.spacing(2),
|
||||||
[theme.breakpoints.up('md')]: {
|
[theme.breakpoints.up('md')]: {
|
||||||
flexBasis: '40%',
|
flexBasis: '40%',
|
||||||
height: 'calc(100vh - 64px)',
|
height: 'calc(100vh - 64px)',
|
||||||
@@ -81,6 +81,10 @@ const TopContentWrapper = ({
|
|||||||
<Stack
|
<Stack
|
||||||
sx={{
|
sx={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
|
borderRadius: `${MATERIAL_YOU_VISUALS.detailRadius}px`,
|
||||||
|
backgroundColor: 'background.paper',
|
||||||
|
p: { xs: 1.5, sm: 2 },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{mangaThumbnailBackdrop && (
|
{mangaThumbnailBackdrop && (
|
||||||
@@ -103,23 +107,22 @@ const TopContentWrapper = ({
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
objectFit: 'cover',
|
objectFit: 'cover',
|
||||||
|
opacity: 0.18,
|
||||||
|
filter: 'blur(24px) saturate(1.2)',
|
||||||
|
transform: 'scale(1.12)',
|
||||||
}}
|
}}
|
||||||
src={url}
|
src={url}
|
||||||
alt="Manga Thumbnail"
|
alt="Manga Thumbnail"
|
||||||
/>
|
/>
|
||||||
<Stack
|
<Stack
|
||||||
sx={{
|
sx={{
|
||||||
'&::before': (theme) =>
|
'&::before': (theme) => ({
|
||||||
applyStyles(mangaThumbnailBackdrop, {
|
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
content: '""',
|
content: '""',
|
||||||
top: 0,
|
inset: 0,
|
||||||
left: 0,
|
backgroundColor: theme.alpha(theme.palette.background.paper, 0.7),
|
||||||
width: '100%',
|
backdropFilter: 'blur(10px)',
|
||||||
height: '100%',
|
|
||||||
background: `linear-gradient(to top, ${theme.palette.background.default}, transparent 100%, transparent 1px),linear-gradient(to right, ${theme.palette.background.default}, transparent 50%, transparent 1px),linear-gradient(to bottom, ${theme.palette.background.default}, transparent 50%, transparent 1px),linear-gradient(to left, ${theme.palette.background.default}, transparent 50%, transparent 1px)`,
|
|
||||||
backdropFilter: 'blur(4.5px) brightness(0.75)',
|
|
||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -131,19 +134,21 @@ const TopContentWrapper = ({
|
|||||||
|
|
||||||
const ThumbnailMetadataWrapper = styled('div')(({ theme }) => ({
|
const ThumbnailMetadataWrapper = styled('div')(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
paddingBottom: theme.spacing(1),
|
gap: theme.spacing(2),
|
||||||
|
paddingBottom: theme.spacing(2),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const MetadataContainer = styled('div')(({ theme }) => ({
|
const MetadataContainer = styled('div')({
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
marginLeft: theme.spacing(1),
|
minWidth: 0,
|
||||||
}));
|
});
|
||||||
|
|
||||||
const Metadata = (props: ComponentProps<typeof BaseMetadata>) => <BaseMetadata {...props} />;
|
const Metadata = (props: ComponentProps<typeof BaseMetadata>) => <BaseMetadata {...props} />;
|
||||||
|
|
||||||
const MangaButtonsContainer = styled('div')(({ theme }) => ({
|
const MangaButtonsContainer = styled('div')(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
gap: theme.spacing(1),
|
gap: theme.spacing(1),
|
||||||
|
flexWrap: 'wrap',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const OpenSourceButton = ({ url }: { url?: string | null }) => {
|
const OpenSourceButton = ({ url }: { url?: string | null }) => {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useTheme } from '@mui/material/styles';
|
import { useTheme } from '@mui/material/styles';
|
||||||
import { useLayoutEffect, useState } from 'react';
|
import { useLayoutEffect, useRef, useState } from 'react';
|
||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
import OpenInFullIcon from '@mui/icons-material/OpenInFull';
|
import OpenInFullIcon from '@mui/icons-material/OpenInFull';
|
||||||
import Modal from '@mui/material/Modal';
|
import Modal from '@mui/material/Modal';
|
||||||
@@ -23,6 +23,10 @@ import type { TAppThemeContext } from '@/features/theme/AppTheme.types.ts';
|
|||||||
import type { ImageRequest } from '@/lib/requests/RequestManager.ts';
|
import type { ImageRequest } from '@/lib/requests/RequestManager.ts';
|
||||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||||
import { noOp } from '@/lib/HelperFunctions.ts';
|
import { noOp } from '@/lib/HelperFunctions.ts';
|
||||||
|
import {
|
||||||
|
isDynamicColorResultCurrent,
|
||||||
|
MATERIAL_YOU_VISUALS,
|
||||||
|
} from '@/features/theme/services/MaterialYouVisualTokens.ts';
|
||||||
|
|
||||||
export const Thumbnail = ({
|
export const Thumbnail = ({
|
||||||
manga,
|
manga,
|
||||||
@@ -39,6 +43,8 @@ export const Thumbnail = ({
|
|||||||
const [isImageReady, setIsImageReady] = useState(false);
|
const [isImageReady, setIsImageReady] = useState(false);
|
||||||
|
|
||||||
const url = Mangas.getThumbnailUrl(manga);
|
const url = Mangas.getThumbnailUrl(manga);
|
||||||
|
const latestUrlRef = useRef(url);
|
||||||
|
latestUrlRef.current = url;
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!mangaDynamicColorSchemes) {
|
if (!mangaDynamicColorSchemes) {
|
||||||
@@ -56,7 +62,7 @@ export const Thumbnail = ({
|
|||||||
imageRequest = await requestManager.requestImage(url);
|
imageRequest = await requestManager.requestImage(url);
|
||||||
const image = await imageRequest.response;
|
const image = await imageRequest.response;
|
||||||
|
|
||||||
if (aborted) {
|
if (!isDynamicColorResultCurrent(url, latestUrlRef.current, aborted)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +71,7 @@ export const Thumbnail = ({
|
|||||||
img.src = image;
|
img.src = image;
|
||||||
|
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
if (aborted) {
|
if (!isDynamicColorResultCurrent(url, latestUrlRef.current, aborted)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +88,7 @@ export const Thumbnail = ({
|
|||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
]).then(([palette, averageColor]) => {
|
]).then(([palette, averageColor]) => {
|
||||||
if (aborted) {
|
if (!isDynamicColorResultCurrent(url, latestUrlRef.current, aborted)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +107,7 @@ export const Thumbnail = ({
|
|||||||
...palette,
|
...palette,
|
||||||
average: averageColor,
|
average: averageColor,
|
||||||
} as TAppThemeContext['dynamicColor']);
|
} as TAppThemeContext['dynamicColor']);
|
||||||
});
|
}).catch(noOp);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -120,7 +126,7 @@ export const Thumbnail = ({
|
|||||||
<Stack
|
<Stack
|
||||||
sx={{
|
sx={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
borderRadius: 1,
|
borderRadius: `${MATERIAL_YOU_VISUALS.cardRadius}px`,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
backgroundColor: 'background.paper',
|
backgroundColor: 'background.paper',
|
||||||
width: '125px',
|
width: '125px',
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import {
|
|||||||
useReaderStore,
|
useReaderStore,
|
||||||
} from '@/features/reader/stores/ReaderStore.ts';
|
} from '@/features/reader/stores/ReaderStore.ts';
|
||||||
import { copyToClipboard } from '@/lib/HelperFunctions.ts';
|
import { copyToClipboard } from '@/lib/HelperFunctions.ts';
|
||||||
|
import { MATERIAL_YOU_VISUALS } from '@/features/theme/services/MaterialYouVisualTokens.ts';
|
||||||
|
|
||||||
const DEFAULT_MANGA = { ...FALLBACK_MANGA, title: '' };
|
const DEFAULT_MANGA = { ...FALLBACK_MANGA, title: '' };
|
||||||
|
|
||||||
@@ -61,9 +62,12 @@ const BaseReaderOverlayHeaderMobile = ({ isVisible, ref }: MobileHeaderProps & {
|
|||||||
right: `${scrollbar.ySize}px`,
|
right: `${scrollbar.ySize}px`,
|
||||||
p: 2,
|
p: 2,
|
||||||
pt: (theme) => `max(env(safe-area-inset-top), ${theme.spacing(2)})`,
|
pt: (theme) => `max(env(safe-area-inset-top), ${theme.spacing(2)})`,
|
||||||
backgroundColor: (theme) => theme.alpha(theme.palette.background.paper, 0.95),
|
backgroundColor: (theme) =>
|
||||||
|
theme.alpha(theme.palette.background.paper, MATERIAL_YOU_VISUALS.readerChromeOpacity),
|
||||||
|
backdropFilter: 'blur(20px) saturate(1.15)',
|
||||||
pointerEvents: 'all',
|
pointerEvents: 'all',
|
||||||
boxShadow: 2,
|
borderBottom: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ReaderExitButton />
|
<ReaderExitButton />
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
useReaderSettingsStore,
|
useReaderSettingsStore,
|
||||||
useReaderStore,
|
useReaderStore,
|
||||||
} from '@/features/reader/stores/ReaderStore.ts';
|
} from '@/features/reader/stores/ReaderStore.ts';
|
||||||
|
import { MATERIAL_YOU_VISUALS } from '@/features/theme/services/MaterialYouVisualTokens.ts';
|
||||||
|
|
||||||
const useGetPreviousNavBarStaticValue = (isVisible: boolean, isStaticNav: boolean) => {
|
const useGetPreviousNavBarStaticValue = (isVisible: boolean, isStaticNav: boolean) => {
|
||||||
const wasNavBarStaticRef = useRef(isStaticNav);
|
const wasNavBarStaticRef = useRef(isStaticNav);
|
||||||
@@ -104,6 +105,12 @@ const BaseReaderNavBarDesktop = ({
|
|||||||
slotProps={{
|
slotProps={{
|
||||||
paper: {
|
paper: {
|
||||||
ref: (ref: HTMLDivElement | null) => setNavBarElement(ref),
|
ref: (ref: HTMLDivElement | null) => setNavBarElement(ref),
|
||||||
|
sx: {
|
||||||
|
m: 1,
|
||||||
|
height: 'calc(100% - 16px)',
|
||||||
|
borderRadius: `${MATERIAL_YOU_VISUALS.detailRadius}px`,
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
transition: {
|
transition: {
|
||||||
unmountOnExit: true,
|
unmountOnExit: true,
|
||||||
@@ -111,7 +118,7 @@ const BaseReaderNavBarDesktop = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ReaderNavContainer sx={{ backgroundColor: 'background.paper', pointerEvents: 'all' }}>
|
<ReaderNavContainer sx={{ backgroundColor: 'background.paper', pointerEvents: 'all' }}>
|
||||||
<Stack sx={{ p: 2, gap: 2, backgroundColor: 'action.hover' }}>
|
<Stack sx={{ p: 2, gap: 2, backgroundColor: 'action.selected' }}>
|
||||||
<Stack sx={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
<Stack sx={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<ReaderExitButton />
|
<ReaderExitButton />
|
||||||
<CustomTooltip title={t`Static navigation`}>
|
<CustomTooltip title={t`Static navigation`}>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { ReaderChapterList } from '@/features/reader/overlay/navigation/componen
|
|||||||
import { ReaderBottomBarMobileQuickSettings } from '@/features/reader/overlay/navigation/mobile/quick-settings/ReaderBottomBarMobileQuickSettings.tsx';
|
import { ReaderBottomBarMobileQuickSettings } from '@/features/reader/overlay/navigation/mobile/quick-settings/ReaderBottomBarMobileQuickSettings.tsx';
|
||||||
import { useResizeObserver } from '@/base/hooks/useResizeObserver.tsx';
|
import { useResizeObserver } from '@/base/hooks/useResizeObserver.tsx';
|
||||||
import { useReaderChaptersStore, useReaderScrollbarStore } from '@/features/reader/stores/ReaderStore.ts';
|
import { useReaderChaptersStore, useReaderScrollbarStore } from '@/features/reader/stores/ReaderStore.ts';
|
||||||
|
import { MATERIAL_YOU_VISUALS } from '@/features/theme/services/MaterialYouVisualTokens.ts';
|
||||||
|
|
||||||
const BaseReaderBottomBarMobile = ({
|
const BaseReaderBottomBarMobile = ({
|
||||||
openSettings,
|
openSettings,
|
||||||
@@ -68,9 +69,13 @@ const BaseReaderBottomBarMobile = ({
|
|||||||
ref={bottomBarRef}
|
ref={bottomBarRef}
|
||||||
sx={{
|
sx={{
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: (theme) => theme.alpha(theme.palette.background.paper, 0.95),
|
backgroundColor: (theme) =>
|
||||||
|
theme.alpha(theme.palette.background.paper, MATERIAL_YOU_VISUALS.readerChromeOpacity),
|
||||||
|
backdropFilter: 'blur(20px) saturate(1.15)',
|
||||||
pb: `max(${scrollbar.xSize}px, env(safe-area-inset-bottom))`,
|
pb: `max(${scrollbar.xSize}px, env(safe-area-inset-bottom))`,
|
||||||
boxShadow: 2,
|
borderTop: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
borderRadius: '28px 28px 0 0',
|
||||||
pointerEvents: 'all',
|
pointerEvents: 'all',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ describe('createMaterialYouThemeOptions', () => {
|
|||||||
assert.equal(options.components.MuiButton.styleOverrides.root.borderRadius, 999);
|
assert.equal(options.components.MuiButton.styleOverrides.root.borderRadius, 999);
|
||||||
assert.equal(options.components.MuiButton.styleOverrides.root.textTransform, 'none');
|
assert.equal(options.components.MuiButton.styleOverrides.root.textTransform, 'none');
|
||||||
assert.equal(options.components.MuiChip.styleOverrides.root.borderRadius, 999);
|
assert.equal(options.components.MuiChip.styleOverrides.root.borderRadius, 999);
|
||||||
|
assert.equal(options.components.MuiListItemButton.styleOverrides.root.borderRadius, 16);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses tonal surfaces instead of elevated app chrome', () => {
|
it('uses tonal surfaces instead of elevated app chrome', () => {
|
||||||
|
|||||||
@@ -121,6 +121,20 @@ export const createMaterialYouThemeOptions = (scheme: MaterialYouScheme) => ({
|
|||||||
root: { borderRadius: 16 },
|
root: { borderRadius: 16 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
MuiListItemButton: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
minHeight: 48,
|
||||||
|
marginBlock: 2,
|
||||||
|
borderRadius: 16,
|
||||||
|
transition: `background-color ${MATERIAL_YOU_MOTION.standard}`,
|
||||||
|
'&.Mui-selected': {
|
||||||
|
backgroundColor: scheme.primaryContainer,
|
||||||
|
color: scheme.onPrimaryContainer,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
MuiAppBar: {
|
MuiAppBar: {
|
||||||
defaultProps: { elevation: 0, color: 'transparent' as const },
|
defaultProps: { elevation: 0, color: 'transparent' as const },
|
||||||
styleOverrides: {
|
styleOverrides: {
|
||||||
|
|||||||
29
src/features/theme/services/MaterialYouVisualTokens.test.ts
Normal file
29
src/features/theme/services/MaterialYouVisualTokens.test.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { describe, it } from 'node:test';
|
||||||
|
import {
|
||||||
|
isDynamicColorResultCurrent,
|
||||||
|
MATERIAL_YOU_VISUALS,
|
||||||
|
} from '@/features/theme/services/MaterialYouVisualTokens.ts';
|
||||||
|
|
||||||
|
describe('Material You visual tokens', () => {
|
||||||
|
it('defines lifted cards and low-distraction reader chrome', () => {
|
||||||
|
assert.equal(MATERIAL_YOU_VISUALS.cardRadius, 20);
|
||||||
|
assert.equal(MATERIAL_YOU_VISUALS.cardHoverLift, -4);
|
||||||
|
assert.equal(MATERIAL_YOU_VISUALS.detailRadius, 24);
|
||||||
|
assert.equal(MATERIAL_YOU_VISUALS.readerChromeOpacity, 0.92);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects stale or aborted dynamic-color work', () => {
|
||||||
|
assert.equal(isDynamicColorResultCurrent('/cover/1', '/cover/1', false), true);
|
||||||
|
assert.equal(isDynamicColorResultCurrent('/cover/1', '/cover/2', false), false);
|
||||||
|
assert.equal(isDynamicColorResultCurrent('/cover/1', '/cover/1', true), false);
|
||||||
|
});
|
||||||
|
});
|
||||||
21
src/features/theme/services/MaterialYouVisualTokens.ts
Normal file
21
src/features/theme/services/MaterialYouVisualTokens.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const MATERIAL_YOU_VISUALS = {
|
||||||
|
cardRadius: 20,
|
||||||
|
cardHoverLift: -4,
|
||||||
|
detailRadius: 24,
|
||||||
|
readerChromeOpacity: 0.92,
|
||||||
|
standardTransition: '180ms cubic-bezier(0.2, 0, 0, 1)',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const isDynamicColorResultCurrent = (
|
||||||
|
requestedUrl: string,
|
||||||
|
currentUrl: string,
|
||||||
|
aborted: boolean,
|
||||||
|
): boolean => !aborted && requestedUrl === currentUrl;
|
||||||
Reference in New Issue
Block a user