@statelyai/flow-dom 0.1.0 → 3.0.0

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,6 +1,91 @@
1
- import { FlowStore } from "@statelyai/flow";
1
+ import { FlowColorMode, FlowEngine, FlowInputEffects, FlowInputEvent, FlowStore, FlowStoreContext, Rect } from "@statelyai/flow";
2
2
 
3
+ //#region src/culling.d.ts
4
+
5
+ /**
6
+ * Node/edge ids worth mounting, or `null` meaning "render everything" — either
7
+ * because culling is disabled, the viewport hasn't been measured yet, or the
8
+ * whole graph already fits the viewport (so culling would mount everything
9
+ * anyway). Adapters mount exactly these ids; `null` means mount all.
10
+ */
11
+ type RenderableIds = {
12
+ nodeIds: Set<string>;
13
+ edgeIds: Set<string>;
14
+ } | null;
15
+ /**
16
+ * Compare two `computeRenderableIds` results by contents. Adapters use this as
17
+ * the equality function for their reactive subscription so a layer only
18
+ * re-renders when the visible set actually changes — not on every store
19
+ * transition (e.g. every drag frame).
20
+ */
21
+ declare function renderableIdsEqual(a: RenderableIds, b: RenderableIds): boolean;
22
+ /**
23
+ * Which entities should be mounted for the current viewport, or `null` to mount
24
+ * everything.
25
+ *
26
+ * Returns `null` when:
27
+ * - `enabled` is false (culling off),
28
+ * - the viewport is unmeasured (0×0 — culling against it would blank the first
29
+ * paint),
30
+ * - or the whole graph fits the viewport (+overscan). In that case culling
31
+ * would mount every entity anyway, so we skip the per-frame O(n+e) cull and
32
+ * the N-id Set build entirely — running it for zero benefit is what makes a
33
+ * zoomed-out drag laggy. The containment check is one O(n) `getGraphBounds`,
34
+ * far cheaper than the cull it avoids.
35
+ *
36
+ * Otherwise returns the visible node/edge id sets (viewport cull + overscan).
37
+ *
38
+ * `context` must be the engine's current snapshot (the engine reads its own
39
+ * `getGraphBounds`/`getRenderableEntities` from the live store) — which holds
40
+ * when called inside a store-subscription selector.
41
+ */
42
+ declare function computeRenderableIds(engine: FlowEngine, context: FlowStoreContext, enabled: boolean | undefined): RenderableIds;
43
+ //#endregion
44
+ //#region src/render-styles.d.ts
45
+ type NodePositionStyle = {
46
+ position: "absolute";
47
+ top: 0;
48
+ left: 0;
49
+ transform: string;
50
+ transformOrigin: "0 0";
51
+ willChange: "transform" | undefined;
52
+ width: number | "max-content";
53
+ height: number | undefined;
54
+ };
55
+ /**
56
+ * Position + size for a node's *wrapper* element. Always uses `transform`
57
+ * (compositor-friendly), never `left/top`, and GPU-promotes the wrapper while
58
+ * it is being dragged so moving it doesn't repaint its neighbours.
59
+ *
60
+ * `autoSize` nodes are sized by their content (the DOM owns the size and the
61
+ * measurement observer feeds it back); all others use the graph-owned size.
62
+ *
63
+ * This styles the wrapper box only — the node's content (including any custom
64
+ * component) renders inside and is untouched.
65
+ */
66
+ declare function nodePositionStyle(bounds: Rect, options?: {
67
+ autoSize?: boolean;
68
+ dragging?: boolean;
69
+ }): NodePositionStyle;
70
+ type ViewportStyle = {
71
+ position: "absolute";
72
+ inset: 0;
73
+ transformOrigin: "0 0";
74
+ pointerEvents: "none";
75
+ };
76
+ /**
77
+ * Style for the single transformed viewport element that wraps all canvas
78
+ * layers (nodes, edges, labels as static children). Its `transform` is set
79
+ * imperatively by `attachViewportTransform` and is deliberately NOT in this
80
+ * recipe — so a reactive framework binding can't re-render the node tree on
81
+ * every pan frame by owning the transform.
82
+ */
83
+ declare function viewportStyle(): ViewportStyle;
84
+ //#endregion
3
85
  //#region src/index.d.ts
