feat(theme): init project

This commit is contained in:
2025-09-17 09:37:16 +08:00
commit 99b5383f84
23 changed files with 1651 additions and 0 deletions

View File

@@ -0,0 +1,32 @@
import SmartLink from '@/components/SmartLink'
/**
* 上一篇,下一篇文章
* @param {prev,next} param0
* @returns
*/
export default function ArticleAround({ prev, next }) {
if (!prev || !next) {
return <></>
}
return (
<section className='text-gray-800 dark:text-gray-400 h-12 flex items-center justify-between space-x-5 my-4'>
{prev && <SmartLink
href={`/${prev.slug}`}
passHref
className='text-sm cursor-pointer justify-start items-center flex hover:underline duration-300'>
<i className='mr-1 fas fa-angle-double-left' />{prev.title}
</SmartLink>}
{next && <SmartLink
href={`/${next.slug}`}
passHref
className='text-sm cursor-pointer justify-end items-center flex hover:underline duration-300'>
{next.title}
<i className='ml-1 my-1 fas fa-angle-double-right' />
</SmartLink>}
</section>
)
}

View File

@@ -0,0 +1,65 @@
import SmartLink from '@/components/SmartLink'
import { useGlobal } from '@/lib/global'
import CONFIG from '../config'
import { siteConfig } from '@/lib/config'
import { formatDateFmt } from '@/lib/utils/formatDate'
import NotionIcon from '@/components/NotionIcon'
/**
* 文章描述
* @param {*} props
* @returns
*/
export default function ArticleInfo(props) {
const { post } = props
const { locale } = useGlobal()
return (
<section className='mt-2 text-gray-600 dark:text-gray-400 leading-8'>
<h2 className='blog-item-title mb-5 font-bold text-black text-xl md:text-2xl no-underline'>
{siteConfig('POST_TITLE_ICON') && <NotionIcon icon={post?.pageIcon} />}
{post?.title}
</h2>
<div className='flex flex-wrap text-[var(--primary-color)] dark:text-gray-300'>
{post?.type !== 'Page' && (
<header className='text-md text-[var(--primary-color)] dark:text-gray-300 flex-wrap flex items-center leading-6'>
<div className='space-x-2'>
<span className='text-sm'>
发布于
<SmartLink
className='p-1 hover:text-red-400 transition-all duration-200'
href={`/archive#${formatDateFmt(post?.publishDate, 'yyyy-MM')}`}>
{post.date?.start_date || post.createdTime}
</SmartLink>
</span>
</div>
<div className='text-sm'>
{/* {post.category && (
<SmartLink href={`/category/${post.category}`} className='p-1'>
{' '}
<span className='hover:text-red-400 transition-all duration-200'>
<i className='fa-regular fa-folder mr-0.5' />
{post.category}
</span>
</SmartLink>
)} */}
{post?.tags &&
post?.tags?.length > 0 &&
post?.tags.map(t => (
<SmartLink
key={t}
href={`/tag/${t}`}
className=' hover:text-red-400 transition-all duration-200'>
<span> #{t}</span>
</SmartLink>
))}
</div>
</header>
)}
</div>
</section>
)
}

View File

@@ -0,0 +1,52 @@
import { useGlobal } from '@/lib/global'
import { useEffect, useRef } from 'react'
/**
* 加密文章校验组件
* @param {password, validPassword} props
* @param password 正确的密码
* @param validPassword(bool) 回调函数校验正确回调入参为true
* @returns
*/
export default function ArticleLock (props) {
const { validPassword } = props
const { locale } = useGlobal()
const submitPassword = () => {
const p = document.getElementById('password')
if (!validPassword(p?.value)) {
const tips = document.getElementById('tips')
if (tips) {
tips.innerHTML = ''
tips.innerHTML = `<div class='text-red-500 animate__shakeX animate__animated'>${locale.COMMON.PASSWORD_ERROR}</div>`
}
}
}
const passwordInputRef = useRef(null)
useEffect(() => {
// 选中密码输入框并将其聚焦
passwordInputRef.current.focus()
}, [])
return <div id='container' className='w-full flex justify-center items-center h-96 '>
<div className='text-center space-y-3'>
<div className='font-bold'>{locale.COMMON.ARTICLE_LOCK_TIPS}</div>
<div className='flex mx-4'>
<input id="password" type='password'
onKeyDown={(e) => {
if (e.key === 'Enter') {
submitPassword()
}
}}
ref={passwordInputRef} // 绑定ref到passwordInputRef变量
className='outline-none w-full text-sm pl-5 rounded-l transition focus:shadow-lg font-light leading-10 text-black dark:bg-gray-500 bg-gray-50'
></input>
<div onClick={submitPassword} className="px-3 whitespace-nowrap cursor-pointer items-center justify-center py-2 rounded-r duration-300 bg-gray-300" >
<i className={'duration-200 cursor-pointer fas fa-key dark:text-black'} >&nbsp;{locale.COMMON.SUBMIT}</i>
</div>
</div>
<div id='tips'>
</div>
</div>
</div>
}

View File

@@ -0,0 +1,36 @@
import SmartLink from '@/components/SmartLink'
/**
* 归档分组文章
* @param {*} param0
* @returns
*/
export default function BlogArchiveItem({ archiveTitle, archivePosts }) {
return (
<div key={archiveTitle} className='pb-16'>
<div id={archiveTitle} className='text-[#111827] opacity-30 pb-2 text-3xl dark:text-gray-300'>
{archiveTitle}
</div>
<ul>
{archivePosts.map(post => {
return (
<li
key={post.id}
className='p-1 pl-0 text-base items-center mb-3'>
<div id={post?.publishDay} className='flex justify-between'>
<SmartLink
href={post?.href}
passHref
className='dark:text-gray-400 dark:hover:text-gray-300 overflow-x-hidden cursor-pointer text-[#111827] font-bold'>
{post.title}
</SmartLink>
<span className='text-gray-400'>{post.date?.start_date}</span>
</div>
</li>
)
})}
</ul>
</div>
)
}

View File

