@viax.io/uxm 4.46.0 → 4.48.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/CHANGELOG.md +14 -0
  2. package/dist/studio/lib/registry/composite/list-item.cjs +17 -0
  3. package/dist/studio/lib/registry/composite/list-item.cjs.map +1 -1
  4. package/dist/studio/lib/registry/composite/list-item.d.ts.map +1 -1
  5. package/dist/studio/lib/registry/composite/list-item.js +17 -0
  6. package/dist/studio/lib/registry/composite/list-item.js.map +1 -1
  7. package/dist/ui/index.cjs +2 -0
  8. package/dist/ui/index.cjs.map +1 -1
  9. package/dist/ui/index.d.ts +2 -2
  10. package/dist/ui/index.d.ts.map +1 -1
  11. package/dist/ui/index.js +2 -1
  12. package/dist/ui/index.js.map +1 -1
  13. package/dist/ui/list/list-item-preview.cjs +46 -10
  14. package/dist/ui/list/list-item-preview.cjs.map +1 -1
  15. package/dist/ui/list/list-item-preview.d.ts.map +1 -1
  16. package/dist/ui/list/list-item-preview.js +46 -10
  17. package/dist/ui/list/list-item-preview.js.map +1 -1
  18. package/dist/ui/list/list.cjs +35 -1
  19. package/dist/ui/list/list.cjs.map +1 -1
  20. package/dist/ui/list/list.css +25 -0
  21. package/dist/ui/list/list.d.ts +20 -2
  22. package/dist/ui/list/list.d.ts.map +1 -1
  23. package/dist/ui/list/list.js +35 -1
  24. package/dist/ui/list/list.js.map +1 -1
  25. package/dist/ui/menu/index.cjs +3 -1
  26. package/dist/ui/menu/index.cjs.map +1 -1
  27. package/dist/ui/menu/index.d.ts +1 -0
  28. package/dist/ui/menu/index.d.ts.map +1 -1
  29. package/dist/ui/menu/index.js +1 -0
  30. package/dist/ui/menu/index.js.map +1 -1
  31. package/dist/ui/menu/menu-button.cjs +89 -0
  32. package/dist/ui/menu/menu-button.cjs.map +1 -0
  33. package/dist/ui/menu/menu-button.css +30 -0
  34. package/dist/ui/menu/menu-button.d.ts +48 -0
  35. package/dist/ui/menu/menu-button.d.ts.map +1 -0
  36. package/dist/ui/menu/menu-button.js +70 -0
  37. package/dist/ui/menu/menu-button.js.map +1 -0
  38. package/dist/ui/styles.css +1 -0
  39. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
2
  import { cn } from "../../helpers/index.js";
3
+ import { Icon } from "../icon/index.js";
3
4
  import { IconTile } from "../icon-tile/index.js";
