@zoneflow/renderer-dom 0.0.11 → 0.0.13

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.
@@ -1,2 +1,26 @@
1
- import type { ComponentLayoutEngine } from "../types";
1
+ import type { Zone } from "@zoneflow/core";
2
+ import type { BuiltInZoneSlotName, ComponentLayoutEngine, DensityLevel } from "../types";
2
3
  export declare const defaultComponentLayoutEngine: ComponentLayoutEngine;
4
+ export type ZoneSlotPlacement = {
5
+ kind: "top";
6
+ height: number;
7
+ widthCap?: number;
8
+ } | {
9
+ kind: "bottom";
10
+ height: number;
11
+ };
12
+ export type ZoneSlotDensityRule = (params: {
13
+ density: DensityLevel;
14
+ zone: Zone;
15
+ }) => boolean;
16
+ export type ExtensibleZoneSlot = {
17
+ name: string;
18
+ placement: ZoneSlotPlacement;
19
+ shouldRender?: ZoneSlotDensityRule;
20
+ };
21
+ export type ExtensibleComponentLayoutConfig = {
22
+ extraSlots?: ExtensibleZoneSlot[];
23
+ disabledBuiltIns?: BuiltInZoneSlotName[];
24
+ builtInDensityOverride?: Partial<Record<BuiltInZoneSlotName, ZoneSlotDensityRule>>;
25
+ };
26
+ export declare function createExtensibleComponentLayoutEngine(config?: ExtensibleComponentLayoutConfig): ComponentLayoutEngine;
@@ -198,3 +198,147 @@ export const defaultComponentLayoutEngine = {
198
198
  };
199
199
  },
200
200
  };
