Spaces:
Running
Running
Color picker for groups.
Browse files
lynxkite-app/web/src/common.ts
CHANGED
@@ -5,3 +5,12 @@ export function usePath() {
|
|
5 |
const path = decodeURIComponent(useLocation().pathname).replace(/[/]$/, "");
|
6 |
return path;
|
7 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5 |
const path = decodeURIComponent(useLocation().pathname).replace(/[/]$/, "");
|
6 |
return path;
|
7 |
}
|
8 |
+
|
9 |
+
export const COLORS: { [key: string]: string } = {
|
10 |
+
gray: "oklch(95% 0 0)",
|
11 |
+
pink: "oklch(75% 0.2 0)",
|
12 |
+
orange: "oklch(75% 0.2 55)",
|
13 |
+
green: "oklch(75% 0.2 150)",
|
14 |
+
blue: "oklch(75% 0.2 230)",
|
15 |
+
purple: "oklch(75% 0.2 290)",
|
16 |
+
};
|
lynxkite-app/web/src/index.css
CHANGED
@@ -98,13 +98,30 @@ body {
|
|
98 |
opacity: 1;
|
99 |
}
|
100 |
|
101 |
-
.
|
102 |
-
.react-flow__node-group.selectable.selected,
|
103 |
-
.react-flow__node-group:hover {
|
104 |
box-shadow: 0px 3px 30px 0px rgba(0, 0, 0, 0.3);
|
105 |
border-radius: 20px;
|
106 |
border: none;
|
107 |
-
background-color:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
108 |
}
|
109 |
|
110 |
.tooltip {
|
@@ -121,6 +138,10 @@ body {
|
|
121 |
max-width: 300px;
|
122 |
}
|
123 |
|
|
|
|
|
|
|
|
|
124 |
.expanded .lynxkite-node {
|
125 |
height: 100%;
|
126 |
}
|
@@ -545,14 +566,14 @@ body {
|
|
545 |
z-index: -10 !important;
|
546 |
}
|
547 |
|
548 |
-
.selected.
|
549 |
.selected .comment-view,
|
550 |
.selected .lynxkite-node {
|
551 |
outline: var(--xy-selection-border, var(--xy-selection-border-default));
|
552 |
outline-offset: 7.5px;
|
553 |
}
|
554 |
|
555 |
-
.selected.
|
556 |
outline-offset: 20px;
|
557 |
}
|
558 |
|
|
|
98 |
opacity: 1;
|
99 |
}
|
100 |
|
101 |
+
.node-group {
|
|
|
|
|
102 |
box-shadow: 0px 3px 30px 0px rgba(0, 0, 0, 0.3);
|
103 |
border-radius: 20px;
|
104 |
border: none;
|
105 |
+
background-color: white;
|
106 |
+
opacity: 0.9;
|
107 |
+
display: flex;
|
108 |
+
flex-direction: column;
|
109 |
+
align-items: end;
|
110 |
+
padding: 10px 20px;
|
111 |
+
}
|
112 |
+
|
113 |
+
.node-group-color-picker-icon {
|
114 |
+
font-size: 30px;
|
115 |
+
opacity: 0.1;
|
116 |
+
transition: opacity 0.3s;
|
117 |
+
}
|
118 |
+
|
119 |
+
.node-group:hover .node-group-color-picker-icon {
|
120 |
+
opacity: 1;
|
121 |
+
}
|
122 |
+
|
123 |
+
.color-picker-button {
|
124 |
+
font-size: 30px;
|
125 |
}
|
126 |
|
127 |
.tooltip {
|
|
|
138 |
max-width: 300px;
|
139 |
}
|
140 |
|
141 |
+
.prose p {
|
142 |
+
margin-bottom: 0;
|
143 |
+
}
|
144 |
+
|
145 |
.expanded .lynxkite-node {
|
146 |
height: 100%;
|
147 |
}
|
|
|
566 |
z-index: -10 !important;
|
567 |
}
|
568 |
|
569 |
+
.selected .node-group,
|
570 |
.selected .comment-view,
|
571 |
.selected .lynxkite-node {
|
572 |
outline: var(--xy-selection-border, var(--xy-selection-border-default));
|
573 |
outline-offset: 7.5px;
|
574 |
}
|
575 |
|
576 |
+
.selected .node-group {
|
577 |
outline-offset: 20px;
|
578 |
}
|
579 |
|
lynxkite-app/web/src/workspace/Workspace.tsx
CHANGED
@@ -42,6 +42,7 @@ import LynxKiteEdge from "./LynxKiteEdge.tsx";
|
|
42 |
import { LynxKiteState } from "./LynxKiteState";
|
43 |
import NodeSearch, { type OpsOp, type Catalog, type Catalogs } from "./NodeSearch.tsx";
|
44 |
import NodeWithGraphCreationView from "./nodes/GraphCreationNode.tsx";
|
|
|
45 |
import NodeWithComment from "./nodes/NodeWithComment.tsx";
|
46 |
import NodeWithImage from "./nodes/NodeWithImage.tsx";
|
47 |
import NodeWithMolecule from "./nodes/NodeWithMolecule.tsx";
|
@@ -83,7 +84,7 @@ function LynxKiteFlow() {
|
|
83 |
if (!state.workspace.nodes) return;
|
84 |
if (!state.workspace.edges) return;
|
85 |
for (const n of state.workspace.nodes) {
|
86 |
-
if (n.type !== "
|
87 |
n.dragHandle = ".drag-handle";
|
88 |
}
|
89 |
}
|
@@ -190,6 +191,7 @@ function LynxKiteFlow() {
|
|
190 |
graph_creation_view: NodeWithGraphCreationView,
|
191 |
molecule: NodeWithMolecule,
|
192 |
comment: NodeWithComment,
|
|
|
193 |
}),
|
194 |
[],
|
195 |
);
|
@@ -364,7 +366,7 @@ function LynxKiteFlow() {
|
|
364 |
const selectedNodes = nodes.filter((n) => n.selected);
|
365 |
const groupNode = {
|
366 |
id: findFreeId("Group"),
|
367 |
-
type: "
|
368 |
position: { x: 0, y: 0 },
|
369 |
width: 0,
|
370 |
height: 0,
|
@@ -418,7 +420,7 @@ function LynxKiteFlow() {
|
|
418 |
}
|
419 |
function ungroupSelection() {
|
420 |
const groups = Object.fromEntries(
|
421 |
-
nodes.filter((n) => n.selected && n.type === "
|
422 |
);
|
423 |
setNodes(
|
424 |
nodes
|
@@ -451,7 +453,7 @@ function LynxKiteFlow() {
|
|
451 |
});
|
452 |
}
|
453 |
const areMultipleNodesSelected = nodes.filter((n) => n.selected).length > 1;
|
454 |
-
const isAnyGroupSelected = nodes.some((n) => n.selected && n.type === "
|
455 |
return (
|
456 |
<div className="workspace">
|
457 |
<div className="top-bar bg-neutral">
|
|
|
42 |
import { LynxKiteState } from "./LynxKiteState";
|
43 |
import NodeSearch, { type OpsOp, type Catalog, type Catalogs } from "./NodeSearch.tsx";
|
44 |
import NodeWithGraphCreationView from "./nodes/GraphCreationNode.tsx";
|
45 |
+
import Group from "./nodes/Group.tsx";
|
46 |
import NodeWithComment from "./nodes/NodeWithComment.tsx";
|
47 |
import NodeWithImage from "./nodes/NodeWithImage.tsx";
|
48 |
import NodeWithMolecule from "./nodes/NodeWithMolecule.tsx";
|
|
|
84 |
if (!state.workspace.nodes) return;
|
85 |
if (!state.workspace.edges) return;
|
86 |
for (const n of state.workspace.nodes) {
|
87 |
+
if (n.type !== "node_group" && n.dragHandle !== ".drag-handle") {
|
88 |
n.dragHandle = ".drag-handle";
|
89 |
}
|
90 |
}
|
|
|
191 |
graph_creation_view: NodeWithGraphCreationView,
|
192 |
molecule: NodeWithMolecule,
|
193 |
comment: NodeWithComment,
|
194 |
+
node_group: Group,
|
195 |
}),
|
196 |
[],
|
197 |
);
|
|
|
366 |
const selectedNodes = nodes.filter((n) => n.selected);
|
367 |
const groupNode = {
|
368 |
id: findFreeId("Group"),
|
369 |
+
type: "node_group",
|
370 |
position: { x: 0, y: 0 },
|
371 |
width: 0,
|
372 |
height: 0,
|
|
|
420 |
}
|
421 |
function ungroupSelection() {
|
422 |
const groups = Object.fromEntries(
|
423 |
+
nodes.filter((n) => n.selected && n.type === "node_group").map((n) => [n.id, n]),
|
424 |
);
|
425 |
setNodes(
|
426 |
nodes
|
|
|
453 |
});
|
454 |
}
|
455 |
const areMultipleNodesSelected = nodes.filter((n) => n.selected).length > 1;
|
456 |
+
const isAnyGroupSelected = nodes.some((n) => n.selected && n.type === "node_group");
|
457 |
return (
|
458 |
<div className="workspace">
|
459 |
<div className="top-bar bg-neutral">
|
lynxkite-app/web/src/workspace/nodes/Group.tsx
ADDED
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { cursorTo } from "node:readline";
|
2 |
+
import { useReactFlow } from "@xyflow/react";
|
3 |
+
import { useState } from "react";
|
4 |
+
// @ts-ignore
|
5 |
+
import Palette from "~icons/tabler/palette-filled.jsx";
|
6 |
+
// @ts-ignore
|
7 |
+
import Square from "~icons/tabler/square-filled.jsx";
|
8 |
+
import Tooltip from "../../Tooltip.tsx";
|
9 |
+
import { COLORS } from "../../common.ts";
|
10 |
+
|
11 |
+
export default function Group(props: any) {
|
12 |
+
const reactFlow = useReactFlow();
|
13 |
+
const [displayingColorPicker, setDisplayingColorPicker] = useState(false);
|
14 |
+
function setColor(newValue: string) {
|
15 |
+
reactFlow.updateNodeData(props.id, (prevData: any) => ({
|
16 |
+
...prevData,
|
17 |
+
params: { color: newValue },
|
18 |
+
}));
|
19 |
+
setDisplayingColorPicker(false);
|
20 |
+
}
|
21 |
+
function toggleColorPicker(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) {
|
22 |
+
console.log("toggleColorPicker", displayingColorPicker);
|
23 |
+
e.stopPropagation();
|
24 |
+
setDisplayingColorPicker(!displayingColorPicker);
|
25 |
+
}
|
26 |
+
const currentColor = props.data?.params?.color || "gray";
|
27 |
+
return (
|
28 |
+
<div
|
29 |
+
className="node-group"
|
30 |
+
style={{
|
31 |
+
width: props.width,
|
32 |
+
height: props.height,
|
33 |
+
backgroundColor: COLORS[currentColor],
|
34 |
+
opacity: 0.9,
|
35 |
+
}}
|
36 |
+
>
|
37 |
+
<button className="node-group-color-picker-icon" onClick={toggleColorPicker}>
|
38 |
+
<Tooltip doc="Change color">
|
39 |
+
<Palette />
|
40 |
+
</Tooltip>
|
41 |
+
</button>
|
42 |
+
{displayingColorPicker && <ColorPicker currentColor={currentColor} onPick={setColor} />}
|
43 |
+
</div>
|
44 |
+
);
|
45 |
+
}
|
46 |
+
|
47 |
+
function ColorPicker(props: { currentColor: string; onPick: (color: string) => void }) {
|
48 |
+
const colors = Object.keys(COLORS).filter((color) => color !== props.currentColor);
|
49 |
+
return (
|
50 |
+
<div className="color-picker">
|
51 |
+
{colors.map((color) => (
|
52 |
+
<button
|
53 |
+
key={color}
|
54 |
+
className="color-picker-button"
|
55 |
+
style={{ color: COLORS[color] }}
|
56 |
+
onClick={() => props.onPick(color)}
|
57 |
+
>
|
58 |
+
<Square />
|
59 |
+
</button>
|
60 |
+
))}
|
61 |
+
</div>
|
62 |
+
);
|
63 |
+
}
|
lynxkite-app/web/src/workspace/nodes/LynxKiteNode.tsx
CHANGED
@@ -12,6 +12,7 @@ import Help from "~icons/tabler/question-mark.jsx";
|
|
12 |
// @ts-ignore
|
13 |
import Skull from "~icons/tabler/skull.jsx";
|
14 |
import Tooltip from "../../Tooltip";
|
|
|
15 |
|
16 |
interface LynxKiteNodeProps {
|
17 |
id: string;
|
@@ -20,6 +21,7 @@ interface LynxKiteNodeProps {
|
|
20 |
nodeStyle: any;
|
21 |
data: any;
|
22 |
children: any;
|
|
|
23 |
}
|
24 |
|
25 |
function getHandles(inputs: any[], outputs: any[]) {
|
@@ -53,15 +55,6 @@ function getHandles(inputs: any[], outputs: any[]) {
|
|
53 |
return handles;
|
54 |
}
|
55 |
|
56 |
-
const OP_COLORS: { [key: string]: string } = {
|
57 |
-
gray: "oklch(95% 0 0)",
|
58 |
-
pink: "oklch(75% 0.2 0)",
|
59 |
-
orange: "oklch(75% 0.2 55)",
|
60 |
-
green: "oklch(75% 0.2 150)",
|
61 |
-
blue: "oklch(75% 0.2 230)",
|
62 |
-
purple: "oklch(75% 0.2 290)",
|
63 |
-
};
|
64 |
-
|
65 |
function LynxKiteNodeComponent(props: LynxKiteNodeProps) {
|
66 |
const reactFlow = useReactFlow();
|
67 |
const data = props.data;
|
@@ -78,7 +71,7 @@ function LynxKiteNodeComponent(props: LynxKiteNodeProps) {
|
|
78 |
};
|
79 |
const titleStyle: { backgroundColor?: string } = {};
|
80 |
if (data.meta?.value?.color) {
|
81 |
-
titleStyle.backgroundColor =
|
82 |
}
|
83 |
return (
|
84 |
<div
|
|
|
12 |
// @ts-ignore
|
13 |
import Skull from "~icons/tabler/skull.jsx";
|
14 |
import Tooltip from "../../Tooltip";
|
15 |
+
import { COLORS } from "../../common.ts";
|
16 |
|
17 |
interface LynxKiteNodeProps {
|
18 |
id: string;
|
|
|
21 |
nodeStyle: any;
|
22 |
data: any;
|
23 |
children: any;
|
24 |
+
parentId?: string;
|
25 |
}
|
26 |
|
27 |
function getHandles(inputs: any[], outputs: any[]) {
|
|
|
55 |
return handles;
|
56 |
}
|
57 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
58 |
function LynxKiteNodeComponent(props: LynxKiteNodeProps) {
|
59 |
const reactFlow = useReactFlow();
|
60 |
const data = props.data;
|
|
|
71 |
};
|
72 |
const titleStyle: { backgroundColor?: string } = {};
|
73 |
if (data.meta?.value?.color) {
|
74 |
+
titleStyle.backgroundColor = COLORS[data.meta.value.color] || data.meta.value.color;
|
75 |
}
|
76 |
return (
|
77 |
<div
|