stickyflow 0.2.0 → 0.4.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +1 -1
  3. package/dist/{chunk-KFJIFHXG.js → chunk-65IVT4GK.js} +475 -199
  4. package/dist/chunk-65IVT4GK.js.map +1 -0
  5. package/dist/{chunk-6JKNT2YC.js → chunk-BYMIYXP5.js} +17 -7
  6. package/dist/chunk-BYMIYXP5.js.map +1 -0
  7. package/dist/{chunk-N6WVHOUQ.js → chunk-CSXQD3ON.js} +28 -15
  8. package/dist/chunk-CSXQD3ON.js.map +1 -0
  9. package/dist/{chunk-YM3XFBI4.cjs → chunk-CXL254BT.cjs} +17 -6
  10. package/dist/chunk-CXL254BT.cjs.map +1 -0
  11. package/dist/{chunk-S5BVAAEF.cjs → chunk-DDCTNLLD.cjs} +989 -713
  12. package/dist/chunk-DDCTNLLD.cjs.map +1 -0
  13. package/dist/{chunk-34PK7HEU.cjs → chunk-HFOGKSNU.cjs} +18 -18
  14. package/dist/{chunk-34PK7HEU.cjs.map → chunk-HFOGKSNU.cjs.map} +1 -1
  15. package/dist/{chunk-IIOJFR73.cjs → chunk-UXDWH2JC.cjs} +56 -43
  16. package/dist/chunk-UXDWH2JC.cjs.map +1 -0
  17. package/dist/{chunk-6LQJUAD4.js → chunk-XEN5QIIJ.js} +6 -6
  18. package/dist/{chunk-6LQJUAD4.js.map → chunk-XEN5QIIJ.js.map} +1 -1
  19. package/dist/index.cjs +24 -24
  20. package/dist/index.d.cts +4 -4
  21. package/dist/index.d.ts +4 -4
  22. package/dist/index.js +4 -4
  23. package/dist/interaction/index.cjs +780 -382
  24. package/dist/interaction/index.cjs.map +1 -1
  25. package/dist/interaction/index.d.cts +12 -3
  26. package/dist/interaction/index.d.ts +12 -3
  27. package/dist/interaction/index.js +562 -165
  28. package/dist/interaction/index.js.map +1 -1
  29. package/dist/{render-r1GYvjNm.d.cts → render-C5X74hds.d.cts} +75 -4
  30. package/dist/{render-r1GYvjNm.d.ts → render-C5X74hds.d.ts} +75 -4
  31. package/dist/{runtimeTypes-ByiafBwg.d.ts → runtimeTypes-D8hPZ62T.d.ts} +1 -1
  32. package/dist/{runtimeTypes-DwLeD-em.d.cts → runtimeTypes-yTSAvtCn.d.cts} +1 -1
  33. package/dist/stickyflow-diagram.global.js +37 -35
  34. package/dist/stickyflow-diagram.global.js.map +1 -1
  35. package/dist/{types-T7W2yTQF.d.cts → types-BDKDf4zr.d.cts} +1 -1
  36. package/dist/{types-ndGA1kbl.d.ts → types-CdgGdtoD.d.ts} +1 -1
  37. package/dist/viewer/index.cjs +9 -9
  38. package/dist/viewer/index.d.cts +4 -4
  39. package/dist/viewer/index.d.ts +4 -4
  40. package/dist/viewer/index.js +4 -4
  41. package/dist/web-component/index.cjs +6 -6
  42. package/dist/web-component/index.d.cts +2 -2
  43. package/dist/web-component/index.d.ts +2 -2
  44. package/dist/web-component/index.js +4 -4
  45. package/package.json +1 -1
  46. package/dist/chunk-6JKNT2YC.js.map +0 -1
  47. package/dist/chunk-IIOJFR73.cjs.map +0 -1
  48. package/dist/chunk-KFJIFHXG.js.map +0 -1
  49. package/dist/chunk-N6WVHOUQ.js.map +0 -1
  50. package/dist/chunk-S5BVAAEF.cjs.map +0 -1
  51. package/dist/chunk-YM3XFBI4.cjs.map +0 -1
@@ -1,5 +1,5 @@
1
- import { screenRectOf, Emitter, resolveOwner, isCameraGesture, pointerUserPoint, clearAlignmentGuides, updateAlignmentGuides, pxPerUnit, findOwnerNode, isPrimary, SVG_NAMESPACE, isTextLabel, findSnapCandidateBounds, symbolDeclaredBounds, updateQuickDuplicateBubbles, isometricGeometryOf, ISO_HALO_ATTR, ISO_DUPLICATE_BUBBLES_ATTR, updateHalo, axisArrowPlacements, updateAxisArrowGroup, PATH_HALO_ATTR, findAllOwnerNodes, findIsometricApexCandidates, DERIVED_HEIGHT_ROLES, resizeHandlePositions, pathGeometryOf } from '../chunk-6LQJUAD4.js';
2
- import { sflToCanvasState, tokenizeSource, roundedOrthogonalPathData, seriesLabelOffsetXProp, seriesLabelOffsetYProp, radarVertexPoint, freeformPathData, CORNER_RADIUS_ATTR, AXIS_COS, isometricFaces, roundedPolygonPath, isometricTopFaceCenter, calculateLinkRoute, pointAtPort, roundedBarPath, barShapePath, formatBarLabel, LABEL_WIDTH, LEADER_INSET, clipSegmentToRect, AXIS_SIN, radarValueAtPoint, radarSeriesPath, estimateTextWidth, wrapText, personLabelArea, isometricFaceCenter, PATH_POINT_INDEX_ATTR, linePathData, barLabelSize, BAR_LABEL, pillPath, getLineMidpoint, labelGapDash, pillCornerRadius, SQUARE_CORNER_ATTR, cornersRoundedRectPath, localShapePath, relativeOffsetOnRectangleSide, edgePointToward, SYMBOL_POINTS_ATTR, SYMBOL_TENSION_ATTR } from '../chunk-6JKNT2YC.js';
1
+ import { screenRectOf, Emitter, resolveOwner, isCameraGesture, pointerUserPoint, clearAlignmentGuides, updateAlignmentGuides, pxPerUnit, findOwnerNode, isPrimary, SVG_NAMESPACE, isTextLabel, findSnapCandidateBounds, symbolDeclaredBounds, updateQuickDuplicateBubbles, isometricGeometryOf, ISO_HALO_ATTR, ISO_DUPLICATE_BUBBLES_ATTR, updateHalo, axisArrowPlacements, updateAxisArrowGroup, PATH_HALO_ATTR, findAllOwnerNodes, findIsometricApexCandidates, DERIVED_HEIGHT_ROLES, resizeHandlePositions, pathGeometryOf } from '../chunk-XEN5QIIJ.js';
2
+ import { sflToCanvasState, isIsometricObject, tokenizeSource, roundedOrthogonalPathData, seriesLabelOffsetXProp, seriesLabelOffsetYProp, radarVertexPoint, AXIS_SIN, AXIS_COS, freeformPathData, CORNER_RADIUS_ATTR, isometricFaces, roundedPolygonPath, isometricTopFaceCenter, calculateLinkRoute, pointAtPort, roundedBarPath, barShapePath, formatBarLabel, LABEL_WIDTH, LEADER_INSET, clipSegmentToRect, radarValueAtPoint, radarSeriesPath, estimateTextWidth, wrapText, personLabelArea, isometricFaceCenter, PATH_POINT_INDEX_ATTR, linePathData, barLabelSize, BAR_LABEL, pillPath, getLineMidpoint, labelGapDash, pillCornerRadius, SQUARE_CORNER_ATTR, cornersRoundedRectPath, localShapePath, relativeOffsetOnRectangleSide, edgePointToward, SYMBOL_POINTS_ATTR, SYMBOL_TENSION_ATTR } from '../chunk-BYMIYXP5.js';
3
3
 
4
4
  // src/runtime/interaction/chartValue/coordinates.ts
