neogestify-ui-components 2.0.1 → 2.2.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.
Files changed (31) hide show
  1. package/README.md +153 -19
  2. package/dist/components/ElementLibraryBuilder/index.d.mts +5 -0
  3. package/dist/components/ElementLibraryBuilder/index.d.ts +5 -0
  4. package/dist/components/ElementLibraryBuilder/index.js +689 -0
  5. package/dist/components/ElementLibraryBuilder/index.js.map +1 -0
  6. package/dist/components/ElementLibraryBuilder/index.mjs +687 -0
  7. package/dist/components/ElementLibraryBuilder/index.mjs.map +1 -0
  8. package/dist/components/VenueMapEditor/index.d.mts +66 -5
  9. package/dist/components/VenueMapEditor/index.d.ts +66 -5
  10. package/dist/components/VenueMapEditor/index.js +199 -34
  11. package/dist/components/VenueMapEditor/index.js.map +1 -1
  12. package/dist/components/VenueMapEditor/index.mjs +199 -36
  13. package/dist/components/VenueMapEditor/index.mjs.map +1 -1
  14. package/dist/index.d.mts +2 -1
  15. package/dist/index.d.ts +2 -1
  16. package/dist/index.js +592 -34
  17. package/dist/index.js.map +1 -1
  18. package/dist/index.mjs +591 -36
  19. package/dist/index.mjs.map +1 -1
  20. package/package.json +1 -1
  21. package/src/components/ElementLibraryBuilder/builder.tsx +400 -0
  22. package/src/components/ElementLibraryBuilder/index.ts +1 -0
  23. package/src/components/VenueMapEditor/VenueMapEditor.tsx +79 -20
  24. package/src/components/VenueMapEditor/components/ElementNode.tsx +23 -0
  25. package/src/components/VenueMapEditor/components/PropertiesPanel.tsx +17 -4
  26. package/src/components/VenueMapEditor/components/Toolbar.tsx +73 -39
  27. package/src/components/VenueMapEditor/hooks/useLibraryStorage.ts +46 -0
  28. package/src/components/VenueMapEditor/index.ts +3 -0
  29. package/src/components/VenueMapEditor/types.ts +45 -3
  30. package/src/components/VenueMapEditor/utils/svgParser.ts +33 -0
  31. package/src/index.ts +1 -0
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
- import { forwardRef, useState, useEffect, useImperativeHandle, createContext, useContext, useRef, useCallback, useMemo } from 'react';
2
+ import { forwardRef, useState, useEffect, useImperativeHandle, createContext, useContext, useCallback, useRef, useMemo } from 'react';
3
3
  import Swal from 'sweetalert2';
4
4
 
5
5
  // src/components/icons/icons.tsx
@@ -902,6 +902,53 @@ function ThemeToggle() {
902
902
  }
903
903
  );
904
904
  }