86
+ type ResolvedFlowColorMode = "light" | "dark";
87
+ declare function resolveFlowColorMode(colorMode: FlowColorMode, targetWindow?: Pick<Window, "matchMedia"> | undefined): ResolvedFlowColorMode;
88
+ declare function applyFlowColorMode(element: HTMLElement, colorMode: FlowColorMode, targetWindow?: Pick<Window, "matchMedia"> | undefined): ResolvedFlowColorMode;
4
89
  type MeasurementTarget = {
5
90
  nodeId: string;
6
91
  } | {
@@ -11,10 +96,42 @@ type MeasurementTarget = {
11
96
  };
12
97
  type MeasurementObserver = {
13
98
  observe(element: Element, target: MeasurementTarget): void;
99
+ /** Re-measure now (position-only changes don't fire ResizeObserver). */
100
+ measure(element: Element): void;
14
101
  unobserve(element: Element): void;
15
102
  disconnect(): void;
16
103
  };
17
- declare function createMeasurementObserver(store: FlowStore): MeasurementObserver;
104
+ type MeasurementObserverOptions = {
105
+ container?: Element | (() => Element | null | undefined) | null;
106
+ };
107
+ declare function createMeasurementObserver(store: FlowStore, options?: MeasurementObserverOptions): MeasurementObserver;
108
+ /** The container element registered for this store via `attachContainer`. */
109
+ declare function getAttachedContainer(store: FlowStore): Element | undefined;
18
110
  declare function attachContainer(store: FlowStore, element: Element): () => void;
111
+ declare function attachViewportTransform(store: FlowStore, element: HTMLElement): () => void;
112
+ type AttachInputOptions = {
113
+ container: HTMLElement;
114
+ store: FlowStore;
115
+ runtime: {
116
+ send(event: FlowInputEvent): FlowInputEffects;
117
+ cancelSession(): void;
118
+ };
119
+ /**
120
+ * Auto-pan policy, evaluated once per animation frame while a gesture
121
+ * session holds the pointer near the container edge. Pass
122
+ * `getAutoPanDelta` from @statelyai/flow (injected as a function so this
123
+ * package keeps no runtime dependency on the optional peer). When it
124
+ * returns a delta, the binder pans the viewport and re-sends the
125
+ * stationary pointer so the dragged entity follows the canvas.
126
+ */
127
+ autoPan?: (context: ReturnType<FlowStore["getSnapshot"]>["context"], screenPoint: {
128
+ x: number;
129
+ y: number;
130
+ }) => {
131
+ x: number;
132
+ y: number;
133
+ } | null;
134
+ };
135
+ declare function attachInput(options: AttachInputOptions): () => void;
19
136
  //#endregion
20
- export { MeasurementObserver, MeasurementTarget, attachContainer, createMeasurementObserver };
137
+ export { AttachInputOptions, MeasurementObserver, MeasurementObserverOptions, MeasurementTarget, type NodePositionStyle, type RenderableIds, ResolvedFlowColorMode, type ViewportStyle, applyFlowColorMode, attachContainer, attachInput, attachViewportTransform, computeRenderableIds, createMeasurementObserver, getAttachedContainer, nodePositionStyle, renderableIdsEqual, resolveFlowColorMode, viewportStyle };
package/dist/esm/index.js CHANGED
@@ -1,54 +1,178 @@
1
+ //#region src/culling.ts
2
+ /** Overscan (canvas px) added around the viewport so entities just off-screen
3
+ * stay mounted and don't pop in/out at the edges. */
4
+ const CULL_OVERSCAN = 100;
5
+ function idSetsEqual(a, b) {
6
+ if (a.size !== b.size) return false;
7
+ for (const value of a) if (!b.has(value)) return false;
8
+ return true;
9
+ }
10
+ /**
11
+ * Compare two `computeRenderableIds` results by contents. Adapters use this as
12
+ * the equality function for their reactive subscription so a layer only
13
+ * re-renders when the visible set actually changes — not on every store
14
+ * transition (e.g. every drag frame).
15
+ */
16
+ function renderableIdsEqual(a, b) {
17
+ return a === b || a !== null && b !== null && idSetsEqual(a.nodeIds, b.nodeIds) && idSetsEqual(a.edgeIds, b.edgeIds);
18
+ }
19
+ /**
20
+ * Which entities should be mounted for the current viewport, or `null` to mount
21
+ * everything.
22
+ *
23
+ * Returns `null` when:
24
+ * - `enabled` is false (culling off),
25
+ * - the viewport is unmeasured (0×0 — culling against it would blank the first
26
+ * paint),
27
+ * - or the whole graph fits the viewport (+overscan). In that case culling
28
+ * would mount every entity anyway, so we skip the per-frame O(n+e) cull and
29
+ * the N-id Set build entirely — running it for zero benefit is what makes a
30
+ * zoomed-out drag laggy. The containment check is one O(n) `getGraphBounds`,
31
+ * far cheaper than the cull it avoids.
32
+ *
33
+ * Otherwise returns the visible node/edge id sets (viewport cull + overscan).
34
+ *
35
+ * `context` must be the engine's current snapshot (the engine reads its own
36
+ * `getGraphBounds`/`getRenderableEntities` from the live store) — which holds
37
+ * when called inside a store-subscription selector.
38
+ */
39
+ function computeRenderableIds(engine, context, enabled) {
40
+ if (!enabled) return null;
41
+ const viewportSize = context.viewportSize;
42
+ if (viewportSize.width === 0 || viewportSize.height === 0) return null;
43
+ const viewport = context.viewport;
44
+ const left = -viewport.x / viewport.zoom - CULL_OVERSCAN;
45
+ const top = -viewport.y / viewport.zoom - CULL_OVERSCAN;
46
+ const right = left + viewportSize.width / viewport.zoom + CULL_OVERSCAN * 2;
47
+ const bottom = top + viewportSize.height / viewport.zoom + CULL_OVERSCAN * 2;
48
+ const bounds = engine.getGraphBounds();
49
+ if (left <= bounds.x && top <= bounds.y && right >= bounds.x + bounds.width && bottom >= bounds.y + bounds.height) return null;
50
+ const renderable = engine.getRenderableEntities({
51
+ cull: "viewport",
52
+ overscan: CULL_OVERSCAN
53
+ });
54
+ return {
55
+ nodeIds: new Set(renderable.nodes.map((node) => node.id)),
56
+ edgeIds: new Set(renderable.edges.map((edge) => edge.id))
57
+ };
58
+ }
59
+ //#endregion
60
+ //#region src/render-styles.ts
61
+ /**
62
+ * Position + size for a node's *wrapper* element. Always uses `transform`
63
+ * (compositor-friendly), never `left/top`, and GPU-promotes the wrapper while
64
+ * it is being dragged so moving it doesn't repaint its neighbours.
65
+ *
66
+ * `autoSize` nodes are sized by their content (the DOM owns the size and the
67
+ * measurement observer feeds it back); all others use the graph-owned size.
68
+ *
69
+ * This styles the wrapper box only — the node's content (including any custom
70
+ * component) renders inside and is untouched.
71
+ */
72
+ function nodePositionStyle(bounds, options = {}) {
73
+ return {
74
+ position: "absolute",
75
+ top: 0,
76
+ left: 0,
77
+ transform: `translate(${bounds.x}px, ${bounds.y}px)`,
78
+ transformOrigin: "0 0",
79
+ willChange: options.dragging ? "transform" : void 0,
80
+ width: options.autoSize ? "max-content" : bounds.width,
81
+ height: options.autoSize ? void 0 : bounds.height
82
+ };
83
+ }
84
+ /**
85
+ * Style for the single transformed viewport element that wraps all canvas
86
+ * layers (nodes, edges, labels as static children). Its `transform` is set
87
+ * imperatively by `attachViewportTransform` and is deliberately NOT in this
88
+ * recipe — so a reactive framework binding can't re-render the node tree on
89
+ * every pan frame by owning the transform.
90
+ */
91
+ function viewportStyle() {
92
+ return {
93
+ position: "absolute",
94
+ inset: 0,
95
+ transformOrigin: "0 0",
96
+ pointerEvents: "none"
97
+ };
98
+ }
99
+ //#endregion
1
100
  //#region src/index.ts
2
- function createMeasurementObserver(store) {
101
+ function resolveFlowColorMode(colorMode, targetWindow = typeof window === "undefined" ? void 0 : window) {
102
+ if (colorMode !== "system") return colorMode;
103
+ return targetWindow?.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
104
+ }
105
+ function applyFlowColorMode(element, colorMode, targetWindow = element.ownerDocument.defaultView ?? void 0) {
106
+ const resolvedColorMode = resolveFlowColorMode(colorMode, targetWindow);
107
+ element.dataset.flowColorMode = resolvedColorMode;
108
+ element.dataset.flowColorModePreference = colorMode;
109
+ element.style.colorScheme = resolvedColorMode;
110
+ return resolvedColorMode;
111
+ }
112
+ const containers = /* @__PURE__ */ new WeakMap();
113
+ function createMeasurementObserver(store, options = {}) {
3
114
  const elementMap = /* @__PURE__ */ new Map();
4
- let pendingMeasurements = /* @__PURE__ */ new Map();
5
- let pendingPorts = [];
115
+ let pendingMeasurements = [];
6
116
  let rafId = null;
7
117
  function flush() {
8
118
  rafId = null;
9
- if (pendingMeasurements.size > 0) {
10
- store.trigger.reportMeasurements({ measurements: pendingMeasurements });
11
- pendingMeasurements = /* @__PURE__ */ new Map();
12
- }
13
- if (pendingPorts.length > 0) {
14
- store.trigger.reportPortMeasurements({ measurements: pendingPorts });
15
- pendingPorts = [];
119
+ if (pendingMeasurements.length > 0) {
120
+ store.trigger.updateMeasurements({ measurements: pendingMeasurements });
121
+ pendingMeasurements = [];
16
122
  }
17
123
  }
18
124
  function scheduleFlush() {
19
125
  if (rafId === null) rafId = requestAnimationFrame(flush);
20
126
  }
127
+ function pushMeasurement(element, target, layoutSize) {
128
+ const viewport = store.getSnapshot().context.viewport;
129
+ const zoom = viewport.zoom || 1;
130
+ const rect = element.getBoundingClientRect();
131
+ const containerRect = (getContainer(options.container) ?? containers.get(store))?.getBoundingClientRect();
132
+ const measurement = {
133
+ x: (rect.left - (containerRect?.left ?? 0) - viewport.x) / zoom,
134
+ y: (rect.top - (containerRect?.top ?? 0) - viewport.y) / zoom,
135
+ width: layoutSize ? layoutSize.width : rect.width / zoom,
136
+ height: layoutSize ? layoutSize.height : rect.height / zoom
137
+ };
138
+ const id = "portName" in target ? `${target.nodeId}:${target.portName}` : "nodeId" in target ? target.nodeId : target.edgeId;
139
+ const existing = store.getSnapshot().context.measurements.get(id);
140
+ if (existing && Math.abs(existing.x - measurement.x) < .5 && Math.abs(existing.y - measurement.y) < .5 && Math.abs(existing.width - measurement.width) < .5 && Math.abs(existing.height - measurement.height) < .5) return;
141
+ pendingMeasurements.push({
142
+ id,
143
+ rect: measurement
144
+ });
145
+ scheduleFlush();
146
+ }
21
147
  const observer = new ResizeObserver((entries) => {
22
148
  for (const entry of entries) {
23
149
  const target = elementMap.get(entry.target);
24
150
  if (!target) continue;
25
- const rect = entry.target.getBoundingClientRect();
26
- if ("portName" in target) {
27
- const nodeRect = entry.target.closest(`[data-entity-id="${target.nodeId}"]`)?.getBoundingClientRect();
28
- if (nodeRect) pendingPorts.push({
29
- nodeId: target.nodeId,
30
- portName: target.portName,
31
- x: rect.left - nodeRect.left,
32
- y: rect.top - nodeRect.top,
33
- width: rect.width,
34
- height: rect.height
35
- });
36
- } else if ("nodeId" in target) pendingMeasurements.set(target.nodeId, {
37
- width: rect.width,
38
- height: rect.height
39
- });
40
- else pendingMeasurements.set(target.edgeId, {
41
- width: rect.width,
42
- height: rect.height
43
- });
151
+ const box = entry.borderBoxSize?.[0];
152
+ const layoutSize = box ? {
153
+ width: box.inlineSize,
154
+ height: box.blockSize
155
+ } : entry.contentRect ? {
156
+ width: entry.contentRect.width,
157
+ height: entry.contentRect.height
158
+ } : void 0;
159
+ pushMeasurement(entry.target, target, layoutSize);
44
160
  }
45
- scheduleFlush();
46
161
  });
47
162
  return {
48
163
  observe(element, target) {
49
164
  elementMap.set(element, target);
50
165
  observer.observe(element);
51
166
  },
167
+ /**
168
+ * Re-measure an observed element immediately. ResizeObserver only fires
169
+ * on SIZE changes — position-only changes (layout passes, graph swaps,
170
+ * a port moving within its node) need an explicit re-measure.
171
+ */
172
+ measure(element) {
173
+ const target = elementMap.get(element);
174
+ if (target) pushMeasurement(element, target);
175
+ },
52
176
  unobserve(element) {
53
177
  elementMap.delete(element);
54
178
  observer.unobserve(element);
@@ -60,12 +184,19 @@ function createMeasurementObserver(store) {
60
184
  cancelAnimationFrame(rafId);
61
185
  rafId = null;
62
186
  }
63
- pendingMeasurements = /* @__PURE__ */ new Map();
64
- pendingPorts = [];
187
+ pendingMeasurements = [];
65
188
  }
66
189
  };
67
190
  }
191
+ function getContainer(container) {
192
+ return typeof container === "function" ? container() ?? null : container ?? null;
193
+ }
194
+ /** The container element registered for this store via `attachContainer`. */
195
+ function getAttachedContainer(store) {
196
+ return containers.get(store);
197
+ }
68
198
  function attachContainer(store, element) {
199
+ containers.set(store, element);
69
200
  const update = () => {
70
201
  const rect = element.getBoundingClientRect();
71
202
  if (rect.width > 0 && rect.height > 0) store.trigger.updateViewportSize({
@@ -76,7 +207,252 @@ function attachContainer(store, element) {
76
207
  update();
77
208
  const observer = new ResizeObserver(update);
78
209
  observer.observe(element);
79
- return () => observer.disconnect();
210
+ return () => {
211
+ if (containers.get(store) === element) containers.delete(store);
212
+ observer.disconnect();
213
+ };
214
+ }
215
+ /** Idle delay (ms) before dropping the GPU-layer hint after interaction stops. */
216
+ const VIEWPORT_PROMOTE_IDLE_MS = 200;
217
+ function attachViewportTransform(store, element) {
218
+ let prevViewport = store.getSnapshot().context.viewport;
219
+ let prevState = store.getSnapshot().context.canvasState;
220
+ let idle;
221
+ const writeTransform = (viewport) => {
222
+ element.style.transform = `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`;
223
+ };
224
+ const promote = () => {
225
+ element.style.willChange = "transform";
226
+ if (idle !== void 0) clearTimeout(idle);
227
+ };
228
+ const scheduleClear = () => {
229
+ if (idle !== void 0) clearTimeout(idle);
230
+ idle = setTimeout(() => {
231
+ element.style.willChange = "auto";
232
+ }, VIEWPORT_PROMOTE_IDLE_MS);
233
+ };
234
+ writeTransform(prevViewport);
235
+ if (prevState !== "idle") promote();
236
+ const subscription = store.subscribe((snapshot) => {
237
+ const context = snapshot.context;
238
+ if (context.viewport !== prevViewport) {
239
+ prevViewport = context.viewport;
240
+ writeTransform(context.viewport);
241
+ promote();
242
+ if (context.canvasState === "idle") scheduleClear();
243
+ }
244
+ if (context.canvasState !== prevState) {
245
+ prevState = context.canvasState;
246
+ if (prevState !== "idle") promote();
247
+ else scheduleClear();
248
+ }
249
+ });
250
+ return () => {
251
+ if (idle !== void 0) clearTimeout(idle);
252
+ subscription.unsubscribe();
253
+ };
254
+ }
255
+ function attachInput(options) {
256
+ const { container, store, runtime } = options;
257
+ const doc = container.ownerDocument;
258
+ const win = doc.defaultView;
259
+ let capturedPointerId = null;
260
+ let lastClient = null;
261
+ let autoPanFrame = null;
262
+ function stopAutoPan() {
263
+ if (autoPanFrame !== null) {
264
+ win?.cancelAnimationFrame(autoPanFrame);
265
+ autoPanFrame = null;
266
+ }
267
+ }
268
+ function autoPanTick() {
269
+ autoPanFrame = null;
270
+ if (!options.autoPan || capturedPointerId === null || !lastClient) return;
271
+ const rect = container.getBoundingClientRect();
272
+ const screenPoint = {
273
+ x: lastClient.x - rect.left,
274
+ y: lastClient.y - rect.top
275
+ };
276
+ const delta = options.autoPan(store.getSnapshot().context, screenPoint);
277
+ if (delta) {
278
+ store.trigger.panBy({
279
+ x: -delta.x,
280
+ y: -delta.y
281
+ });
282
+ sendPointer("pointer.move", lastClient.x, lastClient.y, {
283
+ pointerId: lastClient.pointerId,
284
+ button: -1,
285
+ modifiers: {
286
+ shift: false,
287
+ meta: false,
288
+ ctrl: false,
289
+ alt: false
290
+ },
291
+ target: null
292
+ });
293
+ }
294
+ scheduleAutoPan();
295
+ }
296
+ function scheduleAutoPan() {
297
+ if (options.autoPan && autoPanFrame === null && win) autoPanFrame = win.requestAnimationFrame(autoPanTick);
298
+ }
299
+ function modifiers(event) {
300
+ return {
301
+ shift: event.shiftKey,
302
+ meta: event.metaKey,
303
+ ctrl: event.ctrlKey,
304
+ alt: event.altKey
305
+ };
306
+ }
307
+ function toPoints(event) {
308
+ const rect = container.getBoundingClientRect();
309
+ const screenPoint = {
310
+ x: event.clientX - rect.left,
311
+ y: event.clientY - rect.top
312
+ };
313
+ const { viewport } = store.getSnapshot().context;
314
+ const zoom = viewport.zoom || 1;
315
+ return {
316
+ screenPoint,
317
+ point: {
318
+ x: (screenPoint.x - viewport.x) / zoom,
319
+ y: (screenPoint.y - viewport.y) / zoom
320
+ }
321
+ };
322
+ }
323
+ function resolveTarget(event) {
324
+ const element = capturedPointerId !== null ? doc.elementFromPoint(event.clientX, event.clientY) : event.target instanceof Element ? event.target : null;
325
+ if (!element) return { kind: "none" };
326
+ const portElement = element.closest("[data-flow-port-name]");
327
+ if (portElement?.dataset.flowPortName) return {
328
+ kind: "port",
329
+ nodeId: portElement.dataset.flowNodeId ?? "",
330
+ portName: portElement.dataset.flowPortName
331
+ };
332
+ if (element.closest("[data-flow-interactive]")) return { kind: "none" };
333
+ const entityElement = element.closest("[data-entity-id]");
334
+ if (entityElement?.dataset.entityId) {
335
+ const isLabel = element.closest("[data-flow-edge-label]") !== null;
336
+ const entityHasDragHandle = entityElement.querySelector("[data-flow-drag-handle]") !== null;
337
+ const pressedDragHandle = entityHasDragHandle && element.closest("[data-flow-drag-handle]") !== null;
338
+ return {
339
+ kind: "entity",
340
+ id: entityElement.dataset.entityId,
341
+ ...isLabel ? { part: "label" } : {},
342
+ ...entityHasDragHandle ? {
343
+ entityHasDragHandle,
344
+ pressedDragHandle
345
+ } : {}
346
+ };
347
+ }
348
+ if (element === container || element.closest("[data-flow-canvas-layer]") !== null) return { kind: "pane" };
349
+ return { kind: "none" };
350
+ }
351
+ function applyEffects(event, effects) {
352
+ if (effects.preventDefault) event.preventDefault();
353
+ if (effects.stopPropagation) event.stopPropagation();
354
+ }
355
+ function sendPointer(kind, clientX, clientY, info) {
356
+ const { screenPoint, point } = toPoints({
357
+ clientX,
358
+ clientY
359
+ });
360
+ const effects = runtime.send({
361
+ kind,
362
+ pointerId: info.pointerId,
363
+ button: info.button,
364
+ point,
365
+ screenPoint,
366
+ target: resolveTarget({
367
+ clientX,
368
+ clientY,
369
+ target: info.target
370
+ }),
371
+ modifiers: info.modifiers
372
+ });
373
+ if (effects.sessionStarted && kind === "pointer.down") {
374
+ try {
375
+ container.setPointerCapture(info.pointerId);
376
+ } catch {}
377
+ capturedPointerId = info.pointerId;
378
+ scheduleAutoPan();
379
+ }
380
+ if (effects.sessionEnded && capturedPointerId !== null) {
381
+ try {
382
+ if (container.hasPointerCapture(capturedPointerId)) container.releasePointerCapture(capturedPointerId);
383
+ } catch {}
384
+ capturedPointerId = null;
385
+ lastClient = null;
386
+ stopAutoPan();
387
+ }
388
+ return effects;
389
+ }
390
+ function handlePointer(kind) {
391
+ return (event) => {
392
+ if (kind === "pointer.down" || capturedPointerId !== null && event.pointerId === capturedPointerId) lastClient = {
393
+ x: event.clientX,
394
+ y: event.clientY,
395
+ pointerId: event.pointerId
396
+ };
397
+ applyEffects(event, sendPointer(kind, event.clientX, event.clientY, {
398
+ pointerId: event.pointerId,
399
+ button: event.button,
400
+ modifiers: modifiers(event),
401
+ target: event.target
402
+ }));
403
+ };
404
+ }
405
+ function handleWheel(event) {
406
+ const { screenPoint, point } = toPoints(event);
407
+ applyEffects(event, runtime.send({
408
+ kind: "wheel",
409
+ deltaX: event.deltaX,
410
+ deltaY: event.deltaY,
411
+ deltaMode: event.deltaMode,
412
+ point,
413
+ screenPoint,
414
+ target: resolveTarget(event),
415
+ modifiers: modifiers(event)
416
+ }));
417
+ }
418
+ function isEditableTarget(target) {
419
+ if (!(target instanceof HTMLElement)) return false;
420
+ return target.isContentEditable || target.matches("input, textarea, select, [contenteditable='true']");
421
+ }
422
+ function handleKey(kind) {
423
+ return (event) => {
424
+ applyEffects(event, runtime.send({
425
+ kind,
426
+ key: event.key,
427
+ editableTarget: isEditableTarget(event.target),
428
+ modifiers: modifiers(event)
429
+ }));
430
+ };
431
+ }
432
+ const onPointerDown = handlePointer("pointer.down");
433
+ const onPointerMove = handlePointer("pointer.move");
434
+ const onPointerUp = handlePointer("pointer.up");
435
+ const onPointerCancel = handlePointer("pointer.cancel");
436
+ const onKeyDown = handleKey("key.down");
437
+ const onKeyUp = handleKey("key.up");
438
+ container.addEventListener("pointerdown", onPointerDown);
439
+ container.addEventListener("pointermove", onPointerMove);
440
+ container.addEventListener("pointerup", onPointerUp);
441
+ container.addEventListener("pointercancel", onPointerCancel);
442
+ container.addEventListener("wheel", handleWheel, { passive: false });
443
+ win?.addEventListener("keydown", onKeyDown);
444
+ win?.addEventListener("keyup", onKeyUp);
445
+ return () => {
446
+ stopAutoPan();
447
+ runtime.cancelSession();
448
+ container.removeEventListener("pointerdown", onPointerDown);
449
+ container.removeEventListener("pointermove", onPointerMove);
450
+ container.removeEventListener("pointerup", onPointerUp);
451
+ container.removeEventListener("pointercancel", onPointerCancel);
452
+ container.removeEventListener("wheel", handleWheel);
453
+ win?.removeEventListener("keydown", onKeyDown);
454
+ win?.removeEventListener("keyup", onKeyUp);
455
+ };
80
456
  }
81
457
  //#endregion
82
- export { attachContainer, createMeasurementObserver };
458
+ export { applyFlowColorMode, attachContainer, attachInput, attachViewportTransform, computeRenderableIds, createMeasurementObserver, getAttachedContainer, nodePositionStyle, renderableIdsEqual, resolveFlowColorMode, viewportStyle };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statelyai/flow-dom",
3
- "version": "0.1.0",
3
+ "version": "3.0.0",
4
4
  "description": "DOM-specific helpers for @statelyai/flow.",
5
5
  "keywords": [
6
6
  "flow",
@@ -17,27 +17,32 @@
17
17
  "types": "dist/esm/index.d.ts",
18
18
  "exports": {
19
19
  ".": {
20
+ "source": "./src/index.ts",
20
21
  "types": "./dist/esm/index.d.ts",
21
22
  "import": "./dist/esm/index.js",
22
23
  "default": "./dist/esm/index.js"
23
- },
24
- "./src/index.ts": {
25
- "types": "./src/index.ts",
26
- "import": "./src/index.ts",
27
- "default": "./src/index.ts"
28
24
  }
29
25
  },
30
26
  "license": "MIT",
31
- "scripts": {
32
- "dev": "tsdown --watch",
33
- "build": "tsdown",
34
- "typecheck": "tsc --noEmit"
35
- },
36
27
  "peerDependencies": {
37
- "@statelyai/flow": ">=0.1.0"
28
+ "@statelyai/flow": ">=0.4.0"
29
+ },
30
+ "peerDependenciesMeta": {
31
+ "@statelyai/flow": {
32
+ "optional": true
33
+ }
38
34
  },
39
35
  "devDependencies": {
36
+ "jsdom": "^25.0.0",
40
37
  "tsdown": "^0.12.0",
41
- "typescript": "^5.8.0"
38
+ "typescript": "^5.8.0",
39
+ "vitest": "^4.1.0",
40
+ "@statelyai/flow": "0.4.0"
41
+ },
42
+ "scripts": {
43
+ "dev": "tsdown --watch",
44
+ "build": "tsdown",
45
+ "typecheck": "tsc --noEmit",
46
+ "test": "vitest run"
42
47
  }
43
- }
48
+ }