isotone 1.0.5 → 1.0.7

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,
@@ -775,7 +775,7 @@ ${APPEARANCE_CSS}
775
775
  .isotone-root[data-tools='2'] .isotone-rail-button[aria-label='Variants'] { right: calc(var(--iso-rail-end) + var(--iso-rail-step)); }
776
776
  .isotone-root[data-tools='2'] .isotone-rail-button::before { inset: var(--iso-rail-key-inset) calc(var(--iso-rail-gap) / 2); }
777
777
  .isotone-tool-label { display: none; }
778
- .isotone-badge { position: absolute; right: -3px; top: -3px; display: grid; place-items: center; min-width: 15px; height: 15px; padding: 0 3px; border-radius: 8px; background: var(--iso-label-face); box-shadow: var(--iso-label-shadow); color: var(--iso-launcher-ink); font-size: 9px; font-weight: 500; font-variant-numeric: tabular-nums; }
778
+ .isotone-badge { position: absolute; right: -3px; top: -3px; pointer-events: none; display: grid; place-items: center; min-width: 15px; height: 15px; padding: 0 3px; border-radius: 8px; background: var(--iso-label-face); box-shadow: var(--iso-label-shadow); color: var(--iso-launcher-ink); font-size: 9px; font-weight: 500; font-variant-numeric: tabular-nums; }
779
779
  /* Panels are sections inside the dock. Only long content scrolls. */
780
780
  .isotone-panel { order: 0; position: relative; display: none; flex-direction: column; min-height: 0; flex: 0 1 auto; width: 100%; }
781
781
  .isotone-panel[data-open='true'] { display: flex; }
@@ -878,6 +878,17 @@ ${PILL_ACTIVE_CSS}
878
878
  surrounds a 16px face without reserving trailing space in every row. */
879
879
  .isotone-note > .isotone-x { position: absolute; right: -8px; top: -8px; width: 24px; height: 24px; isolation: isolate; opacity: .6; background: none; box-shadow: none; }
880
880
  .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); }
881
+ /* One chip per page that has open notes, this page first. */
882
+ .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); }
883
+ .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; }
884
+ .isotone-page > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
885
+ .isotone-page small { color: var(--iso-faint); font-size: 9px; font-variant-numeric: tabular-nums; }
886
+ .isotone-page[aria-pressed='true'] { color: var(--iso-fg); background: var(--iso-label-face); box-shadow: var(--iso-label-lift-shadow); }
887
+ /* An agent has said it is working on a note: a soft pulse on the note, and a ring on the launcher.
888
+ Both are driven by one shared clock (see syncPulse), so they beat together. */
889
+ .isotone-note-hit { position: relative; }
890
+ .isotone-working { position: absolute; left: 3px; top: 5px; width: 6px; height: 6px; border-radius: 50%; background: var(--iso-focus); }
891
+ .isotone-ring { position: absolute; inset: 0; border-radius: 999px; pointer-events: none; }
881
892
  .isotone-list:empty { display: none; }
882
893
  ${FEEDBACK_LAYOUT_CSS}
883
894
  .isotone-compose { flex: none; display: flex; flex-direction: column; }
@@ -924,6 +935,7 @@ ${FEEDBACK_LAYOUT_CSS}
924
935
  }
925
936
  @media (max-height: 580px) { .isotone-list { --iso-list-height: 72px; } .isotone-root .isotone-textarea { max-height: calc(64px + var(--iso-textarea-inset)); } }
