From 882e37289514ce7faf60c1d87814d25cc24f4aa0 Mon Sep 17 00:00:00 2001 From: Luke <39926192+LukeWasTakenn@users.noreply.github.com> Date: Wed, 15 Nov 2023 15:28:21 +0100 Subject: [PATCH] feat(web): font awesome icon animations --- package/client/resource/interface/context.ts | 3 ++ package/client/resource/interface/menu.ts | 3 ++ package/client/resource/interface/notify.ts | 2 + package/client/resource/interface/textui.ts | 3 ++ web/src/components/LibIcon.tsx | 30 +++++++++++++ web/src/features/dev/index.tsx | 3 +- .../dialog/components/fields/color.tsx | 3 +- .../dialog/components/fields/date.tsx | 9 ++-- .../dialog/components/fields/input.tsx | 7 +-- .../dialog/components/fields/number.tsx | 3 +- .../dialog/components/fields/select.tsx | 5 ++- .../dialog/components/fields/textarea.tsx | 4 +- .../dialog/components/fields/time.tsx | 4 +- .../menu/context/components/ContextButton.tsx | 7 +-- .../menu/context/components/HeaderButton.tsx | 3 +- web/src/features/menu/list/ListItem.tsx | 13 ++++-- web/src/features/menu/list/index.tsx | 6 +-- web/src/features/menu/radial/index.tsx | 45 +++++++++++-------- .../notifications/NotificationWrapper.tsx | 6 ++- web/src/features/textui/TextUI.tsx | 4 +- web/src/typings/context.ts | 2 + web/src/typings/menu.ts | 2 + web/src/typings/notifications.ts | 2 + web/src/typings/textui.ts | 2 + 24 files changed, 124 insertions(+), 47 deletions(-) create mode 100644 web/src/components/LibIcon.tsx diff --git a/package/client/resource/interface/context.ts b/package/client/resource/interface/context.ts index 20180bd..131c2bd 100644 --- a/package/client/resource/interface/context.ts +++ b/package/client/resource/interface/context.ts @@ -1,5 +1,7 @@ import { IconName, IconPrefix } from '@fortawesome/fontawesome-common-types'; +type IconAnimation = 'spin' | 'spinPulse' | 'spinReverse' | 'pulse' | 'beat' | 'fade' | 'beatFade' | 'bounce' | 'shake'; + interface ContextMenuItem { menu?: string; title?: string; @@ -8,6 +10,7 @@ interface ContextMenuItem { image?: string; icon?: IconName | [IconPrefix, IconName] | string; iconColor?: string; + iconAnimation?: IconAnimation; progress?: number; colorScheme?: string; onSelect?: (args: any) => void; diff --git a/package/client/resource/interface/menu.ts b/package/client/resource/interface/menu.ts index 2b22a62..060e265 100644 --- a/package/client/resource/interface/menu.ts +++ b/package/client/resource/interface/menu.ts @@ -2,10 +2,13 @@ import { IconName, IconPrefix } from '@fortawesome/fontawesome-common-types'; type MenuPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; type ChangeFunction = (selected: number, scrollIndex?: number, args?: any, checked?: boolean) => void; +type IconAnimation = 'spin' | 'spinPulse' | 'spinReverse' | 'pulse' | 'beat' | 'fade' | 'beatFade' | 'bounce' | 'shake'; interface MenuOptions { label: string; icon?: IconName | [IconPrefix, IconName] | string; + iconColor?: string; + iconAnimation?: IconAnimation; checked?: boolean; values?: Array; description?: string; diff --git a/package/client/resource/interface/notify.ts b/package/client/resource/interface/notify.ts index d255514..dc798c3 100644 --- a/package/client/resource/interface/notify.ts +++ b/package/client/resource/interface/notify.ts @@ -11,6 +11,7 @@ type NotificationPosition = | 'center-right' | 'center-left'; type NotificationType = 'inform' | 'error' | 'success'; +type IconAnimation = 'spin' | 'spinPulse' | 'spinReverse' | 'pulse' | 'beat' | 'fade' | 'beatFade' | 'bounce' | 'shake'; interface NotifyProps { id?: string | number; @@ -22,6 +23,7 @@ interface NotifyProps { style?: Sx; icon?: IconName | [IconPrefix, IconName]; iconColor?: string; + iconAnimation?: IconAnimation; alignIcon?: 'top' | 'center'; } diff --git a/package/client/resource/interface/textui.ts b/package/client/resource/interface/textui.ts index 70186de..84c66a9 100644 --- a/package/client/resource/interface/textui.ts +++ b/package/client/resource/interface/textui.ts @@ -1,10 +1,13 @@ import { IconLookup, IconName, IconPrefix } from '@fortawesome/fontawesome-common-types'; import { CSSProperties } from 'react'; +type IconAnimation = 'spin' | 'spinPulse' | 'spinReverse' | 'pulse' | 'beat' | 'fade' | 'beatFade' | 'bounce' | 'shake'; + interface OptionsProps { position?: 'right-center' | 'left-center' | 'top-center'; icon?: IconName | [IconPrefix, IconName]; iconColor?: string; + iconAnimation?: IconAnimation; style?: CSSProperties; alignIcon?: 'top' | 'center'; } diff --git a/web/src/components/LibIcon.tsx b/web/src/components/LibIcon.tsx new file mode 100644 index 0000000..75a4f93 --- /dev/null +++ b/web/src/components/LibIcon.tsx @@ -0,0 +1,30 @@ +import { FontAwesomeIcon, FontAwesomeIconProps } from '@fortawesome/react-fontawesome'; + +export type IconAnimation = + | 'spin' + | 'spinPulse' + | 'spinReverse' + | 'pulse' + | 'beat' + | 'fade' + | 'beatFade' + | 'bounce' + | 'shake'; + +const LibIcon: React.FC = (props) => { + const animationProps = { + spin: props.animation === 'spin', + spinPulse: props.animation === 'spinPulse', + spinReverse: props.animation === 'spinReverse', + pulse: props.animation === 'pulse', + beat: props.animation === 'beat', + fade: props.animation === 'fade', + beatFade: props.animation === 'beatFade', + bounce: props.animation === 'bounce', + shake: props.animation === 'shake', + }; + + return ; +}; + +export default LibIcon; diff --git a/web/src/features/dev/index.tsx b/web/src/features/dev/index.tsx index db4b4d7..c6b5bfb 100644 --- a/web/src/features/dev/index.tsx +++ b/web/src/features/dev/index.tsx @@ -10,6 +10,7 @@ import { debugTextUI } from './debug/textui'; import { debugSkillCheck } from './debug/skillcheck'; import { useState } from 'react'; import { debugRadial } from './debug/radial'; +import LibIcon from '../../components/LibIcon'; const Dev: React.FC = () => { const [opened, setOpened] = useState(false); @@ -27,7 +28,7 @@ const Dev: React.FC = () => { mr={50} mb={50} > - + diff --git a/web/src/features/dialog/components/fields/color.tsx b/web/src/features/dialog/components/fields/color.tsx index 4579cc8..6ce98f8 100644 --- a/web/src/features/dialog/components/fields/color.tsx +++ b/web/src/features/dialog/components/fields/color.tsx @@ -3,6 +3,7 @@ import { Control, useController } from 'react-hook-form'; import { FormValues } from '../../InputDialog'; import { ColorInput } from '@mantine/core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import LibIcon from '../../../../components/LibIcon'; interface Props { row: IColorInput; @@ -32,7 +33,7 @@ const ColorField: React.FC = (props) => { defaultValue={props.row.default} format={props.row.format} withAsterisk={props.row.required} - icon={props.row.icon && } + icon={props.row.icon && } /> ); }; diff --git a/web/src/features/dialog/components/fields/date.tsx b/web/src/features/dialog/components/fields/date.tsx index 30dca2e..d4e007e 100644 --- a/web/src/features/dialog/components/fields/date.tsx +++ b/web/src/features/dialog/components/fields/date.tsx @@ -3,6 +3,7 @@ import { Control, useController } from 'react-hook-form'; import { FormValues } from '../../InputDialog'; import { DatePicker, DateRangePicker } from '@mantine/dates'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import LibIcon from '../../../../components/LibIcon'; interface Props { row: IDateInput; @@ -34,7 +35,7 @@ const DateField: React.FC = (props) => { inputFormat={props.row.format} withAsterisk={props.row.required} clearable={props.row.clearable} - icon={props.row.icon && } + icon={props.row.icon && } minDate={props.row.min ? new Date(props.row.min) : undefined} maxDate={props.row.max ? new Date(props.row.max) : undefined} /> @@ -51,7 +52,9 @@ const DateField: React.FC = (props) => { name={controller.field.name} ref={controller.field.ref} onBlur={controller.field.onBlur} - onChange={(dates) => controller.field.onChange(dates.map((date: Date | null) => date ? date.getTime() : null))} + onChange={(dates) => + controller.field.onChange(dates.map((date: Date | null) => (date ? date.getTime() : null))) + } label={props.row.label} description={props.row.description} placeholder={props.row.format} @@ -59,7 +62,7 @@ const DateField: React.FC = (props) => { inputFormat={props.row.format} withAsterisk={props.row.required} clearable={props.row.clearable} - icon={props.row.icon && } + icon={props.row.icon && } minDate={props.row.min ? new Date(props.row.min) : undefined} maxDate={props.row.max ? new Date(props.row.max) : undefined} /> diff --git a/web/src/features/dialog/components/fields/input.tsx b/web/src/features/dialog/components/fields/input.tsx index 7e772ad..c0014f3 100644 --- a/web/src/features/dialog/components/fields/input.tsx +++ b/web/src/features/dialog/components/fields/input.tsx @@ -3,6 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import React from 'react'; import { IInput } from '../../../../typings/dialog'; import { UseFormRegisterReturn } from 'react-hook-form'; +import LibIcon from '../../../../components/LibIcon'; interface Props { register: UseFormRegisterReturn; @@ -27,7 +28,7 @@ const InputField: React.FC = (props) => { defaultValue={props.row.default} label={props.row.label} description={props.row.description} - icon={props.row.icon && } + icon={props.row.icon && } placeholder={props.row.placeholder} minLength={props.row.min} maxLength={props.row.max} @@ -40,14 +41,14 @@ const InputField: React.FC = (props) => { defaultValue={props.row.default} label={props.row.label} description={props.row.description} - icon={props.row.icon && } + icon={props.row.icon && } placeholder={props.row.placeholder} minLength={props.row.min} maxLength={props.row.max} disabled={props.row.disabled} withAsterisk={props.row.required} visibilityToggleIcon={({ reveal, size }) => ( - = (props) => { step={props.row.step} precision={props.row.precision} disabled={props.row.disabled} - icon={props.row.icon && } + icon={props.row.icon && } withAsterisk={props.row.required} /> ); diff --git a/web/src/features/dialog/components/fields/select.tsx b/web/src/features/dialog/components/fields/select.tsx index a70409e..350f710 100644 --- a/web/src/features/dialog/components/fields/select.tsx +++ b/web/src/features/dialog/components/fields/select.tsx @@ -3,6 +3,7 @@ import { ISelect } from '../../../../typings'; import { Control, useController } from 'react-hook-form'; import { FormValues } from '../../InputDialog'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import LibIcon from '../../../../components/LibIcon'; interface Props { row: ISelect; @@ -32,7 +33,7 @@ const SelectField: React.FC = (props) => { description={props.row.description} withAsterisk={props.row.required} clearable={props.row.clearable} - icon={props.row.icon && } + icon={props.row.icon && } /> ) : ( <> @@ -49,7 +50,7 @@ const SelectField: React.FC = (props) => { description={props.row.description} withAsterisk={props.row.required} clearable={props.row.clearable} - icon={props.row.icon && } + icon={props.row.icon && } /> )} diff --git a/web/src/features/dialog/components/fields/textarea.tsx b/web/src/features/dialog/components/fields/textarea.tsx index 2fbbd3b..e2d097f 100644 --- a/web/src/features/dialog/components/fields/textarea.tsx +++ b/web/src/features/dialog/components/fields/textarea.tsx @@ -1,8 +1,8 @@ import { Textarea } from '@mantine/core'; import { UseFormRegisterReturn } from 'react-hook-form'; import { ITextarea } from '../../../../typings/dialog'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import React from 'react'; +import LibIcon from '../../../../components/LibIcon'; interface Props { register: UseFormRegisterReturn; @@ -17,7 +17,7 @@ const TextareaField: React.FC = (props) => { defaultValue={props.row.default} label={props.row.label} description={props.row.description} - icon={props.row.icon && } + icon={props.row.icon && } placeholder={props.row.placeholder} disabled={props.row.disabled} withAsterisk={props.row.required} diff --git a/web/src/features/dialog/components/fields/time.tsx b/web/src/features/dialog/components/fields/time.tsx index d92f304..eb1678f 100644 --- a/web/src/features/dialog/components/fields/time.tsx +++ b/web/src/features/dialog/components/fields/time.tsx @@ -1,8 +1,8 @@ import { TimeInput } from '@mantine/dates'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Control, useController } from 'react-hook-form'; import { ITimeInput } from '../../../../typings/dialog'; import { FormValues } from '../../InputDialog'; +import LibIcon from '../../../../components/LibIcon'; interface Props { row: ITimeInput; @@ -30,7 +30,7 @@ const TimeField: React.FC = (props) => { format={props.row.format || '12'} withAsterisk={props.row.required} clearable={props.row.clearable} - icon={props.row.icon && } + icon={props.row.icon && } /> ); }; diff --git a/web/src/features/menu/context/components/ContextButton.tsx b/web/src/features/menu/context/components/ContextButton.tsx index 312ddf3..6fa4134 100644 --- a/web/src/features/menu/context/components/ContextButton.tsx +++ b/web/src/features/menu/context/components/ContextButton.tsx @@ -1,11 +1,11 @@ import { Button, Box, Group, Stack, Text, Progress, HoverCard, Image, createStyles } from '@mantine/core'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import ReactMarkdown from 'react-markdown'; import { Option, ContextMenuProps } from '../../../../typings'; import { fetchNui } from '../../../../utils/fetchNui'; import { isIconUrl } from '../../../../utils/isIconUrl'; import { IconProp } from '@fortawesome/fontawesome-svg-core'; import MarkdownComponents from '../../../../config/MarkdownComponents'; +import LibIcon from '../../../../components/LibIcon'; const openMenu = (id: string | undefined) => { fetchNui('openContext', { id: id, back: false }); @@ -112,11 +112,12 @@ const ContextButton: React.FC<{ {typeof button.icon === 'string' && isIconUrl(button.icon) ? ( Missing img ) : ( - )} @@ -137,7 +138,7 @@ const ContextButton: React.FC<{ {(button.menu || button.arrow) && button.arrow !== false && ( - + )} diff --git a/web/src/features/menu/context/components/HeaderButton.tsx b/web/src/features/menu/context/components/HeaderButton.tsx index fec426a..fb77c94 100644 --- a/web/src/features/menu/context/components/HeaderButton.tsx +++ b/web/src/features/menu/context/components/HeaderButton.tsx @@ -1,6 +1,7 @@ import { Button, createStyles } from '@mantine/core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import LibIcon from '../../../../components/LibIcon'; interface Props { icon: IconProp; @@ -38,7 +39,7 @@ const HeaderButton: React.FC = ({ icon, canClose, iconSize, handleClick } disabled={canClose === false} onClick={handleClick} > - + ); }; diff --git a/web/src/features/menu/list/ListItem.tsx b/web/src/features/menu/list/ListItem.tsx index 1730a0b..fd66420 100644 --- a/web/src/features/menu/list/ListItem.tsx +++ b/web/src/features/menu/list/ListItem.tsx @@ -1,11 +1,11 @@ import { Box, Group, Stack, Text, Progress, Image } from '@mantine/core'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import React, { forwardRef } from 'react'; import CustomCheckbox from './CustomCheckbox'; import type { MenuItem } from '../../../typings'; import { createStyles } from '@mantine/core'; import { isIconUrl } from '../../../utils/isIconUrl'; import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import LibIcon from '../../../components/LibIcon'; interface Props { item: MenuItem; @@ -89,7 +89,12 @@ const ListItem = forwardRef, Props>(({ item, index, {typeof item.icon === 'string' && isIconUrl(item.icon) ? ( Missing image ) : ( - + )} )} @@ -105,11 +110,11 @@ const ListItem = forwardRef, Props>(({ item, index, - + {scrollIndex + 1}/{item.values.length} - + ) : item.checked !== undefined ? ( diff --git a/web/src/features/menu/list/index.tsx b/web/src/features/menu/list/index.tsx index c724a64..f413452 100644 --- a/web/src/features/menu/list/index.tsx +++ b/web/src/features/menu/list/index.tsx @@ -5,9 +5,9 @@ import ListItem from './ListItem'; import Header from './Header'; import FocusTrap from 'focus-trap-react'; import { fetchNui } from '../../../utils/fetchNui'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import React from 'react'; import type { MenuPosition, MenuSettings } from '../../../typings'; +import LibIcon from '../../../components/LibIcon'; const useStyles = createStyles((theme, params: { position?: MenuPosition; itemCount: number; selected: number }) => ({ tooltip: { @@ -28,7 +28,7 @@ const useStyles = createStyles((theme, params: { position?: MenuPosition; itemCo left: params.position === 'bottom-left' ? 1 : undefined, bottom: params.position === 'bottom-left' || params.position === 'bottom-right' ? 1 : undefined, fontFamily: 'Roboto', - width: 384 + width: 384, }, buttonsWrapper: { height: 'fit-content', @@ -239,7 +239,7 @@ const ListMenu: React.FC = () => { {menu.items.length > 6 && selected !== menu.items.length - 1 && ( - + )} diff --git a/web/src/features/menu/radial/index.tsx b/web/src/features/menu/radial/index.tsx index 0f433eb..d973f11 100644 --- a/web/src/features/menu/radial/index.tsx +++ b/web/src/features/menu/radial/index.tsx @@ -6,6 +6,7 @@ import { fetchNui } from '../../../utils/fetchNui'; import ScaleFade from '../../../transitions/ScaleFade'; import type { RadialMenuItem } from '../../../typings'; import { useLocales } from '../../../providers/LocaleProvider'; +import LibIcon from '../../../components/LibIcon'; const useStyles = createStyles((theme) => ({ wrapper: { @@ -81,18 +82,23 @@ const RadialMenu: React.FC = () => { useEffect(() => { if (menu.items.length <= PAGE_ITEMS) return setMenuItems(menu.items); - const items = menu.items.slice(PAGE_ITEMS * (menu.page - 1) - (menu.page - 1), PAGE_ITEMS * menu.page - menu.page + 1); + const items = menu.items.slice( + PAGE_ITEMS * (menu.page - 1) - (menu.page - 1), + PAGE_ITEMS * menu.page - menu.page + 1 + ); if (PAGE_ITEMS * menu.page - menu.page + 1 < menu.items.length) { items[items.length - 1] = { icon: 'ellipsis-h', label: locale.ui.more, isMore: true }; } setMenuItems(items); }, [menu.items, menu.page]); - useNuiEvent('openRadialMenu', async (data: { items: RadialMenuItem[]; sub?: boolean, option?: string } | false) => { + useNuiEvent('openRadialMenu', async (data: { items: RadialMenuItem[]; sub?: boolean; option?: string } | false) => { if (!data) return setVisible(false); let initialPage = 1; if (data.option) { - data.items.findIndex((item, index) => item.menu == data.option && (initialPage = Math.floor(index / PAGE_ITEMS) + 1)); + data.items.findIndex( + (item, index) => item.menu == data.option && (initialPage = Math.floor(index / PAGE_ITEMS) + 1) + ); } setMenu({ ...data, page: initialPage }); setVisible(true); @@ -134,7 +140,8 @@ const RadialMenu: React.FC = () => { transform={`rotate(-${index * pieAngle} 175 175) translate(${sinAngle * gap}, ${cosAngle * gap})`} className={classes.sector} onClick={async () => { - const clickIndex = menu.page === 1 ? index : PAGE_ITEMS * (menu.page - 1) - (menu.page - 1) + index; + const clickIndex = + menu.page === 1 ? index : PAGE_ITEMS * (menu.page - 1) - (menu.page - 1) + index; if (!item.isMore) fetchNui('radialClick', clickIndex); else { await changePage(true); @@ -147,19 +154,21 @@ const RadialMenu: React.FC = () => { }, ${175 + (175 - gap) * Math.sin(-degToRad(pieAngle))} z`} /> - - - {item.label.includes(" \n") - ? item.label.split(" \n").map((value) => {value}) - : item.label - } + + + {item.label.includes(' \n') + ? item.label.split(' \n').map((value) => ( + + {value} + + )) + : item.label} @@ -183,7 +192,7 @@ const RadialMenu: React.FC = () => {
- ({ container: { @@ -173,11 +174,12 @@ const Notifications: React.FC = () => { radius="xl" size={32} > - + ) : ( - ({ wrapper: { @@ -53,10 +54,11 @@ const TextUI: React.FC = () => { {data.icon && ( -