5
5
  function pointerUserY(svg, event) {
@@ -466,6 +466,39 @@ function normalizeHex(value) {
466
466
  const full = body.length === 3 ? body.split("").map((char) => char + char).join("") : body;
467
467
  return `#${full.toLowerCase()}`;
468
468
  }
469
+ var COLOR_FUNCTION = /^rgba?\(\s*(\d{1,3})\s*[, ]\s*(\d{1,3})\s*[, ]\s*(\d{1,3})\s*(?:[,/]\s*(\d*\.?\d+)(%?)\s*)?\)$/i;
470
+ var HEX_WITH_ALPHA = /^#([0-9a-f]{6})([0-9a-f]{2})$/i;
471
+ function parseColorAlpha(value) {
472
+ const raw = value.trim();
473
+ const fn = COLOR_FUNCTION.exec(raw);
474
+ if (fn) {
475
+ const alpha = fn[4] === void 0 ? 1 : Number(fn[4]) / (fn[5] ? 100 : 1);
476
+ return {
477
+ hex: rgbToHex({ r: Number(fn[1]), g: Number(fn[2]), b: Number(fn[3]) }),
478
+ alpha: Math.max(0, Math.min(1, alpha))
479
+ };
480
+ }
481
+ const eight = HEX_WITH_ALPHA.exec(raw);
482
+ if (eight) {
483
+ return { hex: `#${eight[1].toLowerCase()}`, alpha: Number.parseInt(eight[2], 16) / 255 };
484
+ }
485
+ const hex = normalizeHex(raw);
486
+ return hex ? { hex, alpha: 1 } : null;
487
+ }
488
+ function formatColorAlpha(hex, alpha) {
489
+ const clamped = Math.max(0, Math.min(1, alpha));
490
+ if (clamped >= 1) {
491
+ return hex;
492
+ }
493
+ const { r, g, b } = hexToRgb(hex);
494
+ return `rgba(${r}, ${g}, ${b}, ${Math.round(clamped * 100) / 100})`;
495
+ }
496
+ function hexOf(value) {
497
+ return parseColorAlpha(value)?.hex ?? "#000000";
498
+ }
499
+ function alphaOf(value) {
500
+ return parseColorAlpha(value)?.alpha ?? 1;
501
+ }
469
502
  function clampChannel(value) {
470
503
  return Math.max(0, Math.min(255, Math.round(value)));
471
504
  }
@@ -532,7 +565,7 @@ function hsvToHex(hsv) {
532
565
  // src/runtime/interaction/colorEdit/gradientValue.ts
533
566
  var DEFAULT_GRADIENT_ANGLE = 90;
534
567
  var GRADIENT = /^gradient\(\s*(-?[\d.]+)\s*,\s*(.+)\)\s*$/i;
535
- var STOP = /(#[0-9a-f]{3}(?:[0-9a-f]{3})?)\s+(-?[\d.]+)%/gi;
568
+ var STOP = /(rgba?\([^)]*\)|#[0-9a-f]{3}(?:[0-9a-f]{3})?)\s+(-?[\d.]+)%/gi;
536
569
  function trim(value) {
537
570
  return Number(value.toFixed(1));
538
571
  }
@@ -553,10 +586,10 @@ function parseGradient(source) {
553
586
  }
554
587
  const stops = [];
555
588
  for (const stop of match[2].matchAll(STOP)) {
556
- const color = normalizeHex(stop[1]);
589
+ const parsed = parseColorAlpha(stop[1]);
557
590
  const position = Number(stop[2]);
558
- if (color && Number.isFinite(position)) {
559
- stops.push({ color, position: clampPosition(position) });
591
+ if (parsed && Number.isFinite(position)) {
592
+ stops.push({ color: formatColorAlpha(parsed.hex, parsed.alpha), position: clampPosition(position) });
560
593
  }
561
594
  }
562
595
  return stops.length >= 2 ? { angle, stops } : null;
@@ -591,10 +624,10 @@ function readGradientValue(svg, fill) {
591
624
  }
592
625
  const stops = [];
593
626
  for (const stop of def.querySelectorAll("stop")) {
594
- const color = normalizeHex(stop.getAttribute("stop-color") ?? "");
627
+ const parsed = parseColorAlpha(stop.getAttribute("stop-color") ?? "");
595
628
  const offset = Number.parseFloat(stop.getAttribute("offset") ?? "");
596
- if (color && Number.isFinite(offset)) {
597
- stops.push({ color, position: clampPosition(offset) });
629
+ if (parsed && Number.isFinite(offset)) {
630
+ stops.push({ color: formatColorAlpha(parsed.hex, parsed.alpha), position: clampPosition(offset) });
598
631
  }
599
632
  }
600
633
  if (stops.length < 2) {
@@ -613,17 +646,21 @@ function seedFrom(fill, gradient) {
613
646
  if (parsed) {
614
647
  return { mode: "gradient", angle: parsed.angle, stops: sortStops(parsed.stops) };
615
648
  }
616
- return { mode: "solid", color: fill && normalizeHex(fill) || FALLBACK_COLOR };
649
+ const solid = fill ? parseColorAlpha(fill) : null;
650
+ return { mode: "solid", color: solid?.hex ?? FALLBACK_COLOR, alpha: solid?.alpha ?? 1 };
617
651
  }
618
652
  function formatPickerValue(value) {
619
- return value.mode === "solid" ? value.color : formatGradient({ angle: value.angle, stops: value.stops });
653
+ return value.mode === "solid" ? formatColorAlpha(value.color, value.alpha ?? 1) : formatGradient({ angle: value.angle, stops: value.stops });
620
654
  }
621
655
  function cssBackground(color) {
622
656
  const gradient = parseGradient(color);
623
657
  return gradient ? gradientCss(gradient) : color;
624
658
  }
659
+ function toAlpha(value) {
660
+ return value.mode === "solid" ? value.alpha ?? 1 : alphaOf(sortStops(value.stops)[0]?.color ?? FALLBACK_COLOR);
661
+ }
625
662
  function toSolid(value) {
626
- return value.mode === "solid" ? value.color : sortStops(value.stops)[0]?.color ?? FALLBACK_COLOR;
663
+ return value.mode === "solid" ? value.color : hexOf(sortStops(value.stops)[0]?.color ?? FALLBACK_COLOR);
627
664
  }
628
665
  function darken(hex) {
629
666
  const hsv = hexToHsv(hex);
@@ -636,8 +673,8 @@ function toGradient(value) {
636
673
  return {
637
674
  angle: DEFAULT_GRADIENT_ANGLE,
638
675
  stops: [
639
- { color: value.color, position: 0 },
640
- { color: darken(value.color), position: 100 }
676
+ { color: formatColorAlpha(value.color, value.alpha ?? 1), position: 0 },
677
+ { color: formatColorAlpha(darken(value.color), value.alpha ?? 1), position: 100 }
641
678
  ]
642
679
  };
643
680
  }
@@ -649,17 +686,30 @@ function activeColor(picker) {
649
686
  const { state } = picker;
650
687
  return state.mode === "solid" ? state.solid : state.gradient.stops[state.selected]?.color ?? state.solid;
651
688
  }
652
- function setActiveColor(picker, hex) {
689
+ function activeHex(picker) {
690
+ return hexOf(activeColor(picker));
691
+ }
692
+ function activeAlpha(picker) {
693
+ return picker.state.mode === "solid" ? picker.state.alpha : alphaOf(activeColor(picker));
694
+ }
695
+ function setActiveColorAlpha(picker, hex, alpha) {
653
696
  const { state } = picker;
654
697
  if (state.mode === "solid") {
655
698
  state.solid = hex;
699
+ state.alpha = alpha;
656
700
  } else if (state.gradient.stops[state.selected]) {
657
- state.gradient.stops[state.selected] = { ...state.gradient.stops[state.selected], color: hex };
701
+ state.gradient.stops[state.selected] = {
702
+ ...state.gradient.stops[state.selected],
703
+ color: formatColorAlpha(hex, alpha)
704
+ };
658
705
  }
659
706
  }
707
+ function setActiveColor(picker, hex) {
708
+ setActiveColorAlpha(picker, hex, activeAlpha(picker));
709
+ }
660
710
  function currentValue(picker) {
661
711
  const { state } = picker;
662
- return state.mode === "solid" ? { mode: "solid", color: state.solid } : { mode: "gradient", ...state.gradient };
712
+ return state.mode === "solid" ? { mode: "solid", color: state.solid, alpha: state.alpha } : { mode: "gradient", ...state.gradient };
663
713
  }
664
714
  function commit(picker) {
665
715
  picker.onCommit(formatPickerValue(currentValue(picker)));
@@ -674,6 +724,14 @@ function applyHex(picker, hex) {
674
724
  setActiveColor(picker, hex);
675
725
  picker.render();
676
726
  }
727
+ function applyAlpha(picker, raw) {
728
+ const value = Number(raw);
729
+ if (!Number.isFinite(value)) {
730
+ return;
731
+ }
732
+ setActiveColorAlpha(picker, activeHex(picker), Math.max(0, Math.min(100, value)) / 100);
733
+ picker.render();
734
+ }
677
735
  function applyAngle(picker, raw) {
678
736
  const value = Number(raw);
679
737
  if (!Number.isFinite(value)) {
@@ -688,7 +746,7 @@ function selectStop(picker, index) {
688
746
  return;
689
747
  }
690
748
  state.selected = index;
691
- state.hsv = hexToHsv(state.gradient.stops[index].color);
749
+ state.hsv = hexToHsv(hexOf(state.gradient.stops[index].color));
692
750
  picker.render();
693
751
  }
694
752
  function removeStop(picker, index) {
@@ -698,7 +756,7 @@ function removeStop(picker, index) {
698
756
  }
699
757
  state.gradient = { ...state.gradient, stops: state.gradient.stops.filter((_stop, at) => at !== index) };
700
758
  state.selected = Math.min(state.selected, state.gradient.stops.length - 1);
701
- state.hsv = hexToHsv(activeColor(picker));
759
+ state.hsv = hexToHsv(activeHex(picker));
702
760
  picker.render();
703
761
  commit(picker);
704
762
  }
@@ -711,12 +769,12 @@ function appendStop(picker) {
711
769
  const last = sorted[sorted.length - 1];
712
770
  const previous = sorted[sorted.length - 2];
713
771
  const inserted = {
714
- color: darken(last.color),
772
+ color: formatColorAlpha(darken(hexOf(last.color)), alphaOf(last.color)),
715
773
  position: clampPosition((last.position + (previous?.position ?? 0)) / 2)
716
774
  };
717
775
  state.gradient = { ...state.gradient, stops: sortStops([...state.gradient.stops, inserted]) };
718
776
  state.selected = state.gradient.stops.indexOf(inserted);
719
- state.hsv = hexToHsv(activeColor(picker));
777
+ state.hsv = hexToHsv(activeHex(picker));
720
778
  picker.render();
721
779
  commit(picker);
722
780
  }
@@ -732,8 +790,9 @@ function setMode(picker, next) {
732
790
  state.selected = 0;
733
791
  } else {
734
792
  state.solid = toSolid(value);
793
+ state.alpha = toAlpha(value);
735
794
  }
736
- state.hsv = hexToHsv(activeColor(picker));
795
+ state.hsv = hexToHsv(activeHex(picker));
737
796
  picker.render();
738
797
  commit(picker);
739
798
  }
@@ -766,12 +825,23 @@ function numberField(parent, min, max) {
766
825
  input.max = String(max);
767
826
  return input;
768
827
  }
769
- function rangeField(parent, min, max) {
770
- const input = el("input", "sf-cp-range", parent);
771
- input.type = "range";
772
- input.min = String(min);
773
- input.max = String(max);
774
- return input;
828
+ function fluidField(parent, text, min, max) {
829
+ const row = el("div", "sf-cp-fluid", parent);
830
+ el("span", "sf-cp-fluid-fill", row);
831
+ el("span", "sf-cp-fluid-thumb", row);
832
+ el("span", "sf-cp-fluid-label", row).textContent = text;
833
+ const range = el("input", "sf-cp-fluid-range", row);
834
+ range.type = "range";
835
+ range.min = String(min);
836
+ range.max = String(max);
837
+ const number = numberField(row, min, max);
838
+ number.classList.add("sf-cp-fluid-number");
839
+ return { row, range, number };
840
+ }
841
+ function showRatio(range) {
842
+ const span = Number(range.max) - Number(range.min);
843
+ const ratio2 = span > 0 ? (Number(range.value) - Number(range.min)) / span : 0;
844
+ range.parentElement?.style.setProperty("--sf-ratio", String(Math.max(0, Math.min(1, ratio2))));
775
845
  }
776
846
  function labelledRow(parent, text) {
777
847
  const row = el("div", "sf-cp-row", parent);
@@ -818,23 +888,36 @@ function buildSolidControls(root) {
818
888
  const channelRanges = {};
819
889
  const channelNumbers = {};
820
890
  for (const channel of CHANNELS) {
821
- const row = labelledRow(rows, channel.toUpperCase());
822
- channelRanges[channel] = rangeField(row, 0, 255);
823
- channelNumbers[channel] = numberField(row, 0, 255);
891
+ const field = fluidField(rows, channel.toUpperCase(), 0, 255);
892
+ channelRanges[channel] = field.range;
893
+ channelNumbers[channel] = field.number;
824
894
  }
895
+ const alpha = fluidField(rows, "A", 0, 100);
896
+ alpha.row.classList.add("sf-cp-fluid--alpha");
897
+ const { row: alphaRow, range: alphaRange, number: alphaNumber } = alpha;
825
898
  const hexRow = labelledRow(rows, "Hex");
826
899
  const hexInput = el("input", "sf-cp-text", hexRow);
827
900
  hexInput.type = "text";
828
901
  hexInput.spellcheck = false;
829
- return { matrix, matrixCursor, hue, hueThumb, channelRanges, channelNumbers, hexInput };
902
+ return {
903
+ matrix,
904
+ matrixCursor,
905
+ hue,
906
+ hueThumb,
907
+ channelRanges,
908
+ channelNumbers,
909
+ alphaRow,
910
+ alphaRange,
911
+ alphaNumber,
912
+ hexInput
913
+ };
830
914
  }
831
915
  function buildGradientControls(root, lockStops) {
832
916
  const section = el("div", "sf-cp-section", root);
833
917
  const preview = el("div", "sf-cp-preview", section);
834
- const angleRow = labelledRow(section, "Angle");
835
- angleRow.hidden = lockStops;
836
- const angleRange = rangeField(angleRow, 0, 360);
837
- const angleNumber = numberField(angleRow, 0, 360);
918
+ const angle = fluidField(section, "Angle", 0, 360);
919
+ angle.row.hidden = lockStops;
920
+ const { range: angleRange, number: angleNumber } = angle;
838
921
  const head = el("div", "sf-cp-section-head", section);
839
922
  el("span", "sf-cp-heading", head).textContent = "Stops";
840
923
  const addStop = el("button", "sf-cp-icon-btn", head);
@@ -916,7 +999,7 @@ function bindChannel(picker, channel) {
916
999
  if (!Number.isFinite(value)) {
917
1000
  return;
918
1001
  }
919
- const next = { ...hexToRgb(activeColor(picker)), [channel]: value };
1002
+ const next = { ...hexToRgb(activeHex(picker)), [channel]: value };
920
1003
  const nextHsv = rgbToHsv(next);
921
1004
  picker.state.hsv = nextHsv.s === 0 ? { ...nextHsv, h: picker.state.hsv.h } : nextHsv;
922
1005
  setActiveColor(picker, rgbToHex(next));
@@ -928,6 +1011,14 @@ function bindChannel(picker, channel) {
928
1011
  channelNumbers[channel].addEventListener("input", (event) => fromChannel(read(event)));
929
1012
  channelNumbers[channel].addEventListener("change", () => commit(picker));
930
1013
  }
1014
+ function bindAlpha(picker) {
1015
+ const { alphaRange, alphaNumber } = picker.view;
1016
+ const read = (event) => event.target.value;
1017
+ alphaRange.addEventListener("input", (event) => applyAlpha(picker, read(event)));
1018
+ alphaRange.addEventListener("change", () => commit(picker));
1019
+ alphaNumber.addEventListener("input", (event) => applyAlpha(picker, read(event)));
1020
+ alphaNumber.addEventListener("change", () => commit(picker));
1021
+ }
931
1022
  function bindHex(picker) {
932
1023
  const { hexInput } = picker.view;
933
1024
  hexInput.addEventListener("input", () => {
@@ -964,6 +1055,7 @@ function bindPickerEvents(picker) {
964
1055
  for (const channel of CHANNELS) {
965
1056
  bindChannel(picker, channel);
966
1057
  }
1058
+ bindAlpha(picker);
967
1059
  bindHex(picker);
968
1060
  bindAngle(picker);
969
1061
  return disposers;
@@ -1014,7 +1106,8 @@ function renderStops(picker) {
1014
1106
  const row = rows[index];
1015
1107
  row.root.setAttribute("aria-selected", String(index === state.selected));
1016
1108
  row.swatch.style.background = stop.color;
1017
- row.hex.textContent = stop.color;
1109
+ const alpha = alphaOf(stop.color);
1110
+ row.hex.textContent = alpha < 1 ? `${hexOf(stop.color)} ${Math.round(alpha * 100)}%` : hexOf(stop.color);
1018
1111
  if (document.activeElement !== row.position) {
1019
1112
  row.position.value = String(Math.round(stop.position));
1020
1113
  }
@@ -1029,7 +1122,7 @@ function renderPicker(picker) {
1029
1122
  view.solidTab.setAttribute("aria-selected", String(state.mode === "solid"));
1030
1123
  view.gradientTab.setAttribute("aria-selected", String(state.mode === "gradient"));
1031
1124
  view.section.hidden = state.mode !== "gradient";
1032
- const hex = activeColor(picker);
1125
+ const hex = activeHex(picker);
1033
1126
  const rgb = hexToRgb(hex);
1034
1127
  view.matrix.style.background = `hsl(${state.hsv.h}, 100%, 50%)`;
1035
1128
  view.matrixCursor.style.left = `${state.hsv.s * 100}%`;
@@ -1037,15 +1130,25 @@ function renderPicker(picker) {
1037
1130
  view.hueThumb.style.top = `${state.hsv.h / 360 * 100}%`;
1038
1131
  for (const channel of CHANNELS) {
1039
1132
  view.channelRanges[channel].value = String(rgb[channel]);
1133
+ showRatio(view.channelRanges[channel]);
1040
1134
  if (document.activeElement !== view.channelNumbers[channel]) {
1041
1135
  view.channelNumbers[channel].value = String(rgb[channel]);
1042
1136
  }
1043
1137
  }
1138
+ const percent = Math.round(activeAlpha(picker) * 100);
1139
+ view.alphaRange.value = String(percent);
1140
+ showRatio(view.alphaRange);
1141
+ if (document.activeElement !== view.alphaNumber) {
1142
+ view.alphaNumber.value = String(percent);
1143
+ }
1144
+ const opaque = `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`;
1145
+ view.alphaRow.style.background = `linear-gradient(to right, rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0), ${opaque}), conic-gradient(var(--sf-cp-check-a) 25%, var(--sf-cp-check-b) 0 50%, var(--sf-cp-check-a) 0 75%, var(--sf-cp-check-b) 0) 0 0 / 8px 8px`;
1044
1146
  if (document.activeElement !== view.hexInput) {
1045
1147
  view.hexInput.value = hex;
1046
1148
  }
1047
1149
  if (state.mode === "gradient") {
1048
1150
  view.angleRange.value = String(Math.round(state.gradient.angle));
1151
+ showRatio(view.angleRange);
1049
1152
  if (document.activeElement !== view.angleNumber) {
1050
1153
  view.angleNumber.value = String(Math.round(state.gradient.angle));
1051
1154
  }
@@ -1061,8 +1164,8 @@ var DARK_TOKENS = `
1061
1164
  --sf-cp-shadow: 0 8px 24px rgba(0, 0, 0, 0.6);
1062
1165
  --sf-cp-fg: rgba(255, 255, 255, 0.92);
1063
1166
  --sf-cp-muted: rgba(255, 255, 255, 0.6);
1064
- --sf-cp-tabs-bg: rgba(255, 255, 255, 0.04);
1065
- --sf-cp-tab-active-bg: rgba(255, 255, 255, 0.12);
1167
+ --sf-cp-tabs-bg: rgba(255, 255, 255, 0.05);
1168
+ --sf-cp-tab-active-bg: rgba(255, 255, 255, 0.1);
1066
1169
  --sf-cp-field-bg: rgba(255, 255, 255, 0.04);
1067
1170
  --sf-cp-field-border: rgba(255, 255, 255, 0.08);
1068
1171
  --sf-cp-row-bg: rgba(255, 255, 255, 0.04);
@@ -1071,6 +1174,9 @@ var DARK_TOKENS = `
1071
1174
  --sf-cp-thumb-bg: #e8e8e8;
1072
1175
  --sf-cp-thumb-border: #555;
1073
1176
  --sf-cp-danger: #ff7a77;
1177
+ --sf-cp-accent: rgba(255, 255, 255, 0.85);
1178
+ --sf-cp-check-a: #3b3b3f;
1179
+ --sf-cp-check-b: #2a2a2d;
1074
1180
  `;
1075
1181
  var BASE_CSS = `
1076
1182
  .sf-cp {
@@ -1079,7 +1185,7 @@ var BASE_CSS = `
1079
1185
  --sf-cp-shadow: 0 8px 24px rgba(15, 23, 42, 0.16);
1080
1186
  --sf-cp-fg: #333333;
1081
1187
  --sf-cp-muted: #666666;
1082
- --sf-cp-tabs-bg: #f5f5f5;
1188
+ --sf-cp-tabs-bg: #f1f5f9;
1083
1189
  --sf-cp-tab-active-bg: #ffffff;
1084
1190
  --sf-cp-field-bg: #f8f8f8;
1085
1191
  --sf-cp-field-border: #e0e0e0;
@@ -1089,16 +1195,18 @@ var BASE_CSS = `
1089
1195
  --sf-cp-thumb-bg: #ffffff;
1090
1196
  --sf-cp-thumb-border: #666666;
1091
1197
  --sf-cp-danger: #e53935;
1092
- --sf-cp-accent: #9b42df;
1198
+ --sf-cp-accent: #0f172a;
1199
+ --sf-cp-check-a: #c8c8c8;
1200
+ --sf-cp-check-b: #ffffff;
1093
1201
 
1094
1202
  position: fixed;
1095
1203
  z-index: 10000;
1096
1204
  box-sizing: border-box;
1097
1205
  width: 268px;
1098
- padding: 12px;
1206
+ padding: 10px;
1099
1207
  display: flex;
1100
1208
  flex-direction: column;
1101
- gap: 14px;
1209
+ gap: 10px;
1102
1210
  border: 1px solid var(--sf-cp-border);
1103
1211
  border-radius: 12px;
1104
1212
  background: var(--sf-cp-bg);
@@ -1126,7 +1234,7 @@ var FIELDS_CSS = `
1126
1234
  .sf-cp-rows {
1127
1235
  display: flex;
1128
1236
  flex-direction: column;
1129
- gap: 8px;
1237
+ gap: 5px;
1130
1238
  }
1131
1239
 
1132
1240
  .sf-cp-row {
@@ -1142,44 +1250,114 @@ var FIELDS_CSS = `
1142
1250
  font-weight: 600;
1143
1251
  }
1144
1252
 
1145
- .sf-cp-range {
1146
- flex: 1;
1147
- min-width: 0;
1148
- height: 6px;
1149
- border-radius: 3px;
1150
- outline: none;
1151
- background: var(--sf-cp-track);
1152
- -webkit-appearance: none;
1153
- appearance: none;
1253
+ .sf-cp-fluid {
1254
+ --sf-ratio: 0;
1255
+ position: relative;
1256
+ display: flex;
1257
+ align-items: center;
1258
+ justify-content: space-between;
1259
+ gap: 10px;
1260
+ height: 26px;
1261
+ padding: 0 8px;
1262
+ border-radius: 8px;
1263
+ background: var(--sf-cp-row-bg);
1264
+ overflow: hidden;
1265
+ user-select: none;
1266
+ }
1267
+
1268
+ .sf-cp-fluid[hidden] {
1269
+ display: none;
1270
+ }
1271
+
1272
+ .sf-cp-fluid-fill {
1273
+ position: absolute;
1274
+ inset: 0 auto 0 0;
1275
+ width: calc(var(--sf-ratio) * 100%);
1276
+ border-radius: 8px;
1277
+ background: linear-gradient(90deg, color-mix(in srgb, currentColor 5%, transparent), color-mix(in srgb, currentColor 18%, transparent));
1278
+ pointer-events: none;
1279
+ }
1280
+
1281
+ .sf-cp-fluid--alpha .sf-cp-fluid-fill {
1282
+ display: none;
1283
+ }
1284
+
1285
+ .sf-cp-fluid-thumb {
1286
+ position: absolute;
1287
+ top: 50%;
1288
+ left: calc(var(--sf-ratio) * 100% - var(--sf-ratio) * 5px);
1289
+ width: 5px;
1290
+ height: 14px;
1291
+ border-radius: 999px;
1292
+ background: var(--sf-cp-fg);
1293
+ box-shadow: 0 0 0 1px var(--sf-cp-bg);
1294
+ transform: translateY(-50%);
1295
+ pointer-events: none;
1296
+ transition: height 180ms cubic-bezier(0.22, 1, 0.36, 1), box-shadow 220ms ease;
1297
+ }
1298
+
1299
+ .sf-cp-fluid:hover .sf-cp-fluid-thumb,
1300
+ .sf-cp-fluid:focus-within .sf-cp-fluid-thumb {
1301
+ height: 20px;
1302
+ }
1303
+
1304
+ .sf-cp-fluid:has(.sf-cp-fluid-range:active) .sf-cp-fluid-thumb {
1305
+ height: 26px;
1306
+ box-shadow: 0 0 0 1px var(--sf-cp-bg), 0 0 0 5px color-mix(in srgb, var(--sf-cp-fg) 15%, transparent);
1307
+ }
1308
+
1309
+ .sf-cp-fluid-label {
1310
+ position: relative;
1311
+ color: var(--sf-cp-muted);
1312
+ font-size: 11px;
1313
+ font-weight: 600;
1314
+ pointer-events: none;
1315
+ }
1316
+
1317
+ .sf-cp-fluid-range {
1318
+ position: absolute;
1319
+ inset: 0;
1320
+ width: 100%;
1321
+ height: 100%;
1322
+ margin: 0;
1323
+ opacity: 0;
1324
+ cursor: ew-resize;
1325
+ }
1326
+
1327
+ .sf-cp-number.sf-cp-fluid-number {
1328
+ position: relative;
1329
+ z-index: 2;
1330
+ width: 44px;
1331
+ padding: 1px 6px;
1332
+ border: none;
1333
+ background: transparent;
1334
+ text-align: right;
1335
+ font-weight: 600;
1336
+ font-variant-numeric: tabular-nums;
1337
+ -moz-appearance: textfield;
1338
+ appearance: textfield;
1154
1339
  }
1155
1340
 
1156
- .sf-cp-range::-webkit-slider-thumb {
1341
+ .sf-cp-fluid-number::-webkit-inner-spin-button,
1342
+ .sf-cp-fluid-number::-webkit-outer-spin-button {
1157
1343
  -webkit-appearance: none;
1158
- appearance: none;
1159
- width: 15px;
1160
- height: 15px;
1161
- border: 2px solid var(--sf-cp-thumb-border);
1162
- border-radius: 50%;
1163
- background: var(--sf-cp-thumb-bg);
1164
- cursor: pointer;
1165
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
1344
+ margin: 0;
1166
1345
  }
1167
1346
 
1168
- .sf-cp-range::-moz-range-thumb {
1169
- width: 15px;
1170
- height: 15px;
1171
- border: 2px solid var(--sf-cp-thumb-border);
1172
- border-radius: 50%;
1173
- background: var(--sf-cp-thumb-bg);
1174
- cursor: pointer;
1175
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
1347
+ .sf-cp-fluid-number:hover {
1348
+ background: var(--sf-cp-hover);
1349
+ }
1350
+
1351
+ .sf-cp-number.sf-cp-fluid-number:focus {
1352
+ background: var(--sf-cp-field-bg);
1353
+ box-shadow: 0 0 0 1px var(--sf-cp-accent);
1176
1354
  }
1177
1355
 
1178
1356
  .sf-cp-number,
1179
1357
  .sf-cp-text {
1180
1358
  flex: none;
1181
1359
  width: 52px;
1182
- padding: 4px 8px;
1360
+ padding: 3px 8px;
1183
1361
  border: 1px solid var(--sf-cp-field-border);
1184
1362
  border-radius: 6px;
1185
1363
  background: var(--sf-cp-field-bg);
@@ -1369,8 +1547,9 @@ var GRADIENT_CSS = `
1369
1547
  var TABS_AND_AREA_CSS = `
1370
1548
  .sf-cp-tabs {
1371
1549
  display: flex;
1372
- gap: 4px;
1373
- padding: 4px;
1550
+ gap: 2px;
1551
+ height: 28px;
1552
+ padding: 2px;
1374
1553
  border-radius: 8px;
1375
1554
  background: var(--sf-cp-tabs-bg);
1376
1555
  }
@@ -1381,12 +1560,13 @@ var TABS_AND_AREA_CSS = `
1381
1560
 
1382
1561
  .sf-cp-tab {
1383
1562
  flex: 1;
1384
- padding: 5px 12px;
1563
+ padding: 0 12px;
1385
1564
  border: none;
1386
1565
  border-radius: 6px;
1387
1566
  background: transparent;
1388
1567
  color: var(--sf-cp-muted);
1389
1568
  font: inherit;
1569
+ font-size: 11px;
1390
1570
  font-weight: 500;
1391
1571
  cursor: pointer;
1392
1572
  transition: background 0.15s ease, color 0.15s ease;
@@ -1399,8 +1579,8 @@ var TABS_AND_AREA_CSS = `
1399
1579
 
1400
1580
  .sf-cp-tab[aria-selected="true"] {
1401
1581
  background: var(--sf-cp-tab-active-bg);
1402
- color: var(--sf-cp-accent);
1403
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
1582
+ color: var(--sf-cp-fg);
1583
+ box-shadow: 0 1px 2px rgba(15, 23, 42, 0.08);
1404
1584
  }
1405
1585
 
1406
1586
  .sf-cp-area {
@@ -1501,6 +1681,7 @@ function openColorPopover(options) {
1501
1681
  state: {
1502
1682
  mode: allowGradient ? seed.mode : "solid",
1503
1683
  solid,
1684
+ alpha: toAlpha(seed),
1504
1685
  gradient: toGradient(seed),
1505
1686
  selected: 0,
1506
1687
  hsv: hexToHsv(solid)
@@ -3649,6 +3830,43 @@ function bindShapeMovement(viewer, emitter) {
3649
3830
  };
3650
3831
  }
3651
3832
 
3833
+ // src/runtime/interaction/nudgeSelection.ts
3834
+ var STEP = 1;
3835
+ var SHIFT_STEP = 10;
3836
+ var DIRECTIONS = {
3837
+ ArrowLeft: { x: -1, y: 0 },
3838
+ ArrowRight: { x: 1, y: 0 },
3839
+ ArrowUp: { x: 0, y: -1 },
3840
+ ArrowDown: { x: 0, y: 1 }
3841
+ };
3842
+ function typingInField(event) {
3843
+ return event.target instanceof Element && event.target.closest('input, textarea, [contenteditable="true"]') !== null;
3844
+ }
3845
+ function bindNudgeSelection(viewer, emitter) {
3846
+ const svg = viewer.svg;
3847
+ const onKeyDown = (e) => {
3848
+ const direction = DIRECTIONS[e.key];
3849
+ if (!direction || e.altKey || e.ctrlKey || e.metaKey || typingInField(e)) {
3850
+ return;
3851
+ }
3852
+ const owners = viewer.getSelection().filter(hasMovableGeometry);
3853
+ if (!owners.length) {
3854
+ return;
3855
+ }
3856
+ e.preventDefault();
3857
+ const step = e.shiftKey ? SHIFT_STEP : STEP;
3858
+ for (const owner of owners) {
3859
+ emitter.emit("moveChange", { owner, dx: direction.x * step, dy: direction.y * step });
3860
+ }
3861
+ };
3862
+ svg.addEventListener("keydown", onKeyDown);
3863
+ return {
3864
+ destroy() {
3865
+ svg.removeEventListener("keydown", onKeyDown);
3866
+ }
3867
+ };
3868
+ }
3869
+
3652
3870
  // src/runtime/interaction/pathDraw/geometry.ts
3653
3871
  var MIN_POINTS_TO_CLOSE = 3;
3654
3872
  var MIN_POINTS_OPEN = 2;
@@ -4940,7 +5158,7 @@ function updateRadarVertexDragPreview(svg, drag, value) {
4940
5158
  x: Number(vertex.getAttribute("cx")),
4941
5159
  y: Number(vertex.getAttribute("cy"))
4942
5160
  }));
4943
- const path = radarSeriesPath(points, drag.frame.curved);
5161
+ const path = radarSeriesPath(points, drag.frame.curved, drag.frame.tension);
4944
5162
  for (const shape of seriesShapes(svg, drag.owner.id, drag.series)) {
4945
5163
  shape.setAttribute("d", path);
4946
5164
  }
@@ -4976,11 +5194,20 @@ function parseRadarFrame(node) {
4976
5194
  if (!raw) {
4977
5195
  return null;
4978
5196
  }
4979
- const [centerX, centerY, radius, min, max, count, curved] = raw.split(",").map(Number);
5197
+ const [centerX, centerY, radius, min, max, count, curved, tension] = raw.split(",").map(Number);
4980
5198
  if (![centerX, centerY, radius, min, max, count].every(Number.isFinite) || count < 3 || max === min) {
4981
5199
  return null;
4982
5200
  }
4983
- return { centerX, centerY, radius, min, max, count, curved: curved === 1 };
5201
+ return {
5202
+ centerX,
5203
+ centerY,
5204
+ radius,
5205
+ min,
5206
+ max,
5207
+ count,
5208
+ curved: curved === 1,
5209
+ tension: Number.isFinite(tension) ? tension : void 0
5210
+ };
4984
5211
  }
4985
5212
  function findVertex(event) {
4986
5213
  return event.target?.closest?.('[data-sf-mark="radar-vertex"]');
@@ -7059,6 +7286,257 @@ function shouldQuote(current, quote, indexed) {
7059
7286
  return indexed && trimmed === "none";
7060
7287
  }
7061
7288
 
7289
+ // src/runtime/interaction/sflPatch/connectionPatch/blockText.ts
7290
+ function buildConnectionBlock(options, baseIndent) {
7291
+ const bodyIndent = `${baseIndent} `;
7292
+ const block = [
7293
+ `${baseIndent}Connection ${options.id} {`,
7294
+ `${bodyIndent}from: ${options.fromId}`,
7295
+ `${bodyIndent}to: ${options.toId}`,
7296
+ `${bodyIndent}fromFace: "${options.fromFace}"`,
7297
+ `${bodyIndent}toFace: "${options.toFace}"`
7298
+ ];
7299
+ if (options.body) {
7300
+ block.push(...indentBlock(options.body, bodyIndent));
7301
+ }
7302
+ block.push(`${baseIndent}}`);
7303
+ return block;
7304
+ }
7305
+
7306
+ // src/runtime/interaction/sflPatch/connectionPatch/insertionTarget.ts
7307
+ function resolveConnectionInsertionTarget(sfl, lines, options) {
7308
+ if (!VALID_IDENT.test(options.id) || nameExists(lines, options.id)) {
7309
+ return null;
7310
+ }
7311
+ const canvasObjects = sflToCanvasState(sfl).canvasObjects;
7312
+ const hasSource = canvasObjects.some((canvasObject) => canvasObject.id === options.fromId);
7313
+ const hasTarget = canvasObjects.some((canvasObject) => canvasObject.id === options.toId);
7314
+ if (!hasSource || !hasTarget) {
7315
+ return null;
7316
+ }
7317
+ return findFirstBlockOfKind(lines, "Model");
7318
+ }
7319
+
7320
+ // src/runtime/interaction/sflPatch/connectionPatch/insert.ts
7321
+ function insertConnection(sfl, options) {
7322
+ const lines = sfl.split("\n");
7323
+ const modelRange = resolveConnectionInsertionTarget(sfl, lines, options);
7324
+ if (!modelRange) {
7325
+ return sfl;
7326
+ }
7327
+ insertBlockLines(lines, modelRange, (baseIndent) => buildConnectionBlock(options, baseIndent));
7328
+ return lines.join("\n");
7329
+ }
7330
+
7331
+ // src/runtime/interaction/sflPatch/changes/quickDuplicate/styleBlock.ts
7332
+ var QUOTED_KEYS = /* @__PURE__ */ new Set(["fill", "stroke", "textColor", "fontFamily", "shadow", "labelBackground"]);
7333
+ function styleLine(key, value) {
7334
+ return QUOTED_KEYS.has(key) ? ` ${key}: "${value.replace(/"/g, '\\"')}"` : ` ${key}: ${value}`;
7335
+ }
7336
+ function styleBlock(props, keys, leading = []) {
7337
+ const copied = keys.flatMap((key) => {
7338
+ const value = props?.[key];
7339
+ return value === void 0 ? [] : [styleLine(key, value)];
7340
+ });
7341
+ const lines = [...leading, ...copied];
7342
+ return lines.length > 0 ? `style {
7343
+ ${lines.join("\n")}
7344
+ }` : void 0;
7345
+ }
7346
+
7347
+ // src/runtime/interaction/sflPatch/changes/quickDuplicate/isometricDuplicate.ts
7348
+ var ISOMETRIC_GAP = 40;
7349
+ var ISOMETRIC_STYLE_KEYS = [
7350
+ "fill",
7351
+ "stroke",
7352
+ "strokeWidth",
7353
+ "cornerRadius",
7354
+ "shadow",
7355
+ "labelOffsetX",
7356
+ "labelOffsetY",
7357
+ "labelBackground"
7358
+ ];
7359
+ var TOUCHING_FACES = {
7360
+ "iso-width-pos": { fromFace: "right", toFace: "left" },
7361
+ "iso-width-neg": { fromFace: "left", toFace: "right" },
7362
+ "iso-depth-pos": { fromFace: "front", toFace: "back" },
7363
+ "iso-depth-neg": { fromFace: "back", toFace: "front" },
7364
+ "iso-height-pos": { fromFace: "bottom", toFace: "top" },
7365
+ "iso-height-neg": { fromFace: "top", toFace: "bottom" }
7366
+ };
7367
+ function isIsometricDirection2(direction) {
7368
+ return direction.startsWith("iso-");
7369
+ }
7370
+ function isometricOffset(direction, object, gap) {
7371
+ const { width, height } = object;
7372
+ const { depth } = object.isometric;
7373
+ switch (direction) {
7374
+ case "iso-width-pos":
7375
+ return { dx: (width + gap) * AXIS_COS, dy: (width + gap) * AXIS_SIN };
7376
+ case "iso-width-neg":
7377
+ return { dx: -(width + gap) * AXIS_COS, dy: -(width + gap) * AXIS_SIN };
7378
+ case "iso-depth-pos":
7379
+ return { dx: -(depth + gap) * AXIS_COS, dy: (depth + gap) * AXIS_SIN };
7380
+ case "iso-depth-neg":
7381
+ return { dx: (depth + gap) * AXIS_COS, dy: -(depth + gap) * AXIS_SIN };
7382
+ case "iso-height-pos":
7383
+ return { dx: 0, dy: height + gap };
7384
+ case "iso-height-neg":
7385
+ return { dx: 0, dy: -(height + gap) };
7386
+ }
7387
+ }
7388
+ function copyStyle(object, direction) {
7389
+ if (!direction.startsWith("iso-height-")) {
7390
+ return styleBlock(object.props, ISOMETRIC_STYLE_KEYS);
7391
+ }
7392
+ return styleBlock(
7393
+ object.props,
7394
+ ISOMETRIC_STYLE_KEYS.filter((key) => key !== "shadow"),
7395
+ [' shadow: "off"']
7396
+ );
7397
+ }
7398
+ function copyOf(object, direction, options) {
7399
+ const { dx, dy } = isometricOffset(direction, object, options.gap ?? ISOMETRIC_GAP);
7400
+ return {
7401
+ id: options.shapeId,
7402
+ kind: "isometric",
7403
+ text: object.text ?? "",
7404
+ x: object.x + dx,
7405
+ y: object.y + dy,
7406
+ width: object.width,
7407
+ height: object.height,
7408
+ depth: object.isometric.depth,
7409
+ parentId: object.parentFrameId,
7410
+ body: copyStyle(object, direction)
7411
+ };
7412
+ }
7413
+ function duplicateIsometricObject(sfl, object, direction, options) {
7414
+ const withCopy = insertCanvasObject(sfl, copyOf(object, direction, options));
7415
+ return insertConnection(withCopy, {
7416
+ id: options.linkId,
7417
+ fromId: object.id,
7418
+ toId: options.shapeId,
7419
+ ...TOUCHING_FACES[direction]
7420
+ });
7421
+ }
7422
+
7423
+ // src/runtime/interaction/sflPatch/linkPatch/blockText.ts
7424
+ function buildLinkBlock(options, baseIndent) {
7425
+ const bodyIndent = `${baseIndent} `;
7426
+ const from = formatLinkEndpoint({ id: options.fromId, side: options.fromSide, fraction: options.fromFraction });
7427
+ const to = formatLinkEndpoint({ id: options.toId, side: options.toSide, fraction: options.toFraction });
7428
+ const block = [`${baseIndent}Link ${options.id} {`, `${bodyIndent}from: ${from}`, `${bodyIndent}to: ${to}`];
7429
+ if (options.text !== void 0) {
7430
+ block.push(`${bodyIndent}text: "${escapeSflString(options.text)}"`);
7431
+ }
7432
+ if (options.body) {
7433
+ block.push(...indentBlock(options.body, bodyIndent));
7434
+ }
7435
+ block.push(`${baseIndent}}`);
7436
+ return block;
7437
+ }
7438
+
7439
+ // src/runtime/interaction/sflPatch/linkPatch/insertionTarget.ts
7440
+ function resolveLinkInsertionTarget(sfl, lines, options) {
7441
+ if (!VALID_IDENT.test(options.id) || nameExists(lines, options.id)) {
7442
+ return null;
7443
+ }
7444
+ const canvasObjects = sflToCanvasState(sfl).canvasObjects;
7445
+ const hasSource = canvasObjects.some((canvasObject) => canvasObject.id === options.fromId);
7446
+ const hasTarget = canvasObjects.some((canvasObject) => canvasObject.id === options.toId);
7447
+ if (!hasSource || !hasTarget) {
7448
+ return null;
7449
+ }
7450
+ return findFirstBlockOfKind(lines, "Model");
7451
+ }
7452
+
7453
+ // src/runtime/interaction/sflPatch/linkPatch/insert.ts
7454
+ function insertLink(sfl, options) {
7455
+ const lines = sfl.split("\n");
7456
+ const modelRange = resolveLinkInsertionTarget(sfl, lines, options);
7457
+ if (!modelRange) {
7458
+ return sfl;
7459
+ }
7460
+ insertBlockLines(lines, modelRange, (baseIndent) => buildLinkBlock(options, baseIndent));
7461
+ return lines.join("\n");
7462
+ }
7463
+
7464
+ // src/runtime/interaction/sflPatch/changes/quickDuplicate/shapeDuplicate.ts
7465
+ var SHAPE_GAP = 200;
7466
+ var OPPOSITE_SIDE = { top: "bottom", right: "left", bottom: "top", left: "right" };
7467
+ var SHAPE_STYLE_KEYS = [
7468
+ "fill",
7469
+ "stroke",
7470
+ "strokeWidth",
7471
+ "strokeStyle",
7472
+ "textColor",
7473
+ "fontSize",
7474
+ "fontFamily",
7475
+ "cornerRadius",
7476
+ "opacity",
7477
+ "shadow"
7478
+ ];
7479
+ var LINK_STYLE_KEYS = ["stroke", "strokeWidth", "strokeStyle", "markerStart", "markerEnd", "route"];
7480
+ function shapeOffset(side, object, gap) {
7481
+ switch (side) {
7482
+ case "top":
7483
+ return { dx: 0, dy: -(object.height + gap) };
7484
+ case "bottom":
7485
+ return { dx: 0, dy: object.height + gap };
7486
+ case "left":
7487
+ return { dx: -(object.width + gap), dy: 0 };
7488
+ case "right":
7489
+ return { dx: object.width + gap, dy: 0 };
7490
+ }
7491
+ }
7492
+ function copyOf2(object, side, options) {
7493
+ const { dx, dy } = shapeOffset(side, object, options.gap ?? SHAPE_GAP);
7494
+ const shape = object.shapeType ? [` shape: ${object.shapeType}`] : [];
7495
+ return {
7496
+ id: options.shapeId,
7497
+ kind: "shape",
7498
+ text: object.text ?? "",
7499
+ x: object.x + dx,
7500
+ y: object.y + dy,
7501
+ width: object.width,
7502
+ height: object.height,
7503
+ parentId: object.parentFrameId,
7504
+ body: styleBlock(object.props, SHAPE_STYLE_KEYS, shape)
7505
+ };
7506
+ }
7507
+ function linkStyleReference(links, objectId) {
7508
+ return links.find((link) => link.fromId === objectId) ?? links[0];
7509
+ }
7510
+ function linkToCopy(object, links, side, options) {
7511
+ return {
7512
+ id: options.linkId,
7513
+ fromId: object.id,
7514
+ toId: options.shapeId,
7515
+ fromSide: side,
7516
+ toSide: OPPOSITE_SIDE[side],
7517
+ body: styleBlock(linkStyleReference(links, object.id)?.props, LINK_STYLE_KEYS)
7518
+ };
7519
+ }
7520
+ function duplicateShape(sfl, input) {
7521
+ const { object, links, side, options } = input;
7522
+ const withCopy = insertCanvasObject(sfl, copyOf2(object, side, options));
7523
+ return insertLink(withCopy, linkToCopy(object, links, side, options));
7524
+ }
7525
+
7526
+ // src/runtime/interaction/sflPatch/changes/quickDuplicateChange.ts
7527
+ function applyQuickDuplicateChange(sfl, event, options) {
7528
+ const state = sflToCanvasState(sfl);
7529
+ const object = state.canvasObjects.find((candidate) => candidate.id === event.owner.id);
7530
+ if (!object) {
7531
+ return sfl;
7532
+ }
7533
+ const { direction } = event;
7534
+ if (isIsometricDirection2(direction)) {
7535
+ return isIsometricObject(object) ? duplicateIsometricObject(sfl, object, direction, options) : sfl;
7536
+ }
7537
+ return isIsometricObject(object) ? sfl : duplicateShape(sfl, { object, links: state.links, side: direction, options });
7538
+ }
7539
+
7062
7540
  // src/runtime/interaction/sflPatch/changes/resizeChange.ts
7063
7541
  function applyResizeChange(sfl, event) {
7064
7542
  const state = sflToCanvasState(sfl);
@@ -7183,89 +7661,6 @@ function applyTextChange(sfl, event) {
7183
7661
  return lines.join("\n");
7184
7662
  }
7185
7663
 
7186
- // src/runtime/interaction/sflPatch/connectionPatch/blockText.ts
7187
- function buildConnectionBlock(options, baseIndent) {
7188
- const bodyIndent = `${baseIndent} `;
7189
- const block = [
7190
- `${baseIndent}Connection ${options.id} {`,
7191
- `${bodyIndent}from: ${options.fromId}`,
7192
- `${bodyIndent}to: ${options.toId}`,
7193
- `${bodyIndent}fromFace: "${options.fromFace}"`,
7194
- `${bodyIndent}toFace: "${options.toFace}"`
7195
- ];
7196
- if (options.body) {
7197
- block.push(...indentBlock(options.body, bodyIndent));
7198
- }
7199
- block.push(`${baseIndent}}`);
7200
- return block;
7201
- }
7202
-
7203
- // src/runtime/interaction/sflPatch/connectionPatch/insertionTarget.ts
7204
- function resolveConnectionInsertionTarget(sfl, lines, options) {
7205
- if (!VALID_IDENT.test(options.id) || nameExists(lines, options.id)) {
7206
- return null;
7207
- }
7208
- const canvasObjects = sflToCanvasState(sfl).canvasObjects;
7209
- const hasSource = canvasObjects.some((canvasObject) => canvasObject.id === options.fromId);
7210
- const hasTarget = canvasObjects.some((canvasObject) => canvasObject.id === options.toId);
7211
- if (!hasSource || !hasTarget) {
7212
- return null;
7213
- }
7214
- return findFirstBlockOfKind(lines, "Model");
7215
- }
7216
-
7217
- // src/runtime/interaction/sflPatch/connectionPatch/insert.ts
7218
- function insertConnection(sfl, options) {
7219
- const lines = sfl.split("\n");
7220
- const modelRange = resolveConnectionInsertionTarget(sfl, lines, options);
7221
- if (!modelRange) {
7222
- return sfl;
7223
- }
7224
- insertBlockLines(lines, modelRange, (baseIndent) => buildConnectionBlock(options, baseIndent));
7225
- return lines.join("\n");
7226
- }
7227
-
7228
- // src/runtime/interaction/sflPatch/linkPatch/blockText.ts
7229
- function buildLinkBlock(options, baseIndent) {
7230
- const bodyIndent = `${baseIndent} `;
7231
- const from = formatLinkEndpoint({ id: options.fromId, side: options.fromSide, fraction: options.fromFraction });
7232
- const to = formatLinkEndpoint({ id: options.toId, side: options.toSide, fraction: options.toFraction });
7233
- const block = [`${baseIndent}Link ${options.id} {`, `${bodyIndent}from: ${from}`, `${bodyIndent}to: ${to}`];
7234
- if (options.text !== void 0) {
7235
- block.push(`${bodyIndent}text: "${escapeSflString(options.text)}"`);
7236
- }
7237
- if (options.body) {
7238
- block.push(...indentBlock(options.body, bodyIndent));
7239
- }
7240
- block.push(`${baseIndent}}`);
7241
- return block;
7242
- }
7243
-
7244
- // src/runtime/interaction/sflPatch/linkPatch/insertionTarget.ts
7245
- function resolveLinkInsertionTarget(sfl, lines, options) {
7246
- if (!VALID_IDENT.test(options.id) || nameExists(lines, options.id)) {
7247
- return null;
7248
- }
7249
- const canvasObjects = sflToCanvasState(sfl).canvasObjects;
7250
- const hasSource = canvasObjects.some((canvasObject) => canvasObject.id === options.fromId);
7251
- const hasTarget = canvasObjects.some((canvasObject) => canvasObject.id === options.toId);
7252
- if (!hasSource || !hasTarget) {
7253
- return null;
7254
- }
7255
- return findFirstBlockOfKind(lines, "Model");
7256
- }
7257
-
7258
- // src/runtime/interaction/sflPatch/linkPatch/insert.ts
7259
- function insertLink(sfl, options) {
7260
- const lines = sfl.split("\n");
7261
- const modelRange = resolveLinkInsertionTarget(sfl, lines, options);
7262
- if (!modelRange) {
7263
- return sfl;
7264
- }
7265
- insertBlockLines(lines, modelRange, (baseIndent) => buildLinkBlock(options, baseIndent));
7266
- return lines.join("\n");
7267
- }
7268
-
7269
7664
  // src/runtime/interaction/sflPatch/linkPatch/remove.ts
7270
7665
  function removeLink(sfl, id) {
7271
7666
  const lines = sfl.split("\n");
@@ -7291,6 +7686,7 @@ function bindEditing(viewer, options = {}) {
7291
7686
  const quickDuplicate = bindQuickDuplicate(viewer, emitter);
7292
7687
  const isometricQuickDuplicate = bindIsometricQuickDuplicate(viewer, emitter);
7293
7688
  const deleteSelection = bindDeleteSelection(viewer, emitter);
7689
+ const nudgeSelection = bindNudgeSelection(viewer, emitter);
7294
7690
  const chartValues = bindChartValueDrag(viewer, emitter);
7295
7691
  const pieLabels = bindPieLabelDrag(viewer, emitter);
7296
7692
  const isometricLabels = bindIsometricLabelDrag(viewer, emitter);
@@ -7315,6 +7711,7 @@ function bindEditing(viewer, options = {}) {
7315
7711
  quickDuplicate.destroy();
7316
7712
  isometricQuickDuplicate.destroy();
7317
7713
  deleteSelection.destroy();
7714
+ nudgeSelection.destroy();
7318
7715
  chartValues.destroy();
7319
7716
  pieLabels.destroy();
7320
7717
  isometricLabels.destroy();
@@ -7328,6 +7725,6 @@ function bindEditing(viewer, options = {}) {
7328
7725
  };
7329
7726
  }
7330
7727
 
7331
- export { applyChartValueChange, applyLinkReconnectChange, applyLinkSegmentChange, applyMoveChange, applyPathPointChange, applyPropertyChange, applyResizeChange, applySankeyNodeMoveChange, applySankeyNodeRenameChange, applyTextChange, bindEditing, bindPropertyEditing, cssBackground, formatPickerValue, insertCanvasObject, insertConnection, insertLink, insertPath, openColorPopover, readGradientValue, removeCanvasObject, removeLink, resolveTheme, seedFrom };
7728
+ export { applyChartValueChange, applyLinkReconnectChange, applyLinkSegmentChange, applyMoveChange, applyPathPointChange, applyPropertyChange, applyQuickDuplicateChange, applyResizeChange, applySankeyNodeMoveChange, applySankeyNodeRenameChange, applyTextChange, bindEditing, bindPropertyEditing, cssBackground, formatPickerValue, insertCanvasObject, insertConnection, insertLink, insertPath, openColorPopover, readGradientValue, removeCanvasObject, removeLink, resolveTheme, seedFrom };
7332
7729
  //# sourceMappingURL=index.js.map
7333
7730
  //# sourceMappingURL=index.js.map