@tensor_1/react 2.26.2 → 2.27.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 (68) hide show
  1. package/README.md +51 -0
  2. package/dist/calendar-model.d.ts +4 -2
  3. package/dist/components/AppShell.d.ts +1 -1
  4. package/dist/components/AppShell.js +6 -2
  5. package/dist/components/Button.d.ts +28 -4
  6. package/dist/components/Button.js +19 -9
  7. package/dist/components/Card.d.ts +7 -1
  8. package/dist/components/Card.js +3 -3
  9. package/dist/components/Chip.d.ts +43 -0
  10. package/dist/components/Chip.js +73 -0
  11. package/dist/components/CollapsibleSection.d.ts +19 -1
  12. package/dist/components/CollapsibleSection.js +5 -2
  13. package/dist/components/CollapsibleSectionState.d.ts +11 -0
  14. package/dist/components/CollapsibleSectionState.js +67 -0
  15. package/dist/components/CommandPalette.d.ts +1 -1
  16. package/dist/components/CommandPalette.js +4 -1
  17. package/dist/components/DateRangePicker.js +3 -1
  18. package/dist/components/Dialogs.d.ts +54 -3
  19. package/dist/components/Dialogs.js +167 -6
  20. package/dist/components/Drawer.d.ts +6 -1
  21. package/dist/components/Drawer.js +5 -2
  22. package/dist/components/EmojiPicker.d.ts +3 -0
  23. package/dist/components/EmojiPicker.js +109 -54
  24. package/dist/components/Kbd.d.ts +27 -0
  25. package/dist/components/Kbd.js +45 -0
  26. package/dist/components/Menu.d.ts +51 -0
  27. package/dist/components/Menu.js +177 -0
  28. package/dist/components/Modal.d.ts +1 -1
  29. package/dist/components/Modal.js +4 -1
  30. package/dist/components/Pagination.d.ts +36 -5
  31. package/dist/components/Pagination.js +59 -37
  32. package/dist/components/Peek.d.ts +1 -1
  33. package/dist/components/Peek.js +6 -1
  34. package/dist/components/Popover.d.ts +15 -5
  35. package/dist/components/Popover.js +66 -6
  36. package/dist/components/SchedulingCalendar.d.ts +47 -2
  37. package/dist/components/SchedulingCalendar.js +69 -22
  38. package/dist/components/SearchSuggest.d.ts +1 -1
  39. package/dist/components/SearchSuggest.js +4 -1
  40. package/dist/components/Sidebar.d.ts +7 -2
  41. package/dist/components/Sidebar.js +15 -4
  42. package/dist/components/Stat.d.ts +16 -0
  43. package/dist/components/Stat.js +12 -5
  44. package/dist/components/Steps.d.ts +48 -0
  45. package/dist/components/Steps.js +42 -0
  46. package/dist/components/Toast.d.ts +2 -2
  47. package/dist/components/Toast.js +7 -2
  48. package/dist/components/Tooltip.d.ts +10 -4
  49. package/dist/components/Tooltip.js +2 -2
  50. package/dist/{emoji-image-WQLh8ZQP.d.cts → emoji-image-6mPZ15Zy.d.cts} +4 -1
  51. package/dist/emoji.cjs +326 -76
  52. package/dist/emoji.d.cts +3 -3
  53. package/dist/hooks/useAnchoredPosition.d.ts +13 -5
  54. package/dist/hooks/useAnchoredPosition.js +146 -15
  55. package/dist/index.cjs +2278 -1372
  56. package/dist/index.d.cts +622 -176
  57. package/dist/index.d.ts +10 -4
  58. package/dist/index.js +24 -2
  59. package/dist/labels-context.d.ts +72 -0
  60. package/dist/labels-context.js +23 -0
  61. package/dist/link.d.ts +20 -0
  62. package/dist/link.js +9 -0
  63. package/dist/{overlay-position-CEGSz6Z2.d.cts → overlay-position-Cjv5E7mn.d.cts} +7 -0
  64. package/dist/overlay-position.d.ts +7 -0
  65. package/dist/overlay-position.js +7 -2
  66. package/dist/rich-text.cjs +324 -74
  67. package/dist/rich-text.d.cts +3 -3
  68. package/package.json +1 -1
@@ -1,10 +1,55 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- import { useEffect, useId, useState } from "react";
3
+ import { useEffect, useId, useRef, useState } from "react";
4
4
  import { Button } from "./Button.js";
5
5
  import { Input } from "./Input.js";
6
6
  import { Modal } from "./Modal.js";
