diff --git a/ui/src/app/store.ts b/ui/src/app/store.ts index d932606578d5e14540d4977953c4341f89a30f0b..3f7ae146c8ef9e198d03464fae625393f43f4356 100644 --- a/ui/src/app/store.ts +++ b/ui/src/app/store.ts @@ -23,6 +23,7 @@ import { dummyPropsFormApi } from '@/services/dummyPropsForm'; import { pageDataFormApi } from '@/services/pageDataForm'; import { configurationSlice } from '@/features/configuration/configurationSlice'; import { sectionApi } from '@/services/sections'; +import { extensionsSlice } from '@/features/extensions/extensionsSlice'; import { extensionsApi } from '@/services/extensions'; import { codeComponentApi } from '@/services/codeComponents'; import { formStateSlice } from '@/features/form/formStateSlice'; @@ -102,6 +103,7 @@ const rootReducer = combineSlices( codeComponentDialogSlice, uiSlice, formStateSlice, + extensionsSlice, pendingChangesApi, publishReviewSlice, contentListApi, diff --git a/ui/src/components/Dialog.tsx b/ui/src/components/Dialog.tsx index e23ed599510b1cc2d477b715cdee8b3914620f21..6e6e394a00ac614a1fcda59d097d32f6c11f1201 100644 --- a/ui/src/components/Dialog.tsx +++ b/ui/src/components/Dialog.tsx @@ -1,14 +1,16 @@ -import { Button, Dialog as RadixDialog, Flex, Text } from '@radix-ui/themes'; +import { Button, Dialog as ThemedDialog, Flex, Text } from '@radix-ui/themes'; import ErrorCard from '@/components/error/ErrorCard'; import styles from './Dialog.module.css'; -import type { ReactNode } from 'react'; +import DraggableDialogWrapper from '@/components/DraggableDialogWrapper'; +import type React from 'react'; export interface DialogProps { open: boolean; onOpenChange: (open: boolean) => void; title: string; - description?: ReactNode; - children?: ReactNode; + modal?: boolean; + description?: React.ReactNode; + children?: React.ReactNode; error?: { title: string; message: string; @@ -25,6 +27,33 @@ export interface DialogProps { }; } +const DialogWrap = ({ open, handleOpenChange, children, description }: any) => ( + <ThemedDialog.Root open={open} onOpenChange={handleOpenChange}> + <ThemedDialog.Content + width="287px" + className={styles.dialogContent} + {...(!description && { 'aria-describedby': undefined })} + > + {children} + </ThemedDialog.Content> + </ThemedDialog.Root> +); + +const DraggableDialogWrap = ({ + handleOpenChange, + open, + description, + children, +}: any) => ( + <DraggableDialogWrapper + open={open} + onOpenChange={handleOpenChange} + description={description} + > + {children} + </DraggableDialogWrapper> +); + const Dialog = ({ open, onOpenChange, @@ -32,6 +61,7 @@ const Dialog = ({ description, children, error, + modal = true, footer = { cancelText: 'Cancel', confirmText: 'Confirm', @@ -41,66 +71,76 @@ const Dialog = ({ onOpenChange(isOpen); }; + const Wrapper = modal ? DialogWrap : DraggableDialogWrap; + return ( - <RadixDialog.Root open={open} onOpenChange={handleOpenChange}> - <RadixDialog.Content - width="287px" - className={styles.dialogContent} - // aria-describedby={undefined} is needed when there is no description. - // @see https://www.radix-ui.com/primitives/docs/components/dialog#description - {...(!description && { 'aria-describedby': undefined })} - > - <RadixDialog.Title className={styles.title}> - <Text size="1" weight="bold"> - {title} - </Text> - </RadixDialog.Title> + <Wrapper + open={open} + handleOpenChange={handleOpenChange} + description={description} + > + <ThemedDialog.Title className={styles.title}> + <Text size="1" weight="bold"> + {title} + </Text> + </ThemedDialog.Title> - {description && ( - <RadixDialog.Description size="2" mb="4"> - {description} - </RadixDialog.Description> - )} + {description && ( + <ThemedDialog.Description size="2" mb="4"> + {description} + </ThemedDialog.Description> + )} - <Flex direction="column" gap="2"> - <Flex direction="column" gap="1"> - {children} - </Flex> + <Flex direction="column" gap="2"> + <Flex direction="column" gap="1"> + {children} + </Flex> - {error && ( - <ErrorCard - title={error.title} - error={error.message} - resetButtonText={error.resetButtonText} - resetErrorBoundary={error.onReset} - /> - )} + {error && ( + <ErrorCard + title={error.title} + error={error.message} + resetButtonText={error.resetButtonText} + resetErrorBoundary={error.onReset} + /> + )} - <Flex gap="2" justify="end"> - <RadixDialog.Close> - <Button variant="outline" size="1"> - {footer.cancelText} - </Button> - </RadixDialog.Close> - {footer.onConfirm && ( - <Button - onClick={footer.onConfirm} - disabled={footer.isConfirmDisabled} - loading={footer.isConfirmLoading} - size="1" - color={footer.isDanger ? 'red' : 'blue'} - > - {footer.confirmText} - </Button> - )} - </Flex> + <Flex gap="2" justify="end"> + <ThemedDialog.Close> + <Button variant="outline" size="1"> + {footer.cancelText} + </Button> + </ThemedDialog.Close> + {footer.onConfirm && ( + <Button + onClick={footer.onConfirm} + disabled={footer.isConfirmDisabled} + loading={footer.isConfirmLoading} + size="1" + color={footer.isDanger ? 'red' : 'blue'} + > + {footer.confirmText} + </Button> + )} </Flex> - </RadixDialog.Content> - </RadixDialog.Root> + </Flex> + </Wrapper> ); }; -const DialogFieldLabel = ({ children }: { children: ReactNode }) => { +// const DialogContent = ({ +// title, +// description, +// children, +// error, +// footer, +// }: any) => ( +// <> +// +// </> +// ); + +const DialogFieldLabel = ({ children }: { children: React.ReactNode }) => { return ( <Text as="label" size="1" weight="bold" className={styles.fieldLabel}> {children} diff --git a/ui/src/components/DraggableDialogWrapper.module.css b/ui/src/components/DraggableDialogWrapper.module.css new file mode 100644 index 0000000000000000000000000000000000000000..496fa85d338331f828421b25fb6725951508fa57 --- /dev/null +++ b/ui/src/components/DraggableDialogWrapper.module.css @@ -0,0 +1,30 @@ +.DialogContent { + position: fixed; + top: 0; + left: 0; + width: 90vw; + max-width: 500px; + max-height: 85vh; + padding: 25px; + animation: content-show 250ms cubic-bezier(0.16, 1, 0.3, 1); + border-radius: 6px; + background-color: var(--gray-1); + box-shadow: var(--shadow-6); +} +.DialogContent:focus { + outline: none; +} +.DraggableArea { + position: absolute; + width: 100%; + height: var(--space-7); +} + +@keyframes content-show { + from { + opacity: 0; + } + to { + opacity: 1; + } +} diff --git a/ui/src/components/DraggableDialogWrapper.tsx b/ui/src/components/DraggableDialogWrapper.tsx new file mode 100644 index 0000000000000000000000000000000000000000..064b881202cbbc27e97127d657d2f4d2ff7898bd --- /dev/null +++ b/ui/src/components/DraggableDialogWrapper.tsx @@ -0,0 +1,135 @@ +import { Dialog } from 'radix-ui'; +import type { ReactNode } from 'react'; +import React, { useCallback, useRef, useState, useMemo } from 'react'; + +import clsx from 'clsx'; +import styles from './DraggableDialogWrapper.module.css'; +import { Box, Theme } from '@radix-ui/themes'; +import Panel from '@/components/Panel'; + +interface DraggableDialogWrapperProps { + onOpenChange: Function; + open: boolean; + description: ReactNode; + children: ReactNode; +} + +const PANEL_PADDING = '4'; + +const DraggableDialogWrapper: React.FC<DraggableDialogWrapperProps> = ({ + onOpenChange, + open, + description, + children, +}) => { + const dialogWidth = 500; + const windowWidth = window.visualViewport?.width || 100; + const windowHeight = window.visualViewport?.height || 100; + const [isDragging, setIsDragging] = useState(false); + const initialPosition = useMemo(() => { + return { + x: windowWidth / 2 - dialogWidth / 2, + y: 200, + }; + }, [windowWidth, dialogWidth]); + const [position, setPosition] = useState(initialPosition); + const dialogRef = useRef<HTMLDivElement | null>(null); + + const handleOpenChange = useCallback( + (open: boolean) => { + onOpenChange(open); + setPosition(initialPosition); + }, + [initialPosition, onOpenChange], + ); + const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => { + setIsDragging(true); + }; + + const handleMouseMove = useCallback( + (e: MouseEvent) => { + if (isDragging && dialogRef.current) { + // Ensure the dialog cannot be dragged so far off the edge that it can't be dragged back on again. + const innerBound = 40; + const minX = 0 - dialogWidth + innerBound; + const maxX = windowWidth - innerBound; + const minY = 0 - innerBound / 2; + const maxY = windowHeight - innerBound; + setPosition((prevPosition) => { + const newX = prevPosition.x + e.movementX; + const newY = prevPosition.y + e.movementY; + + return { + x: Math.max(minX, Math.min(newX, maxX)), + y: Math.max(minY, Math.min(newY, maxY)), + }; + }); + } + }, + [isDragging, windowHeight, windowWidth], + ); + + const handleMouseUp = () => { + setIsDragging(false); + }; + + React.useEffect(() => { + if (isDragging) { + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + } else { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + } + + return () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; + }, [handleMouseMove, isDragging]); + + return ( + <Dialog.Root modal={false} open={open} onOpenChange={handleOpenChange}> + <Dialog.Portal> + <Theme> + <Dialog.Content + className={clsx(styles.DialogContent)} + asChild + style={{ + transform: `translate(${position.x}px, ${position.y}px)`, + position: 'absolute', + }} + onPointerDownOutside={(event) => { + event.preventDefault(); + }} + onInteractOutside={(event) => { + event.preventDefault(); + }} + ref={dialogRef} + // aria-describedby={undefined} is needed when there is no description. + // @see https://www.radix-ui.com/primitives/docs/components/dialog#description + {...(!description && { 'aria-describedby': undefined })} + > + <Panel p={PANEL_PADDING}> + <Box + mt={`-${PANEL_PADDING}`} + pt={PANEL_PADDING} + mx={`-${PANEL_PADDING}`} + px={PANEL_PADDING} + className={styles.DraggableArea} + onMouseDown={handleMouseDown} + style={{ + cursor: 'move', + }} + /> + + {children} + </Panel> + </Dialog.Content> + </Theme> + </Dialog.Portal> + </Dialog.Root> + ); +}; + +export default DraggableDialogWrapper; diff --git a/ui/src/components/extensions/ExtensionButton.tsx b/ui/src/components/extensions/ExtensionButton.tsx new file mode 100644 index 0000000000000000000000000000000000000000..06f1b2deb890946c3dac60491f8de7345198c5f0 --- /dev/null +++ b/ui/src/components/extensions/ExtensionButton.tsx @@ -0,0 +1,42 @@ +import clsx from 'clsx'; +import { Flex, Text, Tooltip } from '@radix-ui/themes'; +import styles from './ExtensionsList.module.css'; +import type React from 'react'; +import { useCallback } from 'react'; +import type { ExtensionDefinition } from '@/types/Extensions'; +import { useAppDispatch } from '@/app/hooks'; +import { setDialogOpen } from '@/features/ui/dialogSlice'; +import { setActiveExtension } from '@/features/extensions/extensionsSlice'; + +const ExtensionButton: React.FC<ExtensionsPopoverProps> = ({ extension }) => { + const { name, imgSrc, description } = extension; + const dispatch = useAppDispatch(); + + const handleClick = useCallback( + (e: React.MouseEvent<HTMLButtonElement>) => { + e.preventDefault(); + dispatch(setDialogOpen('extension')); + dispatch(setActiveExtension(extension)); + }, + [dispatch, extension], + ); + + return ( + <Tooltip content={description}> + <Flex justify="start" align="center" direction="column" asChild> + <button className={clsx(styles.extensionIcon)} onClick={handleClick}> + <img alt={name} src={imgSrc} height="42" width="42" /> + <Text align="center" size="1"> + {name} + </Text> + </button> + </Flex> + </Tooltip> + ); +}; + +interface ExtensionsPopoverProps { + extension: ExtensionDefinition; +} + +export default ExtensionButton; diff --git a/ui/src/components/extensions/ExtensionDialog.module.css b/ui/src/components/extensions/ExtensionDialog.module.css new file mode 100644 index 0000000000000000000000000000000000000000..4f4f9a0d6076adf6ae252c51dd42ff34bb84ef06 --- /dev/null +++ b/ui/src/components/extensions/ExtensionDialog.module.css @@ -0,0 +1,25 @@ +.DialogContent { + position: fixed; + top: 0; + left: 0; + width: 90vw; + max-width: 500px; + max-height: 85vh; + padding: 25px; + animation: content-show 250ms cubic-bezier(0.16, 1, 0.3, 1); + border-radius: 6px; + background-color: var(--gray-1); + box-shadow: var(--shadow-6); +} +.DialogContent:focus { + outline: none; +} + +@keyframes content-show { + from { + opacity: 0; + } + to { + opacity: 1; + } +} diff --git a/ui/src/components/extensions/ExtensionDialog.tsx b/ui/src/components/extensions/ExtensionDialog.tsx new file mode 100644 index 0000000000000000000000000000000000000000..cb9cb4a27d01bca1d4632b2d37f01856f352e5c4 --- /dev/null +++ b/ui/src/components/extensions/ExtensionDialog.tsx @@ -0,0 +1,59 @@ +import Dialog from '@/components/Dialog'; +import type React from 'react'; +import { useCallback } from 'react'; + +import { Text } from '@radix-ui/themes'; +import { useAppDispatch, useAppSelector } from '@/app/hooks'; +import { + selectDialogOpen, + setDialogClosed, + setDialogOpen, +} from '@/features/ui/dialogSlice'; +import { + selectActiveExtension, + unsetActiveExtension, +} from '@/features/extensions/extensionsSlice'; + +interface ExtensionDialogProps {} + +const ExtensionDialog: React.FC<ExtensionDialogProps> = () => { + const { extension } = useAppSelector(selectDialogOpen); + const activeExtension = useAppSelector(selectActiveExtension); + const dispatch = useAppDispatch(); + + const handleOpenChange = useCallback( + (open: boolean) => { + if (open) { + dispatch(setDialogOpen('extension')); + } else { + dispatch(setDialogClosed('extension')); + dispatch(unsetActiveExtension()); + } + }, + [dispatch], + ); + if (!extension || activeExtension === null) { + return null; + } + + return ( + <Dialog + open={extension} + onOpenChange={handleOpenChange} + title={activeExtension.name} + modal={false} + footer={{ cancelText: 'Close' }} + description={activeExtension.description} + > + {/* @todo https://www.drupal.org/i/3485692 - render the proof of concept into this div */} + <div + id="extensionPortalContainer" + className={`xb-extension-${activeExtension.id}`} + > + <Text as="p">Not yet supported</Text> + </div> + </Dialog> + ); +}; + +export default ExtensionDialog; diff --git a/ui/src/components/extensionsPopover/ExtensionsList.module.css b/ui/src/components/extensions/ExtensionsList.module.css similarity index 100% rename from ui/src/components/extensionsPopover/ExtensionsList.module.css rename to ui/src/components/extensions/ExtensionsList.module.css diff --git a/ui/src/components/extensionsPopover/ExtensionsList.tsx b/ui/src/components/extensions/ExtensionsList.tsx similarity index 96% rename from ui/src/components/extensionsPopover/ExtensionsList.tsx rename to ui/src/components/extensions/ExtensionsList.tsx index 1dcc7c6fa7f5b79723e4f91835142f4eaa5ea38f..ccd2b51971699dfd0c875a2bb84b0ef6d1cb8367 100644 --- a/ui/src/components/extensionsPopover/ExtensionsList.tsx +++ b/ui/src/components/extensions/ExtensionsList.tsx @@ -1,6 +1,6 @@ import { Flex, Heading, Link, Grid, Spinner } from '@radix-ui/themes'; import { ExternalLinkIcon } from '@radix-ui/react-icons'; -import ExtensionButton from '@/components/extensionsPopover/ExtensionButton'; +import ExtensionButton from '@/components/extensions/ExtensionButton'; import { handleNonWorkingBtn } from '@/utils/function-utils'; import type React from 'react'; import { useGetExtensionsQuery } from '@/services/extensions'; diff --git a/ui/src/components/extensionsPopover/ExtensionsListDisplay.stories.tsx b/ui/src/components/extensions/ExtensionsListDisplay.stories.tsx similarity index 96% rename from ui/src/components/extensionsPopover/ExtensionsListDisplay.stories.tsx rename to ui/src/components/extensions/ExtensionsListDisplay.stories.tsx index 7ce64b4060fb39d60821ddc8c089757ef4018922..45b4f8b9a9f6319883d96b98e4818c68e000c81f 100644 --- a/ui/src/components/extensionsPopover/ExtensionsListDisplay.stories.tsx +++ b/ui/src/components/extensions/ExtensionsListDisplay.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from '@storybook/react'; -import { ExtensionsListDisplay } from '@/components/extensionsPopover/ExtensionsList'; +import { ExtensionsListDisplay } from '@/components/extensions/ExtensionsList'; const kittenBase64 = /* cspell:disable-next-line */ diff --git a/ui/src/components/extensionsPopover/ExtensionButton.tsx b/ui/src/components/extensionsPopover/ExtensionButton.tsx deleted file mode 100644 index f639da7f415109bcf3f41eeeff7888d58ce98761..0000000000000000000000000000000000000000 --- a/ui/src/components/extensionsPopover/ExtensionButton.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import clsx from 'clsx'; -import { Flex, Text } from '@radix-ui/themes'; -import styles from './ExtensionsList.module.css'; -import type React from 'react'; -import { useCallback } from 'react'; -import { handleNonWorkingBtn } from '@/utils/function-utils'; -import type { Extension } from '@/types/Extensions'; - -interface ExtensionsPopoverProps { - extension: Extension; -} - -const ExtensionButton: React.FC<ExtensionsPopoverProps> = ({ extension }) => { - const { name, imgSrc } = extension; - const handleClick = useCallback((e: React.MouseEvent<HTMLButtonElement>) => { - e.preventDefault(); - handleNonWorkingBtn(); - }, []); - - return ( - <Flex justify="start" align="center" direction="column" asChild> - <button className={clsx(styles.extensionIcon)} onClick={handleClick}> - <img alt={name} src={imgSrc} height="42" width="42" /> - <Text align="center" size="1"> - {name} - </Text> - </button> - </Flex> - ); -}; - -export default ExtensionButton; diff --git a/ui/src/components/topbar/Topbar.tsx b/ui/src/components/topbar/Topbar.tsx index 72cb59bdc0ef53cb51432fc6e9565bb8083b6318..783d601decf1acd14fa38a6f590e6829e3998b55 100644 --- a/ui/src/components/topbar/Topbar.tsx +++ b/ui/src/components/topbar/Topbar.tsx @@ -18,7 +18,7 @@ import { useLocation, useNavigate } from 'react-router-dom'; import clsx from 'clsx'; import UnpublishedChanges from '@/components/review/UnpublishedChanges'; import PageInfo from '../pageInfo/PageInfo'; -import ExtensionsList from '@/components/extensionsPopover/ExtensionsList'; +import ExtensionsList from '@/components/extensions/ExtensionsList'; import type React from 'react'; import { handleNonWorkingBtn } from '@/utils/function-utils'; import TopbarPopover from '@/components/topbar/menu/TopbarPopover'; diff --git a/ui/src/features/editor/Editor.tsx b/ui/src/features/editor/Editor.tsx index 3e7afb6e4e02e4a84eb592aa4ae57c536ea9977c..54e7cbaeec36220760cb5c4ffa74b94a0c9d8be7 100644 --- a/ui/src/features/editor/Editor.tsx +++ b/ui/src/features/editor/Editor.tsx @@ -7,6 +7,7 @@ import ContextualPanel from '@/components/panel/ContextualPanel'; import { useEffect } from 'react'; import { setFirstLoadComplete } from '@/features/ui/uiSlice'; import { useAppDispatch } from '@/app/hooks'; +import ExtensionDialog from '@/components/extensions/ExtensionDialog'; const Editor = () => { const dispatch = useAppDispatch(); @@ -26,6 +27,7 @@ const Editor = () => { <div id="menuBarSubmenuContainer"></div> <SaveSectionDialog /> <CodeComponentDialogs /> + <ExtensionDialog /> </> ); }; diff --git a/ui/src/features/extensions/extensionsSlice.ts b/ui/src/features/extensions/extensionsSlice.ts new file mode 100644 index 0000000000000000000000000000000000000000..ffac25fe44e1a111027a2c7ecd4d001b049fa5ff --- /dev/null +++ b/ui/src/features/extensions/extensionsSlice.ts @@ -0,0 +1,39 @@ +import { createAppSlice } from '@/app/createAppSlice'; +import type { PayloadAction } from '@reduxjs/toolkit'; +import type { ExtensionDefinition, ActiveExtension } from '@/types/Extensions'; + +export interface ExtensionsSliceState { + activeExtension: ActiveExtension; +} + +const initialState: ExtensionsSliceState = { + activeExtension: null, +}; + +type UpdateExtensionsPayload = ExtensionDefinition; + +export const extensionsSlice = createAppSlice({ + name: 'extensions', + initialState, + reducers: (create) => ({ + setActiveExtension: create.reducer( + (state, action: PayloadAction<UpdateExtensionsPayload>) => { + state.activeExtension = action.payload; + }, + ), + unsetActiveExtension: create.reducer((state) => { + state.activeExtension = null; + }), + }), + selectors: { + selectActiveExtension: (state): ActiveExtension => { + return state.activeExtension; + }, + }, +}); + +export const extensionsReducer = extensionsSlice.reducer; + +export const { setActiveExtension, unsetActiveExtension } = + extensionsSlice.actions; +export const { selectActiveExtension } = extensionsSlice.selectors; diff --git a/ui/src/features/ui/dialogSlice.ts b/ui/src/features/ui/dialogSlice.ts index 1b89609e215630ecef95133c96fb3e8a26c13944..80dc1dc0fbab35e4168e3719af373247255f1d83 100644 --- a/ui/src/features/ui/dialogSlice.ts +++ b/ui/src/features/ui/dialogSlice.ts @@ -3,13 +3,15 @@ import type { PayloadAction } from '@reduxjs/toolkit'; export interface DialogSliceState { saveAsSection: boolean; + extension: boolean; } const initialState: DialogSliceState = { saveAsSection: false, + extension: false, }; -type UpdateDialogPayload = 'saveAsSection'; // only one dialog so far +type UpdateDialogPayload = keyof DialogSliceState; export const dialogSlice = createAppSlice({ name: 'dialog', diff --git a/ui/src/services/extensions.ts b/ui/src/services/extensions.ts index 1ac50e3cb2a65eb97b7884e82caec58c904a6bc2..064db720ee6db8f2cce7f01c1fc4ccf4bcbc719a 100644 --- a/ui/src/services/extensions.ts +++ b/ui/src/services/extensions.ts @@ -10,26 +10,31 @@ const kittenBase64 = const dummyExtensionsList = [ { name: 'Extension 1', + description: 'a description of extension 1', imgSrc: kittenBase64, id: 'extension1', }, { name: 'Extension with longer name 2', + description: 'a description of extension 2', imgSrc: kittenBase64, id: 'extension2', }, { name: 'Extension 3', + description: 'a description of extension 3', imgSrc: kittenBase64, id: 'extension3', }, { name: 'Extension 4', + description: 'a description of extension 4', imgSrc: kittenBase64, id: 'extension4', }, { name: 'Extension name 5', + description: 'a description of extension 5', imgSrc: kittenBase64, id: 'extension5', }, diff --git a/ui/src/types/Extensions.ts b/ui/src/types/Extensions.ts index 940b59bd4b6dba3e2fe6b3407e11496465d6e73a..0e04cc61f23d7b312df3e145e04e4a1946a8aa4a 100644 --- a/ui/src/types/Extensions.ts +++ b/ui/src/types/Extensions.ts @@ -1,7 +1,9 @@ -export interface Extension { +export interface ExtensionDefinition { name: string; - imgSrc: string; id: string; + description: string; + imgSrc: string; } -export type ExtensionsList = Extension[]; +export type ExtensionsList = ExtensionDefinition[]; +export type ActiveExtension = ExtensionDefinition | null;