mirror of
https://github.com/stan-smith/FossFLOW.git
synced 2025-12-24 06:58:48 -05:00
feat: allows color selection for nodes
This commit is contained in:
22
src/components/ColorSelector/ColorSelector.tsx
Normal file
22
src/components/ColorSelector/ColorSelector.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import { ColorSwatch } from './ColorSwatch';
|
||||
|
||||
interface Props {
|
||||
colors: string[];
|
||||
onChange: (color: string) => void;
|
||||
activeColor: string;
|
||||
}
|
||||
|
||||
export const ColorSelector = ({ colors, onChange, activeColor }: Props) => (
|
||||
<Box>
|
||||
{colors.map((color) => (
|
||||
<ColorSwatch
|
||||
key={color}
|
||||
hex={color}
|
||||
onClick={() => onChange(color)}
|
||||
isActive={activeColor === color}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
32
src/components/ColorSelector/ColorSwatch.tsx
Normal file
32
src/components/ColorSelector/ColorSwatch.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import { Box, Button } from '@mui/material';
|
||||
|
||||
export type Props = {
|
||||
hex: string;
|
||||
isActive?: boolean;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
export const ColorSwatch = ({ hex, onClick, isActive }: Props) => (
|
||||
<Button
|
||||
onClick={onClick}
|
||||
variant="text"
|
||||
size="small"
|
||||
sx={{ width: 40, height: 40, minWidth: 'auto' }}
|
||||
>
|
||||
<Box>
|
||||
<Box
|
||||
sx={{
|
||||
border: '1px solid',
|
||||
borderColor: isActive ? 'grey.400' : 'grey.800',
|
||||
bgcolor: hex,
|
||||
width: 28,
|
||||
height: 28,
|
||||
trasformOrigin: 'center',
|
||||
transform: `scale(${isActive ? 1.25 : 1})`,
|
||||
borderRadius: '100%'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Button>
|
||||
);
|
||||
@@ -44,6 +44,7 @@ export const NodeControls = ({ nodeId }: Props) => {
|
||||
>
|
||||
{tab === 0 && (
|
||||
<NodeSettings
|
||||
color={node.color}
|
||||
label={node.label}
|
||||
labelHeight={node.labelHeight}
|
||||
onUpdate={onNodeUpdated}
|
||||
|
||||
@@ -1,34 +1,53 @@
|
||||
import React from 'react';
|
||||
import { Slider } from '@mui/material';
|
||||
import { Slider, useTheme } from '@mui/material';
|
||||
import { Node } from 'src/stores/useSceneStore';
|
||||
import { ColorSelector } from 'src/components/ColorSelector/ColorSelector';
|
||||
import { MarkdownEditor } from '../../../MarkdownEditor/MarkdownEditor';
|
||||
|
||||
import { Section } from '../../components/Section';
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
labelHeight: number;
|
||||
onUpdate: (updates: Partial<Node>) => void;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export const NodeSettings = ({ label, labelHeight, onUpdate }: Props) => (
|
||||
<>
|
||||
<Section title="Label">
|
||||
<MarkdownEditor
|
||||
value={label}
|
||||
onChange={(text) => onUpdate({ label: text })}
|
||||
/>
|
||||
</Section>
|
||||
<Section title="Label height">
|
||||
<Slider
|
||||
marks
|
||||
step={20}
|
||||
min={0}
|
||||
max={200}
|
||||
value={labelHeight}
|
||||
onChange={(e, newHeight) =>
|
||||
onUpdate({ labelHeight: newHeight as number })
|
||||
}
|
||||
/>
|
||||
</Section>
|
||||
</>
|
||||
);
|
||||
export const NodeSettings = ({
|
||||
color,
|
||||
label,
|
||||
labelHeight,
|
||||
onUpdate
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section title="Label">
|
||||
<MarkdownEditor
|
||||
value={label}
|
||||
onChange={(text) => onUpdate({ label: text })}
|
||||
/>
|
||||
</Section>
|
||||
<Section title="Label height">
|
||||
<Slider
|
||||
marks
|
||||
step={20}
|
||||
min={0}
|
||||
max={200}
|
||||
value={labelHeight}
|
||||
onChange={(e, newHeight) =>
|
||||
onUpdate({ labelHeight: newHeight as number })
|
||||
}
|
||||
/>
|
||||
</Section>
|
||||
<Section title="Color">
|
||||
<ColorSelector
|
||||
activeColor={color}
|
||||
colors={Object.values(theme.customVars.diagramPalette)}
|
||||
onChange={(col) => onUpdate({ color: col })}
|
||||
/>
|
||||
</Section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,11 +6,17 @@ interface Props {
|
||||
value: string;
|
||||
onChange?: (value: string) => void;
|
||||
readOnly?: boolean;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
const tools = ['bold', 'italic', 'underline', 'strike', 'bullet', 'link'];
|
||||
|
||||
export const MarkdownEditor = ({ value, onChange, readOnly }: Props) => {
|
||||
export const MarkdownEditor = ({
|
||||
value,
|
||||
onChange,
|
||||
readOnly,
|
||||
height = 120
|
||||
}: Props) => {
|
||||
const modules = useMemo(() => {
|
||||
if (!readOnly)
|
||||
return {
|
||||
@@ -33,7 +39,7 @@ export const MarkdownEditor = ({ value, onChange, readOnly }: Props) => {
|
||||
borderColor: 'grey.800',
|
||||
borderTop: 'auto',
|
||||
borderRadius: 1.5,
|
||||
height: 200
|
||||
height
|
||||
},
|
||||
'.ql-container.ql-snow': {
|
||||
...(readOnly ? { border: 'none' } : {})
|
||||
|
||||
@@ -43,15 +43,15 @@ export const Node = ({ node, parentContainer }: NodeProps) => {
|
||||
updateHeight: updateLabelHeight,
|
||||
setVisible: setLabelConnectorVisible
|
||||
} = labelConnector;
|
||||
const { init: initNodeTile } = nodeTile;
|
||||
const { init: initNodeTile, updateColor } = nodeTile;
|
||||
|
||||
useEffect(() => {
|
||||
const nodeIconContainer = initNodeIcon();
|
||||
const labelConnectorContainer = initLabelConnector();
|
||||
const nodeColorContainer = initNodeTile();
|
||||
const nodeTileContainer = initNodeTile();
|
||||
|
||||
groupRef.current.removeChildren();
|
||||
groupRef.current.addChild(nodeColorContainer);
|
||||
groupRef.current.addChild(nodeTileContainer);
|
||||
groupRef.current.addChild(labelConnectorContainer);
|
||||
groupRef.current.addChild(nodeIconContainer);
|
||||
groupRef.current.pivot = nodeIconContainer.bounds.bottomCenter;
|
||||
@@ -112,6 +112,10 @@ export const Node = ({ node, parentContainer }: NodeProps) => {
|
||||
updateLabelHeight(node.labelHeight);
|
||||
}, [node.labelHeight, updateLabelHeight]);
|
||||
|
||||
useEffect(() => {
|
||||
updateColor(node.color);
|
||||
}, [node.color, updateColor]);
|
||||
|
||||
if (!node.label) return null;
|
||||
|
||||
return (
|
||||
|
||||
@@ -16,7 +16,8 @@ export const NodeLabel = ({ label }: Props) => (
|
||||
maxHeight: 150,
|
||||
borderRadius: 2,
|
||||
overflow: 'hidden',
|
||||
p: 1.5
|
||||
py: 1,
|
||||
px: 1.5
|
||||
}}
|
||||
>
|
||||
<MarkdownEditor readOnly value={label} />
|
||||
|
||||
@@ -2,37 +2,47 @@ import { useCallback, useRef } from 'react';
|
||||
import { Group, Shape } from 'paper';
|
||||
import { TILE_SIZE, PIXEL_UNIT } from 'src/renderer/utils/constants';
|
||||
import { applyProjectionMatrix } from 'src/renderer/utils/projection';
|
||||
import { getColorVariant } from 'src/utils';
|
||||
|
||||
export const useNodeTile = () => {
|
||||
const containerRef = useRef(new Group());
|
||||
const shapeRef = useRef<paper.Shape.Rectangle>();
|
||||
|
||||
const updateColor = useCallback((color: string) => {
|
||||
if (!shapeRef.current) return;
|
||||
|
||||
shapeRef.current.set({
|
||||
fillColor: color,
|
||||
strokeColor: getColorVariant(color, 'dark', 2)
|
||||
});
|
||||
}, []);
|
||||
|
||||
const init = useCallback(() => {
|
||||
containerRef.current.removeChildren();
|
||||
containerRef.current.set({ pivot: [0, 0] });
|
||||
|
||||
const rectangle = new Shape.Rectangle({
|
||||
shapeRef.current = new Shape.Rectangle({
|
||||
strokeCap: 'round',
|
||||
fillColor: 'blue',
|
||||
size: [TILE_SIZE, TILE_SIZE],
|
||||
radius: PIXEL_UNIT * 8,
|
||||
strokeWidth: 0,
|
||||
strokeColor: 'transparent',
|
||||
strokeWidth: 1,
|
||||
pivot: [0, 0],
|
||||
position: [0, 0],
|
||||
dashArray: null
|
||||
});
|
||||
|
||||
containerRef.current.addChild(rectangle);
|
||||
containerRef.current.addChild(shapeRef.current);
|
||||
applyProjectionMatrix(containerRef.current);
|
||||
|
||||
rectangle.position.set(0, 0);
|
||||
rectangle.scaling.set(1.2);
|
||||
rectangle.applyMatrix = true;
|
||||
shapeRef.current.position.set(0, 0);
|
||||
shapeRef.current.scaling.set(1.2);
|
||||
shapeRef.current.applyMatrix = true;
|
||||
|
||||
return containerRef.current;
|
||||
}, []);
|
||||
|
||||
return {
|
||||
init
|
||||
init,
|
||||
updateColor
|
||||
};
|
||||
};
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface Node {
|
||||
type: SceneItemTypeEnum.NODE;
|
||||
id: string;
|
||||
iconId: string;
|
||||
color: string;
|
||||
label: string;
|
||||
labelHeight: number;
|
||||
position: Coords;
|
||||
|
||||
@@ -9,7 +9,7 @@ interface CustomThemeVars {
|
||||
height: number;
|
||||
};
|
||||
diagramPalette: {
|
||||
purple: string;
|
||||
[key in string]: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ declare module '@mui/material/styles' {
|
||||
}
|
||||
}
|
||||
|
||||
const customVars: CustomThemeVars = {
|
||||
export const customVars: CustomThemeVars = {
|
||||
appPadding: {
|
||||
x: 40,
|
||||
y: 40
|
||||
@@ -32,7 +32,18 @@ const customVars: CustomThemeVars = {
|
||||
height: 55
|
||||
},
|
||||
diagramPalette: {
|
||||
purple: '#cabffa'
|
||||
green: '#53b435',
|
||||
blue: '#4a82f7',
|
||||
teal: '#5a9f9f',
|
||||
salmon: '#e08079',
|
||||
orange: '#e58b48',
|
||||
white: '#ffffff',
|
||||
pink: '#e1034c',
|
||||
purple: '#5155ad',
|
||||
lightPurple: '#8441fc',
|
||||
lightBlue: '#f4f6fe',
|
||||
yellow: '#ffdc73',
|
||||
red: '#d62727'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { customVars } from '../styles/theme';
|
||||
|
||||
export const NODE_DEFAULTS = {
|
||||
label: '',
|
||||
labelHeight: 100
|
||||
labelHeight: 100,
|
||||
color: customVars.diagramPalette.blue
|
||||
};
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import gsap from 'gsap';
|
||||
import { Coords } from 'src/utils/Coords';
|
||||
import chroma from 'chroma-js';
|
||||
import type { NodeInput } from 'src/validation/SceneSchema';
|
||||
import { Node, SceneItemTypeEnum } from 'src/stores/useSceneStore';
|
||||
import { NODE_DEFAULTS } from 'src/utils/defaults';
|
||||
|
||||
export const clamp = (num: number, min: number, max: number) =>
|
||||
num <= min ? min : num >= max ? max : num;
|
||||
num <= min ? min : num >= max ? max : num; // eslint-disable-line no-nested-ternary
|
||||
|
||||
export const nonZeroCoords = (coords: Coords) => {
|
||||
// For some reason, gsap doesn't like to tween x and y both to 0, so we force 0 to be just above 0.
|
||||
@@ -50,6 +51,7 @@ export const nodeInputToNode = (nodeInput: NodeInput): Node => {
|
||||
id: nodeInput.id,
|
||||
label: nodeInput.label ?? NODE_DEFAULTS.label,
|
||||
labelHeight: nodeInput.labelHeight ?? NODE_DEFAULTS.labelHeight,
|
||||
color: nodeInput.color ?? NODE_DEFAULTS.color,
|
||||
iconId: nodeInput.iconId,
|
||||
position: Coords.fromObject(nodeInput.position),
|
||||
isSelected: false,
|
||||
@@ -58,3 +60,24 @@ export const nodeInputToNode = (nodeInput: NodeInput): Node => {
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
export const getColorVariant = (
|
||||
color: string,
|
||||
variant: 'light' | 'dark',
|
||||
grade?: number
|
||||
) => {
|
||||
switch (variant) {
|
||||
case 'light':
|
||||
return chroma(color)
|
||||
.brighten(grade ?? 1)
|
||||
.saturate(2)
|
||||
.hex();
|
||||
case 'dark':
|
||||
return chroma(color)
|
||||
.darken(grade ?? 1)
|
||||
.saturate(2)
|
||||
.hex();
|
||||
default:
|
||||
return color;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ export const nodeInput = z.object({
|
||||
id: z.string(),
|
||||
label: z.string().optional(),
|
||||
labelHeight: z.number().optional(),
|
||||
color: z.string().optional(),
|
||||
iconId: z.string(),
|
||||
position: z.object({
|
||||
x: z.number(),
|
||||
|
||||
Reference in New Issue
Block a user