@burdenoff/fluidgrids-fe-sdk 2026.918.1 → 2026.922.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.
@@ -224,6 +224,245 @@ function stripSubmitActions(card) {
224
224
  }
225
225
  return result;
226
226
  }
227
+
228
+ // src/builder/components/lib/expression-preview.ts
229
+ var PURE_EXPRESSION_REGEX = /^\{\{(.*)\}\}$/s;
230
+ var PURE_LEGACY_REGEX = /^\$\{(.*)\}$/s;
231
+ function containsExpressionSyntax(str) {
232
+ return typeof str === "string" && str.includes("{{");
233
+ }
234
+ function findEmbeddedExpressions(str) {
235
+ const matches = [];
236
+ let searchFrom = 0;
237
+ while (searchFrom < str.length) {
238
+ const start = str.indexOf("{{", searchFrom);
239
+ if (start === -1) break;
240
+ const end = str.indexOf("}}", start + 2);
241
+ if (end === -1) break;
242
+ matches.push({
243
+ full: str.slice(start, end + 2),
244
+ expression: str.slice(start + 2, end).trim()
245
+ });
246
+ searchFrom = end + 2;
247
+ }
248
+ return matches;
249
+ }
250
+ function toItems(value) {
251
+ if (value === void 0 || value === null) return [];
252
+ if (Array.isArray(value)) return value.map((entry) => ({ json: entry }));
253
+ return [{ json: value }];
254
+ }
255
+ var SAFE_GLOBALS = {
256
+ parseInt,
257
+ parseFloat,
258
+ isNaN,
259
+ isFinite,
260
+ Number,
261
+ String,
262
+ Boolean,
263
+ JSON,
264
+ Date,
265
+ Math,
266
+ Array,
267
+ Object,
268
+ console: {
269
+ log: () => void 0,
270
+ error: () => void 0,
271
+ warn: () => void 0
272
+ }
273
+ };
274
+ function buildExpressionSandbox(scope) {
275
+ const inputItems = toItems(scope.input);
276
+ const currentItem = inputItems[0] ?? { json: {} };
277
+ const itemsForNode = (nodeName) => toItems(scope.nodeOutputs[nodeName]);
278
+ const dollarNode = (nodeName) => {
279
+ const items = itemsForNode(nodeName);
280
+ const at = items[0] ?? { json: {} };
281
+ return {
282
+ item: at.json,
283
+ all: () => items,
284
+ first: () => items[0]?.json ?? {},
285
+ last: () => items[items.length - 1]?.json ?? {}
286
+ };
287
+ };
288
+ const nodeObject = {};
289
+ for (const nodeName of Object.keys(scope.nodeOutputs)) {
290
+ nodeObject[nodeName] = dollarNode(nodeName);
291
+ }
292
+ const now = /* @__PURE__ */ new Date();
293
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
294
+ return {
295
+ ...SAFE_GLOBALS,
296
+ $json: currentItem.json,
297
+ $node: nodeObject,
298
+ $: dollarNode,
299
+ $items: (nodeName) => nodeName ? itemsForNode(nodeName) : inputItems,
300
+ $input: {
301
+ item: currentItem.json,
302
+ all: () => inputItems,
303
+ first: () => inputItems[0]?.json ?? {},
304
+ last: () => inputItems[inputItems.length - 1]?.json ?? {}
305
+ },
306
+ $workflow: {
307
+ id: scope.workflowKey,
308
+ name: scope.workflowKey,
309
+ version: scope.version
310
+ },
311
+ $execution: { id: scope.runId },
312
+ // Stubbed pending BOFF-7517 (Variables model) — mirrors the backend stub.
313
+ $vars: {},
314
+ // Never real env vars in the browser preview.
315
+ $env: {},
316
+ $now: now,
317
+ $today: today
318
+ };
319
+ }
320
+ function evaluateBare(expression, sandbox) {
321
+ const paramNames = Object.keys(sandbox);
322
+ const paramValues = Object.values(sandbox);
323
+ const fn = new Function(
324
+ ...paramNames,
325
+ `"use strict"; return (${expression});`
326
+ );
327
+ return fn(...paramValues);
328
+ }
329
+ function stringifyForInterpolation(value) {
330
+ if (value === null || value === void 0) return "";
331
+ if (typeof value === "object") return JSON.stringify(value);
332
+ return String(value);
333
+ }
334
+ function previewNewSyntax(str, scope) {
335
+ const sandbox = buildExpressionSandbox(scope);
336
+ const pure = PURE_EXPRESSION_REGEX.exec(str);
337
+ try {
338
+ if (pure) {
339
+ const expression = pure[1]?.trim();
340
+ if (!expression) {
341
+ return { isTemplate: true, error: "Empty expression" };
342
+ }
343
+ return { isTemplate: true, value: evaluateBare(expression, sandbox) };
344
+ }
345
+ let result = str;
346
+ for (const match of findEmbeddedExpressions(str)) {
347
+ if (!match.expression) continue;
348
+ const value = evaluateBare(match.expression, sandbox);
349
+ result = result.replace(match.full, stringifyForInterpolation(value));
350
+ }
351
+ return { isTemplate: true, value: result };
352
+ } catch (error) {
353
+ return {
354
+ isTemplate: true,
355
+ error: error instanceof Error ? error.message : String(error)
356
+ };
357
+ }
358
+ }
359
+ function previewLegacySyntax(str, scope) {
360
+ const sandbox = {
361
+ ...SAFE_GLOBALS,
362
+ output: scope.input,
363
+ TriggerInput: {},
364
+ WorkflowState: Object.fromEntries(
365
+ Object.entries(scope.nodeOutputs).map(([nodeId, output]) => [
366
+ nodeId,
367
+ { output, status: "COMPLETED" }
368
+ ])
369
+ ),
370
+ RunContext: {
371
+ runId: scope.runId ?? "preview",
372
+ workflowKey: scope.workflowKey ?? "preview",
373
+ version: scope.version ?? "1.0.0"
374
+ }
375
+ };
376
+ const pure = PURE_LEGACY_REGEX.exec(str);
377
+ try {
378
+ if (pure) {
379
+ const expression = pure[1]?.trim();
380
+ if (!expression) return { isTemplate: true, error: "Empty expression" };
381
+ return { isTemplate: true, value: evaluateBare(expression, sandbox) };
382
+ }
383
+ if (!str.includes("${")) {
384
+ return { isTemplate: false };
385
+ }
386
+ let result = str;
387
+ const templateRegex = /\$\{([^}]+)\}/g;
388
+ for (const match of str.matchAll(templateRegex)) {
389
+ const expression = match[1]?.trim();
390
+ if (!expression) continue;
391
+ const value = evaluateBare(expression, sandbox);
392
+ result = result.replace(match[0], stringifyForInterpolation(value));
393
+ }
394
+ return { isTemplate: true, value: result };
395
+ } catch (error) {
396
+ return {
397
+ isTemplate: true,
398
+ error: error instanceof Error ? error.message : String(error)
399
+ };
400
+ }
401
+ }
402
+ function evaluateExpressionPreview(str, scope) {
403
+ if (typeof str !== "string" || str.trim() === "") {
404
+ return { isTemplate: false };
405
+ }
406
+ if (containsExpressionSyntax(str)) {
407
+ return previewNewSyntax(str, scope);
408
+ }
409
+ return previewLegacySyntax(str, scope);
410
+ }
411
+ var EXPRESSION_BUILTINS = [
412
+ { insert: "$json", label: "$json", description: "Current item's data" },
413
+ {
414
+ insert: '$node["NodeName"]',
415
+ label: '$node["NodeName"]',
416
+ description: "Another node's current item"
417
+ },
418
+ {
419
+ insert: '$("NodeName")',
420
+ label: '$("NodeName")',
421
+ description: "Lineage helper: .item / .all() / .first() / .last()"
422
+ },
423
+ {
424
+ insert: "$items()",
425
+ label: "$items()",
426
+ description: "This node's input as a list of items"
427
+ },
428
+ {
429
+ insert: "$input",
430
+ label: "$input",
431
+ description: "Input helper: .item / .all() / .first() / .last()"
432
+ },
433
+ {
434
+ insert: "$workflow",
435
+ label: "$workflow",
436
+ description: "Workflow id / name / version"
437
+ },
438
+ {
439
+ insert: "$execution",
440
+ label: "$execution",
441
+ description: "Current execution id"
442
+ },
443
+ {
444
+ insert: "$vars",
445
+ label: "$vars",
446
+ description: "Workflow variables (coming soon)"
447
+ },
448
+ {
449
+ insert: "$env",
450
+ label: "$env",
451
+ description: "Allowlisted environment values"
452
+ },
453
+ { insert: "$now", label: "$now", description: "Current date/time" },
454
+ { insert: "$today", label: "$today", description: "Start of today" }
455
+ ];
456
+ var AUTOCOMPLETE_LOOKBACK = 40;
457
+ function openExpressionPrefix(value, cursor) {
458
+ const windowStart = Math.max(0, cursor - AUTOCOMPLETE_LOOKBACK);
459
+ const before = value.slice(windowStart, cursor);
460
+ const openIndex = before.lastIndexOf("{{");
461
+ if (openIndex === -1) return null;
462
+ const between = before.slice(openIndex + 2);
463
+ if (between.includes("}}")) return null;
464
+ return between.trimStart();
465
+ }
227
466
  function ExpressionEditor({
228
467
  value,
229
468
  onChange,
@@ -231,14 +470,18 @@ function ExpressionEditor({
231
470
  className,
232
471
  rows = 4,
233
472
  label,
234
- helpText
473
+ helpText,
474
+ nodeId
235
475
  }) {
236
476
  const tr = useTr();
237
477
  const editorRef = useRef(null);
238
478
  const [isFocused, setIsFocused] = useState(false);
479
+ const [suggestionsOpen, setSuggestionsOpen] = useState(false);
239
480
  const handleChange = useCallback(
240
481
  (e) => {
241
482
  onChange(e.target.value);
483
+ const cursor = e.target.selectionStart;
484
+ setSuggestionsOpen(openExpressionPrefix(e.target.value, cursor) !== null);
242
485
  },
243
486
  [onChange]
244
487
  );
@@ -260,6 +503,83 @@ function ExpressionEditor({
260
503
  },
261
504
  [value, onChange]
262
505
  );
506
+ const applySuggestion = useCallback(
507
+ (insert) => {
508
+ const textarea = editorRef.current;
509
+ if (!textarea) {
510
+ insertTemplate(`{{ ${insert} }}`);
511
+ return;
512
+ }
513
+ const cursor = textarea.selectionStart;
514
+ const windowStart = Math.max(0, cursor - AUTOCOMPLETE_LOOKBACK);
515
+ const before = value.slice(windowStart, cursor);
516
+ const openIndex = before.lastIndexOf("{{");
517
+ if (openIndex === -1) {
518
+ insertTemplate(`{{ ${insert} }}`);
519
+ return;
520
+ }
521
+ const replaceStart = windowStart + openIndex;
522
+ const newValue = value.slice(0, replaceStart) + `{{ ${insert} }}` + value.slice(cursor);
523
+ onChange(newValue);
524
+ setSuggestionsOpen(false);
525
+ const cursorAfter = replaceStart + `{{ ${insert} }}`.length;
526
+ setTimeout(() => {
527
+ textarea.focus();
528
+ textarea.setSelectionRange(cursorAfter, cursorAfter);
529
+ }, 0);
530
+ },
531
+ [value, onChange, insertTemplate]
532
+ );
533
+ const cursorPos = editorRef.current?.selectionStart ?? value.length;
534
+ const filterText = suggestionsOpen ? openExpressionPrefix(value, cursorPos) ?? "" : "";
535
+ const suggestions = useMemo(() => {
536
+ if (!suggestionsOpen) return [];
537
+ const needle = filterText.toLowerCase();
538
+ return EXPRESSION_BUILTINS.filter(
539
+ (b) => b.label.toLowerCase().startsWith(needle || "$")
540
+ ).slice(0, 8);
541
+ }, [suggestionsOpen, filterText]);
542
+ const logs = useFlowStore((s) => s.execution.logs);
543
+ const workflowKey = useFlowStore((s) => s.workflow.workflowKey);
544
+ const workflowVersion = useFlowStore((s) => s.workflow.version);
545
+ const previewScope = useMemo(() => {
546
+ const nodeOutputs = {};
547
+ let input = {};
548
+ let runId;
549
+ for (const log of logs) {
550
+ if (log.status !== "completed" || !log.output) continue;
551
+ nodeOutputs[log.nodeId] = log.output;
552
+ if (log.nodeId === nodeId) {
553
+ input = log.inputArgs ?? {};
554
+ runId = log.runId;
555
+ }
556
+ }
557
+ return { input, nodeOutputs, workflowKey, version: workflowVersion, runId };
558
+ }, [logs, nodeId, workflowKey, workflowVersion]);
559
+ const hasPreviewData = Object.keys(previewScope.input).length > 0;
560
+ const [preview, setPreview] = useState(null);
561
+ useEffect(() => {
562
+ if (!nodeId || !value.trim()) {
563
+ setPreview(null);
564
+ return;
565
+ }
566
+ const timer = setTimeout(() => {
567
+ setPreview(evaluateExpressionPreview(value, previewScope));
568
+ }, 200);
569
+ return () => clearTimeout(timer);
570
+ }, [value, previewScope, nodeId]);
571
+ const previewText = useMemo(() => {
572
+ if (!preview) return null;
573
+ if (!preview.isTemplate) return null;
574
+ if (preview.error) return null;
575
+ if (preview.value === void 0) return "undefined";
576
+ if (typeof preview.value === "string") return preview.value;
577
+ try {
578
+ return JSON.stringify(preview.value);
579
+ } catch {
580
+ return String(preview.value);
581
+ }
582
+ }, [preview]);
263
583
  return /* @__PURE__ */ jsxs("div", { className: cn("space-y-2", className), children: [
264
584
  label && /* @__PURE__ */ jsx("label", { className: "text-sm font-medium text-foreground", children: label }),
265
585
  /* @__PURE__ */ jsxs(
@@ -270,7 +590,7 @@ function ExpressionEditor({
270
590
  isFocused && "ring-2 ring-primary ring-offset-1"
271
591
  ),
272
592
  children: [
273
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 px-2 py-1.5 border-b bg-muted/30", children: [
593
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-1 px-2 py-1.5 border-b bg-muted/30", children: [
274
594
  /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground mr-2", children: tr("fluidgrids.builder.expressionEditor.insertLabel", "Insert:") }),
275
595
  /* @__PURE__ */ jsx(
276
596
  "button",
@@ -298,29 +618,97 @@ function ExpressionEditor({
298
618
  className: "text-xs px-2 py-0.5 rounded bg-primary/10 text-primary hover:bg-primary/20 transition-colors",
299
619
  children: "${WorkflowState}"
300
620
  }
621
+ ),
622
+ /* @__PURE__ */ jsx("span", { className: "mx-1 h-4 w-px bg-border", "aria-hidden": "true" }),
623
+ /* @__PURE__ */ jsx(
624
+ "button",
625
+ {
626
+ type: "button",
627
+ onClick: () => insertTemplate("{{ $json. }}"),
628
+ className: "text-xs px-2 py-0.5 rounded bg-secondary/40 text-secondary-foreground hover:bg-secondary/60 transition-colors",
629
+ children: "{{ $json }}"
630
+ }
301
631
  )
302
632
  ] }),
303
- /* @__PURE__ */ jsx(
304
- "textarea",
305
- {
306
- ref: editorRef,
307
- value,
308
- onChange: handleChange,
309
- onFocus: () => setIsFocused(true),
310
- onBlur: () => setIsFocused(false),
311
- placeholder: placeholder ?? tr(
312
- "fluidgrids.builder.expressionEditor.defaultPlaceholder",
313
- "Enter expression..."
314
- ),
315
- rows,
316
- spellCheck: false,
317
- className: cn(
318
- "w-full p-3 font-mono text-sm bg-background resize-none",
319
- "focus:outline-none",
320
- "placeholder:text-muted-foreground/50"
321
- )
322
- }
323
- )
633
+ /* @__PURE__ */ jsxs("div", { className: "relative", children: [
634
+ /* @__PURE__ */ jsx(
635
+ "textarea",
636
+ {
637
+ ref: editorRef,
638
+ value,
639
+ onChange: handleChange,
640
+ onKeyUp: (e) => {
641
+ const target = e.currentTarget;
642
+ setSuggestionsOpen(
643
+ openExpressionPrefix(target.value, target.selectionStart) !== null
644
+ );
645
+ },
646
+ onFocus: () => setIsFocused(true),
647
+ onBlur: () => {
648
+ setIsFocused(false);
649
+ setTimeout(() => setSuggestionsOpen(false), 150);
650
+ },
651
+ placeholder: placeholder ?? tr(
652
+ "fluidgrids.builder.expressionEditor.defaultPlaceholder",
653
+ "Enter expression..."
654
+ ),
655
+ rows,
656
+ spellCheck: false,
657
+ className: cn(
658
+ "w-full p-3 font-mono text-sm bg-background resize-none",
659
+ "focus:outline-none",
660
+ "placeholder:text-muted-foreground/50"
661
+ )
662
+ }
663
+ ),
664
+ suggestionsOpen && suggestions.length > 0 && /* @__PURE__ */ jsx(
665
+ "div",
666
+ {
667
+ className: "absolute z-20 left-2 right-2 top-full mt-1 rounded-md border bg-popover shadow-md overflow-hidden",
668
+ role: "listbox",
669
+ "aria-label": tr(
670
+ "fluidgrids.builder.expressionEditor.autocompleteLabel",
671
+ "Expression variable suggestions"
672
+ ),
673
+ children: suggestions.map((s) => /* @__PURE__ */ jsxs(
674
+ "button",
675
+ {
676
+ type: "button",
677
+ role: "option",
678
+ "aria-selected": false,
679
+ onMouseDown: (e) => {
680
+ e.preventDefault();
681
+ applySuggestion(s.insert);
682
+ },
683
+ className: "flex w-full items-baseline gap-2 px-3 py-1.5 text-left hover:bg-accent",
684
+ children: [
685
+ /* @__PURE__ */ jsx("span", { className: "font-mono text-xs text-primary", children: s.label }),
686
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground truncate", children: s.description })
687
+ ]
688
+ },
689
+ s.insert
690
+ ))
691
+ }
692
+ )
693
+ ] }),
694
+ nodeId && value.trim() && /* @__PURE__ */ jsx("div", { className: "border-t bg-muted/20 px-3 py-1.5 text-xs", children: preview?.error ? /* @__PURE__ */ jsxs("span", { className: "text-destructive", children: [
695
+ tr(
696
+ "fluidgrids.builder.expressionEditor.previewError",
697
+ "Preview error:"
698
+ ),
699
+ " ",
700
+ preview.error
701
+ ] }) : previewText !== null ? /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
702
+ tr(
703
+ "fluidgrids.builder.expressionEditor.previewLabel",
704
+ "Preview:"
705
+ ),
706
+ " ",
707
+ /* @__PURE__ */ jsx("span", { className: "font-mono text-foreground", children: previewText })
708
+ ] }) : !hasPreviewData ? /* @__PURE__ */ jsx("span", { className: "text-muted-foreground/70 italic", children: tr(
709
+ "fluidgrids.builder.expressionEditor.previewNoData",
710
+ 'Run "Execute step" on this node to preview real values.'
711
+ ) }) : null })
324
712
  ]
325
713
  }
326
714
  ),
@@ -13819,7 +14207,7 @@ var DEFAULT_CONFIG = {
13819
14207
  maxTemplateLength: 1e4,
13820
14208
  debug: false
13821
14209
  };
13822
- var SAFE_GLOBALS = {
14210
+ var SAFE_GLOBALS2 = {
13823
14211
  // Type conversion
13824
14212
  parseInt,
13825
14213
  parseFloat,
@@ -13866,7 +14254,7 @@ function parseTemplate(template) {
13866
14254
  }
13867
14255
  function buildSandbox(scope) {
13868
14256
  return {
13869
- ...SAFE_GLOBALS,
14257
+ ...SAFE_GLOBALS2,
13870
14258
  output: scope.output,
13871
14259
  TriggerInput: scope.TriggerInput,
13872
14260
  WorkflowState: scope.WorkflowState,
@@ -18780,11 +19168,59 @@ function useInsertStickyNote() {
18780
19168
  return sticky;
18781
19169
  }, [addNodes, getNodes, screenToFlowPosition, updateNode]);
18782
19170
  }
19171
+
19172
+ // src/builder/components/flow-builder/components/controls/canvas-controls.drag.ts
19173
+ function clampDragOffset(baseLeft, baseTop, width, height, bounds, margin, rawOffset) {
19174
+ const nextLeft = baseLeft + rawOffset.x;
19175
+ const nextTop = baseTop + rawOffset.y;
19176
+ const maxX = bounds.right - margin - width;
19177
+ const maxY = bounds.bottom - margin - height;
19178
+ const clampedLeft = Math.min(Math.max(nextLeft, bounds.left + margin), maxX);
19179
+ const clampedTop = Math.min(Math.max(nextTop, bounds.top + margin), maxY);
19180
+ return {
19181
+ x: clampedLeft - baseLeft,
19182
+ y: clampedTop - baseTop
19183
+ };
19184
+ }
19185
+ var MINIMAP_WIDTH = 200;
19186
+ var MINIMAP_HEIGHT = 150;
19187
+ var MINIMAP_GAP = 4;
19188
+ function computeMinimapRect(panelRect, bounds, margin) {
19189
+ let left = panelRect.left;
19190
+ let top = panelRect.top - MINIMAP_HEIGHT - MINIMAP_GAP;
19191
+ if (top < bounds.top + margin) {
19192
+ top = panelRect.bottom + MINIMAP_GAP;
19193
+ }
19194
+ const maxLeft = bounds.right - margin - MINIMAP_WIDTH;
19195
+ left = Math.min(Math.max(left, bounds.left + margin), maxLeft);
19196
+ const maxTop = bounds.bottom - margin - MINIMAP_HEIGHT;
19197
+ top = Math.min(Math.max(top, bounds.top + margin), maxTop);
19198
+ return { left, top };
19199
+ }
18783
19200
  var ZOOM_DURATION = 500;
18784
19201
  var NODE_WIDTH = 220;
18785
19202
  var NODE_HEIGHT = 80;
18786
19203
  var H_GAP = 80;
18787
19204
  var V_GAP = 48;
19205
+ var CANVAS_MARGIN = 12;
19206
+ function readCanvasBounds(el) {
19207
+ if (!el) return null;
19208
+ const cs = getComputedStyle(el);
19209
+ const num = (value, fallback) => {
19210
+ const n = parseFloat(value);
19211
+ return Number.isFinite(n) ? n : fallback;
19212
+ };
19213
+ return {
19214
+ left: num(cs.getPropertyValue("--canvas-left"), 0),
19215
+ right: num(cs.getPropertyValue("--canvas-right"), window.innerWidth),
19216
+ top: num(cs.getPropertyValue("--canvas-top"), 0),
19217
+ bottom: num(cs.getPropertyValue("--canvas-bottom"), window.innerHeight)
19218
+ };
19219
+ }
19220
+ function isInteractiveTarget(target) {
19221
+ if (!(target instanceof Element)) return false;
19222
+ return target.closest("button, input, a, [data-no-drag]") !== null;
19223
+ }
18788
19224
  function zoomSelector(s) {
18789
19225
  return {
18790
19226
  zoom: s.transform[2],
@@ -18821,6 +19257,10 @@ function CanvasControls() {
18821
19257
  const isReadOnly = searchParams.get("readonly") === "true";
18822
19258
  const [showMinimap, setShowMinimap] = useState(false);
18823
19259
  const [edgesHidden, setEdgesHidden] = useState(false);
19260
+ const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
19261
+ const [isDragging, setIsDragging] = useState(false);
19262
+ const panelRef = useRef(null);
19263
+ const dragState = useRef(null);
18824
19264
  const { zoom, minZoomReached, maxZoomReached } = useStore(
18825
19265
  zoomSelector,
18826
19266
  shallow
@@ -18900,6 +19340,59 @@ function CanvasControls() {
18900
19340
  );
18901
19341
  setTimeout(() => fitView({ duration: ZOOM_DURATION, padding: 0.15 }), 50);
18902
19342
  }, [getNodes, getEdges, setNodes, fitView]);
19343
+ const handlePointerDown = useCallback(
19344
+ (e) => {
19345
+ if (isInteractiveTarget(e.target)) return;
19346
+ const panel = panelRef.current;
19347
+ if (!panel) return;
19348
+ panel.setPointerCapture(e.pointerId);
19349
+ const rect = panel.getBoundingClientRect();
19350
+ dragState.current = {
19351
+ startX: e.clientX,
19352
+ startY: e.clientY,
19353
+ startOffsetX: dragOffset.x,
19354
+ startOffsetY: dragOffset.y,
19355
+ baseLeft: rect.left - dragOffset.x,
19356
+ baseTop: rect.top - dragOffset.y,
19357
+ width: rect.width,
19358
+ height: rect.height
19359
+ };
19360
+ setIsDragging(true);
19361
+ },
19362
+ [dragOffset]
19363
+ );
19364
+ const handlePointerMove = useCallback((e) => {
19365
+ const ds = dragState.current;
19366
+ if (!ds) return;
19367
+ const bounds = readCanvasBounds(panelRef.current);
19368
+ if (!bounds) return;
19369
+ const rawOffset = {
19370
+ x: ds.startOffsetX + (e.clientX - ds.startX),
19371
+ y: ds.startOffsetY + (e.clientY - ds.startY)
19372
+ };
19373
+ setDragOffset(
19374
+ clampDragOffset(
19375
+ ds.baseLeft,
19376
+ ds.baseTop,
19377
+ ds.width,
19378
+ ds.height,
19379
+ bounds,
19380
+ CANVAS_MARGIN,
19381
+ rawOffset
19382
+ )
19383
+ );
19384
+ }, []);
19385
+ const handlePointerUp = useCallback((e) => {
19386
+ if (!dragState.current) return;
19387
+ if (panelRef.current?.hasPointerCapture(e.pointerId)) {
19388
+ panelRef.current.releasePointerCapture(e.pointerId);
19389
+ }
19390
+ dragState.current = null;
19391
+ setIsDragging(false);
19392
+ }, []);
19393
+ const panelRect = panelRef.current?.getBoundingClientRect();
19394
+ const canvasBounds = readCanvasBounds(panelRef.current);
19395
+ const minimapRect = panelRect && canvasBounds ? computeMinimapRect(panelRect, canvasBounds, CANVAS_MARGIN) : null;
18903
19396
  return /* @__PURE__ */ jsxs(Fragment, { children: [
18904
19397
  showMinimap && /* @__PURE__ */ jsx(
18905
19398
  MiniMap,
@@ -18907,9 +19400,9 @@ function CanvasControls() {
18907
19400
  position: "bottom-left",
18908
19401
  style: {
18909
19402
  position: "fixed",
18910
- bottom: 52,
18911
- left: "calc(var(--canvas-left, 64px) + 12px)",
18912
- marginBottom: 4,
19403
+ left: minimapRect ? minimapRect.left : void 0,
19404
+ bottom: minimapRect ? window.innerHeight - minimapRect.top - MINIMAP_HEIGHT : 52,
19405
+ margin: 0,
18913
19406
  backgroundColor: "var(--color-card)"
18914
19407
  },
18915
19408
  className: "!rounded-lg !border !border-card-foreground/10 !shadow-lg",
@@ -18923,8 +19416,20 @@ function CanvasControls() {
18923
19416
  /* @__PURE__ */ jsxs(
18924
19417
  "div",
18925
19418
  {
18926
- className: "fixed bottom-3 z-50 flex items-center gap-1 rounded-xl border border-card-foreground/10 bg-card px-2 py-1.5 shadow-lg",
18927
- style: { left: "calc(var(--canvas-left, 64px) + 12px)" },
19419
+ ref: panelRef,
19420
+ onPointerDown: handlePointerDown,
19421
+ onPointerMove: handlePointerMove,
19422
+ onPointerUp: handlePointerUp,
19423
+ onPointerCancel: handlePointerUp,
19424
+ className: [
19425
+ "fixed bottom-3 z-50 flex items-center gap-1 rounded-xl border",
19426
+ "border-card-foreground/10 bg-card px-2 py-1.5 shadow-lg touch-none",
19427
+ isDragging ? "cursor-grabbing" : "cursor-grab"
19428
+ ].join(" "),
19429
+ style: {
19430
+ left: "calc(var(--canvas-left, 64px) + 12px)",
19431
+ transform: `translate3d(${dragOffset.x}px, ${dragOffset.y}px, 0)`
19432
+ },
18928
19433
  children: [
18929
19434
  /* @__PURE__ */ jsx(
18930
19435
  ControlBtn,
@@ -23152,13 +23657,16 @@ var FlowBuilder = ({ readOnly = false }) => {
23152
23657
  {
23153
23658
  ref: (el) => {
23154
23659
  if (!el) return;
23155
- const update = () => el.style.setProperty(
23156
- "--canvas-left",
23157
- `${el.getBoundingClientRect().left}px`
23158
- );
23660
+ const update = () => {
23661
+ const rect = el.getBoundingClientRect();
23662
+ el.style.setProperty("--canvas-left", `${rect.left}px`);
23663
+ el.style.setProperty("--canvas-right", `${rect.right}px`);
23664
+ el.style.setProperty("--canvas-top", `${rect.top}px`);
23665
+ el.style.setProperty("--canvas-bottom", `${rect.bottom}px`);
23666
+ };
23159
23667
  update();
23160
23668
  const ro = new ResizeObserver(update);
23161
- ro.observe(document.documentElement);
23669
+ ro.observe(el);
23162
23670
  el._ro?.disconnect();
23163
23671
  el._ro = ro;
23164
23672
  },
@@ -23316,5 +23824,5 @@ var FlowBuilderPage = ({
23316
23824
  };
23317
23825
 
23318
23826
  export { Card2 as Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, ConnectionFormDialog, ConnectionSelector, EntityTagPicker, FLUIDGRIDS_ENTITY_TYPES, FlowBuilderPage, FluidGridsProvider, ProviderPicker, SchemaConnectionForm, ScopePicker, TemplateCard, buildNodeCheck, buildRequiredNodeTypes, listCustomBlocks, mergeStickyNotes, refetchStoreNodes, registerCustomBlock, resolveUiSpec, stripSubmitActions, tagLabel, useAvailableTags, useConnection, useConnectionDialog, useConnections, useConnectionsByProvider, useCreateConnection, useCreateTag, useDraftEntityTags, useEntityTags, useFluidGridsContext, useImportWorkflowTemplate, useInitiateOAuth, useInstalledNodes, useNodeExecution, useProviderByKey, useProviderCatalog, useProviders, useReplaceEntityTags, useSearchTags, useTagEntity, useUntagEntity, useWorkflowTemplates };
23319
- //# sourceMappingURL=chunk-UVBJY5FB.js.map
23320
- //# sourceMappingURL=chunk-UVBJY5FB.js.map
23827
+ //# sourceMappingURL=chunk-XASNLRFQ.js.map
23828
+ //# sourceMappingURL=chunk-XASNLRFQ.js.map