201
+ const DEFAULT_BUILT_IN_DENSITY = {
202
+ title: ({ density }) => density === "mid" || density === "near" || density === "detail",
203
+ type: ({ density }) => density === "near" || density === "detail",
204
+ badge: ({ density }) => density === "near" || density === "detail",
205
+ body: ({ density }) => density === "detail",
206
+ footer: ({ density }) => density === "detail",
207
+ };
208
+ const DEFAULT_EXTRA_DENSITY = ({ density }) => density === "near" || density === "detail";
209
+ function buildBuiltInDescriptor(name, base, disabled, overrides) {
210
+ if (disabled.has(name))
211
+ return null;
212
+ return {
213
+ name,
214
+ height: base.height,
215
+ widthCap: base.widthCap,
216
+ shouldRender: overrides[name] ?? DEFAULT_BUILT_IN_DENSITY[name],
217
+ };
218
+ }
219
+ function computeExtensibleZoneSlots(params) {
220
+ const { rect, density, zone, config } = params;
221
+ const slots = {};
222
+ const disabled = new Set(config.disabledBuiltIns ?? []);
223
+ const overrides = config.builtInDensityOverride ?? {};
224
+ const titleDesc = buildBuiltInDescriptor("title", { height: ZONE_TITLE_HEIGHT }, disabled, overrides);
225
+ const typeDesc = buildBuiltInDescriptor("type", { height: ZONE_TYPE_HEIGHT }, disabled, overrides);
226
+ const badgeDesc = buildBuiltInDescriptor("badge", { height: ZONE_BADGE_HEIGHT, widthCap: 96 }, disabled, overrides);
227
+ const footerDesc = buildBuiltInDescriptor("footer", { height: ZONE_FOOTER_HEIGHT }, disabled, overrides);
228
+ const bodyDesc = buildBuiltInDescriptor("body", { height: ZONE_BODY_MIN_HEIGHT }, disabled, overrides);
229
+ const extraTops = [];
230
+ const extraBottoms = [];
231
+ for (const slot of config.extraSlots ?? []) {
232
+ const desc = {
233
+ name: slot.name,
234
+ height: slot.placement.height,
235
+ widthCap: slot.placement.kind === "top" ? slot.placement.widthCap : undefined,
236
+ shouldRender: slot.shouldRender ?? DEFAULT_EXTRA_DENSITY,
237
+ };
238
+ if (slot.placement.kind === "top")
239
+ extraTops.push(desc);
240
+ else
241
+ extraBottoms.push(desc);
242
+ }
243
+ let content = insetRect(rect, ZONE_PADDING_X, ZONE_PADDING_Y);
244
+ const topQueue = [
245
+ ...(titleDesc ? [titleDesc] : []),
246
+ ...(typeDesc ? [typeDesc] : []),
247
+ ...(badgeDesc ? [badgeDesc] : []),
248
+ ...extraTops,
249
+ ];
250
+ for (const desc of topQueue) {
251
+ if (content.height <= 0)
252
+ break;
253
+ if (!desc.shouldRender({ density, zone }))
254
+ continue;
255
+ if (desc.widthCap !== undefined) {
256
+ const slotWidth = Math.min(desc.widthCap, content.width);
257
+ const slotHeight = Math.min(desc.height, content.height);
258
+ slots[desc.name] = {
259
+ x: content.x,
260
+ y: content.y,
261
+ width: slotWidth,
262
+ height: slotHeight,
263
+ };
264
+ content = addTopGap({
265
+ x: content.x,
266
+ y: content.y + slotHeight,
267
+ width: content.width,
268
+ height: Math.max(0, content.height - desc.height),
269
+ }, ZONE_GAP_Y);
270
+ }
271
+ else {
272
+ const { slot, rest } = takeTop(content, desc.height);
273
+ slots[desc.name] = slot;
274
+ content = addTopGap(rest, ZONE_GAP_Y);
275
+ }
276
+ }
277
+ const bottomQueue = [
278
+ ...(footerDesc ? [footerDesc] : []),
279
+ ...extraBottoms,
280
+ ];
281
+ for (const desc of bottomQueue) {
282
+ if (content.height <= 0)
283
+ break;
284
+ if (!desc.shouldRender({ density, zone }))
285
+ continue;
286
+ const { slot, rest } = takeBottom(content, desc.height);
287
+ slots[desc.name] = slot;
288
+ content = rest;
289
+ }
290
+ if (bodyDesc &&
291
+ bodyDesc.shouldRender({ density, zone }) &&
292
+ content.width > 0 &&
293
+ content.height >= bodyDesc.height) {
294
+ slots[bodyDesc.name] = content;
295
+ }
296
+ return slots;
297
+ }
298
+ export function createExtensibleComponentLayoutEngine(config = {}) {
299
+ return {
300
+ compute(input) {
301
+ const { graphLayout, density, visibility } = input;
302
+ const zonesById = Object.fromEntries(Object.values(graphLayout.zonesById).map((zoneVisual) => {
303
+ const zoneVisibility = visibility.zoneVisibilityById[zoneVisual.zoneId];
304
+ const zoneDensity = density.zoneDensityById[zoneVisual.zoneId];
305
+ const slots = zoneVisibility?.shouldRenderBody !== false
306
+ ? computeExtensibleZoneSlots({
307
+ rect: zoneVisual.rect,
308
+ density: zoneDensity,
309
+ zone: zoneVisual.zone,
310
+ config,
311
+ })
312
+ : {};
313
+ return [
314
+ zoneVisual.zoneId,
315
+ {
316
+ zoneId: zoneVisual.zoneId,
317
+ slots,
318
+ },
319
+ ];
320
+ }));
321
+ const pathsById = Object.fromEntries(Object.values(graphLayout.pathsById).map((path) => {
322
+ const pathVisibility = visibility.pathVisibilityById[path.pathId];
323
+ const pathDensity = density.pathDensityById[path.pathId];
324
+ const slots = path.rect && pathVisibility?.shouldRenderNode
325
+ ? computePathSlots({
326
+ rect: path.rect,
327
+ density: pathDensity,
328
+ })
329
+ : {};
330
+ return [
331
+ path.pathId,
332
+ {
333
+ pathId: path.pathId,
334
+ slots,
335
+ },
336
+ ];
337
+ }));
338
+ return {
339
+ zonesById,
340
+ pathsById,
341
+ };
342
+ },
343
+ };
344
+ }
@@ -3,6 +3,7 @@ import { getZoneDepth, isZoneInputEnabled, isZoneOutputEnabled, } from "@zoneflo
3
3
  import { appendEdgeFlowStyle, resolveCollapsedEdgeStroke, resolveDrawableEdgeSegments, resolveEdgeFlowMotion, } from "./edgeFlow";
