diff --git a/inertia/components/common/modal/_modal_body.tsx b/inertia/components/common/modal/_modal_body.tsx new file mode 100644 index 0000000..b9b9be0 --- /dev/null +++ b/inertia/components/common/modal/_modal_body.tsx @@ -0,0 +1,12 @@ +import styled from '@emotion/styled'; + +const ModalBody = styled.div({ + width: '100%', + display: 'flex', + flex: 1, + alignItems: 'center', + flexDirection: 'column', + overflow: 'auto', +}); + +export default ModalBody; diff --git a/inertia/components/common/modal/_modal_container.tsx b/inertia/components/common/modal/_modal_container.tsx new file mode 100644 index 0000000..2cc9f6a --- /dev/null +++ b/inertia/components/common/modal/_modal_container.tsx @@ -0,0 +1,23 @@ +import styled from '@emotion/styled'; + +const ModalContainer = styled.div(({ theme }) => ({ + minWidth: '500px', + background: theme.colors.secondary, + padding: '1em', + borderRadius: theme.border.radius, + marginTop: '6em', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flexDirection: 'column', + boxShadow: theme.colors.boxShadow, + + [`@media (max-width: ${theme.media.mobile})`]: { + maxHeight: 'calc(100% - 2em)', + width: 'calc(100% - 2em)', + minWidth: 'unset', + marginTop: '1em', + }, +})); + +export default ModalContainer; diff --git a/inertia/components/common/modal/_modal_header.tsx b/inertia/components/common/modal/_modal_header.tsx new file mode 100644 index 0000000..bbfa0df --- /dev/null +++ b/inertia/components/common/modal/_modal_header.tsx @@ -0,0 +1,20 @@ +import styled from '@emotion/styled'; + +const ModalHeader = styled.h3({ + width: '100%', + marginBottom: '0.75em', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', +}); + +const ModalCloseBtn = styled.button(({ theme }) => ({ + cursor: 'pointer', + color: theme.colors.primary, + backgroundColor: 'transparent', + border: 0, + padding: 0, + margin: 0, +})); + +export { ModalHeader, ModalCloseBtn }; diff --git a/inertia/components/common/modal/_modal_wrapper.tsx b/inertia/components/common/modal/_modal_wrapper.tsx new file mode 100644 index 0000000..b026701 --- /dev/null +++ b/inertia/components/common/modal/_modal_wrapper.tsx @@ -0,0 +1,18 @@ +import styled from '@emotion/styled'; +import { rgba } from '~/lib/color'; + +const ModalWrapper = styled.div(({ theme }) => ({ + zIndex: 9999, + position: 'absolute', + top: 0, + left: 0, + height: '100%', + width: '100%', + background: rgba(theme.colors.black, 0.35), + backdropFilter: 'blur(0.25em)', + display: 'flex', + alignItems: 'center', + flexDirection: 'column', +})); + +export default ModalWrapper; diff --git a/inertia/components/common/modal/modal.tsx b/inertia/components/common/modal/modal.tsx new file mode 100644 index 0000000..b163f7f --- /dev/null +++ b/inertia/components/common/modal/modal.tsx @@ -0,0 +1,59 @@ +import { Fragment, ReactNode, useEffect, useRef } from 'react'; +import { createPortal } from 'react-dom'; +import { IoClose } from 'react-icons/io5'; +import ModalBody from '~/components/common/modal/_modal_body'; +import ModalContainer from '~/components/common/modal/_modal_container'; +import { + ModalCloseBtn, + ModalHeader, +} from '~/components/common/modal/_modal_header'; +import ModalWrapper from '~/components/common/modal/_modal_wrapper'; +import TextEllipsis from '~/components/common/text_ellipsis'; +import useClickOutside from '~/hooks/use_click_outside'; +import useGlobalHotkeys from '~/hooks/use_global_hotkeys'; +import useShortcut from '~/hooks/use_shortcut'; + +interface ModalProps { + title?: string; + children: ReactNode; + opened: boolean; + + close: () => void; +} + +export default function Modal({ + title, + children, + opened = true, + close, +}: ModalProps) { + const modalRef = useRef(null); + const { setGlobalHotkeysEnabled } = useGlobalHotkeys(); + + useClickOutside(modalRef, close); + useShortcut('ESCAPE_KEY', close, { ignoreGlobalHotkeysStatus: true }); + + useEffect(() => setGlobalHotkeysEnabled(!opened), [opened]); + + if (typeof window === 'undefined') { + return ; + } + + return ( + opened && + createPortal( + + + + {title && {title}} + + + + + {children} + + , + document.body + ) + ); +} diff --git a/inertia/components/common/rounded_image.tsx b/inertia/components/common/rounded_image.tsx index 3a27416..81195c1 100644 --- a/inertia/components/common/rounded_image.tsx +++ b/inertia/components/common/rounded_image.tsx @@ -1,11 +1,15 @@ import styled from '@emotion/styled'; +import { rgba } from '~/lib/color'; -const RoundedImage = styled.img({ - 'borderRadius': '50%', +const RoundedImage = styled.img(({ theme }) => { + const transparentBlack = rgba(theme.colors.black, 0.1); + return { + borderRadius: '50%', - '&:hover': { - boxShadow: '0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1)', - }, + '&:hover': { + boxShadow: `0 1px 3px 0 ${transparentBlack}, 0 1px 2px -1px ${transparentBlack}`, + }, + }; }); export default RoundedImage; diff --git a/inertia/components/dashboard/collection/collection_container.tsx b/inertia/components/dashboard/collection/collection_container.tsx index 4f091c5..020eb07 100644 --- a/inertia/components/dashboard/collection/collection_container.tsx +++ b/inertia/components/dashboard/collection/collection_container.tsx @@ -21,7 +21,7 @@ const LinksWrapper = styled.div({ const CollectionHeaderWrapper = styled.h2(({ theme }) => ({ fontWeight: 400, color: theme.colors.font, - paddingRight: '1.1em', + paddingInline: '0.8em 1.1em', display: 'flex', gap: '0.4em', alignItems: 'center', diff --git a/inertia/components/dashboard/side_nav/nav_item.tsx b/inertia/components/dashboard/side_nav/nav_item.tsx index fd46eb4..23a14f8 100644 --- a/inertia/components/dashboard/side_nav/nav_item.tsx +++ b/inertia/components/dashboard/side_nav/nav_item.tsx @@ -2,6 +2,7 @@ import styled from '@emotion/styled'; import { Link } from '@inertiajs/react'; export const Item = styled.div(({ theme }) => ({ + cursor: 'pointer', userSelect: 'none', height: '40px', width: '250px', diff --git a/inertia/components/dashboard/side_nav/open_settings.tsx b/inertia/components/dashboard/side_nav/open_settings.tsx new file mode 100644 index 0000000..c20fc9c --- /dev/null +++ b/inertia/components/dashboard/side_nav/open_settings.tsx @@ -0,0 +1,25 @@ +import styled from '@emotion/styled'; +import { BsGear } from 'react-icons/bs'; +import Modal from '~/components/common/modal/modal'; +import { Item } from '~/components/dashboard/side_nav/nav_item'; +import useToggle from '~/hooks/use_modal'; + +const SettingsButton = styled(Item)(({ theme }) => ({ + color: theme.colors.grey, +})); + +export default function OpenSettingsButton() { + const { isShowing, open, close } = useToggle(); + + return ( + <> + + + Settings + + + Modal settings + + + ); +} diff --git a/inertia/components/dashboard/side_nav/side_navigation.tsx b/inertia/components/dashboard/side_nav/side_navigation.tsx index a34d0cc..30cb312 100644 --- a/inertia/components/dashboard/side_nav/side_navigation.tsx +++ b/inertia/components/dashboard/side_nav/side_navigation.tsx @@ -1,11 +1,11 @@ import styled from '@emotion/styled'; import { route } from '@izzyjs/route/client'; import { AiOutlineFolderAdd } from 'react-icons/ai'; -import { BsGear } from 'react-icons/bs'; import { IoAdd } from 'react-icons/io5'; import { MdOutlineAdminPanelSettings } from 'react-icons/md'; import FavoriteList from '~/components/dashboard/side_nav/favorite/favorite_list'; import { Item, ItemLink } from '~/components/dashboard/side_nav/nav_item'; +import OpenSettingsButton from '~/components/dashboard/side_nav/open_settings'; import UserCard from '~/components/dashboard/side_nav/user_card'; import useActiveCollection from '~/hooks/use_active_collection'; import { appendCollectionId } from '~/lib/navigation'; @@ -21,10 +21,6 @@ const AdminButton = styled(Item)(({ theme }) => ({ color: theme.colors.lightRed, })); -const SettingsButton = styled(Item)(({ theme }) => ({ - color: theme.colors.grey, -})); - const AddButton = styled(ItemLink)(({ theme }) => ({ color: theme.colors.primary, })); @@ -38,10 +34,7 @@ export default function SideNavigation() { Administrator - - - Settings - + void) { +type ShortcutOptions = { ignoreGlobalHotkeysStatus?: boolean }; + +export default function useShortcut( + key: keyof typeof KEYS, + cb: () => void, + options: ShortcutOptions = { + ignoreGlobalHotkeysStatus: false, + } +) { const { globalHotkeysEnabled } = useGlobalHotkeys(); return useHotkeys(KEYS[key], cb, { - enabled: globalHotkeysEnabled, + enabled: !options.ignoreGlobalHotkeysStatus ? globalHotkeysEnabled : true, enableOnFormTags: ['INPUT'], }); } diff --git a/inertia/lib/color.ts b/inertia/lib/color.ts new file mode 100644 index 0000000..e409328 --- /dev/null +++ b/inertia/lib/color.ts @@ -0,0 +1,6 @@ +import hexRgb from 'hex-rgb'; + +export const rgba = (hex: string, alpha: number) => { + const rgb = hexRgb(hex, { format: 'array' }).slice(0, -1).join(','); + return `rgba(${rgb},${alpha})`; +}; diff --git a/inertia/styles/theme.ts b/inertia/styles/theme.ts index b354e33..567b52a 100644 --- a/inertia/styles/theme.ts +++ b/inertia/styles/theme.ts @@ -1,4 +1,5 @@ import { Theme } from '@emotion/react'; +import { rgba } from '~/lib/color'; export const primaryColor = '#3f88c5'; export const primaryDarkColor = '#005aa5'; @@ -32,6 +33,7 @@ export const lightTheme: Theme = { primary: primaryColor, secondary: '#fff', + black: '#333', white: '#ffffff', lightGrey: '#dadce0', @@ -45,7 +47,7 @@ export const lightTheme: Theme = { yellow: '#FF8A08', - boxShadow: '0 0 1em 0 rgba(102, 102, 102, 0.25)', + boxShadow: `0 0 1em 0 ${rgba('#aaa', 0.4)}`, }, border, @@ -60,6 +62,7 @@ export const darkTheme: Theme = { primary: '#4fadfc', secondary: '#323a47', + black: '#333', white: '#ffffff', lightGrey: '#323a47', @@ -73,7 +76,7 @@ export const darkTheme: Theme = { yellow: '#ffc107', - boxShadow: '0 0 1em 0 rgb(40 40 40)', + boxShadow: `0 0 1em 0 ${rgba('#111', 0.4)}`, }, border, diff --git a/inertia/types/emotion.d.ts b/inertia/types/emotion.d.ts index 96c79a9..01a10fc 100644 --- a/inertia/types/emotion.d.ts +++ b/inertia/types/emotion.d.ts @@ -8,6 +8,7 @@ declare module '@emotion/react' { primary: string; secondary: string; + black: string; white: string; lightGrey: string; diff --git a/package-lock.json b/package-lock.json index 895d8aa..e6918d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "@izzyjs/route": "^1.1.0-0", "@vinejs/vine": "^2.0.0", "edge.js": "^6.0.2", + "hex-rgb": "^5.0.0", "i18next": "^23.11.3", "luxon": "^3.4.4", "node-html-parser": "^6.1.13", @@ -6399,6 +6400,17 @@ "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", "dev": true }, + "node_modules/hex-rgb": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/hex-rgb/-/hex-rgb-5.0.0.tgz", + "integrity": "sha512-NQO+lgVUCtHxZ792FodgW0zflK+ozS9X9dwGp9XvvmPlH7pyxd588cn24TD3rmPm/N0AIRXF10Otah8yKqGw4w==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", diff --git a/package.json b/package.json index d059111..a712cba 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "@izzyjs/route": "^1.1.0-0", "@vinejs/vine": "^2.0.0", "edge.js": "^6.0.2", + "hex-rgb": "^5.0.0", "i18next": "^23.11.3", "luxon": "^3.4.4", "node-html-parser": "^6.1.13",