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.js CHANGED
@@ -908,6 +908,53 @@ function ThemeToggle() {
908
908
  }
909
909
  );
910
910
  }
911
+ function useLibraryStorage(storageKey) {
912
+ const [libs, setLibs] = react.useState(() => {
913
+ if (!storageKey) return {};
914
+ try {
915
+ const raw = localStorage.getItem(storageKey);
916
+ return raw ? JSON.parse(raw) : {};
917
+ } catch {
918
+ return {};
919
+ }
920
+ });
921
+ const setAndPersist = react.useCallback(
922
+ (newLibs) => {
923
+ setLibs(newLibs);
924
+ if (!storageKey) return;
925
+ try {
926
+ if (Object.keys(newLibs).length === 0) {
927
+ localStorage.removeItem(storageKey);
928
+ } else {
929
+ localStorage.setItem(storageKey, JSON.stringify(newLibs));
930
+ }
931
+ } catch {
932
+ }
933
+ },
934
+ [storageKey]
935
+ );
936
+ return [libs, setAndPersist];
937
+ }
938
+
939
+ // src/components/VenueMapEditor/utils/svgParser.ts
940
+ var DANGEROUS_TAGS = /\b(script|iframe|object|embed|link|style|meta)\b/gi;
941
+ var DANGEROUS_ATTRS = /\bon\w+\s*=/gi;
942
+ var DANGEROUS_HREF = /\bhref\s*=\s*["']?\s*javascript:/gi;
943
+ var DANGEROUS_XLINK = /\bxlink:href\s*=\s*["']?\s*javascript:/gi;
944
+ function sanitize(html) {
945
+ return html.replace(DANGEROUS_TAGS, "").replace(DANGEROUS_ATTRS, "").replace(DANGEROUS_HREF, "").replace(DANGEROUS_XLINK, "");
946
+ }
947
+ var VIEWBOX_RE = /viewBox\s*=\s*"([^"]+)"/i;
948
+ var SVG_OPEN_END_RE = /<svg[^>]*>/i;
949
+ function parseSvgMarkup(markup) {
950
+ const viewBoxMatch = markup.match(VIEWBOX_RE);
951
+ const viewBox = viewBoxMatch?.[1] ?? "0 0 100 100";
952
+ const svgOpenMatch = markup.match(SVG_OPEN_END_RE);
953
+ const afterOpen = svgOpenMatch ? markup.slice(svgOpenMatch.index + svgOpenMatch[0].length) : markup;
954
+ const closeIdx = afterOpen.lastIndexOf("</svg>");
955
+ const inner = closeIdx >= 0 ? afterOpen.slice(0, closeIdx) : afterOpen;
956
+ return { viewBox, innerHtml: sanitize(inner) };
957
+ }
911
958
  function ToolButton({ active, disabled, title, onClick, children }) {
912
959
  return /* @__PURE__ */ jsxRuntime.jsx(
913
960
  "button",
@@ -937,7 +984,15 @@ function TypeChip({ typeDef, active, onClick }) {
937
984
  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"
938
985
  ].join(" "),
939
986
  children: [
940
- /* @__PURE__ */ jsxRuntime.jsx(
987
+ typeDef.shape === "svg" && typeDef.svgMarkup ? /* @__PURE__ */ jsxRuntime.jsx(
988
+ "svg",
989
+ {
990
+ viewBox: parseSvgMarkup(typeDef.svgMarkup).viewBox,
991
+ className: "w-2.5 h-2.5 shrink-0",
992
+ style: { color: typeDef.strokeColor },
993
+ dangerouslySetInnerHTML: { __html: parseSvgMarkup(typeDef.svgMarkup).innerHtml }
994
+ }
995
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
941
996
  "span",
942
997
  {
943
998
  className: "w-2.5 h-2.5 rounded-sm shrink-0",
@@ -972,6 +1027,19 @@ function Toolbar({
972
1027
  onLoadLibrary,
973
1028
  onRemoveLibraryGroup
974
1029
  }) {
1030
+ const [activeGroupId, setActiveGroupId] = react.useState(
1031
+ () => paletteGroups[0]?.id ?? null
1032
+ );
1033
+ react.useEffect(() => {
1034
+ if (paletteGroups.length === 0) {
1035
+ setActiveGroupId(null);
1036
+ return;
1037
+ }
1038
+ if (!paletteGroups.find((g) => g.id === activeGroupId)) {
1039
+ setActiveGroupId(paletteGroups[0].id);
1040
+ }
1041
+ }, [paletteGroups, activeGroupId]);
1042
+ const activeGroup = paletteGroups.find((g) => g.id === activeGroupId) ?? null;
975
1043
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col bg-white border-b border-slate-200 shadow-sm shrink-0", children: [
976
1044
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-0.5 px-2 py-1.5", children: [
977
1045
  /* @__PURE__ */ jsxRuntime.jsx(ToolButton, { title: "Seleccionar (V)", active: tool === "SELECT", onClick: () => onToolChange("SELECT"), children: /* @__PURE__ */ jsxRuntime.jsx(IconCursor, { className: "w-4 h-4" }) }),
@@ -1009,21 +1077,34 @@ function Toolbar({
1009
1077
  /* @__PURE__ */ jsxRuntime.jsx(ToolButton, { title: areaShape === "polygon" ? "Cambiar a rect\xE1ngulo" : "Cambiar a pol\xEDgono", onClick: () => onToggleAreaShape?.(), children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium", children: areaShape === "polygon" ? "Poly" : "Rect" }) })
1010
1078
  ] })
1011
1079
  ] }),
1012
- tool === "PLACE" && /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsxs("div", { className: "flex items-center shrink-0", children: [
1013
- gi > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-px self-stretch bg-slate-200 mx-1" }),
1014
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-0.5 px-1.5 shrink-0", children: [
1015
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] text-slate-400 font-medium whitespace-nowrap select-none", children: group.name }),
1016
- !group.isBase && onRemoveLibraryGroup && /* @__PURE__ */ jsxRuntime.jsx(
1017
- "button",
1018
- {
1019
- title: `Eliminar grupo "${group.name}"`,
1020
- onClick: () => onRemoveLibraryGroup(group.id),
1021
- className: "text-slate-300 hover:text-red-400 leading-none text-xs ml-0.5 transition-colors",
1022
- children: "\xD7"
1023
- }
1024
- )
1025
- ] }),
1026
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-1 px-1 py-1.5", children: group.types.map((typeDef) => /* @__PURE__ */ jsxRuntime.jsx(
1080
+ tool === "PLACE" && paletteGroups.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col border-t border-slate-100", children: [
1081
+ /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx("div", { className: "flex items-center shrink-0", children: /* @__PURE__ */ jsxRuntime.jsxs(
1082
+ "button",
1083
+ {
1084
+ onClick: () => setActiveGroupId(group.id),
1085
+ className: [
1086
+ "flex items-center gap-1 px-3 py-1 text-xs font-medium rounded-t border-x border-t transition-colors whitespace-nowrap",
1087
+ 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"
1088
+ ].join(" "),
1089
+ children: [
1090
+ group.name || "Sin nombre",
1091
+ !group.isBase && onRemoveLibraryGroup && /* @__PURE__ */ jsxRuntime.jsx(
1092
+ "span",
1093
+ {
1094
+ role: "button",
1095
+ title: `Eliminar "${group.name}"`,
1096
+ onClick: (e) => {
1097
+ e.stopPropagation();
1098
+ onRemoveLibraryGroup(group.id);
1099
+ },
1100
+ className: "ml-0.5 text-slate-300 hover:text-red-400 transition-colors leading-none",
1101
+ children: "\xD7"
1102
+ }
1103
+ )
1104
+ ]
1105
+ }
1106
+ ) }, group.id)) }),
1107
+ activeGroup && /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx(
1027
1108
  TypeChip,
1028
1109
  {
1029
1110
  typeDef,
@@ -1032,7 +1113,7 @@ function Toolbar({
1032
1113
  },
1033
1114
  typeDef.id
1034
1115
  )) })
