isotone 1.0.1 → 1.0.3

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
@@ -45,7 +45,7 @@ function showOverflowTitle(event) {
45
45
  }
46
46
 
47
47
  // src/react/IterationPanel.tsx
48
- import { useCallback, useEffect as useEffect3, useId as useId2, useLayoutEffect as useLayoutEffect3, useMemo as useMemo3, useRef as useRef4, useState as useState3 } from "react";
48
+ import { useCallback, useEffect as useEffect3, useId as useId2, useLayoutEffect as useLayoutEffect4, useMemo as useMemo3, useRef as useRef4, useState as useState3 } from "react";
49
49
 
50
50
  // src/react/appearance.ts
51
51
  var darkPalette = {
@@ -788,14 +788,14 @@ ${APPEARANCE_CSS}
788
788
  .isotone-root .isotone-vars > .isotone-panel-head:has(.isotone-save) { position: relative; z-index: 1; margin-top: calc(0px - var(--iso-space)); background: linear-gradient(to bottom, transparent, var(--iso-bg) var(--iso-space)); }
789
789
  .isotone-root .isotone-vars > .isotone-panel-head:not(:has(.isotone-save)) { display: none; }
790
790
  .isotone-root .isotone-vars > .isotone-panel-head :is(h2, .isotone-icon) { display: none; }
791
- .isotone-root .isotone-axis-menu { top: 6px; min-height: min(260px, calc(100% - 12px)); max-height: calc(100% - 12px); }
791
+ .isotone-root .isotone-axis-menu { top: var(--iso-menu-top, 6px); max-height: calc(100% - var(--iso-menu-top, 6px) - 6px); }
792
792
  .isotone-root .isotone-save-state { top: auto; bottom: 27px; }
793
793
  .isotone-panel-head h2 { font-size: 12px; font-weight: 600; letter-spacing: -.01em; }
794
794
  .isotone-head-actions { display: flex; align-items: center; gap: 6px; margin-right: auto; }
795
795
  .isotone-icon { width: 28px; height: 28px; display: inline-grid; place-items: center; flex: none; color: var(--iso-muted); border-radius: 6px; }
796
796
  .isotone-count { margin-left: 5px; color: var(--iso-muted); font-size: 10px; font-variant-numeric: tabular-nums; }
797
797
  .isotone-vars { flex: 1; display: flex; flex-direction: column; min-height: 0; position: relative; }
798
- .isotone-variants-panel:has(.isotone-axis-menu[data-open='true']) { flex-basis: min(360px, calc(100dvh - 76px)); }
798
+ .isotone-vars { min-height: min(var(--iso-menu-need, 0px), calc(100dvh - 76px)); }
799
799
  .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); }
800
800
  /* The footer overlaps only the scroller's bottom padding: content fades beneath
801
801
  it while scrolling, but the last control remains fully visible at the end. */
@@ -863,7 +863,7 @@ ${PILL_ACTIVE_CSS}
863
863
  Cancelling margin/padding keeps the first row in place at scroll start; the
864
864
  expanded height preserves the content allowance and composer position.
865
865
  The side gutters contain the corner action and keyboard focus outlines. */
