@charcuterie/ui 1.0.1 → 2.0.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 (134) hide show
  1. package/dist/ColorSchemeSwitcher/ColorSchemeSwitcher.d.ts +35 -0
  2. package/dist/ColorSchemeSwitcher/ColorSchemeSwitcher.d.ts.map +1 -0
  3. package/dist/ColorSchemeSwitcher/ColorSchemeSwitcher.js +33 -0
  4. package/dist/ColorSchemeSwitcher/ColorSchemeSwitcher.js.map +1 -0
  5. package/dist/ColorSchemeToggle/ColorSchemeToggle.d.ts +54 -0
  6. package/dist/ColorSchemeToggle/ColorSchemeToggle.d.ts.map +1 -0
  7. package/dist/ColorSchemeToggle/ColorSchemeToggle.js +30 -0
  8. package/dist/ColorSchemeToggle/ColorSchemeToggle.js.map +1 -0
  9. package/dist/Combobox/Combobox.d.ts +54 -0
  10. package/dist/Combobox/Combobox.d.ts.map +1 -0
  11. package/dist/Combobox/Combobox.js +216 -0
  12. package/dist/Combobox/Combobox.js.map +1 -0
  13. package/dist/Combobox/ComboboxOption.d.ts +36 -0
  14. package/dist/Combobox/ComboboxOption.d.ts.map +1 -0
  15. package/dist/Combobox/ComboboxOption.js +32 -0
  16. package/dist/Combobox/ComboboxOption.js.map +1 -0
  17. package/dist/Dialog/Dialog.d.ts +47 -0
  18. package/dist/Dialog/Dialog.d.ts.map +1 -0
  19. package/dist/Dialog/Dialog.js +52 -0
  20. package/dist/Dialog/Dialog.js.map +1 -0
  21. package/dist/Lightbox/Lightbox.d.ts +57 -0
  22. package/dist/Lightbox/Lightbox.d.ts.map +1 -0
  23. package/dist/Lightbox/Lightbox.js +50 -0
  24. package/dist/Lightbox/Lightbox.js.map +1 -0
  25. package/dist/Listbox/Listbox.d.ts +56 -0
  26. package/dist/Listbox/Listbox.d.ts.map +1 -0
  27. package/dist/Listbox/Listbox.js +151 -0
  28. package/dist/Listbox/Listbox.js.map +1 -0
  29. package/dist/Listbox/ListboxOption.d.ts +36 -0
  30. package/dist/Listbox/ListboxOption.d.ts.map +1 -0
  31. package/dist/Listbox/ListboxOption.js +42 -0
  32. package/dist/Listbox/ListboxOption.js.map +1 -0
  33. package/dist/Menu/Menu.d.ts.map +1 -1
  34. package/dist/Menu/Menu.js +47 -97
  35. package/dist/Menu/Menu.js.map +1 -1
  36. package/dist/Modal/Modal.d.ts +11 -59
  37. package/dist/Modal/Modal.d.ts.map +1 -1
  38. package/dist/Modal/Modal.js +28 -101
  39. package/dist/Modal/Modal.js.map +1 -1
  40. package/dist/Overlay/OverlayPanel.d.ts +50 -0
  41. package/dist/Overlay/OverlayPanel.d.ts.map +1 -0
  42. package/dist/Overlay/OverlayPanel.js +72 -0
  43. package/dist/Overlay/OverlayPanel.js.map +1 -0
  44. package/dist/Overlay/OverlayStack.d.ts +47 -0
  45. package/dist/Overlay/OverlayStack.d.ts.map +1 -0
  46. package/dist/Overlay/OverlayStack.js +46 -0
  47. package/dist/Overlay/OverlayStack.js.map +1 -0
  48. package/dist/Overlay/SharedBackdrop.d.ts +24 -0
  49. package/dist/Overlay/SharedBackdrop.d.ts.map +1 -0
  50. package/dist/Overlay/SharedBackdrop.js +28 -0
  51. package/dist/Overlay/SharedBackdrop.js.map +1 -0
  52. package/dist/Overlay/lockScrollBehind.d.ts.map +1 -0
  53. package/dist/Overlay/lockScrollBehind.js.map +1 -0
  54. package/dist/Overlay/overlayPanelClass.d.ts +17 -0
  55. package/dist/Overlay/overlayPanelClass.d.ts.map +1 -0
  56. package/dist/Overlay/overlayPanelClass.js +17 -0
  57. package/dist/Overlay/overlayPanelClass.js.map +1 -0
  58. package/dist/Overlay/useAnchoredOverlay.d.ts +97 -0
  59. package/dist/Overlay/useAnchoredOverlay.d.ts.map +1 -0
  60. package/dist/Overlay/useAnchoredOverlay.js +81 -0
  61. package/dist/Overlay/useAnchoredOverlay.js.map +1 -0
  62. package/dist/Popover/Popover.d.ts +16 -24
  63. package/dist/Popover/Popover.d.ts.map +1 -1
  64. package/dist/Popover/Popover.js +32 -68
  65. package/dist/Popover/Popover.js.map +1 -1
  66. package/dist/Tooltip/Tooltip.d.ts.map +1 -1
  67. package/dist/Tooltip/Tooltip.js +25 -25
  68. package/dist/Tooltip/Tooltip.js.map +1 -1
  69. package/dist/index.d.ts +13 -1
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +6 -0
  72. package/dist/index.js.map +1 -1
  73. package/dist/mountStory.testHelpers.d.ts +1 -0
  74. package/dist/mountStory.testHelpers.d.ts.map +1 -1
  75. package/dist/mountStory.testHelpers.js +21 -1
  76. package/dist/mountStory.testHelpers.js.map +1 -1
  77. package/package.json +5 -4
  78. package/src/ColorSchemeSwitcher/ColorSchemeSwitcher.mdx +70 -0
  79. package/src/ColorSchemeSwitcher/ColorSchemeSwitcher.stories.tsx +118 -0
  80. package/src/ColorSchemeSwitcher/ColorSchemeSwitcher.test.tsx +53 -0
  81. package/src/ColorSchemeSwitcher/ColorSchemeSwitcher.tsx +90 -0
  82. package/src/ColorSchemeToggle/ColorSchemeToggle.mdx +48 -0
  83. package/src/ColorSchemeToggle/ColorSchemeToggle.stories.tsx +161 -0
  84. package/src/ColorSchemeToggle/ColorSchemeToggle.test.tsx +80 -0
  85. package/src/ColorSchemeToggle/ColorSchemeToggle.tsx +107 -0
  86. package/src/Combobox/Combobox.mdx +71 -0
  87. package/src/Combobox/Combobox.stories.tsx +245 -0
  88. package/src/Combobox/Combobox.test.tsx +289 -0
  89. package/src/Combobox/Combobox.tsx +513 -0
  90. package/src/Combobox/ComboboxOption.tsx +88 -0
  91. package/src/Dialog/Dialog.mdx +87 -0
  92. package/src/Dialog/Dialog.stories.tsx +347 -0
  93. package/src/Dialog/Dialog.test.tsx +312 -0
  94. package/src/Dialog/Dialog.tsx +127 -0
  95. package/src/Field/Field.test.tsx +6 -4
  96. package/src/Lightbox/Lightbox.mdx +63 -0
  97. package/src/Lightbox/Lightbox.stories.tsx +133 -0
  98. package/src/Lightbox/Lightbox.test.tsx +124 -0
  99. package/src/Lightbox/Lightbox.tsx +143 -0
  100. package/src/Listbox/Listbox.mdx +68 -0
  101. package/src/Listbox/Listbox.stories.tsx +205 -0
  102. package/src/Listbox/Listbox.test.tsx +198 -0
  103. package/src/Listbox/Listbox.tsx +295 -0
  104. package/src/Listbox/ListboxOption.tsx +104 -0
  105. package/src/Menu/Menu.mdx +11 -13
  106. package/src/Menu/Menu.test.tsx +47 -38
  107. package/src/Menu/Menu.tsx +81 -142
  108. package/src/Modal/Modal.mdx +29 -82
  109. package/src/Modal/Modal.stories.tsx +55 -188
  110. package/src/Modal/Modal.test.tsx +44 -177
  111. package/src/Modal/Modal.tsx +54 -204
  112. package/src/Overlay/OverlayPanel.tsx +195 -0
  113. package/src/Overlay/OverlayStack.test.tsx +235 -0
  114. package/src/Overlay/OverlayStack.tsx +132 -0
  115. package/src/Overlay/SharedBackdrop.tsx +40 -0
  116. package/src/Overlay/overlayPanelClass.ts +17 -0
  117. package/src/Overlay/useAnchoredOverlay.ts +197 -0
  118. package/src/Popover/Popover.mdx +17 -20
  119. package/src/Popover/Popover.stories.tsx +33 -0
  120. package/src/Popover/Popover.test.tsx +68 -32
  121. package/src/Popover/Popover.tsx +56 -95
  122. package/src/Tooltip/Tooltip.mdx +12 -9
  123. package/src/Tooltip/Tooltip.test.tsx +17 -19
  124. package/src/Tooltip/Tooltip.tsx +35 -32
  125. package/src/icons.storyHelpers.tsx +22 -0
  126. package/src/index.ts +22 -4
  127. package/src/mountStory.testHelpers.ts +25 -1
  128. package/src/sourceRules.test.ts +26 -1
  129. package/src/storyControls.test.ts +25 -12
  130. package/dist/Modal/lockScrollBehind.d.ts.map +0 -1
  131. package/dist/Modal/lockScrollBehind.js.map +0 -1
  132. /package/dist/{Modal → Overlay}/lockScrollBehind.d.ts +0 -0
  133. /package/dist/{Modal → Overlay}/lockScrollBehind.js +0 -0
  134. /package/src/{Modal → Overlay}/lockScrollBehind.ts +0 -0
@@ -0,0 +1,52 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useUniqueId } from "@charcuterie/logic";
3
+ import { Button } from "../Button/Button.js";
4
+ import { Modal } from "../Modal/Modal.js";
5
+ import { toClassName } from "../toClassName.js";
6
+ /**
7
+ * `85dvh` rather than `85vh` — on a phone the two differ by the
8
+ * address bar, and a dialog whose footer is under it is a dialog
9
+ * with no buttons.
10
+ */
11
+ const SIZE_CLASS = {
12
+ sm: "max-h-[85dvh] w-full max-w-sm",
13
+ md: "max-h-[85dvh] w-full max-w-md",
14
+ lg: "max-h-[85dvh] w-full max-w-2xl",
15
+ // M5 added this, because the scale jumped from 42rem straight to
16
+ // the whole viewport and rip-deck's capture tail lands in the
17
+ // gap: a MakeMKV robot log is fixed-width `MSG:5072,0,1,"…"`
18
+ // lines that wrap badly at 42rem, and a full-screen dialog over a
19
+ // nine-bay dashboard hides the thing being diagnosed. 56rem is
20
+ // what that app's hand-rolled `<dialog>` used before the swap.
21
+ xl: "max-h-[85dvh] w-full max-w-4xl",
22
+ full: "h-[100dvh] max-h-[100dvh] w-full max-w-full rounded-none",
23
+ };
24
+ /**
25
+ * `Modal` plus the chrome — heading, a Close button, a scrolling body,
26
+ * an optional footer, and five sizes. This is what `Modal` *was*
27
+ * before the M8 rebuild renamed it: a dialog is one kind of modal, so
28
+ * it takes the specific name and hands the generic one back to the
29
+ * base layer.
30
+ *
31
+ * Everything modal — the portal, the scrim, Escape and outside-press
32
+ * dismiss, the focus trap, the scroll lock, the stacking — is `Modal`'s
33
+ * (via `OverlayPanel`), so this component is just the box's contents
34
+ * and the size class it wants that box to be. `SIZE_CLASS` is composed
35
+ * over `Modal`'s default surface, which is why `full` carries
36
+ * `rounded-none` to square off the base's `rounded-lg` at the viewport
37
+ * edge.
38
+ *
39
+ * ### The accessible name
40
+ *
41
+ * `heading` is required and is a string, not a `ReactNode`. It names
42
+ * both the visible header and — through `aria-labelledby` on the
43
+ * modal — the dialog itself. A dialog with no name is announced as
44
+ * "dialog" and nothing else, which is what every hand-rolled modal in
45
+ * the inventory does.
46
+ */
47
+ export const Dialog = ({ children, className, footer, heading, headingLevel = 2, isDismissable = true, isVisible, onClose, size = "md", }) => {
48
+ const headingId = useUniqueId();
49
+ const Heading = `h${headingLevel}`;
50
+ return (_jsxs(Modal, { "aria-labelledby": headingId, className: toClassName(SIZE_CLASS[size], className), isDismissable: isDismissable, isVisible: isVisible, onClose: onClose, children: [_jsxs("header", { className: "flex shrink-0 items-start justify-between gap-3 border-b border-border-subtle p-4", children: [_jsx(Heading, { className: "font-semibold text-lg leading-tight", id: headingId, children: heading }), isDismissable ? (_jsx(Button, { appearance: "ghost", intent: "neutral", onClick: onClose, size: "sm", children: "Close" })) : null] }), _jsx("div", { className: "min-h-0 flex-1 overflow-auto p-4", children: children }), footer ? (_jsx("footer", { className: "flex shrink-0 flex-wrap justify-end gap-2 border-t border-border-subtle p-4", children: footer })) : null] }));
51
+ };
52
+ //# sourceMappingURL=Dialog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Dialog.js","sourceRoot":"","sources":["../../src/Dialog/Dialog.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAGhD,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAsB,CAAA;AAC7C,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAoB,CAAA;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAyB/C;;;;GAIG;AACH,MAAM,UAAU,GAA+B;IAC7C,EAAE,EAAE,+BAA+B;IACnC,EAAE,EAAE,+BAA+B;IACnC,EAAE,EAAE,gCAAgC;IACpC,iEAAiE;IACjE,8DAA8D;IAC9D,6DAA6D;IAC7D,kEAAkE;IAClE,+DAA+D;IAC/D,+DAA+D;IAC/D,EAAE,EAAE,gCAAgC;IACpC,IAAI,EAAE,0DAA0D;CACjE,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,EACrB,QAAQ,EACR,SAAS,EACT,MAAM,EACN,OAAO,EACP,YAAY,GAAG,CAAC,EAChB,aAAa,GAAG,IAAI,EACpB,SAAS,EACT,OAAO,EACP,IAAI,GAAG,IAAI,GACC,EAAa,EAAE;IAC3B,MAAM,SAAS,GAAG,WAAW,EAAE,CAAA;IAE/B,MAAM,OAAO,GAAG,IAAI,YAAY,EAAW,CAAA;IAE3C,OAAO,CACL,MAAC,KAAK,uBACa,SAAS,EAC1B,SAAS,EAAE,WAAW,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,SAAS,CAAC,EACnD,aAAa,EAAE,aAAa,EAC5B,SAAS,EAAE,SAAS,EACpB,OAAO,EAAE,OAAO,aAEhB,kBAAQ,SAAS,EAAC,mFAAmF,aACnG,KAAC,OAAO,IACN,SAAS,EAAC,qCAAqC,EAC/C,EAAE,EAAE,SAAS,YAEZ,OAAO,GACA,EAET,aAAa,CAAC,CAAC,CAAC,CACf,KAAC,MAAM,IACL,UAAU,EAAC,OAAO,EAClB,MAAM,EAAC,SAAS,EAChB,OAAO,EAAE,OAAO,EAChB,IAAI,EAAC,IAAI,sBAGF,CACV,CAAC,CAAC,CAAC,IAAI,IACD,EAET,cAAK,SAAS,EAAC,kCAAkC,YAC9C,QAAQ,GACL,EAEL,MAAM,CAAC,CAAC,CAAC,CACR,iBAAQ,SAAS,EAAC,6EAA6E,YAC5F,MAAM,GACA,CACV,CAAC,CAAC,CAAC,IAAI,IACF,CACT,CAAA;AACH,CAAC,CAAA"}
@@ -0,0 +1,57 @@
1
+ import type { ReactNode } from "react";
2
+ export type LightboxProps = {
3
+ /**
4
+ * Required, and the whole reason this is not a bare `<img>` in a
5
+ * dialog. It names both the enlarged image and — via `heading`'s
6
+ * default below — the dialog itself, and a poster grid of unnamed
7
+ * images is unnavigable rather than merely imperfect. Same rule
8
+ * `MediaTile` makes.
9
+ */
10
+ alt: string;
11
+ /** Shown under the enlarged image. A year, a source, a filename. */
12
+ caption?: ReactNode;
13
+ /** Applied to the built-in trigger button, not the dialog. */
14
+ className?: string;
15
+ /**
16
+ * The dialog's accessible name and visible heading. Defaults to
17
+ * `alt`, which is right when the thumbnail's alt is already the
18
+ * title ("THE OUTFIT poster"); pass it when the two should differ.
19
+ */
20
+ heading?: string;
21
+ /**
22
+ * **Controlled** open state. Omit for the uncontrolled default,
23
+ * where the trigger owns it through `useVisibility`. Present means
24
+ * the caller owns it and the built-in trigger becomes a plain
25
+ * button that only *reports* intent through `onOpenChange`.
26
+ */
27
+ isOpen?: boolean;
28
+ onOpenChange?: (isOpen: boolean) => void;
29
+ src: string;
30
+ /**
31
+ * The clickable thumbnail — a smaller `<img>`, usually. Omitted in
32
+ * the controlled, trigger-elsewhere case, where something else in
33
+ * the page opens the lightbox and this renders only the overlay.
34
+ */
35
+ thumbnail?: ReactNode;
36
+ };
37
+ /**
38
+ * A thumbnail that opens its own full-size view — the one image
39
+ * interaction three apps hand-roll and none get the focus trap or
40
+ * the scroll lock right. It is a **skin over `Dialog`**, not a second
41
+ * dialog: `Dialog` (through `Modal`/`OverlayPanel`) owns the portal,
42
+ * the scrim, Escape, the focus trap and `lockScrollBehind`, and
43
+ * everything that makes those correct in every browser the fleet
44
+ * runs. What this adds is the image-specific part — a `cursor-zoom-in`
45
+ * trigger and an `object-contain` image clamped to the viewport so a
46
+ * 2:3 poster and a 16:9 still both land whole.
47
+ *
48
+ * ### Controlled and not, one component
49
+ *
50
+ * `useVisibility` runs unconditionally — hooks cannot be conditional
51
+ * — but is *consulted* only when `isOpen` is absent. A caller that
52
+ * passes `isOpen` owns the truth; the internal store is then an
53
+ * unread two-field object, which is cheaper than splitting this into
54
+ * two components that share a Modal body.
55
+ */
56
+ export declare const Lightbox: ({ alt, caption, className, heading, isOpen, onOpenChange, src, thumbnail, }: LightboxProps) => ReactNode;
57
+ //# sourceMappingURL=Lightbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Lightbox.d.ts","sourceRoot":"","sources":["../../src/Lightbox/Lightbox.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAKtC,MAAM,MAAM,aAAa,GAAG;IAC1B;;;;;;OAMG;IACH,GAAG,EAAE,MAAM,CAAA;IACX,oEAAoE;IACpE,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAA;IACxC,GAAG,EAAE,MAAM,CAAA;IACX;;;;OAIG;IACH,SAAS,CAAC,EAAE,SAAS,CAAA;CACtB,CAAA;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,QAAQ,GAAI,6EAStB,aAAa,KAAG,SAwElB,CAAA"}
@@ -0,0 +1,50 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useVisibility } from "@charcuterie/logic";
3
+ import { Dialog } from "../Dialog/Dialog.js";
4
+ import { toClassName } from "../toClassName.js";
5
+ /**
6
+ * A thumbnail that opens its own full-size view — the one image
7
+ * interaction three apps hand-roll and none get the focus trap or
8
+ * the scroll lock right. It is a **skin over `Dialog`**, not a second
9
+ * dialog: `Dialog` (through `Modal`/`OverlayPanel`) owns the portal,
10
+ * the scrim, Escape, the focus trap and `lockScrollBehind`, and
11
+ * everything that makes those correct in every browser the fleet
12
+ * runs. What this adds is the image-specific part — a `cursor-zoom-in`
13
+ * trigger and an `object-contain` image clamped to the viewport so a
14
+ * 2:3 poster and a 16:9 still both land whole.
15
+ *
16
+ * ### Controlled and not, one component
17
+ *
18
+ * `useVisibility` runs unconditionally — hooks cannot be conditional
19
+ * — but is *consulted* only when `isOpen` is absent. A caller that
20
+ * passes `isOpen` owns the truth; the internal store is then an
21
+ * unread two-field object, which is cheaper than splitting this into
22
+ * two components that share a Modal body.
23
+ */
24
+ export const Lightbox = ({ alt, caption, className, heading, isOpen, onOpenChange, src, thumbnail, }) => {
25
+ const isControlled = isOpen !== undefined;
26
+ const { hide, isVisible, show } = useVisibility();
27
+ const isShown = isControlled ? isOpen : isVisible;
28
+ const requestOpen = () => {
29
+ if (!isControlled) {
30
+ show();
31
+ }
32
+ onOpenChange?.(true);
33
+ };
34
+ const requestClose = () => {
35
+ if (!isControlled) {
36
+ hide();
37
+ }
38
+ onOpenChange?.(false);
39
+ };
40
+ return (_jsxs(_Fragment, { children: [thumbnail === undefined ? null : (_jsx("button", { "aria-label": `Enlarge ${heading ?? alt}`, className: toClassName(
41
+ // `cursor-zoom-in` is the affordance a bare `<img>`
42
+ // never had, and the focus ring is the fleet's standard
43
+ // — the same one `MediaTile`'s link wears.
44
+ "group block cursor-zoom-in rounded-md outline-offset-2 focus-visible:outline-solid focus-visible:outline-(length:--focus-ring-width) focus-visible:outline-focus-ring", className), onClick: requestOpen, type: "button", children: thumbnail })), _jsx(Dialog, { heading: heading ?? alt, isVisible: isShown, onClose: requestClose, size: "xl", children: _jsxs("figure", { className: "flex flex-col items-center gap-3", children: [_jsx("img", { alt: alt,
45
+ // `object-contain`, not `cover`: the enlarged view must
46
+ // show the whole image, and the clamp is `dvh` for the
47
+ // same address-bar reason `Modal`'s is.
48
+ className: "max-h-[75dvh] w-auto max-w-full rounded-md object-contain", src: src }), caption ? (_jsx("figcaption", { className: "text-center text-content-muted text-sm", children: caption })) : null] }) })] }));
49
+ };
50
+ //# sourceMappingURL=Lightbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Lightbox.js","sourceRoot":"","sources":["../../src/Lightbox/Lightbox.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAGlD,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAsB,CAAA;AAC7C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAsC/C;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,EACvB,GAAG,EACH,OAAO,EACP,SAAS,EACT,OAAO,EACP,MAAM,EACN,YAAY,EACZ,GAAG,EACH,SAAS,GACK,EAAa,EAAE;IAC7B,MAAM,YAAY,GAAG,MAAM,KAAK,SAAS,CAAA;IAEzC,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,aAAa,EAAE,CAAA;IAEjD,MAAM,OAAO,GAAG,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAA;IAEjD,MAAM,WAAW,GAAG,GAAG,EAAE;QACvB,IAAI,CAAC,YAAY,EAAE,CAAC;YAClB,IAAI,EAAE,CAAA;QACR,CAAC;QAED,YAAY,EAAE,CAAC,IAAI,CAAC,CAAA;IACtB,CAAC,CAAA;IAED,MAAM,YAAY,GAAG,GAAG,EAAE;QACxB,IAAI,CAAC,YAAY,EAAE,CAAC;YAClB,IAAI,EAAE,CAAA;QACR,CAAC;QAED,YAAY,EAAE,CAAC,KAAK,CAAC,CAAA;IACvB,CAAC,CAAA;IAED,OAAO,CACL,8BACG,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAChC,+BAMc,WAAW,OAAO,IAAI,GAAG,EAAE,EACvC,SAAS,EAAE,WAAW;gBACpB,oDAAoD;gBACpD,wDAAwD;gBACxD,2CAA2C;gBAC3C,uKAAuK,EACvK,SAAS,CACV,EACD,OAAO,EAAE,WAAW,EACpB,IAAI,EAAC,QAAQ,YAEZ,SAAS,GACH,CACV,EAED,KAAC,MAAM,IACL,OAAO,EAAE,OAAO,IAAI,GAAG,EACvB,SAAS,EAAE,OAAO,EAClB,OAAO,EAAE,YAAY,EACrB,IAAI,EAAC,IAAI,YAET,kBAAQ,SAAS,EAAC,kCAAkC,aAClD,cACE,GAAG,EAAE,GAAG;4BACR,wDAAwD;4BACxD,uDAAuD;4BACvD,wCAAwC;4BACxC,SAAS,EAAC,2DAA2D,EACrE,GAAG,EAAE,GAAG,GACR,EAED,OAAO,CAAC,CAAC,CAAC,CACT,qBAAY,SAAS,EAAC,wCAAwC,YAC3D,OAAO,GACG,CACd,CAAC,CAAC,CAAC,IAAI,IACD,GACF,IACR,CACJ,CAAA;AACH,CAAC,CAAA"}
@@ -0,0 +1,56 @@
1
+ import type { Placement } from "@floating-ui/react";
2
+ import type { ReactElement, ReactNode } from "react";
3
+ export type ListboxItem = {
4
+ isDisabled?: boolean;
5
+ label: ReactNode;
6
+ /**
7
+ * Required when `label` is rich (an icon, two lines, a badge): it is
8
+ * the type-ahead target and the accessible name a screen reader
9
+ * reads. When `label` is a plain string it is used as the fallback.
10
+ */
11
+ textValue?: string;
12
+ value: string;
13
+ };
14
+ export type ListboxProps = {
15
+ className?: string;
16
+ isVisible: boolean;
17
+ /** Outside press and Escape both land here. */
18
+ onDismiss: () => void;
19
+ onSelect: (value: string) => void;
20
+ options: readonly ListboxItem[];
21
+ placement?: Placement;
22
+ /** The initial seed. The live selection is the listbox's after that. */
23
+ selectedValue?: string;
24
+ /** The control the listbox hangs off. **Cloned, not wrapped.** */
25
+ trigger: ReactElement;
26
+ };
27
+ /**
28
+ * The single-select fancy dropdown — the rich-option sibling of the
29
+ * native `Select`. `Select` keeps type-ahead, the mobile wheel, form
30
+ * submission and autofill; `Listbox` is for the cases `<option>`
31
+ * cannot render: an icon, two lines, a trailing badge.
32
+ *
33
+ * A listbox is **not** a menu, and that distinction is the whole
34
+ * reason it is a separate component from `Menu`. An `option` *is*
35
+ * something you are choosing — so it carries `aria-selected`, it
36
+ * type-aheads (which the APG requires of a listbox and `Menu`
37
+ * deliberately omits), and a screen reader announces "listbox,
38
+ * selected, 2 of 4". A `menuitem` only ever *does* something.
39
+ *
40
+ * ### Two kinds of state, composed
41
+ *
42
+ * `useRovingFocus` owns which option is tabbable (exactly one, so Tab
43
+ * enters and leaves the group once and the arrow keys move within it),
44
+ * and `useSinglePicker` owns which option is chosen. They are the same
45
+ * two kinds `Tabs` composes, and they never disturb each other:
46
+ * arrowing moves focus without selecting, Enter selects the focused
47
+ * option.
48
+ *
49
+ * ### Portalled, and named by its trigger
50
+ *
51
+ * Like `Menu`, the panel portals to `document.body` and `useRole` names
52
+ * it by pointing `aria-labelledby` at the trigger — so there is no
53
+ * `heading` prop; the button that opens it is its label.
54
+ */
55
+ export declare const Listbox: ({ className, isVisible, onDismiss, onSelect, options, placement, selectedValue, trigger, }: ListboxProps) => ReactNode;
56
+ //# sourceMappingURL=Listbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Listbox.d.ts","sourceRoot":"","sources":["../../src/Listbox/Listbox.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAKnD,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAQpD,MAAM,MAAM,WAAW,GAAG;IACxB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,KAAK,EAAE,SAAS,CAAA;IAChB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,OAAO,CAAA;IAClB,+CAA+C;IAC/C,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACjC,OAAO,EAAE,SAAS,WAAW,EAAE,CAAA;IAC/B,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,wEAAwE;IACxE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,kEAAkE;IAClE,OAAO,EAAE,YAAY,CAAA;CACtB,CAAA;AAQD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,eAAO,MAAM,OAAO,GAAI,4FASrB,YAAY,KAAG,SA+MjB,CAAA"}
@@ -0,0 +1,151 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { selectTabIndex, useRovingFocus, useSinglePicker, } from "@charcuterie/logic";
3
+ import { FloatingFocusManager, FloatingPortal, } from "@floating-ui/react";
4
+ import { useEffect, useRef } from "react";
5
+ import { PANEL_SURFACE_CLASS } from "../Overlay/overlayPanelClass.js";
6
+ import { useAnchoredOverlay } from "../Overlay/useAnchoredOverlay.js";
7
+ import { toClassName } from "../toClassName.js";
8
+ import { ListboxOption } from "./ListboxOption.js";
9
+ const getOptionText = (item) => (item.textValue ??
10
+ (typeof item.label === "string" ? item.label : "")).toLowerCase();
11
+ /**
12
+ * The single-select fancy dropdown — the rich-option sibling of the
13
+ * native `Select`. `Select` keeps type-ahead, the mobile wheel, form
14
+ * submission and autofill; `Listbox` is for the cases `<option>`
15
+ * cannot render: an icon, two lines, a trailing badge.
16
+ *
17
+ * A listbox is **not** a menu, and that distinction is the whole
18
+ * reason it is a separate component from `Menu`. An `option` *is*
19
+ * something you are choosing — so it carries `aria-selected`, it
20
+ * type-aheads (which the APG requires of a listbox and `Menu`
21
+ * deliberately omits), and a screen reader announces "listbox,
22
+ * selected, 2 of 4". A `menuitem` only ever *does* something.
23
+ *
24
+ * ### Two kinds of state, composed
25
+ *
26
+ * `useRovingFocus` owns which option is tabbable (exactly one, so Tab
27
+ * enters and leaves the group once and the arrow keys move within it),
28
+ * and `useSinglePicker` owns which option is chosen. They are the same
29
+ * two kinds `Tabs` composes, and they never disturb each other:
30
+ * arrowing moves focus without selecting, Enter selects the focused
31
+ * option.
32
+ *
33
+ * ### Portalled, and named by its trigger
34
+ *
35
+ * Like `Menu`, the panel portals to `document.body` and `useRole` names
36
+ * it by pointing `aria-labelledby` at the trigger — so there is no
37
+ * `heading` prop; the button that opens it is its label.
38
+ */
39
+ export const Listbox = ({ className, isVisible, onDismiss, onSelect, options, placement = "bottom-start", selectedValue, trigger, }) => {
40
+ const optionElements = useRef(new Map());
41
+ const typeahead = useRef({ buffer: "", stampMs: 0 });
42
+ const focus = useRovingFocus();
43
+ const picker = useSinglePicker({ selectedValue });
44
+ const { activeValue, registeredValues, setActiveValue } = focus;
45
+ const [firstValue] = registeredValues;
46
+ const { clonedTrigger, context, floatingStyles, getFloatingProps, setFloating, triggerId, } = useAnchoredOverlay({
47
+ isVisible,
48
+ maxHeightPx: 384,
49
+ offsetValue: 4,
50
+ onDismiss,
51
+ placement,
52
+ role: "listbox",
53
+ trigger,
54
+ });
55
+ const chooseValue = (value) => {
56
+ picker.select(value);
57
+ onSelect(value);
58
+ // A single choice closes, the way the native `<select>` it stands
59
+ // in for does — the consumer's `onDismiss` is the same one an
60
+ // outside press and Escape use.
61
+ onDismiss();
62
+ };
63
+ /**
64
+ * Opening lands the focus on the chosen option, or the first when
65
+ * nothing is chosen yet — the APG rule for a listbox, and the reason
66
+ * a reopened one resumes on the current value rather than the top.
67
+ */
68
+ useEffect(() => {
69
+ if (!isVisible) {
70
+ return;
71
+ }
72
+ const seed = picker.selectedValue ?? firstValue;
73
+ if (seed !== undefined && seed !== null) {
74
+ setActiveValue(seed);
75
+ }
76
+ }, [
77
+ firstValue,
78
+ isVisible,
79
+ picker.selectedValue,
80
+ setActiveValue,
81
+ ]);
82
+ useEffect(() => {
83
+ if (!isVisible || activeValue === null) {
84
+ return;
85
+ }
86
+ optionElements.current.get(activeValue)?.focus();
87
+ }, [activeValue, isVisible]);
88
+ const runTypeahead = (character) => {
89
+ // Reset the buffer once the typing pauses, so "sk" then a beat
90
+ // then "e" is two searches rather than "ske".
91
+ const now = Date.now();
92
+ typeahead.current.buffer =
93
+ now - typeahead.current.stampMs > 500
94
+ ? character
95
+ : typeahead.current.buffer + character;
96
+ typeahead.current.stampMs = now;
97
+ const query = typeahead.current.buffer.toLowerCase();
98
+ const match = options.find((option) => !option.isDisabled &&
99
+ getOptionText(option).startsWith(query));
100
+ if (match) {
101
+ setActiveValue(match.value);
102
+ }
103
+ };
104
+ return (_jsxs(_Fragment, { children: [clonedTrigger, isVisible ? (_jsx(FloatingPortal, { children: _jsx(FloatingFocusManager, { context: context, initialFocus: -1, modal: false, children: _jsx("div", { ...getFloatingProps(), "aria-labelledby": triggerId, className: toClassName(PANEL_SURFACE_CLASS,
105
+ // The height is clamped to the viewport by the `size`
106
+ // middleware (`maxHeightPx`), written as an inline
107
+ // style; this scrolls within it.
108
+ "z-[var(--layer-modal)] flex min-w-48 flex-col gap-0.5 overflow-auto p-1", className), onKeyDown: (keyEvent) => {
109
+ const commands = {
110
+ ArrowDown: focus.next,
111
+ ArrowUp: focus.previous,
112
+ End: focus.last,
113
+ Home: focus.first,
114
+ };
115
+ const command = commands[keyEvent.key];
116
+ if (command) {
117
+ keyEvent.preventDefault();
118
+ command();
119
+ return;
120
+ }
121
+ if ((keyEvent.key === "Enter" ||
122
+ keyEvent.key === " ") &&
123
+ activeValue !== null) {
124
+ keyEvent.preventDefault();
125
+ chooseValue(activeValue);
126
+ return;
127
+ }
128
+ // Type-ahead: a single printable character with no
129
+ // modifier is a search, not a shortcut.
130
+ if (keyEvent.key.length === 1 &&
131
+ !keyEvent.altKey &&
132
+ !keyEvent.ctrlKey &&
133
+ !keyEvent.metaKey) {
134
+ runTypeahead(keyEvent.key);
135
+ }
136
+ }, ref: setFloating,
137
+ // Duplicated from `getFloatingProps()` on purpose, and
138
+ // the two cannot differ — `useRole(context, { role:
139
+ // "listbox" })` in the hook is where it comes from.
140
+ // Stated here because a linter cannot see a role through
141
+ // a spread.
142
+ role: "listbox", style: floatingStyles, children: options.map((option) => (_jsx(ListboxOption, { isSelected: picker.selectedValue === option.value, item: option, onSelect: chooseValue, registerFocus: focus.register, registerSelection: picker.register, tabIndex: selectTabIndex(focus, option.value), trackElement: (value, element) => {
143
+ if (element) {
144
+ optionElements.current.set(value, element);
145
+ }
146
+ else {
147
+ optionElements.current.delete(value);
148
+ }
149
+ } }, option.value))) }) }) })) : null] }));
150
+ };
151
+ //# sourceMappingURL=Listbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Listbox.js","sourceRoot":"","sources":["../../src/Listbox/Listbox.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import type { ReactNode } from "react";
2
+ import type { ListboxItem } from "./Listbox.tsx";
3
+ export type ListboxOptionProps = {
4
+ isSelected: boolean;
5
+ item: ListboxItem;
6
+ onSelect: (value: string) => void;
7
+ /** `RovingFocus.register` — membership of the arrow-key group. */
8
+ registerFocus: (value: string) => () => void;
9
+ /** `SinglePicker.register` — membership of the choice. */
10
+ registerSelection: (value: string) => () => void;
11
+ tabIndex: number;
12
+ trackElement: (value: string, element: HTMLButtonElement | null) => void;
13
+ };
14
+ /**
15
+ * Its own file for the same reason as `MenuAction` and `TabTrigger`:
16
+ * **both registrations are effects**, and an effect cannot run in a
17
+ * loop.
18
+ *
19
+ * The two registrations are separate on purpose, exactly as
20
+ * `TabTrigger` splits them. A disabled option joins the *choice* — it
21
+ * can still be the seeded `selectedValue` and still announces
22
+ * `aria-selected` — and stays out of the *focus* group, so the arrow
23
+ * keys and type-ahead skip it without `RovingFocus` having to know
24
+ * what "disabled" means.
25
+ *
26
+ * ### `option`, not `menuitem`
27
+ *
28
+ * An `option` **is** something — the value being chosen — where a
29
+ * `menuitem` **does** something. So this carries `aria-selected`
30
+ * (which a menuitem must not), and a screen reader announces
31
+ * "listbox, selected, 2 of 4" rather than "menu, 4 items". It is a
32
+ * `<button>` with `role="option"` so it is natively focusable for the
33
+ * roving-tabindex move, the same shape `Menu` uses.
34
+ */
35
+ export declare const ListboxOption: ({ isSelected, item, onSelect, registerFocus, registerSelection, tabIndex, trackElement, }: ListboxOptionProps) => ReactNode;
36
+ //# sourceMappingURL=ListboxOption.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ListboxOption.d.ts","sourceRoot":"","sources":["../../src/Listbox/ListboxOption.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAKtC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAEhD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,UAAU,EAAE,OAAO,CAAA;IACnB,IAAI,EAAE,WAAW,CAAA;IACjB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACjC,kEAAkE;IAClE,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,IAAI,CAAA;IAC5C,0DAA0D;IAC1D,iBAAiB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,IAAI,CAAA;IAChD,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,CACZ,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,iBAAiB,GAAG,IAAI,KAC9B,IAAI,CAAA;CACV,CAAA;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,aAAa,GAAI,2FAQ3B,kBAAkB,KAAG,SAoDvB,CAAA"}
@@ -0,0 +1,42 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect } from "react";
3
+ import { FOCUS_RING_CLASS } from "../intentStyles.js";
4
+ import { toClassName } from "../toClassName.js";
5
+ /**
6
+ * Its own file for the same reason as `MenuAction` and `TabTrigger`:
7
+ * **both registrations are effects**, and an effect cannot run in a
8
+ * loop.
9
+ *
10
+ * The two registrations are separate on purpose, exactly as
11
+ * `TabTrigger` splits them. A disabled option joins the *choice* — it
12
+ * can still be the seeded `selectedValue` and still announces
13
+ * `aria-selected` — and stays out of the *focus* group, so the arrow
14
+ * keys and type-ahead skip it without `RovingFocus` having to know
15
+ * what "disabled" means.
16
+ *
17
+ * ### `option`, not `menuitem`
18
+ *
19
+ * An `option` **is** something — the value being chosen — where a
20
+ * `menuitem` **does** something. So this carries `aria-selected`
21
+ * (which a menuitem must not), and a screen reader announces
22
+ * "listbox, selected, 2 of 4" rather than "menu, 4 items". It is a
23
+ * `<button>` with `role="option"` so it is natively focusable for the
24
+ * roving-tabindex move, the same shape `Menu` uses.
25
+ */
26
+ export const ListboxOption = ({ isSelected, item, onSelect, registerFocus, registerSelection, tabIndex, trackElement, }) => {
27
+ const { isDisabled = false, label, value } = item;
28
+ useEffect(() => registerSelection(value), [registerSelection, value]);
29
+ useEffect(() => {
30
+ if (isDisabled) {
31
+ return;
32
+ }
33
+ return registerFocus(value);
34
+ }, [isDisabled, registerFocus, value]);
35
+ return (_jsxs("button", { "aria-selected": isSelected, className: toClassName("flex w-full cursor-pointer items-center justify-between gap-2 rounded-sm bg-transparent px-2 py-1.5 text-start text-content-primary text-sm transition-colors duration-(--duration-fast) ease-standard", "hover:bg-intent-neutral-surface", isSelected && "bg-intent-accent-surface", isDisabled &&
36
+ "cursor-not-allowed text-content-disabled hover:bg-transparent", FOCUS_RING_CLASS), disabled: isDisabled, onClick: () => {
37
+ onSelect(value);
38
+ }, ref: (element) => {
39
+ trackElement(value, element);
40
+ }, role: "option", tabIndex: tabIndex, type: "button", children: [label, isSelected ? (_jsx("span", { "aria-hidden": "true", className: "shrink-0 text-intent-accent-content", children: "\u2713" })) : null] }));
41
+ };
42
+ //# sourceMappingURL=ListboxOption.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ListboxOption.js","sourceRoot":"","sources":["../../src/Listbox/ListboxOption.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEjC,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAkB/C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,EAC5B,UAAU,EACV,IAAI,EACJ,QAAQ,EACR,aAAa,EACb,iBAAiB,EACjB,QAAQ,EACR,YAAY,GACO,EAAa,EAAE;IAClC,MAAM,EAAE,UAAU,GAAG,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,IAAI,CAAA;IAEjD,SAAS,CACP,GAAG,EAAE,CAAC,iBAAiB,CAAC,KAAK,CAAC,EAC9B,CAAC,iBAAiB,EAAE,KAAK,CAAC,CAC3B,CAAA;IAED,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,UAAU,EAAE,CAAC;YACf,OAAM;QACR,CAAC;QAED,OAAO,aAAa,CAAC,KAAK,CAAC,CAAA;IAC7B,CAAC,EAAE,CAAC,UAAU,EAAE,aAAa,EAAE,KAAK,CAAC,CAAC,CAAA;IAEtC,OAAO,CACL,mCACiB,UAAU,EACzB,SAAS,EAAE,WAAW,CACpB,wMAAwM,EACxM,iCAAiC,EACjC,UAAU,IAAI,0BAA0B,EACxC,UAAU;YACR,+DAA+D,EACjE,gBAAgB,CACjB,EACD,QAAQ,EAAE,UAAU,EACpB,OAAO,EAAE,GAAG,EAAE;YACZ,QAAQ,CAAC,KAAK,CAAC,CAAA;QACjB,CAAC,EACD,GAAG,EAAE,CAAC,OAAO,EAAE,EAAE;YACf,YAAY,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;QAC9B,CAAC,EACD,IAAI,EAAC,QAAQ,EACb,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAC,QAAQ,aAEZ,KAAK,EAEL,UAAU,CAAC,CAAC,CAAC,CACZ,8BAGc,MAAM,EAClB,SAAS,EAAC,qCAAqC,uBAG1C,CACR,CAAC,CAAC,CAAC,IAAI,IACD,CACV,CAAA;AACH,CAAC,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"Menu.d.ts","sourceRoot":"","sources":["../../src/Menu/Menu.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAYnD,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAMpD,MAAM,MAAM,QAAQ,GAAG;IACrB,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,SAAS,CAAA;IAChB,QAAQ,EAAE,MAAM,IAAI,CAAA;CACrB,CAAA;AAED,MAAM,MAAM,SAAS,GAAG;IACtB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,OAAO,CAAA;IAClB,KAAK,EAAE,QAAQ,EAAE,CAAA;IACjB,iEAAiE;IACjE,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,+DAA+D;IAC/D,OAAO,EAAE,YAAY,CAAA;CACtB,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2DG;AACH,eAAO,MAAM,IAAI,GAAI,iEAOlB,SAAS,KAAG,SA8Ld,CAAA"}
1
+ {"version":3,"file":"Menu.d.ts","sourceRoot":"","sources":["../../src/Menu/Menu.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAKnD,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAQpD,MAAM,MAAM,QAAQ,GAAG;IACrB,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,SAAS,CAAA;IAChB,QAAQ,EAAE,MAAM,IAAI,CAAA;CACrB,CAAA;AAED,MAAM,MAAM,SAAS,GAAG;IACtB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,OAAO,CAAA;IAClB,KAAK,EAAE,QAAQ,EAAE,CAAA;IACjB,iEAAiE;IACjE,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,+DAA+D;IAC/D,OAAO,EAAE,YAAY,CAAA;CACtB,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2DG;AACH,eAAO,MAAM,IAAI,GAAI,iEAOlB,SAAS,KAAG,SAuId,CAAA"}