@jonmilner/ui 0.4.0 → 0.4.1

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.
@@ -1,2 +1,2 @@
1
1
  @layer tokens, reset, atoms, components.box, components.checkbox, components.clickable, components.icon, components.icon-button, components.inline, components.stack, components.text, components.badge, components.button, components.chip, components.field, components.heading, components.dialog, components.alert-dialog, components.menu, components.multiselect, components.search-field, components.tabs, components.tag, components.toast, components.visually-hidden;
2
- @layer components.menu{:where(._393eil0[data-closed])>[data-base-ui-focus-guard]{visibility:hidden}._393eil1{border:var(--space-1) solid var(--color-border);transform-origin:var(--transform-origin);transition:opacity var(--duration-fast) var(--easing-standard), scale var(--duration-fast) var(--easing-enter)}._393eil1:focus-visible{outline:var(--space-2) solid var(--color-focus);outline-offset:var(--space-2)}._393eil1[data-starting-style],._393eil1[data-ending-style]{opacity:0;scale:calc(1 - var(--scale-med))}._393eil2{cursor:pointer;touch-action:manipulation;-webkit-tap-highlight-color:transparent;-webkit-user-select:none;user-select:none}._393eil2:focus-visible{outline:var(--space-2) solid var(--color-focus);outline-offset:var(--space-2)}._393eil2[data-highlighted]{background-color:light-dark(var(--color-bg-muted),color-mix(in srgb, var(--color-fg) 10%, var(--color-bg-raised)))}._393eil2[aria-disabled=true]{color:var(--color-fg-muted);cursor:not-allowed}._393eil3{border-block-start:var(--space-1) solid var(--color-border)}@media (forced-colors:active){._393eil2[data-highlighted]{forced-color-adjust:none;color:highlighttext;background-color:highlight}._393eil2[aria-disabled=true]{color:graytext}}}
2
+ @layer components.menu{:where(._393eil0[data-closed])>[data-base-ui-focus-guard]{visibility:hidden}._393eil1{border:var(--space-1) solid var(--color-border);transform-origin:var(--transform-origin);transition:opacity var(--duration-fast) var(--easing-standard), scale var(--duration-fast) var(--easing-enter)}._393eil1:focus-visible{outline:var(--space-2) solid var(--color-focus);outline-offset:var(--space-2)}._393eil1[data-starting-style],._393eil1[data-ending-style]{opacity:0;scale:calc(1 - var(--scale-med))}._393eil2{cursor:pointer;touch-action:manipulation;-webkit-tap-highlight-color:transparent;-webkit-user-select:none;user-select:none}._393eil2:focus-visible{outline:var(--space-2) solid var(--color-focus);outline-offset:var(--space-2)}._393eil2[data-highlighted]{background-color:light-dark(var(--color-bg-muted),color-mix(in srgb, var(--color-fg) 10%, var(--color-bg-raised)))}._393eil2[aria-disabled=true]{color:var(--color-fg-muted);cursor:not-allowed}._393eil3{text-decoration:none}._393eil4{border-block-start:var(--space-1) solid var(--color-border)}@media (forced-colors:active){._393eil2[data-highlighted]:not([aria-disabled=true]){forced-color-adjust:none;color:highlighttext;background-color:highlight}._393eil2[aria-disabled=true]{color:graytext}}}
@@ -3,6 +3,6 @@ import "./Menu.css";
3
3
  /* empty css */
4
4
  /* empty css */
5
5
  //#region src/Menu/Menu.css.ts
6
- var e = "_393eil0", t = "_393eil1", n = "_393eil2", r = "_393eil3";
6
+ var e = "_393eil0", t = "_393eil1", n = "_393eil2", r = "_393eil3", i = "_393eil4";
7
7
  //#endregion
8
- export { n as item, t as popup, e as positioner, r as separator };
8
+ export { n as item, r as linkItem, t as popup, e as positioner, i as separator };
@@ -14,6 +14,10 @@ export type MenuPopupProps = PartProps<Menu$1.Popup.Props> & Pick<Menu$1.Positio
14
14
  export type MenuItemProps = PartProps<Menu$1.Item.Props> & {
15
15
  variant?: "neutral" | "danger";
16
16
  };
17
+ export type MenuLinkItemProps = PartProps<Menu$1.LinkItem.Props> & {
18
+ disabled?: boolean;
19
+ href: string;
20
+ };
17
21
  export type MenuSeparatorProps = PartProps<Menu$1.Separator.Props>;