1035
- ] }, group.id)) })
1116
+ ] })
1036
1117
  ] });
1037
1118
  }
1038
1119
  var ZOOM_MIN = 0.1;
@@ -1972,6 +2053,7 @@ function ElementNode({
1972
2053
  {
1973
2054
  d: typeDef.svgPath,
1974
2055
  fill: fillColor,
2056
+ fillRule: typeDef.fillRule ?? "nonzero",
1975
2057
  stroke: isSelected ? "#3b82f6" : typeDef.strokeColor,
1976
2058
  strokeWidth: isSelected ? customPath.strokeWidth * 1.5 : customPath.strokeWidth,
1977
2059
  style: { cursor: bodyCursor },
@@ -1979,6 +2061,28 @@ function ElementNode({
1979
2061
  onClick: handleBodyClick
1980
2062
  }
1981
2063
  ) }),
2064
+ typeDef.shape === "svg" && typeDef.svgMarkup && (() => {
2065
+ const parsed = parseSvgMarkup(typeDef.svgMarkup);
2066
+ const parts = parsed.viewBox.split(/[\s,]+/).map(Number);
2067
+ const vw = parts[2] ?? 100;
2068
+ const vh = parts[3] ?? 100;
2069
+ const sx = vw > 0 ? w / vw : 1;
2070
+ const sy = vh > 0 ? h / vh : 1;
2071
+ const avgScale = Math.sqrt(Math.abs(sx * sy)) || 1;
2072
+ return /* @__PURE__ */ jsxRuntime.jsx(
2073
+ "g",
2074
+ {
2075
+ transform: `translate(${x}, ${y}) scale(${sx}, ${sy})`,
2076
+ fill: fillColor,
2077
+ stroke: isSelected ? "#3b82f6" : typeDef.strokeColor,
2078
+ strokeWidth: isSelected ? sw / avgScale * 1.5 : sw / avgScale,
2079
+ style: { cursor: bodyCursor },
2080
+ onMouseDown: tool === "SELECT" && !onViewerClick ? handleBodyDown : void 0,
2081
+ onClick: handleBodyClick,
2082
+ dangerouslySetInnerHTML: { __html: parsed.innerHtml }
2083
+ }
2084
+ );
2085
+ })(),
1982
2086
  (element.label ?? typeDef.label) && /* @__PURE__ */ jsxRuntime.jsx(
1983
2087
  "text",
1984
2088
  {
@@ -2477,14 +2581,35 @@ function PropertiesPanel({
2477
2581
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-3 py-2 border-b border-slate-100 text-xs font-semibold text-slate-500 uppercase tracking-wide", children: "Propiedades" }),
2478
2582
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 flex flex-col gap-4 p-3", children: [
2479
2583
  typeDef && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
2480
- /* @__PURE__ */ jsxRuntime.jsx(
2584
+ typeDef.shape === "svg" ? /* @__PURE__ */ jsxRuntime.jsx(
2585
+ "svg",
2586
+ {
2587
+ viewBox: (() => {
2588
+ try {
2589
+ return typeDef.svgMarkup ? parseSvgMarkup(typeDef.svgMarkup).viewBox : "0 0 100 100";
2590
+ } catch {
2591
+ return "0 0 100 100";
2592
+ }
2593
+ })(),
2594
+ className: "w-3.5 h-3.5 shrink-0 border border-slate-300 rounded-sm",
2595
+ style: { color: typeDef.strokeColor },
2596
+ dangerouslySetInnerHTML: { __html: (() => {
2597
+ try {
2598
+ return typeDef.svgMarkup ? parseSvgMarkup(typeDef.svgMarkup).innerHtml : "";
2599
+ } catch {
2600
+ return "";
2601
+ }
2602
+ })() }
2603
+ }
2604
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
2481
2605
  "span",
2482
2606
  {
2483
2607
  className: "w-3.5 h-3.5 rounded-sm shrink-0 border",
2484
2608
  style: { background: typeDef.color, borderColor: typeDef.strokeColor }
2485
2609
  }
2486
2610
  ),
2487
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-slate-700 truncate", children: typeDef.label })
2611
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-slate-700 truncate", children: typeDef.label }),
2612
+ typeDef.shape === "svg" && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[9px] uppercase tracking-wide text-slate-400 font-medium ml-auto", children: "SVG" })
2488
2613
  ] }),
2489
2614
  /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex flex-col gap-0.5", children: [
2490
2615
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] font-medium text-slate-400 uppercase tracking-wide", children: "Etiqueta" }),
@@ -2819,7 +2944,23 @@ function createDefaultMap() {
2819
2944
  ]
2820
2945
  };
2821
2946
  }