@@ -0,0 +1,101 @@
import LazyImage from '@/components/LazyImage'
import NotionIcon from '@/components/NotionIcon'
import NotionPage from '@/components/NotionPage'
import TwikooCommentCount from '@/components/TwikooCommentCount'
import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global'
import { formatDateFmt } from '@/lib/utils/formatDate'
import SmartLink from '@/components/SmartLink'
import CONFIG from '../config'
export const BlogItem = props => {
const { post } = props
const { NOTION_CONFIG } = useGlobal()
const showPageCover = siteConfig('TYPOGRAPHY_POST_COVER_ENABLE', false, CONFIG)
const showPreview =
siteConfig('POST_LIST_PREVIEW', false, NOTION_CONFIG) && post.blockMap
return (
<div key={post.id} className='h-42 mt-6 mb-10'>
{/* 文章标题 */}
<div className='flex'>
<div className='article-cover h-full'>
{/* 图片封面 */}
{showPageCover && (
<div className='overflow-hidden mr-2 w-56 h-full'>
<SmartLink href={post.href} passHref legacyBehavior>
<LazyImage
src={post?.pageCoverThumbnail}
className='w-56 h-full object-cover object-center group-hover:scale-110 duration-500'
/>
</SmartLink>
</div>
)}
</div>
<article className='article-info'>
<h2 className='mb-2'>
<SmartLink
href={post.href}
className='text-xl underline decoration-2 font-bold text-[var(--primary-color)] dark:text-white dark:hover:bg-white dark:hover:text-[var(--primary-color)] duration-200 transition-all rounded-sm'>
{siteConfig('POST_TITLE_ICON') && (
<NotionIcon icon={post.pageIcon} />
)}
{post.title}
</SmartLink>
</h2>
{/* 文章信息 */}
<header className='text-md text-[var(--primary-color)] dark:text-gray-300 flex-wrap flex items-center leading-6'>
<div className='space-x-2'>
<span className='text-sm'>
发布于
<SmartLink
className='p-1 hover:text-red-400 transition-all duration-200'
href={`/archive#${formatDateFmt(post?.publishDate, 'yyyy-MM')}`}>
{post.date?.start_date || post.createdTime}
</SmartLink>
</span>
</div>
<div className='text-sm'>
{/* {post.category && (
<SmartLink href={`/category/${post.category}`} className='p-1'>
{' '}
<span className='hover:text-red-400 transition-all duration-200'>
<i className='fa-regular fa-folder mr-0.5' />
{post.category}
</span>
</SmartLink>
)} */}
{post?.tags &&
post?.tags?.length > 0 &&
post?.tags.map(t => (
<SmartLink
key={t}
href={`/tag/${t}`}
className=' hover:text-red-400 transition-all duration-200'>
<span> #{t}</span>
</SmartLink>
))}
</div>
</header>
<main className='text-[var(--primary-color)] dark:text-gray-300 line-clamp-4 overflow-hidden text-ellipsis relative leading-[1.7]'>
{!showPreview && (
<>
{post.summary}
</>
)}
{showPreview && post?.blockMap && (
<div className='line-clamp-4 overflow-hidden'>
<NotionPage post={post} />
<hr className='border-dashed py-4' />
</div>
)}
</main>
</article>
</div>
</div>
)
}

View File

@@ -0,0 +1,74 @@
import { AdSlot } from '@/components/GoogleAdsense'
import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global'
import SmartLink from '@/components/SmartLink'
import { useRouter } from 'next/router'
import CONFIG from '../config'
import { BlogItem } from './BlogItem'
/**
* 博客列表
* @param {*} props
* @returns
*/
export default function BlogListPage(props) {
const { page = 1, posts, postCount } = props
const router = useRouter()
const { NOTION_CONFIG } = useGlobal()
const POSTS_PER_PAGE = siteConfig('POSTS_PER_PAGE', null, NOTION_CONFIG)
const totalPage = Math.ceil(postCount / POSTS_PER_PAGE)
const currentPage = +page
// 博客列表嵌入广告
const TYPOGRAPHY_POST_AD_ENABLE = siteConfig(
'TYPOGRAPHY_POST_AD_ENABLE',
false,
CONFIG
)
const showPrev = currentPage > 1
const showNext = page < totalPage
const pagePrefix = router.asPath
.split('?')[0]
.replace(/\/page\/[1-9]\d*/, '')
.replace(/\/$/, '')
.replace('.html', '')
return (
<div className='w-full md:pr-8 mb-12 px-5'>
<div id='posts-wrapper'>
{posts?.map((p, index) => (
<div key={p.id}>
{TYPOGRAPHY_POST_AD_ENABLE && (index + 1) % 3 === 0 && (
<AdSlot type='in-article' />
)}
{TYPOGRAPHY_POST_AD_ENABLE && index + 1 === 4 && <AdSlot type='flow' />}
<BlogItem post={p} />
</div>
))}
</div>
<div className='flex justify-between text-xs mt-1'>
<SmartLink
href={{
pathname:
currentPage - 1 === 1
? `${pagePrefix}/`
: `${pagePrefix}/page/${currentPage - 1}`,
query: router.query.s ? { s: router.query.s } : {}
}}
className={`${showPrev ? 'text-blue-600 border-b border-blue-400 visible ' : ' invisible bg-gray pointer-events-none '} no-underline pb-1 px-3`}>
NEWER POSTS <i className='fa-solid fa-arrow-left'></i>
</SmartLink>
<SmartLink
href={{
pathname: `${pagePrefix}/page/${currentPage + 1}`,
query: router.query.s ? { s: router.query.s } : {}
}}
className={`${showNext ? 'text-blue-600 border-b border-blue-400 visible' : ' invisible bg-gray pointer-events-none '} no-underline pb-1 px-3`}>
OLDER POSTS <i className='fa-solid fa-arrow-right'></i>
</SmartLink>
</div>
</div>
)
}

View File

