chore: build basic layout framework
This commit is contained in:
parent
1a8d2ec675
commit
5bb773c0e3
31 changed files with 999 additions and 190 deletions
|
@ -31,6 +31,7 @@
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"prettier/prettier": "error",
|
"prettier/prettier": "error",
|
||||||
|
"react/prop-types": "off",
|
||||||
"@typescript-eslint/naming-convention": "off",
|
"@typescript-eslint/naming-convention": "off",
|
||||||
"@typescript-eslint/no-non-null-assertion": "off",
|
"@typescript-eslint/no-non-null-assertion": "off",
|
||||||
"@typescript-eslint/explicit-function-return-type": "off",
|
"@typescript-eslint/explicit-function-return-type": "off",
|
||||||
|
|
14
.vscode/launch.json
vendored
Normal file
14
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "debug vite",
|
||||||
|
"request": "launch",
|
||||||
|
"runtimeArgs": ["run-script", "dev"],
|
||||||
|
"runtimeExecutable": "npm",
|
||||||
|
"skipFiles": ["<node_internals>/**"],
|
||||||
|
"type": "node",
|
||||||
|
"sourceMaps": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
15
index.html
Normal file
15
index.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="description" content="Chatting Anonymously with People on the Same Website." />
|
||||||
|
<link rel="shortcut icon" href="https://github.com/shadcn.png" type="image/x-icon" />
|
||||||
|
<title>WebChat</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<h1>WebChat Dev</h1>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -10,7 +10,7 @@ export default defineManifest({
|
||||||
content_scripts: [
|
content_scripts: [
|
||||||
{
|
{
|
||||||
js: ['src/main.tsx'],
|
js: ['src/main.tsx'],
|
||||||
matches: [isDev ? `*://localhost/*` : '<all_urls>']
|
matches: isDev ? ['*://localhost/*', 'https://www.example.com/*'] : ['https://*/*']
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
10
package.json
10
package.json
|
@ -5,13 +5,14 @@
|
||||||
"description": "Chatting Anonymously with People on the Same Website.",
|
"description": "Chatting Anonymously with People on the Same Website.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite --force",
|
||||||
|
"dev:web": "vite -c vite.config.web.ts --force",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"pack": "cross-env NODE_ENV=production run-p pack:*",
|
"pack": "cross-env NODE_ENV=production run-p pack:*",
|
||||||
"pack:zip": "rimraf dist.zip && jszip-cli add dist/* -o ./dist.zip",
|
"pack:zip": "rimraf dist.zip && jszip-cli add dist/* -o ./dist.zip",
|
||||||
"pack:crx": "crx pack dist -o ./dist.crx",
|
"pack:crx": "crx pack dist -o ./dist.crx",
|
||||||
"pack:xpi": "cross-env WEB_EXT_ARTIFACTS_DIR=./ web-ext build --source-dir ./dist --filename dist.xpi --overwrite-dest",
|
"pack:xpi": "cross-env WEB_EXT_ARTIFACTS_DIR=./ web-ext build --source-dir ./dist --filename dist.xpi --overwrite-dest",
|
||||||
"lint": "npx eslint . --ext .js,.jsx,.ts,.tsx --cache --fix",
|
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --cache --fix",
|
||||||
"clear": "rimraf dist dist.*",
|
"clear": "rimraf dist dist.*",
|
||||||
"tsc:check": "tsc --noEmit",
|
"tsc:check": "tsc --noEmit",
|
||||||
"prepare": "husky install"
|
"prepare": "husky install"
|
||||||
|
@ -86,13 +87,16 @@
|
||||||
"*.{js,jsx,ts,tsx}": "eslint --fix"
|
"*.{js,jsx,ts,tsx}": "eslint --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@radix-ui/react-avatar": "^1.0.3",
|
||||||
|
"@radix-ui/react-hover-card": "^1.0.6",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
"class-variance-authority": "^0.6.1",
|
"class-variance-authority": "^0.6.1",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
|
"lucide-react": "^0.263.0",
|
||||||
"peerjs": "^1.4.7",
|
"peerjs": "^1.4.7",
|
||||||
|
"react-use": "^17.4.0",
|
||||||
"tailwind-merge": "^1.13.2",
|
"tailwind-merge": "^1.13.2",
|
||||||
"tailwindcss-animate": "^1.0.6",
|
|
||||||
"type-fest": "^3.13.0"
|
"type-fest": "^3.13.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
746
pnpm-lock.yaml
746
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -1,18 +1,17 @@
|
||||||
import Header from '@/components/Header'
|
import Header from '@/components/Header'
|
||||||
import Footer from '@/components/Footer'
|
import Footer from '@/components/Footer'
|
||||||
import Main from '@/components/Main'
|
import Main from '@/components/Main'
|
||||||
import Sidebar from '@/components/Sidebar'
|
import AppButton from '@/components/AppButton'
|
||||||
import AppButton from './components/AppButton'
|
import AppContainer from '@/components/AppContainer'
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<main className="main">
|
<AppContainer>
|
||||||
<Header />
|
<Header />
|
||||||
<Main />
|
<Main />
|
||||||
<Sidebar />
|
|
||||||
<Footer />
|
<Footer />
|
||||||
</main>
|
</AppContainer>
|
||||||
<AppButton></AppButton>
|
<AppButton></AppButton>
|
||||||
</>
|
</>
|
||||||
|
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
import { type FC } from 'react'
|
|
||||||
import { Button } from '@/components/ui/Button'
|
|
||||||
|
|
||||||
const AppButton: FC = () => {
|
|
||||||
return (
|
|
||||||
<Button className="fixed bottom-20 right-10 z-top h-10 w-10 select-none rounded-full text-red-300 ">ICON</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AppButton
|
|
18
src/components/AppButton/index.tsx
Normal file
18
src/components/AppButton/index.tsx
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { type ReactNode, type FC } from 'react'
|
||||||
|
import { Button } from '@/components/ui/Button'
|
||||||
|
|
||||||
|
export interface AppButtonProps {
|
||||||
|
children?: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const AppButton: FC<AppButtonProps> = ({ children }) => {
|
||||||
|
return (
|
||||||
|
<Button className="fixed bottom-5 right-5 z-top h-10 w-10 select-none rounded-full bg-yellow-400 text-xs">
|
||||||
|
{children}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AppButton.displayName = 'AppButton'
|
||||||
|
|
||||||
|
export default AppButton
|
17
src/components/AppContainer/index.tsx
Normal file
17
src/components/AppContainer/index.tsx
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { type ReactNode, type FC } from 'react'
|
||||||
|
|
||||||
|
export interface AppContainerProps {
|
||||||
|
children?: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const AppContainer: FC<AppContainerProps> = ({ children }) => {
|
||||||
|
return (
|
||||||
|
<div className="fixed bottom-10 right-10 top-10 z-top box-border grid w-1/4 grid-flow-col grid-rows-[auto_1fr_auto] overflow-hidden rounded-xl bg-white shadow-2xl transition-transform">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AppContainer.displayName = 'AppContainer'
|
||||||
|
|
||||||
|
export default AppContainer
|
|
@ -1,7 +0,0 @@
|
||||||
import { type FC } from 'react'
|
|
||||||
|
|
||||||
const Footer: FC = () => {
|
|
||||||
return <div></div>
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Footer
|
|
39
src/components/Footer/index.tsx
Normal file
39
src/components/Footer/index.tsx
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { useState, type FC, type ChangeEvent } from 'react'
|
||||||
|
import { Textarea } from '@/components/ui/Textarea'
|
||||||
|
import { Button } from '@/components/ui/Button'
|
||||||
|
import { Smile, Command, CornerDownLeft } from 'lucide-react'
|
||||||
|
import { useBreakpoint } from '@/hooks/useBreakpoint'
|
||||||
|
|
||||||
|
const Footer: FC = () => {
|
||||||
|
const { is2XL } = useBreakpoint()
|
||||||
|
const [message, setMessage] = useState('')
|
||||||
|
|
||||||
|
const handleInput = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
setMessage(e.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-2 gap-y-2 p-4">
|
||||||
|
<Textarea
|
||||||
|
className="col-span-2"
|
||||||
|
rows={is2XL ? 3 : 2}
|
||||||
|
value={message}
|
||||||
|
placeholder="Type your message here."
|
||||||
|
onInput={handleInput}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button variant="ghost" size="sm" className="place-self-start">
|
||||||
|
<Smile size={20} />
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" className="place-self-end">
|
||||||
|
<span className="mr-2">Send</span>
|
||||||
|
<Command className="text-slate-400" size={12}></Command>
|
||||||
|
<CornerDownLeft className="text-slate-400" size={12}></CornerDownLeft>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Footer.displayName = 'Footer'
|
||||||
|
|
||||||
|
export default Footer
|
|
@ -1,7 +0,0 @@
|
||||||
import { type FC } from 'react'
|
|
||||||
|
|
||||||
const Header: FC = ({ ...props }) => {
|
|
||||||
return <div></div>
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Header
|
|
35
src/components/Header/index.tsx
Normal file
35
src/components/Header/index.tsx
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import { useState, type FC } from 'react'
|
||||||
|
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/HoverCard'
|
||||||
|
import { Button } from '@/components/ui/Button'
|
||||||
|
import getWebSiteInfo from '@/utils/getWebsiteInfo'
|
||||||
|
|
||||||
|
const Header: FC = ({ ...props }) => {
|
||||||
|
const [websiteInfo] = useState(getWebSiteInfo())
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-12 items-center px-4 shadow-sm 2xl:h-14">
|
||||||
|
<img className="h-8 w-8 overflow-hidden rounded-full" src={websiteInfo.icon} />
|
||||||
|
<HoverCard>
|
||||||
|
<HoverCardTrigger asChild>
|
||||||
|
<Button className="overflow-hidden text-xl" variant="link">
|
||||||
|
<h1 className="truncate">{websiteInfo.hostname}</h1>
|
||||||
|
</Button>
|
||||||
|
</HoverCardTrigger>
|
||||||
|
<HoverCardContent className="w-80">
|
||||||
|
<div className="flex justify-between space-x-4">
|
||||||
|
<img className="h-14 w-14 flex-shrink-0 overflow-hidden rounded-full" src={websiteInfo.icon} />
|
||||||
|
<div className="space-y-1">
|
||||||
|
<h4 className="text-sm font-semibold">{websiteInfo.title}</h4>
|
||||||
|
<p className="text-xs text-slate-500">{websiteInfo.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</HoverCardContent>
|
||||||
|
</HoverCard>
|
||||||
|
<div className="ml-auto flex-shrink-0 text-sm text-slate-500">Online 99</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Header.displayName = 'Header'
|
||||||
|
|
||||||
|
export default Header
|
|
@ -1,15 +0,0 @@
|
||||||
import IconPower from '~icons/pixelarticons/power'
|
|
||||||
|
|
||||||
export default function Logo() {
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
className="icon-btn mx-2 text-2xl"
|
|
||||||
rel="noreferrer"
|
|
||||||
href="https://github.com/antfu/vitesse-webext"
|
|
||||||
target="_blank"
|
|
||||||
title="GitHub"
|
|
||||||
>
|
|
||||||
<IconPower />
|
|
||||||
</a>
|
|
||||||
)
|
|
||||||
}
|
|
9
src/components/Main/Message.tsx
Normal file
9
src/components/Main/Message.tsx
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { type FC } from 'react'
|
||||||
|
|
||||||
|
const Message: FC = () => {
|
||||||
|
return <div>Message</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
Message.displayName = 'Message'
|
||||||
|
|
||||||
|
export default Message
|
|
@ -1,7 +1,9 @@
|
||||||
import { type FC } from 'react'
|
import { type FC } from 'react'
|
||||||
|
|
||||||
const Main: FC = () => {
|
const Main: FC = () => {
|
||||||
return <div></div>
|
return <div>Main</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Main.displayName = 'Main'
|
||||||
|
|
||||||
export default Main
|
export default Main
|
|
@ -1,7 +0,0 @@
|
||||||
import { type FC } from 'react'
|
|
||||||
|
|
||||||
const Sidebar: FC = () => {
|
|
||||||
return <div></div>
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Sidebar
|
|
38
src/components/ui/Avatar.tsx
Normal file
38
src/components/ui/Avatar.tsx
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import * as React from 'react'
|
||||||
|
import * as AvatarPrimitive from '@radix-ui/react-avatar'
|
||||||
|
|
||||||
|
import { cn } from '@/utils/index'
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
||||||
|
|
||||||
|
export { Avatar, AvatarImage, AvatarFallback }
|
29
src/components/ui/Badge.tsx
Normal file
29
src/components/ui/Badge.tsx
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import type * as React from 'react'
|
||||||
|
import { cva, type VariantProps } from 'class-variance-authority'
|
||||||
|
|
||||||
|
import { cn } from '@/utils/index'
|
||||||
|
|
||||||
|
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',
|
||||||
|
{
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: 'default'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
|
||||||
|
|
||||||
|
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||||
|
return <div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants }
|
27
src/components/ui/HoverCard.tsx
Normal file
27
src/components/ui/HoverCard.tsx
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
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 }
|
|
@ -11,6 +11,8 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(({ classNa
|
||||||
'flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-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',
|
'flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-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',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
17
src/constants/index.ts
Normal file
17
src/constants/index.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
// https://night-tailwindcss.vercel.app/docs/breakpoints
|
||||||
|
export const BREAKPOINTS = {
|
||||||
|
sm: '640px',
|
||||||
|
// => @media (min-width: 640px) { ... }
|
||||||
|
|
||||||
|
md: '768px',
|
||||||
|
// => @media (min-width: 768px) { ... }
|
||||||
|
|
||||||
|
lg: '1024px',
|
||||||
|
// => @media (min-width: 1024px) { ... }
|
||||||
|
|
||||||
|
xl: '1280px',
|
||||||
|
// => @media (min-width: 1280px) { ... }
|
||||||
|
|
||||||
|
'2xl': '1536px'
|
||||||
|
// => @media (min-width: 1536px) { ... }
|
||||||
|
} as const
|
|
@ -6,10 +6,11 @@ export interface RootOptions {
|
||||||
mode?: ShadowRootMode
|
mode?: ShadowRootMode
|
||||||
style?: string
|
style?: string
|
||||||
script?: string
|
script?: string
|
||||||
|
element?: Element
|
||||||
}
|
}
|
||||||
|
|
||||||
const createShadowRoot = (name: string, options: RootOptions): Root => {
|
const createShadowRoot = (name: string, options: RootOptions): Root => {
|
||||||
const { mode = 'open', style = '', script = '' } = options ?? {}
|
const { mode = 'open', style = '', script = '', element = '' } = options ?? {}
|
||||||
const shadowHost = createElement(`<${name}></${name}>`)
|
const shadowHost = createElement(`<${name}></${name}>`)
|
||||||
const shadowRoot = shadowHost.attachShadow({ mode })
|
const shadowRoot = shadowHost.attachShadow({ mode })
|
||||||
const appRoot = createElement(`<div id="app"></div>`)
|
const appRoot = createElement(`<div id="app"></div>`)
|
||||||
|
@ -17,7 +18,7 @@ const createShadowRoot = (name: string, options: RootOptions): Root => {
|
||||||
const appScript = script && createElement(`<script type="application/javascript">${script}</script>`)
|
const appScript = script && createElement(`<script type="application/javascript">${script}</script>`)
|
||||||
const reactRoot = createRoot(appRoot)
|
const reactRoot = createRoot(appRoot)
|
||||||
|
|
||||||
shadowRoot.append(appStyle, appRoot, appScript)
|
shadowRoot.append(appStyle, appRoot, appScript, element)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...reactRoot,
|
...reactRoot,
|
||||||
|
|
17
src/hooks/useBreakpoint.ts
Normal file
17
src/hooks/useBreakpoint.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { useMedia } from 'react-use'
|
||||||
|
import { BREAKPOINTS } from '@/constants'
|
||||||
|
|
||||||
|
export function useBreakpoint() {
|
||||||
|
const isSM = useMedia(`(min-width: ${BREAKPOINTS.sm})`)
|
||||||
|
const isMD = useMedia(`(min-width: ${BREAKPOINTS.md})`)
|
||||||
|
const isLG = useMedia(`(min-width: ${BREAKPOINTS.lg})`)
|
||||||
|
const isXL = useMedia(`(min-width: ${BREAKPOINTS.xl})`)
|
||||||
|
const is2XL = useMedia(`(min-width: ${BREAKPOINTS['2xl']})`)
|
||||||
|
return {
|
||||||
|
isSM,
|
||||||
|
isMD,
|
||||||
|
isLG,
|
||||||
|
isXL,
|
||||||
|
is2XL
|
||||||
|
}
|
||||||
|
}
|
15
src/main.tsx
15
src/main.tsx
|
@ -3,15 +3,22 @@ import App from './App'
|
||||||
import createShadowRoot from './createShadowRoot'
|
import createShadowRoot from './createShadowRoot'
|
||||||
import style from './index.css?inline'
|
import style from './index.css?inline'
|
||||||
|
|
||||||
// TODO: css hmr not work
|
void (async () => {
|
||||||
// https://github.com/crxjs/chrome-extension-tools/issues/671
|
|
||||||
void (() => {
|
|
||||||
createShadowRoot(__NAME__, {
|
createShadowRoot(__NAME__, {
|
||||||
style,
|
style: __DEV__ ? '' : style,
|
||||||
mode: __DEV__ ? 'open' : 'closed'
|
mode: __DEV__ ? 'open' : 'closed'
|
||||||
}).render(
|
}).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<App />
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// HMR Hack
|
||||||
|
// https://github.com/crxjs/chrome-extension-tools/issues/600
|
||||||
|
if (__DEV__) {
|
||||||
|
await import('./index.css')
|
||||||
|
const styleElement = document.querySelector('[data-vite-dev-id]')!
|
||||||
|
const shadowRoot = document.querySelector(__NAME__)!.shadowRoot!
|
||||||
|
shadowRoot.insertBefore(styleElement, shadowRoot.firstChild)
|
||||||
|
}
|
||||||
})()
|
})()
|
||||||
|
|
24
src/utils/getWebsiteInfo.ts
Normal file
24
src/utils/getWebsiteInfo.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
const getWebSiteInfo = () => {
|
||||||
|
return {
|
||||||
|
host: document.location.host,
|
||||||
|
hostname: document.location.hostname,
|
||||||
|
href: document.location.href,
|
||||||
|
origin: document.location.origin,
|
||||||
|
title:
|
||||||
|
document.querySelector('meta[rel="og:title i"]')?.getAttribute('content') ??
|
||||||
|
document.querySelector('meta[rel="og:title i"]')?.getAttribute('content') ??
|
||||||
|
document.querySelector('meta[rel="og:site_name i"]')?.getAttribute('content') ??
|
||||||
|
document.title,
|
||||||
|
icon:
|
||||||
|
document.querySelector('meta[property="og:image" i]')?.getAttribute('href') ??
|
||||||
|
document.querySelector('link[rel="icon" i]')?.getAttribute('href') ??
|
||||||
|
document.querySelector('link[rel="shortcut icon" i]')?.getAttribute('href') ??
|
||||||
|
`${document.location.origin}/favicon.ico}`,
|
||||||
|
description:
|
||||||
|
document.querySelector('meta[property="og:description i"]')?.getAttribute('content') ??
|
||||||
|
document.querySelector('meta[name="description" i]')?.getAttribute('content') ??
|
||||||
|
''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getWebSiteInfo
|
|
@ -1,14 +1,12 @@
|
||||||
/** @type {import('tailwindcss').Config} */
|
import { type Config } from 'tailwindcss'
|
||||||
module.exports = {
|
|
||||||
|
export default {
|
||||||
darkMode: ['class'],
|
darkMode: ['class'],
|
||||||
content: ['./src/**/*.{ts,tsx}'],
|
content: ['./src/**/*.{ts,tsx,css}', './index.html'],
|
||||||
theme: {
|
theme: {
|
||||||
container: {
|
container: {
|
||||||
center: true,
|
center: true,
|
||||||
padding: '2rem',
|
padding: '2rem'
|
||||||
screens: {
|
|
||||||
'2xl': '1400px'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
extend: {
|
extend: {
|
||||||
zIndex: {
|
zIndex: {
|
||||||
|
@ -56,12 +54,12 @@ module.exports = {
|
||||||
},
|
},
|
||||||
keyframes: {
|
keyframes: {
|
||||||
'accordion-down': {
|
'accordion-down': {
|
||||||
from: { height: 0 },
|
from: { height: '0' },
|
||||||
to: { height: 'var(--radix-accordion-content-height)' }
|
to: { height: 'var(--radix-accordion-content-height)' }
|
||||||
},
|
},
|
||||||
'accordion-up': {
|
'accordion-up': {
|
||||||
from: { height: 'var(--radix-accordion-content-height)' },
|
from: { height: 'var(--radix-accordion-content-height)' },
|
||||||
to: { height: 0 }
|
to: { height: '0' }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
|
@ -69,6 +67,5 @@ module.exports = {
|
||||||
'accordion-up': 'accordion-up 0.2s ease-out'
|
'accordion-up': 'accordion-up 0.2s ease-out'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
plugins: [require('tailwindcss-animate')]
|
} satisfies Config
|
||||||
}
|
|
||||||
|
|
|
@ -22,6 +22,9 @@ export default defineConfig({
|
||||||
react(),
|
react(),
|
||||||
// https://github.com/antfu/unplugin-icons
|
// https://github.com/antfu/unplugin-icons
|
||||||
Icons({ compiler: 'jsx', jsx: 'react' }),
|
Icons({ compiler: 'jsx', jsx: 'react' }),
|
||||||
crx({ manifest })
|
// @ts-expect-error use local package
|
||||||
|
crx({
|
||||||
|
manifest
|
||||||
|
})
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
24
vite.config.web.ts
Normal file
24
vite.config.web.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import path from 'node:path'
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
import Icons from 'unplugin-icons/vite'
|
||||||
|
import packageJson from './package.json'
|
||||||
|
|
||||||
|
const isDev = process.env.NODE_ENV === 'development'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
define: {
|
||||||
|
__DEV__: isDev,
|
||||||
|
__NAME__: JSON.stringify(packageJson.name)
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
// https://github.com/antfu/unplugin-icons
|
||||||
|
Icons({ compiler: 'jsx', jsx: 'react' })
|
||||||
|
]
|
||||||
|
})
|
Loading…
Reference in a new issue