Feat: dark mode (#3652)

* feat(react/svelte): add dark mode defaults

* refactor(darkmode): minimap, edges, edge labels

* chore(style): edge label color

* feat(colorMode): add colorMode prop light/dark/system

* chore(examples): cleanup

* test(colorMode): add tests

* chore(base.css): add dark base

* chore(examples): cleanup
This commit is contained in:
Moritz Klack
2023-11-23 12:15:36 +01:00
committed by GitHub
parent 74c82272aa
commit ab5eef220f
38 changed files with 474 additions and 83 deletions
+6
View File
@@ -1,5 +1,6 @@
import Basic from '../examples/Basic';
import Backgrounds from '../examples/Backgrounds';
import ColorMode from '../examples/ColorMode';
import ControlledUncontrolled from '../examples/ControlledUncontrolled';
import ControlledViewport from '../examples/ControlledViewport';
import CustomConnectionLine from '../examples/CustomConnectionLine';
@@ -62,6 +63,11 @@ const routes: IRoute[] = [
path: 'backgrounds',
component: Backgrounds,
},
{
name: 'Color Mode',
path: 'color-mode',
component: ColorMode,
},
{
name: 'Cancel Connection',
path: 'cancel-connection',
@@ -0,0 +1,88 @@
import { ChangeEventHandler, useCallback, useState } from 'react';
import {
ReactFlow,
addEdge,
Node,
useNodesState,
useEdgesState,
OnConnect,
Edge,
MiniMap,
Background,
Controls,
Panel,
ColorMode,
Position,
} from '@xyflow/react';
const nodeDefaults = {
sourcePosition: Position.Right,
targetPosition: Position.Left,
};
const initialNodes: Node[] = [
{ id: 'A', type: 'input', position: { x: 0, y: 150 }, data: { label: 'A' }, ...nodeDefaults },
{ id: 'B', position: { x: 250, y: 0 }, data: { label: 'B' }, ...nodeDefaults },
{ id: 'C', position: { x: 250, y: 150 }, data: { label: 'C' }, ...nodeDefaults },
{ id: 'D', position: { x: 250, y: 300 }, data: { label: 'D' }, ...nodeDefaults },
];
const initialEdges: Edge[] = [
{
id: 'A-B',
source: 'A',
target: 'B',
},
{
id: 'A-C',
source: 'A',
target: 'C',
},
{
id: 'A-D',
source: 'A',
target: 'D',
},
];
const ColorModeFlow = () => {
const [colorMode, setColorMode] = useState<ColorMode>('light');
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect: OnConnect = useCallback(
(params) => {
console.log('on connect', params);
setEdges((eds) => addEdge(params, eds));
},
[setEdges]
);
const onChange: ChangeEventHandler<HTMLSelectElement> = (evt) => setColorMode(evt.target.value as ColorMode);
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
colorMode={colorMode}
fitView
>
<MiniMap />
<Background />
<Controls />
<Panel position="top-right">
<select onChange={onChange} data-testid="colormode-select">
<option value="light">light</option>
<option value="dark">dark</option>
<option value="system">system</option>
</select>
</Panel>
</ReactFlow>
);
};
export default ColorModeFlow;
@@ -5,7 +5,6 @@ import CustomNode from './CustomNode';
import FloatingEdge from './FloatingEdge';
import CustomConnectionLine from './CustomConnectionLine';
import '@xyflow/react/dist/style.css';
import './style.css';
const initialNodes: Node[] = [
@@ -16,8 +16,6 @@ import CustomResizer from './CustomResizer';
import VerticalResizer from './VerticalResizer';
import HorizontalResizer from './HorizontalResizer';
import '@xyflow/react/dist/style.css';
const nodeTypes = {
defaultResizer: DefaultResizer,
customResizer: CustomResizer,
@@ -23,7 +23,7 @@ const onNodeDragStop = (_: ReactMouseEvent, node: Node, nodes: Node[]) => consol
const onNodeDoubleClick = (_: ReactMouseEvent, node: Node) => console.log('node double click', node);
const onPaneClick = (event: ReactMouseEvent) => console.log('pane click', event);
const onPaneScroll = (event?: ReactMouseEvent) => console.log('pane scroll', event);
const onPaneContextMenu = (event: ReactMouseEvent) => console.log('pane context menu', event);
const onPaneContextMenu = (event: ReactMouseEvent | MouseEvent) => console.log('pane context menu', event);
const onSelectionDrag = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag', nodes);
const onSelectionDragStart = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag start', nodes);
const onSelectionDragStop = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag stop', nodes);
@@ -230,9 +230,9 @@ const OverviewFlow = () => {
onEdgesDelete={onEdgesDelete}
onPaneMouseMove={onPaneMouseMove}
>
<MiniMap nodeStrokeColor={nodeStrokeColor} nodeColor={nodeColor} nodeBorderRadius={2} />
<MiniMap nodeBorderRadius={2} />
<Controls />
<Background color="#aaa" gap={25} />
<Background gap={25} />
</ReactFlow>
);
};
+1 -1
View File
@@ -1,7 +1,7 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import App from './App/index';
import '@xyflow/react/dist/style.css';
import './index.css';
@@ -4,6 +4,7 @@
const routes = [
'add-node-on-drop',
'colormode',
'custom-connection-line',
'customnode',
'dagre',
@@ -0,0 +1,54 @@
<script lang="ts">
import { writable } from 'svelte/store';
import {
SvelteFlow,
Controls,
Background,
BackgroundVariant,
Position,
MiniMap,
Panel,
type ColorMode
} from '@xyflow/svelte';
import '@xyflow/svelte/dist/style.css';
const nodeDefaults = {
sourcePosition: Position.Right,
targetPosition: Position.Left
};
const nodes = writable([
{
id: 'A',
position: { x: 0, y: 150 },
data: { label: 'A' },
...nodeDefaults
},
{ id: 'B', position: { x: 250, y: 0 }, data: { label: 'B' }, ...nodeDefaults },
{ id: 'C', position: { x: 250, y: 150 }, data: { label: 'C' }, ...nodeDefaults },
{ id: 'D', position: { x: 250, y: 300 }, data: { label: 'D' }, ...nodeDefaults }
]);
const edges = writable([
{ id: 'A-B', source: 'A', target: 'B' },
{ id: 'A-C', source: 'A', target: 'C' },
{ id: 'A-D', source: 'A', target: 'D' }
]);
let colorMode: ColorMode = 'light';
</script>
<SvelteFlow {nodes} {edges} {colorMode} fitView>
<Controls />
<Background variant={BackgroundVariant.Dots} />
<MiniMap />
<Panel>
<select bind:value={colorMode} data-testid="colormode-select">
<option value="light">light</option>
<option value="dark">dark</option>
<option value="system">system</option>
</select>
</Panel>
</SvelteFlow>
@@ -145,7 +145,7 @@
]);
</script>
<SvelteFlow {nodes} {edges} fitView nodeDragThreshold={2} ondelete={console.log}>
<SvelteFlow {nodes} {edges} fitView nodeDragThreshold={2}>
<Controls />
<Background variant={BackgroundVariant.Dots} />
<MiniMap />
@@ -26,6 +26,6 @@ type DotPatternProps = {
export function DotPattern({ radius, className }: DotPatternProps) {
return (
<circle cx={radius} cy={radius} r={radius} className={cc(['react-flow__background-pattern', 'dot', className])} />
<circle cx={radius} cy={radius} r={radius} className={cc(['react-flow__background-pattern', 'dots', className])} />
);
}
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { memo, useEffect, useRef, type MouseEvent, useCallback } from 'react';
import { memo, useEffect, useRef, type MouseEvent, useCallback, CSSProperties } from 'react';
import cc from 'classcat';
import { shallow } from 'zustand/shallow';
import { getNodesBounds, getBoundsOfRects, XYMinimap, type Rect, type XYMinimapInstance } from '@xyflow/system';
@@ -40,15 +40,15 @@ const ARIA_LABEL_KEY = 'react-flow__minimap-desc';
function MiniMap({
style,
className,
nodeStrokeColor = 'transparent',
nodeColor = '#e2e2e2',
nodeStrokeColor,
nodeColor,
nodeClassName = '',
nodeBorderRadius = 5,
nodeStrokeWidth = 2,
nodeStrokeWidth,
// We need to rename the prop to be `CapitalCase` so that JSX will render it as
// a component properly.
nodeComponent,
maskColor = 'rgb(240, 240, 240, 0.6)',
maskColor,
maskStrokeColor = 'none',
maskStrokeWidth = 1,
position = 'bottom-right',
@@ -126,7 +126,15 @@ function MiniMap({
return (
<Panel
position={position}
style={style}
style={
{
...style,
'--minimap-mask-color-props': typeof maskColor === 'string' ? maskColor : undefined,
'--minimap-node-background-color-props': typeof nodeColor === 'string' ? nodeColor : undefined,
'--minimap-node-stroke-color-props': typeof nodeStrokeColor === 'string' ? nodeStrokeColor : undefined,
'--minimap-node-stroke-width-props': typeof nodeStrokeWidth === 'string' ? nodeStrokeWidth : undefined,
} as CSSProperties
}
className={cc(['react-flow__minimap', className])}
data-testid="rf__minimap"
>
@@ -153,7 +161,6 @@ function MiniMap({
className="react-flow__minimap-mask"
d={`M${x - offset},${y - offset}h${width + offset * 2}v${height + offset * 2}h${-width - offset * 2}z
M${viewBB.x},${viewBB.y}h${viewBB.width}v${viewBB.height}h${-viewBB.width}z`}
fill={maskColor}
fillRule="evenodd"
stroke={maskStrokeColor}
strokeWidth={maskStrokeWidth}
@@ -31,9 +31,11 @@ function MiniMapNode({
ry={borderRadius}
width={width}
height={height}
fill={fill}
stroke={strokeColor}
strokeWidth={strokeWidth}
style={{
fill,
stroke: strokeColor,
strokeWidth,
}}
shapeRendering={shapeRendering}
onClick={onClick ? (event) => onClick(event, id) : undefined}
/>
@@ -19,11 +19,11 @@ const selectorNodes = (s: ReactFlowState) =>
const getAttrFunction = (func: any): GetMiniMapNodeAttribute => (func instanceof Function ? func : () => func);
function MiniMapNodes({
nodeStrokeColor = 'transparent',
nodeColor = '#e2e2e2',
nodeStrokeColor,
nodeColor,
nodeClassName = '',
nodeBorderRadius = 5,
nodeStrokeWidth = 2,
nodeStrokeWidth,
// We need to rename the prop to be `CapitalCase` so that JSX will render it as
// a component properly.
nodeComponent: NodeComponent = MiniMapNode,
@@ -41,6 +41,8 @@ function MiniMapNodes({
<>
{nodes.map((node) => {
const { x, y } = getNodePositionWithOrigin(node, node.origin || nodeOrigin).positionAbsolute;
const color = nodeColor === undefined ? undefined : nodeColorFunc(node);
const strokeColor = nodeStrokeColor === undefined ? undefined : nodeStrokeColorFunc(node);
return (
<NodeComponent
@@ -52,9 +54,9 @@ function MiniMapNodes({
style={node.style}
selected={!!node.selected}
className={nodeClassNameFunc(node)}
color={nodeColorFunc(node)}
color={color}
borderRadius={nodeBorderRadius}
strokeColor={nodeStrokeColorFunc(node)}
strokeColor={strokeColor}
strokeWidth={nodeStrokeWidth}
shapeRendering={shapeRendering}
onClick={onClick}
@@ -42,10 +42,10 @@ export type MiniMapNodeProps = {
height: number;
borderRadius: number;
className: string;
color: string;
color?: string;
shapeRendering: string;
strokeColor: string;
strokeWidth: number;
strokeColor?: string;
strokeWidth?: number;
style?: CSSProperties;
selected: boolean;
onClick?: (event: MouseEvent, id: string) => void;
@@ -23,6 +23,7 @@ import A11yDescriptions from '../../components/A11yDescriptions';
import GraphView from '../GraphView';
import Wrapper from './Wrapper';
import type { EdgeTypes, NodeTypes, ReactFlowProps, ReactFlowRefType } from '../../types';
import useColorModeClass from '../../hooks/useColorModeClass';
const defaultNodeTypes: NodeTypes = {
input: InputNode,
@@ -169,18 +170,20 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
onViewportChange,
width,
height,
colorMode = 'light',
...rest
},
ref
) => {
const rfId = id || '1';
const colorModeClassName = useColorModeClass(colorMode);
return (
<div
{...rest}
style={{ ...style, ...wrapperStyle }}
ref={ref}
className={cc(['react-flow', className])}
className={cc(['react-flow', className, colorModeClassName])}
data-testid="rf__wrapper"
id={id}
>
@@ -0,0 +1,35 @@
import { useEffect, useState } from 'react';
import type { ColorMode, ColorModeClass } from '@xyflow/system';
function getMediaQuery() {
if (typeof window === 'undefined' || !window.matchMedia) {
return null;
}
return window.matchMedia('(prefers-color-scheme: dark)');
}
export default function useColorModeClass(colorMode: ColorMode): ColorModeClass {
const [colorModeClass, setColorModeClass] = useState<ColorModeClass | null>(
colorMode === 'system' ? null : colorMode
);
useEffect(() => {
if (colorMode !== 'system') {
setColorModeClass(colorMode);
return;
}
const mediaQuery = getMediaQuery();
const updateColorModeClass = () => setColorModeClass(mediaQuery?.matches ? 'dark' : 'light');
updateColorModeClass();
mediaQuery?.addEventListener('change', updateColorModeClass);
return () => {
mediaQuery?.removeEventListener('change', updateColorModeClass);
};
}, [colorMode]);
return colorModeClass !== null ? colorModeClass : getMediaQuery()?.matches ? 'dark' : 'light';
}
+2
View File
@@ -73,6 +73,8 @@ export {
type Box,
type Transform,
type CoordinateExtent,
type ColorMode,
type ColorModeClass,
} from '@xyflow/system';
// system utils
+18
View File
@@ -1,3 +1,21 @@
/* this will be exported as base.css and can be used for a basic styling */
@import '../../../system/src/styles/init.css';
@import '../../../system/src/styles/base.css';
.react-flow {
--edge-label-background-color-default: #ffffff;
--edge-label-color-default: inherit;
}
.react-flow.dark {
--edge-label-background-color-default: #141414;
--edge-label-color-default: #f8f8f8;
}
.react-flow__edge-textbg {
fill: var(--edge-label-background-color, var(--edge-label-background-color-default));
}
.react-flow__edge-text {
fill: var(--edge-label-color, var(--edge-label-color-default));
}
+18
View File
@@ -2,3 +2,21 @@
@import '../../../system/src/styles/init.css';
@import '../../../system/src/styles/style.css';
@import '../../../system/src/styles/node-resizer.css';
.react-flow {
--edge-label-background-color-default: #ffffff;
--edge-label-color-default: inherit;
}
.react-flow.dark {
--edge-label-background-color-default: #141414;
--edge-label-color-default: #f8f8f8;
}
.react-flow__edge-textbg {
fill: var(--edge-label-background-color, var(--edge-label-background-color-default));
}
.react-flow__edge-text {
fill: var(--edge-label-color, var(--edge-label-color-default));
}
@@ -19,6 +19,7 @@ import type {
SelectionMode,
OnError,
IsValidConnection,
ColorMode,
} from '@xyflow/system';
import type {
@@ -155,6 +156,7 @@ export type ReactFlowProps = Omit<HTMLAttributes<HTMLDivElement>, 'onError'> & {
nodeDragThreshold?: number;
width?: number;
height?: number;
colorMode?: ColorMode;
};
export type ReactFlowRefType = HTMLDivElement;
@@ -15,8 +15,14 @@
import { Attribution } from '$lib/components/Attribution';
import { key, useStore, createStoreContext } from '$lib/store';
import type { SvelteFlowProps } from './types';
import { updateStore, updateStoreByKeys, type UpdatableStoreProps } from './utils';
import {
updateStore,
updateStoreByKeys,
type UpdatableStoreProps,
getColorModeClass
} from './utils';
import { get } from 'svelte/store';
import { useColorModeClass } from '$lib/hooks/useColorModeClass';
type $$Props = SvelteFlowProps;
@@ -70,6 +76,7 @@
export let defaultEdgeOptions: $$Props['defaultEdgeOptions'] = undefined;
export let width: $$Props['width'] = undefined;
export let height: $$Props['height'] = undefined;
export let colorMode: $$Props['colorMode'] = 'light';
export let defaultMarkerColor = '#b1b1b7';
@@ -156,6 +163,8 @@
maxZoom,
translateExtent
});
$: colorModeClass = useColorModeClass(colorMode);
</script>
<div
@@ -163,7 +172,7 @@
bind:clientWidth
bind:clientHeight
{style}
class={cc(['svelte-flow', className])}
class={cc(['svelte-flow', className, $colorModeClass])}
data-testid="svelte-flow__wrapper"
on:dragover
on:drop
@@ -14,7 +14,8 @@ import type {
OnError,
ConnectionMode,
PanelPosition,
ProOptions
ProOptions,
ColorMode
} from '@xyflow/system';
import type {
@@ -75,6 +76,7 @@ export type SvelteFlowProps = DOMAttributes<HTMLDivElement> & {
defaultEdgeOptions?: DefaultEdgeOptions;
width?: number;
height?: number;
colorMode?: ColorMode;
class?: string;
style?: string;
@@ -1,6 +1,6 @@
import type { SvelteFlowStore } from '$lib/store/types';
import type { EdgeTypes, NodeTypes } from '$lib/types';
import type { CoordinateExtent } from '@xyflow/system';
import type { ColorMode, CoordinateExtent } from '@xyflow/system';
import type { Writable } from 'svelte/store';
// this is helper function for updating the store
@@ -77,3 +77,15 @@ export function updateStoreByKeys(store: SvelteFlowStore, keys: UpdatableStorePr
}
});
}
export function getColorModeClass(colorMode?: ColorMode) {
if (colorMode !== 'system') {
return colorMode;
}
if (!colorMode || typeof window === 'undefined' || !window.matchMedia) {
return 'light';
}
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
@@ -0,0 +1,32 @@
import type { ColorMode, ColorModeClass } from '@xyflow/system';
import { readable, type Readable } from 'svelte/store';
function getMediaQuery() {
if (typeof window === 'undefined' || !window.matchMedia) {
return null;
}
return window.matchMedia('(prefers-color-scheme: dark)');
}
export function useColorModeClass(colorMode: ColorMode = 'light'): Readable<ColorModeClass> {
const colorModeClass = readable<ColorModeClass>('light', (set) => {
if (colorMode !== 'system') {
set(colorMode);
return;
}
const mediaQuery = getMediaQuery();
const updateColorModeClass = () => set(mediaQuery?.matches ? 'dark' : 'light');
set(mediaQuery?.matches ? 'dark' : 'light');
mediaQuery?.addEventListener('change', updateColorModeClass);
return () => {
mediaQuery?.removeEventListener('change', updateColorModeClass);
};
});
return colorModeClass;
}
+3 -1
View File
@@ -75,7 +75,9 @@ export {
type Rect,
type Box,
type Transform,
type CoordinateExtent
type CoordinateExtent,
type ColorMode,
type ColorModeClass
} from '@xyflow/system';
// system utils
@@ -20,7 +20,7 @@
export let position: $$Props['position'] = 'bottom-right';
export let ariaLabel: $$Props['ariaLabel'] = 'Mini map';
export let nodeStrokeColor: $$Props['nodeStrokeColor'] = 'transparent';
export let nodeColor: $$Props['nodeColor'] = '#e2e2e2';
export let nodeColor: $$Props['nodeColor'] = undefined;
export let nodeClass: $$Props['nodeClass'] = '';
export let nodeBorderRadius: $$Props['nodeBorderRadius'] = 5;
export let nodeStrokeWidth: $$Props['nodeStrokeWidth'] = 2;
@@ -51,7 +51,7 @@
translateExtent
} = useStore();
const nodeColorFunc = getAttrFunction(nodeColor);
const nodeColorFunc = nodeColor === undefined ? undefined : getAttrFunction(nodeColor);
const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor);
const nodeClassFunc = getAttrFunction(nodeClass);
const shapeRendering =
@@ -122,7 +122,7 @@
width={node.computed?.width ?? node.width ?? 0}
height={node.computed?.height ?? node.height ?? 0}
selected={node.selected}
color={nodeColorFunc(node)}
color={nodeColorFunc?.(node)}
borderRadius={nodeBorderRadius}
strokeColor={nodeStrokeColorFunc(node)}
strokeWidth={nodeStrokeWidth}
@@ -142,27 +142,3 @@
</svg>
{/if}
</Panel>
<style>
svg {
background-color: var(
--minimap-background-color-props,
var(--minimap-background-color, var(--minimap-background-color-default))
);
}
.svelte-flow__minimap-mask {
fill: var(
--minimap-mask-color-props,
var(--minimap-mask-color, var(--minimap-mask-color-default))
);
stroke: var(
--minimap-mask-stroke-color-props,
var(--minimap-mask-stroke-color, var(--minimap-mask-stroke-color-default))
);
stroke-width: var(
--minimap-mask-stroke-width-props,
var(--minimap-mask-stroke-width, var(--minimap-mask-stroke-width-default))
);
}
</style>
@@ -6,9 +6,9 @@
export let width: number = 0;
export let height: number = 0;
export let borderRadius: number = 5;
export let color: string;
export let color: string | undefined = undefined;
export let shapeRendering: string;
export let strokeColor: string;
export let strokeColor: string | undefined = undefined;
export let strokeWidth: number = 2;
export let selected: boolean = false;
let className: string = '';
@@ -24,8 +24,8 @@
ry={borderRadius}
{width}
{height}
fill={color}
stroke={strokeColor}
stroke-width={strokeWidth}
style={`${color ? `fill: ${color};` : ''}${strokeColor ? `stroke: ${strokeColor};` : ''}${
strokeWidth ? `stroke-width: ${strokeWidth};` : ''
}`}
shape-rendering={shapeRendering}
/>
+9
View File
@@ -2,7 +2,16 @@
@import '../../../system/src/styles/init.css';
@import '../../../system/src/styles/base.css';
.svelte-flow {
--edge-label-color-default: inherit;
}
.svelte-flow.dark {
--edge-label-color-default: #f8f8f8;
}
.svelte-flow__edge-label {
text-align: center;
position: absolute;
color: var(--edge-label-color, var(--edge-label-color-default));
}
+9
View File
@@ -2,10 +2,19 @@
@import '../../../system/src/styles/init.css';
@import '../../../system/src/styles/style.css';
.svelte-flow {
--edge-label-color-default: inherit;
}
.svelte-flow.dark {
--edge-label-color-default: #f8f8f8;
}
.svelte-flow__edge-label {
text-align: center;
position: absolute;
font-size: 10px;
color: var(--edge-label-color, var(--edge-label-color-default));
}
.svelte-flow__nodes {
+5
View File
@@ -8,6 +8,10 @@
--selection-border-default: 1px dotted rgba(155, 155, 155, 0.8);
}
.xy-flow.dark {
--node-color-default: #f8f8f8;
}
.xy-flow__handle {
background-color: var(--handle-background-color, var(--handle-background-color-default));
}
@@ -17,6 +21,7 @@
.xy-flow__node-output,
.xy-flow__node-group {
border: var(--node-border, var(--node-border-default));
color: var(--node-color, var(--node-color-default));
&.selected,
&:focus,
+58 -10
View File
@@ -11,12 +11,43 @@
--attribution-background-color-default: rgba(255, 255, 255, 0.5);
--minimap-background-color-default: #fff;
--minimap-mask-background-color-default: rgb(240, 240, 240, 0.6);
--minimap-node-background-color-default: #e2e2e2;
--minimap-node-stroke-color-default: transparent;
--minimap-node-stroke-width-default: 2;
--background-pattern-dot-color-default: #91919a;
--background-pattern-line-color-default: #eee;
--background-color-default: transparent;
--background-pattern-dots-color-default: #91919a;
--background-pattern-lines-color-default: #eee;
--background-pattern-cross-color-default: #e2e2e2;
}
.xy-flow.dark {
--edge-stroke-default: #3c3c3c;
--edge-stroke-width-default: 1;
--edge-stroke-selected-default: #727272;
--connectionline-stroke-default: #b1b1b7;
--connectionline-stroke-width-default: 1;
--attribution-background-color-default: rgba(150, 150, 150, 0.25);
--minimap-background-color-default: #141414;
--minimap-mask-background-color-default: rgb(60, 60, 60, 0.6);
--minimap-node-background-color-default: #2b2b2b;
--minimap-node-stroke-color-default: transparent;
--minimap-node-stroke-width-default: 2;
--background-color-default: #141414;
--background-pattern-dots-color-default: #777;
--background-pattern-lines-color-default: #777;
--background-pattern-cross-color-default: #777;
}
.xy-flow {
background-color: var(--background-color-props, var(--background-color-default, 'transparent'));
}
.xy-flow__container {
position: absolute;
width: 100%;
@@ -108,16 +139,11 @@
pointer-events: all;
}
&-textbg {
fill: white;
}
.xy-flow__edge-text {
pointer-events: none;
user-select: none;
}
}
.xy-flow__connection {
pointer-events: none;
@@ -268,26 +294,47 @@
.xy-flow__minimap {
background: var(--minimap-background-color, var(--minimap-background-color-default));
&-mask {
fill: var(
--minimap-mask-background-color-props,
var(--minimap-mask-background-color, var(--minimap-mask-background-color-default))
);
}
&-node {
fill: var(
--minimap-node-background-color-props,
var(--minimap-node-background-color, var(--minimap-node-background-color-default))
);
stroke: var(
--minimap-node-stroke-color-props,
var(--minimap-node-stroke-color, var(--minimap-node-stroke-color-default))
);
stroke-width: var(
--minimap-node-stroke-width-props,
var(--minimap-node-stroke-width, var(--minimap-node-stroke-width-default))
);
}
}
.xy-flow__background {
pointer-events: none;
z-index: -1;
background-color: var(--background-color-props, 'transparent');
}
.xy-flow__background-pattern {
&.dots {
fill: var(
--background-pattern-color-props,
var(--background-pattern-color, var(--background-pattern-dot-color-default))
var(--background-pattern-color, var(--background-pattern-dots-color-default))
);
}
&.lines {
stroke: var(
--background-pattern-color-props,
var(--background-pattern-color, var(--background-pattern-line-color-default))
var(--background-pattern-color, var(--background-pattern-lines-color-default))
);
}
@@ -312,6 +359,7 @@
width: 100%;
max-width: 12px;
max-height: 12px;
fill: currentColor;
}
}
}
+29 -2
View File
@@ -5,6 +5,7 @@
--node-group-background-color-default: rgba(240, 240, 240, 0.25);
--node-boxshadow-hover-default: 0 1px 4px 1px rgba(0, 0, 0, 0.08);
--node-boxshadow-selected-default: 0 0 0 0.5px #1a192b;
--node-border-radius-default: 3px;
--handle-background-color-default: #1a192b;
--handle-border-color-default: #fff;
@@ -20,6 +21,28 @@
--controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, 0.08);
}
.xy-flow.dark {
--node-color-default: #f8f8f8;
--node-border-default: 1px solid #3c3c3c;
--node-background-color-default: #1e1e1e;
--node-group-background-color-default: rgba(240, 240, 240, 0.25);
--node-boxshadow-hover-default: 0 1px 4px 1px rgba(255, 255, 255, 0.08);
--node-boxshadow-selected-default: 0 0 0 0.5px #999;
--handle-background-color-default: #bebebe;
--handle-border-color-default: #1e1e1e;
--selection-background-color-default: rgba(200, 200, 220, 0.08);
--selection-border-default: 1px dotted rgba(200, 200, 220, 0.8);
--controls-button-background-color-default: #2b2b2b;
--controls-button-background-color-hover-default: #3e3e3e;
--controls-button-color-default: #f8f8f8;
--controls-button-color-hover-default: #fff;
--controls-button-border-color-default: #5b5b5b;
--controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, 0.08);
}
.xy-flow__edge {
&.updating {
.xy-flow__edge-path {
@@ -44,7 +67,7 @@
.xy-flow__node-output,
.xy-flow__node-group {
padding: 10px;
border-radius: 3px;
border-radius: var(--node-border-radius, var(--node-border-radius-default));
width: 150px;
font-size: 12px;
color: var(--node-color, var(--node-color-default));
@@ -110,7 +133,7 @@
);
color: var(
--controls-button-color-hover-props,
var(--controls-button-hover-color, var(--controls-button-hover-color-default))
var(--controls-button-color-hover, var(--controls-button-color-hover-default))
);
}
@@ -122,4 +145,8 @@
}
}
}
&-button:last-child {
border-bottom: none;
}
}
+3
View File
@@ -136,3 +136,6 @@ export type UpdateConnection = (params: {
connectionStartHandle: ConnectingHandle | null;
connectionEndHandle: ConnectingHandle | null;
}) => void;
export type ColorModeClass = 'light' | 'dark';
export type ColorMode = ColorModeClass | 'system';
+1 -1
View File
@@ -2,7 +2,7 @@ import { test, expect } from '@playwright/test';
import { FRAMEWORK } from './constants';
test.describe('EDGES', () => {
test.describe('Edges', () => {
test.beforeEach(async ({ page }) => {
// Go to the starting url before each test.
await page.goto('/tests/generic/edges/general');
+1 -1
View File
@@ -1,4 +1,4 @@
import { test, expect, Locator } from '@playwright/test';
import { test, expect } from '@playwright/test';
import { FRAMEWORK } from './constants';
type Position = 'top' | 'right' | 'bottom' | 'left';
+1 -1
View File
@@ -2,7 +2,7 @@ import { test, expect, Locator } from '@playwright/test';
import { FRAMEWORK } from './constants';
test.describe('NODES', () => {
test.describe('Nodes', () => {
test.beforeEach(async ({ page }) => {
// Go to the starting url before each test.
await page.goto('/tests/generic/nodes/general');
+3 -3
View File
@@ -3,7 +3,7 @@ import { test, expect } from '@playwright/test';
import { FRAMEWORK } from './constants';
import { getTransform } from './utils';
test.describe('PANE DEFAULT', () => {
test.describe('Pane default', () => {
test.beforeEach(async ({ page }) => {
// Go to the starting url before each test.
await page.goto('/tests/generic/pane/general');
@@ -126,7 +126,7 @@ test.describe('PANE DEFAULT', () => {
});
});
test.describe('PANE NON-DEFAULT', () => {
test.describe('Pane non-default', () => {
test.beforeEach(async ({ page }) => {
// Go to the starting url before each test.
await page.goto('/tests/generic/pane/non-defaults');
@@ -168,7 +168,7 @@ test.describe('PANE NON-DEFAULT', () => {
});
});
test.describe('PANE ACTIVATION KEYS', () => {
test.describe('Pane activation keys', () => {
test.beforeEach(async ({ page }) => {
// Go to the starting url before each test.
await page.goto('/tests/generic/pane/activation-keys');
+22
View File
@@ -0,0 +1,22 @@
import { test, expect } from '@playwright/test';
import { FRAMEWORK } from './constants';
test.describe('Props', () => {
test.describe('colorMode', async () => {
test('render default light color mode', async ({ page }) => {
await page.goto('/examples/color-mode');
const locator = page.locator(`.${FRAMEWORK}-flow`);
await expect(locator).not.toHaveClass(/dark/);
});
test('render dark color mode', async ({ page }) => {
await page.goto('/examples/color-mode');
const locator = page.locator(`.${FRAMEWORK}-flow`);
await page.getByTestId('colormode-select').selectOption({ label: 'dark' });
await expect(locator).toHaveClass(/dark/);
});
});
});