@@ -0,0 +1,70 @@
import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global'
import throttle from 'lodash.throttle'
import { useCallback, useEffect, useRef, useState } from 'react'
import { BlogItem } from './BlogItem'
/**
* 滚动博客列表
* @param {*} props
* @returns
*/
export default function BlogListScroll(props) {
const { posts } = props
const { locale, NOTION_CONFIG } = useGlobal()
const [page, updatePage] = useState(1)
const POSTS_PER_PAGE = siteConfig('POSTS_PER_PAGE', null, NOTION_CONFIG)
let hasMore = false
const postsToShow = posts
? Object.assign(posts).slice(0, POSTS_PER_PAGE * page)
: []
if (posts) {
const totalCount = posts.length
hasMore = page * POSTS_PER_PAGE < totalCount
}
const handleGetMore = () => {
if (!hasMore) return
updatePage(page + 1)
}
const targetRef = useRef(null)
// 监听滚动自动分页加载
const scrollTrigger = useCallback(
throttle(() => {
const scrollS = window.scrollY + window.outerHeight
const clientHeight = targetRef
? targetRef.current
? targetRef.current.clientHeight
: 0
: 0
if (scrollS > clientHeight + 100) {
handleGetMore()
}
}, 500)
)
useEffect(() => {
window.addEventListener('scroll', scrollTrigger)
return () => {
window.removeEventListener('scroll', scrollTrigger)
}
})
return (
<div id='posts-wrapper' className='w-full md:pr-8 mb-12' ref={targetRef}>
{postsToShow.map(p => (
<BlogItem key={p.id} post={p} />
))}
<div
onClick={handleGetMore}
className='w-full my-4 py-4 text-center cursor-pointer '>
{' '}
{hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`}{' '}
</div>
</div>
)
}

View File

@@ -0,0 +1,29 @@
import { useGlobal } from '@/lib/global'
/**
* 文章列表上方嵌入
* @param {*} props
* @returns
*/
export default function BlogPostBar(props) {
const { tag, category } = props
const { locale } = useGlobal()
if (tag) {
return (
<div className='flex items-center text-xl py-2'>
<i className='mr-2 fas fa-tag' />
{locale.COMMON.TAGS}: {tag}
</div>
)
} else if (category) {
return (
<div className='flex items-center text-xl py-2'>
<i className='mr-2 fas fa-th' />
{locale.COMMON.CATEGORY}: {category}
</div>
)
} else {
return <></>
}
}

View File

@@ -0,0 +1,136 @@
import { useGlobal } from '@/lib/global'
import throttle from 'lodash.throttle'
import { uuidToId } from 'notion-utils'
import { useEffect, useRef, useState } from 'react'
/**
* 目录导航组件
* @param toc
* @returns {JSX.Element}
* @constructor
*/
const Catalog = ({ post }) => {
const { locale } = useGlobal()
// 目录自动滚动
const tRef = useRef(null)
// 同步选中目录事件
const [activeSection, setActiveSection] = useState(null)
// 监听滚动事件
useEffect(() => {
// 如果没有文章或目录,不执行任何操作
if (!post || !post?.toc || post?.toc?.length < 1) {
return
}
const throttleMs = 100 // 降低节流时间提高响应速度
const actionSectionScrollSpy = throttle(() => {
const sections = document.getElementsByClassName('notion-h')
if (!sections || sections.length === 0) return
let prevBBox = null
let currentSectionId = null
// 先检查当前视口中的所有标题
for (let i = 0; i < sections.length; ++i) {
const section = sections[i]
if (!section || !(section instanceof Element)) continue
const bbox = section.getBoundingClientRect()
const offset = 100 // 固定偏移量,避免计算不稳定
// 如果标题在视口上方或接近顶部,认为是当前标题
if (bbox.top - offset < 0) {
currentSectionId = section.getAttribute('data-id')
prevBBox = bbox
} else {
// 找到第一个在视口下方的标题就停止
break
}
}
// 如果没找到任何标题在视口上方,使用第一个标题
if (!currentSectionId && sections.length > 0) {
currentSectionId = sections[0].getAttribute('data-id')
}
// 只有当 ID 变化时才更新状态,减少不必要的渲染
if (currentSectionId !== activeSection) {
setActiveSection(currentSectionId)
// 查找目录中对应的索引并滚动
const index = post?.toc?.findIndex(
obj => uuidToId(obj.id) === currentSectionId
)
if (index !== -1 && tRef?.current) {
tRef.current.scrollTo({ top: 28 * index, behavior: 'smooth' })
}
}
}, throttleMs)
const content = document.querySelector('#container-inner')
if (!content) return // 防止 content 不存在
// 添加滚动和内容变化的监听
content.addEventListener('scroll', actionSectionScrollSpy)
// 初始执行一次
setTimeout(() => {
actionSectionScrollSpy()
}, 300) // 延迟执行确保 DOM 已完全加载
return () => {
content?.removeEventListener('scroll', actionSectionScrollSpy)
}
}, [post])
// 无目录就直接返回空
if (!post || !post?.toc || post?.toc?.length < 1) {
return <></>
}
return (
<div className='px-3 '>
<div className='dark:text-white mb-2'>
<i className='mr-1 fas fa-stream' />
{locale.COMMON.TABLE_OF_CONTENTS}
</div>
<div
className='overflow-y-auto overscroll-none max-h-36 lg:max-h-96 scroll-hidden'
ref={tRef}>
<nav className='h-full text-black group'>
{post?.toc?.map(tocItem => {
const id = uuidToId(tocItem.id)
return (
<a
key={id}
href={`#${id}`}
className={`${activeSection === id
? 'dark:border-white border-red-700 text-red-700 font-bold'
: 'text-[var(--primary-color)] dark:text-gray-500 filter blur-[1px] opacity-50 group-hover:filter-none group-hover:blur-0 group-hover:opacity-100'}
hover:font-semibold hover:text-red-600 hover:filter-none hover:blur-0 hover:opacity-100
border-l pl-4 block border-lduration-300 transform
dark:hover:text-red-400 dark:border-gray-600
notion-table-of-contents-item-indent-level-${tocItem.indentLevel} catalog-item
transition-all duration-300 ease-in-out`}>
<span
style={{
display: 'inline-block',
marginLeft: tocItem.indentLevel * 16
}}
className={`truncate ${activeSection === id ? 'font-bold text-red-600 dark:text-white underline' : ''}`}>
{tocItem.text}
</span>
</a>
)
})}
</nav>
</div>
</div>
)
}
export default Catalog

View File