2822
- var EMPTY_DOMAIN_CONFIG = { id: "__empty__", name: "", elementTypes: [] };
2947
+ var DEFAULT_LIBRARY_KEY = "venueMapEditor:libraries";
2948
+ function mergeLibraries(existing, incoming) {
2949
+ const result = { ...existing };
2950
+ for (const [groupId, incomingGroup] of Object.entries(incoming)) {
2951
+ if (result[groupId]) {
2952
+ const existingIds = new Set(result[groupId].objects.map((o) => o.id));
2953
+ const newObjects = incomingGroup.objects.filter((o) => !existingIds.has(o.id));
2954
+ result[groupId] = {
2955
+ ...result[groupId],
2956
+ objects: [...result[groupId].objects, ...newObjects]
2957
+ };
2958
+ } else {
2959
+ result[groupId] = incomingGroup;
2960
+ }
2961
+ }
2962
+ return result;
2963
+ }
2823
2964
  function updateFloor(map, updatedFloor) {
2824
2965
  return {
2825
2966
  ...map,
@@ -2859,7 +3000,9 @@ function polygonToRect(area) {
2859
3000
  };
2860
3001
  }
2861
3002
  function VenueMapEditor({
2862
- domainConfig = EMPTY_DOMAIN_CONFIG,
3003
+ domainConfigs,
3004
+ domainConfig,
3005
+ libraryStorageKey = DEFAULT_LIBRARY_KEY,
2863
3006
  initialMap,
2864
3007
  onChange,
2865
3008
  width = "100%",
@@ -2873,7 +3016,13 @@ function VenueMapEditor({
2873
3016
  onElementClick,
2874
3017
  onElementTypeClick
2875
3018
  }) {
3019
+ const effectiveConfigs = react.useMemo(() => {
3020
+ if (domainConfigs && domainConfigs.length > 0) return domainConfigs;
3021
+ if (domainConfig) return [domainConfig];
3022
+ return [];
3023
+ }, [domainConfigs, domainConfig]);
2876
3024
  const initialMapRef = react.useRef(initialMap ?? createDefaultMap());
3025
+ const [persistedLibs, setPersistedLibs] = useLibraryStorage(libraryStorageKey);
2877
3026
  const { map, canUndo, canRedo, push, replace, undo, redo } = useHistory(
2878
3027
  initialMapRef.current
2879
3028
  );
@@ -2889,31 +3038,40 @@ function VenueMapEditor({
2889
3038
  const resetViewRef = react.useRef(() => void 0);
2890
3039
  const importInputRef = react.useRef(null);
2891
3040
  const libraryInputRef = react.useRef(null);
3041
+ const effectiveLibs = react.useMemo(() => ({
3042
+ ...map.libraries ?? {},
3043
+ ...persistedLibs
3044
+ }), [map.libraries, persistedLibs]);
2892
3045
  const buildTypeDefs = react.useCallback(() => {
2893
- const m = new Map(domainConfig.elementTypes.map((t) => [t.id, t]));
2894
- const libs = map.libraries ?? {};
2895
- for (const group of Object.values(libs)) {
3046
+ const m = /* @__PURE__ */ new Map();
3047
+ for (const cfg of effectiveConfigs) {
3048
+ for (const t of cfg.elementTypes) {
3049
+ if (!m.has(t.id)) m.set(t.id, t);
3050
+ }
3051
+ }
3052
+ for (const group of Object.values(effectiveLibs)) {
2896
3053
  for (const t of group.objects) {
2897
3054
  if (!m.has(t.id)) m.set(t.id, t);
2898
3055
  }
2899
3056
  }
2900
3057
  return m;
2901
- }, [domainConfig, map.libraries]);
3058
+ }, [effectiveConfigs, effectiveLibs]);
2902
3059
  const elementTypeDefs = react.useRef(buildTypeDefs());
2903
3060
  react.useEffect(() => {
2904
3061
  elementTypeDefs.current = buildTypeDefs();
2905
3062
  }, [buildTypeDefs]);
2906
3063
  const paletteGroups = react.useMemo(() => {
2907
3064
  const groups = [];
2908
- if (domainConfig.elementTypes.length > 0) {
2909
- groups.push({ id: domainConfig.id, name: domainConfig.name, types: domainConfig.elementTypes, isBase: true });
3065
+ for (const cfg of effectiveConfigs) {
3066
+ if (cfg.elementTypes.length > 0) {
3067
+ groups.push({ id: cfg.id, name: cfg.name, types: cfg.elementTypes, isBase: true });
3068
+ }
2910
3069
  }
2911
- const libs = map.libraries ?? {};
2912
- for (const [gid, group] of Object.entries(libs)) {
3070
+ for (const [gid, group] of Object.entries(effectiveLibs)) {
2913
3071
  groups.push({ id: gid, name: group.name, types: group.objects, isBase: false });
2914
3072
  }
2915
3073
  return groups;
2916
- }, [domainConfig, map.libraries]);
3074
+ }, [effectiveConfigs, effectiveLibs]);
2917
3075
  react.useEffect(() => {
2918
3076
  if (activePlaceTypeId) return;
2919
3077
  const firstType = paletteGroups[0]?.types[0];
@@ -3053,22 +3211,27 @@ function VenueMapEditor({
3053
3211
  reader.onload = (e) => {
3054
3212
  try {
3055
3213
  const parsed = JSON.parse(e.target?.result);
3056
- const merged = { ...map.libraries ?? {}, ...parsed };
3057
- push({ ...map, libraries: merged });
3214
+ const mergedPersisted = mergeLibraries(persistedLibs, parsed);
3215
+ setPersistedLibs(mergedPersisted);
3216
+ const mergedMap = mergeLibraries(map.libraries ?? {}, parsed);
3217
+ push({ ...map, libraries: mergedMap });
3058
3218
  } catch {
3059
3219
  }
3060
3220
  };
3061
3221
  reader.readAsText(file);
3062
3222
  },
3063
- [map, push]
3223
+ [map, push, persistedLibs, setPersistedLibs]
3064
3224
  );
3065
3225
  const handleRemoveLibraryGroup = react.useCallback(
3066
3226
  (groupId) => {
3227
+ const newPersistedLibs = { ...persistedLibs };
3228
+ delete newPersistedLibs[groupId];
3229
+ setPersistedLibs(newPersistedLibs);
3067
3230
  const libs = { ...map.libraries ?? {} };
3068
3231
  delete libs[groupId];
3069
3232
  push({ ...map, libraries: Object.keys(libs).length > 0 ? libs : void 0 });
3070
3233
  },
3071
- [map, push]
3234
+ [map, push, persistedLibs, setPersistedLibs]
3072
3235
  );
3073
3236
  const DEFAULT_WALL_THICKNESS = 8;
3074
3237
  const handleAddWall = react.useCallback(
@@ -3514,6 +3677,398 @@ function VenueMapViewer({ elementStatus, onElementClick, ...rest }) {
3514
3677
  }
3515
3678
  );
3516
3679
  }
3680
+ var DEFAULT_ELEMENT = {
3681
+ id: "",
3682
+ label: "",
3683
+ shape: "rect",
3684
+ defaultWidth: 100,
3685
+ defaultHeight: 100,
3686
+ color: "#cccccc",
3687
+ strokeColor: "#000000"
3688
+ };
3689
+ var SHAPE_OPTIONS = [
3690
+ { value: "rect", label: "Rectangle" },
3691
+ { value: "circle", label: "Circle" },
3692
+ { value: "arrow", label: "Arrow" },
3693
+ { value: "path", label: "Path" },
3694
+ { value: "svg", label: "SVG Markup" }
3695
+ ];
3696
+ var ElementLibraryBuilder = () => {
3697
+ const [groups, setGroups] = react.useState([
3698
+ { internalId: crypto.randomUUID(), name: "defaultGroup", objects: [] }
3699
+ ]);
3700
+ const [activeGroupId, setActiveGroupId] = react.useState(groups[0].internalId);
3701
+ const [editingGroupId, setEditingGroupId] = react.useState(null);
3702
+ const [activeElementIndex, setActiveElementIndex] = react.useState(null);
3703
+ const [currentElement, setCurrentElement] = react.useState({ ...DEFAULT_ELEMENT, id: "rect_1", label: "New Rect" });
3704
+ const [downloadFileName, setDownloadFileName] = react.useState("libraries");
3705
+ const handleAddGroup = () => {
3706
+ const newGroupId = crypto.randomUUID();
3707
+ const newName = `group_${groups.length + 1}`;
3708
+ setGroups([...groups, { internalId: newGroupId, name: newName, objects: [] }]);
3709
+ setActiveGroupId(newGroupId);
3710
+ setActiveElementIndex(null);
3711
+ };
3712
+ const handleRemoveGroup = (id) => {
3713
+ const newGroups = groups.filter((g) => g.internalId !== id);
3714
+ setGroups(newGroups);
3715
+ if (activeGroupId === id) {
3716
+ if (newGroups.length > 0) {
3717
+ setActiveGroupId(newGroups[0].internalId);
3718
+ } else {
3719
+ setActiveGroupId("");
3720
+ }
3721
+ setActiveElementIndex(null);
3722
+ }
3723
+ };
3724
+ const activeGroup = groups.find((g) => g.internalId === activeGroupId);
3725
+ const handleSelectGroup = (gId) => {
3726
+ setActiveGroupId(gId);
3727
+ setActiveElementIndex(null);
3728
+ };
3729
+ const handleAddElement = () => {
3730
+ if (!activeGroup) return;
3731
+ const newEl = { ...DEFAULT_ELEMENT, id: `shape_${activeGroup.objects.length + 1}`, label: `Shape ${activeGroup.objects.length + 1}` };
3732
+ const updatedGroups = groups.map((g) => {
3733
+ if (g.internalId === activeGroupId) {
3734
+ return { ...g, objects: [...g.objects, newEl] };
3735
+ }
3736
+ return g;
3737
+ });
3738
+ setGroups(updatedGroups);
3739
+ setActiveElementIndex(activeGroup.objects.length);
3740
+ setCurrentElement(newEl);
3741
+ };
3742
+ const handleSelectElement = (idx) => {
3743
+ if (!activeGroup) return;
3744
+ setActiveElementIndex(idx);
3745
+ setCurrentElement(activeGroup.objects[idx]);
3746
+ };
3747
+ const handleRemoveElement = (idx) => {
3748
+ if (!activeGroup) return;
3749
+ const updatedGroups = groups.map((g) => {
3750
+ if (g.internalId === activeGroupId) {
3751
+ const newObjs = [...g.objects];
3752
+ newObjs.splice(idx, 1);
3753
+ return { ...g, objects: newObjs };
3754
+ }
3755
+ return g;
3756
+ });
3757
+ setGroups(updatedGroups);
3758
+ if (activeElementIndex === idx) {
3759
+ setActiveElementIndex(null);
3760
+ } else if (activeElementIndex !== null && activeElementIndex > idx) {
3761
+ setActiveElementIndex(activeElementIndex - 1);
3762
+ }
3763
+ };
3764
+ const handleSaveElement = () => {
3765
+ if (!activeGroup || activeElementIndex === null) return;
3766
+ const updatedGroups = groups.map((g) => {
3767
+ if (g.internalId === activeGroupId) {
3768
+ const newObjs = [...g.objects];
3769
+ newObjs[activeElementIndex] = { ...currentElement };
3770
+ return { ...g, objects: newObjs };
3771
+ }
3772
+ return g;
3773
+ });
3774
+ setGroups(updatedGroups);
3775
+ };
3776
+ const handleFieldChange = (field, value) => {
3777
+ setCurrentElement((prev) => ({ ...prev, [field]: value }));
3778
+ };
3779
+ const handleSvgMarkupChange = (value) => {
3780
+ handleFieldChange("svgMarkup", value);
3781
+ };
3782
+ const generatedLib = react.useMemo(() => {
3783
+ const lib = {};
3784
+ groups.forEach((g) => {
3785
+ lib[g.name] = {
3786
+ name: g.name,
3787
+ objects: g.objects
3788
+ };
3789
+ });
3790
+ return JSON.stringify(lib, null, 2);
3791
+ }, [groups]);
3792
+ const handleDownload = () => {
3793
+ const blob = new Blob([generatedLib], { type: "application/json" });
3794
+ const url = URL.createObjectURL(blob);
3795
+ const a = document.createElement("a");
3796
+ a.href = url;
3797
+ a.download = `${downloadFileName}.json`;
3798
+ document.body.appendChild(a);
3799
+ a.click();
3800
+ document.body.removeChild(a);
3801
+ URL.revokeObjectURL(url);
3802
+ };
3803
+ return /* @__PURE__ */ jsxRuntime.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: [
3804
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-1/4 flex flex-col gap-4 border-r dark:border-gray-700 pr-4", children: [
3805
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-2", children: [
3806
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
3807
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "font-bold", children: "Libraries (Groups)" }),
3808
+ /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "primary", onClick: handleAddGroup, children: "+ Group" })
3809
+ ] }),
3810
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-1 max-h-48 overflow-y-auto pr-1", children: groups.map((group) => /* @__PURE__ */ jsxRuntime.jsxs(
3811
+ "div",
3812
+ {
3813
+ 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"}`,
3814
+ onClick: () => handleSelectGroup(group.internalId),
3815
+ children: [
3816
+ editingGroupId === group.internalId ? /* @__PURE__ */ jsxRuntime.jsx(
3817
+ Input,
3818
+ {
3819
+ autoFocus: true,
3820
+ value: group.name,
3821
+ onChange: (e) => {
3822
+ setGroups(groups.map((g) => g.internalId === group.internalId ? { ...g, name: e.target.value } : g));
3823
+ },
3824
+ onBlur: () => setEditingGroupId(null),
3825
+ onKeyDown: (e) => e.key === "Enter" && setEditingGroupId(null)
3826
+ }
3827
+ ) : /* @__PURE__ */ jsxRuntime.jsx("span", { onDoubleClick: () => setEditingGroupId(group.internalId), children: group.name }),
3828
+ groups.length > 1 && /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: (e) => {
3829
+ e.stopPropagation();
3830
+ handleRemoveGroup(group.internalId);
3831
+ }, className: "text-red-500 text-xs", children: "x" })
3832
+ ]
3833
+ },
3834
+ group.internalId
3835
+ )) })
3836
+ ] }),
3837
+ /* @__PURE__ */ jsxRuntime.jsx("hr", { className: "dark:border-gray-700" }),
3838
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-2 flex-grow overflow-hidden", children: [
3839
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
3840
+ /* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "font-bold", children: [
3841
+ "Elements in ",
3842
+ activeGroup?.name || "?"
3843
+ ] }),
3844
+ /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "secondary", onClick: handleAddElement, disabled: !activeGroup, children: "+ Element" })
3845
+ ] }),
3846
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1 overflow-y-auto flex-grow pr-1", children: [
3847
+ activeGroup?.objects.map((el, i) => /* @__PURE__ */ jsxRuntime.jsxs(
3848
+ "div",
3849
+ {
3850
+ 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"}`,
3851
+ onClick: () => handleSelectElement(i),
3852
+ children: [
3853
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
3854
+ el.id,
3855
+ " (",
3856
+ el.shape,
3857
+ ")"
3858
+ ] }),
3859
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: (e) => {
3860
+ e.stopPropagation();
3861
+ handleRemoveElement(i);
3862
+ }, className: "text-red-500 text-xs", children: "x" })
3863
+ ]
3864
+ },
3865
+ i
3866
+ )),
3867
+ (!activeGroup || activeGroup.objects.length === 0) && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-400 dark:text-gray-500 italic text-xs", children: "No elements yet" })
3868
+ ] })
3869
+ ] })
3870
+ ] }),
3871
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 flex flex-col gap-4 px-2 overflow-y-auto", children: [
3872
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "font-bold text-lg", children: "Element Editor" }),
3873
+ activeElementIndex !== null ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-4 w-full max-w-2xl", children: [
3874
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-4", children: [
3875
+ /* @__PURE__ */ jsxRuntime.jsx(
3876
+ Input,
3877
+ {
3878
+ label: "Element ID (unique)",
3879
+ value: currentElement.id,
3880
+ onChange: (e) => handleFieldChange("id", e.target.value)
3881
+ }
3882
+ ),
3883
+ /* @__PURE__ */ jsxRuntime.jsx(
3884
+ Input,
3885
+ {
3886
+ label: "Label (display name)",
3887
+ value: currentElement.label,
3888
+ onChange: (e) => handleFieldChange("label", e.target.value)
3889
+ }
3890
+ )
3891
+ ] }),
3892
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-4", children: [
3893
+ /* @__PURE__ */ jsxRuntime.jsx(
3894
+ Select,
3895
+ {
3896
+ label: "Shape",
3897
+ options: SHAPE_OPTIONS,
3898
+ value: currentElement.shape,
3899
+ onChange: (e) => handleFieldChange("shape", e.target.value)
3900
+ }
3901
+ ),
3902
+ /* @__PURE__ */ jsxRuntime.jsx(
3903
+ Input,
3904
+ {
3905
+ label: "Icon (emoji or class)",
3906
+ value: currentElement.icon || "",
3907
+ onChange: (e) => handleFieldChange("icon", e.target.value)
3908
+ }
3909
+ )
3910
+ ] }),
3911
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-4", children: [
3912
+ /* @__PURE__ */ jsxRuntime.jsx(
3913
+ Input,
3914
+ {
3915
+ type: "number",
3916
+ label: "Default Width",
3917
+ value: currentElement.defaultWidth,
3918
+ onChange: (e) => handleFieldChange("defaultWidth", parseFloat(e.target.value) || 0)
3919
+ }
3920
+ ),
3921
+ /* @__PURE__ */ jsxRuntime.jsx(
3922
+ Input,
3923
+ {
3924
+ type: "number",
3925
+ label: "Default Height",
3926
+ value: currentElement.defaultHeight,
3927
+ onChange: (e) => handleFieldChange("defaultHeight", parseFloat(e.target.value) || 0)
3928
+ }
3929
+ )
3930
+ ] }),
3931
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-4", children: [
3932
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [
3933
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-semibold text-gray-700", children: "Fill Color" }),
3934
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
3935
+ /* @__PURE__ */ jsxRuntime.jsx(
3936
+ "input",
3937
+ {
3938
+ type: "color",
3939
+ className: "w-8 h-8 cursor-pointer rounded",
3940
+ value: currentElement.color,
3941
+ onChange: (e) => handleFieldChange("color", e.target.value)
3942
+ }
3943
+ ),
3944
+ /* @__PURE__ */ jsxRuntime.jsx(
3945
+ Input,
3946
+ {
3947
+ value: currentElement.color,
3948
+ onChange: (e) => handleFieldChange("color", e.target.value)
3949
+ }
3950
+ )
3951
+ ] })
3952
+ ] }),
3953
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [
3954
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-semibold text-gray-700", children: "Stroke Color" }),
3955
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
3956
+ /* @__PURE__ */ jsxRuntime.jsx(
3957
+ "input",
3958
+ {
3959
+ type: "color",
3960
+ className: "w-8 h-8 cursor-pointer rounded",
3961
+ value: currentElement.strokeColor,
3962
+ onChange: (e) => handleFieldChange("strokeColor", e.target.value)
3963
+ }
3964
+ ),
3965
+ /* @__PURE__ */ jsxRuntime.jsx(
3966
+ Input,
3967
+ {
3968
+ value: currentElement.strokeColor,
3969
+ onChange: (e) => handleFieldChange("strokeColor", e.target.value)
3970
+ }
3971
+ )
3972
+ ] })
3973
+ ] })
3974
+ ] }),
3975
+ currentElement.shape === "path" && /* @__PURE__ */ jsxRuntime.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: [
3976
+ /* @__PURE__ */ jsxRuntime.jsx("h4", { className: "font-semibold text-sm", children: "Path Config" }),
3977
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-4", children: [
3978
+ /* @__PURE__ */ jsxRuntime.jsx(
3979
+ Input,
3980
+ {
3981
+ label: "ViewBox",
3982
+ placeholder: "0 0 100 100",
3983
+ value: currentElement.viewBox || "",
3984
+ onChange: (e) => handleFieldChange("viewBox", e.target.value)
3985
+ }
3986
+ ),
3987
+ /* @__PURE__ */ jsxRuntime.jsx(
3988
+ Select,
3989
+ {
3990
+ label: "Fill Rule",
3991
+ options: [
3992
+ { value: "nonzero", label: "nonzero" },
3993
+ { value: "evenodd", label: "evenodd" }
3994
+ ],
3995
+ value: currentElement.fillRule || "nonzero",
3996
+ onChange: (e) => handleFieldChange("fillRule", e.target.value)
3997
+ }
3998
+ )
3999
+ ] }),
4000
+ /* @__PURE__ */ jsxRuntime.jsx(
4001
+ TextArea,
4002
+ {
4003
+ label: "SVG Path (d attribute)",
4004
+ placeholder: "M10 10 H 90 V 90 H 10 Z",
4005
+ value: currentElement.svgPath || "",
4006
+ onChange: (e) => handleFieldChange("svgPath", e.target.value),
4007
+ rows: 4
4008
+ }
4009
+ )
4010
+ ] }),
4011
+ currentElement.shape === "svg" && /* @__PURE__ */ jsxRuntime.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: [
4012
+ /* @__PURE__ */ jsxRuntime.jsx("h4", { className: "font-semibold text-sm", children: "SVG Markup (Autosanitized)" }),
4013
+ /* @__PURE__ */ jsxRuntime.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." }),
4014
+ /* @__PURE__ */ jsxRuntime.jsx(
4015
+ TextArea,
4016
+ {
4017
+ label: "raw <svg>...</svg>",
4018
+ value: currentElement.svgMarkup || "",
4019
+ onChange: (e) => handleSvgMarkupChange(e.target.value),
4020
+ rows: 6,
4021
+ placeholder: "<svg viewBox='0 0 100 100'><circle cx='50' cy='50' r='50'/></svg>"
4022
+ }
4023
+ )
4024
+ ] }),
4025
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-end gap-2 mt-4 pt-4 border-t dark:border-gray-700", children: /* @__PURE__ */ jsxRuntime.jsx(Button, { onClick: handleSaveElement, children: "Save Changes to Element" }) })
4026
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center h-full text-gray-400", children: "Select an element to edit or add a new one." })
4027
+ ] }),
4028
+ /* @__PURE__ */ jsxRuntime.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: [
4029
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between shrink-0", children: [
4030
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "font-bold", children: "Output JSON" }),
4031
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
4032
+ /* @__PURE__ */ jsxRuntime.jsx(
4033
+ Input,
4034
+ {
4035
+ value: downloadFileName,
4036
+ onChange: (e) => setDownloadFileName(e.target.value),
4037
+ placeholder: "filename",
4038
+ title: "Filename without extension"
4039
+ }
4040
+ ),
4041
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-500", children: ".json" }),
4042
+ /* @__PURE__ */ jsxRuntime.jsx(
4043
+ Button,
4044
+ {
4045
+ variant: "secondary",
4046
+ onClick: handleDownload,
4047
+ title: "Download JSON file",
4048
+ children: "Descargar"
4049
+ }
4050
+ ),
4051
+ /* @__PURE__ */ jsxRuntime.jsx(
4052
+ Button,
4053
+ {
4054
+ variant: "secondary",
4055
+ onClick: () => navigator.clipboard.writeText(generatedLib),
4056
+ children: "Copy"
4057
+ }
4058
+ )
4059
+ ] })
4060
+ ] }),
4061
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 overflow-hidden h-full pb-4", children: /* @__PURE__ */ jsxRuntime.jsx(
4062
+ TextArea,
4063
+ {
4064
+ readOnly: true,
4065
+ 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",
4066
+ value: generatedLib
4067
+ }
4068
+ ) })
4069
+ ] })
4070
+ ] });
4071
+ };
3517
4072
 
