chore: build basic layout framework

This commit is contained in:
molvqingtai 2023-07-24 04:10:04 +08:00
parent 1a8d2ec675
commit 5bb773c0e3
31 changed files with 999 additions and 190 deletions

View file

@ -31,6 +31,7 @@
},
"rules": {
"prettier/prettier": "error",
"react/prop-types": "off",
"@typescript-eslint/naming-convention": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/explicit-function-return-type": "off",

14
.vscode/launch.json vendored Normal file
View 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
View 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>

View file

@ -10,7 +10,7 @@ export default defineManifest({
content_scripts: [
{
js: ['src/main.tsx'],
matches: [isDev ? `*://localhost/*` : '<all_urls>']
matches: isDev ? ['*://localhost/*', 'https://www.example.com/*'] : ['https://*/*']
}
]
})

View file

@ -5,13 +5,14 @@
"description": "Chatting Anonymously with People on the Same Website.",
"type": "module",
"scripts": {
"dev": "vite",
"dev": "vite --force",
"dev:web": "vite -c vite.config.web.ts --force",
"build": "vite build",
"pack": "cross-env NODE_ENV=production run-p pack:*",
"pack:zip": "rimraf dist.zip && jszip-cli add dist/* -o ./dist.zip",
"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",
"lint": "npx eslint . --ext .js,.jsx,.ts,.tsx --cache --fix",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --cache --fix",
"clear": "rimraf dist dist.*",
"tsc:check": "tsc --noEmit",
"prepare": "husky install"
@ -86,13 +87,16 @@
"*.{js,jsx,ts,tsx}": "eslint --fix"
},
"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-slot": "^1.0.2",
"class-variance-authority": "^0.6.1",
"clsx": "^1.2.1",
"lucide-react": "^0.263.0",
"peerjs": "^1.4.7",
"react-use": "^17.4.0",
"tailwind-merge": "^1.13.2",
"tailwindcss-animate": "^1.0.6",
"type-fest": "^3.13.0"
},
"engines": {

File diff suppressed because it is too large Load diff

View file

@ -1,18 +1,17 @@
import Header from '@/components/Header'
import Footer from '@/components/Footer'
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() {
return (
<>
<main className="main">
<AppContainer>
<Header />
<Main />
<Sidebar />
<Footer />
</main>
</AppContainer>
<AppButton></AppButton>
</>

View file

@ -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

View 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

View 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

View file

@ -1,7 +0,0 @@
import { type FC } from 'react'
const Footer: FC = () => {
return <div></div>
}
export default Footer

View 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

View file

@ -1,7 +0,0 @@
import { type FC } from 'react'
const Header: FC = ({ ...props }) => {
return <div></div>
}
export default Header

View 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

View file

@ -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>
)
}

View file

@ -0,0 +1,9 @@
import { type FC } from 'react'
const Message: FC = () => {
return <div>Message</div>
}
Message.displayName = 'Message'
export default Message

View file

@ -1,7 +1,9 @@
import { type FC } from 'react'
const Main: FC = () => {
return <div></div>
return <div>Main</div>
}
Main.displayName = 'Main'
export default Main

View file

@ -1,7 +0,0 @@
import { type FC } from 'react'
const Sidebar: FC = () => {
return <div></div>
}
export default Sidebar

View 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 }

View 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 }

View 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 }

View file

@ -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',
className
)}
ref={ref}
{...props}
/>
)
})

17
src/constants/index.ts Normal file
View 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

View file

@ -6,10 +6,11 @@ export interface RootOptions {
mode?: ShadowRootMode
style?: string
script?: string
element?: Element
}
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 shadowRoot = shadowHost.attachShadow({ mode })
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 reactRoot = createRoot(appRoot)
shadowRoot.append(appStyle, appRoot, appScript)
shadowRoot.append(appStyle, appRoot, appScript, element)
return {
...reactRoot,

View 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
}
}

View file

@ -3,15 +3,22 @@ import App from './App'
import createShadowRoot from './createShadowRoot'
import style from './index.css?inline'
// TODO: css hmr not work
// https://github.com/crxjs/chrome-extension-tools/issues/671
void (() => {
void (async () => {
createShadowRoot(__NAME__, {
style,
style: __DEV__ ? '' : style,
mode: __DEV__ ? 'open' : 'closed'
}).render(
<React.StrictMode>
<App />
</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)
}
})()

View 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

View file

@ -1,14 +1,12 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
import { type Config } from 'tailwindcss'
export default {
darkMode: ['class'],
content: ['./src/**/*.{ts,tsx}'],
content: ['./src/**/*.{ts,tsx,css}', './index.html'],
theme: {
container: {
center: true,
padding: '2rem',
screens: {
'2xl': '1400px'
}
padding: '2rem'
},
extend: {
zIndex: {
@ -56,12 +54,12 @@ module.exports = {
},
keyframes: {
'accordion-down': {
from: { height: 0 },
from: { height: '0' },
to: { height: 'var(--radix-accordion-content-height)' }
},
'accordion-up': {
from: { height: 'var(--radix-accordion-content-height)' },
to: { height: 0 }
to: { height: '0' }
}
},
animation: {
@ -69,6 +67,5 @@ module.exports = {
'accordion-up': 'accordion-up 0.2s ease-out'
}
}
},
plugins: [require('tailwindcss-animate')]
}
}
} satisfies Config

View file

@ -22,6 +22,9 @@ export default defineConfig({
react(),
// https://github.com/antfu/unplugin-icons
Icons({ compiler: 'jsx', jsx: 'react' }),
crx({ manifest })
// @ts-expect-error use local package
crx({
manifest
})
]
})

24
vite.config.web.ts Normal file
View 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' })
]
})