chore(layout): layout resize
This commit is contained in:
parent
a363becbb9
commit
fd0ecf579d
11 changed files with 693 additions and 545 deletions
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
18
package.json
18
package.json
|
@ -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",
|
||||
|
|
1043
pnpm-lock.yaml
1043
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -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'
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
86
src/hooks/useResizable.ts
Normal 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
|
Loading…
Reference in a new issue