@stackone/malachite 2.42.1 → 2.43.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.
package/COMPONENTS.md CHANGED
@@ -26,8 +26,8 @@ The catalog of components exported from `@stackone/malachite`. **Reach for one o
26
26
  - **`OverflowRow`** — Horizontal row that collapses items that do not fit into an overflow affordance.
27
27
  - **`ActionBar`** — A row of actions that collapses into a single `ActionMenu` once its container drops below `collapseBelow`. Measures the nearest genuinely-wider ancestor rather than itself, since the row shrinks to its own content. `dividerBefore` renders a vertical rule in the row and a divider in the collapsed menu, so both forms group the actions the same way. _Key props:_ `buttons` (`label`, `onClick`, `icon`, `variant`, `size`, `radius`, `tooltip`, `disabled`, `dividerBefore`), `collapseBelow`, `menuLabel`, `className`.
28
28
  - **`Popover`** — An anchored, non-modal panel for arbitrary content — a workspace switcher, an org flyout. The panel renders in its own shadow root, so malachite components inside it are styled, and joins the top layer like the other overlays. It prefers its `placement` and flips to the opposite side when there is no room, clamping along the other axis. Click-opened panels take focus and hand it back to the trigger on close; `openOn="hover"` also opens on focus and stays open while the pointer or focus is in the panel. A nested Popover counts as inside its parent for outside-press and hover dismissal, and Escape closes the topmost only. A click panel also closes when focus moves somewhere outside it and its trigger, and on close it hands focus back to the trigger only if focus would otherwise fall to the page. _Key props:_ `trigger`, `children`, `open`, `defaultOpen`, `onOpenChange`, `placement` (`bottom-start` | `bottom-end` | `top-start` | `top-center` | `right-start` | `right-end` | `left-start`), `openOn` (`click` | `hover`), `width` (`content` | `trigger`), `padding` (`none` | `small` | `medium`), `anchorRef` (position against another element, e.g. the section a trigger row sits in), `offset` (`small` 8px | `medium` 12px | `large` 14px), `elevation` (`default` | `raised`), `initialFocusRef` (what takes focus on open, in place of the first focusable), `role` (`dialog` | `menu` | `listbox`; `menu` brings no roving focus of its own), `aria-label` or `aria-labelledby` (one is required), `className`. The panel is capped to the viewport width, and only overlays opened from inside it (menus, dropdowns, modals) count as inside for outside-press dismissal. A side placement stays level with its anchor and scrolls within the room below, sliding up only when under 160px is left. Parts for the panel body: `Popover.Header` (`title`, `action`), `Popover.Action` (`icon`, `aria-label`, `onClick`, `disabled`, `tooltip`), `Popover.List` (a scrolling list), `Popover.Section` (a bordered section) and `Popover.Empty` (an empty-state line).
29
- - **`Modal`** — Centred dialog over a scrim, with header slots (icon/title/subtitle/extra), a scrolling body and a pinned footer. Locks background scroll and dismisses on ESC, the close button, or a scrim click unless `closeOnOverlayClick` is off; `busy` suppresses the two pointer routes while a request is in flight and sets `aria-busy`, so a stray click cannot tear the dialog down mid-write — but never ESC, which is the only guaranteed keyboard exit from a focus trap and so always reaches `onClose`. ESC, the Tab focus trap and `aria-modal` all apply to the topmost overlay only, so these may be stacked — a dialog opened from a panel keeps Tab to itself and the panel stops claiming to be modal. Set `role="alertdialog"` for a consequential choice, which asks assistive tech to announce the body and not just the name, and point `aria-describedby` at whichever element carries it. Width comes from the `size` union (`small` 420px | `medium` 612px | `large` 760px) rather than a raw pixel prop. `titleAdornment` sits immediately after the title behind a hairline divider, for naming the thing being acted on — distinct from `headerExtra`, which pins to the far right. `tray` paints a tinted plate around the card and hangs a `caption` beneath it; the plate is the dialog element itself, so that caption stays inside the `aria-modal` subtree rather than being stranded outside it by the visual nesting, and it is added to `aria-describedby` automatically because it says what the dialog is asking. It takes a tone (`neutral` | `warning` | `error` | `success`) rather than a colour, so no consumer can paint it off-palette — and the tone sets the plate and its caption colour together, so the two cannot disagree. The caption is a `{ label?, message }` pair rather than a node, because its row height, type scale and colour belong to the plate. `size` names the card, and the plate’s 4px ring is added on top. Like `SidePanel` it sits in a z-index band from 1000 — one step per stacked overlay, so what paints on top is what holds the keyboard — and stays out of the browser top layer, so a host app’s own dialogs can still cover it. For a confirmation use `ConfirmationDialog`, which is a preset over this. _Key props:_ `open`, `onClose`, `title`, `subtitle`, `icon`, `headerExtra`, `titleAdornment`, `tray`, `headerDivider`, `footer`, `size`, `showCloseButton`, `closeOnOverlayClick`, `bodyPadding`, `busy`, `role` (`dialog` | `alertdialog`), `aria-label`, `aria-labelledby`, `aria-describedby`.
30
- - **`SidePanel`** — Right-hand slide-in overlay panel (drawer) with header slots (back/icon/title/subtitle/extra), scrollable body and optional footer. Locks background scroll and dismisses on ESC, an overlay click or close. ESC and the Tab focus trap apply to the topmost overlay only, so a `Modal` may be stacked over it.
29
+ - **`Modal`** — Centred dialog over a scrim, with header slots (icon/title/subtitle/extra), a scrolling body and a pinned footer. Locks background scroll and dismisses on ESC, the close button, or a scrim click unless `closeOnOverlayClick` is off; `busy` suppresses the two pointer routes while a request is in flight and sets `aria-busy`, so a stray click cannot tear the dialog down mid-write — but never ESC, which is the only guaranteed keyboard exit from a focus trap and so always reaches `onClose`. ESC, the Tab focus trap and `aria-modal` all apply to the topmost overlay only, so these may be stacked — a dialog opened from a panel keeps Tab to itself and the panel stops claiming to be modal. The Tab trap follows focus into a malachite popup portaled out of the dialog’s own tree — a `MultiSelect` listbox or a `Popover` opened from the body — and traps Tab within that popup while it holds focus, so Tab walks and wraps that popup’s own stops rather than being yanked back to the dialog’s first control or escaping the dialog altogether. A `Dropdown` list or an `ActionMenu` is dismissed by Tab rather than trapping it: it closes and focus returns to its trigger, so Tab carries on from there. A consumer’s own portaled popup is not known to the trap and Tab will pull focus out of it. Set `role="alertdialog"` for a consequential choice, which asks assistive tech to announce the body and not just the name, and point `aria-describedby` at whichever element carries it. Width comes from the `size` union (`small` 420px | `medium` 612px | `large` 760px) rather than a raw pixel prop. `titleAdornment` sits immediately after the title behind a hairline divider, for naming the thing being acted on — distinct from `headerExtra`, which pins to the far right. `tray` paints a tinted plate around the card and hangs a `caption` beneath it; the plate is the dialog element itself, so that caption stays inside the `aria-modal` subtree rather than being stranded outside it by the visual nesting, and it is added to `aria-describedby` automatically because it says what the dialog is asking. It takes a tone (`neutral` | `warning` | `error` | `success`) rather than a colour, so no consumer can paint it off-palette — and the tone sets the plate and its caption colour together, so the two cannot disagree. The caption is a `{ label?, message }` pair rather than a node, because its row height, type scale and colour belong to the plate. `size` names the card, and the plate’s 4px ring is added on top. Like `SidePanel` it sits in a z-index band from 1000 — one step per stacked overlay, so what paints on top is what holds the keyboard — and stays out of the browser top layer, so a host app’s own dialogs can still cover it. For a confirmation use `ConfirmationDialog`, which is a preset over this. _Key props:_ `open`, `onClose`, `title`, `subtitle`, `icon`, `headerExtra`, `titleAdornment`, `tray`, `headerDivider`, `footer`, `size`, `showCloseButton`, `closeOnOverlayClick`, `bodyPadding`, `busy`, `role` (`dialog` | `alertdialog`), `aria-label`, `aria-labelledby`, `aria-describedby`.
30
+ - **`SidePanel`** — Right-hand slide-in overlay panel (drawer) with header slots (back/icon/title/subtitle/extra), scrollable body and optional footer. Locks background scroll and dismisses on ESC, an overlay click or close. ESC and the Tab focus trap apply to the topmost overlay only, so a `Modal` may be stacked over it. The Tab trap follows focus into a malachite popup portaled out of the panel’s own tree — a `MultiSelect` listbox or a `Popover` opened from the body — and traps Tab within that popup while it holds focus, so Tab walks and wraps that popup’s own stops rather than being yanked back to the panel’s first control or escaping the panel altogether. A `Dropdown` list or an `ActionMenu` is dismissed by Tab rather than trapping it: it closes and focus returns to its trigger, so Tab carries on from there. A consumer’s own portaled popup is not known to the trap and Tab will pull focus out of it.
31
31
  - **`useHasOpenOverlay`** — Hook reporting whether any modal-type overlay - `SidePanel` or `Modal` - is open anywhere in the app. For host chrome that has to get out of an overlay’s way, a floating support widget being the case it was added for: overlays render through a portal, so a host has no DOM signal of its own to read, and reading the library’s internal markers would couple it to something it does not own. Dropdowns and tooltips are deliberately not counted - they do not trap focus, and chrome does not need to move for them. _Key props:_ No arguments. Returns a boolean.