7
- function ConfirmDialog({ open, title, description, confirmLabel, cancelLabel, closeLabel, onConfirm, onCancel, pending, destructive }) {
7
+ import { Popover } from "./Popover.js";
8
+ import { fillLabel } from "../fill-label.js";
9
+ import { resolveAnchor } from "../hooks/useAnchoredPosition.js";
10
+ import { useUixLabels } from "../labels-context.js";
11
+ function ConfirmDialog({
12
+ open,
13
+ title,
14
+ description,
15
+ confirmLabel,
16
+ cancelLabel,
17
+ closeLabel,
18
+ onConfirm,
19
+ onCancel,
20
+ pending,
21
+ destructive,
22
+ children,
23
+ error,
24
+ initialFocus,
25
+ tertiaryLabel,
26
+ onTertiary,
27
+ typeToConfirm,
28
+ typeToConfirmLabel: typeToConfirmLabelProp,
29
+ compensation,
30
+ compensationLabel: compensationLabelProp
31
+ }) {
32
+ const uixLabels = useUixLabels();
33
+ const typeToConfirmLabel = typeToConfirmLabelProp ?? uixLabels.confirmDialog?.typeToConfirm ?? "Type {value} to confirm";
34
+ const compensationLabel = compensationLabelProp ?? uixLabels.confirmDialog?.compensation ?? "How to undo this";
35
+ const id = useId();
36
+ const bodyId = `${id}-body`;
37
+ const typedId = `${id}-typed`;
38
+ const cancelRef = useRef(null);
39
+ const confirmRef = useRef(null);
40
+ const typedRef = useRef(null);
41
+ const [typed, setTyped] = useState("");
42
+ const typedMatches = typeToConfirm == null || typed === typeToConfirm;
43
+ const hasBody = description != null || children != null || compensation != null;
44
+ useEffect(() => {
45
+ if (open) setTyped("");
46
+ }, [open, typeToConfirm]);
47
+ useEffect(() => {
48
+ if (!open) return;
49
+ const focus = initialFocus ?? (destructive ? "cancel" : void 0);
50
+ const target = typeToConfirm != null ? typedRef.current : focus === "cancel" ? cancelRef.current : focus === "confirm" ? confirmRef.current : null;
51
+ target?.focus();
52
+ }, [open, initialFocus, destructive, typeToConfirm]);
8
53
  return /* @__PURE__ */ jsx(
9
54
  Modal,
10
55
  {
@@ -13,11 +58,126 @@ function ConfirmDialog({ open, title, description, confirmLabel, cancelLabel, cl
13
58
  title,
14
59
  closeLabel,
15
60
  role: "alertdialog",
61
+ "aria-describedby": hasBody ? bodyId : void 0,
16
62
  footer: /* @__PURE__ */ jsxs(Fragment, { children: [
17
- /* @__PURE__ */ jsx(Button, { type: "button", onClick: onCancel, disabled: pending, children: cancelLabel }),
18
- /* @__PURE__ */ jsx(Button, { type: "button", variant: destructive ? "danger" : "primary", onClick: () => void onConfirm(), loading: pending, children: confirmLabel })
63
+ tertiaryLabel != null && onTertiary && /* @__PURE__ */ jsx(Button, { type: "button", className: "uix-confirm__tertiary", onClick: () => void onTertiary(), disabled: pending, children: tertiaryLabel }),
64
+ /* @__PURE__ */ jsx(Button, { ref: cancelRef, type: "button", onClick: onCancel, disabled: pending, children: cancelLabel }),
65
+ /* @__PURE__ */ jsx(Button, { ref: confirmRef, type: "button", variant: destructive ? "danger" : "primary", onClick: () => {
66
+ if (typedMatches) void onConfirm();
67
+ }, loading: pending, disabled: !typedMatches, children: confirmLabel })
19
68
  ] }),
20
- children: description
69
+ children: /* @__PURE__ */ jsxs("div", { className: "uix-confirm", children: [
70
+ hasBody && /* @__PURE__ */ jsxs("div", { id: bodyId, className: "uix-confirm__body", children: [
71
+ description != null && /* @__PURE__ */ jsx("div", { children: description }),
72
+ children,
73
+ compensation != null && /* @__PURE__ */ jsxs("div", { className: "uix-confirm__compensation", children: [
74
+ /* @__PURE__ */ jsx("strong", { className: "uix-confirm__compensation-title", children: compensationLabel }),
75
+ /* @__PURE__ */ jsx("div", { children: compensation })
76
+ ] })
77
+ ] }),
78
+ typeToConfirm != null && /* @__PURE__ */ jsxs("div", { className: "uix-field", children: [
79
+ /* @__PURE__ */ jsx("label", { className: "uix-field__label", htmlFor: typedId, children: fillLabel(typeToConfirmLabel, { value: typeToConfirm }) }),
80
+ /* @__PURE__ */ jsx(Input, { ref: typedRef, id: typedId, value: typed, autoComplete: "off", spellCheck: false, onChange: (event) => setTyped(event.currentTarget.value) })
81
+ ] }),
82
+ error != null && /* @__PURE__ */ jsx("div", { className: "uix-confirm__error", role: "alert", children: error })
83
+ ] })
84
+ }
85
+ );
86
+ }
87
+ function Popconfirm({
88
+ open,
89
+ anchor,
90
+ title,
91
+ description,
92
+ confirmLabel,
93
+ cancelLabel,
94
+ onConfirm,
95
+ onCancel,
96
+ pending,
97
+ destructive,
98
+ compensation,
99
+ compensationLabel: compensationLabelProp,
100
+ placement = "bottom-start"
101
+ }) {
102
+ const uixLabels = useUixLabels();
103
+ const compensationLabel = compensationLabelProp ?? uixLabels.confirmDialog?.compensation ?? "How to undo this";
104
+ const id = useId();
105
+ const panelId = `${id}-popconfirm`;
106
+ const cancelRef = useRef(null);
107
+ const onCancelRef = useRef(onCancel);
108
+ onCancelRef.current = onCancel;
109
+ const hasBody = description != null || compensation != null;
110
+ useEffect(() => {
111
+ const panel = document.getElementById(panelId);
112
+ if (!panel) return;
113
+ const shown = () => {
114
+ try {
115
+ return panel.matches(":popover-open");
116
+ } catch {
117
+ return false;
118
+ }
119
+ };
120
+ if (!open) {
121
+ if (shown()) {
122
+ const focusInside = panel.contains(document.activeElement);
123
+ try {
124
+ panel.hidePopover();
125
+ } catch {
126
+ }
127
+ if (focusInside) resolveAnchor(anchor)?.focus();
128
+ }
129
+ return;
130
+ }
131
+ if (!shown()) {
132
+ try {
133
+ panel.showPopover();
134
+ } catch {
135
+ }
136
+ }
137
+ cancelRef.current?.focus();
138
+ const onKey = (event) => {
139
+ if (event.key !== "Escape") return;
140
+ event.preventDefault();
141
+ event.stopPropagation();
142
+ onCancelRef.current();
143
+ };
144
+ const onPointer = (event) => {
145
+ const target = event.target;
146
+ if (panel.contains(target) || resolveAnchor(anchor)?.contains(target)) return;
147
+ onCancelRef.current();
148
+ };
149
+ document.addEventListener("keydown", onKey, true);
150
+ document.addEventListener("pointerdown", onPointer, true);
151
+ return () => {
152
+ document.removeEventListener("keydown", onKey, true);
153
+ document.removeEventListener("pointerdown", onPointer, true);
154
+ };
155
+ }, [open, anchor, panelId]);
156
+ return /* @__PURE__ */ jsxs(
157
+ Popover,
158
+ {
159
+ id: panelId,
160
+ popover: "manual",
161
+ anchor,
162
+ placement,
163
+ role: "dialog",
164
+ "aria-labelledby": `${id}-title`,
165
+ "aria-describedby": hasBody ? `${id}-body` : void 0,
166
+ className: "uix-popconfirm",
167
+ children: [
168
+ /* @__PURE__ */ jsx("div", { id: `${id}-title`, className: "uix-popconfirm__title", children: title }),
169
+ hasBody && /* @__PURE__ */ jsxs("div", { id: `${id}-body`, className: "uix-popconfirm__body", children: [
170
+ description != null && /* @__PURE__ */ jsx("div", { children: description }),
171
+ compensation != null && /* @__PURE__ */ jsxs("div", { className: "uix-confirm__compensation", children: [
172
+ /* @__PURE__ */ jsx("strong", { className: "uix-confirm__compensation-title", children: compensationLabel }),
173
+ /* @__PURE__ */ jsx("div", { children: compensation })
174
+ ] })
175
+ ] }),
176
+ /* @__PURE__ */ jsxs("div", { className: "uix-popconfirm__actions", children: [
177
+ /* @__PURE__ */ jsx(Button, { ref: cancelRef, type: "button", size: "sm", onClick: onCancel, disabled: pending, children: cancelLabel }),
178
+ /* @__PURE__ */ jsx(Button, { type: "button", size: "sm", variant: destructive ? "danger" : "primary", onClick: () => void onConfirm(), loading: pending, children: confirmLabel })
179
+ ] })
180
+ ]
21
181
  }
22
182
  );
23
183
  }
@@ -40,7 +200,7 @@ function PromptDialog({ open, title, description, inputLabel, defaultValue = "",
40
200
  };
41
201
  return /* @__PURE__ */ jsx(Modal, { open, onClose: onCancel, title, closeLabel, children: /* @__PURE__ */ jsxs("form", { className: "uix-prompt", onSubmit: handleSubmit, children: [
42
202
  description != null && /* @__PURE__ */ jsx("div", { children: description }),
43
- /* @__PURE__ */ jsx("label", { className: "uix-label", htmlFor: inputId, children: inputLabel }),
203
+ /* @__PURE__ */ jsx("label", { className: "uix-field__label", htmlFor: inputId, children: inputLabel }),
44
204
  /* @__PURE__ */ jsx(Input, { id: inputId, value, placeholder, "aria-describedby": error ? errorId : void 0, invalid: error != null, onChange: (event) => setValue(event.currentTarget.value), autoFocus: true }),
45
205
  error != null && /* @__PURE__ */ jsx("div", { id: errorId, className: "uix-field__error", role: "alert", children: error }),
46
206
  /* @__PURE__ */ jsxs("div", { className: "uix-dialog__footer uix-prompt__actions", children: [
@@ -51,5 +211,6 @@ function PromptDialog({ open, title, description, inputLabel, defaultValue = "",
51
211
  }
52
212
  export {
53
213
  ConfirmDialog,
214
+ Popconfirm,
54
215
  PromptDialog
55
216
  };
@@ -12,10 +12,15 @@ interface DrawerProps extends Omit<HTMLAttributes<HTMLDialogElement>, 'title'> {
12
12
  * can't discard it; Escape and the close button still close. Default `true`.
13
13
  */
14
14
  dismissOnBackdrop?: boolean;
15
+ /**
16
+ * Edge the drawer slides from (HAR-1351). `end` (default) is the side sheet; `start` is
17
+ * the phone navigation drawer; `bottom` is a bottom sheet capped at 85% of the viewport.
18
+ */
19
+ side?: 'end' | 'start' | 'bottom';
15
20
  title?: ReactNode;
16
21
  children?: ReactNode;
17
22
  footer?: ReactNode;
18
23
  }
19
- declare function Drawer({ open, onClose, dismissOnBackdrop, title, children, footer, closeLabel, className, onClick, ...rest }: DrawerProps): react.JSX.Element;
24
+ declare function Drawer({ open, onClose, dismissOnBackdrop, side, title, children, footer, closeLabel: closeLabelProp, className, onClick, ...rest }: DrawerProps): react.JSX.Element;
20
25
 
21
26
  export { Drawer, type DrawerProps };
@@ -2,13 +2,16 @@
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useId } from "react";
4
4
  import { cx } from "../cx.js";
5
+ import { useUixLabels } from "../labels-context.js";
5
6
  import { useDialog } from "../hooks/useDialog.js";
6
7
  import { backdropDismiss } from "../hooks/backdropDismiss.js";
7
8
  const CloseIcon = () => /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M4 4l8 8M12 4l-8 8" }) });
8
- function Drawer({ open, onClose, dismissOnBackdrop = true, title, children, footer, closeLabel = "Close drawer", className, onClick, ...rest }) {
9
+ function Drawer({ open, onClose, dismissOnBackdrop = true, side = "end", title, children, footer, closeLabel: closeLabelProp, className, onClick, ...rest }) {
10
+ const uixLabels = useUixLabels();
11
+ const closeLabel = closeLabelProp ?? uixLabels.drawer?.close ?? "Close drawer";
9
12
  const ref = useDialog(open, onClose);
10
13
  const titleId = useId();
11
- return /* @__PURE__ */ jsxs("dialog", { ref, className: cx("uix-drawer", className), "aria-labelledby": title ? titleId : void 0, ...rest, onClick: backdropDismiss(onClose, onClick, dismissOnBackdrop), children: [
14
+ return /* @__PURE__ */ jsxs("dialog", { ref, className: cx("uix-drawer", side !== "end" && `uix-drawer--${side}`, className), "aria-labelledby": title ? titleId : void 0, ...rest, onClick: backdropDismiss(onClose, onClick, dismissOnBackdrop), children: [
12
15
  (title != null || onClose) && /* @__PURE__ */ jsxs("div", { className: "uix-drawer__header", children: [
13
16
  title && /* @__PURE__ */ jsx("h2", { id: titleId, style: { fontWeight: 600, fontSize: "var(--uix-text-h3)", fontFamily: "inherit", letterSpacing: "inherit", lineHeight: "inherit" }, children: title }),
14
17
  onClose && /* @__PURE__ */ jsx(
@@ -54,6 +54,9 @@ declare const EMOJI_GRID_COLUMNS = 8;
54
54
  * Emoji picker in a native popover: search, quick picks, recently used and the
55
55
  * Unicode categories, with names in English or German. The emoji dataset loads on
56
56
  * first open. The grid is one tab stop; arrow keys, Home and End move within it.
57
+ * The picker is one size in every state (loading, searching, failed), so it keeps the
58
+ * side it opened on; it stays attached to its trigger while the page scrolls and
59
+ * closes once the trigger is scrolled out of view.
57
60
  */
58
61
  declare function EmojiPicker({ onSelect, trigger, labels: labelOverrides, locale, quickPicks, loadData, recentStorageKey, placement, onOpenChange, className, emojiImageBaseUrl, }: EmojiPickerProps): react.JSX.Element;
59
62
 
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
- import { cloneElement, isValidElement, useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
3
+ import { cloneElement, isValidElement, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
4
4
  import { cx } from "../cx.js";
5
5
  import { Popover } from "./Popover.js";
6
6
  import {
@@ -23,6 +23,14 @@ const DEFAULT_EMOJI_PICKER_LABELS = {
23
23
  loadFailed: "Emoji could not be loaded."
24
24
  };
25
25
  const EMOJI_GRID_COLUMNS = 8;
26
+ const useIsomorphicLayoutEffect = typeof document !== "undefined" ? useLayoutEffect : useEffect;
27
+ function revealInGrid(grid, cell) {
28
+ const g = grid.getBoundingClientRect();
29
+ const c = cell.getBoundingClientRect();
30
+ const top = g.top + (parseFloat(getComputedStyle(grid).scrollPaddingTop) || 0);
31
+ if (c.top < top) grid.scrollTop -= top - c.top;
32
+ else if (c.bottom > g.bottom) grid.scrollTop += c.bottom - g.bottom;
33
+ }
26
34
  function EmojiPicker({
27
35
  onSelect,
28
36
  trigger,
@@ -44,6 +52,8 @@ function EmojiPicker({
44
52
  const searchRef = useRef(null);
45
53
  const gridRef = useRef(null);
46
54
  const pointerWasOpen = useRef(null);
55
+ const closedByAnchor = useRef(false);
56
+ const pendingJump = useRef(null);
47
57
  const [open, setOpen] = useState(false);
48
58
  const [data, setData] = useState(null);
49
59
  const [failed, setFailed] = useState(false);
@@ -67,10 +77,14 @@ function EmojiPicker({
67
77
  setOpen(nowOpen);
68
78
  onOpenChange?.(nowOpen);
69
79
  if (nowOpen) {
80
+ closedByAnchor.current = false;
81
+ pendingJump.current = null;
70
82
  setRecent(readRecentEmoji(recentStorageKey));
71
83
  setQuery("");
72
84
  setActive(0);
73
- requestAnimationFrame(() => searchRef.current?.focus());
85
+ requestAnimationFrame(() => searchRef.current?.focus({ preventScroll: true }));
86
+ } else if (closedByAnchor.current) {
87
+ closedByAnchor.current = false;
74
88
  } else if (el.contains(document.activeElement) || document.activeElement === document.body) {
75
89
  focusTrigger();
76
90
  }
@@ -102,10 +116,7 @@ function EmojiPicker({
102
116
  const el = popoverEl();
103
117
  if (el && isOpen(el)) el.hidePopover();
104
118
  };
105
- const sections = useMemo(() => {
106
- if (query.trim()) {
107
- return data ? [{ key: "results", title: labels.search, emojis: searchEmoji(data, query).map((e) => e.emoji) }] : [];
108
- }
119
+ const browseSections = useMemo(() => {
109
120
  const list = [];
110
121
  if (quickPicks?.length) list.push({ key: "quick", title: labels.quickPicks, emojis: [...new Set(quickPicks)] });
111
122
  if (recent.length) list.push({ key: "recent", title: labels.recent, emojis: recent });
@@ -115,7 +126,11 @@ function EmojiPicker({
115
126
  });
116
127
  }
117
128
  return list;
118
- }, [query, data, quickPicks, recent, labels.search, labels.quickPicks, labels.recent]);
129
+ }, [data, quickPicks, recent, labels.quickPicks, labels.recent]);
130
+ const sections = useMemo(() => {
131
+ if (!query.trim()) return browseSections;
132
+ return data ? [{ key: "results", title: labels.search, emojis: searchEmoji(data, query).map((e) => e.emoji) }] : [];
133
+ }, [query, data, browseSections, labels.search]);
119
134
  const flat = useMemo(() => sections.flatMap((s) => s.emojis), [sections]);
120
135
  const sectionStart = useMemo(() => {
121
136
  const starts = /* @__PURE__ */ new Map();
@@ -132,13 +147,43 @@ function EmojiPicker({
132
147
  onSelect(emoji);
133
148
  hide();
134
149
  };
135
- const focusCell = useCallback((index) => {
150
+ const focusCell = useCallback((index, reveal = true) => {
136
151
  setActive(index);
137
152
  const find = () => gridRef.current?.querySelector(`[data-emoji-index="${index}"]`);
153
+ const move = (cell2) => {
154
+ cell2.focus({ preventScroll: true });
155
+ if (reveal && gridRef.current) revealInGrid(gridRef.current, cell2);
156
+ };
138
157
  const cell = find();
139
- if (cell) cell.focus();
140
- else requestAnimationFrame(() => find()?.focus());
158
+ if (cell) move(cell);
159
+ else requestAnimationFrame(() => {
160
+ const later = find();
161
+ if (later) move(later);
162
+ });
141
163
  }, []);
164
+ const showSection = (key) => {
165
+ const grid = gridRef.current;
166
+ const section = grid?.querySelector(`[data-section="${key}"]`);
167
+ if (!grid || !section) return;
168
+ grid.scrollTop += section.getBoundingClientRect().top - grid.getBoundingClientRect().top;
169
+ const start = sectionStart.get(key);
170
+ if (start !== void 0) focusCell(start, false);
171
+ };
172
+ const onCategory = (key) => {
173
+ if (!query.trim()) {
174
+ showSection(key);
175
+ return;
176
+ }
177
+ pendingJump.current = key;
178
+ setQuery("");
179
+ setActive(0);
180
+ };
181
+ useIsomorphicLayoutEffect(() => {
182
+ const key = pendingJump.current;
183
+ if (key === null || query.trim()) return;
184
+ pendingJump.current = null;
185
+ showSection(key);
186
+ }, [query, sections]);
142
187
  const onGridKeyDown = (event) => {
143
188
  const target = event.target;
144
189
  const index = Number(target.dataset.emojiIndex ?? -1);
@@ -146,7 +191,7 @@ function EmojiPicker({
146
191
  const next = emojiGridMove(index, event.key, flat.length, EMOJI_GRID_COLUMNS);
147
192
  if (event.key === "ArrowUp" && next === null) {
148
193
  event.preventDefault();
149
- searchRef.current?.focus();
194
+ searchRef.current?.focus({ preventScroll: true });
150
195
  return;
151
196
  }
152
197
  if (next === null) return;
@@ -202,6 +247,10 @@ function EmojiPicker({
202
247
  className: "uix-emoji-picker-popover",
203
248
  role: "dialog",
204
249
  "aria-label": labels.dialog,
250
+ closeWhenAnchorHidden: true,
251
+ onAnchorHidden: () => {
252
+ closedByAnchor.current = true;
253
+ },
205
254
  children: open ? /* @__PURE__ */ jsxs("div", { className: "uix-emoji-picker uix-emoji-picker--full", lang: locale, children: [
206
255
  /* @__PURE__ */ jsx(
207
256
  "input",
@@ -221,50 +270,56 @@ function EmojiPicker({
221
270
  spellCheck: false
222
271
  }
223
272
  ),
224
- data && !query.trim() ? /* @__PURE__ */ jsx("div", { className: "uix-emoji-picker__nav", role: "group", "aria-label": labels.categories, children: sections.map((section) => /* @__PURE__ */ jsx(
225
- "button",
273
+ /* @__PURE__ */ jsx(
274
+ "div",
226
275
  {
227
- type: "button",
228
- className: "uix-emoji-picker__nav-btn",
229
- "aria-label": section.title,
230
- title: section.title,
231
- onClick: () => {
232
- const start = section.emojis.length ? sectionStart.get(section.key) ?? -1 : -1;
233
- gridRef.current?.querySelector(`[data-section="${section.key}"]`)?.scrollIntoView({ block: "start" });
234
- if (start >= 0) focusCell(start);
235
- },
236
- children: /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: section.emojis[0] ? renderEmoji(section.emojis[0]) : null })
237
- },
238
- section.key
239
- )) }) : null,
240
- /* @__PURE__ */ jsx("div", { className: "uix-emoji-picker__grid", ref: gridRef, onKeyDown: onGridKeyDown, children: sections.map((section) => {
241
- const start = offset;
242
- offset += section.emojis.length;
243
- if (!section.emojis.length) return null;
244
- const headingId = `${id}-${section.key}`;
245
- return /* @__PURE__ */ jsxs("section", { className: "uix-emoji-picker__section", "data-section": section.key, "aria-labelledby": headingId, children: [
246
- /* @__PURE__ */ jsx("h3", { id: headingId, className: "uix-emoji-picker__heading", children: section.title }),
247
- /* @__PURE__ */ jsx("div", { className: "uix-emoji-picker__cells", role: "group", "aria-labelledby": headingId, children: section.emojis.map((emoji, i) => {
248
- const index = start + i;
249
- return /* @__PURE__ */ jsx(
250
- "button",
251
- {
252
- type: "button",
253
- className: "uix-emoji-picker__btn",
254
- "data-emoji-index": index,
255
- tabIndex: index === activeIndex ? 0 : -1,
256
- "aria-label": names?.get(emoji) ?? emoji,
257
- title: names?.get(emoji),
258
- onClick: () => choose(emoji),
259
- onFocus: () => setActive(index),
260
- children: renderEmoji(emoji)
261
- },
262
- `${section.key}-${emoji}`
263
- );
264
- }) })
265
- ] }, section.key);
266
- }) }),
267
- /* @__PURE__ */ jsx("p", { className: "uix-emoji-picker__status", role: "status", children: status })
276
+ className: "uix-emoji-picker__nav",
277
+ role: data ? "group" : void 0,
278
+ "aria-label": data ? labels.categories : void 0,
279
+ children: data ? browseSections.map((section) => /* @__PURE__ */ jsx(
280
+ "button",
281
+ {
282
+ type: "button",
283
+ className: "uix-emoji-picker__nav-btn",
284
+ "aria-label": section.title,
285
+ title: section.title,
286
+ onClick: () => onCategory(section.key),
287
+ children: /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: section.emojis[0] ? renderEmoji(section.emojis[0]) : null })
288
+ },
289
+ section.key
290
+ )) : null
291
+ }
292
+ ),
293
+ /* @__PURE__ */ jsxs("div", { className: "uix-emoji-picker__grid", ref: gridRef, onKeyDown: onGridKeyDown, children: [
294
+ sections.map((section) => {
295
+ const start = offset;
296
+ offset += section.emojis.length;
297
+ if (!section.emojis.length) return null;
298
+ const headingId = `${id}-${section.key}`;
299
+ return /* @__PURE__ */ jsxs("section", { className: "uix-emoji-picker__section", "data-section": section.key, "aria-labelledby": headingId, children: [
300
+ /* @__PURE__ */ jsx("h3", { id: headingId, className: "uix-emoji-picker__heading", children: section.title }),
301
+ /* @__PURE__ */ jsx("div", { className: "uix-emoji-picker__cells", role: "group", "aria-labelledby": headingId, children: section.emojis.map((emoji, i) => {
302
+ const index = start + i;
303
+ return /* @__PURE__ */ jsx(
304
+ "button",
305
+ {
306
+ type: "button",
307
+ className: "uix-emoji-picker__btn",
308
+ "data-emoji-index": index,
309
+ tabIndex: index === activeIndex ? 0 : -1,
310
+ "aria-label": names?.get(emoji) ?? emoji,
311
+ title: names?.get(emoji),
312
+ onClick: () => choose(emoji),
313
+ onFocus: () => setActive(index),
314
+ children: renderEmoji(emoji)
315
+ },
316
+ `${section.key}-${emoji}`
317
+ );
318
+ }) })
319
+ ] }, section.key);
320
+ }),
321
+ /* @__PURE__ */ jsx("p", { className: "uix-emoji-picker__status", role: "status", children: status })
322
+ ] })
268
323
  ] }) : null
269
324
  }
270
325
  )
@@ -0,0 +1,27 @@
1
+ import * as react from 'react';
2
+ import { HTMLAttributes, ReactNode } from 'react';
3
+
4
+ interface KbdProps extends HTMLAttributes<HTMLElement> {
5
+ children?: ReactNode;
6
+ }
7
+ /** One keyboard key over `.uix-kbd` (HAR-1361; TENSOR C14, MOTUS B-A16). */
8
+ declare function Kbd({ className, children, ...props }: KbdProps): react.JSX.Element;
9
+ type KbdPlatform = 'mac' | 'other';
10
+ interface KbdComboProps extends Omit<HTMLAttributes<HTMLSpanElement>, 'children'> {
11
+ /**
12
+ * Keys in order, e.g. `['Mod', 'K']`. `Mod` is ⌘ on Apple and Ctrl elsewhere; `Shift`,
13
+ * `Alt`, `Enter`, `Escape`, `Backspace`, `Tab` and the arrows get platform glyphs.
14
+ */
15
+ keys: string[];
16
+ /** Fix the platform (tests, docs). Default: detected after mount; the server renders `other`. */
17
+ platform?: KbdPlatform;
18
+ /** Spoken names for keys, e.g. `{ Mod: 'Steuerung' }`; the glyphs stay. */
19
+ keyLabels?: Partial<Record<string, string>>;
20
+ }
21
+ /**
22
+ * A shortcut such as ⌘K / Ctrl+K. Screen readers hear the key names ("Command+K"), not the
23
+ * glyphs. The platform is resolved at display time, after mount, so server and client agree.
24
+ */
25
+ declare function KbdCombo({ keys, platform, keyLabels, className, ...props }: KbdComboProps): react.JSX.Element;
26
+
27
+ export { Kbd, KbdCombo, type KbdComboProps, type KbdPlatform, type KbdProps };
@@ -0,0 +1,45 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useState } from "react";
4
+ import { cx } from "../cx.js";
5
+ function Kbd({ className, children, ...props }) {
6
+ return /* @__PURE__ */ jsx("kbd", { className: cx("uix-kbd", className), ...props, children });
7
+ }
8
+ const KEYS = {
9
+ Mod: { mac: ["\u2318", "Command"], other: ["Ctrl", "Control"] },
10
+ Ctrl: { mac: ["\u2303", "Control"], other: ["Ctrl", "Control"] },
11
+ Shift: { mac: ["\u21E7", "Shift"], other: ["Shift", "Shift"] },
12
+ Alt: { mac: ["\u2325", "Option"], other: ["Alt", "Alt"] },
13
+ Enter: { mac: ["\u21B5", "Enter"], other: ["Enter", "Enter"] },
14
+ Escape: { mac: ["Esc", "Escape"], other: ["Esc", "Escape"] },
15
+ Backspace: { mac: ["\u232B", "Backspace"], other: ["Backspace", "Backspace"] },
16
+ Tab: { mac: ["\u21E5", "Tab"], other: ["Tab", "Tab"] },
17
+ ArrowUp: { mac: ["\u2191", "Up arrow"], other: ["\u2191", "Up arrow"] },
18
+ ArrowDown: { mac: ["\u2193", "Down arrow"], other: ["\u2193", "Down arrow"] },
19
+ ArrowLeft: { mac: ["\u2190", "Left arrow"], other: ["\u2190", "Left arrow"] },
20
+ ArrowRight: { mac: ["\u2192", "Right arrow"], other: ["\u2192", "Right arrow"] }
21
+ };
22
+ const detectPlatform = () => {
23
+ if (typeof navigator === "undefined") return "other";
24
+ const nav = navigator;
25
+ return /mac|iphone|ipad|ipod/i.test(nav.userAgentData?.platform ?? navigator.platform ?? "") ? "mac" : "other";
26
+ };
27
+ function KbdCombo({ keys, platform, keyLabels, className, ...props }) {
28
+ const [detected, setDetected] = useState("other");
29
+ useEffect(() => {
30
+ if (!platform) setDetected(detectPlatform());
31
+ }, [platform]);
32
+ const active = platform ?? detected;
33
+ const parts = keys.map((key) => {
34
+ const known = KEYS[key]?.[active];
35
+ return { glyph: known?.[0] ?? key, spoken: keyLabels?.[key] ?? known?.[1] ?? key };
36
+ });
37
+ return /* @__PURE__ */ jsxs("span", { className: cx("uix-kbd-combo", className), ...props, children: [
38
+ /* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", children: parts.map((p) => p.spoken).join("+") }),
39
+ /* @__PURE__ */ jsx("span", { className: "uix-kbd-combo__keys", "aria-hidden": "true", children: parts.map((p, i) => /* @__PURE__ */ jsx("kbd", { className: "uix-kbd", children: p.glyph }, `${keys[i]}-${i}`)) })
40
+ ] });
41
+ }
42
+ export {
43
+ Kbd,
44
+ KbdCombo
45
+ };
@@ -0,0 +1,51 @@
1
+ import * as react from 'react';
2
+ import { ReactElement, ReactNode } from 'react';
3
+ import { UixRenderLink } from '../link.js';
4
+ import { Placement } from '../overlay-position.js';
5
+
6
+ interface MenuProps {
7
+ /**
8
+ * The menu button, usually a `Button`. It receives `aria-haspopup="menu"`,
9
+ * `aria-expanded`, `aria-controls` and the open/keyboard handlers.
10
+ */
11
+ trigger: ReactElement;
12
+ /** `MenuItem`, `MenuGroup` and `MenuSeparator`. */
13
+ children?: ReactNode;
14
+ /** Controlled open state; pair with `onOpenChange`. */
15
+ open?: boolean;
16
+ defaultOpen?: boolean;
17
+ onOpenChange?: (open: boolean) => void;
18
+ /** Accessible name of the menu. Default: labelled by the trigger. */
19
+ label?: string;
20
+ placement?: Placement;
21
+ className?: string;
22
+ }
23
+ /**
24
+ * A menu button with an APG menu (HAR-1359; TENSOR C1, MOTUS B-A14): ↓/↑ move, Home/End,
25
+ * type-to-find, Enter/Space activate, Esc and Tab close, focus returns to the trigger.
26
+ * The surface is the kit `Popover`, so it escapes clipping containers.
27
+ */
28
+ declare function Menu({ trigger, children, open: controlledOpen, defaultOpen, onOpenChange, label, placement, className }: MenuProps): react.JSX.Element;
29
+ interface MenuItemProps {
30
+ children?: ReactNode;
31
+ /** Runs on click, Enter or Space; the menu then closes and focus returns to the trigger. */
32
+ onSelect?: () => void;
33
+ /** A link item; it navigates instead of running `onSelect`. */
34
+ href?: string;
35
+ renderLink?: UixRenderLink;
36
+ icon?: ReactNode;
37
+ /** Shortcut hint at the end of the row, e.g. `<KbdCombo keys={['Mod', 'D']} />`. */
38
+ shortcut?: ReactNode;
39
+ tone?: 'default' | 'danger';
40
+ disabled?: boolean;
41
+ className?: string;
42
+ }
43
+ declare function MenuItem({ children, onSelect, href, renderLink, icon, shortcut, tone, disabled, className }: MenuItemProps): react.JSX.Element;
44
+ interface MenuGroupProps {
45
+ label: ReactNode;
46
+ children?: ReactNode;
47
+ }
48
+ declare function MenuGroup({ label, children }: MenuGroupProps): react.JSX.Element;
49
+ declare function MenuSeparator(): react.JSX.Element;
50
+
51
+ export { Menu, MenuGroup, type MenuGroupProps, MenuItem, type MenuItemProps, type MenuProps, MenuSeparator };