@handamade/psi-react 0.7.1 → 0.8.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dmitry Kurkin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @handamade/psi-react
2
2
 
3
- 15 React 19 components consuming @handamade/psi-tokens. Zero runtime dependencies, CSS Modules, full TypeScript support.
3
+ 18 React 19 components consuming @handamade/psi-tokens. Zero runtime dependencies, CSS Modules, full TypeScript support.
4
4
 
5
5
  ## Installation
6
6
 
@@ -0,0 +1,57 @@
1
+ import type { ReactElement, ReactNode, Ref } from "react";
2
+ export type MenuPlacement = "bottom-start" | "bottom-end" | "top-start" | "top-end";
3
+ export interface MenuContextValue {
4
+ close: (reason: "item-select") => void;
5
+ }
6
+ export declare const MenuContext: import("react").Context<MenuContextValue>;
7
+ export interface MenuProps {
8
+ /** Controlled open state; syncs to showPopover()/hidePopover(). */
9
+ open: boolean;
10
+ /**
11
+ * Called when a dismissal is *requested*; the consumer flips `open`.
12
+ *
13
+ * Menu never closes itself on `"esc"` or `"item-select"` — the popover stays
14
+ * open until `open` becomes false (D50). `"outside"` is the one exception:
15
+ * light dismiss is performed by the browser before it tells us, so by the
16
+ * time that reason is reported the popover is already closed. Flipping
17
+ * `open` to false is still required, to keep React's state in step.
18
+ */
19
+ onClose: (reason: "esc" | "outside" | "item-select") => void;
20
+ /**
21
+ * The trigger element. Must be a single React element that spreads unknown
22
+ * props onto a focusable node (a Psi `Button`, say): Menu clones it to add
23
+ * `aria-haspopup="menu"` and `aria-expanded`, so assistive tech associates
24
+ * the menu with the control users actually focus.
25
+ */
26
+ trigger: ReactElement<Record<string, unknown>>;
27
+ /** Placement relative to the trigger. @default "bottom-start" */
28
+ placement?: MenuPlacement;
29
+ /** Accessible name for the menu when there is no visible label. */
30
+ "aria-label"?: string;
31
+ children: ReactNode;
32
+ className?: string;
33
+ /** Forwarded ref to the popover element. */
34
+ ref?: Ref<HTMLDivElement>;
35
+ }
36
+ /** Action menu on the native Popover API top layer: the top layer and light
37
+ * dismiss come from the platform; Esc, roving keyboard, placement and
38
+ * dismissal reasons are Psi's (D53). Controlled-only, like Dialog (D50).
39
+ *
40
+ * Controlled-only means Menu does not change its own visibility. Esc and
41
+ * item-select *report* a dismissal via onClose(reason) and leave the popover
42
+ * open; only the consumer flipping `open` to false actually closes it. Esc
43
+ * calls preventDefault() to suppress the platform's own popover dismissal,
44
+ * which is what makes that possible.
45
+ *
46
+ * Light dismiss (outside click) is the one asymmetry, and it is forced by the
47
+ * platform: the browser hides the popover itself and the hide-side
48
+ * `beforetoggle` is not cancelable, so the popover is already closed by the
49
+ * time the resulting `toggle` lets us report onClose("outside"). The consumer
50
+ * must still flip `open` to false so React's state matches the DOM.
51
+ *
52
+ * `toggle` therefore reports only that one reason. A close driven by the
53
+ * consumer (`open` true -> false) runs through the sync effect's own
54
+ * hidePopover(), which raises a `toggle` too — that one is suppressed, because
55
+ * a programmatic close is not a dismissal and must not call onClose. */
56
+ export declare function Menu({ open, onClose, trigger, placement, children, className, ref, ...rest }: MenuProps): import("react").JSX.Element;
57
+ //# sourceMappingURL=Menu.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Menu.d.ts","sourceRoot":"","sources":["../../src/Menu/Menu.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAK1D,MAAM,MAAM,aAAa,GAAG,cAAc,GAAG,YAAY,GAAG,WAAW,GAAG,SAAS,CAAC;AAEpF,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;CACxC;AAED,eAAO,MAAM,WAAW,2CAAuD,CAAC;AAEhF,MAAM,WAAW,SAAS;IACxB,mEAAmE;IACnE,IAAI,EAAE,OAAO,CAAC;IACd;;;;;;;;OAQG;IACH,OAAO,EAAE,CAAC,MAAM,EAAE,KAAK,GAAG,SAAS,GAAG,aAAa,KAAK,IAAI,CAAC;IAC7D;;;;;OAKG;IACH,OAAO,EAAE,YAAY,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC/C,iEAAiE;IACjE,SAAS,CAAC,EAAE,aAAa,CAAC;IAC1B,mEAAmE;IACnE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,SAAS,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,GAAG,CAAC,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;CAC3B;AAMD;;;;;;;;;;;;;;;;;;;wEAmBwE;AACxE,wBAAgB,IAAI,CAAC,EACnB,IAAI,EACJ,OAAO,EACP,OAAO,EACP,SAA0B,EAC1B,QAAQ,EACR,SAAS,EACT,GAAG,EACH,GAAG,IAAI,EACR,EAAE,SAAS,+BAyGX"}
@@ -0,0 +1,14 @@
1
+ import type { ReactNode } from "react";
2
+ export interface MenuItemProps {
3
+ children: ReactNode;
4
+ /** Fires on activation; Menu then reports the dismissal via onClose("item-select") (D50). */
5
+ onSelect: () => void;
6
+ /** danger is for destructive actions only (house rule). @default "neutral" */
7
+ variant?: "neutral" | "danger";
8
+ disabled?: boolean;
9
+ }
10
+ /** One action in a Menu. Renders a real <button> so activation, Enter and
11
+ * Space come from the platform; the roving tabindex is applied by Menu's
12
+ * keyboard hook, which finds items via [data-psi-menu-item] (D53). */
13
+ export declare function MenuItem({ children, onSelect, variant, disabled }: MenuItemProps): import("react").JSX.Element;
14
+ //# sourceMappingURL=MenuItem.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MenuItem.d.ts","sourceRoot":"","sources":["../../src/Menu/MenuItem.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAIvC,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,SAAS,CAAC;IACpB,6FAA6F;IAC7F,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,8EAA8E;IAC9E,OAAO,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IAC/B,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;sEAEsE;AACtE,wBAAgB,QAAQ,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAmB,EAAE,QAAgB,EAAE,EAAE,aAAa,+BAqBpG"}
@@ -0,0 +1,3 @@
1
+ /** Hairline rule between Menu item groups. No props by design (D53). */
2
+ export declare function MenuSeparator(): import("react").JSX.Element;
3
+ //# sourceMappingURL=MenuSeparator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MenuSeparator.d.ts","sourceRoot":"","sources":["../../src/Menu/MenuSeparator.tsx"],"names":[],"mappings":"AAEA,wEAAwE;AACxE,wBAAgB,aAAa,gCAE5B"}
@@ -0,0 +1,15 @@
1
+ import type { KeyboardEvent, RefObject } from "react";
2
+ interface Options {
3
+ popoverRef: RefObject<HTMLDivElement | null>;
4
+ triggerRef: RefObject<HTMLDivElement | null>;
5
+ open: boolean;
6
+ onEsc: () => void;
7
+ }
8
+ /** Roving-tabindex keyboard navigation for Menu (D53): Up/Down with wrap,
9
+ * Home/End, character typeahead, Esc, and focus return on close. Disabled
10
+ * items are skipped everywhere. Psi's only keyboard-navigation JS — a
11
+ * deliberate departure from D52, which refused role="toolbar" precisely to
12
+ * avoid it. */
13
+ export declare function useMenuKeyboard({ popoverRef, triggerRef, open, onEsc }: Options): (event: KeyboardEvent<HTMLDivElement>) => void;
14
+ export {};
15
+ //# sourceMappingURL=useMenuKeyboard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useMenuKeyboard.d.ts","sourceRoot":"","sources":["../../src/Menu/useMenuKeyboard.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAKtD,UAAU,OAAO;IACf,UAAU,EAAE,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IAC7C,UAAU,EAAE,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IAC7C,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED;;;;eAIe;AACf,wBAAgB,eAAe,CAAC,EAAE,UAAU,EAAE,UAAU,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,OAAO,WAsDpE,aAAa,CAAC,cAAc,CAAC,UAoExC"}
@@ -0,0 +1,18 @@
1
+ import type { RefObject } from "react";
2
+ import type { MenuPlacement } from "./Menu.js";
3
+ interface Options {
4
+ popoverRef: RefObject<HTMLDivElement | null>;
5
+ triggerRef: RefObject<HTMLDivElement | null>;
6
+ open: boolean;
7
+ placement: MenuPlacement;
8
+ anchorName: string;
9
+ }
10
+ /** Placement for Menu (D53). Above the anchor floor this hook only declares
11
+ * anchor-name / position-anchor and lets CSS position the popover. Below it,
12
+ * a top-layer element's containing block is the viewport — there is no
13
+ * declarative way to place it near its trigger — so this sets `inset` from
14
+ * the trigger rect and keeps it current. That branch is dead code above the
15
+ * anchor floor and is deletable outright once the floor rises. */
16
+ export declare function useMenuPlacement({ popoverRef, triggerRef, open, placement, anchorName }: Options): void;
17
+ export {};
18
+ //# sourceMappingURL=useMenuPlacement.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useMenuPlacement.d.ts","sourceRoot":"","sources":["../../src/Menu/useMenuPlacement.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAQ/C,UAAU,OAAO;IACf,UAAU,EAAE,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IAC7C,UAAU,EAAE,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IAC7C,IAAI,EAAE,OAAO,CAAC;IACd,SAAS,EAAE,aAAa,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;kEAKkE;AAClE,wBAAgB,gBAAgB,CAAC,EAAE,UAAU,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE,UAAU,EAAE,EAAE,OAAO,QA2ChG"}
@@ -1 +1 @@
1
- {"version":3,"file":"a11y-meta.d.ts","sourceRoot":"","sources":["../src/a11y-meta.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACpD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,eAAO,MAAM,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CA0E9C,CAAC"}
1
+ {"version":3,"file":"a11y-meta.d.ts","sourceRoot":"","sources":["../src/a11y-meta.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACpD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,eAAO,MAAM,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CA8F9C,CAAC"}
package/dist/index.d.ts CHANGED
@@ -28,5 +28,10 @@ export { Tooltip } from "./Tooltip/Tooltip.js";
28
28
  export type { TooltipProps } from "./Tooltip/Tooltip.js";
29
29
  export { Toolbar } from "./Toolbar/Toolbar.js";
30
30
  export type { ToolbarProps } from "./Toolbar/Toolbar.js";
31
+ export { Menu, MenuContext } from "./Menu/Menu.js";
32
+ export type { MenuProps, MenuPlacement, MenuContextValue } from "./Menu/Menu.js";
33
+ export { MenuItem } from "./Menu/MenuItem.js";
34
+ export type { MenuItemProps } from "./Menu/MenuItem.js";
35
+ export { MenuSeparator } from "./Menu/MenuSeparator.js";
31
36
  export { IconPlus, IconMinus, IconClose, IconCheck, IconChevronDown, IconChevronRight, IconSearch, IconSettings, IconUser, IconEdit, IconTrash, IconCopy, IconExternalLink, IconEye, IconEyeOff, IconLoader, IconArrowDown, IconArrowUpRight, IconLinkedIn, IconGitHub, IconX, IconInstagram, } from "./icons/index.js";
32
37
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AACxD,YAAY,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AACtC,YAAY,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAC3D,YAAY,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAClD,YAAY,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AACnC,YAAY,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAC/C,YAAY,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAC/C,YAAY,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EACL,QAAQ,EACR,SAAS,EACT,SAAS,EACT,SAAS,EACT,eAAe,EACf,gBAAgB,EAChB,UAAU,EACV,YAAY,EACZ,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,gBAAgB,EAChB,OAAO,EACP,UAAU,EACV,UAAU,EACV,aAAa,EACb,gBAAgB,EAChB,YAAY,EACZ,UAAU,EACV,KAAK,EACL,aAAa,GACd,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AACxD,YAAY,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AACtC,YAAY,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAC3D,YAAY,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAClD,YAAY,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AACnC,YAAY,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAC/C,YAAY,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAC/C,YAAY,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AACnD,YAAY,EAAE,SAAS,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACjF,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,YAAY,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EACL,QAAQ,EACR,SAAS,EACT,SAAS,EACT,SAAS,EACT,eAAe,EACf,gBAAgB,EAChB,UAAU,EACV,YAAY,EACZ,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,gBAAgB,EAChB,OAAO,EACP,UAAU,EACV,UAAU,EACV,aAAa,EACb,gBAAgB,EAChB,YAAY,EACZ,UAAU,EACV,KAAK,EACL,aAAa,GACd,MAAM,kBAAkB,CAAC"}