32
32
  - **`SidePanelNavFooter`** — Keyboard-hint footer for SidePanel: up/down keycaps to step between records and an esc-to-close hint, with an optional right-aligned actions slot.
33
33
 
@@ -44,7 +44,7 @@ The catalog of components exported from `@stackone/malachite`. **Reach for one o
44
44
  - **`LoadingBar`** — Indeterminate full-width loading line; the variant colour sweeps across a pale track for work of unknown duration. Toggle `active` rather than unmounting, so the reserved height prevents layout shift. _Key props:_ `active`, `LoadingBarVariant` (`default` | `warning` | `error`), `LoadingBarSize` (`small` | `medium` | `large`), `aria-label`, `aria-labelledby`, `className`, `style`.
45
45
  - **`Spinner`** — Loading spinner.
46
46
  - **`Skeleton`** — Loading placeholders. Includes `Skeleton.Avatar` and `Skeleton.Input`.
47
- - **`Tooltip`** — Hover/focus tooltip; also works on disabled controls (see CLAUDE.md). It describes its trigger once shown, so `describesTrigger={false}` where the same sentence is already on screen and referenced — otherwise a screen reader reads it twice. _Key props:_ `text`, `placement`, `delay`, `maxWidth`, `shouldShow`, `triggerAs`, `describesTrigger`, `isShrinkable`, `fill`, `wrapChildren`, `suppressed` (holds the tooltip back, e.g. while the popup its trigger opens is up).
47
+ - **`Tooltip`** — Hover/focus tooltip; also works on disabled controls (see CLAUDE.md). It describes its trigger once shown, so `describesTrigger={false}` where the same sentence is already on screen and referenced — otherwise a screen reader reads it twice. _Key props:_ `text`, `placement`, `delay`, `maxWidth`, `shouldShow`, `triggerAs`, `describesTrigger`, `isShrinkable`, `fill`, `wrapChildren`, `suppressed` (holds the tooltip back, e.g. while the popup its trigger opens is up), `showOnFocus` (false where script can move focus onto the trigger, e.g. an option a panel focuses on open).
48
48
  - **`TruncatedTooltip`** — Shows a tooltip only when its text is truncated.
49
49
  - **`Confetti`** — Celebratory confetti burst for success moments. _Key props:_ `pieces`.
