isotone 1.0.6 → 1.0.8

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/index.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  describeIterationElement,
5
5
  elementAsNode,
6
6
  isRouteVisible
7
- } from "./chunk-UYMMI2IH.js";
7
+ } from "./chunk-JGIX4XEH.js";
8
8
  import {
9
9
  FEEDBACK_STORAGE_KEY,
10
10
  KNOBS_STORAGE_KEY,
@@ -781,7 +781,17 @@ ${APPEARANCE_CSS}
781
781
  .isotone-panel[data-open='true'] { display: flex; }
782
782
  .isotone-variants-panel { flex-shrink: 1; max-height: calc(100dvh - 76px); }
783
783
  .isotone-notes-panel { flex-shrink: 0; max-height: calc(100dvh - 76px); padding: var(--iso-space); gap: var(--iso-space); }
784
- .isotone-root .isotone-panel { background: var(--iso-bg); border: 1px solid var(--iso-line); border-radius: 12px; box-shadow: var(--iso-panel-shadow); overflow: hidden; }
784
+ .isotone-root .isotone-panel { background: var(--iso-bg); border: 1px solid var(--iso-line); border-radius: 12px; box-shadow: var(--iso-panel-shadow); }
785
+ /* The close control sits on the panel's top-left corner, half outside it, with
786
+ the same face as a note's delete button, a touch larger. It fades in while the
787
+ pointer is over the panel; keyboard focus and touch screens always show it. */
788
+ .isotone-panel-close { position: absolute; left: -9px; top: -9px; z-index: 3; width: 26px; height: 26px; display: grid; place-items: center; padding: 0; border: 0; isolation: isolate; color: var(--iso-muted); background: none; opacity: 0; }
789
+ .isotone-panel-close::before { content: ''; position: absolute; inset: 4px; border-radius: 50%; z-index: -1; background: var(--iso-bg); box-shadow: 0 0 0 1px var(--iso-edge); transition: background-color 120ms ease; }
790
+ .isotone-root .isotone-panel > .isotone-panel-close { transition: opacity 220ms ease, color 120ms ease; }
791
+ .isotone-root .isotone-panel:hover > .isotone-panel-close, .isotone-panel-close:focus-visible { opacity: 1; }
792
+ .isotone-root .isotone-panel > .isotone-panel-close:hover { color: var(--iso-fg); }
793
+ .isotone-panel-close:hover::before { background: var(--iso-control-hover-face); }
794
+ @media (hover: none) { .isotone-panel-close { opacity: 1; } }
785
795
  .isotone-panel-head { flex: 0 0 42px; min-height: 42px; display: flex; align-items: center; justify-content: space-between; padding: 0 8px 0 14px; gap: 8px; border-bottom: 1px solid var(--iso-line); }
786
796
  .isotone-root .isotone-notes-panel > .isotone-panel-head { display: none; }
787
797
  .isotone-root .isotone-vars > .isotone-panel-head { order: 2; flex: 0 0 auto; min-height: 48px; padding: var(--iso-space); border: 0; }
@@ -795,6 +805,8 @@ ${APPEARANCE_CSS}
795
805
  .isotone-count { margin-left: 5px; color: var(--iso-muted); font-size: 10px; font-variant-numeric: tabular-nums; }
796
806
  .isotone-vars { flex: 1; display: flex; flex-direction: column; min-height: 0; position: relative; }
797
807
  .isotone-vars { min-height: min(var(--iso-menu-need, 0px), calc(100dvh - 76px)); }
808
+ /* The panel no longer clips, so its corner close can overhang; the variants body clips itself. */
809
+ .isotone-root .isotone-panel > .isotone-vars { overflow: hidden; border-radius: 11px; }
798
810
  .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); }
799
811
  /* The footer overlaps only the scroller's bottom padding: content fades beneath
800
812
  it while scrolling, but the last control remains fully visible at the end. */
@@ -878,6 +890,17 @@ ${PILL_ACTIVE_CSS}
878
890
  surrounds a 16px face without reserving trailing space in every row. */
879
891
  .isotone-note > .isotone-x { position: absolute; right: -8px; top: -8px; width: 24px; height: 24px; isolation: isolate; opacity: .6; background: none; box-shadow: none; }
