feat: support dark mode

This commit is contained in:
guiboji 2024-10-21 18:31:16 +08:00
parent 4eba638a36
commit 010aa2f45e
18 changed files with 69 additions and 48 deletions

4
.gitignore vendored
View file

@ -14,4 +14,8 @@ web-ext.config.ts
*.pem *.pem
*.xpi *.xpi
*.zip *.zip
*.xml
*.gitignore
.idea
*.yaml

View file

@ -34,7 +34,7 @@ export default function App() {
const userInfoSetFinished = useRemeshQuery(userInfoDomain.query.UserInfoSetIsFinishedQuery()) const userInfoSetFinished = useRemeshQuery(userInfoDomain.query.UserInfoSetIsFinishedQuery())
const messageListLoadFinished = useRemeshQuery(messageListDomain.query.LoadIsFinishedQuery()) const messageListLoadFinished = useRemeshQuery(messageListDomain.query.LoadIsFinishedQuery())
const userInfoLoadFinished = useRemeshQuery(userInfoDomain.query.UserInfoLoadIsFinishedQuery()) const userInfoLoadFinished = useRemeshQuery(userInfoDomain.query.UserInfoLoadIsFinishedQuery())
const userInfo = useRemeshQuery(userInfoDomain.query.UserInfoQuery())
const notUserInfo = userInfoLoadFinished && !userInfoSetFinished const notUserInfo = userInfoLoadFinished && !userInfoSetFinished
useEffect(() => { useEffect(() => {
@ -59,7 +59,7 @@ export default function App() {
return ( return (
<> <>
<AppMain> <AppMain className={userInfo?.themeMode}>
<Header /> <Header />
<Main /> <Main />
<Footer /> <Footer />
@ -72,9 +72,10 @@ export default function App() {
</AnimatePresence> </AnimatePresence>
<Toaster richColors offset="70px" visibleToasts={1} position="top-center"></Toaster> <Toaster richColors offset="70px" visibleToasts={1} position="top-center"></Toaster>
</AppMain> </AppMain>
<AppButton></AppButton>
<DanmakuContainer ref={danmakuContainerRef} /> <AppButton className={userInfo?.themeMode}></AppButton>
<DanmakuContainer className={userInfo?.themeMode} ref={danmakuContainerRef} />
</> </>
) )
} }

View file

@ -30,7 +30,7 @@ const EmojiButton: FC<EmojiButtonProps> = ({ onSelect }) => {
return ( return (
<Popover open={open} onOpenChange={setOpen}> <Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button variant="ghost" size="icon"> <Button variant="ghost" size="icon" className="dark:text-white">
<SmileIcon size={20} /> <SmileIcon size={20} />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>

View file

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

View file

@ -52,7 +52,7 @@ const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>(
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
autoFocus={autoFocus} autoFocus={autoFocus}
maxLength={maxLength} maxLength={maxLength}
className="box-border resize-none whitespace-pre-wrap break-words border-none bg-gray-50 pb-5 [field-sizing:content] focus:ring-0 focus:ring-offset-0" className="box-border resize-none whitespace-pre-wrap break-words border-none bg-gray-50 pb-5 [field-sizing:content] focus:ring-0 focus:ring-offset-0 dark:bg-slate-800 dark:text-white"
rows={2} rows={2}
value={value} value={value}
onCompositionStart={onCompositionStart} onCompositionStart={onCompositionStart}
@ -63,7 +63,7 @@ const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>(
/> />
</ScrollArea> </ScrollArea>
)} )}
<div className="absolute bottom-1 right-3 rounded-lg text-xs text-slate-400"> <div className="absolute bottom-1 right-3 rounded-lg text-xs text-slate-400 dark:text-slate-50">
{value?.length ?? 0}/{maxLength} {value?.length ?? 0}/{maxLength}
</div> </div>
</div> </div>

View file

@ -28,7 +28,10 @@ const MessageItem: FC<MessageItemProps> = (props) => {
return ( return (
<div <div
data-index={props.index} data-index={props.index}
className={cn('box-border grid grid-cols-[auto_1fr] gap-x-2 px-4 first:pt-4 last:pb-4', props.className)} className={cn(
'box-border grid grid-cols-[auto_1fr] gap-x-2 px-4 first:pt-4 last:pb-4 dark:text-slate-50',
props.className
)}
> >
<Avatar> <Avatar>
<AvatarImage src={props.data.userAvatar} className="size-full" alt="avatar" /> <AvatarImage src={props.data.userAvatar} className="size-full" alt="avatar" />
@ -36,14 +39,14 @@ const MessageItem: FC<MessageItemProps> = (props) => {
</Avatar> </Avatar>
<div className="overflow-hidden"> <div className="overflow-hidden">
<div className="grid grid-cols-[1fr_auto] items-center gap-x-2 leading-none"> <div className="grid grid-cols-[1fr_auto] items-center gap-x-2 leading-none">
<div className="truncate text-sm font-semibold text-slate-600">{props.data.username}</div> <div className="truncate text-sm font-semibold text-slate-600 dark:text-slate-50">{props.data.username}</div>
<FormatDate className="text-xs text-slate-400" date={props.data.date}></FormatDate> <FormatDate className="text-xs text-slate-400 dark:text-slate-100" date={props.data.date}></FormatDate>
</div> </div>
<div> <div>
<div className="pb-2"> <div className="pb-2">
<Markdown>{props.data.body}</Markdown> <Markdown>{props.data.body}</Markdown>
</div> </div>
<div className="grid grid-flow-col justify-end gap-x-2 leading-none"> <div className="grid grid-flow-col justify-end gap-x-2 leading-none dark:text-slate-600">
<LikeButton <LikeButton
checked={props.like} checked={props.like}
onChange={(checked) => handleLikeChange(checked)} onChange={(checked) => handleLikeChange(checked)}

View file

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

View file

@ -1,4 +1,4 @@
import { type FC, useState, type MouseEvent, useRef, useEffect, useLayoutEffect } from 'react' import { type FC, useState, type MouseEvent, useRef, useEffect, useLayoutEffect, ReactNode } from 'react'
import { SettingsIcon, MoonIcon, SunIcon, HandIcon } from 'lucide-react' import { SettingsIcon, MoonIcon, SunIcon, HandIcon } from 'lucide-react'
import { motion, AnimatePresence } from 'framer-motion' import { motion, AnimatePresence } from 'framer-motion'
@ -22,7 +22,11 @@ import { messenger } from '@/messenger'
import useDarg from '@/hooks/useDarg' import useDarg from '@/hooks/useDarg'
import { useWindowSize } from 'react-use' import { useWindowSize } from 'react-use'
const AppButton: FC = () => { export interface AppButtonProps {
className?: string
}
const AppButton: FC<AppButtonProps> = ({ className }) => {
const send = useRemeshSend() const send = useRemeshSend()
const appStatusDomain = useRemeshDomain(AppStatusDomain()) const appStatusDomain = useRemeshDomain(AppStatusDomain())
const appOpenStatus = useRemeshQuery(appStatusDomain.query.OpenQuery()) const appOpenStatus = useRemeshQuery(appStatusDomain.query.OpenQuery())
@ -86,7 +90,7 @@ const AppButton: FC = () => {
return ( return (
<div <div
ref={menuRef} ref={menuRef}
className="fixed bottom-5 right-5 z-infinity grid w-min select-none justify-center gap-y-3" className={`fixed bottom-5 right-5 z-infinity grid w-min select-none justify-center gap-y-3 ${className}`}
style={{ style={{
left: `calc(${appPosition.x}px)`, left: `calc(${appPosition.x}px)`,
bottom: `calc(100vh - ${appPosition.y}px)`, bottom: `calc(100vh - ${appPosition.y}px)`,

View file

@ -8,9 +8,10 @@ import { useWindowSize } from 'react-use'
export interface AppMainProps { export interface AppMainProps {
children?: ReactNode children?: ReactNode
className?: string
} }
const AppMain: FC<AppMainProps> = ({ children }) => { const AppMain: FC<AppMainProps> = ({ children, className }) => {
const appStatusDomain = useRemeshDomain(AppStatusDomain()) const appStatusDomain = useRemeshDomain(AppStatusDomain())
const appOpenStatus = useRemeshQuery(appStatusDomain.query.OpenQuery()) const appOpenStatus = useRemeshQuery(appStatusDomain.query.OpenQuery())
const { x, y } = useRemeshQuery(appStatusDomain.query.PositionQuery()) const { x, y } = useRemeshQuery(appStatusDomain.query.PositionQuery())
@ -44,7 +45,7 @@ const AppMain: FC<AppMainProps> = ({ children }) => {
bottom: `calc(100vh - ${y}px + 22px)` bottom: `calc(100vh - ${y}px + 22px)`
}} }}
className={cn( className={cn(
'fixed inset-y-10 right-10 z-infinity mb-0 mt-auto box-border grid max-h-[min(calc(100vh_-60px),_1000px)] grid-flow-col grid-rows-[auto_1fr_auto] rounded-xl bg-slate-50 font-sans shadow-2xl', `fixed inset-y-10 right-10 z-infinity dark:bg-slate-800 mb-0 mt-auto box-border grid max-h-[min(calc(100vh_-60px),_1000px)] grid-flow-col grid-rows-[auto_1fr_auto] rounded-xl bg-slate-50 font-sans shadow-2xl ${className}`,
{ 'transition-transform': isAnimationComplete } { 'transition-transform': isAnimationComplete }
)} )}
> >
@ -52,7 +53,7 @@ const AppMain: FC<AppMainProps> = ({ children }) => {
<div <div
ref={ref} ref={ref}
className={cn( className={cn(
'absolute inset-y-3 z-20 w-1 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-20 w-1 dark:bg-slate-800 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' isOnRightSide ? '-left-0.5' : '-right-0.5'
)} )}
></div> ></div>

View file

@ -34,7 +34,7 @@ const Footer: FC = () => {
} }
return ( return (
<div className="relative z-10 grid gap-y-2 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"> <div className="relative z-10 grid gap-y-2 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-950 dark:text-slate-50">
<MessageInput <MessageInput
ref={inputRef} ref={inputRef}
value={message} value={message}
@ -49,9 +49,9 @@ const Footer: FC = () => {
{/* <Button variant="ghost" size="icon"> {/* <Button variant="ghost" size="icon">
<ImageIcon size={20} /> <ImageIcon size={20} />
</Button> */} </Button> */}
<Button className="ml-auto" size="sm" onClick={handleSend}> <Button className="ml-auto dark:bg-white" size="sm" onClick={handleSend}>
<span className="mr-2">Send</span> <span className="mr-2 text-slate-500">Send</span>
<CornerDownLeftIcon className="text-slate-400" size={12}></CornerDownLeftIcon> <CornerDownLeftIcon className="text-slate-500" size={12}></CornerDownLeftIcon>
</Button> </Button>
</div> </div>
</div> </div>

View file

@ -15,8 +15,8 @@ const Header: FC = () => {
const onlineCount = userList.length const onlineCount = userList.length
return ( 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"> <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"> <Avatar className="size-8 dark:text-slate-50">
<AvatarImage src={siteInfo.icon} alt="favicon" /> <AvatarImage src={siteInfo.icon} alt="favicon" />
<AvatarFallback> <AvatarFallback>
<Globe2Icon size="100%" className="text-gray-400" /> <Globe2Icon size="100%" className="text-gray-400" />
@ -25,7 +25,7 @@ const Header: FC = () => {
<HoverCard> <HoverCard>
<HoverCardTrigger asChild> <HoverCardTrigger asChild>
<Button className="overflow-hidden p-2" variant="link"> <Button className="overflow-hidden p-2" variant="link">
<span className="truncate text-lg font-semibold text-slate-600"> <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>
</Button> </Button>
@ -65,7 +65,7 @@ const Header: FC = () => {
)} )}
></span> ></span>
</span> </span>
<span>ONLINE {onlineCount > 99 ? '99+' : onlineCount}</span> <span className="dark:text-slate-50">ONLINE {onlineCount > 99 ? '99+' : onlineCount}</span>
</div> </div>
</Button> </Button>
</HoverCardTrigger> </HoverCardTrigger>

View file

@ -4,9 +4,14 @@ import ProfileForm from './components/ProfileForm'
import BadgeList from './components/BadgeList' import BadgeList from './components/BadgeList'
import Layout from './components/Layout' import Layout from './components/Layout'
import VersionLink from './components/VersionLink' import VersionLink from './components/VersionLink'
import { useRemeshDomain, useRemeshQuery } from 'remesh-react'
import UserInfoDomain from '@/domain/UserInfo'
function App() { function App() {
const userInfoDomain = useRemeshDomain(UserInfoDomain())
const userInfo = useRemeshQuery(userInfoDomain.query.UserInfoQuery())
return ( return (
<div className={userInfo?.themeMode}>
<Layout> <Layout>
<VersionLink></VersionLink> <VersionLink></VersionLink>
<Main> <Main>
@ -15,6 +20,7 @@ function App() {
</Main> </Main>
<BadgeList></BadgeList> <BadgeList></BadgeList>
</Layout> </Layout>
</div>
) )
} }

View file

@ -7,7 +7,7 @@ export interface LayoutProps {
const Layout: FC<LayoutProps> = ({ children }) => { const Layout: FC<LayoutProps> = ({ children }) => {
return ( return (
<div className="h-screen w-screen bg-gray-50 bg-[url(@/assets/images/texture.png)] font-sans"> <div className={`h-screen w-screen bg-gray-50 bg-[url(@/assets/images/texture.png)] font-sans dark:bg-slate-950`}>
<div className="fixed left-0 top-0 h-full w-screen overflow-hidden"> <div className="fixed left-0 top-0 h-full w-screen overflow-hidden">
<Meteors number={30} /> <Meteors number={30} />
</div> </div>

View file

@ -3,7 +3,7 @@ import { useForm } from 'react-hook-form'
import { valibotResolver } from '@hookform/resolvers/valibot' import { valibotResolver } from '@hookform/resolvers/valibot'
import { useRemeshDomain, useRemeshQuery, useRemeshSend } from 'remesh-react' import { useRemeshDomain, useRemeshQuery, useRemeshSend } from 'remesh-react'
import { nanoid } from 'nanoid' import { nanoid } from 'nanoid'
import { useEffect } from 'react' import { ReactNode, useEffect, type FC } from 'react'
import AvatarSelect from './AvatarSelect' import AvatarSelect from './AvatarSelect'
import { Button } from '@/components/ui/Button' import { Button } from '@/components/ui/Button'
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/Form' import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/Form'
@ -56,8 +56,7 @@ const formSchema = v.object({
danmakuEnabled: v.boolean(), danmakuEnabled: v.boolean(),
notificationEnabled: v.boolean() notificationEnabled: v.boolean()
}) })
const ProfileForm: FC = () => {
const ProfileForm = () => {
const send = useRemeshSend() const send = useRemeshSend()
const toast = ToastImpl.value const toast = ToastImpl.value
@ -97,7 +96,7 @@ const ProfileForm = () => {
<form <form
onSubmit={form.handleSubmit(handleSubmit)} onSubmit={form.handleSubmit(handleSubmit)}
autoComplete="off" autoComplete="off"
className="relative w-[450px] space-y-8 p-14 pt-20" className="relative w-[450px] space-y-8 p-14 pt-20 dark:bg-slate-900 dark:text-slate-50"
> >
<FormField <FormField
control={form.control} control={form.control}
@ -135,7 +134,7 @@ const ProfileForm = () => {
control={form.control} control={form.control}
name="name" name="name"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem className="dark:text-slate-50">
<FormLabel>Username</FormLabel> <FormLabel>Username</FormLabel>
<FormControl> <FormControl>
<Input placeholder="Please enter your username" {...field} /> <Input placeholder="Please enter your username" {...field} />
@ -227,7 +226,7 @@ const ProfileForm = () => {
</FormItem> </FormItem>
)} )}
/> />
<Button className="w-full" type="submit"> <Button className="w-full dark:bg-slate-800 dark:text-slate-50" type="submit">
Save Save
</Button> </Button>
</form> </form>

View file

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

View file

@ -29,7 +29,7 @@ const AvatarFallback = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback <AvatarPrimitive.Fallback
ref={ref} ref={ref}
className={cn('flex h-full w-full items-center justify-center rounded-full bg-muted', className)} className={cn('flex h-full w-full items-center justify-center rounded-full bg-muted dark:text-slate-400', className)}
{...props} {...props}
/> />
)) ))

View file

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