Skip to content
This repository has been archived by the owner on Feb 25, 2024. It is now read-only.

feat(viz): add orientation option to viz #360

Open
wants to merge 5 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/modern-ties-enjoy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'xstate-viz-app': minor
---

Add a feature to change orientation of the visualizer
32 changes: 15 additions & 17 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { useInterpretCanvas } from './useInterpretCanvas';
import router, { useRouter } from 'next/router';
import { parseEmbedQuery, withoutEmbedQueryParams } from './utils';
import { registryLinks } from './registryLinks';
import { VizOrientationProvider } from './visualizerOrientationContext';

const defaultHeadProps = {
title: 'XState Visualizer',
Expand Down Expand Up @@ -131,23 +132,20 @@ function App({ isEmbedded = false }: { isEmbedded?: boolean }) {
<EditorThemeProvider>
<PaletteProvider value={paletteService}>
<SimulationProvider value={simService}>
<Box
data-testid="app"
data-viz-theme="dark"
as="main"
display="grid"
gridTemplateColumns="1fr auto"
gridTemplateAreas={`"${getGridArea(embed)}"`}
height="100vh"
>
{!(embed?.isEmbedded && embed.mode === EmbedMode.Panels) && (
<CanvasProvider value={canvasService}>
<CanvasView />
</CanvasProvider>
)}
<PanelsView />
<MachineNameChooserModal />
</Box>
<VizOrientationProvider embed={embed}>
<>
{!(
embed?.isEmbedded && embed.mode === EmbedMode.Panels
) && (
<CanvasProvider value={canvasService}>
<CanvasView />
</CanvasProvider>
)}
<PanelsView />

<MachineNameChooserModal />
</>
</VizOrientationProvider>
</SimulationProvider>
</PaletteProvider>
</EditorThemeProvider>
Expand Down
6 changes: 4 additions & 2 deletions src/PanelsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ import { SpinnerWithText } from './SpinnerWithText';
import { StatePanel } from './StatePanel';
import { EmbedMode } from './types';
import { calculatePanelIndexByPanelName } from './utils';
import { useVizOrientation } from './visualizerOrientationContext';

export const PanelsView = (props: BoxProps) => {
const embed = useEmbed();
const simService = useSimulation();
const vizOrientation = useVizOrientation();
const services = useSelector(simService, selectServices);
const [sourceState, sendToSourceService] = useSourceActor();
const [activePanelIndex, setActiveTabIndex] = useState(() =>
Expand All @@ -40,10 +42,10 @@ export const PanelsView = (props: BoxProps) => {
}
}, [embed]);

return (
return vizOrientation.orientation === 'full' ? null : (
<ResizableBox
{...props}
gridArea="panels"
gridArea={vizOrientation.orientation !== 'top/bottom' ? 'panels' : ''}
minHeight={0}
disabled={embed?.isEmbedded && embed.mode !== EmbedMode.Full}
hidden={embed?.isEmbedded && embed.mode === EmbedMode.Viz}
Expand Down
56 changes: 41 additions & 15 deletions src/ResizableBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import {
PointDelta,
} from './dragSessionTracker';
import { Point } from './types';
import { useVizOrientation } from './visualizerOrientationContext';

const resizableModel = createModel(
{
ref: null as React.MutableRefObject<HTMLElement> | null,
widthDelta: 0,
heightDelta: 0,
},
{
events: {
Expand Down Expand Up @@ -51,6 +53,9 @@ const resizableMachine = resizableModel.createMachine({
widthDelta: (ctx, e) => {
return Math.max(0, ctx.widthDelta - e.delta.x);
},
heightDelta: (ctx, e) => {
return Math.max(0, ctx.heightDelta - e.delta.y);
},
}),
},
DRAG_SESSION_STOPPED: 'idle',
Expand All @@ -63,6 +68,7 @@ const ResizeHandle: React.FC<{
onChange: (width: number) => void;
}> = ({ onChange }) => {
const ref = useRef<HTMLDivElement>(null!);
const vizOrientation = useVizOrientation();

const [state] = useMachine(
resizableMachine.withContext({
Expand All @@ -72,20 +78,27 @@ const ResizeHandle: React.FC<{
);

useEffect(() => {
onChange(state.context.widthDelta);
}, [state.context.widthDelta, onChange]);
if (vizOrientation.orientation === 'top/bottom') {
onChange(state.context.heightDelta);
} else {
onChange(state.context.widthDelta);
}
}, [state.context.widthDelta, onChange, state.context.heightDelta]);

return (
<Box
ref={ref}
data-testid="resize-handle"
width="1"
width={vizOrientation.orientation === 'top/bottom' ? '100%' : '1px'}
css={{
position: 'absolute',
top: 0,
left: 0,
height: '100%',
cursor: 'ew-resize',
height: vizOrientation.orientation === 'top/bottom' ? '4px' : '100%',
cursor:
vizOrientation.orientation === 'top/bottom'
? 'ns-resize'
: 'ew-resize',
opacity: 0,
transition: 'opacity 0.2s ease',
}}
Expand All @@ -106,7 +119,7 @@ const ResizeHandle: React.FC<{
);
};

interface ResizableBoxProps extends Omit<BoxProps, 'width'> {
interface ResizableBoxProps extends Omit<BoxProps, 'width' | 'height'> {
disabled?: boolean;
}

Expand All @@ -116,20 +129,33 @@ export const ResizableBox: React.FC<ResizableBoxProps> = ({
...props
}) => {
const [widthDelta, setWidthDelta] = useState(0);
const [heightDelta, setHeightDelta] = useState(0);
const vizOrientation = useVizOrientation();
const width =
vizOrientation.orientation === 'top/bottom'
? '100%'
: `clamp(36rem, calc(36rem + ${widthDelta}px), 70vw)`;
const height =
vizOrientation.orientation === 'top/bottom'
? `clamp(36rem, calc(36rem + ${heightDelta}px), 90vh)`
: 'auto';

const handleSizeChange = (value: number) => {
if (vizOrientation.orientation === 'top/bottom') {
setHeightDelta(value);
} else {
setWidthDelta(value);
}
};

return (
// 35rem to avoid shortcut codes breaking
// into multiple lines
<Box
{...props}
style={
!disabled
? { width: `clamp(36rem, calc(36rem + ${widthDelta}px), 70vw)` }
: undefined
}
>
<Box {...props} style={!disabled ? { width, height } : undefined}>
{children}
{!disabled && <ResizeHandle onChange={(value) => setWidthDelta(value)} />}
{!disabled && (
<ResizeHandle onChange={(value) => handleSizeChange(value)} />
)}
</Box>
);
};
26 changes: 24 additions & 2 deletions src/SettingsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import { ThemeName, themes } from './editor-themes';
import { useEditorTheme } from './themeContext';
import { useSimulationMode } from './SimulationContext';
import { getPlatformMetaKeyLabel } from './utils';
import {
OrientationType,
useVizOrientation,
} from './visualizerOrientationContext';

const KeyboardShortcuts = () => (
<Box>
Expand Down Expand Up @@ -58,7 +62,8 @@ const KeyboardShortcuts = () => (
<Td>
<VStack alignItems="flex-start">
<span>
<Kbd>Up</Kbd> , <Kbd>Left</Kbd> , <Kbd>Down</Kbd> , <Kbd>Right</Kbd>
<Kbd>Up</Kbd> , <Kbd>Left</Kbd> , <Kbd>Down</Kbd> ,{' '}
<Kbd>Right</Kbd>
</span>
</VStack>
</Td>
Expand All @@ -68,7 +73,7 @@ const KeyboardShortcuts = () => (
</Tr>
<Tr>
<Td>
<Kbd>Shift</Kbd> + <Kbd>1</Kbd>
<Kbd>Shift</Kbd> + <Kbd>1</Kbd>
</Td>
<Td>Fit to content</Td>
</Tr>
Expand All @@ -90,6 +95,7 @@ const KeyboardShortcuts = () => (
export const SettingsPanel: React.FC = () => {
const editorTheme = useEditorTheme();
const simulationMode = useSimulationMode();
const vizOrientation = useVizOrientation();
return (
<VStack paddingY="5" spacing="7" alignItems="stretch">
{simulationMode === 'visualizing' && <KeyboardShortcuts />}
Expand All @@ -112,6 +118,22 @@ export const SettingsPanel: React.FC = () => {
))}
</Select>
</Box>
<Box>
<Heading as="h2" fontSize="l" marginBottom="5">
Visualizer orientation
</Heading>
<Select
maxWidth="fit-content"
defaultValue={vizOrientation.orientation}
onChange={(e) => {
const orientation = e.target.value as OrientationType;
vizOrientation.changeOrientation(orientation);
}}
>
<option value="left/right">left/right</option>
<option value="top/bottom">top/bottom</option>
</Select>
</Box>
</VStack>
);
};
73 changes: 73 additions & 0 deletions src/visualizerOrientationContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Box } from '@chakra-ui/react';
import React, { useMemo, useState } from 'react';
import { EmbedContext, EmbedMode } from './types';
import { createRequiredContext } from './utils';

export type OrientationType = 'top/bottom' | 'left/right' | 'full';

const [VizOrientationProviderLocal, useVizOrientation] = createRequiredContext<{
orientation: OrientationType;
changeOrientation(orientation: OrientationType): void;
}>('VisualizerOrientation');

export function VizOrientationProvider({
children,
embed,
}: {
children: React.ReactNode;
embed: EmbedContext;
}) {
const VIZ_LOCAL_KEY = 'xstate_viz_editor_orientation';
const vizLocalOrientation: OrientationType = window.localStorage.getItem(
VIZ_LOCAL_KEY,
) as OrientationType;
const [orientation, setOrientation] = useState<OrientationType>(
vizLocalOrientation ?? 'left/right',
);
const contextValue = useMemo(
() => ({ orientation, changeOrientation: setOrientation }),
[orientation],
);

React.useEffect(() => {
window.localStorage.setItem(VIZ_LOCAL_KEY, orientation);
}, [orientation]);

const getGridArea = (embed?: EmbedContext) => {
if (orientation === 'top/bottom') {
return '';
}

if (embed?.isEmbedded && embed.mode === EmbedMode.Viz) {
return 'canvas';
}

if (embed?.isEmbedded && embed.mode === EmbedMode.Panels) {
return 'panels';
}

return 'canvas panels';
};

return (
<VizOrientationProviderLocal value={contextValue}>
<Box
data-testid="app"
data-viz-theme="dark"
as="main"
display="grid"
gridTemplateColumns={
orientation === 'top/bottom' ? 'repeat(1, 1fr)' : '1fr auto'
}
gridTemplateRows={
orientation === 'top/bottom' ? 'repeat(2, 1fr)' : '1fr auto'
}
gridTemplateAreas={`"${getGridArea(embed)}"`}
height="100vh"
>
{children}
</Box>
</VizOrientationProviderLocal>
);
}
export { useVizOrientation };