@visualli/react 0.1.7 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -5
- package/dist/index.cjs +1900 -2033
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +354 -97
- package/dist/index.d.ts +354 -97
- package/dist/index.js +2005 -2095
- package/dist/index.js.map +1 -1
- package/fonts/Caveat-Variable.ttf +0 -0
- package/licenses/visualli-design-system/LICENSE +9 -0
- package/licenses/visualli-design-system/NOTICE.md +6 -0
- package/package.json +9 -4
- package/src/VisualliCanvas.tsx +452 -409
- package/src/VisualliRenderer.tsx +90 -112
- package/src/components/A11yLayer.tsx +73 -0
- package/src/components/KonvaContainer.tsx +28 -237
- package/src/components/KonvaContainerLabelLayer.tsx +41 -0
- package/src/components/KonvaContainerLayer.tsx +34 -33
- package/src/components/KonvaEdge.tsx +25 -240
- package/src/components/KonvaEdgeLayer.tsx +69 -49
- package/src/components/KonvaNode.tsx +21 -156
- package/src/components/KonvaNodeLayer.tsx +102 -34
- package/src/components/KonvaStage.tsx +17 -3
- package/src/components/NavigationStack.tsx +16 -159
- package/src/components/Overlays.tsx +327 -0
- package/src/components/ZoomControls.tsx +3 -107
- package/src/components/useFrames.ts +30 -0
- package/src/context/VisualliContext.tsx +3 -3
- package/src/design/choreography.ts +157 -0
- package/src/design/containers.ts +150 -0
- package/src/design/design.ts +115 -0
- package/src/design/drawing.ts +303 -0
- package/src/design/measure.ts +34 -0
- package/src/design/runtime.ts +154 -0
- package/src/design/useDesign.ts +38 -0
- package/src/generated/specCss.ts +15 -0
- package/src/hooks/index.ts +0 -2
- package/src/hooks/useKonvaRenderer.ts +13 -18
- package/src/hooks/useLayerChoreography.ts +159 -0
- package/src/hooks/useViewportNodes.ts +0 -1
- package/src/index.ts +22 -22
- package/src/stores/stores.ts +2 -2
- package/src/stores/useViewportStore.ts +12 -2
- package/src/animations/index.ts +0 -2
- package/src/animations/konvaLayerTransition.ts +0 -223
- package/src/components/SketchyBoxKonva.tsx +0 -121
- package/src/config/blobShapes.ts +0 -204
- package/src/config/index.ts +0 -21
- package/src/config/textScaling.ts +0 -69
- package/src/hooks/useKonvaLayerTransition.ts +0 -221
|
@@ -1,61 +1,129 @@
|
|
|
1
1
|
// ─── KonvaNodeLayer ───────────────────────────────────────────────────────────
|
|
2
2
|
//
|
|
3
|
-
// Konva <
|
|
4
|
-
//
|
|
3
|
+
// One Konva <Shape> paints every visible idea. Culling uses the stage's live
|
|
4
|
+
// transform at draw time (so panning, which moves the stage directly without
|
|
5
|
+
// React state, never shows blank areas) and the idea's centred visual bounds
|
|
6
|
+
// (RBush above 200 ideas). Hit testing stays outside Konva (stage-level RBush).
|
|
5
7
|
|
|
6
|
-
import React, { useRef, useLayoutEffect,
|
|
7
|
-
import { KLayer as Layer } from '../konvaCompat';
|
|
8
|
+
import React, { useRef, useLayoutEffect, useMemo } from 'react';
|
|
9
|
+
import { KLayer as Layer, KShape as Shape } from '../konvaCompat';
|
|
8
10
|
import type Konva from 'konva';
|
|
9
|
-
import type
|
|
10
|
-
import { useViewportStore } from '../stores/useViewportStore';
|
|
11
|
+
import { CANVAS_STYLE, RBushSpatialIndex, layerBlooms, shadowsShown, intersectsViewport, nodeBounds, type FlatNode } from '@visualli/core';
|
|
11
12
|
import { useNodeStore } from '../stores/useNodeStore';
|
|
12
|
-
import {
|
|
13
|
-
import
|
|
13
|
+
import { DEFAULT_DESIGN, type Design } from '../design/design';
|
|
14
|
+
import { useFontsEpoch } from '../design/runtime';
|
|
15
|
+
import { IDLE, drawIdea, type IdeaState } from '../design/drawing';
|
|
16
|
+
import { HoverTweens, type RevealClock, type Arrival } from '../design/choreography';
|
|
17
|
+
import { useFrames } from './useFrames';
|
|
14
18
|
|
|
19
|
+
const SPATIAL_INDEX_THRESHOLD = 200;
|
|
15
20
|
export interface KonvaNodeLayerProps {
|
|
16
21
|
nodes?: FlatNode[];
|
|
17
22
|
isTransitioning?: boolean;
|
|
18
23
|
isDragging?: boolean;
|
|
19
24
|
hoveredNodeId?: string | null;
|
|
20
25
|
pressedNodeId?: string | null;
|
|
26
|
+
selectedNodeId?: string | null;
|
|
27
|
+
/** Idea with keyboard focus (accessible layer). */
|
|
28
|
+
focusedNodeId?: string | null;
|
|
29
|
+
design?: Design;
|
|
30
|
+
/** The layer's arrival timeline (bloom). Omit for no arrival animation. */
|
|
31
|
+
clock?: RevealClock;
|
|
32
|
+
/** Touch layout (`.vi-map.is-touch`): the other ideas fade while one is peeked. */
|
|
33
|
+
touch?: boolean;
|
|
34
|
+
/** Idea whose peek (sheet) is open on touch. */
|
|
35
|
+
peekedNodeId?: string | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** World-space rectangle currently visible in the stage. */
|
|
39
|
+
export function stageViewRect(stage: Konva.Stage) {
|
|
40
|
+
const s = stage.scaleX() || 1;
|
|
41
|
+
return { minX: -stage.x() / s, minY: -stage.y() / s, maxX: (stage.width() - stage.x()) / s, maxY: (stage.height() - stage.y()) / s };
|
|
21
42
|
}
|
|
22
43
|
|
|
23
44
|
export default function KonvaNodeLayer({
|
|
24
45
|
nodes: propNodes,
|
|
25
|
-
isTransitioning = false,
|
|
26
46
|
isDragging = false,
|
|
27
47
|
hoveredNodeId = null,
|
|
28
48
|
pressedNodeId = null,
|
|
49
|
+
selectedNodeId = null,
|
|
50
|
+
focusedNodeId = null,
|
|
51
|
+
design = DEFAULT_DESIGN,
|
|
52
|
+
clock,
|
|
53
|
+
touch = false,
|
|
54
|
+
peekedNodeId = null,
|
|
29
55
|
}: KonvaNodeLayerProps) {
|
|
30
|
-
const layerRef
|
|
31
|
-
const
|
|
32
|
-
const storeNodes = useNodeStore(s => s.nodes);
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
)
|
|
37
|
-
|
|
56
|
+
const layerRef = useRef<Konva.Layer | null>(null);
|
|
57
|
+
const fonts = useFontsEpoch();
|
|
58
|
+
const storeNodes = useNodeStore((s) => s.nodes);
|
|
59
|
+
const nodes: FlatNode[] = useMemo(() => propNodes ?? Array.from(storeNodes.values()), [propNodes, storeNodes]);
|
|
60
|
+
|
|
61
|
+
// Spatial index over visual bounds. Rebuilt only when the node set changes; while dragging we draw everything.
|
|
62
|
+
const index = useMemo(() => {
|
|
63
|
+
if (nodes.length < SPATIAL_INDEX_THRESHOLD || isDragging) return null;
|
|
64
|
+
const idx = new RBushSpatialIndex();
|
|
65
|
+
idx.bulkLoad(nodes.map((n) => ({ nodeId: n.id, bounds: nodeBounds(n) })));
|
|
66
|
+
return idx;
|
|
67
|
+
}, [nodes, isDragging]);
|
|
68
|
+
const byId = useMemo(() => (index ? new Map(nodes.map((n) => [n.id, n])) : null), [index, nodes]);
|
|
69
|
+
|
|
70
|
+
// While an idea is peeked the others are dimmed (`.vi-node.is-dimmed`): unchanged by default, faded in the focus
|
|
71
|
+
// themes and on touch — the design system's opacities (CANVAS_STYLE from css/spec.css).
|
|
72
|
+
const highlighted = hoveredNodeId ?? peekedNodeId ?? selectedNodeId;
|
|
73
|
+
const N = CANVAS_STYLE.node;
|
|
74
|
+
const focusTheme = design.theme.startsWith('focus');
|
|
75
|
+
const dimOpacity = focusTheme ? (touch ? N.dimmedOpacityFocusTouch : N.dimmedOpacityFocus) : touch ? N.dimmedOpacityTouch : N.dimmedOpacity;
|
|
76
|
+
const dimOthers = highlighted !== null && dimOpacity !== 1;
|
|
77
|
+
|
|
78
|
+
// Hover / press / select: the design system's lift and ring turn, tweened rather than snapped.
|
|
79
|
+
const tweens = useRef(new HoverTweens()).current;
|
|
80
|
+
const frames = useFrames(layerRef, () => clock?.active() || tweens.active());
|
|
81
|
+
useLayoutEffect(() => {
|
|
82
|
+
const raised = new Set<string>();
|
|
83
|
+
if (hoveredNodeId) raised.add(hoveredNodeId);
|
|
84
|
+
if (pressedNodeId) raised.add(pressedNodeId);
|
|
85
|
+
if (selectedNodeId) raised.add(selectedNodeId);
|
|
86
|
+
if (tweens.set(raised, design.comfort.reducedMotion || !layerBlooms(nodes.length))) frames.run();
|
|
87
|
+
}, [hoveredNodeId, pressedNodeId, selectedNodeId, design.comfort.reducedMotion, layerBlooms(nodes.length), tweens, frames]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
88
|
+
useLayoutEffect(() => clock?.subscribe(() => frames.run()), [clock, frames]);
|
|
89
|
+
|
|
90
|
+
useLayoutEffect(() => { layerRef.current?.batchDraw(); }, [nodes, index, design, fonts, hoveredNodeId, pressedNodeId, selectedNodeId, focusedNodeId, isDragging, touch, peekedNodeId]);
|
|
38
91
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
92
|
+
const sceneFunc = (ctx: { _context: CanvasRenderingContext2D }, shape: Konva.Shape) => {
|
|
93
|
+
const stage = shape.getStage();
|
|
94
|
+
if (!stage) return;
|
|
95
|
+
const zoom = stage.scaleX() || 1;
|
|
96
|
+
const view = stageViewRect(stage);
|
|
97
|
+
let visible: FlatNode[];
|
|
98
|
+
if (isDragging) visible = nodes;
|
|
99
|
+
else if (index && byId) visible = index.query(view).map((id) => byId.get(id)!).filter(Boolean);
|
|
100
|
+
else visible = nodes.filter((n) => intersectsViewport(n, view));
|
|
42
101
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
102
|
+
const c = ctx._context;
|
|
103
|
+
const m = c.getTransform();
|
|
104
|
+
const opt = { shadows: !isDragging && shadowsShown(visible.length), k: Math.hypot(m.a, m.b), base: m, dimOpacity };
|
|
105
|
+
const st: IdeaState = { ...IDLE };
|
|
106
|
+
const now = performance.now();
|
|
107
|
+
const arrival: Arrival = { alpha: 1, scale: 1, dx: 0, dy: 0 };
|
|
108
|
+
const arriving = clock?.active(now) ?? false;
|
|
109
|
+
for (let i = 0; i < visible.length; i++) {
|
|
110
|
+
const n = visible[i]!;
|
|
111
|
+
st.hovered = n.id === hoveredNodeId;
|
|
112
|
+
st.pressed = n.id === pressedNodeId;
|
|
113
|
+
st.selected = n.id === selectedNodeId;
|
|
114
|
+
st.focused = n.id === focusedNodeId;
|
|
115
|
+
st.dimmed = dimOthers && n.id !== highlighted;
|
|
116
|
+
if (tweens.has(n.id)) { const p = tweens.progress(n.id, now); st.lift = p.lift; st.ring = p.ring; } else { st.lift = undefined; st.ring = undefined; }
|
|
117
|
+
st.arrival = arriving && clock!.node(n.id, n.x, n.y, now, arrival) ? arrival : null;
|
|
118
|
+
drawIdea(c, n, design, zoom, st, opt);
|
|
119
|
+
}
|
|
120
|
+
c.setTransform(m);
|
|
121
|
+
c.globalAlpha = 1;
|
|
122
|
+
};
|
|
46
123
|
|
|
47
124
|
return (
|
|
48
|
-
<Layer ref={layerRef} name="nodes">
|
|
49
|
-
{
|
|
50
|
-
<KonvaNode
|
|
51
|
-
key={n.id}
|
|
52
|
-
node={n}
|
|
53
|
-
zoomLevel={zoomLevel}
|
|
54
|
-
isExternallyHovered={hoveredNodeId === n.id}
|
|
55
|
-
isExternallyPressed={pressedNodeId === n.id}
|
|
56
|
-
isDragging={isDragging}
|
|
57
|
-
/>
|
|
58
|
-
))}
|
|
125
|
+
<Layer ref={layerRef} name="nodes" listening={false}>
|
|
126
|
+
<Shape sceneFunc={sceneFunc} listening={false} perfectDrawEnabled={false} />
|
|
59
127
|
</Layer>
|
|
60
128
|
);
|
|
61
129
|
}
|
|
@@ -4,11 +4,14 @@
|
|
|
4
4
|
// • Binds canvas position/scale from the viewport store
|
|
5
5
|
// • Accepts optional event handlers (wheel, mouse, touch, context-menu)
|
|
6
6
|
// • Calls stage.destroy() on unmount (memory hygiene)
|
|
7
|
+
// • Renders at devicePixelRatio and follows it when it changes (browser zoom,
|
|
8
|
+
// moving the window to another display)
|
|
7
9
|
|
|
8
10
|
import React, { useRef, useEffect, forwardRef } from 'react';
|
|
9
11
|
import { KStage as Stage } from '../konvaCompat';
|
|
10
|
-
import
|
|
12
|
+
import Konva from 'konva';
|
|
11
13
|
import { useViewportStore } from '../stores/useViewportStore';
|
|
14
|
+
import { currentPixelRatio, watchPixelRatio } from '../design/runtime';
|
|
12
15
|
|
|
13
16
|
export interface KonvaStageProps {
|
|
14
17
|
children: React.ReactNode;
|
|
@@ -44,9 +47,21 @@ const KonvaStage = forwardRef<Konva.Stage, KonvaStageProps>(function KonvaStage(
|
|
|
44
47
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
45
48
|
|
|
46
49
|
useEffect(() => {
|
|
47
|
-
|
|
50
|
+
const stage = internalRef.current; // the stage this mount created
|
|
51
|
+
return () => { stage?.destroy(); };
|
|
48
52
|
}, []);
|
|
49
53
|
|
|
54
|
+
// Konva reads the global pixelRatio when a layer creates its canvases, so set it
|
|
55
|
+
// before the first draw and re-size every canvas when the ratio changes later.
|
|
56
|
+
Konva.pixelRatio = currentPixelRatio();
|
|
57
|
+
useEffect(() => watchPixelRatio((ratio) => {
|
|
58
|
+
Konva.pixelRatio = ratio;
|
|
59
|
+
const stage = internalRef.current;
|
|
60
|
+
if (!stage) return;
|
|
61
|
+
stage.getLayers().forEach((l) => { l.getCanvas().setPixelRatio(ratio); l.getHitCanvas().setPixelRatio(ratio); });
|
|
62
|
+
stage.batchDraw();
|
|
63
|
+
}), []);
|
|
64
|
+
|
|
50
65
|
const w = canvasWidth || 800;
|
|
51
66
|
const h = canvasHeight || 600;
|
|
52
67
|
const stageX = w / 2 - centerX * zoomLevel;
|
|
@@ -70,7 +85,6 @@ const KonvaStage = forwardRef<Konva.Stage, KonvaStageProps>(function KonvaStage(
|
|
|
70
85
|
onTouchMove={onTouchMove}
|
|
71
86
|
onTouchEnd={onTouchEnd}
|
|
72
87
|
draggable={false}
|
|
73
|
-
pixelRatio={1}
|
|
74
88
|
>
|
|
75
89
|
{children}
|
|
76
90
|
</Stage>
|
|
@@ -1,182 +1,39 @@
|
|
|
1
|
-
// ─── NavigationStack
|
|
1
|
+
// ─── NavigationStack ──────────────────────────────────────────────────────────
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// • Organic hand-drawn circle SVGs per level
|
|
6
|
-
// • Vertical connector lines between levels
|
|
7
|
-
// • Active level highlighted, previous levels clickable to navigate back
|
|
8
|
-
// • Font: Nunito
|
|
3
|
+
// The depth trail. Kept as a named component for existing callers; it renders
|
|
4
|
+
// the design system's trail (DepthTrail) from layer entries.
|
|
9
5
|
|
|
10
|
-
import React, { memo,
|
|
6
|
+
import React, { memo, useMemo } from 'react';
|
|
11
7
|
import type { VisualliLayer } from '@visualli/core';
|
|
8
|
+
import { DepthTrail, type IdeaPaint } from './Overlays';
|
|
12
9
|
|
|
13
10
|
export interface NavStackEntry {
|
|
14
11
|
layerId: string;
|
|
15
12
|
layer: VisualliLayer;
|
|
16
13
|
label: string;
|
|
14
|
+
/** Colour of the idea this layer opens from (the design system's TRAIL: the clicked idea's own colour); the first
|
|
15
|
+
* entry, the map's title, is TRAIL.rootTopic. */
|
|
16
|
+
paint?: IdeaPaint;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export interface NavigationStackProps {
|
|
20
20
|
stack: NavStackEntry[];
|
|
21
21
|
onNavigateBack: (index: number) => void;
|
|
22
|
+
/** @deprecated theme comes from the surrounding data-theme. */
|
|
22
23
|
isDark?: boolean;
|
|
24
|
+
/** Override the trail's default offset from the top-left corner. */
|
|
23
25
|
top?: string;
|
|
24
26
|
left?: string;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
];
|
|
32
|
-
|
|
33
|
-
// Organic circle SVG paths (small / active variants matching visualli.ai)
|
|
34
|
-
const ORGANIC_PATHS_SMALL = [
|
|
35
|
-
'M13.72,6.31 C15.12,6.29 17.96,8.57 17.99,10.60 C18.07,14.62 16.72,17.90 11.12,18.00 C7.26,18.07 4.07,14.86 4.00,10.85 C3.93,6.83 9.44,2.02 13.69,4.85',
|
|
36
|
-
'M13.88,6.41 C15.28,6.43 18.04,8.82 18.00,10.85 C17.93,14.86 16.48,18.10 10.88,18.00 C7.02,17.93 3.94,14.62 4.01,10.60 C4.08,6.59 9.76,1.97 13.91,4.95',
|
|
37
|
-
];
|
|
38
|
-
const ORGANIC_PATH_ACTIVE =
|
|
39
|
-
'M16.55,9.22 C17.95,9.15 20.87,11.33 20.97,13.36 C21.19,17.37 19.96,20.70 14.37,20.99 C10.51,21.19 7.20,18.10 7.00,14.09 C6.78,10.08 12.13,5.08 16.48,7.77';
|
|
40
|
-
|
|
41
|
-
const CIRCLE_SIZE = 14;
|
|
42
|
-
|
|
43
|
-
function hexToRgba(hex: string, opacity: number): string {
|
|
44
|
-
const h = hex.replace('#', '');
|
|
45
|
-
const r = parseInt(h.substring(0, 2), 16);
|
|
46
|
-
const g = parseInt(h.substring(2, 4), 16);
|
|
47
|
-
const b = parseInt(h.substring(4, 6), 16);
|
|
48
|
-
return `rgba(${r},${g},${b},${opacity})`;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const OrganicCircle = memo(function OrganicCircle({
|
|
52
|
-
size, fill, stroke, strokeWidth, variant, isActive,
|
|
53
|
-
}: {
|
|
54
|
-
size: number; fill: string; stroke: string; strokeWidth: number; variant: number; isActive?: boolean;
|
|
55
|
-
}) {
|
|
56
|
-
const path = isActive ? ORGANIC_PATH_ACTIVE : ORGANIC_PATHS_SMALL[variant % ORGANIC_PATHS_SMALL.length];
|
|
57
|
-
const vb = isActive ? '4 3 20 20' : '2 1 18 18';
|
|
58
|
-
const vbSz = isActive ? 20 : 18;
|
|
59
|
-
const sw = strokeWidth * vbSz / size;
|
|
60
|
-
return (
|
|
61
|
-
<svg width={size} height={size} viewBox={vb} fill="none" style={{ display: 'block' }}>
|
|
62
|
-
<path d={path} fill={fill} fillOpacity={1} />
|
|
63
|
-
<path d={path} fill="none" stroke={stroke} strokeWidth={sw} strokeLinecap="round" />
|
|
64
|
-
</svg>
|
|
29
|
+
const NavigationStack = memo(function NavigationStack({ stack, onNavigateBack, top, left }: NavigationStackProps) {
|
|
30
|
+
const entries = useMemo(
|
|
31
|
+
() => stack.map((e) => ({ layerId: e.layerId, label: e.label, level: e.layer.level, paint: e.paint ?? {} })),
|
|
32
|
+
[stack],
|
|
65
33
|
);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
const NavigationStack = memo(function NavigationStack({
|
|
69
|
-
stack,
|
|
70
|
-
onNavigateBack,
|
|
71
|
-
isDark = false,
|
|
72
|
-
top = '16px',
|
|
73
|
-
left = '16px',
|
|
74
|
-
}: NavigationStackProps) {
|
|
75
|
-
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
|
76
|
-
|
|
77
|
-
// Always render at least the root entry so the user knows where they are.
|
|
78
|
-
if (stack.length === 0) return null;
|
|
79
|
-
|
|
80
|
-
const visitedStroke = isDark ? '#908e8c' : '#747270';
|
|
81
|
-
const visitedText = isDark ? '#908e8c' : '#747270';
|
|
82
|
-
const activeStroke = isDark ? '#e8e6e4' : '#1a1a18';
|
|
83
|
-
const activeText = isDark ? '#e8e6e4' : '#1a1a18';
|
|
84
|
-
const connectorColor = isDark ? '#908e8c' : '#747270';
|
|
85
|
-
const connectorMarginLeft = 10.5; // padding(4) + circleHalf(7) - connectorHalf(0.5)
|
|
86
|
-
const currentIndex = stack.length - 1;
|
|
87
|
-
|
|
88
34
|
return (
|
|
89
|
-
<div
|
|
90
|
-
|
|
91
|
-
style={{
|
|
92
|
-
position: 'absolute',
|
|
93
|
-
top,
|
|
94
|
-
left,
|
|
95
|
-
zIndex: 30,
|
|
96
|
-
pointerEvents: 'auto',
|
|
97
|
-
}}
|
|
98
|
-
>
|
|
99
|
-
<div style={{ display: 'flex', flexDirection: 'column', padding: '8px' }}>
|
|
100
|
-
{stack.flatMap((entry, index) => {
|
|
101
|
-
const isCurrent = index === currentIndex;
|
|
102
|
-
const isClickable = index < currentIndex;
|
|
103
|
-
const isHovered = hoveredIndex === index;
|
|
104
|
-
const pastelFill = PASTEL_FILLS[index % PASTEL_FILLS.length];
|
|
105
|
-
const rowBg = isCurrent
|
|
106
|
-
? hexToRgba(pastelFill, 0.6)
|
|
107
|
-
: isHovered && isClickable
|
|
108
|
-
? hexToRgba(pastelFill, 0.3)
|
|
109
|
-
: 'transparent';
|
|
110
|
-
|
|
111
|
-
const items: React.ReactNode[] = [];
|
|
112
|
-
|
|
113
|
-
if (index > 0) {
|
|
114
|
-
items.push(
|
|
115
|
-
<div
|
|
116
|
-
key={`conn-${index}`}
|
|
117
|
-
style={{
|
|
118
|
-
width: '1px',
|
|
119
|
-
height: '20px',
|
|
120
|
-
marginLeft: `${connectorMarginLeft}px`,
|
|
121
|
-
marginTop: '-6px',
|
|
122
|
-
marginBottom: '-6px',
|
|
123
|
-
background: connectorColor,
|
|
124
|
-
alignSelf: 'flex-start',
|
|
125
|
-
flexShrink: 0,
|
|
126
|
-
position: 'relative',
|
|
127
|
-
zIndex: 1,
|
|
128
|
-
}}
|
|
129
|
-
/>,
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
items.push(
|
|
134
|
-
<div
|
|
135
|
-
key={`${entry.layerId}-${index}`}
|
|
136
|
-
style={{
|
|
137
|
-
position: 'relative',
|
|
138
|
-
zIndex: 2,
|
|
139
|
-
alignSelf: 'flex-start',
|
|
140
|
-
display: 'flex',
|
|
141
|
-
alignItems: 'center',
|
|
142
|
-
gap: '4px',
|
|
143
|
-
height: '22px',
|
|
144
|
-
padding: '4px',
|
|
145
|
-
borderRadius: '8px',
|
|
146
|
-
cursor: isClickable ? 'pointer' : 'default',
|
|
147
|
-
background: rowBg,
|
|
148
|
-
transition: 'background 0.15s',
|
|
149
|
-
}}
|
|
150
|
-
onMouseEnter={() => setHoveredIndex(index)}
|
|
151
|
-
onMouseLeave={() => setHoveredIndex(null)}
|
|
152
|
-
onClick={e => { e.stopPropagation(); if (isClickable) onNavigateBack(index); }}
|
|
153
|
-
>
|
|
154
|
-
<div style={{ width: CIRCLE_SIZE, height: CIRCLE_SIZE, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, zIndex: 4 }}>
|
|
155
|
-
{isCurrent ? (
|
|
156
|
-
<OrganicCircle size={CIRCLE_SIZE} fill={pastelFill} stroke={activeStroke} strokeWidth={1.5} variant={index} isActive />
|
|
157
|
-
) : (
|
|
158
|
-
<OrganicCircle size={CIRCLE_SIZE} fill={pastelFill} stroke={visitedStroke} strokeWidth={1} variant={index} />
|
|
159
|
-
)}
|
|
160
|
-
</div>
|
|
161
|
-
<span
|
|
162
|
-
style={{
|
|
163
|
-
fontFamily: "'Nunito', sans-serif",
|
|
164
|
-
fontSize: isCurrent || (isHovered && isClickable) ? '11px' : '9px',
|
|
165
|
-
fontWeight: isCurrent ? 600 : 400,
|
|
166
|
-
color: isCurrent ? activeText : visitedText,
|
|
167
|
-
lineHeight: 1,
|
|
168
|
-
whiteSpace: 'nowrap',
|
|
169
|
-
transition: 'color 0.15s, font-size 0.15s',
|
|
170
|
-
}}
|
|
171
|
-
>
|
|
172
|
-
{entry.label}
|
|
173
|
-
</span>
|
|
174
|
-
</div>,
|
|
175
|
-
);
|
|
176
|
-
|
|
177
|
-
return items;
|
|
178
|
-
})}
|
|
179
|
-
</div>
|
|
35
|
+
<div className="vi-map__trail" style={top || left ? { top, left } : undefined}>
|
|
36
|
+
<DepthTrail stack={entries} onNavigateBack={onNavigateBack} />
|
|
180
37
|
</div>
|
|
181
38
|
);
|
|
182
39
|
});
|