perf: optimize theme styles

This commit is contained in:
molvqingtai 2024-10-28 09:03:57 +08:00
parent bcdd435e45
commit 2d051fedd7
22 changed files with 105 additions and 72 deletions

View file

@ -74,7 +74,6 @@
"idb-keyval": "^6.2.1",
"lucide-react": "^0.453.0",
"nanoid": "^5.0.7",
"next-themes": "^0.3.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.0",

View file

@ -98,9 +98,6 @@ importers:
nanoid:
specifier: ^5.0.7
version: 5.0.7
next-themes:
specifier: ^0.3.0
version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react:
specifier: ^18.3.1
version: 18.3.1
@ -4949,12 +4946,6 @@ packages:
resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==}
engines: {node: '>= 0.4.0'}
next-themes@0.3.0:
resolution: {integrity: sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w==}
peerDependencies:
react: ^16.8 || ^17 || ^18
react-dom: ^16.8 || ^17 || ^18
nice-try@1.0.5:
resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==}
@ -12482,11 +12473,6 @@ snapshots:
netmask@2.0.2: {}
next-themes@0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
nice-try@1.0.5: {}
no-case@3.0.4:

View file

@ -15,6 +15,7 @@ import { AnimatePresence, motion } from 'framer-motion'
import DanmakuContainer from './components/DanmakuContainer'
import DanmakuDomain from '@/domain/Danmaku'
import AppStatusDomain from '@/domain/AppStatus'
import { cn } from '@/utils'
/**
* Fix requestAnimationFrame error in jest
@ -63,24 +64,29 @@ export default function App() {
return (
appStatusLoadIsFinished && (
<>
<AppMain className={userInfo?.themeMode}>
<div id="app" className={cn('contents', userInfo?.themeMode)}>
<AppMain>
<Header />
<Main />
<Footer />
<AnimatePresence>
{notUserInfo && (
<motion.div initial={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.3 }}>
<motion.div
className="contents"
initial={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
>
<Setup></Setup>
</motion.div>
)}
</AnimatePresence>
<Toaster richColors offset="70px" visibleToasts={1} position="top-center"></Toaster>
</AppMain>
<AppButton className={userInfo?.themeMode}></AppButton>
<AppButton></AppButton>
<DanmakuContainer ref={danmakuContainerRef} className={userInfo?.themeMode} />
</>
<DanmakuContainer ref={danmakuContainerRef} />
</div>
)
)
}

View file

@ -10,7 +10,7 @@ export interface EmojiButtonProps {
onSelect?: (value: string) => void
}
const emojiGroups = chunk([...EMOJI_LIST], 8)
const emojiGroups = chunk([...EMOJI_LIST], 6)
// BUG: https://github.com/radix-ui/primitives/pull/2433
// BUG https://github.com/radix-ui/primitives/issues/1666
@ -34,16 +34,19 @@ const EmojiButton: FC<EmojiButtonProps> = ({ onSelect }) => {
<SmileIcon size={20} />
</Button>
</PopoverTrigger>
<PopoverContent className="z-infinity w-72 rounded-xl px-0" onCloseAutoFocus={handleCloseAutoFocus}>
<ScrollArea className="size-72 px-3">
<PopoverContent
className="z-infinity w-64 overflow-hidden rounded-xl p-0 dark:bg-slate-900"
onCloseAutoFocus={handleCloseAutoFocus}
>
<ScrollArea className="size-64 p-1">
{emojiGroups.map((group, index) => {
return (
<div key={index} className="grid grid-cols-8">
<div key={index} className="grid grid-cols-6">
{group.map((emoji, index) => (
<Button
key={index}
size="sm"
className="text-base"
size="icon"
className="text-xl"
variant="ghost"
onClick={() => handleSelect(emoji)}
>

View file

@ -33,8 +33,8 @@ const LikeButton: FC<LikeButtonProps> & { Icon: FC<LikeButtonIconProps> } = ({
onClick={handleClick}
variant="secondary"
className={cn(
'grid items-center overflow-hidden rounded-full leading-none transition-all select-none dark:bg-slate-200',
checked ? 'text-orange-500' : 'text-slate-500',
'grid items-center overflow-hidden rounded-full leading-none transition-all select-none dark:bg-slate-600',
checked ? 'text-orange-500' : 'text-slate-500 dark:text-slate-100',
count ? 'grid-cols-[auto_1fr] gap-x-1' : 'grid-cols-[auto_0fr] gap-x-0'
)}
size="xs"

View file

@ -12,7 +12,7 @@ const MessageList: FC<MessageListProps> = ({ children }) => {
const [scrollParentRef, setScrollParentRef] = useState<HTMLDivElement | null>(null)
return (
<ScrollArea ref={setScrollParentRef}>
<ScrollArea ref={setScrollParentRef} className="dark:bg-slate-900">
<Virtuoso
defaultItemHeight={108}
followOutput={(isAtBottom: boolean) => (isAtBottom ? 'smooth' : 'auto')}

View file

@ -13,10 +13,7 @@ export interface PromptItemProps {
const PromptItem: FC<PromptItemProps> = ({ data, className }) => {
return (
<div className={cn('flex justify-center py-1 px-4 ', className)}>
<Badge
variant="secondary"
className="gap-x-2 rounded-full px-2 font-medium text-slate-400 dark:bg-slate-600 dark:text-slate-50"
>
<Badge variant="secondary" className="gap-x-2 rounded-full px-2 font-medium text-slate-400 dark:bg-slate-800">
<Avatar className="size-4">
<AvatarImage src={data.userAvatar} className="size-full" alt="avatar" />
<AvatarFallback>{data.username.at(0)}</AvatarFallback>

View file

@ -2,7 +2,7 @@ import React from 'react'
import { createRoot } from 'react-dom/client'
import { Remesh } from 'remesh'
import { RemeshRoot, RemeshScope } from 'remesh-react'
import { RemeshLogger } from 'remesh-logger'
// import { RemeshLogger } from 'remesh-logger'
import { defineContentScript } from 'wxt/sandbox'
import { createShadowRootUi } from 'wxt/client'
@ -15,8 +15,8 @@ import { ToastImpl } from '@/domain/impls/Toast'
import { PeerRoomImpl } from '@/domain/impls/PeerRoom2'
import '@/assets/styles/tailwind.css'
import '@/assets/styles/sonner.css'
import { createElement } from '@/utils'
import NotificationDomain from '@/domain/Notification'
import { createElement } from '@/utils'
export default defineContentScript({
cssInjectionMode: 'ui',
@ -24,6 +24,13 @@ export default defineContentScript({
matches: ['https://*/*'],
excludeMatches: ['*://localhost/*', '*://127.0.0.1/*', '*://*.csdn.net/*', '*://*.csdn.com/*'],
async main(ctx) {
window.CSS.registerProperty({
name: '--shimmer-angle',
syntax: '<angle>',
inherits: false,
initialValue: '0deg'
})
const store = Remesh.store({
externs: [
LocalStorageImpl,
@ -45,9 +52,8 @@ export default defineContentScript({
mode: 'open',
isolateEvents: ['keyup', 'keydown', 'keypress'],
onMount: (container) => {
const app = createElement('<div id="app"></div>')
const app = createElement('<div id="root"></div>')
container.append(app)
const root = createRoot(app)
root.render(
<React.StrictMode>

View file

@ -1,4 +1,4 @@
import { type FC, useState, type MouseEvent, useLayoutEffect, useEffect } from 'react'
import { type FC, useState, type MouseEvent, useEffect } from 'react'
import { SettingsIcon, MoonIcon, SunIcon, HandIcon } from 'lucide-react'
import { motion, AnimatePresence } from 'framer-motion'
@ -19,6 +19,7 @@ import AppStatusDomain from '@/domain/AppStatus'
import { getDay } from 'date-fns'
import { messenger } from '@/messenger'
import useDarg from '@/hooks/useDarg'
import useWindowResize from '@/hooks/useWindowResize'
export interface AppButtonProps {
className?: string
@ -47,21 +48,17 @@ const AppButton: FC<AppButtonProps> = ({ className }) => {
} = useDarg({
initX: appPosition.x,
initY: appPosition.y,
minX: 44,
maxX: window.innerWidth - 44,
minX: 50,
maxX: window.innerWidth - 50,
maxY: window.innerHeight - 22,
minY: window.innerHeight / 2
})
useEffect(() => {
const handler = () => {
send(appStatusDomain.command.UpdatePositionCommand({ x: window.innerWidth - 44, y: window.innerHeight - 22 }))
}
window.addEventListener('resize', handler)
return () => window.removeEventListener('resize', handler)
}, [])
useWindowResize(({ width, height }) => {
send(appStatusDomain.command.UpdatePositionCommand({ x: width - 50, y: height - 22 }))
})
useLayoutEffect(() => {
useEffect(() => {
send(appStatusDomain.command.UpdatePositionCommand({ x, y }))
}, [x, y])
@ -116,7 +113,7 @@ const AppButton: FC<AppButtonProps> = ({ className }) => {
className={cn(
'absolute grid grid-rows-[repeat(2,minmax(0,2.5rem))] w-full justify-center items-center transition-all duration-500',
isDarkMode ? 'top-0' : '-top-10',
isDarkMode ? 'bg-slate-800 text-white' : 'bg-white text-orange-400'
isDarkMode ? 'bg-slate-950 text-white' : 'bg-white text-orange-400'
)}
>
<MoonIcon size={20} />
@ -136,7 +133,7 @@ const AppButton: FC<AppButtonProps> = ({ className }) => {
<Button
onClick={handleToggleApp}
onContextMenu={handleToggleMenu}
className="relative z-20 size-11 rounded-full p-0 text-xs shadow-lg shadow-slate-500/50"
className="relative z-20 size-11 rounded-full p-0 text-xs shadow-lg shadow-slate-500/50 after:absolute after:-inset-0.5 after:z-10 after:animate-[shimmer_2s_linear_infinite] after:rounded-full after:bg-[conic-gradient(from_var(--shimmer-angle),theme(colors.slate.500)_0%,theme(colors.white)_10%,theme(colors.slate.500)_20%)]"
>
<AnimatePresence>
{hasUnreadQuery && (
@ -145,7 +142,7 @@ const AppButton: FC<AppButtonProps> = ({ className }) => {
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.1 }}
className="absolute -right-1 -top-1 flex size-5 items-center justify-center"
className="absolute -right-1 -top-1 z-30 flex size-5 items-center justify-center"
>
<span
className={cn('absolute inline-flex size-full animate-ping rounded-full opacity-75', 'bg-orange-400')}
@ -154,7 +151,8 @@ const AppButton: FC<AppButtonProps> = ({ className }) => {
</motion.div>
)}
</AnimatePresence>
<DayLogo className="max-h-full max-w-full"></DayLogo>
<DayLogo className="relative z-20 max-h-full max-w-full overflow-hidden"></DayLogo>
</Button>
</div>
)

View file

@ -4,7 +4,7 @@ import { motion, AnimatePresence } from 'framer-motion'
import AppStatusDomain from '@/domain/AppStatus'
import { useRemeshDomain, useRemeshQuery } from 'remesh-react'
import { cn } from '@/utils'
import { useWindowSize } from 'react-use'
import useWindowResize from '@/hooks/useWindowResize'
export interface AppMainProps {
children?: ReactNode
@ -16,9 +16,9 @@ const AppMain: FC<AppMainProps> = ({ children, className }) => {
const appOpenStatus = useRemeshQuery(appStatusDomain.query.OpenQuery())
const { x, y } = useRemeshQuery(appStatusDomain.query.PositionQuery())
const { width } = useWindowSize()
const { width } = useWindowResize()
const isOnRightSide = x >= width / 2 + 44
const isOnRightSide = x >= width / 2 + 50
const { size, setRef } = useResizable({
initSize: Math.max(375, width / 6),
@ -54,7 +54,7 @@ const AppMain: FC<AppMainProps> = ({ children, className }) => {
<div
ref={setRef}
className={cn(
'absolute inset-y-3 z-infinity w-1 dark:bg-slate-500 cursor-ew-resize rounded-xl bg-slate-100 opacity-0 shadow transition-opacity duration-200 ease-in hover:opacity-100',
'absolute inset-y-3 z-infinity w-1 dark:bg-slate-600 cursor-ew-resize rounded-xl bg-slate-100 opacity-0 shadow transition-opacity duration-200 ease-in hover:opacity-100',
isOnRightSide ? '-left-0.5' : '-right-0.5'
)}
></div>

View file

@ -15,7 +15,7 @@ import useTriggerAway from '@/hooks/useTriggerAway'
import { ScrollArea } from '@/components/ui/ScrollArea'
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso'
import UserInfoDomain from '@/domain/UserInfo'
import { cn, getTextSimilarity } from '@/utils'
import { cn, getRootNode, getTextSimilarity } from '@/utils'
import { Avatar, AvatarFallback } from '@/components/ui/Avatar'
import { AvatarImage } from '@radix-ui/react-avatar'
import ToastDomain from '@/domain/Toast'
@ -240,7 +240,7 @@ const Footer: FC = () => {
})
}
const root = document.querySelector(__NAME__)?.shadowRoot
const root = getRootNode()
return (
<div className="relative z-10 grid gap-y-2 rounded-b-xl px-4 pb-4 pt-2 before:pointer-events-none before:absolute before:inset-x-4 before:-top-2 before:h-2 before:bg-gradient-to-t before:from-slate-50 before:from-30% before:to-transparent dark:bg-slate-900 before:dark:from-slate-900">
@ -248,8 +248,8 @@ const Footer: FC = () => {
<Portal
container={root}
ref={shareAutoCompleteListRef}
className="fixed z-infinity w-36 -translate-y-full rounded-lg border bg-popover text-popover-foreground shadow-md duration-300 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95"
style={{ left: `min(${x}px, 100vw - 212px)`, top: `${y}px` }}
className="fixed z-infinity w-36 -translate-y-full overflow-hidden rounded-lg border bg-popover text-popover-foreground shadow-md duration-300 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95"
style={{ left: `min(${x}px, 100vw - 160px)`, top: `${y}px` }}
>
<ScrollArea className="max-h-[204px] min-h-9 p-1" ref={setScrollParentRef}>
<Virtuoso

View file

@ -19,7 +19,7 @@ const Header: FC = () => {
return (
<div className="z-10 grid h-12 grid-flow-col grid-cols-[theme('spacing.20')_auto_theme('spacing.20')] items-center justify-between rounded-t-xl bg-white px-4 backdrop-blur-lg dark:bg-slate-950">
<Avatar className="size-8 dark:text-slate-50">
<Avatar className="size-8">
<AvatarImage src={siteInfo.icon} alt="favicon" />
<AvatarFallback>
<Globe2Icon size="100%" className="text-gray-400" />

View file

@ -16,7 +16,7 @@ function App() {
<VersionLink></VersionLink>
<Main>
<ProfileForm></ProfileForm>
<Toaster richColors position="top-center" />
<Toaster richColors position="top-center" duration={1000000} />
</Main>
<BadgeList></BadgeList>
</Layout>

View file

@ -82,3 +82,9 @@
direction: ltr !important;
}
}
/* @property --shimmer-angle {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
} */

View file

@ -113,7 +113,7 @@ const Markdown: FC<MarkdownProps> = ({ children = '', className }) => {
)
}}
remarkPlugins={[remarkGfm, remarkBreaks]}
className={cn(className, 'prose prose-sm prose-slate break-words dark:text-slate-400')}
className={cn(className, 'prose prose-sm prose-slate break-words dark:text-slate-50')}
>
{children}
</ReactMarkdown>

View file

@ -1,6 +1,6 @@
import * as React from 'react'
import * as PopoverPrimitive from '@radix-ui/react-popover'
import { cn } from '@/utils/index'
import { cn, getRootNode } from '@/utils'
const Popover = PopoverPrimitive.Root
@ -10,8 +10,7 @@ const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => {
const root = document.querySelector(__NAME__)?.shadowRoot
const root = getRootNode()
return (
<PopoverPrimitive.Portal container={root}>
<PopoverPrimitive.Content

View file

@ -7,10 +7,7 @@ const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> & { scrollLock?: boolean }
>(({ className, children, scrollLock = true, ...props }, ref) => (
<ScrollAreaPrimitive.Root
className={cn('relative grid grid-rows-[1fr] overflow-hidden dark:bg-slate-900', className)}
{...props}
>
<ScrollAreaPrimitive.Root className={cn('relative grid grid-rows-[1fr] overflow-hidden', className)} {...props}>
<ScrollAreaPrimitive.Viewport
ref={ref}
className={cn('size-full rounded-[inherit]', scrollLock ? 'overscroll-none' : 'overscroll-auto')}

View file

@ -15,7 +15,7 @@ export interface AppStatus {
export const defaultStatusState = {
open: false,
unread: 0,
position: { x: window.innerWidth - 44, y: window.innerHeight - 22 }
position: { x: window.innerWidth - 50, y: window.innerHeight - 22 }
}
const AppStatusDomain = Remesh.domain({

View file

@ -0,0 +1,22 @@
import { useEffect, useState } from 'react'
const useWindowResize = (callback?: ({ width, height }: { width: number; height: number }) => void) => {
const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight })
useEffect(() => {
const handler = () => {
const width = window.innerWidth
const height = window.innerHeight
setSize({ width, height })
callback?.({ width, height })
}
window.addEventListener('resize', handler)
return () => {
window.removeEventListener('resize', handler)
}
}, [])
return size
}
export default useWindowResize

5
src/utils/getRootNode.ts Normal file
View file

@ -0,0 +1,5 @@
export const getRootNode = () => {
return document.querySelector(__NAME__)?.shadowRoot?.querySelector('#app') || document.body
}
export default getRootNode

View file

@ -13,3 +13,4 @@ export { default as generateRandomAvatar } from './generateRandomAvatar'
export { default as generateRandomName } from './generateRandomName'
export { default as getCursorPosition } from './getCursorPosition'
export { default as getTextSimilarity } from './getTextSimilarity'
export { default as getRootNode } from './getRootNode'

View file

@ -81,6 +81,14 @@ export default {
transform: 'rotate(215deg) translateX(-500px)',
opacity: '0'
}
},
shimmer: {
'0%': {
'--shimmer-angle': '0deg'
},
'100%': {
'--shimmer-angle': '360deg'
}
}
},
animation: {