@lv-x-software-house/x_view 1.2.5-dev.4 → 1.2.5-dev.6

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 (3) hide show
  1. package/dist/index.js +222 -51
  2. package/dist/index.mjs +222 -51
  3. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -111,6 +111,7 @@ function ContextMenu({
111
111
  const [menuView, setMenuView] = useState("main");
112
112
  const [selectedAncestry, setSelectedAncestry] = useState(null);
113
113
  const [versionSubMenu, setVersionSubMenu] = useState(null);
114
+ const [labelSubMenu, setLabelSubMenu] = useState(null);
114
115
  const [isLinkCopied, setIsLinkCopied] = useState(false);
115
116
  const [selectedQuestStatus, setSelectedQuestStatus] = useState(null);
116
117
  const ability = useMemo(() => defineAbilityFor(userRole), [userRole]);
@@ -119,6 +120,7 @@ function ContextMenu({
119
120
  setMenuView("main");
120
121
  setSelectedAncestry(null);
121
122
  setVersionSubMenu(null);
123
+ setLabelSubMenu(null);
122
124
  setSelectedQuestStatus(null);
123
125
  }
124
126
  }, [data.visible, (_a = data.nodeData) == null ? void 0 : _a.id]);
@@ -134,7 +136,7 @@ function ContextMenu({
134
136
  if (left + w + 8 > vw) left = Math.max(8, vw - w - 8);
135
137
  if (top + h + 8 > vh) top = Math.max(8, vh - h - 8);
136
138
  setMenuPos({ left, top });
137
- }, [data, menuView, versionSubMenu, selectedQuestStatus]);
139
+ }, [data, menuView, versionSubMenu, labelSubMenu, selectedQuestStatus]);
138
140
  useEffect(() => {
139
141
  if (!data.visible) return;
140
142
  const handleClickOutside = (e) => {
@@ -186,7 +188,21 @@ function ContextMenu({
186
188
  var _a2;
187
189
  return (_a2 = c.targetNode) == null ? void 0 : _a2.is_quest;
188
190
  });
189
- const groupedConnections = commonConnections.reduce((acc, conn) => {
191
+ const getLabelForConnection = (conn) => {
192
+ if (conn.direction === "outgoing") return conn.link.source_label || null;
193
+ if (conn.direction === "incoming") return conn.link.target_label || null;
194
+ return null;
195
+ };
196
+ const commonWithLabel = commonConnections.filter((c) => getLabelForConnection(c));
197
+ const commonWithoutLabel = commonConnections.filter((c) => !getLabelForConnection(c));
198
+ const labelGroups = commonWithLabel.reduce((acc, conn) => {
199
+ const label = getLabelForConnection(conn);
200
+ if (!acc[label]) acc[label] = [];
201
+ acc[label].push(conn);
202
+ return acc;
203
+ }, {});
204
+ const labelGroupEntries = Object.entries(labelGroups).sort(([a], [b]) => a.localeCompare(b));
205
+ const groupedConnections = commonWithoutLabel.reduce((acc, conn) => {
190
206
  var _a2;
191
207
  const { targetNode } = conn;
192
208
  const groupingKey = ((_a2 = targetNode.version_node) == null ? void 0 : _a2.is_version) ? targetNode.version_node.parent_node : targetNode.id;
@@ -314,11 +330,32 @@ function ContextMenu({
314
330
  /* @__PURE__ */ React.createElement("span", { className: "flex-1 truncate" }, conn.targetNode.name)
315
331
  ))));
316
332
  };
333
+ const renderLabelSubMenuView = () => {
334
+ const group = labelSubMenu;
335
+ const isScrollable = group.connections.length > 10;
336
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-2 px-2 pt-1 pb-2" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-2 min-w-0" }, /* @__PURE__ */ React.createElement("button", { onClick: () => {
337
+ setLabelSubMenu(null);
338
+ setMenuView("connections");
339
+ }, className: "p-1 rounded-full hover:bg-white/10 text-slate-400 hover:text-white flex-shrink-0" }, /* @__PURE__ */ React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round" }, /* @__PURE__ */ React.createElement("polyline", { points: "15 18 9 12 15 6" }))), /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-1.5 min-w-0" }, /* @__PURE__ */ React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "text-violet-400 flex-shrink-0" }, /* @__PURE__ */ React.createElement("path", { d: "M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z" }), /* @__PURE__ */ React.createElement("line", { x1: "7", y1: "7", x2: "7.01", y2: "7" })), /* @__PURE__ */ React.createElement("p", { className: "text-[11px] uppercase tracking-wider text-violet-300/80 truncate", title: group.label }, group.label))), group.connections.length > 0 && /* @__PURE__ */ React.createElement("button", { onClick: () => handleExpandAndClose(group.connections.map((c) => c.link)), className: "px-2 py-0.5 text-xs bg-indigo-500/50 hover:bg-indigo-500/80 rounded-md transition-colors" }, "Expandir Todas")), /* @__PURE__ */ React.createElement("div", { className: `flex flex-col gap-1 ${isScrollable ? "max-h-[40vh] overflow-y-auto custom-scrollbar" : ""}` }, group.connections.map((conn) => /* @__PURE__ */ React.createElement(
340
+ "button",
341
+ {
342
+ key: conn.targetNode.id,
343
+ onClick: () => handleExpandAndClose([conn.link]),
344
+ className: baseButtonClass,
345
+ title: `Expandir conex\xE3o com ${conn.targetNode.name}`
346
+ },
347
+ /* @__PURE__ */ React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }, /* @__PURE__ */ React.createElement("line", { x1: "5", y1: "12", x2: "19", y2: "12" }), /* @__PURE__ */ React.createElement("polyline", { points: "12 5 19 12 12 19" })),
348
+ /* @__PURE__ */ React.createElement("span", { className: "flex-1 truncate" }, conn.targetNode.name)
349
+ ))));
350
+ };
317
351
  const renderConnectionsView = () => {
318
352
  if (versionSubMenu) {
319
353
  return renderVersionSubMenuView();
320
354
  }
321
- const totalItems = availableAncestries.length + finalRenderableConnections.length + (questConnections.length > 0 ? 1 : 0);
355
+ if (labelSubMenu) {
356
+ return renderLabelSubMenuView();
357
+ }
358
+ const totalItems = availableAncestries.length + labelGroupEntries.length + finalRenderableConnections.length + (questConnections.length > 0 ? 1 : 0);
322
359
  const isScrollable = totalItems > 10;
323
360
  return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between gap-2 px-2 pt-1 pb-2" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-2" }, /* @__PURE__ */ React.createElement("button", { onClick: () => setMenuView("main"), className: "p-1 rounded-full hover:bg-white/10 text-slate-400 hover:text-white" }, /* @__PURE__ */ React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round" }, /* @__PURE__ */ React.createElement("polyline", { points: "15 18 9 12 15 6" }))), /* @__PURE__ */ React.createElement("p", { className: "text-[11px] uppercase tracking-wider text-slate-400" }, "Conex\xF5es")), commonConnections.length > 0 && /* @__PURE__ */ React.createElement("button", { onClick: () => handleExpandAndClose(commonConnections.map((c) => c.link)), className: "px-2 py-0.5 text-xs bg-indigo-500/50 hover:bg-indigo-500/80 rounded-md transition-colors" }, "Expandir Todas")), /* @__PURE__ */ React.createElement("div", { className: `flex flex-col ${isScrollable ? "max-h-[40vh] overflow-y-auto custom-scrollbar" : ""}` }, availableAncestries.length > 0 && /* @__PURE__ */ React.createElement("div", { className: `flex flex-col gap-1 ${finalRenderableConnections.length > 0 || questConnections.length > 0 ? "mb-2" : ""}` }, /* @__PURE__ */ React.createElement("div", { className: "px-2 py-1 text-[11px] uppercase tracking-wider text-indigo-400/90" }, "Ancestralidades Salvas"), availableAncestries.map((anc) => /* @__PURE__ */ React.createElement(
324
361
  "button",
@@ -330,7 +367,20 @@ function ContextMenu({
330
367
  },
331
368
  /* @__PURE__ */ React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }, /* @__PURE__ */ React.createElement("path", { d: "M6 3v4a2 2 0 0 0 2 2h4" }), /* @__PURE__ */ React.createElement("path", { d: "M18 3v4a2 2 0 0 1-2 2h-4" }), /* @__PURE__ */ React.createElement("circle", { cx: "6", cy: "3", r: "2" }), /* @__PURE__ */ React.createElement("circle", { cx: "18", cy: "3", r: "2" }), /* @__PURE__ */ React.createElement("circle", { cx: "12", cy: "11", r: "2" }), /* @__PURE__ */ React.createElement("path", { d: "M12 13v6" }), /* @__PURE__ */ React.createElement("circle", { cx: "12", cy: "21", r: "2" })),
