feat: upgrade to tailwind v4

This commit is contained in:
molvqingtai
2025-05-21 21:18:29 +08:00
parent 96f19bfc3a
commit 92147083c2
91 changed files with 5322 additions and 5851 deletions

View File

@@ -1,7 +1,6 @@
{
"plugins": {
"tailwindcss": {},
"autoprefixer": {},
"@tailwindcss/postcss": {},
"postcss-rem-to-responsive-pixel": {
"rootValue": 16,
"propList": [

View File

@@ -4,13 +4,17 @@
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "@/assets/styles.tailwind.css",
"config": "",
"css": "src/assets/styles/tailwind.css",
"baseColor": "slate",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/utils/index"
}
"utils": "@/utils/index",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View File

@@ -3,7 +3,8 @@ import globals from 'globals'
import pluginJs from '@eslint/js'
import tseslint from 'typescript-eslint'
import reactPlugin from '@eslint-react/eslint-plugin'
import tailwindPlugin from 'eslint-plugin-tailwindcss'
// https://github.com/francoismassart/eslint-plugin-tailwindcss/issues/325
// import tailwindPlugin from 'eslint-plugin-tailwindcss'
import prettierPlugin from 'eslint-plugin-prettier/recommended'
import * as tsParser from '@typescript-eslint/parser'
@@ -14,7 +15,7 @@ export default [
},
pluginJs.configs.recommended,
...tseslint.configs.recommended,
...tailwindPlugin.configs['flat/recommended'],
// ...tailwindPlugin.configs['flat/recommended'],
prettierPlugin,
{
files: ['**/*.{ts,tsx}'],
@@ -24,7 +25,7 @@ export default [
}
},
{
ignores: ['**/.output/*', '**/.wxt/*', '**/ui/**', '**/magicui/**', '**/lib/**', '**.million**']
ignores: ['**/.output/*', '**/.wxt/*', '**/ui/**', '**/magicui/**', '**/lib/**', '**/patches/**']
},
{
rules: {
@@ -35,7 +36,9 @@ export default [
'@eslint-react/no-array-index-key': 'off',
'@eslint-react/hooks-extra/no-redundant-custom-hook': 'off',
'@eslint-react/dom/no-missing-button-type': 'off',
'@eslint-react/hooks-extra/prefer-use-state-lazy-initialization': 'off'
'@eslint-react/hooks-extra/prefer-use-state-lazy-initialization': 'off',
'@eslint-react/no-unstable-context-value': 'off',
'@typescript-eslint/consistent-type-imports': 'error'
}
}
]

View File

@@ -13,7 +13,7 @@
"pack": "cross-env NODE_ENV=production run-p pack:*",
"pack:chrome": "wxt zip -b chrome",
"pack:firefox": "wxt zip -b firefox",
"lint": "eslint --fix --flag unstable_ts_config",
"lint": "eslint --fix --cache",
"clear": "rimraf .output",
"check": "tsc --noEmit",
"prepare": "husky",
@@ -44,97 +44,100 @@
},
"homepage": "https://github.com/molvqingtai/WebChat",
"dependencies": {
"@hookform/resolvers": "^3.9.1",
"@number-flow/react": "^0.3.4",
"@perfsee/jsonr": "^1.13.0",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-hover-card": "^1.1.2",
"@hookform/resolvers": "^5.0.1",
"@number-flow/react": "^0.5.9",
"@perfsee/jsonr": "^1.14.2",
"@radix-ui/react-avatar": "^1.1.9",
"@radix-ui/react-checkbox": "^1.3.1",
"@radix-ui/react-dropdown-menu": "^2.1.14",
"@radix-ui/react-hover-card": "^1.1.13",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.2",
"@radix-ui/react-portal": "^1.1.2",
"@radix-ui/react-presence": "^1.1.1",
"@radix-ui/react-radio-group": "^1.2.1",
"@radix-ui/react-scroll-area": "^1.2.1",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-label": "^2.1.6",
"@radix-ui/react-popover": "^1.1.13",
"@radix-ui/react-portal": "^1.1.8",
"@radix-ui/react-presence": "^1.1.4",
"@radix-ui/react-radio-group": "^1.3.6",
"@radix-ui/react-scroll-area": "^1.2.8",
"@radix-ui/react-slot": "^1.2.2",
"@radix-ui/react-switch": "^1.2.4",
"@resreq/event-hub": "^1.6.0",
"@resreq/timer": "^1.1.6",
"@rtco/client": "^0.2.17",
"@tailwindcss/typography": "^0.5.15",
"@resreq/timer": "^1.3.2",
"@rtco/client": "^0.3.2",
"@tailwindcss/typography": "^0.5.16",
"@webcomponents/custom-elements": "^1.6.0",
"@webext-core/messaging": "^2.1.0",
"@webext-core/proxy-service": "^1.2.0",
"class-variance-authority": "^0.7.0",
"@webext-core/messaging": "^2.2.0",
"@webext-core/proxy-service": "^1.2.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"danmu": "^0.14.0",
"cobe": "^0.6.3",
"danmu": "^0.16.0",
"date-fns": "^4.1.0",
"framer-motion": "^11.11.17",
"idb-keyval": "^6.2.1",
"lucide-react": "^0.460.0",
"nanoid": "^5.0.8",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.2",
"react-markdown": "^9.0.1",
"react-use": "^17.5.1",
"react-virtuoso": "^4.12.0",
"framer-motion": "^12.12.1",
"idb-keyval": "^6.2.2",
"lucide-react": "^0.511.0",
"motion": "^12.12.1",
"nanoid": "^5.1.5",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-hook-form": "^7.56.4",
"react-markdown": "^10.1.0",
"react-use": "^17.6.0",
"react-virtuoso": "^4.12.7",
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.0",
"remark-gfm": "^4.0.1",
"remesh": "^4.2.2",
"remesh-logger": "^4.1.0",
"remesh-react": "^4.1.2",
"rxjs": "^7.8.1",
"sonner": "^1.7.0",
"tailwind-merge": "^2.5.4",
"type-fest": "^4.27.0",
"unstorage": "^1.13.1",
"valibot": "1.0.0-beta.0"
"rxjs": "^7.8.2",
"sonner": "^2.0.3",
"tailwind-merge": "^3.3.0",
"type-fest": "^4.41.0",
"unstorage": "^1.16.0",
"valibot": "1.1.0",
"zod": "^3.25.7"
},
"devDependencies": {
"@commitlint/cli": "^19.5.0",
"@commitlint/config-conventional": "^19.5.0",
"@eslint-react/eslint-plugin": "^1.16.1",
"@eslint/js": "^9.15.0",
"@commitlint/cli": "^19.8.1",
"@commitlint/config-conventional": "^19.8.1",
"@eslint-react/eslint-plugin": "^1.49.0",
"@eslint/js": "^9.27.0",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/exec": "^6.0.3",
"@semantic-release/exec": "^7.1.0",
"@semantic-release/git": "^10.0.1",
"@tailwindcss/postcss": "^4.1.7",
"@tailwindcss/vite": "^4.1.7",
"@types/eslint": "^9.6.1",
"@types/eslint__js": "^8.42.3",
"@types/eslint-plugin-tailwindcss": "^3.17.0",
"@types/node": "^22.9.0",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@typescript-eslint/parser": "^8.14.0",
"@vitejs/plugin-react": "^4.3.3",
"autoprefixer": "^10.4.20",
"@types/node": "^22.15.19",
"@types/react": "^19.1.4",
"@types/react-dom": "^19.1.5",
"@typescript-eslint/parser": "^8.32.1",
"@vitejs/plugin-react": "^4.4.1",
"autoprefixer": "^10.4.21",
"cross-env": "^7.0.3",
"eslint": "^9.15.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-tailwindcss": "^3.17.5",
"globals": "^15.12.0",
"husky": "^9.1.6",
"jiti": "^2.4.0",
"lint-staged": "^15.2.10",
"eslint": "^9.27.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-prettier": "^5.4.0",
"eslint-plugin-tailwindcss": "^3.18.0",
"globals": "^16.1.0",
"husky": "^9.1.7",
"lint-staged": "^16.0.0",
"npm-run-all": "^4.1.5",
"postcss": "^8.4.49",
"postcss": "^8.5.3",
"postcss-rem-to-responsive-pixel": "^6.0.2",
"prettier": "^3.3.3",
"prettier": "^3.5.3",
"rimraf": "^6.0.1",
"semantic-release": "^24.2.0",
"tailwindcss": "^3.4.15",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.6.3",
"typescript-eslint": "^8.14.0",
"semantic-release": "^24.2.4",
"tailwindcss": "^4.1.7",
"tw-animate-css": "^1.3.0",
"typescript": "^5.8.3",
"typescript-eslint": "^8.32.1",
"vite-plugin-svgr": "^4.3.0",
"webext-bridge": "^6.0.1",
"wxt": "^0.19.16"
"wxt": "^0.20.6"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": "eslint --fix --flag unstable_ts_config"
"*.{js,jsx,ts,tsx}": "eslint --fix --cache"
},
"engines": {
"node": ">=20.0.0"

7934
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View File

@@ -1,7 +1,7 @@
import { EVENT } from '@/constants/event'
import { messenger } from '@/messenger'
import { browser, Tabs } from 'wxt/browser'
import { defineBackground } from 'wxt/sandbox'
import { browser, defineBackground } from '#imports'
import type { Browser } from 'wxt/browser'
export default defineBackground({
type: 'module',
@@ -10,7 +10,7 @@ export default defineBackground({
browser.runtime.openOptionsPage()
})
const historyNotificationTabs = new Map<string, Tabs.Tab>()
const historyNotificationTabs = new Map<string, Browser.tabs.Tab>()
messenger.onMessage(EVENT.OPTIONS_PAGE_OPEN, () => {
browser.runtime.openOptionsPage()
})
@@ -22,8 +22,6 @@ export default defineBackground({
return new URL(tab.url!).origin === new URL(sender.tab!.url!).origin
})
console.log('sender', sender)
if (hasActiveSomeSiteTab) return
browser.notifications.create(message.id, {
@@ -33,7 +31,7 @@ export default defineBackground({
message: message.body,
contextMessage: sender.tab!.url!
})
historyNotificationTabs.set(message.id, sender.tab!)
historyNotificationTabs.set(message.id, sender.tab! as Browser.tabs.Tab)
})
messenger.onMessage(EVENT.NOTIFICATION_CLEAR, async ({ data: id }) => {
browser.notifications.clear(id)
@@ -44,7 +42,7 @@ export default defineBackground({
if (fromTab?.id) {
try {
const tab = await browser.tabs.get(fromTab.id)
browser.tabs.update(tab.id, { active: true, highlighted: true })
browser.tabs.update(tab.id!, { active: true, highlighted: true })
browser.windows.update(tab.windowId!, { focused: true })
} catch {
browser.tabs.create({ url: fromTab.url })
@@ -57,7 +55,7 @@ export default defineBackground({
if (fromTab?.id) {
try {
const tab = await browser.tabs.get(fromTab.id)
browser.tabs.update(tab.id, { active: true })
browser.tabs.update(tab.id!, { active: true })
} catch {
browser.tabs.create({ url: fromTab.url })
}

View File

@@ -1,18 +1,18 @@
import '@webcomponents/custom-elements'
import Header from '@/app/content/views/Header'
import Footer from '@/app/content/views/Footer'
import Main from '@/app/content/views/Main'
import AppButton from '@/app/content/views/AppButton'
import AppMain from '@/app/content/views/AppMain'
import Header from '@/app/content/views/header'
import Footer from '@/app/content/views/footer'
import Main from '@/app/content/views/main'
import AppButton from '@/app/content/views/app-button'
import AppMain from '@/app/content/views/app-main'
import { useRemeshDomain, useRemeshQuery, useRemeshSend } from 'remesh-react'
import ChatRoomDomain from '@/domain/ChatRoom'
import UserInfoDomain from '@/domain/UserInfo'
import Setup from '@/app/content/views/Setup'
import Setup from '@/app/content/views/setup'
import MessageListDomain from '@/domain/MessageList'
import { useEffect, useRef } from 'react'
import { Toaster } from 'sonner'
import DanmakuContainer from './components/DanmakuContainer'
import DanmakuContainer from './components/danmaku-container'
import DanmakuDomain from '@/domain/Danmaku'
import AppStatusDomain from '@/domain/AppStatus'
import { checkDarkMode, cn } from '@/utils'

View File

@@ -1,89 +0,0 @@
import { forwardRef, type ChangeEvent, CompositionEvent, type KeyboardEvent, ClipboardEvent } from 'react'
import { cn } from '@/utils'
import { Textarea } from '@/components/ui/Textarea'
import { ScrollArea } from '@/components/ui/ScrollArea'
import LoadingIcon from '@/assets/images/loading.svg'
export interface MessageInputProps {
value?: string
className?: string
maxLength?: number
preview?: boolean
autoFocus?: boolean
disabled?: boolean
loading?: boolean
onInput?: (e: ChangeEvent<HTMLTextAreaElement>) => void
onPaste?: (e: ClipboardEvent<HTMLTextAreaElement>) => void
onKeyDown?: (e: KeyboardEvent<HTMLTextAreaElement>) => void
onCompositionStart?: (e: CompositionEvent<HTMLTextAreaElement>) => void
onCompositionEnd?: (e: CompositionEvent<HTMLTextAreaElement>) => void
}
/**
* Need @ syntax highlighting? Waiting for textarea to support Highlight API
*
* @see https://github.com/w3c/csswg-drafts/issues/4603
*/
const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>(
(
{
value = '',
className,
maxLength = 500,
onInput,
onPaste,
onKeyDown,
onCompositionStart,
onCompositionEnd,
autoFocus,
disabled,
loading
},
ref
) => {
return (
<div className={cn('relative', className)}>
<ScrollArea className="box-border max-h-28 w-full rounded-lg border border-input bg-background ring-offset-background focus-within:ring-1 focus-within:ring-ring 2xl:max-h-40">
<Textarea
ref={ref}
onPaste={onPaste}
onKeyDown={onKeyDown}
autoFocus={autoFocus}
maxLength={maxLength}
className={cn(
'box-border resize-none whitespace-pre-wrap break-words border-none bg-slate-100 pb-5 [field-sizing:content] [word-break:break-word] focus:ring-0 focus:ring-offset-0 dark:bg-slate-800',
{
'disabled:opacity-100': loading
}
)}
rows={2}
value={value}
spellCheck={false}
onCompositionStart={onCompositionStart}
onCompositionEnd={onCompositionEnd}
placeholder="Type your message here."
onInput={onInput}
disabled={disabled || loading}
/>
</ScrollArea>
<div
className={cn('absolute bottom-1 right-3 rounded-lg text-xs text-slate-400', {
'opacity-50': disabled || loading
})}
>
{value?.length ?? 0}/{maxLength}
</div>
{loading && (
<div className="absolute inset-0 flex items-center justify-center text-slate-800 after:absolute after:inset-0 after:backdrop-blur-xs dark:text-slate-100">
<LoadingIcon className="relative z-10 size-10"></LoadingIcon>
</div>
)}
</div>
)
}
)
MessageInput.displayName = 'MessageInput'
export default MessageInput

View File

@@ -1,18 +1,18 @@
import { cn } from '@/utils'
import { forwardRef } from 'react'
import type { Ref } from 'react'
export interface DanmakuContainerProps {
className?: string
}
const DanmakuContainer = forwardRef<HTMLDivElement, DanmakuContainerProps>(({ className }, ref) => {
const DanmakuContainer = ({ ref, className }: DanmakuContainerProps & { ref?: Ref<HTMLDivElement | null> }) => {
return (
<div
className={cn('fixed left-0 top-0 z-infinity w-full h-full invisible pointer-events-none shadow-md', className)}
ref={ref}
></div>
)
})
}
DanmakuContainer.displayName = 'DanmakuContainer'

View File

@@ -1,9 +1,9 @@
import { Avatar, AvatarFallback } from '@/components/ui/Avatar'
import { Button } from '@/components/ui/Button'
import { TextMessage } from '@/domain/ChatRoom'
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import { Button } from '@/components/ui/button'
import type { TextMessage } from '@/domain/ChatRoom'
import { cn } from '@/utils'
import { AvatarImage } from '@radix-ui/react-avatar'
import { FC, MouseEvent } from 'react'
import type { FC, MouseEvent } from 'react'
export interface PromptItemProps {
data: TextMessage

View File

@@ -1,8 +1,8 @@
import { SmileIcon } from 'lucide-react'
import { useState, type FC } from 'react'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
import { ScrollArea } from '@/components/ui/ScrollArea'
import { Button } from '@/components/ui/Button'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Button } from '@/components/ui/button'
import { EMOJI_LIST } from '@/constants/config'
import { chunk } from '@/utils'
@@ -31,7 +31,7 @@ const EmojiButton: FC<EmojiButtonProps> = ({ onSelect }) => {
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button variant="ghost" size="icon" className="dark:text-white">
<SmileIcon size={20} />
<SmileIcon className="size-5" />
</Button>
</PopoverTrigger>
<PopoverContent

View File

@@ -1,4 +1,4 @@
import { Button } from '@/components/ui/Button'
import { Button } from '@/components/ui/button'
import { createElement } from '@/utils'
import { ImageIcon } from 'lucide-react'
@@ -24,7 +24,7 @@ const ImageButton = ({ onSelect, disabled }: ImageButtonProps) => {
return (
<Button disabled={disabled} onClick={handleClick} variant="ghost" size="icon" className="dark:text-white">
<ImageIcon size={20} />
<ImageIcon className="size-5" />
</Button>
)
}

View File

@@ -1,10 +1,11 @@
import type { ReactNode } 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 NumberFlow from '@number-flow/react'
export interface LikeButtonIconProps {
children: JSX.Element
children: ReactNode
}
export const LikeButtonIcon: FC<LikeButtonIconProps> = ({ children }) => children

View File

@@ -0,0 +1,86 @@
import type { CompositionEvent, ClipboardEvent, Ref } from 'react'
import { type ChangeEvent, type KeyboardEvent } from 'react'
import { cn } from '@/utils'
import { Textarea } from '@/components/ui/textarea'
import { ScrollArea } from '@/components/ui/scroll-area'
import LoadingIcon from '@/assets/images/loading.svg'
export interface MessageInputProps {
value?: string
className?: string
maxLength?: number
preview?: boolean
autoFocus?: boolean
disabled?: boolean
loading?: boolean
onInput?: (e: ChangeEvent<HTMLTextAreaElement>) => void
onPaste?: (e: ClipboardEvent<HTMLTextAreaElement>) => void
onKeyDown?: (e: KeyboardEvent<HTMLTextAreaElement>) => void
onCompositionStart?: (e: CompositionEvent<HTMLTextAreaElement>) => void
onCompositionEnd?: (e: CompositionEvent<HTMLTextAreaElement>) => void
}
/**
* Need @ syntax highlighting? Waiting for textarea to support Highlight API
*
* @see https://github.com/w3c/csswg-drafts/issues/4603
*/
const MessageInput = ({
ref,
value = '',
className,
maxLength = 500,
onInput,
onPaste,
onKeyDown,
onCompositionStart,
onCompositionEnd,
autoFocus,
disabled,
loading
}: MessageInputProps & { ref?: Ref<HTMLTextAreaElement | null> }) => {
return (
<div className={cn('relative', className)}>
<ScrollArea className="box-border max-h-28 w-full rounded-lg border transition-[color,box-shadow] shadow-xs border-input bg-background 2xl:max-h-40 dark:bg-input/30 focus-within:ring-1 focus-within:border-ring focus-within:ring-ring/50">
<Textarea
ref={ref}
onPaste={onPaste}
onKeyDown={onKeyDown}
autoFocus={autoFocus}
maxLength={maxLength}
rows={2}
value={value}
spellCheck={false}
onCompositionStart={onCompositionStart}
onCompositionEnd={onCompositionEnd}
placeholder="Type your message here."
onInput={onInput}
disabled={disabled || loading}
className={cn(
'box-border resize-none whitespace-pre-wrap text-foreground break-words border-none bg-slate-100 pb-5 [word-break:break-word] dark:bg-slate-800 focus-visible:ring-0',
{
'disabled:opacity-100': loading
}
)}
></Textarea>
</ScrollArea>
<div
className={cn('absolute bottom-1 right-3 rounded-lg text-xs text-slate-400', {
'opacity-50': disabled || loading
})}
>
{value?.length ?? 0}/{maxLength}
</div>
{loading && (
<div className="absolute inset-0 flex items-center justify-center text-slate-800 after:absolute after:inset-0 after:backdrop-blur-xs dark:text-slate-100">
<LoadingIcon className="relative z-10 size-10"></LoadingIcon>
</div>
)}
</div>
)
}
MessageInput.displayName = 'MessageInput'
export default MessageInput

View File

@@ -1,10 +1,10 @@
import { type FC } from 'react'
import { FrownIcon, HeartIcon } from 'lucide-react'
import LikeButton from './LikeButton'
import FormatDate from './FormatDate'
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/Avatar'
import LikeButton from './like-button'
import FormatDate from './format-date'
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
import { Markdown } from '@/components/Markdown'
import { Markdown } from '@/components/markdown'
import { type NormalMessage } from '@/domain/MessageList'
import { cn } from '@/utils'

View File

@@ -1,8 +1,9 @@
import { FC, useState, type ReactElement } from 'react'
import type { FC } from 'react'
import { useState, type ReactElement } from 'react'
import { type MessageItemProps } from './MessageItem'
import { type PromptItemProps } from './PromptItem'
import { ScrollArea } from '@/components/ui/ScrollArea'
import { type MessageItemProps } from './message-item'
import { type PromptItemProps } from './prompt-item'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Virtuoso } from 'react-virtuoso'
export interface MessageListProps {

View File

@@ -1,9 +1,9 @@
import { Avatar, AvatarFallback } from '@/components/ui/Avatar'
import { Badge } from '@/components/ui/Badge'
import { PromptMessage } from '@/domain/MessageList'
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import { Badge } from '@/components/ui/badge'
import type { PromptMessage } from '@/domain/MessageList'
import { cn } from '@/utils'
import { AvatarImage } from '@radix-ui/react-avatar'
import { FC } from 'react'
import type { FC } from 'react'
export interface PromptItemProps {
data: PromptMessage

View File

@@ -3,8 +3,7 @@ import { createRoot } from 'react-dom/client'
import { Remesh } from 'remesh'
import { RemeshRoot, RemeshScope } from 'remesh-react'
// import { RemeshLogger } from 'remesh-logger'
import { defineContentScript } from 'wxt/sandbox'
import { createShadowRootUi } from 'wxt/client'
import { defineContentScript, createShadowRootUi } from '#imports'
import App from './App'
import { LocalStorageImpl, IndexDBStorageImpl, BrowserSyncStorageImpl } from '@/domain/impls/Storage'
@@ -14,9 +13,9 @@ import { ToastImpl } from '@/domain/impls/Toast'
import { ChatRoomImpl } from '@/domain/impls/ChatRoom'
import { VirtualRoomImpl } from '@/domain/impls/VirtualRoom'
// Remove import after merging: https://github.com/emilkowalski/sonner/pull/508
import '@/assets/styles/sonner.css'
import '@/assets/styles/overlay.css'
import 'sonner/dist/styles.css'
import '@/assets/styles/tailwind.css'
import '@/assets/styles/overlay.css'
import NotificationDomain from '@/domain/Notification'
import { createElement } from '@/utils'
@@ -26,12 +25,12 @@ 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'
})
// window.CSS.registerProperty({
// name: '--shimmer-angle',
// syntax: '<angle>',
// inherits: false,
// initialValue: '0deg'
// })
const store = Remesh.store({
externs: [

View File

@@ -1,9 +1,10 @@
import { ChangeEvent, useMemo, useRef, useState, KeyboardEvent, type FC, ClipboardEvent } from 'react'
import type { ChangeEvent, KeyboardEvent, ClipboardEvent } from 'react'
import { useMemo, useRef, useState, type FC } from 'react'
import { CornerDownLeftIcon } from 'lucide-react'
import { useRemeshDomain, useRemeshQuery, useRemeshSend } from 'remesh-react'
import MessageInput from '../../components/MessageInput'
import EmojiButton from '../../components/EmojiButton'
import { Button } from '@/components/ui/Button'
import MessageInput from '../../components/message-input'
import EmojiButton from '../../components/emoji-button'
import { Button } from '@/components/ui/button'
import MessageInputDomain from '@/domain/MessageInput'
import { MESSAGE_MAX_LENGTH, WEB_RTC_MAX_MESSAGE_SIZE } from '@/constants/config'
import ChatRoomDomain from '@/domain/ChatRoom'
@@ -12,14 +13,15 @@ import useShareRef from '@/hooks/useShareRef'
import { Presence } from '@radix-ui/react-presence'
import { Portal } from '@radix-ui/react-portal'
import useTriggerAway from '@/hooks/useTriggerAway'
import { ScrollArea } from '@/components/ui/ScrollArea'
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso'
import { ScrollArea } from '@/components/ui/scroll-area'
import type { VirtuosoHandle } from 'react-virtuoso'
import { Virtuoso } from 'react-virtuoso'
import UserInfoDomain from '@/domain/UserInfo'
import { blobToBase64, cn, compressImage, getRootNode, getTextByteSize, getTextSimilarity } from '@/utils'
import { Avatar, AvatarFallback } from '@/components/ui/Avatar'
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import { AvatarImage } from '@radix-ui/react-avatar'
import ToastDomain from '@/domain/Toast'
import ImageButton from '../../components/ImageButton'
import ImageButton from '../../components/image-button'
import { nanoid } from 'nanoid'
const Footer: FC = () => {
@@ -312,7 +314,7 @@ const Footer: FC = () => {
const root = getRootNode()
return (
<div className="relative 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">
<div className="relative 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 dark:before:from-slate-900">
<Presence present={autoCompleteListShow}>
<Portal
container={root}

View File

@@ -1,16 +1,17 @@
import { useState, type FC } from 'react'
import { Globe2Icon } from 'lucide-react'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/Avatar'
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/HoverCard'
import { Button } from '@/components/ui/Button'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'
import { Button } from '@/components/ui/button'
import { cn, getSiteInfo } from '@/utils'
import { useRemeshDomain, useRemeshQuery } from 'remesh-react'
import ChatRoomDomain from '@/domain/ChatRoom'
import VirtualRoomDomain, { FromInfo, RoomUser } from '@/domain/VirtualRoom'
import { ScrollArea } from '@/components/ui/ScrollArea'
import type { FromInfo, RoomUser } from '@/domain/VirtualRoom'
import VirtualRoomDomain from '@/domain/VirtualRoom'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Virtuoso } from 'react-virtuoso'
import AvatarCircles from '@/components/magicui/AvatarCircles'
import Link from '@/components/Link'
import { AvatarCircles } from '@/components/magicui/avatar-circles'
import Link from '@/components/link'
import NumberFlow from '@number-flow/react'
const Header: FC = () => {
@@ -108,7 +109,7 @@ const Header: FC = () => {
</div>
</div>
</div>
<AvatarCircles max={9} size="xs" avatarUrls={site.users.map((user) => user.userAvatar)} />
<AvatarCircles maxLength={9} size="xs" avatarUrls={site.users.map((user) => user.userAvatar)} />
</div>
</Link>
)}

View File

@@ -1,9 +1,9 @@
import { type FC } from 'react'
import { useRemeshDomain, useRemeshQuery, useRemeshSend } from 'remesh-react'
import MessageList from '../../components/MessageList'
import MessageItem from '../../components/MessageItem'
import PromptItem from '../../components/PromptItem'
import MessageList from '../../components/message-list'
import MessageItem from '../../components/message-item'
import PromptItem from '../../components/prompt-item'
import UserInfoDomain from '@/domain/UserInfo'
import ChatRoomDomain, { MessageType } from '@/domain/ChatRoom'
import MessageListDomain from '@/domain/MessageList'

View File

@@ -1,18 +1,21 @@
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/Avatar'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { MAX_AVATAR_SIZE } from '@/constants/config'
import MessageListDomain, { Message, MessageType } from '@/domain/MessageList'
import UserInfoDomain, { UserInfo } from '@/domain/UserInfo'
import type { Message } from '@/domain/MessageList'
import MessageListDomain, { MessageType } from '@/domain/MessageList'
import type { UserInfo } from '@/domain/UserInfo'
import UserInfoDomain from '@/domain/UserInfo'
import { generateRandomAvatar, generateRandomName } from '@/utils'
import { UserIcon } from 'lucide-react'
import { nanoid } from 'nanoid'
import { FC, useEffect, useState } from 'react'
import type { FC } from 'react'
import { useEffect, useState } from 'react'
import { useRemeshDomain, useRemeshSend } from 'remesh-react'
import Timer from '@resreq/timer'
import ExampleImage from '@/assets/images/example.jpg'
import PulsatingButton from '@/components/magicui/PulsatingButton'
import BlurFade from '@/components/magicui/BlurFade'
import WordPullUp from '@/components/magicui/WordPullUp'
import { PulsatingButton } from '@/components/magicui/pulsating-button'
import { BlurFade } from '@/components/magicui/blur-fade'
import { motion } from 'framer-motion'
import { WordRotate } from '@/components/magicui/word-rotate'
const mockTextList = [
`你問我支持不支持,我說我支持`,
@@ -92,7 +95,7 @@ const Setup: FC = () => {
await createMessage(await refreshUserInfo())
}
},
{ delay: 2000, immediate: true, limit: mockTextList.length }
{ interval: 2000, immediate: true, limit: mockTextList.length }
)
timer.start()
return () => {
@@ -112,7 +115,7 @@ const Setup: FC = () => {
</AvatarFallback>
</Avatar>
</BlurFade>
<div className="flex" key={userInfo?.name}>
<div className="flex items-center" key={userInfo?.name}>
<motion.div
className="text-2xl font-bold text-primary"
initial={{ x: -10, opacity: 0 }}
@@ -121,7 +124,7 @@ const Setup: FC = () => {
>
@
</motion.div>
<WordPullUp className="text-2xl font-bold text-primary" words={`${userInfo?.name || ''.padEnd(10, ' ')}`} />
<WordRotate className="text-2xl font-bold text-primary" words={[`${userInfo?.name || ''.padEnd(10, ' ')}`]} />
</div>
<PulsatingButton onClick={handleSetup}>Start chatting</PulsatingButton>
</div>

View File

@@ -3,7 +3,7 @@ import { SettingsIcon, MoonIcon, SunIcon, HandIcon } from 'lucide-react'
import { motion, AnimatePresence } from 'framer-motion'
import { useRemeshDomain, useRemeshQuery, useRemeshSend } from 'remesh-react'
import { Button } from '@/components/ui/Button'
import { Button } from '@/components/ui/button'
import { EVENT } from '@/constants/event'
import UserInfoDomain from '@/domain/UserInfo'
import useTriggerAway from '@/hooks/useTriggerAway'
@@ -112,29 +112,29 @@ const AppButton: FC<AppButtonProps> = ({ className }) => {
>
<div
className={cn(
'absolute grid grid-rows-[repeat(2,minmax(0,2.5rem))] w-full justify-center items-center transition-all duration-300',
'absolute grid grid-rows-[repeat(2,minmax(0,2.5rem))] w-full justify-center items-center transition-all duration-300 hover:bg-accent dark:hover:bg-accent',
isDarkMode ? 'top-0' : '-top-10',
isDarkMode ? 'bg-slate-950 text-white' : 'bg-white text-orange-400'
)}
>
<MoonIcon size={20} />
<SunIcon size={20} />
<MoonIcon className="size-5" />
<SunIcon className="size-5" />
</div>
</Button>
<Button
onClick={handleOpenOptionsPage}
variant="outline"
className="size-10 rounded-full p-0 shadow dark:border-slate-600"
className="size-10 rounded-full p-0 dark:bg-background shadow dark:text-foreground dark:border-slate-600 dark:hover:bg-accent"
>
<SettingsIcon size={20} />
<SettingsIcon className="size-5" />
</Button>
<Button
ref={appButtonRef}
variant="outline"
className="size-10 cursor-grab rounded-full p-0 shadow dark:border-slate-600"
className="size-10 cursor-grab dark:bg-background rounded-full p-0 dark:text-foreground shadow dark:border-slate-600 dark:hover:bg-accent"
>
<HandIcon size={20} />
<HandIcon className="size-5" />
</Button>
</motion.div>
)}
@@ -142,7 +142,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 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%)]"
className="relative z-20 size-11 rounded-full has-[>svg]: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 && (
@@ -161,7 +161,7 @@ const AppButton: FC<AppButtonProps> = ({ className }) => {
)}
</AnimatePresence>
<DayLogo className="relative z-20 max-h-full max-w-full overflow-hidden"></DayLogo>
<DayLogo className="relative z-20 max-h-full max-w-full overflow-hidden size-full"></DayLogo>
</Button>
</div>
)

View File

@@ -1,9 +1,9 @@
import { Toaster } from 'sonner'
import Main from './components/Main'
import ProfileForm from './components/ProfileForm'
import BadgeList from './components/BadgeList'
import Layout from './components/Layout'
import VersionLink from './components/VersionLink'
import Main from './components/main'
import ProfileForm from './components/profile-form'
import BadgeList from './components/badge-list'
import Layout from './components/layout'
import VersionLink from './components/version-link'
import { useRemeshDomain, useRemeshQuery } from 'remesh-react'
import UserInfoDomain from '@/domain/UserInfo'

View File

@@ -1,75 +0,0 @@
import React from 'react'
import { type ChangeEvent } from 'react'
import { ImagePlusIcon } from 'lucide-react'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/Avatar'
import { Label } from '@/components/ui/Label'
import { blobToBase64, cn, compressImage } from '@/utils'
export interface AvatarSelectProps {
value?: string
className?: string
disabled?: boolean
compressSize?: number
onSuccess?: (blob: string) => void
onWarning?: (error: Error) => void
onError?: (error: Error) => void
onChange?: (src: string) => void
}
const AvatarSelect = React.forwardRef<HTMLInputElement, AvatarSelectProps>(
({ onChange, value, onError, onWarning, onSuccess, className, compressSize = 8 * 1024, disabled }, ref) => {
const handleChange = async (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (file) {
if (!/image\/(png|jpeg|webp)/.test(file.type)) {
onWarning?.(new Error('Only PNG, JPEG and WebP image are supported.'))
return
}
try {
/**
* In chrome storage.sync, each key-value pair supports a maximum storage of 8kb
* and all key-value pairs support a maximum storage of 100kb.
*/
const blob = await compressImage({ input: file, targetSize: compressSize, outputType: 'image/webp' })
const base64 = await blobToBase64(blob)
onSuccess?.(base64)
onChange?.(base64)
} catch (error) {
onError?.(error as Error)
}
}
}
return (
<Label className="contents">
<Avatar
tabIndex={disabled ? -1 : 1}
className={cn(
'group h-24 w-24 cursor-pointer border-4 border-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background',
{
'cursor-not-allowed': disabled,
'opacity-50': disabled
},
className
)}
>
<AvatarImage src={value} className="size-full" alt="avatar" />
<AvatarFallback>
<ImagePlusIcon size={30} className="text-slate-400 group-hover:text-slate-500" />
</AvatarFallback>
</Avatar>
<input
ref={ref}
hidden
disabled={disabled}
type="file"
accept="image/png,image/jpeg,image/webp"
onChange={handleChange}
/>
</Label>
)
}
)
AvatarSelect.displayName = 'AvatarSelect'
export default AvatarSelect

View File

@@ -1,5 +1,5 @@
import Meteors from '@/components/magicui/Meteors'
import { FC, ReactNode } from 'react'
import { Meteors } from '@/components/magicui/meteors'
import type { FC, ReactNode } from 'react'
export interface LayoutProps {
children?: ReactNode

View File

@@ -0,0 +1,84 @@
import type { Ref } from 'react'
import React from 'react'
import { type ChangeEvent } from 'react'
import { ImagePlusIcon } from 'lucide-react'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Label } from '@/components/ui/label'
import { blobToBase64, cn, compressImage } from '@/utils'
export interface AvatarSelectProps {
value?: string
className?: string
disabled?: boolean
compressSize?: number
onSuccess?: (blob: string) => void
onWarning?: (error: Error) => void
onError?: (error: Error) => void
onChange?: (src: string) => void
}
const AvatarSelect = ({
ref,
onChange,
value,
onError,
onWarning,
onSuccess,
className,
compressSize = 8 * 1024,
disabled
}: AvatarSelectProps & { ref?: Ref<HTMLInputElement | null> }) => {
const handleChange = async (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (file) {
if (!/image\/(png|jpeg|webp)/.test(file.type)) {
onWarning?.(new Error('Only PNG, JPEG and WebP image are supported.'))
return
}
try {
/**
* In chrome storage.sync, each key-value pair supports a maximum storage of 8kb
* and all key-value pairs support a maximum storage of 100kb.
*/
const blob = await compressImage({ input: file, targetSize: compressSize, outputType: 'image/webp' })
const base64 = await blobToBase64(blob)
onSuccess?.(base64)
onChange?.(base64)
} catch (error) {
onError?.(error as Error)
}
}
}
return (
<Label className="contents">
<Avatar
tabIndex={disabled ? -1 : 1}
className={cn(
'group h-24 w-24 cursor-pointer border-4 border-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background',
{
'cursor-not-allowed': disabled,
'opacity-50': disabled
},
className
)}
>
<AvatarImage src={value} className="size-full" alt="avatar" />
<AvatarFallback>
<ImagePlusIcon size={30} className="text-slate-400 group-hover:text-slate-500" />
</AvatarFallback>
</Avatar>
<input
ref={ref}
hidden
disabled={disabled}
type="file"
accept="image/png,image/jpeg,image/webp"
onChange={handleChange}
/>
</Label>
)
}
AvatarSelect.displayName = 'AvatarSelect'
export default AvatarSelect

View File

@@ -1,7 +1,7 @@
import { FC } from 'react'
import { Button } from '@/components/ui/Button'
import type { FC } from 'react'
import { Button } from '@/components/ui/button'
import { GitHubLogoIcon } from '@radix-ui/react-icons'
import Link from '@/components/Link'
import Link from '@/components/link'
const BadgeList: FC = () => {
return (

View File

@@ -4,20 +4,20 @@ import { valibotResolver } from '@hookform/resolvers/valibot'
import { useRemeshDomain, useRemeshQuery, useRemeshSend } from 'remesh-react'
import { nanoid } from 'nanoid'
import { useEffect, type FC } from 'react'
import AvatarSelect from './AvatarSelect'
import { Button } from '@/components/ui/Button'
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/Form'
import { Input } from '@/components/ui/Input'
import AvatarSelect from './avatar-select'
import { Button } from '@/components/ui/button'
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import UserInfoDomain, { type UserInfo } from '@/domain/UserInfo'
import { cn, generateRandomAvatar } from '@/utils'
import { RadioGroup, RadioGroupItem } from '@/components/ui/RadioGroup'
import { Label } from '@/components/ui/Label'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { Label } from '@/components/ui/label'
import { RefreshCcwIcon } from 'lucide-react'
import { MAX_AVATAR_SIZE } from '@/constants/config'
import { ToastImpl } from '@/domain/impls/Toast'
import BlurFade from '@/components/magicui/BlurFade'
import { Checkbox } from '@/components/ui/Checkbox'
import Link from '@/components/Link'
import { BlurFade } from '@/components/magicui/blur-fade'
import { Checkbox } from '@/components/ui/checkbox'
import Link from '@/components/link'
const defaultUserInfo: UserInfo = {
id: nanoid(),

View File

@@ -1,6 +1,6 @@
import { FC } from 'react'
import { Button } from '@/components/ui/Button'
import Link from '@/components/Link'
import type { FC } from 'react'
import { Button } from '@/components/ui/button'
import Link from '@/components/link'
import { version } from '@/../package.json'
const VersionLink: FC = () => {

View File

@@ -4,6 +4,7 @@ import { Remesh } from 'remesh'
import { RemeshRoot } from 'remesh-react'
import App from './App'
import { BrowserSyncStorageImpl } from '@/domain/impls/Storage'
import 'sonner/dist/styles.css'
import '@/assets/styles/tailwind.css'
import { ToastImpl } from '@/domain/impls/Toast'

View File

@@ -2,14 +2,14 @@ section[aria-live='polite'] {
display: contents;
}
:where([data-sonner-toaster]) {
[data-sonner-toaster] {
max-width: 300px;
position: absolute;
display: flex;
justify-content: center;
}
:where([data-sonner-toast][data-styled='true']) {
[data-sonner-toast][data-styled='true'] {
max-width: 300px;
padding: 6px 12px;
border-radius: 9999px;

View File

@@ -1,686 +0,0 @@
:where(html[dir='ltr']),
:where([data-sonner-toaster][dir='ltr']) {
--toast-icon-margin-start: -3px;
--toast-icon-margin-end: 4px;
--toast-svg-margin-start: -1px;
--toast-svg-margin-end: 0px;
--toast-button-margin-start: auto;
--toast-button-margin-end: 0;
--toast-close-button-start: 0;
--toast-close-button-end: unset;
--toast-close-button-transform: translate(-35%, -35%);
}
:where(html[dir='rtl']),
:where([data-sonner-toaster][dir='rtl']) {
--toast-icon-margin-start: 4px;
--toast-icon-margin-end: -3px;
--toast-svg-margin-start: 0px;
--toast-svg-margin-end: -1px;
--toast-button-margin-start: 0;
--toast-button-margin-end: auto;
--toast-close-button-start: unset;
--toast-close-button-end: 0;
--toast-close-button-transform: translate(35%, -35%);
}
:where([data-sonner-toaster]) {
position: fixed;
width: var(--width);
font-family:
ui-sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont,
Segoe UI,
Roboto,
Helvetica Neue,
Arial,
Noto Sans,
sans-serif,
Apple Color Emoji,
Segoe UI Emoji,
Segoe UI Symbol,
Noto Color Emoji;
--gray1: hsl(0, 0%, 99%);
--gray2: hsl(0, 0%, 97.3%);
--gray3: hsl(0, 0%, 95.1%);
--gray4: hsl(0, 0%, 93%);
--gray5: hsl(0, 0%, 90.9%);
--gray6: hsl(0, 0%, 88.7%);
--gray7: hsl(0, 0%, 85.8%);
--gray8: hsl(0, 0%, 78%);
--gray9: hsl(0, 0%, 56.1%);
--gray10: hsl(0, 0%, 52.3%);
--gray11: hsl(0, 0%, 43.5%);
--gray12: hsl(0, 0%, 9%);
--border-radius: 8px;
box-sizing: border-box;
padding: 0;
margin: 0;
list-style: none;
outline: none;
z-index: 999999999;
transition: transform 400ms ease;
}
:where([data-sonner-toaster][data-lifted='true']) {
transform: translateY(-10px);
}
@media (hover: none) and (pointer: coarse) {
:where([data-sonner-toaster][data-lifted='true']) {
transform: none;
}
}
:where([data-sonner-toaster][data-x-position='right']) {
right: max(var(--offset), env(safe-area-inset-right));
}
:where([data-sonner-toaster][data-x-position='left']) {
left: max(var(--offset), env(safe-area-inset-left));
}
:where([data-sonner-toaster][data-x-position='center']) {
left: 50%;
transform: translateX(-50%);
}
:where([data-sonner-toaster][data-y-position='top']) {
top: max(var(--offset), env(safe-area-inset-top));
}
:where([data-sonner-toaster][data-y-position='bottom']) {
bottom: max(var(--offset), env(safe-area-inset-bottom));
}
:where([data-sonner-toast]) {
--y: translateY(100%);
--lift-amount: calc(var(--lift) * var(--gap));
z-index: var(--z-index);
position: absolute;
opacity: 0;
transform: var(--y);
filter: blur(0);
/* https://stackoverflow.com/questions/48124372/pointermove-event-not-working-with-touch-why-not */
touch-action: none;
transition:
transform 400ms,
opacity 400ms,
height 400ms,
box-shadow 200ms;
box-sizing: border-box;
outline: none;
overflow-wrap: anywhere;
}
:where([data-sonner-toast][data-styled='true']) {
padding: 16px;
background: var(--normal-bg);
border: 1px solid var(--normal-border);
color: var(--normal-text);
border-radius: var(--border-radius);
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1);
width: var(--width);
font-size: 13px;
display: flex;
align-items: center;
gap: 6px;
}
:where([data-sonner-toast]:focus-visible) {
box-shadow:
0px 4px 12px rgba(0, 0, 0, 0.1),
0 0 0 2px rgba(0, 0, 0, 0.2);
}
:where([data-sonner-toast][data-y-position='top']) {
top: 0;
--y: translateY(-100%);
--lift: 1;
--lift-amount: calc(1 * var(--gap));
}
:where([data-sonner-toast][data-y-position='bottom']) {
bottom: 0;
--y: translateY(100%);
--lift: -1;
--lift-amount: calc(var(--lift) * var(--gap));
}
:where([data-sonner-toast]) :where([data-description]) {
font-weight: 400;
line-height: 1.4;
color: inherit;
}
:where([data-sonner-toast]) :where([data-title]) {
font-weight: 500;
line-height: 1.5;
color: inherit;
}
:where([data-sonner-toast]) :where([data-icon]) {
display: flex;
height: 16px;
width: 16px;
position: relative;
justify-content: flex-start;
align-items: center;
flex-shrink: 0;
margin-left: var(--toast-icon-margin-start);
margin-right: var(--toast-icon-margin-end);
}
:where([data-sonner-toast][data-promise='true']) :where([data-icon]) > svg {
opacity: 0;
transform: scale(0.8);
transform-origin: center;
animation: sonner-fade-in 300ms ease forwards;
}
:where([data-sonner-toast]) :where([data-icon]) > * {
flex-shrink: 0;
}
:where([data-sonner-toast]) :where([data-icon]) svg {
margin-left: var(--toast-svg-margin-start);
margin-right: var(--toast-svg-margin-end);
}
:where([data-sonner-toast]) :where([data-content]) {
display: flex;
flex-direction: column;
gap: 2px;
}
[data-sonner-toast][data-styled='true'] [data-button] {
border-radius: 4px;
padding-left: 8px;
padding-right: 8px;
height: 24px;
font-size: 12px;
color: var(--normal-bg);
background: var(--normal-text);
margin-left: var(--toast-button-margin-start);
margin-right: var(--toast-button-margin-end);
border: none;
cursor: pointer;
outline: none;
display: flex;
align-items: center;
flex-shrink: 0;
transition:
opacity 400ms,
box-shadow 200ms;
}
:where([data-sonner-toast]) :where([data-button]):focus-visible {
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.4);
}
:where([data-sonner-toast]) :where([data-button]):first-of-type {
margin-left: var(--toast-button-margin-start);
margin-right: var(--toast-button-margin-end);
}
:where([data-sonner-toast]) :where([data-cancel]) {
color: var(--normal-text);
background: rgba(0, 0, 0, 0.08);
}
:where([data-sonner-toast][data-theme='dark']) :where([data-cancel]) {
background: rgba(255, 255, 255, 0.3);
}
:where([data-sonner-toast]) :where([data-close-button]) {
position: absolute;
left: var(--toast-close-button-start);
right: var(--toast-close-button-end);
top: 0;
height: 20px;
width: 20px;
display: flex;
justify-content: center;
align-items: center;
padding: 0;
color: var(--gray12);
border: 1px solid var(--gray4);
transform: var(--toast-close-button-transform);
border-radius: 50%;
cursor: pointer;
z-index: 1;
transition:
opacity 100ms,
background 200ms,
border-color 200ms;
}
[data-sonner-toast] [data-close-button] {
background: var(--gray1);
}
:where([data-sonner-toast]) :where([data-close-button]):focus-visible {
box-shadow:
0px 4px 12px rgba(0, 0, 0, 0.1),
0 0 0 2px rgba(0, 0, 0, 0.2);
}
:where([data-sonner-toast]) :where([data-disabled='true']) {
cursor: not-allowed;
}
:where([data-sonner-toast]):hover :where([data-close-button]):hover {
background: var(--gray2);
border-color: var(--gray5);
}
/* Leave a ghost div to avoid setting hover to false when swiping out */
:where([data-sonner-toast][data-swiping='true'])::before {
content: '';
position: absolute;
left: 0;
right: 0;
height: 100%;
z-index: -1;
}
:where([data-sonner-toast][data-y-position='top'][data-swiping='true'])::before {
/* y 50% needed to distribute height additional height evenly */
bottom: 50%;
transform: scaleY(3) translateY(50%);
}
:where([data-sonner-toast][data-y-position='bottom'][data-swiping='true'])::before {
/* y -50% needed to distribute height additional height evenly */
top: 50%;
transform: scaleY(3) translateY(-50%);
}
/* Leave a ghost div to avoid setting hover to false when transitioning out */
:where([data-sonner-toast][data-swiping='false'][data-removed='true'])::before {
content: '';
position: absolute;
inset: 0;
transform: scaleY(2);
}
/* Needed to avoid setting hover to false when inbetween toasts */
:where([data-sonner-toast])::after {
content: '';
position: absolute;
left: 0;
height: calc(var(--gap) + 1px);
bottom: 100%;
width: 100%;
}
:where([data-sonner-toast][data-mounted='true']) {
--y: translateY(0);
opacity: 1;
}
:where([data-sonner-toast][data-expanded='false'][data-front='false']) {
--scale: var(--toasts-before) * 0.05 + 1;
--y: translateY(calc(var(--lift-amount) * var(--toasts-before))) scale(calc(-1 * var(--scale)));
height: var(--front-toast-height);
}
:where([data-sonner-toast]) > * {
transition: opacity 400ms;
}
:where([data-sonner-toast][data-expanded='false'][data-front='false'][data-styled='true']) > * {
opacity: 0;
}
:where([data-sonner-toast][data-visible='false']) {
opacity: 0;
pointer-events: none;
}
:where([data-sonner-toast][data-mounted='true'][data-expanded='true']) {
--y: translateY(calc(var(--lift) * var(--offset)));
height: var(--initial-height);
}
:where([data-sonner-toast][data-removed='true'][data-front='true'][data-swipe-out='false']) {
--y: translateY(calc(var(--lift) * -100%));
opacity: 0;
}
:where([data-sonner-toast][data-removed='true'][data-front='false'][data-swipe-out='false'][data-expanded='true']) {
--y: translateY(calc(var(--lift) * var(--offset) + var(--lift) * -100%));
opacity: 0;
}
:where([data-sonner-toast][data-removed='true'][data-front='false'][data-swipe-out='false'][data-expanded='false']) {
--y: translateY(40%);
opacity: 0;
transition:
transform 500ms,
opacity 200ms;
}
/* Bump up the height to make sure hover state doesn't get set to false */
:where([data-sonner-toast][data-removed='true'][data-front='false'])::before {
height: calc(var(--initial-height) + 20%);
}
[data-sonner-toast][data-swiping='true'] {
transform: var(--y) translateY(var(--swipe-amount, 0px));
transition: none;
}
[data-sonner-toast][data-swiped='true'] {
user-select: none;
}
[data-sonner-toast][data-swipe-out='true'][data-y-position='bottom'],
[data-sonner-toast][data-swipe-out='true'][data-y-position='top'] {
animation: swipe-out 200ms ease-out forwards;
}
@keyframes swipe-out {
from {
transform: translateY(calc(var(--lift) * var(--offset) + var(--swipe-amount)));
opacity: 1;
}
to {
transform: translateY(calc(var(--lift) * var(--offset) + var(--swipe-amount) + var(--lift) * -100%));
opacity: 0;
}
}
@media (max-width: 600px) {
[data-sonner-toaster] {
position: fixed;
--mobile-offset: 16px;
right: var(--mobile-offset);
left: var(--mobile-offset);
width: 100%;
}
[data-sonner-toaster][dir='rtl'] {
left: calc(var(--mobile-offset) * -1);
}
[data-sonner-toaster] [data-sonner-toast] {
left: 0;
right: 0;
width: calc(100% - var(--mobile-offset) * 2);
}
[data-sonner-toaster][data-x-position='left'] {
left: var(--mobile-offset);
}
[data-sonner-toaster][data-y-position='bottom'] {
bottom: 20px;
}
[data-sonner-toaster][data-y-position='top'] {
top: 20px;
}
[data-sonner-toaster][data-x-position='center'] {
left: var(--mobile-offset);
right: var(--mobile-offset);
transform: none;
}
}
[data-sonner-toaster][data-theme='light'] {
--normal-bg: #fff;
--normal-border: var(--gray4);
--normal-text: var(--gray12);
--success-bg: hsl(143, 85%, 96%);
--success-border: hsl(145, 92%, 91%);
--success-text: hsl(140, 100%, 27%);
--info-bg: hsl(208, 100%, 97%);
--info-border: hsl(221, 91%, 91%);
--info-text: hsl(210, 92%, 45%);
--warning-bg: hsl(49, 100%, 97%);
--warning-border: hsl(49, 91%, 91%);
--warning-text: hsl(31, 92%, 45%);
--error-bg: hsl(359, 100%, 97%);
--error-border: hsl(359, 100%, 94%);
--error-text: hsl(360, 100%, 45%);
}
[data-sonner-toaster][data-theme='light'] [data-sonner-toast][data-invert='true'] {
--normal-bg: #000;
--normal-border: hsl(0, 0%, 20%);
--normal-text: var(--gray1);
}
[data-sonner-toaster][data-theme='dark'] [data-sonner-toast][data-invert='true'] {
--normal-bg: #fff;
--normal-border: var(--gray3);
--normal-text: var(--gray12);
}
[data-sonner-toaster][data-theme='dark'] {
--normal-bg: #000;
--normal-border: hsl(0, 0%, 20%);
--normal-text: var(--gray1);
--success-bg: hsl(150, 100%, 6%);
--success-border: hsl(147, 100%, 12%);
--success-text: hsl(150, 86%, 65%);
--info-bg: hsl(215, 100%, 6%);
--info-border: hsl(223, 100%, 12%);
--info-text: hsl(216, 87%, 65%);
--warning-bg: hsl(64, 100%, 6%);
--warning-border: hsl(60, 100%, 12%);
--warning-text: hsl(46, 87%, 65%);
--error-bg: hsl(358, 76%, 10%);
--error-border: hsl(357, 89%, 16%);
--error-text: hsl(358, 100%, 81%);
}
[data-rich-colors='true'][data-sonner-toast][data-type='success'] {
background: var(--success-bg);
border-color: var(--success-border);
color: var(--success-text);
}
[data-rich-colors='true'][data-sonner-toast][data-type='success'] [data-close-button] {
background: var(--success-bg);
border-color: var(--success-border);
color: var(--success-text);
}
[data-rich-colors='true'][data-sonner-toast][data-type='info'] {
background: var(--info-bg);
border-color: var(--info-border);
color: var(--info-text);
}
[data-rich-colors='true'][data-sonner-toast][data-type='info'] [data-close-button] {
background: var(--info-bg);
border-color: var(--info-border);
color: var(--info-text);
}
[data-rich-colors='true'][data-sonner-toast][data-type='warning'] {
background: var(--warning-bg);
border-color: var(--warning-border);
color: var(--warning-text);
}
[data-rich-colors='true'][data-sonner-toast][data-type='warning'] [data-close-button] {
background: var(--warning-bg);
border-color: var(--warning-border);
color: var(--warning-text);
}
[data-rich-colors='true'][data-sonner-toast][data-type='error'] {
background: var(--error-bg);
border-color: var(--error-border);
color: var(--error-text);
}
[data-rich-colors='true'][data-sonner-toast][data-type='error'] [data-close-button] {
background: var(--error-bg);
border-color: var(--error-border);
color: var(--error-text);
}
.sonner-loading-wrapper {
--size: 16px;
height: var(--size);
width: var(--size);
position: absolute;
inset: 0;
z-index: 10;
}
.sonner-loading-wrapper[data-visible='false'] {
transform-origin: center;
animation: sonner-fade-out 0.2s ease forwards;
}
.sonner-spinner {
position: relative;
top: 50%;
left: 50%;
height: var(--size);
width: var(--size);
}
.sonner-loading-bar {
animation: sonner-spin 1.2s linear infinite;
background: var(--gray11);
border-radius: 6px;
height: 8%;
left: -10%;
position: absolute;
top: -3.9%;
width: 24%;
}
.sonner-loading-bar:nth-child(1) {
animation-delay: -1.2s;
transform: rotate(0.0001deg) translate(146%);
}
.sonner-loading-bar:nth-child(2) {
animation-delay: -1.1s;
transform: rotate(30deg) translate(146%);
}
.sonner-loading-bar:nth-child(3) {
animation-delay: -1s;
transform: rotate(60deg) translate(146%);
}
.sonner-loading-bar:nth-child(4) {
animation-delay: -0.9s;
transform: rotate(90deg) translate(146%);
}
.sonner-loading-bar:nth-child(5) {
animation-delay: -0.8s;
transform: rotate(120deg) translate(146%);
}
.sonner-loading-bar:nth-child(6) {
animation-delay: -0.7s;
transform: rotate(150deg) translate(146%);
}
.sonner-loading-bar:nth-child(7) {
animation-delay: -0.6s;
transform: rotate(180deg) translate(146%);
}
.sonner-loading-bar:nth-child(8) {
animation-delay: -0.5s;
transform: rotate(210deg) translate(146%);
}
.sonner-loading-bar:nth-child(9) {
animation-delay: -0.4s;
transform: rotate(240deg) translate(146%);
}
.sonner-loading-bar:nth-child(10) {
animation-delay: -0.3s;
transform: rotate(270deg) translate(146%);
}
.sonner-loading-bar:nth-child(11) {
animation-delay: -0.2s;
transform: rotate(300deg) translate(146%);
}
.sonner-loading-bar:nth-child(12) {
animation-delay: -0.1s;
transform: rotate(330deg) translate(146%);
}
@keyframes sonner-fade-in {
0% {
opacity: 0;
transform: scale(0.8);
}
100% {
opacity: 1;
transform: scale(1);
}
}
@keyframes sonner-fade-out {
0% {
opacity: 1;
transform: scale(1);
}
100% {
opacity: 0;
transform: scale(0.8);
}
}
@keyframes sonner-spin {
0% {
opacity: 1;
}
100% {
opacity: 0.15;
}
}
@media (prefers-reduced-motion) {
[data-sonner-toast],
[data-sonner-toast] > *,
.sonner-loading-bar {
transition: none !important;
animation: none !important;
}
}
.sonner-loader {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
transform-origin: center;
transition:
opacity 200ms,
transform 200ms;
}
.sonner-loader[data-visible='false'] {
opacity: 0;
transform: scale(0.8) translate(-50%, -50%);
}

View File

@@ -1,91 +1,167 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import 'tailwindcss';
@import 'tw-animate-css';
@plugin '@tailwindcss/typography';
@layer base {
:host,
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
@custom-variant dark (&:is(.dark *));
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
:root,
:host {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.129 0.042 264.695);
--card: oklch(1 0 0);
--card-foreground: oklch(0.129 0.042 264.695);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.129 0.042 264.695);
--primary: oklch(0.208 0.042 265.755);
--primary-foreground: oklch(0.984 0.003 247.858);
--secondary: oklch(0.968 0.007 247.896);
--secondary-foreground: oklch(0.208 0.042 265.755);
--muted: oklch(0.968 0.007 247.896);
--muted-foreground: oklch(0.554 0.046 257.417);
--accent: oklch(0.968 0.007 247.896);
--accent-foreground: oklch(0.208 0.042 265.755);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.929 0.013 255.508);
--input: oklch(0.929 0.013 255.508);
--ring: oklch(0.704 0.04 256.788);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.984 0.003 247.858);
--sidebar-foreground: oklch(0.129 0.042 264.695);
--sidebar-primary: oklch(0.208 0.042 265.755);
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
--sidebar-accent: oklch(0.968 0.007 247.896);
--sidebar-accent-foreground: oklch(0.208 0.042 265.755);
--sidebar-border: oklch(0.929 0.013 255.508);
--sidebar-ring: oklch(0.704 0.04 256.788);
}
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
.dark {
--background: oklch(0.129 0.042 264.695);
--foreground: oklch(0.984 0.003 247.858);
--card: oklch(0.208 0.042 265.755);
--card-foreground: oklch(0.984 0.003 247.858);
--popover: oklch(0.208 0.042 265.755);
--popover-foreground: oklch(0.984 0.003 247.858);
--primary: oklch(0.929 0.013 255.508);
--primary-foreground: oklch(0.208 0.042 265.755);
--secondary: oklch(0.279 0.041 260.031);
--secondary-foreground: oklch(0.984 0.003 247.858);
--muted: oklch(0.279 0.041 260.031);
--muted-foreground: oklch(0.704 0.04 256.788);
--accent: oklch(0.279 0.041 260.031);
--accent-foreground: oklch(0.984 0.003 247.858);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.551 0.027 264.364);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.208 0.042 265.755);
--sidebar-foreground: oklch(0.984 0.003 247.858);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
--sidebar-accent: oklch(0.279 0.041 260.031);
--sidebar-accent-foreground: oklch(0.984 0.003 247.858);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.551 0.027 264.364);
}
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--z-index-infinity: 'calc(infinity)';
--text-2xs: 0.625rem;
--animate-meteor: meteor 5s linear infinite;
--animate-pulse: pulse var(--duration) ease-out infinite;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--ring: 215 20.2% 65.1%;
--radius: 0.5rem;
@keyframes shimmer {
0% {
--shimmer-angle: 0deg;
}
100% {
--shimmer-angle: 360deg;
}
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
@keyframes meteor {
0% {
transform: rotate(var(--angle)) translateX(0);
opacity: 1;
}
70% {
opacity: 1;
}
100% {
transform: rotate(var(--angle)) translateX(-500px);
opacity: 0;
}
}
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 85.7% 97.3%;
--ring: 217.2 32.6% 17.5%;
@keyframes pulse {
0%,
100% {
box-shadow: 0 0 0 0 var(--pulse-color);
}
50% {
box-shadow: 0 0 0 8px var(--pulse-color);
}
}
}
@layer base {
* {
@apply border-border;
@apply border-border outline-ring/50;
}
:host,
:root {
@apply !bg-background !text-foreground !text-base !visible;
/* Disabled inherit */
all: initial !important;
direction: ltr !important;
:root,
:host {
@apply bg-background text-foreground;
}
}
/* @property --shimmer-angle {
@property --shimmer-angle {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
} */
}

View File

@@ -1,5 +1,5 @@
import { cn } from '@/utils'
import { forwardRef, ReactNode } from 'react'
import type { ReactNode, Ref } from 'react'
export interface LinkProps {
href: string
@@ -8,7 +8,13 @@ export interface LinkProps {
underline?: boolean
}
const Link = forwardRef<HTMLAnchorElement, LinkProps>(({ href, className, children, underline = true }, ref) => {
const Link = ({
ref,
href,
className,
children,
underline = true
}: LinkProps & { ref?: Ref<HTMLAnchorElement | null> }) => {
return (
<a
href={href}
@@ -20,7 +26,7 @@ const Link = forwardRef<HTMLAnchorElement, LinkProps>(({ href, className, childr
{children}
</a>
)
})
}
Link.displayName = 'Link'
export default Link

View File

@@ -3,7 +3,7 @@ import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import remarkBreaks from 'remark-breaks'
import { cn } from '@/utils'
import { ScrollArea, ScrollBar } from '@/components/ui/ScrollArea'
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
export interface MarkdownProps {
children?: string
@@ -42,90 +42,91 @@ const urlTransform = (value: string) => {
const Markdown: FC<MarkdownProps> = ({ children = '', className }) => {
return (
<ReactMarkdown
urlTransform={urlTransform}
components={{
h1: ({ className, ...props }) => (
<h1 className={cn('my-2 mt-0 font-semibold text-2xl dark:text-slate-50', className)} {...props} />
),
h2: ({ className, ...props }) => (
<h2 className={cn('mb-2 mt-0 font-semibold dark:text-slate-50', className)} {...props} />
),
h3: ({ className, ...props }) => (
<h3 className={cn('mb-2 mt-0 font-semibold dark:text-slate-50', className)} {...props} />
),
h4: ({ className, ...props }) => (
<h4 className={cn('mb-2 mt-0 font-semibold dark:text-slate-50', className)} {...props} />
),
img: ({ className, alt, ...props }) => (
<img className={cn('my-2 max-w-[100%] rounded', className)} alt={alt} {...props} />
),
strong: ({ className, ...props }) => <strong className={cn('dark:text-slate-50', className)} {...props} />,
a: ({ className, ...props }) => (
<a
className={cn('text-blue-500', className)}
target={props.href || '_blank'}
rel="noopener noreferrer"
{...props}
/>
),
ul: ({ className, ...props }) => {
Reflect.deleteProperty(props, 'ordered')
return <ul className={cn('text-sm [&:not([depth="0"])]:my-0 ', className)} {...props} />
},
input: ({ className, ...props }) => <input className={cn('my-0', className)} {...props} />,
table: ({ className, ...props }) => (
<div className="my-2 w-full">
<ScrollArea scrollLock={false}>
<table className={cn('my-0 w-full rounded-md', className)} {...props} />
<div className={cn(className, 'prose prose-sm prose-slate break-words dark:text-slate-50')}>
<ReactMarkdown
urlTransform={urlTransform}
components={{
h1: ({ className, ...props }) => (
<h1 className={cn('my-2 mt-0 font-semibold text-2xl dark:text-slate-50', className)} {...props} />
),
h2: ({ className, ...props }) => (
<h2 className={cn('mb-2 mt-0 font-semibold dark:text-slate-50', className)} {...props} />
),
h3: ({ className, ...props }) => (
<h3 className={cn('mb-2 mt-0 font-semibold dark:text-slate-50', className)} {...props} />
),
h4: ({ className, ...props }) => (
<h4 className={cn('mb-2 mt-0 font-semibold dark:text-slate-50', className)} {...props} />
),
img: ({ className, alt, ...props }) => (
<img className={cn('my-2 max-w-[100%] rounded', className)} alt={alt} {...props} />
),
strong: ({ className, ...props }) => <strong className={cn('dark:text-slate-50', className)} {...props} />,
a: ({ className, ...props }) => (
<a
className={cn('text-blue-500', className)}
target={props.href || '_blank'}
rel="noopener noreferrer"
{...props}
/>
),
ul: ({ className, ...props }) => {
Reflect.deleteProperty(props, 'ordered')
return <ul className={cn('text-sm [&:not([depth="0"])]:my-0 ', className)} {...props} />
},
input: ({ className, ...props }) => <input className={cn('my-0', className)} {...props} />,
table: ({ className, ...props }) => (
<div className="my-2 w-full">
<ScrollArea scrollLock={false}>
<table className={cn('my-0 w-full rounded-md', className)} {...props} />
<ScrollBar orientation="horizontal" />
</ScrollArea>
</div>
),
tr: ({ className, ...props }) => {
return <tr className={cn('m-0 border-t p-0 even:bg-muted', className)} {...props} />
},
th: ({ className, ...props }) => {
return (
<th
className={cn(
'border px-3 py-2 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right',
className
)}
{...props}
/>
)
},
td: ({ className, ...props }) => {
return (
<td
className={cn(
'border px-3 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right',
className
)}
{...props}
/>
)
},
pre: ({ className, ...props }) => <pre className={cn('my-2', className)} {...props} />,
/**
* TODO: Code highlight
* @see https://github.com/remarkjs/react-markdown/issues/680
* @see https://shiki.style/guide/install#usage
*
*/
code: ({ className, ...props }) => (
<ScrollArea className="overscroll-y-auto" scrollLock={false}>
<code className={cn('text-sm', className)} {...props}></code>
<ScrollBar orientation="horizontal" />
</ScrollArea>
</div>
),
tr: ({ className, ...props }) => {
return <tr className={cn('m-0 border-t p-0 even:bg-muted', className)} {...props} />
},
th: ({ className, ...props }) => {
return (
<th
className={cn(
'border px-3 py-2 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right',
className
)}
{...props}
/>
)
},
td: ({ className, ...props }) => {
return (
<td
className={cn(
'border px-3 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right',
className
)}
{...props}
/>
)
},
pre: ({ className, ...props }) => <pre className={cn('my-2', className)} {...props} />,
/**
* TODO: Code highlight
* @see https://github.com/remarkjs/react-markdown/issues/680
* @see https://shiki.style/guide/install#usage
*
*/
code: ({ className, ...props }) => (
<ScrollArea className="overscroll-y-auto" scrollLock={false}>
<code className={cn('text-sm', className)} {...props}></code>
<ScrollBar orientation="horizontal" />
</ScrollArea>
)
}}
remarkPlugins={[remarkGfm, remarkBreaks]}
className={cn(className, 'prose prose-sm prose-slate break-words dark:text-slate-50')}
>
{children}
</ReactMarkdown>
}}
remarkPlugins={[remarkGfm, remarkBreaks]}
>
{children}
</ReactMarkdown>
</div>
)
}

View File

@@ -1,61 +0,0 @@
'use client'
import { useRef } from 'react'
import { AnimatePresence, motion, useInView, UseInViewOptions, Variants } from 'framer-motion'
type MarginType = UseInViewOptions['margin']
interface BlurFadeProps {
children: React.ReactNode
className?: string
variant?: {
hidden: { y: number }
visible: { y: number }
}
duration?: number
delay?: number
yOffset?: number
inView?: boolean
inViewMargin?: MarginType
blur?: string
}
export default function BlurFade({
children,
className,
variant,
duration = 0.4,
delay = 0,
yOffset = 6,
inView = false,
inViewMargin = '-50px',
blur = '6px'
}: BlurFadeProps) {
const ref = useRef(null)
const inViewResult = useInView(ref, { once: true, margin: inViewMargin })
const isInView = !inView || inViewResult
const defaultVariants: Variants = {
hidden: { y: yOffset, opacity: 0, filter: `blur(${blur})` },
visible: { y: -yOffset, opacity: 1, filter: `blur(0px)` }
}
const combinedVariants = variant || defaultVariants
return (
<AnimatePresence>
<motion.div
ref={ref}
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
exit="hidden"
variants={combinedVariants}
transition={{
delay: 0.04 + delay,
duration,
ease: 'easeOut'
}}
className={className}
>
{children}
</motion.div>
</AnimatePresence>
)
}

View File

@@ -1,24 +1,43 @@
'use client'
"use client";
import { useEffect, useState } from 'react'
import { cn } from '@/utils/index'
import { cn } from "@/utils";
import React, { useEffect, useState } from "react";
interface MeteorsProps {
number?: number
number?: number;
minDelay?: number;
maxDelay?: number;
minDuration?: number;
maxDuration?: number;
angle?: number;
className?: string;
}
export const Meteors = ({ number = 20 }: MeteorsProps) => {
const [meteorStyles, setMeteorStyles] = useState<Array<React.CSSProperties>>([])
export const Meteors = ({
number = 20,
minDelay = 0.2,
maxDelay = 1.2,
minDuration = 2,
maxDuration = 10,
angle = 215,
className,
}: MeteorsProps) => {
const [meteorStyles, setMeteorStyles] = useState<Array<React.CSSProperties>>(
[],
);
useEffect(() => {
const styles = [...new Array(number)].map(() => ({
top: -5,
left: Math.floor(Math.random() * window.innerWidth) + 'px',
animationDelay: Math.random() * 1 + 0.2 + 's',
animationDuration: Math.floor(Math.random() * 8 + 2) + 's'
}))
setMeteorStyles(styles)
}, [number])
"--angle": -angle + "deg",
top: "-5%",
left: `calc(0% + ${Math.floor(Math.random() * window.innerWidth)}px)`,
animationDelay: Math.random() * (maxDelay - minDelay) + minDelay + "s",
animationDuration:
Math.floor(Math.random() * (maxDuration - minDuration) + minDuration) +
"s",
}));
setMeteorStyles(styles);
}, [number, minDelay, maxDelay, minDuration, maxDuration, angle]);
return (
<>
@@ -26,17 +45,16 @@ export const Meteors = ({ number = 20 }: MeteorsProps) => {
// Meteor Head
<span
key={idx}
style={{ ...style }}
className={cn(
'pointer-events-none absolute left-1/2 top-1/2 size-0.5 rotate-[215deg] animate-meteor rounded-full bg-slate-500 shadow-[0_0_0_1px_#ffffff10]'
"pointer-events-none absolute size-0.5 rotate-[var(--angle)] animate-meteor rounded-full bg-zinc-500 shadow-[0_0_0_1px_#ffffff10]",
className,
)}
style={style}
>
{/* Meteor Tail */}
<div className="pointer-events-none absolute top-1/2 -z-10 h-px w-[50px] -translate-y-1/2 bg-gradient-to-r from-slate-500 to-transparent" />
<div className="pointer-events-none absolute top-1/2 -z-10 h-px w-[50px] -translate-y-1/2 bg-gradient-to-r from-zinc-500 to-transparent" />
</span>
))}
</>
)
}
export default Meteors
);
};

View File

@@ -1,37 +0,0 @@
'use client'
import React from 'react'
import { cn } from '@/utils/index'
interface PulsatingButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
pulseColor?: string
duration?: string
}
export default function PulsatingButton({
className,
children,
pulseColor = '#0f172a50',
duration = '1.5s',
...props
}: PulsatingButtonProps) {
return (
<button
className={cn(
'relative rounded-full text-center cursor-pointer text-sm font-medium flex justify-center items-center text-primary-foreground bg-primary py-2 h-10 px-8 hover:bg-primary/90',
className
)}
style={
{
'--pulse-color': pulseColor,
'--duration': duration
} as React.CSSProperties
}
{...props}
>
<div className="relative z-10">{children}</div>
<div className="absolute left-1/2 top-1/2 size-full -translate-x-1/2 -translate-y-1/2 animate-pulse rounded-full bg-inherit" />
</button>
)
}

View File

@@ -1,53 +0,0 @@
"use client";
import { motion, Variants } from "framer-motion";
import { cn } from "@/utils/index";
interface WordPullUpProps {
words: string;
delayMultiple?: number;
wrapperFramerProps?: Variants;
framerProps?: Variants;
className?: string;
}
export default function WordPullUp({
words,
wrapperFramerProps = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
staggerChildren: 0.2,
},
},
},
framerProps = {
hidden: { y: 20, opacity: 0 },
show: { y: 0, opacity: 1 },
},
className,
}: WordPullUpProps) {
return (
<motion.h1
variants={wrapperFramerProps}
initial="hidden"
animate="show"
className={cn(
"font-display text-center text-4xl font-bold leading-[5rem] tracking-[-0.02em] drop-shadow-sm",
className,
)}
>
{words.split(" ").map((word, i) => (
<motion.span
key={i}
variants={framerProps}
style={{ display: "inline-block", paddingRight: "8px" }}
>
{word === "" ? <span>&nbsp;</span> : word}
</motion.span>
))}
</motion.h1>
);
}

View File

@@ -1,14 +1,13 @@
import { cva, type VariantProps } from 'class-variance-authority'
'use client'
import React from 'react'
import { cn } from '@/utils/index'
import { cn } from '@/utils'
import { cva, VariantProps } from 'class-variance-authority'
interface AvatarCirclesProps {
className?: string
avatarUrls: string[]
maxLength?: number
size?: VariantProps<typeof SizeVariants>['size']
max?: number
}
const SizeVariants = cva('z-10 flex -space-x-4 rtl:space-x-reverse', {
@@ -39,10 +38,10 @@ const spaceVariants = cva('flex -space-x-4 rtl:space-x-reverse', {
}
})
const AvatarCircles = ({ className, avatarUrls, size, max = 10 }: AvatarCirclesProps) => {
export const AvatarCircles = ({ className, avatarUrls, size, maxLength = 10 }: AvatarCirclesProps) => {
return (
<div className={cn(spaceVariants({ size }), className)}>
{avatarUrls.slice(0, max).map((url, index) => (
{avatarUrls.slice(0, maxLength).map((url, index) => (
<img
key={index}
className={cn(
@@ -53,17 +52,17 @@ const AvatarCircles = ({ className, avatarUrls, size, max = 10 }: AvatarCirclesP
alt={`Avatar ${index + 1}`}
/>
))}
<div
className={cn(
'flex items-center justify-center rounded-full border-2 border-white bg-slate-600 text-center text-xs font-medium text-white dark:border-slate-800 p-1',
SizeVariants({ size }),
size === 'xs' && 'text-2xs'
)}
>
+{avatarUrls.length}
</div>
{(avatarUrls.length ?? 0) > 0 && (
<div
className={cn(
'flex items-center justify-center rounded-full border-2 border-white bg-slate-600 text-center text-xs font-medium text-white dark:border-slate-800 p-1',
SizeVariants({ size }),
size === 'xs' && 'text-2xs'
)}
>
+{avatarUrls.length}
</div>
)}
</div>
)
}
export default AvatarCircles

View File

@@ -0,0 +1,81 @@
"use client";
import {
AnimatePresence,
motion,
useInView,
UseInViewOptions,
Variants,
MotionProps,
} from "motion/react";
import { useRef } from "react";
type MarginType = UseInViewOptions["margin"];
interface BlurFadeProps extends MotionProps {
children: React.ReactNode;
className?: string;
variant?: {
hidden: { y: number };
visible: { y: number };
};
duration?: number;
delay?: number;
offset?: number;
direction?: "up" | "down" | "left" | "right";
inView?: boolean;
inViewMargin?: MarginType;
blur?: string;
}
export function BlurFade({
children,
className,
variant,
duration = 0.4,
delay = 0,
offset = 6,
direction = "down",
inView = false,
inViewMargin = "-50px",
blur = "6px",
...props
}: BlurFadeProps) {
const ref = useRef(null);
const inViewResult = useInView(ref, { once: true, margin: inViewMargin });
const isInView = !inView || inViewResult;
const defaultVariants: Variants = {
hidden: {
[direction === "left" || direction === "right" ? "x" : "y"]:
direction === "right" || direction === "down" ? -offset : offset,
opacity: 0,
filter: `blur(${blur})`,
},
visible: {
[direction === "left" || direction === "right" ? "x" : "y"]: 0,
opacity: 1,
filter: `blur(0px)`,
},
};
const combinedVariants = variant || defaultVariants;
return (
<AnimatePresence>
<motion.div
ref={ref}
initial="hidden"
animate={isInView ? "visible" : "hidden"}
exit="hidden"
variants={combinedVariants}
transition={{
delay: 0.04 + delay,
duration,
ease: "easeOut",
}}
className={className}
{...props}
>
{children}
</motion.div>
</AnimatePresence>
);
}

View File

@@ -0,0 +1,46 @@
import React from "react";
import { cn } from "@/utils";
interface PulsatingButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
pulseColor?: string;
duration?: string;
}
export const PulsatingButton = React.forwardRef<
HTMLButtonElement,
PulsatingButtonProps
>(
(
{
className,
children,
pulseColor = "#808080",
duration = "1.5s",
...props
},
ref,
) => {
return (
<button
ref={ref}
className={cn(
"relative flex cursor-pointer items-center justify-center rounded-lg bg-primary px-4 py-2 text-center text-primary-foreground",
className,
)}
style={
{
"--pulse-color": pulseColor,
"--duration": duration,
} as React.CSSProperties
}
{...props}
>
<div className="relative z-10">{children}</div>
<div className="absolute left-1/2 top-1/2 size-full -translate-x-1/2 -translate-y-1/2 animate-pulse rounded-lg bg-inherit" />
</button>
);
},
);
PulsatingButton.displayName = "PulsatingButton";

View File

@@ -0,0 +1,50 @@
"use client";
import { AnimatePresence, motion, MotionProps } from "motion/react";
import { useEffect, useState } from "react";
import { cn } from "@/utils";
interface WordRotateProps {
words: string[];
duration?: number;
motionProps?: MotionProps;
className?: string;
}
export function WordRotate({
words,
duration = 2500,
motionProps = {
initial: { opacity: 0, y: -50 },
animate: { opacity: 1, y: 0 },
exit: { opacity: 0, y: 50 },
transition: { duration: 0.25, ease: "easeOut" },
},
className,
}: WordRotateProps) {
const [index, setIndex] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setIndex((prevIndex) => (prevIndex + 1) % words.length);
}, duration);
// Clean up interval on unmount
return () => clearInterval(interval);
}, [words, duration]);
return (
<div className="overflow-hidden py-2">
<AnimatePresence mode="wait">
<motion.h1
key={words[index]}
className={cn(className)}
{...motionProps}
>
{words[index]}
</motion.h1>
</AnimatePresence>
</div>
);
}

View File

@@ -1,38 +1,51 @@
import * as React from 'react'
import * as AvatarPrimitive from '@radix-ui/react-avatar'
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from '@/utils/index'
import { cn } from "@/utils"
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn('relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full', className)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
function Avatar({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
className={cn(
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
)
}
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image ref={ref} className={cn('aspect-square h-full w-full', className)} {...props} />
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
function AvatarImage({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn("aspect-square size-full", className)}
{...props}
/>
)
}
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn('flex h-full w-full items-center justify-center rounded-full bg-muted dark:text-slate-400', className)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
function AvatarFallback({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"bg-muted flex size-full items-center justify-center rounded-full",
className
)}
{...props}
/>
)
}
export { Avatar, AvatarImage, AvatarFallback }

View File

@@ -1,29 +1,46 @@
import type * as React from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from '@/utils/index'
import { cn } from "@/utils"
const badgeVariants = cva(
'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default: 'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80',
secondary: 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive: 'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80',
outline: 'text-foreground'
}
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: 'default'
}
variant: "default",
},
}
)
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span"
function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }

View File

@@ -2,26 +2,27 @@ import * as React from 'react'
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/utils/index'
import { cn } from '@/utils'
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
"inline-flex items-center cursor-pointer justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
destructive:
'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
outline:
'border border-input text-primary bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: 'text-primary underline-offset-4 hover:underline'
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 rounded-md px-3 text-xs',
xs: 'h-6 rounded-md px-2 text-xs',
lg: 'h-10 rounded-md px-8',
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
xs: 'h-6 rounded-md px-2 text-xs has-[>svg]:px-1',
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
icon: 'size-9'
}
},
@@ -32,18 +33,19 @@ const buttonVariants = cva(
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<'button'> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : 'button'
return <Comp data-slot="button" className={cn(buttonVariants({ variant, size, className }))} {...props} />
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button'
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
}
)
Button.displayName = 'Button'
export { Button, buttonVariants }

View File

@@ -1,26 +1,30 @@
import * as React from 'react'
import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
import { CheckIcon } from '@radix-ui/react-icons'
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { CheckIcon } from "lucide-react"
import { cn } from '@/utils/index'
import { cn } from "@/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
'peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
className
)}
{...props}
>
<CheckboxPrimitive.Indicator className={cn('flex items-center justify-center text-current')}>
<CheckIcon className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
function Checkbox({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="flex items-center justify-center text-current transition-none"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}
export { Checkbox }

View File

@@ -1,32 +1,35 @@
import * as React from 'react'
import type * as LabelPrimitive from '@radix-ui/react-label'
import { Slot } from '@radix-ui/react-slot'
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
Controller,
FormProvider,
useFormContext,
useFormState,
type ControllerProps,
type FieldPath,
type FieldValues,
FormProvider,
useFormContext
} from 'react-hook-form'
} from "react-hook-form"
import { cn } from '@/utils/index'
import { Label } from '@/components/ui/Label'
import { cn } from "@/utils"
import { Label } from "@/components/ui/label"
const Form = FormProvider
interface FormFieldContextValue<
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> {
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue)
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
@@ -40,12 +43,12 @@ const FormField = <
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState, formState } = useFormContext()
const { getFieldState } = useFormContext()
const formState = useFormState({ name: fieldContext.name })
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error('useFormField should be used within <FormField>')
throw new Error("useFormField should be used within <FormField>")
}
const { id } = itemContext
@@ -56,88 +59,107 @@ const useFormField = () => {
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState
...fieldState,
}
}
interface FormItemContextValue {
type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue)
const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn('space-y-2', className)} {...props} />
</FormItemContext.Provider>
)
}
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
FormItem.displayName = 'FormItem'
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField()
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
const id = React.useId()
return <Label ref={ref} className={cn(error && 'text-destructive', className)} htmlFor={formItemId} {...props} />
})
FormLabel.displayName = 'FormLabel'
const FormControl = React.forwardRef<React.ElementRef<typeof Slot>, React.ComponentPropsWithoutRef<typeof Slot>>(
({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
aria-invalid={!!error}
return (
<FormItemContext.Provider value={{ id }}>
<div
data-slot="form-item"
className={cn("grid gap-2", className)}
{...props}
/>
)
</FormItemContext.Provider>
)
}
function FormLabel({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
const { error, formItemId } = useFormField()
return (
<Label
data-slot="form-label"
data-error={!!error}
className={cn("data-[error=true]:text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
)
}
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
data-slot="form-control"
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
}
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
const { formDescriptionId } = useFormField()
return (
<p
data-slot="form-description"
id={formDescriptionId}
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message ?? "") : props.children
if (!body) {
return null
}
)
FormControl.displayName = 'FormControl'
const FormDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()
return (
<p
data-slot="form-message"
id={formMessageId}
className={cn("text-destructive text-sm", className)}
{...props}
>
{body}
</p>
)
}
return (
<p ref={ref} id={formDescriptionId} className={cn('text-[0.8rem] text-muted-foreground', className)} {...props} />
)
}
)
FormDescription.displayName = 'FormDescription'
const FormMessage = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
if (!body) {
return null
}
return (
<p
ref={ref}
id={formMessageId}
className={cn('text-[0.8rem] font-medium text-destructive', className)}
{...props}
>
{body}
</p>
)
}
)
FormMessage.displayName = 'FormMessage'
export { useFormField, Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField }
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}

