@versini/ui-menu 6.2.1 → 6.3.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/README.md +118 -35
- package/dist/index.d.ts +28 -4
- package/dist/index.js +72 -31
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ The Menu package provides dropdown menus with full keyboard navigation, focus ma
|
|
|
17
17
|
|
|
18
18
|
## Features
|
|
19
19
|
|
|
20
|
-
- **Composable**: `Menu`, `MenuItem`, `MenuSeparator`, `
|
|
20
|
+
- **Composable**: `Menu`, `MenuItem`, `MenuSeparator`, `MenuLabel`, `MenuGroup`, `MenuSub`
|
|
21
21
|
- **Nested Sub-menus**: Support for multi-level menu hierarchies with automatic positioning
|
|
22
22
|
- **Accessible**: Built with ARIA roles & WAI-ARIA menu patterns for robust a11y
|
|
23
23
|
- **Keyboard Support**: Arrow navigation, ESC / click outside to close
|
|
@@ -107,36 +107,17 @@ function AccountMenu() {
|
|
|
107
107
|
}
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
-
###
|
|
110
|
+
### Grouped Menu Items
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
<Menu
|
|
114
|
-
trigger={
|
|
115
|
-
<ButtonIcon label="More">
|
|
116
|
-
<IconMenu />
|
|
117
|
-
</ButtonIcon>
|
|
118
|
-
}
|
|
119
|
-
>
|
|
120
|
-
<MenuItem raw ignoreClick>
|
|
121
|
-
<div className="p-2 text-xs uppercase tracking-wide text-copy-medium">
|
|
122
|
-
Custom Header
|
|
123
|
-
</div>
|
|
124
|
-
</MenuItem>
|
|
125
|
-
<MenuItem label="Action" />
|
|
126
|
-
</Menu>
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
### Nested Sub-menus
|
|
130
|
-
|
|
131
|
-
Create hierarchical menus using `MenuSub`:
|
|
112
|
+
Use `MenuGroup` to visually group related items with an optional label:
|
|
132
113
|
|
|
133
114
|
```tsx
|
|
134
|
-
import { Menu,
|
|
115
|
+
import { Menu, MenuGroup, MenuItem } from "@versini/ui-menu";
|
|
135
116
|
import { ButtonIcon } from "@versini/ui-button";
|
|
136
117
|
import { IconSettings, IconOpenAI, IconAnthropic } from "@versini/ui-icons";
|
|
137
118
|
|
|
138
119
|
function SettingsMenu() {
|
|
139
|
-
const [
|
|
120
|
+
const [selected, setSelected] = useState(0);
|
|
140
121
|
|
|
141
122
|
return (
|
|
142
123
|
<Menu
|
|
@@ -146,24 +127,117 @@ function SettingsMenu() {
|
|
|
146
127
|
</ButtonIcon>
|
|
147
128
|
}
|
|
148
129
|
>
|
|
149
|
-
<
|
|
150
|
-
<MenuItem label="Preferences" />
|
|
151
|
-
|
|
152
|
-
{/* Nested sub-menu with icon */}
|
|
153
|
-
<MenuSub label="AI Settings" icon={<IconSettings />}>
|
|
154
|
-
<MenuGroupLabel icon={<IconSettings />}>Engines</MenuGroupLabel>
|
|
130
|
+
<MenuGroup label="Engines">
|
|
155
131
|
<MenuItem
|
|
156
132
|
label="OpenAI"
|
|
157
133
|
icon={<IconOpenAI />}
|
|
158
|
-
selected={
|
|
159
|
-
|
|
134
|
+
selected={selected === 1}
|
|
135
|
+
onClick={() => setSelected(1)}
|
|
160
136
|
/>
|
|
161
137
|
<MenuItem
|
|
162
138
|
label="Anthropic"
|
|
163
139
|
icon={<IconAnthropic />}
|
|
164
|
-
selected={
|
|
165
|
-
|
|
140
|
+
selected={selected === 2}
|
|
141
|
+
onClick={() => setSelected(2)}
|
|
166
142
|
/>
|
|
143
|
+
</MenuGroup>
|
|
144
|
+
|
|
145
|
+
<MenuGroup label="Personas" className="mt-2">
|
|
146
|
+
<MenuItem label="Diggidy" selected={selected === 3} />
|
|
147
|
+
<MenuItem label="French Teacher" selected={selected === 4} />
|
|
148
|
+
</MenuGroup>
|
|
149
|
+
|
|
150
|
+
<MenuItem label="About" />
|
|
151
|
+
</Menu>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Menu with a Label
|
|
157
|
+
|
|
158
|
+
Use `MenuLabel` to add a non-interactive heading inside a menu:
|
|
159
|
+
|
|
160
|
+
```tsx
|
|
161
|
+
import { Menu, MenuItem, MenuLabel } from "@versini/ui-menu";
|
|
162
|
+
import { ButtonIcon } from "@versini/ui-button";
|
|
163
|
+
import { IconBookSparkles, IconMagic, IconProofread } from "@versini/ui-icons";
|
|
164
|
+
|
|
165
|
+
function PromptsMenu() {
|
|
166
|
+
return (
|
|
167
|
+
<Menu
|
|
168
|
+
trigger={
|
|
169
|
+
<ButtonIcon label="Prompts">
|
|
170
|
+
<IconBookSparkles />
|
|
171
|
+
</ButtonIcon>
|
|
172
|
+
}
|
|
173
|
+
>
|
|
174
|
+
<MenuLabel>Prompts</MenuLabel>
|
|
175
|
+
<MenuItem label="Summarize..." icon={<IconMagic />} />
|
|
176
|
+
<MenuItem label="Proofread..." icon={<IconProofread />} />
|
|
177
|
+
</Menu>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Nested Sub-menus
|
|
183
|
+
|
|
184
|
+
Create hierarchical menus using `MenuSub`. Groups work inside sub-menus too:
|
|
185
|
+
|
|
186
|
+
```tsx
|
|
187
|
+
import {
|
|
188
|
+
Menu, MenuItem, MenuSub, MenuGroup, MenuSeparator
|
|
189
|
+
} from "@versini/ui-menu";
|
|
190
|
+
import { ButtonIcon } from "@versini/ui-button";
|
|
191
|
+
import {
|
|
192
|
+
IconSettings, IconOpenAI, IconAnthropic,
|
|
193
|
+
IconStarInCircle, IconFrenchFlag
|
|
194
|
+
} from "@versini/ui-icons";
|
|
195
|
+
|
|
196
|
+
function SettingsMenu() {
|
|
197
|
+
const [model, setModel] = useState("openai");
|
|
198
|
+
const [persona, setPersona] = useState("diggidy");
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<Menu
|
|
202
|
+
trigger={
|
|
203
|
+
<ButtonIcon label="Settings">
|
|
204
|
+
<IconSettings />
|
|
205
|
+
</ButtonIcon>
|
|
206
|
+
}
|
|
207
|
+
>
|
|
208
|
+
<MenuItem label="Profile" />
|
|
209
|
+
<MenuItem label="Statistics" />
|
|
210
|
+
<MenuSeparator />
|
|
211
|
+
|
|
212
|
+
<MenuSub label="Engines and Personas" icon={<IconSettings />}>
|
|
213
|
+
<MenuGroup label="Engines">
|
|
214
|
+
<MenuItem
|
|
215
|
+
label="OpenAI"
|
|
216
|
+
icon={<IconOpenAI />}
|
|
217
|
+
selected={model === "openai"}
|
|
218
|
+
onClick={() => setModel("openai")}
|
|
219
|
+
/>
|
|
220
|
+
<MenuItem
|
|
221
|
+
label="Anthropic"
|
|
222
|
+
icon={<IconAnthropic />}
|
|
223
|
+
selected={model === "anthropic"}
|
|
224
|
+
onClick={() => setModel("anthropic")}
|
|
225
|
+
/>
|
|
226
|
+
</MenuGroup>
|
|
227
|
+
<MenuGroup label="Personas" className="mt-2">
|
|
228
|
+
<MenuItem
|
|
229
|
+
label="Diggidy"
|
|
230
|
+
icon={<IconStarInCircle />}
|
|
231
|
+
selected={persona === "diggidy"}
|
|
232
|
+
onClick={() => setPersona("diggidy")}
|
|
233
|
+
/>
|
|
234
|
+
<MenuItem
|
|
235
|
+
label="French Teacher"
|
|
236
|
+
icon={<IconFrenchFlag />}
|
|
237
|
+
selected={persona === "french_teacher"}
|
|
238
|
+
onClick={() => setPersona("french_teacher")}
|
|
239
|
+
/>
|
|
240
|
+
</MenuGroup>
|
|
167
241
|
</MenuSub>
|
|
168
242
|
|
|
169
243
|
<MenuItem label="About" />
|
|
@@ -221,11 +295,20 @@ function SettingsMenu() {
|
|
|
221
295
|
| `disabled` | `boolean` | `false` | Whether the sub-menu is disabled. |
|
|
222
296
|
| `sideOffset` | `number` | `14` | Offset from sub-menu trigger. |
|
|
223
297
|
|
|
298
|
+
### MenuGroup Props
|
|
299
|
+
|
|
300
|
+
| Prop | Type | Default | Description |
|
|
301
|
+
| ----------- | ----------------- | ------- | ----------------------------------------------- |
|
|
302
|
+
| `label` | `string` | - | Label displayed at the top of the group. |
|
|
303
|
+
| `icon` | `React.ReactNode` | - | Icon to display on the left of the group label. |
|
|
304
|
+
| `children` | `React.ReactNode` | - | MenuItems to render inside the group. |
|
|
305
|
+
| `className` | `string` | - | Custom CSS class for styling. |
|
|
306
|
+
|
|
224
307
|
### MenuSeparator Props
|
|
225
308
|
|
|
226
309
|
Standard `React.HTMLAttributes<HTMLDivElement>` - use `className` for custom styling.
|
|
227
310
|
|
|
228
|
-
###
|
|
311
|
+
### MenuLabel Props
|
|
229
312
|
|
|
230
313
|
| Prop | Type | Default | Description |
|
|
231
314
|
| ----------- | ----------------- | ------- | ----------------------------------------- |
|
package/dist/index.d.ts
CHANGED
|
@@ -5,17 +5,29 @@ export declare const Menu: {
|
|
|
5
5
|
displayName: string;
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
-
export declare const
|
|
9
|
-
({
|
|
8
|
+
export declare const MenuGroup: {
|
|
9
|
+
({ children, label, className, icon, }: MenuGroupProps): JSX.Element;
|
|
10
10
|
displayName: string;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
declare type
|
|
13
|
+
declare type MenuGroupProps = {
|
|
14
|
+
/**
|
|
15
|
+
* The label for the menu group.
|
|
16
|
+
*/
|
|
17
|
+
label: string;
|
|
18
|
+
/**
|
|
19
|
+
* The children to render inside the menu group.
|
|
20
|
+
*/
|
|
21
|
+
children?: React.ReactNode;
|
|
14
22
|
/**
|
|
15
23
|
* A React component of type Icon to be placed on the left of the label.
|
|
16
24
|
*/
|
|
17
25
|
icon?: React.ReactNode;
|
|
18
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Additional class names to apply to the menu group.
|
|
28
|
+
*/
|
|
29
|
+
className?: string;
|
|
30
|
+
};
|
|
19
31
|
|
|
20
32
|
export declare const MenuItem: {
|
|
21
33
|
({ label, disabled, icon, raw, children, ignoreClick, selected, onSelect, onClick, onFocus, onMouseEnter, ...props }: MenuItemProps): JSX.Element;
|
|
@@ -73,6 +85,18 @@ declare type MenuItemProps = {
|
|
|
73
85
|
onMouseEnter?: (event: React.MouseEvent<HTMLDivElement>) => void;
|
|
74
86
|
};
|
|
75
87
|
|
|
88
|
+
export declare const MenuLabel: {
|
|
89
|
+
({ className, icon, children, ...props }: MenuLabelProps): JSX.Element;
|
|
90
|
+
displayName: string;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
declare type MenuLabelProps = {
|
|
94
|
+
/**
|
|
95
|
+
* A React component of type Icon to be placed on the left of the label.
|
|
96
|
+
*/
|
|
97
|
+
icon?: React.ReactNode;
|
|
98
|
+
} & React.HTMLAttributes<HTMLDivElement>;
|
|
99
|
+
|
|
76
100
|
declare type MenuProps = {
|
|
77
101
|
/**
|
|
78
102
|
* The component to use to open the menu, e.g. a ButtonIcon, a Button, etc.
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
@versini/ui-menu v6.
|
|
2
|
+
@versini/ui-menu v6.3.0
|
|
3
3
|
© 2026 gizmette.com
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
7
7
|
import { useUniqueId } from "@versini/ui-hooks/use-unique-id";
|
|
8
|
+
import clsx_0 , { clsx } from "clsx";
|
|
8
9
|
import { cloneElement, createContext, isValidElement, useCallback, useContext, useEffect, useRef, useState } from "react";
|
|
9
|
-
import
|
|
10
|
-
|
|
10
|
+
import { IconNext } from "@versini/ui-icons";
|
|
11
|
+
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
|
|
@@ -245,6 +246,7 @@ function getLastEnabledIndex(items) {
|
|
|
245
246
|
};
|
|
246
247
|
}
|
|
247
248
|
|
|
249
|
+
|
|
248
250
|
const getDisplayName = (element)=>{
|
|
249
251
|
if (typeof element === "string") {
|
|
250
252
|
return element;
|
|
@@ -336,6 +338,12 @@ const calculatePosition = (triggerRect, menuRect, placement, sideOffset, viewpor
|
|
|
336
338
|
left
|
|
337
339
|
};
|
|
338
340
|
};
|
|
341
|
+
const getMenuItemClasses = ({ isSub })=>{
|
|
342
|
+
return clsx("flex flex-row items-center", "w-full", "m-0 first:mt-0 mt-2 sm:mt-1 px-2 py-1", "rounded-md border border-transparent", "text-left text-base select-none cursor-pointer", "outline-hidden", "disabled:cursor-not-allowed disabled:text-copy-medium", "data-highlighted:bg-surface-dark", "data-highlighted:text-copy-light", "in-data-menu-group:data-highlighted:bg-surface-dark", "in-data-menu-group:data-highlighted:border-border-medium", {
|
|
343
|
+
"data-[state=open]:bg-surface-darker data-[state=open]:text-copy-light": isSub,
|
|
344
|
+
"data-disabled:cursor-not-allowed data-disabled:text-copy-medium": !isSub
|
|
345
|
+
});
|
|
346
|
+
};
|
|
339
347
|
|
|
340
348
|
|
|
341
349
|
|
|
@@ -388,7 +396,8 @@ function useMenuPosition({ triggerRef, menuRef, placement, sideOffset, isOpen })
|
|
|
388
396
|
|
|
389
397
|
|
|
390
398
|
|
|
391
|
-
|
|
399
|
+
|
|
400
|
+
const CONTENT_CLASS = clsx("z-100 rounded-md outline-hidden", "bg-surface-light", "text-copy-dark", "p-3 sm:p-2", "plume plume-dark");
|
|
392
401
|
const Menu = ({ trigger, children, label = "Open menu", defaultPlacement = "bottom-start", onOpenChange, mode = "system", focusMode = "system", sideOffset = 10 })=>{
|
|
393
402
|
const [isOpen, setIsOpen] = useState(false);
|
|
394
403
|
const [activeIndex, setActiveIndex] = useState(-1);
|
|
@@ -610,33 +619,38 @@ Menu.displayName = "Menu";
|
|
|
610
619
|
|
|
611
620
|
|
|
612
621
|
|
|
613
|
-
|
|
614
|
-
const
|
|
615
|
-
const
|
|
616
|
-
"flex items-center": icon
|
|
617
|
-
});
|
|
618
|
-
const labelSpanClass = icon ? "px-2" : "";
|
|
622
|
+
const MenuGroup = ({ children, label, className, icon })=>{
|
|
623
|
+
const groupClass = clsx_0("rounded-md", "p-2", "bg-surface-dark", "text-copy-light", className);
|
|
624
|
+
const labelContainerClass = clsx_0("pt-2 pb-2", "px-3 sm:px-2", "-mx-1 -mt-1", "flex items-center justify-between", "text-xs text-copy-medium uppercase font-bold", "rounded-t-md");
|
|
619
625
|
return /*#__PURE__*/ jsxs("div", {
|
|
620
|
-
|
|
621
|
-
|
|
626
|
+
role: "group",
|
|
627
|
+
className: groupClass,
|
|
628
|
+
"data-menu-group": true,
|
|
622
629
|
children: [
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
630
|
+
label && /*#__PURE__*/ jsx("div", {
|
|
631
|
+
className: labelContainerClass,
|
|
632
|
+
children: /*#__PURE__*/ jsxs("span", {
|
|
633
|
+
className: "flex items-center gap-1",
|
|
634
|
+
children: [
|
|
635
|
+
icon,
|
|
636
|
+
label
|
|
637
|
+
]
|
|
638
|
+
})
|
|
639
|
+
}),
|
|
640
|
+
children
|
|
628
641
|
]
|
|
629
642
|
});
|
|
630
643
|
};
|
|
631
|
-
|
|
632
|
-
|
|
644
|
+
MenuGroup.displayName = "MenuGroup";
|
|
633
645
|
|
|
634
646
|
|
|
635
647
|
|
|
636
648
|
|
|
637
649
|
|
|
638
650
|
|
|
639
|
-
const ITEM_CLASS =
|
|
651
|
+
const ITEM_CLASS = getMenuItemClasses({
|
|
652
|
+
isSub: false
|
|
653
|
+
});
|
|
640
654
|
const MenuItem = ({ label, disabled, icon, raw = false, children, ignoreClick = false, selected, onSelect, onClick, onFocus, onMouseEnter, ...props })=>{
|
|
641
655
|
const itemRef = useRef(null);
|
|
642
656
|
const { closeAll } = useContext(MenuRootContext);
|
|
@@ -725,7 +739,7 @@ const MenuItem = ({ label, disabled, icon, raw = false, children, ignoreClick =
|
|
|
725
739
|
if (icon) {
|
|
726
740
|
buttonSpanClass = "pl-2";
|
|
727
741
|
}
|
|
728
|
-
const itemClass =
|
|
742
|
+
const itemClass = clsx_0(ITEM_CLASS, {
|
|
729
743
|
"bg-none": !disabled && !selected
|
|
730
744
|
});
|
|
731
745
|
return /*#__PURE__*/ jsxs("div", {
|
|
@@ -741,13 +755,14 @@ const MenuItem = ({ label, disabled, icon, raw = false, children, ignoreClick =
|
|
|
741
755
|
onMouseEnter: handleMouseEnter,
|
|
742
756
|
...props,
|
|
743
757
|
children: [
|
|
744
|
-
selected === true && /*#__PURE__*/ jsx(
|
|
745
|
-
className: "
|
|
746
|
-
|
|
758
|
+
selected === true && /*#__PURE__*/ jsx("span", {
|
|
759
|
+
className: "mr-2 flex size-4 shrink-0 items-center justify-center rounded-full border border-copy-success",
|
|
760
|
+
children: /*#__PURE__*/ jsx("span", {
|
|
761
|
+
className: "size-1.5 rounded-full bg-copy-success-light"
|
|
762
|
+
})
|
|
747
763
|
}),
|
|
748
|
-
selected === false && /*#__PURE__*/ jsx(
|
|
749
|
-
className: "
|
|
750
|
-
size: "size-4"
|
|
764
|
+
selected === false && /*#__PURE__*/ jsx("span", {
|
|
765
|
+
className: clsx_0("mr-2 size-4 shrink-0", "rounded-full border border-copy-medium")
|
|
751
766
|
}),
|
|
752
767
|
icon,
|
|
753
768
|
label && /*#__PURE__*/ jsx("span", {
|
|
@@ -761,8 +776,29 @@ MenuItem.displayName = "MenuItem";
|
|
|
761
776
|
|
|
762
777
|
|
|
763
778
|
|
|
779
|
+
const MenuLabel = ({ className, icon, children, ...props })=>{
|
|
780
|
+
const groupLabelClass = clsx_0(className, "pt-1 pb-2", "text-xs text-copy-medium uppercase font-bold", "border-b border-border-medium", {
|
|
781
|
+
"flex items-center": icon
|
|
782
|
+
});
|
|
783
|
+
const labelSpanClass = icon ? "px-2" : "";
|
|
784
|
+
return /*#__PURE__*/ jsxs("div", {
|
|
785
|
+
className: groupLabelClass,
|
|
786
|
+
...props,
|
|
787
|
+
children: [
|
|
788
|
+
icon,
|
|
789
|
+
/*#__PURE__*/ jsx("span", {
|
|
790
|
+
className: labelSpanClass,
|
|
791
|
+
children: children
|
|
792
|
+
})
|
|
793
|
+
]
|
|
794
|
+
});
|
|
795
|
+
};
|
|
796
|
+
MenuLabel.displayName = "MenuLabel";
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
|
|
764
800
|
const MenuSeparator = ({ className, ...props })=>{
|
|
765
|
-
const separatorClass =
|
|
801
|
+
const separatorClass = clsx_0(className, "my-1 border-t border-border-medium");
|
|
766
802
|
return /*#__PURE__*/ jsx("div", {
|
|
767
803
|
role: "separator",
|
|
768
804
|
className: separatorClass,
|
|
@@ -779,8 +815,12 @@ MenuSeparator.displayName = "MenuSeparator";
|
|
|
779
815
|
|
|
780
816
|
|
|
781
817
|
|
|
782
|
-
|
|
783
|
-
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
const SUB_CONTENT_CLASS = clsx_0("bg-surface-light", "z-60 rounded-md text-copy-light outline-hidden p-3 sm:p-2 mx-3");
|
|
821
|
+
const SUB_TRIGGER_CLASS = getMenuItemClasses({
|
|
822
|
+
isSub: true
|
|
823
|
+
});
|
|
784
824
|
const MenuSub = ({ label, icon, children, disabled = false, sideOffset = 14 })=>{
|
|
785
825
|
const subMenuId = useUniqueId("av-menu-sub-");
|
|
786
826
|
const [isSubOpen, setIsSubOpen] = useState(false);
|
|
@@ -1005,4 +1045,5 @@ MenuSub.displayName = "MenuSub";
|
|
|
1005
1045
|
|
|
1006
1046
|
|
|
1007
1047
|
|
|
1008
|
-
|
|
1048
|
+
|
|
1049
|
+
export { Menu, MenuGroup, MenuItem, MenuLabel, MenuSeparator, MenuSub };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@versini/ui-menu",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.3.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Arno Versini",
|
|
6
6
|
"publishConfig": {
|
|
@@ -49,5 +49,5 @@
|
|
|
49
49
|
"sideEffects": [
|
|
50
50
|
"**/*.css"
|
|
51
51
|
],
|
|
52
|
-
"gitHead": "
|
|
52
|
+
"gitHead": "6ec1140bfd6c5575eb034224f51149093e8d8683"
|
|
53
53
|
}
|