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:
@@ -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';
|
||||
}
|
||||
@@ -73,6 +73,8 @@ export {
|
||||
type Box,
|
||||
type Transform,
|
||||
type CoordinateExtent,
|
||||
type ColorMode,
|
||||
type ColorModeClass,
|
||||
} from '@xyflow/system';
|
||||
|
||||
// system utils
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user