3518
4073
  exports.AddIcon = AddIcon;
3519
4074
  exports.Alerta = Alerta;
@@ -3549,6 +4104,7 @@ exports.CopyIcon = CopyIcon;
3549
4104
  exports.DeleteIcon = DeleteIcon;
3550
4105
  exports.DocumentIcon = DocumentIcon;
3551
4106
  exports.EditIcon = EditIcon;
4107
+ exports.ElementLibraryBuilder = ElementLibraryBuilder;
3552
4108
  exports.FacturacionIcon = FacturacionIcon;
3553
4109
  exports.FilterIcon = FilterIcon;
3554
4110
  exports.FolderIcon = FolderIcon;
@@ -3613,8 +4169,10 @@ exports.VenueMapViewer = VenueMapViewer;
3613
4169
  exports.WhatsAppIcon = WhatsAppIcon;
3614
4170
  exports.findNearestNode = findNearestNode;
3615
4171
  exports.genId = genId;
4172
+ exports.parseSvgMarkup = parseSvgMarkup;
3616
4173
  exports.snapPoint = snapPoint;
3617
4174
  exports.snapToGrid = snapToGrid;
4175
+ exports.useLibraryStorage = useLibraryStorage;
3618
4176
  exports.usePanZoom = usePanZoom;
3619
4177
  exports.useTheme = useTheme;
3620
4178
  //# sourceMappingURL=index.js.map