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:
@@ -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,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';
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
@@ -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,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');
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user