isotone 1.1.2 → 1.2.0

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 (54) hide show
  1. package/AGENTS.md +84 -6
  2. package/CHANGELOG.md +46 -0
  3. package/README.md +64 -14
  4. package/dist/{IterationPanel-phJRYtUO.d.ts → IterationPanel-Bq77d5NT.d.ts} +3 -1
  5. package/dist/{chunk-FKEKBZ3E.js → chunk-2FY2JNPG.js} +95 -9
  6. package/dist/chunk-2FY2JNPG.js.map +1 -0
  7. package/dist/chunk-BDTB3DFR.js +100 -0
  8. package/dist/chunk-BDTB3DFR.js.map +1 -0
  9. package/dist/{chunk-PUXQGUMQ.js → chunk-BTJHQXF7.js} +104 -5
  10. package/dist/chunk-BTJHQXF7.js.map +1 -0
  11. package/dist/chunk-JCTWHR7U.js +13 -0
  12. package/dist/chunk-JCTWHR7U.js.map +1 -0
  13. package/dist/{chunk-I7CZUPDO.js → chunk-KRS57POF.js} +17 -33
  14. package/dist/chunk-KRS57POF.js.map +1 -0
  15. package/dist/chunk-L6O4HYGP.js +1150 -0
  16. package/dist/chunk-L6O4HYGP.js.map +1 -0
  17. package/dist/chunk-MX4RQJFV.js +9 -0
  18. package/dist/chunk-MX4RQJFV.js.map +1 -0
  19. package/dist/chunk-YPDV3PX4.js +142 -0
  20. package/dist/chunk-YPDV3PX4.js.map +1 -0
  21. package/dist/cli.js +1079 -148
  22. package/dist/cli.js.map +1 -1
  23. package/dist/core.d.ts +135 -10
  24. package/dist/core.js +12 -3
  25. package/dist/index.d.ts +13 -28
  26. package/dist/index.js +618 -281
  27. package/dist/index.js.map +1 -1
  28. package/dist/isotone.global.js +98 -42
  29. package/dist/serve.d.ts +5 -3
  30. package/dist/serve.js +60 -23
  31. package/dist/serve.js.map +1 -1
  32. package/dist/server.d.ts +7 -0
  33. package/dist/server.js +24 -11
  34. package/dist/server.js.map +1 -1
  35. package/dist/vite-plugin.d.ts +4 -3
  36. package/dist/vite-plugin.js +48 -128
  37. package/dist/vite-plugin.js.map +1 -1
  38. package/docs/blast.md +164 -0
  39. package/docs/integrations.md +75 -15
  40. package/docs/migration.md +19 -0
  41. package/docs/updates.md +110 -4
  42. package/docs/variants.md +102 -9
  43. package/package.json +2 -1
  44. package/dist/chunk-A4DX2V4C.js +0 -7
  45. package/dist/chunk-A4DX2V4C.js.map +0 -1
  46. package/dist/chunk-FKEKBZ3E.js.map +0 -1
  47. package/dist/chunk-I7CZUPDO.js.map +0 -1
  48. package/dist/chunk-PUXQGUMQ.js.map +0 -1
  49. package/dist/chunk-RQ2YI5KS.js +0 -24
  50. package/dist/chunk-RQ2YI5KS.js.map +0 -1
  51. package/dist/chunk-TGAU3WTF.js +0 -549
  52. package/dist/chunk-TGAU3WTF.js.map +0 -1
  53. package/dist/chunk-X6V2AKJL.js +0 -375
  54. package/dist/chunk-X6V2AKJL.js.map +0 -1
package/dist/index.js CHANGED
@@ -4,15 +4,22 @@ import {
4
4
  describeIterationElement,
5
5
  elementAsNode,
6
6
  isRouteVisible
7
- } from "./chunk-FKEKBZ3E.js";
7
+ } from "./chunk-2FY2JNPG.js";
8
+ import {
9
+ updateNoteText,
10
+ updatePrompt
11
+ } from "./chunk-JCTWHR7U.js";
8
12
  import {
9
13
  FEEDBACK_STORAGE_KEY,
10
14
  KNOBS_STORAGE_KEY,
15
+ MAX_FEEDBACK_LENGTH,
11
16
  OVERLAY_DEFAULT_OPTION,
12
17
  OVERLAY_GROUP_ID,
18
+ SECTION_SEPARATOR,
13
19
  VARIANTS_ENDPOINT,
14
20
  applyKnobValues,
15
21
  axisDefault,
22
+ axisSection,
16
23
  createOverlayVariantsSource,
17
24
  createVariantsSource,
18
25
  defaultSelections,
@@ -29,13 +36,12 @@ import {
29
36
  parseVariantParam,
30
37
  readBrowserStorage,
31
38
  removeBrowserStorage,
39
+ sectionAxes,
32
40
  serializeVariantParam,
41
+ splitSectionLabel,
33
42
  visibleAxes,
34
43
  writeBrowserStorage
35
- } from "./chunk-PUXQGUMQ.js";
36
- import {
37
- MAX_FEEDBACK_LENGTH
38
- } from "./chunk-A4DX2V4C.js";
44
+ } from "./chunk-BTJHQXF7.js";
39
45
 
40
46
  // src/react/tooltips.ts
