@lv-x-software-house/x_view 1.2.5-dev.17 → 1.2.5-dev.18

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/index.js CHANGED
@@ -619,29 +619,26 @@ function XViewSidebar({
619
619
  const [isFilterMenuOpen, setIsFilterMenuOpen] = (0, import_react2.useState)(false);
620
620
  const [filterCategory, setFilterCategory] = (0, import_react2.useState)("Type");
621
621
  const [filterValue, setFilterValue] = (0, import_react2.useState)("");
622
+ const [isTypeDropdownOpen, setIsTypeDropdownOpen] = (0, import_react2.useState)(false);
623
+ const [highlightedTypeIndex, setHighlightedTypeIndex] = (0, import_react2.useState)(-1);
622
624
  const containerRef = (0, import_react2.useRef)(null);
623
625
  const inputRef = (0, import_react2.useRef)(null);
624
626
  const filterMenuRef = (0, import_react2.useRef)(null);
627
+ const typeDropdownRef = (0, import_react2.useRef)(null);
625
628
  const ability = (0, import_react2.useMemo)(() => {
626
629
  return defineAbilityFor(userRole);
627
630
  }, [userRole]);
628
631
  const contextLabel = (0, import_react2.useMemo)(() => {
629
632
  if (!viewType || !viewName) return null;
630
- console.log("XViewSidebar: contextLabel viewType", viewType);
631
- try {
632
- const typeLower = viewType.toLowerCase();
633
- if (typeLower === "database") {
634
- return `Dataset: ${viewName}`;
635
- } else if (typeLower === "view") {
636
- return `View: ${viewName}`;
637
- } else if (typeLower === "node") {
638
- return `Node: ${viewName}`;
639
- }
640
- return `${viewType}: ${viewName}`;
641
- } catch (err) {
642
- console.error("XViewSidebar: Erro no contextLabel ao converter viewType:", viewType, err);
643
- return `${viewType}: ${viewName}`;
644
- }
633
+ const typeLower = String(viewType).toLowerCase();
634
+ if (typeLower === "database") {
635
+ return `Dataset: ${viewName}`;
636
+ } else if (typeLower === "view") {
637
+ return `View: ${viewName}`;
638
+ } else if (typeLower === "node") {
639
+ return `Node: ${viewName}`;
640
+ }
641
+ return `${viewType}: ${viewName}`;
645
642
  }, [viewType, viewName]);
646
643
  const normalize = (str = "") => {
647
644
  if (str === void 0 || str === null) {
@@ -654,7 +651,6 @@ function XViewSidebar({
654
651
  []
655
652
  );
656
653
  const availableTypes = (0, import_react2.useMemo)(() => {
657
- console.log("XViewSidebar: recalculando availableTypes, dbNodes count:", dbNodes == null ? void 0 : dbNodes.length);
658
654
  const typesSet = /* @__PURE__ */ new Set();
659
655
  (dbNodes || []).forEach((node) => {
660
656
  if (Array.isArray(node.type)) {
@@ -665,10 +661,13 @@ function XViewSidebar({
665
661
  typesSet.add(node.type);
666
662
  }
667
663
  });
668
- const result = Array.from(typesSet).sort();
669
- console.log("XViewSidebar: availableTypes final:", result);
670
- return result;
664
+ return Array.from(typesSet).sort();
671
665
  }, [dbNodes]);
666
+ const filteredTypes = (0, import_react2.useMemo)(() => {
667
+ if (!filterValue.trim()) return availableTypes;
668
+ const search = filterValue.toLowerCase().trim();
669
+ return availableTypes.filter((t) => t.toLowerCase().includes(search));
670
+ }, [availableTypes, filterValue]);
672
671
  const filteredAndSorted = (0, import_react2.useMemo)(() => {
673
672
  const base = Array.isArray(dbNodes) ? dbNodes : [];
674
673
  let pool = base;
@@ -681,22 +680,13 @@ function XViewSidebar({
681
680
  });
682
681
  }
683
682
  if (activeFilters.length > 0) {
684
- console.log("XViewSidebar: aplicando activeFilters:", activeFilters);
685
683
  pool = pool.filter((node) => {
686
684
  return activeFilters.every((filter) => {
687
- try {
688
- if (filter.type === "Type") {
689
- const nodeTypes = Array.isArray(node.type) ? node.type : [node.type];
690
- return nodeTypes.some((t) => {
691
- if (t === void 0) console.log("XViewSidebar: t \xE9 undefined no node:", node.id);
692
- if (filter.value === void 0) console.log("XViewSidebar: filter.value \xE9 undefined");
693
- return String(t).toLowerCase() === String(filter.value).toLowerCase();
694
- });
695
- } else if (filter.type === "Color") {
696
- return String(node.color).toLowerCase() === String(filter.value).toLowerCase();
697
- }
698
- } catch (err) {
699
- console.error("XViewSidebar: Erro ao filtrar node:", node, "com filtro:", filter, err);
685
+ if (filter.type === "Type") {
686
+ const nodeTypes = Array.isArray(node.type) ? node.type : [node.type];
687
+ return nodeTypes.some((t) => String(t).toLowerCase() === String(filter.value).toLowerCase());
688
+ } else if (filter.type === "Color") {
689
+ return String(node.color).toLowerCase() === String(filter.value).toLowerCase();
700
690
  }
701
691
  return true;
702
692
  });
@@ -718,12 +708,15 @@ function XViewSidebar({
718
708
  if (filterMenuRef.current && !filterMenuRef.current.contains(event.target)) {
719
709
  setIsFilterMenuOpen(false);
720
710
  }
711
+ if (typeDropdownRef.current && !typeDropdownRef.current.contains(event.target)) {
712
+ setIsTypeDropdownOpen(false);
713
+ }
721
714
  };
722
- if (isFilterMenuOpen) {
715
+ if (isFilterMenuOpen || isTypeDropdownOpen) {
723
716
  document.addEventListener("mousedown", handleClickOutside);
724
717
  }
725
718
  return () => document.removeEventListener("mousedown", handleClickOutside);
726
- }, [isFilterMenuOpen]);
719
+ }, [isFilterMenuOpen, isTypeDropdownOpen]);
727
720
  (0, import_react2.useEffect)(() => {
728
721
  if (!query) setSelectedNodeId(null);
729
722
  }, [query]);
@@ -777,6 +770,35 @@ function XViewSidebar({
777
770
  const handleRemoveFilter = (index) => {
778
771
  setActiveFilters((prev) => prev.filter((_, i) => i !== index));
779
772
  };
773
+ const handleTypeKeyDown = (e) => {
774
+ if (!isTypeDropdownOpen) {
775
+ setIsTypeDropdownOpen(true);
776
+ }
777
+ if (e.key === "ArrowDown") {
778
+ e.preventDefault();
779
+ setHighlightedTypeIndex(
780
+ (prev) => prev < filteredTypes.length - 1 ? prev + 1 : prev
781
+ );
782
+ } else if (e.key === "ArrowUp") {
783
+ e.preventDefault();
784
+ setHighlightedTypeIndex((prev) => prev > 0 ? prev - 1 : 0);
785
+ } else if (e.key === "Enter") {
786
+ e.preventDefault();
787
+ if (highlightedTypeIndex >= 0 && filteredTypes[highlightedTypeIndex]) {
788
+ setFilterValue(filteredTypes[highlightedTypeIndex]);
789
+ setIsTypeDropdownOpen(false);
790
+ setHighlightedTypeIndex(-1);
791
+ } else {
792
+ handleAddFilter();
793
+ }
794
+ } else if (e.key === "Escape") {
795
+ e.preventDefault();
796
+ setIsTypeDropdownOpen(false);
797
+ setHighlightedTypeIndex(-1);
798
+ } else if (e.key === "Tab") {
799
+ setIsTypeDropdownOpen(false);
800
+ }
801
+ };
780
802
  const ToggleButton = /* @__PURE__ */ import_react2.default.createElement(
781
803
  "button",
782
804
  {
@@ -907,18 +929,36 @@ function XViewSidebar({
907
929
  className: `flex-1 text-xs py-1 rounded transition-colors ${filterCategory === opt ? "bg-indigo-600 text-white shadow-sm" : "text-slate-400 hover:text-slate-200"}`
908
930
  },
909
931
  opt
910
- )))), /* @__PURE__ */ import_react2.default.createElement("div", { className: "space-y-1" }, /* @__PURE__ */ import_react2.default.createElement("label", { className: "text-[10px] uppercase text-slate-400 font-semibold tracking-wider" }, "Valor"), filterCategory === "Type" ? /* @__PURE__ */ import_react2.default.createElement("div", { className: "relative" }, /* @__PURE__ */ import_react2.default.createElement(
932
+ )))), /* @__PURE__ */ import_react2.default.createElement("div", { className: "space-y-1" }, /* @__PURE__ */ import_react2.default.createElement("label", { className: "text-[10px] uppercase text-slate-400 font-semibold tracking-wider" }, "Valor"), filterCategory === "Type" ? /* @__PURE__ */ import_react2.default.createElement("div", { className: "relative", ref: typeDropdownRef }, /* @__PURE__ */ import_react2.default.createElement(
911
933
  "input",
912
934
  {
913
- list: "typeOptions",
914
935
  type: "text",
915
936
  value: filterValue,
916
- onChange: (e) => setFilterValue(e.target.value),
937
+ onChange: (e) => {
938
+ setFilterValue(e.target.value);
939
+ if (!isTypeDropdownOpen) setIsTypeDropdownOpen(true);
940
+ setHighlightedTypeIndex(-1);
941
+ },
942
+ onFocus: () => setIsTypeDropdownOpen(true),
943
+ onKeyDown: handleTypeKeyDown,
917
944
  placeholder: "Ex: Concept",
918
945
  className: "w-full bg-slate-800 p-2 text-xs rounded border border-white/10 focus:outline-none focus:border-indigo-500/50 text-white",
919
946
  autoFocus: true
920
947
  }
921
- ), /* @__PURE__ */ import_react2.default.createElement("datalist", { id: "typeOptions" }, availableTypes.map((t) => /* @__PURE__ */ import_react2.default.createElement("option", { key: t, value: t })))) : /* @__PURE__ */ import_react2.default.createElement("div", { className: "relative flex items-center" }, /* @__PURE__ */ import_react2.default.createElement(
948
+ ), isTypeDropdownOpen && filteredTypes.length > 0 && /* @__PURE__ */ import_react2.default.createElement("div", { className: "absolute z-[60] left-0 right-0 mt-1 max-h-48 overflow-y-auto bg-slate-900/95 backdrop-blur-xl border border-white/10 rounded-md shadow-2xl custom-scrollbar" }, filteredTypes.map((t, idx) => /* @__PURE__ */ import_react2.default.createElement(
949
+ "div",
950
+ {
951
+ key: t,
952
+ className: `px-3 py-2 text-xs cursor-pointer transition-colors ${idx === highlightedTypeIndex ? "bg-indigo-600 text-white" : "text-slate-300 hover:bg-white/10"}`,
953
+ onMouseDown: (e) => {
954
+ e.preventDefault();
955
+ setFilterValue(t);
956
+ setIsTypeDropdownOpen(false);
957
+ setHighlightedTypeIndex(-1);
958
+ }
959
+ },
960
+ t
961
+ )))) : /* @__PURE__ */ import_react2.default.createElement("div", { className: "relative flex items-center" }, /* @__PURE__ */ import_react2.default.createElement(
922
962
  "div",
923
963
  {
924
964
  className: "absolute left-2 w-3 h-3 rounded-full border border-white/20 shadow-sm",
@@ -7688,7 +7728,7 @@ function ColorPicker({ color, onChange, disabled }) {
7688
7728
  style: { backgroundColor: preset },
7689
7729
  title: preset
7690
7730
  },
7691
- color.toLowerCase() === preset.toLowerCase() && /* @__PURE__ */ import_react13.default.createElement(import_fi11.FiCheck, { className: `drop-shadow-md ${["#ffffff", "#4df5cb", "#84cc16", "#f59e0b"].includes(preset) ? "text-black" : "text-white"}`, size: 12 })
7731
+ (color || "").toLowerCase() === (preset || "").toLowerCase() && /* @__PURE__ */ import_react13.default.createElement(import_fi11.FiCheck, { className: `drop-shadow-md ${["#ffffff", "#4df5cb", "#84cc16", "#f59e0b"].includes(preset) ? "text-black" : "text-white"}`, size: 12 })
7692
7732
  )))), /* @__PURE__ */ import_react13.default.createElement("style", null, `
7693
7733
  .custom-react-colorful .react-colorful {
7694
7734
  width: 100%;
package/dist/index.mjs CHANGED
@@ -580,29 +580,26 @@ function XViewSidebar({
580
580
  const [isFilterMenuOpen, setIsFilterMenuOpen] = useState2(false);
581
581
  const [filterCategory, setFilterCategory] = useState2("Type");
582
582
  const [filterValue, setFilterValue] = useState2("");
583
+ const [isTypeDropdownOpen, setIsTypeDropdownOpen] = useState2(false);
584
+ const [highlightedTypeIndex, setHighlightedTypeIndex] = useState2(-1);
583
585
  const containerRef = useRef2(null);
584
586
  const inputRef = useRef2(null);
585
587
  const filterMenuRef = useRef2(null);
588
+ const typeDropdownRef = useRef2(null);
586
589
  const ability = useMemo2(() => {
587
590
  return defineAbilityFor(userRole);
588
591
  }, [userRole]);
589
592
  const contextLabel = useMemo2(() => {
590
593
  if (!viewType || !viewName) return null;
591
- console.log("XViewSidebar: contextLabel viewType", viewType);
592
- try {
593
- const typeLower = viewType.toLowerCase();
594
- if (typeLower === "database") {
595
- return `Dataset: ${viewName}`;
596
- } else if (typeLower === "view") {
597
- return `View: ${viewName}`;
598
- } else if (typeLower === "node") {
599
- return `Node: ${viewName}`;
600
- }
601
- return `${viewType}: ${viewName}`;
602
- } catch (err) {
603
- console.error("XViewSidebar: Erro no contextLabel ao converter viewType:", viewType, err);
604
- return `${viewType}: ${viewName}`;
605
- }
594
+ const typeLower = String(viewType).toLowerCase();
595
+ if (typeLower === "database") {
596
+ return `Dataset: ${viewName}`;
597
+ } else if (typeLower === "view") {
598
+ return `View: ${viewName}`;
599
+ } else if (typeLower === "node") {
600
+ return `Node: ${viewName}`;
601
+ }
602
+ return `${viewType}: ${viewName}`;
606
603
  }, [viewType, viewName]);
607
604
  const normalize = (str = "") => {
608
605
  if (str === void 0 || str === null) {
@@ -615,7 +612,6 @@ function XViewSidebar({
615
612
  []
616
613
  );
617
614
  const availableTypes = useMemo2(() => {
618
- console.log("XViewSidebar: recalculando availableTypes, dbNodes count:", dbNodes == null ? void 0 : dbNodes.length);
619
615
  const typesSet = /* @__PURE__ */ new Set();
620
616
  (dbNodes || []).forEach((node) => {
621
617
  if (Array.isArray(node.type)) {
@@ -626,10 +622,13 @@ function XViewSidebar({
626
622
  typesSet.add(node.type);
627
623
  }
628
624
  });
629
- const result = Array.from(typesSet).sort();
630
- console.log("XViewSidebar: availableTypes final:", result);
631
- return result;
625
+ return Array.from(typesSet).sort();
632
626
  }, [dbNodes]);
627
+ const filteredTypes = useMemo2(() => {
628
+ if (!filterValue.trim()) return availableTypes;
629
+ const search = filterValue.toLowerCase().trim();
630
+ return availableTypes.filter((t) => t.toLowerCase().includes(search));
631
+ }, [availableTypes, filterValue]);
633
632
  const filteredAndSorted = useMemo2(() => {
634
633
  const base = Array.isArray(dbNodes) ? dbNodes : [];
635
634
  let pool = base;
@@ -642,22 +641,13 @@ function XViewSidebar({
642
641
  });
643
642
  }
644
643
  if (activeFilters.length > 0) {
645
- console.log("XViewSidebar: aplicando activeFilters:", activeFilters);
646
644
  pool = pool.filter((node) => {
647
645
  return activeFilters.every((filter) => {
648
- try {
649
- if (filter.type === "Type") {
650
- const nodeTypes = Array.isArray(node.type) ? node.type : [node.type];
651
- return nodeTypes.some((t) => {
652
- if (t === void 0) console.log("XViewSidebar: t \xE9 undefined no node:", node.id);
653
- if (filter.value === void 0) console.log("XViewSidebar: filter.value \xE9 undefined");
654
- return String(t).toLowerCase() === String(filter.value).toLowerCase();
655
- });
656
- } else if (filter.type === "Color") {
657
- return String(node.color).toLowerCase() === String(filter.value).toLowerCase();
658
- }
659
- } catch (err) {
660
- console.error("XViewSidebar: Erro ao filtrar node:", node, "com filtro:", filter, err);
646
+ if (filter.type === "Type") {
647
+ const nodeTypes = Array.isArray(node.type) ? node.type : [node.type];
648
+ return nodeTypes.some((t) => String(t).toLowerCase() === String(filter.value).toLowerCase());
649
+ } else if (filter.type === "Color") {
650
+ return String(node.color).toLowerCase() === String(filter.value).toLowerCase();
661
651
  }
662
652
  return true;
663
653
  });
@@ -679,12 +669,15 @@ function XViewSidebar({
679
669
  if (filterMenuRef.current && !filterMenuRef.current.contains(event.target)) {
680
670
  setIsFilterMenuOpen(false);
681
671
  }
672
+ if (typeDropdownRef.current && !typeDropdownRef.current.contains(event.target)) {
673
+ setIsTypeDropdownOpen(false);
674
+ }
682
675
  };
683
- if (isFilterMenuOpen) {
676
+ if (isFilterMenuOpen || isTypeDropdownOpen) {
684
677
  document.addEventListener("mousedown", handleClickOutside);
685
678
  }
686
679
  return () => document.removeEventListener("mousedown", handleClickOutside);
687
- }, [isFilterMenuOpen]);
680
+ }, [isFilterMenuOpen, isTypeDropdownOpen]);
688
681
  useEffect2(() => {
689
682
  if (!query) setSelectedNodeId(null);
690
683
  }, [query]);
@@ -738,6 +731,35 @@ function XViewSidebar({
738
731
  const handleRemoveFilter = (index) => {
739
732
  setActiveFilters((prev) => prev.filter((_, i) => i !== index));
740
733
  };
734
+ const handleTypeKeyDown = (e) => {
735
+ if (!isTypeDropdownOpen) {
736
+ setIsTypeDropdownOpen(true);
737
+ }
738
+ if (e.key === "ArrowDown") {
739
+ e.preventDefault();
740
+ setHighlightedTypeIndex(
741
+ (prev) => prev < filteredTypes.length - 1 ? prev + 1 : prev
742
+ );
743
+ } else if (e.key === "ArrowUp") {
744
+ e.preventDefault();
745
+ setHighlightedTypeIndex((prev) => prev > 0 ? prev - 1 : 0);
746
+ } else if (e.key === "Enter") {
747
+ e.preventDefault();
748
+ if (highlightedTypeIndex >= 0 && filteredTypes[highlightedTypeIndex]) {
749
+ setFilterValue(filteredTypes[highlightedTypeIndex]);
750
+ setIsTypeDropdownOpen(false);
751
+ setHighlightedTypeIndex(-1);
752
+ } else {
753
+ handleAddFilter();
754
+ }
755
+ } else if (e.key === "Escape") {
756
+ e.preventDefault();
757
+ setIsTypeDropdownOpen(false);
758
+ setHighlightedTypeIndex(-1);
759
+ } else if (e.key === "Tab") {
760
+ setIsTypeDropdownOpen(false);
761
+ }
762
+ };
741
763
  const ToggleButton = /* @__PURE__ */ React2.createElement(
742
764
  "button",
743
765
  {
@@ -868,18 +890,36 @@ function XViewSidebar({
868
890
  className: `flex-1 text-xs py-1 rounded transition-colors ${filterCategory === opt ? "bg-indigo-600 text-white shadow-sm" : "text-slate-400 hover:text-slate-200"}`
869
891
  },
870
892
  opt
871
- )))), /* @__PURE__ */ React2.createElement("div", { className: "space-y-1" }, /* @__PURE__ */ React2.createElement("label", { className: "text-[10px] uppercase text-slate-400 font-semibold tracking-wider" }, "Valor"), filterCategory === "Type" ? /* @__PURE__ */ React2.createElement("div", { className: "relative" }, /* @__PURE__ */ React2.createElement(
893
+ )))), /* @__PURE__ */ React2.createElement("div", { className: "space-y-1" }, /* @__PURE__ */ React2.createElement("label", { className: "text-[10px] uppercase text-slate-400 font-semibold tracking-wider" }, "Valor"), filterCategory === "Type" ? /* @__PURE__ */ React2.createElement("div", { className: "relative", ref: typeDropdownRef }, /* @__PURE__ */ React2.createElement(
872
894
  "input",
873
895
  {
874
- list: "typeOptions",
875
896
  type: "text",
876
897
  value: filterValue,
877
- onChange: (e) => setFilterValue(e.target.value),
898
+ onChange: (e) => {
899
+ setFilterValue(e.target.value);
900
+ if (!isTypeDropdownOpen) setIsTypeDropdownOpen(true);
901
+ setHighlightedTypeIndex(-1);
902
+ },
903
+ onFocus: () => setIsTypeDropdownOpen(true),
904
+ onKeyDown: handleTypeKeyDown,
878
905
  placeholder: "Ex: Concept",
879
906
  className: "w-full bg-slate-800 p-2 text-xs rounded border border-white/10 focus:outline-none focus:border-indigo-500/50 text-white",
880
907
  autoFocus: true
881
908
  }
882
- ), /* @__PURE__ */ React2.createElement("datalist", { id: "typeOptions" }, availableTypes.map((t) => /* @__PURE__ */ React2.createElement("option", { key: t, value: t })))) : /* @__PURE__ */ React2.createElement("div", { className: "relative flex items-center" }, /* @__PURE__ */ React2.createElement(
909
+ ), isTypeDropdownOpen && filteredTypes.length > 0 && /* @__PURE__ */ React2.createElement("div", { className: "absolute z-[60] left-0 right-0 mt-1 max-h-48 overflow-y-auto bg-slate-900/95 backdrop-blur-xl border border-white/10 rounded-md shadow-2xl custom-scrollbar" }, filteredTypes.map((t, idx) => /* @__PURE__ */ React2.createElement(
910
+ "div",
911
+ {
912
+ key: t,
913
+ className: `px-3 py-2 text-xs cursor-pointer transition-colors ${idx === highlightedTypeIndex ? "bg-indigo-600 text-white" : "text-slate-300 hover:bg-white/10"}`,
914
+ onMouseDown: (e) => {
915
+ e.preventDefault();
916
+ setFilterValue(t);
917
+ setIsTypeDropdownOpen(false);
918
+ setHighlightedTypeIndex(-1);
919
+ }
920
+ },
921
+ t
922
+ )))) : /* @__PURE__ */ React2.createElement("div", { className: "relative flex items-center" }, /* @__PURE__ */ React2.createElement(
883
923
  "div",
884
924
  {
885
925
  className: "absolute left-2 w-3 h-3 rounded-full border border-white/20 shadow-sm",
@@ -7680,7 +7720,7 @@ function ColorPicker({ color, onChange, disabled }) {
7680
7720
  style: { backgroundColor: preset },
7681
7721
  title: preset
7682
7722
  },
7683
- color.toLowerCase() === preset.toLowerCase() && /* @__PURE__ */ React12.createElement(FiCheck6, { className: `drop-shadow-md ${["#ffffff", "#4df5cb", "#84cc16", "#f59e0b"].includes(preset) ? "text-black" : "text-white"}`, size: 12 })
7723
+ (color || "").toLowerCase() === (preset || "").toLowerCase() && /* @__PURE__ */ React12.createElement(FiCheck6, { className: `drop-shadow-md ${["#ffffff", "#4df5cb", "#84cc16", "#f59e0b"].includes(preset) ? "text-black" : "text-white"}`, size: 12 })
7684
7724
  )))), /* @__PURE__ */ React12.createElement("style", null, `
7685
7725
  .custom-react-colorful .react-colorful {
7686
7726
  width: 100%;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lv-x-software-house/x_view",
3
- "version": "1.2.5-dev.17",
3
+ "version": "1.2.5-dev.18",
4
4
  "description": "Pacote privado contendo os componentes e lógica de renderização 3D do X View.",
5
5
  "author": "iv.x - Engenharia de Software - ivxsoftwarehouse@gmail.com",
6
6
  "license": "UNLICENSED",