880
892
  .isotone-x::before { content: ''; position: absolute; inset: 4px; border-radius: 50%; z-index: -1; background: var(--iso-bg); box-shadow: 0 0 0 1px var(--iso-edge); }
893
+ /* One chip per page that has open notes, this page first. */
894
+ .isotone-pages { display: flex; flex: none; min-width: 0; border-top: 0; border-bottom: 0; border-inline: 0; gap: 4px; overflow-x: auto; overscroll-behavior: contain; margin: 0 calc(0px - var(--iso-space)); padding: 0 var(--iso-space); }
895
+ .isotone-page { flex: 0 1 auto; display: inline-flex; align-items: center; gap: 6px; min-width: 0; max-width: 160px; height: 22px; padding: 0 7px; border-radius: var(--iso-control-radius); border: 1px solid var(--iso-edge); background: transparent; color: var(--iso-muted); font-size: 10px; }
896
+ .isotone-page > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
897
+ .isotone-page small { color: var(--iso-faint); font-size: 9px; font-variant-numeric: tabular-nums; }
898
+ .isotone-page[aria-pressed='true'] { color: var(--iso-fg); background: var(--iso-label-face); box-shadow: var(--iso-label-lift-shadow); }
899
+ /* An agent has said it is working on a note: a soft pulse on the note, and a ring on the launcher.
900
+ Both are driven by one shared clock (see syncPulse), so they beat together. */
901
+ .isotone-note-hit { position: relative; }
902
+ .isotone-working { position: absolute; left: 3px; top: 5px; width: 6px; height: 6px; border-radius: 50%; background: var(--iso-focus); }
903
+ .isotone-ring { position: absolute; inset: 0; border-radius: 999px; pointer-events: none; }
881
904
  .isotone-list:empty { display: none; }
882
905
  ${FEEDBACK_LAYOUT_CSS}
883
906
  .isotone-compose { flex: none; display: flex; flex-direction: column; }
@@ -903,7 +926,9 @@ ${FEEDBACK_LAYOUT_CSS}
903
926
  .isotone-pick::after, .isotone-send::after { content: ''; position: absolute; inset: -2px; border-radius: inherit; }
904
927
  .isotone-send { display: grid; place-items: center; margin-left: auto; flex: none; }
905
928
  .isotone-notice { font-size: 10px; color: var(--iso-muted); }
906
- .isotone-hint { position: fixed; top: 16px; left: 50%; transform: translateX(-50%); display: flex; align-items: center; gap: 12px; padding: 8px 10px 8px 14px; border-radius: 24px; background: var(--iso-bg); box-shadow: var(--iso-shadow); font-size: 11px; white-space: nowrap; }
929
+ .isotone-hint { position: fixed; top: 16px; left: 50%; transform: translateX(-50%); display: flex; align-items: center; gap: 12px; padding: 8px 10px 8px 14px; border-radius: 24px; background: var(--iso-bg); box-shadow: var(--iso-shadow); font-size: 11px; white-space: nowrap; transition: opacity 120ms ease; }
930
+ /* Near the pointer the hint fades and lets the page underneath be hovered and pinned. */
931
+ .isotone-root .isotone-hint[data-away='true'] { opacity: 0; pointer-events: none; }
907
932
  .isotone-hint button { display: flex; align-items: center; gap: 8px; border-radius: 14px; padding: 5px 9px; }
908
933
  .isotone-hint kbd { font: inherit; color: var(--iso-muted); }
909
934
  .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; }
@@ -924,12 +949,15 @@ ${FEEDBACK_LAYOUT_CSS}
924
949
  }
925
950
  @media (max-height: 580px) { .isotone-list { --iso-list-height: 72px; } .isotone-root .isotone-textarea { max-height: calc(64px + var(--iso-textarea-inset)); } }
926
951
  @media (prefers-reduced-motion: reduce) {
952
+ .isotone-ring { box-shadow: 0 0 0 2px color-mix(in srgb, var(--iso-focus) 50%, transparent); }
927
953
  .isotone-root .isotone-rail-track { transition: none; }
928
954
  .isotone-root .isotone-panel, .isotone-root .isotone-axis-menu, .isotone-root .isotone-rail-tools { transition-property: opacity, visibility; }
929
955
  .isotone-root button { transition: opacity 125ms ease; }
930
956
  .isotone-root button:not(:disabled):active > svg { transform: none; }
931
957
  }