View File

@@ -1,27 +0,0 @@
import * as React from 'react'
import * as HoverCardPrimitive from '@radix-ui/react-hover-card'
import { cn } from '@/utils/index'
const HoverCard = HoverCardPrimitive.Root
const HoverCardTrigger = HoverCardPrimitive.Trigger
const HoverCardContent = React.forwardRef<
React.ElementRef<typeof HoverCardPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
<HoverCardPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
'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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none',
className
)}
{...props}
/>
))
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
export { HoverCard, HoverCardTrigger, HoverCardContent }

View File

@@ -1,22 +1,21 @@
import * as React from 'react'
import * as React from "react"
import { cn } from '@/utils/index'
import { cn } from "@/utils"
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => {
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
ref={ref}
{...props}
/>
)
})
Input.displayName = 'Input'
}
export { Input }

View File

@@ -1,17 +1,24 @@
import * as React from 'react'
import * as LabelPrimitive from '@radix-ui/react-label'
import { cva, type VariantProps } from 'class-variance-authority'
"use client"
import { cn } from '@/utils/index'
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
const labelVariants = cva('text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70')
import { cn } from "@/utils"
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
))
Label.displayName = LabelPrimitive.Root.displayName
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
}
export { Label }

View File

@@ -1,31 +1,41 @@
import * as React from 'react'
import * as PopoverPrimitive from '@radix-ui/react-popover'
import { cn, getRootNode } from '@/utils'
const Popover = PopoverPrimitive.Root
function Popover({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />
}
const PopoverTrigger = PopoverPrimitive.Trigger
function PopoverTrigger({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
}
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => {
function PopoverContent({
className,
align = 'center',
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
const root = getRootNode()
return (
<PopoverPrimitive.Portal container={root}>
<PopoverPrimitive.Content
ref={ref}
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
className={cn(
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
'bg-popover text-popover-foreground 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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden',
className
)}
{...props}
></PopoverPrimitive.Content>
/>
</PopoverPrimitive.Portal>
)
})
PopoverContent.displayName = PopoverPrimitive.Content.displayName
}
export { Popover, PopoverTrigger, PopoverContent }
function PopoverAnchor({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
}
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }

View File

@@ -1,36 +0,0 @@
import * as React from 'react'
import { CheckIcon } from '@radix-ui/react-icons'
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group'
import { cn } from '@/utils/index'
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return <RadioGroupPrimitive.Root className={cn('grid gap-2', className)} {...props} ref={ref} />
})
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
'aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<CheckIcon className="size-3.5 fill-primary" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
})
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
export { RadioGroup, RadioGroupItem }