18
22
  export type MenuGroupProps = PartProps<Menu$1.Group.Props> & {
19
23
  label: ReactNode;
@@ -21,12 +25,14 @@ export type MenuGroupProps = PartProps<Menu$1.Group.Props> & {
21
25
  declare function MenuRoot({ children, ...props }: MenuProps): import("react").JSX.Element;
22
26
  declare function MenuPopup({ side, align, backgroundColor, borderRadius, boxShadow, padding, className, style, ...props }: MenuPopupProps): import("react").JSX.Element;
23
27
  declare function MenuItem({ variant, display, alignItems, gap, minBlockSize, paddingInline, borderRadius, color, className, style, children, ...props }: MenuItemProps): import("react").JSX.Element;
28
+ declare function MenuLinkItem({ href, disabled, closeOnClick, onClick, display, alignItems, gap, minBlockSize, paddingInline, borderRadius, color, className, style, children, ...props }: MenuLinkItemProps): import("react").JSX.Element;
24
29
  declare function MenuSeparator({ marginBlock, marginInline, className, style, ...props }: MenuSeparatorProps): import("react").JSX.Element;
25
30
  declare function MenuGroup({ label, className, style, children, ...props }: MenuGroupProps): import("react").JSX.Element;
26
31
  export declare const Menu: typeof MenuRoot & {
27
32
  Trigger: typeof MenuTrigger;
28
33
  Popup: typeof MenuPopup;
29
34
  Item: typeof MenuItem;
35
+ LinkItem: typeof MenuLinkItem;
30
36
  Separator: typeof MenuSeparator;
31
37
  Group: typeof MenuGroup;
32
38
  };
@@ -3,85 +3,112 @@ import { splitStyleProps as t } from "../styles/split-style-props.mjs";
3
3
  import { Box as n } from "../Box/index.mjs";
4
4
  import { Text as r } from "../Text/index.mjs";
5
5
  import { PopupScope as i } from "../popup/client.mjs";
6
- import { item as a, popup as o, separator as s } from "./Menu.css.mjs";
7
- import { MenuPortal as c, MenuPositioner as l, MenuTrigger as u } from "./client.mjs";
8
- import { jsx as d, jsxs as f } from "react/jsx-runtime";
9
- import { Children as p } from "react";
10
- import { Menu as m } from "@base-ui/react/menu";
6
+ import { item as a, linkItem as o, popup as s, separator as c } from "./Menu.css.mjs";
7
+ import { MenuPortal as l, MenuPositioner as u, MenuTrigger as d } from "./client.mjs";
8
+ import { jsx as f, jsxs as p } from "react/jsx-runtime";
9
+ import { Children as m } from "react";
10
+ import { Menu as h } from "@base-ui/react/menu";
11
11
  //#region src/Menu/index.tsx
12
- function h({ children: e, ...t }) {
13
- return /* @__PURE__ */ d(i, { children: /* @__PURE__ */ d(m.Root, {
12
+ function g({ children: e, ...t }) {
13
+ return /* @__PURE__ */ f(i, { children: /* @__PURE__ */ f(h.Root, {
14
14
  ...t,
15
15
  children: e
16
16
  }) });
17
17
  }
18
- function g({ side: r = "bottom", align: i = "start", backgroundColor: a = "bgRaised", borderRadius: s = "med", boxShadow: u = "med", padding: f = "4", className: p, style: h, ...g }) {
18
+ function _({ side: r = "bottom", align: i = "start", backgroundColor: a = "bgRaised", borderRadius: o = "med", boxShadow: c = "med", padding: d = "4", className: p, style: m, ...g }) {
19
19
  let [_, v] = t(g);
20
- return /* @__PURE__ */ d(c, { children: /* @__PURE__ */ d(l, {
20
+ return /* @__PURE__ */ f(l, { children: /* @__PURE__ */ f(u, {
21
21
  align: i,
22
22
  side: r,
23
- children: /* @__PURE__ */ d(m.Popup, {
23
+ children: /* @__PURE__ */ f(h.Popup, {
24
24
  ...v,
25
- render: /* @__PURE__ */ d(n, {
25
+ render: /* @__PURE__ */ f(n, {
26
26
  backgroundColor: a,
27
- borderRadius: s,
28
- boxShadow: u,
29
- padding: f,
27
+ borderRadius: o,
28
+ boxShadow: c,
29
+ padding: d,
30
30
  ..._,
31
- className: e(o, p),
32
- style: h
31
+ className: e(s, p),
32
+ style: m
33
33
  })
34
34
  })
35
35
  }) });
36
36
  }
37
- function _({ variant: i = "neutral", display: o = "flex", alignItems: s = "center", gap: c = "8", minBlockSize: l = "sm", paddingInline: u = "12", borderRadius: f = "sm", color: h = i === "danger" ? "danger" : "fg", className: g, style: _, children: v, ...y }) {
38
- let [b, x] = t(y);
39
- return /* @__PURE__ */ d(m.Item, {
40
- ...x,
41
- render: /* @__PURE__ */ d(n, {
42
- display: o,
43
- alignItems: s,
44
- gap: c,
45
- minBlockSize: l,
46
- paddingInline: u,
47
- borderRadius: f,
48
- color: h,
49
- ...b,
50
- className: e(a, g),
51
- style: _
37
+ function v(e) {
38
+ return m.map(e, (e) => typeof e == "string" || typeof e == "number" ? /* @__PURE__ */ f(r, {
39
+ as: "span",
40
+ font: "label",
41
+ size: "sm",
42
+ children: e
43
+ }) : e);
44
+ }
45
+ function y({ variant: r = "neutral", display: i = "flex", alignItems: o = "center", gap: s = "8", minBlockSize: c = "sm", paddingInline: l = "12", borderRadius: u = "sm", color: d = r === "danger" ? "danger" : "fg", className: p, style: m, children: g, ..._ }) {
46
+ let [y, b] = t(_);
47
+ return /* @__PURE__ */ f(h.Item, {
48
+ ...b,
49
+ render: /* @__PURE__ */ f(n, {
50
+ display: i,
51
+ alignItems: o,
52
+ gap: s,
53
+ minBlockSize: c,
54
+ paddingInline: l,
55
+ borderRadius: u,
56
+ color: d,
57
+ ...y,
58
+ className: e(a, p),
59
+ style: m
52
60
  }),
53
- children: p.map(v, (e) => typeof e == "string" || typeof e == "number" ? /* @__PURE__ */ d(r, {
54
- as: "span",
55
- font: "label",
56
- size: "sm",
57
- children: e
58
- }) : e)
61
+ children: v(g)
62
+ });
63
+ }
64
+ function b({ href: r, disabled: i = !1, closeOnClick: s = !0, onClick: c, display: l = "flex", alignItems: u = "center", gap: d = "8", minBlockSize: p = "sm", paddingInline: m = "12", borderRadius: g = "sm", color: _ = "fg", className: y, style: b, children: x, ...S }) {
65
+ let [C, w] = t(S);
66
+ return /* @__PURE__ */ f(h.LinkItem, {
67
+ ...w,
68
+ "aria-disabled": i || void 0,
69
+ closeOnClick: !i && s,
70
+ href: i ? void 0 : r,
71
+ onClick: i ? void 0 : c,
72
+ render: /* @__PURE__ */ f(n, {
73
+ as: "a",
74
+ display: l,
75
+ alignItems: u,
76
+ gap: d,
77
+ minBlockSize: p,
78
+ paddingInline: m,
79
+ borderRadius: g,
80
+ color: _,
81
+ ...C,
82
+ className: e(a, o, y),
83
+ style: b
84
+ }),
85
+ children: v(x)
59
86
  });
60
87
  }
61
- function v({ marginBlock: r = "4", marginInline: i = "8", className: a, style: o, ...c }) {
62
- let [l, u] = t(c);
63
- return /* @__PURE__ */ d(m.Separator, {
88
+ function x({ marginBlock: r = "4", marginInline: i = "8", className: a, style: o, ...s }) {
89
+ let [l, u] = t(s);
90
+ return /* @__PURE__ */ f(h.Separator, {
64
91
  ...u,
65
- render: /* @__PURE__ */ d(n, {
92
+ render: /* @__PURE__ */ f(n, {
66
93
  marginBlock: r,
67
94
  marginInline: i,
68
95
  ...l,
69
- className: e(s, a),
96
+ className: e(c, a),
70
97
  style: o
71
98
  })
72
99
  });
73
100
  }
74
- function y({ label: e, className: i, style: a, children: o, ...s }) {
101
+ function S({ label: e, className: i, style: a, children: o, ...s }) {
75
102
  let [c, l] = t(s);
76
- return /* @__PURE__ */ f(m.Group, {
103
+ return /* @__PURE__ */ p(h.Group, {
77
104
  ...l,
78
- render: /* @__PURE__ */ d(n, {
105
+ render: /* @__PURE__ */ f(n, {
79
106
  ...c,
80
107
  className: i,
81
108
  style: a
82
109
  }),
83
- children: [/* @__PURE__ */ d(m.GroupLabel, {
84
- render: /* @__PURE__ */ d(r, {
110
+ children: [/* @__PURE__ */ f(h.GroupLabel, {
111
+ render: /* @__PURE__ */ f(r, {
85
112
  as: "div",
86
113
  color: "fgMuted",
87
114
  font: "label",
@@ -93,12 +120,13 @@ function y({ label: e, className: i, style: a, children: o, ...s }) {
93
120
  }), o]
94
121
  });
95
122
  }
96
- var b = Object.assign(h, {
97
- Trigger: u,
98
- Popup: g,
99
- Item: _,
100
- Separator: v,
101
- Group: y
123
+ var C = Object.assign(g, {
124
+ Trigger: d,
125
+ Popup: _,
126
+ Item: y,
127
+ LinkItem: b,
128
+ Separator: x,
129
+ Group: S
102
130
  });
103
131
  //#endregion
104
- export { b as Menu };
132
+ export { C as Menu };
package/dist/docs/Menu.md CHANGED
@@ -2,31 +2,33 @@
2
2
 
3
3
  ## Purpose
4
4
 
5
- A list of actions that opens from a button: a row's "Edit, Duplicate, Delete", a toolbar's overflow. Built on Base UI's Menu, which owns the keyboard and focus: arrow keys, Home and End move through the items, typing jumps to a matching item, Esc and an outside click close it, and focus returns to the trigger. The parts add the look: a raised surface with a border and shadow, a highlighted item, a zoom-and-fade that is a plain fade under reduced motion.
5
+ A list of actions that opens from a button: a row's "Edit, Duplicate, Delete", a toolbar's overflow. It may mix in links ("Open invoice", "Download PDF") as `Menu.LinkItem`, real anchors that keep the browser's link behavior. Built on Base UI's Menu, which owns the keyboard and focus: arrow keys, Home and End move through the items, typing jumps to a matching item, Esc and an outside click close it, and focus returns to the trigger. The parts add the look: a raised surface with a border and shadow, a highlighted item, a zoom-and-fade that is a plain fade under reduced motion.
6
6
 
7
7
  ## When to use
8
8
 
9
9
  - Several actions on one object that would crowd the layout as buttons.
10
10
  - Actions that need no input of their own; each item runs one thing.
11
+ - Actions mixed with a few links on the same object: the links are `Menu.LinkItem`.
11
12
 
12
13
  ## When not to use
13
14
 
14
15
  - Picking a value for a form field: use a select (v0.2 forms) or radio buttons; for several values from a long list, use `MultiSelect`.
15
- - Navigation between pages: use links; a menu of links hides where they go.
16
+ - Navigation between pages: a menu that is only links is still navigation, so show the links on the page or in a nav. Link items are for a menu that mixes actions and links.
16
17
  - One or two actions: show them as buttons.
17
18
  - Content a user reads, or anything interactive inside the list (fields, toggles): use a popover or a dialog.
18
19
 
19
20
  ## Rules
20
21
 
21
- - Compose the parts: `Menu` > `Menu.Trigger` + `Menu.Popup` > `Menu.Item`, `Menu.Separator`, `Menu.Group`. `Menu` holds the open state and renders no element, so it takes no `className` or `style`; every other part does, plus style props.
22
+ - Compose the parts: `Menu` > `Menu.Trigger` + `Menu.Popup` > `Menu.Item`, `Menu.LinkItem`, `Menu.Separator`, `Menu.Group`. `Menu` holds the open state and renders no element, so it takes no `className` or `style`; every other part does, plus style props.
22
23
  - `Menu.Trigger` is a bare `Clickable`: give it its look with style props (or your own class), and an accessible name. An icon-only trigger needs `aria-label`.
23
24
  - For a trigger that reads as a button ("Actions", "New"), pass Button's `variant`, `size`, `iconStart` or `iconEnd` (a `ChevronDownIcon` for "Actions ▾"): it renders a `Button`, with its fill, border and height, and keeps the trigger's behavior and `aria-disabled`. While the menu is open it shows Button's open look, a held tint and an inset ring (ADR 0063); a bare trigger has none, so style it yourself off `aria-expanded`. Style props still override the defaults.
24
25
  - `Menu.Item` runs its `onClick` on click, Enter and Space, then the menu closes. Pass `closeOnClick={false}` to keep it open.
26
+ - `Menu.LinkItem` is a real `<a role="menuitem">` for an entry that goes somewhere. `href` is required; `target`, `rel` and `download` pass through. Hover shows the URL, Cmd/Ctrl-click and middle-click open a new tab, and a click, Enter or Space follows it and closes the menu. Never navigate from a `Menu.Item`'s `onClick` (`location.assign`, a router's `navigate`): it loses all of that. Router-aware (client-side) links are not offered. It takes no `variant`.
25
27
  - `variant="danger"` marks a destructive item. It still runs on one click, so open a confirm dialog from it (Cancel focused first) before anything irreversible happens.
26
- - `disabled` on an item renders `aria-disabled`: it stays announced and reachable with the arrow keys, and does nothing. On the trigger it keeps the button focusable and stops it opening.
28
+ - `disabled` on an item renders `aria-disabled`: it stays announced and reachable with the arrow keys, and does nothing. A disabled `Menu.LinkItem` also drops its `href`, so no click or context menu reaches the URL. On the trigger it keeps the button focusable and stops it opening.
27
29
  - `Menu.Group` takes a `label` that is shown and names the group. Separate groups with `Menu.Separator`.
28
30
  - The popup portals into the trigger's nearest `ThemeProvider`, so it keeps the brand and mode. That provider must not clip or stack its content: wrap the app root, and keep a nested provider off elements with `overflow: hidden`, a `transform` or their own `z-index`, or the popup is clipped or drawn under siblings. It sits on `zIndex` `popover`. A menu opened only through `open`, with no `Menu.Trigger`, has no provider to find and portals to `<body>`.
29
- - Item text is wrapped in `Text` for you; an `Icon` beside it is spaced by the item's `gap`.
31
+ - Item and link item text is wrapped in `Text` for you; an `Icon` beside it is spaced by the item's `gap`.
30
32
  - `side` (`bottom` by default) and `align` (`start`) place the popup against the trigger; it flips when it would overflow the viewport.
31
33
  - Never add key handlers or `tabIndex` to the parts; Base UI handles the keyboard.
32
34
  - The module has no hooks, so a server component can render the parts; `onClick` and other functions must come from a client component.
@@ -69,6 +71,18 @@ A list of actions that opens from a button: a row's "Edit, Duplicate, Delete", a
69
71
  </Menu>
70
72
  ```
71
73
 
74
+ ```tsx
75
+ // Actions and links on one invoice: the links are real anchors.
76
+ <Menu.Popup>
77
+ <Menu.Item onClick={edit}>Edit</Menu.Item>
78
+ <Menu.Separator />
79
+ <Menu.LinkItem href={`/invoices/${id}`}>Open invoice</Menu.LinkItem>
80
+ <Menu.LinkItem href={`/invoices/${id}.pdf`} target="_blank" rel="noopener">
81
+ Download PDF
82
+ </Menu.LinkItem>
83
+ </Menu.Popup>
84
+ ```
85
+
72
86
  ## Don't
73
87
 
74
88
  ```tsx
@@ -84,16 +98,24 @@ A list of actions that opens from a button: a row's "Edit, Duplicate, Delete", a
84
98
  ```
85
99
 
86
100
  ```tsx
87
- // A link list is navigation, not a menu of actions.
101
+ // Navigates from an action: no URL on hover, no new tab. Use <Menu.LinkItem href="/settings">.
88
102
  <Menu.Item onClick={() => navigate("/settings")}>Settings</Menu.Item>
89
103
  ```
90
104
 
105
+ ```tsx
106
+ // Only links: this is navigation. Show the links on the page or in a nav.
107
+ <Menu.Popup>
108
+ <Menu.LinkItem href="/settings">Settings</Menu.LinkItem>
109
+ <Menu.LinkItem href="/billing">Billing</Menu.LinkItem>
110
+ </Menu.Popup>
111
+ ```
112
+
91
113
  ## Accessibility
92
114
 
93
- - The trigger has `aria-haspopup="menu"` and `aria-expanded`; the popup is `role="menu"` labelled by the trigger, items are `role="menuitem"`, groups are `role="group"` named by their label.
115
+ - The trigger has `aria-haspopup="menu"` and `aria-expanded`; the popup is `role="menu"` labelled by the trigger, items and link items are `role="menuitem"` (a link item is an `<a>` with an `href`, so the browser treats it as a link), groups are `role="group"` named by their label.
94
116
  - Opening from the keyboard focuses the first item; from the pointer, the popup. Focus stays in the menu until it closes, then returns to the trigger.
95
117
  - A closed menu stays on the page while it fades out, but is `aria-hidden` from the moment it closes, so a menu opened during the fade (the next row's) is the only one a screen reader or a role query finds. Nothing in it is in the tab order. Focus stays in it until the fade ends, then returns to the trigger.
96
- - The highlighted item gets a fill (`bgMuted` in light mode; in dark mode `bgRaised` with 10% `fg` mixed in, since `bgMuted` sits too close to the popup there), and the shared focus ring when reached from the keyboard. Under forced colors it is `Highlight` with `HighlightText`, disabled items are `GrayText`, and the popup keeps its border.
118
+ - The highlighted item gets a fill (`bgMuted` in light mode; in dark mode `bgRaised` with 10% `fg` mixed in, since `bgMuted` sits too close to the popup there), and the shared focus ring when reached from the keyboard. Under forced colors it is `Highlight` with `HighlightText`, disabled items are `GrayText` on the page's fill even when highlighted (`GrayText` pairs with `Canvas` only), and the popup keeps its border.
97
119
  - The danger item's `danger` color passes 4.5:1 on the popup and on the highlight fill in every preset (`Menu.test.tsx` checks it, with `fg` and `fgMuted`); the word ("Delete") carries the meaning, not the color.
98
120
 
99
121
  ## Props
@@ -139,6 +161,20 @@ Also accepts the style props (see `manifest.json` `styleProps`).
139
161
 
140
162
  Also accepts the style props (see `manifest.json` `styleProps`).
141
163
 
164
+ ### Menu.LinkItem
165
+
166
+ | Prop | Type | Required | Description |
167
+ | --- | --- | --- | --- |
168
+ | `className` | `string` | no | |
169
+ | `closeOnClick` | `boolean` | no | Whether to close the menu when the item is clicked. |
170
+ | `disabled` | `boolean` | no | |
171
+ | `href` | `string` | yes | |
172
+ | `id` | `string` | no | |
173
+ | `label` | `string` | no | Overrides the text used for keyboard text navigation and filtering. Falls back to the rendered text when not provided. |
174
+ | `style` | `CSSProperties` | no | |
175
+
176
+ Also accepts the style props (see `manifest.json` `styleProps`).
177
+
142
178
  ### Menu.Popup
143
179
 
144
180
  | Prop | Type | Required | Description |
@@ -17,7 +17,7 @@
17
17
  - [Icon](Icon.md): The base for every icon: an `<svg>` on a 24-unit grid that draws its children with round 2-unit strokes in `currentColor`. Generated icons from `@jonmilner/ui/icons` (`ArrowRightIcon`, …) wrap it and take the same props, less `children` and `viewBox`. `size` takes a `fontSize` step, so an icon matches the `Text` beside it; `color` takes a color role.
18
18
  - [IconButton](IconButton.md): A compact button whose only visible content is an icon, for an action the icon makes plain: close, edit, refresh. A required `label` names it for assistive technology (`aria-label`); the icon is decorative. It is a `<button type="button">` on `Clickable`, so it has the shared focus ring, the 24px minimum target and `aria-disabled` that blocks activation while the button stays focusable. Two variants from Button's fills (`primary`, `secondary`) and three square sizes at Button's heights (`sm`, `med`, `lg`: 32, 40 and 48 px).
19
19
  - [Inline](Inline.md): A horizontal row of children that wraps onto new lines, with even space between them on both axes. `Inline` is a `Box` with `display="flex"`, `flexDirection="row"`, `flexWrap="wrap"` and `alignItems="center"` as default style props, spaced by a required `gap`. Children render as they are: no wrapper per child.
20
- - [Menu](Menu.md): A list of actions that opens from a button: a row's "Edit, Duplicate, Delete", a toolbar's overflow. Built on Base UI's Menu, which owns the keyboard and focus: arrow keys, Home and End move through the items, typing jumps to a matching item, Esc and an outside click close it, and focus returns to the trigger. The parts add the look: a raised surface with a border and shadow, a highlighted item, a zoom-and-fade that is a plain fade under reduced motion.
20
+ - [Menu](Menu.md): A list of actions that opens from a button: a row's "Edit, Duplicate, Delete", a toolbar's overflow. It may mix in links ("Open invoice", "Download PDF") as `Menu.LinkItem`, real anchors that keep the browser's link behavior. Built on Base UI's Menu, which owns the keyboard and focus: arrow keys, Home and End move through the items, typing jumps to a matching item, Esc and an outside click close it, and focus returns to the trigger. The parts add the look: a raised surface with a border and shadow, a highlighted item, a zoom-and-fade that is a plain fade under reduced motion.
21
21
  - [MultiSelect](MultiSelect.md): A form field for picking any number of values from a list too long to show at once. A trigger styled as Button's secondary look opens a listbox of options; each selected option shows a check mark, and the trigger summarises the selection as the joined labels, or a count ("3 selected") once the labels would not fit. Built on Base UI's `Select` in multiple mode (ADR 0003), which owns the keyboard and focus: arrow keys, Home and End move through the options, typing jumps to a match, Esc and an outside click close it, and focus returns to the trigger. It composes `Field` for its label, description and error (ADR 0026).
22
22
  - [SearchField](SearchField.md): A form field for a search query: an `<input type="search">` (role `searchbox`) with a search icon at its inline start and, while it holds a value, a clear button at its inline end. Escape empties a field that has a value, and the clear button empties it and puts focus back in the input, so a keyboard user never loses their place (ADR 0087). Built on Base UI's `Input` (ADR 0003); it composes `Field` for its label, description and error (ADR 0026). Three sizes at Button's heights (`sm`, `med`, `lg`: 32, 40 and 48 px).
23
23
  - [Stack](Stack.md): A vertical column of children with even space between them. `Stack` is a `Box` with `display="flex"` and `flexDirection="column"` as default style props, spaced by a required `gap`. Children render as they are: no wrapper per child.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "./manifest.schema.json",
3
3
  "package": "@jonmilner/ui",
4
- "version": "0.4.0",
4
+ "version": "0.4.1",
5
5
  "components": [
6
6
  {
7
7
  "name": "AlertDialog",
@@ -1205,6 +1205,49 @@
1205
1205
  }
1206
1206
  ]
1207
1207
  },
1208
+ {
1209
+ "name": "Menu.LinkItem",
1210
+ "styleProps": true,
1211
+ "props": [
1212
+ {
1213
+ "name": "className",
1214
+ "type": "string",
1215
+ "required": false
1216
+ },
1217
+ {
1218
+ "name": "closeOnClick",
1219
+ "type": "boolean",
1220
+ "required": false,
1221
+ "description": "Whether to close the menu when the item is clicked."
1222
+ },
1223
+ {
1224
+ "name": "disabled",
1225
+ "type": "boolean",
1226
+ "required": false
1227
+ },
1228
+ {
1229
+ "name": "href",
1230
+ "type": "string",
1231
+ "required": true
1232
+ },
1233
+ {
1234
+ "name": "id",
1235
+ "type": "string",
1236
+ "required": false
1237
+ },
1238
+ {
1239
+ "name": "label",
1240
+ "type": "string",
1241
+ "required": false,
1242
+ "description": "Overrides the text used for keyboard text navigation and filtering.\nFalls back to the rendered text when not provided."
1243
+ },
1244
+ {
1245
+ "name": "style",
1246
+ "type": "CSSProperties",
1247
+ "required": false
1248
+ }
1249
+ ]
1250
+ },
1208
1251
  {
1209
1252
  "name": "Menu.Popup",
1210
1253
  "styleProps": true,
package/dist/index.d.mts CHANGED
@@ -14,7 +14,7 @@ import { Heading, HeadingLevel, HeadingProps, HeadingSize } from "./Heading/inde
14
14
  import { IconButton, IconButtonProps } from "./IconButton/index.mjs";
15
15
  import { Inline, InlineProps } from "./Inline/index.mjs";
16
16
  import { MenuTriggerProps } from "./Menu/client.mjs";
17
- import { Menu, MenuGroupProps, MenuItemProps, MenuPopupProps, MenuProps, MenuSeparatorProps } from "./Menu/index.mjs";
17
+ import { Menu, MenuGroupProps, MenuItemProps, MenuLinkItemProps, MenuPopupProps, MenuProps, MenuSeparatorProps } from "./Menu/index.mjs";
18
18
  import { MultiSelect, MultiSelectOption, MultiSelectProps } from "./MultiSelect/index.mjs";
19
19
  import { SearchFieldInputProps } from "./SearchField/client.mjs";
20
20
  import { SearchField, SearchFieldProps } from "./SearchField/index.mjs";
@@ -26,4 +26,4 @@ import { Toast } from "./Toast/index.mjs";
26
26
  import { VisuallyHidden, VisuallyHiddenProps } from "./VisuallyHidden/index.mjs";
27
27
  import { Theme, defineTheme } from "./theme/define-theme.mjs";
28
28
  import { ThemeProvider, ThemeProviderProps } from "./theme/ThemeProvider.mjs";
29
- export { AlertDialog, type AlertDialogProps, type AlertDialogTriggerProps, Badge, type BadgeProps, Box, type BoxProps, Button, type ButtonProps, Checkbox, type CheckboxProps, Chip, ChipGroup, type ChipGroupProps, type ChipProps, Clickable, type ClickableProps, Dialog, type DialogActionsProps, type DialogBodyProps, type DialogCloseProps, type DialogPopupProps, type DialogProps, type DialogTriggerProps, Field, type FieldDescriptionProps, type FieldErrorProps, type FieldLabelProps, type FieldProps, Heading, type HeadingLevel, type HeadingProps, type HeadingSize, Icon, IconButton, type IconButtonProps, type IconProps, Inline, type InlineProps, Menu, type MenuGroupProps, type MenuItemProps, type MenuPopupProps, type MenuProps, type MenuSeparatorProps, type MenuTriggerProps, MultiSelect, type MultiSelectOption, type MultiSelectProps, SearchField, type SearchFieldInputProps, type SearchFieldProps, Stack, type StackProps, type StyleProps, Tabs, type TabsListProps, type TabsPanelProps, type TabsProps, type TabsTabProps, Tag, type TagProps, Text, type TextProps, type Theme, ThemeProvider, type ThemeProviderProps, Toast, type ToastOptions, type ToastProviderProps, type ToastVariant, VisuallyHidden, type VisuallyHiddenProps, defineTheme, useToast };
29
+ export { AlertDialog, type AlertDialogProps, type AlertDialogTriggerProps, Badge, type BadgeProps, Box, type BoxProps, Button, type ButtonProps, Checkbox, type CheckboxProps, Chip, ChipGroup, type ChipGroupProps, type ChipProps, Clickable, type ClickableProps, Dialog, type DialogActionsProps, type DialogBodyProps, type DialogCloseProps, type DialogPopupProps, type DialogProps, type DialogTriggerProps, Field, type FieldDescriptionProps, type FieldErrorProps, type FieldLabelProps, type FieldProps, Heading, type HeadingLevel, type HeadingProps, type HeadingSize, Icon, IconButton, type IconButtonProps, type IconProps, Inline, type InlineProps, Menu, type MenuGroupProps, type MenuItemProps, type MenuLinkItemProps, type MenuPopupProps, type MenuProps, type MenuSeparatorProps, type MenuTriggerProps, MultiSelect, type MultiSelectOption, type MultiSelectProps, SearchField, type SearchFieldInputProps, type SearchFieldProps, Stack, type StackProps, type StyleProps, Tabs, type TabsListProps, type TabsPanelProps, type TabsProps, type TabsTabProps, Tag, type TagProps, Text, type TextProps, type Theme, ThemeProvider, type ThemeProviderProps, Toast, type ToastOptions, type ToastProviderProps, type ToastVariant, VisuallyHidden, type VisuallyHiddenProps, defineTheme, useToast };
@@ -8,6 +8,7 @@ var e = Object.freeze([
8
8
  "Chip",
9
9
  "Tab",
10
10
  "Item",
11
+ "LinkItem",
11
12
  "Label",
12
13
  "Error",
13
14
  "Description",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jonmilner/ui",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "files": [
5
5
  "dist",
6
6
  "THIRD_PARTY_NOTICES.md"