41
47
  function showOverflowTitle(event) {
@@ -45,7 +51,7 @@ function showOverflowTitle(event) {
45
51
  }
46
52
 
47
53
  // src/react/IterationPanel.tsx
48
- import { useCallback, useEffect as useEffect3, useId as useId2, useLayoutEffect as useLayoutEffect4, useMemo as useMemo3, useRef as useRef4, useState as useState3 } from "react";
54
+ import { useCallback, useEffect as useEffect4, useId as useId3, useLayoutEffect as useLayoutEffect4, useMemo as useMemo3, useRef as useRef4, useState as useState3 } from "react";
49
55
 
50
56
  // src/react/appearance.ts
51
57
  var darkPalette = {
@@ -584,11 +590,16 @@ function Icon2({ name, size = 16 }) {
584
590
 
585
591
  // src/react/UpdateNotice.tsx
586
592
  import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
587
- function UpdateNotice({ endpoint, active }) {
593
+ function UpdateNotice({ endpoint, active, route = "/", requested = false, onRequested }) {
588
594
  const [info, setInfo] = useState(null);
589
595
  const [open, setOpen] = useState(false);
590
- const [copied, setCopied] = useState(false);
596
+ const [showCommand, setShowCommand] = useState(false);
597
+ const [status, setStatus] = useState("");
598
+ const [busy, setBusy] = useState(false);
599
+ const [asked, setAsked] = useState(false);
591
600
  const trigger = useRef(null);
601
+ const primary = useRef(null);
602
+ const done = useRef(null);
592
603
  const command = useRef(null);
593
604
  const closeButton = useRef(null);
594
605
  const id = useId();
@@ -618,28 +629,57 @@ function UpdateNotice({ endpoint, active }) {
618
629
  };
619
630
  }, [endpoint, active]);
620
631
  useEffect(() => {
621
- if (open) {
622
- setCopied(false);
623
- (command.current ?? closeButton.current)?.focus();
624
- }
632
+ if (requested) setAsked(false);
633
+ }, [requested]);
634
+ useEffect(() => {
635
+ if (!open) return;
636
+ setStatus("");
637
+ setShowCommand(false);
638
+ (primary.current ?? closeButton.current)?.focus();
625
639
  }, [open]);
640
+ useEffect(() => {
641
+ if (showCommand) command.current?.focus();
642
+ }, [showCommand]);
626
643
  function close() {
627
644
  setOpen(false);
628
645
  trigger.current?.focus();
629
646
  }
630
- async function copy() {
631
- if (!info?.upgrade) return;
647
+ async function ask() {
648
+ if (!endpoint || busy) return;
649
+ setBusy(true);
650
+ setStatus("");
651
+ try {
652
+ const response = await fetch(endpoint, {
653
+ method: "POST",
654
+ headers: { "Content-Type": "application/json" },
655
+ body: JSON.stringify({ route })
656
+ });
657
+ if (!response.ok) throw new Error(String(response.status));
658
+ setAsked(true);
659
+ onRequested?.();
660
+ requestAnimationFrame(() => done.current?.focus());
661
+ } catch {
662
+ setStatus("Could not ask. Copy the prompt instead.");
663
+ } finally {
664
+ setBusy(false);
665
+ }
666
+ }
667
+ async function copyPrompt() {
668
+ if (!info?.upgrade || !info.latestVersion) return;
669
+ const text = updatePrompt({ from: info.currentVersion, to: info.latestVersion, ...info.upgrade });
632
670
  try {
633
- await navigator.clipboard.writeText(info.upgrade.command);
634
- setCopied(true);
671
+ await navigator.clipboard.writeText(text);
672
+ setStatus("Prompt copied");
635
673
  } catch {
636
- command.current?.focus();
637
- command.current?.select();
674
+ setShowCommand(true);
675
+ setStatus("Could not copy. The command is below.");
638
676
  }
639
677
  }
640
678
  if (!active || !info) return null;
679
+ const pending = requested || asked;
680
+ const canAsk = Boolean(onRequested && info.upgrade);
641
681
  return /* @__PURE__ */ jsxs("div", { className: "isotone-update", children: [
642
- /* @__PURE__ */ jsxs(
682
+ /* @__PURE__ */ jsx2(
643
683
  "button",
644
684
  {
645
685
  ref: trigger,
@@ -647,11 +687,8 @@ function UpdateNotice({ endpoint, active }) {
647
687
  className: "isotone-update-trigger",
648
688
  "aria-expanded": open,
649
689
  "aria-controls": id,
650
- onClick: () => setOpen((value) => !value),
651
- children: [
652
- "Update ",
653
- info.latestVersion
654
- ]
690
+ onClick: () => setOpen(!open),
691
+ children: pending ? "Update requested" : `Isotone ${info.latestVersion} is available`
655
692
  }
656
693
  ),
657
694
  open && /* @__PURE__ */ jsxs(
@@ -670,25 +707,27 @@ function UpdateNotice({ endpoint, active }) {
670
707
  },
671
708
  children: [
672
709
  /* @__PURE__ */ jsxs("div", { className: "isotone-menu-title", children: [
673
- /* @__PURE__ */ jsx2("strong", { children: "Update available" }),
674
- /* @__PURE__ */ jsx2("button", { ref: closeButton, type: "button", className: "isotone-icon", "aria-label": "Close update instructions", title: "Close update instructions", onClick: close, children: /* @__PURE__ */ jsx2(Icon2, { name: "close" }) })
675
- ] }),
676
- /* @__PURE__ */ jsxs("p", { children: [
677
- info.currentVersion,
678
- " \u2192 ",
679
- info.latestVersion
710
+ /* @__PURE__ */ jsxs("strong", { className: "isotone-update-versions", children: [
711
+ info.currentVersion,
712
+ " \u2192 ",
713
+ info.latestVersion
714
+ ] }),
715
+ /* @__PURE__ */ jsx2("button", { ref: closeButton, type: "button", className: "isotone-icon", "aria-label": "Close update", title: "Close update", onClick: close, children: /* @__PURE__ */ jsx2(Icon2, { name: "close" }) })
680
716
  ] }),
681
717
  info.upgrade ? /* @__PURE__ */ jsxs(Fragment, { children: [
682
- /* @__PURE__ */ jsxs("p", { children: [
683
- "Run in ",
684
- /* @__PURE__ */ jsx2("span", { className: "isotone-update-path", onPointerOver: showOverflowTitle, children: info.upgrade.cwd })
685
- ] }),
686
- /* @__PURE__ */ jsx2("input", { ref: command, "aria-label": "Upgrade command", readOnly: true, value: info.upgrade.command, onFocus: (event) => event.currentTarget.select() }),
718
+ canAsk && (pending ? /* @__PURE__ */ jsx2("p", { ref: done, tabIndex: -1, className: "isotone-update-requested", children: "Update requested \xB7 in Feedback" }) : /* @__PURE__ */ jsx2("button", { ref: primary, type: "button", className: "isotone-save isotone-update-ask", disabled: busy, onClick: () => void ask(), children: "Ask agent to update" })),
687
719
  /* @__PURE__ */ jsxs("div", { className: "isotone-update-actions", children: [
688
- /* @__PURE__ */ jsx2("button", { type: "button", className: "isotone-secondary", onClick: () => void copy(), children: copied ? "Copied" : "Copy command" }),
689
- /* @__PURE__ */ jsx2("span", { role: "status", children: copied ? "Ready to paste in your terminal" : "" })
720
+ /* @__PURE__ */ jsx2("button", { ref: canAsk ? void 0 : primary, type: "button", className: "isotone-update-quiet", onClick: () => void copyPrompt(), children: "Copy prompt" }),
721
+ /* @__PURE__ */ jsx2("button", { type: "button", className: "isotone-update-quiet", "aria-expanded": showCommand, onClick: () => setShowCommand((value) => !value), children: "Command" }),
722
+ /* @__PURE__ */ jsx2("span", { role: "status", children: status })
690
723
  ] }),
691
- /* @__PURE__ */ jsx2("p", { children: "Restart the dev server after upgrading." })
724
+ showCommand && /* @__PURE__ */ jsxs("div", { className: "isotone-update-command", children: [
725
+ /* @__PURE__ */ jsx2("input", { ref: command, "aria-label": "Upgrade command", readOnly: true, value: info.upgrade.command, onFocus: (event) => event.currentTarget.select() }),
726
+ /* @__PURE__ */ jsxs("span", { className: "isotone-update-path", onPointerOver: showOverflowTitle, children: [
727
+ "in ",
728
+ info.upgrade.cwd
729
+ ] })
730
+ ] })
692
731
  ] }) : /* @__PURE__ */ jsx2("p", { children: info.message ?? "Run isotone check-update in the app directory for upgrade instructions." })
693
732
  ]
694
733
  }
@@ -696,18 +735,26 @@ function UpdateNotice({ endpoint, active }) {
696
735
  ] });
697
736
  }
698
737
  var UPDATE_NOTICE_CSS = `
699
- .isotone-update { position: absolute; z-index: 5; bottom: 10px; right: calc(var(--iso-rail-length) + 10px); pointer-events: auto; }
700
- .isotone-root[data-tools='1'] .isotone-update { right: calc(var(--iso-rail-size) + 10px); }
738
+ .isotone-notices { position: absolute; z-index: 5; bottom: 10px; right: calc(var(--iso-rail-length) + 10px); display: flex; flex-direction: row-reverse; flex-wrap: wrap-reverse; align-items: center; justify-content: flex-start; gap: 6px; width: max-content; max-width: calc(100vw - var(--iso-rail-length) - 36px); pointer-events: none; }
739
+ .isotone-root[data-tools='1'] .isotone-notices { right: calc(var(--iso-rail-size) + 10px); max-width: calc(100vw - var(--iso-rail-size) - 36px); }
740
+ .isotone-notices > * { pointer-events: auto; }
701
741
  .isotone-update-trigger { white-space: nowrap; color: var(--iso-muted); font-size: 10px; padding: 4px 6px; border-radius: 5px; background: var(--iso-bg); box-shadow: 0 0 0 1px var(--iso-line); }
702
- .isotone-update-dialog { position: absolute; right: calc(0px - var(--iso-rail-length) - 10px); bottom: 40px; width: var(--iso-dock-width); max-height: calc(100dvh - 100px); overflow: auto; padding: var(--iso-space); display: grid; gap: 8px; border-radius: 8px; background: var(--iso-bg); box-shadow: var(--iso-popup-shadow); font-size: 11px; }
742
+ .isotone-update-dialog { position: absolute; right: calc(0px - var(--iso-rail-length) - 10px); bottom: 40px; width: min(var(--iso-dock-width), 260px); max-width: calc(100vw - 32px); max-height: calc(100dvh - 100px); overflow: auto; padding: 10px var(--iso-space) var(--iso-space); display: grid; gap: 8px; border-radius: 8px; background: var(--iso-bg); box-shadow: var(--iso-popup-shadow); font-size: 11px; }
703
743
  .isotone-root[data-tools='1'] .isotone-update-dialog { right: calc(0px - var(--iso-rail-size) - 10px); }
704
744
  .isotone-update-dialog .isotone-menu-title { padding: 0; background: none; }
745
+ .isotone-update-versions { font-variant-numeric: tabular-nums; }
705
746
  .isotone-update-dialog > p { color: var(--iso-muted); }
706
- .isotone-update-path { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
707
- .isotone-root .isotone-update-dialog input { width: 100%; padding: 8px; border-radius: 5px; background: var(--iso-well); box-shadow: var(--iso-well-shadow); font-size: 11px; }
708
- .isotone-update-actions { display: flex; align-items: center; gap: 8px; }
709
- .isotone-update-actions [role='status'] { color: var(--iso-muted); font-size: 10px; }
710
- @media (hover: hover) and (pointer: fine) { .isotone-update-trigger:hover { color: var(--iso-fg); } }
747
+ .isotone-root .isotone-update-ask { min-height: 28px; padding: 4px 10px; font-weight: 500; }
748
+ .isotone-update-requested { min-height: 28px; display: flex; align-items: center; outline-offset: 2px; }
749
+ .isotone-update-actions { display: flex; align-items: center; gap: 2px; margin: -4px 0 -4px -6px; }
750
+ .isotone-root .isotone-update-quiet { padding: 4px 6px; border-radius: 5px; color: var(--iso-muted); font-size: 11px; background: none; box-shadow: none; }
751
+ .isotone-update-quiet[aria-expanded='true'] { color: var(--iso-fg); }
752
+ .isotone-update-actions [role='status'] { min-width: 0; margin-left: auto; color: var(--iso-muted); font-size: 10px; text-align: right; }
753
+ .isotone-update-command { display: grid; gap: 4px; min-width: 0; }
754
+ .isotone-update-path { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--iso-muted); font-size: 10px; }
755
+ .isotone-root .isotone-update-dialog input { width: 100%; padding: 6px 8px; border-radius: 5px; background: var(--iso-well); box-shadow: var(--iso-well-shadow); font-size: 11px; }
756
+ @media (pointer: coarse) { .isotone-root .isotone-update-quiet { padding: 9px 6px; } .isotone-update-actions { margin: -9px 0 -9px -6px; } }
757
+ @media (hover: hover) and (pointer: fine) { .isotone-update-trigger:hover, .isotone-root .isotone-update-quiet:hover { color: var(--iso-fg); } }
711
758
  `;
712
759
 
713
760
  // src/react/styles.ts
@@ -742,8 +789,11 @@ ${APPEARANCE_CSS}
742
789
  --iso-ease-out: ${MOTION.easeOut};
743
790
  --iso-fast: ${MOTION.exit}ms; --iso-enter: ${MOTION.enter}ms; --iso-exit: ${MOTION.exit}ms;
744
791
  --iso-dock-width: min(340px, calc(100vw - 32px));
745
- position: fixed; right: max(16px, env(safe-area-inset-right)); bottom: max(16px, env(safe-area-inset-bottom));
746
- z-index: 2147483000; width: 44px; display: flex; flex-direction: column; isolation: isolate; max-height: calc(100dvh - 32px); color: var(--iso-fg);
792
+ /* The height the dock may fill: what is visible (above an on-screen keyboard,
793
+ see viewport.ts) less the safe areas it keeps clear of. */
794
+ --iso-view-height: calc(var(--iso-viewport, 100dvh) - env(safe-area-inset-top, 0px) - max(0px, env(safe-area-inset-bottom, 0px) - 16px));
795
+ position: fixed; right: max(16px, env(safe-area-inset-right)); bottom: max(calc(16px + var(--iso-keyboard, 0px)), env(safe-area-inset-bottom));
796
+ z-index: 2147483000; width: 44px; display: flex; flex-direction: column; isolation: isolate; max-height: calc(var(--iso-view-height) - 32px); color: var(--iso-fg);
747
797
  font: 12px/1.5 var(--iso-sans); -webkit-font-smoothing: antialiased;
748
798
  text-align: left; letter-spacing: normal; color-scheme: dark;
749
799
  }
@@ -779,8 +829,8 @@ ${APPEARANCE_CSS}
779
829
  /* Panels are sections inside the dock. Only long content scrolls. */
780
830
  .isotone-panel { order: 0; position: relative; display: none; flex-direction: column; min-height: 0; flex: 0 1 auto; width: 100%; }
781
831
  .isotone-panel[data-open='true'] { display: flex; }
782
- .isotone-variants-panel { flex-shrink: 1; max-height: calc(100dvh - 76px); }
783
- .isotone-notes-panel { flex-shrink: 0; max-height: calc(100dvh - 76px); padding: var(--iso-space); gap: var(--iso-space); }
832
+ .isotone-variants-panel { flex-shrink: 1; max-height: calc(var(--iso-view-height) - 76px); }
833
+ .isotone-notes-panel { flex-shrink: 0; max-height: calc(var(--iso-view-height) - 76px); padding: var(--iso-space); gap: var(--iso-space); }
784
834
  .isotone-root .isotone-panel { background: var(--iso-bg); border: 1px solid var(--iso-line); border-radius: 12px; box-shadow: var(--iso-panel-shadow); }
785
835
  /* The close control sits on the panel's top-left corner, half outside it, with
786
836
  the same face as a note's delete button, a touch larger. It fades in while the
@@ -804,13 +854,30 @@ ${APPEARANCE_CSS}
804
854
  .isotone-icon { width: 28px; height: 28px; display: inline-grid; place-items: center; flex: none; color: var(--iso-muted); border-radius: 6px; }
805
855
  .isotone-count { margin-left: 5px; color: var(--iso-muted); font-size: 10px; font-variant-numeric: tabular-nums; }
806
856
  .isotone-vars { flex: 1; display: flex; flex-direction: column; min-height: 0; position: relative; }
807
- .isotone-vars { min-height: min(var(--iso-menu-need, 0px), calc(100dvh - 76px)); }
857
+ .isotone-vars { min-height: min(var(--iso-menu-need, 0px), calc(var(--iso-view-height) - 76px)); }
808
858
  /* The panel no longer clips, so its corner close can overhang; the variants body clips itself. */
809
859
  .isotone-root .isotone-panel > .isotone-vars { overflow: hidden; border-radius: 11px; }
810
860
  .isotone-vars-body { flex: 1; overflow-y: auto; min-height: 0; overscroll-behavior: contain; padding: var(--iso-space); display: flex; flex-direction: column; gap: var(--iso-space); }
811
861
  /* The footer overlaps only the scroller's bottom padding: content fades beneath
812
862
  it while scrolling, but the last control remains fully visible at the end. */
813
863
  .isotone-axis { flex: none; }
864
+ /* Sections name the part of the page their selectors change. The header sticks to
865
+ the top of the scroll area while its own selectors pass beneath and fade into the
866
+ surface; the next section's header pushes it out. At rest its padding occupies
867
+ the inset or gap above it, so sticking does not move the text. */
868
+ .isotone-section, .isotone-section-body { flex: none; display: flex; flex-direction: column; gap: var(--iso-space); }
869
+ .isotone-section-body[hidden] { display: none; }
870
+ .isotone-vars-body > * + .isotone-section { padding-top: 8px; }
871
+ /* Sticky offsets start inside the scroller's padding; the negative top reaches its edge. */
872
+ .isotone-section-head { position: sticky; top: calc(0px - var(--iso-space)); z-index: 1; margin: calc(0px - var(--iso-space)) calc(0px - var(--iso-space)) -4px; padding: var(--iso-space) var(--iso-space) 0; background: var(--iso-bg); font: inherit; }
873
+ .isotone-section-head::after { content: ''; position: absolute; left: 0; right: 0; top: 100%; height: 8px; background: linear-gradient(var(--iso-bg), transparent); pointer-events: none; }
874
+ .isotone-section-toggle { display: inline-flex; align-items: center; gap: 5px; min-width: 0; max-width: calc(100% + 7px); min-height: 24px; padding: 2px 7px; margin-left: -7px; border-radius: 6px; text-align: left; color: var(--iso-muted); font-size: 11px; font-weight: 600; }
875
+ .isotone-section-toggle > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
876
+ .isotone-section-toggle > svg, .isotone-section-toggle > small { flex: none; }
877
+ .isotone-section-toggle > small { color: var(--iso-faint); font-size: 10px; font-weight: 500; font-variant-numeric: tabular-nums; }
878
+ .isotone-section[data-collapsed='true'] .isotone-section-toggle > svg { transform: rotate(-90deg); }
879
+ /* Keyboard focus reveal keeps a focused control clear of the sticky header. */
880
+ .isotone-vars-body:has(> .isotone-section) { scroll-padding-top: 44px; }
814
881
  .isotone-axis-head { display: flex; align-items: center; justify-content: space-between; min-height: 24px; gap: 8px; margin-bottom: 6px; }
815
882
  .isotone-axis-label { display: inline-flex; align-items: center; gap: 5px; min-width: 0; max-width: 100%; min-height: 24px; padding: 2px 7px; margin-left: -7px; border-radius: 6px; text-align: left; }
816
883
  .isotone-axis-label, .isotone-menu-title > strong { font: inherit; font-weight: 550; }
@@ -866,8 +933,13 @@ ${PILL_ACTIVE_CSS}
866
933
  .isotone-menu-action { display: flex; align-items: center; gap: 9px; width: 100%; min-height: 34px; border-radius: 6px; text-align: left; padding: 7px; font-size: 11px; }
867
934
  .isotone-menu-options { padding: 7px 0; margin: 5px 0; border-top: 1px solid var(--iso-line); border-bottom: 1px solid var(--iso-line); }
868
935
  .isotone-option-row { display: flex; align-items: center; padding: 2px 4px 2px 7px; gap: 4px; }
869
- .isotone-option-row > span { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
870
- .isotone-option-row small { font-size: 9px; color: var(--iso-muted); margin-left: 6px; }
936
+ .isotone-option-name { flex: 1; min-width: 0; display: flex; align-items: baseline; gap: 6px; }
937
+ .isotone-option-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
938
+ /* Tags never shrink, so a long label truncates before them instead of hiding them. */
939
+ .isotone-option-tags { flex: none; display: flex; gap: 6px; white-space: nowrap; }
940
+ .isotone-option-tags small { font-size: 9px; color: var(--iso-muted); }
941
+ /* A pending removal stays listed and readable; only its text quietens. */
942
+ .isotone-option-row[data-requested] .isotone-option-label { color: var(--iso-muted); }
871
943
  .isotone-menu-footer { flex-shrink: 0; padding: 9px 12px; border-top: 1px solid var(--iso-line); font-size: 10px; color: var(--iso-muted); min-height: 0; }
872
944
  .isotone-confirm { display: flex; justify-content: flex-end; gap: 6px; margin-top: 7px; }
873
945
  .isotone-danger { color: var(--iso-danger); }
@@ -883,6 +955,11 @@ ${PILL_ACTIVE_CSS}
883
955
  .isotone-note-hit { text-align: left; display: block; width: 100%; border-radius: 8px; padding: var(--iso-note-padding-y) var(--iso-note-padding-x); line-height: 18px; }
884
956
  .isotone-note-hit p { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 12px; }
885
957
  .isotone-meta { display: flex; align-items: center; gap: 4px; min-width: 0; margin-top: 4px; }
958
+ /* The agent's question takes a second line under the note, muted, clipped like it. */
959
+ .isotone-note-hit:has(> .isotone-reply) { flex-wrap: wrap; row-gap: 1px; }
960
+ .isotone-note-hit > p.isotone-reply { order: 3; flex: 1 0 100%; color: var(--iso-muted); font-size: 11px; }
961
+ .isotone-reply > span, .isotone-editing-reply > span { color: var(--iso-fg); font-weight: 500; }
962
+ .isotone-editing-reply { flex: none; max-height: 58px; overflow-y: auto; padding: 8px var(--iso-input-padding) 0; color: var(--iso-muted); font-size: 11px; line-height: 16px; overflow-wrap: anywhere; }
886
963
  .isotone-meta .isotone-chip { flex: 0 1 auto; min-width: 0; height: 18px; padding: 0 5px; font-size: 9px; }
887
964
  /* A lone reference gets all available space; multiple references share the row. */
888
965
  .isotone-meta .isotone-chip:not(:only-child) { max-width: 140px; }
@@ -931,14 +1008,15 @@ ${FEEDBACK_LAYOUT_CSS}
931
1008
  .isotone-picking { position: absolute; inset: -4px; z-index: 1; display: flex; align-items: center; gap: 10px; padding: 0 4px 0 8px; border-radius: 6px; background: var(--iso-well); cursor: default; }
932
1009
  .isotone-picking > svg { flex: none; color: var(--iso-fg); }
933
1010
  .isotone-picking p { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; margin: 0; }
934
- .isotone-picking span { font-size: 12px; line-height: 16px; color: var(--iso-fg); font-variant-numeric: tabular-nums; }
935
- .isotone-picking small { font-size: 10px; line-height: 14px; color: var(--iso-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1011
+ /* Hints are never truncated (design.md, "Hints and instructions"): one line, copy short enough to fit. */
1012
+ .isotone-picking span { font-size: 12px; line-height: 16px; color: var(--iso-fg); font-variant-numeric: tabular-nums; white-space: nowrap; }
1013
+ .isotone-picking small { font-size: 10px; line-height: 14px; color: var(--iso-muted); white-space: nowrap; }
936
1014
  .isotone-picking button { flex: none; display: flex; align-items: center; gap: 8px; height: 28px; padding: 0 9px; }
937
1015
  .isotone-picking kbd { font: inherit; color: var(--iso-muted); }
938
1016
  /* After a pin, the composer floats beside the element on a panel surface, placed by
939
1017
  IterationPanel inside the viewport. It grows out of the point that was clicked. */
940
1018
  .isotone-float { position: fixed; z-index: 2147483002; width: min(300px, calc(100vw - 16px)); padding: 4px; background: var(--iso-bg); border: 1px solid var(--iso-line); border-radius: 12px; box-shadow: var(--iso-panel-shadow); }
941
- .isotone-float-hint { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 10px; color: var(--iso-muted); }
1019
+ .isotone-float-hint { flex: 1 0 auto; white-space: nowrap; font-size: 10px; color: var(--iso-muted); }
942
1020
  /* Meanwhile the dock keeps the composer's slot and says where the writing went. */
943
1021
  .isotone-away { display: flex; align-items: center; gap: 10px; width: 100%; min-height: 50px; padding: 8px 8px 8px 10px; text-align: left; color: var(--iso-fg); background: var(--iso-well); border: 1px solid var(--iso-input-border); box-shadow: var(--iso-input-shadow); border-radius: 8px; }
944
1022
  .isotone-away > svg { flex: none; }
@@ -951,8 +1029,8 @@ ${FEEDBACK_LAYOUT_CSS}
951
1029
  .isotone-hover { position: fixed; pointer-events: none; border: 1px solid var(--iso-pick-line); background: var(--iso-pick-fill); border-radius: 5px; z-index: 2147483001; }
952
1030
  .isotone-selected { border: 2px solid var(--iso-pick-line); background: var(--iso-pick-selected-fill); }
953
1031
  @media (hover: hover) and (pointer: fine) {
954
- .isotone-root :where(.isotone-icon:not(.isotone-x), .isotone-menu-action, .isotone-axis-label):not(:disabled):not([aria-pressed='true']):hover { background: var(--iso-hover); box-shadow: var(--iso-hover-shadow); color: var(--iso-fg); }
955
- .isotone-note:not(.isotone-note-editing):hover .isotone-note-hit:not(:disabled) { background: var(--iso-hover); box-shadow: var(--iso-hover-shadow); }
1032
+ .isotone-root :where(.isotone-icon:not(.isotone-x), .isotone-menu-action, .isotone-axis-label, .isotone-section-toggle):not(:disabled):not([aria-pressed='true']):hover { background: var(--iso-hover); box-shadow: var(--iso-hover-shadow); color: var(--iso-fg); }
1033
+ .isotone-note:not(.isotone-note-editing):hover .isotone-note-hit:not(:disabled):not([data-static]) { background: var(--iso-hover); box-shadow: var(--iso-hover-shadow); }
956
1034
  .isotone-rail-button:not(:disabled):hover { color: var(--iso-fg); }
957
1035
  .isotone-seg:not(:disabled):hover { color: var(--iso-fg); }
958
1036
  .isotone-seg[aria-pressed='false']:not(:disabled):hover { box-shadow: var(--iso-hover-shadow); }
@@ -964,6 +1042,23 @@ ${FEEDBACK_LAYOUT_CSS}
964
1042
  .isotone-note .isotone-x { opacity: 0; }
965
1043
  .isotone-note:hover .isotone-x, .isotone-note:focus-within .isotone-x, .isotone-note-editing .isotone-x { opacity: 1; }
966
1044
  }
1045
+ /* Phones. Panels take turns (see viewport.ts). Variants stop at about half the
1046
+ screen, so the page they change stays in view; action dialogs keep their
1047
+ 360px. While selecting, the dock shrinks to the composer so the page can be
1048
+ reached, and the saved notes return afterwards. */
1049
+ @media (max-width: 640px) {
1050
+ .isotone-variants-panel { max-height: min(calc(var(--iso-view-height) - 76px), max(50dvh, 360px)); }
1051
+ }
1052
+ @media (max-width: 640px), (max-height: 500px) {
1053
+ .isotone-root[data-selecting='true'] .isotone-notes-panel > :is(.isotone-list, .isotone-pages) { display: none; }
1054
+ }
1055
+ /* A finger needs more than the drawn faces. Hit areas grow without moving
1056
+ anything; the faces keep Instrument's geometry. */
1057
+ @media (pointer: coarse) {
1058
+ .isotone-pick::after, .isotone-send::after { inset: -8px -10px; }
1059
+ .isotone-chip button { position: relative; }
1060
+ .isotone-chip button::after, .isotone-note > .isotone-x::after, .isotone-panel-close::after { content: ''; position: absolute; inset: -6px; border-radius: 50%; }
1061
+ }
967
1062
  @media (max-height: 580px) { .isotone-list { --iso-list-height: 72px; } .isotone-root .isotone-textarea { max-height: calc(64px + var(--iso-textarea-inset)); } }
968
1063
  @media (prefers-reduced-motion: reduce) {
969
1064
  .isotone-ring { box-shadow: 0 0 0 2px color-mix(in srgb, var(--iso-focus) 50%, transparent); }
@@ -988,7 +1083,7 @@ function ensurePanelStyles(target) {
988
1083
  }
989
1084
 
990
1085
  // src/react/VariantsView.tsx
991
- import { useEffect as useEffect2, useLayoutEffect as useLayoutEffect2, useMemo as useMemo2, useRef as useRef3, useState as useState2 } from "react";
1086
+ import { useEffect as useEffect2, useId as useId2, useLayoutEffect as useLayoutEffect2, useMemo as useMemo2, useRef as useRef3, useState as useState2 } from "react";
992
1087
 
993
1088
  // src/react/useLayoutMotion.ts
994
1089
  import { useLayoutEffect, useRef as useRef2 } from "react";
@@ -1046,13 +1141,13 @@ function useLayoutMotion(ref, revision, active = true) {
1046
1141
  previous.current = next;
1047
1142
  }, [ref, revision, active]);
1048
1143
  useLayoutEffect(() => {
1049
- const media = typeof matchMedia === "function" ? matchMedia("(prefers-reduced-motion: reduce)") : null;
1144
+ const media2 = typeof matchMedia === "function" ? matchMedia("(prefers-reduced-motion: reduce)") : null;
1050
1145
  const changed = () => {
1051
- if (media?.matches) clearPositions(previous.current);
1146
+ if (media2?.matches) clearPositions(previous.current);
1052
1147
  };
1053
- media?.addEventListener("change", changed);
1148
+ media2?.addEventListener("change", changed);
1054
1149
  return () => {
1055
- media?.removeEventListener("change", changed);
1150
+ media2?.removeEventListener("change", changed);
1056
1151
  clearPositions(previous.current);
1057
1152
  };
1058
1153
  }, []);
@@ -1072,6 +1167,7 @@ function useVariantsSource(source) {
1072
1167
  return revision;
1073
1168
  }
1074
1169
  var NO_PENDING_ACTIONS = [];
1170
+ var DEFAULT_EMPTY_TEXT = "No variants on this page yet. Ask your agent to create variants of any design and they will appear here.";
1075
1171
  function VariantsView({
1076
1172
  source,
1077
1173
  showTitle = true,
@@ -1080,26 +1176,35 @@ function VariantsView({
1080
1176
  onClose,
1081
1177
  onSaved,
1082
1178
  onManage,
1083
- pendingActions = NO_PENDING_ACTIONS
1179
+ pendingActions = NO_PENDING_ACTIONS,
1180
+ emptyText = DEFAULT_EMPTY_TEXT
1084
1181
  }) {
1085
1182
  const revision = useVariantsSource(source);
1086
1183
  useEffect2(() => {
1087
1184
  if (active) refreshSource(source);
1088
1185
  }, [active, source]);
1089
- const { selections, shown, sourceError } = useMemo2(() => {
1186
+ const { selections, shown, sections, names, sourceError } = useMemo2(() => {
1090
1187
  const selections2 = source.getSelections();
1188
+ const shown2 = visibleAxes(source.getAxes(), selections2);
1189
+ const sections2 = sectionAxes(shown2);
1091
1190
  return {
1092
1191
  selections: selections2,
1093
- shown: visibleAxes(source.getAxes(), selections2),
1192
+ shown: shown2,
1193
+ sections: sections2,
1194
+ names: new Map(sections2.flatMap((section) => section.axes).map((entry) => [entry.axis.id, entry])),
1094
1195
  sourceError: source.getError?.()
1095
1196
  };
1096
1197
  }, [source, revision]);
1198
+ const [collapsed, setCollapsed] = useState2(() => /* @__PURE__ */ new Set());
1097
1199
  const [menuId, setMenuId] = useState2(null);
1098
1200
  const menu = shown.find((axis) => axis.id === menuId) ?? null;
1201
+ const nameOf = (axis) => names.get(axis.id) ?? axisSection(axis);
1202
+ const menuName = menu ? nameOf(menu) : null;
1099
1203
  const [menuOpen, setMenuOpen] = useState2(false);
1100
1204
  const [busy, setBusy] = useState2(false);
1101
1205
  const [error, setError] = useState2("");
1102
1206
  const menuRef = useRef3(null);
1207
+ const pendingFocus = useRef3(null);
1103
1208
  const bodyRef = useRef3(null);
1104
1209
  useLayoutMotion(bodyRef, shown, active);
1105
1210
  const trigger = useRef3(null);
@@ -1111,15 +1216,28 @@ function VariantsView({
1111
1216
  ...o,
1112
1217
  isDefault: o.id === menu.defaultOption
1113
1218
  })) : [];
1114
- const closeMenu = () => {
1219
+ const closeMenu = (restoreFocus = true) => {
1115
1220
  setMenuOpen(false);
1116
1221
  setError("");
1222
+ pendingFocus.current = null;
1223
+ if (!restoreFocus) return;
1117
1224
  requestAnimationFrame(() => {
1118
1225
  if (trigger.current?.isConnected) trigger.current.focus();
1119
1226
  else
1120
1227
  bodyRef.current?.parentElement?.querySelector("button")?.focus();
1121
1228
  });
1122
1229
  };
1230
+ useEffect2(() => {
1231
+ const target = pendingFocus.current;
1232
+ const dialog = menuRef.current;
1233
+ if (busy || !target || !dialog || !menuOpen) return;
1234
+ pendingFocus.current = null;
1235
+ const rows = Array.from(dialog.querySelectorAll(".isotone-option-row"));
1236
+ const at = rows.findIndex((row) => row.dataset.optionId === target.optionId);
1237
+ const removable = (row) => row?.querySelector("button[data-remove]:not(:disabled)") ?? null;
1238
+ const next = target.retry ? removable(rows[at]) : [...rows.slice(at + 1), ...rows.slice(0, Math.max(at, 0)).reverse()].map(removable).find(Boolean);
1239
+ (next ?? dialog.querySelector("[data-reset]"))?.focus();
1240
+ }, [busy, pendingActions, menuOpen]);
1123
1241
  useEffect2(() => {
1124
1242
  if (!menu) setMenuOpen(false);
1125
1243
  }, [menu]);
@@ -1170,10 +1288,10 @@ function VariantsView({
1170
1288
  const option = axis.kind === "range" ? void 0 : (axis.kind === "color" ? axis.swatches : axis.options).find(
1171
1289
  (o) => o.id === optionId
1172
1290
  );
1173
- closeMenu();
1291
+ closeMenu(false);
1174
1292
  onFeedback?.({
1175
1293
  axisId: axis.id,
1176
- label: axis.label,
1294
+ label: nameOf(axis).fullLabel,
1177
1295
  value: value(axis),
1178
1296
  ...option ? { optionId: option.id, optionLabel: option.label } : {}
1179
1297
  });
@@ -1181,18 +1299,21 @@ function VariantsView({
1181
1299
  const requested = (action) => pendingActions.some((pending) => pending.axisId === action.axisId && pending.action === action.action && pending.optionId === action.optionId);
1182
1300
  async function manage(action) {
1183
1301
  if (!menu || busy || !onManage || requested(action)) return;
1302
+ const option = action.action === "remove-option" ? action.optionId : void 0;
1184
1303
  setBusy(true);
1185
1304
  setError("");
1186
1305
  try {
1187
- const option = menuOptions.find((o) => o.id === action.optionId);
1306
+ const chosen = menuOptions.find((o) => o.id === action.optionId);
1188
1307
  await onManage(action, {
1189
1308
  axisId: menu.id,
1190
- label: menu.label,
1309
+ label: menuName?.fullLabel ?? menu.label,
1191
1310
  value: value(menu),
1192
- ...option ? { optionId: option.id, optionLabel: option.label } : {}
1311
+ ...chosen ? { optionId: chosen.id, optionLabel: chosen.label } : {}
1193
1312
  });
1194
- closeMenu();
1313
+ if (option !== void 0) pendingFocus.current = { optionId: option, retry: false };
1314
+ else closeMenu();
1195
1315
  } catch (e) {
1316
+ if (option !== void 0) pendingFocus.current = { optionId: option, retry: true };
1196
1317
  setError(
1197
1318
  e instanceof Error ? e.message : "Could not update this selector."
1198
1319
  );
@@ -1200,6 +1321,100 @@ function VariantsView({
1200
1321
  setBusy(false);
1201
1322
  }
1202
1323
  }
1324
+ function renderAxis({ axis, label, fullLabel }) {
1325
+ const thumbnails = axis.kind !== "range" && axis.kind !== "color" && axis.options.length > 0 && axis.options.every((option) => option.image);
1326
+ return /* @__PURE__ */ jsxs2(
1327
+ "div",
1328
+ {
1329
+ className: "isotone-axis",
1330
+ "data-motion-key": axis.id,
1331
+ children: [
1332
+ /* @__PURE__ */ jsxs2("div", { className: "isotone-axis-head", children: [
1333
+ /* @__PURE__ */ jsxs2(
1334
+ "button",
1335
+ {
1336
+ type: "button",
1337
+ className: "isotone-axis-label",
1338
+ "aria-label": `${fullLabel} actions`,
1339
+ "aria-haspopup": "dialog",
1340
+ "aria-expanded": menuOpen && menu?.id === axis.id,
1341
+ onClick: (e) => {
1342
+ trigger.current = e.currentTarget;
1343
+ setMenuId(axis.id);
1344
+ setError("");
1345
+ setMenuOpen(true);
1346
+ },
1347
+ children: [
1348
+ /* @__PURE__ */ jsx3("span", { onPointerOver: showOverflowTitle, children: label }),
1349
+ /* @__PURE__ */ jsx3(Icon2, { name: "chevron", size: 12 })
1350
+ ]
1351
+ }
1352
+ ),
1353
+ (axis.kind === "range" || axis.kind === "color") && /* @__PURE__ */ jsx3("span", { className: "isotone-axis-value", children: axis.kind === "range" ? formatRangeValue(axis, value(axis)) : value(axis).toUpperCase() })
1354
+ ] }),
1355
+ axis.kind === "range" ? /* @__PURE__ */ jsx3(
1356
+ "input",
1357
+ {
1358
+ type: "range",
1359
+ className: "isotone-range",
1360
+ "aria-label": fullLabel,
1361
+ "aria-valuetext": formatRangeValue(axis, value(axis)),
1362
+ min: axis.min,
1363
+ max: axis.max,
1364
+ step: axis.step,
1365
+ value: Number(value(axis)),
1366
+ onChange: (e) => source.setSelection(axis.id, e.target.value)
1367
+ }
1368
+ ) : axis.kind === "color" ? /* @__PURE__ */ jsxs2("fieldset", { className: "isotone-swatches", "aria-label": fullLabel, children: [
1369
+ axis.swatches.map((sw) => /* @__PURE__ */ jsx3(
1370
+ "button",
1371
+ {
1372
+ type: "button",
1373
+ title: sw.label,
1374
+ "aria-label": sw.label,
1375
+ "aria-pressed": value(axis).toLowerCase() === sw.value.toLowerCase(),
1376
+ className: "isotone-swatch",
1377
+ style: { backgroundColor: sw.value },
1378
+ onClick: () => source.setSelection(axis.id, sw.value)
1379
+ },
1380
+ sw.id
1381
+ )),
1382
+ /* @__PURE__ */ jsx3("label", { className: "isotone-picker", title: "Custom colour", children: /* @__PURE__ */ jsx3(
1383
+ "input",
1384
+ {
1385
+ type: "color",
1386
+ "aria-label": `${fullLabel}: custom colour`,
1387
+ value: value(axis),
1388
+ onChange: (e) => source.setSelection(axis.id, e.target.value)
1389
+ }
1390
+ ) })
1391
+ ] }) : /* @__PURE__ */ jsx3(
1392
+ "fieldset",
1393
+ {
1394
+ "aria-label": fullLabel,
1395
+ className: thumbnails ? "isotone-grid" : "isotone-segs",
1396
+ children: axis.options.map((o) => /* @__PURE__ */ jsxs2(
1397
+ "button",
1398
+ {
1399
+ type: "button",
1400
+ "aria-label": o.label,
1401
+ "aria-pressed": value(axis) === o.id,
1402
+ className: `${thumbnails ? "isotone-thumb" : "isotone-seg"}${value(axis) === o.id ? " isotone-seg-on" : ""}`,
1403
+ onClick: () => source.setSelection(axis.id, o.id),
1404
+ children: [
1405
+ thumbnails && /* @__PURE__ */ jsx3("img", { src: o.image, alt: "" }),
1406
+ /* @__PURE__ */ jsx3("span", { onPointerOver: showOverflowTitle, children: o.label })
1407
+ ]
1408
+ },
1409
+ o.id
1410
+ ))
1411
+ }
1412
+ )
1413
+ ]
1414
+ },
1415
+ axis.id
1416
+ );
1417
+ }
1203
1418
  return /* @__PURE__ */ jsxs2("div", { className: "isotone-vars", children: [
1204
1419
  /* @__PURE__ */ jsxs2("div", { className: "isotone-panel-head", children: [
1205
1420
  showTitle && /* @__PURE__ */ jsx3("h2", { children: source.title ?? "Variants" }),
@@ -1223,101 +1438,23 @@ function VariantsView({
1223
1438
  node?.toggleAttribute("inert", menuOpen);
1224
1439
  }, children: [
1225
1440
  sourceError && /* @__PURE__ */ jsx3("p", { className: "isotone-danger", role: "alert", children: sourceError }),
1226
- shown.length === 0 && /* @__PURE__ */ jsx3("p", { className: "isotone-empty", children: "No variants on this page yet. Ask your agent to create variants of any design and they will appear here." }),
1227
- shown.map((axis) => {
1228
- const thumbnails = axis.kind !== "range" && axis.kind !== "color" && axis.options.length > 0 && axis.options.every((option) => option.image);
1229
- return /* @__PURE__ */ jsxs2(
1230
- "div",
1231
- {
1232
- className: "isotone-axis",
1233
- "data-motion-key": axis.id,
1234
- children: [
1235
- /* @__PURE__ */ jsxs2("div", { className: "isotone-axis-head", children: [
1236
- /* @__PURE__ */ jsxs2(
1237
- "button",
1238
- {
1239
- type: "button",
1240
- className: "isotone-axis-label",
1241
- "aria-label": `${axis.label} actions`,
1242
- "aria-haspopup": "dialog",
1243
- "aria-expanded": menuOpen && menu?.id === axis.id,
1244
- onClick: (e) => {
1245
- trigger.current = e.currentTarget;
1246
- setMenuId(axis.id);
1247
- setError("");
1248
- setMenuOpen(true);
1249
- },
1250
- children: [
1251
- /* @__PURE__ */ jsx3("span", { onPointerOver: showOverflowTitle, children: axis.label }),
1252
- /* @__PURE__ */ jsx3(Icon2, { name: "chevron", size: 12 })
1253
- ]
1254
- }
1255
- ),
1256
- (axis.kind === "range" || axis.kind === "color") && /* @__PURE__ */ jsx3("span", { className: "isotone-axis-value", children: axis.kind === "range" ? formatRangeValue(axis, value(axis)) : value(axis).toUpperCase() })
1257
- ] }),
1258
- axis.kind === "range" ? /* @__PURE__ */ jsx3(
1259
- "input",
1260
- {
1261
- type: "range",
1262
- className: "isotone-range",
1263
- "aria-label": axis.label,
1264
- "aria-valuetext": formatRangeValue(axis, value(axis)),
1265
- min: axis.min,
1266
- max: axis.max,
1267
- step: axis.step,
1268
- value: Number(value(axis)),
1269
- onChange: (e) => source.setSelection(axis.id, e.target.value)
1270
- }
1271
- ) : axis.kind === "color" ? /* @__PURE__ */ jsxs2("fieldset", { className: "isotone-swatches", "aria-label": axis.label, children: [
1272
- axis.swatches.map((sw) => /* @__PURE__ */ jsx3(
1273
- "button",
1274
- {
1275
- type: "button",
1276
- title: sw.label,
1277
- "aria-label": sw.label,
1278
- "aria-pressed": value(axis).toLowerCase() === sw.value.toLowerCase(),
1279
- className: "isotone-swatch",
1280
- style: { backgroundColor: sw.value },
1281
- onClick: () => source.setSelection(axis.id, sw.value)
1282
- },
1283
- sw.id
1284
- )),
1285
- /* @__PURE__ */ jsx3("label", { className: "isotone-picker", title: "Custom colour", children: /* @__PURE__ */ jsx3(
1286
- "input",
1287
- {
1288
- type: "color",
1289
- "aria-label": `${axis.label}: custom colour`,
1290
- value: value(axis),
1291
- onChange: (e) => source.setSelection(axis.id, e.target.value)
1292
- }
1293
- ) })
1294
- ] }) : /* @__PURE__ */ jsx3(
1295
- "fieldset",
1296
- {
1297
- "aria-label": axis.label,
1298
- className: thumbnails ? "isotone-grid" : "isotone-segs",
1299
- children: axis.options.map((o) => /* @__PURE__ */ jsxs2(
1300
- "button",
1301
- {
1302
- type: "button",
1303
- "aria-label": o.label,
1304
- "aria-pressed": value(axis) === o.id,
1305
- className: `${thumbnails ? "isotone-thumb" : "isotone-seg"}${value(axis) === o.id ? " isotone-seg-on" : ""}`,
1306
- onClick: () => source.setSelection(axis.id, o.id),
1307
- children: [
1308
- thumbnails && /* @__PURE__ */ jsx3("img", { src: o.image, alt: "" }),
1309
- /* @__PURE__ */ jsx3("span", { onPointerOver: showOverflowTitle, children: o.label })
1310
- ]
1311
- },
1312
- o.id
1313
- ))
1314
- }
1315
- )
1316
- ]
1317
- },
1318
- axis.id
1319
- );
1320
- })
1441
+ shown.length === 0 && /* @__PURE__ */ jsx3("p", { className: "isotone-empty", children: emptyText }),
1442
+ sections.map((section) => section.name === null ? section.axes.map(renderAxis) : /* @__PURE__ */ jsx3(
1443
+ SectionBlock,
1444
+ {
1445
+ name: section.name,
1446
+ sectionKey: section.key,
1447
+ count: section.axes.length,
1448
+ collapsed: collapsed.has(section.key),
1449
+ onToggle: () => setCollapsed((previous) => {
1450
+ const next = new Set(previous);
1451
+ if (!next.delete(section.key)) next.add(section.key);
1452
+ return next;
1453
+ }),
1454
+ children: section.axes.map(renderAxis)
1455
+ },
1456
+ `section:${section.key}`
1457
+ ))
1321
1458
  ] }),
1322
1459
  menu && /* @__PURE__ */ jsxs2(
1323
1460
  "div",
@@ -1328,7 +1465,7 @@ function VariantsView({
1328
1465
  node?.toggleAttribute("inert", !menuOpen);
1329
1466
  },
1330
1467
  role: "dialog",
1331
- "aria-label": `${menu.label} actions`,
1468
+ "aria-label": `${menuName?.fullLabel ?? menu.label} actions`,
1332
1469
  "aria-hidden": !menuOpen,
1333
1470
  "data-open": menuOpen,
1334
1471
  onKeyDown: (e) => {
@@ -1340,7 +1477,7 @@ function VariantsView({
1340
1477
  },
1341
1478
  children: [
1342
1479
  /* @__PURE__ */ jsxs2("div", { className: "isotone-menu-title", children: [
1343
- /* @__PURE__ */ jsx3("strong", { children: menu.label }),
1480
+ /* @__PURE__ */ jsx3("strong", { children: menuName?.label ?? menu.label }),
1344
1481
  /* @__PURE__ */ jsx3(
1345
1482
  "button",
1346
1483
  {
@@ -1348,7 +1485,7 @@ function VariantsView({
1348
1485
  className: "isotone-icon",
1349
1486
  "aria-label": "Close selector actions",
1350
1487
  title: "Close selector actions",
1351
- onClick: closeMenu,
1488
+ onClick: () => closeMenu(),
1352
1489
  children: /* @__PURE__ */ jsx3(Icon2, { name: "close" })
1353
1490
  }
1354
1491
  )
@@ -1366,44 +1503,52 @@ function VariantsView({
1366
1503
  ]
1367
1504
  }
1368
1505
  ),
1369
- menu.kind !== "range" && /* @__PURE__ */ jsx3("div", { className: "isotone-menu-options", children: menuOptions.map((option) => /* @__PURE__ */ jsxs2("div", { className: "isotone-option-row", children: [
1370
- /* @__PURE__ */ jsxs2("span", { children: [
1371
- option.label,
1372
- option.isDefault && /* @__PURE__ */ jsx3("small", { children: "Default" })
1373
- ] }),
1374
- onFeedback && /* @__PURE__ */ jsx3(
1375
- "button",
1376
- {
1377
- type: "button",
1378
- className: "isotone-icon",
1379
- "aria-label": `Feedback on ${option.label}`,
1380
- title: "Give feedback",
1381
- onClick: () => feedback(menu, option.id),
1382
- children: /* @__PURE__ */ jsx3(Icon2, { name: "message" })
1383
- }
1384
- ),
1385
- onManage && /* @__PURE__ */ jsx3(
1386
- "button",
1387
- {
1388
- type: "button",
1389
- className: "isotone-icon",
1390
- "aria-label": `Remove ${option.label}`,
1391
- disabled: option.isDefault || busy || requested({ axisId: menu.id, action: "remove-option", optionId: option.id }),
1392
- title: option.isDefault ? "Default option" : "Request option removal",
1393
- onClick: () => void manage({
1394
- axisId: menu.id,
1395
- action: "remove-option",
1396
- optionId: option.id
1397
- }),
1398
- children: /* @__PURE__ */ jsx3(Icon2, { name: "trash" })
1399
- }
1400
- )
1401
- ] }, option.id)) }),
1506
+ menu.kind !== "range" && /* @__PURE__ */ jsx3("div", { className: "isotone-menu-options", children: menuOptions.map((option) => {
1507
+ const pending = requested({ axisId: menu.id, action: "remove-option", optionId: option.id });
1508
+ return /* @__PURE__ */ jsxs2("div", { className: "isotone-option-row", "data-option-id": option.id, "data-requested": pending || void 0, children: [
1509
+ /* @__PURE__ */ jsxs2("span", { className: "isotone-option-name", children: [
1510
+ /* @__PURE__ */ jsx3("span", { className: "isotone-option-label", children: option.label }),
1511
+ (option.isDefault || pending) && /* @__PURE__ */ jsxs2("span", { className: "isotone-option-tags", children: [
1512
+ option.isDefault && /* @__PURE__ */ jsx3("small", { children: "Default" }),
1513
+ pending && /* @__PURE__ */ jsx3("small", { children: "Removal requested" })
1514
+ ] })
1515
+ ] }),
1516
+ onFeedback && /* @__PURE__ */ jsx3(
1517
+ "button",
1518
+ {
1519
+ type: "button",
1520
+ className: "isotone-icon",
1521
+ "aria-label": `Feedback on ${option.label}`,
1522
+ title: "Give feedback",
1523
+ onClick: () => feedback(menu, option.id),
1524
+ children: /* @__PURE__ */ jsx3(Icon2, { name: "message" })
1525
+ }
1526
+ ),
1527
+ onManage && /* @__PURE__ */ jsx3(
1528
+ "button",
1529
+ {
1530
+ type: "button",
1531
+ className: "isotone-icon",
1532
+ "data-remove": "",
1533
+ "aria-label": `Remove ${option.label}`,
1534
+ disabled: option.isDefault || busy || pending,
1535
+ title: option.isDefault ? "Default option" : pending ? "Removal requested" : "Request option removal",
1536
+ onClick: () => void manage({
1537
+ axisId: menu.id,
1538
+ action: "remove-option",
1539
+ optionId: option.id
1540
+ }),
1541
+ children: /* @__PURE__ */ jsx3(Icon2, { name: "trash" })
1542
+ }
1543
+ )
1544
+ ] }, option.id);
1545
+ }) }),
1402
1546
  /* @__PURE__ */ jsxs2(
1403
1547
  "button",
1404
1548
  {
1405
1549
  type: "button",
1406
1550
  className: "isotone-menu-action",
1551
+ "data-reset": "",
1407
1552
  onClick: () => {
1408
1553
  source.setSelection(menu.id, axisDefault(menu));
1409
1554
  closeMenu();
@@ -1449,6 +1594,39 @@ function VariantsView({
1449
1594
  )
1450
1595
  ] });
1451
1596
  }
1597
+ function SectionBlock({
1598
+ name,
1599
+ sectionKey,
1600
+ count,
1601
+ collapsed,
1602
+ onToggle,
1603
+ children
1604
+ }) {
1605
+ const id = useId2();
1606
+ return (
1607
+ // The heading carries the structure (heading navigation, a disclosure button);
1608
+ // a region or group per section would only add landmarks to a long list.
1609
+ /* @__PURE__ */ jsxs2("div", { className: "isotone-section", "data-collapsed": collapsed, children: [
1610
+ /* @__PURE__ */ jsx3("h3", { className: "isotone-section-head", "data-motion-key": `section:${sectionKey}`, children: /* @__PURE__ */ jsxs2(
1611
+ "button",
1612
+ {
1613
+ type: "button",
1614
+ className: "isotone-section-toggle",
1615
+ "aria-label": `${name}, ${count} ${count === 1 ? "selector" : "selectors"}`,
1616
+ "aria-expanded": !collapsed,
1617
+ "aria-controls": `${id}-body`,
1618
+ onClick: onToggle,
1619
+ children: [
1620
+ /* @__PURE__ */ jsx3(Icon2, { name: "chevron", size: 12 }),
1621
+ /* @__PURE__ */ jsx3("span", { onPointerOver: showOverflowTitle, children: name }),
1622
+ /* @__PURE__ */ jsx3("small", { children: count })
1623
+ ]
1624
+ }
1625
+ ) }),
1626
+ /* @__PURE__ */ jsx3("div", { className: "isotone-section-body", id: `${id}-body`, hidden: collapsed, children: collapsed ? null : children })
1627
+ ] })
1628
+ );
1629
+ }
1452
1630
  function SaveDefaults({
1453
1631
  source,
1454
1632
  onSaved
@@ -1626,8 +1804,8 @@ function observeTheme(onTheme, preference = "auto") {
1626
1804
  sample();
1627
1805
  if (preference === "light" || preference === "dark") return () => {
1628
1806
  };
1629
- const media = window.matchMedia?.("(prefers-color-scheme: dark)");
1630
- media?.addEventListener("change", schedule);
1807
+ const media2 = window.matchMedia?.("(prefers-color-scheme: dark)");
1808
+ media2?.addEventListener("change", schedule);
1631
1809
  const pageEvent = (event) => {
1632
1810
  if (!isPanel(event.target instanceof Node ? event.target : null)) schedule();
1633
1811
  };
@@ -1642,7 +1820,7 @@ function observeTheme(onTheme, preference = "auto") {
1642
1820
  disposed = true;
1643
1821
  observer?.disconnect();
1644
1822
  if (frame !== void 0) window.cancelAnimationFrame(frame);
1645
- media?.removeEventListener("change", schedule);
1823
+ media2?.removeEventListener("change", schedule);
1646
1824
  window.removeEventListener("resize", schedule);
1647
1825
  document.removeEventListener("scroll", pageEvent, true);
1648
1826
  document.removeEventListener("load", pageEvent, true);
@@ -1656,7 +1834,20 @@ import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-run
1656
1834
  function SummaryTag({ label }) {
1657
1835
  return /* @__PURE__ */ jsx4("span", { className: "isotone-chip", children: /* @__PURE__ */ jsx4("span", { onPointerOver: showOverflowTitle, children: label }) });
1658
1836
  }
1837
+ function Reply({ note }) {
1838
+ if (!note.reply?.text || note.reply.answered) return null;
1839
+ return /* @__PURE__ */ jsxs3("p", { className: "isotone-reply", onPointerOver: showOverflowTitle, children: [
1840
+ /* @__PURE__ */ jsx4("span", { children: "Agent asks" }),
1841
+ " ",
1842
+ note.reply.text
1843
+ ] });
1844
+ }
1659
1845
  function NoteSummary({ note, page }) {
1846
+ if (note.update) return /* @__PURE__ */ jsxs3(Fragment3, { children: [
1847
+ /* @__PURE__ */ jsx4("p", { children: updateNoteText(note.update) }),
1848
+ /* @__PURE__ */ jsx4(Reply, { note }),
1849
+ /* @__PURE__ */ jsx4("span", { className: "isotone-meta", children: /* @__PURE__ */ jsx4(SummaryTag, { label: "Update" }) })
1850
+ ] });
1660
1851
  if (!note.feedback.startsWith("[set-defaults]")) {
1661
1852
  const elements = note.elements ?? (note.element ? [note.element] : []);
1662
1853
  const references = [
@@ -1669,6 +1860,7 @@ function NoteSummary({ note, page }) {
1669
1860
  ];
1670
1861
  return /* @__PURE__ */ jsxs3(Fragment3, { children: [
1671
1862
  /* @__PURE__ */ jsx4("p", { children: note.feedback }),
1863
+ /* @__PURE__ */ jsx4(Reply, { note }),
1672
1864
  references.length > 0 && /* @__PURE__ */ jsx4("span", { className: "isotone-meta", children: references.map((reference) => /* @__PURE__ */ jsx4(SummaryTag, { label: reference.label }, reference.key)) })
1673
1865
  ] });
1674
1866
  }
@@ -1692,6 +1884,7 @@ function NoteSummary({ note, page }) {
1692
1884
  }
1693
1885
  return /* @__PURE__ */ jsxs3(Fragment3, { children: [
1694
1886
  /* @__PURE__ */ jsx4("p", { children: note.feedback }),
1887
+ /* @__PURE__ */ jsx4(Reply, { note }),
1695
1888
  (page || tags.length > 0) && /* @__PURE__ */ jsxs3("span", { className: "isotone-meta", children: [
1696
1889
  page && /* @__PURE__ */ jsx4(SummaryTag, { label: page }),
1697
1890
  tags.slice(0, 3).map((tag) => /* @__PURE__ */ jsx4(SummaryTag, { label: `${tag.name} ${tag.value}` }, `${tag.name}-${tag.value}`)),
@@ -1789,11 +1982,14 @@ function useDockMotion(ref, visible) {
1789
1982
  if (changed) measure();
1790
1983
  });
1791
1984
  mutations.observe(root, { subtree: true, childList: true, attributes: true, attributeFilter: ["data-open", "data-instant"] });
1985
+ const lifted = new MutationObserver(measure);
1986
+ lifted.observe(root, { attributes: true, attributeFilter: ["style"] });
1792
1987
  reduced?.addEventListener("change", measure);
1793
1988
  window.addEventListener("resize", measure);
1794
1989
  return () => {
1795
1990
  observer?.disconnect();
1796
1991
  mutations.disconnect();
1992
+ lifted.disconnect();
1797
1993
  reduced?.removeEventListener("change", measure);
1798
1994
  window.removeEventListener("resize", measure);
1799
1995
  for (const element of exits.keys()) removeExit(element);
@@ -1802,6 +1998,73 @@ function useDockMotion(ref, visible) {
1802
1998
  }, [ref, visible]);
1803
1999
  }
1804
2000
 
2001
+ // src/react/hints.ts
2002
+ var HINTS = {
2003
+ /** The select-mode cover over the docked composer: what to do, then how to pause. */
2004
+ picking: {
2005
+ pointer: { title: "Click elements on the page", pause: "Hold \u21E7 to use the page" },
2006
+ touch: { title: "Tap elements on the page", pause: "Tap Done to use the page" }
2007
+ },
2008
+ /** The floating composer beside a pinned element, while still selecting. */
2009
+ floating: {
2010
+ pointer: "Click to pin more \xB7 \u21E7 uses the page",
2011
+ touch: "Tap to pin more \xB7 crosshair pauses"
2012
+ }
2013
+ };
2014
+
2015
+ // src/react/viewport.ts
2016
+ import { useEffect as useEffect3 } from "react";
2017
+ var COMPACT_QUERY = "(max-width: 640px), (max-height: 500px)";
2018
+ var TOUCH_QUERY = "(hover: none) and (pointer: coarse)";
2019
+ var KEYBOARD_MIN = 80;
2020
+ function media(query) {
2021
+ return typeof matchMedia === "function" && Boolean(matchMedia(query)?.matches);
2022
+ }
2023
+ function isCompactViewport() {
2024
+ return media(COMPACT_QUERY);
2025
+ }
2026
+ function prefersTouch() {
2027
+ return media(TOUCH_QUERY);
2028
+ }
2029
+ function visibleBand() {
2030
+ const viewport = typeof window !== "undefined" ? window.visualViewport : null;
2031
+ if (!viewport || Math.abs(viewport.scale - 1) > 0.01 || innerHeight - viewport.height < KEYBOARD_MIN)
2032
+ return { top: 0, bottom: innerHeight };
2033
+ return { top: viewport.offsetTop, bottom: viewport.offsetTop + viewport.height };
2034
+ }
2035
+ function useKeyboardInset(ref, active) {
2036
+ useEffect3(() => {
2037
+ const node = ref.current;
2038
+ const viewport = typeof window !== "undefined" ? window.visualViewport : null;
2039
+ if (!active || !node || !viewport) return;
2040
+ let frame = 0;
2041
+ const apply = () => {
2042
+ frame = 0;
2043
+ const band = visibleBand();
2044
+ if (band.top === 0 && band.bottom === innerHeight) {
2045
+ node.style.removeProperty("--iso-keyboard");
2046
+ node.style.removeProperty("--iso-viewport");
2047
+ return;
2048
+ }
2049
+ node.style.setProperty("--iso-keyboard", `${Math.max(0, Math.round(innerHeight - band.bottom))}px`);
2050
+ node.style.setProperty("--iso-viewport", `${Math.round(band.bottom - band.top)}px`);
2051
+ };
2052
+ const schedule = () => {
2053
+ frame ||= requestAnimationFrame(apply);
2054
+ };
2055
+ apply();
2056
+ viewport.addEventListener("resize", schedule);
2057
+ viewport.addEventListener("scroll", schedule);
2058
+ return () => {
2059
+ cancelAnimationFrame(frame);
2060
+ viewport.removeEventListener("resize", schedule);
2061
+ viewport.removeEventListener("scroll", schedule);
2062
+ node.style.removeProperty("--iso-keyboard");
2063
+ node.style.removeProperty("--iso-viewport");
2064
+ };
2065
+ }, [ref, active]);
2066
+ }
2067
+
1805
2068
  // src/react/IterationPanel.tsx
1806
2069
  import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1807
2070
  var PULSE_MS = 2e3;
@@ -1859,6 +2122,7 @@ function IterationPanel({
1859
2122
  rule,
1860
2123
  getPayload,
1861
2124
  variants,
2125
+ variantsEmptyText,
1862
2126
  defaultTab = "notes",
1863
2127
  shortcut = DEFAULT_SHORTCUT
1864
2128
  }) {
@@ -1887,6 +2151,16 @@ function IterationPanel({
1887
2151
  );
1888
2152
  const [hoverRect, setHoverRect] = useState3(null);
1889
2153
  const [rects, setRects] = useState3([]);
2154
+ const pinnedElements = useRef4(/* @__PURE__ */ new Map());
2155
+ const locate = useCallback((selector) => {
2156
+ const pinned = pinnedElements.current.get(selector);
2157
+ if (pinned?.isConnected) return pinned;
2158
+ try {
2159
+ return document.querySelector(selector);
2160
+ } catch {
2161
+ return null;
2162
+ }
2163
+ }, []);
1890
2164
  const [text, setText] = useState3("");
1891
2165
  const [notes, setNotes] = useState3([]);
1892
2166
  const [scope, setScope] = useState3(null);
@@ -1899,17 +2173,19 @@ function IterationPanel({
1899
2173
  const [notice, setNotice] = useState3("");
1900
2174
  const [theme, setTheme] = useState3(() => detectTheme(themePreference));
1901
2175
  const [instant, setInstant] = useState3(false);
2176
+ const [touch, setTouch] = useState3(prefersTouch);
2177
+ const touchRef = useRef4(touch);
1902
2178
  const rootRef = useRef4(null);
1903
2179
  const textareaRef = useRef4(null);
1904
2180
  const focusFrame = useRef4(0);
1905
- useEffect3(() => () => cancelAnimationFrame(focusFrame.current), []);
2181
+ useEffect4(() => () => cancelAnimationFrame(focusFrame.current), []);
1906
2182
  const listRef = useRef4(null);
1907
2183
  useLayoutMotion(listRef, notes, notesOpen);
1908
2184
  const fabRef = useRef4(null);
1909
2185
  const previousFocus = useRef4(null);
1910
2186
  const draftRestored = useRef4("");
1911
2187
  const restoringDraft = useRef4(false);
1912
- const id = useId2();
2188
+ const id = useId3();
1913
2189
  const notesAvailable = Boolean(storeCfg.endpoint);
1914
2190
  useVariantsSource(variants);
1915
2191
  const variantsAvailable = Boolean(
@@ -1937,7 +2213,7 @@ function IterationPanel({
1937
2213
  observer.observe(field);
1938
2214
  return () => observer.disconnect();
1939
2215
  }, [text, notesOpen, visible, hasContext, floating]);
1940
- useEffect3(() => {
2216
+ useEffect4(() => {
1941
2217
  if (!visible) {
1942
2218
  setHovered(false);
1943
2219
  setFocused(false);
@@ -1965,9 +2241,21 @@ function IterationPanel({
1965
2241
  };
1966
2242
  }, [visible]);
1967
2243
  useDockMotion(rootRef, visible);
2244
+ useKeyboardInset(rootRef, visible);
2245
+ const wasOpen = useRef4({ notes: notesOpen, variants: variantsOpen });
2246
+ useLayoutEffect4(() => {
2247
+ const was = wasOpen.current;
2248
+ wasOpen.current = { notes: notesOpen, variants: variantsOpen };
2249
+ if (!notesOpen || !variantsOpen || !isCompactViewport()) return;
2250
+ if (was.variants) setVariantsOpen(false);
2251
+ else {
2252
+ setNotesOpen(false);
2253
+ setPicking(false);
2254
+ }
2255
+ }, [notesOpen, variantsOpen]);
1968
2256
  const open = notesOpen || variantsOpen;
1969
2257
  const railOpen = expanded || hovered || focused && instant || open || picking;
1970
- useEffect3(() => {
2258
+ useEffect4(() => {
1971
2259
  if (!railOpen || !instant) return;
1972
2260
  const active = rootRef.current?.getRootNode()?.activeElement;
1973
2261
  if (active === fabRef.current) rootRef.current?.querySelector(".isotone-rail-button")?.focus();
@@ -1976,7 +2264,7 @@ function IterationPanel({
1976
2264
  const openNotes = useMemo3(() => notes.filter((n) => !n.done), [notes]);
1977
2265
  const pages = useMemo3(() => {
1978
2266
  const counts = /* @__PURE__ */ new Map();
1979
- for (const n of openNotes) counts.set(n.route, (counts.get(n.route) ?? 0) + 1);
2267
+ for (const n of openNotes) if (!n.update) counts.set(n.route, (counts.get(n.route) ?? 0) + 1);
1980
2268
  const others = [...counts.keys()].filter((r) => r !== route).sort();
1981
2269
  return { counts, others };
1982
2270
  }, [openNotes, route]);
@@ -1984,7 +2272,8 @@ function IterationPanel({
1984
2272
  const chosen = (scope ?? [route]).filter((r) => r === route || pages.counts.has(r));
1985
2273
  return new Set(chosen.length ? chosen : [route]);
1986
2274
  }, [scope, route, pages]);
1987
- const visibleNotes = useMemo3(() => openNotes.filter((n) => selectedPages.has(n.route)).sort((a, b) => a.createdAt.localeCompare(b.createdAt)), [openNotes, selectedPages]);
2275
+ const updateRequested = useMemo3(() => openNotes.some((n) => n.update), [openNotes]);
2276
+ const visibleNotes = useMemo3(() => openNotes.filter((n) => n.update || selectedPages.has(n.route)).sort((a, b) => a.createdAt.localeCompare(b.createdAt)), [openNotes, selectedPages]);
1988
2277
  const working = useMemo3(() => new Set(visibleNotes.filter((n) => n.workingAt && clock - Date.parse(n.workingAt) < WORKING_FRESH_MS).map((n) => n.id)), [visibleNotes, clock]);
1989
2278
  function togglePage(page) {
1990
2279
  const next = new Set(selectedPages);
@@ -2184,10 +2473,10 @@ function IterationPanel({
2184
2473
  finish();
2185
2474
  }
2186
2475
  }
2187
- useEffect3(() => {
2476
+ useEffect4(() => {
2188
2477
  loadNotes();
2189
2478
  }, [notesOpen, variantsOpen, route]);
2190
- useEffect3(() => {
2479
+ useEffect4(() => {
2191
2480
  if (!notesAvailable || !visible) return;
2192
2481
  let stopped = false;
2193
2482
  let running = false;
@@ -2236,13 +2525,13 @@ function IterationPanel({
2236
2525
  window.removeEventListener("focus", wake);
2237
2526
  };
2238
2527
  }, [notesAvailable, visible, store]);
2239
- useEffect3(() => {
2528
+ useEffect4(() => {
2240
2529
  if (variants && notesOpen && !variantsOpen) refreshSource(variants);
2241
2530
  }, [notesOpen, variantsOpen, variants]);
2242
- useEffect3(() => {
2531
+ useEffect4(() => {
2243
2532
  if (variants && !variantsAvailable) refreshSource(variants);
2244
2533
  }, [variants]);
2245
- useEffect3(() => {
2534
+ useEffect4(() => {
2246
2535
  if (draftRestored.current === draftKey) return;
2247
2536
  draftRestored.current = draftKey;
2248
2537
  restoringDraft.current = true;
@@ -2267,7 +2556,7 @@ function IterationPanel({
2267
2556
  } catch {
2268
2557
  }
2269
2558
  }, [draftKey]);
2270
- useEffect3(() => {
2559
+ useEffect4(() => {
2271
2560
  if (restoringDraft.current) {
2272
2561
  restoringDraft.current = false;
2273
2562
  return;
@@ -2285,7 +2574,7 @@ function IterationPanel({
2285
2574
  } catch {
2286
2575
  }
2287
2576
  }, [text, targets, variantTarget, editing, draftKey]);
2288
- useEffect3(() => {
2577
+ useEffect4(() => {
2289
2578
  const el = listRef.current;
2290
2579
  if (el) el.scrollTop = el.scrollHeight;
2291
2580
  }, [notes, notesOpen]);
@@ -2296,7 +2585,7 @@ function IterationPanel({
2296
2585
  ensurePanelStyles(root);
2297
2586
  return observeTheme(setTheme, themePreference);
2298
2587
  }, [visible, open, route, themePreference]);
2299
- useEffect3(() => {
2588
+ useEffect4(() => {
2300
2589
  if (!visible) return;
2301
2590
  const onKey = (e) => {
2302
2591
  rootRef.current?.setAttribute("data-instant", "true");
@@ -2317,9 +2606,13 @@ function IterationPanel({
2317
2606
  startFeedback(true);
2318
2607
  }
2319
2608
  };
2320
- const onPointer = () => {
2609
+ const onPointer = (e) => {
2321
2610
  rootRef.current?.setAttribute("data-instant", "false");
2322
2611
  setInstant(false);
2612
+ if (e.pointerType) {
2613
+ touchRef.current = e.pointerType === "touch";
2614
+ setTouch(touchRef.current);
2615
+ }
2323
2616
  };
2324
2617
  window.addEventListener("keydown", onKey, true);
2325
2618
  window.addEventListener("pointerdown", onPointer, true);
@@ -2328,7 +2621,7 @@ function IterationPanel({
2328
2621
  window.removeEventListener("pointerdown", onPointer, true);
2329
2622
  };
2330
2623
  });
2331
- useEffect3(() => {
2624
+ useEffect4(() => {
2332
2625
  if (!open || picking) return;
2333
2626
  const onKey = (e) => {
2334
2627
  if (e.key !== "Escape" || e.defaultPrevented) return;
@@ -2339,7 +2632,7 @@ function IterationPanel({
2339
2632
  window.addEventListener("keydown", onKey);
2340
2633
  return () => window.removeEventListener("keydown", onKey);
2341
2634
  });
2342
- useEffect3(() => {
2635
+ useEffect4(() => {
2343
2636
  if (!picking) return;
2344
2637
  const cursor = document.body.style.cursor;
2345
2638
  document.body.style.cursor = "crosshair";
@@ -2349,6 +2642,7 @@ function IterationPanel({
2349
2642
  return el && !el.closest("[data-isotone]") ? el : null;
2350
2643
  };
2351
2644
  const onMove = (e) => {
2645
+ if (e.pointerType === "touch") return;
2352
2646
  cancelAnimationFrame(frame);
2353
2647
  const deferred = e.shiftKey;
2354
2648
  frame = requestAnimationFrame(() => {
@@ -2358,6 +2652,7 @@ function IterationPanel({
2358
2652
  };
2359
2653
  const toggleTarget = (el) => {
2360
2654
  const target = describeIterationElement(elementAsNode(el));
2655
+ pinnedElements.current.set(target.selector, el);
2361
2656
  const added = !targetsRef.current.some((t) => t.selector === target.selector);
2362
2657
  setTargets(
2363
2658
  (prev) => prev.some((t) => t.selector === target.selector) ? prev.filter((t) => t.selector !== target.selector) : prev.length < 50 ? [...prev, target] : prev
@@ -2378,7 +2673,7 @@ function IterationPanel({
2378
2673
  const y = pointed ? e.clientY : r.bottom;
2379
2674
  if (!anchorRef.current)
2380
2675
  slotHeight.current = rootRef.current?.querySelector(".isotone-notes-panel .isotone-input")?.offsetHeight ?? 0;
2381
- setAnchor({ selector: added.selector, dx: x - r.left, dy: y - r.top, x, y });
2676
+ setAnchor({ selector: added.selector, dx: x - r.left, dy: y - r.top, x, y, beside: touchRef.current && pointed });
2382
2677
  setNotesOpen(true);
2383
2678
  focusWhilePicking();
2384
2679
  }
@@ -2415,19 +2710,19 @@ function IterationPanel({
2415
2710
  setNotesOpen(true);
2416
2711
  focusComposer();
2417
2712
  };
2418
- window.addEventListener("mousemove", onMove, true);
2713
+ window.addEventListener("pointermove", onMove, true);
2419
2714
  window.addEventListener("click", onClick, true);
2420
2715
  window.addEventListener("keydown", onKey, true);
2421
2716
  return () => {
2422
2717
  document.body.style.cursor = cursor;
2423
2718
  cancelAnimationFrame(frame);
2424
2719
  setHoverRect(null);
2425
- window.removeEventListener("mousemove", onMove, true);
2720
+ window.removeEventListener("pointermove", onMove, true);
2426
2721
  window.removeEventListener("click", onClick, true);
2427
2722
  window.removeEventListener("keydown", onKey, true);
2428
2723
  };
2429
2724
  }, [picking, focusComposer, focusWhilePicking]);
2430
- useEffect3(() => {
2725
+ useEffect4(() => {
2431
2726
  const shadow = rootRef.current?.getRootNode();
2432
2727
  if (!(shadow instanceof ShadowRoot)) return;
2433
2728
  const stop = (e) => e.stopPropagation();
@@ -2437,7 +2732,7 @@ function IterationPanel({
2437
2732
  for (const type of types) shadow.removeEventListener(type, stop);
2438
2733
  };
2439
2734
  }, []);
2440
- useEffect3(() => {
2735
+ useEffect4(() => {
2441
2736
  if (!notesOpen) setAnchor(null);
2442
2737
  }, [notesOpen]);
2443
2738
  useLayoutEffect4(() => {
@@ -2453,39 +2748,46 @@ function IterationPanel({
2453
2748
  const place = () => {
2454
2749
  frame = void 0;
2455
2750
  let { x, y } = anchor;
2456
- try {
2457
- const el = document.querySelector(anchor.selector);
2458
- if (el) {
2459
- const r = el.getBoundingClientRect();
2460
- x = r.left + anchor.dx;
2461
- y = r.top + anchor.dy;
2462
- }
2463
- } catch {
2751
+ const el = locate(anchor.selector);
2752
+ const r = el?.getBoundingClientRect();
2753
+ if (r) {
2754
+ x = r.left + anchor.dx;
2755
+ y = r.top + anchor.dy;
2464
2756
  }
2465
2757
  const vw = document.documentElement.clientWidth || innerWidth;
2466
2758
  const vh = innerHeight;
2759
+ const band = visibleBand();
2760
+ const minY = band.top + FLOAT_MARGIN;
2761
+ const maxY = band.bottom - FLOAT_MARGIN;
2467
2762
  const w = float.offsetWidth;
2468
2763
  const h = float.offsetHeight;
2764
+ const fits = (below, above) => [below + FLOAT_GAP + h <= maxY, above - FLOAT_GAP - h >= minY];
2765
+ let belowY = y;
2766
+ let aboveY = y;
2469
2767
  let left = x + FLOAT_GAP;
2470
- if (left + w > vw - FLOAT_MARGIN) left = x - FLOAT_GAP - w;
2768
+ if (anchor.beside && r && fits(r.bottom, r.top).some(Boolean)) {
2769
+ belowY = r.bottom;
2770
+ aboveY = r.top;
2771
+ left = x - w / 2;
2772
+ } else if (left + w > vw - FLOAT_MARGIN) left = x - FLOAT_GAP - w;
2471
2773
  left = Math.max(FLOAT_MARGIN, Math.min(left, vw - w - FLOAT_MARGIN));
2472
- const fitsBelow = y + FLOAT_GAP + h <= vh - FLOAT_MARGIN;
2473
- const fitsAbove = y - FLOAT_GAP - h >= FLOAT_MARGIN;
2774
+ const [fitsBelow, fitsAbove] = fits(belowY, aboveY);
2474
2775
  if (side === null || (side === "below" ? !fitsBelow && fitsAbove : !fitsAbove && fitsBelow))
2475
2776
  side = fitsBelow || !fitsAbove ? "below" : "above";
2476
2777
  float.style.left = `${left}px`;
2778
+ let top;
2477
2779
  if (side === "below") {
2478
- const top = Math.max(FLOAT_MARGIN, Math.min(y + FLOAT_GAP, vh - h - FLOAT_MARGIN));
2780
+ top = Math.max(minY, Math.min(belowY + FLOAT_GAP, maxY - h));
2479
2781
  float.style.top = `${top}px`;
2480
2782
  float.style.bottom = "auto";
2481
- float.style.transformOrigin = `${x - left}px ${y - top}px`;
2482
2783
  } else {
2483
- const bottom = Math.max(FLOAT_MARGIN, Math.min(vh - y + FLOAT_GAP, vh - h - FLOAT_MARGIN));
2784
+ const bottom = Math.max(vh - maxY, Math.min(vh - aboveY + FLOAT_GAP, vh - h - minY));
2484
2785
  float.style.bottom = `${bottom}px`;
2485
2786
  float.style.top = "auto";
2486
- float.style.transformOrigin = `${x - left}px ${y - (vh - bottom - h)}px`;
2787
+ top = vh - bottom - h;
2487
2788
  }
2488
- lastFloat.current = { left, top: side === "below" ? Number.parseFloat(float.style.top) : vh - Number.parseFloat(float.style.bottom) - h };
2789
+ float.style.transformOrigin = `${x - left}px ${y - top}px`;
2790
+ lastFloat.current = { left, top };
2489
2791
  };
2490
2792
  const schedule = () => {
2491
2793
  frame ??= requestAnimationFrame(place);
@@ -2509,14 +2811,19 @@ function IterationPanel({
2509
2811
  observer?.observe(float);
2510
2812
  window.addEventListener("scroll", schedule, { capture: true, passive: true });
2511
2813
  window.addEventListener("resize", schedule);
2814
+ const viewport = window.visualViewport;
2815
+ viewport?.addEventListener("resize", schedule);
2816
+ viewport?.addEventListener("scroll", schedule);
2512
2817
  return () => {
2513
2818
  if (frame !== void 0) cancelAnimationFrame(frame);
2514
2819
  glide?.cancel();
2515
2820
  observer?.disconnect();
2516
2821
  window.removeEventListener("scroll", schedule, true);
2517
2822
  window.removeEventListener("resize", schedule);
2823
+ viewport?.removeEventListener("resize", schedule);
2824
+ viewport?.removeEventListener("scroll", schedule);
2518
2825
  };
2519
- }, [anchor]);
2826
+ }, [anchor, locate]);
2520
2827
  useLayoutEffect4(() => {
2521
2828
  const last = floatNode.current;
2522
2829
  const root = rootRef.current;
@@ -2543,7 +2850,11 @@ function IterationPanel({
2543
2850
  ghost.remove();
2544
2851
  };
2545
2852
  }, [floating]);
2546
- useEffect3(() => {
2853
+ useEffect4(() => {
2854
+ const kept = new Set(targets.map((t) => t.selector));
2855
+ for (const selector of pinnedElements.current.keys()) if (!kept.has(selector)) pinnedElements.current.delete(selector);
2856
+ }, [targets]);
2857
+ useEffect4(() => {
2547
2858
  if (!picking && !floating) {
2548
2859
  setRects([]);
2549
2860
  return;
@@ -2553,22 +2864,18 @@ function IterationPanel({
2553
2864
  frame = void 0;
2554
2865
  setRects(
2555
2866
  targets.flatMap((t) => {
2556
- try {
2557
- const el = document.querySelector(t.selector);
2558
- if (!el) return [];
2559
- const r = el.getBoundingClientRect();
2560
- return [
2561
- {
2562
- top: r.top,
2563
- left: r.left,
2564
- width: r.width,
2565
- height: r.height,
2566
- selector: t.selector
2567
- }
2568
- ];
2569
- } catch {
2570
- return [];
2571
- }
2867
+ const el = locate(t.selector);
2868
+ if (!el) return [];
2869
+ const r = el.getBoundingClientRect();
2870
+ return [
2871
+ {
2872
+ top: r.top,
2873
+ left: r.left,
2874
+ width: r.width,
2875
+ height: r.height,
2876
+ selector: t.selector
2877
+ }
2878
+ ];
2572
2879
  })
2573
2880
  );
2574
2881
  };
@@ -2583,7 +2890,7 @@ function IterationPanel({
2583
2890
  window.removeEventListener("scroll", schedule, true);
2584
2891
  window.removeEventListener("resize", schedule);
2585
2892
  };
2586
- }, [targets, picking, floating]);
2893
+ }, [targets, picking, floating, locate]);
2587
2894
  const variantsPanel = variants && /* @__PURE__ */ jsxs4(
2588
2895
  "section",
2589
2896
  {
@@ -2613,7 +2920,8 @@ function IterationPanel({
2613
2920
  onFeedback: notesAvailable ? feedback : void 0,
2614
2921
  onSaved: loadNotes,
2615
2922
  onManage: notesAvailable ? queueRemoval : void 0,
2616
- pendingActions
2923
+ pendingActions,
2924
+ emptyText: variantsEmptyText
2617
2925
  }
2618
2926
  )
2619
2927
  ]
@@ -2635,6 +2943,11 @@ function IterationPanel({
2635
2943
  textareaRef.current?.focus({ preventScroll: true });
2636
2944
  },
2637
2945
  children: [
2946
+ editing?.reply?.text && !editing.reply.answered && /* @__PURE__ */ jsxs4("p", { className: "isotone-editing-reply", children: [
2947
+ /* @__PURE__ */ jsx5("span", { children: "Agent asks" }),
2948
+ " ",
2949
+ editing.reply.text
2950
+ ] }),
2638
2951
  /* @__PURE__ */ jsxs4("div", { className: "isotone-context", "aria-live": "polite", children: [
2639
2952
  editing && /* @__PURE__ */ jsxs4("span", { className: "isotone-chip", children: [
2640
2953
  "Editing note",
@@ -2734,7 +3047,7 @@ function IterationPanel({
2734
3047
  children: /* @__PURE__ */ jsx5(Icon2, { name: "pin", size: 14 })
2735
3048
  }
2736
3049
  ),
2737
- floating && picking && /* @__PURE__ */ jsx5("span", { ref: fadeIn, className: "isotone-float-hint", children: "Click to pin more \xB7 hold \u21E7 to use the page" }),
3050
+ floating && picking && /* @__PURE__ */ jsx5("span", { ref: fadeIn, className: "isotone-float-hint", children: HINTS.floating[touch ? "touch" : "pointer"] }),
2738
3051
  /* @__PURE__ */ jsx5(
2739
3052
  "button",
2740
3053
  {
@@ -2751,8 +3064,8 @@ function IterationPanel({
2751
3064
  picking && !floating && /* @__PURE__ */ jsxs4("div", { ref: fadeIn, className: "isotone-picking", "data-still": pickingWithPanel, children: [
2752
3065
  /* @__PURE__ */ jsx5(Icon2, { name: "pin", size: 14 }),
2753
3066
  /* @__PURE__ */ jsxs4("p", { children: [
2754
- /* @__PURE__ */ jsx5("span", { children: targets.length ? `${targets.length} selected` : "Click elements on the page" }),
2755
- /* @__PURE__ */ jsx5("small", { children: "Hold \u21E7 to use the page normally" })
3067
+ /* @__PURE__ */ jsx5("span", { children: targets.length ? `${targets.length} selected` : HINTS.picking[touch ? "touch" : "pointer"].title }),
3068
+ /* @__PURE__ */ jsx5("small", { children: HINTS.picking[touch ? "touch" : "pointer"].pause })
2756
3069
  ] }),
2757
3070
  /* @__PURE__ */ jsxs4(
2758
3071
  "button",
@@ -2764,8 +3077,8 @@ function IterationPanel({
2764
3077
  focusComposer();
2765
3078
  },
2766
3079
  children: [
2767
- "Done ",
2768
- /* @__PURE__ */ jsx5("kbd", { children: "\u21B5" })
3080
+ "Done",
3081
+ !touch && /* @__PURE__ */ jsx5("kbd", { children: "\u21B5" })
2769
3082
  ]
2770
3083
  }
2771
3084
  )
@@ -2842,7 +3155,14 @@ function IterationPanel({
2842
3155
  "data-motion-key": note.id,
2843
3156
  className: `isotone-note${editing?.id === note.id ? " isotone-note-editing" : ""}`,
2844
3157
  children: [
2845
- /* @__PURE__ */ jsxs4(
3158
+ note.update ? (
3159
+ // Isotone writes an update note's text from its request, so
3160
+ // there is nothing to edit; its corner action cancels it.
3161
+ /* @__PURE__ */ jsxs4("div", { className: "isotone-note-hit", "data-static": "", children: [
3162
+ working.has(note.id) && /* @__PURE__ */ jsx5("span", { className: "isotone-working", ref: dotPulse, role: "img", "aria-label": "An agent is working on this", title: "An agent is working on this" }),
3163
+ /* @__PURE__ */ jsx5(NoteSummary, { note })
3164
+ ] })
3165
+ ) : /* @__PURE__ */ jsxs4(
2846
3166
  "button",
2847
3167
  {
2848
3168
  type: "button",
@@ -2860,8 +3180,8 @@ function IterationPanel({
2860
3180
  {
2861
3181
  type: "button",
2862
3182
  className: "isotone-x isotone-icon",
2863
- "aria-label": note.variantAction ? "Cancel removal" : "Delete note",
2864
- title: note.variantAction ? "Cancel removal" : "Delete note",
3183
+ "aria-label": note.variantAction ? "Cancel removal" : note.update ? "Cancel update request" : "Delete note",
3184
+ title: note.variantAction ? "Cancel removal" : note.update ? "Cancel update request" : "Delete note",
2865
3185
  onClick: () => void removeNote(note),
2866
3186
  children: /* @__PURE__ */ jsx5(Icon2, { name: "close", size: 12 })
2867
3187
  }
@@ -2904,6 +3224,7 @@ function IterationPanel({
2904
3224
  "data-tools": Number(Boolean(variants)) + Number(notesAvailable),
2905
3225
  "data-instant": instant,
2906
3226
  "data-notes-open": notesOpen,
3227
+ "data-selecting": picking || floating,
2907
3228
  "data-working": working.size > 0,
2908
3229
  "data-open": open,
2909
3230
  className: "isotone-root",
@@ -2944,7 +3265,16 @@ function IterationPanel({
2944
3265
  children: composer
2945
3266
  }
2946
3267
  ),
2947
- /* @__PURE__ */ jsx5(UpdateNotice, { endpoint: updateEndpoint, active: open }),
3268
+ /* @__PURE__ */ jsx5("div", { className: "isotone-notices", children: /* @__PURE__ */ jsx5(
3269
+ UpdateNotice,
3270
+ {
3271
+ endpoint: updateEndpoint,
3272
+ active: open,
3273
+ route,
3274
+ requested: updateRequested,
3275
+ onRequested: notesAvailable ? loadNotes : void 0
3276
+ }
3277
+ ) }),
2948
3278
  /* @__PURE__ */ jsxs4(
2949
3279
  "fieldset",
2950
3280
  {
@@ -3069,9 +3399,10 @@ function withDocumentUntouched(create) {
3069
3399
  }
3070
3400
  return result;
3071
3401
  }
3402
+ var KNOBS_EMPTY_TEXT = "No knobs on this page yet. Ask your agent to add knobs for this page to .isotone/knobs.json and they will appear here.";
3072
3403
  function mountIteration(opts) {
3073
3404
  const getRoute = opts.getRoute ?? (() => window.location.pathname);
3074
- const variants = opts.variants === "overlay" ? overlaySourceWithSave(getRoute, opts.endpoint, opts.key) : opts.variants;
3405
+ const variants = opts.variants === "overlay" || opts.variants === "knobs" ? overlaySourceWithSave(getRoute, opts.endpoint, opts.key, opts.variantsEndpoint) : opts.variants;
3075
3406
  if (!opts.endpoint && !variants) return () => {
3076
3407
  };
3077
3408
  if (!opts.endpoint && variants && variants.getAxes().length === 0 && typeof variants.refresh !== "function") {
@@ -3103,6 +3434,7 @@ function mountIteration(opts) {
3103
3434
  rule: opts.rule,
3104
3435
  getPayload: opts.getPayload,
3105
3436
  variants,
3437
+ variantsEmptyText: opts.variants === "knobs" ? KNOBS_EMPTY_TEXT : void 0,
3106
3438
  defaultTab: opts.defaultTab,
3107
3439
  shortcut: opts.shortcut,
3108
3440
  updateEndpoint: opts.updateEndpoint,
@@ -3149,17 +3481,18 @@ function mountIteration(opts) {
3149
3481
  }, 0);
3150
3482
  };
3151
3483
  }
3152
- function overlaySourceWithSave(getRoute, endpoint, key) {
3153
- const source = createOverlayVariantsSource({ endpoint: VARIANTS_ENDPOINT, getRoute });
3484
+ function overlaySourceWithSave(getRoute, endpoint, key, variantsEndpoint = VARIANTS_ENDPOINT) {
3485
+ const source = createOverlayVariantsSource({ endpoint: variantsEndpoint, getRoute });
3154
3486
  if (!endpoint) return source;
3155
3487
  const notes = createIterationStore({ endpoint, key });
3156
3488
  source.saveDefaults = async (selections) => {
3157
- const picked = source.getAxes().filter((axis) => axis.kind === void 0 || axis.kind === "select").flatMap((axis) => {
3489
+ const picked = source.getAxes().filter((axis) => (axis.kind === void 0 || axis.kind === "select") && !source.knobs.axes.includes(axis)).flatMap((axis) => {
3158
3490
  const id = selections[axis.id];
3159
3491
  if (!id || id === OVERLAY_DEFAULT_OPTION) return [];
3160
3492
  const variant = source.list?.variants.find((v) => v.id === id);
3161
3493
  const label = variant?.label ?? id;
3162
- const named = axis.label === label ? label : `${axis.label}: ${label}`;
3494
+ const selector = axisSection(axis).fullLabel;
3495
+ const named = selector === label || axis.label === label ? label : `${selector}: ${label}`;
3163
3496
  return [{ id, named, element: variant?.element ?? null }];
3164
3497
  });
3165
3498
  const moved = knobDelta(source.knobs.axes, source.knobs.values);
@@ -3168,7 +3501,7 @@ function overlaySourceWithSave(getRoute, endpoint, key) {
3168
3501
  parts.push(`Make the default: ${picked.map((p) => `"${p.named}" (${p.id})`).join("; ")}.`);
3169
3502
  }
3170
3503
  if (moved.length) {
3171
- parts.push(`Knobs: ${moved.map((m) => `${m.axis.label} ${m.shown} (${m.axis.id})`).join("; ")}.`);
3504
+ parts.push(`Knobs: ${moved.map((m) => `${axisSection(m.axis).fullLabel} ${m.shown} (${m.axis.id})`).join("; ")}.`);
3172
3505
  }
3173
3506
  const feedback = parts.length ? `[set-defaults] ${parts.join(" ")}` : "[set-defaults] The current defaults stay on this route.";
3174
3507
  await notes.saveNote(
@@ -3191,10 +3524,12 @@ export {
3191
3524
  OVERLAY_DEFAULT_OPTION,
3192
3525
  OVERLAY_GROUP_ID,
3193
3526
  PANEL_CSS,
3527
+ SECTION_SEPARATOR,
3194
3528
  VARIANTS_ENDPOINT,
3195
3529
  VariantsView,
3196
3530
  applyKnobValues,
3197
3531
  axisDefault,
3532
+ axisSection,
3198
3533
  createOverlayVariantsSource,
3199
3534
  createVariantsSource,
3200
3535
  defaultSelections,
@@ -3214,7 +3549,9 @@ export {
3214
3549
  overlayAxes,
3215
3550
  overlayGroups,
3216
3551
  parseVariantParam,
3552
+ sectionAxes,
3217
3553
  serializeVariantParam,
3554
+ splitSectionLabel,
3218
3555
  visibleAxes
3219
3556
  };
3220
3557
  /*! Bundled license information: