mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-03 13:29:06 +00:00
feat: event finder
This commit is contained in:
committed by
Carlos Valente
parent
a8756c8afe
commit
143917f11a
@@ -11,7 +11,7 @@
|
|||||||
"@emotion/react": "^11.10.6",
|
"@emotion/react": "^11.10.6",
|
||||||
"@emotion/styled": "^11.10.6",
|
"@emotion/styled": "^11.10.6",
|
||||||
"@fontsource/open-sans": "^5.0.28",
|
"@fontsource/open-sans": "^5.0.28",
|
||||||
"@mantine/hooks": "^7.6.2",
|
"@mantine/hooks": "^7.13.3",
|
||||||
"@react-icons/all-files": "^4.1.0",
|
"@react-icons/all-files": "^4.1.0",
|
||||||
"@sentry/react": "^8.19.0",
|
"@sentry/react": "^8.19.0",
|
||||||
"@tanstack/react-query": "^5.17.9",
|
"@tanstack/react-query": "^5.17.9",
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import { lazy, useCallback, useEffect } from 'react';
|
import { lazy, useCallback, useEffect } from 'react';
|
||||||
import { IconButton, useDisclosure } from '@chakra-ui/react';
|
import { IconButton, useDisclosure } from '@chakra-ui/react';
|
||||||
|
import { useHotkeys } from '@mantine/hooks';
|
||||||
import { IoApps } from '@react-icons/all-files/io5/IoApps';
|
import { IoApps } from '@react-icons/all-files/io5/IoApps';
|
||||||
import { IoClose } from '@react-icons/all-files/io5/IoClose';
|
import { IoClose } from '@react-icons/all-files/io5/IoClose';
|
||||||
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
|
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
|
||||||
|
|
||||||
import ProductionNavigationMenu from '../../common/components/navigation-menu/ProductionNavigationMenu';
|
import ProductionNavigationMenu from '../../common/components/navigation-menu/ProductionNavigationMenu';
|
||||||
import useElectronEvent from '../../common/hooks/useElectronEvent';
|
|
||||||
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
||||||
import AppSettings from '../app-settings/AppSettings';
|
import AppSettings from '../app-settings/AppSettings';
|
||||||
import useAppSettingsNavigation from '../app-settings/useAppSettingsNavigation';
|
import useAppSettingsNavigation from '../app-settings/useAppSettingsNavigation';
|
||||||
import { EditorOverview } from '../overview/Overview';
|
import { EditorOverview } from '../overview/Overview';
|
||||||
|
|
||||||
|
import Finder from './finder/Finder';
|
||||||
|
|
||||||
import styles from './Editor.module.scss';
|
import styles from './Editor.module.scss';
|
||||||
|
|
||||||
const Rundown = lazy(() => import('../rundown/RundownExport'));
|
const Rundown = lazy(() => import('../rundown/RundownExport'));
|
||||||
@@ -19,47 +21,8 @@ const MessageControl = lazy(() => import('../control/message/MessageControlExpor
|
|||||||
|
|
||||||
export default function Editor() {
|
export default function Editor() {
|
||||||
const { isOpen: isSettingsOpen, setLocation, close } = useAppSettingsNavigation();
|
const { isOpen: isSettingsOpen, setLocation, close } = useAppSettingsNavigation();
|
||||||
const { isElectron } = useElectronEvent();
|
|
||||||
const { isOpen: isMenuOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen: isMenuOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
const { isOpen: isFinderOpen, onToggle: onFinderToggle, onClose: onFinderClose } = useDisclosure();
|
||||||
const toggleSettings = useCallback(() => {
|
|
||||||
if (isSettingsOpen) {
|
|
||||||
close();
|
|
||||||
} else {
|
|
||||||
setLocation('project');
|
|
||||||
}
|
|
||||||
}, [close, isSettingsOpen, setLocation]);
|
|
||||||
|
|
||||||
// Handle keyboard shortcuts
|
|
||||||
const handleKeyPress = useCallback(
|
|
||||||
(event: KeyboardEvent) => {
|
|
||||||
// handle held key
|
|
||||||
if (event.repeat) return;
|
|
||||||
|
|
||||||
// check if the ctrl key is pressed
|
|
||||||
if (event.ctrlKey || event.metaKey) {
|
|
||||||
// ctrl + , (settings)
|
|
||||||
if (event.key === ',') {
|
|
||||||
toggleSettings();
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[toggleSettings],
|
|
||||||
);
|
|
||||||
|
|
||||||
// register ctrl + , to open settings
|
|
||||||
useEffect(() => {
|
|
||||||
if (isElectron) {
|
|
||||||
document.addEventListener('keydown', handleKeyPress);
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
if (isElectron) {
|
|
||||||
document.removeEventListener('keydown', handleKeyPress);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [handleKeyPress, isElectron]);
|
|
||||||
|
|
||||||
useWindowTitle('Editor');
|
useWindowTitle('Editor');
|
||||||
|
|
||||||
@@ -72,8 +35,23 @@ export default function Editor() {
|
|||||||
}
|
}
|
||||||
}, [setLocation]);
|
}, [setLocation]);
|
||||||
|
|
||||||
|
const toggleSettings = useCallback(() => {
|
||||||
|
if (isSettingsOpen) {
|
||||||
|
close();
|
||||||
|
} else {
|
||||||
|
setLocation('project');
|
||||||
|
}
|
||||||
|
}, [close, isSettingsOpen, setLocation]);
|
||||||
|
|
||||||
|
useHotkeys([
|
||||||
|
['mod + ,', toggleSettings],
|
||||||
|
['mod + f', onFinderToggle],
|
||||||
|
['Escape', onFinderClose],
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.mainContainer} data-testid='event-editor'>
|
<div className={styles.mainContainer} data-testid='event-editor'>
|
||||||
|
<Finder isOpen={isFinderOpen} onClose={onFinderClose} />
|
||||||
<ProductionNavigationMenu isMenuOpen={isMenuOpen} onMenuClose={onClose} />
|
<ProductionNavigationMenu isMenuOpen={isMenuOpen} onMenuClose={onClose} />
|
||||||
<EditorOverview>
|
<EditorOverview>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
.entry,
|
||||||
|
.empty,
|
||||||
|
.error {
|
||||||
|
padding-inline: 0.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
height: 3rem;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry[data-selected='true'] {
|
||||||
|
background-color: $blue-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
color: $label-gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: $error-red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data {
|
||||||
|
display: grid;
|
||||||
|
grid-template-areas:
|
||||||
|
'index cue'
|
||||||
|
'index title';
|
||||||
|
column-gap: 1rem;
|
||||||
|
grid-template-rows: min-content 1fr;
|
||||||
|
|
||||||
|
.index {
|
||||||
|
grid-area: index;
|
||||||
|
background-color: var(--color, $gray-1000);
|
||||||
|
border-radius: 2px;
|
||||||
|
padding-block: 0.25rem;
|
||||||
|
width: 3.5rem;
|
||||||
|
align-self: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
grid-area: title;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cue {
|
||||||
|
grid-area: cue;
|
||||||
|
font-size: calc(1rem - 2px);
|
||||||
|
color: $label-gray;
|
||||||
|
max-height: 1em;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
font-size: calc(1rem - 2px);
|
||||||
|
color: $label-gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.em {
|
||||||
|
color: $ui-white;
|
||||||
|
margin-inline: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollContainer {
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { KeyboardEvent, useState } from 'react';
|
||||||
|
import { Input, Modal, ModalBody, ModalContent, ModalFooter, ModalOverlay } from '@chakra-ui/react';
|
||||||
|
import { useDebouncedCallback } from '@mantine/hooks';
|
||||||
|
import { isOntimeEvent, SupportedEvent } from 'ontime-types';
|
||||||
|
|
||||||
|
import { useEventSelection } from '../../rundown/useEventSelection';
|
||||||
|
|
||||||
|
import useFinder from './useFinder';
|
||||||
|
|
||||||
|
import style from './Finder.module.scss';
|
||||||
|
|
||||||
|
interface FinderProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Finder(props: FinderProps) {
|
||||||
|
const { isOpen, onClose } = props;
|
||||||
|
const { find, results, error } = useFinder();
|
||||||
|
const [selected, setSelected] = useState(0);
|
||||||
|
|
||||||
|
const setSelectedEvents = useEventSelection((state) => state.setSelectedEvents);
|
||||||
|
const debouncedFind = useDebouncedCallback(find, 100);
|
||||||
|
|
||||||
|
const navigate = (event: KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
// all operations need results
|
||||||
|
if (results.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.key === 'ArrowDown') {
|
||||||
|
setSelected((prev) => (prev + 1) % results.length);
|
||||||
|
}
|
||||||
|
if (event.key === 'ArrowUp') {
|
||||||
|
setSelected((prev) => (prev - 1 + results.length) % results.length);
|
||||||
|
}
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
submit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
const selectedEvent = results[selected];
|
||||||
|
setSelectedEvents({ id: selectedEvent.id, index: selectedEvent.index, selectMode: 'click' });
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseMoveEvent = (event: React.MouseEvent<HTMLUListElement>) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
const li = target.closest('li');
|
||||||
|
if (li) {
|
||||||
|
const index = Number(li.dataset.index);
|
||||||
|
if (!isNaN(index)) {
|
||||||
|
setSelected(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose} variant='ontime'>
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent maxWidth='40vw'>
|
||||||
|
<ModalBody onKeyDown={navigate}>
|
||||||
|
<Input size='lg' onChange={debouncedFind} variant='ontime-filled' placeholder='Search...' />
|
||||||
|
<ul className={style.scrollContainer} onMouseMove={handleMouseMoveEvent}>
|
||||||
|
{error && <li className={style.error}>{error}</li>}
|
||||||
|
{results.length === 0 && <li className={style.empty}>No results</li>}
|
||||||
|
{results.length > 0 &&
|
||||||
|
results.map((event, index) => {
|
||||||
|
const isSelected = selected === index;
|
||||||
|
const displayIndex = event.type === SupportedEvent.Block ? '-' : event.index;
|
||||||
|
const colour = event.type === SupportedEvent.Event ? event.colour : '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={event.id}
|
||||||
|
className={style.entry}
|
||||||
|
data-selected={isSelected}
|
||||||
|
data-index={index}
|
||||||
|
onClick={submit}
|
||||||
|
>
|
||||||
|
<div className={style.data}>
|
||||||
|
<div className={style.index} style={{ '--color': colour }}>
|
||||||
|
{displayIndex}
|
||||||
|
</div>
|
||||||
|
{isOntimeEvent(event) && <div className={style.cue}>{event.cue}</div>}
|
||||||
|
<div className={style.title}>{event.title}</div>
|
||||||
|
</div>
|
||||||
|
{isSelected && <span>Go ⏎</span>}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter className={style.footer}>
|
||||||
|
Use the keywords <span className={style.em}>cue</span>, <span className={style.em}>index</span> or
|
||||||
|
<span className={style.em}>title</span> to filter search
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
import { ChangeEvent, useState } from 'react';
|
||||||
|
import { isOntimeBlock, isOntimeEvent, MaybeString, SupportedEvent } from 'ontime-types';
|
||||||
|
|
||||||
|
import { useFlatRundown } from '../../../common/hooks-query/useRundown';
|
||||||
|
|
||||||
|
const maxResults = 12;
|
||||||
|
|
||||||
|
type FilterableBlock = {
|
||||||
|
type: SupportedEvent.Block;
|
||||||
|
id: string;
|
||||||
|
index: number;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FilterableEvent = {
|
||||||
|
type: SupportedEvent.Event;
|
||||||
|
id: string;
|
||||||
|
index: number;
|
||||||
|
eventIndex: number;
|
||||||
|
title: string;
|
||||||
|
cue: string;
|
||||||
|
colour: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FilterableEntry = FilterableBlock | FilterableEvent;
|
||||||
|
|
||||||
|
export default function useFinder() {
|
||||||
|
const { data } = useFlatRundown();
|
||||||
|
const [results, setResults] = useState<FilterableEntry[]>([]);
|
||||||
|
const [error, setError] = useState<MaybeString>(null);
|
||||||
|
|
||||||
|
/** Returns a single item with a matching index */
|
||||||
|
const searchByIndex = (searchString: string) => {
|
||||||
|
const searchIndex = Number(searchString);
|
||||||
|
if (isNaN(searchIndex) || searchIndex < 1) {
|
||||||
|
return { results: [], error: 'Invalid index' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchIndex > data.length) {
|
||||||
|
return { results: [], error: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
// indexes exposed to the UI are 1-based
|
||||||
|
let eventIndex = 1;
|
||||||
|
const results: FilterableEvent[] = [];
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
const event = data[i];
|
||||||
|
if (isOntimeEvent(event)) {
|
||||||
|
if (eventIndex === searchIndex) {
|
||||||
|
results.push({
|
||||||
|
type: SupportedEvent.Event,
|
||||||
|
id: event.id,
|
||||||
|
index: i,
|
||||||
|
eventIndex,
|
||||||
|
title: event.title,
|
||||||
|
cue: event.cue,
|
||||||
|
colour: event.colour,
|
||||||
|
} satisfies FilterableEvent);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
eventIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { results, error: null };
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Returns maxResults of OntimeEvents that match the cue field */
|
||||||
|
const searchByCue = (searchString: string) => {
|
||||||
|
// indexes exposed to the UI are 1-based
|
||||||
|
let eventIndex = 1;
|
||||||
|
// limit amount of results we show
|
||||||
|
let remaining = maxResults;
|
||||||
|
const results: FilterableEvent[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
if (remaining <= 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const event = data[i];
|
||||||
|
if (isOntimeEvent(event)) {
|
||||||
|
if (event.cue.toLowerCase().includes(searchString)) {
|
||||||
|
remaining--;
|
||||||
|
results.push({
|
||||||
|
type: SupportedEvent.Event,
|
||||||
|
id: event.id,
|
||||||
|
index: i,
|
||||||
|
eventIndex,
|
||||||
|
title: event.title,
|
||||||
|
cue: event.cue,
|
||||||
|
colour: event.colour,
|
||||||
|
} satisfies FilterableEvent);
|
||||||
|
}
|
||||||
|
eventIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { results, error: null };
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Returns maxResults of OntimeEvents that match the title field*/
|
||||||
|
const searchByTitle = (searchString: string) => {
|
||||||
|
// indexes exposed to the UI are 1-based
|
||||||
|
let eventIndex = 1;
|
||||||
|
// limit amount of results we show
|
||||||
|
let remaining = maxResults;
|
||||||
|
const results: FilterableEntry[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
if (remaining <= 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = data[i];
|
||||||
|
if (isOntimeEvent(event)) {
|
||||||
|
if (event.title.toLowerCase().includes(searchString)) {
|
||||||
|
remaining--;
|
||||||
|
results.push({
|
||||||
|
type: SupportedEvent.Event,
|
||||||
|
id: event.id,
|
||||||
|
index: i,
|
||||||
|
eventIndex,
|
||||||
|
title: event.title,
|
||||||
|
cue: event.cue,
|
||||||
|
colour: event.colour,
|
||||||
|
} satisfies FilterableEvent);
|
||||||
|
}
|
||||||
|
eventIndex++;
|
||||||
|
}
|
||||||
|
if (isOntimeBlock(event)) {
|
||||||
|
if (event.title.toLowerCase().includes(searchString)) {
|
||||||
|
remaining--;
|
||||||
|
results.push({
|
||||||
|
type: SupportedEvent.Block,
|
||||||
|
id: event.id,
|
||||||
|
index: i,
|
||||||
|
title: event.title,
|
||||||
|
} satisfies FilterableBlock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { results, error: null };
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Filters the rundown to a given evaluation */
|
||||||
|
const find = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
setError('No data');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
if (event.target.value === '') {
|
||||||
|
setResults([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchValue = event.target.value.toLowerCase();
|
||||||
|
|
||||||
|
if (searchValue.startsWith('index ')) {
|
||||||
|
const searchString = searchValue.replace('index ', '').trim();
|
||||||
|
const { results, error } = searchByIndex(searchString);
|
||||||
|
setResults(results);
|
||||||
|
setError(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchValue.startsWith('cue ')) {
|
||||||
|
const searchString = searchValue.replace('cue ', '').trim();
|
||||||
|
const { results, error } = searchByCue(searchString);
|
||||||
|
setResults(results);
|
||||||
|
setError(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchString = searchValue.replace('title ', '').trim();
|
||||||
|
const { results, error } = searchByTitle(searchString);
|
||||||
|
setResults(results);
|
||||||
|
setError(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { find, results, error };
|
||||||
|
}
|
||||||
@@ -14,6 +14,23 @@ function EventEditorEmpty() {
|
|||||||
<div className={style.prompt}>Rundown shortcuts:</div>
|
<div className={style.prompt}>Rundown shortcuts:</div>
|
||||||
<table className={style.shortcuts}>
|
<table className={style.shortcuts}>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Find in rundown</td>
|
||||||
|
<td>
|
||||||
|
<Kbd>{deviceMod}</Kbd>
|
||||||
|
<AuxKey>+</AuxKey>
|
||||||
|
<Kbd>F</Kbd>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Open Settings</td>
|
||||||
|
<td>
|
||||||
|
<Kbd>{deviceMod}</Kbd>
|
||||||
|
<AuxKey>+</AuxKey>
|
||||||
|
<Kbd>,</Kbd>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr className={style.spacer} />
|
||||||
<tr>
|
<tr>
|
||||||
<td>Select entry</td>
|
<td>Select entry</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const ontimeModal = {
|
|||||||
minHeight: 'min(200px, 10vh)',
|
minHeight: 'min(200px, 10vh)',
|
||||||
backgroundColor: '#202020', // $gray-1250
|
backgroundColor: '#202020', // $gray-1250
|
||||||
color: '#fefefe', // $gray-50
|
color: '#fefefe', // $gray-50
|
||||||
|
border: '1px solid #2d2d2d', // $gray-1100
|
||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
padding: '1rem',
|
padding: '1rem',
|
||||||
|
|||||||
Generated
+5
-29
@@ -4,30 +4,6 @@ settings:
|
|||||||
autoInstallPeers: true
|
autoInstallPeers: true
|
||||||
excludeLinksFromLockfile: false
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
catalogs:
|
|
||||||
default:
|
|
||||||
'@types/node':
|
|
||||||
specifier: 20.14.10
|
|
||||||
version: 20.14.10
|
|
||||||
'@typescript-eslint/eslint-plugin':
|
|
||||||
specifier: 7.16.1
|
|
||||||
version: 7.16.1
|
|
||||||
'@typescript-eslint/parser':
|
|
||||||
specifier: 7.16.1
|
|
||||||
version: 7.16.1
|
|
||||||
eslint:
|
|
||||||
specifier: 8.56.0
|
|
||||||
version: 8.56.0
|
|
||||||
eslint-plugin-prettier:
|
|
||||||
specifier: 5.1.3
|
|
||||||
version: 5.1.3
|
|
||||||
prettier:
|
|
||||||
specifier: 3.3.1
|
|
||||||
version: 3.3.1
|
|
||||||
typescript:
|
|
||||||
specifier: 5.5.3
|
|
||||||
version: 5.5.3
|
|
||||||
|
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
@@ -102,8 +78,8 @@ importers:
|
|||||||
specifier: ^5.0.28
|
specifier: ^5.0.28
|
||||||
version: 5.0.28
|
version: 5.0.28
|
||||||
'@mantine/hooks':
|
'@mantine/hooks':
|
||||||
specifier: ^7.6.2
|
specifier: ^7.13.3
|
||||||
version: 7.6.2(react@18.3.1)
|
version: 7.13.3(react@18.3.1)
|
||||||
'@react-icons/all-files':
|
'@react-icons/all-files':
|
||||||
specifier: ^4.1.0
|
specifier: ^4.1.0
|
||||||
version: 4.1.0(react@18.3.1)
|
version: 4.1.0(react@18.3.1)
|
||||||
@@ -1673,8 +1649,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==}
|
resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==}
|
||||||
engines: {node: '>= 10.0.0'}
|
engines: {node: '>= 10.0.0'}
|
||||||
|
|
||||||
'@mantine/hooks@7.6.2':
|
'@mantine/hooks@7.13.3':
|
||||||
resolution: {integrity: sha512-ZrOgrZHoIGCDKrr2/9njDgK0al+jjusYQFlmR0YyEFyRtgY6eNSI4zuYLcAPx1haHmUm5RsLBrqY6Iy/TLdGXA==}
|
resolution: {integrity: sha512-r2c+Z8CdvPKFeOwg6mSJmxOp9K/ave5ZFR7eJbgv4wQU8K1CAS5f5ven9K5uUX8Vf9B5dFnSaSgYp9UY3vOWTw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.2.0
|
react: ^18.2.0
|
||||||
|
|
||||||
@@ -6796,7 +6772,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@mantine/hooks@7.6.2(react@18.3.1)':
|
'@mantine/hooks@7.13.3(react@18.3.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user