866
- .isotone-list { display: flex; flex-direction: column; gap: 0; flex: 0 1 auto; max-height: calc(var(--iso-list-height) + 2 * var(--iso-space) - 8px); min-height: 0; overflow-y: auto; overscroll-behavior: contain; margin: calc(0px - var(--iso-space)) calc(-8px - var(--iso-note-outset)); padding: var(--iso-space) 8px; scroll-padding-block: var(--iso-space); mask-image: linear-gradient(to bottom, transparent, #000 var(--iso-space), #000 calc(100% - var(--iso-space)), transparent); }
866
+ .isotone-list { display: flex; flex-direction: column; gap: 0; flex: 0 1 auto; max-height: calc(var(--iso-list-height) + 2 * var(--iso-space) - 8px); min-height: 0; overflow-y: auto; overscroll-behavior: contain; margin: calc(0px - var(--iso-space)) calc(-8px - var(--iso-note-outset)) calc(-2px - var(--iso-space)); padding: var(--iso-space) 8px; scroll-padding-block: var(--iso-space); mask-image: linear-gradient(to bottom, transparent, #000 var(--iso-space), #000 calc(100% - var(--iso-space)), transparent); }
867
867
  .isotone-empty { color: var(--iso-muted); font-size: 11px; line-height: 1.7; padding: 8px; }
868
868
  .isotone-note { position: relative; flex: none; }
869
869
  .isotone-request-heading { display: flex; align-items: center; gap: 8px; line-height: 14px; }
@@ -895,14 +895,13 @@ ${FEEDBACK_LAYOUT_CSS}
895
895
  .isotone-context > .isotone-chip:not(:only-child) { max-width: calc(100% - 32px); }
896
896
  .isotone-chip > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
897
897
  .isotone-chip button { display: grid; place-items: center; flex: none; width: 16px; height: 16px; border-radius: 3px; background: transparent; box-shadow: none; }
898
- .isotone-root .isotone-textarea { --iso-textarea-inset: 8px; display: block; width: 100%; flex: none; height: 24px; min-height: calc(24px + var(--iso-textarea-inset)); max-height: calc(120px + var(--iso-textarea-inset)); overflow-y: auto; resize: none; margin-top: calc(0px - var(--iso-textarea-inset)); padding: calc(var(--iso-textarea-inset) + 2px) 2px 2px; color: var(--iso-fg); background: transparent; border: 0; border-radius: 0; font-size: 12px; line-height: 20px; box-shadow: none; cursor: text; }
898
+ .isotone-root .isotone-textarea { --iso-textarea-inset: 8px; display: block; width: 100%; flex: none; height: 22px; min-height: calc(22px + var(--iso-textarea-inset)); max-height: calc(120px + var(--iso-textarea-inset)); overflow-y: auto; resize: none; margin-top: calc(0px - var(--iso-textarea-inset)); padding: calc(var(--iso-textarea-inset) + 1px) 1px 1px; scrollbar-width: thin; scrollbar-color: color-mix(in srgb, var(--iso-fg) 14%, transparent) transparent; color: var(--iso-fg); background: transparent; border: 0; border-radius: 0; font-size: 12px; line-height: 20px; box-shadow: none; cursor: text; }
899
899
  /* Text scrolls through the gap below tags, or the field inset without tags. */
900
900
  .isotone-root .isotone-context:empty + .isotone-textarea { --iso-textarea-inset: var(--iso-input-padding); }
901
901
  .isotone-textarea::placeholder { color: var(--iso-faint); }
902
902
  .isotone-root .isotone-textarea:focus-visible { outline: none; }
903
903
  .isotone-textarea[data-scrolled='true'] { mask-image: linear-gradient(to bottom, transparent, #000 8px); }
904
904
  .isotone-toolbar { display: flex; flex: none; align-items: center; gap: 7px; height: 28px; }
905
- .isotone-key-hint { color: var(--iso-faint); font-size: 9px; }
906
905
  .isotone-pick[aria-pressed='true'] { color: var(--iso-fg); }
907
906
  .isotone-pick, .isotone-send { position: relative; width: 24px; height: 24px; }
908
907
  /* Keep a 28px hit area around the quieter composer faces. */
@@ -949,7 +948,7 @@ function ensurePanelStyles(target) {
949
948
  }
950
949
 
951
950
  // src/react/VariantsView.tsx
952
- import { useEffect as useEffect2, useMemo as useMemo2, useRef as useRef3, useState as useState2 } from "react";
951
+ import { useEffect as useEffect2, useLayoutEffect as useLayoutEffect2, useMemo as useMemo2, useRef as useRef3, useState as useState2 } from "react";
953
952
 
954
953
  // src/react/useLayoutMotion.ts
955
954
  import { useLayoutEffect, useRef as useRef2 } from "react";
@@ -1084,6 +1083,45 @@ function VariantsView({
1084
1083
  useEffect2(() => {
1085
1084
  if (!menu) setMenuOpen(false);
1086
1085
  }, [menu]);
1086
+ useLayoutEffect2(() => {
1087
+ const root = bodyRef.current?.parentElement;
1088
+ const dialog = menuRef.current;
1089
+ const scroll = dialog?.querySelector(".isotone-menu-scroll");
1090
+ if (!root) return;
1091
+ const clear = () => {
1092
+ root.style.removeProperty("--iso-menu-need");
1093
+ root.style.removeProperty("--iso-menu-top");
1094
+ };
1095
+ if (!menuOpen || !dialog || !scroll) {
1096
+ clear();
1097
+ return;
1098
+ }
1099
+ const place = () => {
1100
+ const natural = dialog.offsetHeight + scroll.scrollHeight - scroll.clientHeight;
1101
+ root.style.setProperty("--iso-menu-need", `${natural + 12}px`);
1102
+ const heading = trigger.current?.isConnected ? trigger.current.getBoundingClientRect().top : 0;
1103
+ const top = heading - root.getBoundingClientRect().top - natural - 6;
1104
+ root.style.setProperty("--iso-menu-top", `${Number.isFinite(top) && top >= 6 ? top : 6}px`);
1105
+ };
1106
+ place();
1107
+ window.addEventListener("resize", place);
1108
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(place);
1109
+ observer?.observe(scroll);
1110
+ return () => {
1111
+ window.removeEventListener("resize", place);
1112
+ observer?.disconnect();
1113
+ clear();
1114
+ };
1115
+ }, [menuOpen, menu, menuOptions.length, error]);
1116
+ useEffect2(() => {
1117
+ if (!menuOpen) return;
1118
+ const onPointerDown = (e) => {
1119
+ const dialog = menuRef.current;
1120
+ if (dialog && !e.composedPath().includes(dialog)) closeMenu();
1121
+ };
1122
+ window.addEventListener("pointerdown", onPointerDown, true);
1123
+ return () => window.removeEventListener("pointerdown", onPointerDown, true);
1124
+ }, [menuOpen]);
1087
1125
  useEffect2(() => {
1088
1126
  if (menuOpen)
1089
1127
  menuRef.current?.querySelector("button")?.focus();
@@ -1145,7 +1183,7 @@ function VariantsView({
1145
1183
  node?.toggleAttribute("inert", menuOpen);
1146
1184
  }, children: [
1147
1185
  sourceError && /* @__PURE__ */ jsx3("p", { className: "isotone-danger", role: "alert", children: sourceError }),
1148
- shown.length === 0 && /* @__PURE__ */ jsx3("p", { className: "isotone-empty", children: "No variant selectors on this page." }),
1186
+ 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." }),
1149
1187
  shown.map((axis) => {
1150
1188
  const thumbnails = axis.kind !== "range" && axis.kind !== "color" && axis.options.length > 0 && axis.options.every((option) => option.image);
1151
1189
  return /* @__PURE__ */ jsxs2(
@@ -1598,9 +1636,9 @@ function NoteSummary({ note }) {
1598
1636
  }
1599
1637
 
1600
1638
  // src/react/useDockMotion.ts
1601
- import { useLayoutEffect as useLayoutEffect2 } from "react";
1639
+ import { useLayoutEffect as useLayoutEffect3 } from "react";
1602
1640
  function useDockMotion(ref, visible) {
1603
- useLayoutEffect2(() => {
1641
+ useLayoutEffect3(() => {
1604
1642
  const root = ref.current;
1605
1643
  if (!root || !visible) return;
1606
1644
  const previous = /* @__PURE__ */ new Map();
@@ -1734,7 +1772,6 @@ function IterationPanel({
1734
1772
  const pendingWrites = useRef4(0);
1735
1773
  const noteRequest = useRef4(0);
1736
1774
  const [editing, setEditing] = useState3(null);
1737
- const [unsynced, setUnsynced] = useState3(false);
1738
1775
  const [notice, setNotice] = useState3("");
1739
1776
  const [theme, setTheme] = useState3(() => detectTheme(themePreference));
1740
1777
  const [instant, setInstant] = useState3(false);
@@ -1755,13 +1792,13 @@ function IterationPanel({
1755
1792
  variants && visibleAxes(variants.getAxes(), variants.getSelections()).length
1756
1793
  );
1757
1794
  const visible = isRouteVisible(route, rule) && (notesAvailable || variantsAvailable || variantsOpen);
1758
- const hasContext = Boolean(editing || variantTarget || targets.length || notice || unsynced);
1759
- useLayoutEffect3(() => {
1795
+ const hasContext = Boolean(editing || variantTarget || targets.length || notice);
1796
+ useLayoutEffect4(() => {
1760
1797
  const field = textareaRef.current;
1761
1798
  if (!field || !notesOpen || !visible) return;
1762
1799
  const resize = () => {
1763
1800
  field.style.height = "0px";
1764
- field.style.height = `${text ? Math.max(24, field.scrollHeight) : 24}px`;
1801
+ field.style.height = `${text ? Math.max(22, field.scrollHeight) : 22}px`;
1765
1802
  field.dataset.scrolled = String(field.scrollTop > 0);
1766
1803
  };
1767
1804
  resize();
@@ -1827,7 +1864,6 @@ function IterationPanel({
1827
1864
  void store.listNotes().then((fresh) => {
1828
1865
  if (request !== noteRequest.current || revision !== noteRevision.current || pendingWrites.current) return;
1829
1866
  setNotes(fresh);
1830
- setUnsynced(store.hasUnsyncedNotes);
1831
1867
  }).catch(() => {
1832
1868
  });
1833
1869
  }
@@ -1839,6 +1875,15 @@ function IterationPanel({
1839
1875
  pendingWrites.current--;
1840
1876
  };
1841
1877
  }
1878
+ const keepPicking = useRef4(false);
1879
+ const focusWhilePicking = useCallback(() => {
1880
+ cancelAnimationFrame(focusFrame.current);
1881
+ focusFrame.current = requestAnimationFrame(() => {
1882
+ keepPicking.current = true;
1883
+ textareaRef.current?.focus();
1884
+ keepPicking.current = false;
1885
+ });
1886
+ }, []);
1842
1887
  const focusComposer = useCallback(() => {
1843
1888
  cancelAnimationFrame(focusFrame.current);
1844
1889
  focusFrame.current = requestAnimationFrame(
@@ -1894,7 +1939,6 @@ function IterationPanel({
1894
1939
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1895
1940
  ...context
1896
1941
  }]);
1897
- setUnsynced(store.hasUnsyncedNotes);
1898
1942
  setNotesOpen(true);
1899
1943
  } finally {
1900
1944
  finish();
@@ -1949,7 +1993,6 @@ function IterationPanel({
1949
1993
  setNotes(
1950
1994
  (prev) => prev.map((n) => n.id === tempId ? { ...n, id: savedId } : n)
1951
1995
  );
1952
- setUnsynced(store.hasUnsyncedNotes);
1953
1996
  } catch {
1954
1997
  setNotes(
1955
1998
  (prev) => previous ? prev.map((n) => n.id === tempId ? previous : n) : prev.filter((n) => n.id !== tempId)
@@ -2038,7 +2081,7 @@ function IterationPanel({
2038
2081
  const el = listRef.current;
2039
2082
  if (el) el.scrollTop = el.scrollHeight;
2040
2083
  }, [notes, notesOpen]);
2041
- useLayoutEffect3(() => {
2084
+ useLayoutEffect4(() => {
2042
2085
  if (!visible) return;
2043
2086
  const root = rootRef.current?.getRootNode();
2044
2087
  if (root instanceof ShadowRoot || root instanceof Document)
@@ -2098,8 +2141,9 @@ function IterationPanel({
2098
2141
  };
2099
2142
  const onMove = (e) => {
2100
2143
  cancelAnimationFrame(frame);
2144
+ const deferred = e.shiftKey;
2101
2145
  frame = requestAnimationFrame(() => {
2102
- const el = elementAt(e);
2146
+ const el = deferred ? null : elementAt(e);
2103
2147
  setHoverRect(el ? el.getBoundingClientRect() : null);
2104
2148
  });
2105
2149
  };
@@ -2110,11 +2154,13 @@ function IterationPanel({
2110
2154
  );
2111
2155
  };
2112
2156
  const onClick = (e) => {
2157
+ if (e.shiftKey) return;
2113
2158
  const el = elementAt(e);
2114
2159
  if (!el) return;
2115
2160
  e.preventDefault();
2116
2161
  e.stopImmediatePropagation();
2117
2162
  toggleTarget(el);
2163
+ focusWhilePicking();
2118
2164
  };
2119
2165
  const onKey = (e) => {
2120
2166
  if (e.key === " ") {
@@ -2145,7 +2191,17 @@ function IterationPanel({
2145
2191
  window.removeEventListener("click", onClick, true);
2146
2192
  window.removeEventListener("keydown", onKey, true);
2147
2193
  };
2148
- }, [picking, focusComposer]);
2194
+ }, [picking, focusComposer, focusWhilePicking]);
2195
+ useEffect3(() => {
2196
+ const shadow = rootRef.current?.getRootNode();
2197
+ if (!(shadow instanceof ShadowRoot)) return;
2198
+ const stop = (e) => e.stopPropagation();
2199
+ const types = ["pointerdown", "pointerup", "mousedown", "mouseup", "touchstart", "touchend", "click"];
2200
+ for (const type of types) shadow.addEventListener(type, stop);
2201
+ return () => {
2202
+ for (const type of types) shadow.removeEventListener(type, stop);
2203
+ };
2204
+ }, []);
2149
2205
  useEffect3(() => {
2150
2206
  if (!picking) {
2151
2207
  setRects([]);
@@ -2347,7 +2403,7 @@ function IterationPanel({
2347
2403
  },
2348
2404
  target.selector
2349
2405
  )),
2350
- (notice || unsynced) && /* @__PURE__ */ jsx5("p", { role: "status", className: "isotone-notice", children: notice || "Saved in this browser. It will sync when the dev server returns." })
2406
+ notice && /* @__PURE__ */ jsx5("p", { role: "status", className: "isotone-notice", children: notice })
2351
2407
  ] }),
2352
2408
  /* @__PURE__ */ jsx5(
2353
2409
  "textarea",
@@ -2356,7 +2412,10 @@ function IterationPanel({
2356
2412
  value: text,
2357
2413
  "aria-label": "Feedback for the agent",
2358
2414
  onChange: (e) => setText(e.target.value),
2359
- onFocus: () => setPicking(false),
2415
+ onFocus: () => {
2416
+ if (!keepPicking.current) setPicking(false);
2417
+ },
2418
+ onPointerDown: () => setPicking(false),
2360
2419
  onScroll: (event) => {
2361
2420
  event.currentTarget.dataset.scrolled = String(event.currentTarget.scrollTop > 0);
2362
2421
  },
@@ -2380,6 +2439,7 @@ function IterationPanel({
2380
2439
  "aria-label": "Select elements",
2381
2440
  "aria-pressed": picking,
2382
2441
  className: "isotone-pick isotone-icon",
2442
+ onMouseDown: (e) => e.preventDefault(),
2383
2443
  onClick: () => {
2384
2444
  cancelAnimationFrame(focusFrame.current);
2385
2445
  setPicking((v) => !v);
@@ -2387,7 +2447,6 @@ function IterationPanel({
2387
2447
  children: /* @__PURE__ */ jsx5(Icon2, { name: "pin", size: 14 })
2388
2448
  }
2389
2449
  ),
2390
- picking && /* @__PURE__ */ jsx5("span", { className: "isotone-key-hint", children: "Click or Space to pin \xB7 Enter to finish" }),
2391
2450
  /* @__PURE__ */ jsx5(
2392
2451
  "button",
2393
2452
  {
@@ -2425,7 +2484,7 @@ function IterationPanel({
2425
2484
  picking && /* @__PURE__ */ jsxs4("div", { className: "isotone-hint", "data-isotone": true, children: [
2426
2485
  /* @__PURE__ */ jsxs4("span", { children: [
2427
2486
  targets.length,
2428
- " selected \xB7 click to add or remove"
2487
+ " selected \xB7 click to add or remove \xB7 hold \u21E7 to use the page"
2429
2488
  ] }),
2430
2489
  /* @__PURE__ */ jsxs4(
2431
2490
  "button",