View File

@@ -1,43 +0,0 @@
import * as React from 'react'
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'
import { cn } from '@/utils/index'
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', className)} {...props}>
<ScrollAreaPrimitive.Viewport
ref={ref}
className={cn('size-full rounded-[inherit]', scrollLock ? 'overscroll-none' : 'overscroll-auto')}
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = 'vertical', ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
'flex touch-none select-none transition-colors',
orientation === 'vertical' && 'h-full w-2.5 border-l border-l-transparent p-[1px]',
orientation === 'horizontal' && 'h-2.5 border-t border-t-transparent p-[1px]',
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }

View File

@@ -1,27 +1,29 @@
import * as React from 'react'
import * as SwitchPrimitives from '@radix-ui/react-switch'
import * as React from "react"
import * as SwitchPrimitive from "@radix-ui/react-switch"
import { cn } from '@/utils/index'
import { cn } from "@/utils"
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
'peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
function Switch({
className,
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
return (
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
'pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0'
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitive.Root>
)
}
export { Switch }

View File

@@ -1,21 +1,18 @@
import * as React from 'react'
import { cn } from '@/utils/index'
import { cn } from '@/utils'
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(({ className, ...props }, ref) => {
function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
return (
<textarea
data-slot="textarea"
className={cn(
'flex min-h-[60px] w-full rounded-md border border-input text-primary bg-transparent p-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
'border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
className
)}
ref={ref}
{...props}
/>
)
})
Textarea.displayName = 'Textarea'
}
export { Textarea }