932
958
  .isotone-root[data-instant='true'] *, .isotone-root[data-instant='true'] *::before, .isotone-root[data-instant='true'] *::after { transition-duration: 0s !important; transition-delay: 0s !important; animation: none !important; }
959
+ /* Instant mode follows any key press, but the corner close only ever appears under the pointer, so it keeps its fade. */
960
+ .isotone-root[data-instant='true'] .isotone-panel > .isotone-panel-close { transition-duration: 220ms, 120ms !important; }
933
961
  ${UPDATE_NOTICE_CSS}
934
962
  `;
935
963
  function ensurePanelStyles(target) {
@@ -1611,10 +1639,11 @@ import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-run
1611
1639
  function SummaryTag({ label }) {
1612
1640
  return /* @__PURE__ */ jsx4("span", { className: "isotone-chip", children: /* @__PURE__ */ jsx4("span", { onPointerOver: showOverflowTitle, children: label }) });
1613
1641
  }
1614
- function NoteSummary({ note }) {
1642
+ function NoteSummary({ note, page }) {
1615
1643
  if (!note.feedback.startsWith("[set-defaults]")) {
1616
1644
  const elements = note.elements ?? (note.element ? [note.element] : []);
1617
1645
  const references = [
1646
+ ...page ? [{ key: `page:${page}`, label: page }] : [],
1618
1647
  ...note.variant ? [{
1619
1648
  key: `variant:${note.variant.axisId}`,
1620
1649
  label: `${note.variant.label}${note.variant.optionLabel ? ` / ${note.variant.optionLabel}` : ""}`
@@ -1646,7 +1675,8 @@ function NoteSummary({ note }) {
1646
1675
  }
1647
1676
  return /* @__PURE__ */ jsxs3(Fragment3, { children: [
1648
1677
  /* @__PURE__ */ jsx4("p", { children: note.feedback }),
1649
- tags.length > 0 && /* @__PURE__ */ jsxs3("span", { className: "isotone-meta", children: [
1678
+ (page || tags.length > 0) && /* @__PURE__ */ jsxs3("span", { className: "isotone-meta", children: [
1679
+ page && /* @__PURE__ */ jsx4(SummaryTag, { label: page }),
1650
1680
  tags.slice(0, 3).map((tag) => /* @__PURE__ */ jsx4(SummaryTag, { label: `${tag.name} ${tag.value}` }, `${tag.name}-${tag.value}`)),
1651
1681
  tags.length > 3 && /* @__PURE__ */ jsxs3("span", { className: "isotone-request-more", children: [
1652
1682
  "+",
@@ -1757,6 +1787,33 @@ function useDockMotion(ref, visible) {
1757
1787
 
1758
1788
  // src/react/IterationPanel.tsx
1759
1789
  import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1790
+ var PULSE_MS = 2e3;
1791
+ function syncPulse(spread) {
1792
+ return (node) => {
1793
+ if (!node || typeof node.animate !== "function") return;
1794
+ if (node.closest('[data-instant="true"]') || typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches) return;
1795
+ const tint = "color-mix(in srgb, var(--iso-focus) 55%, transparent)";
1796
+ const pulse = node.animate([
1797
+ { boxShadow: `0 0 0 0 ${tint}`, offset: 0 },
1798
+ { boxShadow: `0 0 0 ${spread}px transparent`, offset: 0.7 },
1799
+ { boxShadow: `0 0 0 ${spread}px transparent`, offset: 1 }
1800
+ ], { duration: PULSE_MS, iterations: Infinity, easing: "ease-out" });
1801
+ pulse.startTime = 0;
1802
+ };
1803
+ }
1804
+ var dotPulse = syncPulse(6);
1805
+ var ringPulse = syncPulse(8);
1806
+ var POLL_MS = 2500;
1807
+ var POLL_MAX_MS = 3e4;
1808
+ function sameNotes(a, b) {
1809
+ if (a.length !== b.length) return false;
1810
+ const byId = new Map(a.map((note) => [note.id, note]));
1811
+ return b.every((note) => {
1812
+ const old = byId.get(note.id);
1813
+ return old !== void 0 && JSON.stringify(old) === JSON.stringify(note);
1814
+ });
1815
+ }
1816
+ var WORKING_FRESH_MS = 20 * 60 * 1e3;
1760
1817
  var DEFAULT_SHORTCUT = {
1761
1818
  code: "Period",
1762
1819
  mod: true,
@@ -1786,9 +1843,14 @@ function IterationPanel({
1786
1843
  null
1787
1844
  );
1788
1845
  const [hoverRect, setHoverRect] = useState3(null);
1846
+ const [hintAway, setHintAway] = useState3(false);
1847
+ const hintRef = useRef4(null);
1789
1848
  const [rects, setRects] = useState3([]);
1790
1849
  const [text, setText] = useState3("");
1791
1850
  const [notes, setNotes] = useState3([]);
1851
+ const [scope, setScope] = useState3(null);
1852
+ const [clock, setClock] = useState3(() => Math.floor(Date.now() / 6e4) * 6e4);
1853
+ const pollRev = useRef4(null);
1792
1854
  const noteRevision = useRef4(0);
1793
1855
  const pendingWrites = useRef4(0);
1794
1856
  const noteRequest = useRef4(0);
@@ -1870,7 +1932,25 @@ function IterationPanel({
1870
1932
  if (active === fabRef.current) rootRef.current?.querySelector(".isotone-rail-button")?.focus();
1871
1933
  }, [railOpen, instant]);
1872
1934
  const draftKey = `${nsKey}::draft::${route}`;
1873
- const visibleNotes = useMemo3(() => notes.filter((n) => !n.done).sort((a, b) => a.createdAt.localeCompare(b.createdAt)), [notes]);
1935
+ const openNotes = useMemo3(() => notes.filter((n) => !n.done), [notes]);
1936
+ const pages = useMemo3(() => {
1937
+ const counts = /* @__PURE__ */ new Map();
1938
+ for (const n of openNotes) counts.set(n.route, (counts.get(n.route) ?? 0) + 1);
1939
+ const others = [...counts.keys()].filter((r) => r !== route).sort();
1940
+ return { counts, others };
1941
+ }, [openNotes, route]);
1942
+ const selectedPages = useMemo3(() => {
1943
+ const chosen = (scope ?? [route]).filter((r) => r === route || pages.counts.has(r));
1944
+ return new Set(chosen.length ? chosen : [route]);
1945
+ }, [scope, route, pages]);
1946
+ const visibleNotes = useMemo3(() => openNotes.filter((n) => selectedPages.has(n.route)).sort((a, b) => a.createdAt.localeCompare(b.createdAt)), [openNotes, selectedPages]);
1947
+ const working = useMemo3(() => new Set(visibleNotes.filter((n) => n.workingAt && clock - Date.parse(n.workingAt) < WORKING_FRESH_MS).map((n) => n.id)), [visibleNotes, clock]);
1948
+ function togglePage(page) {
1949
+ const next = new Set(selectedPages);
1950
+ if (next.has(page)) next.delete(page);
1951
+ else next.add(page);
1952
+ setScope(next.size ? [...next] : [route]);
1953
+ }
1874
1954
  const pendingActions = useMemo3(() => notes.flatMap((n) => !n.done && n.route === route && n.variantAction ? [n.variantAction] : []), [notes, route]);
1875
1955
  const shortcutLabel = shortcut ? [
1876
1956
  shortcut.mod ? "\u2318/Ctrl" : "",
@@ -1882,8 +1962,9 @@ function IterationPanel({
1882
1962
  if (!notesAvailable) return;
1883
1963
  const request = ++noteRequest.current;
1884
1964
  const revision = noteRevision.current;
1885
- void store.listNotes().then((fresh) => {
1965
+ void store.pollNotes(null).then((result) => ({ rev: result.rev, notes: result.notes ?? [] })).catch(() => store.pendingNotes().then((fresh) => ({ rev: null, notes: fresh }))).then(({ rev, notes: fresh }) => {
1886
1966
  if (request !== noteRequest.current || revision !== noteRevision.current || pendingWrites.current) return;
1967
+ pollRev.current = rev;
1887
1968
  setNotes(fresh);
1888
1969
  }).catch(() => {
1889
1970
  });
@@ -2050,6 +2131,55 @@ function IterationPanel({
2050
2131
  useEffect3(() => {
2051
2132
  loadNotes();
2052
2133
  }, [notesOpen, variantsOpen, route]);
2134
+ useEffect3(() => {
2135
+ if (!notesAvailable || !visible) return;
2136
+ let stopped = false;
2137
+ let running = false;
2138
+ let failures = 0;
2139
+ let timer;
2140
+ const schedule = () => {
2141
+ clearTimeout(timer);
2142
+ if (stopped || document.hidden) return;
2143
+ timer = setTimeout(() => void tick(), Math.min(POLL_MS * 2 ** failures, POLL_MAX_MS));
2144
+ };
2145
+ const tick = async () => {
2146
+ if (stopped || running || document.hidden) return;
2147
+ running = true;
2148
+ const request = noteRequest.current;
2149
+ const revision = noteRevision.current;
2150
+ try {
2151
+ const result = await store.pollNotes(pollRev.current);
2152
+ failures = 0;
2153
+ if (stopped || request !== noteRequest.current || revision !== noteRevision.current || pendingWrites.current) return;
2154
+ pollRev.current = result.rev;
2155
+ const fresh = result.notes;
2156
+ if (fresh) setNotes((prev) => sameNotes(prev, fresh) ? prev : fresh);
2157
+ setClock(Math.floor(Date.now() / 6e4) * 6e4);
2158
+ } catch {
2159
+ failures = Math.min(failures + 1, 8);
2160
+ } finally {
2161
+ running = false;
2162
+ schedule();
2163
+ }
2164
+ };
2165
+ const wake = () => {
2166
+ if (document.hidden) {
2167
+ clearTimeout(timer);
2168
+ return;
2169
+ }
2170
+ failures = 0;
2171
+ void tick();
2172
+ };
2173
+ schedule();
2174
+ document.addEventListener("visibilitychange", wake);
2175
+ window.addEventListener("focus", wake);
2176
+ return () => {
2177
+ stopped = true;
2178
+ clearTimeout(timer);
2179
+ document.removeEventListener("visibilitychange", wake);
2180
+ window.removeEventListener("focus", wake);
2181
+ };
2182
+ }, [notesAvailable, visible, store]);
2053
2183
  useEffect3(() => {
2054
2184
  if (variants && notesOpen && !variantsOpen) refreshSource(variants);
2055
2185
  }, [notesOpen, variantsOpen, variants]);
@@ -2164,6 +2294,11 @@ function IterationPanel({
2164
2294
  cancelAnimationFrame(frame);
2165
2295
  const deferred = e.shiftKey;
2166
2296
  frame = requestAnimationFrame(() => {
2297
+ const hint = hintRef.current?.getBoundingClientRect();
2298
+ const reach = 48;
2299
+ setHintAway(
2300
+ !!hint && e.clientX > hint.left - reach && e.clientX < hint.right + reach && e.clientY > hint.top - reach && e.clientY < hint.bottom + reach
2301
+ );
2167
2302
  const el = deferred ? null : elementAt(e);
2168
2303
  setHoverRect(el ? el.getBoundingClientRect() : null);
2169
2304
  });
@@ -2208,6 +2343,7 @@ function IterationPanel({
2208
2343
  document.body.style.cursor = cursor;
2209
2344
  cancelAnimationFrame(frame);
2210
2345
  setHoverRect(null);
2346
+ setHintAway(false);
2211
2347
  window.removeEventListener("mousemove", onMove, true);
2212
2348
  window.removeEventListener("click", onClick, true);
2213
2349
  window.removeEventListener("keydown", onKey, true);
@@ -2217,7 +2353,7 @@ function IterationPanel({
2217
2353
  const shadow = rootRef.current?.getRootNode();
2218
2354
  if (!(shadow instanceof ShadowRoot)) return;
2219
2355
  const stop = (e) => e.stopPropagation();
2220
- const types = ["pointerdown", "pointerup", "mousedown", "mouseup", "touchstart", "touchend", "click"];
2356
+ const types = ["pointerdown", "mousedown", "touchstart", "click"];
2221
2357
  for (const type of types) shadow.addEventListener(type, stop);
2222
2358
  return () => {
2223
2359
  for (const type of types) shadow.removeEventListener(type, stop);
@@ -2264,7 +2400,7 @@ function IterationPanel({
2264
2400
  window.removeEventListener("resize", schedule);
2265
2401
  };
2266
2402
  }, [targets, picking]);
2267
- const variantsPanel = variants && /* @__PURE__ */ jsx5(
2403
+ const variantsPanel = variants && /* @__PURE__ */ jsxs4(
2268
2404
  "section",
2269
2405
  {
2270
2406
  id: `${id}-variants`,
@@ -2276,21 +2412,27 @@ function IterationPanel({
2276
2412
  ref: (node) => {
2277
2413
  node?.toggleAttribute("inert", !variantsOpen);
2278
2414
  },
2279
- children: /* @__PURE__ */ jsx5(
2280
- VariantsView,
2281
- {
2282
- source: variants,
2283
- active: variantsOpen,
2284
- onClose: () => {
2285
- setVariantsOpen(false);
2286
- setExpanded(false);
2287
- },
2288
- onFeedback: notesAvailable ? feedback : void 0,
2289
- onSaved: loadNotes,
2290
- onManage: notesAvailable ? queueRemoval : void 0,
2291
- pendingActions
2292
- }
2293
- )
2415
+ children: [
2416
+ /* @__PURE__ */ jsx5(PanelClose, { label: "Close variants", onClose: () => {
2417
+ setVariantsOpen(false);
2418
+ setExpanded(false);
2419
+ } }),
2420
+ /* @__PURE__ */ jsx5(
2421
+ VariantsView,
2422
+ {
2423
+ source: variants,
2424
+ active: variantsOpen,
2425
+ onClose: () => {
2426
+ setVariantsOpen(false);
2427
+ setExpanded(false);
2428
+ },
2429
+ onFeedback: notesAvailable ? feedback : void 0,
2430
+ onSaved: loadNotes,
2431
+ onManage: notesAvailable ? queueRemoval : void 0,
2432
+ pendingActions
2433
+ }
2434
+ )
2435
+ ]
2294
2436
  }
2295
2437
  );
2296
2438
  const notesPanel = notesAvailable && /* @__PURE__ */ jsxs4(
@@ -2306,6 +2448,17 @@ function IterationPanel({
2306
2448
  node?.toggleAttribute("inert", !notesOpen);
2307
2449
  },
2308
2450
  children: [
2451
+ /* @__PURE__ */ jsx5(
2452
+ PanelClose,
2453
+ {
2454
+ label: "Close feedback",
2455
+ onClose: () => {
2456
+ setNotesOpen(false);
2457
+ setExpanded(false);
2458
+ setPicking(false);
2459
+ }
2460
+ }
2461
+ ),
2309
2462
  /* @__PURE__ */ jsxs4("div", { className: "isotone-panel-head", children: [
2310
2463
  /* @__PURE__ */ jsxs4("h2", { children: [
2311
2464
  "Feedback",
@@ -2328,20 +2481,38 @@ function IterationPanel({
2328
2481
  }
2329
2482
  )
2330
2483
  ] }),
2484
+ pages.counts.size > 1 && /* @__PURE__ */ jsx5("fieldset", { className: "isotone-pages", "aria-label": "Pages", children: [route, ...pages.others].map((page) => /* @__PURE__ */ jsxs4(
2485
+ "button",
2486
+ {
2487
+ type: "button",
2488
+ className: "isotone-page",
2489
+ "aria-pressed": selectedPages.has(page),
2490
+ title: page === route ? `This page \xB7 ${page}` : page,
2491
+ onClick: () => togglePage(page),
2492
+ children: [
2493
+ /* @__PURE__ */ jsx5("span", { onPointerOver: showOverflowTitle, children: page === route ? "This page" : page }),
2494
+ /* @__PURE__ */ jsx5("small", { children: pages.counts.get(page) ?? 0 })
2495
+ ]
2496
+ },
2497
+ page
2498
+ )) }),
2331
2499
  /* @__PURE__ */ jsx5("ul", { ref: listRef, className: "isotone-list", children: visibleNotes.map((note) => /* @__PURE__ */ jsxs4(
2332
2500
  "li",
2333
2501
  {
2334
2502
  "data-motion-key": note.id,
2335
2503
  className: `isotone-note${editing?.id === note.id ? " isotone-note-editing" : ""}`,
2336
2504
  children: [
2337
- /* @__PURE__ */ jsx5(
2505
+ /* @__PURE__ */ jsxs4(
2338
2506
  "button",
2339
2507
  {
2340
2508
  type: "button",
2341
2509
  className: "isotone-note-hit",
2342
2510
  "aria-current": editing?.id === note.id ? true : void 0,
2343
2511
  onClick: () => beginEdit(note),
2344
- children: /* @__PURE__ */ jsx5(NoteSummary, { note })
2512
+ children: [
2513
+ 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" }),
2514
+ /* @__PURE__ */ jsx5(NoteSummary, { note, page: note.route !== route ? note.route : void 0 })
2515
+ ]
2345
2516
  }
2346
2517
  ),
2347
2518
  /* @__PURE__ */ jsx5(
@@ -2498,11 +2669,12 @@ function IterationPanel({
2498
2669
  "data-tools": Number(Boolean(variants)) + Number(notesAvailable),
2499
2670
  "data-instant": instant,
2500
2671
  "data-notes-open": notesOpen,
2672
+ "data-working": working.size > 0,
2501
2673
  "data-open": open,
2502
2674
  className: "isotone-root",
2503
2675
  children: [
2504
2676
  /* @__PURE__ */ jsx5("div", { className: "isotone-rail-track", "aria-hidden": "true" }),
2505
- picking && /* @__PURE__ */ jsxs4("div", { className: "isotone-hint", "data-isotone": true, children: [
2677
+ picking && /* @__PURE__ */ jsxs4("div", { ref: hintRef, className: "isotone-hint", "data-isotone": true, "data-away": hintAway, children: [
2506
2678
  /* @__PURE__ */ jsxs4("span", { children: [
2507
2679
  targets.length,
2508
2680
  " selected \xB7 click to add or remove \xB7 hold \u21E7 to use the page"
@@ -2634,7 +2806,7 @@ function IterationPanel({
2634
2806
  "aria-hidden": railOpen,
2635
2807
  tabIndex: railOpen ? -1 : 0,
2636
2808
  "aria-label": notesAvailable ? "Isotone" : "Variants",
2637
- title: `Isotone${shortcutLabel ? ` \xB7 ${shortcutLabel}` : ""}`,
2809
+ title: `Isotone${working.size ? ` \xB7 ${working.size} in progress` : ""}${shortcutLabel ? ` \xB7 ${shortcutLabel}` : ""}`,
2638
2810
  "aria-expanded": railOpen,
2639
2811
  onClick: () => {
2640
2812
  if (open) closeAll();
@@ -2642,6 +2814,7 @@ function IterationPanel({
2642
2814
  else openInitial();
2643
2815
  },
2644
2816
  children: [
2817
+ working.size > 0 && /* @__PURE__ */ jsx5("span", { className: "isotone-ring", ref: ringPulse }),
2645
2818
  /* @__PURE__ */ jsx5(Icon2, { name: "iterate" }),
2646
2819
  visibleNotes.length > 0 && /* @__PURE__ */ jsx5("span", { className: "isotone-badge", children: visibleNotes.length })
2647
2820
  ]
@@ -2654,6 +2827,9 @@ function IterationPanel({
2654
2827
  }
2655
2828
  );
2656
2829
  }
2830
+ function PanelClose({ label, onClose }) {
2831
+ return /* @__PURE__ */ jsx5("button", { type: "button", className: "isotone-panel-close", "aria-label": label, title: label, onClick: onClose, children: /* @__PURE__ */ jsx5(Icon2, { name: "close", size: 12 }) });
2832
+ }
2657
2833
 
2658
2834
  // src/react/IterationMount.tsx
2659
2835
  import { createRoot } from "react-dom/client";