Merged PR 203: Improve responsive design and refactor layout

This commit is contained in:
Eric Nguyen 2022-10-03 12:05:16 +00:00
parent 50626218ba
commit 0d05f0959c
27 changed files with 968 additions and 485 deletions

View file

@ -1,18 +1,31 @@
import * as React from 'react'; import * as React from 'react';
import { ClockIcon, CubeIcon, LinkIcon, EnvelopeIcon } from '@heroicons/react/24/outline'; import {
import { ClockIcon as ClockIconS, CubeIcon as CubeIconS, LinkIcon as LinkIconS, EnvelopeIcon as EnvolopeIconS } from '@heroicons/react/24/solid'; ClockIcon,
CubeIcon,
LinkIcon,
EnvelopeIcon,
Cog8ToothIcon
} from '@heroicons/react/24/outline';
import {
ClockIcon as ClockIconS,
CubeIcon as CubeIconS,
LinkIcon as LinkIconS,
EnvelopeIcon as EnvolopeIconS,
Cog8ToothIcon as Cog8ToothIconS
} from '@heroicons/react/24/solid';
import { BarIcon } from './BarIcon'; import { BarIcon } from './BarIcon';
interface IBarProps { interface IBarProps {
isSidebarOpen: boolean isComponentsOpen: boolean
isSymbolsOpen: boolean isSymbolsOpen: boolean
isElementsSidebarOpen: boolean
isHistoryOpen: boolean isHistoryOpen: boolean
isMessagesOpen: boolean isMessagesOpen: boolean
toggleSidebar: () => void isSettingsOpen: boolean
toggleComponents: () => void
toggleSymbols: () => void toggleSymbols: () => void
toggleTimeline: () => void toggleTimeline: () => void
toggleMessages: () => void toggleMessages: () => void
toggleSettings: () => void
} }
export const BAR_WIDTH = 64; // 4rem export const BAR_WIDTH = 64; // 4rem
@ -21,11 +34,11 @@ export function Bar(props: IBarProps): JSX.Element {
return ( return (
<div className='bar'> <div className='bar'>
<BarIcon <BarIcon
isActive={props.isSidebarOpen} isActive={props.isComponentsOpen}
title='Components' title='Components'
onClick={() => props.toggleSidebar()}> onClick={() => props.toggleComponents()}>
{ {
props.isSidebarOpen props.isComponentsOpen
? <CubeIconS className='heroicon' /> ? <CubeIconS className='heroicon' />
: <CubeIcon className='heroicon' /> : <CubeIcon className='heroicon' />
} }
@ -40,6 +53,17 @@ export function Bar(props: IBarProps): JSX.Element {
: <LinkIcon className='heroicon' /> : <LinkIcon className='heroicon' />
} }
</BarIcon> </BarIcon>
<BarIcon
isActive={props.isMessagesOpen}
title='Messages'
onClick={() => props.toggleMessages()}>
{
props.isMessagesOpen
? <EnvolopeIconS className='heroicon' />
: <EnvelopeIcon className='heroicon' />
}
</BarIcon>
<div className='grow'></div>
<BarIcon <BarIcon
isActive={props.isHistoryOpen} isActive={props.isHistoryOpen}
title='Timeline' title='Timeline'
@ -51,13 +75,13 @@ export function Bar(props: IBarProps): JSX.Element {
} }
</BarIcon> </BarIcon>
<BarIcon <BarIcon
isActive={props.isMessagesOpen} isActive={props.isSettingsOpen}
title='Messages' title='Settings'
onClick={() => props.toggleMessages()}> onClick={() => props.toggleSettings()}>
{ {
props.isMessagesOpen props.isMessagesOpen
? <EnvolopeIconS className='heroicon' /> ? <Cog8ToothIconS className='heroicon' />
: <EnvelopeIcon className='heroicon' /> : <Cog8ToothIcon className='heroicon' />
} }
</BarIcon> </BarIcon>
</div> </div>

View file

@ -50,7 +50,7 @@ export function CheckboxGroupButtons(props: ICheckboxGroupButtonsProps): JSX.Ele
const newSelectedValues = SetChecked(Number(event.target.value), event.target.checked); const newSelectedValues = SetChecked(Number(event.target.value), event.target.checked);
props.onChange(newSelectedValues); props.onChange(newSelectedValues);
}} /> }} />
<label htmlFor={inputGroup.key} className='text-gray-400 peer-checked:text-blue-500'> <label htmlFor={inputGroup.key} className='text-gray-400 peer-checked:text-blue-500 group'>
{inputGroup.text} {inputGroup.text}
</label> </label>
</div> </div>

View file

@ -0,0 +1,119 @@
import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline';
import * as React from 'react';
import { IAvailableContainer } from '../../Interfaces/IAvailableContainer';
import { ICategory } from '../../Interfaces/ICategory';
import { IContainerModel } from '../../Interfaces/IContainerModel';
import { TruncateString } from '../../utils/stringtools';
import { Category } from '../Category/Category';
interface IComponentsProps {
selectedContainer: IContainerModel | undefined
componentOptions: IAvailableContainer[]
categories: ICategory[]
buttonOnClick: (type: string) => void
}
function HandleDragStart(event: React.DragEvent<HTMLButtonElement>): void {
event.dataTransfer.setData('type', (event.target as HTMLButtonElement).id);
}
interface SidebarCategory {
category: ICategory
children: JSX.Element[]
}
export function Components(props: IComponentsProps): JSX.Element {
const [hideDisabled, setHideDisabled] = React.useState<boolean>(false);
const disabledTitle = hideDisabled ? 'Show disabled components' : 'Hide disabled components';
const rootElements: Array<JSX.Element | undefined> = [];
const categories = new Map<string, SidebarCategory>(props.categories.map(category => [
category.Type,
{
category,
children: []
}
]));
// build the categories (sorted with categories first)
categories.forEach((categoryWrapper, categoryName) => {
rootElements.push(
<Category
key={categoryName}
category={categoryWrapper.category}
>
{ categoryWrapper.children }
</Category>);
});
const selectedContainer = props.selectedContainer;
const config = props.componentOptions.find(option => option.Type === selectedContainer?.properties.type);
// build the components
props.componentOptions.forEach(componentOption => {
if (componentOption.IsHidden === true) {
return;
}
let disabled = false;
if (config?.Whitelist !== undefined) {
disabled = config.Whitelist?.find(type => type === componentOption.Type) === undefined;
} else if (config?.Blacklist !== undefined) {
disabled = config.Blacklist?.find(type => type === componentOption.Type) !== undefined ?? false;
}
if (disabled && hideDisabled) {
return;
}
const componentButton = (<button
key={componentOption.Type}
type="button"
className='w-full justify-center h-16 transition-all sidebar-component'
id={componentOption.Type}
title={componentOption.Type}
onClick={() => props.buttonOnClick(componentOption.Type)}
draggable={true}
onDragStart={(event) => HandleDragStart(event)}
disabled={disabled}
>
{TruncateString(componentOption.DisplayedText ?? componentOption.Type, 25)}
</button>);
if (componentOption.Category === null || componentOption.Category === undefined) {
rootElements.push(componentButton);
return;
}
const category = categories.get(componentOption.Category);
if (category === undefined) {
console.error(`[Category] Category does not exists in configuration.Categories: ${componentOption.Category}`);
return;
}
category.children.push(componentButton);
});
return (
<div className='h-full'>
<div className='hover:bg-slate-300 h-7 text-right pr-1 pl-1'>
<button
onClick={() => { setHideDisabled(!hideDisabled); }}
className='h-full hover:bg-slate-400 rounded-lg p-1'
aria-label={disabledTitle}
title={disabledTitle}
>
{
hideDisabled
? <EyeSlashIcon className='heroicon'></EyeSlashIcon>
: <EyeIcon className='heroicon'></EyeIcon>
}
</button>
</div>
<div className='h-[calc(100%_-_1.75rem)] overflow-y-auto'>
<div className='transition-all grid grid-cols-1 md:grid-cols-1 gap-2
m-2 md:text-xs font-bold'>
{rootElements}
</div>
</div>
</div>
);
};

View file

@ -39,7 +39,7 @@ function GetCSSInputs(properties: IContainerProperties,
export function ContainerForm(props: IContainerFormProps): JSX.Element { export function ContainerForm(props: IContainerFormProps): JSX.Element {
return ( return (
<div className='grid grid-cols-2 gap-y-6 items-center'> <div className='grid grid-cols-1 md:grid-cols-2 gap-y-6 items-center'>
<InputGroup <InputGroup
labelText='Name' labelText='Name'
inputKey='id' inputKey='id'

View file

@ -16,7 +16,7 @@ export function Properties(props: IPropertiesProps): JSX.Element {
} }
return ( return (
<div className='h-3/5 p-3 bg-slate-200 overflow-y-auto'> <div className='h-full p-3 bg-slate-200 overflow-y-auto'>
<ContainerForm <ContainerForm
properties={props.properties} properties={props.properties}
symbols={props.symbols} symbols={props.symbols}

View file

@ -1,7 +1,6 @@
import React, { Dispatch, SetStateAction, useEffect, useRef } from 'react'; import React, { Dispatch, SetStateAction, useEffect, useRef } from 'react';
import './Editor.scss'; import './Editor.scss';
import { IConfiguration } from '../../Interfaces/IConfiguration'; import { IConfiguration } from '../../Interfaces/IConfiguration';
import { SVG } from '../SVG/SVG';
import { IHistoryState } from '../../Interfaces/IHistoryState'; import { IHistoryState } from '../../Interfaces/IHistoryState';
import { UI } from '../UI/UI'; import { UI } from '../UI/UI';
import { SelectContainer, DeleteContainer, OnPropertyChange } from './Actions/ContainerOperations'; import { SelectContainer, DeleteContainer, OnPropertyChange } from './Actions/ContainerOperations';
@ -234,54 +233,6 @@ export function Editor(props: IEditorProps): JSX.Element {
const current = GetCurrentHistoryState(history, historyCurrentStep); const current = GetCurrentHistoryState(history, historyCurrentStep);
const selected = FindContainerById(current.mainContainer, current.selectedContainerId); const selected = FindContainerById(current.mainContainer, current.selectedContainerId);
function Draw(ctx: CanvasRenderingContext2D, frameCount: number, scale: number, translatePos: IPoint): void {
const topDim = current.mainContainer.properties.y;
const leftDim = current.mainContainer.properties.x;
const rightDim = current.mainContainer.properties.x + current.mainContainer.properties.width;
const bottomDim = current.mainContainer.properties.y + current.mainContainer.properties.height;
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.save();
ctx.translate(translatePos.x, translatePos.y);
ctx.scale(scale, scale);
ctx.fillStyle = '#000000';
const it = MakeRecursionDFSIterator(current.mainContainer, 0, [0, 0]);
for (const { container, depth, currentTransform } of it) {
const [x, y] = [
container.properties.x + currentTransform[0],
container.properties.y + currentTransform[1]
];
// Draw container
ctx.strokeStyle = container.properties.style?.stroke ?? '#000000';
ctx.fillStyle = container.properties.style?.fill ?? '#000000';
ctx.lineWidth = Number(container.properties.style?.strokeWidth ?? 1);
ctx.globalAlpha = Number(container.properties.style?.fillOpacity ?? 1);
ctx.fillRect(x, y, container.properties.width, container.properties.height);
ctx.globalAlpha = Number(container.properties.style?.strokeOpacity ?? 1);
ctx.strokeRect(x, y, container.properties.width, container.properties.height);
ctx.globalAlpha = 1;
ctx.lineWidth = 1;
ctx.fillStyle = '#000000';
ctx.strokeStyle = '#000000';
// Draw dimensions
const containerLeftDim = leftDim - (DIMENSION_MARGIN * (depth + 1)) / scale;
const containerTopDim = topDim - (DIMENSION_MARGIN * (depth + 1)) / scale;
const containerBottomDim = bottomDim + (DIMENSION_MARGIN * (depth + 1)) / scale;
const containerRightDim = rightDim + (DIMENSION_MARGIN * (depth + 1)) / scale;
const dimMapped = [containerLeftDim, containerBottomDim, containerTopDim, containerRightDim];
AddDimensions(ctx, container, dimMapped, currentTransform, scale, depth);
// Draw selector
RenderSelector(ctx, frameCount, {
scale,
selected
});
}
ctx.restore();
}
return ( return (
<div ref={editorRef} className="Editor font-sans h-full"> <div ref={editorRef} className="Editor font-sans h-full">
<UI <UI
@ -366,24 +317,6 @@ export function Editor(props: IEditorProps): JSX.Element {
saveEditorAsSVG={() => SaveEditorAsSVG()} saveEditorAsSVG={() => SaveEditorAsSVG()}
loadState={(move) => setHistoryCurrentStep(move)} loadState={(move) => setHistoryCurrentStep(move)}
/> />
{
USE_EXPERIMENTAL_CANVAS_API
? <Canvas
draw={Draw}
className='ml-16'
width={window.innerWidth - BAR_WIDTH}
height={window.innerHeight}
/>
: <SVG
width={current.mainContainer?.properties.width}
height={current.mainContainer?.properties.height}
selected={selected}
symbols={current.symbols}
>
{current.mainContainer}
</SVG>
}
<Menu <Menu
getListener={() => editorRef.current} getListener={() => editorRef.current}
actions={menuActions} actions={menuActions}

View file

@ -1,7 +1,7 @@
import { describe, expect, it, vi } from 'vitest'; import { describe, expect, it, vi } from 'vitest';
import * as React from 'react'; import * as React from 'react';
import { fireEvent, render, screen } from '../../utils/test-utils'; import { fireEvent, render, screen } from '../../utils/test-utils';
import { ElementsSidebar } from './ElementsSidebar'; import { ElementsList } from './ElementsList';
import { IContainerModel } from '../../Interfaces/IContainerModel'; import { IContainerModel } from '../../Interfaces/IContainerModel';
import { PositionReference } from '../../Enums/PositionReference'; import { PositionReference } from '../../Enums/PositionReference';
import { FindContainerById } from '../../utils/itertools'; import { FindContainerById } from '../../utils/itertools';
@ -10,7 +10,7 @@ import { Orientation } from '../../Enums/Orientation';
describe.concurrent('Elements sidebar', () => { describe.concurrent('Elements sidebar', () => {
it('With a MainContainer', () => { it('With a MainContainer', () => {
render(<ElementsSidebar render(<ElementsList
symbols={new Map()} symbols={new Map()}
mainContainer={{ mainContainer={{
children: [], children: [],
@ -18,8 +18,6 @@ describe.concurrent('Elements sidebar', () => {
properties: DEFAULT_MAINCONTAINER_PROPS, properties: DEFAULT_MAINCONTAINER_PROPS,
userData: {} userData: {}
}} }}
isOpen={true}
isHistoryOpen={false}
selectedContainer={undefined} selectedContainer={undefined}
onPropertyChange={() => {}} onPropertyChange={() => {}}
selectContainer={() => {}} selectContainer={() => {}}
@ -39,11 +37,9 @@ describe.concurrent('Elements sidebar', () => {
userData: {} userData: {}
}; };
const { container } = render(<ElementsSidebar const { container } = render(<ElementsList
symbols={new Map()} symbols={new Map()}
mainContainer={mainContainer} mainContainer={mainContainer}
isOpen={true}
isHistoryOpen={false}
selectedContainer={mainContainer} selectedContainer={mainContainer}
onPropertyChange={() => {}} onPropertyChange={() => {}}
selectContainer={() => {}} selectContainer={() => {}}
@ -154,11 +150,9 @@ describe.concurrent('Elements sidebar', () => {
} }
); );
render(<ElementsSidebar render(<ElementsList
symbols={new Map()} symbols={new Map()}
mainContainer={mainContainer} mainContainer={mainContainer}
isOpen={true}
isHistoryOpen={false}
selectedContainer={mainContainer} selectedContainer={mainContainer}
onPropertyChange={() => {}} onPropertyChange={() => {}}
selectContainer={() => {}} selectContainer={() => {}}
@ -219,11 +213,9 @@ describe.concurrent('Elements sidebar', () => {
selectedContainer = FindContainerById(mainContainer, containerId); selectedContainer = FindContainerById(mainContainer, containerId);
}); });
const { container, rerender } = render(<ElementsSidebar const { container, rerender } = render(<ElementsList
symbols={new Map()} symbols={new Map()}
mainContainer={mainContainer} mainContainer={mainContainer}
isOpen={true}
isHistoryOpen={false}
selectedContainer={selectedContainer} selectedContainer={selectedContainer}
onPropertyChange={() => {}} onPropertyChange={() => {}}
selectContainer={selectContainer} selectContainer={selectContainer}
@ -242,11 +234,9 @@ describe.concurrent('Elements sidebar', () => {
fireEvent.click(child1); fireEvent.click(child1);
rerender(<ElementsSidebar rerender(<ElementsList
symbols={new Map()} symbols={new Map()}
mainContainer={mainContainer} mainContainer={mainContainer}
isOpen={true}
isHistoryOpen={false}
selectedContainer={selectedContainer} selectedContainer={selectedContainer}
onPropertyChange={() => {}} onPropertyChange={() => {}}
selectContainer={selectContainer} selectContainer={selectContainer}

View file

@ -7,11 +7,9 @@ import { ISymbolModel } from '../../Interfaces/ISymbolModel';
import { PropertyType } from '../../Enums/PropertyType'; import { PropertyType } from '../../Enums/PropertyType';
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'; import { ExclamationTriangleIcon } from '@heroicons/react/24/outline';
interface IElementsSidebarProps { interface IElementsListProps {
mainContainer: IContainerModel mainContainer: IContainerModel
symbols: Map<string, ISymbolModel> symbols: Map<string, ISymbolModel>
isOpen: boolean
isHistoryOpen: boolean
selectedContainer: IContainerModel | undefined selectedContainer: IContainerModel | undefined
onPropertyChange: ( onPropertyChange: (
key: string, key: string,
@ -117,13 +115,8 @@ function HandleOnDrop(
} }
} }
export function ElementsSidebar(props: IElementsSidebarProps): JSX.Element { export function ElementsList(props: IElementsListProps): JSX.Element {
// Render // Render
let isOpenClasses = '-right-64';
if (props.isOpen) {
isOpenClasses = props.isHistoryOpen ? 'right-64' : 'right-0';
}
const it = MakeRecursionDFSIterator(props.mainContainer, 0, [0, 0], true); const it = MakeRecursionDFSIterator(props.mainContainer, 0, [0, 0], true);
const containers = [...it]; const containers = [...it];
function Row({ function Row({
@ -138,16 +131,19 @@ export function ElementsSidebar(props: IElementsSidebarProps): JSX.Element {
const text = container.properties.displayedText === key const text = container.properties.displayedText === key
? `${key}` ? `${key}`
: `${container.properties.displayedText}`; : `${container.properties.displayedText}`;
const selectedClass: string = props.selectedContainer !== undefined &&
const isSelected = props.selectedContainer !== undefined &&
props.selectedContainer !== null && props.selectedContainer !== null &&
props.selectedContainer.properties.id === container.properties.id props.selectedContainer.properties.id === container.properties.id;
? 'border-l-4 bg-slate-400/60 hover:bg-slate-400'
: 'bg-slate-300/60 hover:bg-slate-300'; const selectedClass: string = isSelected
? 'border-l-4 bg-blue-500 shadow-lg shadow-blue-500/60 hover:bg-blue-600 hover:shadow-blue-500 text-slate-50'
: 'bg-slate-300/60 hover:bg-slate-400 hover:shadow-slate-400';
return ( return (
<button type="button" <button type="button"
className={`w-full border-blue-500 elements-sidebar-row whitespace-pre className={`transition-all w-full border-blue-500 hover:shadow-lg elements-sidebar-row whitespace-pre
text-left text-sm font-medium transition-all inline-flex ${container.properties.type} ${selectedClass}`} text-left text-sm font-medium inline-flex ${container.properties.type} ${selectedClass}`}
id={key} id={key}
key={key} key={key}
style={style} style={style}
@ -167,25 +163,25 @@ export function ElementsSidebar(props: IElementsSidebarProps): JSX.Element {
} }
return ( return (
<div <div className='h-full flex flex-col'>
className={`fixed flex flex-col bg-slate-100 text-gray-800 transition-all h-full w-64 overflow-y-auto z-20 ${isOpenClasses}`} <div className='h-48'>
>
<div className="bg-slate-100 font-bold sidebar-title">Elements</div>
<div className="h-96 text-gray-800">
<List <List
className="List divide-y divide-black" className="List divide-y divide-black overflow-y-auto"
itemCount={containers.length} itemCount={containers.length}
itemSize={35} itemSize={35}
height={384} height={192}
width={256} width={'100%'}
> >
{Row} {Row}
</List> </List>
</div> </div>
<Properties <div className='grow overflow-auto'>
properties={props.selectedContainer?.properties} <Properties
symbols={props.symbols} properties={props.selectedContainer?.properties}
onChange={props.onPropertyChange} /> symbols={props.symbols}
onChange={props.onPropertyChange}
/>
</div>
</div> </div>
); );
} }

View file

@ -0,0 +1,183 @@
import * as React from 'react';
import { FixedSizeList as List } from 'react-window';
import { Properties } from '../ContainerProperties/ContainerProperties';
import { IContainerModel } from '../../Interfaces/IContainerModel';
import { FindContainerById, MakeRecursionDFSIterator } from '../../utils/itertools';
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
import { PropertyType } from '../../Enums/PropertyType';
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline';
interface IElementsProps {
mainContainer: IContainerModel
symbols: Map<string, ISymbolModel>
selectedContainer: IContainerModel | undefined
onPropertyChange: (
key: string,
value: string | number | boolean | number[],
type?: PropertyType
) => void
selectContainer: (containerId: string) => void
addContainer: (index: number, type: string, parent: string) => void
}
function RemoveBorderClasses(target: HTMLButtonElement, exception: string = ''): void {
const bordersClasses = ['border-t-8', 'border-8', 'border-b-8'].filter(className => className !== exception);
target.classList.remove(...bordersClasses);
}
function HandleDragLeave(event: React.DragEvent): void {
const target: HTMLButtonElement = event.target as HTMLButtonElement;
RemoveBorderClasses(target);
}
function HandleDragOver(
event: React.DragEvent,
mainContainer: IContainerModel
): void {
event.preventDefault();
const target: HTMLButtonElement = event.target as HTMLButtonElement;
const rect = target.getBoundingClientRect();
const y = event.clientY - rect.top; // y position within the element.
if (target.id === mainContainer.properties.id) {
target.classList.add('border-8');
return;
}
if (y < 12) {
RemoveBorderClasses(target, 'border-t-8');
target.classList.add('border-t-8');
} else if (y < 24) {
RemoveBorderClasses(target, 'border-8');
target.classList.add('border-8');
} else {
RemoveBorderClasses(target, 'border-b-8');
target.classList.add('border-b-8');
}
}
function HandleOnDrop(
event: React.DragEvent,
mainContainer: IContainerModel,
addContainer: (index: number, type: string, parent: string) => void
): void {
event.preventDefault();
const type = event.dataTransfer.getData('type');
const target: HTMLButtonElement = event.target as HTMLButtonElement;
RemoveBorderClasses(target);
const targetContainer: IContainerModel | undefined = FindContainerById(
mainContainer,
target.id
);
if (targetContainer === undefined) {
throw new Error('[handleOnDrop] Tried to drop onto a unknown container!');
}
if (targetContainer === mainContainer) {
// if the container is the root, only add type as child
addContainer(
targetContainer.children.length,
type,
targetContainer.properties.id);
return;
}
if (targetContainer.parent === null ||
targetContainer.parent === undefined) {
throw new Error('[handleDrop] Tried to drop into a child container without a parent!');
}
const rect = target.getBoundingClientRect();
const y = event.clientY - rect.top; // y position within the element.
// locate the hitboxes
if (y < 12) {
const index = targetContainer.parent.children.indexOf(targetContainer);
addContainer(
index,
type,
targetContainer.parent.properties.id
);
} else if (y < 24) {
addContainer(
targetContainer.children.length,
type,
targetContainer.properties.id);
} else {
const index = targetContainer.parent.children.indexOf(targetContainer);
addContainer(
index + 1,
type,
targetContainer.parent.properties.id
);
}
}
export function Elements(props: IElementsProps): JSX.Element {
// Render
const it = MakeRecursionDFSIterator(props.mainContainer, 0, [0, 0], true);
const containers = [...it];
function Row({
index, style
}: {
index: number
style: React.CSSProperties
}): JSX.Element {
const { container, depth } = containers[index];
const key = container.properties.id.toString();
const tabs = '|\t'.repeat(depth);
const text = container.properties.displayedText === key
? `${key}`
: `${container.properties.displayedText}`;
const isSelected = props.selectedContainer !== undefined &&
props.selectedContainer !== null &&
props.selectedContainer.properties.id === container.properties.id;
const selectedClass: string = isSelected
? 'border-l-4 bg-blue-500 shadow-lg shadow-blue-500/60 hover:bg-blue-600 hover:shadow-blue-500 text-slate-50'
: 'bg-slate-300/60 hover:bg-slate-400 hover:shadow-slate-400';
return (
<button type="button"
className={`transition-all w-full border-blue-500 hover:shadow-lg elements-sidebar-row whitespace-pre
text-left text-sm font-medium inline-flex ${container.properties.type} ${selectedClass}`}
id={key}
key={key}
style={style}
title={container.properties.warning}
onClick={() => props.selectContainer(container.properties.id)}
onDrop={(event) => HandleOnDrop(event, props.mainContainer, props.addContainer)}
onDragOver={(event) => HandleDragOver(event, props.mainContainer)}
onDragLeave={(event) => HandleDragLeave(event)}
>
{tabs}
{text}
{container.properties.warning.length > 0 &&
<ExclamationTriangleIcon className='w-8'/>
}
</button>
);
}
return (
<>
<List
className="List divide-y divide-black overflow-y-auto"
itemCount={containers.length}
itemSize={35}
height={192}
width={'100%'}
>
{Row}
</List>
<Properties
properties={props.selectedContainer?.properties}
symbols={props.symbols}
onChange={props.onPropertyChange}
/>
</>
);
}

View file

@ -0,0 +1,23 @@
import * as React from 'react';
import { CameraIcon, ArrowUpOnSquareIcon } from '@heroicons/react/24/outline';
import { FloatingButton } from './FloatingButton';
import { IUIProps } from '../UI/UI';
export function MenuButton(props: IUIProps): JSX.Element {
return <FloatingButton className={'fixed z-10 flex flex-col gap-2 items-center bottom-12 right-12'}>
<button type="button"
className={'transition-all w-10 h-10 p-2 align-middle items-center justify-center rounded-full bg-blue-500 hover:bg-blue-800'}
title='Export as JSON'
onClick={props.saveEditorAsJSON}
>
<ArrowUpOnSquareIcon className="heroicon text-white" />
</button>
<button type="button"
className={'transition-all w-10 h-10 p-2 align-middle items-center justify-center rounded-full bg-blue-500 hover:bg-blue-800'}
title='Export as SVG'
onClick={props.saveEditorAsSVG}
>
<CameraIcon className="heroicon text-white" />
</button>
</FloatingButton>;
}

View file

@ -1,24 +1,23 @@
import * as React from 'react'; import * as React from 'react';
import { FixedSizeList as List } from 'react-window'; import { FixedSizeList as List } from 'react-window';
import { IHistoryState } from '../../Interfaces/IHistoryState'; import { IHistoryState } from '../../Interfaces/IHistoryState';
import { TITLE_BAR_HEIGHT } from '../Sidebar/Sidebar';
interface IHistoryProps { interface IHistoryProps {
history: IHistoryState[] history: IHistoryState[]
historyCurrentStep: number historyCurrentStep: number
isOpen: boolean
jumpTo: (move: number) => void jumpTo: (move: number) => void
} }
export function History(props: IHistoryProps): JSX.Element { export function History(props: IHistoryProps): JSX.Element {
const isOpenClasses = props.isOpen ? 'right-0' : '-right-64';
function Row({ index, style }: { index: number, style: React.CSSProperties }): JSX.Element { function Row({ index, style }: { index: number, style: React.CSSProperties }): JSX.Element {
const reversedIndex = (props.history.length - 1) - index; const reversedIndex = (props.history.length - 1) - index;
const step = props.history[reversedIndex]; const step = props.history[reversedIndex];
const desc = step.lastAction; const desc = step.lastAction;
const selectedClass = reversedIndex === props.historyCurrentStep const selectedClass: string = reversedIndex === props.historyCurrentStep
? 'bg-blue-500 hover:bg-blue-600' ? 'border-l-4 bg-blue-500 shadow-lg shadow-blue-500/60 hover:bg-blue-600 hover:shadow-blue-500 text-slate-50'
: 'bg-slate-500 hover:bg-slate-700'; : 'bg-slate-300/60 hover:bg-slate-400 hover:shadow-slate-400';
return ( return (
<button type="button" <button type="button"
@ -26,7 +25,7 @@ export function History(props: IHistoryProps): JSX.Element {
style={style} style={style}
onClick={() => props.jumpTo(reversedIndex)} onClick={() => props.jumpTo(reversedIndex)}
title={step.lastAction} title={step.lastAction}
className={`w-full elements-sidebar-row whitespace-pre overflow-hidden className={`w-full elements-sidebar-row border-blue-500 whitespace-pre overflow-hidden
text-left text-sm font-medium transition-all ${selectedClass}`} text-left text-sm font-medium transition-all ${selectedClass}`}
> >
{desc} {desc}
@ -35,19 +34,14 @@ export function History(props: IHistoryProps): JSX.Element {
} }
return ( return (
<div className={`fixed flex flex-col bg-slate-300 text-white transition-all h-full w-64 overflow-y-auto z-20 ${isOpenClasses}`}> <List
<div className='bg-slate-600 font-bold sidebar-title'> className='List overflow-x-hidden'
Timeline itemCount={props.history.length}
</div> itemSize={35}
<List height={window.innerHeight - TITLE_BAR_HEIGHT}
className='List overflow-x-hidden' width={'100%'}
itemCount={props.history.length} >
itemSize={35} {Row}
height={window.innerHeight} </List>
width={256}
>
{Row}
</List>
</div>
); );
} }

View file

@ -28,7 +28,13 @@ export function MainMenu(props: IMainMenuProps): JSX.Element {
switch (windowState) { switch (windowState) {
case WindowState.Load: case WindowState.Load:
return ( return (
<div className='flex flex-col drop-shadow-lg bg-blue-50 p-12 rounded-lg absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'> <div
className='flex flex-col drop-shadow-lg
bg-blue-50 p-12 rounded-lg absolute
h-full sm:h-auto
w-full sm:w-auto
sm:top-1/2 sm:left-1/2 sm:-translate-x-1/2 sm:-translate-y-1/2'
>
<form className="flex items-center space-x-6"> <form className="flex items-center space-x-6">
<label className="block"> <label className="block">
<span className="sr-only">Import save</span> <span className="sr-only">Import save</span>
@ -67,7 +73,12 @@ export function MainMenu(props: IMainMenuProps): JSX.Element {
); );
default: default:
return ( return (
<div className='absolute bg-blue-50 p-12 rounded-lg drop-shadow-lg grid grid-cols-1 md:grid-cols-2 gap-8 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'> <div className='absolute bg-blue-50 p-12
rounded-lg drop-shadow-lg
grid grid-cols-1 md:grid-cols-2 gap-8
h-full sm:h-auto
w-full sm:w-auto
sm:top-1/2 sm:left-1/2 sm:-translate-x-1/2 sm:-translate-y-1/2'>
<button type="button" className='mainmenu-btn' onClick={() => { <button type="button" className='mainmenu-btn' onClick={() => {
setWindowState(WindowState.Loading); setWindowState(WindowState.Loading);
props.newEditor(); props.newEditor();

View file

@ -8,10 +8,10 @@ import { IHistoryState } from '../../Interfaces/IHistoryState';
import { IMessage } from '../../Interfaces/IMessage'; import { IMessage } from '../../Interfaces/IMessage';
import { DISABLE_API } from '../../utils/default'; import { DISABLE_API } from '../../utils/default';
import { GetCircularReplacerKeepDataStructure } from '../../utils/saveload'; import { GetCircularReplacerKeepDataStructure } from '../../utils/saveload';
import { TITLE_BAR_HEIGHT } from '../Sidebar/Sidebar';
interface IMessagesSidebarProps { interface IMessagesProps {
historyState: IHistoryState historyState: IHistoryState
isOpen: boolean
} }
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
@ -45,6 +45,7 @@ function UseAsync(
): void { ): void {
React.useEffect(() => { React.useEffect(() => {
const request: IGetFeedbackRequest = { const request: IGetFeedbackRequest = {
// eslint-disable-next-line @typescript-eslint/naming-convention
ApplicationState: state ApplicationState: state
}; };
const dataParsed = JSON.stringify(request, GetCircularReplacerKeepDataStructure()); const dataParsed = JSON.stringify(request, GetCircularReplacerKeepDataStructure());
@ -65,7 +66,7 @@ function UseAsync(
}, [state]); }, [state]);
} }
export function MessagesSidebar(props: IMessagesSidebarProps): JSX.Element { export function Messages(props: IMessagesProps): JSX.Element {
const [messages, setMessages] = React.useState<IMessage[]>([]); const [messages, setMessages] = React.useState<IMessage[]>([]);
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
@ -107,16 +108,14 @@ export function MessagesSidebar(props: IMessagesSidebarProps): JSX.Element {
</p>); </p>);
} }
const isOpenClasses = props.isOpen ? 'left-16' : '-left-64'; const toolbarHeight = 28;
return ( return (
<div className={`fixed z-10 bg-slate-200 <div>
text-gray-700 transition-all h-full w-64 <div className='hover:bg-slate-300 h-7 text-right pr-1 pl-1'>
${isOpenClasses}`}>
<div className='bg-slate-100 sidebar-title flex place-content-between'>
Messages
<button <button
onClick={() => { setMessages([]); }} onClick={() => { setMessages([]); }}
className='h-6' className='h-full hover:bg-slate-400 rounded-lg p-1'
aria-label='Clear all messages' aria-label='Clear all messages'
title='Clear all messages' title='Clear all messages'
> >
@ -127,8 +126,8 @@ export function MessagesSidebar(props: IMessagesSidebarProps): JSX.Element {
className='List md:text-xs font-bold' className='List md:text-xs font-bold'
itemCount={messages.length} itemCount={messages.length}
itemSize={65} itemSize={65}
height={window.innerHeight} height={window.innerHeight - TITLE_BAR_HEIGHT - toolbarHeight}
width={256} width={'100%'}
> >
{Row} {Row}
</List> </List>

View file

@ -0,0 +1,131 @@
import { TrashIcon } from '@heroicons/react/24/outline';
import * as React from 'react';
import { FixedSizeList as List } from 'react-window';
import { MessageType } from '../../Enums/MessageType';
import { IGetFeedbackRequest } from '../../Interfaces/IGetFeedbackRequest';
import { IGetFeedbackResponse } from '../../Interfaces/IGetFeedbackResponse';
import { IHistoryState } from '../../Interfaces/IHistoryState';
import { IMessage } from '../../Interfaces/IMessage';
import { DISABLE_API } from '../../utils/default';
import { GetCircularReplacerKeepDataStructure } from '../../utils/saveload';
import { TITLE_BAR_HEIGHT } from '../Sidebar/Sidebar';
interface IMessagesSidebarProps {
historyState: IHistoryState
}
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
const myWorker = window.Worker && new Worker('workers/message_worker.js');
function UseWorker(
state: IHistoryState,
setMessages: React.Dispatch<React.SetStateAction<IMessage[]>>
): void {
React.useEffect(() => {
// use webworker for the stringify to avoid freezing
myWorker.postMessage({
state,
url: import.meta.env.VITE_API_GET_FEEDBACK_URL
});
return () => {
};
}, [state]);
React.useEffect(() => {
myWorker.onmessage = (event) => {
setMessages(event.data as IMessage[]);
};
}, [setMessages]);
}
function UseAsync(
state: IHistoryState,
setMessages: React.Dispatch<React.SetStateAction<IMessage[]>>
): void {
React.useEffect(() => {
const request: IGetFeedbackRequest = {
// eslint-disable-next-line @typescript-eslint/naming-convention
ApplicationState: state
};
const dataParsed = JSON.stringify(request, GetCircularReplacerKeepDataStructure());
fetch(import.meta.env.VITE_API_GET_FEEDBACK_URL, {
method: 'POST',
headers: new Headers({
// eslint-disable-next-line @typescript-eslint/naming-convention
'Content-Type': 'application/json'
}),
body: dataParsed
})
.then(async(response) =>
await response.json()
)
.then(async(json: IGetFeedbackResponse) => {
setMessages(json.messages);
});
}, [state]);
}
export function MessagesSidebar(props: IMessagesSidebarProps): JSX.Element {
const [messages, setMessages] = React.useState<IMessage[]>([]);
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (window.Worker && !DISABLE_API) {
UseWorker(
props.historyState,
setMessages
);
} else if (!DISABLE_API) {
UseAsync(
props.historyState,
setMessages
);
}
function Row({ index, style }: {index: number, style: React.CSSProperties}): JSX.Element {
const reversedIndex = (messages.length - 1) - index;
const message = messages[reversedIndex];
let classType = '';
switch (message.type) {
case MessageType.Success:
classType = 'bg-green-400 hover:bg-green-400/60';
break;
case MessageType.Warning:
classType = 'bg-yellow-400 hover:bg-yellow-400/60';
break;
case MessageType.Error:
classType = 'bg-red-400 hover:bg-red-400/60';
break;
}
return (<p
key={`m-${reversedIndex}`}
className={`p-2 ${classType}`}
style={style}
>
{message.text}
</p>);
}
return (
// <button
// onClick={() => { setMessages([]); }}
// className='h-6'
// aria-label='Clear all messages'
// title='Clear all messages'
// >
// <TrashIcon className='heroicon'></TrashIcon>
// </button>
// </div>
<List
className='List md:text-xs font-bold'
itemCount={messages.length}
itemSize={65}
height={window.innerHeight - TITLE_BAR_HEIGHT}
width={'100%'}
>
{Row}
</List>
);
};

View file

@ -9,6 +9,7 @@ interface IContainerProps {
model: IContainerModel model: IContainerModel
depth: number depth: number
scale: number scale: number
selectContainer: (containerId: string) => void
} }
/** /**
@ -22,6 +23,7 @@ export function Container(props: IContainerProps): JSX.Element {
model={child} model={child}
depth={props.depth + 1} depth={props.depth + 1}
scale={props.scale} scale={props.scale}
selectContainer={props.selectContainer}
/>); />);
const width: number = props.model.properties.width; const width: number = props.model.properties.width;
@ -54,6 +56,7 @@ export function Container(props: IContainerProps): JSX.Element {
width={width} width={width}
height={height} height={height}
style={style} style={style}
onClick={() => props.selectContainer(props.model.properties.id)}
> >
</rect>); </rect>);

View file

@ -1,12 +0,0 @@
text {
font-family: 'Open Sans', 'Helvetica Neue', sans-serif;
font-size: 18px;
//font-weight: 800;
//fill: #fff;
fill-opacity: 1;
//stroke: #ffffff;
stroke-width: 1px;
stroke-linecap: butt;
stroke-linejoin: miter;
stroke-opacity: 1;
}

View file

@ -1,10 +1,8 @@
import './SVG.scss';
import * as React from 'react'; import * as React from 'react';
import { ReactSVGPanZoom, Tool, TOOL_PAN, Value } from 'react-svg-pan-zoom'; import { ReactSVGPanZoom, Tool, TOOL_PAN, Value } from 'react-svg-pan-zoom';
import { Container } from './Elements/Container'; import { Container } from './Elements/Container';
import { ContainerModel } from '../../Interfaces/IContainerModel'; import { ContainerModel } from '../../Interfaces/IContainerModel';
import { Selector } from './Elements/Selector/Selector'; import { Selector } from './Elements/Selector/Selector';
import { BAR_WIDTH } from '../Bar/Bar';
import { DepthDimensionLayer } from './Elements/DepthDimensionLayer'; import { DepthDimensionLayer } from './Elements/DepthDimensionLayer';
import { MAX_FRAMERATE, SHOW_DIMENSIONS_PER_DEPTH } from '../../utils/default'; import { MAX_FRAMERATE, SHOW_DIMENSIONS_PER_DEPTH } from '../../utils/default';
import { SymbolLayer } from './Elements/SymbolLayer'; import { SymbolLayer } from './Elements/SymbolLayer';
@ -12,11 +10,15 @@ import { ISymbolModel } from '../../Interfaces/ISymbolModel';
import { DimensionLayer } from './Elements/DimensionLayer'; import { DimensionLayer } from './Elements/DimensionLayer';
interface ISVGProps { interface ISVGProps {
className?: string
viewerWidth: number
viewerHeight: number
width: number width: number
height: number height: number
children: ContainerModel children: ContainerModel
selected?: ContainerModel selected?: ContainerModel
symbols: Map<string, ISymbolModel> symbols: Map<string, ISymbolModel>
selectContainer: (containerId: string) => void
} }
interface Viewer { interface Viewer {
@ -26,29 +28,7 @@ interface Viewer {
export const ID = 'svg'; export const ID = 'svg';
function UseSVGAutoResizer(
setViewer: React.Dispatch<React.SetStateAction<Viewer>>
): void {
React.useEffect(() => {
function OnResize(): void {
setViewer({
viewerWidth: window.innerWidth - BAR_WIDTH,
viewerHeight: window.innerHeight
});
}
window.addEventListener('resize', OnResize);
return () => {
window.removeEventListener('resize', OnResize);
};
});
}
export function SVG(props: ISVGProps): JSX.Element { export function SVG(props: ISVGProps): JSX.Element {
const [viewer, setViewer] = React.useState<Viewer>({
viewerWidth: window.innerWidth - BAR_WIDTH,
viewerHeight: window.innerHeight
});
const [tool, setTool] = React.useState<Tool>(TOOL_PAN); const [tool, setTool] = React.useState<Tool>(TOOL_PAN);
const [value, setValue] = React.useState<Value>({} as Value); const [value, setValue] = React.useState<Value>({} as Value);
const [scale, setScale] = React.useState<number>(value.a ?? 1); const [scale, setScale] = React.useState<number>(value.a ?? 1);
@ -63,7 +43,6 @@ export function SVG(props: ISVGProps): JSX.Element {
// const startTimer = React.useRef(Date.now()); // const startTimer = React.useRef(Date.now());
// console.log(renderCounter.current / ((Date.now() - startTimer.current) / 1000)); // console.log(renderCounter.current / ((Date.now() - startTimer.current) / 1000));
UseSVGAutoResizer(setViewer);
UseFitOnce(svgViewer, props.width, props.height); UseFitOnce(svgViewer, props.width, props.height);
const xmlns = '<http://www.w3.org/2000/svg>'; const xmlns = '<http://www.w3.org/2000/svg>';
@ -79,14 +58,15 @@ export function SVG(props: ISVGProps): JSX.Element {
model={props.children} model={props.children}
depth={0} depth={0}
scale={scale} scale={scale}
selectContainer={props.selectContainer}
/>; />;
return ( return (
<div id={ID} className='ml-16'> <div id={ID} className={props.className}>
<ReactSVGPanZoom <ReactSVGPanZoom
ref={svgViewer} ref={svgViewer}
width={viewer.viewerWidth} width={props.viewerWidth}
height={viewer.viewerHeight} height={props.viewerHeight}
tool={tool} onChangeTool={setTool} tool={tool} onChangeTool={setTool}
value={value} onChangeValue={(value: Value) => { value={value} onChangeValue={(value: Value) => {
// Framerate limiter // Framerate limiter

View file

@ -0,0 +1,31 @@
import { ArrowUpOnSquareIcon, CameraIcon } from '@heroicons/react/24/outline';
import React from 'react';
interface ISettingsProps {
saveEditorAsJSON: () => void
saveEditorAsSVG: () => void
};
export function Settings(props: ISettingsProps): JSX.Element {
return (
<div className='transition-all grid grid-cols-1 overflow-auto md:grid-cols-1 gap-2
m-2 md:text-xs font-bold'>
<button type="button"
className={'w-full transition-all flex sidebar-component'}
title='Export as JSON'
onClick={props.saveEditorAsJSON}
>
<ArrowUpOnSquareIcon className="heroicon w-16 h-7" />
Export as JSON
</button>
<button type="button"
className={'w-full transition-all flex sidebar-component'}
title='Export as SVG'
onClick={props.saveEditorAsSVG}
>
<CameraIcon className="heroicon w-16 h-7" />
Export as SVG
</button>
</div>
);
}

View file

@ -1,55 +0,0 @@
import * as React from 'react';
import { describe, it, expect, vi } from 'vitest';
import { fireEvent, render, screen } from '../../utils/test-utils';
import { Sidebar } from './Sidebar';
describe.concurrent('Sidebar', () => {
it('Start default', () => {
render(
<Sidebar
selectedContainer={undefined}
componentOptions={[]}
isOpen={true}
buttonOnClick={() => { } } categories={[]}
/>
);
const stuff = screen.queryByText(/stuff/i);
expect(screen.getByText(/Components/i).classList.contains('left-0')).toBeDefined();
expect(stuff).toBeNull();
});
it('Start close', () => {
render(<Sidebar
componentOptions={[]}
isOpen={false}
buttonOnClick={() => { } } selectedContainer={undefined} categories={[]} />);
const stuff = screen.queryByText(/stuff/i);
expect(screen.getByText(/Components/i).classList.contains('-left-64')).toBeDefined();
expect(stuff).toBeNull();
});
it('With stuff', () => {
const type = 'stuff';
const handleButtonClick = vi.fn();
render(<Sidebar
componentOptions={[
{
/* eslint-disable @typescript-eslint/naming-convention */
Type: type,
Width: 30,
Height: 30,
Style: {}
/* eslint-enable */
}
]}
isOpen={true}
buttonOnClick={handleButtonClick} selectedContainer={undefined} categories={[]} />);
const stuff = screen.getByText(/stuff/i);
expect(stuff).toBeDefined();
fireEvent.click(stuff);
expect(handleButtonClick).toHaveBeenCalledTimes(1);
});
});

View file

@ -1,121 +1,25 @@
import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline';
import * as React from 'react'; import * as React from 'react';
import { IAvailableContainer } from '../../Interfaces/IAvailableContainer';
import { ICategory } from '../../Interfaces/ICategory';
import { IContainerModel } from '../../Interfaces/IContainerModel';
import { TruncateString } from '../../utils/stringtools';
import { Category } from '../Category/Category';
interface ISidebarProps { interface ISidebarProps {
selectedContainer: IContainerModel | undefined className?: string
componentOptions: IAvailableContainer[] title: string
categories: ICategory[] titleButtons?: JSX.Element | JSX.Element[]
isOpen: boolean children?: JSX.Element | JSX.Element[]
buttonOnClick: (type: string) => void
} }
function HandleDragStart(event: React.DragEvent<HTMLButtonElement>): void { export const TITLE_BAR_HEIGHT = 64;
event.dataTransfer.setData('type', (event.target as HTMLButtonElement).id);
}
interface SidebarCategory {
category: ICategory
children: JSX.Element[]
}
export function Sidebar(props: ISidebarProps): JSX.Element { export function Sidebar(props: ISidebarProps): JSX.Element {
const [hideDisabled, setHideDisabled] = React.useState<boolean>(false);
const rootElements: Array<JSX.Element | undefined> = [];
const categories = new Map<string, SidebarCategory>(props.categories.map(category => [
category.Type,
{
category,
children: []
}
]));
// build the categories (sorted with categories first)
categories.forEach((categoryWrapper, categoryName) => {
rootElements.push(
<Category
key={categoryName}
category={categoryWrapper.category}
>
{ categoryWrapper.children }
</Category>);
});
const selectedContainer = props.selectedContainer;
const config = props.componentOptions.find(option => option.Type === selectedContainer?.properties.type);
// build the components
props.componentOptions.forEach(componentOption => {
if (componentOption.IsHidden === true) {
return;
}
let disabled = false;
if (config?.Whitelist !== undefined) {
disabled = config.Whitelist?.find(type => type === componentOption.Type) === undefined;
} else if (config?.Blacklist !== undefined) {
disabled = config.Blacklist?.find(type => type === componentOption.Type) !== undefined ?? false;
}
if (disabled && hideDisabled) {
return;
}
const componentButton = (<button
key={componentOption.Type}
type="button"
className='w-full justify-center h-16 transition-all sidebar-component'
id={componentOption.Type}
title={componentOption.Type}
onClick={() => props.buttonOnClick(componentOption.Type)}
draggable={true}
onDragStart={(event) => HandleDragStart(event)}
disabled={disabled}
>
{TruncateString(componentOption.DisplayedText ?? componentOption.Type, 25)}
</button>);
if (componentOption.Category === null || componentOption.Category === undefined) {
rootElements.push(componentButton);
return;
}
const category = categories.get(componentOption.Category);
if (category === undefined) {
console.error(`[Category] Category does not exists in configuration.Categories: ${componentOption.Category}`);
return;
}
category.children.push(componentButton);
});
const isOpenClasses = props.isOpen ? 'left-16' : '-left-64';
return ( return (
<div className={`fixed z-10 bg-slate-200 <div className={`transition-all bg-slate-200
text-gray-700 transition-all h-full w-64 text-gray-700 flex flex-col
overflow-y-auto ${isOpenClasses}`}> ${props.className ?? ''}`}>
<div className='bg-slate-100 sidebar-title flex place-content-between'> <div className='bg-slate-100 sidebar-title flex place-content-between'>
Components { props.title }
<button { props.titleButtons }
onClick={() => { setHideDisabled(!hideDisabled); }}
className='h-6'
aria-label='Hide disabled component'
title='Hide disabled component'
>
{
hideDisabled
? <EyeSlashIcon className='heroicon' />
: <EyeIcon className='heroicon' />
}
</button>
</div> </div>
<div className='transition-all grid grid-cols-1 md:grid-cols-1 gap-2 <div className='overflow-y-hidden grow'>
m-2 md:text-xs font-bold'> { props.children }
{rootElements}
</div> </div>
</div> </div>
); );

View file

@ -14,7 +14,7 @@ export function SymbolProperties(props: ISymbolPropertiesProps): JSX.Element {
} }
return ( return (
<div className='h-3/5 p-3 bg-slate-200 overflow-y-auto'> <div className='h-full p-3 bg-slate-200 overflow-y-auto'>
<SymbolForm <SymbolForm
symbol={props.symbol} symbol={props.symbol}
symbols={props.symbols} symbols={props.symbols}

View file

@ -4,7 +4,6 @@ import { TruncateString } from '../../utils/stringtools';
interface ISymbolsProps { interface ISymbolsProps {
componentOptions: IAvailableSymbol[] componentOptions: IAvailableSymbol[]
isOpen: boolean
buttonOnClick: (type: string) => void buttonOnClick: (type: string) => void
} }
@ -51,16 +50,10 @@ export function Symbols(props: ISymbolsProps): JSX.Element {
</button>); </button>);
}); });
const isOpenClasses = props.isOpen ? 'left-16' : '-left-64';
return ( return (
<div className={`fixed z-10 bg-slate-200 <div className='h-full overflow-y-auto'>
text-gray-700 transition-all h-full w-64
overflow-y-auto ${isOpenClasses}`}>
<div className='bg-slate-100 sidebar-title'>
Symbols
</div>
<div className='grid grid-cols-1 md:grid-cols-3 gap-2 <div className='grid grid-cols-1 md:grid-cols-3 gap-2
m-2 md:text-xs font-bold'> overflow-auto m-2 md:text-xs font-bold'>
{listElements} {listElements}
</div> </div>
</div> </div>

View file

@ -6,21 +6,12 @@ import { SymbolProperties } from '../SymbolProperties/SymbolProperties';
interface ISymbolsSidebarProps { interface ISymbolsSidebarProps {
selectedSymbolId: string selectedSymbolId: string
symbols: Map<string, ISymbolModel> symbols: Map<string, ISymbolModel>
isOpen: boolean
isHistoryOpen: boolean
onPropertyChange: (key: string, value: string | number | boolean) => void onPropertyChange: (key: string, value: string | number | boolean) => void
selectSymbol: (symbolId: string) => void selectSymbol: (symbolId: string) => void
} }
export function SymbolsSidebar(props: ISymbolsSidebarProps): JSX.Element { export function SymbolsSidebar(props: ISymbolsSidebarProps): JSX.Element {
// Render // Render
let isOpenClasses = '-right-64';
if (props.isOpen) {
isOpenClasses = props.isHistoryOpen
? 'right-64'
: 'right-0';
}
const containers = [...props.symbols.values()]; const containers = [...props.symbols.values()];
function Row({ index, style }: { index: number, style: React.CSSProperties }): JSX.Element { function Row({ index, style }: { index: number, style: React.CSSProperties }): JSX.Element {
const container = containers[index]; const container = containers[index];
@ -46,17 +37,14 @@ export function SymbolsSidebar(props: ISymbolsSidebarProps): JSX.Element {
} }
return ( return (
<div className={`fixed flex flex-col bg-slate-100 text-gray-800 transition-all h-full w-64 overflow-y-auto z-20 ${isOpenClasses}`}> <div>
<div className='bg-slate-100 font-bold sidebar-title'> <div className='h-80 text-gray-800'>
Elements
</div>
<div className='h-96 text-gray-800'>
<List <List
className='List divide-y divide-black' className='List divide-y divide-black'
itemCount={containers.length} itemCount={containers.length}
itemSize={35} itemSize={35}
height={384} height={320}
width={256} width={'100%'}
> >
{Row} {Row}
</List> </List>

View file

@ -1,21 +1,22 @@
import * as React from 'react'; import * as React from 'react';
import { ElementsSidebar } from '../ElementsSidebar/ElementsSidebar'; import { ElementsList } from '../ElementsList/ElementsList';
import { Sidebar } from '../Sidebar/Sidebar';
import { History } from '../History/History'; import { History } from '../History/History';
import { IAvailableContainer } from '../../Interfaces/IAvailableContainer'; import { IAvailableContainer } from '../../Interfaces/IAvailableContainer';
import { IContainerModel } from '../../Interfaces/IContainerModel'; import { IContainerModel } from '../../Interfaces/IContainerModel';
import { IHistoryState } from '../../Interfaces/IHistoryState'; import { IHistoryState } from '../../Interfaces/IHistoryState';
import { CameraIcon, ArrowUpOnSquareIcon } from '@heroicons/react/24/outline';
import { FloatingButton } from '../FloatingButton/FloatingButton';
import { Bar } from '../Bar/Bar'; import { Bar } from '../Bar/Bar';
import { IAvailableSymbol } from '../../Interfaces/IAvailableSymbol'; import { IAvailableSymbol } from '../../Interfaces/IAvailableSymbol';
import { Symbols } from '../Symbols/Symbols'; import { Symbols } from '../Symbols/Symbols';
import { SymbolsSidebar } from '../SymbolsSidebar/SymbolsSidebar'; import { SymbolsSidebar } from '../SymbolsList/SymbolsList';
import { PropertyType } from '../../Enums/PropertyType'; import { PropertyType } from '../../Enums/PropertyType';
import { MessagesSidebar } from '../MessagesSidebar/MessagesSidebar'; import { Messages } from '../Messages/Messages';
import { ICategory } from '../../Interfaces/ICategory'; import { ICategory } from '../../Interfaces/ICategory';
import { Sidebar } from '../Sidebar/Sidebar';
import { Components } from '../Components/Components';
import { Viewer } from '../Viewer/Viewer';
import { Settings } from '../Settings/Settings';
interface IUIProps { export interface IUIProps {
selectedContainer: IContainerModel | undefined selectedContainer: IContainerModel | undefined
current: IHistoryState current: IHistoryState
history: IHistoryState[] history: IHistoryState[]
@ -37,106 +38,157 @@ interface IUIProps {
loadState: (move: number) => void loadState: (move: number) => void
} }
function CloseOtherSidebars( export enum SidebarType {
setIsSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>, None,
setIsSymbolsOpen: React.Dispatch<React.SetStateAction<boolean>>, Components,
setIsMessagesOpen: React.Dispatch<React.SetStateAction<boolean>> Symbols,
): void { History,
setIsSidebarOpen(false); Messages,
setIsSymbolsOpen(false); Settings
setIsMessagesOpen(false); }
function UseSetOrToggleSidebar(
selectedSidebar: SidebarType,
setSelectedSidebar: React.Dispatch<React.SetStateAction<SidebarType>>
): (newSidebarType: SidebarType) => void {
return (newSidebarType) => {
if (newSidebarType === selectedSidebar) {
setSelectedSidebar(SidebarType.None);
return;
}
setSelectedSidebar(newSidebarType);
};
} }
export function UI(props: IUIProps): JSX.Element { export function UI(props: IUIProps): JSX.Element {
const [isSidebarOpen, setIsSidebarOpen] = React.useState(true); const [selectedSidebar, setSelectedSidebar] = React.useState<SidebarType>(SidebarType.Components);
const [isSymbolsOpen, setIsSymbolsOpen] = React.useState(false);
const [isHistoryOpen, setIsHistoryOpen] = React.useState(false);
const [isMessagesOpen, setIsMessagesOpen] = React.useState(false);
let buttonRightOffsetClasses = 'right-12'; // Please use setOrToggleSidebar rather than setSelectedSidebar so we can close the sidebar
if (isSidebarOpen || isHistoryOpen || isSymbolsOpen) { const setOrToggleSidebar = UseSetOrToggleSidebar(selectedSidebar, setSelectedSidebar);
buttonRightOffsetClasses = 'right-72';
let leftSidebarTitle = '';
let rightSidebarTitle = '';
let leftChildren: JSX.Element = (<></>);
let rightChildren: JSX.Element = (<></>);
switch (selectedSidebar) {
case SidebarType.Components:
leftSidebarTitle = 'Components';
leftChildren = <Components
selectedContainer={props.selectedContainer}
componentOptions={props.availableContainers}
categories={props.categories}
buttonOnClick={props.addContainer}
/>;
rightSidebarTitle = 'Elements';
rightChildren = <ElementsList
mainContainer={props.current.mainContainer}
symbols={props.current.symbols}
selectedContainer={props.selectedContainer}
onPropertyChange={props.onPropertyChange}
selectContainer={props.selectContainer}
addContainer={props.addContainerAt}
/>;
break;
case SidebarType.Symbols:
leftSidebarTitle = 'Symbols';
leftChildren = <Symbols
componentOptions={props.availableSymbols}
buttonOnClick={props.addSymbol}
/>;
rightSidebarTitle = 'Symbols';
rightChildren = <SymbolsSidebar
selectedSymbolId={props.current.selectedSymbolId}
symbols={props.current.symbols}
onPropertyChange={props.onSymbolPropertyChange}
selectSymbol={props.selectSymbol}
/>;
break;
case SidebarType.History:
leftSidebarTitle = 'Timeline';
leftChildren = <History
history={props.history}
historyCurrentStep={props.historyCurrentStep}
jumpTo={props.loadState}
/>;
break;
case SidebarType.Messages:
leftSidebarTitle = 'Messages';
leftChildren = <Messages
historyState={props.current}
/>;
break;
case SidebarType.Settings:
leftSidebarTitle = 'Settings';
leftChildren = <Settings
saveEditorAsJSON={props.saveEditorAsJSON}
saveEditorAsSVG={props.saveEditorAsSVG}
/>;
break;
} }
if (isHistoryOpen && (isSidebarOpen || isSymbolsOpen)) {
buttonRightOffsetClasses = 'right-[544px]'; const isLeftSidebarOpen = selectedSidebar !== SidebarType.None;
const isRightSidebarOpen = selectedSidebar === SidebarType.Components || selectedSidebar === SidebarType.Symbols;
let isLeftSidebarOpenClasses = 'left-16 -bottom-full md:-left-64 md:bottom-0';
let isRightSidebarOpenClasses = 'right-0 -bottom-full md:-right-80 md:bottom-0';
if (isLeftSidebarOpen) {
isLeftSidebarOpenClasses = 'left-16';
}
if (isRightSidebarOpen) {
isRightSidebarOpenClasses = 'right-0';
} }
return ( return (
<> <>
<Bar <Bar
isSidebarOpen={isSidebarOpen} isComponentsOpen={selectedSidebar === SidebarType.Components}
isSymbolsOpen={isSymbolsOpen} isSymbolsOpen={selectedSidebar === SidebarType.Symbols}
isElementsSidebarOpen={isSidebarOpen} isHistoryOpen={selectedSidebar === SidebarType.History}
isHistoryOpen={isHistoryOpen} isMessagesOpen={selectedSidebar === SidebarType.Messages}
isMessagesOpen={isMessagesOpen} isSettingsOpen={selectedSidebar === SidebarType.Settings}
toggleSidebar={() => { toggleComponents={() => {
CloseOtherSidebars(setIsSidebarOpen, setIsSymbolsOpen, setIsMessagesOpen); setOrToggleSidebar(SidebarType.Components);
setIsSidebarOpen(!isSidebarOpen);
} } } }
toggleSymbols={() => { toggleSymbols={() => {
CloseOtherSidebars(setIsSidebarOpen, setIsSymbolsOpen, setIsMessagesOpen); setOrToggleSidebar(SidebarType.Symbols);
setIsSymbolsOpen(!isSymbolsOpen); } }
toggleTimeline={() => {
setOrToggleSidebar(SidebarType.History);
} } } }
toggleTimeline={() => setIsHistoryOpen(!isHistoryOpen)}
toggleMessages={() => { toggleMessages={() => {
CloseOtherSidebars(setIsSidebarOpen, setIsSymbolsOpen, setIsMessagesOpen); setOrToggleSidebar(SidebarType.Messages);
setIsMessagesOpen(!isMessagesOpen); } }
} }/> toggleSettings={() => {
setOrToggleSidebar(SidebarType.Settings);
} }
/>
<Sidebar <Sidebar
className={`left-sidebar ${isLeftSidebarOpenClasses}`}
title={leftSidebarTitle}
>
{ leftChildren }
</Sidebar>
<Viewer
isLeftSidebarOpen={isLeftSidebarOpen}
isRightSidebarOpen={isRightSidebarOpen}
current={props.current}
selectedContainer={props.selectedContainer} selectedContainer={props.selectedContainer}
componentOptions={props.availableContainers}
categories={props.categories}
isOpen={isSidebarOpen}
buttonOnClick={props.addContainer} />
<Symbols
componentOptions={props.availableSymbols}
isOpen={isSymbolsOpen}
buttonOnClick={props.addSymbol} />
<ElementsSidebar
mainContainer={props.current.mainContainer}
symbols={props.current.symbols}
selectedContainer={props.selectedContainer}
isOpen={isSidebarOpen}
isHistoryOpen={isHistoryOpen}
onPropertyChange={props.onPropertyChange}
selectContainer={props.selectContainer} selectContainer={props.selectContainer}
addContainer={props.addContainerAt}
/> />
<SymbolsSidebar <Sidebar
selectedSymbolId={props.current.selectedSymbolId} className={`right-sidebar ${isRightSidebarOpenClasses}`}
symbols={props.current.symbols} title={rightSidebarTitle}
isOpen={isSymbolsOpen} >
isHistoryOpen={isHistoryOpen} { rightChildren }
onPropertyChange={props.onSymbolPropertyChange} </Sidebar>
selectSymbol={props.selectSymbol}
/>
<MessagesSidebar
historyState={props.current}
isOpen={isMessagesOpen}
/>
<History
history={props.history}
historyCurrentStep={props.historyCurrentStep}
isOpen={isHistoryOpen}
jumpTo={props.loadState} />
<FloatingButton className={`fixed z-10 flex flex-col gap-2 items-center bottom-40 ${buttonRightOffsetClasses}`}>
<button type="button"
className={'transition-all w-10 h-10 p-2 align-middle items-center justify-center rounded-full bg-blue-500 hover:bg-blue-800'}
title='Export as JSON'
onClick={props.saveEditorAsJSON}
>
<ArrowUpOnSquareIcon className="heroicon text-white" />
</button>
<button type="button"
className={'transition-all w-10 h-10 p-2 align-middle items-center justify-center rounded-full bg-blue-500 hover:bg-blue-800'}
title='Export as SVG'
onClick={props.saveEditorAsSVG}
>
<CameraIcon className="heroicon text-white" />
</button>
</FloatingButton>
</> </>
); );
} }

View file

@ -0,0 +1,173 @@
import * as React from 'react';
import { IContainerModel } from '../../Interfaces/IContainerModel';
import { IHistoryState } from '../../Interfaces/IHistoryState';
import { IPoint } from '../../Interfaces/IPoint';
import { DIMENSION_MARGIN, USE_EXPERIMENTAL_CANVAS_API } from '../../utils/default';
import { MakeRecursionDFSIterator } from '../../utils/itertools';
import { BAR_WIDTH } from '../Bar/Bar';
import { Canvas } from '../Canvas/Canvas';
import { AddDimensions } from '../Canvas/DimensionLayer';
import { RenderSelector } from '../Canvas/Selector';
import { SVG } from '../SVG/SVG';
interface IViewerProps {
isLeftSidebarOpen: boolean
isRightSidebarOpen: boolean
current: IHistoryState
selectedContainer: IContainerModel | undefined
selectContainer: (containerId: string) => void
}
interface IViewer {
viewerWidth: number
viewerHeight: number
}
function OnResize(
isLeftSidebarOpen: boolean,
isRightSidebarOpen: boolean,
setViewer: React.Dispatch<React.SetStateAction<IViewer>>
): void {
let marginSidebar = BAR_WIDTH;
if (isLeftSidebarOpen) {
marginSidebar += 256;
}
if (isRightSidebarOpen) {
marginSidebar += 256;
}
const margin = window.innerWidth < 768 ? BAR_WIDTH : marginSidebar;
setViewer({
viewerWidth: window.innerWidth - margin,
viewerHeight: window.innerHeight
});
}
function UseSVGAutoResizerOnWindowResize(
isLeftSidebarOpen: boolean,
isRightSidebarOpen: boolean,
setViewer: React.Dispatch<React.SetStateAction<IViewer>>
): void {
React.useEffect(() => {
function SVGAutoResizer(): void {
OnResize(isLeftSidebarOpen, isRightSidebarOpen, setViewer);
}
window.addEventListener('resize', SVGAutoResizer);
return () => {
window.removeEventListener('resize', SVGAutoResizer);
};
});
}
function UseSVGAutoResizerOnSidebar(
isLeftSidebarOpen: boolean,
isRightSidebarOpen: boolean,
setViewer: React.Dispatch<React.SetStateAction<IViewer>>
): void {
React.useEffect(() => {
OnResize(isLeftSidebarOpen, isRightSidebarOpen, setViewer);
}, [isLeftSidebarOpen, isRightSidebarOpen, setViewer]);
}
export function Viewer({
isLeftSidebarOpen, isRightSidebarOpen,
current,
selectedContainer,
selectContainer
}: IViewerProps): JSX.Element {
let marginClasses = 'ml-16';
let marginSidebar = BAR_WIDTH;
if (isLeftSidebarOpen) {
marginClasses += ' md:ml-80';
marginSidebar += 256;
}
if (isRightSidebarOpen) {
marginClasses += ' md:mr-64';
marginSidebar += 256;
}
const margin = window.innerWidth < 768 ? BAR_WIDTH : marginSidebar;
const [viewer, setViewer] = React.useState<IViewer>({
viewerWidth: window.innerWidth - margin,
viewerHeight: window.innerHeight
});
UseSVGAutoResizerOnWindowResize(isLeftSidebarOpen, isRightSidebarOpen, setViewer);
UseSVGAutoResizerOnSidebar(isLeftSidebarOpen, isRightSidebarOpen, setViewer);
if (USE_EXPERIMENTAL_CANVAS_API) {
function Draw(ctx: CanvasRenderingContext2D, frameCount: number, scale: number, translatePos: IPoint): void {
const topDim = current.mainContainer.properties.y;
const leftDim = current.mainContainer.properties.x;
const rightDim = current.mainContainer.properties.x + current.mainContainer.properties.width;
const bottomDim = current.mainContainer.properties.y + current.mainContainer.properties.height;
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.save();
ctx.translate(translatePos.x, translatePos.y);
ctx.scale(scale, scale);
ctx.fillStyle = '#000000';
const it = MakeRecursionDFSIterator(current.mainContainer, 0, [0, 0]);
for (const { container, depth, currentTransform } of it) {
const [x, y] = [
container.properties.x + currentTransform[0],
container.properties.y + currentTransform[1]
];
// Draw container
ctx.strokeStyle = container.properties.style?.stroke ?? '#000000';
ctx.fillStyle = container.properties.style?.fill ?? '#000000';
ctx.lineWidth = Number(container.properties.style?.strokeWidth ?? 1);
ctx.globalAlpha = Number(container.properties.style?.fillOpacity ?? 1);
ctx.fillRect(x, y, container.properties.width, container.properties.height);
ctx.globalAlpha = Number(container.properties.style?.strokeOpacity ?? 1);
ctx.strokeRect(x, y, container.properties.width, container.properties.height);
ctx.globalAlpha = 1;
ctx.lineWidth = 1;
ctx.fillStyle = '#000000';
ctx.strokeStyle = '#000000';
// Draw dimensions
const containerLeftDim = leftDim - (DIMENSION_MARGIN * (depth + 1)) / scale;
const containerTopDim = topDim - (DIMENSION_MARGIN * (depth + 1)) / scale;
const containerBottomDim = bottomDim + (DIMENSION_MARGIN * (depth + 1)) / scale;
const containerRightDim = rightDim + (DIMENSION_MARGIN * (depth + 1)) / scale;
const dimMapped = [containerLeftDim, containerBottomDim, containerTopDim, containerRightDim];
AddDimensions(ctx, container, dimMapped, currentTransform, scale, depth);
// Draw selector
RenderSelector(ctx, frameCount, {
scale,
selected: selectedContainer
});
}
ctx.restore();
}
return (
<Canvas
draw={Draw}
className='ml-16'
width={window.innerWidth - BAR_WIDTH}
height={window.innerHeight}
/>
);
}
return (
<SVG
className={marginClasses}
viewerWidth={viewer.viewerWidth}
viewerHeight={viewer.viewerHeight}
width={current.mainContainer?.properties.width}
height={current.mainContainer?.properties.height}
selected={selectedContainer}
symbols={current.symbols}
selectContainer={selectContainer}
>
{current.mainContainer}
</SVG>
);
}

View file

@ -3,22 +3,45 @@
@tailwind utilities; @tailwind utilities;
@layer components { @layer components {
#root, .Editor, .App {
@apply overflow-hidden
}
.left-sidebar {
@apply fixed shadow-lg z-20
w-[calc(100%_-_4rem)] md:w-64
h-1/4 md:h-full bottom-1/2 md:bottom-0
}
.left-sidebar-single {
@apply fixed shadow-lg z-20
w-[calc(100%_-_4rem)] md:w-64
h-1/3 md:h-full bottom-0
}
.right-sidebar {
@apply fixed shadow-lg z-20
w-[calc(100%_-_4rem)] md:w-64
h-1/2 md:h-full bottom-0 md:bottom-0
}
.sidebar-title { .sidebar-title {
@apply p-6 font-bold @apply p-3 md:p-5 font-bold h-12 md:h-16
} }
.sidebar-component { .sidebar-component {
@apply transition-all px-2 py-6 text-sm rounded-lg @apply transition-all px-2 h-12 flex items-center align-middle
text-sm rounded-lg
bg-slate-300/80 hover:bg-blue-500 hover:text-slate-50 bg-slate-300/80 hover:bg-blue-500 hover:text-slate-50
disabled:bg-slate-400 disabled:text-slate-500 disabled:bg-slate-400 disabled:text-slate-500
} }
.sidebar-component-left { .sidebar-component-left {
@apply transition-all px-2 py-6 text-sm rounded-l-lg bg-slate-300/80 group-hover:bg-blue-500 group-hover:text-slate-50 @apply transition-all px-2 h-12 align-middle flex items-center text-sm rounded-l-lg bg-slate-300/80 group-hover:bg-blue-500 group-hover:text-slate-50
} }
.sidebar-component-right { .sidebar-component-right {
@apply transition-all px-2 py-6 text-sm rounded-r-lg bg-slate-400/80 group-hover:bg-blue-600 group-hover:text-slate-50 @apply transition-all px-2 h-12 align-middle flex items-center text-sm rounded-r-lg bg-slate-400/80 group-hover:bg-blue-600 group-hover:text-slate-50
} }
.sidebar-component-card { .sidebar-component-card {
@ -60,8 +83,8 @@
} }
.bar { .bar {
@apply fixed z-20 flex flex-col top-0 left-0 @apply fixed z-30 flex flex-col top-0 left-0
h-full w-16 bg-slate-100 h-full w-16 bg-slate-100 shadow-sm
} }
.bar-btn { .bar-btn {

View file

@ -12,7 +12,7 @@ import { Position } from '../Enums/Position';
/// EDITOR DEFAULTS /// /// EDITOR DEFAULTS ///
/** Enable fast boot and disable main menu (default = false) */ /** Enable fast boot and disable main menu (default = false) */
export const FAST_BOOT = false; export const FAST_BOOT = true;
/** Disable any call to the API (default = false) */ /** Disable any call to the API (default = false) */
export const DISABLE_API = false; export const DISABLE_API = false;