Merged PR 203: Improve responsive design and refactor layout
This commit is contained in:
parent
50626218ba
commit
0d05f0959c
27 changed files with 968 additions and 485 deletions
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
119
src/Components/Components/Components.tsx
Normal file
119
src/Components/Components/Components.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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'
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
|
@ -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>
|
||||||
|
<div className='grow overflow-auto'>
|
||||||
<Properties
|
<Properties
|
||||||
properties={props.selectedContainer?.properties}
|
properties={props.selectedContainer?.properties}
|
||||||
symbols={props.symbols}
|
symbols={props.symbols}
|
||||||
onChange={props.onPropertyChange} />
|
onChange={props.onPropertyChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
183
src/Components/ElementsSidebar/Elements.tsx
Normal file
183
src/Components/ElementsSidebar/Elements.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
23
src/Components/FloatingButton/MenuButton.tsx
Normal file
23
src/Components/FloatingButton/MenuButton.tsx
Normal 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>;
|
||||||
|
}
|
|
@ -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}`}>
|
|
||||||
<div className='bg-slate-600 font-bold sidebar-title'>
|
|
||||||
Timeline
|
|
||||||
</div>
|
|
||||||
<List
|
<List
|
||||||
className='List overflow-x-hidden'
|
className='List overflow-x-hidden'
|
||||||
itemCount={props.history.length}
|
itemCount={props.history.length}
|
||||||
itemSize={35}
|
itemSize={35}
|
||||||
height={window.innerHeight}
|
height={window.innerHeight - TITLE_BAR_HEIGHT}
|
||||||
width={256}
|
width={'100%'}
|
||||||
>
|
>
|
||||||
{Row}
|
{Row}
|
||||||
</List>
|
</List>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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>
|
131
src/Components/MessagesSidebar/Messages.tsx
Normal file
131
src/Components/MessagesSidebar/Messages.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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>);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
|
31
src/Components/Settings/Settings.tsx
Normal file
31
src/Components/Settings/Settings.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -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}
|
|
||||||
onPropertyChange={props.onSymbolPropertyChange}
|
|
||||||
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" />
|
{ rightChildren }
|
||||||
</button>
|
</Sidebar>
|
||||||
<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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
173
src/Components/Viewer/Viewer.tsx
Normal file
173
src/Components/Viewer/Viewer.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue