Merge pull request #117 from guarzo/guarzo/import

Clean-up theme swap logic for nodes
This commit is contained in:
Aleksei Chichenkov
2025-01-14 17:15:53 +03:00
committed by GitHub
13 changed files with 372 additions and 234 deletions

View File

@@ -1,7 +1,6 @@
import { ForwardedRef, forwardRef, MouseEvent, useCallback, useEffect, useMemo } from 'react';
import ReactFlow, {
Background,
ConnectionMode,
Edge,
MiniMap,
Node,
@@ -23,12 +22,10 @@ import {
ContextMenuConnection,
ContextMenuRoot,
SolarSystemEdge,
SolarSystemNodeDefault,
SolarSystemNodeTheme,
useContextMenuConnectionHandlers,
useContextMenuRootHandlers,
} from './components';
import { wrapNode } from './utils/wrapNode';
import { getBehaviorForTheme } from './helpers/getThemeBehavior';
import { OnMapAddSystemCallback, OnMapSelectionChange } from './map.types';
import { SESSION_KEY } from '@/hooks/Mapper/constants.ts';
import { SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
@@ -77,9 +74,6 @@ const initialEdges = [
},
];
const edgeTypes = {
floating: SolarSystemEdge,
};
@@ -123,23 +117,20 @@ const MapComp = ({
const [nodes, , onNodesChange] = useNodesState<Node<SolarSystemRawType>>(initialNodes);
const [edges, , onEdgesChange] = useEdgesState<Edge<SolarSystemConnection>>(initialEdges);
const nodeTypes = useMemo(() => {
return {
custom:
theme !== '' && theme !== 'default'
? wrapNode(SolarSystemNodeTheme)
: wrapNode(SolarSystemNodeDefault),
};
}, [theme]);
useMapHandlers(refn, onSelectionChange);
useUpdateNodes(nodes);
const { handleRootContext, ...rootCtxProps } = useContextMenuRootHandlers({ onAddSystem });
const { handleConnectionContext, ...connectionCtxProps } = useContextMenuConnectionHandlers();
const { update } = useMapState();
const { variant, gap, size, color } = useBackgroundVars(theme);
const { isPanAndDrag, nodeComponent, connectionMode } = getBehaviorForTheme(theme || 'default');
// You can create nodeTypes dynamically based on the node component
const nodeTypes = useMemo(() => {
return {
custom: nodeComponent,
};
}, [nodeComponent]);
const onConnect: OnConnect = useCallback(
params => {
@@ -228,7 +219,7 @@ const MapComp = ({
onNodesChange(nextChanges);
},
[getNode, onManualDelete, onNodesChange],
[getNode, getNodes, onManualDelete, onNodesChange],
);
useEffect(() => {
@@ -253,7 +244,7 @@ const MapComp = ({
defaultViewport={getViewPortFromStore()}
edgeTypes={edgeTypes}
nodeTypes={nodeTypes}
connectionMode={ConnectionMode.Loose}
connectionMode={connectionMode}
snapToGrid
nodeDragThreshold={10}
onNodeDragStop={handleDragStop}
@@ -286,6 +277,12 @@ const MapComp = ({
maxZoom={1.5}
elevateNodesOnSelect
deleteKeyCode={['Delete']}
{...(isPanAndDrag
? {
selectionOnDrag: true,
panOnDrag: [2],
}
: {})}
// TODO need create clear example with problem with that flag
// if system is not visible edge not drawing (and any render in Custom node is not happening)
// onlyRenderVisibleElements

View File

@@ -9,6 +9,7 @@ export type MapData = MapUnionTypes & {
visibleNodes: Set<string>;
showKSpaceBG: boolean;
isThickConnections: boolean;
linkedSigEveId: string;
};
interface MapProviderProps {

View File

@@ -6,7 +6,14 @@ $pastel-green: #88b04b;
$pastel-yellow: #ffdd59;
$dark-bg: #2d2d2d;
$text-color: #ffffff;
$tooltip-bg: #202020; // Dark background for tooltips
$tooltip-bg: #202020;
$node-bg-color: #202020;
$node-soft-bg-color: #202020;
$text-color: #ffffff;
$tag-color: #38BDF8;
$region-name: #D6D3D1;
$custom-name: #93C5FD;
.RootCustomNode {
display: flex;

View File

@@ -1,12 +1,10 @@
import { memo } from 'react';
import { Handle, Position } from 'reactflow';
import { MapSolarSystemType } from '../../map.types';
import { Handle, Position, NodeProps } from 'reactflow';
import clsx from 'clsx';
import classes from './SolarSystemNodeDefault.module.scss';
import { PrimeIcons } from 'primereact/api';
import { useSolarSystemNode } from '../../hooks/useSolarSystemNode';
import {
MARKER_BOOKMARK_BG_STYLES,
STATUS_CLASSES,
@@ -15,64 +13,36 @@ import {
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
export const SolarSystemNodeDefault = memo(props => {
const {
charactersInSystem,
classTitle,
classTitleColor,
customName,
effectName,
hasUserCharacters,
hubs,
visible,
labelCustom,
labelsInfo,
locked,
isShattered,
isThickConnections,
isWormhole,
killsCount,
killsActivityType,
regionClass,
regionName,
status,
selected,
tag,
showHandlers,
systemName,
sortedStatics,
solarSystemId,
unsplashedLeft,
unsplashedRight,
dbClick: handleDbClick,
} = useSolarSystemNode(props);
// eslint-disable-next-line react/display-name
export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>) => {
const nodeVars = useSolarSystemNode(props);
return (
<>
{visible && (
{nodeVars.visible && (
<div className={classes.Bookmarks}>
{labelCustom !== '' && (
{nodeVars.labelCustom !== '' && (
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.custom)}>
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] ">{labelCustom}</span>
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] ">{nodeVars.labelCustom}</span>
</div>
)}
{isShattered && (
{nodeVars.isShattered && (
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.shattered)}>
<span className={clsx('pi pi-chart-pie', classes.icon)} />
</div>
)}
{killsCount && (
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[killsActivityType!])}>
{nodeVars.killsCount && (
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[nodeVars.killsActivityType!])}>
<div className={clsx(classes.BookmarkWithIcon)}>
<span className={clsx(PrimeIcons.BOLT, classes.icon)} />
<span className={clsx(classes.text)}>{killsCount}</span>
<span className={clsx(classes.text)}>{nodeVars.killsCount}</span>
</div>
</div>
)}
{labelsInfo.map(x => (
{nodeVars.labelsInfo.map(x => (
<div key={x.id} className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[x.id])}>
{x.shortName}
</div>
@@ -81,19 +51,30 @@ export const SolarSystemNodeDefault = memo(props => {
)}
<div
className={clsx(classes.RootCustomNode, regionClass && classes[regionClass], classes[STATUS_CLASSES[status]], {
[classes.selected]: selected,
})}
className={clsx(
classes.RootCustomNode,
nodeVars.regionClass && classes[nodeVars.regionClass],
classes[STATUS_CLASSES[nodeVars.status]],
{
[classes.selected]: nodeVars.selected,
},
)}
>
{visible && (
{nodeVars.visible && (
<>
<div className={classes.HeadRow}>
<div className={clsx(classes.classTitle, classTitleColor, '[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]')}>
{classTitle ?? '-'}
<div
className={clsx(
classes.classTitle,
nodeVars.classTitleColor,
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]',
)}
>
{nodeVars.classTitle ?? '-'}
</div>
{tag != null && tag !== '' && (
<div className={clsx(classes.TagTitle, 'text-sky-400 font-medium')}>{tag}</div>
{nodeVars.tag != null && nodeVars.tag !== '' && (
<div className={clsx(classes.TagTitle, 'text-sky-400 font-medium')}>{nodeVars.tag}</div>
)}
<div
@@ -102,53 +83,55 @@ export const SolarSystemNodeDefault = memo(props => {
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] flex-grow overflow-hidden text-ellipsis whitespace-nowrap font-sans',
)}
>
{systemName}
{nodeVars.systemName}
</div>
{isWormhole && (
{nodeVars.isWormhole && (
<div className={classes.statics}>
{sortedStatics.map(whClass => (
{nodeVars.sortedStatics.map(whClass => (
<WormholeClassComp key={whClass} id={whClass} />
))}
</div>
)}
{effectName !== null && isWormhole && (
<div className={clsx(classes.effect, EFFECT_BACKGROUND_STYLES[effectName])} />
{nodeVars.effectName !== null && nodeVars.isWormhole && (
<div className={clsx(classes.effect, EFFECT_BACKGROUND_STYLES[nodeVars.effectName])} />
)}
</div>
<div className={clsx(classes.BottomRow, 'flex items-center justify-between')}>
{customName && (
{nodeVars.customName && (
<div className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] text-blue-300 whitespace-nowrap overflow-hidden text-ellipsis mr-0.5">
{customName}
{nodeVars.customName}
</div>
)}
{!isWormhole && !customName && (
{!nodeVars.isWormhole && !nodeVars.customName && (
<div className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] text-stone-300 whitespace-nowrap overflow-hidden text-ellipsis mr-0.5">
{regionName}
{nodeVars.regionName}
</div>
)}
{isWormhole && !customName && <div />}
{nodeVars.isWormhole && !nodeVars.customName && <div />}
<div className="flex items-center justify-end">
<div className="flex gap-1 items-center">
{locked && <i className={PrimeIcons.LOCK} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />}
{nodeVars.locked && (
<i className={PrimeIcons.LOCK} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />
)}
{hubs.includes(solarSystemId.toString()) && (
{nodeVars.hubs.includes(nodeVars.solarSystemId.toString()) && (
<i className={PrimeIcons.MAP_MARKER} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />
)}
{charactersInSystem.length > 0 && (
{nodeVars.charactersInSystem.length > 0 && (
<div
className={clsx(classes.localCounter, {
['text-amber-300']: hasUserCharacters,
['text-amber-300']: nodeVars.hasUserCharacters,
})}
>
<i className="pi pi-users" style={{ fontSize: '0.50rem' }} />
<span className="font-sans">{charactersInSystem.length}</span>
<span className="font-sans">{nodeVars.charactersInSystem.length}</span>
</div>
)}
</div>
@@ -158,19 +141,19 @@ export const SolarSystemNodeDefault = memo(props => {
)}
</div>
{visible && (
{nodeVars.visible && (
<>
{unsplashedLeft.length > 0 && (
{nodeVars.unsplashedLeft.length > 0 && (
<div className={classes.Unsplashed}>
{unsplashedLeft.map(sig => (
{nodeVars.unsplashedLeft.map(sig => (
<UnsplashedSignature key={sig.sig_id} signature={sig} />
))}
</div>
)}
{unsplashedRight.length > 0 && (
{nodeVars.unsplashedRight.length > 0 && (
<div className={clsx(classes.Unsplashed, classes['Unsplashed--right'])}>
{unsplashedRight.map(sig => (
{nodeVars.unsplashedRight.map(sig => (
<UnsplashedSignature key={sig.sig_id} signature={sig} />
))}
</div>
@@ -178,44 +161,44 @@ export const SolarSystemNodeDefault = memo(props => {
</>
)}
<div onMouseDownCapture={handleDbClick} className={classes.Handlers}>
<div onMouseDownCapture={nodeVars.dbClick} className={classes.Handlers}>
<Handle
type="source"
className={clsx(classes.Handle, classes.HandleTop, {
[classes.selected]: selected,
[classes.Tick]: isThickConnections,
[classes.selected]: nodeVars.selected,
[classes.Tick]: nodeVars.isThickConnections,
})}
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
position={Position.Top}
id="a"
/>
<Handle
type="source"
className={clsx(classes.Handle, classes.HandleRight, {
[classes.selected]: selected,
[classes.Tick]: isThickConnections,
[classes.selected]: nodeVars.selected,
[classes.Tick]: nodeVars.isThickConnections,
})}
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
position={Position.Right}
id="b"
/>
<Handle
type="source"
className={clsx(classes.Handle, classes.HandleBottom, {
[classes.selected]: selected,
[classes.Tick]: isThickConnections,
[classes.selected]: nodeVars.selected,
[classes.Tick]: nodeVars.isThickConnections,
})}
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
position={Position.Bottom}
id="c"
/>
<Handle
type="source"
className={clsx(classes.Handle, classes.HandleLeft, {
[classes.selected]: selected,
[classes.Tick]: isThickConnections,
[classes.selected]: nodeVars.selected,
[classes.Tick]: nodeVars.isThickConnections,
})}
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
position={Position.Left}
id="d"
/>

View File

@@ -6,7 +6,7 @@ $pastel-green: #88b04b;
$pastel-yellow: #ffdd59;
$dark-bg: #2d2d2d;
$text-color: #ffffff;
$tooltip-bg: #202020; // Dark background for tooltips
$tooltip-bg: #202020;
.RootCustomNode {
display: flex;
@@ -160,6 +160,9 @@ $tooltip-bg: #202020; // Dark background for tooltips
z-index: 1;
display: flex;
left: 4px;
font-family: var(--rf-node-font-family, inherit) !important;
font-weight: var(--rf-node-font-weight, inherit) !important;
& > .Bookmark {
min-width: 13px;
@@ -254,32 +257,28 @@ $tooltip-bg: #202020; // Dark background for tooltips
font-weight: 500;
position: relative;
top: 1px;
font-family: var(--rf-node-font-family, inherit) !important;
font-weight: var(--rf-node-font-weight, inherit) !important;
.classTitle {
font-size: 11px;
font-weight: bold;
font-family: var(--rf-node-font-family, inherit) !important;
font-weight: var(--rf-node-font-weight, inherit) !important;
text-shadow: 0 0 2px rgb(0 0 0 / 73%);
}
.TagTitle {
font-size: 11px;
font-weight: medium;
text-shadow: 0 0 2px rgba(231, 146, 52, 0.73);
color: var(--rf-tag-color, #38BDF8);
}
/* Firefox kostyl */
@-moz-document url-prefix() {
.classSystemName {
font-family: inherit !important;
font-weight: bold;
font-family: var(--rf-node-font-family, inherit) !important;
font-weight: var(--rf-node-font-weight, inherit) !important;
}
}
.classSystemName {
font-family: inherit !important;
font-weight: bold;
font-family: var(--rf-node-font-family, inherit) !important;
font-weight: var(--rf-node-font-weight, inherit) !important;
}
.solarSystemName {
@@ -291,22 +290,42 @@ $tooltip-bg: #202020; // Dark background for tooltips
justify-content: space-between;
align-items: center;
height: 19px;
font-family: var(--rf-node-font-family, inherit) !important;
font-weight: var(--rf-node-font-weight, inherit) !important;
.tagTitle {
font-size: 11px;
font-weight: medium;
text-shadow: 0 0 2px rgba(231, 146, 52, 0.73);
font-family: var(--rf-node-font-family, inherit) !important;
font-weight: var(--rf-node-font-weight, inherit) !important;
color: var(--rf-tag-color, #38BDF8);
}
.regionName {
color: var(--rf-region-name, #D6D3D1)
color: var(--rf-region-name, #D6D3D1);
font-family: var(--rf-node-font-family, inherit) !important;
font-weight: var(--rf-node-font-weight, inherit) !important;
}
.customName {
color: var(--rf-custom-name, #93C5FD)
color: var(--rf-custom-name, #93C5FD);
font-family: var(--rf-node-font-family, inherit) !important;
font-weight: var(--rf-node-font-weight, inherit) !important;
}
.localCounter {
display: flex;
//align-items: center;
color: var(--rf-has-user-characters, #fbbf24);
font-family: var(--rf-node-font-family, inherit) !important;
font-weight: var(--rf-node-font-weight, inherit) !important;
gap: 2px;
.hasUserCharacters {
color: var(--rf-has-user-characters, #fbbf24);
font-family: var(--rf-node-font-family, inherit) !important;
font-weight: var(--rf-node-font-weight, inherit) !important;
}
& > i {

View File

@@ -1,12 +1,10 @@
import { memo } from 'react';
import { Handle, Position } from 'reactflow';
import { MapSolarSystemType } from '../../map.types';
import { Handle, Position, NodeProps } from 'reactflow';
import clsx from 'clsx';
import classes from './SolarSystemNodeTheme.module.scss';
import { PrimeIcons } from 'primereact/api';
import { useSolarSystemNode } from '../../hooks/useSolarSystemNode';
import {
MARKER_BOOKMARK_BG_STYLES,
STATUS_CLASSES,
@@ -15,64 +13,35 @@ import {
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
export const SolarSystemNodeTheme = memo(props => {
const {
charactersInSystem,
classTitle,
classTitleColor,
customName,
effectName,
hasUserCharacters,
hubs,
visible,
labelCustom,
labelsInfo,
locked,
isShattered,
isThickConnections,
isWormhole,
killsCount,
killsActivityType,
regionClass,
regionName,
status,
selected,
tag,
showHandlers,
systemName,
sortedStatics,
solarSystemId,
unsplashedLeft,
unsplashedRight,
dbClick: handleDbClick,
} = useSolarSystemNode(props);
export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>) => {
const nodeVars = useSolarSystemNode(props);
return (
<>
{visible && (
{nodeVars.visible && (
<div className={classes.Bookmarks}>
{labelCustom !== '' && (
{nodeVars.labelCustom !== '' && (
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.custom)}>
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] ">{labelCustom}</span>
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] ">{nodeVars.labelCustom}</span>
</div>
)}
{isShattered && (
{nodeVars.isShattered && (
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.shattered)}>
<span className={clsx('pi pi-chart-pie', classes.icon)} />
</div>
)}
{killsCount && (
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[killsActivityType!])}>
{nodeVars.killsCount && (
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[nodeVars.killsActivityType!])}>
<div className={clsx(classes.BookmarkWithIcon)}>
<span className={clsx(PrimeIcons.BOLT, classes.icon)} />
<span className={clsx(classes.text)}>{killsCount}</span>
<span className={clsx(classes.text)}>{nodeVars.killsCount}</span>
</div>
</div>
)}
{labelsInfo.map(x => (
{nodeVars.labelsInfo.map(x => (
<div key={x.id} className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[x.id])}>
{x.shortName}
</div>
@@ -81,18 +50,31 @@ export const SolarSystemNodeTheme = memo(props => {
)}
<div
className={clsx(classes.RootCustomNode, regionClass && classes[regionClass], classes[STATUS_CLASSES[status]], {
[classes.selected]: selected,
})}
className={clsx(
classes.RootCustomNode,
nodeVars.regionClass && classes[nodeVars.regionClass],
classes[STATUS_CLASSES[nodeVars.status]],
{
[classes.selected]: nodeVars.selected,
},
)}
>
{visible && (
{nodeVars.visible && (
<>
<div className={classes.HeadRow}>
<div className={clsx(classes.classTitle, classTitleColor, '[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]')}>
{classTitle ?? '-'}
<div
className={clsx(
classes.classTitle,
nodeVars.classTitleColor,
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]',
)}
>
{nodeVars.classTitle ?? '-'}
</div>
{tag != null && tag !== '' && <div className={clsx(classes.TagTitle)}>{tag}</div>}
{nodeVars.tag != null && nodeVars.tag !== '' && (
<div className={clsx(classes.TagTitle)}>{nodeVars.tag}</div>
)}
<div
className={clsx(
@@ -100,63 +82,65 @@ export const SolarSystemNodeTheme = memo(props => {
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] flex-grow overflow-hidden text-ellipsis whitespace-nowrap',
)}
>
{systemName}
{nodeVars.systemName}
</div>
{isWormhole && (
{nodeVars.isWormhole && (
<div className={classes.statics}>
{sortedStatics.map(whClass => (
{nodeVars.sortedStatics.map(whClass => (
<WormholeClassComp key={whClass} id={whClass} />
))}
</div>
)}
{effectName !== null && isWormhole && (
<div className={clsx(classes.effect, EFFECT_BACKGROUND_STYLES[effectName])} />
{nodeVars.effectName !== null && nodeVars.isWormhole && (
<div className={clsx(classes.effect, EFFECT_BACKGROUND_STYLES[nodeVars.effectName])} />
)}
</div>
<div className={clsx(classes.BottomRow, 'flex items-center justify-between')}>
{customName && (
{nodeVars.customName && (
<div
className={clsx(
classes.CustomName,
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] whitespace-nowrap overflow-hidden text-ellipsis mr-0.5',
)}
>
{customName}
{nodeVars.customName}
</div>
)}
{!isWormhole && !customName && (
{!nodeVars.isWormhole && !nodeVars.customName && (
<div
className={clsx(
classes.RegionName,
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] whitespace-nowrap overflow-hidden text-ellipsis mr-0.5',
)}
>
{regionName}
{nodeVars.regionName}
</div>
)}
{isWormhole && !customName && <div />}
{nodeVars.isWormhole && !nodeVars.customName && <div />}
<div className="flex items-center justify-end">
<div className="flex gap-1 items-center">
{locked && <i className={PrimeIcons.LOCK} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />}
{nodeVars.locked && (
<i className={PrimeIcons.LOCK} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />
)}
{hubs.includes(solarSystemId.toString()) && (
{nodeVars.hubs.includes(nodeVars.solarSystemId.toString()) && (
<i className={PrimeIcons.MAP_MARKER} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />
)}
{charactersInSystem.length > 0 && (
{nodeVars.charactersInSystem.length > 0 && (
<div
className={clsx(classes.localCounter, {
[classes.hasUserCharacters]: hasUserCharacters,
[classes.hasUserCharacters]: nodeVars.hasUserCharacters,
})}
>
<i className="pi pi-users" style={{ fontSize: '0.50rem' }} />
<span className="font-sans">{charactersInSystem.length}</span>
<span className="font-sans">{nodeVars.charactersInSystem.length}</span>
</div>
)}
</div>
@@ -166,19 +150,19 @@ export const SolarSystemNodeTheme = memo(props => {
)}
</div>
{visible && (
{nodeVars.visible && (
<>
{unsplashedLeft.length > 0 && (
{nodeVars.unsplashedLeft.length > 0 && (
<div className={classes.Unsplashed}>
{unsplashedLeft.map(sig => (
{nodeVars.unsplashedLeft.map(sig => (
<UnsplashedSignature key={sig.sig_id} signature={sig} />
))}
</div>
)}
{unsplashedRight.length > 0 && (
{nodeVars.unsplashedRight.length > 0 && (
<div className={clsx(classes.Unsplashed, classes['Unsplashed--right'])}>
{unsplashedRight.map(sig => (
{nodeVars.unsplashedRight.map(sig => (
<UnsplashedSignature key={sig.sig_id} signature={sig} />
))}
</div>
@@ -186,44 +170,44 @@ export const SolarSystemNodeTheme = memo(props => {
</>
)}
<div onMouseDownCapture={handleDbClick} className={classes.Handlers}>
<div onMouseDownCapture={nodeVars.dbClick} className={classes.Handlers}>
<Handle
type="source"
className={clsx(classes.Handle, classes.HandleTop, {
[classes.selected]: selected,
[classes.Tick]: isThickConnections,
[classes.selected]: nodeVars.selected,
[classes.Tick]: nodeVars.isThickConnections,
})}
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
position={Position.Top}
id="a"
/>
<Handle
type="source"
className={clsx(classes.Handle, classes.HandleRight, {
[classes.selected]: selected,
[classes.Tick]: isThickConnections,
[classes.selected]: nodeVars.selected,
[classes.Tick]: nodeVars.isThickConnections,
})}
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
position={Position.Right}
id="b"
/>
<Handle
type="source"
className={clsx(classes.Handle, classes.HandleBottom, {
[classes.selected]: selected,
[classes.Tick]: isThickConnections,
[classes.selected]: nodeVars.selected,
[classes.Tick]: nodeVars.isThickConnections,
})}
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
position={Position.Bottom}
id="c"
/>
<Handle
type="source"
className={clsx(classes.Handle, classes.HandleLeft, {
[classes.selected]: selected,
[classes.Tick]: isThickConnections,
[classes.selected]: nodeVars.selected,
[classes.Tick]: nodeVars.isThickConnections,
})}
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
position={Position.Left}
id="d"
/>

View File

@@ -0,0 +1,32 @@
import { SolarSystemNodeDefault, SolarSystemNodeTheme } from '../components/SolarSystemNode';
import type { NodeProps } from 'reactflow';
import type { ComponentType } from 'react';
import { MapSolarSystemType } from '../map.types';
import { ConnectionMode } from 'reactflow';
export type SolarSystemNodeComponent = ComponentType<NodeProps<MapSolarSystemType>>;
interface ThemeBehavior {
isPanAndDrag: boolean;
nodeComponent: SolarSystemNodeComponent;
connectionMode: ConnectionMode;
}
const THEME_BEHAVIORS: {
[key: string]: ThemeBehavior;
} = {
default: {
isPanAndDrag: false,
nodeComponent: SolarSystemNodeDefault,
connectionMode: ConnectionMode.Loose,
},
pathfinder: {
isPanAndDrag: true,
nodeComponent: SolarSystemNodeTheme,
connectionMode: ConnectionMode.Loose,
},
};
export function getBehaviorForTheme(themeName: string) {
return THEME_BEHAVIORS[themeName] ?? THEME_BEHAVIORS.default;
}

View File

@@ -1,5 +1,6 @@
import { useMemo } from 'react';
import { MapSolarSystemType } from '../map.types';
import { NodeProps } from 'reactflow';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useMapGetOption } from '@/hooks/Mapper/mapRootProvider/hooks/api';
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider';
@@ -30,19 +31,9 @@ function sortedLabels(labels: string[]) {
return LABELS_ORDER.filter(x => labels.includes(x)).map(x => LABELS_INFO[x]);
}
export function useSolarSystemNode(props: any) {
const { data, selected, id } = props;
const {
system_static_info,
system_signatures,
locked,
name,
tag,
status,
labels,
temporary_name,
linked_sig_eve_id: linkedSigEveId = '',
} = data;
export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>) {
const { id, data, selected } = props;
const { system_static_info, system_signatures, locked, name, tag, status, labels, temporary_name } = data;
const {
system_class,
@@ -57,7 +48,6 @@ export function useSolarSystemNode(props: any) {
solar_system_name,
} = system_static_info;
// Global map state
const {
interfaceSettings,
data: { systemSignatures: mapSystemSignatures },
@@ -81,11 +71,11 @@ export function useSolarSystemNode(props: any) {
visibleNodes,
showKSpaceBG,
isThickConnections,
linkedSigEveId,
},
outCommand,
} = useMapState();
// logic
const visible = useMemo(() => visibleNodes.has(id), [id, visibleNodes]);
const systemSignatures = useMemo(
@@ -174,10 +164,9 @@ export function useSolarSystemNode(props: any) {
}, [isShowUnsplashedSignatures, systemSignatures]);
const nodeVars = {
// original props
id,
selected,
// computed
visible,
isWormhole,
classTitleColor,

View File

@@ -1,11 +0,0 @@
// wrapNode.ts
import { NodeProps } from 'reactflow';
import { SolarSystemNodeProps } from '../components/SolarSystemNode';
export function wrapNode<T>(
SolarSystemNode: React.FC<SolarSystemNodeProps<T>>
): React.FC<NodeProps<T>> {
return function NodeAdapter(props) {
return <SolarSystemNode {...props} />;
};
}

View File

@@ -130,4 +130,3 @@ export type SearchSystemItem = {
system_static_info: SolarSystemStaticInfoRaw;
value: number;
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,138 @@
%{
title: "How to Add a Custom Theme",
author: "Wanderer Community",
cover_image_uri: "/images/news/01-13-theme-swap/theme-selector.png",
tags: ~w(themes),
description: "",
}
---
# How to Add a Custom Theme
Adding a custom theme to your map is a great way to give it a unique look and feel. In this guide, well walk you through the necessary steps to create and enable a brand-new theme, from updating the theme selector to creating custom SCSS files.
---
1. Add Your Theme to the Theme Selector
Open the file:
assets/js/hooks/Mapper/components/mapRootContent/components/MapSettings/MapSettings.tsx
In this file, youll find an array called `THEME_OPTIONS`. Simply add your new theme to this array, giving it both a `label` and a `value`. For example:
```ts
const THEME_OPTIONS = [
{ label: 'Default', value: 'default' },
{ label: 'Pathfinder', value: 'pathfinder' },
{ label: 'YourTheme', value: 'yourtheme' },
];
```
This ensures your new theme will appear in the theme selection menu.
---
2. Create the SCSS File for Your Theme
Next, youll need to create a new SCSS file to define your themes custom styles. Navigate to:
```
assets/js/hooks/Mapper/components/map/styles
```
and add a new file. You can use `pathfinder-theme.scss` as a reference. The **filename must be in the format**:
```
yourthemename-theme.scss
```
> **Why the specific format?**
> The system looks for theme files following this naming pattern. If you choose a different format, it will not load correctly.
# 2.1. Define Your CSS Variables
Inside your theme SCSS file, you can override the variables below to customize colors, backgrounds, patterns, text, and more. For example:
```scss
// yourtheme-theme.scss
:root {
/* Main pane background color */
--rf-bg-color: #222;
--rf-soft-bg-color: #333;
/* Background pattern settings */
--rf-bg-variant: lines;
--rf-bg-gap: 10px;
--rf-bg-size: 1px;
--rf-bg-pattern-color: rgba(255, 255, 255, 0.15);
/* Node (system) appearance */
--rf-node-bg-color: #444;
--rf-node-soft-bg-color: #555;
--rf-node-font-family: "Roboto", sans-serif;
--rf-text-color: #f5f5f5;
--rf-region-name: #a3e4d7;
--rf-custom-name: #d7bde2;
--rf-tag-color: #e59866;
--rf-has-user-character: #f9e79f;
/* Eve-specific overrides */
--eve-effect-nullsec: #ff0000;
--eve-wh-type-color-C1: #aaffaa;
/* ...etc... */
}
```
> **Tip:** Feel free to rename or add new custom variables as necessary, but keep in mind the defaults and naming conventions used throughout the existing code.
---
3. Customize Node-Related Styles
If you want to override more specific aspects of the node styling, review the file:
```
assets/js/hooks/Mapper/components/map/components/SolarSystemNode/SolarSystemNodeTheme.module.scss
```
This file shows which variables are already set up for styling through CSS variables. If the element you want to style already has a variable reference, you can simply override that variable in your SCSS theme file.
---
4. Update Theme Behavior (Optional)
Finally, if your theme requires special interactions, you can update the theme behavior in:
```
assets/js/hooks/Mapper/components/map/helpers/getThemeBehavior.ts
```
By default, some overrides are already set up. For example:
- `isPanAndDrag: true` sets left-click to select, and right-click to pan. (When `false`, it uses the default behavior)
- `nodeComponent: SolarSystemNodeTheme` specifies a special node component that uses theme CSS overrides -- you could also provide your own node component here
- `connectionMode: ConnectionMode.Loose` allows you to control how strict the connection handles are.
If your theme needs custom logic—like a different node component or a unique interaction pattern—this is where youd implement it.
---
Summary
1. **Add your theme** to `THEME_OPTIONS` in `MapSettings.tsx`.
2. **Create a custom SCSS file** with the pattern `yourtheme-theme.scss` and override any relevant variables.
3. **Check for additional styling** in `SolarSystemNodeTheme.module.scss` to see if there are more elements youd like to override.
4. (Optional) **Modify the theme behavior** in `getThemeBehavior.ts` if you want your theme to have unique interaction patterns or different default behaviors.
By following these steps, youll be able to quickly add your own themed experience to the map. If you need to make further changes (like adding new variables or hooking into different node components), just follow the same pattern and refer to existing examples in the codebase. Happy theming!
---
### Example of heavily customize node component and theme
![Faoble Theme]("/images/news/01-13-theme-swap/faoble-theme.png")