332
369
  /* @__PURE__ */ React.createElement("span", { className: "flex-1 truncate" }, anc.name || `Ancestralidade #${anc.ancestry_id.substring(0, 8)}`)
333
- ))), finalRenderableConnections.length > 0 && /* @__PURE__ */ React.createElement("div", { className: "flex flex-col gap-1" }, availableAncestries.length > 0 && /* @__PURE__ */ React.createElement("div", { className: "my-1 h-px w-full bg-white/10" }), finalRenderableConnections.map((group) => {
370
+ ))), labelGroupEntries.length > 0 && /* @__PURE__ */ React.createElement("div", { className: `flex flex-col gap-1 ${availableAncestries.length > 0 ? "mt-2" : ""} ${finalRenderableConnections.length > 0 || questConnections.length > 0 ? "mb-2" : ""}` }, availableAncestries.length > 0 && /* @__PURE__ */ React.createElement("div", { className: "my-1 h-px w-full bg-white/10" }), labelGroupEntries.map(([label, conns]) => /* @__PURE__ */ React.createElement(
371
+ "button",
372
+ {
373
+ key: label,
374
+ onClick: () => {
375
+ setLabelSubMenu({ label, connections: conns });
376
+ },
377
+ className: baseButtonClass,
378
+ title: `Ver grupo: ${label}`
379
+ },
380
+ /* @__PURE__ */ React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "text-violet-400" }, /* @__PURE__ */ React.createElement("path", { d: "M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z" }), /* @__PURE__ */ React.createElement("line", { x1: "7", y1: "7", x2: "7.01", y2: "7" })),
381
+ /* @__PURE__ */ React.createElement("span", { className: "flex-1 truncate" }, label),
382
+ /* @__PURE__ */ React.createElement("span", { className: "text-xs px-2 py-0.5 bg-violet-500/20 text-violet-300 rounded-full" }, conns.length)
383
+ ))), finalRenderableConnections.length > 0 && /* @__PURE__ */ React.createElement("div", { className: "flex flex-col gap-1" }, (availableAncestries.length > 0 || labelGroupEntries.length > 0) && /* @__PURE__ */ React.createElement("div", { className: "my-1 h-px w-full bg-white/10" }), finalRenderableConnections.map((group) => {
334
384
  if (group.isVersionGroup) {
335
385
  return /* @__PURE__ */ React.createElement(
336
386
  "button",
@@ -666,7 +716,7 @@ function XViewSidebar({
666
716
  "div",
667
717
  {
668
718
  ref: containerRef,
669
- className: "ui-overlay fixed left-0 top-0 h-full w-[min(92vw,320px)] z-40",
719
+ className: "ui-overlay fixed left-0 top-0 h-[100dvh] w-[min(92vw,320px)] z-40 overflow-hidden",
670
720
  onPointerDown: swallow,
671
721
  onPointerMove: swallow,
672
722
  onPointerUp: swallow,
@@ -675,7 +725,7 @@ function XViewSidebar({
675
725
  onContextMenu: swallow,
676
726
  onDoubleClick: swallow
677
727
  },
678
- /* @__PURE__ */ React2.createElement("div", { className: "h-full flex flex-col bg-slate-950/80 backdrop-blur-xl border-r border-white/10 shadow-[0_0_40px_rgba(0,0,0,0.45)]" }, /* @__PURE__ */ React2.createElement("div", { className: "relative px-4 py-3 border-b border-white/10 flex items-center gap-2" }, /* @__PURE__ */ React2.createElement("span", { className: "inline-flex h-2 w-2 rounded-full bg-indigo-400/80 shadow-[0_0_10px_2px_rgba(99,102,241,0.55)]" }), /* @__PURE__ */ React2.createElement("h3", { className: "text-sm font-medium text-slate-100" }, "Ferramentas"), /* @__PURE__ */ React2.createElement(
728
+ /* @__PURE__ */ React2.createElement("div", { className: "h-full flex flex-col overflow-hidden bg-slate-950/80 backdrop-blur-xl border-r border-white/10 shadow-[0_0_40px_rgba(0,0,0,0.45)]" }, /* @__PURE__ */ React2.createElement("div", { className: "relative px-4 py-3 border-b border-white/10 flex items-center gap-2" }, /* @__PURE__ */ React2.createElement("span", { className: "inline-flex h-2 w-2 rounded-full bg-indigo-400/80 shadow-[0_0_10px_2px_rgba(99,102,241,0.55)]" }), /* @__PURE__ */ React2.createElement("h3", { className: "text-sm font-medium text-slate-100" }, "Ferramentas"), /* @__PURE__ */ React2.createElement(
679
729
  "button",
680
730
  {
681
731
  className: "ml-auto p-2 rounded-md text-slate-400 hover:text-white hover:bg-white/10 transition-colors",
@@ -732,7 +782,7 @@ function XViewSidebar({
732
782
  autoComplete: "off"
733
783
  }
734
784
  )
735
- ), showList && /* @__PURE__ */ React2.createElement("ul", { className: "custom-scrollbar absolute mt-1 z-10 w-full max-h-[28rem] overflow-y-auto rounded-lg bg-slate-900/95 border border-white/10 shadow-xl" }, filteredAndSorted.length > 0 ? filteredAndSorted.map((n) => {
785
+ ), showList && /* @__PURE__ */ React2.createElement("ul", { className: "custom-scrollbar absolute mt-1 z-10 w-full max-h-[min(448px,50dvh)] overflow-y-auto rounded-lg bg-slate-900/95 border border-white/10 shadow-xl" }, filteredAndSorted.length > 0 ? filteredAndSorted.map((n) => {
736
786
  const inView = isNodeInView(n.id);
737
787
  const active = selectedNodeId === n.id;
738
788
  const typeLabel = Array.isArray(n.type) ? n.type.join(", ") : n.type;
@@ -3108,6 +3158,7 @@ function calculateNodePositions(nodes) {
3108
3158
  return positions;
3109
3159
  }
3110
3160
  function updateTooltip({ tooltipEl, hoveredNode, hoveredLink, camera, mountEl, isSceneBusy, parentData, ancestryData }) {
3161
+ var _a, _b;
3111
3162
  if (!tooltipEl || !camera || !mountEl) return;
3112
3163
  let content = "";
3113
3164
  let positionTarget = null;
@@ -3120,17 +3171,21 @@ function updateTooltip({ tooltipEl, hoveredNode, hoveredLink, camera, mountEl, i
3120
3171
  content = generateTooltipHtml(hoveredNode.userData, parentData, ancestryData);
3121
3172
  }
3122
3173
  } else if (hoveredLink && !isSceneBusy) {
3123
- currentId = `link_${hoveredLink.userData.id}`;
3124
- if (hoveredLink.userData.isCurved) {
3125
- const positions = hoveredLink.geometry.attributes.position.array;
3126
- const midIndex = Math.floor(positions.length / 2 / 3) * 3;
3127
- positionTarget = new THREE2.Vector3(positions[midIndex], positions[midIndex + 1], positions[midIndex + 2]);
3128
- } else {
3129
- positionTarget = new THREE2.Vector3().addVectors(hoveredLink.userData.sourceNode.position, hoveredLink.userData.targetNode.position).multiplyScalar(0.5);
3130
- }
3131
- isLink = true;
3132
- if (tooltipEl.dataset.currentId !== currentId) {
3133
- content = hoveredLink.userData.isAncestryLink ? generateLinkTooltipHtml(hoveredLink.userData.relationship || {}, parentData, ancestryData) : generateLinkTooltipHtml(hoveredLink.userData, parentData, ancestryData);
3174
+ const linkData = hoveredLink.userData.isAncestryLink ? hoveredLink.userData.relationship || {} : hoveredLink.userData;
3175
+ const hasContent = ((_a = linkData.name) == null ? void 0 : _a.trim()) || ((_b = linkData.description) == null ? void 0 : _b.trim());
3176
+ if (hasContent) {
3177
+ currentId = `link_${hoveredLink.userData.id}`;
3178
+ if (hoveredLink.userData.isCurved) {
3179
+ const positions = hoveredLink.geometry.attributes.position.array;
3180
+ const midIndex = Math.floor(positions.length / 2 / 3) * 3;
3181
+ positionTarget = new THREE2.Vector3(positions[midIndex], positions[midIndex + 1], positions[midIndex + 2]);
3182
+ } else {
3183
+ positionTarget = new THREE2.Vector3().addVectors(hoveredLink.userData.sourceNode.position, hoveredLink.userData.targetNode.position).multiplyScalar(0.5);
3184
+ }
3185
+ isLink = true;
3186
+ if (tooltipEl.dataset.currentId !== currentId) {
3187
+ content = generateLinkTooltipHtml(linkData, parentData, ancestryData);
3188
+ }
3134
3189
  }
3135
3190
  }
3136
3191
  if (positionTarget) {
@@ -3439,9 +3494,9 @@ function CustomPropertyDisplay({
3439
3494
  };
3440
3495
  const handleRemoveListItem = (j) => setTempProp((p) => ({ ...p, value: p.value.filter((_, k) => k !== j) }));
3441
3496
  const handleListItemChange = (j, f, v) => setTempProp((p) => {
3442
- const newValue = [...p.value];
3443
- newValue[j] = { ...newValue[j], [f]: v };
3444
- return { ...p, value: newValue };
3497
+ const newValue2 = [...p.value];
3498
+ newValue2[j] = { ...newValue2[j], [f]: v };
3499
+ return { ...p, value: newValue2 };
3445
3500
  });
3446
3501
  const baseInput = "w-full bg-slate-800/70 p-2 text-sm rounded-lg border border-white/10 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-400/60 transition-all duration-150";
3447
3502
  const renderEditView = () => {
@@ -3495,14 +3550,14 @@ function CustomPropertyDisplay({
3495
3550
  const inputClass = `${baseInput} ${noSpinnerClass}`;
3496
3551
  const handleDateTypeChange = (newDateType) => {
3497
3552
  var _a3, _b2, _c2;
3498
- let newValue = { type: newDateType };
3553
+ let newValue2 = { type: newDateType };
3499
3554
  if (newDateType === "Date Interval") {
3500
- newValue.start = ((_a3 = tempProp.value) == null ? void 0 : _a3.start) || "";
3501
- newValue.end = ((_b2 = tempProp.value) == null ? void 0 : _b2.end) || "";
3555
+ newValue2.start = ((_a3 = tempProp.value) == null ? void 0 : _a3.start) || "";
3556
+ newValue2.end = ((_b2 = tempProp.value) == null ? void 0 : _b2.end) || "";
3502
3557
  } else {
3503
- newValue.value = ((_c2 = tempProp.value) == null ? void 0 : _c2.type) === newDateType ? tempProp.value.value : "";
3558
+ newValue2.value = ((_c2 = tempProp.value) == null ? void 0 : _c2.type) === newDateType ? tempProp.value.value : "";
3504
3559
  }
3505
- handlePropChange("value", newValue);
3560
+ handlePropChange("value", newValue2);
3506
3561
  };
3507
3562
  const handleDateValueChange = (dateField, dateValue) => {
3508
3563
  handlePropChange("value", { ...tempProp.value, [dateField]: dateValue });
@@ -5409,7 +5464,7 @@ function AncestryRelationshipPanel({
5409
5464
  }, 100);
5410
5465
  };
5411
5466
  const handleRemoveProp = (i) => setCustomProps((p) => p.filter((_, idx) => idx !== i));
5412
- const handleUpdateProp = (index, updatedProp) => {
5467
+ const handleUpdateProp2 = (index, updatedProp) => {
5413
5468
  setCustomProps((props) => {
5414
5469
  const newProps = [...props];
5415
5470
  newProps[index] = updatedProp;
@@ -5521,7 +5576,7 @@ function AncestryRelationshipPanel({
5521
5576
  {
5522
5577
  key: prop.id,
5523
5578
  prop,
5524
- onUpdate: (updatedProp) => handleUpdateProp(idx, updatedProp),
5579
+ onUpdate: (updatedProp) => handleUpdateProp2(idx, updatedProp),
5525
5580
  onRemove: () => handleRemoveProp(idx),
5526
5581
  onOpenImageViewer,
5527
5582
  unavailableTypes: currentUsedTypes.filter((t) => t !== prop.type),
@@ -5731,6 +5786,7 @@ var NodeItem = ({ nodeData, onSelectParent, onViewSelect, highlightedPathIds = [
5731
5786
  e.stopPropagation();
5732
5787
  if (isEditable) {
5733
5788
  onSelectParent(itemId);
5789
+ setHasUnsavedChanges(true);
5734
5790
  } else if (onViewSelect) {
5735
5791
  onViewSelect(itemId);
5736
5792
  }
@@ -5743,9 +5799,11 @@ var NodeItem = ({ nodeData, onSelectParent, onViewSelect, highlightedPathIds = [
5743
5799
  level > 0 && isEditable && /* @__PURE__ */ React10.createElement("div", { className: "flex items-center gap-1 animate-in fade-in duration-200" }, !nodeData.is_section && /* @__PURE__ */ React10.createElement("button", { onClick: (e) => {
5744
5800
  e.stopPropagation();
5745
5801
  onEditRelationship(path, nodeData.relationship || {});
5802
+ setHasUnsavedChanges(true);
5746
5803
  }, className: "w-6 h-6 grid place-content-center rounded-full hover:bg-cyan-500/20 text-cyan-400 transition-colors flex-shrink-0", title: "Editar detalhes da rela\xE7\xE3o" }, /* @__PURE__ */ React10.createElement(FiEdit23, { size: 12 })), /* @__PURE__ */ React10.createElement("button", { onClick: (e) => {
5747
5804
  e.stopPropagation();
5748
5805
  onRemoveNode(path);
5806
+ setHasUnsavedChanges(true);
5749
5807
  }, className: "w-6 h-6 grid place-content-center rounded-full hover:bg-red-500/20 text-red-400 text-lg transition-colors flex-shrink-0", title: "Remover Node" }, "\xD7"))
5750
5808
  ), hasChildren && /* @__PURE__ */ React10.createElement("div", { className: "mt-2 space-y-2 pl-8" }, nodeData.children.map((child, index) => /* @__PURE__ */ React10.createElement(
5751
5809
  NodeItem,
@@ -5801,6 +5859,7 @@ function CreateAncestryPanel({
5801
5859
  } = ancestryMode;
5802
5860
  const [isSaving, setIsSaving] = useState11(false);
5803
5861
  const [isLinkCopied, setIsLinkCopied] = useState11(false);
5862
+ const [hasUnsavedChanges, setHasUnsavedChanges2] = useState11(false);
5804
5863
  const [showDeleteBranchConfirm, setShowDeleteBranchConfirm] = useState11(false);
5805
5864
  const handleCopyLink = (e) => {
5806
5865
  e.stopPropagation();
@@ -5868,12 +5927,14 @@ function CreateAncestryPanel({
5868
5927
  };
5869
5928
  const handleSelectAncestryParent = (nodeId, isAbstraction = false) => {
5870
5929
  setAncestryMode((prev) => isAbstraction ? { ...prev, selectedAbstractionParentId: nodeId } : { ...prev, selectedParentId: nodeId });
5930
+ setHasUnsavedChanges2(true);
5871
5931
  };
5872
5932
  const handleToggleAddMode = (isAbstraction = false) => {
5873
5933
  if (isAbstraction && !ancestryMode.isAddingAbstractionNodes) {
5874
5934
  setTargetRenderNodeId(null);
5875
5935
  }
5876
5936
  setAncestryMode((prev) => isAbstraction ? { ...prev, isAddingAbstractionNodes: !prev.isAddingAbstractionNodes } : { ...prev, isAddingNodes: !prev.isAddingNodes });
5937
+ setHasUnsavedChanges2(true);
5877
5938
  };
5878
5939
  const handleRemoveNode = useCallback2((pathToRemove, isAbstraction = false) => {
5879
5940
  if (!Array.isArray(pathToRemove) || pathToRemove.length === 0) return;
@@ -5891,6 +5952,7 @@ function CreateAncestryPanel({
5891
5952
  const indexToRemove = pathToRemove[pathToRemove.length - 1];
5892
5953
  if (currentParent.children && currentParent.children.length > indexToRemove) {
5893
5954
  currentParent.children.splice(indexToRemove, 1);
5955
+ setHasUnsavedChanges2(true);
5894
5956
  }
5895
5957
  return { ...prev, [treeKey]: newTree };
5896
5958
  });
@@ -5949,6 +6011,7 @@ function CreateAncestryPanel({
5949
6011
  updateGlobalTree(rootTreeClone);
5950
6012
  }
5951
6013
  setAncestryMode((prev) => ({ ...prev, [treeKey]: rootTreeClone }));
6014
+ setHasUnsavedChanges2(true);
5952
6015
  } else {
5953
6016
  alert("N\xE3o \xE9 poss\xEDvel mover um node para dentro de seus pr\xF3prios descendentes.");
5954
6017
  }
@@ -6021,6 +6084,7 @@ function CreateAncestryPanel({
6021
6084
  const handleAddProp = () => {
6022
6085
  const newProp = createNewCustomProperty(customProps);
6023
6086
  setCustomProps((p) => [...p, newProp]);
6087
+ setHasUnsavedChanges2(true);
6024
6088
  setTimeout(() => {
6025
6089
  var _a;
6026
6090
  (_a = propsEndRef.current) == null ? void 0 : _a.scrollIntoView({ behavior: "smooth", block: "center" });
@@ -6029,11 +6093,13 @@ function CreateAncestryPanel({
6029
6093
  const handleRemoveProp = (i) => {
6030
6094
  const newProps = customProps.filter((_, idx) => idx !== i);
6031
6095
  setCustomProps(newProps);
6096
+ setHasUnsavedChanges2(true);
6032
6097
  };
6033
- const handleUpdateProp = (index, updatedProp) => {
6098
+ const handleUpdateProp2 = (index, updatedProp) => {
6034
6099
  const newProps = [...customProps];
6035
6100
  newProps[index] = updatedProp;
6036
6101
  setCustomProps(newProps);
6102
+ setHasUnsavedChanges2(true);
6037
6103
  };
6038
6104
  const currentUsedTypes = customProps.map((p) => p.type).filter((t) => UNIQUE_PROP_TYPES.includes(t));
6039
6105
  useEffect10(() => {
@@ -6143,6 +6209,7 @@ function CreateAncestryPanel({
6143
6209
  updateGlobalTree(rootTreeClone);
6144
6210
  setBranchStack([...branchStack]);
6145
6211
  setIsPickerOpen(false);
6212
+ setHasUnsavedChanges2(true);
6146
6213
  try {
6147
6214
  setIsSaving(true);
6148
6215
  const rootProps = extractCustomPropsFromNode(ancestryMode);
@@ -6156,6 +6223,7 @@ function CreateAncestryPanel({
6156
6223
  rootExtras
6157
6224
  );
6158
6225
  setLastSavedSnapshot(takeSnapshot(rootTreeClone, ancestryName, description, processedSections, [], isPrivate, ancestryMode.abstraction_tree));
6226
+ setHasUnsavedChanges2(false);
6159
6227
  if (onRenderFullAncestry) {
6160
6228
  const fullTreePayload = {
6161
6229
  ancestry_id: ancestryMode.currentAncestryId || "temp_root",
@@ -6198,6 +6266,7 @@ function CreateAncestryPanel({
6198
6266
  if (branchIndex !== -1) {
6199
6267
  foundParentPath.node.parallel_branches.splice(branchIndex, 1);
6200
6268
  updateGlobalTree(rootTreeClone);
6269
+ setHasUnsavedChanges2(true);
6201
6270
  try {
6202
6271
  setIsSaving(true);
6203
6272
  const currentRootProps = extractCustomPropsFromNode(ancestryMode);
@@ -6219,6 +6288,7 @@ function CreateAncestryPanel({
6219
6288
  isPrivate,
6220
6289
  ancestryMode.abstraction_tree
6221
6290
  ));
6291
+ setHasUnsavedChanges2(false);
6222
6292
  if (onClearAncestryVisuals) {
6223
6293
  onClearAncestryVisuals(currentStep.branchId);
6224
6294
  }
@@ -6251,6 +6321,7 @@ function CreateAncestryPanel({
6251
6321
  if (branchIndex !== -1) {
6252
6322
  foundParentPath.node.parallel_branches.splice(branchIndex, 1);
6253
6323
  updateGlobalTree(rootTreeClone);
6324
+ setHasUnsavedChanges2(true);
6254
6325
  try {
6255
6326
  setIsSaving(true);
6256
6327
  const currentRootProps = extractCustomPropsFromNode(ancestryMode);
@@ -6272,6 +6343,7 @@ function CreateAncestryPanel({
6272
6343
  isPrivate,
6273
6344
  ancestryMode.abstraction_tree
6274
6345
  ));
6346
+ setHasUnsavedChanges2(false);
6275
6347
  if (onClearAncestryVisuals) {
6276
6348
  onClearAncestryVisuals(currentStep.branchId);
6277
6349
  }
@@ -6533,6 +6605,7 @@ function CreateAncestryPanel({
6533
6605
  }
6534
6606
  setBranchStack(parentStack);
6535
6607
  setTargetScrollSectionId(targetFocusId);
6608
+ setHasUnsavedChanges2(true);
6536
6609
  if (onRenderFullAncestry) {
6537
6610
  const parentStack2 = currentStack;
6538
6611
  const rotation = parentStack2.reduce((acc, step) => {
@@ -6594,7 +6667,6 @@ function CreateAncestryPanel({
6594
6667
  direction,
6595
6668
  tree: {
6596
6669
  node: nodeData,
6597
- node_id: nodeId,
6598
6670
  children: [],
6599
6671
  relationship: {}
6600
6672
  }
@@ -6616,6 +6688,7 @@ function CreateAncestryPanel({
6616
6688
  savedMaxIndex: parentIndexToSave,
6617
6689
  entryDirection: direction
6618
6690
  }]);
6691
+ setHasUnsavedChanges2(true);
6619
6692
  if (branch && branch.tree && onRenderFullAncestry) {
6620
6693
  const branchAncestryObj = {
6621
6694
  ancestry_id: branch.id,
@@ -6666,6 +6739,10 @@ function CreateAncestryPanel({
6666
6739
  const currentInputName = overrides.ancestryName !== void 0 ? overrides.ancestryName : ancestryName;
6667
6740
  const currentInputDesc = overrides.description !== void 0 ? overrides.description : description;
6668
6741
  const currentInputSections = overrides.existingSections !== void 0 ? overrides.existingSections : existingSections;
6742
+ if (!keepOpen && !hasUnsavedChanges) {
6743
+ onClose();
6744
+ return;
6745
+ }
6669
6746
  if (!currentInputName.trim()) {
6670
6747
  alert("O nome n\xE3o pode estar vazio.");
6671
6748
  return;
@@ -6719,6 +6796,7 @@ function CreateAncestryPanel({
6719
6796
  isPrivate,
6720
6797
  ancestryMode.abstraction_tree
6721
6798
  ));
6799
+ setHasUnsavedChanges2(false);
6722
6800
  if (onRenderFullAncestry) {
6723
6801
  const rotation = branchStack.reduce((acc, step) => {
6724
6802
  return acc + (step.entryDirection === "left" ? -Math.PI / 2 : Math.PI / 2);
@@ -6770,6 +6848,7 @@ function CreateAncestryPanel({
6770
6848
  updatedRootTree,
6771
6849
  extrasObj
6772
6850
  );
6851
+ setHasUnsavedChanges2(false);
6773
6852
  setLastSavedSnapshot(takeSnapshot(
6774
6853
  updatedRootTree,
6775
6854
  currentInputName,
@@ -6792,6 +6871,7 @@ function CreateAncestryPanel({
6792
6871
  const newTreeString = JSON.stringify(newRootTree);
6793
6872
  if (currentTreeString !== newTreeString) {
6794
6873
  updateGlobalTree(newRootTree);
6874
+ setHasUnsavedChanges2(true);
6795
6875
  }
6796
6876
  }, [description, existingSections]);
6797
6877
  const handleTriggerFullRender = () => {
@@ -6814,6 +6894,7 @@ function CreateAncestryPanel({
6814
6894
  };
6815
6895
  const handleSaveDescriptionInline = (newDesc) => {
6816
6896
  setDescription(newDesc);
6897
+ setHasUnsavedChanges2(true);
6817
6898
  handleLocalSave(true, { description: newDesc });
6818
6899
  };
6819
6900
  const swallow = (e) => e.stopPropagation();
@@ -6943,7 +7024,11 @@ function CreateAncestryPanel({
6943
7024
  {
6944
7025
  type: "text",
6945
7026
  value: ancestryName,
6946
- onChange: (e) => setAncestryName(e.target.value),
7027
+ onChange: (e) => {
7028
+ setAncestryName(e.target.value);
7029
+ setHasUnsavedChanges2(true);
7030
+ },
7031
+ readOnly: isContextLinked,
6947
7032
  placeholder: "Nome da Ancestralidade",
6948
7033
  className: "text-xl sm:text-2xl font-semibold tracking-tight bg-transparent border-none p-0 focus:ring-2 focus:ring-indigo-500 rounded-md -ml-1.5 px-1.5 w-full outline-none transition-all focus:bg-slate-800/70"
6949
7034
  }
@@ -7080,7 +7165,7 @@ function CreateAncestryPanel({
7080
7165
  {
7081
7166
  key: prop.id,
7082
7167
  prop,
7083
- onUpdate: (updatedProp) => handleUpdateProp(idx, updatedProp),
7168
+ onUpdate: (updatedProp) => handleUpdateProp2(idx, updatedProp),
7084
7169
  onRemove: () => handleRemoveProp(idx),
7085
7170
  unavailableTypes: currentUsedTypes.filter((t) => t !== prop.type),
7086
7171
  onUploadFile
@@ -7562,7 +7647,7 @@ function InSceneCreationForm({
7562
7647
  }, 100);
7563
7648
  };
7564
7649
  const handleRemoveProp = (index) => setCustomProps(customProps.filter((_, i) => i !== index));
7565
- const handleUpdateProp = (index, updatedProp) => {
7650
+ const handleUpdateProp2 = (index, updatedProp) => {
7566
7651
  const newProps = [...customProps];
7567
7652
  newProps[index] = updatedProp;
7568
7653
  setCustomProps(newProps);
@@ -7592,9 +7677,9 @@ function InSceneCreationForm({
7592
7677
  };
7593
7678
  const handleToggleImageMode = () => {
7594
7679
  var _a2, _b;
7595
- const newValue = !useImageAsTexture;
7596
- setUseImageAsTexture(newValue);
7597
- if (newValue) {
7680
+ const newValue2 = !useImageAsTexture;
7681
+ setUseImageAsTexture(newValue2);
7682
+ if (newValue2) {
7598
7683
  const firstImageProp = customProps.find((p) => p.type === "images");
7599
7684
  if (firstImageProp && ((_b = (_a2 = firstImageProp.value) == null ? void 0 : _a2[0]) == null ? void 0 : _b.value)) {
7600
7685
  const url = firstImageProp.value[0].value;
@@ -7800,7 +7885,7 @@ function InSceneCreationForm({
7800
7885
  {
7801
7886
  key: prop.id,
7802
7887
  prop,
7803
- onUpdate: (updatedProp) => handleUpdateProp(index, updatedProp),
7888
+ onUpdate: (updatedProp) => handleUpdateProp2(index, updatedProp),
7804
7889
  onRemove: () => handleRemoveProp(index),
7805
7890
  onOpenImageViewer,
7806
7891
  unavailableTypes: currentUsedTypes.filter((t) => t !== prop.type),
@@ -7881,7 +7966,7 @@ function InSceneVersionForm({
7881
7966
  }
7882
7967
  setCustomProps(customProps.filter((_, i) => i !== index));
7883
7968
  };
7884
- const handleUpdateProp = (index, updatedProp) => {
7969
+ const handleUpdateProp2 = (index, updatedProp) => {
7885
7970
  const newProps = [...customProps];
7886
7971
  newProps[index] = updatedProp;
7887
7972
  setCustomProps(newProps);
@@ -7892,9 +7977,9 @@ function InSceneVersionForm({
7892
7977
  };
7893
7978
  const handleToggleImageMode = () => {
7894
7979
  var _a, _b;
7895
- const newValue = !useImageAsTexture;
7896
- setUseImageAsTexture(newValue);
7897
- if (newValue) {
7980
+ const newValue2 = !useImageAsTexture;
7981
+ setUseImageAsTexture(newValue2);
7982
+ if (newValue2) {
7898
7983
  const firstImageProp = customProps.find((p) => p.type === "images");
7899
7984
  if (firstImageProp && ((_b = (_a = firstImageProp.value) == null ? void 0 : _a[0]) == null ? void 0 : _b.value)) {
7900
7985
  const url = firstImageProp.value[0].value;
@@ -8019,7 +8104,7 @@ function InSceneVersionForm({
8019
8104
  {
8020
8105
  key: prop.id,
8021
8106
  prop,
8022
- onUpdate: (updatedProp) => handleUpdateProp(index, updatedProp),
8107
+ onUpdate: (updatedProp) => handleUpdateProp2(index, updatedProp),
8023
8108
  onRemove: () => handleRemoveProp(index),
8024
8109
  onOpenImageViewer,
8025
8110
  unavailableTypes: currentUsedTypes.filter((t) => t !== prop.type),
@@ -8094,7 +8179,7 @@ function InSceneQuestForm({
8094
8179
  }, 100);
8095
8180
  };
8096
8181
  const handleRemoveProp = (index) => setCustomProps(customProps.filter((_, i) => i !== index));
8097
- const handleUpdateProp = (index, updatedProp) => {
8182
+ const handleUpdateProp2 = (index, updatedProp) => {
8098
8183
  const newProps = [...customProps];
8099
8184
  newProps[index] = updatedProp;
8100
8185
  setCustomProps(newProps);
@@ -8248,7 +8333,7 @@ function InSceneQuestForm({
8248
8333
  {
8249
8334
  key: prop.id,
8250
8335
  prop,
8251
- onUpdate: (updatedProp) => handleUpdateProp(index, updatedProp),
8336
+ onUpdate: (updatedProp) => handleUpdateProp2(index, updatedProp),
8252
8337
  onRemove: () => handleRemoveProp(index),
8253
8338
  onOpenImageViewer,
8254
8339
  unavailableTypes: currentUsedTypes.filter((t) => t !== prop.type),
@@ -8314,6 +8399,7 @@ function NodeDetailsPanel({
8314
8399
  return !!(node == null ? void 0 : node.useImageAsTexture);
8315
8400
  });
8316
8401
  const [selectedImageUrl, setSelectedImageUrl] = useState17((node == null ? void 0 : node.textureImageUrl) ?? null);
8402
+ const [hasUnsavedChanges, setHasUnsavedChanges2] = useState17(false);
8317
8403
  const maxPanelW = typeof window !== "undefined" ? window.innerWidth * 0.92 : 1200;
8318
8404
  const { width: panelWidth, isResizing, handlePointerDown: handleResize, setWidth } = useResizablePanel({
8319
8405
  initialWidth: isReadMode ? 700 : 440,
@@ -8351,6 +8437,7 @@ function NodeDetailsPanel({
8351
8437
  else if ((node == null ? void 0 : node.useImageAsTexture) === "false") setUseImageAsTexture(false);
8352
8438
  else setUseImageAsTexture(!!(node == null ? void 0 : node.useImageAsTexture));
8353
8439
  setSelectedImageUrl((node == null ? void 0 : node.textureImageUrl) ?? null);
8440
+ setHasUnsavedChanges2(false);
8354
8441
  }
8355
8442
  }, [node]);
8356
8443
  const hasImages = customProps.some((p) => p.type === "images" && Array.isArray(p.value) && p.value.length > 0 && p.value.some((img) => img.value));
@@ -8377,6 +8464,7 @@ function NodeDetailsPanel({
8377
8464
  setIntensity(val);
8378
8465
  onIntensityChange == null ? void 0 : onIntensityChange(node.id, val);
8379
8466
  onDataUpdate == null ? void 0 : onDataUpdate({ ...node, intensity: val });
8467
+ setHasUnsavedChanges2(true);
8380
8468
  };
8381
8469
  const handleCopyLink = () => {
8382
8470
  if (!(node == null ? void 0 : node.id)) return;
@@ -8394,14 +8482,17 @@ function NodeDetailsPanel({
8394
8482
  const v = e.target.value;
8395
8483
  setName(v);
8396
8484
  onNameChange == null ? void 0 : onNameChange(node.id, v);
8485
+ setHasUnsavedChanges2(true);
8397
8486
  };
8398
8487
  const handleColorChange = (val) => {
8399
8488
  setColor(val);
8400
8489
  onColorChange == null ? void 0 : onColorChange(node.id, val);
8490
+ setHasUnsavedChanges2(true);
8401
8491
  };
8402
8492
  const handleSizeChange = (newSize) => {
8403
8493
  setSize(newSize);
8404
8494
  onSizeChange == null ? void 0 : onSizeChange(node.id, newSize);
8495
+ setHasUnsavedChanges2(true);
8405
8496
  };
8406
8497
  const handleAddType = (newType) => {
8407
8498
  const trimmed = newType.trim();
@@ -8409,10 +8500,12 @@ function NodeDetailsPanel({
8409
8500
  setTypes([...types, trimmed]);
8410
8501
  setTypeInput("");
8411
8502
  setShowTypeSuggestions(false);
8503
+ setHasUnsavedChanges2(true);
8412
8504
  }
8413
8505
  };
8414
8506
  const handleRemoveType = (indexToRemove) => {
8415
8507
  setTypes(types.filter((_, index) => index !== indexToRemove));
8508
+ setHasUnsavedChanges2(true);
8416
8509
  };
8417
8510
  const handleTypeInputKeyDown = (e) => {
8418
8511
  if (e.key === "Enter") {
@@ -8425,6 +8518,7 @@ function NodeDetailsPanel({
8425
8518
  const handleAddProp = () => {
8426
8519
  const newProp = createNewCustomProperty(customProps);
8427
8520
  setCustomProps((p) => [...p, newProp]);
8521
+ setHasUnsavedChanges2(true);
8428
8522
  setTimeout(() => {
8429
8523
  var _a;
8430
8524
  (_a = propsEndRef.current) == null ? void 0 : _a.scrollIntoView({ behavior: "smooth", block: "center" });
@@ -8433,19 +8527,21 @@ function NodeDetailsPanel({
8433
8527
  const handleRemoveProp = (i) => {
8434
8528
  const newProps = customProps.filter((_, idx) => idx !== i);
8435
8529
  setCustomProps(newProps);
8530
+ setHasUnsavedChanges2(true);
8436
8531
  triggerAutoSave({ customProps: newProps });
8437
8532
  };
8438
- const handleUpdateProp = (index, updatedProp) => {
8533
+ const handlePropChange = (index, updatedProp) => {
8439
8534
  const newProps = [...customProps];
8440
8535
  newProps[index] = updatedProp;
8441
8536
  setCustomProps(newProps);
8537
+ setHasUnsavedChanges2(true);
8442
8538
  if (!updatedProp.isEditing) {
8443
8539
  triggerAutoSave({ customProps: newProps });
8444
8540
  }
8445
8541
  };
8446
8542
  const handleToggleImageMode = () => {
8447
- const newValue = !useImageAsTexture;
8448
8543
  setUseImageAsTexture(newValue);
8544
+ setHasUnsavedChanges2(true);
8449
8545
  let activeUrl = null;
8450
8546
  if (newValue) {
8451
8547
  const firstImageProp = customProps.find((p) => p.type === "images");
@@ -8467,6 +8563,7 @@ function NodeDetailsPanel({
8467
8563
  };
8468
8564
  const handleSelectTexture = (url) => {
8469
8565
  setSelectedImageUrl(url);
8566
+ setHasUnsavedChanges2(true);
8470
8567
  onImageChange == null ? void 0 : onImageChange(true, url, color);
8471
8568
  onDataUpdate == null ? void 0 : onDataUpdate({
8472
8569
  ...node,
@@ -8477,6 +8574,7 @@ function NodeDetailsPanel({
8477
8574
  };
8478
8575
  const handleSaveDescriptionInline = (newDescription) => {
8479
8576
  setDescription(newDescription);
8577
+ setHasUnsavedChanges2(true);
8480
8578
  onDataUpdate({ ...node, description: newDescription });
8481
8579
  triggerAutoSave({ description: newDescription });
8482
8580
  };
@@ -8487,6 +8585,10 @@ function NodeDetailsPanel({
8487
8585
  const currentCustomProps = overrides.customProps !== void 0 ? overrides.customProps : customProps;
8488
8586
  const currentExistingSections = overrides.existingSections !== void 0 ? overrides.existingSections : existingSections;
8489
8587
  const currentIntensity = overrides.intensity !== void 0 ? overrides.intensity : intensity;
8588
+ if (!keepOpen && !hasUnsavedChanges) {
8589
+ onClose();
8590
+ return;
8591
+ }
8490
8592
  if (!currentName.trim() || currentTypes.length === 0) {
8491
8593
  alert("O campo 'Nome' e pelo menos um 'Tipo' s\xE3o obrigat\xF3rios.");
8492
8594
  return;
@@ -8511,6 +8613,7 @@ function NodeDetailsPanel({
8511
8613
  };
8512
8614
  await onSave(dataToSave, keepOpen);
8513
8615
  onDataUpdate(dataToSave);
8616
+ setHasUnsavedChanges2(false);
8514
8617
  if (!keepOpen) {
8515
8618
  onClose();
8516
8619
  }
@@ -8806,6 +8909,7 @@ function QuestDetailsPanel({
8806
8909
  const [existingSections, setExistingSections] = useState18((node == null ? void 0 : node.description_sections) || []);
8807
8910
  const [isSaving, setIsSaving] = useState18(false);
8808
8911
  const [isLinkCopied, setIsLinkCopied] = useState18(false);
8912
+ const [hasUnsavedChanges, setHasUnsavedChanges2] = useState18(false);
8809
8913
  const maxPanelW = typeof window !== "undefined" ? window.innerWidth * 0.92 : 1200;
8810
8914
  const { width: panelWidth, isResizing, handlePointerDown: handleResize, setWidth } = useResizablePanel({
8811
8915
  initialWidth: isReadMode ? 700 : 440,
@@ -8837,6 +8941,7 @@ function QuestDetailsPanel({
8837
8941
  setIntensity((node == null ? void 0 : node.intensity) !== void 0 ? node.intensity : 0);
8838
8942
  setExistingSections((node == null ? void 0 : node.description_sections) || []);
8839
8943
  setCustomProps(extractCustomPropsFromNode(node || {}));
8944
+ setHasUnsavedChanges2(false);
8840
8945
  }
8841
8946
  }, [node]);
8842
8947
  useEffect16(() => {
@@ -8868,16 +8973,19 @@ function QuestDetailsPanel({
8868
8973
  setRawTitle(val);
8869
8974
  const newStandardName = questPrefix ? `${questPrefix} - \xBB ${val || "Sem t\xEDtulo"}` : val;
8870
8975
  onNameChange == null ? void 0 : onNameChange(node.id, newStandardName, val);
8976
+ setHasUnsavedChanges2(true);
8871
8977
  };
8872
8978
  const handleSizeChange = (newSize) => {
8873
8979
  setSize(newSize);
8874
8980
  onSizeChange == null ? void 0 : onSizeChange(node.id, newSize);
8981
+ setHasUnsavedChanges2(true);
8875
8982
  };
8876
8983
  const handleStatusChange = (newStatus) => {
8877
8984
  setStatus(newStatus);
8878
8985
  const newColor = QUEST_STATUS_COLORS3[newStatus];
8879
8986
  onColorChange == null ? void 0 : onColorChange(node.id, newColor);
8880
8987
  onDataUpdate == null ? void 0 : onDataUpdate({ ...node, status: newStatus, color: newColor });
8988
+ setHasUnsavedChanges2(true);
8881
8989
  };
8882
8990
  const handleAddType = (newType) => {
8883
8991
  const trimmed = newType.trim();
@@ -8885,11 +8993,13 @@ function QuestDetailsPanel({
8885
8993
  setTypes([...types, trimmed]);
8886
8994
  setTypeInput("");
8887
8995
  setShowTypeSuggestions(false);
8996
+ setHasUnsavedChanges2(true);
8888
8997
  }
8889
8998
  };
8890
8999
  const handleRemoveType = (indexToRemove) => {
8891
9000
  if (types[indexToRemove] === "quest") return;
8892
9001
  setTypes(types.filter((_, index) => index !== indexToRemove));
9002
+ setHasUnsavedChanges2(true);
8893
9003
  };
8894
9004
  const handleTypeInputKeyDown = (e) => {
8895
9005
  if (e.key === "Enter") {
@@ -8902,6 +9012,7 @@ function QuestDetailsPanel({
8902
9012
  const handleAddProp = () => {
8903
9013
  const newProp = createNewCustomProperty(customProps);
8904
9014
  setCustomProps((p) => [...p, newProp]);
9015
+ setHasUnsavedChanges2(true);
8905
9016
  setTimeout(() => {
8906
9017
  var _a2;
8907
9018
  (_a2 = propsEndRef.current) == null ? void 0 : _a2.scrollIntoView({ behavior: "smooth", block: "center" });
@@ -8910,18 +9021,21 @@ function QuestDetailsPanel({
8910
9021
  const handleRemoveProp = (i) => {
8911
9022
  const newProps = customProps.filter((_, idx) => idx !== i);
8912
9023
  setCustomProps(newProps);
9024
+ setHasUnsavedChanges2(true);
8913
9025
  triggerAutoSave({ customProps: newProps });
8914
9026
  };
8915
- const handleUpdateProp = (index, updatedProp) => {
9027
+ const handleCustomPropChange = (index, updatedProp) => {
8916
9028
  const newProps = [...customProps];
8917
9029
  newProps[index] = updatedProp;
8918
9030
  setCustomProps(newProps);
9031
+ setHasUnsavedChanges2(true);
8919
9032
  if (!updatedProp.isEditing) {
8920
9033
  triggerAutoSave({ customProps: newProps });
8921
9034
  }
8922
9035
  };
8923
9036
  const handleSaveDescriptionInline = (newDescription) => {
8924
9037
  setDescription(newDescription);
9038
+ setHasUnsavedChanges2(true);
8925
9039
  onDataUpdate({ ...node, description: newDescription });
8926
9040
  triggerAutoSave({ description: newDescription });
8927
9041
  };
@@ -8933,6 +9047,10 @@ function QuestDetailsPanel({
8933
9047
  const currentCustomProps = overrides.customProps !== void 0 ? overrides.customProps : customProps;
8934
9048
  const currentExistingSections = overrides.existingSections !== void 0 ? overrides.existingSections : existingSections;
8935
9049
  const currentStatus = overrides.status !== void 0 ? overrides.status : status;
9050
+ if (!keepOpen && !hasUnsavedChanges) {
9051
+ onClose();
9052
+ return;
9053
+ }
8936
9054
  if (!currentRawTitle.trim() || currentTypes.length === 0) {
8937
9055
  alert("O campo 'T\xEDtulo' e pelo menos um 'Tipo' s\xE3o obrigat\xF3rios.");
8938
9056
  return;
@@ -8962,6 +9080,7 @@ function QuestDetailsPanel({
8962
9080
  };
8963
9081
  await onSave(dataToSave, keepOpen);
8964
9082
  onDataUpdate(dataToSave);
9083
+ setHasUnsavedChanges2(false);
8965
9084
  if (!keepOpen) {
8966
9085
  onClose();
8967
9086
  }
@@ -9193,9 +9312,12 @@ function RelationshipDetailsPanel({
9193
9312
  const [description, setDescription] = useState20((link == null ? void 0 : link.description) ?? "");
9194
9313
  const [customProps, setCustomProps] = useState20(() => extractCustomPropsFromNode(link || {}));
9195
9314
  const [existingSections, setExistingSections] = useState20((link == null ? void 0 : link.description_sections) || []);
9315
+ const [sourceLabel, setSourceLabel] = useState20((link == null ? void 0 : link.source_label) ?? "");
9316
+ const [targetLabel, setTargetLabel] = useState20((link == null ? void 0 : link.target_label) ?? "");
9196
9317
  const [isDescriptionModalOpen, setIsDescriptionModalOpen] = useState20(false);
9197
9318
  const [isSaving, setIsSaving] = useState20(false);
9198
9319
  const [isReadMode, setIsReadMode] = useState20(false);
9320
+ const [hasUnsavedChanges, setHasUnsavedChanges2] = useState20(false);
9199
9321
  const propsEndRef = useRef16(null);
9200
9322
  const canEdit = useMemo9(() => {
9201
9323
  const ability = defineAbilityFor(userRole);
@@ -9206,12 +9328,16 @@ function RelationshipDetailsPanel({
9206
9328
  setDescription((link == null ? void 0 : link.description) ?? "");
9207
9329
  setExistingSections((link == null ? void 0 : link.description_sections) || []);
9208
9330
  setCustomProps(extractCustomPropsFromNode(link || {}));
9331
+ setSourceLabel((link == null ? void 0 : link.source_label) ?? "");
9332
+ setTargetLabel((link == null ? void 0 : link.target_label) ?? "");
9333
+ setHasUnsavedChanges2(false);
9209
9334
  }, [link]);
9210
9335
  const swallow = (e) => e.stopPropagation();
9211
9336
  const handleAddProp = () => {
9212
9337
  if (!canEdit) return;
9213
9338
  const newProp = createNewCustomProperty(customProps);
9214
9339
  setCustomProps((p) => [...p, newProp]);
9340
+ setHasUnsavedChanges2(true);
9215
9341
  setTimeout(() => {
9216
9342
  var _a;
9217
9343
  (_a = propsEndRef.current) == null ? void 0 : _a.scrollIntoView({ behavior: "smooth", block: "center" });
@@ -9223,6 +9349,12 @@ function RelationshipDetailsPanel({
9223
9349
  const currentCustomProps = overrides.customProps !== void 0 ? overrides.customProps : customProps;
9224
9350
  const currentExistingSections = overrides.existingSections !== void 0 ? overrides.existingSections : existingSections;
9225
9351
  const currentName = overrides.name !== void 0 ? overrides.name : name;
9352
+ const currentSourceLabel = overrides.sourceLabel !== void 0 ? overrides.sourceLabel : sourceLabel;
9353
+ const currentTargetLabel = overrides.targetLabel !== void 0 ? overrides.targetLabel : targetLabel;
9354
+ if (!keepOpen && !hasUnsavedChanges) {
9355
+ onClose();
9356
+ return;
9357
+ }
9226
9358
  setIsSaving(true);
9227
9359
  try {
9228
9360
  const extrasObj = toObjectFromCustomProps(currentCustomProps.filter((p) => !p.isEditing));
@@ -9238,8 +9370,11 @@ function RelationshipDetailsPanel({
9238
9370
  isCurved: link.isCurved,
9239
9371
  curveOffset: link.curveOffset
9240
9372
  };
9373
+ if (currentSourceLabel.trim()) dataToSave.source_label = currentSourceLabel.trim();
9374
+ if (currentTargetLabel.trim()) dataToSave.target_label = currentTargetLabel.trim();
9241
9375
  await onSave(dataToSave, keepOpen);
9242
9376
  onDataUpdate(dataToSave);
9377
+ setHasUnsavedChanges2(false);
9243
9378
  if (!keepOpen) {
9244
9379
  onClose();
9245
9380
  }
@@ -9253,18 +9388,21 @@ function RelationshipDetailsPanel({
9253
9388
  const handleSaveDescriptionInline = (newDescription) => {
9254
9389
  if (!canEdit) return;
9255
9390
  setDescription(newDescription);
9391
+ setHasUnsavedChanges2(true);
9256
9392
  onDataUpdate((prev) => ({ ...prev, description: newDescription }));
9257
9393
  triggerAutoSave({ description: newDescription });
9258
9394
  };
9259
9395
  const handleRemoveProp = (i) => {
9260
9396
  const newProps = customProps.filter((_, idx) => idx !== i);
9261
9397
  setCustomProps(newProps);
9398
+ setHasUnsavedChanges2(true);
9262
9399
  triggerAutoSave({ customProps: newProps });
9263
9400
  };
9264
- const handleUpdateProp = (index, updatedProp) => {
9401
+ const handleUpdateProp2 = (index, updatedProp) => {
9265
9402
  const newProps = [...customProps];
9266
9403
  newProps[index] = updatedProp;
9267
9404
  setCustomProps(newProps);
9405
+ setHasUnsavedChanges2(true);
9268
9406
  if (!updatedProp.isEditing) {
9269
9407
  triggerAutoSave({ customProps: newProps });
9270
9408
  }
@@ -9317,14 +9455,47 @@ function RelationshipDetailsPanel({
9317
9455
  {
9318
9456
  type: "text",
9319
9457
  value: name,
9320
- onChange: (e) => setName(e.target.value),
9458
+ onChange: (e) => {
9459
+ setName(e.target.value);
9460
+ setHasUnsavedChanges2(true);
9461
+ },
9321
9462
  placeholder: "Ex: Controla, Pertence a, Fornece...",
9322
9463
  disabled: !canEdit,
9323
9464
  className: `w-full bg-slate-800/70 p-2 text-sm rounded-lg border border-white/10 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-400/60
9324
9465
  ${!canEdit ? "opacity-50 cursor-not-allowed" : ""}
9325
9466
  `
9326
9467
  }
9327
- )), /* @__PURE__ */ React19.createElement("div", { className: "space-y-1.5 relative" }, /* @__PURE__ */ React19.createElement("label", { className: "text-xs text-slate-300" }, "Descri\xE7\xE3o"), /* @__PURE__ */ React19.createElement("div", { className: "relative group min-h-[60px] bg-slate-800/40 rounded-lg border border-white/10 hover:border-white/20 transition-colors" }, /* @__PURE__ */ React19.createElement(
9468
+ )), /* @__PURE__ */ React19.createElement("div", { className: "rounded-xl border border-white/8 bg-slate-800/30 p-3 space-y-3" }, /* @__PURE__ */ React19.createElement("div", { className: "flex items-center gap-2" }, /* @__PURE__ */ React19.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", width: "13", height: "13", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "text-violet-400 flex-shrink-0" }, /* @__PURE__ */ React19.createElement("path", { d: "M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z" }), /* @__PURE__ */ React19.createElement("line", { x1: "7", y1: "7", x2: "7.01", y2: "7" })), /* @__PURE__ */ React19.createElement("p", { className: "text-xs font-medium text-violet-300/80 uppercase tracking-wider" }, "Labels de Agrupamento")), /* @__PURE__ */ React19.createElement("p", { className: "text-[11px] text-slate-400 leading-relaxed" }, "Labels agrupam conex\xF5es no menu de Conex\xF5es. Cada lado da conex\xE3o pode ter sua pr\xF3pria label."), /* @__PURE__ */ React19.createElement("div", { className: "grid grid-cols-2 gap-2" }, /* @__PURE__ */ React19.createElement("div", { className: "space-y-1" }, /* @__PURE__ */ React19.createElement("label", { className: "text-[11px] text-slate-400" }, "Label do Source"), /* @__PURE__ */ React19.createElement(
9469
+ "input",
9470
+ {
9471
+ type: "text",
9472
+ value: sourceLabel,
9473
+ onChange: (e) => {
9474
+ setSourceLabel(e.target.value);
9475
+ setHasUnsavedChanges2(true);
9476
+ },
9477
+ placeholder: "Ex: Conceitos",
9478
+ disabled: !canEdit,
9479
+ className: `w-full bg-slate-900/60 p-2 text-xs rounded-lg border border-white/10 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-violet-400/60 placeholder:text-slate-600
9480
+ ${!canEdit ? "opacity-50 cursor-not-allowed" : ""}
9481
+ `
9482
+ }
9483
+ )), /* @__PURE__ */ React19.createElement("div", { className: "space-y-1" }, /* @__PURE__ */ React19.createElement("label", { className: "text-[11px] text-slate-400" }, "Label do Target"), /* @__PURE__ */ React19.createElement(
9484
+ "input",
9485
+ {
9486
+ type: "text",
9487
+ value: targetLabel,
9488
+ onChange: (e) => {
9489
+ setTargetLabel(e.target.value);
9490
+ setHasUnsavedChanges2(true);
9491
+ },
9492
+ placeholder: "Ex: Refer\xEAncias",
9493
+ disabled: !canEdit,
9494
+ className: `w-full bg-slate-900/60 p-2 text-xs rounded-lg border border-white/10 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-violet-400/60 placeholder:text-slate-600
9495
+ ${!canEdit ? "opacity-50 cursor-not-allowed" : ""}
9496
+ `
9497
+ }
9498
+ )))), /* @__PURE__ */ React19.createElement("div", { className: "space-y-1.5 relative" }, /* @__PURE__ */ React19.createElement("label", { className: "text-xs text-slate-300" }, "Descri\xE7\xE3o"), /* @__PURE__ */ React19.createElement("div", { className: "relative group min-h-[60px] bg-slate-800/40 rounded-lg border border-white/10 hover:border-white/20 transition-colors" }, /* @__PURE__ */ React19.createElement(
9328
9499
  DescriptionDisplay,
9329
9500
  {
9330
9501
  description,
@@ -9366,7 +9537,7 @@ function RelationshipDetailsPanel({
9366
9537
  {
9367
9538
  key: prop.id,
9368
9539
  prop,
9369
- onUpdate: (updatedProp) => handleUpdateProp(idx, updatedProp),
9540
+ onUpdate: (updatedProp) => handleUpdateProp2(idx, updatedProp),
9370
9541
  onRemove: () => handleRemoveProp(idx),
9371
9542
  onOpenImageViewer,
9372
9543
  unavailableTypes: currentUsedTypes.filter((t) => t !== prop.type),