@blakesteve/roster 5.8.0 → 5.9.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 (29) hide show
  1. package/README.md +86 -2
  2. package/dist/components/molecules/Menu/Menu.cjs +1 -0
  3. package/dist/components/molecules/Menu/Menu.d.ts +169 -0
  4. package/dist/components/molecules/Menu/Menu.js +577 -0
  5. package/dist/components/molecules/Menu/menu-typeahead.cjs +1 -0
  6. package/dist/components/molecules/Menu/menu-typeahead.d.ts +7 -0
  7. package/dist/components/molecules/Menu/menu-typeahead.js +13 -0
  8. package/dist/components/organisms/Navbar/Navbar.cjs +1 -1
  9. package/dist/components/organisms/Navbar/Navbar.d.ts +1 -1
  10. package/dist/components/organisms/Navbar/Navbar.js +275 -302
  11. package/dist/index.d.ts +1 -0
  12. package/dist/meta.json +3 -3
  13. package/dist/node_modules/@floating-ui/core/dist/floating-ui.core.js +7 -7
  14. package/dist/node_modules/@floating-ui/dom/dist/floating-ui.dom.js +2 -2
  15. package/dist/node_modules/@floating-ui/react-dom/dist/floating-ui.react-dom.js +5 -5
  16. package/dist/node_modules/@headlessui/react/dist/components/portal/portal.js +6 -6
  17. package/dist/node_modules/@headlessui/react/dist/utils/focus-management.cjs +1 -1
  18. package/dist/node_modules/@headlessui/react/dist/utils/focus-management.js +52 -61
  19. package/dist/node_modules/@radix-ui/react-popper/dist/index.js +6 -6
  20. package/dist/roster.cjs +1 -1
  21. package/dist/roster.css +1 -1
  22. package/dist/roster.js +127 -116
  23. package/package.json +2 -1
  24. package/dist/node_modules/@headlessui/react/dist/components/menu/menu-machine-glue.cjs +0 -1
  25. package/dist/node_modules/@headlessui/react/dist/components/menu/menu-machine-glue.js +0 -21
  26. package/dist/node_modules/@headlessui/react/dist/components/menu/menu-machine.cjs +0 -1
  27. package/dist/node_modules/@headlessui/react/dist/components/menu/menu-machine.js +0 -120
  28. package/dist/node_modules/@headlessui/react/dist/components/menu/menu.cjs +0 -1
  29. package/dist/node_modules/@headlessui/react/dist/components/menu/menu.js +0 -197
package/README.md CHANGED
@@ -924,6 +924,10 @@ look like oversights until you know why:
924
924
  page does not reach it. All three components copy the nearest `.dark` onto
925
925
  the panel to fix that. Custom properties are not carried the same way — set
926
926
  `--roster-popover-*` at `:root`.