926
937
  @media (prefers-reduced-motion: reduce) {
938
+ .isotone-ring { box-shadow: 0 0 0 2px color-mix(in srgb, var(--iso-focus) 50%, transparent); }
927
939
  .isotone-root .isotone-rail-track { transition: none; }
928
940
  .isotone-root .isotone-panel, .isotone-root .isotone-axis-menu, .isotone-root .isotone-rail-tools { transition-property: opacity, visibility; }
929
941
  .isotone-root button { transition: opacity 125ms ease; }
@@ -1611,10 +1623,11 @@ import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-run
1611
1623
  function SummaryTag({ label }) {
1612
1624
  return /* @__PURE__ */ jsx4("span", { className: "isotone-chip", children: /* @__PURE__ */ jsx4("span", { onPointerOver: showOverflowTitle, children: label }) });
1613
1625
  }
1614
- function NoteSummary({ note }) {
1626
+ function NoteSummary({ note, page }) {
1615
1627
  if (!note.feedback.startsWith("[set-defaults]")) {
1616
1628
  const elements = note.elements ?? (note.element ? [note.element] : []);
1617
1629
  const references = [
1630
+ ...page ? [{ key: `page:${page}`, label: page }] : [],
1618
1631
  ...note.variant ? [{
1619
1632
  key: `variant:${note.variant.axisId}`,
1620
1633
  label: `${note.variant.label}${note.variant.optionLabel ? ` / ${note.variant.optionLabel}` : ""}`
@@ -1646,7 +1659,8 @@ function NoteSummary({ note }) {
1646
1659
  }
1647
1660
  return /* @__PURE__ */ jsxs3(Fragment3, { children: [
1648
1661
  /* @__PURE__ */ jsx4("p", { children: note.feedback }),
1649
- tags.length > 0 && /* @__PURE__ */ jsxs3("span", { className: "isotone-meta", children: [
1662
+ (page || tags.length > 0) && /* @__PURE__ */ jsxs3("span", { className: "isotone-meta", children: [
1663
+ page && /* @__PURE__ */ jsx4(SummaryTag, { label: page }),
1650
1664
  tags.slice(0, 3).map((tag) => /* @__PURE__ */ jsx4(SummaryTag, { label: `${tag.name} ${tag.value}` }, `${tag.name}-${tag.value}`)),
1651
1665
  tags.length > 3 && /* @__PURE__ */ jsxs3("span", { className: "isotone-request-more", children: [
1652
1666
  "+",
@@ -1757,6 +1771,33 @@ function useDockMotion(ref, visible) {
1757
1771
 
1758
1772
  // src/react/IterationPanel.tsx
1759
1773
  import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1774
+ var PULSE_MS = 2e3;
1775
+ function syncPulse(spread) {
1776
+ return (node) => {
1777
+ if (!node || typeof node.animate !== "function") return;
1778
+ if (node.closest('[data-instant="true"]') || typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches) return;
1779
+ const tint = "color-mix(in srgb, var(--iso-focus) 55%, transparent)";
1780
+ const pulse = node.animate([
1781
+ { boxShadow: `0 0 0 0 ${tint}`, offset: 0 },
1782
+ { boxShadow: `0 0 0 ${spread}px transparent`, offset: 0.7 },
1783
+ { boxShadow: `0 0 0 ${spread}px transparent`, offset: 1 }
1784
+ ], { duration: PULSE_MS, iterations: Infinity, easing: "ease-out" });
1785
+ pulse.startTime = 0;
1786
+ };
1787
+ }
1788
+ var dotPulse = syncPulse(6);
1789
+ var ringPulse = syncPulse(8);
1790
+ var POLL_MS = 2500;
1791
+ var POLL_MAX_MS = 3e4;
1792
+ function sameNotes(a, b) {
1793
+ if (a.length !== b.length) return false;
1794
+ const byId = new Map(a.map((note) => [note.id, note]));
1795
+ return b.every((note) => {
1796
+ const old = byId.get(note.id);
1797
+ return old !== void 0 && JSON.stringify(old) === JSON.stringify(note);
1798
+ });
1799
+ }
1800
+ var WORKING_FRESH_MS = 20 * 60 * 1e3;
1760
1801
  var DEFAULT_SHORTCUT = {
1761
1802
  code: "Period",
1762
1803
  mod: true,
@@ -1789,6 +1830,9 @@ function IterationPanel({
1789
1830
  const [rects, setRects] = useState3([]);
1790
1831
  const [text, setText] = useState3("");
1791
1832
  const [notes, setNotes] = useState3([]);
1833
+ const [scope, setScope] = useState3(null);
1834
+ const [clock, setClock] = useState3(() => Math.floor(Date.now() / 6e4) * 6e4);
1835
+ const pollRev = useRef4(null);
1792
1836
  const noteRevision = useRef4(0);
1793
1837
  const pendingWrites = useRef4(0);
1794
1838
  const noteRequest = useRef4(0);
@@ -1870,7 +1914,25 @@ function IterationPanel({
1870
1914
  if (active === fabRef.current) rootRef.current?.querySelector(".isotone-rail-button")?.focus();
1871
1915
  }, [railOpen, instant]);
1872
1916
  const draftKey = `${nsKey}::draft::${route}`;
1873
- const visibleNotes = useMemo3(() => notes.filter((n) => !n.done).sort((a, b) => a.createdAt.localeCompare(b.createdAt)), [notes]);
1917
+ const openNotes = useMemo3(() => notes.filter((n) => !n.done), [notes]);
1918
+ const pages = useMemo3(() => {
1919
+ const counts = /* @__PURE__ */ new Map();
1920
+ for (const n of openNotes) counts.set(n.route, (counts.get(n.route) ?? 0) + 1);
1921
+ const others = [...counts.keys()].filter((r) => r !== route).sort();
1922
+ return { counts, others };
1923
+ }, [openNotes, route]);
1924
+ const selectedPages = useMemo3(() => {
1925
+ const chosen = (scope ?? [route]).filter((r) => r === route || pages.counts.has(r));
1926
+ return new Set(chosen.length ? chosen : [route]);
1927
+ }, [scope, route, pages]);
1928
+ const visibleNotes = useMemo3(() => openNotes.filter((n) => selectedPages.has(n.route)).sort((a, b) => a.createdAt.localeCompare(b.createdAt)), [openNotes, selectedPages]);
1929
+ const working = useMemo3(() => new Set(visibleNotes.filter((n) => n.workingAt && clock - Date.parse(n.workingAt) < WORKING_FRESH_MS).map((n) => n.id)), [visibleNotes, clock]);
1930
+ function togglePage(page) {
1931
+ const next = new Set(selectedPages);
1932
+ if (next.has(page)) next.delete(page);
1933
+ else next.add(page);
1934
+ setScope(next.size ? [...next] : [route]);
1935
+ }
1874
1936
  const pendingActions = useMemo3(() => notes.flatMap((n) => !n.done && n.route === route && n.variantAction ? [n.variantAction] : []), [notes, route]);
1875
1937
  const shortcutLabel = shortcut ? [
1876
1938
  shortcut.mod ? "\u2318/Ctrl" : "",
@@ -1882,8 +1944,9 @@ function IterationPanel({
1882
1944
  if (!notesAvailable) return;
1883
1945
  const request = ++noteRequest.current;
1884
1946
  const revision = noteRevision.current;
1885
- void store.listNotes().then((fresh) => {
1947
+ 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
1948
  if (request !== noteRequest.current || revision !== noteRevision.current || pendingWrites.current) return;
1949
+ pollRev.current = rev;
1887
1950
  setNotes(fresh);
1888
1951
  }).catch(() => {
1889
1952
  });
@@ -2050,6 +2113,55 @@ function IterationPanel({
2050
2113
  useEffect3(() => {
2051
2114
  loadNotes();
2052
2115
  }, [notesOpen, variantsOpen, route]);
2116
+ useEffect3(() => {
2117
+ if (!notesAvailable || !visible) return;
2118
+ let stopped = false;
2119
+ let running = false;
2120
+ let failures = 0;
2121
+ let timer;
2122
+ const schedule = () => {
2123
+ clearTimeout(timer);
2124
+ if (stopped || document.hidden) return;
2125
+ timer = setTimeout(() => void tick(), Math.min(POLL_MS * 2 ** failures, POLL_MAX_MS));
2126
+ };
2127
+ const tick = async () => {
2128
+ if (stopped || running || document.hidden) return;
2129
+ running = true;
2130
+ const request = noteRequest.current;
2131
+ const revision = noteRevision.current;
2132
+ try {
2133
+ const result = await store.pollNotes(pollRev.current);
2134
+ failures = 0;
2135
+ if (stopped || request !== noteRequest.current || revision !== noteRevision.current || pendingWrites.current) return;
2136
+ pollRev.current = result.rev;
2137
+ const fresh = result.notes;
2138
+ if (fresh) setNotes((prev) => sameNotes(prev, fresh) ? prev : fresh);
2139
+ setClock(Math.floor(Date.now() / 6e4) * 6e4);
2140
+ } catch {
2141
+ failures = Math.min(failures + 1, 8);
2142
+ } finally {
2143
+ running = false;
2144
+ schedule();
2145
+ }
2146
+ };
2147
+ const wake = () => {
2148
+ if (document.hidden) {
2149
+ clearTimeout(timer);
2150
+ return;
2151
+ }
2152
+ failures = 0;
2153
+ void tick();
2154
+ };
2155
+ schedule();
2156
+ document.addEventListener("visibilitychange", wake);
2157
+ window.addEventListener("focus", wake);
2158
+ return () => {
2159
+ stopped = true;
2160
+ clearTimeout(timer);
2161
+ document.removeEventListener("visibilitychange", wake);
2162
+ window.removeEventListener("focus", wake);
2163
+ };
2164
+ }, [notesAvailable, visible, store]);
2053
2165
  useEffect3(() => {
2054
2166
  if (variants && notesOpen && !variantsOpen) refreshSource(variants);
2055
2167
  }, [notesOpen, variantsOpen, variants]);
@@ -2217,7 +2329,7 @@ function IterationPanel({
2217
2329
  const shadow = rootRef.current?.getRootNode();
2218
2330
  if (!(shadow instanceof ShadowRoot)) return;
2219
2331
  const stop = (e) => e.stopPropagation();
2220
- const types = ["pointerdown", "pointerup", "mousedown", "mouseup", "touchstart", "touchend", "click"];
2332
+ const types = ["pointerdown", "mousedown", "touchstart", "click"];
2221
2333
  for (const type of types) shadow.addEventListener(type, stop);
2222
2334
  return () => {
2223
2335
  for (const type of types) shadow.removeEventListener(type, stop);
@@ -2328,20 +2440,38 @@ function IterationPanel({
2328
2440
  }
2329
2441
  )
2330
2442
  ] }),
2443
+ pages.counts.size > 1 && /* @__PURE__ */ jsx5("fieldset", { className: "isotone-pages", "aria-label": "Pages", children: [route, ...pages.others].map((page) => /* @__PURE__ */ jsxs4(
2444
+ "button",
2445
+ {
2446
+ type: "button",
2447
+ className: "isotone-page",
2448
+ "aria-pressed": selectedPages.has(page),
2449
+ title: page === route ? `This page \xB7 ${page}` : page,
2450
+ onClick: () => togglePage(page),
2451
+ children: [
2452
+ /* @__PURE__ */ jsx5("span", { onPointerOver: showOverflowTitle, children: page === route ? "This page" : page }),
2453
+ /* @__PURE__ */ jsx5("small", { children: pages.counts.get(page) ?? 0 })
2454
+ ]
2455
+ },
2456
+ page
2457
+ )) }),
2331
2458
  /* @__PURE__ */ jsx5("ul", { ref: listRef, className: "isotone-list", children: visibleNotes.map((note) => /* @__PURE__ */ jsxs4(
2332
2459
  "li",
2333
2460
  {
2334
2461
  "data-motion-key": note.id,
2335
2462
  className: `isotone-note${editing?.id === note.id ? " isotone-note-editing" : ""}`,
2336
2463
  children: [
2337
- /* @__PURE__ */ jsx5(
2464
+ /* @__PURE__ */ jsxs4(
2338
2465
  "button",
2339
2466
  {
2340
2467
  type: "button",
2341
2468
  className: "isotone-note-hit",
2342
2469
  "aria-current": editing?.id === note.id ? true : void 0,
2343
2470
  onClick: () => beginEdit(note),
2344
- children: /* @__PURE__ */ jsx5(NoteSummary, { note })
2471
+ children: [
2472
+ 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" }),
2473
+ /* @__PURE__ */ jsx5(NoteSummary, { note, page: note.route !== route ? note.route : void 0 })
2474
+ ]
2345
2475
  }
2346
2476
  ),
2347
2477
  /* @__PURE__ */ jsx5(
@@ -2498,6 +2628,7 @@ function IterationPanel({
2498
2628
  "data-tools": Number(Boolean(variants)) + Number(notesAvailable),
2499
2629
  "data-instant": instant,
2500
2630
  "data-notes-open": notesOpen,
2631
+ "data-working": working.size > 0,
2501
2632
  "data-open": open,
2502
2633
  className: "isotone-root",
2503
2634
  children: [
@@ -2634,7 +2765,7 @@ function IterationPanel({
2634
2765
  "aria-hidden": railOpen,
2635
2766
  tabIndex: railOpen ? -1 : 0,
2636
2767
  "aria-label": notesAvailable ? "Isotone" : "Variants",
2637
- title: `Isotone${shortcutLabel ? ` \xB7 ${shortcutLabel}` : ""}`,
2768
+ title: `Isotone${working.size ? ` \xB7 ${working.size} in progress` : ""}${shortcutLabel ? ` \xB7 ${shortcutLabel}` : ""}`,
2638
2769
  "aria-expanded": railOpen,
2639
2770
  onClick: () => {
2640
2771
  if (open) closeAll();
@@ -2642,6 +2773,7 @@ function IterationPanel({
2642
2773
  else openInitial();
2643
2774
  },
2644
2775
  children: [
2776
+ working.size > 0 && /* @__PURE__ */ jsx5("span", { className: "isotone-ring", ref: ringPulse }),
2645
2777
  /* @__PURE__ */ jsx5(Icon2, { name: "iterate" }),
2646
2778
  visibleNotes.length > 0 && /* @__PURE__ */ jsx5("span", { className: "isotone-badge", children: visibleNotes.length })
2647
2779
  ]