Merge pull request 'Add steps to the editor' (#114) from editor/steps into master
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
Reviewed-on: #114pull/116/head
commit
cca7ee1b1b
Before Width: | Height: | Size: 405 B After Width: | Height: | Size: 216 B |
@ -0,0 +1,56 @@
|
|||||||
|
import React, { ReactNode, useCallback, useRef, useState } from "react"
|
||||||
|
|
||||||
|
export interface SplitLayoutProps {
|
||||||
|
children: [ReactNode, ReactNode]
|
||||||
|
rightWidth: number
|
||||||
|
onRightWidthChange: (w: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SplitLayout({
|
||||||
|
children,
|
||||||
|
rightWidth,
|
||||||
|
onRightWidthChange,
|
||||||
|
}: SplitLayoutProps) {
|
||||||
|
const curtainRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const resize = useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
|
const sliderPosX = e.clientX
|
||||||
|
const curtainWidth =
|
||||||
|
curtainRef.current!.getBoundingClientRect().width
|
||||||
|
|
||||||
|
onRightWidthChange((sliderPosX / curtainWidth) * 100)
|
||||||
|
},
|
||||||
|
[curtainRef, onRightWidthChange],
|
||||||
|
)
|
||||||
|
|
||||||
|
const [resizing, setResizing] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={"curtain"}
|
||||||
|
ref={curtainRef}
|
||||||
|
style={{ display: "flex" }}
|
||||||
|
onMouseMove={resizing ? resize : undefined}
|
||||||
|
onMouseUp={() => setResizing(false)}>
|
||||||
|
<div className={"curtain-left"} style={{ width: `${rightWidth}%` }}>
|
||||||
|
{children[0]}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
onMouseDown={() => setResizing(true)}
|
||||||
|
style={{
|
||||||
|
width: 4,
|
||||||
|
height: "100%",
|
||||||
|
backgroundColor: "grey",
|
||||||
|
cursor: "col-resize",
|
||||||
|
userSelect: "none",
|
||||||
|
}}></div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={"curtain-right"}
|
||||||
|
style={{ width: `${100 - rightWidth}%` }}>
|
||||||
|
{children[1]}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,154 @@
|
|||||||
|
import "../../style/steps_tree.css"
|
||||||
|
import { StepInfoNode } from "../../model/tactic/Tactic"
|
||||||
|
import BendableArrow from "../arrows/BendableArrow"
|
||||||
|
import { ReactNode, useMemo, useRef } from "react"
|
||||||
|
import AddSvg from "../../assets/icon/add.svg?react"
|
||||||
|
import RemoveSvg from "../../assets/icon/remove.svg?react"
|
||||||
|
import { getStepName } from "../../editor/StepsDomain.ts"
|
||||||
|
|
||||||
|
export interface StepsTreeProps {
|
||||||
|
root: StepInfoNode
|
||||||
|
selectedStepId: number
|
||||||
|
onAddChildren: (parent: StepInfoNode) => void
|
||||||
|
onRemoveNode: (node: StepInfoNode) => void
|
||||||
|
onStepSelected: (node: StepInfoNode) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function StepsTree({
|
||||||
|
root,
|
||||||
|
selectedStepId,
|
||||||
|
onAddChildren,
|
||||||
|
onRemoveNode,
|
||||||
|
onStepSelected,
|
||||||
|
}: StepsTreeProps) {
|
||||||
|
return (
|
||||||
|
<div className="steps-tree">
|
||||||
|
<StepsTreeNode
|
||||||
|
node={root}
|
||||||
|
rootNode={root}
|
||||||
|
selectedStepId={selectedStepId}
|
||||||
|
onAddChildren={onAddChildren}
|
||||||
|
onRemoveNode={onRemoveNode}
|
||||||
|
onStepSelected={onStepSelected}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StepsTreeContentProps {
|
||||||
|
node: StepInfoNode
|
||||||
|
rootNode: StepInfoNode
|
||||||
|
|
||||||
|
selectedStepId: number
|
||||||
|
onAddChildren: (parent: StepInfoNode) => void
|
||||||
|
onRemoveNode: (node: StepInfoNode) => void
|
||||||
|
onStepSelected: (node: StepInfoNode) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function StepsTreeNode({
|
||||||
|
node,
|
||||||
|
rootNode,
|
||||||
|
selectedStepId,
|
||||||
|
onAddChildren,
|
||||||
|
onRemoveNode,
|
||||||
|
onStepSelected,
|
||||||
|
}: StepsTreeContentProps) {
|
||||||
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref} className={"step-group"}>
|
||||||
|
{node.children.map((child) => (
|
||||||
|
<BendableArrow
|
||||||
|
key={child.id}
|
||||||
|
area={ref}
|
||||||
|
startPos={"step-piece-" + node.id}
|
||||||
|
segments={[
|
||||||
|
{
|
||||||
|
next: "step-piece-" + child.id,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onSegmentsChanges={() => {}}
|
||||||
|
forceStraight={true}
|
||||||
|
wavy={false}
|
||||||
|
//TODO remove magic constants
|
||||||
|
startRadius={10}
|
||||||
|
endRadius={10}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<StepPiece
|
||||||
|
id={node.id}
|
||||||
|
isSelected={selectedStepId === node.id}
|
||||||
|
onAddButtonClicked={() => onAddChildren(node)}
|
||||||
|
onRemoveButtonClicked={
|
||||||
|
rootNode.id === node.id
|
||||||
|
? undefined
|
||||||
|
: () => onRemoveNode(node)
|
||||||
|
}
|
||||||
|
onSelected={() => onStepSelected(node)}>
|
||||||
|
<p>
|
||||||
|
{useMemo(
|
||||||
|
() => getStepName(rootNode, node.id),
|
||||||
|
[node.id, rootNode],
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</StepPiece>
|
||||||
|
<div className={"step-children"}>
|
||||||
|
{node.children.map((child) => (
|
||||||
|
<StepsTreeNode
|
||||||
|
key={child.id}
|
||||||
|
rootNode={rootNode}
|
||||||
|
selectedStepId={selectedStepId}
|
||||||
|
node={child}
|
||||||
|
onAddChildren={onAddChildren}
|
||||||
|
onRemoveNode={onRemoveNode}
|
||||||
|
onStepSelected={onStepSelected}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StepPieceProps {
|
||||||
|
id: number
|
||||||
|
isSelected: boolean
|
||||||
|
onAddButtonClicked?: () => void
|
||||||
|
onRemoveButtonClicked?: () => void
|
||||||
|
onSelected: () => void
|
||||||
|
children?: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
function StepPiece({
|
||||||
|
id,
|
||||||
|
isSelected,
|
||||||
|
onAddButtonClicked,
|
||||||
|
onRemoveButtonClicked,
|
||||||
|
onSelected,
|
||||||
|
children,
|
||||||
|
}: StepPieceProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id={"step-piece-" + id}
|
||||||
|
tabIndex={1}
|
||||||
|
className={
|
||||||
|
"step-piece " + (isSelected ? "step-piece-selected" : "")
|
||||||
|
}
|
||||||
|
onClick={onSelected}>
|
||||||
|
<div className="step-piece-actions">
|
||||||
|
{onAddButtonClicked && (
|
||||||
|
<AddSvg
|
||||||
|
onClick={onAddButtonClicked}
|
||||||
|
className={"add-icon"}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{onRemoveButtonClicked && (
|
||||||
|
<RemoveSvg
|
||||||
|
onClick={onRemoveButtonClicked}
|
||||||
|
className={"remove-icon"}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,112 @@
|
|||||||
|
import { StepInfoNode } from "../model/tactic/Tactic"
|
||||||
|
|
||||||
|
export function addStepNode(
|
||||||
|
root: StepInfoNode,
|
||||||
|
parent: StepInfoNode,
|
||||||
|
child: StepInfoNode,
|
||||||
|
): StepInfoNode {
|
||||||
|
if (root.id === parent.id) {
|
||||||
|
return {
|
||||||
|
...root,
|
||||||
|
children: root.children.concat(child),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...root,
|
||||||
|
children: root.children.map((c) => addStepNode(c, parent, child)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStepName(root: StepInfoNode, step: number): string {
|
||||||
|
let ord = 1
|
||||||
|
const nodes = [root]
|
||||||
|
while (nodes.length > 0) {
|
||||||
|
const node = nodes.pop()!
|
||||||
|
|
||||||
|
if (node.id === step) break
|
||||||
|
|
||||||
|
ord++
|
||||||
|
nodes.push(...[...node.children].reverse())
|
||||||
|
}
|
||||||
|
|
||||||
|
return ord.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStepNode(
|
||||||
|
root: StepInfoNode,
|
||||||
|
stepId: number,
|
||||||
|
): StepInfoNode | undefined {
|
||||||
|
if (root.id === stepId) return root
|
||||||
|
|
||||||
|
for (const child of root.children) {
|
||||||
|
const result = getStepNode(child, stepId)
|
||||||
|
if (result) return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeStepNode(
|
||||||
|
root: StepInfoNode,
|
||||||
|
targetId: number,
|
||||||
|
): StepInfoNode | undefined {
|
||||||
|
const path = getPathTo(root, targetId)
|
||||||
|
|
||||||
|
path.reverse()
|
||||||
|
|
||||||
|
const [removedNode, ...pathToRoot] = path
|
||||||
|
|
||||||
|
let child = removedNode
|
||||||
|
|
||||||
|
for (const node of pathToRoot) {
|
||||||
|
child = {
|
||||||
|
id: node.id,
|
||||||
|
children: node.children.flatMap((c) => {
|
||||||
|
if (c.id === removedNode.id) return []
|
||||||
|
else if (c.id === child.id) {
|
||||||
|
return [child]
|
||||||
|
}
|
||||||
|
return [c]
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return child
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPathTo(
|
||||||
|
root: StepInfoNode,
|
||||||
|
targetId: number,
|
||||||
|
): StepInfoNode[] {
|
||||||
|
if (root.id === targetId) return [root]
|
||||||
|
|
||||||
|
for (const child of root.children) {
|
||||||
|
const subPath = getPathTo(child, targetId)
|
||||||
|
if (subPath.length > 0) return [root, ...subPath]
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an available identifier that is not already present into the given node tree
|
||||||
|
* @param root
|
||||||
|
*/
|
||||||
|
export function getAvailableId(root: StepInfoNode): number {
|
||||||
|
const acc = (root: StepInfoNode): number =>
|
||||||
|
Math.max(root.id, ...root.children.map(acc))
|
||||||
|
return acc(root) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getParent(
|
||||||
|
root: StepInfoNode,
|
||||||
|
node: StepInfoNode,
|
||||||
|
): StepInfoNode | null {
|
||||||
|
if (root.children.find((n) => n.id === node.id)) return root
|
||||||
|
|
||||||
|
for (const child of root.children) {
|
||||||
|
const result = getParent(child, node)
|
||||||
|
if (result != null) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,84 @@
|
|||||||
|
import { TacticService, ServiceError, TacticContext } from "./TacticService.ts"
|
||||||
|
import { StepContent, StepInfoNode } from "../model/tactic/Tactic.ts"
|
||||||
|
import { fetchAPI, fetchAPIGet } from "../Fetcher.ts"
|
||||||
|
|
||||||
|
export class APITacticService implements TacticService {
|
||||||
|
private readonly tacticId: number
|
||||||
|
|
||||||
|
constructor(tacticId: number) {
|
||||||
|
this.tacticId = tacticId
|
||||||
|
}
|
||||||
|
|
||||||
|
async getContext(): Promise<TacticContext | ServiceError> {
|
||||||
|
const infoResponsePromise = fetchAPIGet(`tactics/${this.tacticId}`)
|
||||||
|
const treeResponsePromise = fetchAPIGet(`tactics/${this.tacticId}/tree`)
|
||||||
|
|
||||||
|
const infoResponse = await infoResponsePromise
|
||||||
|
const treeResponse = await treeResponsePromise
|
||||||
|
|
||||||
|
if (infoResponse.status == 401 || treeResponse.status == 401) {
|
||||||
|
return ServiceError.UNAUTHORIZED
|
||||||
|
}
|
||||||
|
const { name, courtType } = await infoResponse.json()
|
||||||
|
const { root } = await treeResponse.json()
|
||||||
|
|
||||||
|
return { courtType, name, stepsTree: root }
|
||||||
|
}
|
||||||
|
|
||||||
|
async addStep(
|
||||||
|
parent: StepInfoNode,
|
||||||
|
content: StepContent,
|
||||||
|
): Promise<StepInfoNode | ServiceError> {
|
||||||
|
const response = await fetchAPI(`tactics/${this.tacticId}/steps`, {
|
||||||
|
parentId: parent.id,
|
||||||
|
content,
|
||||||
|
})
|
||||||
|
if (response.status == 404) return ServiceError.NOT_FOUND
|
||||||
|
if (response.status == 401) return ServiceError.UNAUTHORIZED
|
||||||
|
|
||||||
|
const { stepId } = await response.json()
|
||||||
|
return { id: stepId, children: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeStep(id: number): Promise<void | ServiceError> {
|
||||||
|
const response = await fetchAPI(
|
||||||
|
`tactics/${this.tacticId}/steps/${id}`,
|
||||||
|
{},
|
||||||
|
"DELETE",
|
||||||
|
)
|
||||||
|
if (response.status == 404) return ServiceError.NOT_FOUND
|
||||||
|
if (response.status == 401) return ServiceError.UNAUTHORIZED
|
||||||
|
}
|
||||||
|
|
||||||
|
async setName(name: string): Promise<void | ServiceError> {
|
||||||
|
const response = await fetchAPI(
|
||||||
|
`tactics/${this.tacticId}/name`,
|
||||||
|
{ name },
|
||||||
|
"PUT",
|
||||||
|
)
|
||||||
|
if (response.status == 404) return ServiceError.NOT_FOUND
|
||||||
|
if (response.status == 401) return ServiceError.UNAUTHORIZED
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveContent(
|
||||||
|
step: number,
|
||||||
|
content: StepContent,
|
||||||
|
): Promise<void | ServiceError> {
|
||||||
|
const response = await fetchAPI(
|
||||||
|
`tactics/${this.tacticId}/steps/${step}`,
|
||||||
|
{ content },
|
||||||
|
"PUT",
|
||||||
|
)
|
||||||
|
if (response.status == 404) return ServiceError.NOT_FOUND
|
||||||
|
if (response.status == 401) return ServiceError.UNAUTHORIZED
|
||||||
|
}
|
||||||
|
|
||||||
|
async getContent(step: number): Promise<StepContent | ServiceError> {
|
||||||
|
const response = await fetchAPIGet(
|
||||||
|
`tactics/${this.tacticId}/steps/${step}`,
|
||||||
|
)
|
||||||
|
if (response.status == 404) return ServiceError.NOT_FOUND
|
||||||
|
if (response.status == 401) return ServiceError.UNAUTHORIZED
|
||||||
|
return await response.json()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,99 @@
|
|||||||
|
import { TacticService, ServiceError, TacticContext } from "./TacticService.ts"
|
||||||
|
import { StepContent, StepInfoNode } from "../model/tactic/Tactic.ts"
|
||||||
|
import {
|
||||||
|
addStepNode,
|
||||||
|
getAvailableId,
|
||||||
|
removeStepNode,
|
||||||
|
} from "../editor/StepsDomain.ts"
|
||||||
|
|
||||||
|
const GUEST_MODE_STEP_CONTENT_STORAGE_KEY = "guest_mode_step"
|
||||||
|
const GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY = "guest_mode_step_tree"
|
||||||
|
const GUEST_MODE_TITLE_STORAGE_KEY = "guest_mode_title"
|
||||||
|
|
||||||
|
export class LocalStorageTacticService implements TacticService {
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
static init(): LocalStorageTacticService {
|
||||||
|
const root = localStorage.getItem(
|
||||||
|
GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (root === null) {
|
||||||
|
localStorage.setItem(
|
||||||
|
GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY,
|
||||||
|
JSON.stringify(<StepInfoNode>{ id: 1, children: [] }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new LocalStorageTacticService()
|
||||||
|
}
|
||||||
|
|
||||||
|
async getContext(): Promise<TacticContext | ServiceError> {
|
||||||
|
const stepsTree: StepInfoNode = JSON.parse(
|
||||||
|
localStorage.getItem(GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY)!,
|
||||||
|
)
|
||||||
|
const name =
|
||||||
|
localStorage.getItem(GUEST_MODE_TITLE_STORAGE_KEY) ??
|
||||||
|
"Nouvelle Tactique"
|
||||||
|
return {
|
||||||
|
stepsTree,
|
||||||
|
name,
|
||||||
|
courtType: "PLAIN",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async addStep(
|
||||||
|
parent: StepInfoNode,
|
||||||
|
content: StepContent,
|
||||||
|
): Promise<StepInfoNode | ServiceError> {
|
||||||
|
const root: StepInfoNode = JSON.parse(
|
||||||
|
localStorage.getItem(GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY)!,
|
||||||
|
)
|
||||||
|
|
||||||
|
const nodeId = getAvailableId(root)
|
||||||
|
const node = { id: nodeId, children: [] }
|
||||||
|
|
||||||
|
const resultTree = addStepNode(root, parent, node)
|
||||||
|
|
||||||
|
localStorage.setItem(
|
||||||
|
GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY,
|
||||||
|
JSON.stringify(resultTree),
|
||||||
|
)
|
||||||
|
localStorage.setItem(
|
||||||
|
GUEST_MODE_STEP_CONTENT_STORAGE_KEY + node.id,
|
||||||
|
JSON.stringify(content),
|
||||||
|
)
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
async getContent(step: number): Promise<StepContent | ServiceError> {
|
||||||
|
const content = localStorage.getItem(
|
||||||
|
GUEST_MODE_STEP_CONTENT_STORAGE_KEY + step,
|
||||||
|
)
|
||||||
|
return content ? JSON.parse(content) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeStep(id: number): Promise<void | ServiceError> {
|
||||||
|
const root: StepInfoNode = JSON.parse(
|
||||||
|
localStorage.getItem(GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY)!,
|
||||||
|
)
|
||||||
|
localStorage.setItem(
|
||||||
|
GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY,
|
||||||
|
JSON.stringify(removeStepNode(root, id)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveContent(
|
||||||
|
step: number,
|
||||||
|
content: StepContent,
|
||||||
|
): Promise<void | ServiceError> {
|
||||||
|
localStorage.setItem(
|
||||||
|
GUEST_MODE_STEP_CONTENT_STORAGE_KEY + step,
|
||||||
|
JSON.stringify(content),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async setName(name: string): Promise<void | ServiceError> {
|
||||||
|
localStorage.setItem(GUEST_MODE_TITLE_STORAGE_KEY, name)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
import { CourtType, StepContent, StepInfoNode } from "../model/tactic/Tactic.ts"
|
||||||
|
|
||||||
|
export interface TacticContext {
|
||||||
|
stepsTree: StepInfoNode
|
||||||
|
name: string
|
||||||
|
courtType: CourtType
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ServiceError {
|
||||||
|
UNAUTHORIZED = "UNAUTHORIZED",
|
||||||
|
NOT_FOUND = "NOT_FOUND",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TacticService {
|
||||||
|
getContext(): Promise<TacticContext | ServiceError>
|
||||||
|
|
||||||
|
addStep(
|
||||||
|
parent: StepInfoNode,
|
||||||
|
content: StepContent,
|
||||||
|
): Promise<StepInfoNode | ServiceError>
|
||||||
|
|
||||||
|
removeStep(id: number): Promise<void | ServiceError>
|
||||||
|
|
||||||
|
setName(name: string): Promise<void | ServiceError>
|
||||||
|
|
||||||
|
saveContent(
|
||||||
|
step: number,
|
||||||
|
content: StepContent,
|
||||||
|
): Promise<void | ServiceError>
|
||||||
|
|
||||||
|
getContent(step: number): Promise<StepContent | ServiceError>
|
||||||
|
}
|
@ -0,0 +1,91 @@
|
|||||||
|
.step-piece {
|
||||||
|
position: relative;
|
||||||
|
font-family: monospace;
|
||||||
|
pointer-events: all;
|
||||||
|
|
||||||
|
background-color: var(--editor-tree-step-piece);
|
||||||
|
color: var(--selected-team-secondarycolor);
|
||||||
|
|
||||||
|
border-radius: 100px;
|
||||||
|
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
user-select: none;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
border: 2px solid var(--editor-tree-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-piece-selected {
|
||||||
|
border: 2px solid var(--selection-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-piece-selected,
|
||||||
|
.step-piece:focus,
|
||||||
|
.step-piece:hover {
|
||||||
|
background-color: var(--editor-tree-step-piece-hovered);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-piece-selected .step-piece-actions,
|
||||||
|
.step-piece:hover .step-piece-actions,
|
||||||
|
.step-piece:focus-within .step-piece-actions {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-piece-actions {
|
||||||
|
visibility: hidden;
|
||||||
|
display: flex;
|
||||||
|
position: absolute;
|
||||||
|
column-gap: 5px;
|
||||||
|
top: -140%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-icon,
|
||||||
|
.remove-icon {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-icon {
|
||||||
|
fill: var(--add-icon-fill);
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-icon {
|
||||||
|
fill: var(--remove-icon-fill);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-children {
|
||||||
|
margin-top: 10vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-group {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps-tree {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 10%;
|
||||||
|
|
||||||
|
height: 100%;
|
||||||
|
}
|
Loading…
Reference in new issue