@eventcatalog/visualiser 5.2.0 → 5.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/studio.mjs CHANGED
@@ -834,17 +834,7 @@ var deleteNodes = (doc, ids) => doc.transact(() => {
834
834
  thread.delete("offset");
835
835
  });
836
836
  hotspots.forEach((hotspot, id) => {
837
- const node = hotspot.nodeId && removed.has(hotspot.nodeId) ? nodes.get(hotspot.nodeId) : void 0;
838
- if (!node) return;
839
- const position = getAbsolutePosition(node, lookup);
840
- const { nodeId: _, ...rest } = hotspot;
841
- hotspots.set(id, {
842
- ...rest,
843
- position: {
844
- x: position.x + (node.measured?.width ?? node.width ?? 0),
845
- y: position.y
846
- }
847
- });
837
+ if (hotspot.nodeId && removed.has(hotspot.nodeId)) hotspots.delete(id);
848
838
  });
849
839
  removed.forEach((id) => {
850
840
  nodes.delete(id);
@@ -3181,6 +3171,21 @@ var EditingContext = createContext(
3181
3171
  );
3182
3172
  var DropTargetContext = createContext(null);
3183
3173
  var StartWritingContext = createContext(null);
3174
+ var START_WRITING_EVENT = "studio:start-writing";
3175
+ var startWriting = (id) => window.dispatchEvent(
3176
+ new CustomEvent(START_WRITING_EVENT, { detail: id })
3177
+ );
3178
+ function useStartWriting(id, start) {
3179
+ const latest = useRef3(start);
3180
+ latest.current = start;
3181
+ useEffect4(() => {
3182
+ const listener = (event) => {
3183
+ if (event.detail === id) latest.current();
3184
+ };
3185
+ window.addEventListener(START_WRITING_EVENT, listener);
3186
+ return () => window.removeEventListener(START_WRITING_EVENT, listener);
3187
+ }, [id]);
3188
+ }
3184
3189
  var LockedContext = createContext(false);
3185
3190
  var SystemGroupComponent = SystemGroupNode_default;
3186
3191
  function WidthHandles() {
@@ -3289,6 +3294,7 @@ var StickyNote = memo3(function StickyNote2({ id, data, selected }) {
3289
3294
  useContext(StartWritingContext) === id
3290
3295
  );
3291
3296
  const locked = useContext(LockedContext);
3297
+ useStartWriting(id, () => !locked && setEditing(true));
3292
3298
  const colorName = typeof data.color === "string" && NOTE_COLORS[data.color] ? data.color : "yellow";
3293
3299
  const color = NOTE_COLORS[colorName];
3294
3300
  return /* @__PURE__ */ jsxs3(
@@ -3444,6 +3450,7 @@ var TextNode = memo3(function TextNode2({ id, data, selected }) {
3444
3450
  if (!text.trim()) void deleteElements({ nodes: [{ id }] });
3445
3451
  };
3446
3452
  const locked = useContext(LockedContext);
3453
+ useStartWriting(id, () => !locked && setEditing(true));
3447
3454
  const framed = (selected || scaling !== null || turning !== null) && !editing && !locked;
3448
3455
  return /* @__PURE__ */ jsxs3(
3449
3456
  "div",
@@ -4461,7 +4468,9 @@ function CommentList({
4461
4468
 
4462
4469
  // src/studio/components/ContextMenu.tsx
4463
4470
  import {
4471
+ Fragment as Fragment5,
4464
4472
  memo as memo5,
4473
+ useContext as useContext3,
4465
4474
  useEffect as useEffect6,
4466
4475
  useLayoutEffect as useLayoutEffect2,
4467
4476
  useRef as useRef5,
@@ -4473,18 +4482,75 @@ import {
4473
4482
  CopyPlus,
4474
4483
  MessageCircle,
4475
4484
  MessageCircleQuestion,
4485
+ Palette,
4476
4486
  Pencil,
4487
+ Scissors,
4477
4488
  SendToBack,
4478
4489
  StickyNote as StickyNote3,
4479
4490
  Trash2 as Trash22,
4480
4491
  TriangleAlert
4481
4492
  } from "lucide-react";
4482
- import { Fragment as Fragment5, jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
4493
+ import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
4483
4494
  var EDGE = 8;
4495
+ var itemClass = "flex w-full items-center gap-2.5 rounded-md px-2 py-1.5 text-left text-[13px] leading-5 transition-colors hover:bg-[rgb(var(--ec-page-border)/0.5)]";
4496
+ var iconClass = "flex-none text-[rgb(var(--ec-icon-color))]";
4497
+ function Item({
4498
+ icon,
4499
+ label,
4500
+ onClick,
4501
+ shortcut,
4502
+ danger
4503
+ }) {
4504
+ return /* @__PURE__ */ jsxs5(
4505
+ "button",
4506
+ {
4507
+ type: "button",
4508
+ role: "menuitem",
4509
+ className: `${itemClass} ${danger ? `${STATUS.danger.text} ${STATUS.danger.hover}` : ""}`,
4510
+ onClick,
4511
+ children: [
4512
+ /* @__PURE__ */ jsx7("span", { className: danger ? "flex-none" : iconClass, children: icon }),
4513
+ /* @__PURE__ */ jsx7("span", { className: "min-w-0 flex-1 truncate", children: label }),
4514
+ shortcut && /* @__PURE__ */ jsx7("kbd", { className: "ml-4 flex-none font-sans text-[11px] text-[rgb(var(--ec-page-text-muted))]", children: shortcut })
4515
+ ]
4516
+ }
4517
+ );
4518
+ }
4519
+ function NoteColours({ node, onPicked }) {
4520
+ const updateNodeData2 = useContext3(UpdateNodeDataContext);
4521
+ const current = typeof node.data.color === "string" && NOTE_COLORS[node.data.color] ? node.data.color : "yellow";
4522
+ return /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-2.5 px-2 py-1.5 text-[13px] leading-5", children: [
4523
+ /* @__PURE__ */ jsx7(Palette, { size: 14, className: iconClass }),
4524
+ /* @__PURE__ */ jsx7("span", { className: "flex-1", children: "Colour" }),
4525
+ /* @__PURE__ */ jsx7("span", { role: "radiogroup", "aria-label": "Note colour", className: "flex gap-1", children: Object.entries(NOTE_COLORS).map(([name, { fill, border }]) => /* @__PURE__ */ jsx7(
4526
+ "button",
4527
+ {
4528
+ type: "button",
4529
+ role: "radio",
4530
+ "aria-checked": name === current,
4531
+ title: name.charAt(0).toUpperCase() + name.slice(1),
4532
+ onClick: () => {
4533
+ updateNodeData2(node.id, (data) => ({ ...data, color: name }));
4534
+ onPicked();
4535
+ },
4536
+ className: "h-4 w-4 rounded-full border transition-transform hover:scale-110",
4537
+ style: {
4538
+ background: fill,
4539
+ borderColor: border,
4540
+ ...name === current && {
4541
+ boxShadow: "0 0 0 1.5px rgb(var(--ec-card-bg)), 0 0 0 3px rgb(var(--ec-page-text))"
4542
+ }
4543
+ }
4544
+ },
4545
+ name
4546
+ )) })
4547
+ ] });
4548
+ }
4484
4549
  var ContextMenu = memo5(function ContextMenu2({
4485
4550
  menu,
4486
4551
  questions,
4487
4552
  onCopy,
4553
+ onCut,
4488
4554
  onDuplicate,
4489
4555
  onPaste,
4490
4556
  onAddNote,
@@ -4495,6 +4561,7 @@ var ContextMenu = memo5(function ContextMenu2({
4495
4561
  onDeleteNode,
4496
4562
  onClose
4497
4563
  }) {
4564
+ const locked = useContext3(LockedContext);
4498
4565
  useEffect6(() => {
4499
4566
  if (!menu) return;
4500
4567
  const close = (event) => {
@@ -4525,70 +4592,211 @@ var ContextMenu = memo5(function ContextMenu2({
4525
4592
  });
4526
4593
  }, [menu]);
4527
4594
  if (!menu) return null;
4528
- const itemClass = "flex w-full items-center gap-2 rounded px-2.5 py-1.5 text-left text-xs hover:bg-[rgb(var(--ec-page-border)/0.5)]";
4529
- const shortcutClass = "ml-auto text-[rgb(var(--ec-page-text-muted))]";
4530
- const item = (icon, label, onClick, shortcut, className = "") => /* @__PURE__ */ jsxs5("button", { className: `${itemClass} ${className}`, onClick, children: [
4531
- icon,
4532
- label,
4533
- shortcut && /* @__PURE__ */ jsx7("span", { className: shortcutClass, children: shortcut })
4534
- ] });
4535
- const separator = /* @__PURE__ */ jsx7("div", { className: "my-1 h-px bg-[rgb(var(--ec-page-border))]" });
4536
- return /* @__PURE__ */ jsxs5(
4595
+ const node = menu.node;
4596
+ const written = !!node && isWrittenOnCanvas(node.type);
4597
+ const editable = !locked;
4598
+ const clipboard = [
4599
+ editable && onCut && /* @__PURE__ */ jsx7(
4600
+ Item,
4601
+ {
4602
+ icon: /* @__PURE__ */ jsx7(Scissors, { size: 14 }),
4603
+ label: "Cut",
4604
+ onClick: onCut,
4605
+ shortcut: "\u2318X"
4606
+ },
4607
+ "cut"
4608
+ ),
4609
+ /* @__PURE__ */ jsx7(
4610
+ Item,
4611
+ {
4612
+ icon: /* @__PURE__ */ jsx7(Copy, { size: 14 }),
4613
+ label: "Copy",
4614
+ onClick: onCopy,
4615
+ shortcut: "\u2318C"
4616
+ },
4617
+ "copy"
4618
+ ),
4619
+ editable && onPaste && /* @__PURE__ */ jsx7(
4620
+ Item,
4621
+ {
4622
+ icon: /* @__PURE__ */ jsx7(ClipboardPaste, { size: 14 }),
4623
+ label: "Paste",
4624
+ onClick: onPaste,
4625
+ shortcut: "\u2318V"
4626
+ },
4627
+ "paste"
4628
+ ),
4629
+ editable && /* @__PURE__ */ jsx7(
4630
+ Item,
4631
+ {
4632
+ icon: /* @__PURE__ */ jsx7(CopyPlus, { size: 14 }),
4633
+ label: "Duplicate",
4634
+ onClick: onDuplicate,
4635
+ shortcut: "\u2318D"
4636
+ },
4637
+ "duplicate"
4638
+ )
4639
+ ];
4640
+ const comment = onAddComment && /* @__PURE__ */ jsx7(
4641
+ Item,
4642
+ {
4643
+ icon: /* @__PURE__ */ jsx7(MessageCircle, { size: 14 }),
4644
+ label: "Add comment",
4645
+ onClick: onAddComment,
4646
+ shortcut: "C"
4647
+ },
4648
+ "comment"
4649
+ );
4650
+ const remove = editable && /* @__PURE__ */ jsx7(
4651
+ Item,
4652
+ {
4653
+ icon: /* @__PURE__ */ jsx7(Trash22, { size: 14 }),
4654
+ label: "Delete",
4655
+ onClick: onDeleteNode,
4656
+ shortcut: "\u232B",
4657
+ danger: true
4658
+ },
4659
+ "delete"
4660
+ );
4661
+ const groups = node ? written ? [
4662
+ [
4663
+ editable && /* @__PURE__ */ jsx7(
4664
+ Item,
4665
+ {
4666
+ icon: /* @__PURE__ */ jsx7(Pencil, { size: 14 }),
4667
+ label: "Edit text",
4668
+ onClick: () => {
4669
+ startWriting(node.id);
4670
+ onClose();
4671
+ }
4672
+ },
4673
+ "edit"
4674
+ )
4675
+ ],
4676
+ clipboard,
4677
+ [
4678
+ editable && node.type === "note" && /* @__PURE__ */ jsx7(NoteColours, { node, onPicked: onClose }, "colour")
4679
+ ],
4680
+ [comment],
4681
+ [remove]
4682
+ ] : [
4683
+ (questions ?? []).map((question) => /* @__PURE__ */ jsx7(
4684
+ Item,
4685
+ {
4686
+ icon: /* @__PURE__ */ jsx7(MessageCircleQuestion, { size: 14 }),
4687
+ label: question.label,
4688
+ onClick: question.onSelect
4689
+ },
4690
+ question.label
4691
+ )),
4692
+ [
4693
+ onEditDetails && /* @__PURE__ */ jsx7(
4694
+ Item,
4695
+ {
4696
+ icon: /* @__PURE__ */ jsx7(Pencil, { size: 14 }),
4697
+ label: "Edit details",
4698
+ onClick: onEditDetails
4699
+ },
4700
+ "details"
4701
+ )
4702
+ ],
4703
+ clipboard,
4704
+ [
4705
+ comment,
4706
+ onAddHotspot && /* @__PURE__ */ jsx7(
4707
+ Item,
4708
+ {
4709
+ icon: /* @__PURE__ */ jsx7(TriangleAlert, { size: 14 }),
4710
+ label: "Add hotspot",
4711
+ onClick: onAddHotspot,
4712
+ shortcut: "A"
4713
+ },
4714
+ "hotspot"
4715
+ ),
4716
+ editable && /* @__PURE__ */ jsx7(
4717
+ Item,
4718
+ {
4719
+ icon: /* @__PURE__ */ jsx7(StickyNote3, { size: 14 }),
4720
+ label: "Add note",
4721
+ onClick: onAddNote
4722
+ },
4723
+ "note"
4724
+ )
4725
+ ],
4726
+ [remove]
4727
+ ] : [
4728
+ [
4729
+ editable && onPaste && /* @__PURE__ */ jsx7(
4730
+ Item,
4731
+ {
4732
+ icon: /* @__PURE__ */ jsx7(ClipboardPaste, { size: 14 }),
4733
+ label: "Paste here",
4734
+ onClick: onPaste,
4735
+ shortcut: "\u2318V"
4736
+ },
4737
+ "paste"
4738
+ )
4739
+ ],
4740
+ [
4741
+ editable && /* @__PURE__ */ jsx7(
4742
+ Item,
4743
+ {
4744
+ icon: /* @__PURE__ */ jsx7(StickyNote3, { size: 14 }),
4745
+ label: "Add sticky note",
4746
+ onClick: onAddNote,
4747
+ shortcut: "N"
4748
+ },
4749
+ "note"
4750
+ ),
4751
+ comment,
4752
+ onAddHotspot && /* @__PURE__ */ jsx7(
4753
+ Item,
4754
+ {
4755
+ icon: /* @__PURE__ */ jsx7(TriangleAlert, { size: 14 }),
4756
+ label: "Add hotspot",
4757
+ onClick: onAddHotspot,
4758
+ shortcut: "A"
4759
+ },
4760
+ "hotspot"
4761
+ )
4762
+ ],
4763
+ [
4764
+ onReorder && /* @__PURE__ */ jsx7(
4765
+ Item,
4766
+ {
4767
+ icon: /* @__PURE__ */ jsx7(SendToBack, { size: 14 }),
4768
+ label: "Reorder diagram",
4769
+ onClick: onReorder
4770
+ },
4771
+ "reorder"
4772
+ )
4773
+ ]
4774
+ ];
4775
+ const sections = groups.map((group) => group.filter(Boolean)).filter((group) => group.length > 0);
4776
+ return /* @__PURE__ */ jsx7(
4537
4777
  "div",
4538
4778
  {
4779
+ role: "menu",
4539
4780
  onPointerDown: (event) => event.stopPropagation(),
4540
4781
  onContextMenu: (event) => event.preventDefault(),
4541
4782
  ref: menuRef,
4542
- className: "fixed z-50 w-56 overflow-y-auto rounded-lg border p-1 shadow-xl bg-[rgb(var(--ec-card-bg))] border-[rgb(var(--ec-page-border))] text-[rgb(var(--ec-page-text))]",
4783
+ className: "fixed z-50 w-60 overflow-y-auto rounded-xl border p-1.5 shadow-xl bg-[rgb(var(--ec-card-bg))] border-[rgb(var(--ec-page-border))] text-[rgb(var(--ec-page-text))]",
4543
4784
  style: {
4544
4785
  left: placed?.left ?? menu.x,
4545
4786
  top: placed?.top ?? menu.y,
4546
4787
  maxHeight: `calc(100vh - ${EDGE * 2}px)`,
4547
4788
  visibility: placed ? "visible" : "hidden"
4548
4789
  },
4549
- children: [
4550
- menu.node && questions?.length ? /* @__PURE__ */ jsxs5(Fragment5, { children: [
4551
- questions.map((question) => /* @__PURE__ */ jsxs5(
4552
- "button",
4553
- {
4554
- className: itemClass,
4555
- onClick: question.onSelect,
4556
- children: [
4557
- /* @__PURE__ */ jsx7(MessageCircleQuestion, { size: 14 }),
4558
- question.label
4559
- ]
4560
- },
4561
- question.label
4562
- )),
4563
- separator
4564
- ] }) : null,
4565
- menu.node && /* @__PURE__ */ jsxs5(Fragment5, { children: [
4566
- onAddComment && item(/* @__PURE__ */ jsx7(MessageCircle, { size: 14 }), "Add comment", onAddComment, "C"),
4567
- onAddHotspot && !isWrittenOnCanvas(menu.node.type) && item(/* @__PURE__ */ jsx7(TriangleAlert, { size: 14 }), "Add hotspot", onAddHotspot, "A"),
4568
- !isWrittenOnCanvas(menu.node.type) && item(/* @__PURE__ */ jsx7(StickyNote3, { size: 14 }), "Add a note", onAddNote),
4569
- onEditDetails && !isWrittenOnCanvas(menu.node.type) && item(/* @__PURE__ */ jsx7(Pencil, { size: 14 }), "Edit details", onEditDetails),
4570
- item(/* @__PURE__ */ jsx7(Copy, { size: 14 }), "Copy", onCopy, "\u2318C"),
4571
- item(/* @__PURE__ */ jsx7(CopyPlus, { size: 14 }), "Duplicate", onDuplicate, "\u2318D"),
4572
- separator,
4573
- item(
4574
- /* @__PURE__ */ jsx7(Trash22, { size: 14 }),
4575
- "Delete",
4576
- onDeleteNode,
4577
- "\u232B",
4578
- STATUS.danger.text
4579
- )
4580
- ] }),
4581
- !menu.node && /* @__PURE__ */ jsxs5(Fragment5, { children: [
4582
- onAddComment && item(/* @__PURE__ */ jsx7(MessageCircle, { size: 14 }), "Add comment", onAddComment, "C"),
4583
- onAddHotspot && item(/* @__PURE__ */ jsx7(TriangleAlert, { size: 14 }), "Add hotspot", onAddHotspot, "A"),
4584
- item(/* @__PURE__ */ jsx7(StickyNote3, { size: 14 }), "Add a sticky note", onAddNote, "N"),
4585
- onPaste && item(/* @__PURE__ */ jsx7(ClipboardPaste, { size: 14 }), "Paste here", onPaste, "\u2318V"),
4586
- onReorder && /* @__PURE__ */ jsxs5(Fragment5, { children: [
4587
- separator,
4588
- item(/* @__PURE__ */ jsx7(SendToBack, { size: 14 }), "Reorder diagram", onReorder)
4589
- ] })
4590
- ] })
4591
- ]
4790
+ children: sections.map((section, index) => /* @__PURE__ */ jsxs5(Fragment5, { children: [
4791
+ index > 0 && /* @__PURE__ */ jsx7(
4792
+ "div",
4793
+ {
4794
+ role: "separator",
4795
+ className: "-mx-1.5 my-1.5 h-px bg-[rgb(var(--ec-page-border))]"
4796
+ }
4797
+ ),
4798
+ section
4799
+ ] }, index))
4592
4800
  }
4593
4801
  );
4594
4802
  });
@@ -4704,6 +4912,7 @@ function DragGhost({ ghost, onEnd }) {
4704
4912
  // src/studio/components/Hotspots.tsx
4705
4913
  import {
4706
4914
  memo as memo6,
4915
+ useEffect as useEffect8,
4707
4916
  useLayoutEffect as useLayoutEffect3,
4708
4917
  useRef as useRef7,
4709
4918
  useState as useState9
@@ -4715,6 +4924,7 @@ import {
4715
4924
  } from "@xyflow/react";
4716
4925
  import {
4717
4926
  Info,
4927
+ LocateFixed,
4718
4928
  OctagonAlert,
4719
4929
  Pencil as Pencil2,
4720
4930
  Trash2 as Trash23,
@@ -4738,7 +4948,6 @@ var topSeverity = (hotspots) => [...hotspots].sort(bySeverity)[0]?.severity;
4738
4948
  var MARKER_SCALE = "--studio-hotspot-scale";
4739
4949
  var PANEL_SCALE = "--studio-hotspot-panel-scale";
4740
4950
  var MIN_SCREEN_SCALE = 0.5;
4741
- var ICONS_ONLY_BELOW_ZOOM = 0.6;
4742
4951
  function useMarkerScale(ref) {
4743
4952
  const store = useStoreApi4();
4744
4953
  useLayoutEffect3(() => {
@@ -4748,7 +4957,6 @@ function useMarkerScale(ref) {
4748
4957
  const scale = zoom >= 1 ? 1 / zoom : Math.max(1, MIN_SCREEN_SCALE / zoom);
4749
4958
  element.style.setProperty(MARKER_SCALE, String(scale));
4750
4959
  element.style.setProperty(PANEL_SCALE, String(1 / zoom / scale));
4751
- element.classList.toggle("is-icons-only", zoom < ICONS_ONLY_BELOW_ZOOM);
4752
4960
  };
4753
4961
  apply(store.getState().transform[2]);
4754
4962
  return store.subscribe((state, previous) => {
@@ -4795,6 +5003,48 @@ function SeverityIcon({
4795
5003
  }
4796
5004
  var DRAG_THRESHOLD_PX2 = 4;
4797
5005
  var ICON_CENTRE_PX = 14;
5006
+ function useMarkerDrag(onClick, onDrag) {
5007
+ const drag = useRef7(null);
5008
+ const pointOf = (event, grab) => ({ x: event.clientX - grab.x, y: event.clientY - grab.y });
5009
+ return {
5010
+ onKeyDown: (event) => (event.key === "Enter" || event.key === " ") && onClick(),
5011
+ onPointerDown: (event) => {
5012
+ if (event.button !== 0) return;
5013
+ try {
5014
+ event.currentTarget.setPointerCapture(event.pointerId);
5015
+ } catch {
5016
+ }
5017
+ const rect = event.currentTarget.getBoundingClientRect();
5018
+ drag.current = {
5019
+ start: { x: event.clientX, y: event.clientY },
5020
+ grab: {
5021
+ x: event.clientX - rect.left - ICON_CENTRE_PX * (rect.height / 28),
5022
+ y: event.clientY - rect.top - rect.height / 2
5023
+ },
5024
+ moved: false
5025
+ };
5026
+ },
5027
+ onPointerMove: (event) => {
5028
+ const current = drag.current;
5029
+ if (!current || !onDrag) return;
5030
+ if (!current.moved && Math.hypot(
5031
+ event.clientX - current.start.x,
5032
+ event.clientY - current.start.y
5033
+ ) < DRAG_THRESHOLD_PX2)
5034
+ return;
5035
+ current.moved = true;
5036
+ onDrag(pointOf(event, current.grab), "move");
5037
+ },
5038
+ onPointerUp: (event) => {
5039
+ const current = drag.current;
5040
+ drag.current = null;
5041
+ if (!current) return;
5042
+ if (current.moved) onDrag?.(pointOf(event, current.grab), "end");
5043
+ else onClick();
5044
+ },
5045
+ onPointerCancel: () => drag.current = null
5046
+ };
5047
+ }
4798
5048
  function Marker({
4799
5049
  spot,
4800
5050
  open,
@@ -4802,67 +5052,22 @@ function Marker({
4802
5052
  onClick,
4803
5053
  onDrag
4804
5054
  }) {
4805
- const drag = useRef7(null);
4806
- const pointOf = (event) => ({
4807
- x: event.clientX - drag.current.grab.x,
4808
- y: event.clientY - drag.current.grab.y
4809
- });
5055
+ const handlers = useMarkerDrag(onClick, onDrag);
4810
5056
  const sorted = [...spot.hotspots].sort(bySeverity);
4811
5057
  const severity = sorted[0].severity;
4812
5058
  const count = sorted.length;
4813
- const label = spot.inside ? `${count} hotspot${count === 1 ? "" : "s"}` : count > 1 ? `${sorted[0].title} +${count - 1}` : sorted[0].title;
5059
+ const label = count > 1 || spot.inside ? `${count} hotspot${count === 1 ? "" : "s"}` : sorted[0].title;
4814
5060
  return /* @__PURE__ */ jsxs6(
4815
5061
  "button",
4816
5062
  {
4817
5063
  type: "button",
4818
5064
  "aria-expanded": open,
5065
+ "aria-label": label,
4819
5066
  title: sorted.map(
4820
5067
  (hotspot) => `${SEVERITIES[hotspot.severity].label}: ${hotspot.title}`
4821
5068
  ).join("\n"),
4822
- onKeyDown: (event) => (event.key === "Enter" || event.key === " ") && onClick(),
4823
- onPointerDown: (event) => {
4824
- if (event.button !== 0) return;
4825
- try {
4826
- event.currentTarget.setPointerCapture(event.pointerId);
4827
- } catch {
4828
- }
4829
- const rect = event.currentTarget.getBoundingClientRect();
4830
- drag.current = {
4831
- start: { x: event.clientX, y: event.clientY },
4832
- grab: {
4833
- x: event.clientX - rect.left - ICON_CENTRE_PX * (rect.height / 28),
4834
- y: event.clientY - rect.top - rect.height / 2
4835
- },
4836
- moved: false
4837
- };
4838
- },
4839
- onPointerMove: (event) => {
4840
- const current = drag.current;
4841
- if (!current || !onDrag) return;
4842
- if (!current.moved && Math.hypot(
4843
- event.clientX - current.start.x,
4844
- event.clientY - current.start.y
4845
- ) < DRAG_THRESHOLD_PX2)
4846
- return;
4847
- current.moved = true;
4848
- onDrag(pointOf(event), "move");
4849
- },
4850
- onPointerUp: (event) => {
4851
- const current = drag.current;
4852
- drag.current = null;
4853
- if (!current) return;
4854
- if (current.moved)
4855
- onDrag?.(
4856
- {
4857
- x: event.clientX - current.grab.x,
4858
- y: event.clientY - current.grab.y
4859
- },
4860
- "end"
4861
- );
4862
- else onClick();
4863
- },
4864
- onPointerCancel: () => drag.current = null,
4865
- className: `studio-hotspot-marker flex h-7 max-w-[240px] items-center gap-1.5 rounded-full pl-1 pr-2.5 text-[11px] font-semibold text-white ${onDrag ? "cursor-grab active:cursor-grabbing" : ""}`,
5069
+ ...handlers,
5070
+ className: `studio-hotspot-marker relative flex h-7 w-7 items-center justify-center rounded-full text-white ${onDrag ? "cursor-grab active:cursor-grabbing" : ""}`,
4866
5071
  style: {
4867
5072
  background: SEVERITIES[severity].color,
4868
5073
  touchAction: "none",
@@ -4872,18 +5077,37 @@ function Marker({
4872
5077
  },
4873
5078
  children: [
4874
5079
  /* @__PURE__ */ jsx9("span", { className: "flex h-5 w-5 flex-none items-center justify-center rounded-full bg-white/20", children: /* @__PURE__ */ jsx9(SeverityIcon, { severity }) }),
4875
- /* @__PURE__ */ jsx9("span", { className: "studio-hotspot-label truncate", children: label })
5080
+ count > 1 && /* @__PURE__ */ jsx9(
5081
+ "span",
5082
+ {
5083
+ "aria-hidden": true,
5084
+ className: "absolute -right-1.5 -top-1.5 flex h-4 min-w-4 items-center justify-center rounded-full bg-[rgb(var(--ec-card-bg))] px-1 text-[10px] font-bold leading-none shadow-sm",
5085
+ style: { color: SEVERITIES[severity].color },
5086
+ children: count
5087
+ }
5088
+ )
4876
5089
  ]
4877
5090
  }
4878
5091
  );
4879
5092
  }
4880
- var panelClass2 = "studio-hotspot-panel absolute left-0 top-[34px] w-80 rounded-xl border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-card-bg))] text-[rgb(var(--ec-page-text))] shadow-xl";
5093
+ var panelClass2 = "studio-hotspot-panel absolute left-[40px] top-[-8px] w-80 rounded-xl border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-card-bg))] text-[rgb(var(--ec-page-text))] shadow-xl";
5094
+ var PanelArrow = ({ tint }) => /* @__PURE__ */ jsx9(
5095
+ "span",
5096
+ {
5097
+ "aria-hidden": true,
5098
+ className: "absolute left-[-6.5px] top-[16px] h-3 w-3 rotate-45 rounded-bl-[2px] border-b border-l border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-card-bg))]",
5099
+ style: tint ? {
5100
+ background: `linear-gradient(${tint}14, ${tint}14), rgb(var(--ec-card-bg))`
5101
+ } : void 0
5102
+ }
5103
+ );
4881
5104
  var inputClass = "block w-full rounded-md border border-[rgb(var(--ec-input-border))] bg-[rgb(var(--ec-input-bg))] px-2.5 py-1.5 text-sm text-[rgb(var(--ec-input-text))] outline-none focus:border-[rgb(var(--ec-accent))]";
4882
5105
  function HotspotForm({
4883
5106
  initial,
4884
5107
  submitLabel,
4885
5108
  onSubmit,
4886
- onCancel
5109
+ onCancel,
5110
+ onSeverityChange
4887
5111
  }) {
4888
5112
  const [severity, setSeverity] = useState9(
4889
5113
  initial?.severity ?? "warning"
@@ -4919,7 +5143,10 @@ function HotspotForm({
4919
5143
  type: "button",
4920
5144
  role: "radio",
4921
5145
  "aria-checked": picked,
4922
- onClick: () => setSeverity(value),
5146
+ onClick: () => {
5147
+ setSeverity(value);
5148
+ onSeverityChange?.(value);
5149
+ },
4923
5150
  className: "flex flex-1 items-center justify-center gap-1 rounded-md border px-2 py-1.5 text-xs font-semibold transition-colors",
4924
5151
  style: picked ? {
4925
5152
  background: SEVERITIES[value].color,
@@ -4953,14 +5180,14 @@ function HotspotForm({
4953
5180
  {
4954
5181
  value: description,
4955
5182
  maxLength: 1e3,
4956
- rows: 3,
5183
+ rows: 5,
4957
5184
  onChange: (event) => setDescription(event.target.value),
4958
5185
  onKeyDown: (event) => {
4959
5186
  if (event.key === "Enter" && (event.metaKey || event.ctrlKey))
4960
5187
  submit();
4961
5188
  },
4962
5189
  placeholder: "Why it matters, and what to do about it (optional)",
4963
- className: `${inputClass} resize-none`
5190
+ className: `${inputClass} min-h-20 max-h-[60vh] resize-y`
4964
5191
  }
4965
5192
  ),
4966
5193
  /* @__PURE__ */ jsxs6("div", { className: "flex justify-end gap-2", children: [
@@ -4978,7 +5205,7 @@ function HotspotForm({
4978
5205
  {
4979
5206
  type: "submit",
4980
5207
  disabled: !title.trim(),
4981
- className: "rounded-md bg-[rgb(var(--ec-primary-bg))] px-3 py-1.5 text-xs font-semibold text-[rgb(var(--ec-primary-text))] disabled:opacity-40",
5208
+ className: "rounded-md bg-[rgb(var(--ec-button-bg))] px-3 py-1.5 text-xs font-semibold text-[rgb(var(--ec-button-text))] disabled:opacity-40",
4982
5209
  children: submitLabel
4983
5210
  }
4984
5211
  )
@@ -4987,41 +5214,28 @@ function HotspotForm({
4987
5214
  }
4988
5215
  );
4989
5216
  }
4990
- function SeverityTag({ severity }) {
4991
- return /* @__PURE__ */ jsxs6(
4992
- "span",
4993
- {
4994
- className: "inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wide text-white",
4995
- style: { background: SEVERITIES[severity].color },
4996
- children: [
4997
- /* @__PURE__ */ jsx9(SeverityIcon, { severity, size: 10 }),
4998
- SEVERITIES[severity].label
4999
- ]
5000
- }
5001
- );
5217
+ function HotspotItem({ hotspot }) {
5218
+ return /* @__PURE__ */ jsxs6("div", { children: [
5219
+ /* @__PURE__ */ jsx9("p", { className: "text-[15px] font-semibold leading-snug text-[rgb(var(--ec-page-text))]", children: hotspot.title }),
5220
+ hotspot.description && /* @__PURE__ */ jsx9("p", { className: "mt-1.5 whitespace-pre-wrap text-[13px] leading-relaxed text-[rgb(var(--ec-page-text-muted))]", children: hotspot.description })
5221
+ ] });
5002
5222
  }
5003
- function HotspotItem({
5223
+ function Flagged({
5004
5224
  hotspot,
5005
- actions
5225
+ className = ""
5006
5226
  }) {
5007
5227
  return /* @__PURE__ */ jsxs6(
5008
5228
  "div",
5009
5229
  {
5010
- className: "border-l-[3px] pl-2.5",
5011
- style: { borderColor: SEVERITIES[hotspot.severity].color },
5230
+ className: `flex items-center gap-1.5 text-[11px] text-[rgb(var(--ec-page-text-muted))] ${className}`,
5012
5231
  children: [
5013
- /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2", children: [
5014
- /* @__PURE__ */ jsx9(SeverityTag, { severity: hotspot.severity }),
5015
- /* @__PURE__ */ jsxs6("span", { className: "flex min-w-0 items-center gap-1 truncate text-[11px] text-[rgb(var(--ec-page-text-muted))]", children: [
5016
- hotspot.author.agent && /* @__PURE__ */ jsx9(AgentMark, { name: hotspot.author.name, size: 11 }),
5017
- hotspot.author.name,
5018
- " \xB7 ",
5019
- timeAgo(hotspot.createdAt)
5020
- ] }),
5021
- actions && /* @__PURE__ */ jsx9("span", { className: "ml-auto flex flex-none items-center gap-0.5", children: actions })
5022
- ] }),
5023
- /* @__PURE__ */ jsx9("p", { className: "mt-1.5 text-sm font-semibold leading-snug", children: hotspot.title }),
5024
- hotspot.description && /* @__PURE__ */ jsx9("p", { className: "mt-1 whitespace-pre-wrap text-xs leading-relaxed text-[rgb(var(--ec-page-text-muted))]", children: hotspot.description })
5232
+ hotspot.author.agent && /* @__PURE__ */ jsx9(AgentMark, { name: hotspot.author.name, size: 12 }),
5233
+ /* @__PURE__ */ jsxs6("span", { className: "min-w-0 truncate", children: [
5234
+ "Flagged by ",
5235
+ hotspot.author.name,
5236
+ " \xB7 ",
5237
+ timeAgo(hotspot.createdAt)
5238
+ ] })
5025
5239
  ]
5026
5240
  }
5027
5241
  );
@@ -5043,6 +5257,32 @@ function IconButton2({
5043
5257
  }
5044
5258
  );
5045
5259
  }
5260
+ function SeverityHeader({
5261
+ severity,
5262
+ actions,
5263
+ className = ""
5264
+ }) {
5265
+ const { label, color } = SEVERITIES[severity];
5266
+ return /* @__PURE__ */ jsxs6(
5267
+ "div",
5268
+ {
5269
+ className: `flex items-center gap-2 border-b px-3 py-2.5 ${className}`,
5270
+ style: { background: `${color}14`, borderColor: `${color}33` },
5271
+ children: [
5272
+ /* @__PURE__ */ jsx9(
5273
+ "span",
5274
+ {
5275
+ className: "flex h-6 w-6 flex-none items-center justify-center rounded-full text-white",
5276
+ style: { background: color },
5277
+ children: /* @__PURE__ */ jsx9(SeverityIcon, { severity, size: 13 })
5278
+ }
5279
+ ),
5280
+ /* @__PURE__ */ jsx9("span", { className: "text-xs font-semibold", style: { color }, children: label }),
5281
+ actions && /* @__PURE__ */ jsx9("span", { className: "ml-auto flex items-center gap-0.5", children: actions })
5282
+ ]
5283
+ }
5284
+ );
5285
+ }
5046
5286
  function SpotPanel({
5047
5287
  spot,
5048
5288
  locked,
@@ -5051,64 +5291,108 @@ function SpotPanel({
5051
5291
  onDelete,
5052
5292
  onGo
5053
5293
  }) {
5054
- const [editing, setEditing] = useState9(null);
5294
+ const [editing, setEditingId] = useState9(null);
5295
+ const [picked, setPicked] = useState9(null);
5296
+ const setEditing = (id) => {
5297
+ setEditingId(id);
5298
+ setPicked(null);
5299
+ };
5055
5300
  const sorted = [...spot.hotspots].sort(bySeverity);
5056
- return /* @__PURE__ */ jsxs6("div", { className: panelClass2, children: [
5057
- /* @__PURE__ */ jsxs6("div", { className: "flex items-center justify-between border-b border-[rgb(var(--ec-page-border))] px-3 py-2", children: [
5058
- /* @__PURE__ */ jsx9("span", { className: "text-xs font-semibold", children: sorted.length === 1 ? "Hotspot" : `${sorted.length} hotspots` }),
5059
- /* @__PURE__ */ jsx9(IconButton2, { label: "Close", onClick: onClose, children: /* @__PURE__ */ jsx9(X3, { size: 14 }) })
5060
- ] }),
5061
- /* @__PURE__ */ jsx9("div", { className: "max-h-80 space-y-3 overflow-y-auto p-3", children: sorted.map(
5062
- (hotspot) => editing === hotspot.id ? /* @__PURE__ */ jsx9("div", { className: "-m-3", children: /* @__PURE__ */ jsx9(
5063
- HotspotForm,
5064
- {
5065
- initial: hotspot,
5066
- submitLabel: "Save",
5067
- onCancel: () => setEditing(null),
5068
- onSubmit: (fields) => {
5069
- onUpdate(hotspot.id, fields);
5070
- setEditing(null);
5071
- }
5301
+ const single = sorted.length === 1 && !spot.inside;
5302
+ const close = /* @__PURE__ */ jsx9(IconButton2, { label: "Close", onClick: onClose, children: /* @__PURE__ */ jsx9(X3, { size: 14 }) });
5303
+ const severityOf = (hotspot) => editing === hotspot.id && picked || hotspot.severity;
5304
+ const section = (hotspot, first) => {
5305
+ const changing = editing === hotspot.id;
5306
+ const actions = /* @__PURE__ */ jsxs6(Fragment6, { children: [
5307
+ !locked && !spot.inside && !changing && /* @__PURE__ */ jsxs6(Fragment6, { children: [
5308
+ /* @__PURE__ */ jsx9(IconButton2, { label: "Edit", onClick: () => setEditing(hotspot.id), children: /* @__PURE__ */ jsx9(Pencil2, { size: 13 }) }),
5309
+ /* @__PURE__ */ jsx9(IconButton2, { label: "Remove", onClick: () => onDelete(hotspot.id), children: /* @__PURE__ */ jsx9(Trash23, { size: 13 }) })
5310
+ ] }),
5311
+ single && close
5312
+ ] });
5313
+ const body = changing ? /* @__PURE__ */ jsx9(
5314
+ HotspotForm,
5315
+ {
5316
+ initial: hotspot,
5317
+ submitLabel: "Save",
5318
+ onSeverityChange: setPicked,
5319
+ onCancel: () => setEditing(null),
5320
+ onSubmit: (fields) => {
5321
+ onUpdate(hotspot.id, fields);
5322
+ setEditing(null);
5072
5323
  }
5073
- ) }, hotspot.id) : spot.inside ? (
5074
- // Inside a node on L1 or L2: goes to it on L3, where it is
5075
- /* @__PURE__ */ jsx9(
5076
- "button",
5077
- {
5078
- type: "button",
5079
- onClick: () => onGo(hotspot),
5080
- className: "block w-full rounded-md text-left hover:bg-[rgb(var(--ec-content-hover))]!",
5081
- children: /* @__PURE__ */ jsx9(HotspotItem, { hotspot })
5082
- },
5083
- hotspot.id
5084
- )
5085
- ) : /* @__PURE__ */ jsx9(
5086
- HotspotItem,
5324
+ }
5325
+ ) : /* @__PURE__ */ jsxs6(Fragment6, { children: [
5326
+ /* @__PURE__ */ jsx9("div", { className: "px-4 pb-3.5 pt-3", children: /* @__PURE__ */ jsx9(HotspotItem, { hotspot }) }),
5327
+ /* @__PURE__ */ jsx9(
5328
+ Flagged,
5087
5329
  {
5088
5330
  hotspot,
5089
- actions: !locked && /* @__PURE__ */ jsxs6(Fragment6, { children: [
5090
- /* @__PURE__ */ jsx9(
5091
- IconButton2,
5092
- {
5093
- label: "Edit",
5094
- onClick: () => setEditing(hotspot.id),
5095
- children: /* @__PURE__ */ jsx9(Pencil2, { size: 13 })
5096
- }
5097
- ),
5098
- /* @__PURE__ */ jsx9(
5099
- IconButton2,
5100
- {
5101
- label: "Remove",
5102
- onClick: () => onDelete(hotspot.id),
5103
- children: /* @__PURE__ */ jsx9(Trash23, { size: 13 })
5104
- }
5105
- )
5106
- ] })
5107
- },
5108
- hotspot.id
5331
+ className: "border-t border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-page-text)/0.02)] px-4 py-2.5"
5332
+ }
5109
5333
  )
5110
- ) }),
5111
- spot.inside && /* @__PURE__ */ jsx9("p", { className: "border-t border-[rgb(var(--ec-page-border))] px-3 py-2 text-[11px] text-[rgb(var(--ec-page-text-muted))]", children: "Inside this: click one to go to it." })
5334
+ ] });
5335
+ return /* @__PURE__ */ jsx9(
5336
+ HotspotSection,
5337
+ {
5338
+ header: /* @__PURE__ */ jsx9(
5339
+ SeverityHeader,
5340
+ {
5341
+ severity: severityOf(hotspot),
5342
+ actions,
5343
+ className: single && first ? "rounded-t-[11px]" : ""
5344
+ }
5345
+ ),
5346
+ onGo: spot.inside ? () => onGo(hotspot) : void 0,
5347
+ children: body
5348
+ },
5349
+ hotspot.id
5350
+ );
5351
+ };
5352
+ return /* @__PURE__ */ jsxs6("div", { className: panelClass2, children: [
5353
+ /* @__PURE__ */ jsx9(
5354
+ PanelArrow,
5355
+ {
5356
+ tint: single ? SEVERITIES[severityOf(sorted[0])].color : void 0
5357
+ }
5358
+ ),
5359
+ !single && /* @__PURE__ */ jsxs6("div", { className: "flex items-center justify-between border-b border-[rgb(var(--ec-page-border))] px-3 py-2.5", children: [
5360
+ /* @__PURE__ */ jsxs6("span", { className: "text-xs font-semibold", children: [
5361
+ sorted.length,
5362
+ " hotspot",
5363
+ sorted.length === 1 ? "" : "s"
5364
+ ] }),
5365
+ close
5366
+ ] }),
5367
+ /* @__PURE__ */ jsx9(
5368
+ "div",
5369
+ {
5370
+ className: `max-h-[min(70vh,640px)] divide-y divide-[rgb(var(--ec-page-border))] overflow-y-auto ${spot.inside ? "" : "rounded-b-[11px]"} ${single ? "rounded-t-[11px]" : ""}`,
5371
+ children: sorted.map((hotspot, index) => section(hotspot, index === 0))
5372
+ }
5373
+ ),
5374
+ spot.inside && /* @__PURE__ */ jsx9("p", { className: "rounded-b-[11px] border-t border-[rgb(var(--ec-page-border))] px-4 py-2 text-[11px] text-[rgb(var(--ec-page-text-muted))]", children: "Inside this: click one to go to it." })
5375
+ ] });
5376
+ }
5377
+ function HotspotSection({
5378
+ header,
5379
+ onGo,
5380
+ children
5381
+ }) {
5382
+ return onGo ? /* @__PURE__ */ jsxs6(
5383
+ "button",
5384
+ {
5385
+ type: "button",
5386
+ onClick: onGo,
5387
+ className: "block w-full text-left transition-colors hover:bg-[rgb(var(--ec-accent)/0.06)]",
5388
+ children: [
5389
+ header,
5390
+ children
5391
+ ]
5392
+ }
5393
+ ) : /* @__PURE__ */ jsxs6("div", { children: [
5394
+ header,
5395
+ children
5112
5396
  ] });
5113
5397
  }
5114
5398
  var HotspotLayer = memo6(function HotspotLayer2({
@@ -5126,11 +5410,13 @@ var HotspotLayer = memo6(function HotspotLayer2({
5126
5410
  onUpdate,
5127
5411
  onDelete,
5128
5412
  onGo,
5413
+ onMoveDraft,
5129
5414
  live
5130
5415
  }) {
5131
5416
  const layer = useRef7(null);
5132
5417
  useMarkerScale(layer);
5133
5418
  const { screenToFlowPosition } = useReactFlow4();
5419
+ const store = useStoreApi4();
5134
5420
  const [moving, setMoving] = useState9(null);
5135
5421
  const remote = useRemoteMoves(live, "hotspot");
5136
5422
  const remoteAt = (spot) => {
@@ -5162,6 +5448,50 @@ var HotspotLayer = memo6(function HotspotLayer2({
5162
5448
  );
5163
5449
  sendMove(live, "hotspot", spot.hotspots[0].id, null);
5164
5450
  } : void 0;
5451
+ const [draftSeverity, setDraftSeverity] = useState9("warning");
5452
+ useEffect8(() => {
5453
+ if (!draft) setDraftSeverity("warning");
5454
+ }, [draft]);
5455
+ const [draftMoving, setDraftMoving] = useState9(null);
5456
+ const draftDrag = onMoveDraft && !locked ? (point, phase) => {
5457
+ const position = screenToFlowPosition(point);
5458
+ const over = nodeUnder(point);
5459
+ if (phase === "move") {
5460
+ if (over !== draftMoving?.over)
5461
+ onDropTarget(
5462
+ over ? { nodeId: over, severity: draftSeverity } : null
5463
+ );
5464
+ return setDraftMoving({ position, over });
5465
+ }
5466
+ setDraftMoving(null);
5467
+ onDropTarget(null);
5468
+ onMoveDraft(over ? { nodeId: over, position } : { position });
5469
+ } : void 0;
5470
+ const openSpot = useRef7(void 0);
5471
+ openSpot.current = spots.find((spot) => spot.key === openKey);
5472
+ const deleteOpen = useRef7(onDelete);
5473
+ deleteOpen.current = onDelete;
5474
+ const closeOpen = useRef7(onOpen);
5475
+ closeOpen.current = onOpen;
5476
+ useEffect8(() => {
5477
+ if (!openKey || locked) return;
5478
+ const onKey = (event) => {
5479
+ if (event.key !== "Delete" && event.key !== "Backspace") return;
5480
+ const target = event.target instanceof Element ? event.target : null;
5481
+ if (target?.closest("input, textarea, select, [contenteditable]")) return;
5482
+ const spot = openSpot.current;
5483
+ if (!spot || spot.inside) return;
5484
+ const { nodes, edges } = store.getState();
5485
+ if (nodes.some((node) => node.selected) || edges.some((edge) => edge.selected))
5486
+ return;
5487
+ event.preventDefault();
5488
+ event.stopPropagation();
5489
+ spot.hotspots.forEach((hotspot) => deleteOpen.current(hotspot.id));
5490
+ closeOpen.current(null);
5491
+ };
5492
+ window.addEventListener("keydown", onKey, true);
5493
+ return () => window.removeEventListener("keydown", onKey, true);
5494
+ }, [openKey, locked, store]);
5165
5495
  return /* @__PURE__ */ jsx9(CanvasPortal, { children: /* @__PURE__ */ jsxs6(
5166
5496
  "div",
5167
5497
  {
@@ -5186,11 +5516,14 @@ var HotspotLayer = memo6(function HotspotLayer2({
5186
5516
  spot,
5187
5517
  open,
5188
5518
  selected: !!selectedIds && spot.hotspots.some((hotspot) => selectedIds.has(hotspot.id)),
5189
- onClick: () => onOpen(open ? null : spot.key),
5519
+ onClick: () => {
5520
+ if (!open) store.getState().unselectNodesAndEdges();
5521
+ onOpen(open ? null : spot.key);
5522
+ },
5190
5523
  onDrag: dragHandler(spot)
5191
5524
  }
5192
5525
  ),
5193
- open && !dragged && /* @__PURE__ */ jsx9(
5526
+ open && (!dragged || moving?.key === spot.key) && /* @__PURE__ */ jsx9(
5194
5527
  SpotPanel,
5195
5528
  {
5196
5529
  spot,
@@ -5209,26 +5542,23 @@ var HotspotLayer = memo6(function HotspotLayer2({
5209
5542
  draft && /* @__PURE__ */ jsxs6(
5210
5543
  SpotAt,
5211
5544
  {
5212
- nodeId: draft.nodeId,
5213
- position: draft.nodeId ? void 0 : draft.position,
5545
+ nodeId: draftMoving ? void 0 : draft.nodeId,
5546
+ position: draftMoving ? draftMoving.position : draft.nodeId ? void 0 : draft.position,
5214
5547
  zIndex: 4,
5215
5548
  children: [
5216
- /* @__PURE__ */ jsx9(
5217
- "span",
5218
- {
5219
- className: "studio-hotspot-marker flex h-7 w-7 items-center justify-center rounded-full text-white",
5220
- style: { background: SEVERITIES.warning.color },
5221
- children: /* @__PURE__ */ jsx9(SeverityIcon, { severity: "warning" })
5222
- }
5223
- ),
5224
- /* @__PURE__ */ jsx9("div", { className: panelClass2, children: /* @__PURE__ */ jsx9(
5225
- HotspotForm,
5226
- {
5227
- submitLabel: "Add hotspot",
5228
- onSubmit: onCreate,
5229
- onCancel: onCancelDraft
5230
- }
5231
- ) })
5549
+ /* @__PURE__ */ jsx9(DraftMarker, { severity: draftSeverity, onDrag: draftDrag }),
5550
+ /* @__PURE__ */ jsxs6("div", { className: panelClass2, children: [
5551
+ /* @__PURE__ */ jsx9(PanelArrow, {}),
5552
+ /* @__PURE__ */ jsx9(
5553
+ HotspotForm,
5554
+ {
5555
+ onSeverityChange: setDraftSeverity,
5556
+ submitLabel: "Add hotspot",
5557
+ onSubmit: onCreate,
5558
+ onCancel: onCancelDraft
5559
+ }
5560
+ )
5561
+ ] })
5232
5562
  ]
5233
5563
  }
5234
5564
  )
@@ -5236,33 +5566,101 @@ var HotspotLayer = memo6(function HotspotLayer2({
5236
5566
  }
5237
5567
  ) });
5238
5568
  });
5569
+ function DraftMarker({
5570
+ severity,
5571
+ onDrag
5572
+ }) {
5573
+ const handlers = useMarkerDrag(() => {
5574
+ }, onDrag);
5575
+ return /* @__PURE__ */ jsx9(
5576
+ "span",
5577
+ {
5578
+ ...handlers,
5579
+ title: onDrag ? "Drag to move it" : void 0,
5580
+ className: `studio-hotspot-marker flex h-7 w-7 items-center justify-center rounded-full text-white ${onDrag ? "cursor-grab active:cursor-grabbing" : ""}`,
5581
+ style: { background: SEVERITIES[severity].color, touchAction: "none" },
5582
+ children: /* @__PURE__ */ jsx9(SeverityIcon, { severity })
5583
+ }
5584
+ );
5585
+ }
5239
5586
  function HotspotList({
5240
5587
  hotspots,
5241
5588
  nameOf,
5242
- onOpen
5589
+ onOpen,
5590
+ goOnHover = true
5243
5591
  }) {
5244
5592
  const sorted = [...hotspots].sort(bySeverity);
5245
5593
  if (!sorted.length)
5246
5594
  return /* @__PURE__ */ jsx9("p", { className: "px-4 py-6 text-center text-xs leading-relaxed text-[rgb(var(--ec-page-text-muted))]", children: "No hotspots yet. Press A, or right-click a node, to flag what people should know: a risk, a problem, or the key part." });
5247
- return /* @__PURE__ */ jsx9("div", { className: "space-y-1.5 p-3", children: sorted.map((hotspot) => {
5248
- const on = hotspot.nodeId && nameOf(hotspot.nodeId);
5249
- return /* @__PURE__ */ jsxs6(
5250
- "button",
5251
- {
5252
- type: "button",
5253
- onClick: () => onOpen(hotspot),
5254
- className: "w-full rounded-md border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-page-bg))] px-2.5 py-2 text-left hover:border-[rgb(var(--ec-accent))]!",
5255
- children: [
5256
- /* @__PURE__ */ jsx9(HotspotItem, { hotspot }),
5257
- on && /* @__PURE__ */ jsxs6("p", { className: "mt-1.5 pl-[13px] text-[11px] text-[rgb(var(--ec-page-text-muted))]", children: [
5595
+ return /* @__PURE__ */ jsx9("ul", { className: "divide-y divide-[rgb(var(--ec-page-border))]", children: sorted.map((hotspot) => /* @__PURE__ */ jsx9(
5596
+ HotspotRow,
5597
+ {
5598
+ hotspot,
5599
+ on: hotspot.nodeId && nameOf(hotspot.nodeId),
5600
+ onOpen,
5601
+ goOnHover
5602
+ },
5603
+ hotspot.id
5604
+ )) });
5605
+ }
5606
+ var GO_ON_HOVER_MS = 250;
5607
+ function HotspotRow({
5608
+ hotspot,
5609
+ on,
5610
+ onOpen,
5611
+ goOnHover
5612
+ }) {
5613
+ const hover = useRef7(null);
5614
+ const cancel = () => {
5615
+ if (hover.current !== null) clearTimeout(hover.current);
5616
+ hover.current = null;
5617
+ };
5618
+ useEffect8(() => cancel, []);
5619
+ const { label, color, icon: Icon } = SEVERITIES[hotspot.severity];
5620
+ return /* @__PURE__ */ jsx9("li", { children: /* @__PURE__ */ jsxs6(
5621
+ "button",
5622
+ {
5623
+ type: "button",
5624
+ onClick: () => {
5625
+ cancel();
5626
+ onOpen(hotspot);
5627
+ },
5628
+ onMouseEnter: () => {
5629
+ cancel();
5630
+ if (goOnHover)
5631
+ hover.current = setTimeout(() => onOpen(hotspot), GO_ON_HOVER_MS);
5632
+ },
5633
+ onMouseLeave: cancel,
5634
+ title: "Show me on the canvas",
5635
+ className: "flex w-full cursor-pointer items-center gap-2.5 px-3 py-2 text-left text-xs text-[rgb(var(--ec-page-text))] transition-colors hover:bg-[rgb(var(--ec-accent)/0.06)]",
5636
+ children: [
5637
+ /* @__PURE__ */ jsx9(
5638
+ Icon,
5639
+ {
5640
+ size: 14,
5641
+ className: "flex-none",
5642
+ style: { color },
5643
+ "aria-label": label
5644
+ }
5645
+ ),
5646
+ /* @__PURE__ */ jsxs6("span", { className: "min-w-0 flex-1", children: [
5647
+ /* @__PURE__ */ jsx9("span", { className: "block", children: hotspot.title }),
5648
+ on && /* @__PURE__ */ jsxs6("span", { className: "mt-0.5 block truncate text-[11px] text-[rgb(var(--ec-page-text-muted))]", children: [
5258
5649
  "On ",
5259
5650
  on
5260
5651
  ] })
5261
- ]
5262
- },
5263
- hotspot.id
5264
- );
5265
- }) });
5652
+ ] }),
5653
+ /* @__PURE__ */ jsx9(
5654
+ LocateFixed,
5655
+ {
5656
+ size: 12,
5657
+ className: "flex-none text-[rgb(var(--ec-icon-color))]",
5658
+ "aria-hidden": true
5659
+ }
5660
+ )
5661
+ ]
5662
+ }
5663
+ ) });
5266
5664
  }
5267
5665
 
5268
5666
  // src/studio/components/CanvasControls.tsx
@@ -5572,7 +5970,7 @@ var ViewControls = memo7(function ViewControls2({
5572
5970
  });
5573
5971
 
5574
5972
  // src/studio/components/ContainerChooser.tsx
5575
- import { useEffect as useEffect8, useState as useState10 } from "react";
5973
+ import { useEffect as useEffect9, useState as useState10 } from "react";
5576
5974
  import { X as X4 } from "lucide-react";
5577
5975
  import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
5578
5976
  var plural = (count, noun) => `${count} ${noun}${count === 1 ? "" : "s"}`;
@@ -5728,7 +6126,7 @@ function ContainerChooser({
5728
6126
  contents.systems > 0 && plural(contents.systems, "system"),
5729
6127
  contents.services > 0 && plural(contents.services, "service")
5730
6128
  ].filter(Boolean).join(" and ") : "";
5731
- useEffect8(() => {
6129
+ useEffect9(() => {
5732
6130
  const onKeyDown = (event) => event.key === "Escape" && onCancel();
5733
6131
  window.addEventListener("keydown", onKeyDown);
5734
6132
  return () => window.removeEventListener("keydown", onKeyDown);
@@ -5853,9 +6251,11 @@ var EditButton_default = memo8(function EditButton({
5853
6251
  import {
5854
6252
  memo as memo9,
5855
6253
  useCallback as useCallback5,
5856
- useEffect as useEffect9,
6254
+ useEffect as useEffect10,
6255
+ useLayoutEffect as useLayoutEffect4,
5857
6256
  useRef as useRef8,
5858
- useState as useState11
6257
+ useState as useState11,
6258
+ useSyncExternalStore
5859
6259
  } from "react";
5860
6260
  import { ChevronDown, GripVertical as GripVertical2 } from "lucide-react";
5861
6261
  import { Fragment as Fragment9, jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
@@ -5891,6 +6291,21 @@ var readCollapsed = () => {
5891
6291
  return false;
5892
6292
  }
5893
6293
  };
6294
+ var shownCollapsed = false;
6295
+ var collapsedListeners = /* @__PURE__ */ new Set();
6296
+ var showCollapsed = (collapsed) => {
6297
+ if (collapsed === shownCollapsed) return;
6298
+ shownCollapsed = collapsed;
6299
+ collapsedListeners.forEach((listener) => listener());
6300
+ };
6301
+ var usePaletteCollapsed = () => useSyncExternalStore(
6302
+ (listener) => {
6303
+ collapsedListeners.add(listener);
6304
+ return () => collapsedListeners.delete(listener);
6305
+ },
6306
+ () => shownCollapsed,
6307
+ () => false
6308
+ );
5894
6309
  function PaletteItem({
5895
6310
  icon: Icon,
5896
6311
  iconClassName,
@@ -5949,7 +6364,7 @@ function PaletteSection({
5949
6364
  );
5950
6365
  }
5951
6366
  var COMPONENTS_TAB = "components";
5952
- var Palette = memo9(function Palette2({
6367
+ var Palette2 = memo9(function Palette3({
5953
6368
  level,
5954
6369
  onAddComponent,
5955
6370
  footer,
@@ -5969,6 +6384,8 @@ var Palette = memo9(function Palette2({
5969
6384
  } catch {
5970
6385
  }
5971
6386
  };
6387
+ useLayoutEffect4(() => showCollapsed(collapsed), [collapsed]);
6388
+ useLayoutEffect4(() => () => showCollapsed(false), []);
5972
6389
  const toggle = useCallback5(() => {
5973
6390
  setCollapsed((current) => {
5974
6391
  remember(!current);
@@ -5986,7 +6403,7 @@ var Palette = memo9(function Palette2({
5986
6403
  }
5987
6404
  };
5988
6405
  const chosen = useRef8(tab);
5989
- useEffect9(() => {
6406
+ useEffect10(() => {
5990
6407
  if (tab === chosen.current) return;
5991
6408
  chosen.current = tab;
5992
6409
  if (tab === void 0 || !collapsed) return;
@@ -6187,7 +6604,7 @@ function ShortcutsList({ omit = [] }) {
6187
6604
  }
6188
6605
 
6189
6606
  // src/studio/components/EmptyCanvas.tsx
6190
- import { useEffect as useEffect10, useState as useState12 } from "react";
6607
+ import { useEffect as useEffect11, useState as useState12 } from "react";
6191
6608
  import { CircleHelp, FolderOpen, Plug } from "lucide-react";
6192
6609
  import { jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
6193
6610
  var hintText = "font-[Excalifont,Inter,sans-serif] text-[14px] leading-snug text-[color-mix(in_srgb,rgb(var(--ec-page-text))_22%,transparent)]";
@@ -6199,8 +6616,16 @@ var arrow = {
6199
6616
  strokeLinejoin: "round"
6200
6617
  };
6201
6618
  function Hints({ palette, live }) {
6619
+ const paletteCollapsed = usePaletteCollapsed();
6202
6620
  return /* @__PURE__ */ jsxs12("div", { "aria-hidden": true, className: `hidden md:block! ${hintText}`, children: [
6203
- palette && /* @__PURE__ */ jsxs12("div", { className: "absolute bottom-[120px] left-[290px] flex items-start gap-1", children: [
6621
+ palette && paletteCollapsed && /* @__PURE__ */ jsxs12("div", { className: "absolute left-[284px] top-[75px] flex items-start gap-1", children: [
6622
+ /* @__PURE__ */ jsxs12("svg", { width: "56", height: "32", viewBox: "0 0 70 40", ...arrow, children: [
6623
+ /* @__PURE__ */ jsx15("path", { d: "M66 30 C 45 34, 22 28, 6 12" }),
6624
+ /* @__PURE__ */ jsx15("path", { d: "M6 12 L15.7 14.6 M6 12 L8.6 21.7" })
6625
+ ] }),
6626
+ /* @__PURE__ */ jsx15("p", { className: "mt-4 max-w-[160px]", children: "Drag resources onto the canvas, then connect them" })
6627
+ ] }),
6628
+ palette && !paletteCollapsed && /* @__PURE__ */ jsxs12("div", { className: "absolute bottom-[120px] left-[290px] flex items-start gap-1", children: [
6204
6629
  /* @__PURE__ */ jsxs12("svg", { width: "56", height: "32", viewBox: "0 0 70 40", ...arrow, children: [
6205
6630
  /* @__PURE__ */ jsx15("path", { d: "M66 8 C 45 4, 22 10, 6 26" }),
6206
6631
  /* @__PURE__ */ jsx15("path", { d: "M6 26 L15.7 23.4 M6 26 L8.6 16.3" })
@@ -6245,7 +6670,7 @@ function EmptyCanvas({
6245
6670
  }) {
6246
6671
  const [dragging, setDragging] = useState12(false);
6247
6672
  const [dropped, setDropped] = useState12(false);
6248
- useEffect10(() => {
6673
+ useEffect11(() => {
6249
6674
  const start = () => setDragging(true);
6250
6675
  const end = () => setDragging(false);
6251
6676
  const drop = () => {
@@ -6306,7 +6731,7 @@ function EmptyCanvas({
6306
6731
  }
6307
6732
 
6308
6733
  // src/studio/components/RemeasureHandles.tsx
6309
- import { memo as memo10, useEffect as useEffect11 } from "react";
6734
+ import { memo as memo10, useEffect as useEffect12 } from "react";
6310
6735
  import {
6311
6736
  useStore as useStore2,
6312
6737
  useUpdateNodeInternals as useUpdateNodeInternals2
@@ -6315,7 +6740,7 @@ var selectDomNode = (state) => state.domNode;
6315
6740
  var RemeasureHandles = memo10(function RemeasureHandles2() {
6316
6741
  const domNode = useStore2(selectDomNode);
6317
6742
  const updateNodeInternals = useUpdateNodeInternals2();
6318
- useEffect11(() => {
6743
+ useEffect12(() => {
6319
6744
  if (!domNode) return;
6320
6745
  const pending = /* @__PURE__ */ new Set();
6321
6746
  let frame = 0;
@@ -6398,9 +6823,10 @@ export {
6398
6823
  MESSAGE_COLLECTIONS,
6399
6824
  NODE_CATEGORIES,
6400
6825
  NODE_ICONS,
6826
+ NOTE_COLORS,
6401
6827
  NodeCard,
6402
6828
  OpenAILogo,
6403
- Palette,
6829
+ Palette2 as Palette,
6404
6830
  PaletteItem,
6405
6831
  PaletteSection,
6406
6832
  Picture,
@@ -6534,6 +6960,7 @@ export {
6534
6960
  shortId,
6535
6961
  sizeOf,
6536
6962
  sortByHierarchy,
6963
+ startWriting,
6537
6964
  timeAgo,
6538
6965
  toDiagramFile,
6539
6966
  toRelativePosition,
@@ -6546,6 +6973,7 @@ export {
6546
6973
  useCounterScale,
6547
6974
  useDragGhost,
6548
6975
  useNodeAnchor,
6976
+ usePaletteCollapsed,
6549
6977
  useRemoteMoves,
6550
6978
  useSavedFlash,
6551
6979
  withDescendants,