perf: add number animation

This commit is contained in:
molvqingtai 2024-11-15 08:55:02 +08:00
parent b860b16e90
commit eb37dd2833
7 changed files with 113 additions and 42 deletions

View file

@ -45,6 +45,7 @@
"homepage": "https://github.com/molvqingtai/WebChat", "homepage": "https://github.com/molvqingtai/WebChat",
"dependencies": { "dependencies": {
"@hookform/resolvers": "^3.9.1", "@hookform/resolvers": "^3.9.1",
"@number-flow/react": "^0.3.2",
"@perfsee/jsonr": "^1.13.0", "@perfsee/jsonr": "^1.13.0",
"@radix-ui/react-avatar": "^1.1.1", "@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-checkbox": "^1.1.2",
@ -63,6 +64,7 @@
"@resreq/timer": "^1.1.6", "@resreq/timer": "^1.1.6",
"@rtco/client": "^0.2.17", "@rtco/client": "^0.2.17",
"@tailwindcss/typography": "^0.5.15", "@tailwindcss/typography": "^0.5.15",
"@webcomponents/custom-elements": "^1.6.0",
"@webext-core/messaging": "^2.0.2", "@webext-core/messaging": "^2.0.2",
"@webext-core/proxy-service": "^1.2.0", "@webext-core/proxy-service": "^1.2.0",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
@ -100,8 +102,8 @@
"@semantic-release/exec": "^6.0.3", "@semantic-release/exec": "^6.0.3",
"@semantic-release/git": "^10.0.1", "@semantic-release/git": "^10.0.1",
"@types/eslint": "^9.6.1", "@types/eslint": "^9.6.1",
"@types/eslint__js": "^8.42.3",
"@types/eslint-plugin-tailwindcss": "^3.17.0", "@types/eslint-plugin-tailwindcss": "^3.17.0",
"@types/eslint__js": "^8.42.3",
"@types/node": "^22.9.0", "@types/node": "^22.9.0",
"@types/react": "^18.3.12", "@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1", "@types/react-dom": "^18.3.1",

View file

@ -11,6 +11,9 @@ importers:
'@hookform/resolvers': '@hookform/resolvers':
specifier: ^3.9.1 specifier: ^3.9.1
version: 3.9.1(react-hook-form@7.53.2(react@18.3.1)) version: 3.9.1(react-hook-form@7.53.2(react@18.3.1))
'@number-flow/react':
specifier: ^0.3.2
version: 0.3.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@perfsee/jsonr': '@perfsee/jsonr':
specifier: ^1.13.0 specifier: ^1.13.0
version: 1.13.0 version: 1.13.0
@ -65,6 +68,9 @@ importers:
'@tailwindcss/typography': '@tailwindcss/typography':
specifier: ^0.5.15 specifier: ^0.5.15
version: 0.5.15(tailwindcss@3.4.14) version: 0.5.15(tailwindcss@3.4.14)
'@webcomponents/custom-elements':
specifier: ^1.6.0
version: 1.6.0
'@webext-core/messaging': '@webext-core/messaging':
specifier: ^2.0.2 specifier: ^2.0.2
version: 2.0.2 version: 2.0.2
@ -85,7 +91,7 @@ importers:
version: 4.1.0 version: 4.1.0
framer-motion: framer-motion:
specifier: ^11.11.13 specifier: ^11.11.13
version: 11.11.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 11.11.13(@emotion/is-prop-valid@0.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
idb-keyval: idb-keyval:
specifier: ^6.2.1 specifier: ^6.2.1
version: 6.2.1 version: 6.2.1
@ -463,6 +469,12 @@ packages:
engines: {node: '>= 0.10.4'} engines: {node: '>= 0.10.4'}
hasBin: true hasBin: true
'@emotion/is-prop-valid@0.8.8':
resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==}
'@emotion/memoize@0.7.4':
resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==}
'@esbuild/aix-ppc64@0.21.5': '@esbuild/aix-ppc64@0.21.5':
resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -744,6 +756,12 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
'@number-flow/react@0.3.2':
resolution: {integrity: sha512-/Rg7WjIZR/yjHJAzRHN7+Cif+s9U02QewMl9WEKPoAY9O6jg0wA/IsAl3lJgeM1ic31bDJ92wfCkwE9ud62VmQ==}
peerDependencies:
react: ^18 || ^19.0.0-rc-915b914b3a-20240515
react-dom: ^18
'@octokit/auth-token@5.1.1': '@octokit/auth-token@5.1.1':
resolution: {integrity: sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==} resolution: {integrity: sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==}
engines: {node: '>= 18'} engines: {node: '>= 18'}
@ -1708,6 +1726,9 @@ packages:
peerDependencies: peerDependencies:
vite: ^4.2.0 || ^5.0.0 vite: ^4.2.0 || ^5.0.0
'@webcomponents/custom-elements@1.6.0':
resolution: {integrity: sha512-CqTpxOlUCPWRNUPZDxT5v2NnHXA4oox612iUGnmTUGQFhZ1Gkj8kirtl/2wcF6MqX7+PqqicZzOCBKKfIn0dww==}
'@webext-core/fake-browser@1.3.1': '@webext-core/fake-browser@1.3.1':
resolution: {integrity: sha512-NpBl0rXL6rT3msdl9Fb1GPLd/MKJEZ3pHpxuMdlu+qKW78T6SWJqDvyAVs8VjAmYs9RHoQJc+yObxQoGWdskXQ==} resolution: {integrity: sha512-NpBl0rXL6rT3msdl9Fb1GPLd/MKJEZ3pHpxuMdlu+qKW78T6SWJqDvyAVs8VjAmYs9RHoQJc+yObxQoGWdskXQ==}
@ -4143,6 +4164,9 @@ packages:
nth-check@2.1.1: nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
number-flow@0.3.7:
resolution: {integrity: sha512-N3pKXV7hw4PhdhZ3Z6QQspRO4djveotVBetxedHB3QrFw9oDluGfdwh7ju7mK9GO9CjTV9XmGjEcaCIwJ3IJMQ==}
nypm@0.3.12: nypm@0.3.12:
resolution: {integrity: sha512-D3pzNDWIvgA+7IORhD/IuWzEk4uXv6GsgOxiid4UU3h9oq5IqV1KtPDi63n4sZJ/xcWlr88c0QM2RgN5VbOhFA==} resolution: {integrity: sha512-D3pzNDWIvgA+7IORhD/IuWzEk4uXv6GsgOxiid4UU3h9oq5IqV1KtPDi63n4sZJ/xcWlr88c0QM2RgN5VbOhFA==}
engines: {node: ^14.16.0 || >=16.10.0} engines: {node: ^14.16.0 || >=16.10.0}
@ -6029,6 +6053,14 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@emotion/is-prop-valid@0.8.8':
dependencies:
'@emotion/memoize': 0.7.4
optional: true
'@emotion/memoize@0.7.4':
optional: true
'@esbuild/aix-ppc64@0.21.5': '@esbuild/aix-ppc64@0.21.5':
optional: true optional: true
@ -6318,6 +6350,12 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5 '@nodelib/fs.scandir': 2.1.5
fastq: 1.17.1 fastq: 1.17.1
'@number-flow/react@0.3.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
number-flow: 0.3.7
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
'@octokit/auth-token@5.1.1': {} '@octokit/auth-token@5.1.1': {}
'@octokit/core@6.1.2': '@octokit/core@6.1.2':
@ -7324,6 +7362,8 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@webcomponents/custom-elements@1.6.0': {}
'@webext-core/fake-browser@1.3.1': '@webext-core/fake-browser@1.3.1':
dependencies: dependencies:
lodash.merge: 4.6.2 lodash.merge: 4.6.2
@ -8612,10 +8652,11 @@ snapshots:
fraction.js@4.3.7: {} fraction.js@4.3.7: {}
framer-motion@11.11.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1): framer-motion@11.11.13(@emotion/is-prop-valid@0.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies: dependencies:
tslib: 2.7.0 tslib: 2.7.0
optionalDependencies: optionalDependencies:
'@emotion/is-prop-valid': 0.8.8
react: 18.3.1 react: 18.3.1
react-dom: 18.3.1(react@18.3.1) react-dom: 18.3.1(react@18.3.1)
@ -10066,6 +10107,8 @@ snapshots:
dependencies: dependencies:
boolbase: 1.0.0 boolbase: 1.0.0
number-flow@0.3.7: {}
nypm@0.3.12: nypm@0.3.12:
dependencies: dependencies:
citty: 0.1.6 citty: 0.1.6

View file

@ -1,3 +1,4 @@
import '@webcomponents/custom-elements'
import Header from '@/app/content/views/Header' import Header from '@/app/content/views/Header'
import Footer from '@/app/content/views/Footer' import Footer from '@/app/content/views/Footer'
import Main from '@/app/content/views/Main' import Main from '@/app/content/views/Main'

View file

@ -1,6 +1,7 @@
import { type MouseEvent, type FC, type ReactElement } from 'react' import { type MouseEvent, type FC, type ReactElement } from 'react'
import { Button } from '@/components/ui/Button' import { Button } from '@/components/ui/Button'
import { cn } from '@/utils' import { cn } from '@/utils'
import NumberFlow from '@number-flow/react'
export interface LikeButtonIconProps { export interface LikeButtonIconProps {
children: JSX.Element children: JSX.Element
@ -40,7 +41,11 @@ const LikeButton: FC<LikeButtonProps> & { Icon: FC<LikeButtonIconProps> } = ({
size="xs" size="xs"
> >
{children} {children}
{!!count && <span className="min-w-0 text-xs">{count}</span>} {!!count && (
<span className="min-w-0 text-xs">
{import.meta.env.FIREFOX ? <span className="tabular-nums">{count}</span> : <NumberFlow value={count} />}
</span>
)}
</Button> </Button>
) )
} }

View file

@ -1,5 +1,5 @@
import { type FC } from 'react' import { type FC } from 'react'
import { FrownIcon, ThumbsUpIcon } from 'lucide-react' import { FrownIcon, HeartIcon } from 'lucide-react'
import LikeButton from './LikeButton' import LikeButton from './LikeButton'
import FormatDate from './FormatDate' import FormatDate from './FormatDate'
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/Avatar' import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/Avatar'
@ -71,7 +71,7 @@ const MessageItem: FC<MessageItemProps> = (props) => {
count={props.data.likeUsers.length} count={props.data.likeUsers.length}
> >
<LikeButton.Icon> <LikeButton.Icon>
<ThumbsUpIcon size={14}></ThumbsUpIcon> <HeartIcon size={14}></HeartIcon>
</LikeButton.Icon> </LikeButton.Icon>
</LikeButton> </LikeButton>
<LikeButton <LikeButton

View file

@ -50,7 +50,7 @@ const AppButton: FC<AppButtonProps> = ({ className }) => {
minX: 50, minX: 50,
maxX: window.innerWidth - 50, maxX: window.innerWidth - 50,
maxY: window.innerHeight - 22, maxY: window.innerHeight - 22,
minY: window.innerHeight / 2 minY: 750
}) })
useWindowResize(({ width, height }) => { useWindowResize(({ width, height }) => {

View file

@ -11,6 +11,7 @@ import { ScrollArea } from '@/components/ui/ScrollArea'
import { Virtuoso } from 'react-virtuoso' import { Virtuoso } from 'react-virtuoso'
import AvatarCircles from '@/components/magicui/AvatarCircles' import AvatarCircles from '@/components/magicui/AvatarCircles'
import Link from '@/components/Link' import Link from '@/components/Link'
import NumberFlow from '@number-flow/react'
const Header: FC = () => { const Header: FC = () => {
const siteInfo = getSiteInfo() const siteInfo = getSiteInfo()
@ -49,7 +50,7 @@ const Header: FC = () => {
</Avatar> </Avatar>
<HoverCard> <HoverCard>
<HoverCardTrigger asChild> <HoverCardTrigger asChild>
<Button className="overflow-hidden p-2" variant="link"> <Button className="overflow-hidden rounded-md p-2" variant="link">
<span className="truncate text-lg font-semibold text-slate-600 dark:text-slate-50"> <span className="truncate text-lg font-semibold text-slate-600 dark:text-slate-50">
{siteInfo.hostname.replace(/^www\./i, '')} {siteInfo.hostname.replace(/^www\./i, '')}
</span> </span>
@ -78,6 +79,7 @@ const Header: FC = () => {
<h4 className="flex-1 truncate text-sm font-semibold">{site.hostname.replace(/^www\./i, '')}</h4> <h4 className="flex-1 truncate text-sm font-semibold">{site.hostname.replace(/^www\./i, '')}</h4>
<div className="shrink-0 text-sm"> <div className="shrink-0 text-sm">
<div className="flex items-center gap-x-1 text-nowrap text-xs text-slate-500"> <div className="flex items-center gap-x-1 text-nowrap text-xs text-slate-500">
<div className="flex items-center gap-x-1 pt-px">
<span className="relative flex size-2"> <span className="relative flex size-2">
<span <span
className={cn( className={cn(
@ -92,10 +94,16 @@ const Header: FC = () => {
)} )}
></span> ></span>
</span> </span>
<span className="dark:text-slate-50"> <span className="flex items-center leading-none dark:text-slate-50">
ONLINE {site.users.length > 99 ? '99+' : site.users.length} <span className="py-[0.25em]">ONLINE</span>
</span> </span>
</div> </div>
{import.meta.env.FIREFOX ? (
<span className="tabular-nums">{site.users.length}</span>
) : (
<NumberFlow className="tabular-nums" willChange value={site.users.length} />
)}
</div>
</div> </div>
</div> </div>
<AvatarCircles max={9} size="xs" avatarUrls={site.users.map((user) => user.userAvatar)} /> <AvatarCircles max={9} size="xs" avatarUrls={site.users.map((user) => user.userAvatar)} />
@ -108,8 +116,9 @@ const Header: FC = () => {
</HoverCard> </HoverCard>
<HoverCard> <HoverCard>
<HoverCardTrigger asChild> <HoverCardTrigger asChild>
<Button className="p-0" variant="link"> <Button className=" rounded-md p-0 hover:no-underline" variant="link">
<div className="flex items-center gap-x-1 text-nowrap text-xs text-slate-500"> <div className="relative flex items-center gap-x-1 text-nowrap text-xs text-slate-500 hover:after:absolute hover:after:bottom-0 hover:after:left-0 hover:after:h-px hover:after:w-full hover:after:bg-black">
<div className="flex items-center gap-x-1 pt-px">
<span className="relative flex size-2"> <span className="relative flex size-2">
<span <span
className={cn( className={cn(
@ -124,7 +133,18 @@ const Header: FC = () => {
)} )}
></span> ></span>
</span> </span>
<span className="dark:text-slate-50">ONLINE {chatOnlineCount > 99 ? '99+' : chatOnlineCount}</span> <span className="flex items-center leading-none dark:text-slate-50">
<span className="py-[0.25em]">ONLINE</span>
</span>
</div>
{import.meta.env.FIREFOX ? (
<span className="tabular-nums">{Math.min(chatUserList.length, 99)}</span>
) : (
<span className="tabular-nums">
<NumberFlow className="tabular-nums" willChange value={Math.min(chatUserList.length, 99)} />
{chatUserList.length > 99 && <span className="text-xs">+</span>}
</span>
)}
</div> </div>
</Button> </Button>
</HoverCardTrigger> </HoverCardTrigger>