@tensor_1/react 2.41.0 → 2.42.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 (39) hide show
  1. package/dist/components/Button.d.ts +19 -1
  2. package/dist/components/Button.js +13 -8
  3. package/dist/components/DescriptionList.d.ts +11 -0
  4. package/dist/components/DiscardPrompt.d.ts +54 -0
  5. package/dist/components/DiscardPrompt.js +245 -0
  6. package/dist/components/Drawer.d.ts +19 -3
  7. package/dist/components/Drawer.js +60 -21
  8. package/dist/components/EditorialHome.d.ts +1 -1
  9. package/dist/components/Field.d.ts +7 -1
  10. package/dist/components/Field.js +5 -4
  11. package/dist/components/Input.d.ts +5 -0
  12. package/dist/components/Input.js +2 -1
  13. package/dist/components/Kbd.d.ts +20 -1
  14. package/dist/components/Kbd.js +30 -7
  15. package/dist/components/Menu.d.ts +11 -4
  16. package/dist/components/Menu.js +17 -3
  17. package/dist/components/MetricInput.d.ts +8 -1
  18. package/dist/components/MetricInput.js +11 -3
  19. package/dist/components/Modal.d.ts +15 -1
  20. package/dist/components/Modal.js +58 -13
  21. package/dist/components/SearchBar.d.ts +67 -0
  22. package/dist/components/SearchBar.js +117 -0
  23. package/dist/components/Select.js +5 -4
  24. package/dist/components/ShortcutHint.d.ts +29 -0
  25. package/dist/components/ShortcutHint.js +149 -0
  26. package/dist/components/Tooltip.d.ts +10 -1
  27. package/dist/components/Tooltip.js +15 -4
  28. package/dist/emoji.cjs +108 -31
  29. package/dist/hooks/useAnchoredPosition.d.ts +13 -1
  30. package/dist/hooks/useAnchoredPosition.js +19 -11
  31. package/dist/hooks/useDialog.d.ts +6 -1
  32. package/dist/hooks/useDialog.js +10 -1
  33. package/dist/index.cjs +3184 -2521
  34. package/dist/index.d.cts +221 -39
  35. package/dist/index.d.ts +3 -1
  36. package/dist/index.js +6 -1
  37. package/dist/labels-context.d.ts +8 -0
  38. package/dist/rich-text.cjs +17 -10
  39. package/package.json +1 -1
