Stylings
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
.root {
|
||||
height: fit-content;
|
||||
width: fit-content;
|
||||
min-width: 15ch;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.tiny {
|
||||
font-size: 0.5em;
|
||||
margin-top: -1.6em;
|
||||
font-size: 0.5em;
|
||||
}
|
||||
|
||||
.small {
|
||||
|
||||
@@ -5,10 +5,11 @@ import styles from './NodeOverview.module.css'
|
||||
import Link from 'next/link'
|
||||
import { EntityIcon } from '../../home/EntityIcon/EntityIcon'
|
||||
import { GraphNode } from '../../../src/graph-untangle/types'
|
||||
import { fixedEncodeURIComponent } from '../../../src/utils'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
export type OverviewGraphNode = GraphNode<{
|
||||
icons: (EnrichedEntity | string)[]
|
||||
linkOut: string
|
||||
}>
|
||||
|
||||
interface Props extends HTMLProps<HTMLDivElement> {
|
||||
@@ -16,11 +17,19 @@ interface Props extends HTMLProps<HTMLDivElement> {
|
||||
}
|
||||
|
||||
export const NodeOverview: FC<Props> = ({ node, className, ...props }) => {
|
||||
const router = useRouter()
|
||||
return (
|
||||
<div {...props} className={cx(className, styles.root)}>
|
||||
<h3>
|
||||
<span className={styles.linkOut}>
|
||||
<Link href={node.linkOut}>🔗</Link>
|
||||
<Link
|
||||
href={{
|
||||
pathname: `/visualize/${fixedEncodeURIComponent(node.name)}`,
|
||||
query: router.query
|
||||
}}
|
||||
>
|
||||
👁
|
||||
</Link>
|
||||
</span>
|
||||
{node.name}
|
||||
</h3>
|
||||
|
||||
@@ -7,8 +7,8 @@ import { DetailGraphNode, NodeDetails } from './NodeDetails/NodeDetails'
|
||||
import { groupBy, isNonNullable, uniquify } from '../../src/utils'
|
||||
import { EnrichedEntity } from '../../src/types'
|
||||
import Head from 'next/head'
|
||||
import { ScrollContainer } from '../shared/ScrollContainer/ScrollContainer'
|
||||
import { ProducingGraph } from '../shared/ProducingGraph/ProducingGraph'
|
||||
import { ScrollContainer } from './ScrollContainer/ScrollContainer'
|
||||
import { ProducingGraph } from './ProducingGraph/ProducingGraph'
|
||||
|
||||
export const PageDetails: FC = () => {
|
||||
const {
|
||||
|
||||
@@ -2,10 +2,9 @@ import { useGroups } from '../contexts/GroupProvider'
|
||||
import { useFactories } from '../../src/hooks/useFactories'
|
||||
import { FC, useMemo } from 'react'
|
||||
import { calculateInputs } from '../../src/calculateInputs'
|
||||
import { fixedEncodeURIComponent } from '../../src/utils'
|
||||
import Head from 'next/head'
|
||||
import { ScrollContainer } from '../shared/ScrollContainer/ScrollContainer'
|
||||
import { ProducingGraph } from '../shared/ProducingGraph/ProducingGraph'
|
||||
import { ScrollContainer } from './ScrollContainer/ScrollContainer'
|
||||
import { ProducingGraph } from './ProducingGraph/ProducingGraph'
|
||||
import { NodeOverview, OverviewGraphNode } from './NodeOverview/NodeOverview'
|
||||
|
||||
export const PageOverview: FC = () => {
|
||||
@@ -22,8 +21,7 @@ export const PageOverview: FC = () => {
|
||||
)[0],
|
||||
outputs: group.exports,
|
||||
name: group.name,
|
||||
icons: [...group.exports, ...group.malls],
|
||||
linkOut: `./visualize/${fixedEncodeURIComponent(group.name)}`
|
||||
icons: [...group.exports, ...group.malls]
|
||||
}))
|
||||
}, [baseFactories, exportedFactories, findFactory, groups])
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
.plane {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
flex-direction: column;
|
||||
padding: 2em;
|
||||
box-shadow: 0 0 0 1px red; /* Border left */
|
||||
gap: 10em;
|
||||
}
|
||||
|
||||
.svg {
|
||||
position: absolute;
|
||||
z-index: -10;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
stroke: black;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
gap: 2em;
|
||||
}
|
||||
|
||||
.node {
|
||||
padding: 0.5em;
|
||||
border: 1px solid #ddd;
|
||||
background-color: #eee;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.svg {
|
||||
stroke: white;
|
||||
}
|
||||
|
||||
.node {
|
||||
border-color: #444;
|
||||
background-color: #222;
|
||||
}
|
||||
}
|
||||
122
components/visualize/ProducingGraph/ProducingGraph.tsx
Normal file
122
components/visualize/ProducingGraph/ProducingGraph.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import { FC, HTMLProps, PropsWithChildren, useEffect, useRef, useState } from 'react'
|
||||
import styles from './ProducingGraph.module.css'
|
||||
import { EntityIcon } from '../../home/EntityIcon/EntityIcon'
|
||||
import { createPath, drawLine } from '../../../src/svg'
|
||||
import {
|
||||
AdditionalNode,
|
||||
GraphNode,
|
||||
GraphNodeWithIds,
|
||||
isAdditionalNode
|
||||
} from '../../../src/graph-untangle/types'
|
||||
import { graphUntangled } from '../../../src/graph-untangle'
|
||||
import { Dict } from '../../../src/types'
|
||||
|
||||
interface Props<T extends Dict<unknown>> {
|
||||
nodes: GraphNode<T>[]
|
||||
inputs: string[]
|
||||
outputs?: string[]
|
||||
childType: FC<HTMLProps<HTMLDivElement> & { node: GraphNode<T> }>
|
||||
}
|
||||
|
||||
export const ProducingGraph = <T extends Dict<unknown>>({
|
||||
nodes,
|
||||
inputs,
|
||||
outputs,
|
||||
childType: ChildType
|
||||
}: PropsWithChildren<Props<T>>) => {
|
||||
const planeRef = useRef<HTMLDivElement>(null)
|
||||
const [[rows, nodeMap]] = useState<[string[][], Dict<GraphNodeWithIds<T | AdditionalNode>>]>(
|
||||
graphUntangled(nodes, inputs, outputs ?? [], 3000)
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!planeRef.current) return
|
||||
const plane = planeRef.current
|
||||
function createSvgElement() {
|
||||
const elem = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
||||
elem.id = 'arrows'
|
||||
elem.setAttribute('class', styles.svg)
|
||||
elem.setAttributeNS(null, 'fill', 'none')
|
||||
plane.appendChild(elem)
|
||||
return elem
|
||||
}
|
||||
const svg = document.getElementById('arrows') ?? createSvgElement()
|
||||
const width = Math.round(plane.getBoundingClientRect().width)
|
||||
const height = Math.round(plane.getBoundingClientRect().height)
|
||||
svg.setAttribute('width', `${width}`)
|
||||
svg.setAttribute('height', `${height}`)
|
||||
svg.setAttributeNS(null, 'viewBox', `0 0 ${width} ${height}`)
|
||||
svg.replaceChildren()
|
||||
|
||||
const paths: string[] = []
|
||||
plane.querySelectorAll('[data-outputs]').forEach(startNode => {
|
||||
if (!(startNode instanceof HTMLElement)) return
|
||||
;(startNode.dataset.outputs?.split(' ') ?? []).forEach(output => {
|
||||
const endNode = plane.querySelector(`[data-name="${output}"]`)
|
||||
if (!(endNode instanceof HTMLElement)) return
|
||||
paths.push(
|
||||
createPath(
|
||||
plane.getBoundingClientRect(),
|
||||
startNode.getBoundingClientRect(),
|
||||
endNode.getBoundingClientRect()
|
||||
)
|
||||
)
|
||||
})
|
||||
})
|
||||
plane.querySelectorAll('[data-hidden="true"][data-outputs]').forEach(elem => {
|
||||
if (!(elem instanceof HTMLElement)) return
|
||||
paths.push(drawLine(plane.getBoundingClientRect(), elem.getBoundingClientRect()))
|
||||
})
|
||||
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
|
||||
path.setAttributeNS(null, 'd', paths.join(' '))
|
||||
svg.appendChild(path)
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={styles.plane} ref={planeRef}>
|
||||
{rows.map((row, colIdx) => (
|
||||
<div className={styles.row} key={colIdx} data-row={true}>
|
||||
{row.map(uid => {
|
||||
const node = nodeMap[uid]
|
||||
return !isAdditionalNode(node) ? (
|
||||
<span
|
||||
data-name={node.___uid}
|
||||
data-inputs={node.___uidInputs.join(' ')}
|
||||
data-outputs={node.___uidOutputs.join(' ')}
|
||||
key={node.___uid}
|
||||
data-hidden={node.___uidOutputs.length > 0}
|
||||
>
|
||||
<ChildType className={styles.node} node={node} />
|
||||
</span>
|
||||
) : node.___type === 'h' ? (
|
||||
<span
|
||||
className={styles.hidden}
|
||||
key={node.___uid}
|
||||
data-name={node.___uid}
|
||||
data-inputs={node.___uidInputs.join(' ')}
|
||||
data-outputs={node.___uidOutputs.join(' ')}
|
||||
data-hidden={true}
|
||||
></span>
|
||||
) : node.___type === 'i' ? (
|
||||
<span
|
||||
key={node.___uid}
|
||||
data-name={node.___uid}
|
||||
data-outputs={node.___uidOutputs.join(' ')}
|
||||
data-hidden={true}
|
||||
>
|
||||
<EntityIcon value={node.name} />
|
||||
</span>
|
||||
) : (
|
||||
<EntityIcon
|
||||
key={node.___uid}
|
||||
value={node.name}
|
||||
data-name={node.___uid}
|
||||
data-inputs={node.___uidInputs.join(' ')}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
.container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.inner {
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
padding: 2em;
|
||||
}
|
||||
25
components/visualize/ScrollContainer/ScrollContainer.tsx
Normal file
25
components/visualize/ScrollContainer/ScrollContainer.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { FC, PropsWithChildren, useEffect, useRef } from 'react'
|
||||
import IndianaDragScoll from 'react-indiana-drag-scroll'
|
||||
import styles from './ScrollContainer.module.css'
|
||||
|
||||
export const ScrollContainer: FC<PropsWithChildren> = ({ children }) => {
|
||||
const container = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (container.current) {
|
||||
container.current.oncontextmenu = e => e.preventDefault()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<IndianaDragScoll
|
||||
className={styles.container}
|
||||
buttons={[2]}
|
||||
innerRef={container}
|
||||
hideScrollbars={false}
|
||||
activationDistance={5}
|
||||
>
|
||||
<div className={styles.inner}>{children}</div>
|
||||
</IndianaDragScoll>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user