@@ -0,0 +1,35 @@
import SmartLink from '@/components/SmartLink'
import { RecentComments } from '@waline/client'
import { useEffect, useState } from 'react'
import { siteConfig } from '@/lib/config'
/**
* @see https://waline.js.org/guide/get-started.html
* @param {*} props
* @returns
*/
const ExampleRecentComments = (props) => {
const [comments, updateComments] = useState([])
const [onLoading, changeLoading] = useState(true)
useEffect(() => {
RecentComments({
serverURL: siteConfig('COMMENT_WALINE_SERVER_URL'),
count: 5
}).then(({ comments }) => {
changeLoading(false)
updateComments(comments)
})
}, [])
return <>
{onLoading && <div>Loading...<i className='ml-2 fas fa-spinner animate-spin' /></div>}
{!onLoading && comments && comments.length === 0 && <div>No Comments</div>}
{!onLoading && comments && comments.length > 0 && comments.map((comment) => <div key={comment.objectId} className='pb-2'>
<div className='dark:text-gray-300 text-gray-600 text-xs waline-recent-content wl-content' dangerouslySetInnerHTML={{ __html: comment.comment }} />
<div className='dark:text-gray-400 text-gray-400 text-sm text-right cursor-pointer hover:text-red-500 hover:underline pt-1'><SmartLink href={{ pathname: comment.url, hash: comment.objectId, query: { target: 'comment' } }}>--{comment.nick}</SmartLink></div>
</div>)}
</>
}
export default ExampleRecentComments

View File

@@ -0,0 +1,29 @@
import { BeiAnGongAn } from '@/components/BeiAnGongAn'
import DarkModeButton from '@/components/DarkModeButton'
import { siteConfig } from '@/lib/config'
/**
* 页脚
* @param {*} props
* @returns
*/
export default function Footer(props) {
const d = new Date()
const currentYear = d.getFullYear()
const since = siteConfig('SINCE')
const copyrightDate =
parseInt(since) < currentYear ? since + '-' + currentYear : currentYear
return (
<footer>
<DarkModeButton className='pt-4' />
<div className='font-bold text-[var(--primary-color)] dark:text-white py-6 text-sm flex flex-col gap-2 items-center'>
<div>
&copy;{`${copyrightDate}`} {siteConfig('AUTHOR')}.
</div>
<div>All rights reserved.</div>
</div>
</footer>
)
}

View File

@@ -0,0 +1,35 @@
import { useGlobal } from '@/lib/global'
import { useEffect, useState } from 'react'
/**
* 跳转到网页顶部
* 当屏幕下滑500像素后会出现该控件
* @param targetRef 关联高度的目标html标签
* @param showPercent 是否显示百分比
* @returns {JSX.Element}
* @constructor
*/
const JumpToTopButton = () => {
const { locale } = useGlobal()
const [show, switchShow] = useState(false)
const scrollListener = () => {
const scrollY = window.pageYOffset
const shouldShow = scrollY > 200
if (shouldShow !== show) {
switchShow(shouldShow)
}
}
useEffect(() => {
document.addEventListener('scroll', scrollListener)
return () => document.removeEventListener('scroll', scrollListener)
}, [show])
return <div title={locale.POST.TOP}
className={(show ? ' opacity-100 ' : 'invisible opacity-0') + ' transition-all duration-300 flex items-center justify-center cursor-pointer bg-black h-10 w-10 bg-opacity-40 rounded-sm'}
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
><i className='fas fa-angle-up text-white ' />
</div>
}
export default JumpToTopButton

View File

@@ -0,0 +1,92 @@
import Collapse from '@/components/Collapse'
import SmartLink from '@/components/SmartLink'
import { useState } from 'react'
/**
* 折叠菜单
* @param {*} param0
* @returns
*/
export const MenuItemCollapse = props => {
const { link } = props
const [show, changeShow] = useState(false)
const hasSubMenu = link?.subMenus?.length > 0
const [isOpen, changeIsOpen] = useState(false)
const toggleShow = () => {
changeShow(!show)
}
const toggleOpenSubMenu = () => {
changeIsOpen(!isOpen)
}
if (!link || !link.show) {
return null
}
return (
<>
<div
className='w-full px-8 py-3 text-left border-b dark:bg-hexo-black-gray dark:border-black'
onClick={toggleShow}>
{!hasSubMenu && (
<SmartLink
href={link?.href}
target={link?.target}
className='items-center flex justify-between pl-2 pr-4 dark:text-gray-200 no-underline tracking-widest pb-1'>
<span className='text-blue-600 dark:text-blue-300 hover:text-red-400 transition-all items-center duration-200'>
{link?.icon && (
<span className='mr-2'>
<i className={link.icon} />
</span>
)}
{link?.name}
</span>
</SmartLink>
)}
{hasSubMenu && (
<div
onClick={hasSubMenu ? toggleOpenSubMenu : null}
className='items-center flex justify-between pl-2 pr-4 cursor-pointer dark:text-gray-200 no-underline tracking-widest pb-1'>
<span className='text-blue-600 dark:text-blue-300 hover:text-red-400 transition-all items-center duration-200'>
{link?.icon && (
<span className='mr-2'>
<i className={link.icon} />
</span>
)}
{link?.name}
</span>
<i
className={`px-2 fa fa-plus transition-all duration-200 ${isOpen && 'rotate-45'} text-gray-400`}></i>
</div>
)}
</div>
{/* 折叠子菜单 */}
{hasSubMenu && (
<Collapse isOpen={isOpen} onHeightChange={props.onHeightChange}>
{link.subMenus.map((sLink, index) => {
return (
<div
key={index}
className='dark:bg-black text-left px-10 justify-start text-blue-600 dark:text-blue-300 bg-gray-50 hover:bg-gray-50 dark:hover:bg-gray-900 tracking-widest transition-all duration-200 border-b dark:border-gray-800 py-3 pr-6'>
<SmartLink href={sLink.href} target={link?.target}>
<span className='ml-4 text-sm'>
{sLink?.icon && (
<span className='mr-2 w-4'>
<i className={sLink.icon} />
</span>
)}
{sLink.title}
</span>
</SmartLink>
</div>
)
})}
</Collapse>
)}
</>
)
}

View File

@@ -0,0 +1,78 @@
import SmartLink from '@/components/SmartLink'
import { useRouter } from 'next/router'
import { useState } from 'react'
export const MenuItemDrop = ({ link }) => {
const hasSubMenu = link?.subMenus?.length > 0
const [show, changeShow] = useState(false)
const router = useRouter()
if (!link || !link.show) {
return null
}
const selected = router.pathname === link.href || router.asPath === link.href
return (
<div className='menu-item'>
{!hasSubMenu && (
<SmartLink
href={link?.href}
target={link?.target}
className='dark:hover:text-[var(--primary-color)] dark:hover:bg-white menu-link underline decoration-2 hover:no-underline hover:bg-[#2E405B] hover:text-white text-[var(--primary-color)] dark:text-gray-200 tracking-widest pb-1 font-bold'>
{link?.name}
</SmartLink>
)
}
{hasSubMenu && (
<>
<div
onMouseOver={() => changeShow(true)}
onMouseOut={() => changeShow(false)}
className={
'relative ' +
(selected
? 'bg-green-600 text-white hover:text-white'
: 'hover:text-green-600')
}>
<div>
<span className='dark:hover:text-[var(--primary-color)] dark:hover:bg-white menu-link underline decoration-2 hover:no-underline hover:bg-[#2E405B] hover:text-white text-[var(--primary-color)] dark:text-gray-200 tracking-widest pb-1 font-bold'>
{link?.icon && <i className={link?.icon} />} {link?.name}
</span>
{hasSubMenu && (
<i
className={`px-2 fas fa-chevron-right duration-500 transition-all ${show ? ' rotate-180' : ''}`}></i>
)}
</div>
{/* 子菜單 */}
<ul
className={`${show ? 'visible opacity-100' : 'invisible opacity-0'} absolute glassmorphism md:left-28 md:top-0 top-6 w-full border-gray-100 transition-all duration-300 z-20 block`}>
{link?.subMenus?.map((sLink, index) => {
return (
<li
key={index}
className='dark:hover:bg-gray-900 tracking-widest transition-all duration-200 dark:border-gray-800 pb-3'>
<SmartLink href={sLink.href} target={link?.target}>
<span className='dark:hover:text-[var(--primary-color)] dark:hover:bg-white menu-link underline decoration-2 hover:no-underline hover:bg-[#2E405B] hover:text-white text-[var(--primary-color)] dark:text-gray-200 tracking-widest pb-1 font-bold'>
{link?.icon && <i className={sLink?.icon}> &nbsp; </i>}
{sLink.title}
</span>
</SmartLink>
</li>
)
})}
</ul>
</div>
</>
)}
</div>
)
}

View File

@@ -0,0 +1,83 @@
import Collapse from '@/components/Collapse'
import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global'
import { useRouter } from 'next/router'
import { useEffect, useRef, useState } from 'react'
import CONFIG from '../config'
import { MenuItemCollapse } from './MenuItemCollapse'
import { MenuItemDrop } from './MenuItemDrop'
/**
* 菜单导航
* @param {*} props
* @returns
*/
export const MenuList = ({ customNav, customMenu }) => {
const { locale } = useGlobal()
const [isOpen, changeIsOpen] = useState(false)
const toggleIsOpen = () => {
changeIsOpen(!isOpen)
}
const closeMenu = e => {
changeIsOpen(false)
}
const router = useRouter()
const collapseRef = useRef(null)
useEffect(() => {
router.events.on('routeChangeStart', closeMenu)
})
let links = [
{
icon: 'fas fa-archive',
name: locale.NAV.ARCHIVE,
href: '/archive',
show: siteConfig('TYPOGRAPHY_MENU_ARCHIVE', null, CONFIG)
},
{
icon: 'fas fa-folder',
name: locale.COMMON.CATEGORY,
href: '/category',
show: siteConfig('TYPOGRAPHY_MENU_CATEGORY', null, CONFIG)
},
{
icon: 'fas fa-tag',
name: locale.COMMON.TAGS,
href: '/tag',
show: siteConfig('TYPOGRAPHY_MENU_TAG', null, CONFIG)
}
]
if (customNav) {
links = links.concat(customNav)
}
// 如果 开启自定义菜单,则覆盖 Page 生成的菜单
if (siteConfig('CUSTOM_MENU')) {
links = customMenu
}
if (!links || links.length === 0) {
return null
}
return (
<>
{/* 大屏模式菜单 - 垂直排列 */}
<div id='nav-menu-pc' className='hidden md:flex md:flex-col md:gap-2'>
{links?.map((link, index) => (
<MenuItemDrop key={index} link={link} />
))}
</div>
{/* 移动端小屏菜单 - 水平排列 */}
<div
id='nav-menu-mobile'
className='flex md:hidden my-auto justify-center space-x-4'>
{links?.map((link, index) => (
<MenuItemDrop key={index} link={link} />
))}
</div>
</>
)
}

View File

@@ -0,0 +1,37 @@
import { siteConfig } from '@/lib/config'
import { useRouter } from 'next/router'
import { useState } from 'react'
import { useSimpleGlobal } from '..'
import { MenuList } from './MenuList'
import SocialButton from './SocialButton'
import SmartLink from '@/components/SmartLink'
/**
* 菜单导航
* @param {*} props
* @returns
*/
export default function NavBar(props) {
return (
<div className='flex flex-col justify-between md:mt-20 md:h-[70vh]'>
<header className='w-fit self-center md:self-start md:pb-8 md:border-l-2 dark:md:border-white dark:text-white md:border-[var(--primary-color)] text-[var(--primary-color)] md:[writing-mode:vertical-lr] px-4 hover:bg-[var(--primary-color)] dark:hover:bg-white hover:text-white dark:hover:text-[var(--primary-color)] ease-in-out duration-700 md:hover:pt-4 md:hover:pb-4 mb-2'>
<SmartLink href='/'>
<div className='flex flex-col-reverse md:flex-col items-center md:items-start'>
<div className='font-bold text-4xl text-center' id='blog-name'>
{siteConfig('TYPOGRAPHY_BLOG_NAME')}
</div>
<div className='font-bold text-xl text-center' id='blog-name-en'>
{siteConfig('TYPOGRAPHY_BLOG_NAME_EN')}
</div>
</div>
</SmartLink>
</header>
<nav className='md:pt-0 z-20 flex-shrink-0'>
<div id='nav-bar-inner' className='text-sm md:text-md'>
<MenuList {...props} />
</div>
<SocialButton />
</nav>
</div>
)
}

View File