50
50
  - **`ConfirmationDialog`** — Asks the customer to confirm one consequential action — a preset over `Modal`, not a second dialog implementation. `variant` picks a whole coherent set at once (badge fill, tray tint, caption colour, badge icon and confirm-button variant), so `destructive` cannot end up with a non-destructive confirm button; `destructive` also renders as `role="alertdialog"`, and `description` is wired to the dialog through `aria-describedby`, so the consequence is announced and not just the title. `confirmPhrase` gates the confirm action on typing that exact phrase, for a consequence severe enough that one click should not be able to cause it — the typed value is trimmed, since copying a name picks up a trailing space, but stays case-sensitive, and the field clears each time the dialog opens, being a per-decision gate rather than a value. Neither a destructive dialog nor one carrying a `confirmPhrase` dismisses on a scrim click, and `loading` suppresses the pointer routes — the scrim and the close button — as well as disabling the buttons. It never suppresses ESC: that is the only guaranteed keyboard exit from a focus trap, so it always reaches `onClose` and whether to abort is left to the caller. ESC reaches it whenever it is the topmost overlay, so it can be opened over a panel. `callout` renders an `Alert` in a full-width band at the top of the body — it scrolls with the body rather than being pinned under the header — and its `type` is chosen independently of `variant` — the frames pair a green badge with an amber callout. `titleAdornment` names the entity beside the title, `badgeIcon` overrides the variant’s glyph for an action-specific one, and `tip` is the `{ label?, message }` caption on the tray beneath the card, which `Modal` renders and describes. `children` is where extra body content goes. `onConfirm` does not close the dialog, so the caller can hold it open on `loading` while the request is in flight. `confirmPhraseLabel` also takes `{ prefix, suffix }`, which renders the phrase itself as a copyable token between the two — the gate is case-sensitive and space-exact, so a customer who has identified the right entity should not be locked out by a glyph they cannot tell apart. The copy control sits outside the element that names the field, so “Copy…” never lands in the field’s accessible name; `confirmPhraseCopyLabel` names it and `onPhraseCopied` reports a completed write with the phrase it copied. A rejected write says so in the field rather than failing silently, and the control is disabled where the Clipboard API is unavailable. A near miss is named rather than left to a disabled button: typing the phrase with the wrong case or the wrong spacing says which, once the whole phrase is there, and that message is the field’s `aria-describedby`. _Key props:_ `open`, `onClose`, `onConfirm`, `title`, `description`, `titleAdornment`, `badgeIcon`, `variant` (`default` | `warning` | `destructive` | `success`), `callout`, `confirmPhrase`, `confirmPhraseLabel`, `confirmPhraseCopyLabel`, `confirmPhrasePlaceholder`, `onPhraseCopied`, `confirmLabel`, `cancelLabel`, `loading`, `tip`, `size`, `aria-label`, `aria-labelledby`.
package/README.md CHANGED
@@ -38,8 +38,15 @@ Node 22 (`.nvmrc`) and pnpm 10+, which self-delegates to the version pinned in
38
38
  ```bash
39
39
  pnpm install --frozen-lockfile
40
40
  pnpm test
41
+ pnpm typecheck
41
42
  ```
42
43
 
44
+ `pnpm typecheck` selects the pinned native TypeScript 7 compiler for both the library
45
+ and demo. The existing `typescript` package stays on 5.8 for `vite-plugin-dts` and the
46
+ declaration build. Both packages expose `tsc`, so use the script instead of the shared
47
+ `node_modules/.bin/tsc` link. The legacy compiler remains available at
48
+ `node node_modules/typescript/bin/tsc`.
49
+
43
50
  Not npm — a stray `npm install` leaves a clean `git status` and an unvalidated
44
51
  `node_modules`. See [`CONTRIBUTING.md`](CONTRIBUTING.md) and [`CLAUDE.md`](CLAUDE.md).
45
52
 
