chore(layout): layout resize

This commit is contained in:
molvqingtai 2023-11-14 21:32:00 +08:00
parent a363becbb9
commit fd0ecf579d
11 changed files with 693 additions and 545 deletions

View file

@ -43,6 +43,7 @@
"@typescript-eslint/restrict-template-expressions": "off",
"@typescript-eslint/no-misused-promises": "off",
"@typescript-eslint/consistent-type-assertions": "off",
"import/no-absolute-path": "off"
"import/no-absolute-path": "off",
"@typescript-eslint/no-unused-vars": "warn"
}
}

View file

@ -57,7 +57,7 @@
"date-fns": "^2.30.0",
"idb-keyval": "^6.2.1",
"lucide-react": "^0.292.0",
"nanoid": "^5.0.2",
"nanoid": "^5.0.3",
"peerjs": "^1.5.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@ -71,14 +71,14 @@
"remesh-react": "^4.1.0",
"rxjs": "^7.8.1",
"tailwind-merge": "^2.0.0",
"type-fest": "^4.6.0"
"type-fest": "^4.7.1"
},
"devDependencies": {
"@commitlint/cli": "^18.2.0",
"@commitlint/config-conventional": "^18.1.0",
"@types/node": "^20.8.10",
"@types/react": "^18.2.35",
"@types/react-dom": "^18.2.14",
"@commitlint/cli": "^18.4.0",
"@commitlint/config-conventional": "^18.4.0",
"@types/node": "^20.9.0",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@vitejs/plugin-react": "^4.1.1",
"autoprefixer": "^10.4.16",
"cross-env": "^7.0.3",
@ -86,14 +86,14 @@
"eslint-config-prettier": "^9.0.0",
"eslint-config-standard-with-typescript": "^39.1.1",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-n": "^16.2.0",
"eslint-plugin-n": "^16.3.1",
"eslint-plugin-prettier": "^5.0.1",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-tailwindcss": "^3.13.0",
"husky": "^8.0.3",
"lint-staged": "^15.0.2",
"lint-staged": "^15.1.0",
"npm-run-all": "^4.1.5",
"postcss": "^8.4.31",
"prettier": "^3.0.3",

File diff suppressed because it is too large Load diff

View file

@ -4,8 +4,8 @@ export default defineBackground({
type: 'module',
main() {
browser.runtime.onMessage.addListener(async (message) => {
console.log('Background recieved:', message)
browser.runtime.onMessage.addListener(async (message, options) => {
console.log('Background recieved:', message, options)
console.log('Background sending:', 'pong')
browser.runtime.openOptionsPage()
return 'pong'

View file

@ -1,6 +1,8 @@
import { type ReactNode, type FC, useState } from 'react'
import { type ReactNode, type FC, useState, type MouseEvent, useRef } from 'react'
import { SettingsIcon, MoonIcon, SunIcon } from 'lucide-react'
import { useClickAway } from 'react-use'
import { Button } from '@/components/ui/Button'
import { EVENTS } from '@/constants'
export interface AppButtonProps {
children?: ReactNode
@ -8,29 +10,41 @@ export interface AppButtonProps {
const AppButton: FC<AppButtonProps> = ({ children }) => {
const [open, setOpen] = useState(false)
const [isDarkMode, setIsDarkMode] = useState(window.matchMedia('(prefers-color-scheme: dark)').matches)
const handleToggle = () => {
const menuRef = useRef<HTMLDivElement>(null)
/**
* Waiting for PR merge
* https://github.com/streamich/react-use/pull/2528
*/
useClickAway(
menuRef,
() => {
setOpen(false)
},
['click']
)
const handleToggle = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault()
setOpen(!open)
}
const handleOpenOptionsPage = () => {
console.log(browser.runtime)
browser.runtime.sendMessage('open-options-page').then((response) => {
console.log('Popup response:', response)
})
browser.runtime.sendMessage(EVENTS.OPEN_OPTIONS_PAGE)
}
return (
<div className="fixed bottom-5 right-5 z-top grid select-none justify-center gap-y-3">
<div ref={menuRef} className="fixed bottom-5 right-5 z-top grid select-none justify-center gap-y-3">
<div className="grid gap-y-3" inert={!open && ''}>
<Button
{/* <Button
variant="outline"
data-state={open ? 'open' : 'closed'}
className="h-10 w-10 rounded-full p-0 shadow fill-mode-forwards data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom"
className="h-10 w-10 rounded-full p-0 shadow fill-mode-forwards data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom"
>
<MoonIcon size={20} />
</Button>
</Button> */}
<Button
variant="outline"
data-state={open ? 'open' : 'closed'}
@ -47,7 +61,7 @@ const AppButton: FC<AppButtonProps> = ({ children }) => {
<SettingsIcon size={20} />
</Button>
</div>
<Button onClick={handleToggle} className="relative z-10 h-10 w-10 rounded-full p-0 text-xs shadow">
<Button onContextMenu={handleToggle} className="relative z-10 h-10 w-10 rounded-full p-0 text-xs shadow">
{children}
</Button>
</div>

View file

@ -1,13 +1,29 @@
import { type ReactNode, type FC } from 'react'
import useResizable from '@/hooks/useResizable'
export interface AppContainerProps {
children?: ReactNode
}
const AppContainer: FC<AppContainerProps> = ({ children }) => {
const { size, ref } = useResizable({
initSize: 375,
maxSize: 1000,
minSize: 375,
direction: 'left'
})
return (
<div className="fixed bottom-10 right-10 top-5 z-top box-border grid w-1/4 min-w-[375px] grid-flow-col grid-rows-[auto_1fr_auto] overflow-hidden rounded-xl bg-slate-50 font-sans shadow-2xl transition-transform">
<div
style={{
width: `${size}px`
}}
className="fixed bottom-10 right-10 top-5 z-top box-border grid grid-flow-col grid-rows-[auto_1fr_auto] rounded-xl bg-slate-50 font-sans shadow-2xl"
>
{children}
<div
ref={ref}
className="absolute inset-y-3 -left-0.5 z-20 w-1 cursor-ew-resize rounded-sm bg-slate-100 opacity-0 shadow transition-opacity duration-200 ease-in hover:opacity-100"
></div>
</div>
)
}

View file

@ -47,7 +47,7 @@ const Footer: FC = () => {
}
return (
<div className="relative z-10 grid gap-y-2 px-4 pb-4 pt-2 before:absolute before:-top-4 before:left-0 before:h-4 before:w-full before:bg-gradient-to-t before:from-slate-50 before:from-30% before:to-transparent">
<div className="relative z-10 grid gap-y-2 px-4 pb-4 pt-2 before:pointer-events-none before:absolute before:inset-x-4 before:-top-4 before:h-4 before:bg-gradient-to-t before:from-slate-50 before:from-30% before:to-transparent">
<MessageInput
ref={inputRef}
value={messageBody}

View file

@ -7,7 +7,7 @@ const Header: FC = () => {
const websiteInfo = getWebSiteInfo()
return (
<div className="z-10 grid h-12 grid-flow-col items-center justify-between gap-x-4 bg-white px-4 backdrop-blur-lg 2xl:h-14">
<div className="z-10 grid h-12 grid-flow-col items-center justify-between gap-x-4 rounded-t-xl bg-white px-4 backdrop-blur-lg 2xl:h-14">
<img className="h-8 w-8 overflow-hidden rounded-full" src={websiteInfo.icon} />
<HoverCard>
<HoverCardTrigger asChild>

View file

@ -1,10 +1,21 @@
import { useState } from 'react'
import { useRef, useState } from 'react'
import { useClickAway } from 'react-use'
import wxtLogo from '/wxt.svg'
import reactLogo from '@/assets/react.svg'
import './App.css'
function App() {
const [count, setCount] = useState(0)
const menuRef = useRef<HTMLDivElement>(null)
useClickAway(
menuRef,
(...params) => {
console.log(params)
// setOpen(false)
},
['click']
)
return (
<>
@ -16,7 +27,9 @@ function App() {
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>WXT + React</h1>
<h1 ref={menuRef}>
<button> WXT + React</button>
</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>count is {count}</button>
<p>

View file

@ -1,4 +1,5 @@
// https://www.webfx.com/tools/emoji-cheat-sheet/
export const EMOJI_LIST = [
'😀',
'😃',
@ -185,3 +186,7 @@ export const BREAKPOINTS = {
export const MESSAGE_MAX_LENGTH = 500 as const
export const STORAGE_NAME = 'WEB_CHAT' as const
export enum EVENTS {
OPEN_OPTIONS_PAGE = 'OPEN_OPTIONS_PAGE'
}

86
src/hooks/useResizable.ts Normal file
View file

@ -0,0 +1,86 @@
import { useCallback, useEffect, useRef, useState } from 'react'
export interface ResizableOptions {
minSize: number
maxSize: number
initSize: number
direction: 'left' | 'right' | 'top' | 'bottom'
}
const useResizable = (options: ResizableOptions) => {
const { minSize, maxSize, initSize = 0, direction } = options
const [size, setSize] = useState(initSize)
const [position, setPosition] = useState(0)
const [isMove, setIsMove] = useState(false)
const directionXY = direction === 'left' || direction === 'right' ? 'X' : 'Y'
const handleStart = (e: MouseEvent) => {
const { screenY, screenX } = e
setIsMove(true)
setPosition(directionXY === 'Y' ? screenY : screenX)
document.documentElement.style.userSelect = 'none'
document.documentElement.style.cursor = directionXY === 'Y' ? 'ns-resize' : 'ew-resize'
}
const handleEnd = () => {
setIsMove(false)
document.documentElement.style.cursor = ''
document.documentElement.style.userSelect = ''
}
useEffect(() => {
const handleMove = (e: MouseEvent) => {
if (isMove) {
console.log('move')
const { screenY, screenX } = e
let delta = 0
switch (direction) {
case 'left':
delta = position - screenX
break
case 'right':
delta = screenX - position
break
case 'top':
delta = position - screenY
break
case 'bottom':
delta = screenY - position
break
}
const newSize = size + delta
if (size !== newSize && newSize >= minSize && newSize <= maxSize) {
setSize(newSize)
}
} else {
document.removeEventListener('mousemove', handleMove)
}
}
document.addEventListener('mousemove', handleMove)
return () => {
document.removeEventListener('mousemove', handleMove)
}
}, [isMove])
const ref = useRef<HTMLElement | null>(null)
const setRef = useCallback((node: HTMLElement | null) => {
if (ref.current) {
ref.current.removeEventListener('mousedown', handleStart)
document.removeEventListener('mouseup', handleEnd)
}
if (node) {
node.addEventListener('mousedown', handleStart)
document.addEventListener('mouseup', handleEnd)
}
ref.current = node
}, [])
return { size, ref: setRef }
}
export default useResizable