@@ -0,0 +1,32 @@
import SmartLink from '@/components/SmartLink'
import { useGlobal } from '@/lib/global'
import CONFIG from '../config'
import { siteConfig } from '@/lib/config'
/**
* 展示文章推荐
*/
const RecommendPosts = ({ recommendPosts }) => {
const { locale } = useGlobal()
if (!siteConfig('TYPOGRAPHY_ARTICLE_RECOMMEND_POSTS', null, CONFIG) || !recommendPosts || recommendPosts.length < 1) {
return <></>
}
return (
<div className="pt-2 border pl-4 py-2 my-4 dark:text-gray-300 ">
<div className="mb-2 font-bold text-lg">{locale.COMMON.RELATE_POSTS} :</div>
<ul className="font-light text-sm">
{recommendPosts.map(post => (
<li className="py-1" key={post.id}>
<SmartLink href={`/${post.slug}`} className="cursor-pointer hover:underline">
{post.title}
</SmartLink>
</li>
))}
</ul>
</div>
)
}
export default RecommendPosts

View File

@@ -0,0 +1,115 @@
import { siteConfig } from '@/lib/config'
/**
* 社交联系方式按钮组
* @returns {JSX.Element}
* @constructor
*/
const SocialButton = () => {
return (
<div className='justify-center w-full md:justify-start md:w-52 flex-wrap flex my-2'>
<div className='space-x-5 text-xl text-gray-600 dark:text-gray-400 text-center'>
{siteConfig('CONTACT_GITHUB') && (
<a
target='_blank'
rel='noreferrer'
title={'github'}
href={siteConfig('CONTACT_GITHUB')}>
<i className='fab fa-github transform hover:scale-125 duration-150' />
</a>
)}
{siteConfig('CONTACT_TWITTER') && (
<a
target='_blank'
rel='noreferrer'
title={'twitter'}
href={siteConfig('CONTACT_TWITTER')}>
<i className='fab fa-twitter transform hover:scale-125 duration-150' />
</a>
)}
{siteConfig('CONTACT_TELEGRAM') && (
<a
target='_blank'
rel='noreferrer'
href={siteConfig('CONTACT_TELEGRAM')}
title={'telegram'}>
<i className='fab fa-telegram transform hover:scale-125 duration-150' />
</a>
)}
{siteConfig('CONTACT_LINKEDIN') && (
<a
target='_blank'
rel='noreferrer'
href={siteConfig('CONTACT_LINKEDIN')}
title={'linkedIn'}>
<i className='fab fa-linkedin transform hover:scale-125 duration-150' />
</a>
)}
{siteConfig('CONTACT_WEIBO') && (
<a
target='_blank'
rel='noreferrer'
title={'weibo'}
href={siteConfig('CONTACT_WEIBO')}>
<i className='fab fa-weibo transform hover:scale-125 duration-150' />
</a>
)}
{siteConfig('CONTACT_INSTAGRAM') && (
<a
target='_blank'
rel='noreferrer'
title={'instagram'}
href={siteConfig('CONTACT_INSTAGRAM')}>
<i className='fab fa-instagram transform hover:scale-125 duration-150' />
</a>
)}
{siteConfig('CONTACT_EMAIL') && (
<a
target='_blank'
rel='noreferrer'
title={'email'}
href={`mailto:${siteConfig('CONTACT_EMAIL')}`}>
<i className='fas fa-envelope transform hover:scale-125 duration-150' />
</a>
)}
{JSON.parse(siteConfig('ENABLE_RSS')) && (
<a
target='_blank'
rel='noreferrer'
title={'RSS'}
href={'/rss/feed.xml'}>
<i className='fas fa-rss transform hover:scale-125 duration-150' />
</a>
)}
{siteConfig('CONTACT_BILIBILI') && (
<a
target='_blank'
rel='noreferrer'
title={'bilibili'}
href={siteConfig('CONTACT_BILIBILI')}>
<i className='fab fa-bilibili transform hover:scale-125 duration-150' />
</a>
)}
{siteConfig('CONTACT_YOUTUBE') && (
<a
target='_blank'
rel='noreferrer'
title={'youtube'}
href={siteConfig('CONTACT_YOUTUBE')}>
<i className='fab fa-youtube transform hover:scale-125 duration-150' />
</a>
)}
{siteConfig('CONTACT_THREADS') && (
<a
target='_blank'
rel='noreferrer'
title={'threads'}
href={siteConfig('CONTACT_THREADS')}>
<i className='fab fa-threads transform hover:scale-125 duration-150' />
</a>
)}
</div>
</div>
)
}
export default SocialButton

View File

@@ -0,0 +1,19 @@
import { siteConfig } from '@/lib/config'
/**
* 标题栏
* @param {*} props
* @returns
*/
export const Title = (props) => {
const { post } = props
const title = post?.title || siteConfig('DESCRIPTION')
const description = post?.description || siteConfig('AUTHOR')
return <div className="text-center px-6 py-12 mb-6 bg-gray-100 dark:bg-hexo-black-gray dark:border-hexo-black-gray border-b">
<h1 className=" text-xl md:text-4xl pb-4">{title}</h1>
<p className="leading-loose text-gray-dark">
{description}
</p>
</div>
}

View File

@@ -0,0 +1,19 @@
import CONFIG from '../config'
import { siteConfig } from '@/lib/config'
/**
* 网站顶部 提示栏
* @returns
*/
export default function TopBar (props) {
const content = siteConfig('SIMPLE_TOP_BAR_CONTENT', null, CONFIG)
if (content) {
return <header className="flex justify-center items-center bg-black dark:bg-hexo-black-gray">
<div id='top-bar-inner' className='max-w-9/10 w-full z-20'>
<div className='text-xs text-center float-left text-white z-50 leading-5 py-2.5' dangerouslySetInnerHTML={{ __html: content }}/>
</div>
</header>
}
return <></>
}

17
mhhung/config.js Normal file
View File

@@ -0,0 +1,17 @@
const CONFIG = {
// 博客標題 雙語言
TYPOGRAPHY_BLOG_NAME: process.env.NEXT_PUBLIC_TYPOGRAPHY_BLOG_NAME || '活字印刷',
TYPOGRAPHY_BLOG_NAME_EN: process.env.NEXT_PUBLIC_TYPOGRAPHY_BLOG_NAME_EN || process.env.NEXT_PUBLIC_TYPOGRAPHY_BLOG_NAME || 'Typography',
TYPOGRAPHY_POST_AD_ENABLE: process.env.NEXT_PUBLIC_TYPOGRAPHY_POST_AD_ENABLE || false, // 文章列表是否插入广告
TYPOGRAPHY_POST_COVER_ENABLE: process.env.NEXT_PUBLIC_TYPOGRAPHY_POST_COVER_ENABLE || false, // 是否展示博客封面
TYPOGRAPHY_ARTICLE_RECOMMEND_POSTS: process.env.NEXT_PUBLIC_TYPOGRAPHY_ARTICLE_RECOMMEND_POSTS || true, // 文章详情底部显示推荐
// 菜单配置
TYPOGRAPHY_MENU_CATEGORY: true, // 显示分类
TYPOGRAPHY_MENU_TAG: true, // 显示标签
TYPOGRAPHY_MENU_ARCHIVE: true, // 显示归档
}
export default CONFIG

