@zoneflow/renderer-dom 0.0.21 → 0.0.22
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/dist/diffDecorations.d.ts +67 -0
- package/dist/diffDecorations.js +99 -0
- package/dist/engines/drawEngine.js +86 -13
- package/dist/engines/edgeFlow.d.ts +12 -0
- package/dist/engines/edgeFlow.js +25 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/renderer.js +4 -1
- package/dist/types.d.ts +47 -1
- package/dist/zoneShape.d.ts +32 -0
- package/package.json +7 -3
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { UniverseModelDiff } from "@zoneflow/core";
|
|
2
|
+
import type { ResolvePathColor, ResolvePathLineColor, ResolvePathStyle } from "./types";
|
|
3
|
+
import type { ResolveZoneColor, ResolveZoneStyle, ZoneStyleOverride } from "./zoneShape";
|
|
4
|
+
export type DiffDecorationStatus = "removed" | "added" | "changed";
|
|
5
|
+
/** Default status colors — red / green / amber. */
|
|
6
|
+
export declare const DIFF_DECORATION_COLORS: Record<DiffDecorationStatus, string>;
|
|
7
|
+
export type DiffDecorationOptions = {
|
|
8
|
+
/** Per-status color overrides. Defaults to {@link DIFF_DECORATION_COLORS}. */
|
|
9
|
+
colors?: Partial<Record<DiffDecorationStatus, string>>;
|
|
10
|
+
/**
|
|
11
|
+
* Style applied to zones marked removed (the "ghost"). Defaults to a dashed
|
|
12
|
+
* outline at 45% opacity. Pass `null` to disable the ghost and keep only
|
|
13
|
+
* the accent color.
|
|
14
|
+
*/
|
|
15
|
+
removedZoneStyle?: ZoneStyleOverride | null;
|
|
16
|
+
/**
|
|
17
|
+
* Blink elements marked removed (zones, path nodes/labels, connector
|
|
18
|
+
* lines). On by default — colors alone are ambiguous when the app already
|
|
19
|
+
* colors zones/paths for its own meaning. Set `false` for static colors
|
|
20
|
+
* only. (Pulse is suppressed by the renderer under
|
|
21
|
+
* `prefers-reduced-motion` regardless.)
|
|
22
|
+
*/
|
|
23
|
+
pulseRemoved?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Resolvers to fall back to for elements the diff does not mark — so the
|
|
26
|
+
* decorations can wrap an app's existing presentation (e.g. `meta.color`
|
|
27
|
+
* based resolvers) instead of replacing it.
|
|
28
|
+
*/
|
|
29
|
+
base?: {
|
|
30
|
+
resolveZoneColor?: ResolveZoneColor;
|
|
31
|
+
resolveZoneStyle?: ResolveZoneStyle;
|
|
32
|
+
resolvePathColor?: ResolvePathColor;
|
|
33
|
+
resolvePathLineColor?: ResolvePathLineColor;
|
|
34
|
+
resolvePathStyle?: ResolvePathStyle;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
export type DiffDecorations = {
|
|
38
|
+
resolveZoneColor: ResolveZoneColor;
|
|
39
|
+
resolveZoneStyle: ResolveZoneStyle;
|
|
40
|
+
resolvePathColor: ResolvePathColor;
|
|
41
|
+
resolvePathLineColor: ResolvePathLineColor;
|
|
42
|
+
resolvePathStyle: ResolvePathStyle;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Turn a {@link UniverseModelDiff} into the presentation resolvers that
|
|
46
|
+
* paint diff status onto the canvas:
|
|
47
|
+
*
|
|
48
|
+
* - removed → red accent, red connector lines, dashed ghost zones, and a
|
|
49
|
+
* blink (pulse) so removal stands out even in apps that already use color
|
|
50
|
+
* for their own meaning
|
|
51
|
+
* - added → green accent / lines
|
|
52
|
+
* - changed → amber accent / lines
|
|
53
|
+
*
|
|
54
|
+
* The resolvers match purely by id, so they work with whichever side of the
|
|
55
|
+
* diff you render: draw the `before` model to preview removals and changes
|
|
56
|
+
* ("what will happen"), or the `after` model to review additions and changes
|
|
57
|
+
* ("what just happened"). Ids absent from the rendered model simply never
|
|
58
|
+
* come up.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* const diff = diffUniverseModels(model, cleaned);
|
|
62
|
+
* const deco = createDiffDecorations(diff, {
|
|
63
|
+
* base: { resolvePathColor: myMetaColorResolver },
|
|
64
|
+
* });
|
|
65
|
+
* <UniverseCanvas model={model} {...deco} />
|
|
66
|
+
*/
|
|
67
|
+
export declare function createDiffDecorations(diff: UniverseModelDiff, options?: DiffDecorationOptions): DiffDecorations;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/** Default status colors — red / green / amber. */
|
|
2
|
+
export const DIFF_DECORATION_COLORS = {
|
|
3
|
+
removed: "#dc2626",
|
|
4
|
+
added: "#16a34a",
|
|
5
|
+
changed: "#d97706",
|
|
6
|
+
};
|
|
7
|
+
const DEFAULT_REMOVED_ZONE_STYLE = {
|
|
8
|
+
borderStyle: "dashed",
|
|
9
|
+
opacity: 0.45,
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Turn a {@link UniverseModelDiff} into the presentation resolvers that
|
|
13
|
+
* paint diff status onto the canvas:
|
|
14
|
+
*
|
|
15
|
+
* - removed → red accent, red connector lines, dashed ghost zones, and a
|
|
16
|
+
* blink (pulse) so removal stands out even in apps that already use color
|
|
17
|
+
* for their own meaning
|
|
18
|
+
* - added → green accent / lines
|
|
19
|
+
* - changed → amber accent / lines
|
|
20
|
+
*
|
|
21
|
+
* The resolvers match purely by id, so they work with whichever side of the
|
|
22
|
+
* diff you render: draw the `before` model to preview removals and changes
|
|
23
|
+
* ("what will happen"), or the `after` model to review additions and changes
|
|
24
|
+
* ("what just happened"). Ids absent from the rendered model simply never
|
|
25
|
+
* come up.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* const diff = diffUniverseModels(model, cleaned);
|
|
29
|
+
* const deco = createDiffDecorations(diff, {
|
|
30
|
+
* base: { resolvePathColor: myMetaColorResolver },
|
|
31
|
+
* });
|
|
32
|
+
* <UniverseCanvas model={model} {...deco} />
|
|
33
|
+
*/
|
|
34
|
+
export function createDiffDecorations(diff, options = {}) {
|
|
35
|
+
const colors = { ...DIFF_DECORATION_COLORS, ...options.colors };
|
|
36
|
+
const pulseRemoved = options.pulseRemoved ?? true;
|
|
37
|
+
const baseRemovedZoneStyle = options.removedZoneStyle === undefined
|
|
38
|
+
? DEFAULT_REMOVED_ZONE_STYLE
|
|
39
|
+
: options.removedZoneStyle;
|
|
40
|
+
const removedZoneStyle = baseRemovedZoneStyle === null
|
|
41
|
+
? pulseRemoved
|
|
42
|
+
? { pulse: true }
|
|
43
|
+
: null
|
|
44
|
+
: pulseRemoved
|
|
45
|
+
? { ...baseRemovedZoneStyle, pulse: true }
|
|
46
|
+
: baseRemovedZoneStyle;
|
|
47
|
+
const removedPathStyle = pulseRemoved
|
|
48
|
+
? { pulse: true }
|
|
49
|
+
: null;
|
|
50
|
+
const base = options.base ?? {};
|
|
51
|
+
const zoneStatusById = new Map();
|
|
52
|
+
for (const zoneId of diff.zones.removed)
|
|
53
|
+
zoneStatusById.set(zoneId, "removed");
|
|
54
|
+
for (const zoneId of diff.zones.added)
|
|
55
|
+
zoneStatusById.set(zoneId, "added");
|
|
56
|
+
for (const zoneId of Object.keys(diff.zones.changed)) {
|
|
57
|
+
zoneStatusById.set(zoneId, "changed");
|
|
58
|
+
}
|
|
59
|
+
const pathStatusById = new Map();
|
|
60
|
+
for (const ref of diff.paths.removed)
|
|
61
|
+
pathStatusById.set(ref.pathId, "removed");
|
|
62
|
+
for (const ref of diff.paths.added)
|
|
63
|
+
pathStatusById.set(ref.pathId, "added");
|
|
64
|
+
for (const pathId of Object.keys(diff.paths.changed)) {
|
|
65
|
+
pathStatusById.set(pathId, "changed");
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
resolveZoneColor: (zone) => {
|
|
69
|
+
const status = zoneStatusById.get(zone.id);
|
|
70
|
+
return status ? colors[status] : base.resolveZoneColor?.(zone);
|
|
71
|
+
},
|
|
72
|
+
resolveZoneStyle: (zone) => {
|
|
73
|
+
const baseStyle = base.resolveZoneStyle?.(zone) ?? undefined;
|
|
74
|
+
if (zoneStatusById.get(zone.id) === "removed" && removedZoneStyle) {
|
|
75
|
+
// Merge so an app's own zone styling (e.g. a custom border) survives
|
|
76
|
+
// under the diff ghost/pulse instead of being dropped.
|
|
77
|
+
return { ...baseStyle, ...removedZoneStyle };
|
|
78
|
+
}
|
|
79
|
+
return baseStyle;
|
|
80
|
+
},
|
|
81
|
+
resolvePathColor: (path) => {
|
|
82
|
+
const status = pathStatusById.get(path.id);
|
|
83
|
+
return status ? colors[status] : base.resolvePathColor?.(path);
|
|
84
|
+
},
|
|
85
|
+
resolvePathLineColor: (path) => {
|
|
86
|
+
const status = pathStatusById.get(path.id);
|
|
87
|
+
return status ? colors[status] : base.resolvePathLineColor?.(path);
|
|
88
|
+
},
|
|
89
|
+
resolvePathStyle: (path) => {
|
|
90
|
+
const baseStyle = base.resolvePathStyle?.(path) ?? undefined;
|
|
91
|
+
if (pathStatusById.get(path.id) === "removed" && removedPathStyle) {
|
|
92
|
+
// A removed path that the app also marks (e.g. dashed "unconfigured")
|
|
93
|
+
// stays dashed AND gains the removal pulse.
|
|
94
|
+
return { ...baseStyle, ...removedPathStyle };
|
|
95
|
+
}
|
|
96
|
+
return baseStyle;
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { resolveZoneAnchorRect } from "../anchors";
|
|
2
2
|
import { normalizeZoneShape } from "../zoneShape";
|
|
3
3
|
import { getZoneDepth, isZoneInputEnabled, isZoneOutputEnabled, } from "@zoneflow/core";
|
|
4
|
-
import { appendEdgeFlowStyle, resolveCollapsedEdgeStroke, resolveDrawableEdgeSegments, resolveEdgeFlowMotion, } from "./edgeFlow";
|
|
4
|
+
import { appendEdgeFlowStyle, appendPulseStyle, resolveCollapsedEdgeStroke, resolveDrawableEdgeSegments, resolveEdgeFlowMotion, } from "./edgeFlow";
|
|
5
5
|
const SCENE_PADDING = 64;
|
|
6
6
|
const RENDER_Z_INDEX = {
|
|
7
7
|
backgroundLayer: 0,
|
|
@@ -13,6 +13,8 @@ const RENDER_Z_INDEX = {
|
|
|
13
13
|
pathLayer: 30,
|
|
14
14
|
};
|
|
15
15
|
const EDGE_FLOW_CLASS = "zoneflow-edge-flow";
|
|
16
|
+
const PULSE_CLASS = "zoneflow-pulse";
|
|
17
|
+
const PULSE_ANIMATION_NAME = "zoneflow-pulse";
|
|
16
18
|
function applyStyles(el, styles) {
|
|
17
19
|
for (const [key, value] of Object.entries(styles)) {
|
|
18
20
|
// @ts-expect-error CSSStyleDeclaration index access
|
|
@@ -175,6 +177,19 @@ function getEdgeColor(params) {
|
|
|
175
177
|
? params.theme.pathEdge
|
|
176
178
|
: params.theme.pathInboundEdge;
|
|
177
179
|
}
|
|
180
|
+
// SVG stroke-dasharray for a consumer-chosen line style. Dotted relies on the
|
|
181
|
+
// round linecap (already set on every edge stroke) to render the ~0-length
|
|
182
|
+
// dashes as dots. Returns null for solid/undefined (no dash pattern).
|
|
183
|
+
function getEdgeDashPattern(lineStyle) {
|
|
184
|
+
switch (lineStyle) {
|
|
185
|
+
case "dashed":
|
|
186
|
+
return "7 6";
|
|
187
|
+
case "dotted":
|
|
188
|
+
return "0.1 6";
|
|
189
|
+
default:
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
178
193
|
function getBezierCurvePathD(params) {
|
|
179
194
|
const { source, target } = params;
|
|
180
195
|
const distanceX = Math.abs(target.x - source.x);
|
|
@@ -500,6 +515,13 @@ function drawEdges(params) {
|
|
|
500
515
|
className: EDGE_FLOW_CLASS,
|
|
501
516
|
motion: edgeFlowMotion,
|
|
502
517
|
});
|
|
518
|
+
// CSS rules are document-global even from an SVG <style>, so this also
|
|
519
|
+
// powers the pulse class on the DOM zone/path layers.
|
|
520
|
+
appendPulseStyle({
|
|
521
|
+
svg,
|
|
522
|
+
animationName: PULSE_ANIMATION_NAME,
|
|
523
|
+
className: PULSE_CLASS,
|
|
524
|
+
});
|
|
503
525
|
for (const [pathId, edges] of Object.entries(input.pipeline.graphLayout.edgesByPathId)) {
|
|
504
526
|
const visibility = input.pipeline.visibility.pathVisibilityById[pathId];
|
|
505
527
|
if (!visibility?.shouldRenderEdge)
|
|
@@ -509,27 +531,66 @@ function drawEdges(params) {
|
|
|
509
531
|
edges,
|
|
510
532
|
visibility,
|
|
511
533
|
});
|
|
534
|
+
// Consumer-resolved per-path line color overrides the theme's edge colors
|
|
535
|
+
// for every segment of this path, collapsed ones included.
|
|
536
|
+
const pathVisual = input.pipeline.graphLayout.pathsById[pathId];
|
|
537
|
+
const lineColor = pathVisual
|
|
538
|
+
? input.resolvePathLineColor?.(pathVisual.path) ?? undefined
|
|
539
|
+
: undefined;
|
|
540
|
+
const pathStyle = pathVisual
|
|
541
|
+
? input.resolvePathStyle?.(pathVisual.path) ?? undefined
|
|
542
|
+
: undefined;
|
|
543
|
+
// Pulsing segments are wrapped in a group so the blink animates the
|
|
544
|
+
// group's opacity without fighting the per-stroke flow animation class.
|
|
545
|
+
let edgeOwner = svg;
|
|
546
|
+
if (pathStyle?.pulse) {
|
|
547
|
+
const pulseGroup = createSvgElement("g");
|
|
548
|
+
pulseGroup.setAttribute("class", PULSE_CLASS);
|
|
549
|
+
svg.appendChild(pulseGroup);
|
|
550
|
+
edgeOwner = pulseGroup;
|
|
551
|
+
}
|
|
552
|
+
// dashed/dotted draws a single static patterned stroke and suppresses the
|
|
553
|
+
// moving flow layers — a flowing dash on top of a static dash reads as
|
|
554
|
+
// noise, and an inert path is exactly what "not wired up yet" should look
|
|
555
|
+
// like. Solid keeps the normal base + animated flow stack.
|
|
556
|
+
const patterned = getEdgeDashPattern(pathStyle?.lineStyle);
|
|
512
557
|
for (const { edge, collapsed } of drawableEdges) {
|
|
513
|
-
const stroke =
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
558
|
+
const stroke = lineColor ??
|
|
559
|
+
(collapsed
|
|
560
|
+
? resolveCollapsedEdgeStroke(input.theme)
|
|
561
|
+
: getEdgeColor({
|
|
562
|
+
kind: edge.kind,
|
|
563
|
+
theme: input.theme,
|
|
564
|
+
}));
|
|
519
565
|
const pathD = getBezierCurvePathD({
|
|
520
566
|
source: edge.source,
|
|
521
567
|
target: edge.target,
|
|
522
568
|
});
|
|
523
569
|
const opacity = getOpacity(visibility.emphasis);
|
|
570
|
+
const baseWidth = edge.kind === "path-to-zone" ? 2.25 : 1.85;
|
|
571
|
+
if (patterned) {
|
|
572
|
+
const dashed = createSvgElement("path");
|
|
573
|
+
dashed.setAttribute("d", pathD);
|
|
574
|
+
dashed.setAttribute("fill", "none");
|
|
575
|
+
dashed.setAttribute("stroke", stroke);
|
|
576
|
+
dashed.setAttribute("stroke-width", String(baseWidth));
|
|
577
|
+
dashed.setAttribute("stroke-linecap", "round");
|
|
578
|
+
dashed.setAttribute("stroke-linejoin", "round");
|
|
579
|
+
dashed.setAttribute("stroke-dasharray", patterned);
|
|
580
|
+
// Sole layer, so render it at full emphasis (no faint base stack).
|
|
581
|
+
dashed.setAttribute("opacity", String(opacity * 0.9));
|
|
582
|
+
edgeOwner.appendChild(dashed);
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
524
585
|
const path = createSvgElement("path");
|
|
525
586
|
path.setAttribute("d", pathD);
|
|
526
587
|
path.setAttribute("fill", "none");
|
|
527
588
|
path.setAttribute("stroke", stroke);
|
|
528
|
-
path.setAttribute("stroke-width",
|
|
589
|
+
path.setAttribute("stroke-width", String(baseWidth));
|
|
529
590
|
path.setAttribute("stroke-linecap", "round");
|
|
530
591
|
path.setAttribute("stroke-linejoin", "round");
|
|
531
592
|
path.setAttribute("opacity", String(opacity * 0.42));
|
|
532
|
-
|
|
593
|
+
edgeOwner.appendChild(path);
|
|
533
594
|
const flowGlow = createSvgElement("path");
|
|
534
595
|
flowGlow.setAttribute("d", pathD);
|
|
535
596
|
flowGlow.setAttribute("fill", "none");
|
|
@@ -541,7 +602,7 @@ function drawEdges(params) {
|
|
|
541
602
|
flowGlow.setAttribute("stroke-dashoffset", edgeFlowMotion.dashOffset);
|
|
542
603
|
flowGlow.setAttribute("opacity", String(opacity * 0.18));
|
|
543
604
|
flowGlow.setAttribute("class", EDGE_FLOW_CLASS);
|
|
544
|
-
|
|
605
|
+
edgeOwner.appendChild(flowGlow);
|
|
545
606
|
const flow = createSvgElement("path");
|
|
546
607
|
flow.setAttribute("d", pathD);
|
|
547
608
|
flow.setAttribute("fill", "none");
|
|
@@ -553,7 +614,7 @@ function drawEdges(params) {
|
|
|
553
614
|
flow.setAttribute("stroke-dashoffset", edgeFlowMotion.dashOffset);
|
|
554
615
|
flow.setAttribute("opacity", String(opacity * 0.94));
|
|
555
616
|
flow.setAttribute("class", EDGE_FLOW_CLASS);
|
|
556
|
-
|
|
617
|
+
edgeOwner.appendChild(flow);
|
|
557
618
|
}
|
|
558
619
|
}
|
|
559
620
|
}
|
|
@@ -869,19 +930,28 @@ export const domDrawEngine = {
|
|
|
869
930
|
const zoneBodyEl = document.createElement("div");
|
|
870
931
|
zoneEl.dataset.zoneflowZoneId = zoneVisual.zoneId;
|
|
871
932
|
zoneBodyEl.dataset.zoneflowZoneBody = zoneVisual.zoneId;
|
|
933
|
+
// Consumer-resolved style overrides (diff-preview ghosts etc.). Opacity
|
|
934
|
+
// composes onto the visibility-driven value and dims the whole subtree —
|
|
935
|
+
// body, slots, and anchors alike.
|
|
936
|
+
const zoneStyle = input.resolveZoneStyle?.(zoneVisual.zone) ?? undefined;
|
|
937
|
+
const zoneStyleOpacity = Math.min(Math.max(zoneStyle?.opacity ?? 1, 0), 1);
|
|
872
938
|
applyStyles(zoneEl, {
|
|
873
939
|
position: "absolute",
|
|
874
940
|
left: `${zoneVisual.rect.x}px`,
|
|
875
941
|
top: `${zoneVisual.rect.y}px`,
|
|
876
942
|
width: `${zoneVisual.rect.width}px`,
|
|
877
943
|
height: `${zoneVisual.rect.height}px`,
|
|
878
|
-
opacity: getOpacity(visibility.emphasis),
|
|
944
|
+
opacity: getOpacity(visibility.emphasis) * zoneStyleOpacity,
|
|
879
945
|
overflow: "visible",
|
|
880
946
|
zIndex: zoneDepth + RENDER_Z_INDEX.zoneBase,
|
|
881
947
|
// zoneLayer 가 pointer-events: none 이고 이 속성은 상속되므로,
|
|
882
948
|
// 명시하지 않으면 카드에서 슬롯 사각형 밖 영역의 클릭이 배경으로 빠진다.
|
|
883
949
|
pointerEvents: "auto",
|
|
884
950
|
});
|
|
951
|
+
if (zoneStyle?.pulse) {
|
|
952
|
+
// Pulses from the inline opacity above, so it composes with ghosting.
|
|
953
|
+
zoneEl.classList.add(PULSE_CLASS);
|
|
954
|
+
}
|
|
885
955
|
const shape = normalizeZoneShape(input.resolveZoneShape?.(zoneVisual.zone));
|
|
886
956
|
// Consumer-resolved per-zone color overrides the theme's border + accent
|
|
887
957
|
// (body background and text stay theme-driven to preserve contrast).
|
|
@@ -938,7 +1008,7 @@ export const domDrawEngine = {
|
|
|
938
1008
|
width: "100%",
|
|
939
1009
|
height: "100%",
|
|
940
1010
|
borderRadius: shape.borderRadius,
|
|
941
|
-
border: `1px solid ${zoneBorderColor}`,
|
|
1011
|
+
border: `1px ${zoneStyle?.borderStyle ?? "solid"} ${zoneBorderColor}`,
|
|
942
1012
|
background: theme.surface.zone.background,
|
|
943
1013
|
boxSizing: "border-box",
|
|
944
1014
|
boxShadow: theme.surface.zone.shadow,
|
|
@@ -1008,6 +1078,9 @@ export const domDrawEngine = {
|
|
|
1008
1078
|
// pathLayer 의 pointer-events: none 상속 차단 — zoneEl 과 동일한 이유.
|
|
1009
1079
|
pointerEvents: "auto",
|
|
1010
1080
|
});
|
|
1081
|
+
if (input.resolvePathStyle?.(pathVisual.path)?.pulse) {
|
|
1082
|
+
pathEl.classList.add(PULSE_CLASS);
|
|
1083
|
+
}
|
|
1011
1084
|
pathEl.addEventListener("click", (event) => {
|
|
1012
1085
|
event.stopPropagation();
|
|
1013
1086
|
interactionHandlers?.onPathClick?.(pathVisual.pathId);
|
|
@@ -8,6 +8,18 @@ export type EdgeFlowMotion = {
|
|
|
8
8
|
dashOffset: string;
|
|
9
9
|
};
|
|
10
10
|
export declare function resolveEdgeFlowMotion(theme: ZoneflowTheme): EdgeFlowMotion;
|
|
11
|
+
/**
|
|
12
|
+
* Inject the shared blink animation used by pulse-decorated elements (diff
|
|
13
|
+
* previews etc.). The keyframes only define the 50% stop, so each element
|
|
14
|
+
* pulses from its own current opacity — ghosted zones dip from 0.45, full
|
|
15
|
+
* elements from 1. The <style> lives inside the edge SVG but CSS rules are
|
|
16
|
+
* document-global, so the same class works on the DOM zone/path layers too.
|
|
17
|
+
*/
|
|
18
|
+
export declare function appendPulseStyle(params: {
|
|
19
|
+
svg: SVGSVGElement;
|
|
20
|
+
animationName: string;
|
|
21
|
+
className: string;
|
|
22
|
+
}): void;
|
|
11
23
|
export declare function appendEdgeFlowStyle(params: {
|
|
12
24
|
svg: SVGSVGElement;
|
|
13
25
|
animationName: string;
|
package/dist/engines/edgeFlow.js
CHANGED
|
@@ -16,6 +16,31 @@ export function resolveEdgeFlowMotion(theme) {
|
|
|
16
16
|
dashOffset: String(cycleLength),
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Inject the shared blink animation used by pulse-decorated elements (diff
|
|
21
|
+
* previews etc.). The keyframes only define the 50% stop, so each element
|
|
22
|
+
* pulses from its own current opacity — ghosted zones dip from 0.45, full
|
|
23
|
+
* elements from 1. The <style> lives inside the edge SVG but CSS rules are
|
|
24
|
+
* document-global, so the same class works on the DOM zone/path layers too.
|
|
25
|
+
*/
|
|
26
|
+
export function appendPulseStyle(params) {
|
|
27
|
+
const { svg, animationName, className } = params;
|
|
28
|
+
const style = document.createElementNS("http://www.w3.org/2000/svg", "style");
|
|
29
|
+
style.textContent = `
|
|
30
|
+
@keyframes ${animationName} {
|
|
31
|
+
50% { opacity: 0.12; }
|
|
32
|
+
}
|
|
33
|
+
.${className} {
|
|
34
|
+
animation: ${animationName} 1100ms ease-in-out infinite;
|
|
35
|
+
}
|
|
36
|
+
@media (prefers-reduced-motion: reduce) {
|
|
37
|
+
.${className} {
|
|
38
|
+
animation: none;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
`;
|
|
42
|
+
svg.appendChild(style);
|
|
43
|
+
}
|
|
19
44
|
export function appendEdgeFlowStyle(params) {
|
|
20
45
|
const { svg, animationName, className, motion } = params;
|
|
21
46
|
const style = document.createElementNS("http://www.w3.org/2000/svg", "style");
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/renderer.js
CHANGED
|
@@ -69,7 +69,7 @@ export function createRenderer() {
|
|
|
69
69
|
update(input) {
|
|
70
70
|
if (!host)
|
|
71
71
|
return;
|
|
72
|
-
const { model, layoutModel, theme, textScale = "md", camera = DEFAULT_CAMERA, graphLayoutEngine = defaultGraphLayoutEngine, densityEngine = defaultDensityEngine, visibilityEngine = defaultVisibilityEngine, componentLayoutEngine = defaultComponentLayoutEngine, drawEngine = domDrawEngine, zoneComponentRenderers, pathComponentRenderers, resolveZoneShape, resolveZoneColor, resolvePathColor, backgroundRenderer, gridOptions, interactionHandlers, exclusionState, debug, } = input;
|
|
72
|
+
const { model, layoutModel, theme, textScale = "md", camera = DEFAULT_CAMERA, graphLayoutEngine = defaultGraphLayoutEngine, densityEngine = defaultDensityEngine, visibilityEngine = defaultVisibilityEngine, componentLayoutEngine = defaultComponentLayoutEngine, drawEngine = domDrawEngine, zoneComponentRenderers, pathComponentRenderers, resolveZoneShape, resolveZoneColor, resolveZoneStyle, resolvePathColor, resolvePathLineColor, resolvePathStyle, backgroundRenderer, gridOptions, interactionHandlers, exclusionState, debug, } = input;
|
|
73
73
|
const mergedTheme = resolveTheme(theme);
|
|
74
74
|
const viewportInfo = resolveViewportInfo(host, camera, input);
|
|
75
75
|
const pipeline = runRenderPipeline({
|
|
@@ -121,7 +121,10 @@ export function createRenderer() {
|
|
|
121
121
|
pathComponentRenderers,
|
|
122
122
|
resolveZoneShape,
|
|
123
123
|
resolveZoneColor,
|
|
124
|
+
resolveZoneStyle,
|
|
124
125
|
resolvePathColor,
|
|
126
|
+
resolvePathLineColor,
|
|
127
|
+
resolvePathStyle,
|
|
125
128
|
backgroundRenderer,
|
|
126
129
|
gridOptions,
|
|
127
130
|
interactionHandlers,
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AnchorRect, Path, PathId, Point, UniverseId, UniverseLayoutModel, UniverseModel, Zone, ZoneId } from "@zoneflow/core";
|
|
2
2
|
import type { TextScaleLevel, ZoneflowTheme } from "./theme";
|
|
3
|
-
import type { ResolveZoneColor, ResolveZoneShape } from "./zoneShape";
|
|
3
|
+
import type { ResolveZoneColor, ResolveZoneShape, ResolveZoneStyle } from "./zoneShape";
|
|
4
4
|
export type CameraState = {
|
|
5
5
|
x: number;
|
|
6
6
|
y: number;
|
|
@@ -155,6 +155,46 @@ export type PathComponentRendererMap = Partial<Record<PathComponentSlotName, Pat
|
|
|
155
155
|
* is affected — the rule/target/body slots stay theme-driven for contrast.
|
|
156
156
|
*/
|
|
157
157
|
export type ResolvePathColor = (path: Path) => string | null | undefined;
|
|
158
|
+
/**
|
|
159
|
+
* Resolver invoked once per path to decide the color of its connector lines
|
|
160
|
+
* (the zone→path and path→zone edge strokes, including the flow animation).
|
|
161
|
+
* Return `undefined`/`null` to fall back to the theme's edge colors. The
|
|
162
|
+
* override also applies to the path's collapsed edge segments so a decorated
|
|
163
|
+
* path stays recognizable while a subtree is folded. Pair with
|
|
164
|
+
* {@link ResolvePathColor} when the label should match the line — built for
|
|
165
|
+
* states like a diff preview ("this connection will be removed/retargeted").
|
|
166
|
+
*/
|
|
167
|
+
export type ResolvePathLineColor = (path: Path) => string | null | undefined;
|
|
168
|
+
/**
|
|
169
|
+
* Per-path presentation overrides beyond color — the path-side counterpart
|
|
170
|
+
* of {@link ZoneStyleOverride}.
|
|
171
|
+
*/
|
|
172
|
+
export type PathLineStyle = "solid" | "dashed" | "dotted";
|
|
173
|
+
export type PathStyleOverride = {
|
|
174
|
+
/**
|
|
175
|
+
* Blink the path's node (label slots included) and its connector lines.
|
|
176
|
+
* Built for diff previews where color alone is ambiguous because apps may
|
|
177
|
+
* color paths for their own reasons. Disabled automatically under
|
|
178
|
+
* `prefers-reduced-motion`.
|
|
179
|
+
*/
|
|
180
|
+
pulse?: boolean;
|
|
181
|
+
/**
|
|
182
|
+
* Dash pattern for the path's connector lines. `"dashed"`/`"dotted"` draw a
|
|
183
|
+
* single static stroke (the moving flow animation is suppressed so the
|
|
184
|
+
* pattern stays legible), which reads as "inert / not yet wired up" — e.g.
|
|
185
|
+
* an unconfigured path with no rule or target. Defaults to `"solid"` (the
|
|
186
|
+
* normal animated flow). Pair with {@link ResolvePathLineColor} to also tint
|
|
187
|
+
* it, and with the node `border-style` is left untouched — only the
|
|
188
|
+
* connector line is affected.
|
|
189
|
+
*/
|
|
190
|
+
lineStyle?: PathLineStyle;
|
|
191
|
+
};
|
|
192
|
+
/**
|
|
193
|
+
* Resolver invoked once per path to decide its style overrides. Return
|
|
194
|
+
* `undefined`/`null` for the default presentation. Purely presentational —
|
|
195
|
+
* geometry and hit-testing are unaffected.
|
|
196
|
+
*/
|
|
197
|
+
export type ResolvePathStyle = (path: Path) => PathStyleOverride | null | undefined;
|
|
158
198
|
export type ZoneComponentMount = {
|
|
159
199
|
key: string;
|
|
160
200
|
zoneId: ZoneId;
|
|
@@ -232,7 +272,10 @@ export type RendererDrawInput = {
|
|
|
232
272
|
pathComponentRenderers?: PathComponentRendererMap;
|
|
233
273
|
resolveZoneShape?: ResolveZoneShape;
|
|
234
274
|
resolveZoneColor?: ResolveZoneColor;
|
|
275
|
+
resolveZoneStyle?: ResolveZoneStyle;
|
|
235
276
|
resolvePathColor?: ResolvePathColor;
|
|
277
|
+
resolvePathLineColor?: ResolvePathLineColor;
|
|
278
|
+
resolvePathStyle?: ResolvePathStyle;
|
|
236
279
|
backgroundRenderer?: BackgroundRenderer;
|
|
237
280
|
gridOptions?: GridOptions;
|
|
238
281
|
interactionHandlers?: RendererInteractionHandlers;
|
|
@@ -298,7 +341,10 @@ export type RendererInput = {
|
|
|
298
341
|
pathComponentRenderers?: PathComponentRendererMap;
|
|
299
342
|
resolveZoneShape?: ResolveZoneShape;
|
|
300
343
|
resolveZoneColor?: ResolveZoneColor;
|
|
344
|
+
resolveZoneStyle?: ResolveZoneStyle;
|
|
301
345
|
resolvePathColor?: ResolvePathColor;
|
|
346
|
+
resolvePathLineColor?: ResolvePathLineColor;
|
|
347
|
+
resolvePathStyle?: ResolvePathStyle;
|
|
302
348
|
backgroundRenderer?: BackgroundRenderer;
|
|
303
349
|
gridOptions?: GridOptions;
|
|
304
350
|
interactionHandlers?: RendererInteractionHandlers;
|
package/dist/zoneShape.d.ts
CHANGED
|
@@ -52,6 +52,38 @@ export type ResolveZoneShape = (zone: Zone) => ZoneShape | null | undefined;
|
|
|
52
52
|
* background and text stay theme-driven so contrast is preserved.
|
|
53
53
|
*/
|
|
54
54
|
export type ResolveZoneColor = (zone: Zone) => string | null | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* Per-zone presentation overrides beyond color — built for transient states
|
|
57
|
+
* like a diff preview's "will be removed" ghost (dashed outline + dimmed).
|
|
58
|
+
*/
|
|
59
|
+
export type ZoneStyleOverride = {
|
|
60
|
+
/**
|
|
61
|
+
* CSS `border-style` for the zone outline. Applies to radius-based shapes;
|
|
62
|
+
* clipped shapes (diamond/hexagon/custom `clipPath`) synthesize their
|
|
63
|
+
* outline from a fill layer, so `borderStyle` is ignored there.
|
|
64
|
+
*/
|
|
65
|
+
borderStyle?: "solid" | "dashed" | "dotted";
|
|
66
|
+
/**
|
|
67
|
+
* 0..1 multiplier composed onto the zone's computed visibility opacity.
|
|
68
|
+
* Dims the whole zone including its slots and anchors.
|
|
69
|
+
*/
|
|
70
|
+
opacity?: number;
|
|
71
|
+
/**
|
|
72
|
+
* Blink the whole zone (slots and anchors included) — for states color
|
|
73
|
+
* alone can't carry, e.g. "will be removed" in a diff preview where apps
|
|
74
|
+
* may already use colors for their own meaning. Pulses from the zone's
|
|
75
|
+
* computed opacity, so it composes with `opacity`. Disabled automatically
|
|
76
|
+
* under `prefers-reduced-motion`.
|
|
77
|
+
*/
|
|
78
|
+
pulse?: boolean;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Resolver invoked once per zone to decide its style overrides. Return
|
|
82
|
+
* `undefined`/`null` for the default presentation. Like
|
|
83
|
+
* {@link ResolveZoneColor}, purely presentational — geometry, hit-testing,
|
|
84
|
+
* and anchors are unaffected.
|
|
85
|
+
*/
|
|
86
|
+
export type ResolveZoneStyle = (zone: Zone) => ZoneStyleOverride | null | undefined;
|
|
55
87
|
/** Normalized geometry consumed by the draw engine. */
|
|
56
88
|
export type ResolvedZoneShape = {
|
|
57
89
|
borderRadius: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zoneflow/renderer-dom",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.22",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Low-level DOM renderer engines for Zoneflow.",
|
|
6
6
|
"type": "module",
|
|
@@ -19,10 +19,14 @@
|
|
|
19
19
|
"dist"
|
|
20
20
|
],
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@zoneflow/core": "0.0.
|
|
22
|
+
"@zoneflow/core": "0.0.22"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"vitest": "^4.1.8"
|
|
23
26
|
},
|
|
24
27
|
"scripts": {
|
|
25
28
|
"build": "tsc -p tsconfig.json",
|
|
26
|
-
"type-check": "tsc -p tsconfig.json --noEmit"
|
|
29
|
+
"type-check": "tsc -p tsconfig.json --noEmit",
|
|
30
|
+
"test": "vitest run"
|
|
27
31
|
}
|
|
28
32
|
}
|