4
4
  const SCENE_PADDING = 64;
5
5
  const RENDER_Z_INDEX = {
6
+ backgroundLayer: 0,
6
7
  zoneBase: 1,
7
8
  pathNode: 1,
8
9
  pathStatusBadge: 2,
@@ -21,11 +22,56 @@ function createEmptyMountRegistry() {
21
22
  return {
22
23
  zones: [],
23
24
  paths: [],
25
+ background: null,
24
26
  };
25
27
  }
26
28
  function clearHost(host) {
27
29
  host.innerHTML = "";
28
30
  }
31
+ function positiveModulo(value, divisor) {
32
+ return ((value % divisor) + divisor) % divisor;
33
+ }
34
+ function createGridLayer(params) {
35
+ const { options, camera, theme } = params;
36
+ const worldSize = Math.max(options.size ?? 16, 2);
37
+ const majorEvery = Math.max(options.majorEvery ?? 4, 2);
38
+ const minorSize = worldSize * camera.zoom;
39
+ const majorSize = minorSize * majorEvery;
40
+ if (minorSize < 2)
41
+ return null;
42
+ const minorOffsetX = positiveModulo(camera.x, minorSize);
43
+ const minorOffsetY = positiveModulo(camera.y, minorSize);
44
+ const majorOffsetX = positiveModulo(camera.x, majorSize);
45
+ const majorOffsetY = positiveModulo(camera.y, majorSize);
46
+ const minorColor = options.color ?? "rgba(148, 163, 184, 0.10)";
47
+ const majorColor = options.majorColor ?? "rgba(148, 163, 184, 0.18)";
48
+ const grid = document.createElement("div");
49
+ applyStyles(grid, {
50
+ position: "absolute",
51
+ inset: "0",
52
+ pointerEvents: "none",
53
+ backgroundColor: options.backgroundColor ?? "transparent",
54
+ backgroundImage: [
55
+ `linear-gradient(to right, ${minorColor} 1px, transparent 1px)`,
56
+ `linear-gradient(to bottom, ${minorColor} 1px, transparent 1px)`,
57
+ `linear-gradient(to right, ${majorColor} 1px, transparent 1px)`,
58
+ `linear-gradient(to bottom, ${majorColor} 1px, transparent 1px)`,
59
+ ].join(", "),
60
+ backgroundSize: [
61
+ `${minorSize}px ${minorSize}px`,
62
+ `${minorSize}px ${minorSize}px`,
63
+ `${majorSize}px ${majorSize}px`,
64
+ `${majorSize}px ${majorSize}px`,
65
+ ].join(", "),
66
+ backgroundPosition: [
67
+ `${minorOffsetX}px ${minorOffsetY}px`,
68
+ `${minorOffsetX}px ${minorOffsetY}px`,
69
+ `${majorOffsetX}px ${majorOffsetY}px`,
70
+ `${majorOffsetX}px ${majorOffsetY}px`,
71
+ ].join(", "),
72
+ });
73
+ return grid;
74
+ }
29
75
  function createIdSet(ids) {
30
76
  return new Set(ids ?? []);
31
77
  }
@@ -623,7 +669,9 @@ export const domDrawEngine = {
623
669
  });
624
670
  const sceneBounds = computeSceneBounds(input);
625
671
  const viewportRoot = document.createElement("div");
672
+ const worldBgRoot = document.createElement("div");
626
673
  const worldRoot = document.createElement("div");
674
+ const backgroundLayer = document.createElement("div");
627
675
  const edgeSvg = createSvgElement("svg");
628
676
  const zoneLayer = document.createElement("div");
629
677
  const pathLayer = document.createElement("div");
@@ -641,13 +689,25 @@ export const domDrawEngine = {
641
689
  interactionHandlers?.onBackgroundClick?.();
642
690
  }
643
691
  });
692
+ const worldTransform = `translate(${camera.x - viewportInfo.effective.x}px, ${camera.y - viewportInfo.effective.y}px) scale(${camera.zoom})`;
693
+ applyStyles(worldBgRoot, {
694
+ position: "absolute",
695
+ left: "0",
696
+ top: "0",
697
+ width: `${sceneBounds.width}px`,
698
+ height: `${sceneBounds.height}px`,
699
+ transform: worldTransform,
700
+ transformOrigin: "0 0",
701
+ willChange: "transform",
702
+ pointerEvents: "none",
703
+ });
644
704
  applyStyles(worldRoot, {
645
705
  position: "absolute",
646
706
  left: "0",
647
707
  top: "0",
648
708
  width: `${sceneBounds.width}px`,
649
709
  height: `${sceneBounds.height}px`,
650
- transform: `translate(${camera.x - viewportInfo.effective.x}px, ${camera.y - viewportInfo.effective.y}px) scale(${camera.zoom})`,
710
+ transform: worldTransform,
651
711
  transformOrigin: "0 0",
652
712
  willChange: "transform",
653
713
  });
@@ -664,6 +724,15 @@ export const domDrawEngine = {
664
724
  pointerEvents: "none",
665
725
  zIndex: RENDER_Z_INDEX.edgeLayer,
666
726
  });