View File

@@ -0,0 +1,37 @@
import * as React from 'react'
import * as HoverCardPrimitive from '@radix-ui/react-hover-card'
import { cn, getRootNode } from '@/utils'
function HoverCard({ ...props }: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />
}
function HoverCardTrigger({ ...props }: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
return <HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
}
function HoverCardContent({
className,
align = 'center',
sideOffset = 4,
...props
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
const root = getRootNode()
return (
<HoverCardPrimitive.Portal data-slot="hover-card-portal" container={root}>
<HoverCardPrimitive.Content
data-slot="hover-card-content"
align={align}
sideOffset={sideOffset}
className={cn(
'bg-popover text-popover-foreground 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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden',
className
)}
{...props}
/>
</HoverCardPrimitive.Portal>
)
}
export { HoverCard, HoverCardTrigger, HoverCardContent }

View File

@@ -0,0 +1,43 @@
import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { CircleIcon } from "lucide-react"
import { cn } from "@/utils"
function RadioGroup({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
return (
<RadioGroupPrimitive.Root
data-slot="radio-group"
className={cn("grid gap-3", className)}
{...props}
/>
)
}
function RadioGroupItem({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
return (
<RadioGroupPrimitive.Item
data-slot="radio-group-item"
className={cn(
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator
data-slot="radio-group-indicator"
className="relative flex items-center justify-center"
>
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
}
export { RadioGroup, RadioGroupItem }

View File

@@ -0,0 +1,60 @@
import * as React from 'react'
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'
import { cn } from '@/utils'
function ScrollArea({
className,
children,
scrollLock,
ref,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root> & { scrollLock?: boolean }) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn('relative grid grid-rows-[1fr] overflow-hidden', className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
ref={ref}
data-slot="scroll-area-viewport"
className={cn(
'focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1',
scrollLock ? 'overscroll-none' : 'overscroll-auto'
)}
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
)
}
function ScrollBar({
className,
orientation = 'vertical',
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
'flex touch-none p-px transition-colors select-none',
orientation === 'vertical' && 'h-full w-2.5 border-l border-l-transparent',
orientation === 'horizontal' && 'h-2.5 flex-col border-t border-t-transparent',
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
)
}
export { ScrollArea, ScrollBar }

View File

@@ -1,6 +1,7 @@
import { Remesh } from 'remesh'
import { map, merge, of, EMPTY, mergeMap, fromEventPattern } from 'rxjs'
import { AtUser, NormalMessage, type MessageUser } from './MessageList'
import type { AtUser, NormalMessage } from './MessageList'
import { type MessageUser } from './MessageList'
import { ChatRoomExtern } from '@/domain/externs/ChatRoom'
import MessageListDomain, { MessageType } from '@/domain/MessageList'
import UserInfoDomain from '@/domain/UserInfo'

View File

@@ -1,6 +1,7 @@
import { Remesh } from 'remesh'
import { DanmakuExtern } from './externs/Danmaku'
import ChatRoomDomain, { TextMessage } from '@/domain/ChatRoom'
import type { TextMessage } from '@/domain/ChatRoom'
import ChatRoomDomain from '@/domain/ChatRoom'
import UserInfoDomain from './UserInfo'
import { map, merge } from 'rxjs'

View File

@@ -1,6 +1,7 @@
import { Remesh } from 'remesh'
import { NotificationExtern } from './externs/Notification'
import ChatRoomDomain, { TextMessage } from '@/domain/ChatRoom'
import type { TextMessage } from '@/domain/ChatRoom'
import ChatRoomDomain from '@/domain/ChatRoom'
import UserInfoDomain from './UserInfo'
import { map, merge } from 'rxjs'

View File

@@ -7,7 +7,8 @@ import { upsert } from '@/utils'
import { nanoid } from 'nanoid'
import StatusModule from '@/domain/modules/Status'
import * as v from 'valibot'
import getSiteInfo, { SiteInfo } from '@/utils/getSiteInfo'
import type { SiteInfo } from '@/utils/getSiteInfo'
import getSiteInfo from '@/utils/getSiteInfo'
export enum SendType {
SyncUser = 'SyncUser'

View File

@@ -1,5 +1,5 @@
import { Remesh } from 'remesh'
import { RoomMessage } from '../ChatRoom'
import type { RoomMessage } from '../ChatRoom'
export interface ChatRoom {
readonly peerId: string

View File

@@ -1,5 +1,5 @@
import { Remesh } from 'remesh'
import { TextMessage } from '@/domain/ChatRoom'
import type { TextMessage } from '@/domain/ChatRoom'
export interface Danmaku {
push: (message: TextMessage) => void

View File

@@ -1,5 +1,5 @@
import { Remesh } from 'remesh'
import { TextMessage } from '@/domain/ChatRoom'
import type { TextMessage } from '@/domain/ChatRoom'
export interface Notification {
push: (message: TextMessage) => Promise<string>

View File

@@ -1,5 +1,5 @@
import { Remesh } from 'remesh'
import { RoomMessage } from '@/domain/VirtualRoom'
import type { RoomMessage } from '@/domain/VirtualRoom'
export interface VirtualRoom {
readonly peerId: string

View File

@@ -1,9 +1,9 @@
import { Room } from '@rtco/client'
import type { Room } from '@rtco/client'
import { ChatRoomExtern } from '@/domain/externs/ChatRoom'
import { stringToHex } from '@/utils'
import EventHub from '@resreq/event-hub'
import { RoomMessage } from '@/domain/ChatRoom'
import type { RoomMessage } from '@/domain/ChatRoom'
import { JSONR } from '@/utils'
import Peer from './Peer'

View File

@@ -1,12 +1,13 @@
import { DanmakuExtern } from '@/domain/externs/Danmaku'
import { TextMessage } from '@/domain/ChatRoom'
import type { TextMessage } from '@/domain/ChatRoom'
import { createElement } from 'react'
import DanmakuMessage from '@/app/content/components/DanmakuMessage'
import DanmakuMessage from '@/app/content/components/danmaku-message'
import { createRoot } from 'react-dom/client'
import { create, Manager } from 'danmu'
import type { Manager } from 'danmu'
import { create } from 'danmu'
import { LocalStorageImpl } from './Storage'
import { AppStatus } from '../AppStatus'
import type { AppStatus } from '../AppStatus'
import { APP_STATUS_STORAGE_KEY } from '@/constants/config'
import { EVENT } from '@/constants/event'

View File

@@ -1,5 +1,5 @@
import { NotificationExtern } from '@/domain/externs/Notification'
import { TextMessage } from '@/domain/ChatRoom'
import type { TextMessage } from '@/domain/ChatRoom'
import { EVENT } from '@/constants/event'
import { messenger } from '@/messenger'

View File

@@ -3,9 +3,9 @@ import indexedDbDriver from 'unstorage/drivers/indexedb'
import localStorageDriver from 'unstorage/drivers/localstorage'
import { LocalStorageExtern, IndexDBStorageExtern, BrowserSyncStorageExtern } from '@/domain/externs/Storage'
import { STORAGE_NAME } from '@/constants/config'
import { webExtensionDriver } from '@/utils/webExtensionDriver'
import webExtensionDriver from '@/utils/webExtensionDriver'
import { Storage } from '@/domain/externs/Storage'
import type { Storage } from '@/domain/externs/Storage'
import { EVENT } from '@/constants/event'
/**

View File

@@ -1,9 +1,9 @@
import { Room } from '@rtco/client'
import type { Room } from '@rtco/client'
import { VirtualRoomExtern } from '@/domain/externs/VirtualRoom'
import { stringToHex } from '@/utils'
import EventHub from '@resreq/event-hub'
import { RoomMessage } from '@/domain/VirtualRoom'
import type { RoomMessage } from '@/domain/VirtualRoom'
import { JSONR } from '@/utils'
import { VIRTUAL_ROOM_ID } from '@/constants/config'
import Peer from './Peer'

View File

@@ -1,4 +1,4 @@
import { DomainConceptName, RemeshDomainContext } from 'remesh'
import type { DomainConceptName, RemeshDomainContext } from 'remesh'
export enum Status {
Initial = 0b001, // 1

View File

@@ -1,7 +1,7 @@
import { type RemeshEvent, type RemeshAction, type RemeshDomainContext, type RemeshExtern } from 'remesh'
import { from, map, Observable, switchMap } from 'rxjs'
import { Storage, StorageValue } from '@/domain/externs/Storage'
import type { Storage, StorageValue } from '@/domain/externs/Storage'
export interface Options {
domain: RemeshDomainContext
@@ -50,7 +50,7 @@ export default class StorageEffect {
name: 'WatchStorageToStateEffect',
impl: () => {
// TODO: Report the bug to https://github.com/unjs/unstorage
return new Observable((observer) => {
return new Observable<void>((observer) => {
const unwatchPromise = this.storage.watch(() => observer.next())
return () => unwatchPromise.then((unwatch) => unwatch())
}).pipe(

View File

@@ -3,6 +3,7 @@ import { BREAKPOINTS } from '@/constants/config'
const _useBreakpoint = createBreakpoint(BREAKPOINTS)
// eslint-disable-next-line @eslint-react/hooks-extra/no-unnecessary-use-prefix
const useBreakpoint = () => {
const breakpoint = _useBreakpoint() as keyof typeof BREAKPOINTS

View File

@@ -1,5 +1,7 @@
import { RefCallback, useCallback, useRef, useState } from 'react'
import getCursorPosition, { Position } from '@/utils/getCursorPosition'
import type { RefCallback } from 'react'
import { useCallback, useRef, useState } from 'react'
import type { Position } from '@/utils/getCursorPosition'
import getCursorPosition from '@/utils/getCursorPosition'
const useCursorPosition = () => {
const [position, setPosition] = useState<Position>({ x: 0, y: 0, selectionStart: 0, selectionEnd: 0 })

View File

@@ -1,4 +1,5 @@
import { RefCallback, useCallback, useLayoutEffect, useRef, useState } from 'react'
import type { RefCallback } from 'react'
import { useCallback, useLayoutEffect, useRef, useState } from 'react'
import { clamp, isInRange } from '@/utils'
export interface ResizableOptions {

View File

@@ -1,4 +1,5 @@
import { ForwardedRef, MutableRefObject, RefCallback, useCallback } from 'react'
import type { ForwardedRef, MutableRefObject, RefCallback } from 'react'
import { useCallback } from 'react'
const useShareRef = <T extends HTMLElement | null>(
...refs: (MutableRefObject<T> | ForwardedRef<T> | RefCallback<T>)[]

View File

@@ -1,4 +1,5 @@
import { RefCallback, useCallback, useRef } from 'react'
import type { RefCallback } from 'react'
import { useCallback, useRef } from 'react'
export type Events = Array<keyof GlobalEventHandlersEventMap>

View File

@@ -1,6 +1,6 @@
import { EVENT } from '@/constants/event'
import type { EVENT } from '@/constants/event'
import { defineExtensionMessaging } from '@webext-core/messaging'
import { TextMessage } from '@/domain/ChatRoom'
import type { TextMessage } from '@/domain/ChatRoom'
interface ProtocolMap {
[EVENT.OPTIONS_PAGE_OPEN]: () => void

2
src/types/shim.d.ts vendored
View File

@@ -1,5 +1,5 @@
declare module '*.svg' {
import * as React from 'react'
import type * as React from 'react'
const ReactComponent: React.FunctionComponent<React.ComponentProps<'svg'> & { title?: string }>

View File

@@ -1,5 +1,6 @@
import generateUglyAvatar from '@/lib/uglyAvatar'
import compressImage, { ImageType } from './compressImage'
import type { ImageType } from './compressImage'
import compressImage from './compressImage'
const generateRandomAvatar = async (targetSize: number, outputType: ImageType = 'image/webp') => {
const svgBlob = generateUglyAvatar()

View File

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

View File

@@ -1,4 +1,5 @@
import { type PublicPath, browser } from 'wxt/browser'
import { browser } from '#imports'
import type { PublicPath } from 'wxt/browser'
import createElement from './createElement'
const injectScript = async (path: PublicPath) => {

View File

@@ -1,16 +1,17 @@
import { type Driver, type WatchCallback, defineDriver } from 'unstorage'
import { browser, type Storage as BrowserStorage } from 'wxt/browser'
import type { Browser } from '#imports'
import { browser } from '#imports'
export interface WebExtensionDriverOptions {
storageArea: 'sync' | 'local' | 'managed' | 'session'
}
export const webExtensionDriver: (opts: WebExtensionDriverOptions) => Driver = defineDriver((opts) => {
const webExtensionDriver: (opts: WebExtensionDriverOptions) => Driver = defineDriver((opts) => {
const checkPermission = () => {
if (browser.storage == null) throw Error("You must request the 'storage' permission to use webExtensionDriver")
}
const _storageListener: (changes: BrowserStorage.StorageAreaSyncOnChangedChangesType) => void = (changes) => {
const _storageListener: (changes: Browser.storage.StorageChange) => void = (changes) => {
Object.entries(changes).forEach(([key, { newValue }]) => {
_listeners.forEach((callback) => {
callback(newValue ? 'update' : 'remove', key)
@@ -81,3 +82,5 @@ export const webExtensionDriver: (opts: WebExtensionDriverOptions) => Driver = d
}
}
})
export default webExtensionDriver

View File

@@ -1,106 +0,0 @@
import { type Config } from 'tailwindcss'
import typography from '@tailwindcss/typography'
import animate from 'tailwindcss-animate'
export default {
darkMode: ['class'],
content: ['./src/**/*.{ts,tsx,css,html}'],
theme: {
container: {
center: true,
padding: '2rem'
},
extend: {
fontSize: {
'2xs': '0.625rem'
},
zIndex: {
infinity: 'calc(infinity)'
},
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
}
},
minWidth: {
screen: '100vw'
},
backdropBlur: {
xs: '2px'
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
},
keyframes: {
'accordion-down': {
from: { height: '0' },
to: { height: 'var(--radix-accordion-content-height)' }
},
'accordion-up': {
from: { height: 'var(--radix-accordion-content-height)' },
to: { height: '0' }
},
pulse: {
'0%, 100%': { boxShadow: '0 0 0 0 var(--pulse-color)' },
'50%': { boxShadow: '0 0 0 8px var(--pulse-color)' }
},
meteor: {
'0%': { transform: 'rotate(215deg) translateX(0)', opacity: '1' },
'70%': { opacity: '1' },
'100%': {
transform: 'rotate(215deg) translateX(-500px)',
opacity: '0'
}
},
shimmer: {
'0%': {
'--shimmer-angle': '0deg'
},
'100%': {
'--shimmer-angle': '360deg'
}
}
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
pulse: 'pulse var(--duration) ease-out infinite',
meteor: 'meteor 5s linear infinite'
}
}
},
plugins: [animate, typography()]
} satisfies Config

View File

@@ -3,14 +3,16 @@ import { defineConfig } from 'wxt'
import react from '@vitejs/plugin-react'
import { name, displayName, homepage } from './package.json'
import svgr from 'vite-plugin-svgr'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
srcDir: path.resolve('src'),
imports: false,
entrypointsDir: 'app',
runner: {
webExt: {
startUrls: ['https://www.example.com/']
},
manifestVersion: 3,
manifest: ({ browser }) => {
const common = {
name: displayName,
@@ -45,6 +47,7 @@ export default defineConfig({
},
plugins: [
react(),
tailwindcss(),
svgr({
include: '**/*.svg'
})