905
+ function useLibraryStorage(storageKey) {
906
+ const [libs, setLibs] = useState(() => {
907
+ if (!storageKey) return {};
908
+ try {
909
+ const raw = localStorage.getItem(storageKey);
910
+ return raw ? JSON.parse(raw) : {};
911
+ } catch {
912
+ return {};
913
+ }
914
+ });
915
+ const setAndPersist = useCallback(
916
+ (newLibs) => {
917
+ setLibs(newLibs);
918
+ if (!storageKey) return;
919
+ try {
920
+ if (Object.keys(newLibs).length === 0) {
921
+ localStorage.removeItem(storageKey);
922
+ } else {
923
+ localStorage.setItem(storageKey, JSON.stringify(newLibs));
924
+ }
925
+ } catch {
926
+ }
927
+ },
928
+ [storageKey]
929
+ );
930
+ return [libs, setAndPersist];
931
+ }
932
+
933
+ // src/components/VenueMapEditor/utils/svgParser.ts
934
+ var DANGEROUS_TAGS = /\b(script|iframe|object|embed|link|style|meta)\b/gi;
935
+ var DANGEROUS_ATTRS = /\bon\w+\s*=/gi;
936
+ var DANGEROUS_HREF = /\bhref\s*=\s*["']?\s*javascript:/gi;
937
+ var DANGEROUS_XLINK = /\bxlink:href\s*=\s*["']?\s*javascript:/gi;
938
+ function sanitize(html) {
939
+ return html.replace(DANGEROUS_TAGS, "").replace(DANGEROUS_ATTRS, "").replace(DANGEROUS_HREF, "").replace(DANGEROUS_XLINK, "");
940
+ }
941
+ var VIEWBOX_RE = /viewBox\s*=\s*"([^"]+)"/i;
942
+ var SVG_OPEN_END_RE = /<svg[^>]*>/i;
943
+ function parseSvgMarkup(markup) {
944
+ const viewBoxMatch = markup.match(VIEWBOX_RE);
945
+ const viewBox = viewBoxMatch?.[1] ?? "0 0 100 100";
946
+ const svgOpenMatch = markup.match(SVG_OPEN_END_RE);
947
+ const afterOpen = svgOpenMatch ? markup.slice(svgOpenMatch.index + svgOpenMatch[0].length) : markup;
948
+ const closeIdx = afterOpen.lastIndexOf("</svg>");
949
+ const inner = closeIdx >= 0 ? afterOpen.slice(0, closeIdx) : afterOpen;
950
+ return { viewBox, innerHtml: sanitize(inner) };
951
+ }
905
952
  function ToolButton({ active, disabled, title, onClick, children }) {
906
953
  return /* @__PURE__ */ jsx(
907
954
  "button",
@@ -931,7 +978,15 @@ function TypeChip({ typeDef, active, onClick }) {
931
978
  active ? "border-blue-400 bg-blue-50 text-blue-700 font-medium" : "border-slate-200 bg-white text-slate-600 hover:border-slate-300 hover:bg-slate-50"
932
979
  ].join(" "),
933
980
  children: [
934
- /* @__PURE__ */ jsx(
981
+ typeDef.shape === "svg" && typeDef.svgMarkup ? /* @__PURE__ */ jsx(
982
+ "svg",
983
+ {
984
+ viewBox: parseSvgMarkup(typeDef.svgMarkup).viewBox,
985
+ className: "w-2.5 h-2.5 shrink-0",
986
+ style: { color: typeDef.strokeColor },
987
+ dangerouslySetInnerHTML: { __html: parseSvgMarkup(typeDef.svgMarkup).innerHtml }
988
+ }
989
+ ) : /* @__PURE__ */ jsx(
935
990
  "span",
936
991
  {
937
992
  className: "w-2.5 h-2.5 rounded-sm shrink-0",
@@ -966,6 +1021,19 @@ function Toolbar({
966
1021
  onLoadLibrary,
967
1022
  onRemoveLibraryGroup
968
1023
  }) {
1024
+ const [activeGroupId, setActiveGroupId] = useState(
1025
+ () => paletteGroups[0]?.id ?? null
1026
+ );
1027
+ useEffect(() => {
1028
+ if (paletteGroups.length === 0) {
1029
+ setActiveGroupId(null);
1030
+ return;
1031
+ }
1032
+ if (!paletteGroups.find((g) => g.id === activeGroupId)) {
1033
+ setActiveGroupId(paletteGroups[0].id);
1034
+ }
1035
+ }, [paletteGroups, activeGroupId]);
1036
+ const activeGroup = paletteGroups.find((g) => g.id === activeGroupId) ?? null;
969
1037
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col bg-white border-b border-slate-200 shadow-sm shrink-0", children: [
970
1038
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5 px-2 py-1.5", children: [
971
1039
  /* @__PURE__ */ jsx(ToolButton, { title: "Seleccionar (V)", active: tool === "SELECT", onClick: () => onToolChange("SELECT"), children: /* @__PURE__ */ jsx(IconCursor, { className: "w-4 h-4" }) }),
@@ -1003,21 +1071,34 @@ function Toolbar({
1003
1071
  /* @__PURE__ */ jsx(ToolButton, { title: areaShape === "polygon" ? "Cambiar a rect\xE1ngulo" : "Cambiar a pol\xEDgono", onClick: () => onToggleAreaShape?.(), children: /* @__PURE__ */ jsx("span", { className: "text-xs font-medium", children: areaShape === "polygon" ? "Poly" : "Rect" }) })
1004
1072
  ] })
1005
1073
  ] }),
1006
- tool === "PLACE" && /* @__PURE__ */ jsx("div", { className: "flex items-stretch gap-0 border-t border-slate-100 bg-slate-50 overflow-x-auto", children: paletteGroups.map((group, gi) => /* @__PURE__ */ jsxs("div", { className: "flex items-center shrink-0", children: [
1007
- gi > 0 && /* @__PURE__ */ jsx("div", { className: "w-px self-stretch bg-slate-200 mx-1" }),
1008
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5 px-1.5 shrink-0", children: [
1009
- /* @__PURE__ */ jsx("span", { className: "text-[10px] text-slate-400 font-medium whitespace-nowrap select-none", children: group.name }),
1010
- !group.isBase && onRemoveLibraryGroup && /* @__PURE__ */ jsx(
1011
- "button",
1012
- {
1013
- title: `Eliminar grupo "${group.name}"`,
1014
- onClick: () => onRemoveLibraryGroup(group.id),
1015
- className: "text-slate-300 hover:text-red-400 leading-none text-xs ml-0.5 transition-colors",
1016
- children: "\xD7"
1017
- }
1018
- )
1019
- ] }),
1020
- /* @__PURE__ */ jsx("div", { className: "flex items-center gap-1 px-1 py-1.5", children: group.types.map((typeDef) => /* @__PURE__ */ jsx(
1074
+ tool === "PLACE" && paletteGroups.length > 0 && /* @__PURE__ */ jsxs("div", { className: "flex flex-col border-t border-slate-100", children: [
1075
+ /* @__PURE__ */ jsx("div", { className: "flex items-end gap-0 overflow-x-auto bg-slate-50 border-b border-slate-200 px-2 pt-1", children: paletteGroups.map((group) => /* @__PURE__ */ jsx("div", { className: "flex items-center shrink-0", children: /* @__PURE__ */ jsxs(
1076
+ "button",
1077
+ {
1078
+ onClick: () => setActiveGroupId(group.id),
1079
+ className: [
1080
+ "flex items-center gap-1 px-3 py-1 text-xs font-medium rounded-t border-x border-t transition-colors whitespace-nowrap",
1081
+ group.id === activeGroupId ? "bg-white border-slate-200 text-slate-800 -mb-px pb-[5px]" : "bg-slate-50 border-transparent text-slate-400 hover:text-slate-600 hover:bg-slate-100"
1082
+ ].join(" "),
1083
+ children: [
1084
+ group.name || "Sin nombre",
1085
+ !group.isBase && onRemoveLibraryGroup && /* @__PURE__ */ jsx(
1086
+ "span",
1087
+ {
1088
+ role: "button",
1089
+ title: `Eliminar "${group.name}"`,
1090
+ onClick: (e) => {
1091
+ e.stopPropagation();
1092
+ onRemoveLibraryGroup(group.id);
1093
+ },
1094
+ className: "ml-0.5 text-slate-300 hover:text-red-400 transition-colors leading-none",
1095
+ children: "\xD7"
1096
+ }
1097
+ )
1098
+ ]
1099
+ }
1100
+ ) }, group.id)) }),
1101
+ activeGroup && /* @__PURE__ */ jsx("div", { className: "flex items-center gap-1 flex-wrap px-2 py-1.5 bg-white min-h-[36px]", children: activeGroup.types.map((typeDef) => /* @__PURE__ */ jsx(
1021
1102
  TypeChip,
1022
1103
  {
1023
1104
  typeDef,
@@ -1026,7 +1107,7 @@ function Toolbar({
1026
1107
  },
1027
1108
  typeDef.id
1028
1109
  )) })
1029
- ] }, group.id)) })
1110
+ ] })
1030
1111
  ] });
1031
1112
  }
1032
1113
  var ZOOM_MIN = 0.1;
@@ -1966,6 +2047,7 @@ function ElementNode({
1966
2047
  {
1967
2048
  d: typeDef.svgPath,
1968
2049
  fill: fillColor,
2050
+ fillRule: typeDef.fillRule ?? "nonzero",
1969
2051
  stroke: isSelected ? "#3b82f6" : typeDef.strokeColor,
1970
2052
  strokeWidth: isSelected ? customPath.strokeWidth * 1.5 : customPath.strokeWidth,
1971
2053
  style: { cursor: bodyCursor },
@@ -1973,6 +2055,28 @@ function ElementNode({
1973
2055
  onClick: handleBodyClick
1974
2056
  }
1975
2057
  ) }),
2058
+ typeDef.shape === "svg" && typeDef.svgMarkup && (() => {
2059
+ const parsed = parseSvgMarkup(typeDef.svgMarkup);
2060
+ const parts = parsed.viewBox.split(/[\s,]+/).map(Number);
2061
+ const vw = parts[2] ?? 100;
2062
+ const vh = parts[3] ?? 100;
2063
+ const sx = vw > 0 ? w / vw : 1;
2064
+ const sy = vh > 0 ? h / vh : 1;
2065
+ const avgScale = Math.sqrt(Math.abs(sx * sy)) || 1;
2066
+ return /* @__PURE__ */ jsx(
2067
+ "g",
2068
+ {
2069
+ transform: `translate(${x}, ${y}) scale(${sx}, ${sy})`,
2070
+ fill: fillColor,
2071
+ stroke: isSelected ? "#3b82f6" : typeDef.strokeColor,
2072
+ strokeWidth: isSelected ? sw / avgScale * 1.5 : sw / avgScale,
2073
+ style: { cursor: bodyCursor },
2074
+ onMouseDown: tool === "SELECT" && !onViewerClick ? handleBodyDown : void 0,
2075
+ onClick: handleBodyClick,
2076
+ dangerouslySetInnerHTML: { __html: parsed.innerHtml }
2077
+ }
2078
+ );
2079
+ })(),
1976
2080
  (element.label ?? typeDef.label) && /* @__PURE__ */ jsx(
1977
2081
  "text",
1978
2082
  {
@@ -2471,14 +2575,35 @@ function PropertiesPanel({
2471
2575
  /* @__PURE__ */ jsx("div", { className: "px-3 py-2 border-b border-slate-100 text-xs font-semibold text-slate-500 uppercase tracking-wide", children: "Propiedades" }),
2472
2576
  /* @__PURE__ */ jsxs("div", { className: "flex-1 flex flex-col gap-4 p-3", children: [
2473
2577
  typeDef && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2474
- /* @__PURE__ */ jsx(
2578
+ typeDef.shape === "svg" ? /* @__PURE__ */ jsx(
2579
+ "svg",
2580
+ {
2581
+ viewBox: (() => {
2582
+ try {
2583
+ return typeDef.svgMarkup ? parseSvgMarkup(typeDef.svgMarkup).viewBox : "0 0 100 100";
2584
+ } catch {
2585
+ return "0 0 100 100";
2586
+ }
2587
+ })(),
2588
+ className: "w-3.5 h-3.5 shrink-0 border border-slate-300 rounded-sm",
2589
+ style: { color: typeDef.strokeColor },
2590
+ dangerouslySetInnerHTML: { __html: (() => {
2591
+ try {
2592
+ return typeDef.svgMarkup ? parseSvgMarkup(typeDef.svgMarkup).innerHtml : "";
2593
+ } catch {
2594
+ return "";
2595
+ }
2596
+ })() }
2597
+ }
2598
+ ) : /* @__PURE__ */ jsx(
2475
2599
  "span",
2476
2600
  {
2477
2601
  className: "w-3.5 h-3.5 rounded-sm shrink-0 border",
2478
2602
  style: { background: typeDef.color, borderColor: typeDef.strokeColor }
2479
2603
  }
2480
2604
  ),
2481
- /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-slate-700 truncate", children: typeDef.label })
2605
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-slate-700 truncate", children: typeDef.label }),
2606
+ typeDef.shape === "svg" && /* @__PURE__ */ jsx("span", { className: "text-[9px] uppercase tracking-wide text-slate-400 font-medium ml-auto", children: "SVG" })
2482
2607
  ] }),
2483
2608
  /* @__PURE__ */ jsxs("label", { className: "flex flex-col gap-0.5", children: [
2484
2609
  /* @__PURE__ */ jsx("span", { className: "text-[10px] font-medium text-slate-400 uppercase tracking-wide", children: "Etiqueta" }),
@@ -2813,7 +2938,23 @@ function createDefaultMap() {
2813
2938
  ]
2814
2939
  };
2815
2940
  }
2816
- var EMPTY_DOMAIN_CONFIG = { id: "__empty__", name: "", elementTypes: [] };
2941
+ var DEFAULT_LIBRARY_KEY = "venueMapEditor:libraries";
2942
+ function mergeLibraries(existing, incoming) {
2943
+ const result = { ...existing };
2944
+ for (const [groupId, incomingGroup] of Object.entries(incoming)) {
2945
+ if (result[groupId]) {
2946
+ const existingIds = new Set(result[groupId].objects.map((o) => o.id));
2947
+ const newObjects = incomingGroup.objects.filter((o) => !existingIds.has(o.id));
2948
+ result[groupId] = {
2949
+ ...result[groupId],
2950
+ objects: [...result[groupId].objects, ...newObjects]
2951
+ };
2952
+ } else {
2953
+ result[groupId] = incomingGroup;
2954
+ }
2955
+ }
2956
+ return result;
2957
+ }
2817
2958
  function updateFloor(map, updatedFloor) {
2818
2959
  return {
2819
2960
  ...map,
@@ -2853,7 +2994,9 @@ function polygonToRect(area) {
2853
2994
  };
2854
2995
  }
2855
2996
  function VenueMapEditor({
2856
- domainConfig = EMPTY_DOMAIN_CONFIG,
2997
+ domainConfigs,
2998
+ domainConfig,
2999
+ libraryStorageKey = DEFAULT_LIBRARY_KEY,
2857
3000
  initialMap,
2858
3001
  onChange,
2859
3002
  width = "100%",
@@ -2867,7 +3010,13 @@ function VenueMapEditor({
2867
3010
  onElementClick,
2868
3011
  onElementTypeClick
2869
3012
  }) {
3013
+ const effectiveConfigs = useMemo(() => {
3014
+ if (domainConfigs && domainConfigs.length > 0) return domainConfigs;
3015
+ if (domainConfig) return [domainConfig];
3016
+ return [];
3017
+ }, [domainConfigs, domainConfig]);
2870
3018
  const initialMapRef = useRef(initialMap ?? createDefaultMap());
3019
+ const [persistedLibs, setPersistedLibs] = useLibraryStorage(libraryStorageKey);
2871
3020
  const { map, canUndo, canRedo, push, replace, undo, redo } = useHistory(
2872
3021
  initialMapRef.current
2873
3022
  );
@@ -2883,31 +3032,40 @@ function VenueMapEditor({
2883
3032
  const resetViewRef = useRef(() => void 0);
2884
3033
  const importInputRef = useRef(null);
2885
3034
  const libraryInputRef = useRef(null);
3035
+ const effectiveLibs = useMemo(() => ({
3036
+ ...map.libraries ?? {},
3037
+ ...persistedLibs
3038
+ }), [map.libraries, persistedLibs]);
2886
3039
  const buildTypeDefs = useCallback(() => {
2887
- const m = new Map(domainConfig.elementTypes.map((t) => [t.id, t]));
2888
- const libs = map.libraries ?? {};
2889
- for (const group of Object.values(libs)) {
3040
+ const m = /* @__PURE__ */ new Map();
3041
+ for (const cfg of effectiveConfigs) {
3042
+ for (const t of cfg.elementTypes) {
3043
+ if (!m.has(t.id)) m.set(t.id, t);
3044
+ }
3045
+ }
3046
+ for (const group of Object.values(effectiveLibs)) {
2890
3047
  for (const t of group.objects) {
2891
3048
  if (!m.has(t.id)) m.set(t.id, t);
2892
3049
  }
2893
3050
  }
2894
3051
  return m;
2895
- }, [domainConfig, map.libraries]);
3052
+ }, [effectiveConfigs, effectiveLibs]);
2896
3053
  const elementTypeDefs = useRef(buildTypeDefs());
2897
3054
  useEffect(() => {
2898
3055
  elementTypeDefs.current = buildTypeDefs();
2899
3056
  }, [buildTypeDefs]);
2900
3057
  const paletteGroups = useMemo(() => {
2901
3058
  const groups = [];
2902
- if (domainConfig.elementTypes.length > 0) {
2903
- groups.push({ id: domainConfig.id, name: domainConfig.name, types: domainConfig.elementTypes, isBase: true });
3059
+ for (const cfg of effectiveConfigs) {
3060
+ if (cfg.elementTypes.length > 0) {
3061
+ groups.push({ id: cfg.id, name: cfg.name, types: cfg.elementTypes, isBase: true });
3062
+ }
2904
3063
  }
2905
- const libs = map.libraries ?? {};
2906
- for (const [gid, group] of Object.entries(libs)) {
3064
+ for (const [gid, group] of Object.entries(effectiveLibs)) {
2907
3065
  groups.push({ id: gid, name: group.name, types: group.objects, isBase: false });
2908
3066
  }
2909
3067
  return groups;
2910
- }, [domainConfig, map.libraries]);
3068
+ }, [effectiveConfigs, effectiveLibs]);
2911
3069
  useEffect(() => {
2912
3070
  if (activePlaceTypeId) return;
2913
3071
  const firstType = paletteGroups[0]?.types[0];
@@ -3047,22 +3205,27 @@ function VenueMapEditor({
3047
3205
  reader.onload = (e) => {
3048
3206
  try {
3049
3207
  const parsed = JSON.parse(e.target?.result);
3050
- const merged = { ...map.libraries ?? {}, ...parsed };
3051
- push({ ...map, libraries: merged });
3208
+ const mergedPersisted = mergeLibraries(persistedLibs, parsed);
3209
+ setPersistedLibs(mergedPersisted);
3210
+ const mergedMap = mergeLibraries(map.libraries ?? {}, parsed);
3211
+ push({ ...map, libraries: mergedMap });
3052
3212
  } catch {
3053
3213
  }
3054
3214
  };
3055
3215
  reader.readAsText(file);
3056
3216
  },
3057
- [map, push]
3217
+ [map, push, persistedLibs, setPersistedLibs]
3058
3218
  );
3059
3219
  const handleRemoveLibraryGroup = useCallback(
3060
3220
  (groupId) => {
3221
+ const newPersistedLibs = { ...persistedLibs };
3222
+ delete newPersistedLibs[groupId];
3223
+ setPersistedLibs(newPersistedLibs);
3061
3224
  const libs = { ...map.libraries ?? {} };
3062
3225
  delete libs[groupId];
3063
3226
  push({ ...map, libraries: Object.keys(libs).length > 0 ? libs : void 0 });
3064
3227
  },
3065
- [map, push]
3228
+ [map, push, persistedLibs, setPersistedLibs]
3066
3229
  );
3067
3230
  const DEFAULT_WALL_THICKNESS = 8;
3068
3231
  const handleAddWall = useCallback(
@@ -3508,7 +3671,399 @@ function VenueMapViewer({ elementStatus, onElementClick, ...rest }) {
3508
3671
  }
3509
3672
  );
3510
3673
  }
3674
+ var DEFAULT_ELEMENT = {
3675
+ id: "",
3676
+ label: "",
3677
+ shape: "rect",
3678
+ defaultWidth: 100,
3679
+ defaultHeight: 100,
3680
+ color: "#cccccc",
3681
+ strokeColor: "#000000"
3682
+ };
3683
+ var SHAPE_OPTIONS = [
3684
+ { value: "rect", label: "Rectangle" },
3685
+ { value: "circle", label: "Circle" },
3686
+ { value: "arrow", label: "Arrow" },
3687
+ { value: "path", label: "Path" },
3688
+ { value: "svg", label: "SVG Markup" }
3689
+ ];
3690
+ var ElementLibraryBuilder = () => {
3691
+ const [groups, setGroups] = useState([
3692
+ { internalId: crypto.randomUUID(), name: "defaultGroup", objects: [] }
3693
+ ]);
3694
+ const [activeGroupId, setActiveGroupId] = useState(groups[0].internalId);
3695
+ const [editingGroupId, setEditingGroupId] = useState(null);
3696
+ const [activeElementIndex, setActiveElementIndex] = useState(null);
3697
+ const [currentElement, setCurrentElement] = useState({ ...DEFAULT_ELEMENT, id: "rect_1", label: "New Rect" });
3698
+ const [downloadFileName, setDownloadFileName] = useState("libraries");
3699
+ const handleAddGroup = () => {
3700
+ const newGroupId = crypto.randomUUID();
3701
+ const newName = `group_${groups.length + 1}`;
3702
+ setGroups([...groups, { internalId: newGroupId, name: newName, objects: [] }]);
3703
+ setActiveGroupId(newGroupId);
3704
+ setActiveElementIndex(null);
3705
+ };
3706
+ const handleRemoveGroup = (id) => {
3707
+ const newGroups = groups.filter((g) => g.internalId !== id);
3708
+ setGroups(newGroups);
3709
+ if (activeGroupId === id) {
3710
+ if (newGroups.length > 0) {
3711
+ setActiveGroupId(newGroups[0].internalId);
3712
+ } else {
3713
+ setActiveGroupId("");
3714
+ }
3715
+ setActiveElementIndex(null);
3716
+ }
3717
+ };
3718
+ const activeGroup = groups.find((g) => g.internalId === activeGroupId);
3719
+ const handleSelectGroup = (gId) => {
3720
+ setActiveGroupId(gId);
3721
+ setActiveElementIndex(null);
3722
+ };
3723
+ const handleAddElement = () => {
3724
+ if (!activeGroup) return;
3725
+ const newEl = { ...DEFAULT_ELEMENT, id: `shape_${activeGroup.objects.length + 1}`, label: `Shape ${activeGroup.objects.length + 1}` };
3726
+ const updatedGroups = groups.map((g) => {
3727
+ if (g.internalId === activeGroupId) {
3728
+ return { ...g, objects: [...g.objects, newEl] };
3729
+ }
3730
+ return g;
3731
+ });
3732
+ setGroups(updatedGroups);
3733
+ setActiveElementIndex(activeGroup.objects.length);
3734
+ setCurrentElement(newEl);
3735
+ };
3736
+ const handleSelectElement = (idx) => {
3737
+ if (!activeGroup) return;
3738
+ setActiveElementIndex(idx);
3739
+ setCurrentElement(activeGroup.objects[idx]);
3740
+ };
3741
+ const handleRemoveElement = (idx) => {
3742
+ if (!activeGroup) return;
3743
+ const updatedGroups = groups.map((g) => {
3744
+ if (g.internalId === activeGroupId) {
3745
+ const newObjs = [...g.objects];
3746
+ newObjs.splice(idx, 1);
3747
+ return { ...g, objects: newObjs };
3748
+ }
3749
+ return g;
3750
+ });
3751
+ setGroups(updatedGroups);
3752
+ if (activeElementIndex === idx) {
3753
+ setActiveElementIndex(null);
3754
+ } else if (activeElementIndex !== null && activeElementIndex > idx) {
3755
+ setActiveElementIndex(activeElementIndex - 1);
3756
+ }
3757
+ };
3758
+ const handleSaveElement = () => {
3759
+ if (!activeGroup || activeElementIndex === null) return;
3760
+ const updatedGroups = groups.map((g) => {
3761
+ if (g.internalId === activeGroupId) {
3762
+ const newObjs = [...g.objects];
3763
+ newObjs[activeElementIndex] = { ...currentElement };
3764
+ return { ...g, objects: newObjs };
3765
+ }
3766
+ return g;
3767
+ });
3768
+ setGroups(updatedGroups);
3769
+ };
3770
+ const handleFieldChange = (field, value) => {
3771
+ setCurrentElement((prev) => ({ ...prev, [field]: value }));
3772
+ };
3773
+ const handleSvgMarkupChange = (value) => {
3774
+ handleFieldChange("svgMarkup", value);
3775
+ };
3776
+ const generatedLib = useMemo(() => {
3777
+ const lib = {};
3778
+ groups.forEach((g) => {
3779
+ lib[g.name] = {
3780
+ name: g.name,
3781
+ objects: g.objects
3782
+ };
3783
+ });
3784
+ return JSON.stringify(lib, null, 2);
3785
+ }, [groups]);
3786
+ const handleDownload = () => {
3787
+ const blob = new Blob([generatedLib], { type: "application/json" });
3788
+ const url = URL.createObjectURL(blob);
3789
+ const a = document.createElement("a");
3790
+ a.href = url;
3791
+ a.download = `${downloadFileName}.json`;
3792
+ document.body.appendChild(a);
3793
+ a.click();
3794
+ document.body.removeChild(a);
3795
+ URL.revokeObjectURL(url);
3796
+ };
3797
+ return /* @__PURE__ */ jsxs("div", { className: "flex gap-4 p-4 h-full min-h-[600px] text-sm text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-900", children: [
3798
+ /* @__PURE__ */ jsxs("div", { className: "w-1/4 flex flex-col gap-4 border-r dark:border-gray-700 pr-4", children: [
3799
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
3800
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
3801
+ /* @__PURE__ */ jsx("h3", { className: "font-bold", children: "Libraries (Groups)" }),
3802
+ /* @__PURE__ */ jsx(Button, { variant: "primary", onClick: handleAddGroup, children: "+ Group" })
3803
+ ] }),
3804
+ /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-1 max-h-48 overflow-y-auto pr-1", children: groups.map((group) => /* @__PURE__ */ jsxs(
3805
+ "div",
3806
+ {
3807
+ className: `flex items-center justify-between p-2 rounded cursor-pointer ${activeGroupId === group.internalId ? "bg-indigo-100 text-indigo-900 dark:bg-indigo-900/50 dark:text-indigo-100 font-semibold" : "hover:bg-gray-100 dark:hover:bg-gray-800"}`,
3808
+ onClick: () => handleSelectGroup(group.internalId),
3809
+ children: [
3810
+ editingGroupId === group.internalId ? /* @__PURE__ */ jsx(
3811
+ Input,
3812
+ {
3813
+ autoFocus: true,
3814
+ value: group.name,
3815
+ onChange: (e) => {
3816
+ setGroups(groups.map((g) => g.internalId === group.internalId ? { ...g, name: e.target.value } : g));
3817
+ },
3818
+ onBlur: () => setEditingGroupId(null),
3819
+ onKeyDown: (e) => e.key === "Enter" && setEditingGroupId(null)
3820
+ }
3821
+ ) : /* @__PURE__ */ jsx("span", { onDoubleClick: () => setEditingGroupId(group.internalId), children: group.name }),
3822
+ groups.length > 1 && /* @__PURE__ */ jsx("button", { onClick: (e) => {
3823
+ e.stopPropagation();
3824
+ handleRemoveGroup(group.internalId);
3825
+ }, className: "text-red-500 text-xs", children: "x" })
3826
+ ]
3827
+ },
3828
+ group.internalId
3829
+ )) })
3830
+ ] }),
3831
+ /* @__PURE__ */ jsx("hr", { className: "dark:border-gray-700" }),
3832
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 flex-grow overflow-hidden", children: [
3833
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
3834
+ /* @__PURE__ */ jsxs("h3", { className: "font-bold", children: [
3835
+ "Elements in ",
3836
+ activeGroup?.name || "?"
3837
+ ] }),
3838
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: handleAddElement, disabled: !activeGroup, children: "+ Element" })
3839
+ ] }),
3840
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1 overflow-y-auto flex-grow pr-1", children: [
3841
+ activeGroup?.objects.map((el, i) => /* @__PURE__ */ jsxs(
3842
+ "div",
3843
+ {
3844
+ className: `flex items-center justify-between p-2 rounded cursor-pointer ${activeElementIndex === i ? "bg-indigo-100 text-indigo-900 dark:bg-indigo-900/50 dark:text-indigo-100 font-semibold" : "hover:bg-gray-100 dark:hover:bg-gray-800"}`,
3845
+ onClick: () => handleSelectElement(i),
3846
+ children: [
3847
+ /* @__PURE__ */ jsxs("span", { children: [
3848
+ el.id,
3849
+ " (",
3850
+ el.shape,
3851
+ ")"
3852
+ ] }),
3853
+ /* @__PURE__ */ jsx("button", { onClick: (e) => {
3854
+ e.stopPropagation();
3855
+ handleRemoveElement(i);
3856
+ }, className: "text-red-500 text-xs", children: "x" })
3857
+ ]
3858
+ },
3859
+ i
3860
+ )),
3861
+ (!activeGroup || activeGroup.objects.length === 0) && /* @__PURE__ */ jsx("span", { className: "text-gray-400 dark:text-gray-500 italic text-xs", children: "No elements yet" })
3862
+ ] })
3863
+ ] })
3864
+ ] }),
3865
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 flex flex-col gap-4 px-2 overflow-y-auto", children: [
3866
+ /* @__PURE__ */ jsx("h3", { className: "font-bold text-lg", children: "Element Editor" }),
3867
+ activeElementIndex !== null ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4 w-full max-w-2xl", children: [
3868
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-4", children: [
3869
+ /* @__PURE__ */ jsx(
3870
+ Input,
3871
+ {
3872
+ label: "Element ID (unique)",
3873
+ value: currentElement.id,
3874
+ onChange: (e) => handleFieldChange("id", e.target.value)
3875
+ }
3876
+ ),
3877
+ /* @__PURE__ */ jsx(
3878
+ Input,
3879
+ {
3880
+ label: "Label (display name)",
3881
+ value: currentElement.label,
3882
+ onChange: (e) => handleFieldChange("label", e.target.value)
3883
+ }
3884
+ )
3885
+ ] }),
3886
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-4", children: [
3887
+ /* @__PURE__ */ jsx(
3888
+ Select,
3889
+ {
3890
+ label: "Shape",
3891
+ options: SHAPE_OPTIONS,
3892
+ value: currentElement.shape,
3893
+ onChange: (e) => handleFieldChange("shape", e.target.value)
3894
+ }
3895
+ ),
3896
+ /* @__PURE__ */ jsx(
3897
+ Input,
3898
+ {
3899
+ label: "Icon (emoji or class)",
3900
+ value: currentElement.icon || "",
3901
+ onChange: (e) => handleFieldChange("icon", e.target.value)
3902
+ }
3903
+ )
3904
+ ] }),
3905
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-4", children: [
3906
+ /* @__PURE__ */ jsx(
3907
+ Input,
3908
+ {
3909
+ type: "number",
3910
+ label: "Default Width",
3911
+ value: currentElement.defaultWidth,
3912
+ onChange: (e) => handleFieldChange("defaultWidth", parseFloat(e.target.value) || 0)
3913
+ }
3914
+ ),
3915
+ /* @__PURE__ */ jsx(
3916
+ Input,
3917
+ {
3918
+ type: "number",
3919
+ label: "Default Height",
3920
+ value: currentElement.defaultHeight,
3921
+ onChange: (e) => handleFieldChange("defaultHeight", parseFloat(e.target.value) || 0)
3922
+ }
3923
+ )
3924
+ ] }),
3925
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-4", children: [
3926
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
3927
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-semibold text-gray-700", children: "Fill Color" }),
3928
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
3929
+ /* @__PURE__ */ jsx(
3930
+ "input",
3931
+ {
3932
+ type: "color",
3933
+ className: "w-8 h-8 cursor-pointer rounded",
3934
+ value: currentElement.color,
3935
+ onChange: (e) => handleFieldChange("color", e.target.value)
3936
+ }
3937
+ ),
3938
+ /* @__PURE__ */ jsx(
3939
+ Input,
3940
+ {
3941
+ value: currentElement.color,
3942
+ onChange: (e) => handleFieldChange("color", e.target.value)
3943
+ }
3944
+ )
3945
+ ] })
3946
+ ] }),
3947
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
3948
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-semibold text-gray-700", children: "Stroke Color" }),
3949
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
3950
+ /* @__PURE__ */ jsx(
3951
+ "input",
3952
+ {
3953
+ type: "color",
3954
+ className: "w-8 h-8 cursor-pointer rounded",
3955
+ value: currentElement.strokeColor,
3956
+ onChange: (e) => handleFieldChange("strokeColor", e.target.value)
3957
+ }
3958
+ ),
3959
+ /* @__PURE__ */ jsx(
3960
+ Input,
3961
+ {
3962
+ value: currentElement.strokeColor,
3963
+ onChange: (e) => handleFieldChange("strokeColor", e.target.value)
3964
+ }
3965
+ )
3966
+ ] })
3967
+ ] })
3968
+ ] }),
3969
+ currentElement.shape === "path" && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4 border dark:border-gray-700 p-4 rounded bg-gray-50 dark:bg-gray-800/50", children: [
3970
+ /* @__PURE__ */ jsx("h4", { className: "font-semibold text-sm", children: "Path Config" }),
3971
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-4", children: [
3972
+ /* @__PURE__ */ jsx(
3973
+ Input,
3974
+ {
3975
+ label: "ViewBox",
3976
+ placeholder: "0 0 100 100",
3977
+ value: currentElement.viewBox || "",
3978
+ onChange: (e) => handleFieldChange("viewBox", e.target.value)
3979
+ }
3980
+ ),
3981
+ /* @__PURE__ */ jsx(
3982
+ Select,
3983
+ {
3984
+ label: "Fill Rule",
3985
+ options: [
3986
+ { value: "nonzero", label: "nonzero" },
3987
+ { value: "evenodd", label: "evenodd" }
3988
+ ],
3989
+ value: currentElement.fillRule || "nonzero",
3990
+ onChange: (e) => handleFieldChange("fillRule", e.target.value)
3991
+ }
3992
+ )
3993
+ ] }),
3994
+ /* @__PURE__ */ jsx(
3995
+ TextArea,
3996
+ {
3997
+ label: "SVG Path (d attribute)",
3998
+ placeholder: "M10 10 H 90 V 90 H 10 Z",
3999
+ value: currentElement.svgPath || "",
4000
+ onChange: (e) => handleFieldChange("svgPath", e.target.value),
4001
+ rows: 4
4002
+ }
4003
+ )
4004
+ ] }),
4005
+ currentElement.shape === "svg" && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4 border dark:border-amber-700/50 p-4 rounded bg-amber-50 dark:bg-amber-900/10", children: [
4006
+ /* @__PURE__ */ jsx("h4", { className: "font-semibold text-sm", children: "SVG Markup (Autosanitized)" }),
4007
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-amber-800 dark:text-amber-400", children: "Paste your raw SVG here. Double quotes will be converted to single quotes automatically to safely embed the string in JSON." }),
4008
+ /* @__PURE__ */ jsx(
4009
+ TextArea,
4010
+ {
4011
+ label: "raw <svg>...</svg>",
4012
+ value: currentElement.svgMarkup || "",
4013
+ onChange: (e) => handleSvgMarkupChange(e.target.value),
4014
+ rows: 6,
4015
+ placeholder: "<svg viewBox='0 0 100 100'><circle cx='50' cy='50' r='50'/></svg>"
4016
+ }
4017
+ )
4018
+ ] }),
4019
+ /* @__PURE__ */ jsx("div", { className: "flex justify-end gap-2 mt-4 pt-4 border-t dark:border-gray-700", children: /* @__PURE__ */ jsx(Button, { onClick: handleSaveElement, children: "Save Changes to Element" }) })
4020
+ ] }) : /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center h-full text-gray-400", children: "Select an element to edit or add a new one." })
4021
+ ] }),
4022
+ /* @__PURE__ */ jsxs("div", { className: "w-1/3 flex flex-col gap-2 border-l dark:border-gray-700 pl-4 h-full max-h-full", children: [
4023
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between shrink-0", children: [
4024
+ /* @__PURE__ */ jsx("h3", { className: "font-bold", children: "Output JSON" }),
4025
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
4026
+ /* @__PURE__ */ jsx(
4027
+ Input,
4028
+ {
4029
+ value: downloadFileName,
4030
+ onChange: (e) => setDownloadFileName(e.target.value),
4031
+ placeholder: "filename",
4032
+ title: "Filename without extension"
4033
+ }
4034
+ ),
4035
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-500", children: ".json" }),
4036
+ /* @__PURE__ */ jsx(
4037
+ Button,
4038
+ {
4039
+ variant: "secondary",
4040
+ onClick: handleDownload,
4041
+ title: "Download JSON file",
4042
+ children: "Descargar"
4043
+ }
4044
+ ),
4045
+ /* @__PURE__ */ jsx(
4046
+ Button,
4047
+ {
4048
+ variant: "secondary",
4049
+ onClick: () => navigator.clipboard.writeText(generatedLib),
4050
+ children: "Copy"
4051
+ }
4052
+ )
4053
+ ] })
4054
+ ] }),
4055
+ /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-hidden h-full pb-4", children: /* @__PURE__ */ jsx(
4056
+ TextArea,
4057
+ {
4058
+ readOnly: true,
4059
+ className: "h-full resize-none font-mono text-xs text-green-600 dark:text-green-400 bg-gray-50 dark:bg-gray-900 border-gray-200 dark:border-gray-800",
4060
+ value: generatedLib
4061
+ }
4062
+ ) })
4063
+ ] })
4064
+ ] });
4065
+ };
3511
4066
 
3512
- export { AddIcon, Alerta, AlertaAdvertencia, AlertaConfirmacion, AlertaError, AlertaExito, AlertaInfo, AlertaToast, AnimateSpin, ArchiveIcon, ArrowIcon, ArrowLeftIcon, ArrowRightIcon, BackIcon, BarsChartsIcon, BoxIcon, BuildingIcon, Button, CajasIcon, CalendarIcon, CamaraIcon, CancelIcon, CashIcon, CategorieIcon, ChartIcon, CheckCircleIcon, CheckIcon, ClockIcon, CloseIcon, CloudIcon, CopyIcon, DeleteIcon, DocumentIcon, EditIcon, FacturacionIcon, FilterIcon, FolderIcon, Form, GearIcon, HomeIcon, IconCursor, IconDownload, IconDuplicate, IconErase, IconGrid, IconHand, IconLayers, IconPlace, IconPolygon, IconRedo, IconReset, IconUndo, IconUpload, IconWall, IconZoomIn, IconZoomOut, InfoAlert, InfoIcon, Input, LifeGuardIcon, LightingIcon, Loading, LocationIcon, LogoutIcon, MenuIcon, MinusIcon, Modal, MoneyIcon, MonitorIcon, MoonIcon, NetworkIcon, NotFoundIcon, PasteIcon, PercentIcon, PrinterIcon, QuestionIcon, RestaurantMenuIcon, SaveIcon, SearchIcon, Select, ShieldIcon, SpinnerIcon, StackIcon, SunIcon, Table, TestIcon, TextArea, ThemeContext, ThemeProvider, ThemeToggle, TrashIcon, TruckIcon, UsersIcon, VenueMapEditor, VenueMapViewer, WhatsAppIcon, findNearestNode, genId, snapPoint, snapToGrid, usePanZoom, useTheme };
4067
+ export { AddIcon, Alerta, AlertaAdvertencia, AlertaConfirmacion, AlertaError, AlertaExito, AlertaInfo, AlertaToast, AnimateSpin, ArchiveIcon, ArrowIcon, ArrowLeftIcon, ArrowRightIcon, BackIcon, BarsChartsIcon, BoxIcon, BuildingIcon, Button, CajasIcon, CalendarIcon, CamaraIcon, CancelIcon, CashIcon, CategorieIcon, ChartIcon, CheckCircleIcon, CheckIcon, ClockIcon, CloseIcon, CloudIcon, CopyIcon, DeleteIcon, DocumentIcon, EditIcon, ElementLibraryBuilder, FacturacionIcon, FilterIcon, FolderIcon, Form, GearIcon, HomeIcon, IconCursor, IconDownload, IconDuplicate, IconErase, IconGrid, IconHand, IconLayers, IconPlace, IconPolygon, IconRedo, IconReset, IconUndo, IconUpload, IconWall, IconZoomIn, IconZoomOut, InfoAlert, InfoIcon, Input, LifeGuardIcon, LightingIcon, Loading, LocationIcon, LogoutIcon, MenuIcon, MinusIcon, Modal, MoneyIcon, MonitorIcon, MoonIcon, NetworkIcon, NotFoundIcon, PasteIcon, PercentIcon, PrinterIcon, QuestionIcon, RestaurantMenuIcon, SaveIcon, SearchIcon, Select, ShieldIcon, SpinnerIcon, StackIcon, SunIcon, Table, TestIcon, TextArea, ThemeContext, ThemeProvider, ThemeToggle, TrashIcon, TruckIcon, UsersIcon, VenueMapEditor, VenueMapViewer, WhatsAppIcon, findNearestNode, genId, parseSvgMarkup, snapPoint, snapToGrid, useLibraryStorage, usePanZoom, useTheme };
3513
4068
  //# sourceMappingURL=index.mjs.map
3514
4069
  //# sourceMappingURL=index.mjs.map