feat: action bar at the bottom on mobile devices (#681)

This commit is contained in:
Huang Xin
2025-03-23 15:56:57 +08:00
committed by GitHub
parent 0d93a13407
commit ef864a32c2
30 changed files with 402 additions and 75 deletions
@@ -247,5 +247,7 @@
"Border Color": "لون الحدود",
"Border Frame": "إطار الحدود",
"Show Header": "عرض الرأس",
"Show Footer": "عرض التذييل"
"Show Footer": "عرض التذييل",
"Small": "صغير",
"Large": "كبير"
}
@@ -247,5 +247,7 @@
"Border Color": "Randfarbe",
"Border Frame": "Rahmenrand",
"Show Header": "Kopfzeile anzeigen",
"Show Footer": "Fußzeile anzeigen"
"Show Footer": "Fußzeile anzeigen",
"Small": "Klein",
"Large": "Groß"
}
@@ -247,5 +247,7 @@
"Border Color": "Χρώμα περιγράμματος",
"Border Frame": "Πλαίσιο περιγράμματος",
"Show Header": "Εμφάνιση κεφαλίδας",
"Show Footer": "Εμφάνιση υποσέλιδου"
"Show Footer": "Εμφάνιση υποσέλιδου",
"Small": "Μικρό",
"Large": "Μεγάλο"
}
@@ -247,5 +247,7 @@
"Border Color": "Color del borde",
"Border Frame": "Marco de borde",
"Show Header": "Mostrar encabezado",
"Show Footer": "Mostrar pie de página"
"Show Footer": "Mostrar pie de página",
"Small": "Pequeño",
"Large": "Grande"
}
@@ -247,5 +247,7 @@
"Border Color": "Couleur de la bordure",
"Border Frame": "Cadre de bordure",
"Show Header": "Afficher l'en-tête",
"Show Footer": "Afficher le pied de page"
"Show Footer": "Afficher le pied de page",
"Small": "Petit",
"Large": "Grand"
}
@@ -247,5 +247,7 @@
"Border Color": "बॉर्डर रंग",
"Border Frame": "बॉर्डर फ्रेम",
"Show Header": "हेडर दिखाएं",
"Show Footer": "फ़ुटर दिखाएं"
"Show Footer": "फ़ुटर दिखाएं",
"Small": "छोटा",
"Large": "बड़ा"
}
@@ -247,5 +247,7 @@
"Border Color": "Warna Garis",
"Border Frame": "Bingkai Garis",
"Show Header": "Tampilkan Header",
"Show Footer": "Tampilkan Footer"
"Show Footer": "Tampilkan Footer",
"Small": "Kecil",
"Large": "Besar"
}
@@ -247,5 +247,7 @@
"Border Color": "Colore bordo",
"Border Frame": "Cornice bordo",
"Show Header": "Mostra intestazione",
"Show Footer": "Mostra piè di pagina"
"Show Footer": "Mostra piè di pagina",
"Small": "Piccolo",
"Large": "Grande"
}
@@ -247,5 +247,7 @@
"Border Color": "ボーダーカラー",
"Border Frame": "ボーダーフレーム",
"Show Header": "ヘッダーを表示",
"Show Footer": "フッターを表示"
"Show Footer": "フッターを表示",
"Small": "小",
"Large": "大"
}
@@ -247,5 +247,7 @@
"Border Color": "테두리 색상",
"Border Frame": "테두리 프레임",
"Show Header": "헤더 표시",
"Show Footer": "푸터 표시"
"Show Footer": "푸터 표시",
"Small": "작음",
"Large": "큼"
}
@@ -247,5 +247,7 @@
"Border Color": "Kolor ramki",
"Border Frame": "Ramka",
"Show Header": "Pokaż nagłówek",
"Show Footer": "Pokaż stopkę"
"Show Footer": "Pokaż stopkę",
"Small": "Mały",
"Large": "Duży"
}
@@ -247,5 +247,7 @@
"Border Color": "Cor da Borda",
"Border Frame": "Moldura da Borda",
"Show Header": "Mostrar Cabeçalho",
"Show Footer": "Mostrar Rodapé"
"Show Footer": "Mostrar Rodapé",
"Small": "Pequeno",
"Large": "Grande"
}
@@ -247,5 +247,7 @@
"Border Color": "Цвет границы",
"Border Frame": "Граница рамки",
"Show Header": "Показать заголовок",
"Show Footer": "Показать нижний колонтитул"
"Show Footer": "Показать нижний колонтитул",
"Small": "Маленький",
"Large": "Большой"
}
@@ -247,5 +247,7 @@
"Border Color": "Sınır Rengi",
"Border Frame": "Sınır Çerçevesi",
"Show Header": "Başlık Göster",
"Show Footer": "Altbilgi Göster"
"Show Footer": "Altbilgi Göster",
"Small": "Küçük",
"Large": "Büyük"
}
@@ -247,5 +247,7 @@
"Border Color": "Колір рамки",
"Border Frame": "Рамка рамки",
"Show Header": "Показати заголовок",
"Show Footer": "Показати нижню частину"
"Show Footer": "Показати нижню частину",
"Small": "Малий",
"Large": "Великий"
}
@@ -247,5 +247,7 @@
"Border Color": "Màu viền",
"Border Frame": "Khung viền",
"Show Header": "Hiển thị tiêu đề",
"Show Footer": "Hiển thị chân trang"
"Show Footer": "Hiển thị chân trang",
"Small": "Nhỏ",
"Large": "Lớn"
}
@@ -247,5 +247,7 @@
"Border Color": "版框颜色",
"Border Frame": "页面版框",
"Show Header": "显示页眉",
"Show Footer": "显示页脚"
"Show Footer": "显示页脚",
"Small": "小",
"Large": "大"
}
@@ -247,5 +247,7 @@
"Border Color": "邊框顏色",
"Border Frame": "頁面邊框",
"Show Header": "顯示頁眉",
"Show Footer": "顯示頁腳"
"Show Footer": "顯示頁腳",
"Small": "小",
"Large": "大"
}
@@ -3,14 +3,25 @@ import clsx from 'clsx';
import { RiArrowLeftWideLine, RiArrowRightWideLine } from 'react-icons/ri';
import { RiArrowGoBackLine, RiArrowGoForwardLine } from 'react-icons/ri';
import { FaHeadphones } from 'react-icons/fa6';
import { IoIosList as TOCIcon } from 'react-icons/io';
import { PiNotePencil as NoteIcon } from 'react-icons/pi';
import { RxSlider as SliderIcon } from 'react-icons/rx';
import { RiFontFamily as FontIcon } from 'react-icons/ri';
import { MdOutlineHeadphones as TTSIcon } from 'react-icons/md';
import { TbBoxMargin } from 'react-icons/tb';
import { RxLineHeight } from 'react-icons/rx';
import { useEnv } from '@/context/EnvContext';
import { useReaderStore } from '@/store/readerStore';
import { useSidebarStore } from '@/store/sidebarStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { eventDispatcher } from '@/utils/event';
import { saveViewSettings } from '../utils/viewSettingsHelper';
import { PageInfo } from '@/types/book';
import { getStyles } from '@/utils/style';
import Button from '@/components/Button';
import Slider from '@/components/Slider';
interface FooterBarProps {
bookKey: string;
@@ -28,17 +39,44 @@ const FooterBar: React.FC<FooterBarProps> = ({
isHoveredAnim,
}) => {
const _ = useTranslation();
const { appService } = useEnv();
const { envConfig, appService } = useEnv();
const { hoveredBookKey, setHoveredBookKey, getView, getProgress, getViewSettings } =
useReaderStore();
const { isSideBarVisible } = useSidebarStore();
const { isSideBarVisible, setSideBarVisible } = useSidebarStore();
const [actionTab, setActionTab] = React.useState('');
const sliderHeight = useResponsiveSize(28);
const tocIconSize = useResponsiveSize(23);
const fontIconSize = useResponsiveSize(18);
const marginIconSize = useResponsiveSize(20);
const view = getView(bookKey);
const progress = getProgress(bookKey);
const viewSettings = getViewSettings(bookKey);
const handleProgressChange = (event: React.ChangeEvent) => {
const newProgress = parseInt((event.target as HTMLInputElement).value, 10);
view?.goToFraction(newProgress / 100.0);
const handleProgressChange = (value: number) => {
view?.goToFraction(value / 100.0);
};
const handleFontSizeChange = (value: number) => {
saveViewSettings(envConfig, bookKey, 'defaultFontSize', value);
view?.renderer.setStyles?.(getStyles(viewSettings!));
};
const handleMarginChange = (value: number) => {
const marginPx = Math.round((value / 100) * 88);
const gapPercent = Math.round((value / 100) * 10);
saveViewSettings(envConfig, bookKey, 'marginPx', marginPx);
saveViewSettings(envConfig, bookKey, 'gapPercent', gapPercent);
view?.renderer.setAttribute('margin', `${marginPx}px`);
view?.renderer.setAttribute('gap', `${gapPercent}%`);
if (viewSettings?.scrolled) {
view?.renderer.setAttribute('flow', 'scrolled');
}
};
const handleLineHeightChange = (value: number) => {
saveViewSettings(envConfig, bookKey, 'lineHeight', value / 10);
view?.renderer.setStyles?.(getStyles(viewSettings!));
};
const handleGoPrev = () => {
@@ -67,6 +105,31 @@ const FooterBar: React.FC<FooterBarProps> = ({
}
};
const handleSetActionTab = (tab: string) => {
console.log('handleSetActionTab', tab);
setActionTab(actionTab === tab ? '' : tab);
if (tab === 'tts') {
setHoveredBookKey('');
handleSpeakText();
} else if (tab === 'toc') {
setHoveredBookKey('');
if (viewSettings) {
viewSettings.sideBarTab = 'toc';
}
setSideBarVisible(true);
} else if (tab === 'note') {
setHoveredBookKey('');
setSideBarVisible(true);
if (viewSettings) {
viewSettings.sideBarTab = 'annotations';
}
}
};
const getMarginProgressValue = (marginPx: number, gapPercent: number) => {
return (marginPx / 88 + gapPercent / 10) * 50;
};
const isVisible = hoveredBookKey === bookKey;
const progressInfo = bookFormat === 'PDF' ? section : pageinfo;
const progressValid = !!progressInfo;
@@ -78,59 +141,182 @@ const FooterBar: React.FC<FooterBarProps> = ({
<>
<div
className={clsx(
'absolute bottom-0 left-0 z-10 h-12 w-full',
viewSettings?.vertical && viewSettings?.scrolled ? 'bottom-3 !h-6' : '',
'absolute bottom-0 left-0 z-10 h-12 w-full sm:h-[52px]',
// show scroll bar when vertical and scrolled in desktop
viewSettings?.vertical && viewSettings?.scrolled && 'sm:!bottom-3 sm:!h-7',
)}
onMouseEnter={() => !appService?.isMobile && setHoveredBookKey(bookKey)}
onTouchStart={() => !appService?.isMobile && setHoveredBookKey(bookKey)}
/>
<div
className={clsx(
'footer-bar absolute bottom-0 z-10 flex h-12 w-full items-center gap-x-4 px-4',
'shadow-xs bg-base-100 transition-opacity duration-300',
appService?.hasSafeAreaInset && 'pb-[env(safe-area-inset-bottom)]',
'footer-bar shadow-xs absolute bottom-0 z-50 flex w-full flex-col sm:h-[52px]',
'sm:bg-base-100 border-base-300/50 border sm:border-none',
'transition-[opacity,transform] duration-300',
appService?.hasRoundedWindow && 'rounded-window-bottom-right',
!isSideBarVisible && appService?.hasRoundedWindow && 'rounded-window-bottom-left',
isHoveredAnim && 'hover-bar-anim',
viewSettings?.vertical && viewSettings?.scrolled ? 'mb-3 !h-6' : '',
isVisible ? `pointer-events-auto opacity-100` : `pointer-events-none opacity-0`,
// show scroll bar when vertical and scrolled in desktop
viewSettings?.vertical &&
viewSettings?.scrolled &&
'sm:!bottom-3 sm:!h-7 sm:justify-center',
isVisible
? `pointer-events-auto translate-y-0 opacity-100`
: `pointer-events-none translate-y-full opacity-0 sm:translate-y-0`,
)}
dir={viewSettings?.rtl ? 'rtl' : 'ltr'}
onMouseLeave={() => setHoveredBookKey('')}
onMouseLeave={() => window.innerWidth >= 640 && setHoveredBookKey('')}
aria-hidden={!isVisible}
>
<div className='hidden sm:flex'>
{/* Mobile footer bar */}
<div
className={clsx(
'bg-base-200 absolute bottom-16 flex w-full items-center gap-x-2 px-4 transition-all sm:hidden',
actionTab === 'progress'
? 'pointer-events-auto translate-y-0 pb-4 pt-8 ease-out'
: 'pointer-events-none invisible translate-y-full overflow-hidden pb-0 pt-0 ease-in',
)}
style={{
bottom: appService?.hasSafeAreaInset
? 'calc(env(safe-area-inset-bottom) + 64px)'
: '64px',
}}
>
<Button
icon={viewSettings?.rtl ? <RiArrowRightWideLine /> : <RiArrowLeftWideLine />}
onClick={viewSettings?.rtl ? handleGoNext : handleGoPrev}
tooltip={viewSettings?.rtl ? _('Go Right') : _('Go Left')}
/>
<Button
icon={viewSettings?.rtl ? <RiArrowGoForwardLine /> : <RiArrowGoBackLine />}
onClick={handleGoBack}
tooltip={_('Go Back')}
disabled={!view?.history.canGoBack}
/>
<Button
icon={viewSettings?.rtl ? <RiArrowGoBackLine /> : <RiArrowGoForwardLine />}
onClick={handleGoForward}
tooltip={_('Go Forward')}
disabled={!view?.history.canGoForward}
/>
<Slider
heightPx={sliderHeight}
bubbleLabel={`${Math.round(progressFraction * 100)}%`}
initialValue={progressValid ? progressFraction * 100 : 0}
onChange={(e) => handleProgressChange(e)}
/>
<Button
icon={viewSettings?.rtl ? <RiArrowLeftWideLine /> : <RiArrowRightWideLine />}
onClick={viewSettings?.rtl ? handleGoPrev : handleGoNext}
tooltip={viewSettings?.rtl ? _('Go Left') : _('Go Right')}
/>
</div>
<Button
icon={viewSettings?.rtl ? <RiArrowGoForwardLine /> : <RiArrowGoBackLine />}
onClick={handleGoBack}
tooltip={_('Go Back')}
disabled={!view?.history.canGoBack}
/>
<Button
icon={viewSettings?.rtl ? <RiArrowGoBackLine /> : <RiArrowGoForwardLine />}
onClick={handleGoForward}
tooltip={_('Go Forward')}
disabled={!view?.history.canGoForward}
/>
<span className='mx-2 text-center text-sm'>
{progressValid ? `${Math.round(progressFraction * 100)}%` : ''}
</span>
<input
type='range'
className='text-base-content mx-2 w-full'
min={0}
max={100}
value={progressValid ? progressFraction * 100 : 0}
onChange={(e) => handleProgressChange(e)}
/>
<Button icon={<FaHeadphones />} onClick={handleSpeakText} tooltip={_('Speak')} />
<div className='hidden sm:flex'>
<div
className={clsx(
'bg-base-200 absolute flex w-full flex-col items-center gap-y-8 px-4 transition-all sm:hidden',
actionTab === 'font'
? 'pointer-events-auto translate-y-0 pb-4 pt-8 ease-out'
: 'pointer-events-none invisible translate-y-full overflow-hidden pb-0 pt-0 ease-in',
)}
style={{
bottom: appService?.hasSafeAreaInset
? 'calc(env(safe-area-inset-bottom) + 64px)'
: '64px',
}}
>
<Slider
initialValue={viewSettings?.defaultFontSize ?? 16}
bubbleLabel={`${viewSettings?.defaultFontSize ?? 16}`}
minLabel='A'
maxLabel='A'
minClassName='text-xs'
maxClassName='text-base'
onChange={handleFontSizeChange}
min={8}
max={30}
/>
<div className='flex w-full items-center justify-between gap-x-6'>
<Slider
initialValue={getMarginProgressValue(
viewSettings?.marginPx ?? 44,
viewSettings?.gapPercent ?? 5,
)}
bubbleElement={<TbBoxMargin size={marginIconSize} />}
minLabel={_('Small')}
maxLabel={_('Large')}
step={10}
onChange={handleMarginChange}
/>
<Slider
initialValue={(viewSettings?.lineHeight ?? 1.6) * 10}
bubbleElement={<RxLineHeight size={marginIconSize} />}
minLabel={_('Small')}
maxLabel={_('Large')}
min={8}
max={24}
onChange={handleLineHeightChange}
/>
</div>
</div>
<div
className={clsx(
'bg-base-200 z-50 mt-auto flex w-full justify-between px-8 py-4 sm:hidden',
appService?.hasSafeAreaInset && 'pb-[calc(env(safe-area-inset-bottom)+16px)]',
)}
>
<Button
icon={<TOCIcon size={tocIconSize} className='' />}
onClick={() => handleSetActionTab('toc')}
/>
<Button icon={<NoteIcon className='' />} onClick={() => handleSetActionTab('note')} />
<Button
icon={<SliderIcon className={clsx(actionTab === 'progress' && 'text-blue-500')} />}
onClick={() => handleSetActionTab('progress')}
/>
<Button
icon={
<FontIcon
size={fontIconSize}
className={clsx(actionTab === 'font' && 'text-blue-500')}
/>
}
onClick={() => handleSetActionTab('font')}
/>
<Button icon={<TTSIcon className='' />} onClick={() => handleSetActionTab('tts')} />
</div>
{/* Desktop footer bar */}
<div className='hidden w-full items-center gap-x-4 px-4 sm:flex'>
<Button
icon={viewSettings?.rtl ? <RiArrowRightWideLine /> : <RiArrowLeftWideLine />}
onClick={viewSettings?.rtl ? handleGoNext : handleGoPrev}
tooltip={viewSettings?.rtl ? _('Go Right') : _('Go Left')}
/>
<Button
icon={viewSettings?.rtl ? <RiArrowGoForwardLine /> : <RiArrowGoBackLine />}
onClick={handleGoBack}
tooltip={_('Go Back')}
disabled={!view?.history.canGoBack}
/>
<Button
icon={viewSettings?.rtl ? <RiArrowGoBackLine /> : <RiArrowGoForwardLine />}
onClick={handleGoForward}
tooltip={_('Go Forward')}
disabled={!view?.history.canGoForward}
/>
<span className='mx-2 text-center text-sm'>
{progressValid ? `${Math.round(progressFraction * 100)}%` : ''}
</span>
<input
type='range'
className='text-base-content mx-2 w-full'
min={0}
max={100}
value={progressValid ? progressFraction * 100 : 0}
onChange={(e) =>
handleProgressChange(parseInt((e.target as HTMLInputElement).value, 10))
}
/>
<Button icon={<FaHeadphones />} onClick={handleSpeakText} tooltip={_('Speak')} />
<Button
icon={viewSettings?.rtl ? <RiArrowLeftWideLine /> : <RiArrowRightWideLine />}
onClick={viewSettings?.rtl ? handleGoPrev : handleGoNext}
@@ -86,8 +86,10 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
onMouseEnter={() => setHoveredBookKey(bookKey)}
onMouseLeave={() => setHoveredBookKey('')}
>
<div className='sidebar-bookmark-toggler bg-base-100 z-20 flex h-full items-center space-x-4'>
<SidebarToggler bookKey={bookKey} />
<div className='sidebar-bookmark-toggler bg-base-100 z-20 flex h-full items-center gap-x-4'>
<div className='hidden sm:flex'>
<SidebarToggler bookKey={bookKey} />
</div>
<BookmarkToggler bookKey={bookKey} />
</div>
@@ -1,5 +1,6 @@
import clsx from 'clsx';
import React from 'react';
import { useEnv } from '@/context/EnvContext';
import { useTranslation } from '@/hooks/useTranslation';
import { PageInfo } from '@/types/book';
@@ -23,6 +24,7 @@ const PageInfoView: React.FC<PageInfoProps> = ({
verticalMargin,
}) => {
const _ = useTranslation();
const { appService } = useEnv();
const pageInfo =
bookFormat === 'PDF'
? section
@@ -54,7 +56,10 @@ const PageInfoView: React.FC<PageInfoProps> = ({
width: showDoubleBorder ? '32px' : `${horizontalGap}%`,
height: `calc(100% - ${verticalMargin * 2}px)`,
}
: { insetInlineEnd: `${horizontalGap}%` }
: {
insetInlineEnd: `${horizontalGap}%`,
paddingBottom: appService?.hasSafeAreaInset ? 'env(safe-area-inset-bottom)' : 0,
}
}
>
<h2 className='text-neutral-content text-right font-sans text-xs font-extralight'>
@@ -78,7 +78,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const transPopupHeight = Math.min(360, maxHeight);
const annotPopupWidth = useResponsiveSize(300);
const annotPopupHeight = useResponsiveSize(44);
const androidSelectionHandlerHeight = 8;
const androidSelectionHandlerHeight = 0;
const onLoad = (event: Event) => {
const detail = (event as CustomEvent).detail;
@@ -114,7 +114,7 @@ const WikipediaPopup: React.FC<WikipediaPopupProps> = ({
<main className='flex-grow overflow-y-auto px-2 font-sans'></main>
<footer className='mt-auto hidden data-[state=loaded]:block data-[state=error]:hidden data-[state=loading]:hidden'>
<div className='flex items-center px-4 py-2 text-sm opacity-60'>
From Wikipedia, released under the CC BY-SA License.
Source: Wikipedia (CC BY-SA)
</div>
</footer>
</div>
@@ -169,7 +169,7 @@ const WiktionaryPopup: React.FC<WiktionaryPopupProps> = ({
<main className='flex-grow overflow-y-auto p-4 font-sans' />
<footer className='mt-auto hidden data-[state=loaded]:block data-[state=error]:hidden data-[state=loading]:hidden'>
<div className='flex items-center px-4 py-2 text-sm opacity-60'>
From Wiktionary, released under the CC BY-SA License.
Source: Wiktionary (CC BY-SA)
</div>
</footer>
</div>
@@ -38,7 +38,10 @@ const SidebarHeader: React.FC<{
>
<MdArrowBackIosNew size={iconSize22} />
</button>
<button className='btn btn-ghost h-8 min-h-8 w-8 p-0' onClick={onGoToLibrary}>
<button
className='btn btn-ghost hidden h-8 min-h-8 w-8 p-0 sm:flex'
onClick={onGoToLibrary}
>
<GiBookshelf className='fill-base-content' />
</button>
</div>
@@ -68,7 +68,7 @@ const TOCItemView: React.FC<{
aria-selected={isActive ? 'true' : 'false'}
data-href={item.href ? getContentMd5(item.href) : undefined}
className={`flex w-full cursor-pointer items-center rounded-md py-2 ${
isActive ? 'bg-base-300/85 hover:bg-base-300' : 'hover:bg-base-300/85'
isActive ? 'bg-base-300/85 hover:bg-base-300' : 'sm:hover:bg-base-300/85'
}`}
>
{item.subitems && (
@@ -1,6 +1,8 @@
import clsx from 'clsx';
import React from 'react';
import { MdToc, MdEditNote, MdBookmarkBorder } from 'react-icons/md';
import { MdBookmarkBorder as BookmarkIcon } from 'react-icons/md';
import { IoIosList as TOCIcon } from 'react-icons/io';
import { PiNotePencil as NoteIcon } from 'react-icons/pi';
import { useTranslation } from '@/hooks/useTranslation';
@@ -34,13 +36,13 @@ const TabNavigation: React.FC<{
tab === 'toc' ? _('TOC') : tab === 'annotations' ? _('Annotate') : _('Bookmark')
}
>
<div className={clsx('')} onClick={() => onTabChange(tab)}>
<div className={clsx('flex h-6 items-center')} onClick={() => onTabChange(tab)}>
{tab === 'toc' ? (
<MdToc className='mx-auto' />
<TOCIcon className='mx-auto' />
) : tab === 'annotations' ? (
<MdEditNote className='mx-auto' />
<NoteIcon className='mx-auto' />
) : (
<MdBookmarkBorder className='mx-auto' />
<BookmarkIcon className='mx-auto' />
)}
</div>
</div>
+3 -3
View File
@@ -169,9 +169,9 @@ const ProfilePage = () => {
>
<IoArrowBack className='text-base-content' />
</button>
<div className='w-full max-w-4xl px-6 py-10'>
<div className='w-full max-w-4xl px-4 py-10'>
<div className='bg-base-200 overflow-hidden rounded-lg p-2 shadow-md sm:p-6'>
<div className='p-6'>
<div className='p-2 sm:p-6'>
<div className='mb-8 flex flex-col items-center gap-x-6 gap-y-4 md:flex-row md:items-start'>
<div className='flex-shrink-0'>
{avatarUrl ? (
@@ -202,7 +202,7 @@ const ProfilePage = () => {
</div>
</div>
<div className='bg-base-100 mb-8 rounded-lg p-6'>
<div className='bg-base-100 mb-8 rounded-lg px-4 py-6'>
<h3 className='text-base-content mb-2 text-lg font-semibold'>{_('Features')}</h3>
<div className='mt-6'>
<ul className='text-base-content/60 grid grid-cols-1 gap-2 md:grid-cols-2'>
@@ -0,0 +1,93 @@
import React, { useEffect, useState } from 'react';
interface SliderProps {
min?: number;
max?: number;
step?: number;
initialValue?: number;
heightPx?: number;
minLabel?: string;
maxLabel?: string;
bubbleElement?: React.ReactNode;
bubbleLabel?: string;
className?: string;
minClassName?: string;
maxClassName?: string;
bubbleClassName?: string;
onChange?: (value: number) => void;
}
const Slider: React.FC<SliderProps> = ({
min = 0,
max = 100,
step = 1,
initialValue = 50,
heightPx = 40,
minLabel = '',
maxLabel = '',
bubbleElement,
bubbleLabel = '',
className = '',
minClassName = '',
maxClassName = '',
bubbleClassName = '',
onChange,
}) => {
const [value, setValue] = useState(initialValue);
const handleChange = (e: React.ChangeEvent) => {
const newValue = parseInt((e.target as HTMLInputElement).value, 10);
setValue(newValue);
if (onChange) {
onChange(newValue);
}
};
useEffect(() => {
setValue(initialValue);
}, [initialValue]);
const percentage = ((value - min) / (max - min)) * 100;
return (
<div className={`slider bg-base-200 mx-auto w-full max-w-md rounded-xl ${className}`}>
<div className='relative' style={{ height: `${heightPx}px` }}>
<div className='bg-base-300/40 absolute h-full w-full rounded-full'></div>
<div
className='bg-base-300 absolute h-full rounded-full'
style={{ width: percentage > 0 ? `calc(${percentage}% + ${heightPx / 2}px)` : '0px' }}
></div>
<div className='absolute inset-0 flex items-center justify-between px-4 text-sm'>
<span className={`ml-2 ${minClassName}`}>{minLabel}</span>
<span className={`mr-2 ${maxClassName}`}>{maxLabel}</span>
</div>
<div
className='pointer-events-none absolute top-0 z-10'
style={{
left: `max(${heightPx / 2}px, calc(${percentage}%))`,
transform: 'translateX(calc(-50%))',
height: '100%',
}}
>
<div
className={`bg-base-200 flex h-full items-center justify-center rounded-full text-sm shadow-md ${bubbleClassName}`}
style={{ width: `${heightPx}px` }}
>
{bubbleElement || bubbleLabel}
</div>
</div>
<input
type='range'
min={min}
max={max}
step={step}
value={value}
className='absolute inset-0 h-full w-full cursor-pointer opacity-0'
onChange={handleChange}
/>
</div>
</div>
);
};
export default Slider;
+2 -2
View File
@@ -268,8 +268,8 @@ export const getFootnoteStyles = () => `
}
p, li, blockquote, dd {
margin: unset;
text-indent: unset;
margin: unset !important;
text-indent: unset !important;
}
`;