727
+ applyStyles(backgroundLayer, {
728
+ position: "absolute",
729
+ left: "0",
730
+ top: "0",
731
+ width: `${sceneBounds.width}px`,
732
+ height: `${sceneBounds.height}px`,
733
+ zIndex: RENDER_Z_INDEX.backgroundLayer,
734
+ pointerEvents: "none",
735
+ });
667
736
  applyStyles(zoneLayer, {
668
737
  position: "absolute",
669
738
  left: "0",
@@ -671,6 +740,7 @@ export const domDrawEngine = {
671
740
  width: `${sceneBounds.width}px`,
672
741
  height: `${sceneBounds.height}px`,
673
742
  zIndex: RENDER_Z_INDEX.zoneLayer,
743
+ pointerEvents: "none",
674
744
  });
675
745
  applyStyles(pathLayer, {
676
746
  position: "absolute",
@@ -679,10 +749,35 @@ export const domDrawEngine = {
679
749
  width: `${sceneBounds.width}px`,
680
750
  height: `${sceneBounds.height}px`,
681
751
  zIndex: RENDER_Z_INDEX.pathLayer,
752
+ pointerEvents: "none",
682
753
  });
754
+ const backgroundContext = {
755
+ sceneBounds,
756
+ camera,
757
+ viewportInfo,
758
+ theme,
759
+ };
760
+ if (input.backgroundRenderer) {
761
+ input.backgroundRenderer(backgroundLayer, backgroundContext);
762
+ }
763
+ mounts.background = {
764
+ host: backgroundLayer,
765
+ context: backgroundContext,
766
+ };
767
+ worldBgRoot.appendChild(backgroundLayer);
683
768
  worldRoot.appendChild(edgeSvg);
684
769
  worldRoot.appendChild(zoneLayer);
685
770
  worldRoot.appendChild(pathLayer);
771
+ viewportRoot.appendChild(worldBgRoot);
772
+ if (input.gridOptions?.enabled) {
773
+ const gridLayer = createGridLayer({
774
+ options: input.gridOptions,
775
+ camera,
776
+ theme,
777
+ });
778
+ if (gridLayer)
779
+ viewportRoot.appendChild(gridLayer);
780
+ }
686
781
  viewportRoot.appendChild(worldRoot);
687
782
  host.appendChild(viewportRoot);
688
783
  drawEdges({
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, 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, 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({
@@ -104,6 +104,7 @@ export function createRenderer() {
104
104
  mounts: {
105
105
  zones: [],
106
106
  paths: [],
107
+ background: null,
107
108
  },
108
109
  };
109
110
  }
@@ -118,6 +119,8 @@ export function createRenderer() {
118
119
  pipeline,
119
120
  zoneComponentRenderers,
120
121
  pathComponentRenderers,
122
+ backgroundRenderer,
123
+ gridOptions,
121
124
  interactionHandlers,
122
125
  exclusionState,
123
126
  });
package/dist/types.d.ts CHANGED
@@ -84,8 +84,10 @@ export type VisibilityResult = {
84
84
  zoneVisibilityById: Record<ZoneId, ZoneVisibility>;
85
85
  pathVisibilityById: Record<PathId, PathVisibility>;
86
86
  };
87
- export type ZoneComponentSlotName = "title" | "type" | "badge" | "body" | "footer";
88
- export type PathComponentSlotName = "label" | "rule" | "target" | "body";
87
+ export type BuiltInZoneSlotName = "title" | "type" | "badge" | "body" | "footer";
88
+ export type ZoneComponentSlotName = BuiltInZoneSlotName | (string & {});
89
+ export type BuiltInPathSlotName = "label" | "rule" | "target" | "body";
90
+ export type PathComponentSlotName = BuiltInPathSlotName | (string & {});
89
91
  export type ZoneComponentLayout = {
90
92
  zoneId: ZoneId;
91
93
  slots: Partial<Record<ZoneComponentSlotName, Rect>>;
@@ -142,9 +144,29 @@ export type PathComponentMount = {
142
144
  rect: Rect;
143
145
  context: PathComponentRendererContext;
144
146
  };
147
+ export type BackgroundRendererContext = {
148
+ sceneBounds: Rect;
149
+ camera: CameraState;
150
+ viewportInfo: RenderViewportInfo;
151
+ theme: ZoneflowTheme;
152
+ };
153
+ export type GridOptions = {
154
+ enabled?: boolean;
155
+ size?: number;
156
+ color?: string;
157
+ majorEvery?: number;
158
+ majorColor?: string;
159
+ backgroundColor?: string;
160
+ };
161
+ export type BackgroundRenderer = (host: HTMLElement, context: BackgroundRendererContext) => void;
162
+ export type BackgroundMount = {
163
+ host: HTMLElement;
164
+ context: BackgroundRendererContext;
165
+ };
145
166
  export type RenderMountRegistry = {
146
167
  zones: ZoneComponentMount[];
147
168
  paths: PathComponentMount[];
169
+ background: BackgroundMount | null;
148
170
  };
149
171
  export type RendererInteractionHandlers = {
150
172
  onZoneClick?: (zoneId: ZoneId) => void;
@@ -181,6 +203,8 @@ export type RendererDrawInput = {
181
203
  pipeline: RenderPipelineResult;
182
204
  zoneComponentRenderers?: ZoneComponentRendererMap;
183
205
  pathComponentRenderers?: PathComponentRendererMap;
206
+ backgroundRenderer?: BackgroundRenderer;
207
+ gridOptions?: GridOptions;
184
208
  interactionHandlers?: RendererInteractionHandlers;
185
209
  exclusionState?: RendererExclusionState;
186
210
  };
@@ -242,6 +266,8 @@ export type RendererInput = {
242
266
  drawEngine?: DrawEngine;
243
267
  zoneComponentRenderers?: ZoneComponentRendererMap;
244
268
  pathComponentRenderers?: PathComponentRendererMap;
269
+ backgroundRenderer?: BackgroundRenderer;
270
+ gridOptions?: GridOptions;
245
271
  interactionHandlers?: RendererInteractionHandlers;
246
272
  exclusionState?: RendererExclusionState;
247
273
  debug?: RendererDebugOptions;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zoneflow/renderer-dom",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "license": "MIT",
5
5
  "description": "Low-level DOM renderer engines for Zoneflow.",
6
6
  "type": "module",
@@ -19,7 +19,7 @@
19
19
  "dist"
20
20
  ],
21
21
  "dependencies": {
22
- "@zoneflow/core": "0.0.11"
22
+ "@zoneflow/core": "0.0.13"
23
23
  },
24
24
  "scripts": {
25
25
  "build": "tsc -p tsconfig.json",