372
mhhung/index.js Normal file
View File

@@ -0,0 +1,372 @@
import { AdSlot } from '@/components/GoogleAdsense'
import replaceSearchResult from '@/components/Mark'
import NotionPage from '@/components/NotionPage'
import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global'
import { isBrowser } from '@/lib/utils'
import dynamic from 'next/dynamic'
import SmartLink from '@/components/SmartLink'
import { useRouter } from 'next/router'
import { createContext, useContext, useEffect, useRef } from 'react'
import BlogPostBar from './components/BlogPostBar'
import CONFIG from './config'
import { Style } from './style'
import Catalog from './components/Catalog'
const AlgoliaSearchModal = dynamic(
() => import('@/components/AlgoliaSearchModal'),
{ ssr: false }
)
// 主题组件
const BlogArchiveItem = dynamic(() => import('./components/BlogArchiveItem'), {
ssr: false
})
const ArticleLock = dynamic(() => import('./components/ArticleLock'), {
ssr: false
})
const ArticleInfo = dynamic(() => import('./components/ArticleInfo'), {
ssr: false
})
const Comment = dynamic(() => import('@/components/Comment'), { ssr: false })
const ArticleAround = dynamic(() => import('./components/ArticleAround'), {
ssr: false
})
const TopBar = dynamic(() => import('./components/TopBar'), { ssr: false })
const NavBar = dynamic(() => import('./components/NavBar'), { ssr: false })
const JumpToTopButton = dynamic(() => import('./components/JumpToTopButton'), {
ssr: false
})
const Footer = dynamic(() => import('./components/Footer'), { ssr: false })
const WWAds = dynamic(() => import('@/components/WWAds'), { ssr: false })
const BlogListPage = dynamic(() => import('./components/BlogListPage'), {
ssr: false
})
const RecommendPosts = dynamic(() => import('./components/RecommendPosts'), {
ssr: false
})
// 主题全局状态
const ThemeGlobalSimple = createContext()
export const useSimpleGlobal = () => useContext(ThemeGlobalSimple)
/**
* 基础布局
*
* @param {*} props
* @returns
*/
const LayoutBase = props => {
const { children } = props
const { onLoading, fullWidth } = useGlobal()
// const onLoading = true
const searchModal = useRef(null)
return (
<ThemeGlobalSimple.Provider value={{ searchModal }}>
<div
id='theme-typography'
className={`${siteConfig('FONT_STYLE')} font-typography h-screen flex flex-col dark:text-gray-300 bg-white dark:bg-[#232222] overflow-hidden`}>
<Style />
{siteConfig('SIMPLE_TOP_BAR', null, CONFIG) && <TopBar {...props} />}
<div className='flex flex-1 mx-auto overflow-hidden py-8 md:p-0 md:max-w-7xl md:px-24 w-screen'>
{/* 主体 - 使用 flex 布局 */}
{/* 文章详情才显示 */}
{/* {props.post && (
<div className='mt-20 hidden md:block md:fixed md:left-5 md:w-[300px]'>
<Catalog {...props} />
</div>
)} */}
<div className='overflow-hidden md:mt-20 flex-1 '>
{/* 左侧内容区域 - 可滚动 */}
<div
id='container-inner'
className='h-full w-full md:px-24 overflow-y-auto scroll-hidden relative'>
{/* 移动端导航 - 显示在顶部 */}
<div className='md:hidden'>
<NavBar {...props} />
</div>
{onLoading ? (
// loading 时显示 spinner
<div className='flex items-center justify-center min-h-[500px] w-full'>
<div className='animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-gray-900 dark:border-white'></div>
</div>
) : (
<>{children}</>
)}
<AdSlot type='native' />
{/* 移动端页脚 - 显示在底部 */}
<div className='md:hidden z-30 '>
<Footer {...props} />
</div>
</div>
</div>
{/* 右侧导航和页脚 - 固定不滚动 */}
<div className='hidden md:flex md:flex-col md:flex-shrink-0 md:h-[100vh] sticky top-20'>
<NavBar {...props} />
<Footer {...props} />
</div>
</div>
<div className='fixed right-4 bottom-4 z-20'>
<JumpToTopButton />
</div>
{/* 搜索框 */}
<AlgoliaSearchModal cRef={searchModal} {...props} />
</div>
</ThemeGlobalSimple.Provider>
)
}
/**
* 博客首页
* 首页就是列表
* @param {*} props
* @returns
*/
const LayoutIndex = props => {
return <LayoutPostList {...props} />
}
/**
* 博客列表
* @param {*} props
* @returns
*/
const LayoutPostList = props => {
return (
<>
<BlogPostBar {...props} />
<BlogListPage {...props} />
</>
)
}
/**
* 搜索页
* 也是博客列表
* @param {*} props
* @returns
*/
const LayoutSearch = props => {
const { keyword } = props
useEffect(() => {
if (isBrowser) {
replaceSearchResult({
doms: document.getElementById('posts-wrapper'),
search: keyword,
target: {
element: 'span',
className: 'text-red-500 border-b border-dashed'
}
})
}
}, [])
return <LayoutPostList {...props} />
}
function groupArticlesByYearArray(articles) {
const grouped = {};
for (const article of articles) {
const year = new Date(article.publishDate).getFullYear().toString();
if (!grouped[year]) {
grouped[year] = [];
}
grouped[year].push(article);
}
for (const year in grouped) {
grouped[year].sort((a, b) => b.publishDate - a.publishDate);
}
// 转成数组并按年份倒序
return Object.entries(grouped)
.sort(([a], [b]) => b - a)
.map(([year, posts]) => ({ year, posts }));
}
/**
* 归档页
* @param {*} props
* @returns
*/
const LayoutArchive = props => {
const { posts } = props
const sortPosts = groupArticlesByYearArray(posts)
return (
<>
<div className='mb-10 pb-20 md:pb-12 p-5 min-h-screen w-full'>
{sortPosts.map(p => (
<BlogArchiveItem
key={p.year}
archiveTitle={p.year}
archivePosts={p.posts}
/>
))}
</div>
</>
)
}
/**
* 文章详情
* @param {*} props
* @returns
*/
const LayoutSlug = props => {
const { post, lock, validPassword, prev, next, recommendPosts } = props
const { fullWidth } = useGlobal()
return (
<>
{lock && <ArticleLock validPassword={validPassword} />}
{!lock && post && (
<div
className={`px-5 pt-3 ${fullWidth ? '' : 'xl:max-w-4xl 2xl:max-w-6xl'}`}>
{/* 文章信息 */}
<ArticleInfo post={post} />
{/* 广告嵌入 */}
{/* <AdSlot type={'in-article'} /> */}
<WWAds orientation='horizontal' className='w-full' />
<div id='article-wrapper'>
{/* Notion 文章主体 */}
{!lock && <NotionPage post={post} />}
</div>
{/* 分享 */}
{/* <ShareBar post={post} /> */}
{/* 广告嵌入 */}
<AdSlot type={'in-article'} />
{post?.type === 'Post' && (
<>
<ArticleAround prev={prev} next={next} />
<RecommendPosts recommendPosts={recommendPosts} />
</>
)}
{/* 评论区 */}
<Comment frontMatter={post} />
</div>
)}
</>
)
}
/**
* 404
* @param {*} props
* @returns
*/
const Layout404 = props => {
const { post } = props
const router = useRouter()
const waiting404 = siteConfig('POST_WAITING_TIME_FOR_404') * 1000
useEffect(() => {
// 404
if (!post) {
setTimeout(() => {
if (isBrowser) {
const article = document.querySelector(
'#article-wrapper #notion-article'
)
if (!article) {
router.push('/404').then(() => {
console.warn('找不到页面', router.asPath)
})
}
}
}, waiting404)
}
}, [post])
return <>404 Not found.</>
}
/**
* 分类列表
* @param {*} props
* @returns
*/
const LayoutCategoryIndex = props => {
const { categoryOptions } = props
return (
<>
<div id='category-list' className='px-5 duration-200 flex flex-wrap'>
{categoryOptions?.map(category => {
return (
<SmartLink
key={category.name}
href={`/category/${category.name}`}
passHref
legacyBehavior>
<div
className={
'hover:text-black dark:hover:text-white dark:text-gray-300 dark:hover:bg-gray-600 px-5 cursor-pointer py-2 hover:bg-gray-100'
}>
<i className='mr-4 fas fa-folder' />
{category.name}({category.count})
</div>
</SmartLink>
)
})}
</div>
</>
)
}
/**
* 标签列表
* @param {*} props
* @returns
*/
const LayoutTagIndex = props => {
const { tagOptions } = props
return (
<>
<div id='tags-list' className='px-5 duration-200 flex flex-wrap'>
{tagOptions.map(tag => {
return (
<div key={tag.name} className='p-2'>
<SmartLink
key={tag}
href={`/tag/${encodeURIComponent(tag.name)}`}
passHref
className={`cursor-pointer inline-block rounded hover:bg-gray-500 hover:text-white duration-200 mr-2 py-1 px-2 text-xs whitespace-nowrap dark:hover:text-white text-gray-600 hover:shadow-xl dark:border-gray-400 notion-${tag.color}_background dark:bg-gray-800`}>
<div className='font-light dark:text-gray-400'>
<i className='mr-1 fas fa-tag' />{' '}
{tag.name + (tag.count ? `(${tag.count})` : '')}{' '}
</div>
</SmartLink>
</div>
)
})}
</div>
</>
)
}
export {
Layout404,
LayoutArchive,
LayoutBase,
LayoutCategoryIndex,
LayoutIndex,
LayoutPostList,
LayoutSearch,
LayoutSlug,
LayoutTagIndex,
CONFIG as THEME_CONFIG
}