@@ -0,0 +1,149 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useLayoutEffect, useRef, useState } from "react";
4
+ import { createPortal } from "react-dom";
5
+ import { useAnchoredPosition } from "../hooks/useAnchoredPosition.js";
6
+ import { KbdCombo, ariaKeyShortcuts, useKbdPlatform } from "./Kbd.js";
7
+ const useIsomorphicLayoutEffect = typeof document !== "undefined" ? useLayoutEffect : useEffect;
8
+ const isShown = (el) => {
9
+ try {
10
+ return !!el?.matches(":popover-open");
11
+ } catch {
12
+ return false;
13
+ }
14
+ };
15
+ const focusVisible = (el) => {
16
+ try {
17
+ return el.matches(":focus-visible");
18
+ } catch {
19
+ return true;
20
+ }
21
+ };
22
+ const stop = (event) => event.stopPropagation();
23
+ function ShortcutHint({ keys, label, ownsAria }) {
24
+ const markerRef = useRef(null);
25
+ const bubbleRef = useRef(null);
26
+ const anchorRef = useRef(null);
27
+ const [host, setHost] = useState(null);
28
+ const [open, setOpen] = useState(false);
29
+ const platform = useKbdPlatform();
30
+ const shortcuts = ariaKeyShortcuts(keys, platform);
31
+ useAnchoredPosition(anchorRef, bubbleRef, { open, placement: "top", offset: 6 });
32
+ useIsomorphicLayoutEffect(() => {
33
+ const button = markerRef.current?.parentElement;
34
+ if (!button || !ownsAria) return void 0;
35
+ const before = button.getAttribute("aria-keyshortcuts");
36
+ button.setAttribute("aria-keyshortcuts", shortcuts);
37
+ return () => {
38
+ if (button.getAttribute("aria-keyshortcuts") !== shortcuts) return;
39
+ if (before == null) button.removeAttribute("aria-keyshortcuts");
40
+ else button.setAttribute("aria-keyshortcuts", before);
41
+ };
42
+ }, [shortcuts, ownsAria]);
43
+ useIsomorphicLayoutEffect(() => {
44
+ const button = markerRef.current?.parentElement ?? null;
45
+ anchorRef.current = button;
46
+ if (!button || button.parentElement?.closest(".uix-tooltip")) {
47
+ setHost(null);
48
+ return;
49
+ }
50
+ setHost(button.closest("dialog") ?? button.ownerDocument.body);
51
+ }, []);
52
+ useEffect(() => {
53
+ const button = anchorRef.current;
54
+ const bubble = bubbleRef.current;
55
+ if (!host || !button || !bubble) return void 0;
56
+ let timer;
57
+ const show = () => {
58
+ window.clearTimeout(timer);
59
+ if (!isShown(bubble)) {
60
+ try {
61
+ bubble.showPopover();
62
+ } catch {
63
+ }
64
+ }
65
+ setOpen(true);
66
+ };
67
+ const hide = () => {
68
+ window.clearTimeout(timer);
69
+ if (isShown(bubble)) {
70
+ try {
71
+ bubble.hidePopover();
72
+ } catch {
73
+ }
74
+ }
75
+ setOpen(false);
76
+ };
77
+ const hideSoon = () => {
78
+ window.clearTimeout(timer);
79
+ timer = window.setTimeout(() => {
80
+ if (button.ownerDocument.activeElement === button && focusVisible(button)) return;
81
+ hide();
82
+ }, 100);
83
+ };
84
+ const onFocus = () => {
85
+ if (focusVisible(button)) show();
86
+ };
87
+ const onKey = (event) => {
88
+ if (event.key === "Escape") hide();
89
+ };
90
+ const watch = new MutationObserver(() => {
91
+ if (button.hasAttribute("disabled") || button.getAttribute("aria-disabled") === "true") hide();
92
+ });
93
+ watch.observe(button, { attributes: true, attributeFilter: ["disabled", "aria-disabled"] });
94
+ button.addEventListener("mouseenter", show);
95
+ button.addEventListener("mouseleave", hideSoon);
96
+ button.addEventListener("focus", onFocus);
97
+ button.addEventListener("blur", hide);
98
+ bubble.addEventListener("mouseenter", show);
99
+ bubble.addEventListener("mouseleave", hideSoon);
100
+ button.ownerDocument.addEventListener("keydown", onKey);
101
+ return () => {
102
+ window.clearTimeout(timer);
103
+ watch.disconnect();
104
+ button.removeEventListener("mouseenter", show);
105
+ button.removeEventListener("mouseleave", hideSoon);
106
+ button.removeEventListener("focus", onFocus);
107
+ button.removeEventListener("blur", hide);
108
+ bubble.removeEventListener("mouseenter", show);
109
+ bubble.removeEventListener("mouseleave", hideSoon);
110
+ button.ownerDocument.removeEventListener("keydown", onKey);
111
+ if (isShown(bubble)) {
112
+ try {
113
+ bubble.hidePopover();
114
+ } catch {
115
+ }
116
+ }
117
+ };
118
+ }, [host]);
119
+ const bubbleAttr = { popover: "manual" };
120
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
121
+ /* @__PURE__ */ jsx("span", { ref: markerRef, hidden: true, "data-uix-shortcut-hint": "" }),
122
+ host && createPortal(
123
+ /* @__PURE__ */ jsxs(
124
+ "span",
125
+ {
126
+ ref: bubbleRef,
127
+ className: "uix-tooltip__bubble uix-tooltip__bubble--hint",
128
+ "aria-hidden": "true",
129
+ ...bubbleAttr,
130
+ onClick: stop,
131
+ onDoubleClick: stop,
132
+ onContextMenu: stop,
133
+ onPointerDown: stop,
134
+ onPointerUp: stop,
135
+ onMouseDown: stop,
136
+ onMouseUp: stop,
137
+ children: [
138
+ label && /* @__PURE__ */ jsx("span", { children: label }),
139
+ /* @__PURE__ */ jsx("span", { className: "uix-tooltip__shortcut", children: /* @__PURE__ */ jsx(KbdCombo, { keys: [...keys], platform }) })
140
+ ]
141
+ }
142
+ ),
143
+ host
144
+ )
145
+ ] });
146
+ }
147
+ export {
148
+ ShortcutHint
149
+ };
@@ -1,6 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { HTMLAttributes, ReactNode } from 'react';
3
3
  import { Placement } from '../overlay-position.js';
4
+ import { KeyShortcut } from './Kbd.js';
4
5
 
5
6
  interface TooltipProps extends Omit<HTMLAttributes<HTMLSpanElement>, 'content'> {
6
7
  /** Tooltip text. Pass this or `content`. */
@@ -13,6 +14,14 @@ interface TooltipProps extends Omit<HTMLAttributes<HTMLSpanElement>, 'content'>
13
14
  content?: ReactNode;
14
15
  /** Preferred placement. Default `'top'`. */
15
16
  placement?: Placement;
17
+ /**
18
+ * The trigger's keyboard shortcut, in `KbdCombo` key names, e.g. `['Mod', 'K']` (HAR-2031):
19
+ * shown with `Kbd` after the text, and set as the trigger's `aria-keyshortcuts` for the
20
+ * viewer's platform. Default: the `shortcut` of a `Button` child, so a Tooltip around a Button
21
+ * with a shortcut shows one bubble with the label and the keys. The kit only shows it: the
22
+ * product binds the key.
23
+ */
24
+ shortcut?: KeyShortcut;
16
25
  children?: ReactNode;
17
26
  }
18
27
  /**
@@ -23,6 +32,6 @@ interface TooltipProps extends Omit<HTMLAttributes<HTMLSpanElement>, 'content'>
23
32
  * (document-level, per WCAG 1.4.13), and hoverable: the bubble stays open while
24
33
  * the pointer is over it (UIX-A11Y-1).
25
34
  */
