@octaviaflow/core 3.0.18-beta.22 → 3.0.18-beta.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/workflow.js CHANGED
@@ -619,12 +619,20 @@ function ConfigPanel({
619
619
 
620
620
  // src/workflow/components/FxPanel/FxPanel.tsx
621
621
  import {
622
+ useEffect as useEffect3,
622
623
  useMemo as useMemo2,
624
+ useRef as useRef4,
623
625
  useState as useState4
624
626
  } from "react";
625
627
  import {
628
+ CheckmarkIcon,
629
+ ChevronDownIcon,
626
630
  ChevronRightIcon,
627
631
  CloseIcon,
632
+ CollapseAllIcon,
633
+ CopyIcon,
634
+ DraggableIcon,
635
+ ExpandAllIcon,
628
636
  SearchIcon
629
637
  } from "@octaviaflow/icons";
630
638
  import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
@@ -632,13 +640,48 @@ var KIND_GLYPH = {
632
640
  function: "\u0192",
633
641
  variable: "\u2B21"
634
642
  };
643
+ function countLeaves(items) {
644
+ let n = 0;
645
+ for (const it of items) {
646
+ if (it.children && it.children.length > 0) n += countLeaves(it.children);
647
+ else n += 1;
648
+ }
649
+ return n;
650
+ }
651
+ function collectBranchIds(items, out) {
652
+ for (const it of items) {
653
+ if (it.children && it.children.length > 0) {
654
+ out.add(it.id);
655
+ collectBranchIds(it.children, out);
656
+ }
657
+ }
658
+ }
659
+ function flattenTree(items, depth, expandedItems, out) {
660
+ for (const it of items) {
661
+ const hasChildren = !!it.children && it.children.length > 0;
662
+ const expanded = hasChildren && expandedItems.has(it.id);
663
+ out.push({ item: it, depth, hasChildren, expanded });
664
+ if (expanded) flattenTree(it.children, depth + 1, expandedItems, out);
665
+ }
666
+ }
667
+ function collectHits(items, ancestors, q, out) {
668
+ for (const it of items) {
669
+ if (it.children && it.children.length > 0) {
670
+ collectHits(it.children, [...ancestors, it.label], q, out);
671
+ } else {
672
+ const path = [...ancestors, it.label].join(" / ");
673
+ const hay = `${it.label} ${it.description ?? ""} ${path} ${it.insertValue ?? ""}`.toLowerCase();
674
+ if (hay.includes(q)) out.push({ item: it, ancestors });
675
+ }
676
+ }
677
+ }
635
678
  function FxPanel({
636
679
  categories,
637
680
  title = "FX / IO",
638
681
  hint = /* @__PURE__ */ jsxs2(Fragment2, { children: [
639
- "Drag items into any input field with the ",
682
+ "Drag a field into any input with the ",
640
683
  /* @__PURE__ */ jsx2("strong", { children: "FX" }),
641
- " indicator"
684
+ " badge, or click to insert."
642
685
  ] }),
643
686
  search: controlledSearch,
644
687
  defaultSearch = "",
@@ -649,44 +692,85 @@ function FxPanel({
649
692
  onClose,
650
693
  onItemDragStart,
651
694
  onItemSelect,
652
- width = 292,
695
+ width = 300,
653
696
  emptyLabel = "No matches",
654
- searchPlaceholder = "Search functions & variables\u2026",
697
+ searchPlaceholder = "Search fields, functions & variables\u2026",
655
698
  className,
656
699
  style
657
700
  }) {
658
701
  const [internalSearch, setInternalSearch] = useState4(defaultSearch);
659
702
  const search = controlledSearch ?? internalSearch;
703
+ const query = search.trim().toLowerCase();
660
704
  const setSearch = (next) => {
661
705
  if (controlledSearch === void 0) setInternalSearch(next);
662
706
  onSearchChange?.(next);
663
707
  };
664
- const [internalExpanded, setInternalExpanded] = useState4(
665
- defaultExpandedCategory !== void 0 ? defaultExpandedCategory : categories.find((c) => c.items.length > 0)?.id ?? null
708
+ const visibleCategories = useMemo2(
709
+ () => categories.filter((c) => c.items.length > 0),
710
+ [categories]
711
+ );
712
+ const [internalCat, setInternalCat] = useState4(
713
+ defaultExpandedCategory !== void 0 ? defaultExpandedCategory : visibleCategories[0]?.id ?? null
666
714
  );
667
- const expanded = controlledExpanded ?? internalExpanded;
668
- const setExpanded = (id) => {
669
- if (controlledExpanded === void 0) setInternalExpanded(id);
715
+ const openCategoryId = controlledExpanded ?? internalCat;
716
+ const setOpenCategory = (id) => {
717
+ if (controlledExpanded === void 0) setInternalCat(id);
670
718
  onExpandedCategoryChange?.(id);
671
719
  };
720
+ const [expandedItems, setExpandedItems] = useState4(() => {
721
+ const ids = /* @__PURE__ */ new Set();
722
+ for (const cat of categories) {
723
+ for (const item of cat.items) {
724
+ if (item.children && item.children.length > 0) ids.add(item.id);
725
+ }
726
+ }
727
+ return ids;
728
+ });
729
+ const toggleItem = (id) => {
730
+ setExpandedItems((prev) => {
731
+ const next = new Set(prev);
732
+ if (next.has(id)) next.delete(id);
733
+ else next.add(id);
734
+ return next;
735
+ });
736
+ };
737
+ const setCategoryExpansion = (cat, expand) => {
738
+ const branchIds = /* @__PURE__ */ new Set();
739
+ collectBranchIds(cat.items, branchIds);
740
+ setExpandedItems((prev) => {
741
+ const next = new Set(prev);
742
+ for (const id of branchIds) {
743
+ if (expand) next.add(id);
744
+ else next.delete(id);
745
+ }
746
+ return next;
747
+ });
748
+ };
672
749
  const [draggingId, setDraggingId] = useState4(null);
673
- const filtered = useMemo2(() => {
674
- const q = search.trim().toLowerCase();
675
- const matched = q ? categories.map((cat) => {
676
- if (cat.label.toLowerCase().includes(q)) return cat;
677
- const items = cat.items.filter(
678
- (it) => it.label.toLowerCase().includes(q) || (it.description?.toLowerCase().includes(q) ?? false)
679
- );
680
- return { ...cat, items };
681
- }) : categories;
682
- return matched.filter((cat) => cat.items.length > 0);
683
- }, [categories, search]);
684
- const hasResults = filtered.length > 0;
750
+ const [copiedId, setCopiedId] = useState4(null);
751
+ const [focusedId, setFocusedId] = useState4(null);
752
+ const copyTimer = useRef4(null);
753
+ useEffect3(
754
+ () => () => {
755
+ if (copyTimer.current) clearTimeout(copyTimer.current);
756
+ },
757
+ []
758
+ );
759
+ const hits = useMemo2(() => {
760
+ if (!query) return null;
761
+ const all = [];
762
+ for (const cat of visibleCategories) {
763
+ const out = [];
764
+ collectHits(cat.items, [], query, out);
765
+ for (const hit of out) all.push({ category: cat, hit });
766
+ }
767
+ return all;
768
+ }, [query, visibleCategories]);
685
769
  const handleDragStart = (item, category) => (e) => {
686
770
  setDraggingId(item.id);
687
771
  if (onItemDragStart) {
688
772
  onItemDragStart(item, category, e);
689
- } else {
773
+ } else if (item.insertValue) {
690
774
  e.dataTransfer.setData("text/plain", item.insertValue);
691
775
  e.dataTransfer.setData("application/x-fx-insert", item.insertValue);
692
776
  e.dataTransfer.setData(
@@ -696,6 +780,144 @@ function FxPanel({
696
780
  e.dataTransfer.effectAllowed = "copy";
697
781
  }
698
782
  };
783
+ const handleCopy = (item) => {
784
+ if (!item.insertValue || typeof navigator === "undefined") return;
785
+ void navigator.clipboard?.writeText(item.insertValue);
786
+ setCopiedId(item.id);
787
+ if (copyTimer.current) clearTimeout(copyTimer.current);
788
+ copyTimer.current = setTimeout(() => setCopiedId(null), 1400);
789
+ };
790
+ const listRef = useRef4(null);
791
+ const focusRow = (id) => {
792
+ setFocusedId(id);
793
+ requestAnimationFrame(() => {
794
+ listRef.current?.querySelector(`[data-fx-row="${CSS.escape(id)}"]`)?.focus();
795
+ });
796
+ };
797
+ const onRowKeyDown = (e, rows, row) => {
798
+ const idx = rows.findIndex((r) => r.item.id === row.item.id);
799
+ if (idx < 0) return;
800
+ switch (e.key) {
801
+ case "ArrowDown":
802
+ e.preventDefault();
803
+ if (idx < rows.length - 1) focusRow(rows[idx + 1].item.id);
804
+ break;
805
+ case "ArrowUp":
806
+ e.preventDefault();
807
+ if (idx > 0) focusRow(rows[idx - 1].item.id);
808
+ break;
809
+ case "ArrowRight":
810
+ if (row.hasChildren) {
811
+ e.preventDefault();
812
+ if (!row.expanded) toggleItem(row.item.id);
813
+ else if (idx < rows.length - 1) focusRow(rows[idx + 1].item.id);
814
+ }
815
+ break;
816
+ case "ArrowLeft":
817
+ if (row.hasChildren && row.expanded) {
818
+ e.preventDefault();
819
+ toggleItem(row.item.id);
820
+ } else {
821
+ for (let i = idx - 1; i >= 0; i--) {
822
+ if (rows[i].depth < row.depth) {
823
+ e.preventDefault();
824
+ focusRow(rows[i].item.id);
825
+ break;
826
+ }
827
+ }
828
+ }
829
+ break;
830
+ case "Home":
831
+ e.preventDefault();
832
+ if (rows.length) focusRow(rows[0].item.id);
833
+ break;
834
+ case "End":
835
+ e.preventDefault();
836
+ if (rows.length) focusRow(rows[rows.length - 1].item.id);
837
+ break;
838
+ case "Enter":
839
+ case " ":
840
+ e.preventDefault();
841
+ if (row.hasChildren) toggleItem(row.item.id);
842
+ else {
843
+ const cat = visibleCategories.find((c) => c.id === openCategoryId);
844
+ if (cat) onItemSelect?.(row.item, cat);
845
+ }
846
+ break;
847
+ default:
848
+ break;
849
+ }
850
+ };
851
+ const renderRow = (row, category, rows, opts = {}) => {
852
+ const { item, depth, hasChildren, expanded } = row;
853
+ const mono = category.kind === "function";
854
+ const draggable = !!item.insertValue;
855
+ const isFocused = focusedId === item.id;
856
+ return /* @__PURE__ */ jsxs2(
857
+ "div",
858
+ {
859
+ "data-fx-row": item.id,
860
+ className: cn(
861
+ "ods-flow-fx-panel__row",
862
+ hasChildren ? "ods-flow-fx-panel__row--branch" : "ods-flow-fx-panel__row--leaf",
863
+ draggingId === item.id && "ods-flow-fx-panel__row--dragging",
864
+ isFocused && "ods-flow-fx-panel__row--focused"
865
+ ),
866
+ role: "treeitem",
867
+ "aria-expanded": hasChildren ? expanded : void 0,
868
+ "aria-level": depth + 1,
869
+ tabIndex: isFocused || focusedId === null && rows[0]?.item.id === item.id ? 0 : -1,
870
+ draggable,
871
+ onDragStart: draggable ? handleDragStart(item, category) : void 0,
872
+ onDragEnd: () => setDraggingId(null),
873
+ onFocus: () => setFocusedId(item.id),
874
+ onKeyDown: opts.searching ? void 0 : (e) => onRowKeyDown(e, rows, row),
875
+ onClick: () => {
876
+ if (hasChildren) toggleItem(item.id);
877
+ else onItemSelect?.(item, category);
878
+ },
879
+ title: item.insertValue,
880
+ children: [
881
+ Array.from({ length: depth }, (_, i) => /* @__PURE__ */ jsx2("span", { className: "ods-flow-fx-panel__indent", "aria-hidden": "true" }, i)),
882
+ hasChildren ? /* @__PURE__ */ jsx2("span", { className: "ods-flow-fx-panel__twisty", "aria-hidden": "true", children: expanded ? /* @__PURE__ */ jsx2(ChevronDownIcon, { size: "sm" }) : /* @__PURE__ */ jsx2(ChevronRightIcon, { size: "sm" }) }) : /* @__PURE__ */ jsx2("span", { className: "ods-flow-fx-panel__grip", "aria-hidden": "true", children: /* @__PURE__ */ jsx2(DraggableIcon, { size: "sm" }) }),
883
+ /* @__PURE__ */ jsxs2("span", { className: "ods-flow-fx-panel__row-main", children: [
884
+ /* @__PURE__ */ jsxs2("span", { className: "ods-flow-fx-panel__row-line", children: [
885
+ mono ? /* @__PURE__ */ jsx2("code", { className: "ods-flow-fx-panel__row-label ods-flow-fx-panel__row-label--mono", children: item.label }) : /* @__PURE__ */ jsx2("span", { className: "ods-flow-fx-panel__row-label", children: item.label }),
886
+ item.valueType && /* @__PURE__ */ jsx2(
887
+ "span",
888
+ {
889
+ className: cn(
890
+ "ods-flow-fx-panel__type",
891
+ `ods-flow-fx-panel__type--${item.valueType}`
892
+ ),
893
+ children: item.valueType
894
+ }
895
+ ),
896
+ item.preview !== void 0 && /* @__PURE__ */ jsx2("span", { className: "ods-flow-fx-panel__preview", title: item.preview, children: item.preview })
897
+ ] }),
898
+ (item.description || opts.crumb) && /* @__PURE__ */ jsxs2("span", { className: "ods-flow-fx-panel__row-sub", children: [
899
+ opts.crumb && /* @__PURE__ */ jsx2("span", { className: "ods-flow-fx-panel__crumb", children: opts.crumb }),
900
+ item.description
901
+ ] })
902
+ ] }),
903
+ item.insertValue && /* @__PURE__ */ jsx2(
904
+ "button",
905
+ {
906
+ type: "button",
907
+ className: "ods-flow-fx-panel__copy",
908
+ "aria-label": `Copy ${item.label}`,
909
+ onClick: (e) => {
910
+ e.stopPropagation();
911
+ handleCopy(item);
912
+ },
913
+ children: copiedId === item.id ? /* @__PURE__ */ jsx2(CheckmarkIcon, { size: "sm" }) : /* @__PURE__ */ jsx2(CopyIcon, { size: "sm" })
914
+ }
915
+ )
916
+ ]
917
+ },
918
+ item.id
919
+ );
920
+ };
699
921
  return /* @__PURE__ */ jsxs2(
700
922
  "aside",
701
923
  {
@@ -724,7 +946,7 @@ function FxPanel({
724
946
  type: "text",
725
947
  className: "ods-flow-fx-panel__search-input",
726
948
  placeholder: searchPlaceholder,
727
- "aria-label": "Search functions and variables",
949
+ "aria-label": "Search fields, functions and variables",
728
950
  value: search,
729
951
  onChange: (e) => setSearch(e.target.value)
730
952
  }
@@ -740,76 +962,116 @@ function FxPanel({
740
962
  }
741
963
  )
742
964
  ] }),
743
- hint && /* @__PURE__ */ jsx2("p", { className: "ods-flow-fx-panel__hint", children: hint }),
744
- /* @__PURE__ */ jsxs2("div", { className: "ods-flow-fx-panel__list", children: [
745
- filtered.map((cat) => {
746
- const isOpen = expanded === cat.id;
965
+ hint && !query && /* @__PURE__ */ jsx2("p", { className: "ods-flow-fx-panel__hint", children: hint }),
966
+ /* @__PURE__ */ jsxs2("div", { className: "ods-flow-fx-panel__list", ref: listRef, role: "tree", children: [
967
+ query && hits && (hits.length > 0 ? /* @__PURE__ */ jsxs2("div", { className: "ods-flow-fx-panel__results", children: [
968
+ /* @__PURE__ */ jsxs2("div", { className: "ods-flow-fx-panel__results-count", children: [
969
+ hits.length,
970
+ " ",
971
+ hits.length === 1 ? "match" : "matches"
972
+ ] }),
973
+ hits.map(
974
+ ({ category, hit }) => renderRow(
975
+ { item: hit.item, depth: 0, hasChildren: false, expanded: false },
976
+ category,
977
+ hits.map((h) => ({
978
+ item: h.hit.item,
979
+ depth: 0,
980
+ hasChildren: false,
981
+ expanded: false
982
+ })),
983
+ {
984
+ searching: true,
985
+ crumb: hit.ancestors.length ? hit.ancestors.join(" / ") : category.label
986
+ }
987
+ )
988
+ )
989
+ ] }) : /* @__PURE__ */ jsx2("div", { className: "ods-flow-fx-panel__empty", children: emptyLabel })),
990
+ !query && visibleCategories.map((cat) => {
991
+ const isOpen = openCategoryId === cat.id;
747
992
  const kind = cat.kind ?? "function";
993
+ const branchIds = /* @__PURE__ */ new Set();
994
+ collectBranchIds(cat.items, branchIds);
995
+ const hasTree = branchIds.size > 0;
996
+ const rows = [];
997
+ if (isOpen) flattenTree(cat.items, 0, expandedItems, rows);
998
+ const allExpanded = hasTree && [...branchIds].every((id) => expandedItems.has(id));
748
999
  return /* @__PURE__ */ jsxs2("div", { className: "ods-flow-fx-panel__category", children: [
749
1000
  /* @__PURE__ */ jsxs2(
750
- "button",
1001
+ "div",
751
1002
  {
752
- type: "button",
753
- className: "ods-flow-fx-panel__category-header",
754
- "aria-expanded": isOpen,
755
- onClick: () => setExpanded(isOpen ? null : cat.id),
1003
+ className: cn(
1004
+ "ods-flow-fx-panel__cat-header",
1005
+ isOpen && "ods-flow-fx-panel__cat-header--open"
1006
+ ),
756
1007
  children: [
757
- /* @__PURE__ */ jsx2(
758
- ChevronRightIcon,
759
- {
760
- size: "sm",
761
- className: cn(
762
- "ods-flow-fx-panel__chevron",
763
- isOpen && "ods-flow-fx-panel__chevron--open"
764
- )
765
- }
766
- ),
767
1008
  /* @__PURE__ */ jsxs2(
768
- "span",
1009
+ "button",
769
1010
  {
770
- className: cn(
771
- "ods-flow-fx-panel__badge",
772
- `ods-flow-fx-panel__badge--${kind}`
773
- ),
1011
+ type: "button",
1012
+ className: "ods-flow-fx-panel__cat-toggle",
1013
+ "aria-expanded": isOpen,
1014
+ onClick: () => setOpenCategory(isOpen ? null : cat.id),
774
1015
  children: [
775
1016
  /* @__PURE__ */ jsx2(
1017
+ ChevronRightIcon,
1018
+ {
1019
+ size: "sm",
1020
+ className: cn(
1021
+ "ods-flow-fx-panel__chevron",
1022
+ isOpen && "ods-flow-fx-panel__chevron--open"
1023
+ )
1024
+ }
1025
+ ),
1026
+ /* @__PURE__ */ jsxs2(
776
1027
  "span",
777
1028
  {
778
- className: "ods-flow-fx-panel__badge-glyph",
779
- "aria-hidden": "true",
780
- children: KIND_GLYPH[kind]
1029
+ className: cn(
1030
+ "ods-flow-fx-panel__badge",
1031
+ `ods-flow-fx-panel__badge--${kind}`
1032
+ ),
1033
+ children: [
1034
+ /* @__PURE__ */ jsx2(
1035
+ "span",
1036
+ {
1037
+ className: "ods-flow-fx-panel__badge-glyph",
1038
+ "aria-hidden": "true",
1039
+ children: KIND_GLYPH[kind]
1040
+ }
1041
+ ),
1042
+ cat.label
1043
+ ]
781
1044
  }
782
1045
  ),
783
- cat.label
1046
+ /* @__PURE__ */ jsx2("span", { className: "ods-flow-fx-panel__count", children: countLeaves(cat.items) })
784
1047
  ]
785
1048
  }
786
1049
  ),
787
- /* @__PURE__ */ jsx2("span", { className: "ods-flow-fx-panel__count", children: cat.items.length })
1050
+ isOpen && hasTree && /* @__PURE__ */ jsx2(
1051
+ "button",
1052
+ {
1053
+ type: "button",
1054
+ className: "ods-flow-fx-panel__cat-action",
1055
+ "aria-label": allExpanded ? "Collapse all" : "Expand all",
1056
+ title: allExpanded ? "Collapse all" : "Expand all",
1057
+ onClick: () => setCategoryExpansion(cat, !allExpanded),
1058
+ children: allExpanded ? /* @__PURE__ */ jsx2(CollapseAllIcon, { size: "sm" }) : /* @__PURE__ */ jsx2(ExpandAllIcon, { size: "sm" })
1059
+ }
1060
+ )
788
1061
  ]
789
1062
  }
790
1063
  ),
791
- isOpen && /* @__PURE__ */ jsx2("div", { className: "ods-flow-fx-panel__items", children: cat.items.map((item) => /* @__PURE__ */ jsxs2(
1064
+ isOpen && /* @__PURE__ */ jsx2(
792
1065
  "div",
793
1066
  {
794
- className: cn(
795
- "ods-flow-fx-panel__item",
796
- draggingId === item.id && "ods-flow-fx-panel__item--dragging"
797
- ),
798
- draggable: true,
799
- title: item.insertValue,
800
- onDragStart: handleDragStart(item, cat),
801
- onDragEnd: () => setDraggingId(null),
802
- onClick: onItemSelect ? () => onItemSelect(item, cat) : void 0,
803
- children: [
804
- /* @__PURE__ */ jsx2("code", { className: "ods-flow-fx-panel__item-label", children: item.label }),
805
- item.description && /* @__PURE__ */ jsx2("span", { className: "ods-flow-fx-panel__item-desc", children: item.description })
806
- ]
807
- },
808
- item.id
809
- )) })
1067
+ className: "ods-flow-fx-panel__rows",
1068
+ role: "group",
1069
+ "aria-label": cat.label,
1070
+ children: rows.map((row) => renderRow(row, cat, rows))
1071
+ }
1072
+ )
810
1073
  ] }, cat.id);
811
- }),
812
- !hasResults && /* @__PURE__ */ jsx2("div", { className: "ods-flow-fx-panel__empty", children: emptyLabel })
1074
+ })
813
1075
  ] })
814
1076
  ]
815
1077
  }