mirror of
https://github.com/roc-lang/roc.git
synced 2025-09-26 21:39:07 +00:00
More layout options, and focus on variable on re-selection
This commit is contained in:
parent
734111711d
commit
bea445bafa
2 changed files with 169 additions and 94 deletions
|
@ -5,14 +5,22 @@ import { assertExhaustive } from "../../utils/exhaustive";
|
||||||
import { contentStyles } from "../Content";
|
import { contentStyles } from "../Content";
|
||||||
import { VariableElPretty } from "../Common/Variable";
|
import { VariableElPretty } from "../Common/Variable";
|
||||||
import { SubsSnapshot, TypeDescriptor } from "../../engine/subs";
|
import { SubsSnapshot, TypeDescriptor } from "../../engine/subs";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { TypedEmitter } from "tiny-typed-emitter";
|
||||||
|
|
||||||
type AddSubVariableLink = (from: Variable, subVariable: Variable) => void;
|
type AddSubVariableLink = (from: Variable, subVariable: Variable) => void;
|
||||||
|
|
||||||
|
export interface VariableMessageEvents {
|
||||||
|
focus: (variable: Variable) => void;
|
||||||
|
}
|
||||||
|
|
||||||
export interface VariableNodeProps {
|
export interface VariableNodeProps {
|
||||||
data: {
|
data: {
|
||||||
subs: SubsSnapshot;
|
subs: SubsSnapshot;
|
||||||
variable: Variable;
|
variable: Variable;
|
||||||
addSubVariableLink: AddSubVariableLink;
|
addSubVariableLink: AddSubVariableLink;
|
||||||
|
isOutlined: boolean;
|
||||||
|
ee: TypedEmitter<VariableMessageEvents>;
|
||||||
};
|
};
|
||||||
targetPosition?: Position;
|
targetPosition?: Position;
|
||||||
sourcePosition?: Position;
|
sourcePosition?: Position;
|
||||||
|
@ -23,7 +31,33 @@ export default function VariableNode({
|
||||||
targetPosition,
|
targetPosition,
|
||||||
sourcePosition,
|
sourcePosition,
|
||||||
}: VariableNodeProps): JSX.Element {
|
}: VariableNodeProps): JSX.Element {
|
||||||
const { variable, subs, addSubVariableLink } = data;
|
const {
|
||||||
|
variable,
|
||||||
|
subs,
|
||||||
|
addSubVariableLink,
|
||||||
|
isOutlined: isOutlinedProp,
|
||||||
|
ee: eeProp,
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
const [isOutlined, setIsOutlined] = useState(isOutlinedProp);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
eeProp.on("focus", (focusVar: Variable) => {
|
||||||
|
if (focusVar !== variable) return;
|
||||||
|
setIsOutlined(true);
|
||||||
|
});
|
||||||
|
}, [eeProp, variable]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOutlined) return;
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setIsOutlined(false);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
};
|
||||||
|
}, [isOutlined]);
|
||||||
|
|
||||||
const desc = subs.get_root(variable);
|
const desc = subs.get_root(variable);
|
||||||
const styles = contentStyles(desc);
|
const styles = contentStyles(desc);
|
||||||
|
@ -54,7 +88,8 @@ export default function VariableNode({
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
styles.bg,
|
styles.bg,
|
||||||
"bg-opacity-50 py-2 px-4 rounded-lg border",
|
"bg-opacity-50 py-2 px-4 rounded-lg border transition ease-in-out duration-700",
|
||||||
|
isOutlined && "ring-2 ring-blue-500",
|
||||||
"text-center font-mono"
|
"text-center font-mono"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
|
@ -19,14 +19,18 @@ import ReactFlow, {
|
||||||
ReactFlowState,
|
ReactFlowState,
|
||||||
Position,
|
Position,
|
||||||
} from "reactflow";
|
} from "reactflow";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { Variable } from "../../schema";
|
import { Variable } from "../../schema";
|
||||||
|
|
||||||
import "reactflow/dist/style.css";
|
import "reactflow/dist/style.css";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import VariableNode, { VariableNodeProps } from "./VariableNode";
|
import VariableNode, {
|
||||||
|
VariableMessageEvents,
|
||||||
|
VariableNodeProps,
|
||||||
|
} from "./VariableNode";
|
||||||
import { SubsSnapshot } from "../../engine/subs";
|
import { SubsSnapshot } from "../../engine/subs";
|
||||||
import { KeydownHandler } from "../Events";
|
import { KeydownHandler } from "../Events";
|
||||||
|
import { TypedEmitter } from "tiny-typed-emitter";
|
||||||
|
|
||||||
export interface VariablesGraphProps {
|
export interface VariablesGraphProps {
|
||||||
subs: SubsSnapshot;
|
subs: SubsSnapshot;
|
||||||
|
@ -34,36 +38,10 @@ export interface VariablesGraphProps {
|
||||||
onKeydown: (handler: KeydownHandler) => void;
|
onKeydown: (handler: KeydownHandler) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum GraphDirection {
|
function horizontalityToPositions(isHorizontal: boolean): {
|
||||||
LeftRight,
|
|
||||||
TopBottom,
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_DIRECTION: GraphDirection = GraphDirection.TopBottom;
|
|
||||||
|
|
||||||
function directionToElkDirection(direction: GraphDirection): string {
|
|
||||||
switch (direction) {
|
|
||||||
case GraphDirection.TopBottom:
|
|
||||||
return "DOWN";
|
|
||||||
case GraphDirection.LeftRight:
|
|
||||||
return "RIGHT";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function horizontalDirectionality(direction: GraphDirection): boolean {
|
|
||||||
switch (direction) {
|
|
||||||
case GraphDirection.TopBottom:
|
|
||||||
return false;
|
|
||||||
case GraphDirection.LeftRight:
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function directionalityToPositions(direction: GraphDirection): {
|
|
||||||
targetPosition: Position;
|
targetPosition: Position;
|
||||||
sourcePosition: Position;
|
sourcePosition: Position;
|
||||||
} {
|
} {
|
||||||
const isHorizontal = horizontalDirectionality(direction);
|
|
||||||
return {
|
return {
|
||||||
targetPosition: isHorizontal ? Position.Left : Position.Top,
|
targetPosition: isHorizontal ? Position.Left : Position.Top,
|
||||||
sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
|
sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
|
||||||
|
@ -75,17 +53,69 @@ interface LayoutedElements {
|
||||||
edges: Edge[];
|
edges: Edge[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ComputeLayoutedElementsProps extends LayoutedElements {
|
const LAYOUT_CONFIG_DOWN = {
|
||||||
direction: GraphDirection;
|
keypress: "j",
|
||||||
}
|
emoji: "⬇️",
|
||||||
|
elkLayoutOptions: {
|
||||||
|
"elk.algorithm": "layered",
|
||||||
|
"elk.direction": "DOWN",
|
||||||
|
},
|
||||||
|
isHorizontal: false,
|
||||||
|
} as const;
|
||||||
|
const LAYOUT_CONFIG_RIGHT = {
|
||||||
|
keypress: "l",
|
||||||
|
emoji: "➡️",
|
||||||
|
elkLayoutOptions: {
|
||||||
|
"elk.algorithm": "layered",
|
||||||
|
"elk.direction": "RIGHT",
|
||||||
|
},
|
||||||
|
isHorizontal: true,
|
||||||
|
} as const;
|
||||||
|
const LAYOUT_CONFIG_RADIAL = {
|
||||||
|
keypress: "r",
|
||||||
|
emoji: "🌐",
|
||||||
|
elkLayoutOptions: {
|
||||||
|
"elk.algorithm": "radial",
|
||||||
|
},
|
||||||
|
isHorizontal: false,
|
||||||
|
} as const;
|
||||||
|
const LAYOUT_CONFIG_FORCE = {
|
||||||
|
keypress: "f",
|
||||||
|
emoji: "🧲",
|
||||||
|
elkLayoutOptions: {
|
||||||
|
"elk.algorithm": "force",
|
||||||
|
},
|
||||||
|
isHorizontal: false,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
type LayoutConfiguration =
|
||||||
|
| typeof LAYOUT_CONFIG_DOWN
|
||||||
|
| typeof LAYOUT_CONFIG_RIGHT
|
||||||
|
| typeof LAYOUT_CONFIG_RADIAL
|
||||||
|
| typeof LAYOUT_CONFIG_FORCE;
|
||||||
|
|
||||||
|
const LAYOUT_CONFIGURATIONS: LayoutConfiguration[] = [
|
||||||
|
LAYOUT_CONFIG_DOWN,
|
||||||
|
LAYOUT_CONFIG_RIGHT,
|
||||||
|
LAYOUT_CONFIG_RADIAL,
|
||||||
|
LAYOUT_CONFIG_FORCE,
|
||||||
|
];
|
||||||
|
|
||||||
|
type ComputeElkLayoutOptions = Pick<
|
||||||
|
LayoutConfiguration,
|
||||||
|
"elkLayoutOptions" | "isHorizontal"
|
||||||
|
>;
|
||||||
|
|
||||||
|
interface ComputeLayoutedElementsProps
|
||||||
|
extends LayoutedElements,
|
||||||
|
ComputeElkLayoutOptions {}
|
||||||
|
|
||||||
// Elk has a *huge* amount of options to configure. To see everything you can
|
// Elk has a *huge* amount of options to configure. To see everything you can
|
||||||
// tweak check out:
|
// tweak check out:
|
||||||
//
|
//
|
||||||
// - https://www.eclipse.org/elk/reference/algorithms.html
|
// - https://www.eclipse.org/elk/reference/algorithms.html
|
||||||
// - https://www.eclipse.org/elk/reference/options.html
|
// - https://www.eclipse.org/elk/reference/options.html
|
||||||
const elkOptions: LayoutOptions = {
|
const baseElkOptions: LayoutOptions = {
|
||||||
"elk.algorithm": "layered",
|
|
||||||
"elk.layered.spacing.nodeNodeBetweenLayers": "100",
|
"elk.layered.spacing.nodeNodeBetweenLayers": "100",
|
||||||
"elk.spacing.nodeNode": "80",
|
"elk.spacing.nodeNode": "80",
|
||||||
};
|
};
|
||||||
|
@ -93,7 +123,8 @@ const elkOptions: LayoutOptions = {
|
||||||
async function computeLayoutedElements({
|
async function computeLayoutedElements({
|
||||||
nodes,
|
nodes,
|
||||||
edges,
|
edges,
|
||||||
direction,
|
elkLayoutOptions,
|
||||||
|
isHorizontal,
|
||||||
}: ComputeLayoutedElementsProps): Promise<LayoutedElements> {
|
}: ComputeLayoutedElementsProps): Promise<LayoutedElements> {
|
||||||
if (nodes.length === 0) {
|
if (nodes.length === 0) {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
|
@ -102,14 +133,12 @@ async function computeLayoutedElements({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const isHorizontal = horizontalDirectionality(direction);
|
|
||||||
|
|
||||||
const elk = new ELK();
|
const elk = new ELK();
|
||||||
const graph: ElkNode = {
|
const graph: ElkNode = {
|
||||||
id: "root",
|
id: "root",
|
||||||
layoutOptions: {
|
layoutOptions: {
|
||||||
...elkOptions,
|
...baseElkOptions,
|
||||||
"elk.direction": directionToElkDirection(direction),
|
...elkLayoutOptions,
|
||||||
},
|
},
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
children: nodes.map((node) => ({
|
children: nodes.map((node) => ({
|
||||||
|
@ -150,17 +179,20 @@ const NODE_TYPES: NodeTypes = {
|
||||||
variable: VariableNode,
|
variable: VariableNode,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type VariableNodeData = VariableNodeProps["data"];
|
||||||
|
type RFVariableNode = Node<VariableNodeData>;
|
||||||
|
|
||||||
function newVariable(
|
function newVariable(
|
||||||
id: string,
|
id: string,
|
||||||
data: VariableNodeProps["data"],
|
data: VariableNodeData,
|
||||||
direction: GraphDirection
|
isHorizontal: boolean
|
||||||
): Node {
|
): RFVariableNode {
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
position: { x: 0, y: 0 },
|
position: { x: 0, y: 0 },
|
||||||
type: "variable",
|
type: "variable",
|
||||||
data,
|
data,
|
||||||
...directionalityToPositions(direction),
|
...horizontalityToPositions(isHorizontal),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -199,11 +231,7 @@ const nodesSetInViewSelector = (state: ReactFlowState) =>
|
||||||
);
|
);
|
||||||
|
|
||||||
type RedoLayoutFn = () => Promise<void>;
|
type RedoLayoutFn = () => Promise<void>;
|
||||||
function useRedoLayout({
|
function useRedoLayout(options: ComputeElkLayoutOptions): RedoLayoutFn {
|
||||||
direction,
|
|
||||||
}: {
|
|
||||||
direction: GraphDirection;
|
|
||||||
}): RedoLayoutFn {
|
|
||||||
const nodeCount = useStore(nodeCountSelector);
|
const nodeCount = useStore(nodeCountSelector);
|
||||||
const nodesInitialized = useStore(nodesSetInViewSelector);
|
const nodesInitialized = useStore(nodesSetInViewSelector);
|
||||||
const { getNodes, setNodes, getEdges } = useReactFlow();
|
const { getNodes, setNodes, getEdges } = useReactFlow();
|
||||||
|
@ -216,7 +244,7 @@ function useRedoLayout({
|
||||||
const { nodes } = await computeLayoutedElements({
|
const { nodes } = await computeLayoutedElements({
|
||||||
nodes: getNodes(),
|
nodes: getNodes(),
|
||||||
edges: getEdges(),
|
edges: getEdges(),
|
||||||
direction,
|
...options,
|
||||||
});
|
});
|
||||||
setNodes(nodes);
|
setNodes(nodes);
|
||||||
window.requestAnimationFrame(() => {
|
window.requestAnimationFrame(() => {
|
||||||
|
@ -227,15 +255,15 @@ function useRedoLayout({
|
||||||
nodesInitialized,
|
nodesInitialized,
|
||||||
getNodes,
|
getNodes,
|
||||||
getEdges,
|
getEdges,
|
||||||
direction,
|
options,
|
||||||
setNodes,
|
setNodes,
|
||||||
instance,
|
instance,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Does positioning of the nodes in the graph.
|
// Does positioning of the nodes in the graph.
|
||||||
function useAutoLayout({ direction }: { direction: GraphDirection }) {
|
function useAutoLayout(options: ComputeElkLayoutOptions) {
|
||||||
const redoLayout = useRedoLayout({ direction });
|
const redoLayout = useRedoLayout(options);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// This wrapping is of course redundant, but exercised for the purpose of
|
// This wrapping is of course redundant, but exercised for the purpose of
|
||||||
|
@ -244,38 +272,40 @@ function useAutoLayout({ direction }: { direction: GraphDirection }) {
|
||||||
await redoLayout();
|
await redoLayout();
|
||||||
}
|
}
|
||||||
inner();
|
inner();
|
||||||
}, [direction, redoLayout]);
|
}, [redoLayout]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function useKeydown({
|
function useKeydown({
|
||||||
direction,
|
layoutConfig,
|
||||||
|
setLayoutConfig,
|
||||||
onKeydown,
|
onKeydown,
|
||||||
setDirection,
|
|
||||||
}: {
|
}: {
|
||||||
direction: GraphDirection;
|
layoutConfig: LayoutConfiguration;
|
||||||
setDirection: React.Dispatch<React.SetStateAction<GraphDirection>>;
|
setLayoutConfig: React.Dispatch<React.SetStateAction<LayoutConfiguration>>;
|
||||||
onKeydown: (handler: KeydownHandler) => void;
|
onKeydown: (handler: KeydownHandler) => void;
|
||||||
}) {
|
}) {
|
||||||
const redoLayout = useRedoLayout({ direction });
|
const redoLayout = useRedoLayout(layoutConfig);
|
||||||
|
|
||||||
const keyDownHandler = useCallback(
|
const keyDownHandler = useCallback(
|
||||||
async (key: string) => {
|
async (key: string) => {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case "c": {
|
case "c": {
|
||||||
await redoLayout();
|
await redoLayout();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "j": {
|
|
||||||
setDirection(GraphDirection.TopBottom);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
case "l": {
|
|
||||||
setDirection(GraphDirection.LeftRight);
|
for (const config of LAYOUT_CONFIGURATIONS) {
|
||||||
break;
|
if (key === config.keypress) {
|
||||||
|
setLayoutConfig(config);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[redoLayout, setDirection]
|
[redoLayout, setLayoutConfig]
|
||||||
);
|
);
|
||||||
onKeydown(async (key) => {
|
onKeydown(async (key) => {
|
||||||
await keyDownHandler(key);
|
await keyDownHandler(key);
|
||||||
|
@ -290,14 +320,21 @@ function Graph({
|
||||||
const initialNodes: Node[] = [];
|
const initialNodes: Node[] = [];
|
||||||
const initialEdges: Edge[] = [];
|
const initialEdges: Edge[] = [];
|
||||||
|
|
||||||
const [direction, setDirection] = useState(DEFAULT_DIRECTION);
|
const ee = useRef(new TypedEmitter<VariableMessageEvents>());
|
||||||
|
|
||||||
|
const [layoutConfig, setLayoutConfig] =
|
||||||
|
useState<LayoutConfiguration>(LAYOUT_CONFIG_DOWN);
|
||||||
const [elements, setElements] = useState<LayoutedElements>({
|
const [elements, setElements] = useState<LayoutedElements>({
|
||||||
nodes: initialNodes,
|
nodes: initialNodes,
|
||||||
edges: initialEdges,
|
edges: initialEdges,
|
||||||
});
|
});
|
||||||
|
|
||||||
useAutoLayout({ direction });
|
useAutoLayout(layoutConfig);
|
||||||
useKeydown({ direction, onKeydown, setDirection });
|
useKeydown({
|
||||||
|
layoutConfig,
|
||||||
|
setLayoutConfig,
|
||||||
|
onKeydown,
|
||||||
|
});
|
||||||
|
|
||||||
const onNodesChange = useCallback((changes: NodeChange[]) => {
|
const onNodesChange = useCallback((changes: NodeChange[]) => {
|
||||||
setElements(({ nodes, edges }) => {
|
setElements(({ nodes, edges }) => {
|
||||||
|
@ -332,8 +369,10 @@ function Graph({
|
||||||
subs,
|
subs,
|
||||||
variable: subLinkN,
|
variable: subLinkN,
|
||||||
addSubVariableLink,
|
addSubVariableLink,
|
||||||
|
isOutlined: true,
|
||||||
|
ee: ee.current,
|
||||||
},
|
},
|
||||||
direction
|
layoutConfig.isHorizontal
|
||||||
),
|
),
|
||||||
nodes
|
nodes
|
||||||
);
|
);
|
||||||
|
@ -351,8 +390,10 @@ function Graph({
|
||||||
|
|
||||||
return { nodes: newNodes, edges: newEdges };
|
return { nodes: newNodes, edges: newEdges };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ee.current.emit("focus", subLinkN);
|
||||||
},
|
},
|
||||||
[direction, subs]
|
[layoutConfig, subs]
|
||||||
);
|
);
|
||||||
|
|
||||||
const addNode = useCallback(
|
const addNode = useCallback(
|
||||||
|
@ -368,8 +409,10 @@ function Graph({
|
||||||
subs,
|
subs,
|
||||||
variable: variableN,
|
variable: variableN,
|
||||||
addSubVariableLink,
|
addSubVariableLink,
|
||||||
|
isOutlined: true,
|
||||||
|
ee: ee.current,
|
||||||
},
|
},
|
||||||
direction
|
layoutConfig.isHorizontal
|
||||||
),
|
),
|
||||||
nodes
|
nodes
|
||||||
);
|
);
|
||||||
|
@ -379,8 +422,10 @@ function Graph({
|
||||||
|
|
||||||
return { nodes: newNodes, edges: edges };
|
return { nodes: newNodes, edges: edges };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ee.current.emit("focus", variableN);
|
||||||
},
|
},
|
||||||
[subs, addSubVariableLink, direction]
|
[subs, addSubVariableLink, layoutConfig]
|
||||||
);
|
);
|
||||||
|
|
||||||
onVariable(addNode);
|
onVariable(addNode);
|
||||||
|
@ -401,9 +446,9 @@ function Graph({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Panel position="top-right">
|
<Panel position="top-right">
|
||||||
<DirectionPanel
|
<LayoutPanel
|
||||||
direction={direction}
|
layoutConfig={layoutConfig}
|
||||||
onChange={(e) => setDirection(e)}
|
setLayoutConfig={setLayoutConfig}
|
||||||
/>
|
/>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
|
@ -412,37 +457,32 @@ function Graph({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DirectionPanelProps {
|
interface LayoutPanelProps {
|
||||||
direction: GraphDirection;
|
layoutConfig: LayoutConfiguration;
|
||||||
onChange: (direction: GraphDirection) => void;
|
setLayoutConfig: React.Dispatch<React.SetStateAction<LayoutConfiguration>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DirectionPanel({
|
function LayoutPanel({
|
||||||
direction,
|
layoutConfig,
|
||||||
onChange,
|
setLayoutConfig,
|
||||||
}: DirectionPanelProps): JSX.Element {
|
}: LayoutPanelProps): JSX.Element {
|
||||||
const commonStyle = "rounded cursor-pointer text-2xl select-none";
|
const commonStyle = "rounded cursor-pointer text-2xl select-none";
|
||||||
|
|
||||||
const dirs: { dir: GraphDirection; text: string }[] = [
|
|
||||||
{ dir: GraphDirection.TopBottom, text: "⬇️" },
|
|
||||||
{ dir: GraphDirection.LeftRight, text: "➡️" },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{dirs.map(({ dir, text }, i) => (
|
{LAYOUT_CONFIGURATIONS.map((config, i) => (
|
||||||
<span
|
<span
|
||||||
key={i}
|
key={i}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
commonStyle,
|
commonStyle,
|
||||||
i !== 0 ? "ml-2" : "",
|
i !== 0 ? "ml-2" : "",
|
||||||
dir !== direction ? "opacity-50" : ""
|
config !== layoutConfig ? "opacity-50" : ""
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onChange(dir);
|
setLayoutConfig(config);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{text}
|
{config.emoji}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue