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 { ClockIcon, CubeIcon, LinkIcon, EnvelopeIcon } from '@heroicons/react/24/outline';
|
||||
import { ClockIcon as ClockIconS, CubeIcon as CubeIconS, LinkIcon as LinkIconS, EnvelopeIcon as EnvolopeIconS } from '@heroicons/react/24/solid';
|
||||
import {
|
||||
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';
|
||||
|
||||
interface IBarProps {
|
||||
isSidebarOpen: boolean
|
||||
isComponentsOpen: boolean
|
||||
isSymbolsOpen: boolean
|
||||
isElementsSidebarOpen: boolean
|
||||
isHistoryOpen: boolean
|
||||
isMessagesOpen: boolean
|
||||
toggleSidebar: () => void
|
||||
isSettingsOpen: boolean
|
||||
toggleComponents: () => void
|
||||
toggleSymbols: () => void
|
||||
toggleTimeline: () => void
|
||||
toggleMessages: () => void
|
||||
toggleSettings: () => void
|
||||
}
|
||||
|
||||
export const BAR_WIDTH = 64; // 4rem
|
||||
|
@ -21,11 +34,11 @@ export function Bar(props: IBarProps): JSX.Element {
|
|||
return (
|
||||
<div className='bar'>
|
||||
<BarIcon
|
||||
isActive={props.isSidebarOpen}
|
||||
isActive={props.isComponentsOpen}
|
||||
title='Components'
|
||||
onClick={() => props.toggleSidebar()}>
|
||||
onClick={() => props.toggleComponents()}>
|
||||
{
|
||||
props.isSidebarOpen
|
||||
props.isComponentsOpen
|
||||
? <CubeIconS className='heroicon' />
|
||||
: <CubeIcon className='heroicon' />
|
||||
}
|
||||
|
@ -40,6 +53,17 @@ export function Bar(props: IBarProps): JSX.Element {
|
|||
: <LinkIcon className='heroicon' />
|
||||
}
|
||||
</BarIcon>
|
||||
<BarIcon
|
||||
isActive={props.isMessagesOpen}
|
||||
title='Messages'
|
||||
onClick={() => props.toggleMessages()}>
|
||||
{
|
||||
props.isMessagesOpen
|
||||
? <EnvolopeIconS className='heroicon' />
|
||||
: <EnvelopeIcon className='heroicon' />
|
||||
}
|
||||
</BarIcon>
|
||||
<div className='grow'></div>
|
||||
<BarIcon
|
||||
isActive={props.isHistoryOpen}
|
||||
title='Timeline'
|
||||
|
@ -51,13 +75,13 @@ export function Bar(props: IBarProps): JSX.Element {
|
|||
}
|
||||
</BarIcon>
|
||||
<BarIcon
|
||||
isActive={props.isMessagesOpen}
|
||||
title='Messages'
|
||||
onClick={() => props.toggleMessages()}>
|
||||
isActive={props.isSettingsOpen}
|
||||
title='Settings'
|
||||
onClick={() => props.toggleSettings()}>
|
||||
{
|
||||
props.isMessagesOpen
|
||||
? <EnvolopeIconS className='heroicon' />
|
||||
: <EnvelopeIcon className='heroicon' />
|
||||
? <Cog8ToothIconS className='heroicon' />
|
||||
: <Cog8ToothIcon className='heroicon' />
|
||||
}
|
||||
</BarIcon>
|
||||
</div>
|
||||
|
|
|
@ -50,7 +50,7 @@ export function CheckboxGroupButtons(props: ICheckboxGroupButtonsProps): JSX.Ele
|
|||
const newSelectedValues = SetChecked(Number(event.target.value), event.target.checked);
|
||||
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}
|
||||
</label>
|
||||
</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 {
|
||||
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
|
||||
labelText='Name'
|
||||
inputKey='id'
|
||||
|
|
|
@ -16,7 +16,7 @@ export function Properties(props: IPropertiesProps): JSX.Element {
|
|||
}
|
||||
|
||||
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
|
||||
properties={props.properties}
|
||||
symbols={props.symbols}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React, { Dispatch, SetStateAction, useEffect, useRef } from 'react';
|
||||
import './Editor.scss';
|
||||
import { IConfiguration } from '../../Interfaces/IConfiguration';
|
||||
import { SVG } from '../SVG/SVG';
|
||||
import { IHistoryState } from '../../Interfaces/IHistoryState';
|
||||
import { UI } from '../UI/UI';
|
||||
import { SelectContainer, DeleteContainer, OnPropertyChange } from './Actions/ContainerOperations';
|
||||
|
@ -234,54 +233,6 @@ export function Editor(props: IEditorProps): JSX.Element {
|
|||
const current = GetCurrentHistoryState(history, historyCurrentStep);
|
||||
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 (
|
||||
<div ref={editorRef} className="Editor font-sans h-full">
|
||||
<UI
|
||||
|
@ -366,24 +317,6 @@ export function Editor(props: IEditorProps): JSX.Element {
|
|||
saveEditorAsSVG={() => SaveEditorAsSVG()}
|
||||
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
|
||||
getListener={() => editorRef.current}
|
||||
actions={menuActions}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { describe, expect, it, vi } from 'vitest';
|
||||
import * as React from 'react';
|
||||
import { fireEvent, render, screen } from '../../utils/test-utils';
|
||||
import { ElementsSidebar } from './ElementsSidebar';
|
||||
import { ElementsList } from './ElementsList';
|
||||
import { IContainerModel } from '../../Interfaces/IContainerModel';
|
||||
import { PositionReference } from '../../Enums/PositionReference';
|
||||
import { FindContainerById } from '../../utils/itertools';
|
||||
|
@ -10,7 +10,7 @@ import { Orientation } from '../../Enums/Orientation';
|
|||
|
||||
describe.concurrent('Elements sidebar', () => {
|
||||
it('With a MainContainer', () => {
|
||||
render(<ElementsSidebar
|
||||
render(<ElementsList
|
||||
symbols={new Map()}
|
||||
mainContainer={{
|
||||
children: [],
|
||||
|
@ -18,8 +18,6 @@ describe.concurrent('Elements sidebar', () => {
|
|||
properties: DEFAULT_MAINCONTAINER_PROPS,
|
||||
userData: {}
|
||||
}}
|
||||
isOpen={true}
|
||||
isHistoryOpen={false}
|
||||
selectedContainer={undefined}
|
||||
onPropertyChange={() => {}}
|
||||
selectContainer={() => {}}
|
||||
|
@ -39,11 +37,9 @@ describe.concurrent('Elements sidebar', () => {
|
|||
userData: {}
|
||||
};
|
||||
|
||||
const { container } = render(<ElementsSidebar
|
||||
const { container } = render(<ElementsList
|
||||
symbols={new Map()}
|
||||
mainContainer={mainContainer}
|
||||
isOpen={true}
|
||||
isHistoryOpen={false}
|
||||
selectedContainer={mainContainer}
|
||||
onPropertyChange={() => {}}
|
||||
selectContainer={() => {}}
|
||||
|
@ -154,11 +150,9 @@ describe.concurrent('Elements sidebar', () => {
|
|||
}
|
||||
);
|
||||
|
||||
render(<ElementsSidebar
|
||||
render(<ElementsList
|
||||
symbols={new Map()}
|
||||
mainContainer={mainContainer}
|
||||
isOpen={true}
|
||||
isHistoryOpen={false}
|
||||
selectedContainer={mainContainer}
|
||||
onPropertyChange={() => {}}
|
||||
selectContainer={() => {}}
|
||||
|
@ -219,11 +213,9 @@ describe.concurrent('Elements sidebar', () => {
|
|||
selectedContainer = FindContainerById(mainContainer, containerId);
|
||||
});
|
||||
|
||||
const { container, rerender } = render(<ElementsSidebar
|
||||
const { container, rerender } = render(<ElementsList
|
||||
symbols={new Map()}
|
||||
mainContainer={mainContainer}
|
||||
isOpen={true}
|
||||
isHistoryOpen={false}
|
||||
selectedContainer={selectedContainer}
|
||||
onPropertyChange={() => {}}
|
||||
selectContainer={selectContainer}
|
||||
|
@ -242,11 +234,9 @@ describe.concurrent('Elements sidebar', () => {
|
|||
|
||||
fireEvent.click(child1);
|
||||
|
||||
rerender(<ElementsSidebar
|
||||
rerender(<ElementsList
|
||||
symbols={new Map()}
|
||||
mainContainer={mainContainer}
|
||||
isOpen={true}
|
||||
isHistoryOpen={false}
|
||||
selectedContainer={selectedContainer}
|
||||
onPropertyChange={() => {}}
|
||||
selectContainer={selectContainer}
|
|
@ -7,11 +7,9 @@ import { ISymbolModel } from '../../Interfaces/ISymbolModel';
|
|||
import { PropertyType } from '../../Enums/PropertyType';
|
||||
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
interface IElementsSidebarProps {
|
||||
interface IElementsListProps {
|
||||
mainContainer: IContainerModel
|
||||
symbols: Map<string, ISymbolModel>
|
||||
isOpen: boolean
|
||||
isHistoryOpen: boolean
|
||||
selectedContainer: IContainerModel | undefined
|
||||
onPropertyChange: (
|
||||
key: string,
|
||||
|
@ -117,13 +115,8 @@ function HandleOnDrop(
|
|||
}
|
||||
}
|
||||
|
||||
export function ElementsSidebar(props: IElementsSidebarProps): JSX.Element {
|
||||
export function ElementsList(props: IElementsListProps): JSX.Element {
|
||||
// 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 containers = [...it];
|
||||
function Row({
|
||||
|
@ -138,16 +131,19 @@ export function ElementsSidebar(props: IElementsSidebarProps): JSX.Element {
|
|||
const text = container.properties.displayedText === key
|
||||
? `${key}`
|
||||
: `${container.properties.displayedText}`;
|
||||
const selectedClass: string = props.selectedContainer !== undefined &&
|
||||
|
||||
const isSelected = props.selectedContainer !== undefined &&
|
||||
props.selectedContainer !== null &&
|
||||
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';
|
||||
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={`w-full border-blue-500 elements-sidebar-row whitespace-pre
|
||||
text-left text-sm font-medium transition-all inline-flex ${container.properties.type} ${selectedClass}`}
|
||||
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}
|
||||
|
@ -167,25 +163,25 @@ export function ElementsSidebar(props: IElementsSidebarProps): JSX.Element {
|
|||
}
|
||||
|
||||
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 className="bg-slate-100 font-bold sidebar-title">Elements</div>
|
||||
<div className="h-96 text-gray-800">
|
||||
<div className='h-full flex flex-col'>
|
||||
<div className='h-48'>
|
||||
<List
|
||||
className="List divide-y divide-black"
|
||||
className="List divide-y divide-black overflow-y-auto"
|
||||
itemCount={containers.length}
|
||||
itemSize={35}
|
||||
height={384}
|
||||
width={256}
|
||||
height={192}
|
||||
width={'100%'}
|
||||
>
|
||||
{Row}
|
||||
</List>
|
||||
</div>
|
||||
<div className='grow overflow-auto'>
|
||||
<Properties
|
||||
properties={props.selectedContainer?.properties}
|
||||
symbols={props.symbols}
|
||||
onChange={props.onPropertyChange} />
|
||||
onChange={props.onPropertyChange}
|
||||
/>
|
||||
</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 { FixedSizeList as List } from 'react-window';
|
||||
import { IHistoryState } from '../../Interfaces/IHistoryState';
|
||||
import { TITLE_BAR_HEIGHT } from '../Sidebar/Sidebar';
|
||||
|
||||
interface IHistoryProps {
|
||||
history: IHistoryState[]
|
||||
historyCurrentStep: number
|
||||
isOpen: boolean
|
||||
jumpTo: (move: number) => void
|
||||
}
|
||||
|
||||
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 {
|
||||
const reversedIndex = (props.history.length - 1) - index;
|
||||
const step = props.history[reversedIndex];
|
||||
const desc = step.lastAction;
|
||||
|
||||
const selectedClass = reversedIndex === props.historyCurrentStep
|
||||
? 'bg-blue-500 hover:bg-blue-600'
|
||||
: 'bg-slate-500 hover:bg-slate-700';
|
||||
const selectedClass: string = reversedIndex === props.historyCurrentStep
|
||||
? '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"
|
||||
|
@ -26,7 +25,7 @@ export function History(props: IHistoryProps): JSX.Element {
|
|||
style={style}
|
||||
onClick={() => props.jumpTo(reversedIndex)}
|
||||
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}`}
|
||||
>
|
||||
{desc}
|
||||
|
@ -35,19 +34,14 @@ export function History(props: IHistoryProps): JSX.Element {
|
|||
}
|
||||
|
||||
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
|
||||
className='List overflow-x-hidden'
|
||||
itemCount={props.history.length}
|
||||
itemSize={35}
|
||||
height={window.innerHeight}
|
||||
width={256}
|
||||
height={window.innerHeight - TITLE_BAR_HEIGHT}
|
||||
width={'100%'}
|
||||
>
|
||||
{Row}
|
||||
</List>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -28,7 +28,13 @@ export function MainMenu(props: IMainMenuProps): JSX.Element {
|
|||
switch (windowState) {
|
||||
case WindowState.Load:
|
||||
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">
|
||||
<label className="block">
|
||||
<span className="sr-only">Import save</span>
|
||||
|
@ -67,7 +73,12 @@ export function MainMenu(props: IMainMenuProps): JSX.Element {
|
|||
);
|
||||
default:
|
||||
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={() => {
|
||||
setWindowState(WindowState.Loading);
|
||||
props.newEditor();
|
||||
|
|
|
@ -8,10 +8,10 @@ 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 {
|
||||
interface IMessagesProps {
|
||||
historyState: IHistoryState
|
||||
isOpen: boolean
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
||||
|
@ -45,6 +45,7 @@ function UseAsync(
|
|||
): void {
|
||||
React.useEffect(() => {
|
||||
const request: IGetFeedbackRequest = {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
ApplicationState: state
|
||||
};
|
||||
const dataParsed = JSON.stringify(request, GetCircularReplacerKeepDataStructure());
|
||||
|
@ -65,7 +66,7 @@ function UseAsync(
|
|||
}, [state]);
|
||||
}
|
||||
|
||||
export function MessagesSidebar(props: IMessagesSidebarProps): JSX.Element {
|
||||
export function Messages(props: IMessagesProps): JSX.Element {
|
||||
const [messages, setMessages] = React.useState<IMessage[]>([]);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
||||
|
@ -107,16 +108,14 @@ export function MessagesSidebar(props: IMessagesSidebarProps): JSX.Element {
|
|||
</p>);
|
||||
}
|
||||
|
||||
const isOpenClasses = props.isOpen ? 'left-16' : '-left-64';
|
||||
const toolbarHeight = 28;
|
||||
|
||||
return (
|
||||
<div className={`fixed z-10 bg-slate-200
|
||||
text-gray-700 transition-all h-full w-64
|
||||
${isOpenClasses}`}>
|
||||
<div className='bg-slate-100 sidebar-title flex place-content-between'>
|
||||
Messages
|
||||
<div>
|
||||
<div className='hover:bg-slate-300 h-7 text-right pr-1 pl-1'>
|
||||
<button
|
||||
onClick={() => { setMessages([]); }}
|
||||
className='h-6'
|
||||
className='h-full hover:bg-slate-400 rounded-lg p-1'
|
||||
aria-label='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'
|
||||
itemCount={messages.length}
|
||||
itemSize={65}
|
||||
height={window.innerHeight}
|
||||
width={256}
|
||||
height={window.innerHeight - TITLE_BAR_HEIGHT - toolbarHeight}
|
||||
width={'100%'}
|
||||
>
|
||||
{Row}
|
||||
</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
|
||||
depth: number
|
||||
scale: number
|
||||
selectContainer: (containerId: string) => void
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -22,6 +23,7 @@ export function Container(props: IContainerProps): JSX.Element {
|
|||
model={child}
|
||||
depth={props.depth + 1}
|
||||
scale={props.scale}
|
||||
selectContainer={props.selectContainer}
|
||||
/>);
|
||||
|
||||
const width: number = props.model.properties.width;
|
||||
|
@ -54,6 +56,7 @@ export function Container(props: IContainerProps): JSX.Element {
|
|||
width={width}
|
||||
height={height}
|
||||
style={style}
|
||||
onClick={() => props.selectContainer(props.model.properties.id)}
|
||||
>
|
||||
</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 { ReactSVGPanZoom, Tool, TOOL_PAN, Value } from 'react-svg-pan-zoom';
|
||||
import { Container } from './Elements/Container';
|
||||
import { ContainerModel } from '../../Interfaces/IContainerModel';
|
||||
import { Selector } from './Elements/Selector/Selector';
|
||||
import { BAR_WIDTH } from '../Bar/Bar';
|
||||
import { DepthDimensionLayer } from './Elements/DepthDimensionLayer';
|
||||
import { MAX_FRAMERATE, SHOW_DIMENSIONS_PER_DEPTH } from '../../utils/default';
|
||||
import { SymbolLayer } from './Elements/SymbolLayer';
|
||||
|
@ -12,11 +10,15 @@ import { ISymbolModel } from '../../Interfaces/ISymbolModel';
|
|||
import { DimensionLayer } from './Elements/DimensionLayer';
|
||||
|
||||
interface ISVGProps {
|
||||
className?: string
|
||||
viewerWidth: number
|
||||
viewerHeight: number
|
||||
width: number
|
||||
height: number
|
||||
children: ContainerModel
|
||||
selected?: ContainerModel
|
||||
symbols: Map<string, ISymbolModel>
|
||||
selectContainer: (containerId: string) => void
|
||||
}
|
||||
|
||||
interface Viewer {
|
||||
|
@ -26,29 +28,7 @@ interface Viewer {
|
|||
|
||||
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 {
|
||||
const [viewer, setViewer] = React.useState<Viewer>({
|
||||
viewerWidth: window.innerWidth - BAR_WIDTH,
|
||||
viewerHeight: window.innerHeight
|
||||
});
|
||||
const [tool, setTool] = React.useState<Tool>(TOOL_PAN);
|
||||
const [value, setValue] = React.useState<Value>({} as Value);
|
||||
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());
|
||||
// console.log(renderCounter.current / ((Date.now() - startTimer.current) / 1000));
|
||||
|
||||
UseSVGAutoResizer(setViewer);
|
||||
UseFitOnce(svgViewer, props.width, props.height);
|
||||
|
||||
const xmlns = '<http://www.w3.org/2000/svg>';
|
||||
|
@ -79,14 +58,15 @@ export function SVG(props: ISVGProps): JSX.Element {
|
|||
model={props.children}
|
||||
depth={0}
|
||||
scale={scale}
|
||||
selectContainer={props.selectContainer}
|
||||
/>;
|
||||
|
||||
return (
|
||||
<div id={ID} className='ml-16'>
|
||||
<div id={ID} className={props.className}>
|
||||
<ReactSVGPanZoom
|
||||
ref={svgViewer}
|
||||
width={viewer.viewerWidth}
|
||||
height={viewer.viewerHeight}
|
||||
width={props.viewerWidth}
|
||||
height={props.viewerHeight}
|
||||
tool={tool} onChangeTool={setTool}
|
||||
value={value} onChangeValue={(value: Value) => {
|
||||
// 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 { 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 {
|
||||
selectedContainer: IContainerModel | undefined
|
||||
componentOptions: IAvailableContainer[]
|
||||
categories: ICategory[]
|
||||
isOpen: boolean
|
||||
buttonOnClick: (type: string) => void
|
||||
className?: string
|
||||
title: string
|
||||
titleButtons?: JSX.Element | JSX.Element[]
|
||||
children?: JSX.Element | JSX.Element[]
|
||||
}
|
||||
|
||||
function HandleDragStart(event: React.DragEvent<HTMLButtonElement>): void {
|
||||
event.dataTransfer.setData('type', (event.target as HTMLButtonElement).id);
|
||||
}
|
||||
|
||||
interface SidebarCategory {
|
||||
category: ICategory
|
||||
children: JSX.Element[]
|
||||
}
|
||||
export const TITLE_BAR_HEIGHT = 64;
|
||||
|
||||
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 (
|
||||
<div className={`fixed z-10 bg-slate-200
|
||||
text-gray-700 transition-all h-full w-64
|
||||
overflow-y-auto ${isOpenClasses}`}>
|
||||
<div className={`transition-all bg-slate-200
|
||||
text-gray-700 flex flex-col
|
||||
${props.className ?? ''}`}>
|
||||
<div className='bg-slate-100 sidebar-title flex place-content-between'>
|
||||
Components
|
||||
<button
|
||||
onClick={() => { setHideDisabled(!hideDisabled); }}
|
||||
className='h-6'
|
||||
aria-label='Hide disabled component'
|
||||
title='Hide disabled component'
|
||||
>
|
||||
{
|
||||
hideDisabled
|
||||
? <EyeSlashIcon className='heroicon' />
|
||||
: <EyeIcon className='heroicon' />
|
||||
}
|
||||
</button>
|
||||
{ props.title }
|
||||
{ props.titleButtons }
|
||||
</div>
|
||||
<div className='transition-all grid grid-cols-1 md:grid-cols-1 gap-2
|
||||
m-2 md:text-xs font-bold'>
|
||||
{rootElements}
|
||||
<div className='overflow-y-hidden grow'>
|
||||
{ props.children }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -14,7 +14,7 @@ export function SymbolProperties(props: ISymbolPropertiesProps): JSX.Element {
|
|||
}
|
||||
|
||||
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
|
||||
symbol={props.symbol}
|
||||
symbols={props.symbols}
|
||||
|
|
|
@ -4,7 +4,6 @@ import { TruncateString } from '../../utils/stringtools';
|
|||
|
||||
interface ISymbolsProps {
|
||||
componentOptions: IAvailableSymbol[]
|
||||
isOpen: boolean
|
||||
buttonOnClick: (type: string) => void
|
||||
}
|
||||
|
||||
|
@ -51,16 +50,10 @@ export function Symbols(props: ISymbolsProps): JSX.Element {
|
|||
</button>);
|
||||
});
|
||||
|
||||
const isOpenClasses = props.isOpen ? 'left-16' : '-left-64';
|
||||
return (
|
||||
<div className={`fixed z-10 bg-slate-200
|
||||
text-gray-700 transition-all h-full w-64
|
||||
overflow-y-auto ${isOpenClasses}`}>
|
||||
<div className='bg-slate-100 sidebar-title'>
|
||||
Symbols
|
||||
</div>
|
||||
<div className='h-full overflow-y-auto'>
|
||||
<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}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -6,21 +6,12 @@ import { SymbolProperties } from '../SymbolProperties/SymbolProperties';
|
|||
interface ISymbolsSidebarProps {
|
||||
selectedSymbolId: string
|
||||
symbols: Map<string, ISymbolModel>
|
||||
isOpen: boolean
|
||||
isHistoryOpen: boolean
|
||||
onPropertyChange: (key: string, value: string | number | boolean) => void
|
||||
selectSymbol: (symbolId: string) => void
|
||||
}
|
||||
|
||||
export function SymbolsSidebar(props: ISymbolsSidebarProps): JSX.Element {
|
||||
// Render
|
||||
let isOpenClasses = '-right-64';
|
||||
if (props.isOpen) {
|
||||
isOpenClasses = props.isHistoryOpen
|
||||
? 'right-64'
|
||||
: 'right-0';
|
||||
}
|
||||
|
||||
const containers = [...props.symbols.values()];
|
||||
function Row({ index, style }: { index: number, style: React.CSSProperties }): JSX.Element {
|
||||
const container = containers[index];
|
||||
|
@ -46,17 +37,14 @@ export function SymbolsSidebar(props: ISymbolsSidebarProps): JSX.Element {
|
|||
}
|
||||
|
||||
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 className='bg-slate-100 font-bold sidebar-title'>
|
||||
Elements
|
||||
</div>
|
||||
<div className='h-96 text-gray-800'>
|
||||
<div>
|
||||
<div className='h-80 text-gray-800'>
|
||||
<List
|
||||
className='List divide-y divide-black'
|
||||
itemCount={containers.length}
|
||||
itemSize={35}
|
||||
height={384}
|
||||
width={256}
|
||||
height={320}
|
||||
width={'100%'}
|
||||
>
|
||||
{Row}
|
||||
</List>
|
|
@ -1,21 +1,22 @@
|
|||
import * as React from 'react';
|
||||
import { ElementsSidebar } from '../ElementsSidebar/ElementsSidebar';
|
||||
import { Sidebar } from '../Sidebar/Sidebar';
|
||||
import { ElementsList } from '../ElementsList/ElementsList';
|
||||
import { History } from '../History/History';
|
||||
import { IAvailableContainer } from '../../Interfaces/IAvailableContainer';
|
||||
import { IContainerModel } from '../../Interfaces/IContainerModel';
|
||||
import { IHistoryState } from '../../Interfaces/IHistoryState';
|
||||
import { CameraIcon, ArrowUpOnSquareIcon } from '@heroicons/react/24/outline';
|
||||
import { FloatingButton } from '../FloatingButton/FloatingButton';
|
||||
import { Bar } from '../Bar/Bar';
|
||||
import { IAvailableSymbol } from '../../Interfaces/IAvailableSymbol';
|
||||
import { Symbols } from '../Symbols/Symbols';
|
||||
import { SymbolsSidebar } from '../SymbolsSidebar/SymbolsSidebar';
|
||||
import { SymbolsSidebar } from '../SymbolsList/SymbolsList';
|
||||
import { PropertyType } from '../../Enums/PropertyType';
|
||||
import { MessagesSidebar } from '../MessagesSidebar/MessagesSidebar';
|
||||
import { Messages } from '../Messages/Messages';
|
||||
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
|
||||
current: IHistoryState
|
||||
history: IHistoryState[]
|
||||
|
@ -37,106 +38,157 @@ interface IUIProps {
|
|||
loadState: (move: number) => void
|
||||
}
|
||||
|
||||
function CloseOtherSidebars(
|
||||
setIsSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
setIsSymbolsOpen: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
setIsMessagesOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||
): void {
|
||||
setIsSidebarOpen(false);
|
||||
setIsSymbolsOpen(false);
|
||||
setIsMessagesOpen(false);
|
||||
export enum SidebarType {
|
||||
None,
|
||||
Components,
|
||||
Symbols,
|
||||
History,
|
||||
Messages,
|
||||
Settings
|
||||
}
|
||||
|
||||
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 {
|
||||
const [isSidebarOpen, setIsSidebarOpen] = React.useState(true);
|
||||
const [isSymbolsOpen, setIsSymbolsOpen] = React.useState(false);
|
||||
const [isHistoryOpen, setIsHistoryOpen] = React.useState(false);
|
||||
const [isMessagesOpen, setIsMessagesOpen] = React.useState(false);
|
||||
const [selectedSidebar, setSelectedSidebar] = React.useState<SidebarType>(SidebarType.Components);
|
||||
|
||||
let buttonRightOffsetClasses = 'right-12';
|
||||
if (isSidebarOpen || isHistoryOpen || isSymbolsOpen) {
|
||||
buttonRightOffsetClasses = 'right-72';
|
||||
// Please use setOrToggleSidebar rather than setSelectedSidebar so we can close the sidebar
|
||||
const setOrToggleSidebar = UseSetOrToggleSidebar(selectedSidebar, setSelectedSidebar);
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Bar
|
||||
isSidebarOpen={isSidebarOpen}
|
||||
isSymbolsOpen={isSymbolsOpen}
|
||||
isElementsSidebarOpen={isSidebarOpen}
|
||||
isHistoryOpen={isHistoryOpen}
|
||||
isMessagesOpen={isMessagesOpen}
|
||||
toggleSidebar={() => {
|
||||
CloseOtherSidebars(setIsSidebarOpen, setIsSymbolsOpen, setIsMessagesOpen);
|
||||
setIsSidebarOpen(!isSidebarOpen);
|
||||
isComponentsOpen={selectedSidebar === SidebarType.Components}
|
||||
isSymbolsOpen={selectedSidebar === SidebarType.Symbols}
|
||||
isHistoryOpen={selectedSidebar === SidebarType.History}
|
||||
isMessagesOpen={selectedSidebar === SidebarType.Messages}
|
||||
isSettingsOpen={selectedSidebar === SidebarType.Settings}
|
||||
toggleComponents={() => {
|
||||
setOrToggleSidebar(SidebarType.Components);
|
||||
} }
|
||||
toggleSymbols={() => {
|
||||
CloseOtherSidebars(setIsSidebarOpen, setIsSymbolsOpen, setIsMessagesOpen);
|
||||
setIsSymbolsOpen(!isSymbolsOpen);
|
||||
setOrToggleSidebar(SidebarType.Symbols);
|
||||
} }
|
||||
toggleTimeline={() => {
|
||||
setOrToggleSidebar(SidebarType.History);
|
||||
} }
|
||||
toggleTimeline={() => setIsHistoryOpen(!isHistoryOpen)}
|
||||
toggleMessages={() => {
|
||||
CloseOtherSidebars(setIsSidebarOpen, setIsSymbolsOpen, setIsMessagesOpen);
|
||||
setIsMessagesOpen(!isMessagesOpen);
|
||||
} }/>
|
||||
|
||||
setOrToggleSidebar(SidebarType.Messages);
|
||||
} }
|
||||
toggleSettings={() => {
|
||||
setOrToggleSidebar(SidebarType.Settings);
|
||||
} }
|
||||
/>
|
||||
<Sidebar
|
||||
className={`left-sidebar ${isLeftSidebarOpenClasses}`}
|
||||
title={leftSidebarTitle}
|
||||
>
|
||||
{ leftChildren }
|
||||
</Sidebar>
|
||||
<Viewer
|
||||
isLeftSidebarOpen={isLeftSidebarOpen}
|
||||
isRightSidebarOpen={isRightSidebarOpen}
|
||||
current={props.current}
|
||||
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}
|
||||
addContainer={props.addContainerAt}
|
||||
/>
|
||||
<SymbolsSidebar
|
||||
selectedSymbolId={props.current.selectedSymbolId}
|
||||
symbols={props.current.symbols}
|
||||
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}
|
||||
<Sidebar
|
||||
className={`right-sidebar ${isRightSidebarOpenClasses}`}
|
||||
title={rightSidebarTitle}
|
||||
>
|
||||
<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>
|
||||
{ rightChildren }
|
||||
</Sidebar>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
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;
|
||||
|
||||
@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 {
|
||||
@apply p-6 font-bold
|
||||
@apply p-3 md:p-5 font-bold h-12 md:h-16
|
||||
}
|
||||
|
||||
.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
|
||||
disabled:bg-slate-400 disabled:text-slate-500
|
||||
}
|
||||
|
||||
.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 {
|
||||
@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 {
|
||||
|
@ -60,8 +83,8 @@
|
|||
}
|
||||
|
||||
.bar {
|
||||
@apply fixed z-20 flex flex-col top-0 left-0
|
||||
h-full w-16 bg-slate-100
|
||||
@apply fixed z-30 flex flex-col top-0 left-0
|
||||
h-full w-16 bg-slate-100 shadow-sm
|
||||
}
|
||||
|
||||
.bar-btn {
|
||||
|
|
|
@ -12,7 +12,7 @@ import { Position } from '../Enums/Position';
|
|||
/// EDITOR DEFAULTS ///
|
||||
|
||||
/** 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) */
|
||||
export const DISABLE_API = false;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue