@ahrowe/ui 0.47.3 → 0.49.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 (47) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/LICENSE +1 -1
  3. package/README.md +2 -2
  4. package/dist/cjs-types/common/actionIcon/actionIcon.types.d.ts +1 -1
  5. package/dist/cjs-types/common/button/button.types.d.ts +6 -1
  6. package/dist/cjs-types/common/hooks/useHotkeys.d.ts +11 -0
  7. package/dist/cjs-types/common/menu/menu.types.d.ts +5 -0
  8. package/dist/cjs-types/common/menu/menuHotkeys.d.ts +2 -2
  9. package/dist/cjs-types/common/menu/menuItem.d.ts +10 -0
  10. package/dist/esm/common/button/button.mjs +1 -1
  11. package/dist/esm/common/button/button.mjs.map +1 -1
  12. package/dist/esm/common/button/button.module.mjs +1 -1
  13. package/dist/esm/common/button/button.module.mjs.map +1 -1
  14. package/dist/esm/common/button/button.types.mjs.map +1 -1
  15. package/dist/esm/common/commandPalette/commandPalette.mjs +1 -1
  16. package/dist/esm/common/commandPalette/commandPalette.mjs.map +1 -1
  17. package/dist/esm/common/hooks/useHotkeys.mjs +1 -1
  18. package/dist/esm/common/hooks/useHotkeys.mjs.map +1 -1
  19. package/dist/esm/common/menu/menu.mjs +1 -1
  20. package/dist/esm/common/menu/menu.mjs.map +1 -1
  21. package/dist/esm/common/menu/menu.module.mjs.map +1 -1
  22. package/dist/esm/common/menu/menu.types.mjs.map +1 -1
  23. package/dist/esm/common/menu/menuHotkeys.mjs +1 -1
  24. package/dist/esm/common/menu/menuHotkeys.mjs.map +1 -1
  25. package/dist/esm/common/menu/menuItem.mjs +2 -0
  26. package/dist/esm/common/menu/menuItem.mjs.map +1 -0
  27. package/dist/esm/common/radioGroup/radioGroup.module.mjs.map +1 -1
  28. package/dist/esm/index.mjs +1 -1
  29. package/dist/index.cjs +3 -3
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/style.css +1 -1
  32. package/dist/types/common/actionIcon/actionIcon.types.d.ts +1 -1
  33. package/dist/types/common/button/button.types.d.ts +6 -1
  34. package/dist/types/common/hooks/useHotkeys.d.ts +11 -0
  35. package/dist/types/common/menu/menu.types.d.ts +5 -0
  36. package/dist/types/common/menu/menuHotkeys.d.ts +2 -2
  37. package/dist/types/common/menu/menuItem.d.ts +10 -0
  38. package/docs/ActionIcon.md +2 -2
  39. package/docs/Button.md +18 -2
  40. package/docs/CommandPalette.md +1 -1
  41. package/docs/Hooks.md +23 -4
  42. package/docs/InteractableDiv.md +1 -1
  43. package/docs/Menu.md +7 -6
  44. package/docs/ProgressBar.md +1 -1
  45. package/docs/RadioGroup.md +1 -1
  46. package/docs/ThemeProvider.md +1 -1
  47. package/package.json +4 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # @ahrowe/ui
2
2
 
3
+ ## 0.49.0
4
+
5
+ ### Minor Changes
6
+
7
+ - fa4844e: Button takes a `shortcut` and draws it in a pill after the label, `⌘S` or `Ctrl+S` for the platform, with `aria-keyshortcuts` to match; binding it stays with `useHotkeys`
8
+ - fa4844e: `useHotkeys` fires once for a held combo, so holding ⌘S saves once; `repeat: true` brings the repeats back for keys meant to be held, see the migration section in Hooks.md
9
+
10
+ ### Patch Changes
11
+
12
+ - fa4844e: Menu and CommandPalette announce an entry's shortcut through `aria-keyshortcuts` instead of reading it out as part of the entry's name
13
+
14
+ ## 0.48.0
15
+
16
+ ### Minor Changes
17
+
18
+ - 60d1ee6: Menu entries take an `href` and render as real links, so Ctrl/⌘-click and a middle click open them in a new tab; CommandPalette and `menuHotkeys` follow it too
19
+
20
+ ### Patch Changes
21
+
22
+ - 60d1ee6: Menu and CommandPalette line up their labels when only some entries have an icon
23
+ - f1eab37: ProgressBar.md gives the track colour's real fallback, `--background-accent-light`
24
+ - f2dd48a: Centred RadioGroup's dot, which rendered a pixel off towards the bottom right
25
+ - 79a9eb7: Moved the repository to github.com/ahrowe-dev/ahrowe-ui, and changed the copyright holder in LICENSE to ahrowe, from 2026
26
+ - f1eab37: ThemeProvider.md points to the full variable list in the installed package instead of a source path npm does not ship
27
+
3
28
  ## 0.47.3
4
29
 
5
30
  ### Patch Changes
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 AndreasWeinzierl
3
+ Copyright (c) 2026 Ahrowe
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -72,7 +72,7 @@ The docs ship inside the package and Claude reads them out of `node_modules`. Th
72
72
  until Claude actually works on UI.
73
73
 
74
74
  ```bash
75
- /plugin marketplace add AndreasWeinzierl/ahrowe-ui
75
+ /plugin marketplace add ahrowe-dev/ahrowe-ui
76
76
  /plugin install ahrowe-ui@ahrowe
77
77
  ```
78
78
 
@@ -81,7 +81,7 @@ For a whole team at once, commit this to the project's `.claude/settings.json` i
81
81
  ```json
82
82
  {
83
83
  "extraKnownMarketplaces": {
84
- "ahrowe": { "source": { "source": "github", "repo": "AndreasWeinzierl/ahrowe-ui" } }
84
+ "ahrowe": { "source": { "source": "github", "repo": "ahrowe-dev/ahrowe-ui" } }
85
85
  },
86
86
  "enabledPlugins": [{ "name": "ahrowe-ui", "marketplace": "ahrowe" }]
87
87
  }
@@ -6,7 +6,7 @@ export type ActionIconSlots = 'root' | 'icon';
6
6
  * A Button with only an icon, sized by that icon, so every Button prop applies except the ones
7
7
  * that describe a label or a size.
8
8
  */
9
- export interface ActionIconProps extends Omit<ButtonProps, 'icon' | 'size' | 'children' | 'isLoading' | 'classNames' | 'styles'> {
9
+ export interface ActionIconProps extends Omit<ButtonProps, 'icon' | 'size' | 'children' | 'isLoading' | 'shortcut' | 'classNames' | 'styles'> {
10
10
  icon: IconContent;
11
11
  classNames?: SlotClassNames<ActionIconSlots>;
12
12
  styles?: SlotStyles<ActionIconSlots>;
@@ -26,7 +26,7 @@ export declare enum ButtonType {
26
26
  Reset = "reset",
27
27
  Submit = "submit"
28
28
  }
29
- export type ButtonSlots = 'background' | 'content' | 'loadingContainer' | 'label';
29
+ export type ButtonSlots = 'background' | 'content' | 'loadingContainer' | 'label' | 'shortcut';
30
30
  /**
31
31
  * `DataProps` supplies the `data-*` index signature, which `ButtonHTMLAttributes` doesn't cover.
32
32
  * `type`, `color` and `onClick` below deliberately narrow their inherited counterparts.
@@ -43,6 +43,11 @@ export interface ButtonProps extends DataProps, React.ButtonHTMLAttributes<HTMLB
43
43
  onClick?: (event: React.MouseEvent<HTMLButtonElement>) => Promise<void> | void;
44
44
  children?: React.ReactNode;
45
45
  icon?: IconContent;
46
+ /**
47
+ * A shortcut drawn after the label, in `useHotkeys` spelling (`'mod+s'`): `⌘S` on Apple
48
+ * platforms, `Ctrl+S` elsewhere. Drawn only; bind it with `useHotkeys`.
49
+ */
50
+ shortcut?: string;
46
51
  isLoading?: boolean;
47
52
  disabled?: boolean;
48
53
  title?: string;
@@ -18,6 +18,11 @@ export interface UseHotkeysOptions {
18
18
  * Default `true`.
19
19
  */
20
20
  preventDefault?: boolean;
21
+ /**
22
+ * Fire again for each repeat while the combo is held. Default `false`, so holding `mod+s` saves
23
+ * once. Turn it on for keys that step through something, such as arrows or zoom.
24
+ */
25
+ repeat?: boolean;
21
26
  }
22
27
  /**
23
28
  * A combo written for the reader: `'mod+shift+k'` reads `⇧⌘K` on an Apple platform and
@@ -30,6 +35,12 @@ export interface UseHotkeysOptions {
30
35
  * Resolved from the platform at call time, so a server-rendered page renders the non-Apple form.
31
36
  */
32
37
  export declare function formatHotkey(combo: string): string;
38
+ /**
39
+ * A combo as `aria-keyshortcuts` spells it: `'mod+shift+k'` is `Control+Shift+K`, or
40
+ * `Meta+Shift+K` on an Apple platform. Resolved from the platform like `formatHotkey`, so the two
41
+ * always name the same keys.
42
+ */
43
+ export declare function ariaKeyshortcuts(combo: string | undefined): string | undefined;
33
44
  /**
34
45
  * Keyboard shortcuts, for a whole app or for one element.
35
46
  *
@@ -25,6 +25,11 @@ export interface MenuAction {
25
25
  disabled?: boolean;
26
26
  /** Renders in the destructive colour. For deleting, discarding, revoking. */
27
27
  danger?: boolean;
28
+ /**
29
+ * Followed after `onClick` and `onAction`. In `Menu` a real `<a>`, so Ctrl/⌘-click opens a new tab.
30
+ * Loads the whole page: with a client-side router, navigate in `onClick` instead.
31
+ */
32
+ href?: string;
28
33
  onClick?: () => void;
29
34
  }
30
35
  /** A line between groups of actions. */
@@ -14,7 +14,7 @@ import { MenuEntry } from './menu.types.js';
14
14
  * useHotkeys(menuHotkeys(items, run), { enabled: isOpen }); // only while the menu is open
15
15
  * ```
16
16
  *
17
- * Runs each entry the way clicking it does: its own `onClick`, then `onAction`. A `disabled`
18
- * entry, and an entry with no `shortcut`, is left out.
17
+ * Runs each entry the way clicking it does: its own `onClick`, then `onAction`, then its `href`.
18
+ * A `disabled` entry, and an entry with no `shortcut`, is left out.
19
19
  */
20
20
  export declare function menuHotkeys(items: MenuEntry[], onAction?: (id: string) => void): HotkeyMap;
@@ -0,0 +1,10 @@
1
+ import { CSSProperties, ReactNode } from 'react';
2
+ import { MenuAction, MenuEntry } from './menu.types.js';
3
+ export declare function hasAnyIcon(items: MenuEntry[]): boolean;
4
+ /** The action's icon, or an empty cell as wide when another action has one, so the labels line up. */
5
+ export declare function renderItemIcon(icon: MenuAction['icon'], reserveCell: boolean, props: {
6
+ className: string;
7
+ style?: CSSProperties;
8
+ }): ReactNode;
9
+ /** Runs an action the way clicking it does: its own `onClick`, then `onAction`, then its `href`. */
10
+ export declare function runMenuAction(action: MenuAction, onAction?: (id: string) => void): void;
@@ -1,2 +1,2 @@
1
- "use client";import{useComponentDefaults as e}from"../configProvider/useComponentDefaults.mjs";import{ThemeColor as t}from"../themeProvider/color.types.mjs";import{toneProps as n,useToneCheck as r}from"../styles/tone.mjs";import i from"./button.module.mjs";import a from"../loader/loader.mjs";import{ButtonSize as o,ButtonStyleType as s,ButtonType as c}from"./button.types.mjs";import l from"../ripple/ripple.mjs";import{renderIcon as u}from"../utils/renderIcon.mjs";import{useRef as d}from"react";import f from"classnames";import{jsx as p,jsxs as m}from"react/jsx-runtime";var h={filled:i.buttonStyleFilled,light:i.buttonStyleLight,outlined:i.buttonStyleOutlined,plain:i.buttonStylePlain},g={small:i.buttonSizeSmall,medium:i.buttonSizeMedium,large:i.buttonSizeLarge,icon:i.buttonSizeIcon},_=`color-mix(in srgb, currentcolor 20%, transparent)`;function v(v){let{className:y,style:b,type:x=c.Button,styleType:S=s.Outlined,color:C=t.Primary,size:w=o.Medium,onClick:T=()=>{},children:E,icon:D,isLoading:O=!1,disabled:k=!1,title:A=void 0,ref:j,classNames:M,styles:N,...P}=e(`Button`,v),F=d(null),I=n(C);r(`Button`,[C]);function L(e){return e.detail===0&&F.current?.addRipple(),T(e)}return p(`button`,{...P,className:f(i.button,h[S],I.className,g[w],{[i.buttonNotAllowed]:O||k,[i.buttonDisabled]:k},y),style:{...I.style,...b},type:x,onClick:L,disabled:k||O,title:A,ref:j,children:p(l,{ref:F,className:i.buttonRipple,disabled:O||k,rippleBackground:_,children:p(`div`,{className:f(i.buttonBackground,M?.background),style:N?.background,children:m(`div`,{className:f(i.buttonContent,M?.content),style:N?.content,children:[O?p(`div`,{className:f(i.buttonLoadingContainer,M?.loadingContainer),style:N?.loadingContainer,children:p(a,{size:`1.2em`,color:t.Neutral})}):null,m(`div`,{className:f(M?.label,{[i.buttonLoading]:O,[i.buttonWithIcon]:!!D}),style:N?.label,children:[D?u(D):null,E]})]})})})})}v.StyleType=s,v.Size=o,v.Type=c;export{v as default};
1
+ "use client";import{useComponentDefaults as e}from"../configProvider/useComponentDefaults.mjs";import{ThemeColor as t}from"../themeProvider/color.types.mjs";import{toneProps as n,useToneCheck as r}from"../styles/tone.mjs";import i from"./button.module.mjs";import a from"../loader/loader.mjs";import{ButtonSize as o,ButtonStyleType as s,ButtonType as c}from"./button.types.mjs";import l from"../ripple/ripple.mjs";import{renderIcon as u}from"../utils/renderIcon.mjs";import{ariaKeyshortcuts as d,formatHotkey as f}from"../hooks/useHotkeys.mjs";import{useRef as p}from"react";import m from"classnames";import{jsx as h,jsxs as g}from"react/jsx-runtime";var _={filled:i.buttonStyleFilled,light:i.buttonStyleLight,outlined:i.buttonStyleOutlined,plain:i.buttonStylePlain},v={small:i.buttonSizeSmall,medium:i.buttonSizeMedium,large:i.buttonSizeLarge,icon:i.buttonSizeIcon},y=`color-mix(in srgb, currentcolor 20%, transparent)`;function b(b){let{className:x,style:S,type:C=c.Button,styleType:w=s.Outlined,color:T=t.Primary,size:E=o.Medium,onClick:D=()=>{},children:O,icon:k,shortcut:A,"aria-keyshortcuts":j=d(A),isLoading:M=!1,disabled:N=!1,title:P=void 0,ref:F,classNames:I,styles:L,...R}=e(`Button`,b),z=p(null),B=n(T);r(`Button`,[T]);function V(e){return e.detail===0&&z.current?.addRipple(),D(e)}return h(`button`,{...R,className:m(i.button,_[w],B.className,v[E],{[i.buttonNotAllowed]:M||N,[i.buttonDisabled]:N},x),style:{...B.style,...S},type:C,onClick:V,disabled:N||M,title:P,"aria-keyshortcuts":j,ref:F,children:h(l,{ref:z,className:i.buttonRipple,disabled:M||N,rippleBackground:y,children:h(`div`,{className:m(i.buttonBackground,I?.background),style:L?.background,children:g(`div`,{className:m(i.buttonContent,I?.content),style:L?.content,children:[M?h(`div`,{className:m(i.buttonLoadingContainer,I?.loadingContainer),style:L?.loadingContainer,children:h(a,{size:`1.2em`,color:t.Neutral})}):null,g(`div`,{className:m(I?.label,{[i.buttonLoading]:M,[i.buttonWithIcon]:!!k}),style:L?.label,children:[k?u(k):null,A?g(`span`,{children:[O,h(`span`,{"aria-hidden":!0,className:m(i.buttonShortcut,I?.shortcut),style:L?.shortcut,children:f(A)})]}):O]})]})})})})}b.StyleType=s,b.Size=o,b.Type=c;export{b as default};
2
2
  //# sourceMappingURL=button.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"button.mjs","names":[],"sources":["../../../../package/common/button/button.tsx"],"sourcesContent":["import cx from 'classnames';\nimport { useRef, type ReactElement } from 'react';\n\nimport styles from './button.module.pcss';\nimport Loader from '../loader';\nimport { ButtonStyleType, ButtonSize, ButtonType } from './button.types';\nimport { ThemeColor } from '../themeProvider/color.types';\nimport type { ButtonProps } from './button.types';\nimport Ripple from '../ripple';\nimport type { RippleHandle } from '../ripple';\nimport { useComponentDefaults } from '../configProvider';\nimport { renderIcon } from '../utils/renderIcon';\nimport { toneProps, useToneCheck } from '../styles/tone';\n\nconst CLASS_BY_STYLE: Record<ButtonStyleType, string> = {\n filled: styles.buttonStyleFilled,\n light: styles.buttonStyleLight,\n outlined: styles.buttonStyleOutlined,\n plain: styles.buttonStylePlain,\n};\n\nconst CLASS_BY_SIZE: Record<ButtonSize, string> = {\n small: styles.buttonSizeSmall,\n medium: styles.buttonSizeMedium,\n large: styles.buttonSizeLarge,\n icon: styles.buttonSizeIcon,\n};\n\n// Ripple's own default is white, which vanishes on a transparent button over a light page.\nconst RIPPLE_BACKGROUND = 'color-mix(in srgb, currentcolor 20%, transparent)';\n\nfunction Button(props: ButtonProps): ReactElement {\n const {\n className,\n style,\n type = ButtonType.Button,\n styleType = ButtonStyleType.Outlined,\n color = ThemeColor.Primary,\n size = ButtonSize.Medium,\n onClick = (): void => {},\n children,\n icon,\n isLoading = false,\n disabled = false,\n title = undefined,\n ref,\n classNames,\n styles: slotStyles,\n ...rest\n } = useComponentDefaults('Button', props);\n\n const rippleRef = useRef<RippleHandle>(null);\n const tone = toneProps(color);\n useToneCheck('Button', [color]);\n\n function handleClick(event: React.MouseEvent<HTMLButtonElement>) {\n if (event.detail === 0) rippleRef.current?.addRipple();\n return onClick(event);\n }\n\n return (\n <button\n {...rest}\n className={cx(\n styles.button,\n CLASS_BY_STYLE[styleType],\n tone.className,\n CLASS_BY_SIZE[size],\n {\n [styles.buttonNotAllowed]: isLoading || disabled,\n [styles.buttonDisabled]: disabled,\n },\n className,\n )}\n style={{ ...tone.style, ...style }}\n type={type}\n onClick={handleClick}\n disabled={disabled || isLoading}\n title={title}\n ref={ref}\n >\n <Ripple\n ref={rippleRef}\n className={styles.buttonRipple}\n disabled={isLoading || disabled}\n rippleBackground={RIPPLE_BACKGROUND}\n >\n <div className={cx(styles.buttonBackground, classNames?.background)} style={slotStyles?.background}>\n <div className={cx(styles.buttonContent, classNames?.content)} style={slotStyles?.content}>\n {isLoading ? (\n <div\n className={cx(styles.buttonLoadingContainer, classNames?.loadingContainer)}\n style={slotStyles?.loadingContainer}\n >\n <Loader size='1.2em' color={ThemeColor.Neutral} />\n </div>\n ) : null}\n <div\n className={cx(classNames?.label, {\n [styles.buttonLoading]: isLoading,\n [styles.buttonWithIcon]: !!icon,\n })}\n style={slotStyles?.label}\n >\n {icon ? renderIcon(icon) : null}\n {children}\n </div>\n </div>\n </div>\n </Ripple>\n </button>\n );\n}\n\nButton.StyleType = ButtonStyleType;\nButton.Size = ButtonSize;\nButton.Type = ButtonType;\n\nexport default Button;\n"],"mappings":"8jBAcA,IAAM,EAAkD,CACtD,OAAQ,EAAO,kBACf,MAAO,EAAO,iBACd,SAAU,EAAO,oBACjB,MAAO,EAAO,gBAChB,EAEM,EAA4C,CAChD,MAAO,EAAO,gBACd,OAAQ,EAAO,iBACf,MAAO,EAAO,gBACd,KAAM,EAAO,cACf,EAGM,EAAoB,oDAE1B,SAAS,EAAO,EAAkC,CAChD,GAAM,CACJ,YACA,QACA,OAAO,EAAW,OAClB,YAAY,EAAgB,SAC5B,QAAQ,EAAW,QACnB,OAAO,EAAW,OAClB,cAAsB,CAAC,EACvB,WACA,OACA,YAAY,GACZ,WAAW,GACX,QAAQ,IAAA,GACR,MACA,aACA,OAAQ,EACR,GAAG,GACD,EAAqB,SAAU,CAAK,EAElC,EAAY,EAAqB,IAAI,EACrC,EAAO,EAAU,CAAK,EAC5B,EAAa,SAAU,CAAC,CAAK,CAAC,EAE9B,SAAS,EAAY,EAA4C,CAE/D,OADI,EAAM,SAAW,GAAG,EAAU,SAAS,UAAU,EAC9C,EAAQ,CAAK,CACtB,CAEA,OACE,EAAC,SAAD,CACE,GAAI,EACJ,UAAW,EACT,EAAO,OACP,EAAe,GACf,EAAK,UACL,EAAc,GACd,EACG,EAAO,kBAAmB,GAAa,GACvC,EAAO,gBAAiB,CAC3B,EACA,CACF,EACA,MAAO,CAAE,GAAG,EAAK,MAAO,GAAG,CAAM,EAC3B,OACN,QAAS,EACT,SAAU,GAAY,EACf,QACF,MAEL,SAAA,EAAC,EAAD,CACE,IAAK,EACL,UAAW,EAAO,aAClB,SAAU,GAAa,EACvB,iBAAkB,EAElB,SAAA,EAAC,MAAD,CAAK,UAAW,EAAG,EAAO,iBAAkB,GAAY,UAAU,EAAG,MAAO,GAAY,WACtF,SAAA,EAAC,MAAD,CAAK,UAAW,EAAG,EAAO,cAAe,GAAY,OAAO,EAAG,MAAO,GAAY,QAAlF,SAAA,CACG,EACC,EAAC,MAAD,CACE,UAAW,EAAG,EAAO,uBAAwB,GAAY,gBAAgB,EACzE,MAAO,GAAY,iBAEnB,SAAA,EAAC,EAAD,CAAQ,KAAK,QAAQ,MAAO,EAAW,OAAU,CAAA,CAC9C,CAAA,EACH,KACJ,EAAC,MAAD,CACE,UAAW,EAAG,GAAY,MAAO,EAC9B,EAAO,eAAgB,GACvB,EAAO,gBAAiB,CAAC,CAAC,CAC7B,CAAC,EACD,MAAO,GAAY,MALrB,SAAA,CAOG,EAAO,EAAW,CAAI,EAAI,KAC1B,CACE,CACF,CAAA,CAAA,GACF,CAAA,CACC,CAAA,CACF,CAAA,CAEZ,CAEA,EAAO,UAAY,EACnB,EAAO,KAAO,EACd,EAAO,KAAO"}
1
+ {"version":3,"file":"button.mjs","names":[],"sources":["../../../../package/common/button/button.tsx"],"sourcesContent":["import cx from 'classnames';\nimport { useRef, type ReactElement } from 'react';\n\nimport styles from './button.module.pcss';\nimport Loader from '../loader';\nimport { ButtonStyleType, ButtonSize, ButtonType } from './button.types';\nimport { ThemeColor } from '../themeProvider/color.types';\nimport type { ButtonProps } from './button.types';\nimport Ripple from '../ripple';\nimport type { RippleHandle } from '../ripple';\nimport { useComponentDefaults } from '../configProvider';\nimport { renderIcon } from '../utils/renderIcon';\nimport { ariaKeyshortcuts, formatHotkey } from '../hooks/useHotkeys';\nimport { toneProps, useToneCheck } from '../styles/tone';\n\nconst CLASS_BY_STYLE: Record<ButtonStyleType, string> = {\n filled: styles.buttonStyleFilled,\n light: styles.buttonStyleLight,\n outlined: styles.buttonStyleOutlined,\n plain: styles.buttonStylePlain,\n};\n\nconst CLASS_BY_SIZE: Record<ButtonSize, string> = {\n small: styles.buttonSizeSmall,\n medium: styles.buttonSizeMedium,\n large: styles.buttonSizeLarge,\n icon: styles.buttonSizeIcon,\n};\n\n// Ripple's own default is white, which vanishes on a transparent button over a light page.\nconst RIPPLE_BACKGROUND = 'color-mix(in srgb, currentcolor 20%, transparent)';\n\nfunction Button(props: ButtonProps): ReactElement {\n const {\n className,\n style,\n type = ButtonType.Button,\n styleType = ButtonStyleType.Outlined,\n color = ThemeColor.Primary,\n size = ButtonSize.Medium,\n onClick = (): void => {},\n children,\n icon,\n shortcut,\n // The caller's own wins: it may list a second binding the hint leaves out.\n 'aria-keyshortcuts': keyshortcuts = ariaKeyshortcuts(shortcut),\n isLoading = false,\n disabled = false,\n title = undefined,\n ref,\n classNames,\n styles: slotStyles,\n ...rest\n } = useComponentDefaults('Button', props);\n\n const rippleRef = useRef<RippleHandle>(null);\n const tone = toneProps(color);\n useToneCheck('Button', [color]);\n\n function handleClick(event: React.MouseEvent<HTMLButtonElement>) {\n if (event.detail === 0) rippleRef.current?.addRipple();\n return onClick(event);\n }\n\n return (\n <button\n {...rest}\n className={cx(\n styles.button,\n CLASS_BY_STYLE[styleType],\n tone.className,\n CLASS_BY_SIZE[size],\n {\n [styles.buttonNotAllowed]: isLoading || disabled,\n [styles.buttonDisabled]: disabled,\n },\n className,\n )}\n style={{ ...tone.style, ...style }}\n type={type}\n onClick={handleClick}\n disabled={disabled || isLoading}\n title={title}\n aria-keyshortcuts={keyshortcuts}\n ref={ref}\n >\n <Ripple\n ref={rippleRef}\n className={styles.buttonRipple}\n disabled={isLoading || disabled}\n rippleBackground={RIPPLE_BACKGROUND}\n >\n <div className={cx(styles.buttonBackground, classNames?.background)} style={slotStyles?.background}>\n <div className={cx(styles.buttonContent, classNames?.content)} style={slotStyles?.content}>\n {isLoading ? (\n <div\n className={cx(styles.buttonLoadingContainer, classNames?.loadingContainer)}\n style={slotStyles?.loadingContainer}\n >\n <Loader size='1.2em' color={ThemeColor.Neutral} />\n </div>\n ) : null}\n <div\n className={cx(classNames?.label, {\n [styles.buttonLoading]: isLoading,\n [styles.buttonWithIcon]: !!icon,\n })}\n style={slotStyles?.label}\n >\n {icon ? renderIcon(icon) : null}\n {shortcut ? (\n // One inline run with the label, so the hint sits on its baseline: the icon's flex\n // row would make the hint its own item and centre it instead.\n <span>\n {children}\n {/* Out of the accessible name, which would read \"Save Ctrl+S\": aria-keyshortcuts says it. */}\n <span\n aria-hidden\n className={cx(styles.buttonShortcut, classNames?.shortcut)}\n style={slotStyles?.shortcut}\n >\n {formatHotkey(shortcut)}\n </span>\n </span>\n ) : (\n children\n )}\n </div>\n </div>\n </div>\n </Ripple>\n </button>\n );\n}\n\nButton.StyleType = ButtonStyleType;\nButton.Size = ButtonSize;\nButton.Type = ButtonType;\n\nexport default Button;\n"],"mappings":"2oBAeA,IAAM,EAAkD,CACtD,OAAQ,EAAO,kBACf,MAAO,EAAO,iBACd,SAAU,EAAO,oBACjB,MAAO,EAAO,gBAChB,EAEM,EAA4C,CAChD,MAAO,EAAO,gBACd,OAAQ,EAAO,iBACf,MAAO,EAAO,gBACd,KAAM,EAAO,cACf,EAGM,EAAoB,oDAE1B,SAAS,EAAO,EAAkC,CAChD,GAAM,CACJ,YACA,QACA,OAAO,EAAW,OAClB,YAAY,EAAgB,SAC5B,QAAQ,EAAW,QACnB,OAAO,EAAW,OAClB,cAAsB,CAAC,EACvB,WACA,OACA,WAEA,oBAAqB,EAAe,EAAiB,CAAQ,EAC7D,YAAY,GACZ,WAAW,GACX,QAAQ,IAAA,GACR,MACA,aACA,OAAQ,EACR,GAAG,GACD,EAAqB,SAAU,CAAK,EAElC,EAAY,EAAqB,IAAI,EACrC,EAAO,EAAU,CAAK,EAC5B,EAAa,SAAU,CAAC,CAAK,CAAC,EAE9B,SAAS,EAAY,EAA4C,CAE/D,OADI,EAAM,SAAW,GAAG,EAAU,SAAS,UAAU,EAC9C,EAAQ,CAAK,CACtB,CAEA,OACE,EAAC,SAAD,CACE,GAAI,EACJ,UAAW,EACT,EAAO,OACP,EAAe,GACf,EAAK,UACL,EAAc,GACd,EACG,EAAO,kBAAmB,GAAa,GACvC,EAAO,gBAAiB,CAC3B,EACA,CACF,EACA,MAAO,CAAE,GAAG,EAAK,MAAO,GAAG,CAAM,EAC3B,OACN,QAAS,EACT,SAAU,GAAY,EACf,QACP,oBAAmB,EACd,MAEL,SAAA,EAAC,EAAD,CACE,IAAK,EACL,UAAW,EAAO,aAClB,SAAU,GAAa,EACvB,iBAAkB,EAElB,SAAA,EAAC,MAAD,CAAK,UAAW,EAAG,EAAO,iBAAkB,GAAY,UAAU,EAAG,MAAO,GAAY,WACtF,SAAA,EAAC,MAAD,CAAK,UAAW,EAAG,EAAO,cAAe,GAAY,OAAO,EAAG,MAAO,GAAY,QAAlF,SAAA,CACG,EACC,EAAC,MAAD,CACE,UAAW,EAAG,EAAO,uBAAwB,GAAY,gBAAgB,EACzE,MAAO,GAAY,iBAEnB,SAAA,EAAC,EAAD,CAAQ,KAAK,QAAQ,MAAO,EAAW,OAAU,CAAA,CAC9C,CAAA,EACH,KACJ,EAAC,MAAD,CACE,UAAW,EAAG,GAAY,MAAO,EAC9B,EAAO,eAAgB,GACvB,EAAO,gBAAiB,CAAC,CAAC,CAC7B,CAAC,EACD,MAAO,GAAY,MALrB,SAAA,CAOG,EAAO,EAAW,CAAI,EAAI,KAC1B,EAGC,EAAC,OAAD,CAAA,SAAA,CACG,EAED,EAAC,OAAD,CACE,cAAA,GACA,UAAW,EAAG,EAAO,eAAgB,GAAY,QAAQ,EACzD,MAAO,GAAY,SAElB,SAAA,EAAa,CAAQ,CAClB,CAAA,CACF,CAAA,CAAA,EAEN,CAEC,CACF,CAAA,CAAA,GACF,CAAA,CACC,CAAA,CACF,CAAA,CAEZ,CAEA,EAAO,UAAY,EACnB,EAAO,KAAO,EACd,EAAO,KAAO"}
@@ -1,2 +1,2 @@
1
- "use client";var e=`button_wYLPJ`,t=`button-notAllowed_xGT-E`,n=`button-disabled_XDm5w`,r=`button-content_Q-0-4`,i=`button-loading_TY6y7`,a=`button-loading-container_pMSta`,o=`button-withIcon_IXEKA`,s=`button-style-filled_gsaJ4`,c=`button-style-light_HBi34`,l=`button-style-outlined_OSfMq`,u=`button-style-plain_CUtTy`,d=`button-background_9Vfo5`,f=`button-size-small_mn6jE`,p=`button-size-medium_gNVDM`,m=`button-size-large_xSIcS`,h=`button-size-icon_pkqwl`,g=`button-ripple_UGtt8`,_={button:e,"button-notAllowed":`button-notAllowed_xGT-E`,buttonNotAllowed:t,"button-disabled":`button-disabled_XDm5w`,buttonDisabled:n,"button-content":`button-content_Q-0-4`,buttonContent:r,"button-loading":`button-loading_TY6y7`,buttonLoading:i,"button-loading-container":`button-loading-container_pMSta`,buttonLoadingContainer:a,"button-withIcon":`button-withIcon_IXEKA`,buttonWithIcon:o,"button-style-filled":`button-style-filled_gsaJ4`,buttonStyleFilled:s,"button-style-light":`button-style-light_HBi34`,buttonStyleLight:c,"button-style-outlined":`button-style-outlined_OSfMq`,buttonStyleOutlined:l,"button-style-plain":`button-style-plain_CUtTy`,buttonStylePlain:u,"button-background":`button-background_9Vfo5`,buttonBackground:d,"button-size-small":`button-size-small_mn6jE`,buttonSizeSmall:f,"button-size-medium":`button-size-medium_gNVDM`,buttonSizeMedium:p,"button-size-large":`button-size-large_xSIcS`,buttonSizeLarge:m,"button-size-icon":`button-size-icon_pkqwl`,buttonSizeIcon:h,"button-ripple":`button-ripple_UGtt8`,buttonRipple:g};export{e as button,d as buttonBackground,r as buttonContent,n as buttonDisabled,i as buttonLoading,a as buttonLoadingContainer,t as buttonNotAllowed,g as buttonRipple,h as buttonSizeIcon,m as buttonSizeLarge,p as buttonSizeMedium,f as buttonSizeSmall,s as buttonStyleFilled,c as buttonStyleLight,l as buttonStyleOutlined,u as buttonStylePlain,o as buttonWithIcon,_ as default};
1
+ "use client";var e=`button_wYLPJ`,t=`button-notAllowed_xGT-E`,n=`button-disabled_XDm5w`,r=`button-content_Q-0-4`,i=`button-loading_TY6y7`,a=`button-loading-container_pMSta`,o=`button-withIcon_IXEKA`,s=`button-shortcut_0bg8S`,c=`button-style-filled_gsaJ4`,l=`button-style-light_HBi34`,u=`button-style-outlined_OSfMq`,d=`button-style-plain_CUtTy`,f=`button-background_9Vfo5`,p=`button-size-small_mn6jE`,m=`button-size-medium_gNVDM`,h=`button-size-large_xSIcS`,g=`button-size-icon_pkqwl`,_=`button-ripple_UGtt8`,v={button:e,"button-notAllowed":`button-notAllowed_xGT-E`,buttonNotAllowed:t,"button-disabled":`button-disabled_XDm5w`,buttonDisabled:n,"button-content":`button-content_Q-0-4`,buttonContent:r,"button-loading":`button-loading_TY6y7`,buttonLoading:i,"button-loading-container":`button-loading-container_pMSta`,buttonLoadingContainer:a,"button-withIcon":`button-withIcon_IXEKA`,buttonWithIcon:o,"button-shortcut":`button-shortcut_0bg8S`,buttonShortcut:s,"button-style-filled":`button-style-filled_gsaJ4`,buttonStyleFilled:c,"button-style-light":`button-style-light_HBi34`,buttonStyleLight:l,"button-style-outlined":`button-style-outlined_OSfMq`,buttonStyleOutlined:u,"button-style-plain":`button-style-plain_CUtTy`,buttonStylePlain:d,"button-background":`button-background_9Vfo5`,buttonBackground:f,"button-size-small":`button-size-small_mn6jE`,buttonSizeSmall:p,"button-size-medium":`button-size-medium_gNVDM`,buttonSizeMedium:m,"button-size-large":`button-size-large_xSIcS`,buttonSizeLarge:h,"button-size-icon":`button-size-icon_pkqwl`,buttonSizeIcon:g,"button-ripple":`button-ripple_UGtt8`,buttonRipple:_};export{e as button,f as buttonBackground,r as buttonContent,n as buttonDisabled,i as buttonLoading,a as buttonLoadingContainer,t as buttonNotAllowed,_ as buttonRipple,s as buttonShortcut,g as buttonSizeIcon,h as buttonSizeLarge,m as buttonSizeMedium,p as buttonSizeSmall,c as buttonStyleFilled,l as buttonStyleLight,u as buttonStyleOutlined,d as buttonStylePlain,o as buttonWithIcon,v as default};
2
2
  //# sourceMappingURL=button.module.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"button.module.mjs","names":[],"sources":["../../../../package/common/button/button.module.pcss"],"sourcesContent":[".button {\n appearance: none;\n -webkit-appearance: none;\n box-sizing: border-box;\n font-size: var(--default-font-size);\n background: transparent;\n border: 1px solid transparent;\n padding: 0;\n font-family: var(--default-font);\n border-radius: var(--default-border-radius);\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n position: relative;\n overflow: hidden;\n min-width: min-content;\n\n /* Hover and press layer in currentcolor, so one rule fits every style. On the root, which a\n stretched button's inner element would not fill; under the Ripple wrapper, which comes later. */\n &::before {\n content: '';\n position: absolute;\n inset: 0;\n background: currentcolor;\n opacity: 0;\n transition: opacity 0.15s;\n pointer-events: none;\n }\n\n &-notAllowed {\n cursor: not-allowed;\n }\n\n /* Disabled only, not loading: a faded spinner would read as disabled rather than working. */\n &-disabled {\n opacity: 0.5;\n }\n\n /* Inside the edge: outside, a ButtonGroup neighbour with the same fill swallows the ring. A\n Filled button draws it in its text colour instead, as Pagination's active item does. */\n &:focus-visible {\n outline: 2px solid var(--focus-border-color, var(--primary-color));\n outline-offset: -4px;\n }\n\n /* (hover: hover) because on touch a tap latches :hover until the next tap elsewhere. */\n @media (hover: hover) {\n &:hover:not(:disabled)::before {\n opacity: 0.08;\n }\n }\n\n /* After the hover rule, which it ties on specificity: a press is a hover too. */\n &:active:not(:disabled)::before {\n opacity: 0.16;\n }\n\n &-content {\n position: relative;\n white-space: break-spaces;\n }\n\n &-loading {\n visibility: hidden;\n\n &-container {\n position: absolute;\n width: 100%;\n height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n }\n\n &-withIcon {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 0.5em;\n }\n\n &-style {\n &-filled {\n background: var(--tone-fill);\n\n /* Must follow the shorthand above, which resets background-image. */\n background-image: var(--tone-gradient);\n color: var(--tone-contrast);\n\n /* After the base :focus-visible rule, which it ties on specificity. */\n &:focus-visible {\n outline-color: currentcolor;\n }\n }\n\n &-light {\n background: var(--tone-light);\n color: var(--tone-text);\n }\n\n &-outlined {\n color: var(--tone-text);\n border-color: var(--tone-outline);\n }\n\n &-plain {\n color: var(--tone-text);\n }\n }\n\n &-background {\n display: block;\n padding: 0.5em 0.75em;\n }\n\n /* Multiples of the theme's base, not em: an em on font-size resolves against the parent,\n so these would follow whatever the button sits in rather than the theme. */\n &-size {\n &-small {\n font-size: calc(var(--default-font-size) * 0.75);\n }\n\n &-medium {\n font-size: var(--default-font-size);\n }\n\n &-large {\n font-size: calc(var(--default-font-size) * 1.25);\n }\n\n /* Square, padding-free button for a lone icon; the caller sets the box size. */\n &-icon {\n display: inline-flex;\n\n /* Inherits, unlike the other sizes: a hardcoded px value would have to be out-specified\n from another stylesheet, a specificity tie decided only by bundle order. */\n font-size: inherit;\n\n .button-ripple,\n .button-background,\n .button-content {\n display: flex;\n flex: 1;\n align-items: center;\n justify-content: center;\n padding: 0;\n }\n }\n }\n}\n"],"mappings":""}
1
+ {"version":3,"file":"button.module.mjs","names":[],"sources":["../../../../package/common/button/button.module.pcss"],"sourcesContent":[".button {\n appearance: none;\n -webkit-appearance: none;\n box-sizing: border-box;\n font-size: var(--default-font-size);\n background: transparent;\n border: 1px solid transparent;\n padding: 0;\n font-family: var(--default-font);\n border-radius: var(--default-border-radius);\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n position: relative;\n overflow: hidden;\n min-width: min-content;\n\n /* Hover and press layer in currentcolor, so one rule fits every style. On the root, which a\n stretched button's inner element would not fill; under the Ripple wrapper, which comes later. */\n &::before {\n content: '';\n position: absolute;\n inset: 0;\n background: currentcolor;\n opacity: 0;\n transition: opacity 0.15s;\n pointer-events: none;\n }\n\n &-notAllowed {\n cursor: not-allowed;\n }\n\n /* Disabled only, not loading: a faded spinner would read as disabled rather than working. */\n &-disabled {\n opacity: 0.5;\n }\n\n /* Inside the edge: outside, a ButtonGroup neighbour with the same fill swallows the ring. A\n Filled button draws it in its text colour instead, as Pagination's active item does. */\n &:focus-visible {\n outline: 2px solid var(--focus-border-color, var(--primary-color));\n outline-offset: -4px;\n }\n\n /* (hover: hover) because on touch a tap latches :hover until the next tap elsewhere. */\n @media (hover: hover) {\n &:hover:not(:disabled)::before {\n opacity: 0.08;\n }\n }\n\n /* After the hover rule, which it ties on specificity: a press is a hover too. */\n &:active:not(:disabled)::before {\n opacity: 0.16;\n }\n\n &-content {\n position: relative;\n white-space: break-spaces;\n }\n\n &-loading {\n visibility: hidden;\n\n &-container {\n position: absolute;\n width: 100%;\n height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n }\n\n &-withIcon {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 0.5em;\n }\n\n /* A pill, because plain text right after the label reads as part of it. On the label's\n baseline rather than centred, so it follows the label wherever the font puts it. */\n &-shortcut {\n /* Not inline: Chrome trims only block containers, see the @supports below. */\n display: inline-block;\n\n /* Negative margins cancel the vertical padding in layout, so the button is no taller. */\n margin: -0.2em 0 -0.2em 0.6em;\n padding: 0.2em 0.6em;\n border-radius: 99999px;\n background: color-mix(in srgb, currentcolor 15%, transparent);\n font-size: 0.85em;\n\n /* Smaller text on a shared baseline sits lower. Raised so its capitals centre on the label's:\n (1 - 0.85) / 2 x ~0.72 cap height / 0.85. */\n vertical-align: 0.065em;\n white-space: nowrap;\n\n /* Even padding around the letters. Untrimmed, engines round the font's ascent and descent\n differently at each size, and the letters sat up to 2px off-centre. */\n @supports (text-box: trim-both cap alphabetic) {\n margin-block: -0.4em;\n padding-block: 0.4em;\n text-box: trim-both cap alphabetic;\n }\n\n /* A touch screen usually has no keyboard to press it on. */\n @media (pointer: coarse) {\n display: none;\n }\n }\n\n &-style {\n &-filled {\n background: var(--tone-fill);\n\n /* Must follow the shorthand above, which resets background-image. */\n background-image: var(--tone-gradient);\n color: var(--tone-contrast);\n\n /* After the base :focus-visible rule, which it ties on specificity. */\n &:focus-visible {\n outline-color: currentcolor;\n }\n }\n\n &-light {\n background: var(--tone-light);\n color: var(--tone-text);\n }\n\n &-outlined {\n color: var(--tone-text);\n border-color: var(--tone-outline);\n }\n\n &-plain {\n color: var(--tone-text);\n }\n }\n\n &-background {\n display: block;\n padding: 0.5em 0.75em;\n }\n\n /* Multiples of the theme's base, not em: an em on font-size resolves against the parent,\n so these would follow whatever the button sits in rather than the theme. */\n &-size {\n &-small {\n font-size: calc(var(--default-font-size) * 0.75);\n }\n\n &-medium {\n font-size: var(--default-font-size);\n }\n\n &-large {\n font-size: calc(var(--default-font-size) * 1.25);\n }\n\n /* Square, padding-free button for a lone icon; the caller sets the box size. */\n &-icon {\n display: inline-flex;\n\n /* Inherits, unlike the other sizes: a hardcoded px value would have to be out-specified\n from another stylesheet, a specificity tie decided only by bundle order. */\n font-size: inherit;\n\n .button-ripple,\n .button-background,\n .button-content {\n display: flex;\n flex: 1;\n align-items: center;\n justify-content: center;\n padding: 0;\n }\n }\n }\n}\n"],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"button.types.mjs","names":[],"sources":["../../../../package/common/button/button.types.ts"],"sourcesContent":["import type React from 'react';\nimport type { CSSProperties } from 'react';\nimport type { IconContent } from '../types/icon.types';\nimport type { SlotClassNames, SlotStyles, DataProps } from '../types/slots.types';\nimport type { ColorValue } from '../themeProvider/color.types';\n\n/** How much the button stands out. Independent of its colour. */\nexport enum ButtonStyleType {\n /** Solid fill, for the one main action. */\n Filled = 'filled',\n /** A light tint of the colour behind text in the colour. */\n Light = 'light',\n /** Border and text in the colour, no fill. The default. */\n Outlined = 'outlined',\n /** Text in the colour, no fill and no border. For toolbar, icon and close buttons. */\n Plain = 'plain',\n}\n\nexport enum ButtonSize {\n Small = 'small',\n /** The default. */\n Medium = 'medium',\n Large = 'large',\n /** Square, padding-free button for a lone icon. Give it its own width/height. */\n Icon = 'icon',\n}\n\nexport enum ButtonType {\n Button = 'button',\n Reset = 'reset',\n Submit = 'submit',\n}\n\nexport type ButtonSlots = 'background' | 'content' | 'loadingContainer' | 'label';\n\n/**\n * `DataProps` supplies the `data-*` index signature, which `ButtonHTMLAttributes` doesn't cover.\n * `type`, `color` and `onClick` below deliberately narrow their inherited counterparts.\n */\nexport interface ButtonProps extends DataProps, React.ButtonHTMLAttributes<HTMLButtonElement> {\n className?: string;\n style?: CSSProperties;\n type?: ButtonType;\n /** Emphasis: `Filled`, `Light`, `Outlined` (default) or `Plain`. */\n styleType?: ButtonStyleType;\n /** A `ThemeColor` (default `Primary`) or any CSS colour, such as `'var(--brand)'`. */\n color?: ColorValue;\n size?: ButtonSize;\n onClick?: (event: React.MouseEvent<HTMLButtonElement>) => Promise<void> | void;\n children?: React.ReactNode;\n icon?: IconContent;\n isLoading?: boolean;\n disabled?: boolean;\n title?: string;\n ref?: React.Ref<HTMLButtonElement>;\n classNames?: SlotClassNames<ButtonSlots>;\n styles?: SlotStyles<ButtonSlots>;\n}\n"],"mappings":"aAOA,IAAY,EAAL,SAAA,EAAA,OAEL,GAAA,OAAA,SAEA,EAAA,MAAA,QAEA,EAAA,SAAA,WAEA,EAAA,MAAA,SACF,EAAA,CAAA,CAAA,EAEY,EAAL,SAAA,EAAA,OACL,GAAA,MAAA,QAEA,EAAA,OAAA,SACA,EAAA,MAAA,QAEA,EAAA,KAAA,QACF,EAAA,CAAA,CAAA,EAEY,EAAL,SAAA,EAAA,OACL,GAAA,OAAA,SACA,EAAA,MAAA,QACA,EAAA,OAAA,UACF,EAAA,CAAA,CAAA"}
1
+ {"version":3,"file":"button.types.mjs","names":[],"sources":["../../../../package/common/button/button.types.ts"],"sourcesContent":["import type React from 'react';\nimport type { CSSProperties } from 'react';\nimport type { IconContent } from '../types/icon.types';\nimport type { SlotClassNames, SlotStyles, DataProps } from '../types/slots.types';\nimport type { ColorValue } from '../themeProvider/color.types';\n\n/** How much the button stands out. Independent of its colour. */\nexport enum ButtonStyleType {\n /** Solid fill, for the one main action. */\n Filled = 'filled',\n /** A light tint of the colour behind text in the colour. */\n Light = 'light',\n /** Border and text in the colour, no fill. The default. */\n Outlined = 'outlined',\n /** Text in the colour, no fill and no border. For toolbar, icon and close buttons. */\n Plain = 'plain',\n}\n\nexport enum ButtonSize {\n Small = 'small',\n /** The default. */\n Medium = 'medium',\n Large = 'large',\n /** Square, padding-free button for a lone icon. Give it its own width/height. */\n Icon = 'icon',\n}\n\nexport enum ButtonType {\n Button = 'button',\n Reset = 'reset',\n Submit = 'submit',\n}\n\nexport type ButtonSlots = 'background' | 'content' | 'loadingContainer' | 'label' | 'shortcut';\n\n/**\n * `DataProps` supplies the `data-*` index signature, which `ButtonHTMLAttributes` doesn't cover.\n * `type`, `color` and `onClick` below deliberately narrow their inherited counterparts.\n */\nexport interface ButtonProps extends DataProps, React.ButtonHTMLAttributes<HTMLButtonElement> {\n className?: string;\n style?: CSSProperties;\n type?: ButtonType;\n /** Emphasis: `Filled`, `Light`, `Outlined` (default) or `Plain`. */\n styleType?: ButtonStyleType;\n /** A `ThemeColor` (default `Primary`) or any CSS colour, such as `'var(--brand)'`. */\n color?: ColorValue;\n size?: ButtonSize;\n onClick?: (event: React.MouseEvent<HTMLButtonElement>) => Promise<void> | void;\n children?: React.ReactNode;\n icon?: IconContent;\n /**\n * A shortcut drawn after the label, in `useHotkeys` spelling (`'mod+s'`): `⌘S` on Apple\n * platforms, `Ctrl+S` elsewhere. Drawn only; bind it with `useHotkeys`.\n */\n shortcut?: string;\n isLoading?: boolean;\n disabled?: boolean;\n title?: string;\n ref?: React.Ref<HTMLButtonElement>;\n classNames?: SlotClassNames<ButtonSlots>;\n styles?: SlotStyles<ButtonSlots>;\n}\n"],"mappings":"aAOA,IAAY,EAAL,SAAA,EAAA,OAEL,GAAA,OAAA,SAEA,EAAA,MAAA,QAEA,EAAA,SAAA,WAEA,EAAA,MAAA,SACF,EAAA,CAAA,CAAA,EAEY,EAAL,SAAA,EAAA,OACL,GAAA,MAAA,QAEA,EAAA,OAAA,SACA,EAAA,MAAA,QAEA,EAAA,KAAA,QACF,EAAA,CAAA,CAAA,EAEY,EAAL,SAAA,EAAA,OACL,GAAA,OAAA,SACA,EAAA,MAAA,QACA,EAAA,OAAA,UACF,EAAA,CAAA,CAAA"}
@@ -1,2 +1,2 @@
1
- "use client";import{useComponentDefaults as e}from"../configProvider/useComponentDefaults.mjs";import{useLabels as t}from"../configProvider/useLabels.mjs";import{renderIcon as n}from"../utils/renderIcon.mjs";import r from"../modal/modal.mjs";import{formatHotkey as ee}from"../hooks/useHotkeys.mjs";import{edgeIndex as i,nextIndex as a,scrollOptionIntoView as o}from"../utils/listNavigation.mjs";import{filterCommands as s,groupCommands as c}from"./filterCommands.mjs";import l from"./commandPalette.module.mjs";import{useEffect as u,useId as te,useRef as d,useState as f}from"react";import p from"classnames";import{faMagnifyingGlass as m}from"@fortawesome/free-solid-svg-icons";import{jsx as h,jsxs as g}from"react/jsx-runtime";var _={label:`Search commands`,placeholder:`Type a command or search…`,empty:`No results`,loading:`Searching…`,results:e=>e===1?`1 result`:`${e} results`};function v(v){let{items:y,isOpen:b,onClose:x,onAction:S,onQueryChange:C,filter:w=!0,isLoading:T=!1,presentation:E,labels:D,className:O,style:k,classNames:A,styles:j,...ne}=e(`CommandPalette`,v),M=t(`CommandPalette`,D,_),N=te(),P=d(null),[F,I]=f(``),[L,R]=f(null),[z,B]=f(b);b!==z&&(B(b),b&&(I(``),R(null)));let V=d(C);u(()=>{V.current=C}),u(()=>{b&&V.current?.(``)},[b]);let H=w?s(c(y),F):c(y),U=H.flatMap(e=>e.actions),W=e=>!!U[e]?.disabled,G=U.findIndex(e=>e.id===L&&!e.disabled),K=G>=0?G:i(`first`,U.length,{isDisabled:W}),q=e=>`${N}-${encodeURIComponent(e.id)}`,J=K===null?void 0:q(U[K]);u(()=>{J&&o(P.current,document.getElementById(J))},[J]);function Y(e){e&&!e.disabled&&(e.onClick?.(),S?.(e.id),x())}function X(e){if(!e.nativeEvent.isComposing){if(e.key===`ArrowDown`||e.key===`ArrowUp`){let t=a(K,e.key===`ArrowDown`?1:-1,U.length,{isDisabled:W});R(t===null?null:U[t].id)}else if(e.key===`Enter`)Y(K===null?void 0:U[K]);else return;e.preventDefault()}}let Z=F.trim()!==``,Q=T?M.loading:M.empty,$=-1;return g(r,{...ne,"aria-label":M.label,isOpen:b,onClose:x,hideHeader:!0,presentation:E,className:p(l.commandPalette,O,A?.root),style:{...k,...j?.root},children:[g(`div`,{className:p(l.commandPaletteSearch,A?.search),style:j?.search,children:[n(m,{className:l.commandPaletteSearchIcon}),h(`input`,{type:`text`,role:`combobox`,"aria-label":M.label,"aria-expanded":`true`,"aria-controls":N,"aria-autocomplete":`list`,"aria-activedescendant":J,autoComplete:`off`,spellCheck:!1,placeholder:M.placeholder,value:F,onChange:e=>{I(e.target.value),R(null),C?.(e.target.value)},onKeyDown:X,className:p(l.commandPaletteInput,A?.input),style:j?.input})]}),h(`div`,{ref:P,id:N,role:`listbox`,"aria-label":M.label,className:p(l.commandPaletteList,A?.list),style:j?.list,children:H.map((e,t)=>{let r=`${N}-heading-${t}`;return g(`div`,{role:e.heading===void 0?`presentation`:`group`,"aria-labelledby":e.heading===void 0?void 0:r,children:[e.separated&&t>0&&h(`div`,{"aria-hidden":!0,className:p(l.commandPaletteSeparator,A?.separator),style:j?.separator}),e.heading!==void 0&&h(`div`,{id:r,role:`presentation`,className:p(l.commandPaletteHeading,A?.heading),style:j?.heading,children:e.heading}),e.actions.map(e=>{$+=1;let t=$;return g(`div`,{id:q(e),role:`option`,"aria-selected":t===K,"aria-disabled":e.disabled||void 0,className:p(l.commandPaletteItem,t===K&&l.commandPaletteItemActive,e.danger&&l.commandPaletteItemDanger,e.disabled&&l.commandPaletteItemDisabled,A?.item),style:j?.item,onMouseDown:e=>e.preventDefault(),onMouseMove:()=>!e.disabled&&R(e.id),onClick:()=>Y(e),children:[e.icon!=null&&n(e.icon,{className:p(l.commandPaletteItemIcon,A?.itemIcon),style:j?.itemIcon}),h(`span`,{className:p(l.commandPaletteItemLabel,A?.itemLabel),style:j?.itemLabel,children:e.label}),e.shortcut!=null&&h(`span`,{className:p(l.commandPaletteItemShortcut,A?.itemShortcut),style:j?.itemShortcut,children:ee(e.shortcut)})]},e.id)})]},t)})}),Z&&U.length===0&&h(`div`,{className:p(l.commandPaletteEmpty,A?.empty),style:j?.empty,children:Q}),h(`span`,{className:l.commandPaletteStatus,role:`status`,"aria-live":`polite`,children:Z?U.length>0?M.results(U.length):Q:``})]})}export{v as default};
1
+ "use client";import{useComponentDefaults as e}from"../configProvider/useComponentDefaults.mjs";import{useLabels as t}from"../configProvider/useLabels.mjs";import{renderIcon as n}from"../utils/renderIcon.mjs";import{ariaKeyshortcuts as r,formatHotkey as i}from"../hooks/useHotkeys.mjs";import a from"../modal/modal.mjs";import{edgeIndex as o,nextIndex as ee,scrollOptionIntoView as s}from"../utils/listNavigation.mjs";import{hasAnyIcon as c,renderItemIcon as te,runMenuAction as l}from"../menu/menuItem.mjs";import{filterCommands as ne,groupCommands as u}from"./filterCommands.mjs";import d from"./commandPalette.module.mjs";import{useEffect as f,useId as p,useRef as m,useState as h}from"react";import g from"classnames";import{faMagnifyingGlass as re}from"@fortawesome/free-solid-svg-icons";import{jsx as _,jsxs as v}from"react/jsx-runtime";var y={label:`Search commands`,placeholder:`Type a command or search…`,empty:`No results`,loading:`Searching…`,results:e=>e===1?`1 result`:`${e} results`};function b(b){let{items:x,isOpen:S,onClose:C,onAction:w,onQueryChange:T,filter:E=!0,isLoading:D=!1,presentation:O,labels:ie,className:ae,style:oe,classNames:k,styles:A,...j}=e(`CommandPalette`,b),M=t(`CommandPalette`,ie,y),N=p(),P=m(null),[F,I]=h(``),[L,R]=h(null),[z,B]=h(S);S!==z&&(B(S),S&&(I(``),R(null)));let V=m(T);f(()=>{V.current=T}),f(()=>{S&&V.current?.(``)},[S]);let H=E?ne(u(x),F):u(x),U=c(x),W=H.flatMap(e=>e.actions),G=e=>!!W[e]?.disabled,K=W.findIndex(e=>e.id===L&&!e.disabled),q=K>=0?K:o(`first`,W.length,{isDisabled:G}),J=e=>`${N}-${encodeURIComponent(e.id)}`,Y=q===null?void 0:J(W[q]);f(()=>{Y&&s(P.current,document.getElementById(Y))},[Y]);function X(e){e&&!e.disabled&&(l(e,w),C())}function se(e){if(!e.nativeEvent.isComposing){if(e.key===`ArrowDown`||e.key===`ArrowUp`){let t=ee(q,e.key===`ArrowDown`?1:-1,W.length,{isDisabled:G});R(t===null?null:W[t].id)}else if(e.key===`Enter`)X(q===null?void 0:W[q]);else return;e.preventDefault()}}let Z=F.trim()!==``,Q=D?M.loading:M.empty,$=-1;return v(a,{...j,"aria-label":M.label,isOpen:S,onClose:C,hideHeader:!0,presentation:O,className:g(d.commandPalette,ae,k?.root),style:{...oe,...A?.root},children:[v(`div`,{className:g(d.commandPaletteSearch,k?.search),style:A?.search,children:[n(re,{className:d.commandPaletteSearchIcon}),_(`input`,{type:`text`,role:`combobox`,"aria-label":M.label,"aria-expanded":`true`,"aria-controls":N,"aria-autocomplete":`list`,"aria-activedescendant":Y,autoComplete:`off`,spellCheck:!1,placeholder:M.placeholder,value:F,onChange:e=>{I(e.target.value),R(null),T?.(e.target.value)},onKeyDown:se,className:g(d.commandPaletteInput,k?.input),style:A?.input})]}),_(`div`,{ref:P,id:N,role:`listbox`,"aria-label":M.label,className:g(d.commandPaletteList,k?.list),style:A?.list,children:H.map((e,t)=>{let n=`${N}-heading-${t}`;return v(`div`,{role:e.heading===void 0?`presentation`:`group`,"aria-labelledby":e.heading===void 0?void 0:n,children:[e.separated&&t>0&&_(`div`,{"aria-hidden":!0,className:g(d.commandPaletteSeparator,k?.separator),style:A?.separator}),e.heading!==void 0&&_(`div`,{id:n,role:`presentation`,className:g(d.commandPaletteHeading,k?.heading),style:A?.heading,children:e.heading}),e.actions.map(e=>{$+=1;let t=$;return v(`div`,{id:J(e),role:`option`,"aria-selected":t===q,"aria-disabled":e.disabled||void 0,"aria-keyshortcuts":r(e.shortcut),className:g(d.commandPaletteItem,t===q&&d.commandPaletteItemActive,e.danger&&d.commandPaletteItemDanger,e.disabled&&d.commandPaletteItemDisabled,k?.item),style:A?.item,onMouseDown:e=>e.preventDefault(),onMouseMove:()=>!e.disabled&&R(e.id),onClick:()=>X(e),children:[te(e.icon,U,{className:g(d.commandPaletteItemIcon,k?.itemIcon),style:A?.itemIcon}),_(`span`,{className:g(d.commandPaletteItemLabel,k?.itemLabel),style:A?.itemLabel,children:e.label}),e.shortcut!=null&&_(`span`,{"aria-hidden":!0,className:g(d.commandPaletteItemShortcut,k?.itemShortcut),style:A?.itemShortcut,children:i(e.shortcut)})]},e.id)})]},t)})}),Z&&W.length===0&&_(`div`,{className:g(d.commandPaletteEmpty,k?.empty),style:A?.empty,children:Q}),_(`span`,{className:d.commandPaletteStatus,role:`status`,"aria-live":`polite`,children:Z?W.length>0?M.results(W.length):Q:``})]})}export{b as default};
2
2
  //# sourceMappingURL=commandPalette.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"commandPalette.mjs","names":[],"sources":["../../../../package/common/commandPalette/commandPalette.tsx"],"sourcesContent":["import { useEffect, useId, useRef, useState, type KeyboardEvent, type ReactElement } from 'react';\nimport cx from 'classnames';\nimport { faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons';\n\nimport Modal from '../modal';\nimport { useComponentDefaults, useLabels } from '../configProvider';\nimport { formatHotkey } from '../hooks/useHotkeys';\nimport { edgeIndex, nextIndex, scrollOptionIntoView } from '../utils/listNavigation';\nimport { renderIcon } from '../utils/renderIcon';\nimport type { MenuAction } from '../menu/menu.types';\nimport { filterCommands, groupCommands } from './filterCommands';\nimport type { CommandPaletteLabels, CommandPaletteProps } from './commandPalette.types';\nimport styles from './commandPalette.module.pcss';\n\nconst DEFAULT_LABELS: Required<CommandPaletteLabels> = {\n label: 'Search commands',\n placeholder: 'Type a command or search…',\n empty: 'No results',\n loading: 'Searching…',\n results: (count) => (count === 1 ? '1 result' : `${count} results`),\n};\n\nfunction CommandPalette(props: CommandPaletteProps): ReactElement {\n const {\n items,\n isOpen,\n onClose,\n onAction,\n onQueryChange,\n filter = true,\n isLoading = false,\n presentation,\n labels,\n className,\n style,\n classNames,\n styles: slotStyles,\n ...rest\n } = useComponentDefaults('CommandPalette', props);\n const text = useLabels('CommandPalette', labels, DEFAULT_LABELS);\n const listId = useId();\n const listRef = useRef<HTMLDivElement>(null);\n\n const [query, setQuery] = useState('');\n // An id rather than an index, so the highlight stays on the same command while server results\n // are replaced around it. Typing clears it instead, so Enter runs the best match.\n const [activeId, setActiveId] = useState<string | null>(null);\n const [wasOpen, setWasOpen] = useState(isOpen);\n if (isOpen !== wasOpen) {\n setWasOpen(isOpen);\n if (isOpen) {\n setQuery('');\n setActiveId(null);\n }\n }\n\n // Read through a ref so an inline handler does not re-fire the effect on every parent render.\n const onQueryChangeRef = useRef(onQueryChange);\n useEffect(() => {\n onQueryChangeRef.current = onQueryChange;\n });\n useEffect(() => {\n if (isOpen) onQueryChangeRef.current?.('');\n }, [isOpen]);\n\n const groups = filter ? filterCommands(groupCommands(items), query) : groupCommands(items);\n const actions = groups.flatMap((group) => group.actions);\n const isDisabled = (index: number) => !!actions[index]?.disabled;\n const found = actions.findIndex((action) => action.id === activeId && !action.disabled);\n const activeIndex = found >= 0 ? found : edgeIndex('first', actions.length, { isDisabled });\n // From the command's id, not its position: an id that stayed the same while the option under it\n // changed would not make a screen reader announce the new highlight.\n const optionId = (action: MenuAction) => `${listId}-${encodeURIComponent(action.id)}`;\n const activeOptionId = activeIndex === null ? undefined : optionId(actions[activeIndex]);\n\n useEffect(() => {\n if (!activeOptionId) return;\n scrollOptionIntoView(listRef.current, document.getElementById(activeOptionId));\n }, [activeOptionId]);\n\n function run(action: MenuAction | undefined) {\n if (!action || action.disabled) return;\n action.onClick?.();\n onAction?.(action.id);\n onClose();\n }\n\n function handleKeyDown(event: KeyboardEvent<HTMLInputElement>) {\n // Enter confirms an IME composition before it means anything here.\n if (event.nativeEvent.isComposing) return;\n if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {\n const next = nextIndex(activeIndex, event.key === 'ArrowDown' ? 1 : -1, actions.length, { isDisabled });\n setActiveId(next === null ? null : actions[next].id);\n } else if (event.key === 'Enter') {\n run(activeIndex === null ? undefined : actions[activeIndex]);\n } else {\n return;\n }\n event.preventDefault();\n }\n\n const hasQuery = query.trim() !== '';\n const emptyText = isLoading ? text.loading : text.empty;\n let index = -1;\n\n return (\n <Modal\n {...rest}\n aria-label={text.label}\n isOpen={isOpen}\n onClose={onClose}\n hideHeader\n presentation={presentation}\n className={cx(styles.commandPalette, className, classNames?.root)}\n style={{ ...style, ...slotStyles?.root }}\n >\n <div className={cx(styles.commandPaletteSearch, classNames?.search)} style={slotStyles?.search}>\n {renderIcon(faMagnifyingGlass, { className: styles.commandPaletteSearchIcon })}\n <input\n type='text'\n role='combobox'\n aria-label={text.label}\n aria-expanded='true'\n aria-controls={listId}\n aria-autocomplete='list'\n aria-activedescendant={activeOptionId}\n autoComplete='off'\n spellCheck={false}\n placeholder={text.placeholder}\n value={query}\n onChange={(event) => {\n setQuery(event.target.value);\n setActiveId(null);\n onQueryChange?.(event.target.value);\n }}\n onKeyDown={handleKeyDown}\n className={cx(styles.commandPaletteInput, classNames?.input)}\n style={slotStyles?.input}\n />\n </div>\n\n <div\n ref={listRef}\n id={listId}\n role='listbox'\n aria-label={text.label}\n className={cx(styles.commandPaletteList, classNames?.list)}\n style={slotStyles?.list}\n >\n {groups.map((group, groupIndex) => {\n const headingId = `${listId}-heading-${groupIndex}`;\n return (\n <div\n // Groups carry no id of their own; a new query re-renders the list wholesale anyway.\n key={groupIndex}\n role={group.heading === undefined ? 'presentation' : 'group'}\n aria-labelledby={group.heading === undefined ? undefined : headingId}\n >\n {group.separated && groupIndex > 0 && (\n <div\n aria-hidden\n className={cx(styles.commandPaletteSeparator, classNames?.separator)}\n style={slotStyles?.separator}\n />\n )}\n {group.heading !== undefined && (\n <div\n id={headingId}\n role='presentation'\n className={cx(styles.commandPaletteHeading, classNames?.heading)}\n style={slotStyles?.heading}\n >\n {group.heading}\n </div>\n )}\n {group.actions.map((action) => {\n index += 1;\n const own = index;\n return (\n <div\n key={action.id}\n id={optionId(action)}\n role='option'\n aria-selected={own === activeIndex}\n aria-disabled={action.disabled || undefined}\n className={cx(\n styles.commandPaletteItem,\n own === activeIndex && styles.commandPaletteItemActive,\n action.danger && styles.commandPaletteItemDanger,\n action.disabled && styles.commandPaletteItemDisabled,\n classNames?.item,\n )}\n style={slotStyles?.item}\n // Keeps the focus, and so the caret, in the search field.\n onMouseDown={(event) => event.preventDefault()}\n // Move, not enter: a list scrolling under a resting pointer must not steal the highlight.\n onMouseMove={() => !action.disabled && setActiveId(action.id)}\n onClick={() => run(action)}\n >\n {action.icon != null &&\n renderIcon(action.icon, {\n className: cx(styles.commandPaletteItemIcon, classNames?.itemIcon),\n style: slotStyles?.itemIcon,\n })}\n <span\n className={cx(styles.commandPaletteItemLabel, classNames?.itemLabel)}\n style={slotStyles?.itemLabel}\n >\n {action.label}\n </span>\n {action.shortcut != null && (\n <span\n className={cx(styles.commandPaletteItemShortcut, classNames?.itemShortcut)}\n style={slotStyles?.itemShortcut}\n >\n {formatHotkey(action.shortcut)}\n </span>\n )}\n </div>\n );\n })}\n </div>\n );\n })}\n </div>\n\n {hasQuery && actions.length === 0 && (\n <div className={cx(styles.commandPaletteEmpty, classNames?.empty)} style={slotStyles?.empty}>\n {emptyText}\n </div>\n )}\n <span className={styles.commandPaletteStatus} role='status' aria-live='polite'>\n {hasQuery ? (actions.length > 0 ? text.results(actions.length) : emptyText) : ''}\n </span>\n </Modal>\n );\n}\n\nexport default CommandPalette;\n"],"mappings":"ytBAcA,IAAM,EAAiD,CACrD,MAAO,kBACP,YAAa,4BACb,MAAO,aACP,QAAS,aACT,QAAU,GAAW,IAAU,EAAI,WAAa,GAAG,EAAM,SAC3D,EAEA,SAAS,EAAe,EAA0C,CAChE,GAAM,CACJ,QACA,SACA,UACA,WACA,gBACA,SAAS,GACT,YAAY,GACZ,eACA,SACA,YACA,QACA,aACA,OAAQ,EACR,GAAG,IACD,EAAqB,iBAAkB,CAAK,EAC1C,EAAO,EAAU,iBAAkB,EAAQ,CAAc,EACzD,EAAS,GAAM,EACf,EAAU,EAAuB,IAAI,EAErC,CAAC,EAAO,GAAY,EAAS,EAAE,EAG/B,CAAC,EAAU,GAAe,EAAwB,IAAI,EACtD,CAAC,EAAS,GAAc,EAAS,CAAM,EACzC,IAAW,IACb,EAAW,CAAM,EACb,IACF,EAAS,EAAE,EACX,EAAY,IAAI,IAKpB,IAAM,EAAmB,EAAO,CAAa,EAC7C,MAAgB,CACd,EAAiB,QAAU,CAC7B,CAAC,EACD,MAAgB,CACV,GAAQ,EAAiB,UAAU,EAAE,CAC3C,EAAG,CAAC,CAAM,CAAC,EAEX,IAAM,EAAS,EAAS,EAAe,EAAc,CAAK,EAAG,CAAK,EAAI,EAAc,CAAK,EACnF,EAAU,EAAO,QAAS,GAAU,EAAM,OAAO,EACjD,EAAc,GAAkB,CAAC,CAAC,EAAQ,EAAM,EAAE,SAClD,EAAQ,EAAQ,UAAW,GAAW,EAAO,KAAO,GAAY,CAAC,EAAO,QAAQ,EAChF,EAAc,GAAS,EAAI,EAAQ,EAAU,QAAS,EAAQ,OAAQ,CAAE,YAAW,CAAC,EAGpF,EAAY,GAAuB,GAAG,EAAO,GAAG,mBAAmB,EAAO,EAAE,IAC5E,EAAiB,IAAgB,KAAO,IAAA,GAAY,EAAS,EAAQ,EAAY,EAEvF,MAAgB,CACT,GACL,EAAqB,EAAQ,QAAS,SAAS,eAAe,CAAc,CAAC,CAC/E,EAAG,CAAC,CAAc,CAAC,EAEnB,SAAS,EAAI,EAAgC,CACtC,GAAU,GAAO,WACtB,EAAO,UAAU,EACjB,IAAW,EAAO,EAAE,EACpB,EAAQ,EACV,CAEA,SAAS,EAAc,EAAwC,CAEzD,MAAM,YAAY,YACtB,IAAI,EAAM,MAAQ,aAAe,EAAM,MAAQ,UAAW,CACxD,IAAM,EAAO,EAAU,EAAa,EAAM,MAAQ,YAAc,EAAI,GAAI,EAAQ,OAAQ,CAAE,YAAW,CAAC,EACtG,EAAY,IAAS,KAAO,KAAO,EAAQ,EAAK,CAAC,EAAE,CACrD,MAAO,GAAI,EAAM,MAAQ,QACvB,EAAI,IAAgB,KAAO,IAAA,GAAY,EAAQ,EAAY,OAE3D,OAEF,EAAM,eAAe,CAFnB,CAGJ,CAEA,IAAM,EAAW,EAAM,KAAK,IAAM,GAC5B,EAAY,EAAY,EAAK,QAAU,EAAK,MAC9C,EAAQ,GAEZ,OACE,EAAC,EAAD,CACE,GAAI,GACJ,aAAY,EAAK,MACT,SACC,UACT,WAAA,GACc,eACd,UAAW,EAAG,EAAO,eAAgB,EAAW,GAAY,IAAI,EAChE,MAAO,CAAE,GAAG,EAAO,GAAG,GAAY,IAAK,EARzC,SAAA,CAUE,EAAC,MAAD,CAAK,UAAW,EAAG,EAAO,qBAAsB,GAAY,MAAM,EAAG,MAAO,GAAY,OAAxF,SAAA,CACG,EAAW,EAAmB,CAAE,UAAW,EAAO,wBAAyB,CAAC,EAC7E,EAAC,QAAD,CACE,KAAK,OACL,KAAK,WACL,aAAY,EAAK,MACjB,gBAAc,OACd,gBAAe,EACf,oBAAkB,OAClB,wBAAuB,EACvB,aAAa,MACb,WAAY,GACZ,YAAa,EAAK,YAClB,MAAO,EACP,SAAW,GAAU,CACnB,EAAS,EAAM,OAAO,KAAK,EAC3B,EAAY,IAAI,EAChB,IAAgB,EAAM,OAAO,KAAK,CACpC,EACA,UAAW,EACX,UAAW,EAAG,EAAO,oBAAqB,GAAY,KAAK,EAC3D,MAAO,GAAY,KACpB,CAAA,CACE,IAEL,EAAC,MAAD,CACE,IAAK,EACL,GAAI,EACJ,KAAK,UACL,aAAY,EAAK,MACjB,UAAW,EAAG,EAAO,mBAAoB,GAAY,IAAI,EACzD,MAAO,GAAY,KAElB,SAAA,EAAO,KAAK,EAAO,IAAe,CACjC,IAAM,EAAY,GAAG,EAAO,WAAW,IACvC,OACE,EAAC,MAAD,CAGE,KAAM,EAAM,UAAY,IAAA,GAAY,eAAiB,QACrD,kBAAiB,EAAM,UAAY,IAAA,GAAY,IAAA,GAAY,EAJ7D,SAAA,CAMG,EAAM,WAAa,EAAa,GAC/B,EAAC,MAAD,CACE,cAAA,GACA,UAAW,EAAG,EAAO,wBAAyB,GAAY,SAAS,EACnE,MAAO,GAAY,SACpB,CAAA,EAEF,EAAM,UAAY,IAAA,IACjB,EAAC,MAAD,CACE,GAAI,EACJ,KAAK,eACL,UAAW,EAAG,EAAO,sBAAuB,GAAY,OAAO,EAC/D,MAAO,GAAY,QAElB,SAAA,EAAM,OACJ,CAAA,EAEN,EAAM,QAAQ,IAAK,GAAW,CAC7B,GAAS,EACT,IAAM,EAAM,EACZ,OACE,EAAC,MAAD,CAEE,GAAI,EAAS,CAAM,EACnB,KAAK,SACL,gBAAe,IAAQ,EACvB,gBAAe,EAAO,UAAY,IAAA,GAClC,UAAW,EACT,EAAO,mBACP,IAAQ,GAAe,EAAO,yBAC9B,EAAO,QAAU,EAAO,yBACxB,EAAO,UAAY,EAAO,2BAC1B,GAAY,IACd,EACA,MAAO,GAAY,KAEnB,YAAc,GAAU,EAAM,eAAe,EAE7C,gBAAmB,CAAC,EAAO,UAAY,EAAY,EAAO,EAAE,EAC5D,YAAe,EAAI,CAAM,EAlB3B,SAAA,CAoBG,EAAO,MAAQ,MACd,EAAW,EAAO,KAAM,CACtB,UAAW,EAAG,EAAO,uBAAwB,GAAY,QAAQ,EACjE,MAAO,GAAY,QACrB,CAAC,EACH,EAAC,OAAD,CACE,UAAW,EAAG,EAAO,wBAAyB,GAAY,SAAS,EACnE,MAAO,GAAY,UAElB,SAAA,EAAO,KACJ,CAAA,EACL,EAAO,UAAY,MAClB,EAAC,OAAD,CACE,UAAW,EAAG,EAAO,2BAA4B,GAAY,YAAY,EACzE,MAAO,GAAY,aAElB,SAAA,GAAa,EAAO,QAAQ,CACzB,CAAA,CAEL,CAtCE,EAAA,EAAO,EAsCT,CAET,CAAC,CACE,CAnEE,EAAA,CAmEF,CAET,CAAC,CACE,CAAA,EAEJ,GAAY,EAAQ,SAAW,GAC9B,EAAC,MAAD,CAAK,UAAW,EAAG,EAAO,oBAAqB,GAAY,KAAK,EAAG,MAAO,GAAY,MACnF,SAAA,CACE,CAAA,EAEP,EAAC,OAAD,CAAM,UAAW,EAAO,qBAAsB,KAAK,SAAS,YAAU,SACnE,SAAA,EAAY,EAAQ,OAAS,EAAI,EAAK,QAAQ,EAAQ,MAAM,EAAI,EAAa,EAC1E,CAAA,CACD,GAEX"}
1
+ {"version":3,"file":"commandPalette.mjs","names":[],"sources":["../../../../package/common/commandPalette/commandPalette.tsx"],"sourcesContent":["import { useEffect, useId, useRef, useState, type KeyboardEvent, type ReactElement } from 'react';\nimport cx from 'classnames';\nimport { faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons';\n\nimport Modal from '../modal';\nimport { useComponentDefaults, useLabels } from '../configProvider';\nimport { ariaKeyshortcuts, formatHotkey } from '../hooks/useHotkeys';\nimport { edgeIndex, nextIndex, scrollOptionIntoView } from '../utils/listNavigation';\nimport { renderIcon } from '../utils/renderIcon';\nimport type { MenuAction } from '../menu/menu.types';\nimport { hasAnyIcon, renderItemIcon, runMenuAction } from '../menu/menuItem';\nimport { filterCommands, groupCommands } from './filterCommands';\nimport type { CommandPaletteLabels, CommandPaletteProps } from './commandPalette.types';\nimport styles from './commandPalette.module.pcss';\n\nconst DEFAULT_LABELS: Required<CommandPaletteLabels> = {\n label: 'Search commands',\n placeholder: 'Type a command or search…',\n empty: 'No results',\n loading: 'Searching…',\n results: (count) => (count === 1 ? '1 result' : `${count} results`),\n};\n\nfunction CommandPalette(props: CommandPaletteProps): ReactElement {\n const {\n items,\n isOpen,\n onClose,\n onAction,\n onQueryChange,\n filter = true,\n isLoading = false,\n presentation,\n labels,\n className,\n style,\n classNames,\n styles: slotStyles,\n ...rest\n } = useComponentDefaults('CommandPalette', props);\n const text = useLabels('CommandPalette', labels, DEFAULT_LABELS);\n const listId = useId();\n const listRef = useRef<HTMLDivElement>(null);\n\n const [query, setQuery] = useState('');\n // An id rather than an index, so the highlight stays on the same command while server results\n // are replaced around it. Typing clears it instead, so Enter runs the best match.\n const [activeId, setActiveId] = useState<string | null>(null);\n const [wasOpen, setWasOpen] = useState(isOpen);\n if (isOpen !== wasOpen) {\n setWasOpen(isOpen);\n if (isOpen) {\n setQuery('');\n setActiveId(null);\n }\n }\n\n // Read through a ref so an inline handler does not re-fire the effect on every parent render.\n const onQueryChangeRef = useRef(onQueryChange);\n useEffect(() => {\n onQueryChangeRef.current = onQueryChange;\n });\n useEffect(() => {\n if (isOpen) onQueryChangeRef.current?.('');\n }, [isOpen]);\n\n const groups = filter ? filterCommands(groupCommands(items), query) : groupCommands(items);\n // From all items, not the matches, so the labels do not jump sideways while the user types.\n const hasIcons = hasAnyIcon(items);\n const actions = groups.flatMap((group) => group.actions);\n const isDisabled = (index: number) => !!actions[index]?.disabled;\n const found = actions.findIndex((action) => action.id === activeId && !action.disabled);\n const activeIndex = found >= 0 ? found : edgeIndex('first', actions.length, { isDisabled });\n // From the command's id, not its position: an id that stayed the same while the option under it\n // changed would not make a screen reader announce the new highlight.\n const optionId = (action: MenuAction) => `${listId}-${encodeURIComponent(action.id)}`;\n const activeOptionId = activeIndex === null ? undefined : optionId(actions[activeIndex]);\n\n useEffect(() => {\n if (!activeOptionId) return;\n scrollOptionIntoView(listRef.current, document.getElementById(activeOptionId));\n }, [activeOptionId]);\n\n function run(action: MenuAction | undefined) {\n if (!action || action.disabled) return;\n runMenuAction(action, onAction);\n onClose();\n }\n\n function handleKeyDown(event: KeyboardEvent<HTMLInputElement>) {\n // Enter confirms an IME composition before it means anything here.\n if (event.nativeEvent.isComposing) return;\n if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {\n const next = nextIndex(activeIndex, event.key === 'ArrowDown' ? 1 : -1, actions.length, { isDisabled });\n setActiveId(next === null ? null : actions[next].id);\n } else if (event.key === 'Enter') {\n run(activeIndex === null ? undefined : actions[activeIndex]);\n } else {\n return;\n }\n event.preventDefault();\n }\n\n const hasQuery = query.trim() !== '';\n const emptyText = isLoading ? text.loading : text.empty;\n let index = -1;\n\n return (\n <Modal\n {...rest}\n aria-label={text.label}\n isOpen={isOpen}\n onClose={onClose}\n hideHeader\n presentation={presentation}\n className={cx(styles.commandPalette, className, classNames?.root)}\n style={{ ...style, ...slotStyles?.root }}\n >\n <div className={cx(styles.commandPaletteSearch, classNames?.search)} style={slotStyles?.search}>\n {renderIcon(faMagnifyingGlass, { className: styles.commandPaletteSearchIcon })}\n <input\n type='text'\n role='combobox'\n aria-label={text.label}\n aria-expanded='true'\n aria-controls={listId}\n aria-autocomplete='list'\n aria-activedescendant={activeOptionId}\n autoComplete='off'\n spellCheck={false}\n placeholder={text.placeholder}\n value={query}\n onChange={(event) => {\n setQuery(event.target.value);\n setActiveId(null);\n onQueryChange?.(event.target.value);\n }}\n onKeyDown={handleKeyDown}\n className={cx(styles.commandPaletteInput, classNames?.input)}\n style={slotStyles?.input}\n />\n </div>\n\n <div\n ref={listRef}\n id={listId}\n role='listbox'\n aria-label={text.label}\n className={cx(styles.commandPaletteList, classNames?.list)}\n style={slotStyles?.list}\n >\n {groups.map((group, groupIndex) => {\n const headingId = `${listId}-heading-${groupIndex}`;\n return (\n <div\n // Groups carry no id of their own; a new query re-renders the list wholesale anyway.\n key={groupIndex}\n role={group.heading === undefined ? 'presentation' : 'group'}\n aria-labelledby={group.heading === undefined ? undefined : headingId}\n >\n {group.separated && groupIndex > 0 && (\n <div\n aria-hidden\n className={cx(styles.commandPaletteSeparator, classNames?.separator)}\n style={slotStyles?.separator}\n />\n )}\n {group.heading !== undefined && (\n <div\n id={headingId}\n role='presentation'\n className={cx(styles.commandPaletteHeading, classNames?.heading)}\n style={slotStyles?.heading}\n >\n {group.heading}\n </div>\n )}\n {group.actions.map((action) => {\n index += 1;\n const own = index;\n return (\n <div\n key={action.id}\n id={optionId(action)}\n role='option'\n aria-selected={own === activeIndex}\n aria-disabled={action.disabled || undefined}\n aria-keyshortcuts={ariaKeyshortcuts(action.shortcut)}\n className={cx(\n styles.commandPaletteItem,\n own === activeIndex && styles.commandPaletteItemActive,\n action.danger && styles.commandPaletteItemDanger,\n action.disabled && styles.commandPaletteItemDisabled,\n classNames?.item,\n )}\n style={slotStyles?.item}\n // Keeps the focus, and so the caret, in the search field.\n onMouseDown={(event) => event.preventDefault()}\n // Move, not enter: a list scrolling under a resting pointer must not steal the highlight.\n onMouseMove={() => !action.disabled && setActiveId(action.id)}\n onClick={() => run(action)}\n >\n {renderItemIcon(action.icon, hasIcons, {\n className: cx(styles.commandPaletteItemIcon, classNames?.itemIcon),\n style: slotStyles?.itemIcon,\n })}\n <span\n className={cx(styles.commandPaletteItemLabel, classNames?.itemLabel)}\n style={slotStyles?.itemLabel}\n >\n {action.label}\n </span>\n {action.shortcut != null && (\n <span\n aria-hidden\n className={cx(styles.commandPaletteItemShortcut, classNames?.itemShortcut)}\n style={slotStyles?.itemShortcut}\n >\n {formatHotkey(action.shortcut)}\n </span>\n )}\n </div>\n );\n })}\n </div>\n );\n })}\n </div>\n\n {hasQuery && actions.length === 0 && (\n <div className={cx(styles.commandPaletteEmpty, classNames?.empty)} style={slotStyles?.empty}>\n {emptyText}\n </div>\n )}\n <span className={styles.commandPaletteStatus} role='status' aria-live='polite'>\n {hasQuery ? (actions.length > 0 ? text.results(actions.length) : emptyText) : ''}\n </span>\n </Modal>\n );\n}\n\nexport default CommandPalette;\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- "use client";import{useEffect as e,useRef as t}from"react";var n=`input, textarea, select, [contenteditable=""], [contenteditable="true"]`,r={esc:`escape`,space:` `,spacebar:` `,plus:`+`,up:`arrowup`,down:`arrowdown`,left:`arrowleft`,right:`arrowright`};function i(){return typeof navigator<`u`&&/Mac|iPhone|iPad|iPod/i.test(navigator.userAgent)}function a(e){let t=e.split(`+`).map(e=>e.trim().toLowerCase()).filter(Boolean);if(t.length===0)return null;let n={key:``,ctrl:!1,meta:!1,alt:!1,shift:!1,shiftNamed:!1},a=i();for(let e of t)switch(e){case`mod`:a?n.meta=!0:n.ctrl=!0;break;case`ctrl`:case`control`:n.ctrl=!0;break;case`meta`:case`cmd`:case`command`:n.meta=!0;break;case`alt`:case`option`:n.alt=!0;break;case`shift`:n.shift=!0,n.shiftNamed=!0;break;default:n.key=r[e]??e}return n.key?n:null}function o(e){let t=[e.key.toLowerCase()],n=/^(?:Key|Digit)(.)$/.exec(e.code);return n&&t.push(n[1].toLowerCase()),t}function s(e,t){return e.ctrl!==t.ctrlKey||e.meta!==t.metaKey||e.alt!==t.altKey||e.shiftNamed&&e.shift!==t.shiftKey?!1:o(t).includes(e.key)}function c(e){return e instanceof Element&&!!e.closest(n)}var l=new Set([`arrowup`,`arrowdown`,`arrowleft`,`arrowright`,`home`,`end`,`pageup`,`pagedown`,`backspace`,`delete`,`enter`,`tab`]);function u(e){return e.ctrl||e.meta||e.alt?!1:e.key.length===1||l.has(e.key)}var d={" ":`Space`,arrowup:`↑`,arrowdown:`↓`,arrowleft:`←`,arrowright:`→`,escape:`Esc`,enter:`↵`,backspace:`⌫`,delete:`Del`,pageup:`PgUp`,pagedown:`PgDn`};function f(e){let t=a(e);if(!t)return e;let n=d[t.key]??(t.key.length===1?t.key.toUpperCase():p(t.key));return i()?[t.ctrl&&`⌃`,t.alt&&`⌥`,t.shift&&`⇧`,t.meta&&`⌘`].filter(Boolean).join(``)+n:[...[t.ctrl&&`Ctrl`,t.meta&&`Win`,t.alt&&`Alt`,t.shift&&`Shift`].filter(Boolean),n].join(`+`)}function p(e){return e[0].toUpperCase()+e.slice(1)}function m(n,r={}){let{enabled:i=!0,target:o,enableInFields:l=!1,preventDefault:d=!0}=r,f=t(n);e(()=>{f.current=n}),e(()=>{if(!i)return;let e=o?o.current:window;if(!e)return;function t(e){let t=e;if(!t.isComposing)for(let[e,n]of Object.entries(f.current)){let r=a(e);if(r&&s(r,t)&&!(u(r)&&!l&&c(t.target))){d&&t.preventDefault(),n(t);return}}}return e.addEventListener(`keydown`,t),()=>e.removeEventListener(`keydown`,t)},[i,o,l,d])}export{m as default,m as useHotkeys,f as formatHotkey};
1
+ "use client";import{useEffect as e,useRef as t}from"react";var n=`input, textarea, select, [contenteditable=""], [contenteditable="true"]`,r={esc:`escape`,space:` `,spacebar:` `,plus:`+`,up:`arrowup`,down:`arrowdown`,left:`arrowleft`,right:`arrowright`};function i(){return typeof navigator<`u`&&/Mac|iPhone|iPad|iPod/i.test(navigator.userAgent)}function a(e){let t=e.split(`+`).map(e=>e.trim().toLowerCase()).filter(Boolean);if(t.length===0)return null;let n={key:``,ctrl:!1,meta:!1,alt:!1,shift:!1,shiftNamed:!1},a=i();for(let e of t)switch(e){case`mod`:a?n.meta=!0:n.ctrl=!0;break;case`ctrl`:case`control`:n.ctrl=!0;break;case`meta`:case`cmd`:case`command`:n.meta=!0;break;case`alt`:case`option`:n.alt=!0;break;case`shift`:n.shift=!0,n.shiftNamed=!0;break;default:n.key=r[e]??e}return n.key?n:null}function o(e){let t=[e.key.toLowerCase()],n=/^(?:Key|Digit)(.)$/.exec(e.code);return n&&t.push(n[1].toLowerCase()),t}function s(e,t){return e.ctrl!==t.ctrlKey||e.meta!==t.metaKey||e.alt!==t.altKey||e.shiftNamed&&e.shift!==t.shiftKey?!1:o(t).includes(e.key)}function c(e){return e instanceof Element&&!!e.closest(n)}var l=new Set([`arrowup`,`arrowdown`,`arrowleft`,`arrowright`,`home`,`end`,`pageup`,`pagedown`,`backspace`,`delete`,`enter`,`tab`]);function u(e){return e.ctrl||e.meta||e.alt?!1:e.key.length===1||l.has(e.key)}var d={" ":`Space`,arrowup:`↑`,arrowdown:`↓`,arrowleft:`←`,arrowright:`→`,escape:`Esc`,enter:`↵`,backspace:`⌫`,delete:`Del`,pageup:`PgUp`,pagedown:`PgDn`};function f(e){let t=a(e);if(!t)return e;let n=d[t.key]??(t.key.length===1?t.key.toUpperCase():p(t.key));return i()?[t.ctrl&&`⌃`,t.alt&&`⌥`,t.shift&&`⇧`,t.meta&&`⌘`].filter(Boolean).join(``)+n:[...[t.ctrl&&`Ctrl`,t.meta&&`Win`,t.alt&&`Alt`,t.shift&&`Shift`].filter(Boolean),n].join(`+`)}function p(e){return e[0].toUpperCase()+e.slice(1)}var m={" ":`Space`,"+":`Plus`,arrowup:`ArrowUp`,arrowdown:`ArrowDown`,arrowleft:`ArrowLeft`,arrowright:`ArrowRight`,pageup:`PageUp`,pagedown:`PageDown`};function h(e){let t=e?a(e):null;if(!t)return;let n=m[t.key]??p(t.key);return[...[t.ctrl&&`Control`,t.alt&&`Alt`,t.shift&&`Shift`,t.meta&&`Meta`].filter(Boolean),n].join(`+`)}function g(n,r={}){let{enabled:i=!0,target:o,enableInFields:l=!1,preventDefault:d=!0,repeat:f=!1}=r,p=t(n);e(()=>{p.current=n}),e(()=>{if(!i)return;let e=o?o.current:window;if(!e)return;function t(e){let t=e;if(!t.isComposing)for(let[e,n]of Object.entries(p.current)){let r=a(e);if(r&&s(r,t)&&!(u(r)&&!l&&c(t.target))){if(d&&t.preventDefault(),t.repeat&&!f)return;n(t);return}}}return e.addEventListener(`keydown`,t),()=>e.removeEventListener(`keydown`,t)},[i,o,l,d,f])}export{h as ariaKeyshortcuts,g as default,g as useHotkeys,f as formatHotkey};
2
2
  //# sourceMappingURL=useHotkeys.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"useHotkeys.mjs","names":[],"sources":["../../../../package/common/hooks/useHotkeys.ts"],"sourcesContent":["import { useEffect, useRef, type RefObject } from 'react';\n\nexport type HotkeyHandler = (event: KeyboardEvent) => void;\n\n/** Combo → handler, e.g. `{ 'mod+k': open, Escape: close }`. */\nexport type HotkeyMap = Record<string, HotkeyHandler>;\n\nexport interface UseHotkeysOptions {\n /** Attach the listener at all. Default `true`; pass `false` to suspend every combo at once. */\n enabled?: boolean;\n /** Scope the shortcuts to one element instead of the whole window. */\n target?: RefObject<HTMLElement | null>;\n /**\n * Let plain-key combos fire while a text field or `contenteditable` has focus. Default `false`,\n * so `'n'` does not fire for every `n` typed into an input. Combos carrying Ctrl, Cmd or Alt\n * are never suppressed — they cannot be confused with typing.\n */\n enableInFields?: boolean;\n /**\n * Call `preventDefault()` on a match, so `mod+s` does not also open the browser's save dialog.\n * Default `true`.\n */\n preventDefault?: boolean;\n}\n\ninterface Combo {\n key: string;\n ctrl: boolean;\n meta: boolean;\n alt: boolean;\n shift: boolean;\n /** Set when the combo named Shift itself — see `matches`. */\n shiftNamed: boolean;\n}\n\nconst FIELD_SELECTOR = 'input, textarea, select, [contenteditable=\"\"], [contenteditable=\"true\"]';\n\n/** Spellings accepted for keys whose `event.key` is awkward to write inside a `+`-separated combo. */\nconst KEY_ALIASES: Record<string, string> = {\n esc: 'escape',\n space: ' ',\n spacebar: ' ',\n plus: '+',\n up: 'arrowup',\n down: 'arrowdown',\n left: 'arrowleft',\n right: 'arrowright',\n};\n\n/** `mod` is Cmd where Cmd is the command modifier, and Ctrl everywhere else. */\nfunction isApplePlatform(): boolean {\n return typeof navigator !== 'undefined' && /Mac|iPhone|iPad|iPod/i.test(navigator.userAgent);\n}\n\nfunction parseCombo(combo: string): Combo | null {\n const parts = combo\n .split('+')\n .map((part) => part.trim().toLowerCase())\n .filter(Boolean);\n // A trailing separator (`'mod+'`) loses its key to the filter above, so the whole combo is\n // dropped rather than matching on the modifier alone. `'mod+plus'` is how to mean `+` itself.\n if (parts.length === 0) return null;\n\n const parsed: Combo = { key: '', ctrl: false, meta: false, alt: false, shift: false, shiftNamed: false };\n const apple = isApplePlatform();\n\n for (const part of parts) {\n switch (part) {\n case 'mod':\n if (apple) parsed.meta = true;\n else parsed.ctrl = true;\n break;\n case 'ctrl':\n case 'control':\n parsed.ctrl = true;\n break;\n case 'meta':\n case 'cmd':\n case 'command':\n parsed.meta = true;\n break;\n case 'alt':\n case 'option':\n parsed.alt = true;\n break;\n case 'shift':\n parsed.shift = true;\n parsed.shiftNamed = true;\n break;\n default:\n parsed.key = KEY_ALIASES[part] ?? part;\n }\n }\n\n return parsed.key ? parsed : null;\n}\n\n/**\n * The key the event carries, plus the one its physical position would produce unshifted.\n *\n * Both are needed: Alt+K on a Mac arrives as `key: '˚'` and Shift+1 as `key: '!'`, so a combo\n * written against the letter or digit on the keycap only matches through `code`.\n */\nfunction pressedKeys(event: KeyboardEvent): string[] {\n const keys = [event.key.toLowerCase()];\n const physical = /^(?:Key|Digit)(.)$/.exec(event.code);\n if (physical) keys.push(physical[1].toLowerCase());\n return keys;\n}\n\nfunction matches(combo: Combo, event: KeyboardEvent): boolean {\n if (combo.ctrl !== event.ctrlKey || combo.meta !== event.metaKey || combo.alt !== event.altKey) return false;\n // Shift is only compared when the combo named it. A symbol is typed *with* Shift on most\n // layouts, so requiring `shiftKey === false` would make `'?'` and `'mod+/'` unreachable.\n if (combo.shiftNamed && combo.shift !== event.shiftKey) return false;\n return pressedKeys(event).includes(combo.key);\n}\n\nfunction isInField(target: EventTarget | null): boolean {\n return target instanceof Element && !!target.closest(FIELD_SELECTOR);\n}\n\n/** Keys a text field uses for itself, beyond the characters it inserts. */\nconst FIELD_KEYS = new Set([\n 'arrowup',\n 'arrowdown',\n 'arrowleft',\n 'arrowright',\n 'home',\n 'end',\n 'pageup',\n 'pagedown',\n 'backspace',\n 'delete',\n 'enter',\n 'tab',\n]);\n\n/**\n * Whether a combo would be taken out of a focused field's mouth.\n *\n * Ctrl, Cmd and Alt take a key out of the field's hands whatever it is. Without one, a single\n * character is typing and the listed keys are editing, but `F2` and `Escape` are neither, and\n * suppressing those made a menu's `F2` do nothing the moment the page had a focused input.\n */\nfunction isFieldKey(combo: Combo): boolean {\n if (combo.ctrl || combo.meta || combo.alt) return false;\n return combo.key.length === 1 || FIELD_KEYS.has(combo.key);\n}\n\n/** Keys whose name is not what a keycap says. Anything else is capitalised, so `f2` reads `F2`. */\nconst KEY_LABELS: Record<string, string> = {\n ' ': 'Space',\n arrowup: '↑',\n arrowdown: '↓',\n arrowleft: '←',\n arrowright: '→',\n escape: 'Esc',\n enter: '↵',\n backspace: '⌫',\n delete: 'Del',\n pageup: 'PgUp',\n pagedown: 'PgDn',\n};\n\n/**\n * A combo written for the reader: `'mod+shift+k'` reads `⇧⌘K` on an Apple platform and\n * `Ctrl+Shift+K` everywhere else.\n *\n * So a shortcut is written once, in one spelling, and the hint beside it cannot contradict what\n * the binding actually listens for. Hand-written `'⌘K'` hints are wrong on every other platform,\n * silently, because nothing compares the label to the binding.\n *\n * Resolved from the platform at call time, so a server-rendered page renders the non-Apple form.\n */\nexport function formatHotkey(combo: string): string {\n const parsed = parseCombo(combo);\n if (!parsed) return combo;\n\n const key = KEY_LABELS[parsed.key] ?? (parsed.key.length === 1 ? parsed.key.toUpperCase() : capitalise(parsed.key));\n\n // Apple orders modifier symbols ⌃⌥⇧⌘ and runs them into the key; everywhere else they are\n // words joined by `+`.\n if (isApplePlatform()) {\n const symbols = [parsed.ctrl && '⌃', parsed.alt && '⌥', parsed.shift && '⇧', parsed.meta && '⌘'];\n return symbols.filter(Boolean).join('') + key;\n }\n const words = [parsed.ctrl && 'Ctrl', parsed.meta && 'Win', parsed.alt && 'Alt', parsed.shift && 'Shift'];\n return [...words.filter(Boolean), key].join('+');\n}\n\nfunction capitalise(value: string): string {\n return value[0].toUpperCase() + value.slice(1);\n}\n\n/**\n * Keyboard shortcuts, for a whole app or for one element.\n *\n * ```tsx\n * useHotkeys({ 'mod+k': () => setPaletteOpen(true), '?': () => setHelpOpen(true) });\n * ```\n *\n * The map is read through a ref at event time, so an inline object literal is fine and no handler\n * needs memoising. Shortcuts keep firing while a Modal or Drawer is open — pass `enabled` for the\n * ones that should not.\n */\nexport function useHotkeys(hotkeys: HotkeyMap, options: UseHotkeysOptions = {}): void {\n const { enabled = true, target, enableInFields = false, preventDefault = true } = options;\n\n const hotkeysRef = useRef(hotkeys);\n useEffect(() => {\n hotkeysRef.current = hotkeys;\n });\n\n useEffect(() => {\n if (!enabled) return;\n const node: HTMLElement | Window | null = target ? target.current : window;\n if (!node) return;\n\n function onKeyDown(event: Event) {\n const keyboardEvent = event as KeyboardEvent;\n // A key pressed to compose a character in an IME is not a shortcut.\n if (keyboardEvent.isComposing) return;\n\n for (const [spelling, handler] of Object.entries(hotkeysRef.current)) {\n const combo = parseCombo(spelling);\n if (!combo || !matches(combo, keyboardEvent)) continue;\n if (isFieldKey(combo) && !enableInFields && isInField(keyboardEvent.target)) continue;\n if (preventDefault) keyboardEvent.preventDefault();\n handler(keyboardEvent);\n return;\n }\n }\n\n node.addEventListener('keydown', onKeyDown);\n return () => node.removeEventListener('keydown', onKeyDown);\n }, [enabled, target, enableInFields, preventDefault]);\n}\n\nexport default useHotkeys;\n"],"mappings":"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"}
1
+ {"version":3,"file":"useHotkeys.mjs","names":[],"sources":["../../../../package/common/hooks/useHotkeys.ts"],"sourcesContent":["import { useEffect, useRef, type RefObject } from 'react';\n\nexport type HotkeyHandler = (event: KeyboardEvent) => void;\n\n/** Combo → handler, e.g. `{ 'mod+k': open, Escape: close }`. */\nexport type HotkeyMap = Record<string, HotkeyHandler>;\n\nexport interface UseHotkeysOptions {\n /** Attach the listener at all. Default `true`; pass `false` to suspend every combo at once. */\n enabled?: boolean;\n /** Scope the shortcuts to one element instead of the whole window. */\n target?: RefObject<HTMLElement | null>;\n /**\n * Let plain-key combos fire while a text field or `contenteditable` has focus. Default `false`,\n * so `'n'` does not fire for every `n` typed into an input. Combos carrying Ctrl, Cmd or Alt\n * are never suppressed — they cannot be confused with typing.\n */\n enableInFields?: boolean;\n /**\n * Call `preventDefault()` on a match, so `mod+s` does not also open the browser's save dialog.\n * Default `true`.\n */\n preventDefault?: boolean;\n /**\n * Fire again for each repeat while the combo is held. Default `false`, so holding `mod+s` saves\n * once. Turn it on for keys that step through something, such as arrows or zoom.\n */\n repeat?: boolean;\n}\n\ninterface Combo {\n key: string;\n ctrl: boolean;\n meta: boolean;\n alt: boolean;\n shift: boolean;\n /** Set when the combo named Shift itself — see `matches`. */\n shiftNamed: boolean;\n}\n\nconst FIELD_SELECTOR = 'input, textarea, select, [contenteditable=\"\"], [contenteditable=\"true\"]';\n\n/** Spellings accepted for keys whose `event.key` is awkward to write inside a `+`-separated combo. */\nconst KEY_ALIASES: Record<string, string> = {\n esc: 'escape',\n space: ' ',\n spacebar: ' ',\n plus: '+',\n up: 'arrowup',\n down: 'arrowdown',\n left: 'arrowleft',\n right: 'arrowright',\n};\n\n/** `mod` is Cmd where Cmd is the command modifier, and Ctrl everywhere else. */\nfunction isApplePlatform(): boolean {\n return typeof navigator !== 'undefined' && /Mac|iPhone|iPad|iPod/i.test(navigator.userAgent);\n}\n\nfunction parseCombo(combo: string): Combo | null {\n const parts = combo\n .split('+')\n .map((part) => part.trim().toLowerCase())\n .filter(Boolean);\n // A trailing separator (`'mod+'`) loses its key to the filter above, so the whole combo is\n // dropped rather than matching on the modifier alone. `'mod+plus'` is how to mean `+` itself.\n if (parts.length === 0) return null;\n\n const parsed: Combo = { key: '', ctrl: false, meta: false, alt: false, shift: false, shiftNamed: false };\n const apple = isApplePlatform();\n\n for (const part of parts) {\n switch (part) {\n case 'mod':\n if (apple) parsed.meta = true;\n else parsed.ctrl = true;\n break;\n case 'ctrl':\n case 'control':\n parsed.ctrl = true;\n break;\n case 'meta':\n case 'cmd':\n case 'command':\n parsed.meta = true;\n break;\n case 'alt':\n case 'option':\n parsed.alt = true;\n break;\n case 'shift':\n parsed.shift = true;\n parsed.shiftNamed = true;\n break;\n default:\n parsed.key = KEY_ALIASES[part] ?? part;\n }\n }\n\n return parsed.key ? parsed : null;\n}\n\n/**\n * The key the event carries, plus the one its physical position would produce unshifted.\n *\n * Both are needed: Alt+K on a Mac arrives as `key: '˚'` and Shift+1 as `key: '!'`, so a combo\n * written against the letter or digit on the keycap only matches through `code`.\n */\nfunction pressedKeys(event: KeyboardEvent): string[] {\n const keys = [event.key.toLowerCase()];\n const physical = /^(?:Key|Digit)(.)$/.exec(event.code);\n if (physical) keys.push(physical[1].toLowerCase());\n return keys;\n}\n\nfunction matches(combo: Combo, event: KeyboardEvent): boolean {\n if (combo.ctrl !== event.ctrlKey || combo.meta !== event.metaKey || combo.alt !== event.altKey) return false;\n // Shift is only compared when the combo named it. A symbol is typed *with* Shift on most\n // layouts, so requiring `shiftKey === false` would make `'?'` and `'mod+/'` unreachable.\n if (combo.shiftNamed && combo.shift !== event.shiftKey) return false;\n return pressedKeys(event).includes(combo.key);\n}\n\nfunction isInField(target: EventTarget | null): boolean {\n return target instanceof Element && !!target.closest(FIELD_SELECTOR);\n}\n\n/** Keys a text field uses for itself, beyond the characters it inserts. */\nconst FIELD_KEYS = new Set([\n 'arrowup',\n 'arrowdown',\n 'arrowleft',\n 'arrowright',\n 'home',\n 'end',\n 'pageup',\n 'pagedown',\n 'backspace',\n 'delete',\n 'enter',\n 'tab',\n]);\n\n/**\n * Whether a combo would be taken out of a focused field's mouth.\n *\n * Ctrl, Cmd and Alt take a key out of the field's hands whatever it is. Without one, a single\n * character is typing and the listed keys are editing, but `F2` and `Escape` are neither, and\n * suppressing those made a menu's `F2` do nothing the moment the page had a focused input.\n */\nfunction isFieldKey(combo: Combo): boolean {\n if (combo.ctrl || combo.meta || combo.alt) return false;\n return combo.key.length === 1 || FIELD_KEYS.has(combo.key);\n}\n\n/** Keys whose name is not what a keycap says. Anything else is capitalised, so `f2` reads `F2`. */\nconst KEY_LABELS: Record<string, string> = {\n ' ': 'Space',\n arrowup: '↑',\n arrowdown: '↓',\n arrowleft: '←',\n arrowright: '→',\n escape: 'Esc',\n enter: '↵',\n backspace: '⌫',\n delete: 'Del',\n pageup: 'PgUp',\n pagedown: 'PgDn',\n};\n\n/**\n * A combo written for the reader: `'mod+shift+k'` reads `⇧⌘K` on an Apple platform and\n * `Ctrl+Shift+K` everywhere else.\n *\n * So a shortcut is written once, in one spelling, and the hint beside it cannot contradict what\n * the binding actually listens for. Hand-written `'⌘K'` hints are wrong on every other platform,\n * silently, because nothing compares the label to the binding.\n *\n * Resolved from the platform at call time, so a server-rendered page renders the non-Apple form.\n */\nexport function formatHotkey(combo: string): string {\n const parsed = parseCombo(combo);\n if (!parsed) return combo;\n\n const key = KEY_LABELS[parsed.key] ?? (parsed.key.length === 1 ? parsed.key.toUpperCase() : capitalise(parsed.key));\n\n // Apple orders modifier symbols ⌃⌥⇧⌘ and runs them into the key; everywhere else they are\n // words joined by `+`.\n if (isApplePlatform()) {\n const symbols = [parsed.ctrl && '⌃', parsed.alt && '⌥', parsed.shift && '⇧', parsed.meta && '⌘'];\n return symbols.filter(Boolean).join('') + key;\n }\n const words = [parsed.ctrl && 'Ctrl', parsed.meta && 'Win', parsed.alt && 'Alt', parsed.shift && 'Shift'];\n return [...words.filter(Boolean), key].join('+');\n}\n\nfunction capitalise(value: string): string {\n return value[0].toUpperCase() + value.slice(1);\n}\n\n/** UI Events key names that are not the lowercase spelling capitalised. */\nconst ARIA_KEY_NAMES: Record<string, string> = {\n // The attribute's syntax uses both characters as separators, so ARIA names them instead.\n ' ': 'Space',\n '+': 'Plus',\n arrowup: 'ArrowUp',\n arrowdown: 'ArrowDown',\n arrowleft: 'ArrowLeft',\n arrowright: 'ArrowRight',\n pageup: 'PageUp',\n pagedown: 'PageDown',\n};\n\n/**\n * A combo as `aria-keyshortcuts` spells it: `'mod+shift+k'` is `Control+Shift+K`, or\n * `Meta+Shift+K` on an Apple platform. Resolved from the platform like `formatHotkey`, so the two\n * always name the same keys.\n */\nexport function ariaKeyshortcuts(combo: string | undefined): string | undefined {\n const parsed = combo ? parseCombo(combo) : null;\n if (!parsed) return undefined;\n\n const key = ARIA_KEY_NAMES[parsed.key] ?? capitalise(parsed.key);\n const modifiers = [parsed.ctrl && 'Control', parsed.alt && 'Alt', parsed.shift && 'Shift', parsed.meta && 'Meta'];\n return [...modifiers.filter(Boolean), key].join('+');\n}\n\n/**\n * Keyboard shortcuts, for a whole app or for one element.\n *\n * ```tsx\n * useHotkeys({ 'mod+k': () => setPaletteOpen(true), '?': () => setHelpOpen(true) });\n * ```\n *\n * The map is read through a ref at event time, so an inline object literal is fine and no handler\n * needs memoising. Shortcuts keep firing while a Modal or Drawer is open — pass `enabled` for the\n * ones that should not.\n */\nexport function useHotkeys(hotkeys: HotkeyMap, options: UseHotkeysOptions = {}): void {\n const { enabled = true, target, enableInFields = false, preventDefault = true, repeat = false } = options;\n\n const hotkeysRef = useRef(hotkeys);\n useEffect(() => {\n hotkeysRef.current = hotkeys;\n });\n\n useEffect(() => {\n if (!enabled) return;\n const node: HTMLElement | Window | null = target ? target.current : window;\n if (!node) return;\n\n function onKeyDown(event: Event) {\n const keyboardEvent = event as KeyboardEvent;\n // A key pressed to compose a character in an IME is not a shortcut.\n if (keyboardEvent.isComposing) return;\n\n for (const [spelling, handler] of Object.entries(hotkeysRef.current)) {\n const combo = parseCombo(spelling);\n if (!combo || !matches(combo, keyboardEvent)) continue;\n if (isFieldKey(combo) && !enableInFields && isInField(keyboardEvent.target)) continue;\n // Prevented on a skipped repeat too, or the second keydown of a held mod+s opens the\n // browser's save dialog.\n if (preventDefault) keyboardEvent.preventDefault();\n if (keyboardEvent.repeat && !repeat) return;\n handler(keyboardEvent);\n return;\n }\n }\n\n node.addEventListener('keydown', onKeyDown);\n return () => node.removeEventListener('keydown', onKeyDown);\n }, [enabled, target, enableInFields, preventDefault, repeat]);\n}\n\nexport default useHotkeys;\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- "use client";import{renderIcon as e}from"../utils/renderIcon.mjs";import{PopoverAlign as t,PopoverPlacement as n}from"../popover/popover.types.mjs";import{useFocusBoundary as r}from"../hooks/useFocusBoundary.mjs";import i from"../popover/popover.mjs";import{formatHotkey as a}from"../hooks/useHotkeys.mjs";import{isMenuAction as o,isMenuHeading as s,isMenuSeparator as c}from"./menu.types.mjs";import l from"./menu.module.mjs";import{cloneElement as u,isValidElement as d,useEffect as f,useId as ee,useRef as p,useState as m}from"react";import h from"classnames";import{jsx as g,jsxs as _}from"react/jsx-runtime";var v=600;function y(e,t,n){for(let r=1;r<=e.length;r++){let i=(t+n*r+e.length*r)%e.length,a=e[i];if(o(a)&&!a.disabled)return i}return-1}function b(e){return e.findIndex(e=>o(e)&&!e.disabled)}function x(e){for(let t=e.length-1;t>=0;t--){let n=e[t];if(o(n)&&!n.disabled)return t}return-1}function S({items:S,children:C,onAction:w,isOpen:T,onOpenChange:E,placement:D=n.Bottom,align:O=t.Start,presentation:k,className:A,style:j,classNames:M,styles:N,...P}){let F=T!==void 0,[I,L]=m(!1),R=F?T:I,[z,B]=m(-1),V=p([]),H=p(null),U=p(null),{restoreFocusToTrigger:W,focusAfterTrigger:G}=r({triggerRef:H,contentRef:U}),K=ee(),q=p(`first`),J=p({query:``,timer:void 0}),Y=p(!1);function X(e,t=!1){e&&(Y.current=t),F||L(e),E?.(e)}let Z=p(S);f(()=>{Z.current=S}),f(()=>{if(!R)return;if(!Y.current){B(-1);return}let e=Z.current,t=q.current===`last`?x(e):b(e);B(t);let n=requestAnimationFrame(()=>{V.current[t]?.focus()});return()=>{cancelAnimationFrame(n),q.current=`first`}},[R]),f(()=>()=>clearTimeout(J.current.timer),[]);function Q(e){e<0||(B(e),V.current[e]?.focus())}function $(e){X(!1),e&&W()}function te(e){if(e.length!==1||!e.trim())return!1;clearTimeout(J.current.timer),J.current.query+=e.toLowerCase(),J.current.timer=setTimeout(()=>{J.current.query=``},v);let t=J.current.query,n=t.length>1&&[...t].every(e=>e===t[0]),r=n?t[0]:t,i=n||t.length===1?z:z-1;for(let e=1;e<=S.length;e++){let t=(i+e+S.length)%S.length,n=S[t];if(o(n)&&!n.disabled&&(V.current[t]?.textContent?.trim().toLowerCase()??``).startsWith(r))return Q(t),!0}return!1}function ne(e){if(e.key===`ArrowDown`||e.key===`ArrowUp`){if(e.preventDefault(),e.stopPropagation(),R){Q(e.key===`ArrowUp`?x(S):b(S));return}q.current=e.key===`ArrowUp`?`last`:`first`,X(!0,!0)}}function re(e){switch(e.key){case`ArrowDown`:Q(y(S,z,1));break;case`ArrowUp`:Q(z<0?x(S):y(S,z,-1));break;case`Home`:Q(b(S));break;case`End`:Q(x(S));break;case`Escape`:$(!0);break;case`Tab`:$(!1),e.shiftKey?W():G();break;default:if(!te(e.key))return}e.preventDefault(),e.stopPropagation()}function ie(e){let t=S[e];o(t)&&!t.disabled&&(t.onClick?.(),w?.(t.id),$(!0))}let ae=d(C)?u(C,{"aria-haspopup":C.props[`aria-haspopup`]??`menu`,"aria-expanded":C.props[`aria-expanded`]??R,"aria-controls":R?C.props[`aria-controls`]??K:C.props[`aria-controls`]}):C,oe=g(`div`,{ref:U,id:K,role:`menu`,className:h(l.menu,M?.menu),style:N?.menu,onKeyDown:re,children:S.map((t,n)=>c(t)?g(`div`,{role:`separator`,className:h(l.menuSeparator,M?.separator),style:N?.separator},`separator-${n}`):s(t)?g(`div`,{role:`presentation`,className:h(l.menuHeading,M?.heading),style:N?.heading,children:t.heading},`heading-${n}`):_(`button`,{ref:e=>{V.current[n]=e},type:`button`,role:`menuitem`,disabled:t.disabled,tabIndex:n===z?0:-1,className:h(l.menuItem,t.danger&&l.menuItemDanger,M?.item),style:N?.item,onClick:()=>ie(n),onMouseEnter:()=>B(n),children:[t.icon!=null&&e(t.icon,{className:h(l.menuItemIcon,M?.itemIcon),style:N?.itemIcon}),g(`span`,{className:h(l.menuItemLabel,M?.itemLabel),style:N?.itemLabel,children:t.label}),t.shortcut!=null&&g(`span`,{className:h(l.menuItemShortcut,M?.itemShortcut),style:N?.itemShortcut,children:a(t.shortcut)})]},t.id))});return g(i,{...P,isOpen:R,onOpenChange:(e,{fromKeyboard:t})=>X(e,t),placement:D,align:O,presentation:k,content:oe,focusOnOpen:!1,constrainWidth:!1,classNames:{panel:l.menuPanel},className:h(A,M?.root),style:{...j,...N?.root},children:g(`span`,{ref:H,className:h(l.menuTrigger,M?.trigger),style:N?.trigger,onKeyDown:ne,children:ae})})}export{S as default};
1
+ "use client";import{isSelfActivation as e}from"../utils/activationKey.mjs";import{ariaKeyshortcuts as t,formatHotkey as n}from"../hooks/useHotkeys.mjs";import{PopoverAlign as r,PopoverPlacement as i}from"../popover/popover.types.mjs";import{useFocusBoundary as a}from"../hooks/useFocusBoundary.mjs";import o from"../popover/popover.mjs";import{isMenuAction as s,isMenuHeading as c,isMenuSeparator as l}from"./menu.types.mjs";import{hasAnyIcon as u,renderItemIcon as d}from"./menuItem.mjs";import f from"./menu.module.mjs";import{cloneElement as ee,isValidElement as p,useEffect as m,useId as h,useRef as g,useState as _}from"react";import v from"classnames";import{Fragment as te,jsx as y,jsxs as b}from"react/jsx-runtime";var x=600;function S(e,t,n){for(let r=1;r<=e.length;r++){let i=(t+n*r+e.length*r)%e.length,a=e[i];if(s(a)&&!a.disabled)return i}return-1}function C(e){return e.findIndex(e=>s(e)&&!e.disabled)}function w(e){for(let t=e.length-1;t>=0;t--){let n=e[t];if(s(n)&&!n.disabled)return t}return-1}function T({items:T,children:E,onAction:D,isOpen:O,onOpenChange:k,placement:A=i.Bottom,align:ne=r.Start,presentation:re,className:ie,style:ae,classNames:j,styles:M,...N}){let P=O!==void 0,[F,I]=_(!1),L=P?O:F,[R,z]=_(-1),B=g([]),V=g(null),H=g(null),{restoreFocusToTrigger:U,focusAfterTrigger:W}=a({triggerRef:V,contentRef:H}),G=h(),K=g(`first`),q=g({query:``,timer:void 0}),J=g(!1);function Y(e,t=!1){e&&(J.current=t),P||I(e),k?.(e)}let X=g(T);m(()=>{X.current=T}),m(()=>{if(!L)return;if(!J.current){z(-1);return}let e=X.current,t=K.current===`last`?w(e):C(e);z(t);let n=requestAnimationFrame(()=>{B.current[t]?.focus()});return()=>{cancelAnimationFrame(n),K.current=`first`}},[L]),m(()=>()=>clearTimeout(q.current.timer),[]);function Z(e){e<0||(z(e),B.current[e]?.focus())}function Q(e){Y(!1),e&&U()}function oe(e){if(e.length!==1||!e.trim())return!1;clearTimeout(q.current.timer),q.current.query+=e.toLowerCase(),q.current.timer=setTimeout(()=>{q.current.query=``},x);let t=q.current.query,n=t.length>1&&[...t].every(e=>e===t[0]),r=n?t[0]:t,i=n||t.length===1?R:R-1;for(let e=1;e<=T.length;e++){let t=(i+e+T.length)%T.length,n=T[t];if(s(n)&&!n.disabled&&(B.current[t]?.textContent?.trim().toLowerCase()??``).startsWith(r))return Z(t),!0}return!1}function se(e){if(e.key===`ArrowDown`||e.key===`ArrowUp`){if(e.preventDefault(),e.stopPropagation(),L){Z(e.key===`ArrowUp`?w(T):C(T));return}K.current=e.key===`ArrowUp`?`last`:`first`,Y(!0,!0)}}function ce(e){switch(e.key){case`ArrowDown`:Z(S(T,R,1));break;case`ArrowUp`:Z(R<0?w(T):S(T,R,-1));break;case`Home`:Z(C(T));break;case`End`:Z(w(T));break;case`Escape`:Q(!0);break;case`Tab`:Q(!1),e.shiftKey?U():W();break;default:if(!oe(e.key))return}e.preventDefault(),e.stopPropagation()}function le(e){let t=T[e];s(t)&&!t.disabled&&(t.onClick?.(),D?.(t.id),Q(!0))}let $=p(E)?ee(E,{"aria-haspopup":E.props[`aria-haspopup`]??`menu`,"aria-expanded":E.props[`aria-expanded`]??L,"aria-controls":L?E.props[`aria-controls`]??G:E.props[`aria-controls`]}):E,ue=u(T),de=y(`div`,{ref:H,id:G,role:`menu`,className:v(f.menu,j?.menu),style:M?.menu,onKeyDown:ce,children:T.map((r,i)=>{if(l(r))return y(`div`,{role:`separator`,className:v(f.menuSeparator,j?.separator),style:M?.separator},`separator-${i}`);if(c(r))return y(`div`,{role:`presentation`,className:v(f.menuHeading,j?.heading),style:M?.heading,children:r.heading},`heading-${i}`);let a={ref:e=>{B.current[i]=e},role:`menuitem`,"aria-keyshortcuts":t(r.shortcut),tabIndex:i===R?0:-1,className:v(f.menuItem,r.danger&&f.menuItemDanger,j?.item),style:M?.item,onClick:()=>le(i),onMouseEnter:()=>z(i)},o=b(te,{children:[d(r.icon,ue,{className:v(f.menuItemIcon,j?.itemIcon),style:M?.itemIcon}),y(`span`,{className:v(f.menuItemLabel,j?.itemLabel),style:M?.itemLabel,children:r.label}),r.shortcut!=null&&y(`span`,{"aria-hidden":!0,className:v(f.menuItemShortcut,j?.itemShortcut),style:M?.itemShortcut,children:n(r.shortcut)})]});return r.href&&!r.disabled?y(`a`,{href:r.href,...a,onKeyDown:t=>{t.key===` `&&e(t)&&(t.preventDefault(),t.currentTarget.click())},children:o},r.id):y(`button`,{type:`button`,disabled:r.disabled,...a,children:o},r.id)})});return y(o,{...N,isOpen:L,onOpenChange:(e,{fromKeyboard:t})=>Y(e,t),placement:A,align:ne,presentation:re,content:de,focusOnOpen:!1,constrainWidth:!1,classNames:{panel:f.menuPanel},className:v(ie,j?.root),style:{...ae,...M?.root},children:y(`span`,{ref:V,className:v(f.menuTrigger,j?.trigger),style:M?.trigger,onKeyDown:se,children:$})})}export{T as default};
2
2
  //# sourceMappingURL=menu.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"menu.mjs","names":[],"sources":["../../../../package/common/menu/menu.tsx"],"sourcesContent":["import { cloneElement, isValidElement, useEffect, useId, useRef, useState, type ReactElement } from 'react';\nimport cx from 'classnames';\n\nimport styles from './menu.module.pcss';\nimport Popover from '../popover';\nimport { PopoverAlign, PopoverPlacement } from '../popover/popover.types';\nimport { renderIcon } from '../utils/renderIcon';\nimport { useFocusBoundary } from '../hooks/useFocusBoundary';\nimport { formatHotkey } from '../hooks/useHotkeys';\nimport { isMenuAction, isMenuHeading, isMenuSeparator, type MenuEntry, type MenuProps } from './menu.types';\n\n/** How long a typed run stays one search before it starts over. */\nconst TYPEAHEAD_RESET_MS = 600;\n\n/** Index of the next action that can be focused, wrapping, or -1 if there is none. */\nfunction nextEnabled(items: MenuEntry[], from: number, step: 1 | -1): number {\n for (let i = 1; i <= items.length; i++) {\n // The `items.length * i` term only keeps the value positive for a backwards\n // step; JavaScript's `%` would otherwise return a negative index.\n const index = (from + step * i + items.length * i) % items.length;\n const entry = items[index];\n if (isMenuAction(entry) && !entry.disabled) return index;\n }\n return -1;\n}\n\nfunction firstEnabled(items: MenuEntry[]): number {\n return items.findIndex((entry) => isMenuAction(entry) && !entry.disabled);\n}\n\nfunction lastEnabled(items: MenuEntry[]): number {\n for (let i = items.length - 1; i >= 0; i--) {\n const entry = items[i];\n if (isMenuAction(entry) && !entry.disabled) return i;\n }\n return -1;\n}\n\n/**\n * A menu of actions on a trigger of your choosing.\n *\n * `Popover` does the anchoring, portalling and outside-click work but takes\n * arbitrary `content`, which leaves every consumer to write their own `ul`/`li`\n * and, in practice, to leave out the roles and the arrow keys. This is that list,\n * done once: `role=\"menu\"`, a roving focus that skips disabled entries, Escape\n * back to the trigger, and separators and headings that stay out of the keyboard\n * order.\n */\nfunction Menu({\n items,\n children,\n onAction,\n isOpen: controlledOpen,\n onOpenChange,\n placement = PopoverPlacement.Bottom,\n align = PopoverAlign.Start,\n presentation,\n className,\n style,\n classNames,\n styles: slotStyles,\n ...rest\n}: MenuProps): ReactElement {\n const isControlled = controlledOpen !== undefined;\n const [internalOpen, setInternalOpen] = useState(false);\n const isOpen = isControlled ? controlledOpen : internalOpen;\n\n const [activeIndex, setActiveIndex] = useState(-1);\n const itemRefs = useRef<(HTMLButtonElement | null)[]>([]);\n const triggerRef = useRef<HTMLSpanElement>(null);\n const menuRef = useRef<HTMLDivElement>(null);\n // Tab leaves the panel in both directions, so the menu needs the same boundary\n // Popover uses: back to the trigger, or on to whatever follows it.\n const { restoreFocusToTrigger, focusAfterTrigger } = useFocusBoundary({ triggerRef, contentRef: menuRef });\n const menuId = useId();\n // Which end to land on when the menu opens: `ArrowUp` on the trigger opens at\n // the bottom, everything else at the top.\n const openAtRef = useRef<'first' | 'last'>('first');\n const typeahead = useRef<{ query: string; timer: ReturnType<typeof setTimeout> | undefined }>({\n query: '',\n timer: undefined,\n });\n\n // Whether a key opened the menu, which decides between landing the focus on an\n // entry and leaving it on the trigger. Set from Popover, which is what sees the\n // trigger's events, or here when an arrow key did the opening.\n const openedByKeyboardRef = useRef(false);\n\n function setOpen(next: boolean, fromKeyboard = false) {\n if (next) openedByKeyboardRef.current = fromKeyboard;\n if (!isControlled) setInternalOpen(next);\n onOpenChange?.(next);\n }\n\n // The effect below needs the entries, but must not re-run when the array is rebuilt. A consumer\n // writes `items={[…]}` inline, so it is a new identity on every render of *their* component:\n // keyed off it, any parent re-render while the menu was open threw the focus back to the first\n // entry, mid-navigation. Declared above the effect that reads it, so it has already run.\n const itemsRef = useRef(items);\n useEffect(() => {\n itemsRef.current = items;\n });\n\n // Focus follows the active entry rather than the menu keeping focus and\n // pointing at it: the items are real buttons, so Enter and Space are theirs.\n // Popover's own `focusOnOpen` would take the first focusable and stop there;\n // the active index has to move with it, so the menu does it itself.\n useEffect(() => {\n if (!isOpen) return;\n // A pointer open leaves the focus where the pointer put it. A highlight on an\n // entry nobody pointed at reads as a selection, and the mouse is about to land\n // somewhere else anyway; the first arrow key moves in, see the trigger's keys.\n if (!openedByKeyboardRef.current) {\n setActiveIndex(-1);\n return;\n }\n const entries = itemsRef.current;\n const target = openAtRef.current === 'last' ? lastEnabled(entries) : firstEnabled(entries);\n setActiveIndex(target);\n // The panel is portalled and animated in, so its buttons exist a frame later.\n const frame = requestAnimationFrame(() => {\n itemRefs.current[target]?.focus();\n });\n return () => {\n cancelAnimationFrame(frame);\n openAtRef.current = 'first';\n };\n }, [isOpen]);\n\n useEffect(() => () => clearTimeout(typeahead.current.timer), []);\n\n function moveTo(index: number) {\n if (index < 0) return;\n setActiveIndex(index);\n itemRefs.current[index]?.focus();\n }\n\n function close(returnFocus: boolean) {\n setOpen(false);\n if (returnFocus) restoreFocusToTrigger();\n }\n\n /**\n * Jumps to the next entry whose label starts with what has been typed. The\n * text comes off the rendered button rather than the `label` prop, which is a\n * `ReactNode` and may be an icon, a badge or anything else.\n */\n function handleTypeahead(key: string): boolean {\n if (key.length !== 1 || !key.trim()) return false;\n clearTimeout(typeahead.current.timer);\n typeahead.current.query += key.toLowerCase();\n typeahead.current.timer = setTimeout(() => {\n typeahead.current.query = '';\n }, TYPEAHEAD_RESET_MS);\n\n const query = typeahead.current.query;\n // A repeated single character walks through the entries starting with it,\n // rather than sticking to the first one.\n const repeated = query.length > 1 && [...query].every((c) => c === query[0]);\n const needle = repeated ? query[0] : query;\n const from = repeated || query.length === 1 ? activeIndex : activeIndex - 1;\n\n for (let i = 1; i <= items.length; i++) {\n const index = (from + i + items.length) % items.length;\n const entry = items[index];\n if (!isMenuAction(entry) || entry.disabled) continue;\n const text = itemRefs.current[index]?.textContent?.trim().toLowerCase() ?? '';\n if (text.startsWith(needle)) {\n moveTo(index);\n return true;\n }\n }\n return false;\n }\n\n /** The trigger's own keys, which decide where the menu opens. */\n function handleTriggerKeyDown(event: React.KeyboardEvent) {\n if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;\n // Popover opens on these keys too; letting both act would be harmless but\n // this keeps the decision about where to land in one place.\n event.preventDefault();\n event.stopPropagation();\n // Already open means a pointer opened it and the focus never left the trigger,\n // since an entry's own keys never reach here. This is the key that moves in.\n if (isOpen) {\n moveTo(event.key === 'ArrowUp' ? lastEnabled(items) : firstEnabled(items));\n return;\n }\n openAtRef.current = event.key === 'ArrowUp' ? 'last' : 'first';\n setOpen(true, true);\n }\n\n function handleKeyDown(event: React.KeyboardEvent) {\n switch (event.key) {\n case 'ArrowDown':\n moveTo(nextEnabled(items, activeIndex, 1));\n break;\n case 'ArrowUp':\n // Stepping back from \"nothing active\" lands two short of the end, so the end is named.\n moveTo(activeIndex < 0 ? lastEnabled(items) : nextEnabled(items, activeIndex, -1));\n break;\n case 'Home':\n moveTo(firstEnabled(items));\n break;\n case 'End':\n moveTo(lastEnabled(items));\n break;\n // Popover handles both of these too, but only once the focus has actually\n // reached the panel: its Escape restores focus on that condition, and its\n // Tab handling acts at the first and last focusable. A menu focuses its\n // first entry a frame after opening, so a key pressed in between would\n // fall through. Here they are unconditional, which is also what a menu\n // wants from Tab: leave, whatever is focused.\n case 'Escape':\n close(true);\n break;\n case 'Tab':\n // Carry on from the trigger, as if the panel had never been in the tab\n // order. Closing and letting the browser take it from here would move on\n // from an entry that is about to be removed, which lands at the top of\n // the document.\n close(false);\n if (event.shiftKey) restoreFocusToTrigger();\n else focusAfterTrigger();\n break;\n default:\n if (!handleTypeahead(event.key)) return;\n }\n event.preventDefault();\n event.stopPropagation();\n }\n\n function handleAction(index: number) {\n const entry = items[index];\n if (!isMenuAction(entry) || entry.disabled) return;\n entry.onClick?.();\n onAction?.(entry.id);\n close(true);\n }\n\n // A screen reader has no way to tell that this button opens a menu unless the\n // button itself says so. Only filled in where the consumer has not, so a\n // trigger that describes itself keeps doing that.\n const triggerWithMenuSemantics = isValidElement<Record<string, unknown>>(children)\n ? cloneElement(children, {\n 'aria-haspopup': children.props['aria-haspopup'] ?? 'menu',\n 'aria-expanded': children.props['aria-expanded'] ?? isOpen,\n 'aria-controls': isOpen ? (children.props['aria-controls'] ?? menuId) : children.props['aria-controls'],\n })\n : children;\n\n const content = (\n <div\n ref={menuRef}\n id={menuId}\n role='menu'\n className={cx(styles.menu, classNames?.menu)}\n style={slotStyles?.menu}\n onKeyDown={handleKeyDown}\n >\n {items.map((entry, index) => {\n if (isMenuSeparator(entry))\n return (\n <div\n // Separators and headings carry no id of their own, and reordering\n // the list re-renders it wholesale anyway.\n key={`separator-${index}`}\n role='separator'\n className={cx(styles.menuSeparator, classNames?.separator)}\n style={slotStyles?.separator}\n />\n );\n\n if (isMenuHeading(entry))\n return (\n <div\n key={`heading-${index}`}\n role='presentation'\n className={cx(styles.menuHeading, classNames?.heading)}\n style={slotStyles?.heading}\n >\n {entry.heading}\n </div>\n );\n\n return (\n <button\n key={entry.id}\n ref={(el) => {\n itemRefs.current[index] = el;\n }}\n type='button'\n role='menuitem'\n disabled={entry.disabled}\n // One tab stop for the whole menu: the arrow keys move between items.\n tabIndex={index === activeIndex ? 0 : -1}\n className={cx(styles.menuItem, entry.danger && styles.menuItemDanger, classNames?.item)}\n style={slotStyles?.item}\n onClick={() => handleAction(index)}\n onMouseEnter={() => setActiveIndex(index)}\n >\n {entry.icon != null &&\n renderIcon(entry.icon, {\n className: cx(styles.menuItemIcon, classNames?.itemIcon),\n style: slotStyles?.itemIcon,\n })}\n <span className={cx(styles.menuItemLabel, classNames?.itemLabel)} style={slotStyles?.itemLabel}>\n {entry.label}\n </span>\n {entry.shortcut != null && (\n <span className={cx(styles.menuItemShortcut, classNames?.itemShortcut)} style={slotStyles?.itemShortcut}>\n {formatHotkey(entry.shortcut)}\n </span>\n )}\n </button>\n );\n })}\n </div>\n );\n\n return (\n <Popover\n {...rest}\n isOpen={isOpen}\n onOpenChange={(next, { fromKeyboard }) => setOpen(next, fromKeyboard)}\n placement={placement}\n align={align}\n presentation={presentation}\n content={content}\n // The list pads its own rows, so a hover highlight reaches the panel edge,\n // and it sizes itself. Focus is the menu's own business, see above.\n focusOnOpen={false}\n constrainWidth={false}\n classNames={{ panel: styles.menuPanel }}\n className={cx(className, classNames?.root)}\n style={{ ...style, ...slotStyles?.root }}\n >\n <span\n ref={triggerRef}\n className={cx(styles.menuTrigger, classNames?.trigger)}\n style={slotStyles?.trigger}\n onKeyDown={handleTriggerKeyDown}\n >\n {triggerWithMenuSemantics}\n </span>\n </Popover>\n );\n}\n\nexport default Menu;\n"],"mappings":"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"}
1
+ {"version":3,"file":"menu.mjs","names":[],"sources":["../../../../package/common/menu/menu.tsx"],"sourcesContent":["import { cloneElement, isValidElement, useEffect, useId, useRef, useState, type ReactElement } from 'react';\nimport cx from 'classnames';\n\nimport styles from './menu.module.pcss';\nimport Popover from '../popover';\nimport { PopoverAlign, PopoverPlacement } from '../popover/popover.types';\nimport { isSelfActivation } from '../utils/activationKey';\nimport { useFocusBoundary } from '../hooks/useFocusBoundary';\nimport { ariaKeyshortcuts, formatHotkey } from '../hooks/useHotkeys';\nimport { isMenuAction, isMenuHeading, isMenuSeparator, type MenuEntry, type MenuProps } from './menu.types';\nimport { hasAnyIcon, renderItemIcon } from './menuItem';\n\n/** How long a typed run stays one search before it starts over. */\nconst TYPEAHEAD_RESET_MS = 600;\n\n/** Index of the next action that can be focused, wrapping, or -1 if there is none. */\nfunction nextEnabled(items: MenuEntry[], from: number, step: 1 | -1): number {\n for (let i = 1; i <= items.length; i++) {\n // The `items.length * i` term only keeps the value positive for a backwards\n // step; JavaScript's `%` would otherwise return a negative index.\n const index = (from + step * i + items.length * i) % items.length;\n const entry = items[index];\n if (isMenuAction(entry) && !entry.disabled) return index;\n }\n return -1;\n}\n\nfunction firstEnabled(items: MenuEntry[]): number {\n return items.findIndex((entry) => isMenuAction(entry) && !entry.disabled);\n}\n\nfunction lastEnabled(items: MenuEntry[]): number {\n for (let i = items.length - 1; i >= 0; i--) {\n const entry = items[i];\n if (isMenuAction(entry) && !entry.disabled) return i;\n }\n return -1;\n}\n\n/**\n * A menu of actions on a trigger of your choosing.\n *\n * `Popover` does the anchoring, portalling and outside-click work but takes\n * arbitrary `content`, which leaves every consumer to write their own `ul`/`li`\n * and, in practice, to leave out the roles and the arrow keys. This is that list,\n * done once: `role=\"menu\"`, a roving focus that skips disabled entries, Escape\n * back to the trigger, and separators and headings that stay out of the keyboard\n * order.\n */\nfunction Menu({\n items,\n children,\n onAction,\n isOpen: controlledOpen,\n onOpenChange,\n placement = PopoverPlacement.Bottom,\n align = PopoverAlign.Start,\n presentation,\n className,\n style,\n classNames,\n styles: slotStyles,\n ...rest\n}: MenuProps): ReactElement {\n const isControlled = controlledOpen !== undefined;\n const [internalOpen, setInternalOpen] = useState(false);\n const isOpen = isControlled ? controlledOpen : internalOpen;\n\n const [activeIndex, setActiveIndex] = useState(-1);\n const itemRefs = useRef<(HTMLElement | null)[]>([]);\n const triggerRef = useRef<HTMLSpanElement>(null);\n const menuRef = useRef<HTMLDivElement>(null);\n // Tab leaves the panel in both directions, so the menu needs the same boundary\n // Popover uses: back to the trigger, or on to whatever follows it.\n const { restoreFocusToTrigger, focusAfterTrigger } = useFocusBoundary({ triggerRef, contentRef: menuRef });\n const menuId = useId();\n // Which end to land on when the menu opens: `ArrowUp` on the trigger opens at\n // the bottom, everything else at the top.\n const openAtRef = useRef<'first' | 'last'>('first');\n const typeahead = useRef<{ query: string; timer: ReturnType<typeof setTimeout> | undefined }>({\n query: '',\n timer: undefined,\n });\n\n // Whether a key opened the menu, which decides between landing the focus on an\n // entry and leaving it on the trigger. Set from Popover, which is what sees the\n // trigger's events, or here when an arrow key did the opening.\n const openedByKeyboardRef = useRef(false);\n\n function setOpen(next: boolean, fromKeyboard = false) {\n if (next) openedByKeyboardRef.current = fromKeyboard;\n if (!isControlled) setInternalOpen(next);\n onOpenChange?.(next);\n }\n\n // The effect below needs the entries, but must not re-run when the array is rebuilt. A consumer\n // writes `items={[…]}` inline, so it is a new identity on every render of *their* component:\n // keyed off it, any parent re-render while the menu was open threw the focus back to the first\n // entry, mid-navigation. Declared above the effect that reads it, so it has already run.\n const itemsRef = useRef(items);\n useEffect(() => {\n itemsRef.current = items;\n });\n\n // Focus follows the active entry rather than the menu keeping focus and\n // pointing at it: the items are real buttons, so Enter and Space are theirs.\n // Popover's own `focusOnOpen` would take the first focusable and stop there;\n // the active index has to move with it, so the menu does it itself.\n useEffect(() => {\n if (!isOpen) return;\n // A pointer open leaves the focus where the pointer put it. A highlight on an\n // entry nobody pointed at reads as a selection, and the mouse is about to land\n // somewhere else anyway; the first arrow key moves in, see the trigger's keys.\n if (!openedByKeyboardRef.current) {\n setActiveIndex(-1);\n return;\n }\n const entries = itemsRef.current;\n const target = openAtRef.current === 'last' ? lastEnabled(entries) : firstEnabled(entries);\n setActiveIndex(target);\n // The panel is portalled and animated in, so its buttons exist a frame later.\n const frame = requestAnimationFrame(() => {\n itemRefs.current[target]?.focus();\n });\n return () => {\n cancelAnimationFrame(frame);\n openAtRef.current = 'first';\n };\n }, [isOpen]);\n\n useEffect(() => () => clearTimeout(typeahead.current.timer), []);\n\n function moveTo(index: number) {\n if (index < 0) return;\n setActiveIndex(index);\n itemRefs.current[index]?.focus();\n }\n\n function close(returnFocus: boolean) {\n setOpen(false);\n if (returnFocus) restoreFocusToTrigger();\n }\n\n /**\n * Jumps to the next entry whose label starts with what has been typed. The\n * text comes off the rendered button rather than the `label` prop, which is a\n * `ReactNode` and may be an icon, a badge or anything else.\n */\n function handleTypeahead(key: string): boolean {\n if (key.length !== 1 || !key.trim()) return false;\n clearTimeout(typeahead.current.timer);\n typeahead.current.query += key.toLowerCase();\n typeahead.current.timer = setTimeout(() => {\n typeahead.current.query = '';\n }, TYPEAHEAD_RESET_MS);\n\n const query = typeahead.current.query;\n // A repeated single character walks through the entries starting with it,\n // rather than sticking to the first one.\n const repeated = query.length > 1 && [...query].every((c) => c === query[0]);\n const needle = repeated ? query[0] : query;\n const from = repeated || query.length === 1 ? activeIndex : activeIndex - 1;\n\n for (let i = 1; i <= items.length; i++) {\n const index = (from + i + items.length) % items.length;\n const entry = items[index];\n if (!isMenuAction(entry) || entry.disabled) continue;\n const text = itemRefs.current[index]?.textContent?.trim().toLowerCase() ?? '';\n if (text.startsWith(needle)) {\n moveTo(index);\n return true;\n }\n }\n return false;\n }\n\n /** The trigger's own keys, which decide where the menu opens. */\n function handleTriggerKeyDown(event: React.KeyboardEvent) {\n if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;\n // Popover opens on these keys too; letting both act would be harmless but\n // this keeps the decision about where to land in one place.\n event.preventDefault();\n event.stopPropagation();\n // Already open means a pointer opened it and the focus never left the trigger,\n // since an entry's own keys never reach here. This is the key that moves in.\n if (isOpen) {\n moveTo(event.key === 'ArrowUp' ? lastEnabled(items) : firstEnabled(items));\n return;\n }\n openAtRef.current = event.key === 'ArrowUp' ? 'last' : 'first';\n setOpen(true, true);\n }\n\n function handleKeyDown(event: React.KeyboardEvent) {\n switch (event.key) {\n case 'ArrowDown':\n moveTo(nextEnabled(items, activeIndex, 1));\n break;\n case 'ArrowUp':\n // Stepping back from \"nothing active\" lands two short of the end, so the end is named.\n moveTo(activeIndex < 0 ? lastEnabled(items) : nextEnabled(items, activeIndex, -1));\n break;\n case 'Home':\n moveTo(firstEnabled(items));\n break;\n case 'End':\n moveTo(lastEnabled(items));\n break;\n // Popover handles both of these too, but only once the focus has actually\n // reached the panel: its Escape restores focus on that condition, and its\n // Tab handling acts at the first and last focusable. A menu focuses its\n // first entry a frame after opening, so a key pressed in between would\n // fall through. Here they are unconditional, which is also what a menu\n // wants from Tab: leave, whatever is focused.\n case 'Escape':\n close(true);\n break;\n case 'Tab':\n // Carry on from the trigger, as if the panel had never been in the tab\n // order. Closing and letting the browser take it from here would move on\n // from an entry that is about to be removed, which lands at the top of\n // the document.\n close(false);\n if (event.shiftKey) restoreFocusToTrigger();\n else focusAfterTrigger();\n break;\n default:\n if (!handleTypeahead(event.key)) return;\n }\n event.preventDefault();\n event.stopPropagation();\n }\n\n function handleAction(index: number) {\n const entry = items[index];\n if (!isMenuAction(entry) || entry.disabled) return;\n // Not runMenuAction: a link entry is an <a>, which the browser follows by itself.\n entry.onClick?.();\n onAction?.(entry.id);\n close(true);\n }\n\n // A screen reader has no way to tell that this button opens a menu unless the\n // button itself says so. Only filled in where the consumer has not, so a\n // trigger that describes itself keeps doing that.\n const triggerWithMenuSemantics = isValidElement<Record<string, unknown>>(children)\n ? cloneElement(children, {\n 'aria-haspopup': children.props['aria-haspopup'] ?? 'menu',\n 'aria-expanded': children.props['aria-expanded'] ?? isOpen,\n 'aria-controls': isOpen ? (children.props['aria-controls'] ?? menuId) : children.props['aria-controls'],\n })\n : children;\n\n const hasIcons = hasAnyIcon(items);\n\n const content = (\n <div\n ref={menuRef}\n id={menuId}\n role='menu'\n className={cx(styles.menu, classNames?.menu)}\n style={slotStyles?.menu}\n onKeyDown={handleKeyDown}\n >\n {items.map((entry, index) => {\n if (isMenuSeparator(entry))\n return (\n <div\n // Separators and headings carry no id of their own, and reordering\n // the list re-renders it wholesale anyway.\n key={`separator-${index}`}\n role='separator'\n className={cx(styles.menuSeparator, classNames?.separator)}\n style={slotStyles?.separator}\n />\n );\n\n if (isMenuHeading(entry))\n return (\n <div\n key={`heading-${index}`}\n role='presentation'\n className={cx(styles.menuHeading, classNames?.heading)}\n style={slotStyles?.heading}\n >\n {entry.heading}\n </div>\n );\n\n const itemProps = {\n ref: (el: HTMLElement | null) => {\n itemRefs.current[index] = el;\n },\n role: 'menuitem',\n 'aria-keyshortcuts': ariaKeyshortcuts(entry.shortcut),\n // One tab stop for the whole menu: the arrow keys move between items.\n tabIndex: index === activeIndex ? 0 : -1,\n className: cx(styles.menuItem, entry.danger && styles.menuItemDanger, classNames?.item),\n style: slotStyles?.item,\n onClick: () => handleAction(index),\n onMouseEnter: () => setActiveIndex(index),\n };\n const itemContent = (\n <>\n {renderItemIcon(entry.icon, hasIcons, {\n className: cx(styles.menuItemIcon, classNames?.itemIcon),\n style: slotStyles?.itemIcon,\n })}\n <span className={cx(styles.menuItemLabel, classNames?.itemLabel)} style={slotStyles?.itemLabel}>\n {entry.label}\n </span>\n {entry.shortcut != null && (\n <span\n aria-hidden\n className={cx(styles.menuItemShortcut, classNames?.itemShortcut)}\n style={slotStyles?.itemShortcut}\n >\n {formatHotkey(entry.shortcut)}\n </span>\n )}\n </>\n );\n\n // An <a> cannot be disabled, so a disabled link is a disabled button.\n return entry.href && !entry.disabled ? (\n <a\n key={entry.id}\n href={entry.href}\n {...itemProps}\n // A link takes Enter but not Space, which every other entry answers to.\n onKeyDown={(event) => {\n if (event.key !== ' ' || !isSelfActivation(event)) return;\n event.preventDefault();\n event.currentTarget.click();\n }}\n >\n {itemContent}\n </a>\n ) : (\n <button key={entry.id} type='button' disabled={entry.disabled} {...itemProps}>\n {itemContent}\n </button>\n );\n })}\n </div>\n );\n\n return (\n <Popover\n {...rest}\n isOpen={isOpen}\n onOpenChange={(next, { fromKeyboard }) => setOpen(next, fromKeyboard)}\n placement={placement}\n align={align}\n presentation={presentation}\n content={content}\n // The list pads its own rows, so a hover highlight reaches the panel edge,\n // and it sizes itself. Focus is the menu's own business, see above.\n focusOnOpen={false}\n constrainWidth={false}\n classNames={{ panel: styles.menuPanel }}\n className={cx(className, classNames?.root)}\n style={{ ...style, ...slotStyles?.root }}\n >\n <span\n ref={triggerRef}\n className={cx(styles.menuTrigger, classNames?.trigger)}\n style={slotStyles?.trigger}\n onKeyDown={handleTriggerKeyDown}\n >\n {triggerWithMenuSemantics}\n </span>\n </Popover>\n );\n}\n\nexport default Menu;\n"],"mappings":"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"}