26
- declare function Tooltip({ label, content, placement, className, children, ...props }: TooltipProps): react.JSX.Element;
35
+ declare function Tooltip({ label, content, placement, shortcut, className, children, ...props }: TooltipProps): react.JSX.Element;
27
36
 
28
37
  export { Tooltip, type TooltipProps };
@@ -3,12 +3,18 @@ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { cloneElement, isValidElement, useEffect, useId, useRef, useState } from "react";
4
4
  import { cx } from "../cx.js";
5
5
  import { useAnchoredPosition } from "../hooks/useAnchoredPosition.js";
6
- function Tooltip({ label, content, placement = "top", className, children, ...props }) {
6
+ import { KbdCombo, ariaKeyShortcuts, isKeyShortcut, useKbdPlatform } from "./Kbd.js";
7
+ function Tooltip({ label, content, placement = "top", shortcut, className, children, ...props }) {
7
8
  const triggerRef = useRef(null);
8
9
  const bubbleRef = useRef(null);
9
10
  const closeTimer = useRef(null);
10
11
  const [open, setOpen] = useState(false);
11
12
  const id = useId();
13
+ const platform = useKbdPlatform();
14
+ const childShortcut = isValidElement(children) ? children.props.shortcut : void 0;
15
+ const own = shortcut ?? (isKeyShortcut(childShortcut) ? childShortcut : void 0);
16
+ const keys = own && own.length > 0 ? own : null;
17
+ const keyshortcuts = keys ? ariaKeyShortcuts(keys, platform) : void 0;
12
18
  useAnchoredPosition(triggerRef, bubbleRef, { open, placement, offset: 6 });
13
19
  const cancelClose = () => {
14
20
  if (closeTimer.current != null) {
@@ -63,7 +69,8 @@ function Tooltip({ label, content, placement = "top", className, children, ...pr
63
69
  const bubbleAttr = { popover: "manual" };
64
70
  const single = isValidElement(children);
65
71
  const child = single ? cloneElement(children, {
66
- "aria-describedby": [children.props["aria-describedby"], id].filter(Boolean).join(" ")
72
+ "aria-describedby": [children.props["aria-describedby"], id].filter(Boolean).join(" "),
73
+ ...keyshortcuts ? { "aria-keyshortcuts": keyshortcuts } : {}
67
74
  }) : children;
68
75
  return /* @__PURE__ */ jsxs(
69
76
  "span",
@@ -71,6 +78,7 @@ function Tooltip({ label, content, placement = "top", className, children, ...pr
71
78
  ref: triggerRef,
72
79
  className: cx("uix-tooltip", className),
73
80
  "aria-describedby": single ? void 0 : id,
81
+ "aria-keyshortcuts": single ? void 0 : keyshortcuts,
74
82
  tabIndex: single ? void 0 : 0,
75
83
  onMouseEnter: show,
76
84
  onMouseLeave: scheduleHide,
@@ -82,7 +90,7 @@ function Tooltip({ label, content, placement = "top", className, children, ...pr
82
90
  ...props,
83
91
  children: [
84
92
  child,
85
- /* @__PURE__ */ jsx(
93
+ /* @__PURE__ */ jsxs(
86
94
  "span",
87
95
  {
88
96
  ref: bubbleRef,
@@ -92,7 +100,10 @@ function Tooltip({ label, content, placement = "top", className, children, ...pr
92
100
  onMouseEnter: cancelClose,
93
101
  onMouseLeave: scheduleHide,
94
102
  ...bubbleAttr,
95
- children: content ?? label
103
+ children: [
104
+ content ?? label,
105
+ keys && /* @__PURE__ */ jsx("span", { className: "uix-tooltip__shortcut", children: /* @__PURE__ */ jsx(KbdCombo, { keys: [...keys], platform }) })
106
+ ]
96
107
  }
97
108
  )
98
109
  ]
package/dist/emoji.cjs CHANGED
@@ -173,11 +173,16 @@ function translationOf(el) {
173
173
  if (!t || t === "none" || typeof DOMMatrixReadOnly === "undefined") return { x: 0, y: 0 };
174
174
  try {
175
175
  const m = new DOMMatrixReadOnly(t);
176
- return { x: m.m41, y: m.m42 };
176
+ const z = zoomOf(el);
177
+ return { x: m.m41 * z, y: m.m42 * z };
177
178
  } catch {
178
179
  return { x: 0, y: 0 };
179
180
  }
180
181
  }
182
+ function zoomOf(el) {
183
+ const z = el.currentCSSZoom;
184
+ return typeof z === "number" && Number.isFinite(z) && z > 0 ? z : 1;
185
+ }
181
186
  var signed = (n) => `${n < 0 ? "-" : "+"} ${Math.round(Math.abs(n) * 100) / 100}px`;
182
187
  var prepared = /* @__PURE__ */ new WeakSet();
183
188
  function useAnchoredPosition(anchor, floatingRef, { open, placement = "bottom-start", offset = 6, padding = 8, shiftMainAxis = true, capHeight = false, onAnchorHidden }) {
@@ -202,8 +207,9 @@ function useAnchoredPosition(anchor, floatingRef, { open, placement = "bottom-st
202
207
  const s = floating.style;
203
208
  if (keepPlace && current.anchor.isConnected) {
204
209
  const a = current.anchor.getBoundingClientRect();
205
- s.top = `${Math.round((current.yEdge === "bottom" ? a.bottom : a.top) + current.dy)}px`;
206
- s.left = `${Math.round((current.xEdge === "right" ? a.right : a.left) + current.dx)}px`;
210
+ const z = zoomOf(floating);
211
+ s.top = `${Math.round(((current.yEdge === "bottom" ? a.bottom : a.top) + current.dy) / z)}px`;
212
+ s.left = `${Math.round(((current.xEdge === "right" ? a.right : a.left) + current.dx) / z)}px`;
207
213
  }
208
214
  s.removeProperty("position-anchor");
209
215
  }
@@ -231,6 +237,7 @@ function useAnchoredPosition(anchor, floatingRef, { open, placement = "bottom-st
231
237
  written.current = "";
232
238
  }
233
239
  const { placement: p, offset: o, padding: pad, shiftMainAxis: keepInView, capHeight: cap } = opts.current;
240
+ const zoom = floating.getClientRects().length > 0 ? zoomOf(floating) : zoomOf(anchorEl);
234
241
  if (cap) {
235
242
  if (!heightCap.current) {
236
243
  const own = getComputedStyle(floating);
@@ -244,7 +251,7 @@ function useAnchoredPosition(anchor, floatingRef, { open, placement = "bottom-st
244
251
  };
245
252
  if (setOverflow) s.overflowY = "auto";
246
253
  }
247
- const room = `${Math.max(0, window.innerHeight - 2 * pad)}px`;
254
+ const room = `${Math.max(0, (window.innerHeight - 2 * pad * zoom) / zoom)}px`;
248
255
  const max = heightCap.current.ownMaxHeight ? `min(${room}, ${heightCap.current.ownMaxHeight})` : room;
249
256
  if (max !== heightCap.current.written) {
250
257
  s.maxHeight = max;
@@ -254,19 +261,19 @@ function useAnchoredPosition(anchor, floatingRef, { open, placement = "bottom-st
254
261
  uncap();
255
262
  }
256
263
  const a = anchorEl.getBoundingClientRect();
257
- const size = { width: floating.offsetWidth, height: floating.offsetHeight };
264
+ const size = { width: floating.offsetWidth * zoom, height: floating.offsetHeight * zoom };
258
265
  const pos = computePosition(
259
266
  { x: a.x, y: a.y, width: a.width, height: a.height },
260
267
  size,
261
268
  { width: window.innerWidth, height: window.innerHeight },
262
- { placement: p, offset: o, padding: pad, shiftMainAxis: keepInView, stickySide: side.current ?? void 0 }
269
+ { placement: p, offset: o * zoom, padding: pad * zoom, shiftMainAxis: keepInView, stickySide: side.current ?? void 0 }
263
270
  );
264
271
  const measured = size.width > 0 || size.height > 0;
265
272
  if (measured && session.current) side.current = pos.side;
266
273
  if (floating.dataset.placement !== pos.placement) floating.dataset.placement = pos.placement;
267
274
  const writeFixed = (at) => {
268
- const left2 = `${Math.round(at.x)}px`;
269
- const top2 = `${Math.round(at.y)}px`;
275
+ const left2 = `${Math.round(at.x / zoom)}px`;
276
+ const top2 = `${Math.round(at.y / zoom)}px`;
270
277
  const key2 = `fixed ${left2} ${top2}`;
271
278
  if (key2 === written.current) return;
272
279
  s.left = left2;
@@ -293,8 +300,8 @@ function useAnchoredPosition(anchor, floatingRef, { open, placement = "bottom-st
293
300
  current.xEdge = pos.side === "right" ? "right" : "left";
294
301
  current.dy = pos.y - (current.yEdge === "bottom" ? a.bottom : a.top);
295
302
  current.dx = pos.x - (current.xEdge === "right" ? a.right : a.left);
296
- const top = `round(anchor(${current.yEdge}) ${signed(current.dy)}, 1px)`;
297
- const left = `round(anchor(${current.xEdge}) ${signed(current.dx)}, 1px)`;
303
+ const top = `round(anchor(${current.yEdge}) ${signed(current.dy / zoom)}, 1px)`;
304
+ const left = `round(anchor(${current.xEdge}) ${signed(current.dx / zoom)}, 1px)`;
298
305
  const key = `anchored ${top} ${left}`;
299
306
  if (key !== written.current) {
300
307
  s.top = top;
@@ -1079,14 +1086,79 @@ function EditorIcon({ name, ...props }) {
1079
1086
  }
1080
1087
 
1081
1088
  // src/components/Tooltip.tsx
1089
+ var import_react6 = require("react");
1090
+
1091
+ // src/components/Kbd.tsx
1082
1092
  var import_react5 = require("react");
1083
1093
  var import_jsx_runtime5 = require("react/jsx-runtime");
1084
- function Tooltip({ label, content, placement = "top", className, children, ...props }) {
1085
- const triggerRef = (0, import_react5.useRef)(null);
1086
- const bubbleRef = (0, import_react5.useRef)(null);
1087
- const closeTimer = (0, import_react5.useRef)(null);
1088
- const [open, setOpen] = (0, import_react5.useState)(false);
1089
- const id = (0, import_react5.useId)();
1094
+ var KEYS = {
1095
+ Mod: { mac: ["\u2318", "Command"], other: ["Ctrl", "Control"] },
1096
+ Ctrl: { mac: ["\u2303", "Control"], other: ["Ctrl", "Control"] },
1097
+ Shift: { mac: ["\u21E7", "Shift"], other: ["Shift", "Shift"] },
1098
+ Alt: { mac: ["\u2325", "Option"], other: ["Alt", "Alt"] },
1099
+ Enter: { mac: ["\u21B5", "Enter"], other: ["Enter", "Enter"] },
1100
+ Escape: { mac: ["Esc", "Escape"], other: ["Esc", "Escape"] },
1101
+ Esc: { mac: ["Esc", "Escape"], other: ["Esc", "Escape"] },
1102
+ Delete: { mac: ["Del", "Delete"], other: ["Del", "Delete"] },
1103
+ Del: { mac: ["Del", "Delete"], other: ["Del", "Delete"] },
1104
+ Space: { mac: ["Space", "Space"], other: ["Space", "Space"] },
1105
+ " ": { mac: ["Space", "Space"], other: ["Space", "Space"] },
1106
+ Backspace: { mac: ["\u232B", "Backspace"], other: ["Backspace", "Backspace"] },
1107
+ Tab: { mac: ["\u21E5", "Tab"], other: ["Tab", "Tab"] },
1108
+ ArrowUp: { mac: ["\u2191", "Up arrow"], other: ["\u2191", "Up arrow"] },
1109
+ ArrowDown: { mac: ["\u2193", "Down arrow"], other: ["\u2193", "Down arrow"] },
1110
+ ArrowLeft: { mac: ["\u2190", "Left arrow"], other: ["\u2190", "Left arrow"] },
1111
+ ArrowRight: { mac: ["\u2192", "Right arrow"], other: ["\u2192", "Right arrow"] }
1112
+ };
1113
+ var detectPlatform = () => {
1114
+ if (typeof navigator === "undefined") return "other";
1115
+ const nav = navigator;
1116
+ return /mac|iphone|ipad|ipod/i.test(nav.userAgentData?.platform ?? navigator.platform ?? "") ? "mac" : "other";
1117
+ };
1118
+ var noSubscription = () => () => {
1119
+ };
1120
+ var serverPlatform = () => "other";
1121
+ function useKbdPlatform(platform) {
1122
+ const detected = (0, import_react5.useSyncExternalStore)(noSubscription, detectPlatform, serverPlatform);
1123
+ return platform ?? detected;
1124
+ }
1125
+ var ARIA_KEYS = {
1126
+ Mod: { mac: "Meta", other: "Control" },
1127
+ Ctrl: { mac: "Control", other: "Control" },
1128
+ Esc: { mac: "Escape", other: "Escape" },
1129
+ Del: { mac: "Delete", other: "Delete" },
1130
+ " ": { mac: "Space", other: "Space" },
1131
+ "+": { mac: "Plus", other: "Plus" }
1132
+ };
1133
+ function ariaKeyShortcuts(keys, platform) {
1134
+ return keys.map((key) => ARIA_KEYS[key]?.[platform] ?? (key.length === 1 ? key.toUpperCase() : key)).join("+");
1135
+ }
1136
+ var isKeyShortcut = (value) => Array.isArray(value) && value.length > 0 && value.every((key) => typeof key === "string");
1137
+ function KbdCombo({ keys, platform, keyLabels, className, ...props }) {
1138
+ const active = useKbdPlatform(platform);
1139
+ const parts = keys.map((key) => {
1140
+ const known = KEYS[key]?.[active];
1141
+ return { glyph: known?.[0] ?? key, spoken: keyLabels?.[key] ?? known?.[1] ?? key };
1142
+ });
1143
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: cx("uix-kbd-combo", className), ...props, children: [
1144
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "uix-visually-hidden", children: parts.map((p) => p.spoken).join("+") }),
1145
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "uix-kbd-combo__keys", "aria-hidden": "true", children: parts.map((p, i) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("kbd", { className: "uix-kbd", children: p.glyph }, `${keys[i]}-${i}`)) })
1146
+ ] });
1147
+ }
1148
+
1149
+ // src/components/Tooltip.tsx
1150
+ var import_jsx_runtime6 = require("react/jsx-runtime");
1151
+ function Tooltip({ label, content, placement = "top", shortcut, className, children, ...props }) {
1152
+ const triggerRef = (0, import_react6.useRef)(null);
1153
+ const bubbleRef = (0, import_react6.useRef)(null);
1154
+ const closeTimer = (0, import_react6.useRef)(null);
1155
+ const [open, setOpen] = (0, import_react6.useState)(false);
1156
+ const id = (0, import_react6.useId)();
1157
+ const platform = useKbdPlatform();
1158
+ const childShortcut = (0, import_react6.isValidElement)(children) ? children.props.shortcut : void 0;
1159
+ const own = shortcut ?? (isKeyShortcut(childShortcut) ? childShortcut : void 0);
1160
+ const keys = own && own.length > 0 ? own : null;
1161
+ const keyshortcuts = keys ? ariaKeyShortcuts(keys, platform) : void 0;
1090
1162
  useAnchoredPosition(triggerRef, bubbleRef, { open, placement, offset: 6 });
1091
1163
  const cancelClose = () => {
1092
1164
  if (closeTimer.current != null) {
@@ -1120,7 +1192,7 @@ function Tooltip({ label, content, placement = "top", className, children, ...pr
1120
1192
  cancelClose();
1121
1193
  closeTimer.current = window.setTimeout(hide, 100);
1122
1194
  };
1123
- (0, import_react5.useEffect)(() => {
1195
+ (0, import_react6.useEffect)(() => {
1124
1196
  if (!open) return;
1125
1197
  const onKey = (e) => {
1126
1198
  if (e.key === "Escape") hide();
@@ -1128,7 +1200,7 @@ function Tooltip({ label, content, placement = "top", className, children, ...pr
1128
1200
  document.addEventListener("keydown", onKey);
1129
1201
  return () => document.removeEventListener("keydown", onKey);
1130
1202
  }, [open]);
1131
- (0, import_react5.useEffect)(() => () => {
1203
+ (0, import_react6.useEffect)(() => () => {
1132
1204
  if (closeTimer.current != null) clearTimeout(closeTimer.current);
1133
1205
  const b = bubbleRef.current;
1134
1206
  if (b && b.matches(":popover-open")) {
@@ -1139,16 +1211,18 @@ function Tooltip({ label, content, placement = "top", className, children, ...pr
1139
1211
  }
1140
1212
  }, []);
1141
1213
  const bubbleAttr = { popover: "manual" };
1142
- const single = (0, import_react5.isValidElement)(children);
1143
- const child = single ? (0, import_react5.cloneElement)(children, {
1144
- "aria-describedby": [children.props["aria-describedby"], id].filter(Boolean).join(" ")
1214
+ const single = (0, import_react6.isValidElement)(children);
1215
+ const child = single ? (0, import_react6.cloneElement)(children, {
1216
+ "aria-describedby": [children.props["aria-describedby"], id].filter(Boolean).join(" "),
1217
+ ...keyshortcuts ? { "aria-keyshortcuts": keyshortcuts } : {}
1145
1218
  }) : children;
1146
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
1219
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1147
1220
  "span",
1148
1221
  {
1149
1222
  ref: triggerRef,
1150
1223
  className: cx("uix-tooltip", className),
1151
1224
  "aria-describedby": single ? void 0 : id,
1225
+ "aria-keyshortcuts": single ? void 0 : keyshortcuts,
1152
1226
  tabIndex: single ? void 0 : 0,
1153
1227
  onMouseEnter: show,
1154
1228
  onMouseLeave: scheduleHide,
@@ -1160,7 +1234,7 @@ function Tooltip({ label, content, placement = "top", className, children, ...pr
1160
1234
  ...props,
1161
1235
  children: [
1162
1236
  child,
1163
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1237
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1164
1238
  "span",
1165
1239
  {
1166
1240
  ref: bubbleRef,
@@ -1170,7 +1244,10 @@ function Tooltip({ label, content, placement = "top", className, children, ...pr
1170
1244
  onMouseEnter: cancelClose,
1171
1245
  onMouseLeave: scheduleHide,
1172
1246
  ...bubbleAttr,
1173
- children: content ?? label
1247
+ children: [
1248
+ content ?? label,
1249
+ keys && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "uix-tooltip__shortcut", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(KbdCombo, { keys: [...keys], platform }) })
1250
+ ]
1174
1251
  }
1175
1252
  )
1176
1253
  ]
@@ -1179,7 +1256,7 @@ function Tooltip({ label, content, placement = "top", className, children, ...pr
1179
1256
  }
1180
1257
 
1181
1258
  // src/components/ReactionBar.tsx
1182
- var import_jsx_runtime6 = require("react/jsx-runtime");
1259
+ var import_jsx_runtime7 = require("react/jsx-runtime");
1183
1260
  var DEFAULT_REACTION_BAR_LABELS = {
1184
1261
  group: "Reactions",
1185
1262
  addReaction: "Add reaction",
@@ -1212,10 +1289,10 @@ function ReactionBar({
1212
1289
  const names = hidden > 0 ? `${shown} ${formatLabel(labels.others, { count: hidden })}` : shown;
1213
1290
  return formatLabel(labels.reactedWith, { names, emoji: reaction.emoji, count: reaction.count });
1214
1291
  };
1215
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: cx("uix-reactions", className), role: "group", "aria-label": labels.group, children: [
1292
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: cx("uix-reactions", className), role: "group", "aria-label": labels.group, children: [
1216
1293
  reactions.filter((r) => r.count > 0).map((reaction) => {
1217
1294
  const text = describe(reaction);
1218
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Tooltip, { label: text, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1295
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Tooltip, { label: text, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
1219
1296
  "button",
1220
1297
  {
1221
1298
  type: "button",
@@ -1226,13 +1303,13 @@ function ReactionBar({
1226
1303
  disabled,
1227
1304
  onClick: () => onToggle(reaction.emoji),
1228
1305
  children: [
1229
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { "aria-hidden": "true", children: renderEmoji(reaction.emoji) }),
1230
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "uix-reaction__count", "aria-hidden": "true", children: reaction.count })
1306
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { "aria-hidden": "true", children: renderEmoji(reaction.emoji) }),
1307
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "uix-reaction__count", "aria-hidden": "true", children: reaction.count })
1231
1308
  ]
1232
1309
  }
1233
1310
  ) }, reaction.emoji);
1234
1311
  }),
1235
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1312
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1236
1313
  EmojiPicker,
1237
1314
  {
1238
1315
  quickPicks,
@@ -1243,7 +1320,7 @@ function ReactionBar({
1243
1320
  onSelect: (emoji) => {
1244
1321
  if (!reactions.some((r) => r.emoji === emoji && r.reactedByMe)) onToggle(emoji);
1245
1322
  },
1246
- trigger: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "uix-reaction-add", "aria-label": labels.addReaction, title: labels.addReaction, disabled, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(EditorIcon, { name: "addReaction" }) })
1323
+ trigger: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: "uix-reaction-add", "aria-label": labels.addReaction, title: labels.addReaction, disabled, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(EditorIcon, { name: "addReaction" }) })
1247
1324
  }
1248
1325
  )
1249
1326
  ] });
@@ -30,6 +30,18 @@ interface UseAnchoredPositionOptions {
30
30
  type AnchorArg = RefObject<HTMLElement | null> | HTMLElement | null | undefined;
31
31
  /** @internal The element behind an anchor ref or element. */
32
32
  declare const resolveAnchor: (a: AnchorArg) => HTMLElement | null;
33
+ /**
34
+ * The element's effective CSS `zoom` (HAR-1798): `currentCSSZoom` where the engine has it, else 1.
35
+ *
36
+ * Under `html { zoom: 2 }` (how a page emulates 200 % reflow) `getBoundingClientRect()` and the
37
+ * viewport size are in zoomed, on-screen pixels, while the `top` / `left` / `max-height` written
38
+ * on the element, its offsetWidth and its transform are in its own unzoomed CSS pixels, which the
39
+ * browser multiplies by the zoom. The placement is computed on screen and divided back on write;
40
+ * without that, an overlay opened low on the page landed at twice its offset, off-screen.
41
+ * Browser zoom (Ctrl +) changes the CSS pixel itself and leaves this at 1.
42
+ * @internal Also used by components that size an overlay from its anchor (Select).
43
+ */
44
+ declare function zoomOf(el: HTMLElement): number;
33
45
  /**
34
46
  * Position a floating element (a native-popover overlay) against an anchor with
35
47
  * cross-browser flip/shift — the DOM half of the overlay-position engine (UIX-FIX-02).
@@ -50,4 +62,4 @@ declare const resolveAnchor: (a: AnchorArg) => HTMLElement | null;
50
62
  */
51
63
  declare function useAnchoredPosition(anchor: RefObject<HTMLElement | null> | HTMLElement | null | undefined, floatingRef: RefObject<HTMLElement | null>, { open, placement, offset, padding, shiftMainAxis, capHeight, onAnchorHidden }: UseAnchoredPositionOptions): () => void;
52
64
 
53
- export { type UseAnchoredPositionOptions, resolveAnchor, useAnchoredPosition };
65
+ export { type UseAnchoredPositionOptions, resolveAnchor, useAnchoredPosition, zoomOf };
@@ -33,11 +33,16 @@ function translationOf(el) {
33
33
  if (!t || t === "none" || typeof DOMMatrixReadOnly === "undefined") return { x: 0, y: 0 };
34
34
  try {
35
35
  const m = new DOMMatrixReadOnly(t);
36
- return { x: m.m41, y: m.m42 };
36
+ const z = zoomOf(el);
37
+ return { x: m.m41 * z, y: m.m42 * z };
37
38
  } catch {
38
39
  return { x: 0, y: 0 };
39
40
  }
40
41
  }
42
+ function zoomOf(el) {
43
+ const z = el.currentCSSZoom;
44
+ return typeof z === "number" && Number.isFinite(z) && z > 0 ? z : 1;
45
+ }
41
46
  const signed = (n) => `${n < 0 ? "-" : "+"} ${Math.round(Math.abs(n) * 100) / 100}px`;
42
47
  const prepared = /* @__PURE__ */ new WeakSet();
43
48
  function useAnchoredPosition(anchor, floatingRef, { open, placement = "bottom-start", offset = 6, padding = 8, shiftMainAxis = true, capHeight = false, onAnchorHidden }) {
@@ -62,8 +67,9 @@ function useAnchoredPosition(anchor, floatingRef, { open, placement = "bottom-st
62
67
  const s = floating.style;
63
68
  if (keepPlace && current.anchor.isConnected) {
64
69
  const a = current.anchor.getBoundingClientRect();
65
- s.top = `${Math.round((current.yEdge === "bottom" ? a.bottom : a.top) + current.dy)}px`;
66
- s.left = `${Math.round((current.xEdge === "right" ? a.right : a.left) + current.dx)}px`;
70
+ const z = zoomOf(floating);
71
+ s.top = `${Math.round(((current.yEdge === "bottom" ? a.bottom : a.top) + current.dy) / z)}px`;
72
+ s.left = `${Math.round(((current.xEdge === "right" ? a.right : a.left) + current.dx) / z)}px`;
67
73
  }
68
74
  s.removeProperty("position-anchor");
69
75
  }
@@ -91,6 +97,7 @@ function useAnchoredPosition(anchor, floatingRef, { open, placement = "bottom-st
91
97
  written.current = "";
92
98
  }
93
99
  const { placement: p, offset: o, padding: pad, shiftMainAxis: keepInView, capHeight: cap } = opts.current;
100
+ const zoom = floating.getClientRects().length > 0 ? zoomOf(floating) : zoomOf(anchorEl);
94
101
  if (cap) {
95
102
  if (!heightCap.current) {
96
103
  const own = getComputedStyle(floating);
@@ -104,7 +111,7 @@ function useAnchoredPosition(anchor, floatingRef, { open, placement = "bottom-st
104
111
  };
105
112
  if (setOverflow) s.overflowY = "auto";
106
113
  }
107
- const room = `${Math.max(0, window.innerHeight - 2 * pad)}px`;
114
+ const room = `${Math.max(0, (window.innerHeight - 2 * pad * zoom) / zoom)}px`;
108
115
  const max = heightCap.current.ownMaxHeight ? `min(${room}, ${heightCap.current.ownMaxHeight})` : room;
109
116
  if (max !== heightCap.current.written) {
110
117
  s.maxHeight = max;
@@ -114,19 +121,19 @@ function useAnchoredPosition(anchor, floatingRef, { open, placement = "bottom-st
114
121
  uncap();
115
122
  }
116
123
  const a = anchorEl.getBoundingClientRect();
117
- const size = { width: floating.offsetWidth, height: floating.offsetHeight };
124
+ const size = { width: floating.offsetWidth * zoom, height: floating.offsetHeight * zoom };
118
125
  const pos = computePosition(
119
126
  { x: a.x, y: a.y, width: a.width, height: a.height },
120
127
  size,
121
128
  { width: window.innerWidth, height: window.innerHeight },
122
- { placement: p, offset: o, padding: pad, shiftMainAxis: keepInView, stickySide: side.current ?? void 0 }
129
+ { placement: p, offset: o * zoom, padding: pad * zoom, shiftMainAxis: keepInView, stickySide: side.current ?? void 0 }
123
130
  );
124
131
  const measured = size.width > 0 || size.height > 0;
125
132
  if (measured && session.current) side.current = pos.side;
126
133
  if (floating.dataset.placement !== pos.placement) floating.dataset.placement = pos.placement;
127
134
  const writeFixed = (at) => {
128
- const left2 = `${Math.round(at.x)}px`;
129
- const top2 = `${Math.round(at.y)}px`;
135
+ const left2 = `${Math.round(at.x / zoom)}px`;
136
+ const top2 = `${Math.round(at.y / zoom)}px`;
130
137
  const key2 = `fixed ${left2} ${top2}`;
131
138
  if (key2 === written.current) return;
132
139
  s.left = left2;
@@ -153,8 +160,8 @@ function useAnchoredPosition(anchor, floatingRef, { open, placement = "bottom-st
153
160
  current.xEdge = pos.side === "right" ? "right" : "left";
154
161
  current.dy = pos.y - (current.yEdge === "bottom" ? a.bottom : a.top);
155
162
  current.dx = pos.x - (current.xEdge === "right" ? a.right : a.left);
156
- const top = `round(anchor(${current.yEdge}) ${signed(current.dy)}, 1px)`;
157
- const left = `round(anchor(${current.xEdge}) ${signed(current.dx)}, 1px)`;
163
+ const top = `round(anchor(${current.yEdge}) ${signed(current.dy / zoom)}, 1px)`;
164
+ const left = `round(anchor(${current.xEdge}) ${signed(current.dx / zoom)}, 1px)`;
158
165
  const key = `anchored ${top} ${left}`;
159
166
  if (key !== written.current) {
160
167
  s.top = top;
@@ -221,5 +228,6 @@ function useAnchoredPosition(anchor, floatingRef, { open, placement = "bottom-st
221
228
  }
222
229
  export {
223
230
  resolveAnchor,
224
- useAnchoredPosition
231
+ useAnchoredPosition,
232
+ zoomOf
225
233
  };
@@ -1,3 +1,8 @@
1
- declare function useDialog(open: boolean, onClose?: () => void): React.RefObject<HTMLDialogElement>;
1
+ /**
2
+ * `keepOpen` is asked when the browser closed the dialog on its own (Escape or a close request
3
+ * nobody cancelled, a `method="dialog"` form). Returning `true` reopens it instead of calling
4
+ * `onClose`: a form dialog with unsaved input or a save in flight (HAR-2029).
5
+ */
6
+ declare function useDialog(open: boolean, onClose?: () => void, keepOpen?: () => boolean): React.RefObject<HTMLDialogElement>;
2
7
 
3
8
  export { useDialog };