4
5
  const LIST_ITEM_ICON_TILE_STYLE = {
5
6
  "--uxm-icon-tile-bg": "var(--uxm-list-item-icon-bg, var(--color-accent))",
@@ -33,16 +34,38 @@ function ListItem({
33
34
  active,
34
35
  disabled,
35
36
  href,
37
+ selected,
38
+ onSelectedChange,
36
39
  className,
37
40
  ...rest
38
41
  }) {
42
+ const isSelectable = selected !== void 0 || onSelectedChange !== void 0;
39
43
  const classes = cn(
40
44
  "uxm-list-item",
41
45
  active && "uxm-list-item--active",
46
+ isSelectable && "uxm-list-item--selectable",
42
47
  className
43
48
  );
44
- const isInteractive = interactive || href !== void 0;
49
+ const isInteractive = (interactive || href !== void 0) && !isSelectable;
50
+ if (process.env.NODE_ENV !== "production" && isSelectable && (interactive || href !== void 0)) {
51
+ console.warn(
52
+ "[uxm] ListItem: `selected`/`onSelectedChange` cannot be combined with `interactive`/`href` \u2014 a row is either a navigate/activate row or a member of a checkable set. Selectable mode wins."
53
+ );
54
+ }
45
55
  const inner = /* @__PURE__ */ jsxs(Fragment, { children: [
56
+ isSelectable && /* @__PURE__ */ jsxs("span", { className: "uxm-list-item__check", children: [
57
+ /* @__PURE__ */ jsx(
58
+ "input",
59
+ {
60
+ type: "checkbox",
61
+ className: "uxm-checkbox__input",
62
+ checked: selected ?? false,
63
+ disabled,
64
+ onChange: (e) => onSelectedChange?.(e.target.checked, e)
65
+ }
66
+ ),
67
+ /* @__PURE__ */ jsx("span", { className: "uxm-checkbox__box", "aria-hidden": "true", children: /* @__PURE__ */ jsx(Icon, { glyph: "check", strokeWidth: 3 }) })
68
+ ] }),
46
69
  media ? /* @__PURE__ */ jsx("span", { className: "uxm-list-item__media", style: LIST_ITEM_MEDIA_STYLE, children: media }) : icon && /* @__PURE__ */ jsx(IconTile, { className: "uxm-list-item__icon", style: LIST_ITEM_ICON_TILE_STYLE, children: icon }),
47
70
  /* @__PURE__ */ jsxs("span", { className: "uxm-list-item__content", children: [
48
71
  /* @__PURE__ */ jsx("span", { className: "uxm-list-item__title", children }),
@@ -50,6 +73,17 @@ function ListItem({
50
73
  ] }),
51
74
  trailing && /* @__PURE__ */ jsx("span", { className: "uxm-list-item__trailing", children: trailing })
52
75
  ] });
76
+ if (isSelectable) {
77
+ return /* @__PURE__ */ jsx(
78
+ "label",
79
+ {
80
+ className: classes,
81
+ ...disabled ? { "aria-disabled": true } : {},
82
+ ...rest,
83
+ children: inner
84
+ }
85
+ );
86
+ }
53
87
  if (href !== void 0) {
54
88
  return /* @__PURE__ */ jsx(
55
89
  "a",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/ui/list/list.tsx"],"sourcesContent":["import { cn } from '@/helpers';\n\nimport { IconTile } from '../icon-tile';\n\nimport type {\n AnchorHTMLAttributes,\n ButtonHTMLAttributes,\n CSSProperties,\n HTMLAttributes,\n ReactNode,\n} from 'react';\n\n/**\n * Forward the studio icon knobs (projected as `--uxm-list-item-icon-*` on the\n * row) into the `IconTile` atom's own var namespace, so the accent-tile\n * pattern lives in one place — the `IconTile` atom — instead of being\n * re-implemented in `list.scss`. The glyph tracks the tile at 0.6× (the\n * row-icon proportion used across the design system). Kept as a module\n * constant so the studio preview can reuse the exact same forwarding.\n */\nexport const LIST_ITEM_ICON_TILE_STYLE = {\n '--uxm-icon-tile-bg': 'var(--uxm-list-item-icon-bg, var(--color-accent))',\n '--uxm-icon-tile-color': 'var(--uxm-list-item-icon-color, var(--color-card))',\n '--uxm-icon-tile-size': 'var(--uxm-list-item-icon-size, 24px)',\n '--uxm-icon-tile-radius': 'var(--uxm-list-item-icon-radius, 6px)',\n '--uxm-icon-tile-icon-size': 'calc(var(--uxm-list-item-icon-size, 24px) * 0.6)',\n} as CSSProperties;\n\n/**\n * Forward the row's media-size knob into the size vars of the atoms that\n * plausibly fill the `media` slot, so each one sizes itself through its OWN\n * machinery — frame, border, radius and inner glyph / initials all scale with\n * it — instead of being stretched from the outside. Mirrors\n * `LIST_ITEM_ICON_TILE_STYLE` above, and is the only order-independent fix:\n * both `Thumbnail` (`.uxm-thumbnail`, 0,1,0 but imported AFTER list in\n * `styles.css`) and `Avatar` (double-class size presets at 0,2,0, deliberately\n * so per its own note) would otherwise win or lose the cascade against a slot\n * rule purely on stylesheet order. The generic `> *` stretch in `list.scss`\n * stays as the fallback for a bare `<img>` / `<svg>` or any atom with no size\n * var of its own; both mechanisms resolve to the same number. Kept as a module\n * constant so the studio preview can reuse the exact same forwarding.\n */\nexport const LIST_ITEM_MEDIA_STYLE = {\n '--uxm-thumbnail-size': 'var(--uxm-list-item-media-size, 36px)',\n '--uxm-avatar-size': 'var(--uxm-list-item-media-size, 36px)',\n '--uxm-avatar-small-size': 'var(--uxm-list-item-media-size, 36px)',\n} as CSSProperties;\n\n/** Container presentation. See `ListProps.variant`. */\nexport type ListVariant = 'card' | 'plain';\n\nexport interface ListProps extends HTMLAttributes<HTMLDivElement> {\n children: ReactNode;\n /**\n * Container presentation. `card` (default) keeps the bordered, rounded\n * surface — right for a free-standing list on a page ground. `plain` drops\n * the background, border and radius for a list that is ALREADY inside a\n * surface (a flexpane body, a card body, a disclosure section), where a card\n * container reads as a card inside a card.\n *\n * `plain` also releases `overflow` to `visible`. The card has to clip so rows\n * cannot spill past its radius; with no radius there is nothing to clip to,\n * and `hidden` would only crop what a row deliberately renders OUTSIDE the\n * container box — an overhanging corner control, a focus ring with a positive\n * offset. Clipped, those are neither painted nor hit-testable.\n *\n * Row styling is untouched — every `--uxm-list-item-*` knob behaves\n * identically in both.\n */\n variant?: ListVariant;\n}\n\nexport function List({ children, className, variant = 'card', ...rest }: ListProps) {\n return (\n <div\n className={cn('uxm-list', variant === 'plain' && 'uxm-list--plain', className)}\n {...rest}\n >\n {children}\n </div>\n );\n}\n\n/**\n * ListItem renders as one of three element types depending on `interactive`:\n * - `<div>` (default) — display row, no state styling, not focusable\n * - `<button>` — interactive without navigation; emits click events\n * - `<a>` — interactive with navigation; pass `href`\n *\n * The state knobs (hover / focus / active / disabled) in the `list-item`\n * registry only paint when the row renders as button or anchor — the CSS\n * scopes them to element-type selectors so display-only divs stay inert.\n */\nexport interface ListItemProps {\n /** Leading icon — typically <Icon ... /> from @modo/uxm/ui. */\n icon?: ReactNode;\n /** Leading media rendered as-is (a <Thumbnail>, <img>, avatar…) — NOT\n * wrapped in the accent `IconTile` the `icon` slot uses. Mutually\n * exclusive with `icon`: when both are passed, `media` wins. The slot\n * box is `--uxm-list-item-media-size` (36px) square; a `Thumbnail` or\n * `Avatar` gets that size forwarded into its own size var (see\n * `LIST_ITEM_MEDIA_STYLE`) and anything else is stretched to fill, so\n * rows stay on one leading grid either way.\n *\n * Prefer decorative media in a row whose title already names the thing\n * (`alt=\"\"`): the slot sits INSIDE the row's `<button>`/`<a>`, so a\n * described image is concatenated into the row's accessible name.\n *\n * Note: this shadows the native `media` attribute of `<a>` for the\n * anchor form of the row — pass it through `className`/a wrapper if\n * you genuinely need the HTML attribute. */\n media?: ReactNode;\n /** Secondary text shown under the title (e.g. a value). */\n value?: ReactNode;\n /** Trailing content — meta text, chevron icon, or a custom node. */\n trailing?: ReactNode;\n /** Primary label. */\n children: ReactNode;\n /** Render as a clickable element. When false (default), renders as a\n * static `<div>` with no state styling. When true, renders as\n * `<button>` (or `<a>` if `href` is also passed). */\n interactive?: boolean;\n /** Show the selected/active treatment (only meaningful when\n * `interactive` is true). Adds the `--active` modifier class. */\n active?: boolean;\n /** Disable interaction — only meaningful when `interactive`.\n * Forwards to the native `disabled` attribute on `<button>`, or\n * sets `aria-disabled` on `<a>`. */\n disabled?: boolean;\n /** Navigate to this URL on click. Implies `interactive`. */\n href?: string;\n className?: string;\n}\n\ntype AnchorRest = Omit<\n AnchorHTMLAttributes<HTMLAnchorElement>,\n keyof ListItemProps\n>;\ntype ButtonRest = Omit<\n ButtonHTMLAttributes<HTMLButtonElement>,\n keyof ListItemProps\n>;\ntype DivRest = Omit<HTMLAttributes<HTMLDivElement>, keyof ListItemProps>;\n\nexport function ListItem({\n icon,\n media,\n value,\n trailing,\n children,\n interactive,\n active,\n disabled,\n href,\n className,\n ...rest\n}: ListItemProps & (AnchorRest | ButtonRest | DivRest)) {\n const classes = cn(\n 'uxm-list-item',\n active && 'uxm-list-item--active',\n className,\n );\n const isInteractive = interactive || href !== undefined;\n const inner = (\n <>\n {/* Leading slot. `media` renders as-is so a product photo / avatar\n keeps its own frame; `icon` keeps the accent IconTile treatment.\n `media` wins when both are passed — a row has one leading grid\n position, and the as-is rendering is the more specific intent. */}\n {media ? (\n <span className=\"uxm-list-item__media\" style={LIST_ITEM_MEDIA_STYLE}>\n {media}\n </span>\n ) : (\n icon && (\n <IconTile className=\"uxm-list-item__icon\" style={LIST_ITEM_ICON_TILE_STYLE}>\n {icon}\n </IconTile>\n )\n )}\n <span className=\"uxm-list-item__content\">\n <span className=\"uxm-list-item__title\">{children}</span>\n {value !== undefined && value !== null && (\n <span className=\"uxm-list-item__value\">{value}</span>\n )}\n </span>\n {trailing && <span className=\"uxm-list-item__trailing\">{trailing}</span>}\n </>\n );\n\n if (href !== undefined) {\n return (\n <a\n href={href}\n className={classes}\n {...(disabled ? { 'aria-disabled': true as const, tabIndex: -1 } : {})}\n {...(rest as AnchorRest)}\n >\n {inner}\n </a>\n );\n }\n if (isInteractive) {\n return (\n <button\n type=\"button\"\n className={classes}\n disabled={disabled}\n aria-pressed={active}\n {...(rest as ButtonRest)}\n >\n {inner}\n </button>\n );\n }\n return (\n <div\n className={classes}\n {...(disabled ? { 'aria-disabled': true as const } : {})}\n {...(rest as DivRest)}\n >\n {inner}\n </div>\n );\n}\n"],"mappings":"AA0EI,SA0FA,UA1FA,KA0GE,YA1GF;AA1EJ,SAAS,UAAU;AAEnB,SAAS,gBAAgB;AAkBlB,MAAM,4BAA4B;AAAA,EACvC,sBAAsB;AAAA,EACtB,yBAAyB;AAAA,EACzB,wBAAwB;AAAA,EACxB,0BAA0B;AAAA,EAC1B,6BAA6B;AAC/B;AAgBO,MAAM,wBAAwB;AAAA,EACnC,wBAAwB;AAAA,EACxB,qBAAqB;AAAA,EACrB,2BAA2B;AAC7B;AA0BO,SAAS,KAAK,EAAE,UAAU,WAAW,UAAU,QAAQ,GAAG,KAAK,GAAc;AAClF,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,GAAG,YAAY,YAAY,WAAW,mBAAmB,SAAS;AAAA,MAC5E,GAAG;AAAA,MAEH;AAAA;AAAA,EACH;AAEJ;AA+DO,SAAS,SAAS;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAwD;AACtD,QAAM,UAAU;AAAA,IACd;AAAA,IACA,UAAU;AAAA,IACV;AAAA,EACF;AACA,QAAM,gBAAgB,eAAe,SAAS;AAC9C,QAAM,QACJ,iCAKG;AAAA,YACC,oBAAC,UAAK,WAAU,wBAAuB,OAAO,uBAC3C,iBACH,IAEA,QACE,oBAAC,YAAS,WAAU,uBAAsB,OAAO,2BAC9C,gBACH;AAAA,IAGJ,qBAAC,UAAK,WAAU,0BACd;AAAA,0BAAC,UAAK,WAAU,wBAAwB,UAAS;AAAA,MAChD,UAAU,UAAa,UAAU,QAChC,oBAAC,UAAK,WAAU,wBAAwB,iBAAM;AAAA,OAElD;AAAA,IACC,YAAY,oBAAC,UAAK,WAAU,2BAA2B,oBAAS;AAAA,KACnE;AAGF,MAAI,SAAS,QAAW;AACtB,WACE;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,WAAW;AAAA,QACV,GAAI,WAAW,EAAE,iBAAiB,MAAe,UAAU,GAAG,IAAI,CAAC;AAAA,QACnE,GAAI;AAAA,QAEJ;AAAA;AAAA,IACH;AAAA,EAEJ;AACA,MAAI,eAAe;AACjB,WACE;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,WAAW;AAAA,QACX;AAAA,QACA,gBAAc;AAAA,QACb,GAAI;AAAA,QAEJ;AAAA;AAAA,IACH;AAAA,EAEJ;AACA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,MACV,GAAI,WAAW,EAAE,iBAAiB,KAAc,IAAI,CAAC;AAAA,MACrD,GAAI;AAAA,MAEJ;AAAA;AAAA,EACH;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../../src/ui/list/list.tsx"],"sourcesContent":["import { cn } from '@/helpers';\n\nimport { Icon } from '../icon';\nimport { IconTile } from '../icon-tile';\n\nimport type {\n AnchorHTMLAttributes,\n ChangeEvent,\n ButtonHTMLAttributes,\n CSSProperties,\n HTMLAttributes,\n LabelHTMLAttributes,\n ReactNode,\n} from 'react';\n\n/**\n * Forward the studio icon knobs (projected as `--uxm-list-item-icon-*` on the\n * row) into the `IconTile` atom's own var namespace, so the accent-tile\n * pattern lives in one place — the `IconTile` atom — instead of being\n * re-implemented in `list.scss`. The glyph tracks the tile at 0.6× (the\n * row-icon proportion used across the design system). Kept as a module\n * constant so the studio preview can reuse the exact same forwarding.\n */\nexport const LIST_ITEM_ICON_TILE_STYLE = {\n '--uxm-icon-tile-bg': 'var(--uxm-list-item-icon-bg, var(--color-accent))',\n '--uxm-icon-tile-color': 'var(--uxm-list-item-icon-color, var(--color-card))',\n '--uxm-icon-tile-size': 'var(--uxm-list-item-icon-size, 24px)',\n '--uxm-icon-tile-radius': 'var(--uxm-list-item-icon-radius, 6px)',\n '--uxm-icon-tile-icon-size': 'calc(var(--uxm-list-item-icon-size, 24px) * 0.6)',\n} as CSSProperties;\n\n/**\n * Forward the row's media-size knob into the size vars of the atoms that\n * plausibly fill the `media` slot, so each one sizes itself through its OWN\n * machinery — frame, border, radius and inner glyph / initials all scale with\n * it — instead of being stretched from the outside. Mirrors\n * `LIST_ITEM_ICON_TILE_STYLE` above, and is the only order-independent fix:\n * both `Thumbnail` (`.uxm-thumbnail`, 0,1,0 but imported AFTER list in\n * `styles.css`) and `Avatar` (double-class size presets at 0,2,0, deliberately\n * so per its own note) would otherwise win or lose the cascade against a slot\n * rule purely on stylesheet order. The generic `> *` stretch in `list.scss`\n * stays as the fallback for a bare `<img>` / `<svg>` or any atom with no size\n * var of its own; both mechanisms resolve to the same number. Kept as a module\n * constant so the studio preview can reuse the exact same forwarding.\n */\nexport const LIST_ITEM_MEDIA_STYLE = {\n '--uxm-thumbnail-size': 'var(--uxm-list-item-media-size, 36px)',\n '--uxm-avatar-size': 'var(--uxm-list-item-media-size, 36px)',\n '--uxm-avatar-small-size': 'var(--uxm-list-item-media-size, 36px)',\n} as CSSProperties;\n\n/** Container presentation. See `ListProps.variant`. */\nexport type ListVariant = 'card' | 'plain';\n\nexport interface ListProps extends HTMLAttributes<HTMLDivElement> {\n children: ReactNode;\n /**\n * Container presentation. `card` (default) keeps the bordered, rounded\n * surface — right for a free-standing list on a page ground. `plain` drops\n * the background, border and radius for a list that is ALREADY inside a\n * surface (a flexpane body, a card body, a disclosure section), where a card\n * container reads as a card inside a card.\n *\n * `plain` also releases `overflow` to `visible`. The card has to clip so rows\n * cannot spill past its radius; with no radius there is nothing to clip to,\n * and `hidden` would only crop what a row deliberately renders OUTSIDE the\n * container box — an overhanging corner control, a focus ring with a positive\n * offset. Clipped, those are neither painted nor hit-testable.\n *\n * Row styling is untouched — every `--uxm-list-item-*` knob behaves\n * identically in both.\n */\n variant?: ListVariant;\n}\n\nexport function List({ children, className, variant = 'card', ...rest }: ListProps) {\n return (\n <div\n className={cn('uxm-list', variant === 'plain' && 'uxm-list--plain', className)}\n {...rest}\n >\n {children}\n </div>\n );\n}\n\n/**\n * ListItem renders as one of three element types depending on `interactive`:\n * - `<div>` (default) — display row, no state styling, not focusable\n * - `<button>` — interactive without navigation; emits click events\n * - `<a>` — interactive with navigation; pass `href`\n *\n * The state knobs (hover / focus / active / disabled) in the `list-item`\n * registry only paint when the row renders as button or anchor — the CSS\n * scopes them to element-type selectors so display-only divs stay inert.\n */\nexport interface ListItemProps {\n /** Leading icon — typically <Icon ... /> from @modo/uxm/ui. */\n icon?: ReactNode;\n /** Leading media rendered as-is (a <Thumbnail>, <img>, avatar…) — NOT\n * wrapped in the accent `IconTile` the `icon` slot uses. Mutually\n * exclusive with `icon`: when both are passed, `media` wins. The slot\n * box is `--uxm-list-item-media-size` (36px) square; a `Thumbnail` or\n * `Avatar` gets that size forwarded into its own size var (see\n * `LIST_ITEM_MEDIA_STYLE`) and anything else is stretched to fill, so\n * rows stay on one leading grid either way.\n *\n * Prefer decorative media in a row whose title already names the thing\n * (`alt=\"\"`): the slot sits INSIDE the row's `<button>`/`<a>`, so a\n * described image is concatenated into the row's accessible name.\n *\n * Note: this shadows the native `media` attribute of `<a>` for the\n * anchor form of the row — pass it through `className`/a wrapper if\n * you genuinely need the HTML attribute. */\n media?: ReactNode;\n /** Secondary text shown under the title (e.g. a value). */\n value?: ReactNode;\n /** Trailing content — meta text, chevron icon, or a custom node. */\n trailing?: ReactNode;\n /** Primary label. */\n children: ReactNode;\n /** Render as a clickable element. When false (default), renders as a\n * static `<div>` with no state styling. When true, renders as\n * `<button>` (or `<a>` if `href` is also passed). */\n interactive?: boolean;\n /** Show the selected/active treatment (only meaningful when\n * `interactive` is true). Adds the `--active` modifier class. */\n active?: boolean;\n /** Disable interaction — only meaningful when `interactive`.\n * Forwards to the native `disabled` attribute on `<button>`, or\n * sets `aria-disabled` on `<a>`. */\n disabled?: boolean;\n /** Navigate to this URL on click. Implies `interactive`. */\n href?: string;\n /**\n * Controlled selection state. Passing `selected` OR `onSelectedChange` puts\n * the row in **selectable** mode: it renders as a `<label>` around a real\n * checkbox, so activating anywhere on the row toggles it natively — the\n * shape for \"tick several, then commit\" lists (a product picker, recipients,\n * file selection).\n *\n * Distinct from `active`, which is a *toggle button* (`aria-pressed`) —\n * the wrong semantic for a member of a checkable set. The two can still\n * combine if a consumer wants the row-fill as well as the tick.\n *\n * Mutually exclusive with `interactive` / `href`: a row either navigates or\n * is ticked, and selectable mode wins if both are passed (dev warning).\n */\n selected?: boolean;\n /** Toggle handler for selectable mode; `next` is the new checked state. */\n onSelectedChange?: (next: boolean, e: ChangeEvent<HTMLInputElement>) => void;\n className?: string;\n}\n\ntype AnchorRest = Omit<\n AnchorHTMLAttributes<HTMLAnchorElement>,\n keyof ListItemProps\n>;\ntype ButtonRest = Omit<\n ButtonHTMLAttributes<HTMLButtonElement>,\n keyof ListItemProps\n>;\ntype DivRest = Omit<HTMLAttributes<HTMLDivElement>, keyof ListItemProps>;\n// Selectable rows render a <label>, whose handlers are typed to\n// HTMLLabelElement — a DivRest cast does not fit it.\ntype LabelRest = Omit<LabelHTMLAttributes<HTMLLabelElement>, keyof ListItemProps>;\n\nexport function ListItem({\n icon,\n media,\n value,\n trailing,\n children,\n interactive,\n active,\n disabled,\n href,\n selected,\n onSelectedChange,\n className,\n ...rest\n}: ListItemProps & (AnchorRest | ButtonRest | DivRest | LabelRest)) {\n const isSelectable = selected !== undefined || onSelectedChange !== undefined;\n const classes = cn(\n 'uxm-list-item',\n active && 'uxm-list-item--active',\n isSelectable && 'uxm-list-item--selectable',\n className,\n );\n const isInteractive = (interactive || href !== undefined) && !isSelectable;\n\n if (\n process.env.NODE_ENV !== 'production' &&\n isSelectable &&\n (interactive || href !== undefined)\n ) {\n \n console.warn(\n '[uxm] ListItem: `selected`/`onSelectedChange` cannot be combined with `interactive`/`href` — ' +\n 'a row is either a navigate/activate row or a member of a checkable set. Selectable mode wins.',\n );\n }\n const inner = (\n <>\n {/* Selection affordance, before the leading slot. It renders `Checkbox`'s\n OWN classes rather than nesting the component: `Checkbox` is itself a\n `<label>`, and a `<label>` inside the row `<label>` is invalid HTML\n (Chrome tolerates it — measured, one change event either way — but the\n spec does not, and assistive tech is not guaranteed to). Reusing the\n classes keeps the box pixel-identical and re-tints with the checkbox's\n own knobs, at the cost of a coupling to its internals: `list-item.test`\n asserts these classes still match what `Checkbox` renders, so a change\n there fails here rather than shipping an unstyled box. */}\n {isSelectable && (\n <span className=\"uxm-list-item__check\">\n <input\n type=\"checkbox\"\n className=\"uxm-checkbox__input\"\n // `?? false`, never a bare `undefined`: selectable mode is\n // documented as controlled, and a consumer deriving it from async\n // data (`selected={data?.picked.includes(id)}`) would otherwise\n // hand React an uncontrolled input that turns controlled the\n // moment the data lands — React warns, and until it does the DOM\n // owns the checked state, so a tick made while loading is taken\n // over instead of reported. `RadioGroup` pins its own mode for the\n // component's lifetime for exactly this reason.\n checked={selected ?? false}\n disabled={disabled}\n onChange={(e) => onSelectedChange?.(e.target.checked, e)}\n />\n <span className=\"uxm-checkbox__box\" aria-hidden=\"true\">\n <Icon glyph=\"check\" strokeWidth={3} />\n </span>\n </span>\n )}\n {/* Leading slot. `media` renders as-is so a product photo / avatar\n keeps its own frame; `icon` keeps the accent IconTile treatment.\n `media` wins when both are passed — a row has one leading grid\n position, and the as-is rendering is the more specific intent. */}\n {media ? (\n <span className=\"uxm-list-item__media\" style={LIST_ITEM_MEDIA_STYLE}>\n {media}\n </span>\n ) : (\n icon && (\n <IconTile className=\"uxm-list-item__icon\" style={LIST_ITEM_ICON_TILE_STYLE}>\n {icon}\n </IconTile>\n )\n )}\n <span className=\"uxm-list-item__content\">\n <span className=\"uxm-list-item__title\">{children}</span>\n {value !== undefined && value !== null && (\n <span className=\"uxm-list-item__value\">{value}</span>\n )}\n </span>\n {trailing && <span className=\"uxm-list-item__trailing\">{trailing}</span>}\n </>\n );\n\n if (isSelectable) {\n // A real `<label>`: whole-row activation is the browser's, not a click\n // handler's, so it works for pointer and keyboard and needs no ARIA of its\n // own. The row is NOT `aria-pressed` — that is a toggle button, the wrong\n // semantic for one member of a checkable set.\n return (\n <label\n className={classes}\n {...(disabled ? { 'aria-disabled': true as const } : {})}\n {...(rest as LabelRest)}\n >\n {inner}\n </label>\n );\n }\n\n if (href !== undefined) {\n return (\n <a\n href={href}\n className={classes}\n {...(disabled ? { 'aria-disabled': true as const, tabIndex: -1 } : {})}\n {...(rest as AnchorRest)}\n >\n {inner}\n </a>\n );\n }\n if (isInteractive) {\n return (\n <button\n type=\"button\"\n className={classes}\n disabled={disabled}\n aria-pressed={active}\n {...(rest as ButtonRest)}\n >\n {inner}\n </button>\n );\n }\n return (\n <div\n className={classes}\n {...(disabled ? { 'aria-disabled': true as const } : {})}\n {...(rest as DivRest)}\n >\n {inner}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -16,8 +16,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
16
16
  var menu_exports = {};
17
17
  module.exports = __toCommonJS(menu_exports);
18
18
  __reExport(menu_exports, require("./menu.cjs"), module.exports);
19
+ __reExport(menu_exports, require("./menu-button.cjs"), module.exports);
19
20
  // Annotate the CommonJS export names for ESM import in node:
20
21
  0 && (module.exports = {
21
- ...require("./menu.cjs")
22
+ ...require("./menu.cjs"),
23
+ ...require("./menu-button.cjs")
22
24
  });
23
25
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/ui/menu/index.ts"],"sourcesContent":["export * from './menu';\n"],"mappings":";;;;;;;;;;;;;;;AAAA;AAAA;AAAA,yBAAc,mBAAd;","names":[]}
1
+ {"version":3,"sources":["../../../src/ui/menu/index.ts"],"sourcesContent":["export * from './menu';\nexport * from './menu-button';\n"],"mappings":";;;;;;;;;;;;;;;AAAA;AAAA;AAAA,yBAAc,mBAAd;AACA,yBAAc,0BADd;","names":[]}
@@ -1,2 +1,3 @@
1
1
  export * from './menu.js';
2
+ export * from './menu-button.js';
2
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ui/menu/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ui/menu/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC;AACvB,cAAc,eAAe,CAAC"}
@@ -1,2 +1,3 @@
1
1
  export * from "./menu.js";
2
+ export * from "./menu-button.js";
2
3
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/ui/menu/index.ts"],"sourcesContent":["export * from './menu';\n"],"mappings":"AAAA,cAAc;","names":[]}
1
+ {"version":3,"sources":["../../../src/ui/menu/index.ts"],"sourcesContent":["export * from './menu';\nexport * from './menu-button';\n"],"mappings":"AAAA,cAAc;AACd,cAAc;","names":[]}
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var menu_button_exports = {};
20
+ __export(menu_button_exports, {
21
+ MenuButton: () => MenuButton
22
+ });
23
+ module.exports = __toCommonJS(menu_button_exports);
24
+ var import_jsx_runtime = (
25
+ // `triggerProps` spreads straight onto the button — its `ref` included.
26
+ // Before 4.45.0 the button family did not declare `ref`, which is why
27
+ // consumers wrapped the trigger in a `<span ref>`; that wrapper is what
28
+ // this composite exists to stop re-deriving.
29
+ require("react/jsx-runtime")
30
+ );
31
+ var import_helpers = require("../../helpers/index.cjs");
32
+ var import_button = require("../button/index.cjs");
33
+ var import_icon = require("../icon/index.cjs");
34
+ var import_menu = require("./menu.cjs");
35
+ const TRIGGER = {
36
+ primary: import_button.ButtonPrimary,
37
+ secondary: import_button.ButtonSecondary,
38
+ tertiary: import_button.ButtonTertiary,
39
+ ghost: import_button.ButtonGhost
40
+ };
41
+ function MenuButton({
42
+ items,
43
+ children,
44
+ variant = "secondary",
45
+ icon,
46
+ chevron = true,
47
+ placement,
48
+ disabled,
49
+ "aria-label": ariaLabel,
50
+ className
51
+ }) {
52
+ const Trigger = TRIGGER[variant];
53
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
54
+ import_menu.Menu,
55
+ {
56
+ items,
57
+ placement,
58
+ "aria-label": ariaLabel,
59
+ renderTrigger: ({ open, triggerProps }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
60
+ Trigger,
61
+ {
62
+ ...triggerProps,
63
+ disabled,
64
+ className: (0, import_helpers.cn)("uxm-menu-button", className),
65
+ children: [
66
+ icon,
67
+ children,
68
+ chevron && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
69
+ import_icon.Icon,
70
+ {
71
+ glyph: "chevron-down",
72
+ size: 14,
73
+ className: (0, import_helpers.cn)(
74
+ "uxm-menu-button__chevron",
75
+ open && "uxm-menu-button__chevron--open"
76
+ )
77
+ }
78
+ )
79
+ ]
80
+ }
81
+ )
82
+ }
83
+ );
84
+ }
85
+ // Annotate the CommonJS export names for ESM import in node:
86
+ 0 && (module.exports = {
87
+ MenuButton
88
+ });
89
+ //# sourceMappingURL=menu-button.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/ui/menu/menu-button.tsx"],"sourcesContent":["import { cn } from '@/helpers';\nimport {\n ButtonGhost,\n ButtonPrimary,\n ButtonSecondary,\n ButtonTertiary,\n} from '@/ui/button';\nimport { Icon } from '@/ui/icon';\nimport type { PopoverPlacement } from '@/ui/popover';\n\nimport { Menu } from './menu';\n\nimport type { MenuEntry } from './menu';\nimport type { ComponentPropsWithRef, ReactNode } from 'react';\n\n/** Which shipped button renders the trigger. */\nexport type MenuButtonVariant = 'primary' | 'secondary' | 'tertiary' | 'ghost';\n\nconst TRIGGER: Record<\n MenuButtonVariant,\n (props: ComponentPropsWithRef<'button'>) => ReactNode\n> = {\n primary: ButtonPrimary,\n secondary: ButtonSecondary,\n tertiary: ButtonTertiary,\n ghost: ButtonGhost,\n};\n\nexport interface MenuButtonProps {\n /** Menu entries — the same `MenuEntry[]` `Menu` takes. */\n items: MenuEntry[];\n /** Button label. */\n children: ReactNode;\n /** Which shipped button renders the trigger. Default `secondary`. */\n variant?: MenuButtonVariant;\n /** Optional leading icon, before the label. */\n icon?: ReactNode;\n /**\n * Trailing dropdown affordance. Default a `chevron-down` that rotates while\n * the menu is open; pass `false` to omit it (for a trigger whose label\n * already reads as a menu, or where space is tight).\n */\n chevron?: boolean;\n /** Forwarded to `Menu`. Default `bottom-end`, as `Menu`'s own default. */\n placement?: PopoverPlacement;\n /** Disables the trigger. The menu cannot be opened. */\n disabled?: boolean;\n /** Accessible name for the menu panel. Forwarded to `Menu`. */\n 'aria-label'?: string;\n /** Class on the trigger button. */\n className?: string;\n}\n\n/**\n * The labelled dropdown button — a `Menu` whose trigger is one of the shipped\n * `Button*` atoms, with the chevron, the ARIA and the trigger ref already\n * wired.\n *\n * `Menu` is deliberately trigger-agnostic: `renderTrigger` is required so a ⋮\n * `IconButton`, an avatar or a field can all open one. That is the right\n * primitive, but it means the single most common shape — a button that says\n * what it does and drops a menu — is re-derived at every call site, along with\n * a hand-added chevron and the `triggerProps` plumbing. This is that shape,\n * shipped once.\n *\n * It is a composition, not a new visual primitive: the button is a real\n * `Button*`, the menu is a real `Menu`. Anything this does not expose —\n * controlled `open`, `matchAnchorWidth`, a bespoke trigger — is a sign to drop\n * to `Menu` + `renderTrigger`, which is unchanged and always available.\n */\nexport function MenuButton({\n items,\n children,\n variant = 'secondary',\n icon,\n chevron = true,\n placement,\n disabled,\n 'aria-label': ariaLabel,\n className,\n}: MenuButtonProps) {\n const Trigger = TRIGGER[variant];\n\n return (\n <Menu\n items={items}\n placement={placement}\n aria-label={ariaLabel}\n renderTrigger={({ open, triggerProps }) => (\n // `triggerProps` spreads straight onto the button — its `ref` included.\n // Before 4.45.0 the button family did not declare `ref`, which is why\n // consumers wrapped the trigger in a `<span ref>`; that wrapper is what\n // this composite exists to stop re-deriving.\n <Trigger\n {...triggerProps}\n disabled={disabled}\n className={cn('uxm-menu-button', className)}\n >\n {icon}\n {children}\n {chevron && (\n <Icon\n glyph=\"chevron-down\"\n size={14}\n className={cn(\n 'uxm-menu-button__chevron',\n open && 'uxm-menu-button__chevron--open',\n )}\n />\n )}\n </Trigger>\n )}\n />\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA6FQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA7FR,qBAAmB;AACnB,oBAKO;AACP,kBAAqB;AAGrB,kBAAqB;AAQrB,MAAM,UAGF;AAAA,EACF,SAAS;AAAA,EACT,WAAW;AAAA,EACX,UAAU;AAAA,EACV,OAAO;AACT;AA4CO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd;AACF,GAAoB;AAClB,QAAM,UAAU,QAAQ,OAAO;AAE/B,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA;AAAA,MACA,cAAY;AAAA,MACZ,eAAe,CAAC,EAAE,MAAM,aAAa,MAKnC;AAAA,QAAC;AAAA;AAAA,UACE,GAAG;AAAA,UACJ;AAAA,UACA,eAAW,mBAAG,mBAAmB,SAAS;AAAA,UAEzC;AAAA;AAAA,YACA;AAAA,YACA,WACC;AAAA,cAAC;AAAA;AAAA,gBACC,OAAM;AAAA,gBACN,MAAM;AAAA,gBACN,eAAW;AAAA,kBACT;AAAA,kBACA,QAAQ;AAAA,gBACV;AAAA;AAAA,YACF;AAAA;AAAA;AAAA,MAEJ;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
@@ -0,0 +1,30 @@
1
+ /* ── menu-button ── */
2
+ /* `MenuButton` is a composition: the trigger IS a shipped `Button*`, so the
3
+ whole row — `display: inline-flex`, the alignment, and
4
+ `gap: var(--uxm-button-{variant}-gap, 8px)` — already comes from that atom.
5
+ Nothing here re-declares any of it.
6
+
7
+ That is not tidiness, it is the difference between the button's gap knob
8
+ working and not: `styles.css` imports `button.css` at line 16 and this file
9
+ at 65, so a `gap` declared here would win the cascade at equal specificity
10
+ and quietly pin a MenuButton to its own value while every other button in
11
+ the app followed `--uxm-button-{variant}-gap`.
12
+
13
+ So this block owns exactly one thing the button cannot know about: the
14
+ chevron's open state. */
15
+ .uxm-menu-button__chevron {
16
+ flex-shrink: 0;
17
+ /* Turns to point at the open panel. Purely decorative — `aria-expanded` on
18
+ the trigger is what actually announces the state. */
19
+ transition: transform 0.15s ease;
20
+ }
21
+ .uxm-menu-button__chevron--open {
22
+ transform: rotate(180deg);
23
+ }
24
+
25
+ /* The rotation is decoration; the chevron must still point the right way. */
26
+ @media (prefers-reduced-motion: reduce) {
27
+ .uxm-menu-button__chevron {
28
+ transition: none;
29
+ }
30
+ }
@@ -0,0 +1,48 @@
1
+ import type { PopoverPlacement } from '../popover/index.js';
2
+ import type { MenuEntry } from './menu.js';
3
+ import type { ReactNode } from 'react';
4
+ /** Which shipped button renders the trigger. */
5
+ export type MenuButtonVariant = 'primary' | 'secondary' | 'tertiary' | 'ghost';
6
+ export interface MenuButtonProps {
7
+ /** Menu entries — the same `MenuEntry[]` `Menu` takes. */
8
+ items: MenuEntry[];
9
+ /** Button label. */
10
+ children: ReactNode;
11
+ /** Which shipped button renders the trigger. Default `secondary`. */
12
+ variant?: MenuButtonVariant;
13
+ /** Optional leading icon, before the label. */
14
+ icon?: ReactNode;
15
+ /**
16
+ * Trailing dropdown affordance. Default a `chevron-down` that rotates while
17
+ * the menu is open; pass `false` to omit it (for a trigger whose label
18
+ * already reads as a menu, or where space is tight).
19
+ */
20
+ chevron?: boolean;
21
+ /** Forwarded to `Menu`. Default `bottom-end`, as `Menu`'s own default. */
22
+ placement?: PopoverPlacement;
23
+ /** Disables the trigger. The menu cannot be opened. */
24
+ disabled?: boolean;
25
+ /** Accessible name for the menu panel. Forwarded to `Menu`. */
26
+ 'aria-label'?: string;
27
+ /** Class on the trigger button. */
28
+ className?: string;
29
+ }
30
+ /**
31
+ * The labelled dropdown button — a `Menu` whose trigger is one of the shipped
32
+ * `Button*` atoms, with the chevron, the ARIA and the trigger ref already
33
+ * wired.
34
+ *
35
+ * `Menu` is deliberately trigger-agnostic: `renderTrigger` is required so a ⋮
36
+ * `IconButton`, an avatar or a field can all open one. That is the right
37
+ * primitive, but it means the single most common shape — a button that says
38
+ * what it does and drops a menu — is re-derived at every call site, along with
39
+ * a hand-added chevron and the `triggerProps` plumbing. This is that shape,
40
+ * shipped once.
41
+ *
42
+ * It is a composition, not a new visual primitive: the button is a real
43
+ * `Button*`, the menu is a real `Menu`. Anything this does not expose —
44
+ * controlled `open`, `matchAnchorWidth`, a bespoke trigger — is a sign to drop
45
+ * to `Menu` + `renderTrigger`, which is unchanged and always available.
46
+ */
47
+ export declare function MenuButton({ items, children, variant, icon, chevron, placement, disabled, 'aria-label': ariaLabel, className, }: MenuButtonProps): import("react").JSX.Element;
48
+ //# sourceMappingURL=menu-button.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"menu-button.d.ts","sourceRoot":"","sources":["../../../src/ui/menu/menu-button.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAIrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,KAAK,EAAyB,SAAS,EAAE,MAAM,OAAO,CAAC;AAE9D,gDAAgD;AAChD,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,WAAW,GAAG,UAAU,GAAG,OAAO,CAAC;AAY/E,MAAM,WAAW,eAAe;IAC9B,0DAA0D;IAC1D,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,oBAAoB;IACpB,QAAQ,EAAE,SAAS,CAAC;IACpB,qEAAqE;IACrE,OAAO,CAAC,EAAE,iBAAiB,CAAC;IAC5B,+CAA+C;IAC/C,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,0EAA0E;IAC1E,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAC7B,uDAAuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,+DAA+D;IAC/D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mCAAmC;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,OAAqB,EACrB,IAAI,EACJ,OAAc,EACd,SAAS,EACT,QAAQ,EACR,YAAY,EAAE,SAAS,EACvB,SAAS,GACV,EAAE,eAAe,+BAkCjB"}
@@ -0,0 +1,70 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { cn } from "../../helpers/index.js";
3
+ import {
4
+ ButtonGhost,
5
+ ButtonPrimary,
6
+ ButtonSecondary,
7
+ ButtonTertiary
8
+ } from "../button/index.js";
9
+ import { Icon } from "../icon/index.js";
10
+ import { Menu } from "./menu.js";
11
+ const TRIGGER = {
12
+ primary: ButtonPrimary,
13
+ secondary: ButtonSecondary,
14
+ tertiary: ButtonTertiary,
15
+ ghost: ButtonGhost
16
+ };
17
+ function MenuButton({
18
+ items,
19
+ children,
20
+ variant = "secondary",
21
+ icon,
22
+ chevron = true,
23
+ placement,
24
+ disabled,
25
+ "aria-label": ariaLabel,
26
+ className
27
+ }) {
28
+ const Trigger = TRIGGER[variant];
29
+ return /* @__PURE__ */ jsx(
30
+ Menu,
31
+ {
32
+ items,
33
+ placement,
34
+ "aria-label": ariaLabel,
35
+ renderTrigger: ({ open, triggerProps }) => (
36
+ // `triggerProps` spreads straight onto the button — its `ref` included.
37
+ // Before 4.45.0 the button family did not declare `ref`, which is why
38
+ // consumers wrapped the trigger in a `<span ref>`; that wrapper is what
39
+ // this composite exists to stop re-deriving.
40
+ /* @__PURE__ */ jsxs(
41
+ Trigger,
42
+ {
43
+ ...triggerProps,
44
+ disabled,
45
+ className: cn("uxm-menu-button", className),
46
+ children: [
47
+ icon,
48
+ children,
49
+ chevron && /* @__PURE__ */ jsx(
50
+ Icon,
51
+ {
52
+ glyph: "chevron-down",
53
+ size: 14,
54
+ className: cn(
55
+ "uxm-menu-button__chevron",
56
+ open && "uxm-menu-button__chevron--open"
57
+ )
58
+ }
59
+ )
60
+ ]
61
+ }
62
+ )
63
+ )
64
+ }
65
+ );
66
+ }
67
+ export {
68
+ MenuButton
69
+ };
70
+ //# sourceMappingURL=menu-button.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/ui/menu/menu-button.tsx"],"sourcesContent":["import { cn } from '@/helpers';\nimport {\n ButtonGhost,\n ButtonPrimary,\n ButtonSecondary,\n ButtonTertiary,\n} from '@/ui/button';\nimport { Icon } from '@/ui/icon';\nimport type { PopoverPlacement } from '@/ui/popover';\n\nimport { Menu } from './menu';\n\nimport type { MenuEntry } from './menu';\nimport type { ComponentPropsWithRef, ReactNode } from 'react';\n\n/** Which shipped button renders the trigger. */\nexport type MenuButtonVariant = 'primary' | 'secondary' | 'tertiary' | 'ghost';\n\nconst TRIGGER: Record<\n MenuButtonVariant,\n (props: ComponentPropsWithRef<'button'>) => ReactNode\n> = {\n primary: ButtonPrimary,\n secondary: ButtonSecondary,\n tertiary: ButtonTertiary,\n ghost: ButtonGhost,\n};\n\nexport interface MenuButtonProps {\n /** Menu entries — the same `MenuEntry[]` `Menu` takes. */\n items: MenuEntry[];\n /** Button label. */\n children: ReactNode;\n /** Which shipped button renders the trigger. Default `secondary`. */\n variant?: MenuButtonVariant;\n /** Optional leading icon, before the label. */\n icon?: ReactNode;\n /**\n * Trailing dropdown affordance. Default a `chevron-down` that rotates while\n * the menu is open; pass `false` to omit it (for a trigger whose label\n * already reads as a menu, or where space is tight).\n */\n chevron?: boolean;\n /** Forwarded to `Menu`. Default `bottom-end`, as `Menu`'s own default. */\n placement?: PopoverPlacement;\n /** Disables the trigger. The menu cannot be opened. */\n disabled?: boolean;\n /** Accessible name for the menu panel. Forwarded to `Menu`. */\n 'aria-label'?: string;\n /** Class on the trigger button. */\n className?: string;\n}\n\n/**\n * The labelled dropdown button — a `Menu` whose trigger is one of the shipped\n * `Button*` atoms, with the chevron, the ARIA and the trigger ref already\n * wired.\n *\n * `Menu` is deliberately trigger-agnostic: `renderTrigger` is required so a ⋮\n * `IconButton`, an avatar or a field can all open one. That is the right\n * primitive, but it means the single most common shape — a button that says\n * what it does and drops a menu — is re-derived at every call site, along with\n * a hand-added chevron and the `triggerProps` plumbing. This is that shape,\n * shipped once.\n *\n * It is a composition, not a new visual primitive: the button is a real\n * `Button*`, the menu is a real `Menu`. Anything this does not expose —\n * controlled `open`, `matchAnchorWidth`, a bespoke trigger — is a sign to drop\n * to `Menu` + `renderTrigger`, which is unchanged and always available.\n */\nexport function MenuButton({\n items,\n children,\n variant = 'secondary',\n icon,\n chevron = true,\n placement,\n disabled,\n 'aria-label': ariaLabel,\n className,\n}: MenuButtonProps) {\n const Trigger = TRIGGER[variant];\n\n return (\n <Menu\n items={items}\n placement={placement}\n aria-label={ariaLabel}\n renderTrigger={({ open, triggerProps }) => (\n // `triggerProps` spreads straight onto the button — its `ref` included.\n // Before 4.45.0 the button family did not declare `ref`, which is why\n // consumers wrapped the trigger in a `<span ref>`; that wrapper is what\n // this composite exists to stop re-deriving.\n <Trigger\n {...triggerProps}\n disabled={disabled}\n className={cn('uxm-menu-button', className)}\n >\n {icon}\n {children}\n {chevron && (\n <Icon\n glyph=\"chevron-down\"\n size={14}\n className={cn(\n 'uxm-menu-button__chevron',\n open && 'uxm-menu-button__chevron--open',\n )}\n />\n )}\n </Trigger>\n )}\n />\n );\n}\n"],"mappings":"AA6FQ,SAQI,KARJ;AA7FR,SAAS,UAAU;AACnB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,YAAY;AAGrB,SAAS,YAAY;AAQrB,MAAM,UAGF;AAAA,EACF,SAAS;AAAA,EACT,WAAW;AAAA,EACX,UAAU;AAAA,EACV,OAAO;AACT;AA4CO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd;AACF,GAAoB;AAClB,QAAM,UAAU,QAAQ,OAAO;AAE/B,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA;AAAA,MACA,cAAY;AAAA,MACZ,eAAe,CAAC,EAAE,MAAM,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA,QAKnC;AAAA,UAAC;AAAA;AAAA,YACE,GAAG;AAAA,YACJ;AAAA,YACA,WAAW,GAAG,mBAAmB,SAAS;AAAA,YAEzC;AAAA;AAAA,cACA;AAAA,cACA,WACC;AAAA,gBAAC;AAAA;AAAA,kBACC,OAAM;AAAA,kBACN,MAAM;AAAA,kBACN,WAAW;AAAA,oBACT;AAAA,oBACA,QAAQ;AAAA,kBACV;AAAA;AAAA,cACF;AAAA;AAAA;AAAA,QAEJ;AAAA;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
@@ -62,6 +62,7 @@
62
62
  @import "./listbox/listbox.css";
63
63
  @import "./loader/loader.css";
64
64
  @import "./menu/menu.css";
65
+ @import "./menu/menu-button.css";
65
66
  @import "./meta-row/meta-row.css";
66
67
  @import "./modal/modal.css";
67
68
  @import "./number-stepper/number-stepper.css";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viax.io/uxm",
3
- "version": "4.46.0",
3
+ "version": "4.48.0",
4
4
  "description": "Viax UXM — React 19 UI primitives and design tokens.",
5
5
  "license": "MIT",
6
6
  "private": false,