@@ -1,16 +1,16 @@
1
- import { jsxs as h, jsx as o, Fragment as B } from "react/jsx-runtime";
2
- import I, { useState as j, useRef as E, useEffect as z, useMemo as G } from "react";
3
- import { createPortal as $ } from "react-dom";
4
- import { Button as U } from "./Button.js";
5
- import X from "./CustomIcons/MoreHorizontalIcon.js";
6
- import { focusEdge as C, edgeForTriggerKey as Z, itemForMenuKey as q, enabledItems as J } from "./actionMenuKeyboard.js";
7
- import { TRIGGER_SIZES as Q, EMPTY_ITEM_STYLE as V, menuStyle as W, itemStyle as R, TEXT_ITEM_STYLE as H, ITEM_CLASS as ee, ITEM_ICON_LAYOUT as ne, ITEM_ICON_STYLE as re, ITEM_ICON_CLASS as oe, LABEL_STYLE as te } from "./actionMenuStyles.js";
8
- import { getDeepActiveElement as ae } from "./activeElement.js";
9
- import { elementRef as le, composeRefs as se, chainHandler as v } from "./composeRefs.js";
10
- import { pushEscapeLayer as ce } from "./escapeLayers.js";
11
- import { overlayIdentity as ie } from "./portalOverlay.js";
12
- import { useActionMenuPosition as ue } from "./useActionMenuPosition.js";
13
- const de = I.lazy(() => import("./ActionMenuTagHost.js")), fe = (n) => n != null && n !== !1, g = (n) => n.stopPropagation(), pe = ({ item: n, tone: u, onSelect: M }) => n.type === "divider" ? /* @__PURE__ */ o("div", { style: R(n) }) : n.type === "text" ? /* @__PURE__ */ o("div", { role: "menuitem", "aria-disabled": "true", style: H, children: n.label }) : /* @__PURE__ */ h(
1
+ import { jsxs as h, jsx as r, Fragment as j } from "react/jsx-runtime";
2
+ import I, { useState as z, useRef as E, useEffect as G, useMemo as $ } from "react";
3
+ import { createPortal as U } from "react-dom";
4
+ import { Button as X } from "./Button.js";
5
+ import Z from "./CustomIcons/MoreHorizontalIcon.js";
6
+ import { focusEdge as C, edgeForTriggerKey as q, itemForMenuKey as J, enabledItems as Q } from "./actionMenuKeyboard.js";
7
+ import { TRIGGER_SIZES as V, EMPTY_ITEM_STYLE as W, menuStyle as H, itemStyle as L, TEXT_ITEM_STYLE as ee, ITEM_CLASS as ne, ITEM_ICON_LAYOUT as oe, ITEM_ICON_STYLE as re, ITEM_ICON_CLASS as te, LABEL_STYLE as ae } from "./actionMenuStyles.js";
8
+ import { getDeepActiveElement as se } from "./activeElement.js";
9
+ import { elementRef as le, composeRefs as ce, chainHandler as v } from "./composeRefs.js";
10
+ import { pushEscapeLayer as ie } from "./escapeLayers.js";
11
+ import { usePopupLayer as ue } from "./overlayLayers.js";
12
+ import { useActionMenuPosition as de } from "./useActionMenuPosition.js";
13
+ const pe = I.lazy(() => import("./ActionMenuTagHost.js")), fe = (n) => n != null && n !== !1, g = (n) => n.stopPropagation(), ye = ({ item: n, tone: u, onSelect: M }) => n.type === "divider" ? /* @__PURE__ */ r("div", { style: L(n) }) : n.type === "text" ? /* @__PURE__ */ r("div", { role: "menuitem", "aria-disabled": "true", style: ee, children: n.label }) : /* @__PURE__ */ h(
14
14
  "button",
15
15
  {
16
16
  type: "button",
@@ -18,105 +18,105 @@ const de = I.lazy(() => import("./ActionMenuTagHost.js")), fe = (n) => n != null
18
18
  disabled: n.disabled,
19
19
  title: n.tooltip,
20
20
  "aria-current": n.current ? "page" : void 0,
21
- className: ee,
21
+ className: ne,
22
22
  "data-danger": n.danger === "hover" ? "hover" : void 0,
23
23
  "data-tone": u,
24
24
  onClick: () => M(n),
25
- style: R(n, u),
25
+ style: L(n, u),
26
26
  children: [
27
- n.icon && /* @__PURE__ */ o(
27
+ n.icon && /* @__PURE__ */ r(
28
28
  "span",
29
29
  {
30
- className: oe,
31
- style: n.danger === "hover" ? ne : re,
30
+ className: te,
31
+ style: n.danger === "hover" ? oe : re,
32
32
  children: n.icon
33
33
  }
34
34
  ),
35
- /* @__PURE__ */ o("span", { style: te, children: n.label }),
36
- fe(n.tag) ? /* @__PURE__ */ o(I.Suspense, { fallback: null, children: /* @__PURE__ */ o(de, { children: n.tag }) }) : null
35
+ /* @__PURE__ */ r("span", { style: ae, children: n.label }),
36
+ fe(n.tag) ? /* @__PURE__ */ r(I.Suspense, { fallback: null, children: /* @__PURE__ */ r(pe, { children: n.tag }) }) : null
37
37
  ]
38
38
  }
39
- ), De = ({
39
+ ), we = ({
40
40
  items: n,
41
41
  label: u = "More actions",
42
42
  triggerIcon: M,
43
- minWidth: x,
43
+ minWidth: R,
44
44
  size: S = "small",
45
45
  showLabel: y = !1,
46
- variant: L = "outline",
47
- trigger: s,
46
+ variant: x = "outline",
47
+ trigger: c,
48
48
  placement: b = "bottom-end",
49
49
  offset: O,
50
50
  tone: K = "default"
51
51
  }) => {
52
- const T = Q[S], [t, c] = j(!1), d = E(null), i = E(null), p = E(null), f = E(!1), A = ue(t, d, i, {
53
- minWidth: x,
52
+ const T = V[S], [t, i] = z(!1), d = E(null), l = E(null), f = E(null), p = E(!1), P = ue("actionmenu", t, l), A = de(t, d, l, {
53
+ minWidth: R,
54
54
  placement: b,
55
55
  offset: O
56
56
  });
57
- z(() => {
57
+ G(() => {
58
58
  if (!t)
59
59
  return;
60
- p.current && (C(i.current, p.current), p.current = null);
61
- const e = (l) => {
62
- const m = l.composedPath();
63
- [d.current, i.current].some(
60
+ f.current && (C(l.current, f.current), f.current = null);
61
+ const e = (s) => {
62
+ const m = s.composedPath();
63
+ [d.current, l.current].some(
64
64
  (w) => w && m.includes(w)
65
- ) || (f.current = !1, c(!1));
65
+ ) || (p.current = !1, i(!1));
66
66
  };
67
67
  document.addEventListener("mousedown", e, !0);
68
- const r = ce(() => c(!1));
68
+ const o = ie(() => i(!1));
69
69
  return () => {
70
70
  var m;
71
- document.removeEventListener("mousedown", e, !0), r();
72
- const l = ae();
73
- f.current && (!l || l === document.body) && ((m = d.current) == null || m.focus({ preventScroll: !0 })), f.current = !1;
71
+ document.removeEventListener("mousedown", e, !0), o();
72
+ const s = se();
73
+ p.current && (!s || s === document.body) && ((m = d.current) == null || m.focus({ preventScroll: !0 })), p.current = !1;
74
74
  };
75
75
  }, [t]);
76
76
  const _ = (e) => {
77
77
  e.stopPropagation();
78
- const r = !t && e.detail === 0;
79
- p.current = r ? "first" : null, c((l) => !l);
78
+ const o = !t && e.detail === 0;
79
+ f.current = o ? "first" : null, i((s) => !s);
80
80
  }, k = (e) => {
81
81
  if (e.key === "Tab" && t) {
82
- c(!1);
82
+ i(!1);
83
83
  return;
84
84
  }
85
- const r = Z(e.key, b);
86
- if (r) {
85
+ const o = q(e.key, b);
86
+ if (o) {
87
87
  if (e.preventDefault(), t) {
88
- C(i.current, r);
88
+ C(l.current, o);
89
89
  return;
90
90
  }
91
- p.current = r, c(!0);
91
+ f.current = o, i(!0);
92
92
  }
93
93
  }, N = (e) => {
94
- var l;
94
+ var s;
95
95
  if (e.key === "Tab") {
96
- f.current = !1, (l = d.current) == null || l.focus({ preventScroll: !0 }), c(!1);
96
+ p.current = !1, (s = d.current) == null || s.focus({ preventScroll: !0 }), i(!1);
97
97
  return;
98
98
  }
99
- const r = q(J(i.current), e.target, e.key);
100
- r && (e.preventDefault(), r.focus());
101
- }, P = (e) => {
102
- var r;
103
- e.type === "divider" || e.type === "text" || e.disabled || ((r = e.onClick) == null || r.call(e), c(!1));
104
- }, D = s ? le(s) : void 0, F = G(
105
- () => se(D, d),
99
+ const o = J(Q(l.current), e.target, e.key);
100
+ o && (e.preventDefault(), o.focus());
101
+ }, F = (e) => {
102
+ var o;
103
+ e.type === "divider" || e.type === "text" || e.disabled || ((o = e.onClick) == null || o.call(e), i(!1));
104
+ }, D = c ? le(c) : void 0, Y = $(
105
+ () => ce(D, d),
106
106
  [D]
107
- ), a = s == null ? void 0 : s.props, Y = s ? I.cloneElement(s, {
108
- ref: F,
107
+ ), a = c == null ? void 0 : c.props, B = c ? I.cloneElement(c, {
108
+ ref: Y,
109
109
  "aria-haspopup": "menu",
110
110
  "aria-expanded": t,
111
111
  onClick: v(a == null ? void 0 : a.onClick, _),
112
112
  onMouseDown: v(a == null ? void 0 : a.onMouseDown, g),
113
113
  onKeyDown: v(a == null ? void 0 : a.onKeyDown, k)
114
114
  }) : /* @__PURE__ */ h(
115
- U,
115
+ X,
116
116
  {
117
117
  ref: d,
118
118
  type: "button",
119
- variant: L,
119
+ variant: x,
120
120
  size: S,
121
121
  iconOnly: !y,
122
122
  "aria-label": y ? void 0 : u,
@@ -127,36 +127,36 @@ const de = I.lazy(() => import("./ActionMenuTagHost.js")), fe = (n) => n != null
127
127
  onMouseDown: g,
128
128
  onKeyDown: k,
129
129
  children: [
130
- /* @__PURE__ */ o("span", { className: `inline-flex items-center justify-center ${T.iconBox}`, children: M ?? /* @__PURE__ */ o(X, { size: T.icon }) }),
131
- y && /* @__PURE__ */ o("span", { children: u })
130
+ /* @__PURE__ */ r("span", { className: `inline-flex items-center justify-center ${T.iconBox}`, children: M ?? /* @__PURE__ */ r(Z, { size: T.icon }) }),
131
+ y && /* @__PURE__ */ r("span", { children: u })
132
132
  ]
133
133
  }
134
134
  );
135
- return /* @__PURE__ */ h(B, { children: [
136
- Y,
137
- t && $(
135
+ return /* @__PURE__ */ h(j, { children: [
136
+ B,
137
+ t && U(
138
138
  /* @__PURE__ */ h(
139
139
  "div",
140
140
  {
141
- ref: i,
141
+ ref: l,
142
142
  role: "menu",
143
143
  tabIndex: -1,
144
144
  "aria-label": u,
145
- ...ie("actionmenu"),
146
- style: W(A),
145
+ ...P,
146
+ style: H(A),
147
147
  onMouseDown: g,
148
148
  onClick: g,
149
149
  onKeyDown: N,
150
150
  onFocus: () => {
151
- f.current = !0;
151
+ p.current = !0;
152
152
  },
153
153
  onBlur: (e) => {
154
- var r;
155
- (r = i.current) != null && r.contains(e.relatedTarget) || (f.current = !1);
154
+ var o;
155
+ (o = l.current) != null && o.contains(e.relatedTarget) || (p.current = !1);
156
156
  },
157
157
  children: [
158
- n.length === 0 && /* @__PURE__ */ o("div", { role: "menuitem", "aria-disabled": "true", style: V, children: "No actions available" }),
159
- n.map((e) => /* @__PURE__ */ o(pe, { item: e, tone: K, onSelect: P }, e.key))
158
+ n.length === 0 && /* @__PURE__ */ r("div", { role: "menuitem", "aria-disabled": "true", style: W, children: "No actions available" }),
159
+ n.map((e) => /* @__PURE__ */ r(ye, { item: e, tone: K, onSelect: F }, e.key))
160
160
  ]
161
161
  }
162
162
  ),
@@ -165,6 +165,6 @@ const de = I.lazy(() => import("./ActionMenuTagHost.js")), fe = (n) => n != null
165
165
  ] });
166
166
  };
167
167
  export {
168
- De as default
168
+ we as default
169
169
  };
170
170
  //# sourceMappingURL=ActionMenu.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ActionMenu.js","sources":["../../src/components/ActionMenu.tsx"],"sourcesContent":["import React, { useEffect, useMemo, useRef, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport Button from './Button';\nimport type { ButtonVariant } from './Button';\nimport MoreHorizontalIcon from './CustomIcons/MoreHorizontalIcon';\nimport {\n type MenuEdge,\n edgeForTriggerKey,\n enabledItems,\n focusEdge,\n itemForMenuKey,\n} from './actionMenuKeyboard';\nimport {\n EMPTY_ITEM_STYLE,\n ITEM_CLASS,\n ITEM_ICON_CLASS,\n ITEM_ICON_LAYOUT,\n ITEM_ICON_STYLE,\n LABEL_STYLE,\n TEXT_ITEM_STYLE,\n TRIGGER_SIZES,\n type ActionMenuTone,\n itemStyle,\n menuStyle,\n} from './actionMenuStyles';\nimport type { ActionMenuItem, ActionMenuPlacement } from './actionMenuTypes';\nimport { getDeepActiveElement } from './activeElement';\nimport { chainHandler, composeRefs, elementRef } from './composeRefs';\nimport { pushEscapeLayer } from './escapeLayers';\nimport type { FloatingOffset } from './floatingMenuGeometry';\nimport { overlayIdentity } from './portalOverlay';\nimport { useActionMenuPosition } from './useActionMenuPosition';\n\nexport type { ActionMenuItem, ActionMenuPlacement, ActionMenuTone };\n\nexport interface ActionMenuProps {\n items: ActionMenuItem[];\n label?: string;\n triggerIcon?: React.ReactNode;\n minWidth?: number;\n size?: 'small' | 'xsmall';\n /**\n * Shows `label` beside the icon instead of leaving it to the accessible name. For a menu\n * that is the point of its corner rather than an overflow for a row that already reads —\n * a kebab nobody can name is only safe when the row it belongs to says what it acts on.\n */\n showLabel?: boolean;\n /** Trigger's Button variant. Defaults to `outline`, the overflow-kebab look. */\n variant?: ButtonVariant;\n trigger?: React.ReactElement;\n placement?: ActionMenuPlacement;\n offset?: FloatingOffset;\n /** `muted` reads items in the secondary text colour, as in a sidebar's menus. */\n tone?: ActionMenuTone;\n}\n\ntype TriggerHandlers = {\n onClick?: (event: React.MouseEvent) => void;\n onMouseDown?: (event: React.MouseEvent) => void;\n onKeyDown?: (event: React.KeyboardEvent) => void;\n};\n\n// Loaded only when an item has a tag: its shadow host pulls in the Tailwind sheet, which a menu\n// without tags should not pay for.\nconst ActionMenuTagHost = React.lazy(() => import('./ActionMenuTagHost'));\n\nconst hasTag = (tag: React.ReactNode) => tag !== undefined && tag !== null && tag !== false;\n\nconst stopPropagation = (event: React.SyntheticEvent) => event.stopPropagation();\n\nconst MenuItemRow: React.FC<{\n item: ActionMenuItem;\n tone: ActionMenuTone;\n onSelect: (item: ActionMenuItem) => void;\n}> = ({ item, tone, onSelect }) => {\n if (item.type === 'divider') {\n return <div style={itemStyle(item)} />;\n }\n if (item.type === 'text') {\n return (\n <div role=\"menuitem\" aria-disabled=\"true\" style={TEXT_ITEM_STYLE}>\n {item.label}\n </div>\n );\n }\n return (\n <button\n type=\"button\"\n role=\"menuitem\"\n disabled={item.disabled}\n title={item.tooltip}\n aria-current={item.current ? 'page' : undefined}\n className={ITEM_CLASS}\n data-danger={item.danger === 'hover' ? 'hover' : undefined}\n data-tone={tone}\n onClick={() => onSelect(item)}\n style={itemStyle(item, tone)}\n >\n {item.icon && (\n <span\n className={ITEM_ICON_CLASS}\n style={item.danger === 'hover' ? ITEM_ICON_LAYOUT : ITEM_ICON_STYLE}\n >\n {item.icon}\n </span>\n )}\n <span style={LABEL_STYLE}>{item.label}</span>\n {hasTag(item.tag) ? (\n <React.Suspense fallback={null}>\n <ActionMenuTagHost>{item.tag}</ActionMenuTagHost>\n </React.Suspense>\n ) : null}\n </button>\n );\n};\n\n/**\n * ActionMenu is an overflow action menu. The trigger is a three-dot `Button` by default, or\n * your own control via `trigger`. The menu is portaled to document.body, so it escapes any\n * Shadow DOM or overflow:hidden ancestor, which is also why it is styled inline.\n */\nconst ActionMenu: React.FC<ActionMenuProps> = ({\n items,\n label = 'More actions',\n triggerIcon,\n minWidth,\n size = 'small',\n showLabel = false,\n variant = 'outline',\n trigger: customTrigger,\n placement = 'bottom-end',\n offset,\n tone = 'default',\n}) => {\n const triggerSize = TRIGGER_SIZES[size];\n const [open, setOpen] = useState(false);\n const triggerRef = useRef<HTMLElement | null>(null);\n const menuRef = useRef<HTMLDivElement>(null);\n const focusOnOpen = useRef<MenuEdge | null>(null);\n const focusInMenu = useRef(false);\n\n const position = useActionMenuPosition(open, triggerRef, menuRef, {\n minWidth,\n placement,\n offset,\n });\n\n useEffect(() => {\n if (!open) {\n return;\n }\n if (focusOnOpen.current) {\n focusEdge(menuRef.current, focusOnOpen.current);\n focusOnOpen.current = null;\n }\n const closeOnOutsidePress = (event: MouseEvent) => {\n const path = event.composedPath();\n const inside = [triggerRef.current, menuRef.current].some(\n (node) => node && path.includes(node),\n );\n if (!inside) {\n focusInMenu.current = false;\n setOpen(false);\n }\n };\n document.addEventListener('mousedown', closeOnOutsidePress, true);\n const popEscapeLayer = pushEscapeLayer(() => setOpen(false));\n return () => {\n document.removeEventListener('mousedown', closeOnOutsidePress, true);\n popEscapeLayer();\n const active = getDeepActiveElement();\n if (focusInMenu.current && (!active || active === document.body)) {\n triggerRef.current?.focus({ preventScroll: true });\n }\n focusInMenu.current = false;\n };\n }, [open]);\n\n const toggleFromTrigger = (event: React.MouseEvent) => {\n event.stopPropagation();\n const openedFromKeyboard = !open && event.detail === 0;\n focusOnOpen.current = openedFromKeyboard ? 'first' : null;\n setOpen((v) => !v);\n };\n\n const handleTriggerKeyDown = (event: React.KeyboardEvent) => {\n if (event.key === 'Tab' && open) {\n setOpen(false);\n return;\n }\n const edge = edgeForTriggerKey(event.key, placement);\n if (!edge) {\n return;\n }\n event.preventDefault();\n if (open) {\n focusEdge(menuRef.current, edge);\n return;\n }\n focusOnOpen.current = edge;\n setOpen(true);\n };\n\n const handleMenuKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (event.key === 'Tab') {\n focusInMenu.current = false;\n triggerRef.current?.focus({ preventScroll: true });\n setOpen(false);\n return;\n }\n const target = itemForMenuKey(enabledItems(menuRef.current), event.target, event.key);\n if (target) {\n event.preventDefault();\n target.focus();\n }\n };\n\n const selectItem = (item: ActionMenuItem) => {\n if (item.type === 'divider' || item.type === 'text' || item.disabled) {\n return;\n }\n item.onClick?.();\n setOpen(false);\n };\n\n const ownTriggerRef = customTrigger ? elementRef<HTMLElement>(customTrigger) : undefined;\n const composedTriggerRef = useMemo(\n () => composeRefs(ownTriggerRef, triggerRef),\n [ownTriggerRef],\n );\n const customHandlers = customTrigger?.props as TriggerHandlers | undefined;\n\n const triggerElement = customTrigger ? (\n React.cloneElement(customTrigger, {\n ref: composedTriggerRef,\n 'aria-haspopup': 'menu',\n 'aria-expanded': open,\n onClick: chainHandler(customHandlers?.onClick, toggleFromTrigger),\n onMouseDown: chainHandler(customHandlers?.onMouseDown, stopPropagation),\n onKeyDown: chainHandler(customHandlers?.onKeyDown, handleTriggerKeyDown),\n } as Record<string, unknown>)\n ) : (\n <Button\n ref={triggerRef as React.RefObject<HTMLButtonElement>}\n type=\"button\"\n variant={variant}\n size={size}\n iconOnly={!showLabel}\n aria-label={showLabel ? undefined : label}\n aria-haspopup=\"menu\"\n aria-expanded={open}\n className={`shrink-0${showLabel ? ' gap-2' : ''}`}\n onClick={toggleFromTrigger}\n onMouseDown={stopPropagation}\n onKeyDown={handleTriggerKeyDown}\n >\n <span className={`inline-flex items-center justify-center ${triggerSize.iconBox}`}>\n {triggerIcon ?? <MoreHorizontalIcon size={triggerSize.icon} />}\n </span>\n {showLabel && <span>{label}</span>}\n </Button>\n );\n\n return (\n <>\n {triggerElement}\n {open &&\n createPortal(\n <div\n ref={menuRef}\n role=\"menu\"\n tabIndex={-1}\n aria-label={label}\n {...overlayIdentity('actionmenu')}\n style={menuStyle(position)}\n onMouseDown={stopPropagation}\n onClick={stopPropagation}\n onKeyDown={handleMenuKeyDown}\n onFocus={() => {\n focusInMenu.current = true;\n }}\n onBlur={(e) => {\n if (!menuRef.current?.contains(e.relatedTarget as Node | null)) {\n focusInMenu.current = false;\n }\n }}\n >\n {items.length === 0 && (\n <div role=\"menuitem\" aria-disabled=\"true\" style={EMPTY_ITEM_STYLE}>\n No actions available\n </div>\n )}\n {items.map((item) => (\n <MenuItemRow key={item.key} item={item} tone={tone} onSelect={selectItem} />\n ))}\n </div>,\n document.body,\n )}\n </>\n );\n};\n\nexport default ActionMenu;\n"],"names":["ActionMenuTagHost","React","hasTag","tag","stopPropagation","event","MenuItemRow","item","tone","onSelect","jsx","itemStyle","TEXT_ITEM_STYLE","jsxs","ITEM_CLASS","ITEM_ICON_CLASS","ITEM_ICON_LAYOUT","ITEM_ICON_STYLE","LABEL_STYLE","ActionMenu","items","label","triggerIcon","minWidth","size","showLabel","variant","customTrigger","placement","offset","triggerSize","TRIGGER_SIZES","open","setOpen","useState","triggerRef","useRef","menuRef","focusOnOpen","focusInMenu","position","useActionMenuPosition","useEffect","focusEdge","closeOnOutsidePress","path","node","popEscapeLayer","pushEscapeLayer","active","getDeepActiveElement","_a","toggleFromTrigger","openedFromKeyboard","v","handleTriggerKeyDown","edge","edgeForTriggerKey","handleMenuKeyDown","target","itemForMenuKey","enabledItems","selectItem","ownTriggerRef","elementRef","composedTriggerRef","useMemo","composeRefs","customHandlers","triggerElement","chainHandler","Button","MoreHorizontalIcon","Fragment","createPortal","overlayIdentity","menuStyle","EMPTY_ITEM_STYLE"],"mappings":"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"}
1
+ {"version":3,"file":"ActionMenu.js","sources":["../../src/components/ActionMenu.tsx"],"sourcesContent":["import React, { useEffect, useMemo, useRef, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport Button from './Button';\nimport type { ButtonVariant } from './Button';\nimport MoreHorizontalIcon from './CustomIcons/MoreHorizontalIcon';\nimport {\n type MenuEdge,\n edgeForTriggerKey,\n enabledItems,\n focusEdge,\n itemForMenuKey,\n} from './actionMenuKeyboard';\nimport {\n EMPTY_ITEM_STYLE,\n ITEM_CLASS,\n ITEM_ICON_CLASS,\n ITEM_ICON_LAYOUT,\n ITEM_ICON_STYLE,\n LABEL_STYLE,\n TEXT_ITEM_STYLE,\n TRIGGER_SIZES,\n type ActionMenuTone,\n itemStyle,\n menuStyle,\n} from './actionMenuStyles';\nimport type { ActionMenuItem, ActionMenuPlacement } from './actionMenuTypes';\nimport { getDeepActiveElement } from './activeElement';\nimport { chainHandler, composeRefs, elementRef } from './composeRefs';\nimport { pushEscapeLayer } from './escapeLayers';\nimport type { FloatingOffset } from './floatingMenuGeometry';\nimport { usePopupLayer } from './overlayLayers';\nimport { useActionMenuPosition } from './useActionMenuPosition';\n\nexport type { ActionMenuItem, ActionMenuPlacement, ActionMenuTone };\n\nexport interface ActionMenuProps {\n items: ActionMenuItem[];\n label?: string;\n triggerIcon?: React.ReactNode;\n minWidth?: number;\n size?: 'small' | 'xsmall';\n /**\n * Shows `label` beside the icon instead of leaving it to the accessible name. For a menu\n * that is the point of its corner rather than an overflow for a row that already reads —\n * a kebab nobody can name is only safe when the row it belongs to says what it acts on.\n */\n showLabel?: boolean;\n /** Trigger's Button variant. Defaults to `outline`, the overflow-kebab look. */\n variant?: ButtonVariant;\n trigger?: React.ReactElement;\n placement?: ActionMenuPlacement;\n offset?: FloatingOffset;\n /** `muted` reads items in the secondary text colour, as in a sidebar's menus. */\n tone?: ActionMenuTone;\n}\n\ntype TriggerHandlers = {\n onClick?: (event: React.MouseEvent) => void;\n onMouseDown?: (event: React.MouseEvent) => void;\n onKeyDown?: (event: React.KeyboardEvent) => void;\n};\n\n// Loaded only when an item has a tag: its shadow host pulls in the Tailwind sheet, which a menu\n// without tags should not pay for.\nconst ActionMenuTagHost = React.lazy(() => import('./ActionMenuTagHost'));\n\nconst hasTag = (tag: React.ReactNode) => tag !== undefined && tag !== null && tag !== false;\n\nconst stopPropagation = (event: React.SyntheticEvent) => event.stopPropagation();\n\nconst MenuItemRow: React.FC<{\n item: ActionMenuItem;\n tone: ActionMenuTone;\n onSelect: (item: ActionMenuItem) => void;\n}> = ({ item, tone, onSelect }) => {\n if (item.type === 'divider') {\n return <div style={itemStyle(item)} />;\n }\n if (item.type === 'text') {\n return (\n <div role=\"menuitem\" aria-disabled=\"true\" style={TEXT_ITEM_STYLE}>\n {item.label}\n </div>\n );\n }\n return (\n <button\n type=\"button\"\n role=\"menuitem\"\n disabled={item.disabled}\n title={item.tooltip}\n aria-current={item.current ? 'page' : undefined}\n className={ITEM_CLASS}\n data-danger={item.danger === 'hover' ? 'hover' : undefined}\n data-tone={tone}\n onClick={() => onSelect(item)}\n style={itemStyle(item, tone)}\n >\n {item.icon && (\n <span\n className={ITEM_ICON_CLASS}\n style={item.danger === 'hover' ? ITEM_ICON_LAYOUT : ITEM_ICON_STYLE}\n >\n {item.icon}\n </span>\n )}\n <span style={LABEL_STYLE}>{item.label}</span>\n {hasTag(item.tag) ? (\n <React.Suspense fallback={null}>\n <ActionMenuTagHost>{item.tag}</ActionMenuTagHost>\n </React.Suspense>\n ) : null}\n </button>\n );\n};\n\n/**\n * ActionMenu is an overflow action menu. The trigger is a three-dot `Button` by default, or\n * your own control via `trigger`. The menu is portaled to document.body, so it escapes any\n * Shadow DOM or overflow:hidden ancestor, which is also why it is styled inline.\n */\nconst ActionMenu: React.FC<ActionMenuProps> = ({\n items,\n label = 'More actions',\n triggerIcon,\n minWidth,\n size = 'small',\n showLabel = false,\n variant = 'outline',\n trigger: customTrigger,\n placement = 'bottom-end',\n offset,\n tone = 'default',\n}) => {\n const triggerSize = TRIGGER_SIZES[size];\n const [open, setOpen] = useState(false);\n const triggerRef = useRef<HTMLElement | null>(null);\n const menuRef = useRef<HTMLDivElement>(null);\n const focusOnOpen = useRef<MenuEdge | null>(null);\n const focusInMenu = useRef(false);\n const popupIdentity = usePopupLayer('actionmenu', open, menuRef);\n\n const position = useActionMenuPosition(open, triggerRef, menuRef, {\n minWidth,\n placement,\n offset,\n });\n\n useEffect(() => {\n if (!open) {\n return;\n }\n if (focusOnOpen.current) {\n focusEdge(menuRef.current, focusOnOpen.current);\n focusOnOpen.current = null;\n }\n const closeOnOutsidePress = (event: MouseEvent) => {\n const path = event.composedPath();\n const inside = [triggerRef.current, menuRef.current].some(\n (node) => node && path.includes(node),\n );\n if (!inside) {\n focusInMenu.current = false;\n setOpen(false);\n }\n };\n document.addEventListener('mousedown', closeOnOutsidePress, true);\n const popEscapeLayer = pushEscapeLayer(() => setOpen(false));\n return () => {\n document.removeEventListener('mousedown', closeOnOutsidePress, true);\n popEscapeLayer();\n const active = getDeepActiveElement();\n if (focusInMenu.current && (!active || active === document.body)) {\n triggerRef.current?.focus({ preventScroll: true });\n }\n focusInMenu.current = false;\n };\n }, [open]);\n\n const toggleFromTrigger = (event: React.MouseEvent) => {\n event.stopPropagation();\n const openedFromKeyboard = !open && event.detail === 0;\n focusOnOpen.current = openedFromKeyboard ? 'first' : null;\n setOpen((v) => !v);\n };\n\n const handleTriggerKeyDown = (event: React.KeyboardEvent) => {\n if (event.key === 'Tab' && open) {\n setOpen(false);\n return;\n }\n const edge = edgeForTriggerKey(event.key, placement);\n if (!edge) {\n return;\n }\n event.preventDefault();\n if (open) {\n focusEdge(menuRef.current, edge);\n return;\n }\n focusOnOpen.current = edge;\n setOpen(true);\n };\n\n const handleMenuKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (event.key === 'Tab') {\n focusInMenu.current = false;\n triggerRef.current?.focus({ preventScroll: true });\n setOpen(false);\n return;\n }\n const target = itemForMenuKey(enabledItems(menuRef.current), event.target, event.key);\n if (target) {\n event.preventDefault();\n target.focus();\n }\n };\n\n const selectItem = (item: ActionMenuItem) => {\n if (item.type === 'divider' || item.type === 'text' || item.disabled) {\n return;\n }\n item.onClick?.();\n setOpen(false);\n };\n\n const ownTriggerRef = customTrigger ? elementRef<HTMLElement>(customTrigger) : undefined;\n const composedTriggerRef = useMemo(\n () => composeRefs(ownTriggerRef, triggerRef),\n [ownTriggerRef],\n );\n const customHandlers = customTrigger?.props as TriggerHandlers | undefined;\n\n const triggerElement = customTrigger ? (\n React.cloneElement(customTrigger, {\n ref: composedTriggerRef,\n 'aria-haspopup': 'menu',\n 'aria-expanded': open,\n onClick: chainHandler(customHandlers?.onClick, toggleFromTrigger),\n onMouseDown: chainHandler(customHandlers?.onMouseDown, stopPropagation),\n onKeyDown: chainHandler(customHandlers?.onKeyDown, handleTriggerKeyDown),\n } as Record<string, unknown>)\n ) : (\n <Button\n ref={triggerRef as React.RefObject<HTMLButtonElement>}\n type=\"button\"\n variant={variant}\n size={size}\n iconOnly={!showLabel}\n aria-label={showLabel ? undefined : label}\n aria-haspopup=\"menu\"\n aria-expanded={open}\n className={`shrink-0${showLabel ? ' gap-2' : ''}`}\n onClick={toggleFromTrigger}\n onMouseDown={stopPropagation}\n onKeyDown={handleTriggerKeyDown}\n >\n <span className={`inline-flex items-center justify-center ${triggerSize.iconBox}`}>\n {triggerIcon ?? <MoreHorizontalIcon size={triggerSize.icon} />}\n </span>\n {showLabel && <span>{label}</span>}\n </Button>\n );\n\n return (\n <>\n {triggerElement}\n {open &&\n createPortal(\n <div\n ref={menuRef}\n role=\"menu\"\n tabIndex={-1}\n aria-label={label}\n {...popupIdentity}\n style={menuStyle(position)}\n onMouseDown={stopPropagation}\n onClick={stopPropagation}\n onKeyDown={handleMenuKeyDown}\n onFocus={() => {\n focusInMenu.current = true;\n }}\n onBlur={(e) => {\n if (!menuRef.current?.contains(e.relatedTarget as Node | null)) {\n focusInMenu.current = false;\n }\n }}\n >\n {items.length === 0 && (\n <div role=\"menuitem\" aria-disabled=\"true\" style={EMPTY_ITEM_STYLE}>\n No actions available\n </div>\n )}\n {items.map((item) => (\n <MenuItemRow key={item.key} item={item} tone={tone} onSelect={selectItem} />\n ))}\n </div>,\n document.body,\n )}\n </>\n );\n};\n\nexport default ActionMenu;\n"],"names":["ActionMenuTagHost","React","hasTag","tag","stopPropagation","event","MenuItemRow","item","tone","onSelect","jsx","itemStyle","TEXT_ITEM_STYLE","jsxs","ITEM_CLASS","ITEM_ICON_CLASS","ITEM_ICON_LAYOUT","ITEM_ICON_STYLE","LABEL_STYLE","ActionMenu","items","label","triggerIcon","minWidth","size","showLabel","variant","customTrigger","placement","offset","triggerSize","TRIGGER_SIZES","open","setOpen","useState","triggerRef","useRef","menuRef","focusOnOpen","focusInMenu","popupIdentity","usePopupLayer","position","useActionMenuPosition","useEffect","focusEdge","closeOnOutsidePress","path","node","popEscapeLayer","pushEscapeLayer","active","getDeepActiveElement","_a","toggleFromTrigger","openedFromKeyboard","v","handleTriggerKeyDown","edge","edgeForTriggerKey","handleMenuKeyDown","target","itemForMenuKey","enabledItems","selectItem","ownTriggerRef","elementRef","composedTriggerRef","useMemo","composeRefs","customHandlers","triggerElement","chainHandler","Button","MoreHorizontalIcon","Fragment","createPortal","menuStyle","EMPTY_ITEM_STYLE"],"mappings":"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"}