Merge pull request #5596 from xyflow/chore/svelte-typed-context
Chore/svelte typed context
This commit is contained in:
@@ -1,29 +1,34 @@
|
||||
import globals from 'globals';
|
||||
import prettier from 'eslint-config-prettier';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { includeIgnoreFile } from '@eslint/compat';
|
||||
import js from '@eslint/js';
|
||||
import ts from 'typescript-eslint';
|
||||
import svelte from 'eslint-plugin-svelte';
|
||||
import eslintConfigPrettier from 'eslint-config-prettier';
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import globals from 'globals';
|
||||
import ts from 'typescript-eslint';
|
||||
import svelteConfig from './svelte.config.js';
|
||||
|
||||
export default ts.config(
|
||||
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
|
||||
|
||||
export default defineConfig(
|
||||
includeIgnoreFile(gitignorePath),
|
||||
js.configs.recommended,
|
||||
...ts.configs.recommended,
|
||||
...svelte.configs.recommended,
|
||||
eslintConfigPrettier,
|
||||
prettier,
|
||||
...svelte.configs.prettier,
|
||||
{
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.es2017,
|
||||
...globals.node
|
||||
}
|
||||
globals: { ...globals.browser, ...globals.node }
|
||||
},
|
||||
rules: {
|
||||
// typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
|
||||
// see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
|
||||
'no-undef': 'off'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
|
||||
ignores: ['eslint.config.js', 'svelte.config.js'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
@@ -32,8 +37,5 @@ export default ts.config(
|
||||
svelteConfig
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
ignores: ['.svelte-kit/*', 'build/*', 'dist/*'] // Add common ignore patterns
|
||||
}
|
||||
);
|
||||
|
||||
@@ -55,35 +55,36 @@
|
||||
"@xyflow/system": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.28.0",
|
||||
"@sveltejs/adapter-auto": "^6.0.1",
|
||||
"@sveltejs/kit": "^2.21.4",
|
||||
"@sveltejs/package": "^2.3.11",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.34.0",
|
||||
"@typescript-eslint/parser": "^8.34.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"cssnano": "^7.0.7",
|
||||
"dotenv": "^16.5.0",
|
||||
"eslint": "^9.28.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-svelte": "^3.9.2",
|
||||
"globals": "^16.2.0",
|
||||
"postcss": "^8.5.4",
|
||||
"@eslint/compat": "^2.0.0",
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@sveltejs/adapter-auto": "^7.0.0",
|
||||
"@sveltejs/kit": "^2.49.1",
|
||||
"@sveltejs/package": "^2.5.7",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.48.1",
|
||||
"@typescript-eslint/parser": "^8.48.1",
|
||||
"autoprefixer": "^10.4.22",
|
||||
"cssnano": "^7.1.2",
|
||||
"dotenv": "^17.2.3",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-svelte": "^3.13.0",
|
||||
"globals": "^16.5.0",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss-cli": "^11.0.1",
|
||||
"postcss-combine-duplicated-selectors": "^10.0.3",
|
||||
"postcss-import": "^16.1.0",
|
||||
"postcss-import": "^16.1.1",
|
||||
"postcss-nested": "^7.0.2",
|
||||
"postcss-rename": "^0.8.0",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier": "^3.7.4",
|
||||
"prettier-plugin-svelte": "^3.4.0",
|
||||
"svelte": "^5.33.19",
|
||||
"svelte-check": "^4.2.1",
|
||||
"svelte-eslint-parser": "^1.2.0",
|
||||
"svelte": "^5.45.4",
|
||||
"svelte-check": "^4.3.4",
|
||||
"svelte-eslint-parser": "^1.4.0",
|
||||
"svelte-preprocess": "^6.0.3",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.34.0"
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.48.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^5.25.0"
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
|
||||
import { getEdgeIdContext } from '$lib/store/context';
|
||||
import { hideOnSSR, portal } from '$lib/actions/portal';
|
||||
import { useStore } from '$lib/store';
|
||||
import type { EdgeLabelProps } from './types';
|
||||
import { toPxString } from '$lib/utils';
|
||||
|
||||
import type { EdgeLabelProps } from './types';
|
||||
|
||||
let {
|
||||
x = 0,
|
||||
y = 0,
|
||||
@@ -19,13 +19,12 @@
|
||||
}: EdgeLabelProps = $props();
|
||||
|
||||
const store = useStore();
|
||||
const edgeId = getContext<string>('svelteflow__edge_id');
|
||||
|
||||
if (!edgeId) {
|
||||
throw new Error('EdgeLabel must be used within an edge');
|
||||
}
|
||||
const edgeId = getEdgeIdContext('EdgeLabel must be used within a Custom Edge component');
|
||||
|
||||
let z = $derived(store.visible.edges.get(edgeId)?.zIndex);
|
||||
let z = $derived.by(() => {
|
||||
return store.visible.edges.get(edgeId)?.zIndex;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -40,7 +39,7 @@
|
||||
style:z-index={z}
|
||||
tabindex="-1"
|
||||
onclick={() => {
|
||||
if (selectEdgeOnClick) store.handleEdgeSelection(edgeId);
|
||||
if (selectEdgeOnClick && edgeId) store.handleEdgeSelection(edgeId);
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { useStore } from '$lib/store';
|
||||
import { getEdgeIdContext } from '$lib/store/context';
|
||||
import type { Edge } from '$lib/types';
|
||||
import { XYHandle, type HandleType, type OnConnectStart } from '@xyflow/system';
|
||||
import { getContext } from 'svelte';
|
||||
import { EdgeLabel } from '../EdgeLabel';
|
||||
import type { EdgeReconnectAnchorProps } from './types';
|
||||
|
||||
@@ -19,11 +19,9 @@
|
||||
|
||||
const store = useStore();
|
||||
|
||||
let edgeId: string | undefined = getContext('svelteflow__edge_id');
|
||||
|
||||
if (!edgeId) {
|
||||
throw new Error('EdgeReconnectAnchor must be used within an Edge component');
|
||||
}
|
||||
const edgeId = getEdgeIdContext(
|
||||
'EdgeReconnectAnchor must be used within a Custom Edge component'
|
||||
);
|
||||
|
||||
const onPointerDown = (event: PointerEvent) => {
|
||||
if (event.button !== 0) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts" generics="NodeType extends Node = Node, EdgeType extends Edge = Edge">
|
||||
import { setContext } from 'svelte';
|
||||
|
||||
import { elementSelectionKeys, getMarkerId } from '@xyflow/system';
|
||||
|
||||
import { setEdgeIdContext } from '$lib/store/context';
|
||||
|
||||
import { BezierEdgeInternal } from '$lib/components/edges';
|
||||
|
||||
import type { Node, EdgeLayouted, Edge, EdgeEvents } from '$lib/types';
|
||||
@@ -22,6 +22,7 @@
|
||||
} & EdgeEvents<EdgeType> = $props();
|
||||
|
||||
let {
|
||||
id,
|
||||
source,
|
||||
target,
|
||||
sourceX,
|
||||
@@ -51,12 +52,12 @@
|
||||
ariaLabel
|
||||
} = $derived(edge);
|
||||
|
||||
// svelte-ignore state_referenced_locally
|
||||
setEdgeIdContext(id);
|
||||
|
||||
// svelte-ignore non_reactive_update
|
||||
let edgeRef: SVGGElement | null = null;
|
||||
|
||||
const { id } = edge;
|
||||
setContext('svelteflow__edge_id', id);
|
||||
|
||||
let selectable = $derived(_selectable ?? store.elementsSelectable);
|
||||
let focusable = $derived(_focusable ?? store.edgesFocusable);
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import {
|
||||
Position,
|
||||
XYHandle,
|
||||
@@ -16,8 +15,8 @@
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
|
||||
import type { ConnectableContext } from '../NodeWrapper/types';
|
||||
import type { HandleProps } from './types';
|
||||
import { getNodeConnectableContext, getNodeIdContext } from '$lib/store/context';
|
||||
|
||||
let {
|
||||
id: handleId = null,
|
||||
@@ -35,8 +34,10 @@
|
||||
...rest
|
||||
}: HandleProps = $props();
|
||||
|
||||
const nodeId = getContext<string>('svelteflow__node_id');
|
||||
const isConnectableContext = getContext<ConnectableContext>('svelteflow__node_connectable');
|
||||
const nodeId = getNodeIdContext('Handle must be used within a Custom Node component');
|
||||
const isConnectableContext = getNodeConnectableContext(
|
||||
'Handle must be used within a Custom Node component'
|
||||
);
|
||||
|
||||
let isTarget = $derived(type === 'target');
|
||||
let isConnectable = $derived(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" generics="NodeType extends Node = Node, EdgeType extends Edge = Edge">
|
||||
import { setContext, onDestroy } from 'svelte';
|
||||
import { onDestroy } from 'svelte';
|
||||
import {
|
||||
elementSelectionKeys,
|
||||
errorMessages,
|
||||
@@ -10,13 +10,15 @@
|
||||
} from '@xyflow/system';
|
||||
|
||||
import drag from '$lib/actions/drag';
|
||||
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
|
||||
|
||||
import type { ConnectableContext, NodeWrapperProps } from './types';
|
||||
import type { Node, Edge, NodeEvents } from '$lib/types';
|
||||
import { setNodeConnectableContext, setNodeIdContext } from '$lib/store/context';
|
||||
import { arrowKeyDiffs, toPxString } from '$lib/utils';
|
||||
import { ARIA_NODE_DESC_KEY } from '../A11yDescriptions';
|
||||
|
||||
import type { Node, Edge, NodeEvents } from '$lib/types';
|
||||
import type { ConnectableContext, NodeWrapperProps } from './types';
|
||||
|
||||
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
|
||||
|
||||
let {
|
||||
store = $bindable(),
|
||||
node,
|
||||
@@ -95,8 +97,9 @@
|
||||
return connectable;
|
||||
}
|
||||
};
|
||||
setContext('svelteflow__node_connectable', connectableContext);
|
||||
setContext('svelteflow__node_id', id);
|
||||
|
||||
setNodeIdContext(id);
|
||||
setNodeConnectableContext(connectableContext);
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
$effect(() => {
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { key } from '$lib/store';
|
||||
import type { StoreContext } from '$lib/store/types';
|
||||
import { getContext } from 'svelte';
|
||||
|
||||
/**
|
||||
* Warns the user that they should use $derived() when calling a hook.
|
||||
* This is not neccessarry when the hook is called inside a child of <SvelteFlowFlow />,
|
||||
* however exceptions can be made if you don't want to return a closure.
|
||||
* @param functionName - The name of the function that is being called
|
||||
* @param force - If true, the warning will be shown regardless if child of <SvelteFlowFlow />
|
||||
*/
|
||||
export function derivedWarning(functionName: string) {
|
||||
const storeContext = getContext<StoreContext>(key);
|
||||
|
||||
if (!storeContext) {
|
||||
throw new Error(
|
||||
`In order to use ${functionName}() you need to wrap your component in a <SvelteFlowProvider />`
|
||||
);
|
||||
}
|
||||
|
||||
if (storeContext.provider && typeof window === 'object' && !$effect.tracking()) {
|
||||
throw new Error(`Use $derived(${functionName}()) to receive updates when values change.`);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from '@xyflow/system';
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
import { getContext } from 'svelte';
|
||||
import { getNodeIdContext } from '$lib/store/context';
|
||||
|
||||
type UseNodeConnectionsParams = {
|
||||
id?: string;
|
||||
@@ -42,7 +42,7 @@ export function useNodeConnections({
|
||||
}: UseNodeConnectionsParams = {}) {
|
||||
const { edges, connectionLookup } = $derived(useStore());
|
||||
|
||||
const contextNodeId = getContext<string>('svelteflow__node_id');
|
||||
const contextNodeId = getNodeIdContext();
|
||||
const nodeId = id ?? contextNodeId;
|
||||
|
||||
let connectionMaps: { previous: ConnectionMap; next: ConnectionMap } = {
|
||||
|
||||
@@ -2,7 +2,6 @@ import { getContext } from 'svelte';
|
||||
import type { StoreContext, SvelteFlowStore } from '../store/types';
|
||||
|
||||
import { key } from '../store';
|
||||
import { derivedWarning } from './derivedWarning.svelte';
|
||||
import type { Node, Edge } from '$lib/types';
|
||||
|
||||
export function useStore<
|
||||
@@ -17,9 +16,5 @@ export function useStore<
|
||||
);
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
derivedWarning('useStore');
|
||||
}
|
||||
|
||||
return storeContext.getStore();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable svelte/prefer-svelte-reactivity */
|
||||
import { useStore } from '$lib/store';
|
||||
import { getContext } from 'svelte';
|
||||
import { getNodeIdContext } from '$lib/store/context';
|
||||
|
||||
/**
|
||||
* When you programmatically add or remove handles to a node or update a node's
|
||||
@@ -14,7 +14,7 @@ import { getContext } from 'svelte';
|
||||
*/
|
||||
export function useUpdateNodeInternals(): (nodeId?: string | string[]) => void {
|
||||
const { domNode, updateNodeInternals } = $derived(useStore());
|
||||
const nodeId = getContext('svelteflow__node_id') as string | undefined;
|
||||
const nodeId = getNodeIdContext();
|
||||
|
||||
// @todo: do we want to add this to system?
|
||||
const updateInternals = (id?: string | string[]) => {
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
import { getEdgeToolbarTransform } from '@xyflow/system';
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
import { getEdgeIdContext } from '$lib/store/context';
|
||||
|
||||
import { EdgeLabel } from '$lib/components/EdgeLabel';
|
||||
import type { EdgeToolbarProps } from './types';
|
||||
import { getContext } from 'svelte';
|
||||
|
||||
let {
|
||||
x,
|
||||
@@ -19,11 +20,8 @@
|
||||
}: EdgeToolbarProps = $props();
|
||||
|
||||
const store = useStore();
|
||||
const edgeId = getContext<string>('svelteflow__edge_id');
|
||||
|
||||
if (!edgeId) {
|
||||
throw new Error('EdgeToolbar must be used within an edge');
|
||||
}
|
||||
const edgeId = getEdgeIdContext('EdgeToolbar must be used within an edge');
|
||||
|
||||
const isActive = $derived(
|
||||
typeof isVisible === 'boolean' ? isVisible : store.edgeLookup.get(edgeId)?.selected
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { getContext, onMount } from 'svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
import { getNodeIdContext } from '$lib/store/context';
|
||||
import {
|
||||
XYResizer,
|
||||
ResizeControlVariant,
|
||||
@@ -9,8 +11,8 @@
|
||||
type XYResizerChange,
|
||||
type XYResizerChildChange
|
||||
} from '@xyflow/system';
|
||||
|
||||
import type { ResizeControlProps } from './types';
|
||||
import type { Node } from '$lib/types';
|
||||
|
||||
let {
|
||||
nodeId,
|
||||
@@ -34,10 +36,14 @@
|
||||
}: ResizeControlProps = $props();
|
||||
|
||||
const store = useStore();
|
||||
const contextNodeId = getNodeIdContext();
|
||||
|
||||
let id = $derived(
|
||||
typeof nodeId === 'string' ? nodeId : getContext<string>('svelteflow__node_id')
|
||||
);
|
||||
let id = $derived(typeof nodeId === 'string' ? nodeId : contextNodeId);
|
||||
|
||||
// svelte-ignore state_referenced_locally
|
||||
if (!id) {
|
||||
throw new Error('Either pass a nodeId or use within a Custom Node component');
|
||||
}
|
||||
|
||||
let resizeControlRef: HTMLDivElement;
|
||||
let resizer: XYResizerInstance | null = $state(null);
|
||||
@@ -72,7 +78,7 @@
|
||||
changes.set(id, change);
|
||||
|
||||
for (const childChange of childChanges) {
|
||||
changes.set(childChange.id, {x: childChange.position.x, y: childChange.position.y });
|
||||
changes.set(childChange.id, { x: childChange.position.x, y: childChange.position.y });
|
||||
}
|
||||
|
||||
store.nodes = store.nodes.map((node) => {
|
||||
@@ -81,7 +87,7 @@
|
||||
const vertical = !resizeDirection || resizeDirection === 'vertical';
|
||||
|
||||
if (change) {
|
||||
return {
|
||||
return {
|
||||
...node,
|
||||
position: {
|
||||
x: horizontal ? (change.x ?? node.position.x) : node.position.x,
|
||||
|
||||
@@ -53,7 +53,6 @@ export type NodeResizerProps = {
|
||||
|
||||
export type ResizeControlProps = Pick<
|
||||
NodeResizerProps,
|
||||
| 'nodeId'
|
||||
| 'color'
|
||||
| 'minWidth'
|
||||
| 'minHeight'
|
||||
@@ -76,5 +75,7 @@ export type ResizeControlProps = Pick<
|
||||
* @example ResizeControlVariant.Handle, ResizeControlVariant.Line
|
||||
*/
|
||||
variant?: ResizeControlVariant;
|
||||
/** nodeId must be provided when used outside a custom node */
|
||||
nodeId?: string;
|
||||
children?: Snippet;
|
||||
} & HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import { Position, getNodeToolbarTransform } from '@xyflow/system';
|
||||
|
||||
import { hideOnSSR, portal } from '$lib/actions/portal';
|
||||
import { useStore } from '$lib/store';
|
||||
import { getNodeIdContext } from '$lib/store/context';
|
||||
import { hideOnSSR, portal } from '$lib/actions/portal';
|
||||
import { useSvelteFlow } from '$lib/hooks/useSvelteFlow.svelte';
|
||||
|
||||
import type { InternalNode } from '$lib/types';
|
||||
@@ -22,7 +22,7 @@
|
||||
const store = useStore();
|
||||
|
||||
const { getNodesBounds } = useSvelteFlow();
|
||||
const contextNodeId = getContext<string>('svelteflow__node_id');
|
||||
const contextNodeId = getNodeIdContext();
|
||||
|
||||
let toolbarNodes: InternalNode[] = $derived.by(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
@@ -30,6 +30,9 @@
|
||||
const nodeIds = Array.isArray(nodeId) ? nodeId : [nodeId ?? contextNodeId];
|
||||
|
||||
return nodeIds.reduce<InternalNode[]>((res, nodeId) => {
|
||||
if (!nodeId) {
|
||||
throw new Error('Either pass a nodeId or use within a Custom Node component');
|
||||
}
|
||||
const node = store.nodeLookup.get(nodeId);
|
||||
|
||||
if (node) {
|
||||
|
||||
35
packages/svelte/src/lib/store/context.ts
Normal file
35
packages/svelte/src/lib/store/context.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { ConnectableContext } from '$lib/components/NodeWrapper/types';
|
||||
import { setContext, getContext, hasContext } from 'svelte';
|
||||
|
||||
/**
|
||||
* Creates a type-safe context getter and setter pair.
|
||||
* Extended from Svelte's official createContext pattern.
|
||||
* - When called with an error message string, it throws if the context is not set
|
||||
* - When called without arguments, it returns the context value or undefined
|
||||
*/
|
||||
function createContext<T>(): [
|
||||
{
|
||||
(errorMessage: string): T;
|
||||
(): T | undefined;
|
||||
},
|
||||
(context: T) => T
|
||||
] {
|
||||
const key = {};
|
||||
|
||||
return [
|
||||
(errorMessage?: string) => {
|
||||
if (errorMessage && !hasContext(key)) {
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
return getContext(key);
|
||||
},
|
||||
(context) => setContext(key, context)
|
||||
];
|
||||
}
|
||||
|
||||
export const [getNodeIdContext, setNodeIdContext] = createContext<string>();
|
||||
export const [getNodeConnectableContext, setNodeConnectableContext] =
|
||||
createContext<ConnectableContext>();
|
||||
|
||||
export const [getEdgeIdContext, setEdgeIdContext] = createContext<string>();
|
||||
@@ -36,8 +36,10 @@ export interface EdgeLayoutBaseOptions<NodeType extends Node = Node, EdgeType ex
|
||||
onerror: OnError;
|
||||
}
|
||||
|
||||
export interface EdgeLayoutAllOptions<NodeType extends Node = Node, EdgeType extends Edge = Edge>
|
||||
extends EdgeLayoutBaseOptions<NodeType, EdgeType> {
|
||||
export interface EdgeLayoutAllOptions<
|
||||
NodeType extends Node = Node,
|
||||
EdgeType extends Edge = Edge
|
||||
> extends EdgeLayoutBaseOptions<NodeType, EdgeType> {
|
||||
onlyRenderVisible: never;
|
||||
visibleNodes: never;
|
||||
transform: never;
|
||||
|
||||
Reference in New Issue
Block a user