93
mhhung/style.js Normal file
View File

@@ -0,0 +1,93 @@
/* eslint-disable react/no-unknown-property */
/**
* 此处样式只对当前主题生效
* 此处不支持 tailwindCSS 的 @apply 语法
* @returns
*/
const Style = () => {
return (
<style jsx global>{`
html {
-webkit-font-smoothing: antialiased;
}
.font-typography {
font-weight: 400;
font-family:
Source Sans Pro,
Roboto,
Helvetica,
Helvetica Neue,
Source Han Sans SC,
Source Han Sans TC,
PingFang SC,
PingFang HK,
PingFang TC,
sans-serif !important;
}
}
// 底色
.dark body {
background-color: rgb(35, 34, 34);
}
// 文本不可选取
.forbid-copy {
user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
.dark #theme-typography {
background-image: linear-gradient(
to right,
rgb(255 255 255 / 0.04) 1px,
transparent 1px
),
linear-gradient(to bottom, rgb(255 255 255 / 0.04) 1px, transparent 1px);
}
#theme-typography {
--primary-color: #2e405b;
background-color: rgb(255 255 255) / 1;
color: #2e405b;
background-size: 7px 7px;
text-shadow: 1px 1px 1px rgb(0 0 0 / 0.04);
background-image: linear-gradient(
to right,
rgb(0 0 0 / 0.04) 1px,
transparent 1px
),
linear-gradient(to bottom, rgb(0 0 0 / 0.04) 1px, transparent 1px);
}
#theme-typography #blog-name {
font-family: HiraMinProN-W6, 'Source Han Serif CN',
'Source Han Serif SC', 'Source Han Serif TC', serif;
}
#theme-typography #blog-name-en {
font-family: HiraMinProN-W6, 'Source Han Serif CN',
'Source Han Serif SC', 'Source Han Serif TC', serif;
}
#theme-typography .blog-item-title {
color: #276077;
}
.dark #theme-typography .blog-item-title {
color: #d1d5db;
}
.notion {
margin-top: 0 !important;
margin-bottom: 0 !important;
}
#container-wrapper .scroll-hidden {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
`}</style>
)
}
export { Style }