@nuniapp/widget 0.1.7 → 0.1.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.d.ts CHANGED
@@ -30,6 +30,8 @@ declare const EN: {
30
30
  noOpenKey: string;
31
31
  noResolved: string;
32
32
  notFoundHere: string;
33
+ searchComments: string;
34
+ noMatches: string;
33
35
  otherPages: string;
34
36
  openOnPage: string;
35
37
  replyCount: {
@@ -105,7 +107,7 @@ type MessageKey = keyof typeof EN;
105
107
  type Messages = { [K in MessageKey]: string | Plural; };
106
108
  //#endregion
107
109
  //#region src/config.d.ts
108
- type Position = "bottom-right" | "bottom-left" | "top-right" | "top-left";
110
+ type Position = "bottom-right" | "bottom-center" | "bottom-left" | "top-right" | "top-center" | "top-left";
109
111
  type Theme = "auto" | "light" | "dark";
110
112
  /** Built-in page keys, for when `getPageKey` is not set. */
111
113
  type PageKey = "path" | "path+search" | "path+hash";
@@ -125,7 +127,10 @@ interface NuniOptions {
125
127
  * the path (default), the path and query string, or the path and hash.
126
128
  */
127
129
  pageKey?: PageKey;
128
- /** Corner for the toolbar. Default "bottom-right". */
130
+ /**
131
+ * Where the toolbar starts. Default "bottom-right". Visitors can drag it to
132
+ * another corner or edge middle, and their choice is remembered.
133
+ */
129
134
  position?: Position;
130
135
  /** Brand color for buttons, pins and highlights (any CSS color). */
131
136
  accentColor?: string;
package/dist/index.js CHANGED
@@ -165,7 +165,7 @@ function buildCommentPrompt(comment, options = {}) {
165
165
  }
166
166
  //#endregion
167
167
  //#region src/config.ts
168
- const VERSION = "0.1.7";
168
+ const VERSION = "0.1.8";
169
169
  function resolveConfig(options) {
170
170
  return {
171
171
  project: options.project,
@@ -191,8 +191,10 @@ function resolveConfig(options) {
191
191
  }
192
192
  const POSITIONS = [
193
193
  "bottom-right",
194
+ "bottom-center",
194
195
  "bottom-left",
195
196
  "top-right",
197
+ "top-center",
196
198
  "top-left"
197
199
  ];
198
200
  const THEMES = [
@@ -5555,6 +5557,8 @@ const EN = {
5555
5557
  noOpenKey: "No open comments on this page. Press {key} to add one.",
5556
5558
  noResolved: "Nothing resolved yet.",
5557
5559
  notFoundHere: "Couldn't find on this page",
5560
+ searchComments: "Search comments",
5561
+ noMatches: "No comments match “{query}”.",
5558
5562
  otherPages: "Other pages",
5559
5563
  openOnPage: "{count} open",
5560
5564
  replyCount: {
@@ -5690,6 +5694,7 @@ const ICONS = {
5690
5694
  smile: s("<circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M8 14s1.5 2 4 2 4-2 4-2M9 9h.01M15 9h.01\"/>"),
5691
5695
  user: s("<circle cx=\"12\" cy=\"8\" r=\"4\"/><path d=\"M4 21a8 8 0 0 1 16 0\"/>"),
5692
5696
  arrowUp: s("<path d=\"M12 19V5M5 12l7-7 7 7\"/>"),
5697
+ search: s("<circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m21 21-4.3-4.3\"/>"),
5693
5698
  target: s("<circle cx=\"12\" cy=\"12\" r=\"9\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/>"),
5694
5699
  github: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M12 .5a11.5 11.5 0 0 0-3.6 22.4c.6.1.8-.3.8-.6v-2c-3.2.7-3.9-1.5-3.9-1.5-.5-1.3-1.3-1.7-1.3-1.7-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.7-1.6-2.6-.3-5.3-1.3-5.3-5.7 0-1.3.5-2.3 1.2-3.1-.1-.3-.5-1.5.1-3.1 0 0 1-.3 3.2 1.2a11 11 0 0 1 5.8 0c2.2-1.5 3.2-1.2 3.2-1.2.6 1.6.2 2.8.1 3.1.8.8 1.2 1.9 1.2 3.1 0 4.4-2.7 5.4-5.3 5.7.4.4.8 1.1.8 2.2v3.3c0 .3.2.7.8.6A11.5 11.5 0 0 0 12 .5z"/></svg>`,
5695
5700
  link: s("<path d=\"M15 3h6v6M10 14 21 3M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\"/>")
@@ -5945,7 +5950,11 @@ function write(key, value) {
5945
5950
  const KEYS = {
5946
5951
  name: "nuni:name",
5947
5952
  secret: "nuni:author-secret",
5948
- session: (project) => `nuni:session:${project}`
5953
+ session: (project) => `nuni:session:${project}`,
5954
+ /** Where this visitor dragged the toolbar. */
5955
+ position: (project) => `nuni:position:${project}`,
5956
+ /** The comment this visitor opened last, marked in the panel. */
5957
+ lastViewed: (project) => `nuni:last-viewed:${project}`
5949
5958
  };
5950
5959
  //#endregion
5951
5960
  //#region src/styles.ts
@@ -6022,7 +6031,18 @@ button { cursor: pointer; }
6022
6031
  border: 1px solid var(--n-border);
6023
6032
  box-shadow: var(--n-shadow);
6024
6033
  pointer-events: auto;
6025
- }
6034
+ cursor: grab;
6035
+ touch-action: none;
6036
+ user-select: none;
6037
+ -webkit-user-select: none;
6038
+ }
6039
+ .toolbar[data-dragging] {
6040
+ cursor: grabbing;
6041
+ box-shadow: 0 2px 4px rgba(0,0,0,0.08), 0 16px 40px rgba(0,0,0,0.2);
6042
+ }
6043
+ /* While dragging, the host covers the page so no move is lost to it. */
6044
+ :host([data-dragging]) { pointer-events: auto; cursor: grabbing; }
6045
+ :host([data-dragging]) .tb-btn { pointer-events: none; }
6026
6046
  .tb-btn {
6027
6047
  display: inline-flex;
6028
6048
  align-items: center;
@@ -6179,7 +6199,6 @@ button { cursor: pointer; }
6179
6199
  gap: 12px;
6180
6200
  }
6181
6201
  .confirm p { margin: 0; }
6182
- .btn-danger-solid { background: var(--n-danger); color: #fff; border-color: transparent; }
6183
6202
 
6184
6203
  /* Pins */
6185
6204
  .pin {
@@ -6454,6 +6473,10 @@ textarea.field { resize: vertical; min-height: 72px; max-height: 240px; }
6454
6473
  .btn-ghost { border-color: transparent; background: transparent; }
6455
6474
  .btn-icon { width: 32px; padding: 0; justify-content: center; }
6456
6475
  .btn-danger { color: var(--n-danger); }
6476
+ /* After .btn, which would otherwise paint over the red with the card's background. */
6477
+ .btn-danger-solid,
6478
+ .btn-danger-solid:hover { background: var(--n-danger); border-color: var(--n-danger); color: #fff; }
6479
+ .btn-danger-solid:hover { filter: brightness(1.08); }
6457
6480
  .error { color: var(--n-danger); font-size: 12px; }
6458
6481
  .divider { height: 1px; background: var(--n-border); }
6459
6482
 
@@ -6559,7 +6582,7 @@ textarea.field { resize: vertical; min-height: 72px; max-height: 240px; }
6559
6582
  }
6560
6583
  .panel-head { display: flex; align-items: center; gap: 8px; padding: 12px; border-bottom: 1px solid var(--n-border); }
6561
6584
  .panel-title { font-weight: 650; flex: 1; }
6562
- .tabs { display: flex; gap: 4px; padding: 8px 12px 0; }
6585
+ .tabs { display: flex; gap: 4px; padding: 0 12px; margin-top: 8px; }
6563
6586
  .tab {
6564
6587
  border: 0;
6565
6588
  background: transparent;
@@ -6582,6 +6605,11 @@ textarea.field { resize: vertical; min-height: 72px; max-height: 240px; }
6582
6605
  border-radius: 10px;
6583
6606
  }
6584
6607
  .item:hover { background: var(--n-bg-subtle); }
6608
+ /* The comment opened last, so it's easy to find your place again. */
6609
+ .item[aria-current="true"] { background: var(--n-accent-soft); box-shadow: inset 3px 0 0 var(--n-accent); }
6610
+ .panel-search { position: relative; padding: 0 12px; margin-top: 8px; }
6611
+ .panel-search .icon { position: absolute; left: 22px; top: 50%; transform: translateY(-50%); color: var(--n-fg-muted); pointer-events: none; }
6612
+ .panel-search .field { padding-left: 32px; }
6585
6613
  .item-text { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; word-break: break-word; }
6586
6614
  .section-label { padding: 12px 10px 4px; font-size: 11px; font-weight: 650; letter-spacing: 0.04em; text-transform: uppercase; color: var(--n-fg-muted); }
6587
6615
  .empty { padding: 32px 16px; text-align: center; color: var(--n-fg-muted); }
@@ -6625,8 +6653,10 @@ a.item { color: inherit; text-decoration: none; }
6625
6653
  * { transition: none !important; animation: none !important; }
6626
6654
  }
6627
6655
 
6628
- /* The toolbar's corner (the position option) */
6656
+ /* The toolbar's spot (the position option, or where it was dragged) */
6629
6657
  :host([data-position$="left"]) .toolbar { right: auto; left: 16px; }
6658
+ :host([data-position$="center"]) .toolbar { left: 0; right: 0; width: max-content; margin-inline: auto; }
6659
+ :host([data-position="top-center"]) .pick-hint { top: auto; bottom: 12px; }
6630
6660
  :host([data-position^="top"]) .toolbar { bottom: auto; top: 16px; }
6631
6661
  :host([data-position$="left"]) .panel { right: auto; left: 12px; }
6632
6662
  :host([data-position^="top"]) .panel { top: 72px; bottom: 12px; }
@@ -6738,6 +6768,8 @@ var NuniWidget = class {
6738
6768
  _defineProperty(this, "picking", false);
6739
6769
  _defineProperty(this, "panelOpen", false);
6740
6770
  _defineProperty(this, "tab", "open");
6771
+ _defineProperty(this, "search", "");
6772
+ _defineProperty(this, "lastViewedId", void 0);
6741
6773
  _defineProperty(this, "card", null);
6742
6774
  _defineProperty(this, "activeId", null);
6743
6775
  _defineProperty(this, "hoverEl", null);
@@ -6775,6 +6807,10 @@ var NuniWidget = class {
6775
6807
  _defineProperty(this, "resolveTimer", 0);
6776
6808
  _defineProperty(this, "shownPlacements", "");
6777
6809
  _defineProperty(this, "i18n", void 0);
6810
+ _defineProperty(this, "position", void 0);
6811
+ _defineProperty(this, "toolbarDrag", null);
6812
+ _defineProperty(this, "endToolbarDrag", null);
6813
+ _defineProperty(this, "toolbarDropped", false);
6778
6814
  _defineProperty(this, "isIgnored", (el) => el === this.host || this.host.contains(el));
6779
6815
  _defineProperty(this, "onFrameKey", (e) => this.onKeyDown(e));
6780
6816
  _defineProperty(this, "swallow", (e) => {
@@ -6854,6 +6890,8 @@ var NuniWidget = class {
6854
6890
  }
6855
6891
  this.secret = secret;
6856
6892
  this.ownerToken = read(KEYS.session(config.project));
6893
+ this.lastViewedId = read(KEYS.lastViewed(config.project));
6894
+ this.position = oneOf(read(KEYS.position(config.project)) ?? void 0, POSITIONS, config.position);
6857
6895
  this.host = document.createElement("div");
6858
6896
  this.host.id = "nuni-root";
6859
6897
  this.host.setAttribute("data-nuni", "");
@@ -6884,6 +6922,11 @@ var NuniWidget = class {
6884
6922
  });
6885
6923
  this.root.append(style, this.markLayer, this.pinLayer, this.overlayLayer, this.uiLayer, this.selectButton, this.live);
6886
6924
  this.uiLayer.addEventListener("keydown", (e) => this.trapFocus(e));
6925
+ this.root.addEventListener("click", (e) => {
6926
+ if (!this.toolbarDropped) return;
6927
+ e.preventDefault();
6928
+ e.stopPropagation();
6929
+ }, true);
6887
6930
  this.api = new NuniApi(config);
6888
6931
  }
6889
6932
  t(key, vars) {
@@ -6896,8 +6939,8 @@ var NuniWidget = class {
6896
6939
  }
6897
6940
  /** Position, theme, brand color and stacking order from the options. */
6898
6941
  applyAppearance() {
6899
- const { position, theme, accentColor, zIndex } = this.config;
6900
- this.host.dataset.position = position;
6942
+ const { theme, accentColor, zIndex } = this.config;
6943
+ this.host.dataset.position = this.position;
6901
6944
  this.host.dataset.theme = theme;
6902
6945
  if (zIndex !== null) this.host.style.zIndex = String(zIndex);
6903
6946
  const rgb = accentColor ? parseColor(accentColor) : null;
@@ -6944,6 +6987,7 @@ var NuniWidget = class {
6944
6987
  if (e.composedPath().includes(this.host)) return;
6945
6988
  this.pointerDown = true;
6946
6989
  this.scheduleSelection();
6990
+ this.closeCardFromOutside(e);
6947
6991
  };
6948
6992
  const onUp = () => {
6949
6993
  this.pointerDown = false;
@@ -6991,6 +7035,7 @@ var NuniWidget = class {
6991
7035
  }
6992
7036
  destroy() {
6993
7037
  this.setPicking(false);
7038
+ this.endToolbarDrag?.();
6994
7039
  this.pageUnsub?.();
6995
7040
  this.sessionUnsub?.();
6996
7041
  this.ownerDetailUnsub?.();
@@ -7515,7 +7560,10 @@ var NuniWidget = class {
7515
7560
  if (this.confirming) this.answer(false);
7516
7561
  else if (this.picking) this.setPicking(false);
7517
7562
  else if (this.card) this.closeCard();
7518
- else if (this.panelOpen) this.togglePanel(false);
7563
+ else if (this.panelOpen && this.search) {
7564
+ this.search = "";
7565
+ this.render();
7566
+ } else if (this.panelOpen) this.togglePanel(false);
7519
7567
  else return;
7520
7568
  e.stopPropagation();
7521
7569
  return;
@@ -7922,10 +7970,22 @@ var NuniWidget = class {
7922
7970
  this.keepFocus();
7923
7971
  this.focusNext = ".panel [role=\"tab\"][aria-selected=\"true\"]";
7924
7972
  if (window.matchMedia(MOBILE_QUERY).matches) this.card = null;
7925
- }
7973
+ } else this.search = "";
7926
7974
  this.render();
7975
+ if (open) this.uiLayer.querySelector(".panel .item[aria-current=\"true\"]")?.scrollIntoView({ block: "nearest" });
7927
7976
  if (!open && !this.card) this.restoreFocus();
7928
7977
  }
7978
+ /**
7979
+ * A press on the page closes the open card, like Figma, unless the card
7980
+ * holds words that haven't been sent yet.
7981
+ */
7982
+ closeCardFromOutside(e) {
7983
+ if (!this.card || this.picking || this.confirming || !e.isPrimary) return;
7984
+ if (onScrollbar(e)) return;
7985
+ if ([...this.uiLayer.querySelector("[data-card]")?.querySelectorAll("textarea") ?? []].some((t) => t.value.trim())) return;
7986
+ if (!this.panelOpen) this.returnFocus = null;
7987
+ this.closeCard();
7988
+ }
7929
7989
  closeCard() {
7930
7990
  const had = this.card !== null;
7931
7991
  this.card = null;
@@ -7937,6 +7997,8 @@ var NuniWidget = class {
7937
7997
  openThread(id) {
7938
7998
  this.keepFocus();
7939
7999
  this.activeId = id;
8000
+ this.lastViewedId = id;
8001
+ write(KEYS.lastViewed(this.config.project), id);
7940
8002
  this.card = {
7941
8003
  kind: "thread",
7942
8004
  id
@@ -8035,10 +8097,14 @@ var NuniWidget = class {
8035
8097
  }
8036
8098
  renderToolbar() {
8037
8099
  const open = this.comments.filter((c) => c.status === "open").length;
8100
+ const drag = this.toolbarDrag?.moving ? this.toolbarDrag : null;
8038
8101
  return h("div", {
8039
8102
  class: "toolbar",
8040
8103
  role: "toolbar",
8041
- "aria-label": this.t("toolbar")
8104
+ "aria-label": this.t("toolbar"),
8105
+ "data-dragging": !!drag,
8106
+ style: drag && `transform: translate(${drag.dx}px, ${drag.dy}px)`,
8107
+ onpointerdown: (e) => this.startToolbarDrag(e)
8042
8108
  }, h("button", {
8043
8109
  class: "tb-btn",
8044
8110
  type: "button",
@@ -8057,16 +8123,94 @@ var NuniWidget = class {
8057
8123
  onclick: () => this.togglePanel()
8058
8124
  }, icon(ICONS.list), h("span", { class: "tb-count" }, String(open))));
8059
8125
  }
8126
+ /**
8127
+ * Drag the toolbar anywhere; on release it glides to the nearest corner or
8128
+ * edge middle, which is remembered for this visitor.
8129
+ */
8130
+ startToolbarDrag(e) {
8131
+ if (e.button !== 0 || !e.isPrimary) return;
8132
+ this.endToolbarDrag?.();
8133
+ const drag = {
8134
+ id: e.pointerId,
8135
+ x: e.clientX,
8136
+ y: e.clientY,
8137
+ dx: 0,
8138
+ dy: 0,
8139
+ moving: false
8140
+ };
8141
+ this.toolbarDrag = drag;
8142
+ const toolbar = () => this.uiLayer.querySelector(".toolbar");
8143
+ const onMove = (ev) => {
8144
+ if (ev.pointerId !== drag.id) return;
8145
+ drag.dx = ev.clientX - drag.x;
8146
+ drag.dy = ev.clientY - drag.y;
8147
+ if (!drag.moving && Math.hypot(drag.dx, drag.dy) < 5) return;
8148
+ if (!drag.moving) {
8149
+ drag.moving = true;
8150
+ this.host.dataset.dragging = "";
8151
+ toolbar()?.setAttribute("data-dragging", "");
8152
+ }
8153
+ ev.preventDefault();
8154
+ toolbar()?.style.setProperty("transform", `translate(${drag.dx}px, ${drag.dy}px)`);
8155
+ };
8156
+ const onUp = (ev) => {
8157
+ if (ev.pointerId !== drag.id) return;
8158
+ this.endToolbarDrag?.();
8159
+ if (drag.moving) this.dropToolbar(toolbar(), true);
8160
+ };
8161
+ const onCancel = (ev) => {
8162
+ if (ev instanceof PointerEvent && ev.pointerId !== drag.id) return;
8163
+ this.endToolbarDrag?.();
8164
+ if (drag.moving) this.dropToolbar(toolbar(), false);
8165
+ };
8166
+ window.addEventListener("pointermove", onMove, true);
8167
+ window.addEventListener("pointerup", onUp, true);
8168
+ window.addEventListener("pointercancel", onCancel, true);
8169
+ window.addEventListener("blur", onCancel);
8170
+ this.endToolbarDrag = () => {
8171
+ window.removeEventListener("pointermove", onMove, true);
8172
+ window.removeEventListener("pointerup", onUp, true);
8173
+ window.removeEventListener("pointercancel", onCancel, true);
8174
+ window.removeEventListener("blur", onCancel);
8175
+ delete this.host.dataset.dragging;
8176
+ this.toolbarDrag = null;
8177
+ this.endToolbarDrag = null;
8178
+ };
8179
+ }
8180
+ /** Glide the toolbar to the spot nearest where it was let go, or back. */
8181
+ dropToolbar(el, snap) {
8182
+ this.toolbarDropped = true;
8183
+ setTimeout(() => this.toolbarDropped = false);
8184
+ if (!el) return;
8185
+ const from = el.getBoundingClientRect();
8186
+ if (snap) {
8187
+ const position = nearestPosition(from.left + from.width / 2, from.top + from.height / 2);
8188
+ this.position = position;
8189
+ this.host.dataset.position = position;
8190
+ write(KEYS.position(this.config.project), position === this.config.position ? null : position);
8191
+ }
8192
+ el.removeAttribute("data-dragging");
8193
+ el.style.removeProperty("transform");
8194
+ const to = el.getBoundingClientRect();
8195
+ if (matchMedia("(prefers-reduced-motion: reduce)").matches) return;
8196
+ el.animate([{ transform: `translate(${from.left - to.left}px, ${from.top - to.top}px)` }, { transform: "none" }], {
8197
+ duration: 320,
8198
+ easing: "cubic-bezier(0.2, 0.9, 0.3, 1.15)"
8199
+ });
8200
+ }
8060
8201
  renderPanel() {
8061
8202
  const open = this.comments.filter((c) => c.status === "open");
8062
8203
  const resolved = this.comments.filter((c) => c.status === "resolved");
8063
- const list = this.tab === "open" ? open : resolved;
8204
+ const query = this.search.trim().toLocaleLowerCase();
8205
+ const matches = (...texts) => !query || texts.some((t) => t?.toLocaleLowerCase().includes(query));
8206
+ const list = (this.tab === "open" ? open : resolved).filter((c) => matches(c.body, c.authorName, c.anchor.quote?.exact));
8064
8207
  const placed = list.filter((c) => this.placements.get(c._id)?.element);
8065
8208
  const lost = list.filter((c) => !this.placements.get(c._id)?.element);
8066
- const otherPages = this.pages.filter((p) => p.path !== this.pageKey);
8209
+ const otherPages = this.pages.filter((p) => p.path !== this.pageKey && matches(p.path));
8067
8210
  const item = (c) => h("button", {
8068
8211
  class: "item",
8069
8212
  type: "button",
8213
+ "aria-current": c._id === this.lastViewedId ? "true" : null,
8070
8214
  "data-focus-key": `item-${c._id}`,
8071
8215
  onclick: () => this.focusComment(c._id)
8072
8216
  }, h("div", { class: "row" }, h("span", {
@@ -8078,6 +8222,7 @@ var NuniWidget = class {
8078
8222
  }, new URL(c.page.origin).host) : null), c.anchor.quote ? h("div", { class: "item-quote" }, c.anchor.quote.exact) : null, h("div", { class: "item-text" }, c.body));
8079
8223
  const children = [];
8080
8224
  if (!this.loaded) children.push(h("div", { class: "empty" }, this.t("loading")));
8225
+ else if (!list.length && query) children.push(h("div", { class: "empty" }, this.t("noMatches", { query: this.search.trim() })));
8081
8226
  else if (!list.length) children.push(h("div", { class: "empty" }, this.tab === "open" ? this.withKey("noOpen", "noOpenKey") : this.t("noResolved")));
8082
8227
  else {
8083
8228
  children.push(...placed.map(item));
@@ -8105,7 +8250,19 @@ var NuniWidget = class {
8105
8250
  type: "button",
8106
8251
  "aria-label": this.t("close"),
8107
8252
  onclick: () => this.togglePanel(false)
8108
- }, icon(ICONS.close))), h("div", {
8253
+ }, icon(ICONS.close))), this.comments.length ? h("div", { class: "panel-search" }, icon(ICONS.search), h("input", {
8254
+ class: "field",
8255
+ type: "search",
8256
+ value: this.search,
8257
+ placeholder: this.t("searchComments"),
8258
+ "aria-label": this.t("searchComments"),
8259
+ "aria-controls": "nuni-panel-list",
8260
+ "data-focus-key": "panel-search",
8261
+ oninput: (e) => {
8262
+ this.search = e.target.value;
8263
+ this.render();
8264
+ }
8265
+ })) : null, h("div", {
8109
8266
  class: "tabs",
8110
8267
  role: "tablist",
8111
8268
  "aria-label": this.t("comments"),
@@ -8543,8 +8700,8 @@ var NuniWidget = class {
8543
8700
  top = point.y - 28;
8544
8701
  if (left + width > vw - 12) left = point.x - width - 12;
8545
8702
  } else {
8546
- const { position } = this.config;
8547
- left = position.endsWith("left") ? 16 : vw - width - 16;
8703
+ const { position } = this;
8704
+ left = position.endsWith("left") ? 16 : position.endsWith("center") ? (vw - width) / 2 : vw - width - 16;
8548
8705
  top = position.startsWith("top") ? 72 : vh - height - 72;
8549
8706
  }
8550
8707
  left = Math.max(12, Math.min(left, vw - width - 12));
@@ -8558,6 +8715,22 @@ var NuniWidget = class {
8558
8715
  });
8559
8716
  }
8560
8717
  };
8718
+ /** The toolbar spot nearest a point: thirds across the screen, halves down. */
8719
+ function nearestPosition(x, y) {
8720
+ const width = window.innerWidth;
8721
+ const across = x < width / 3 ? "left" : x > width * 2 / 3 ? "right" : "center";
8722
+ return `${y < window.innerHeight / 2 ? "top" : "bottom"}-${across}`;
8723
+ }
8724
+ /** A press on a scrollbar, which scrolls rather than points at the page. */
8725
+ function onScrollbar(e) {
8726
+ const el = e.composedPath()[0];
8727
+ if (!(el instanceof Element)) return false;
8728
+ const root = document.documentElement;
8729
+ if (el === root) return e.clientX >= root.clientWidth || e.clientY >= root.clientHeight;
8730
+ if (!(el.scrollHeight > el.clientHeight || el.scrollWidth > el.clientWidth)) return false;
8731
+ const box = el.getBoundingClientRect();
8732
+ return e.clientX - box.left - el.clientLeft >= el.clientWidth || e.clientY - box.top - el.clientTop >= el.clientHeight;
8733
+ }
8561
8734
  //#endregion
8562
8735
  //#region src/mount.ts
8563
8736
  /** Nuni's own requests are not the page's problem. */