927
+ - **`Menu` shares the panel, not the anchoring.** Its panel takes the same
928
+ surface, tokens and dark carry, but `Menu` places itself (see
929
+ [Menu](#menu)), writing its own inline `max-height`. It reads the same
930
+ `--anchor-max-height`, so the hatch above works on `MenuItems` too.
927
931
  - **`variant="slate"` opens on the dark palette**, in light mode too. That
928
932
  variant paints a dark trigger, and its panel was rendering white: a dark
929
933
  control opening a white sheet, which is the mismatch `--roster-popover-*`
@@ -1499,6 +1503,84 @@ shown beside it.
1499
1503
 
1500
1504
  Every string in all five is a prop with an English default.
1501
1505
 
1506
+ ### Menu
1507
+
1508
+ A button that opens a panel of actions: the menu button pattern. Compose it:
1509
+
1510
+ ```tsx
1511
+ import { Menu, MenuButton, MenuItems, MenuItem, MenuSeparator } from "@blakesteve/roster";
1512
+
1513
+ <Menu>
1514
+ <MenuButton variant="ghost" size="icon" aria-label="More for Banded agate">⋯</MenuButton>
1515
+ <MenuItems align="end">
1516
+ <MenuItem icon={<EditIcon />} shortcut="⌘E" onSelect={edit}>Edit</MenuItem>
1517
+ <MenuItem href="/rocks/agate/share">Share</MenuItem>
1518
+ <MenuSeparator />
1519
+ <MenuItem destructive onSelect={confirmDelete}>Delete</MenuItem>
1520
+ </MenuItems>
1521
+ </Menu>;
1522
+ ```
1523
+
1524
+ - **Items.** `MenuItem` is an action, or a link with `href` (rendered through
1525
+ the menu's `linkComponent`; pass `next/link` from a client component, as with
1526
+ Breadcrumbs). Each takes a leading `icon`, a `shortcut` to show (display
1527
+ only: your app handles the keys; `keyshortcuts` tells assistive technology),
1528
+ a `description` line, a trailing `badge`, `destructive` and `disabled`.
1529
+ `MenuCheckboxItem` is an on/off choice, `MenuRadioGroup` with
1530
+ `MenuRadioItem` one of several, `MenuGroup` puts items under an optional
1531
+ label, `MenuSeparator` draws a rule, and `MenuSubmenu` opens a menu beside
1532
+ its item. Submenus nest.
1533
+ - **Keyboard.** Enter, Space or Down open it on the first item and Up on the
1534
+ last; a click opens it on the panel. The arrows move, Home and End jump, and
1535
+ typing jumps to the next item starting with what's typed, a space included
1536
+ ("save a"). Escape closes it and returns focus to the button; Tab closes it
1537
+ and moves on from the button, to what the browser's own Tab would reach. A
1538
+ submenu opens with the arrow away from its parent, Enter, Space, a click or
1539
+ the pointer resting on it, and closes with the arrow back or Escape. A click
1540
+ only ever opens it.
1541
+ - **Choosing.** A click or Enter chooses and closes, with focus back on the
1542
+ button, so a choice that opens a Dialog hands focus to it and gets it back.
1543
+ Space on a checkbox or radio item chooses and keeps the menu open, so
1544
+ several can be set in one visit. `closeOnSelect={false}` keeps it open on
1545
+ any choice. A disabled item can be reached, so it can be read, but not
1546
+ chosen. A press outside closes it without moving the page, and focus stays
1547
+ where the press put it.
1548
+ - **Events.** Keys inside the panel stay there: the panel is portaled, and
1549
+ React would otherwise carry them to the components around the menu, so
1550
+ Enter on Delete would also open the row whose overflow menu it is. A key
1551
+ with Ctrl, Meta or Alt goes on, for the app's shortcuts. Clicks go on to the
1552
+ page, where analytics listen, and so React carries an item's click to that
1553
+ row too: a row with its own `onClick` checks that the click landed inside
1554
+ it (`event.currentTarget.contains(event.target as Node)`). The button is
1555
+ in your markup, and its keys and clicks reach the row like any other.
1556
+ - **Placement.** Below the button, lined up with its start (`align="end"` for
1557
+ the other edge); flipped above when there's more room there, shifted to stay
1558
+ on screen, and capped at the room there is. A taller menu scrolls with
1559
+ Roster's scrollbar and a shadow at whichever edge has more. Under
1560
+ `dir="rtl"` it lines up with the right edge and submenus open to the left.
1561
+ It takes its button's direction, not the page's.
1562
+ - **Inside other layers.** The panel opens in a portal, so nothing it sits in
1563
+ clips it, a card with `overflow: hidden` included. A `Dialog`, a `Sheet` or
1564
+ Navbar's mobile panel counts it as inside: choosing from it leaves them
1565
+ open.
1566
+ - **Styling.** `className` on `MenuItems` and on each item merges last, so a
1567
+ fill such as `bg-gray-100` takes; the scroll shadows are drawn over it. Its
1568
+ ring and separators follow `--roster-border-width`. Items show the pointer
1569
+ cursor, a disabled one `not-allowed`. In forced colors the focused item gets
1570
+ an outline, since its fill is dropped.
1571
+ - **Targets.** Every item is at least 44px tall, and `MenuButton` at least 44
1572
+ by 44 whatever its size. `MenuButton plain` renders a bare `<button>` for a
1573
+ trigger drawn its own way; keep its target at least 44 by 44 with a hit
1574
+ area, as Navbar's avatar does.
1575
+
1576
+ Built on Roster's own popup panel and on Floating UI for placement, which
1577
+ Roster already ships through its other popups, rather than on Headless UI's
1578
+ `Menu`: that has no submenus, and no checkbox or radio items. Navbar's user
1579
+ menu is built on it since 5.9.0. It looks as it did, in every state, except
1580
+ that its rows are 44px tall rather than 36, and that with neither an inbox nor
1581
+ a theme row it no longer draws an empty band and rule at the top. It keeps its
1582
+ own white fill, whatever `--roster-popover-bg` says.
1583
+
1502
1584
  ### Pagination
1503
1585
 
1504
1586
  Moves through pages of a list, built for a long list browsed on a phone.
@@ -1778,6 +1860,7 @@ function App() {
1778
1860
  | `LiquidTabs` | Tab strip with a sliding pill; arrow keys, roving focus, 44px `lg` size |
1779
1861
  | `MatchupCard` | Head-to-head comparison card |
1780
1862
  | `MediaPreview` | Thumbnail that falls back to "Preview unavailable, open original" when it can't render |
1863
+ | `Menu` | A button's menu of actions: icons, shortcuts, checkbox and radio items, links, groups, submenus; the full keyboard pattern, 44px targets (see [Menu](#menu)) |
1781
1864
  | `FileUpload` | Picks files and uploads each through the app's `upload`, with per-file progress, retry and rules |
1782
1865
  | `Pagination` | Page numbers with gaps, or a compact page picker on narrow screens; buttons or real links, 44px targets |
1783
1866
  | `SkeletonCard`, `SkeletonTableRow`, `SkeletonAvatar`, `SkeletonStat` | Skeleton presets in the real component's box |
@@ -1799,7 +1882,7 @@ function App() {
1799
1882
  | `ModerationQueue` | Items waiting for a decision: busy per action, confirm, reason and edit steps, focus after an item leaves, a live region. Never fetches (see [ModerationQueue](#moderationqueue)) |
1800
1883
  | `QueueEditDialog` | A ModerationQueue edit step in a Dialog: pass it as an edit's `surface`. A separate export, so a queue that never uses it doesn't ship it |
1801
1884
  | `QueueEditSheet` | A ModerationQueue edit step in a Sheet, for a long form: pass it as an edit's `surface`. A separate export, like `QueueEditDialog` |
1802
- | `Navbar` | Responsive navigation bar with mobile slide-out. A wide logo keeps its own proportions through `logoHeight` and `logoWidth`; its built-in words are in `labels` |
1885
+ | `Navbar` | Responsive navigation bar with mobile slide-out; its user menu is a `Menu`. A wide logo keeps its own proportions through `logoHeight` and `logoWidth`; its built-in words are in `labels` |
1803
1886
  | `Sheet` | Bottom sheet: drag, Escape or backdrop to dismiss; only its content scrolls; returns focus, even from a deep link; the title may be a node |
1804
1887
  | `Table` | Static data table |
1805
1888
 
@@ -1953,7 +2036,8 @@ reason and the build goes green on the state it inherited while still failing on
1953
2036
  anything new. It also fails when a listed class starts emitting, so a fix cannot
1954
2037
  leave a stale entry behind. Run it with `--verbose` to print the list.
1955
2038
 
1956
- Story tests that need the user's own input, a wheel or a mouse drag, get it
2039
+ Story tests that need the user's own input, a wheel, a mouse drag or a pointer
2040
+ resting on something, get it
1957
2041
  from `.storybook/real-input.ts`: Vitest browser commands that drive
1958
2042
  Playwright's mouse, whose events are trusted. A synthetic wheel or pointer
1959
2043
  event never scrolls anything, so "a vertical wheel scrolls the page, not the
@@ -0,0 +1 @@
1
+ "use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const c=require("react/jsx-runtime"),ge=require("react"),W=require("../../../node_modules/@headlessui/react/dist/components/portal/portal.cjs"),A=require("../../../node_modules/@floating-ui/react-dom/dist/floating-ui.react-dom.cjs"),E=require("../../../lib/utils.cjs"),he=require("../../../internal/popup.cjs"),be=require("../../atoms/Button/Button.cjs"),xe=require("./menu-typeahead.cjs"),ye=require("../../../node_modules/@floating-ui/dom/dist/floating-ui.dom.cjs");function ve(e){const r=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(e){for(const n in e)if(n!=="default"){const o=Object.getOwnPropertyDescriptor(e,n);Object.defineProperty(r,n,o.get?o:{enumerable:!0,get:()=>e[n]})}}return r.default=e,Object.freeze(r)}const i=ve(ge);function we({href:e,to:r,children:n,...o}){return c.jsx("a",{href:e,...o,children:n})}const G=i.createContext(null),_=i.createContext(null),V=i.createContext(null);function R(e){const r=i.useContext(G);if(!r)throw new Error(`<${e}> must be inside a <Menu>.`);return r}function K(e){const r=i.useContext(_);if(!r)throw new Error(`<${e}> must be inside <MenuItems> or a <MenuSubmenu>.`);return r}const ke="[data-menu-item]",Y=100,Ie=75,Ee='a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), summary, [tabindex]:not([tabindex="-1"]), [contenteditable]:not([contenteditable="false"])';function L(e){return e?[...e.querySelectorAll(ke)].filter(r=>r.closest("[data-menu-panel]")===e):[]}function S(e){e&&(e.focus({preventScroll:!0}),e.scrollIntoView?.({block:"nearest"}))}function $(e){return!!e&&getComputedStyle(e).direction==="rtl"}function Se(e,r){const n=[...document.querySelectorAll(Ee)].filter(t=>t===e||t.tabIndex>=0&&!t.matches(":disabled")&&!t.closest("[data-menu-panel], [inert]")&&t.getClientRects().length>0&&getComputedStyle(t).visibility!=="hidden"&&!Me(t,r)),o=n.indexOf(e);((r?n[o-1]:n[o+1])??e).focus()}function Me(e,r){if(!(e instanceof HTMLInputElement)||e.type!=="radio"||!e.name)return!1;const n=[...e.ownerDocument.querySelectorAll(`input[type="radio"][name="${CSS.escape(e.name)}"]`)].filter(a=>a.form===e.form&&!a.disabled);return(n.find(a=>a.checked)??(r?n[n.length-1]:n[0]))!==e}const D={x:Number.NaN,y:Number.NaN};let H=!1;function je(){H||typeof window>"u"||(H=!0,window.addEventListener("pointermove",e=>{D.x=e.clientX,D.y=e.clientY},{passive:!0}))}function X(e){return!Number.isNaN(D.x)&&(e.clientX!==D.x||e.clientY!==D.y)}const U=e=>`radial-gradient(farthest-side at 50% ${e==="top"?"0":"100%"}, var(--menu-edge), transparent)`,J={backgroundImage:"var(--menu-edge-top, none), var(--menu-edge-bottom, none)",backgroundPosition:"top, bottom",backgroundSize:"100% 0.5rem",backgroundRepeat:"no-repeat"};function Pe(e){i.useLayoutEffect(()=>{if(!e)return;const r=()=>{const o=e.scrollTop>8,a=e.scrollTop+e.clientHeight<e.scrollHeight-8;e.toggleAttribute("data-more-above",o),e.toggleAttribute("data-more-below",a),e.style.setProperty("--menu-edge-top",o?U("top"):"none"),e.style.setProperty("--menu-edge-bottom",a?U("bottom"):"none")};r(),e.addEventListener("scroll",r,{passive:!0});const n=typeof ResizeObserver>"u"?null:new ResizeObserver(r);return n?.observe(e),()=>{e.removeEventListener("scroll",r),n?.disconnect()}},[e])}function Q(e,r){const[n,o]=e.split("-"),a=r?"right":"left",t=r?"left":"right";return n==="top"||n==="bottom"?`${n==="bottom"?"top":"bottom"} ${o==="end"?t:o==="start"?a:"center"}`:`${o==="end"?"bottom":"top"} ${n==="right"?"left":"right"}`}const Z=E.cn("rst:font-ui",he.POPUP_PANEL,"rst:w-max rst:min-w-48 rst:max-w-[min(20rem,calc(100vw-1rem))] rst:overflow-y-auto rst:overscroll-contain rst:custom-scrollbar","rst:[--menu-edge:rgb(0_0_0/0.18)] rst:dark:[--menu-edge:rgb(0_0_0/0.6)]","rst:transition-[opacity,scale] rst:duration-100 rst:ease-out rst:data-closed:opacity-0 rst:data-closed:scale-95 rst:data-leaving:duration-75 rst:data-leaving:ease-in rst:data-closed:pointer-events-none rst:motion-reduce:transition-none");function ee(e){const[r,n]=i.useState(e),[o,a]=i.useState(!0);return i.useLayoutEffect(()=>{if(e){n(!0);const s=requestAnimationFrame(()=>a(!1));return()=>cancelAnimationFrame(s)}a(!0);const t=window.matchMedia?.("(prefers-reduced-motion: reduce)").matches,d=window.setTimeout(()=>n(!1),t?0:Ie);return()=>window.clearTimeout(d)},[e]),{mounted:r,closed:o,leaving:r&&!e}}function te(e,r,n,o,a,t=!1){const[d,s]=i.useState(null),u=A.useFloating({open:r,strategy:"fixed",transform:!1,placement:n,elements:{reference:e},whileElementsMounted:ye.autoUpdate,middleware:[A.offset(o),A.flip({padding:8}),A.shift({padding:8,crossAxis:t}),A.size({padding:8,apply({availableHeight:f,elements:l}){l.floating.style.maxHeight=`min(var(--anchor-max-height, 100vh), ${Math.max(f,96)}px)`}})]}),{setFloating:p}=u.refs,m=i.useCallback(f=>{a.current=f,s(f),p(f)},[a,p]);return Pe(d),{...u,setPanel:m,panelEl:d}}const re=500;function ne(){const e=i.useRef({text:"",at:0}),r=i.useCallback(()=>e.current.text!==""&&performance.now()-e.current.at<=re,[]);return{typedRef:e,typingAhead:r}}function oe({panelRef:e,typedRef:r,onEscape:n,onBack:o}){const a=R("MenuItems");return t=>{const d=e.current;if(!d||!d.contains(t.target)||(!t.ctrlKey&&!t.metaKey&&!t.altKey&&t.stopPropagation(),t.defaultPrevented))return;const s=L(d),u=s.indexOf(document.activeElement),p=$(d);switch(t.key){case"ArrowDown":t.preventDefault(),t.stopPropagation(),S(s[(u+1)%s.length]);return;case"ArrowUp":t.preventDefault(),t.stopPropagation(),S(s[u<=0?s.length-1:u-1]);return;case"Home":case"PageUp":t.preventDefault(),t.stopPropagation(),S(s[0]);return;case"End":case"PageDown":t.preventDefault(),t.stopPropagation(),S(s[s.length-1]);return;case"Escape":t.preventDefault(),t.stopPropagation(),n();return;case"Tab":{t.preventDefault(),t.stopPropagation();const m=a.triggerRef.current;a.close(!1),m&&Se(m,t.shiftKey);return}case"ArrowLeft":case"ArrowRight":o&&t.key===(p?"ArrowRight":"ArrowLeft")&&(t.preventDefault(),t.stopPropagation(),o());return}if(t.key.length===1&&!t.ctrlKey&&!t.metaKey&&!t.altKey){const m=performance.now(),f=r.current.text!==""&&m-r.current.at<=re;if(t.key===" "&&!f)return;t.preventDefault(),r.current={text:f?r.current.text+t.key:t.key,at:m};const l=s.map(w=>w.dataset.menuText??w.textContent??""),b=xe.nextByTypeahead(l,u,r.current.text);b!==-1&&S(s[b])}}}function Te({children:e,open:r,onOpenChange:n,linkComponent:o=we}){const[a,t]=i.useState(!1),d=r??a,s=i.useId(),u=i.useRef(null),[p,m]=i.useState(null),f=i.useCallback(g=>{u.current=g,m(g)},[]),l=i.useRef("panel"),b=i.useCallback(g=>{r===void 0&&t(g),n?.(g)},[r,n]),w=i.useMemo(()=>({treeId:s,open:d,openWith:g=>{l.current=g,b(!0)},close:g=>{b(!1),g&&u.current?.focus()},triggerRef:u,triggerEl:p,registerTrigger:f,triggerId:`${s}-button`,panelId:`${s}-menu`,focusOnOpen:l,linkComponent:o}),[s,d,b,o,p,f]);return i.useEffect(()=>{if(!d)return;je();const g=I=>{I.target?.closest?.(`[data-menu-tree="${s}"]`)||(b(!1),requestAnimationFrame(()=>{(document.activeElement===document.body||!document.activeElement)&&u.current?.focus({preventScroll:!0})}))};return document.addEventListener("pointerdown",g,!0),()=>document.removeEventListener("pointerdown",g,!0)},[d,s,b]),c.jsx(G.Provider,{value:w,children:e})}const Re=i.forwardRef(function({plain:r=!1,className:n,onClick:o,onKeyDown:a,children:t,...d},s){const{variant:u,colorScheme:p,size:m,isLoading:f,loadingLabel:l,startIcon:b,endIcon:w,surface:g,...I}=d,h=R("MenuButton"),{registerTrigger:M}=h,j=i.useCallback(x=>{M(x),typeof s=="function"?s(x):s&&(s.current=x)},[M,s]),P={id:h.triggerId,"aria-haspopup":"menu","aria-expanded":h.open,"aria-controls":h.open?h.panelId:void 0,"data-menu-tree":h.treeId,onClick:x=>{o?.(x),!x.defaultPrevented&&(h.open?h.close(!1):h.openWith("panel"))},onKeyDown:x=>{if(a?.(x),x.defaultPrevented)return;const y=x.key==="Enter"||x.key===" "||x.key==="ArrowDown"?"first":x.key==="ArrowUp"?"last":null;if(!y)return;x.preventDefault();const k=h.open?document.getElementById(h.panelId):null;if(k){const C=L(k);S(y==="last"?C[C.length-1]:C[0]);return}h.openWith(y)}};return r?c.jsx("button",{ref:j,type:"button",className:n,...I,...P,children:t}):c.jsx(be.Button,{ref:j,className:E.cn("rst:min-h-11 rst:min-w-11",n),variant:u,colorScheme:p,size:m,isLoading:f,loadingLabel:l,startIcon:b,endIcon:w,surface:g,...I,...P,children:t})});function Ce({children:e,align:r="start",offset:n=4,className:o,...a}){const t=R("MenuItems"),{mounted:d,closed:s,leaving:u}=ee(t.open),p=i.useRef(null),m=$(t.triggerEl),{setPanel:f,panelEl:l,floatingStyles:b,placement:w,isPositioned:g}=te(t.triggerEl,d,`bottom-${r}`,n,p),I=se(),{typedRef:h,typingAhead:M}=ne(),j=oe({panelRef:p,typedRef:h,onEscape:()=>t.close(!0)});i.useLayoutEffect(()=>{if(!t.open||!l)return;const y=L(l),k=t.focusOnOpen.current;k==="first"&&y[0]?S(y[0]):k==="last"&&y.length?S(y[y.length-1]):l.focus({preventScroll:!0})},[t.open,l,t.focusOnOpen]),i.useLayoutEffect(()=>{if(!g||!l)return;const y=document.activeElement;y&&y!==l&&l.contains(y)&&y.scrollIntoView?.({block:"nearest"})},[g,l]),i.useLayoutEffect(()=>{!t.open&&l?.contains(document.activeElement)&&t.triggerRef.current?.focus({preventScroll:!0})},[t.open,l,t.triggerRef]);const P=y=>{const k=y.relatedTarget;k&&!k.closest(`[data-menu-tree="${t.treeId}"]`)&&t.close(!1)};if(!d)return null;const x=!!t.triggerEl?.closest(".dark");return c.jsx(W.Portal,{children:c.jsx(_.Provider,{value:{...I,panelRef:p,typingAhead:M},children:c.jsx("div",{ref:f,id:t.panelId,dir:m?"rtl":"ltr",role:"menu","aria-labelledby":a["aria-label"]?void 0:t.triggerId,"aria-label":a["aria-label"],tabIndex:-1,"data-menu-panel":"","data-menu-tree":t.treeId,"data-closed":s?"":void 0,"data-leaving":u?"":void 0,onKeyDown:j,onBlur:P,style:{...b,...J,transformOrigin:Q(w,m)},className:E.cn(x&&"dark",Z,o),children:e})})})}function se(){const[e,r]=i.useState(null),n=i.useRef(!1),o=i.useRef(void 0);return i.useEffect(()=>()=>window.clearTimeout(o.current),[]),{openSubmenu:e,focusFirstInSubmenu:n,setOpenSubmenu:(a,t=!1)=>{window.clearTimeout(o.current),n.current=t,r(a)},hoverItem:a=>{window.clearTimeout(o.current),a===null&&(o.current=window.setTimeout(()=>r(null),Y))}}}const O=E.cn("rst:relative rst:flex rst:w-full rst:min-h-11 rst:items-center rst:gap-2.5 rst:border-0 rst:bg-transparent rst:px-3 rst:py-2 rst:text-start rst:text-sm rst:text-inherit rst:no-underline rst:[font-family:inherit] rst:cursor-pointer rst:select-none rst:outline-none rst:transition-colors","rst:focus:bg-primary-100 rst:focus:text-primary-900 rst:dark:focus:bg-primary-900/30 rst:dark:focus:text-primary-100 rst:forced-colors:focus:outline-solid rst:forced-colors:focus:outline-2 rst:forced-colors:focus:-outline-offset-2","rst:aria-disabled:opacity-50 rst:aria-disabled:cursor-not-allowed"),q="rst:text-error-600 rst:dark:text-error-400 rst:focus:bg-error-50 rst:focus:text-error-700 rst:dark:focus:bg-error-900/30 rst:dark:focus:text-error-300",Ae=()=>c.jsx("svg",{viewBox:"0 0 16 16",width:"16",height:"16",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",children:c.jsx("path",{d:"M3.5 8.5 6.5 11.5 12.5 4.5"})}),De=()=>c.jsx("svg",{viewBox:"0 0 16 16",width:"16",height:"16","aria-hidden":"true",children:c.jsx("circle",{cx:"8",cy:"8",r:"3.5",fill:"currentColor"})}),Le=()=>c.jsx("svg",{viewBox:"0 0 16 16",width:"16",height:"16",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",className:"rst:rtl:rotate-180",children:c.jsx("path",{d:"M6 3.5 10.5 8 6 12.5"})});function T({icon:e,shortcut:r,description:n,badge:o,children:a},t,d,s){const u=t??e;return c.jsxs(c.Fragment,{children:[u!=null&&c.jsx("span",{"aria-hidden":"true",className:E.cn("rst:flex rst:size-4 rst:shrink-0 rst:items-center rst:justify-center",n!=null&&"rst:self-start rst:mt-0.5"),"data-menu-lead":"",children:u}),c.jsxs("span",{className:"rst:min-w-0 rst:flex-1 rst:break-words","data-menu-label":"",children:[a,n!=null&&c.jsx("span",{id:s,"aria-hidden":"true",className:"rst:block rst:text-xs rst:opacity-75","data-menu-description":"",children:n})]}),o!=null&&c.jsx("span",{className:"rst:flex rst:shrink-0 rst:items-center","data-menu-badge":"",children:o}),r&&c.jsx("span",{"aria-hidden":"true",className:"rst:ms-auto rst:shrink-0 rst:ps-4 rst:text-xs rst:tracking-wide rst:opacity-70","data-menu-shortcut":"",children:r}),d]})}function z({disabled:e,closeOnSelect:r,onChoose:n,spaceKeepsOpen:o}){const a=R("MenuItem"),t=K("MenuItem"),d=(s,u)=>{if(e){s.preventDefault();return}n(s),u&&a.close(!0)};return{onClick:s=>d(s,r),onKeyDown:s=>{if(s.key===" "){if(t.typingAhead())return;s.preventDefault(),d(s,r&&!o)}},onPointerMove:s=>{X(s)&&(t.hoverItem(null),document.activeElement!==s.currentTarget&&s.currentTarget.focus({preventScroll:!0}))}}}function Oe({onSelect:e,href:r,closeOnSelect:n=!0,disabled:o,destructive:a,keyshortcuts:t,textValue:d,className:s,...u}){const p=R("MenuItem"),m=K("MenuItem"),f=i.useId(),l=z({disabled:o,closeOnSelect:n,onChoose:g=>e?.(g)}),b=E.cn(O,a&&q,s),w={role:"menuitem",tabIndex:-1,"aria-disabled":o?!0:void 0,"aria-keyshortcuts":t,"aria-describedby":u.description!=null?f:void 0,"data-menu-item":"","data-menu-text":d,"data-destructive":a?"":void 0};if(r!==void 0){const g=p.linkComponent;return c.jsx(g,{href:r,to:r,className:b,...w,onClick:l.onClick,onPointerMove:l.onPointerMove,onKeyDown:I=>{if(I.key===" "){if(m.typingAhead())return;I.preventDefault(),o||I.currentTarget.click()}},children:T(u,void 0,void 0,f)})}return c.jsx("button",{type:"button",className:b,...w,...l,children:T(u,void 0,void 0,f)})}function Ne({checked:e,onCheckedChange:r,indicator:n,closeOnSelect:o=!0,disabled:a,destructive:t,keyshortcuts:d,textValue:s,className:u,...p}){const m=z({disabled:a,closeOnSelect:o,spaceKeepsOpen:!0,onChoose:()=>r(!e)}),f=i.useId();return c.jsx("button",{type:"button",role:"menuitemcheckbox","aria-checked":e,"aria-describedby":p.description!=null?f:void 0,tabIndex:-1,"aria-disabled":a||void 0,"aria-keyshortcuts":d,"data-menu-item":"","data-menu-text":s,className:E.cn(O,t&&q,u),...m,children:n===void 0?T(p,c.jsx("span",{className:e?void 0:"rst:invisible","data-menu-check":"",children:c.jsx(Ae,{})}),void 0,f):T(p,void 0,c.jsx("span",{"aria-hidden":"true",className:"rst:ms-auto rst:flex rst:shrink-0 rst:items-center",children:n}),f)})}function Be({value:e,onValueChange:r,label:n,children:o,className:a}){return c.jsx(V.Provider,{value:{value:e,onValueChange:r},children:c.jsx(ae,{label:n,className:a,children:o})})}function _e({value:e,closeOnSelect:r=!0,disabled:n,destructive:o,keyshortcuts:a,textValue:t,className:d,...s}){const u=i.useContext(V);if(!u)throw new Error("<MenuRadioItem> must be inside a <MenuRadioGroup>.");const p=u.value===e,m=z({disabled:n,closeOnSelect:r,spaceKeepsOpen:!0,onChoose:()=>u.onValueChange?.(e)}),f=i.useId();return c.jsx("button",{type:"button",role:"menuitemradio","aria-checked":p,"aria-describedby":s.description!=null?f:void 0,tabIndex:-1,"aria-disabled":n||void 0,"aria-keyshortcuts":a,"data-menu-item":"","data-menu-text":t,className:E.cn(O,o&&q,d),...m,children:T(s,c.jsx("span",{className:p?void 0:"rst:invisible","data-menu-check":"",children:c.jsx(De,{})}),void 0,f)})}function ae({label:e,children:r,className:n}){const o=i.useId();return c.jsxs("div",{role:"group","aria-labelledby":e?o:void 0,className:n,children:[e&&c.jsx("div",{id:o,className:"rst:px-3 rst:pt-2 rst:pb-1 rst:text-xs rst:font-semibold rst:text-gray-600 rst:dark:text-gray-400","data-menu-heading":"",children:e}),r]})}function Ke({className:e}){return c.jsx("div",{role:"separator",className:E.cn("rst:my-1 rst:h-[var(--roster-border-width,1px)] rst:bg-[var(--roster-popover-border)]",e)})}function $e({label:e,children:r,icon:n,disabled:o,textValue:a,className:t,panelClassName:d}){const s=R("MenuSubmenu"),u=K("MenuSubmenu"),p=i.useId(),m=`${p}-item`,f=`${p}-menu`,l=u.openSubmenu===m,b=i.useRef(null),[w,g]=i.useState(null),I=i.useCallback(v=>{b.current=v,g(v)},[]),h=i.useRef(null),M=i.useRef(void 0),j=i.useRef("mouse"),{mounted:P,closed:x,leaving:y}=ee(l),k=$(w),{setPanel:C,panelEl:N,floatingStyles:ie,placement:ue}=te(w,P,k?"left-start":"right-start",{mainAxis:0,alignmentAxis:-4},h,!0),ce=se();i.useEffect(()=>()=>window.clearTimeout(M.current),[]);const F=()=>{u.setOpenSubmenu(null),b.current?.focus({preventScroll:!0})},{typedRef:le,typingAhead:de}=ne(),fe=oe({panelRef:h,typedRef:le,onEscape:F,onBack:F});i.useLayoutEffect(()=>{!l||!N||u.focusFirstInSubmenu.current&&S(L(N)[0])},[l,N,u.focusFirstInSubmenu]);const B=v=>{if(!o){if(l&&v&&h.current){S(L(h.current)[0]);return}u.setOpenSubmenu(m,v)}},me=!!s.triggerEl?.closest(".dark");return c.jsxs(c.Fragment,{children:[c.jsx("button",{ref:I,id:m,type:"button",role:"menuitem",tabIndex:-1,"aria-haspopup":"menu","aria-expanded":l,"aria-controls":l?f:void 0,"aria-disabled":o||void 0,"data-menu-item":"","data-menu-text":a,"data-menu-tree":s.treeId,className:E.cn(O,l&&"rst:bg-gray-100 rst:dark:bg-gray-700/50",t),onPointerDown:v=>{j.current=v.pointerType},onClick:()=>l&&j.current!=="mouse"?u.setOpenSubmenu(null):B(!1),onKeyDown:v=>{const pe=k?"ArrowLeft":"ArrowRight";v.key===" "&&u.typingAhead()||(v.key===pe||v.key==="Enter"||v.key===" ")&&(v.preventDefault(),B(!0))},onPointerMove:v=>{X(v)&&(u.hoverItem(m),document.activeElement!==v.currentTarget&&v.currentTarget.focus({preventScroll:!0}),!l&&!o&&(window.clearTimeout(M.current),M.current=window.setTimeout(()=>B(!1),Y)))},onPointerLeave:()=>window.clearTimeout(M.current),children:T({icon:n,children:e},void 0,c.jsx("span",{"aria-hidden":"true",className:"rst:ms-auto rst:flex rst:shrink-0 rst:items-center rst:opacity-70","data-menu-chevron":"",children:c.jsx(Le,{})}))}),P&&c.jsx(W.Portal,{children:c.jsx(_.Provider,{value:{...ce,panelRef:h,typingAhead:de},children:c.jsx("div",{ref:C,id:f,dir:k?"rtl":"ltr",role:"menu","aria-labelledby":m,tabIndex:-1,"data-menu-panel":"","data-menu-tree":s.treeId,"data-closed":x?"":void 0,"data-leaving":y?"":void 0,onKeyDown:fe,onPointerEnter:()=>u.hoverItem(m),style:{...ie,...J,transformOrigin:Q(ue,k)},className:E.cn(me&&"dark",Z,d),children:r})})})]})}exports.Menu=Te;exports.MenuButton=Re;exports.MenuCheckboxItem=Ne;exports.MenuGroup=ae;exports.MenuItem=Oe;exports.MenuItems=Ce;exports.MenuRadioGroup=Be;exports.MenuRadioItem=_e;exports.MenuSeparator=Ke;exports.MenuSubmenu=$e;
@@ -0,0 +1,169 @@
1
+ import { ButtonProps } from '../../atoms/Button/Button';
2
+ import * as React from "react";
3
+ /**
4
+ * The props every link item is rendered with. `href` and `to` carry the same
5
+ * value, so `next/link`, React Router's `Link` and a plain `<a>` all work
6
+ * without a wrapper. The rest must reach the DOM: the role, the tab index and
7
+ * the handlers are what make the link an item of the menu.
8
+ */
9
+ export interface MenuLinkProps {
10
+ href: string;
11
+ to: string;
12
+ className: string;
13
+ children: React.ReactNode;
14
+ role: "menuitem";
15
+ tabIndex: -1;
16
+ "aria-disabled"?: true;
17
+ "aria-keyshortcuts"?: string;
18
+ "aria-describedby"?: string;
19
+ "data-menu-item": "";
20
+ onClick: (event: React.MouseEvent<HTMLAnchorElement>) => void;
21
+ onKeyDown: (event: React.KeyboardEvent<HTMLAnchorElement>) => void;
22
+ onPointerMove: (event: React.PointerEvent<HTMLAnchorElement>) => void;
23
+ }
24
+ export interface MenuProps {
25
+ /** The trigger (`MenuButton`) and the panel (`MenuItems`). */
26
+ children: React.ReactNode;
27
+ /**
28
+ * Controlled open state. Leave both off to let the menu keep its own. Pass
29
+ * `onOpenChange` with `open`: every way the menu closes itself (Escape,
30
+ * Tab, a press outside, a choice) is a call to it.
31
+ */
32
+ open?: boolean;
33
+ onOpenChange?: (open: boolean) => void;
34
+ /**
35
+ * Renders every link item (a `MenuItem` with an `href`). Defaults to a plain
36
+ * `<a>`, which inside a router turns each choice into a full page load: pass
37
+ * `next/link` or your router's link, from a client component.
38
+ */
39
+ linkComponent?: React.ComponentType<MenuLinkProps>;
40
+ }
41
+ /**
42
+ * A button that opens a panel of actions: the menu button pattern. Compose it:
43
+ *
44
+ * ```tsx
45
+ * <Menu>
46
+ * <MenuButton>Actions</MenuButton>
47
+ * <MenuItems>
48
+ * <MenuItem icon={<EditIcon />} shortcut="⌘E" onSelect={edit}>Edit</MenuItem>
49
+ * <MenuSeparator />
50
+ * <MenuItem destructive onSelect={remove}>Delete</MenuItem>
51
+ * </MenuItems>
52
+ * </Menu>
53
+ * ```
54
+ *
55
+ * The keyboard follows the pattern: Enter, Space or Down open it on the first
56
+ * item and Up on the last; arrows move, Home and End jump, a typed letter
57
+ * jumps to the next item starting with it; Escape closes it and puts focus
58
+ * back on the button; Tab closes it and moves on. A submenu opens with the
59
+ * arrow away from the parent and closes with the arrow back.
60
+ */
61
+ declare function Menu({ children, open: openProp, onOpenChange, linkComponent }: MenuProps): import("react/jsx-runtime").JSX.Element;
62
+ export interface MenuButtonProps extends ButtonProps {
63
+ /**
64
+ * A plain `<button>` with only your `className`, for a trigger drawn its own
65
+ * way: an avatar, an icon. Keep its target at least 44 by 44, with
66
+ * padding or a hit area, if you draw it smaller.
67
+ */
68
+ plain?: boolean;
69
+ }
70
+ declare const MenuButton: React.ForwardRefExoticComponent<MenuButtonProps & React.RefAttributes<HTMLButtonElement>>;
71
+ export interface MenuItemsProps {
72
+ children: React.ReactNode;
73
+ /** Which edge of the button the panel lines up with. */
74
+ align?: "start" | "end";
75
+ /** The gap between the button and the panel, in px. */
76
+ offset?: number;
77
+ /** Names the menu when the button's own name shouldn't. By default the button names it. */
78
+ "aria-label"?: string;
79
+ /** Applied to the panel. Its surface reads `--roster-popover-*`; set those to repaint it. */
80
+ className?: string;
81
+ }
82
+ declare function MenuItems({ children, align, offset, className, ...aria }: MenuItemsProps): import("react/jsx-runtime").JSX.Element | null;
83
+ interface ItemContent {
84
+ /** A leading icon. Decorative: the item's words name it. */
85
+ icon?: React.ReactNode;
86
+ /** A second, quieter line under the label: what the item does, or to what. Read as the item's description. */
87
+ description?: React.ReactNode;
88
+ /** A count or a status at the end of the row, read with the item's name. */
89
+ badge?: React.ReactNode;
90
+ /** A keyboard shortcut to show, right-aligned: display only. The app handles the keys. */
91
+ shortcut?: string;
92
+ /** The same shortcut in `aria-keyshortcuts` form ("Control+E"), for assistive technology. */
93
+ keyshortcuts?: string;
94
+ /** Reads as a consequence: red. The words still have to say what it does. */
95
+ destructive?: boolean;
96
+ /** Can be reached by the arrows but not chosen. */
97
+ disabled?: boolean;
98
+ /** What typeahead matches, when the item's text isn't the right thing to match. */
99
+ textValue?: string;
100
+ className?: string;
101
+ children: React.ReactNode;
102
+ }
103
+ export interface MenuItemProps extends ItemContent {
104
+ /** Called when the item is chosen: a click, Enter or Space. */
105
+ onSelect?: (event: React.SyntheticEvent) => void;
106
+ /** Makes it a link item. It navigates through the menu's `linkComponent`. */
107
+ href?: string;
108
+ /** Close the menu once chosen. On by default. */
109
+ closeOnSelect?: boolean;
110
+ }
111
+ /** One action. With `href`, a link. */
112
+ declare function MenuItem({ onSelect, href, closeOnSelect, disabled, destructive, keyshortcuts, textValue, className, ...content }: MenuItemProps): import("react/jsx-runtime").JSX.Element;
113
+ export interface MenuCheckboxItemProps extends ItemContent {
114
+ checked: boolean;
115
+ onCheckedChange: (checked: boolean) => void;
116
+ /**
117
+ * Replaces the leading check mark with an indicator of your own at the end
118
+ * of the row: a switch's track, say. Its state still reaches assistive
119
+ * technology through `aria-checked`.
120
+ */
121
+ indicator?: React.ReactNode;
122
+ /** Close the menu once chosen by click or Enter. Space always keeps it open. */
123
+ closeOnSelect?: boolean;
124
+ }
125
+ /** An on/off choice: `menuitemcheckbox`. */
126
+ declare function MenuCheckboxItem({ checked, onCheckedChange, indicator, closeOnSelect, disabled, destructive, keyshortcuts, textValue, className, ...content }: MenuCheckboxItemProps): import("react/jsx-runtime").JSX.Element;
127
+ export interface MenuRadioGroupProps {
128
+ value: string | undefined;
129
+ onValueChange: (value: string) => void;
130
+ /** The choice's name, shown above its items and read with them. */
131
+ label?: React.ReactNode;
132
+ children: React.ReactNode;
133
+ className?: string;
134
+ }
135
+ /** One choice among several: its items are `menuitemradio`. */
136
+ declare function MenuRadioGroup({ value, onValueChange, label, children, className }: MenuRadioGroupProps): import("react/jsx-runtime").JSX.Element;
137
+ export interface MenuRadioItemProps extends ItemContent {
138
+ value: string;
139
+ /** Close the menu once chosen by click or Enter. Space always keeps it open. */
140
+ closeOnSelect?: boolean;
141
+ }
142
+ declare function MenuRadioItem({ value, closeOnSelect, disabled, destructive, keyshortcuts, textValue, className, ...content }: MenuRadioItemProps): import("react/jsx-runtime").JSX.Element;
143
+ export interface MenuGroupProps {
144
+ /** Shown above the group's items, and names the group. */
145
+ label?: React.ReactNode;
146
+ children: React.ReactNode;
147
+ className?: string;
148
+ }
149
+ /** Items that belong together, under an optional label. */
150
+ declare function MenuGroup({ label, children, className }: MenuGroupProps): import("react/jsx-runtime").JSX.Element;
151
+ /** A rule between groups of items. */
152
+ declare function MenuSeparator({ className }: {
153
+ className?: string;
154
+ }): import("react/jsx-runtime").JSX.Element;
155
+ export interface MenuSubmenuProps extends Omit<ItemContent, "shortcut" | "keyshortcuts" | "children" | "destructive" | "description" | "badge"> {
156
+ /** The item that opens it. */
157
+ label: React.ReactNode;
158
+ /** Its items. */
159
+ children: React.ReactNode;
160
+ /** Applied to the submenu's panel. */
161
+ panelClassName?: string;
162
+ }
163
+ /**
164
+ * An item that opens a menu of its own beside it. It opens with the arrow away
165
+ * from its parent, Enter, Space, a click, or the pointer resting on it; the
166
+ * arrow back, or Escape, closes it and returns to its item.
167
+ */
168
+ declare function MenuSubmenu({ label, children, icon, disabled, textValue, className, panelClassName }: MenuSubmenuProps): import("react/jsx-runtime").JSX.Element;
169
+ export { Menu, MenuButton, MenuItems, MenuItem, MenuCheckboxItem, MenuRadioGroup, MenuRadioItem, MenuGroup, MenuSeparator, MenuSubmenu, };