diff --git a/package.json b/package.json index e2c051e9c1a13eacc31d797e500f0e156bcb2707..8862bde66511dfc2137d040ee0ba624c7af41a57 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "build": "rimraf build && tsc && vite build", "preview": "vite preview", "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'", - "fmt": "prettier --write 'src/**/*.{js,jsx,ts,tsx}'" + "fmt": "prettier --write 'src/**/*.{js,jsx,ts,tsx}'", + "typecheck": "tsc --noEmit" }, "eslintConfig": { "parser": "@typescript-eslint/parser", @@ -54,6 +55,7 @@ "markdown-it-emoji": "^2.0.0", "markdown-it-sub": "^1.0.0", "markdown-it-sup": "^1.0.0", + "preact-context-menu": "^0.1.5", "preact-i18n": "^2.4.0-preactx", "prettier": "^2.3.1", "prismjs": "^1.23.0", diff --git a/src/components/ui/Header.tsx b/src/components/ui/Header.tsx index 6a97dd3aeeee46c1a2f5b48c3e156117fa57e7d6..cb17b8c563e02cd8399aa475759b41d79337faaf 100644 --- a/src/components/ui/Header.tsx +++ b/src/components/ui/Header.tsx @@ -17,7 +17,7 @@ export default styled.div<Props>` flex-shrink: 0; align-items: center; - background-color: var(--primary-background); + background-color: var(--primary-header); background-size: cover !important; background-position: center !important; @@ -27,6 +27,7 @@ export default styled.div<Props>` ` } ${ props => props.placement === 'secondary' && css` + background-color: var(--secondary-header); padding: 14px; ` } `; diff --git a/src/components/ui/IconButton.tsx b/src/components/ui/IconButton.tsx new file mode 100644 index 0000000000000000000000000000000000000000..497f287cb37d000e560ba8b3109b4904e0dfc5c1 --- /dev/null +++ b/src/components/ui/IconButton.tsx @@ -0,0 +1,43 @@ +import styled, { css } from "styled-components"; + +interface Props { + type?: 'default' | 'circle' +} + +const normal = `var(--secondary-foreground)`; +const hover = `var(--foreground)`; + +export default styled.div<Props>` + z-index: 1; + display: grid; + cursor: pointer; + place-items: center; + + fill: ${normal}; + color: ${normal}; + stroke: ${normal}; + + a { + color: ${normal}; + } + + &:hover { + fill: ${hover}; + color: ${hover}; + stroke: ${hover}; + + a { + color: ${hover}; + } + } + + ${ props => props.type === 'circle' && css` + padding: 4px; + border-radius: 50%; + background-color: var(--secondary-header); + + &:hover { + background-color: var(--primary-header); + } + ` } +`; diff --git a/src/components/ui/Modal.tsx b/src/components/ui/Modal.tsx index 65a93960bc65129e660bf02cced723959f7fd3b6..7d80cb81b4f5addba8304d696b806c1dd94158df 100644 --- a/src/components/ui/Modal.tsx +++ b/src/components/ui/Modal.tsx @@ -2,7 +2,7 @@ import Button from "./Button"; import classNames from "classnames"; import { Children } from "../../types/Preact"; import { createPortal, useEffect } from "preact/compat"; -import styled, { keyframes } from "styled-components"; +import styled, { css, keyframes } from "styled-components"; const open = keyframes` 0% {opacity: 0;} @@ -48,6 +48,26 @@ const ModalContainer = styled.div` `; const ModalContent = styled.div<{ [key in 'attachment' | 'noBackground' | 'border']?: boolean }>` + border-radius: 8px; + text-overflow: ellipsis; + + h3 { + margin-top: 0; + } + + ${ props => !props.noBackground && css` + padding: 1.5em; + background: var(--secondary-header); + ` } + + ${ props => props.attachment && css` + border-radius: 8px 8px 0 0; + ` } + + ${ props => props.border && css` + border-radius: 10px; + border: 2px solid var(--secondary-background); + ` } `; const ModalActions = styled.div` @@ -64,7 +84,8 @@ export interface Action { text: Children; onClick: () => void; confirmation?: boolean; - style?: 'default' | 'contrast' | 'error' | 'contrast-error'; + contrast?: boolean; + error?: boolean; } interface Props { @@ -123,7 +144,9 @@ export default function Modal(props: Props) { {props.actions && ( <ModalActions> {props.actions.map(x => ( - <Button style={x.style ?? "contrast"} + <Button + contrast={x.contrast ?? true} + error={x.error ?? false} onClick={x.onClick} disabled={props.disabled}> {x.text} diff --git a/src/components/ui/Overline.tsx b/src/components/ui/Overline.tsx index ee43499fad6a88a2a92a9a7afaadba261493d73e..86233697b79b414e4523da41141a4862bb749c3d 100644 --- a/src/components/ui/Overline.tsx +++ b/src/components/ui/Overline.tsx @@ -1,9 +1,10 @@ import styled, { css } from "styled-components"; import { Children } from "../../types/Preact"; +import { Text } from 'preact-i18n'; interface Props { + error?: string; block?: boolean; - error?: Children; children?: Children; type?: "default" | "subtle" | "error"; } @@ -45,7 +46,9 @@ export default function Overline(props: Props) { <OverlineBase {...props}> {props.children} {props.children && props.error && <> · </>} - {props.error && <Overline type="error">{props.error}</Overline>} + {props.error && <Overline type="error"> + <Text id={`error.${props.error}`}>{props.error}</Text> + </Overline>} </OverlineBase> ); } diff --git a/src/context/intermediate/Intermediate.tsx b/src/context/intermediate/Intermediate.tsx index a623d9f0be35695c58c0ed4d90e730b886273266..1c6bc8ec33f9006b5b8e97f72be159b941aca8b1 100644 --- a/src/context/intermediate/Intermediate.tsx +++ b/src/context/intermediate/Intermediate.tsx @@ -27,7 +27,7 @@ export type Screen = { type: "ban_member", target: Servers.Server, user: string } )) | ({ id: "special_input" } & ( - { type: "create_group" | "create_server" | "set_custom_status" } | + { type: "create_group" | "create_server" | "set_custom_status" | "add_friend" } | { type: "create_channel", server: string } )) | { diff --git a/src/context/intermediate/Modals.tsx b/src/context/intermediate/Modals.tsx index a70c13703c9864ac898e0afd19aa597a8d61e38d..b6035595c6a8380217e7d773a03bf52884e7a0f9 100644 --- a/src/context/intermediate/Modals.tsx +++ b/src/context/intermediate/Modals.tsx @@ -1,12 +1,12 @@ import { Screen } from "./Intermediate"; import { ErrorModal } from "./modals/Error"; +import { InputModal } from "./modals/Input"; +import { PromptModal } from "./modals/Prompt"; import { SignedOutModal } from "./modals/SignedOut"; import { ClipboardModal } from "./modals/Clipboard"; import { OnboardingModal } from "./modals/Onboarding"; import { ModifyAccountModal } from "./modals/ModifyAccount"; -import { InputModal, SpecialInputModal } from "./modals/Input"; -import { PromptModal, SpecialPromptModal } from "./modals/Prompt"; export interface Props { screen: Screen; @@ -19,12 +19,8 @@ export default function Modals({ screen, openScreen }: Props) { switch (screen.id) { case "_prompt": return <PromptModal onClose={onClose} {...screen} />; - case "special_prompt": - return <SpecialPromptModal onClose={onClose} {...screen} />; case "_input": return <InputModal onClose={onClose} {...screen} />; - case "special_input": - return <SpecialInputModal onClose={onClose} {...screen} />; case "error": return <ErrorModal onClose={onClose} {...screen} />; case "signed_out": diff --git a/src/context/intermediate/Popovers.tsx b/src/context/intermediate/Popovers.tsx index 565d762b2b7b6df26b8a164efcb702f065ef6a14..f91b02dcee6a073c1bc58cf9c66983a050b11735 100644 --- a/src/context/intermediate/Popovers.tsx +++ b/src/context/intermediate/Popovers.tsx @@ -2,6 +2,8 @@ import { IntermediateContext, useIntermediate } from "./Intermediate"; import { useContext } from "preact/hooks"; import { UserPicker } from "./popovers/UserPicker"; +import { SpecialInputModal } from "./modals/Input"; +import { SpecialPromptModal } from "./modals/Prompt"; import { UserProfile } from "./popovers/UserProfile"; import { ImageViewer } from "./popovers/ImageViewer"; import { ChannelInfo } from "./popovers/ChannelInfo"; @@ -21,6 +23,10 @@ export default function Popovers() { return <ImageViewer {...screen} onClose={onClose} />; case "channel_info": return <ChannelInfo {...screen} onClose={onClose} />; + case "special_prompt": + return <SpecialPromptModal onClose={onClose} {...screen} />; + case "special_input": + return <SpecialInputModal onClose={onClose} {...screen} />; } return null; diff --git a/src/context/intermediate/modals/Input.tsx b/src/context/intermediate/modals/Input.tsx index 4aa68f9b960d9eaafdc31b85af69b360a594d49f..3d2878522e04e1d4bcc13096baa03e3b8f7f18b8 100644 --- a/src/context/intermediate/modals/Input.tsx +++ b/src/context/intermediate/modals/Input.tsx @@ -12,7 +12,7 @@ import { AppContext } from "../../revoltjs/RevoltClient"; interface Props { onClose: () => void; question: Children; - field: Children; + field?: Children; defaultValue?: string; callback: (value: string) => Promise<void>; } @@ -53,9 +53,9 @@ export function InputModal({ ]} onClose={onClose} > - <Overline error={error} block> + { field ? <Overline error={error} block> {field} - </Overline> + </Overline> : (error && <Overline error={error} type="error" block />) } <InputBox value={value} onChange={e => setValue(e.currentTarget.value)} @@ -65,7 +65,7 @@ export function InputModal({ } type SpecialProps = { onClose: () => void } & ( - { type: "create_group" | "create_server" | "set_custom_status" } | + { type: "create_group" | "create_server" | "set_custom_status" | "add_friend" } | { type: "create_channel", server: string } ) @@ -144,6 +144,15 @@ export function SpecialInputModal(props: SpecialProps) { } />; } + case "add_friend": { + return <InputModal + onClose={onClose} + question={"Add Friend"} + callback={username => + client.users.addFriend(username) + } + />; + } default: return null; } } diff --git a/src/context/intermediate/modals/Prompt.tsx b/src/context/intermediate/modals/Prompt.tsx index c5aedfa9c113a078db32e2648b2d9e6322954f71..645d395e30ca494cc5d9792932c765d5b50120f4 100644 --- a/src/context/intermediate/modals/Prompt.tsx +++ b/src/context/intermediate/modals/Prompt.tsx @@ -1,7 +1,7 @@ import { Text } from "preact-i18n"; import styles from './Prompt.module.scss'; import { Children } from "../../../types/Preact"; -import { IntermediateContext, useIntermediate } from "../Intermediate"; +import { useIntermediate } from "../Intermediate"; import InputBox from "../../../components/ui/InputBox"; import Overline from "../../../components/ui/Overline"; import UserIcon from "../../../components/common/UserIcon"; @@ -82,7 +82,8 @@ export function SpecialPromptModal(props: SpecialProps) { actions={[ { confirmation: true, - style: 'contrast-error', + contrast: true, + error: true, text: <Text id="app.special.modals.actions.delete" />, onClick: async () => { setProcessing(true); @@ -162,7 +163,8 @@ export function SpecialPromptModal(props: SpecialProps) { actions={[ { text: <Text id="app.special.modals.actions.kick" />, - style: 'contrast-error', + contrast: true, + error: true, confirmation: true, onClick: async () => { setProcessing(true); @@ -200,7 +202,8 @@ export function SpecialPromptModal(props: SpecialProps) { actions={[ { text: <Text id="app.special.modals.actions.ban" />, - style: 'contrast-error', + contrast: true, + error: true, confirmation: true, onClick: async () => { setProcessing(true); diff --git a/src/lib/stopPropagation.ts b/src/lib/stopPropagation.ts new file mode 100644 index 0000000000000000000000000000000000000000..98c9fa60450edf28b6846036eb981c17860dc03c --- /dev/null +++ b/src/lib/stopPropagation.ts @@ -0,0 +1,4 @@ +export const stopPropagation = (ev: JSX.TargetedMouseEvent<HTMLDivElement>, _consume?: any) => { + ev.preventDefault(); + ev.stopPropagation(); +}; diff --git a/src/pages/App.tsx b/src/pages/App.tsx index ebe37fe558257d96740abc928bd4f3caf82e4e57..4648649f8738c73e52010ac8101c1258964b7eed 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -1,12 +1,22 @@ import { Docked, OverlappingPanels } from "react-overlapping-panels"; import { isTouchscreenDevice } from "../lib/isTouchscreenDevice"; +import Popovers from "../context/intermediate/Popovers"; import { Switch, Route } from "react-router-dom"; +import styled from "styled-components"; import LeftSidebar from "../components/navigation/LeftSidebar"; import RightSidebar from "../components/navigation/RightSidebar"; import Home from './home/Home'; -import Popovers from "../context/intermediate/Popovers"; +import Friends from "./friends/Friends"; + +const Routes = styled.div` + min-width: 0; + display: flex; + overflow: hidden; + flex-direction: column; + background: var(--primary-background); +`; export default function App() { return ( @@ -16,12 +26,76 @@ export default function App() { leftPanel={{ width: 292, component: <LeftSidebar /> }} rightPanel={{ width: 240, component: <RightSidebar /> }} docked={isTouchscreenDevice ? Docked.None : Docked.Left}> - <Switch> - <Route path="/"> - <Home /> - </Route> - </Switch> + <Routes> + <Switch> + <Route path="/friends"> + <Friends /> + </Route> + + <Route path="/"> + <Home /> + </Route> + </Switch> + </Routes> <Popovers /> </OverlappingPanels> ); }; + +/** + * + * <Route path="/channel/:channel/message/:message"> + <ChannelWrapper /> + </Route> + <Route path="/server/:server/channel/:channel/settings/:page"> + <ChannelSettings key="channel_settings" /> + </Route> + <Route path="/server/:server/channel/:channel/settings"> + <ChannelSettings key="channel_settings" /> + </Route> + <Route path="/server/:server/settings/:page"> + <ServerSettings key="channel_settings" /> + </Route> + <Route path="/server/:server/settings"> + <ServerSettings key="channel_settings" /> + </Route> + <Route path="/channel/:channel/settings/:page"> + <ChannelSettings key="channel_settings" /> + </Route> + <Route path="/channel/:channel/settings"> + <ChannelSettings key="channel_settings" /> + </Route> + + <Route path="/settings/:page"> + <Settings key="settings" /> + </Route> + <Route path="/settings"> + <Settings key="settings" /> + </Route> + + <Route path="/server/:server/channel/:channel"> + <ChannelWrapper /> + </Route> + <Route path="/server/:server" /> + <Route path="/channel/:channel"> + <ChannelWrapper /> + </Route> + + <Route path="/friends"> + <Friends /> + </Route> + <Route path="/dev"> + <Developer /> + </Route> + + <Route path="/open/:id"> + <Open /> + </Route> + {/*<Route path="/invite/:code"> + <OpenInvite /> + </Route> + + <Route path="/"> + <Home /> + </Route> + */ diff --git a/src/pages/friends/Friend.module.scss b/src/pages/friends/Friend.module.scss new file mode 100644 index 0000000000000000000000000000000000000000..b9e90c0091b5650e4b221a3a3acc64f4d1e00d61 --- /dev/null +++ b/src/pages/friends/Friend.module.scss @@ -0,0 +1,71 @@ +.list { + padding: 16px; + user-select: none; + overflow-y: scroll; + + &[data-empty="true"] { + img { + height: 120px; + border-radius: 8px; + } + + gap: 16px; + height: 100%; + display: flex; + align-items: center; + flex-direction: column; + justify-content: center; + } +} + +.friend { + padding: 10px; + display: flex; + border-radius: 5px; + align-items: center; + flex-direction: row; + cursor: pointer; + + &:hover { + background: var(--secondary-background); + + :global(.button) { + background-color: var(--primary-background); + } + } + + .name { + flex-grow: 1; + margin: 0 12px; + font-size: 16px; + + display: flex; + flex-direction: column; + justify-content: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + .subtext { + font-size: 12px; + color: var(--tertiary-foreground); + } + } + + .actions { + display: flex; + gap: 12px; + + > div { + height: 32px; + width: 32px; + } + } +} + +//! FIXME: Move this to the Header component, do this: +// 1. Check if header has topic, if yes, flex-grow: 0 on the title. +// 2. If header has no topic (example: friends page), flex-grow 1 on the header title. +.title { + flex-grow: 1; +} diff --git a/src/pages/friends/Friend.tsx b/src/pages/friends/Friend.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d27ee852560c97f79136baa5e18458507604cd6c --- /dev/null +++ b/src/pages/friends/Friend.tsx @@ -0,0 +1,90 @@ +import { Text } from "preact-i18n"; +import { Link } from "react-router-dom"; +import styles from "./Friend.module.scss"; +import { useContext } from "preact/hooks"; +import { Children } from "../../types/Preact"; +import { X, Plus, Mail } from "@styled-icons/feather"; +import UserIcon from "../../components/common/UserIcon"; +import IconButton from "../../components/ui/IconButton"; +import { attachContextMenu } from "preact-context-menu"; +import { User, Users } from "revolt.js/dist/api/objects"; +import UserStatus from '../../components/common/UserStatus'; +import { stopPropagation } from "../../lib/stopPropagation"; +import { AppContext } from "../../context/revoltjs/RevoltClient"; +import { useIntermediate } from "../../context/intermediate/Intermediate"; + +interface Props { + user: User; +} + +export function Friend({ user }: Props) { + const client = useContext(AppContext); + const { openScreen } = useIntermediate(); + + const actions: Children[] = []; + let subtext: Children = null; + + if (user.relationship === Users.Relationship.Friend) { + subtext = <UserStatus user={user} /> + actions.push( + <IconButton type="circle" + onClick={stopPropagation}> + <Link to={'/open/' + user._id}> + <Mail size={20} /> + </Link> + </IconButton> + ); + } + + if (user.relationship === Users.Relationship.Incoming) { + actions.push( + <IconButton type="circle" + onClick={ev => stopPropagation(ev, client.users.addFriend(user.username))}> + <Plus size={24} /> + </IconButton> + ); + + subtext = <Text id="app.special.friends.incoming" />; + } + + if (user.relationship === Users.Relationship.Outgoing) { + subtext = <Text id="app.special.friends.outgoing" />; + } + + if ( + user.relationship === Users.Relationship.Friend || + user.relationship === Users.Relationship.Outgoing || + user.relationship === Users.Relationship.Incoming + ) { + actions.push( + <IconButton type="circle" + onClick={ev => stopPropagation(ev, client.users.removeFriend(user._id))}> + <X size={24} /> + </IconButton> + ); + } + + if (user.relationship === Users.Relationship.Blocked) { + actions.push( + <IconButton type="circle" + onClick={ev => stopPropagation(ev, client.users.unblockUser(user._id))}> + <X size={24} /> + </IconButton> + ); + } + + return ( + <div className={styles.friend} + onClick={() => openScreen({ id: 'profile', user_id: user._id })} + onContextMenu={attachContextMenu('Menu', { user: user._id })}> + <UserIcon target={user} size={32} status /> + <div className={styles.name}> + <span>@{user.username}</span> + {subtext && ( + <span className={styles.subtext}>{subtext}</span> + )} + </div> + <div className={styles.actions}>{actions}</div> + </div> + ); +} diff --git a/src/pages/friends/Friends.tsx b/src/pages/friends/Friends.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9460f8e4619b42dc24f4fbc1857bde49e68a493e --- /dev/null +++ b/src/pages/friends/Friends.tsx @@ -0,0 +1,85 @@ +import styles from "./Friend.module.scss"; +import { UserPlus } from "@styled-icons/feather"; + +import { Friend } from "./Friend"; +import { Text } from "preact-i18n"; +import Header from "../../components/ui/Header"; +import Overline from "../../components/ui/Overline"; +import IconButton from "../../components/ui/IconButton"; +import { useUsers } from "../../context/revoltjs/hooks"; +import { User, Users } from "revolt.js/dist/api/objects"; +import { useIntermediate } from "../../context/intermediate/Intermediate"; + +export default function Friends() { + const { openScreen } = useIntermediate(); + + const users = useUsers() as User[]; + users.sort((a, b) => a.username.localeCompare(b.username)); + + const pending = users.filter( + x => + x.relationship === Users.Relationship.Incoming || + x.relationship === Users.Relationship.Outgoing + ); + const friends = users.filter( + x => x.relationship === Users.Relationship.Friend + ); + const blocked = users.filter( + x => x.relationship === Users.Relationship.Blocked + ); + + return ( + <> + <Header placement="primary"> + <div className={styles.title}> + <Text id="app.navigation.tabs.friends" /> + </div> + <div className="actions"> + <IconButton onClick={() => openScreen({ id: 'special_input', type: 'add_friend' })}> + <UserPlus size={24} /> + </IconButton> + </div> + </Header> + <div + className={styles.list} + data-empty={ + pending.length + friends.length + blocked.length === 0 + } + > + {pending.length + friends.length + blocked.length === 0 && ( + <> + <img src="https://img.insrt.uk/xexu7/XOPoBUTI47.png/raw" /> + <Text id="app.special.friends.nobody" /> + </> + )} + {pending.length > 0 && ( + <Overline type="subtle"> + <Text id="app.special.friends.pending" /> –{" "} + {pending.length} + </Overline> + )} + {pending.map(y => ( + <Friend key={y._id} user={y} /> + ))} + {friends.length > 0 && ( + <Overline type="subtle"> + <Text id="app.navigation.tabs.friends" /> –{" "} + {friends.length} + </Overline> + )} + {friends.map(y => ( + <Friend key={y._id} user={y} /> + ))} + {blocked.length > 0 && ( + <Overline type="subtle"> + <Text id="app.special.friends.blocked" /> –{" "} + {blocked.length} + </Overline> + )} + {blocked.map(y => ( + <Friend key={y._id} user={y} /> + ))} + </div> + </> + ); +} diff --git a/src/styles/_elements.scss b/src/styles/_elements.scss index d0e652411d01e81e1cb099267e89a5afdf677dbd..f019eff332e59e959c63136d2ccb4133d7a17cda 100644 --- a/src/styles/_elements.scss +++ b/src/styles/_elements.scss @@ -1,3 +1,8 @@ +:disabled { + opacity: 0.5; + pointer-events: none; +} + ::-webkit-scrollbar { width: 3px; height: 3px; diff --git a/src/types/Preact.ts b/src/types/Preact.ts index 80ac4895ea495941fd9d076d4c9c6fb5aac13fb1..dbbea16e674bee4ccced7e757685e020d8ffb6d0 100644 --- a/src/types/Preact.ts +++ b/src/types/Preact.ts @@ -1,4 +1,4 @@ import { VNode } from "preact"; -export type Child = VNode | string | false | undefined; +export type Child = VNode | string | number | boolean | undefined | null; export type Children = Child | Child[] | Children[]; diff --git a/yarn.lock b/yarn.lock index 16f513a7788d5a8922ed7d1f46969fb2a198a561..effb671864e9d8a26b967918ea0d5cd5849ec2ab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3038,6 +3038,13 @@ postcss@^8.3.0: nanoid "^3.1.23" source-map-js "^0.6.2" +preact-context-menu@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/preact-context-menu/-/preact-context-menu-0.1.5.tgz#51d13b0eceed8bd53493f2bdbce36e7c74ff8575" + integrity sha512-cQxcOf4w8MZAQtLpQeX80TQ2TzSKD/z6rnV+654xiHzqQp2pSV+qE0IEZLkUNz88ZmtpIy6GnB/6pCnCGjS4Ww== + dependencies: + preact "^10.4.6" + preact-i18n@^2.4.0-preactx: version "2.4.0-preactx" resolved "https://registry.yarnpkg.com/preact-i18n/-/preact-i18n-2.4.0-preactx.tgz#fbcb2e3ae22744c7fef5a102db2ef7506057d082" @@ -3051,7 +3058,7 @@ preact-markup@^2.0.0: resolved "https://registry.yarnpkg.com/preact-markup/-/preact-markup-2.1.1.tgz#0451e7eed1dac732d7194c34a7f16ff45a2cfdd7" integrity sha512-8JL2p36mzK8XkspOyhBxUSPjYwMxDM0L5BWBZWxsZMVW8WsGQrYQDgVuDKkRspt2hwrle+Cxr/053hpc9BJwfw== -preact@^10.0.0, preact@^10.5.13: +preact@^10.0.0, preact@^10.4.6, preact@^10.5.13: version "10.5.13" resolved "https://registry.yarnpkg.com/preact/-/preact-10.5.13.tgz#85f6c9197ecd736ce8e3bec044d08fd1330fa019" integrity sha512-q/vlKIGNwzTLu+jCcvywgGrt+H/1P/oIRSD6mV4ln3hmlC+Aa34C7yfPI4+5bzW8pONyVXYS7SvXosy2dKKtWQ==