@-reddacted-/react-ui 0.3.1 → 0.4.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/dist/components/Accordion.d.ts +27 -0
- package/dist/components/Accordion.d.ts.map +1 -0
- package/dist/components/Accordion.js +35 -0
- package/dist/components/Badge.d.ts +15 -0
- package/dist/components/Badge.d.ts.map +1 -0
- package/dist/components/Badge.js +7 -0
- package/dist/components/Button.d.ts +15 -2
- package/dist/components/Button.d.ts.map +1 -1
- package/dist/components/Button.js +76 -44
- package/dist/components/Card.d.ts +21 -0
- package/dist/components/Card.d.ts.map +1 -0
- package/dist/components/Card.js +17 -0
- package/dist/components/ColorField.d.ts.map +1 -1
- package/dist/components/ColorField.js +1 -1
- package/dist/components/ConfirmDeleteButton.d.ts +4 -1
- package/dist/components/ConfirmDeleteButton.d.ts.map +1 -1
- package/dist/components/ConfirmDeleteButton.js +6 -7
- package/dist/components/Drawer.d.ts +33 -0
- package/dist/components/Drawer.d.ts.map +1 -0
- package/dist/components/Drawer.js +123 -0
- package/dist/components/DropdownMenu.d.ts +61 -0
- package/dist/components/DropdownMenu.d.ts.map +1 -0
- package/dist/components/DropdownMenu.js +302 -0
- package/dist/components/Modal.d.ts.map +1 -1
- package/dist/components/Modal.js +102 -134
- package/dist/components/NavBar.d.ts.map +1 -1
- package/dist/components/NavBar.js +4 -2
- package/dist/components/PageStatus.d.ts +12 -12
- package/dist/components/PageStatus.d.ts.map +1 -1
- package/dist/components/PageStatus.js +9 -29
- package/dist/components/Pagination.d.ts +44 -0
- package/dist/components/Pagination.d.ts.map +1 -0
- package/dist/components/Pagination.js +57 -0
- package/dist/components/Skeleton.d.ts +13 -0
- package/dist/components/Skeleton.d.ts.map +1 -0
- package/dist/components/Skeleton.js +15 -0
- package/dist/components/Spinner.d.ts +14 -0
- package/dist/components/Spinner.d.ts.map +1 -0
- package/dist/components/Spinner.js +14 -0
- package/dist/components/Stack.d.ts +13 -0
- package/dist/components/Stack.d.ts.map +1 -0
- package/dist/components/Stack.js +15 -0
- package/dist/components/Table.d.ts +54 -0
- package/dist/components/Table.d.ts.map +1 -0
- package/dist/components/Table.js +101 -0
- package/dist/components/Tabs.d.ts +34 -0
- package/dist/components/Tabs.d.ts.map +1 -0
- package/dist/components/Tabs.js +82 -0
- package/dist/components/Tag.d.ts +10 -0
- package/dist/components/Tag.d.ts.map +1 -0
- package/dist/components/Tag.js +7 -0
- package/dist/components/Tooltip.d.ts +22 -0
- package/dist/components/Tooltip.d.ts.map +1 -0
- package/dist/components/Tooltip.js +76 -0
- package/dist/components/drawerStore.d.ts +11 -0
- package/dist/components/drawerStore.d.ts.map +1 -0
- package/dist/components/drawerStore.js +40 -0
- package/dist/components/paginationItems.d.ts +18 -0
- package/dist/components/paginationItems.d.ts.map +1 -0
- package/dist/components/paginationItems.js +32 -0
- package/dist/index.d.ts +22 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -2
- package/dist/popup/PopupHost.d.ts +19 -3
- package/dist/popup/PopupHost.d.ts.map +1 -1
- package/dist/popup/PopupHost.js +65 -44
- package/dist/popup/PopupProvider.d.ts +19 -2
- package/dist/popup/PopupProvider.d.ts.map +1 -1
- package/dist/popup/PopupProvider.js +69 -17
- package/dist/popup/usePopup.d.ts +11 -4
- package/dist/popup/usePopup.d.ts.map +1 -1
- package/dist/style/accordionCss.d.ts +2 -0
- package/dist/style/accordionCss.d.ts.map +1 -0
- package/dist/style/accordionCss.js +101 -0
- package/dist/style/badgeCss.d.ts +5 -0
- package/dist/style/badgeCss.d.ts.map +1 -0
- package/dist/style/badgeCss.js +68 -0
- package/dist/style/breadcrumbCss.d.ts +2 -0
- package/dist/style/breadcrumbCss.d.ts.map +1 -0
- package/dist/style/breadcrumbCss.js +53 -0
- package/dist/style/cardCss.d.ts +2 -0
- package/dist/style/cardCss.d.ts.map +1 -0
- package/dist/style/cardCss.js +49 -0
- package/dist/style/inputCss.d.ts +2 -0
- package/dist/style/inputCss.d.ts.map +1 -0
- package/dist/style/inputCss.js +107 -0
- package/dist/style/loaderCss.d.ts +2 -0
- package/dist/style/loaderCss.d.ts.map +1 -0
- package/dist/style/loaderCss.js +39 -0
- package/dist/style/menuCss.d.ts +2 -0
- package/dist/style/menuCss.d.ts.map +1 -0
- package/dist/style/menuCss.js +71 -0
- package/dist/style/paginationCss.d.ts +2 -0
- package/dist/style/paginationCss.d.ts.map +1 -0
- package/dist/style/paginationCss.js +57 -0
- package/dist/style/resolveColor.d.ts +8 -0
- package/dist/style/resolveColor.d.ts.map +1 -0
- package/dist/style/resolveColor.js +13 -0
- package/dist/style/stackCss.d.ts +2 -0
- package/dist/style/stackCss.d.ts.map +1 -0
- package/dist/style/stackCss.js +21 -0
- package/dist/style/statusCss.d.ts +2 -0
- package/dist/style/statusCss.d.ts.map +1 -0
- package/dist/style/statusCss.js +38 -0
- package/dist/style/tableCss.d.ts +2 -0
- package/dist/style/tableCss.d.ts.map +1 -0
- package/dist/style/tableCss.js +92 -0
- package/dist/style/tabsCss.d.ts +2 -0
- package/dist/style/tabsCss.d.ts.map +1 -0
- package/dist/style/tabsCss.js +72 -0
- package/dist/style/toLength.d.ts +4 -0
- package/dist/style/toLength.d.ts.map +1 -0
- package/dist/style/toLength.js +3 -0
- package/dist/style/tooltipCss.d.ts +2 -0
- package/dist/style/tooltipCss.d.ts.map +1 -0
- package/dist/style/tooltipCss.js +44 -0
- package/dist/style/useStyle.d.ts +7 -0
- package/dist/style/useStyle.d.ts.map +1 -0
- package/dist/style/useStyle.js +16 -0
- package/dist/theme/ThemeProvider.d.ts.map +1 -1
- package/dist/theme/ThemeProvider.js +32 -0
- package/dist/theme/palette.d.ts.map +1 -1
- package/dist/theme/palette.js +0 -2
- package/dist/tooltip/attributeTooltips.d.ts +7 -0
- package/dist/tooltip/attributeTooltips.d.ts.map +1 -0
- package/dist/tooltip/attributeTooltips.js +76 -0
- package/dist/tooltip/tooltipEngine.d.ts +41 -0
- package/dist/tooltip/tooltipEngine.d.ts.map +1 -0
- package/dist/tooltip/tooltipEngine.js +128 -0
- package/dist/utils/childRef.d.ts +5 -0
- package/dist/utils/childRef.d.ts.map +1 -0
- package/dist/utils/childRef.js +14 -0
- package/dist/utils/isDev.d.ts +3 -0
- package/dist/utils/isDev.d.ts.map +1 -0
- package/dist/utils/isDev.js +4 -0
- package/package.json +5 -4
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Children, cloneElement, createContext, isValidElement, useCallback, useContext, useEffect, useId, useLayoutEffect, useRef, useState, } from "react";
|
|
3
|
+
import { createPortal } from "react-dom";
|
|
4
|
+
import { autoUpdate, computePosition, flip, offset, shift } from "@floating-ui/dom";
|
|
5
|
+
import { useEscapeKey, useOverlayStack } from "@-reddacted-/react-hooks";
|
|
6
|
+
import { menuCss } from "../style/menuCss.js";
|
|
7
|
+
import { resolveColor } from "../style/resolveColor.js";
|
|
8
|
+
import { toLength } from "../style/toLength.js";
|
|
9
|
+
import { useStyle } from "../style/useStyle.js";
|
|
10
|
+
import { assignRef, getElementRef } from "../utils/childRef.js";
|
|
11
|
+
const MenuContext = createContext(null);
|
|
12
|
+
function useMenuContext(part) {
|
|
13
|
+
const ctx = useContext(MenuContext);
|
|
14
|
+
if (!ctx)
|
|
15
|
+
throw new Error(`DropdownMenu.${part} must be used within DropdownMenu`);
|
|
16
|
+
return ctx;
|
|
17
|
+
}
|
|
18
|
+
const ITEM_SELECTOR = '[role^="menuitem"]:not([aria-disabled="true"])';
|
|
19
|
+
const TYPEAHEAD_RESET = 500;
|
|
20
|
+
function menuItems(content) {
|
|
21
|
+
return content ? Array.from(content.querySelectorAll(ITEM_SELECTOR)) : [];
|
|
22
|
+
}
|
|
23
|
+
function focusItem(content, intent) {
|
|
24
|
+
const items = menuItems(content);
|
|
25
|
+
if (intent === "first")
|
|
26
|
+
items[0]?.focus();
|
|
27
|
+
else if (intent === "last")
|
|
28
|
+
items[items.length - 1]?.focus();
|
|
29
|
+
}
|
|
30
|
+
export function DropdownMenu({ trigger = "click", placement = "bottom-start", open: controlledOpen, defaultOpen = false, onOpenChange, closeDelay = 200, color = "--color-primary", children, }) {
|
|
31
|
+
useStyle("menu", menuCss);
|
|
32
|
+
const baseId = useId();
|
|
33
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
|
|
34
|
+
const controlled = controlledOpen !== undefined;
|
|
35
|
+
const open = controlled ? controlledOpen : uncontrolledOpen;
|
|
36
|
+
const triggerRef = useRef(null);
|
|
37
|
+
const contentRef = useRef(null);
|
|
38
|
+
const focusIntent = useRef("none");
|
|
39
|
+
const closeTimer = useRef();
|
|
40
|
+
const onOpenChangeRef = useRef(onOpenChange);
|
|
41
|
+
onOpenChangeRef.current = onOpenChange;
|
|
42
|
+
const openRef = useRef(open);
|
|
43
|
+
openRef.current = open;
|
|
44
|
+
const setOpen = useCallback((next, options = {}) => {
|
|
45
|
+
clearTimeout(closeTimer.current);
|
|
46
|
+
if (next)
|
|
47
|
+
focusIntent.current = options.focus ?? "none";
|
|
48
|
+
if (!next && options.returnFocus)
|
|
49
|
+
triggerRef.current?.focus();
|
|
50
|
+
if (next === openRef.current) {
|
|
51
|
+
// Already open: still honour a keyboard request to move into the menu.
|
|
52
|
+
if (next)
|
|
53
|
+
focusItem(contentRef.current, focusIntent.current);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (!controlled)
|
|
57
|
+
setUncontrolledOpen(next);
|
|
58
|
+
onOpenChangeRef.current?.(next);
|
|
59
|
+
}, [controlled]);
|
|
60
|
+
const cancelClose = useCallback(() => clearTimeout(closeTimer.current), []);
|
|
61
|
+
const scheduleClose = useCallback(() => {
|
|
62
|
+
clearTimeout(closeTimer.current);
|
|
63
|
+
closeTimer.current = setTimeout(() => setOpen(false), closeDelay);
|
|
64
|
+
}, [setOpen, closeDelay]);
|
|
65
|
+
useEffect(() => () => clearTimeout(closeTimer.current), []);
|
|
66
|
+
const value = {
|
|
67
|
+
open,
|
|
68
|
+
setOpen,
|
|
69
|
+
focusIntent,
|
|
70
|
+
triggerRef,
|
|
71
|
+
contentRef,
|
|
72
|
+
menuId: `${baseId}-menu`,
|
|
73
|
+
triggerId: `${baseId}-trigger`,
|
|
74
|
+
trigger,
|
|
75
|
+
placement,
|
|
76
|
+
color,
|
|
77
|
+
scheduleClose,
|
|
78
|
+
cancelClose,
|
|
79
|
+
};
|
|
80
|
+
return _jsx(MenuContext.Provider, { value: value, children: children });
|
|
81
|
+
}
|
|
82
|
+
/** Wraps one element of the project (e.g. a Button), which must accept a ref. */
|
|
83
|
+
function Trigger({ children }) {
|
|
84
|
+
const ctx = useMenuContext("Trigger");
|
|
85
|
+
const child = Children.only(children);
|
|
86
|
+
const childProps = child.props;
|
|
87
|
+
const childRef = getElementRef(child);
|
|
88
|
+
const lastPointerType = useRef("mouse");
|
|
89
|
+
const setRef = useCallback((node) => {
|
|
90
|
+
ctx.triggerRef.current = node;
|
|
91
|
+
assignRef(childRef, node);
|
|
92
|
+
}, [ctx.triggerRef, childRef]);
|
|
93
|
+
const hoverMode = ctx.trigger === "hover";
|
|
94
|
+
return cloneElement(child, {
|
|
95
|
+
ref: setRef,
|
|
96
|
+
id: childProps.id ?? ctx.triggerId,
|
|
97
|
+
"aria-haspopup": "menu",
|
|
98
|
+
"aria-expanded": ctx.open,
|
|
99
|
+
"aria-controls": ctx.open ? ctx.menuId : undefined,
|
|
100
|
+
onPointerDown: (event) => {
|
|
101
|
+
childProps.onPointerDown?.(event);
|
|
102
|
+
lastPointerType.current = event.pointerType;
|
|
103
|
+
},
|
|
104
|
+
onClick: (event) => {
|
|
105
|
+
childProps.onClick?.(event);
|
|
106
|
+
if (event.defaultPrevented)
|
|
107
|
+
return;
|
|
108
|
+
// Hover mode on a mouse: the pointer already opened it, a click keeps it open.
|
|
109
|
+
if (hoverMode && lastPointerType.current !== "touch")
|
|
110
|
+
ctx.setOpen(true);
|
|
111
|
+
else
|
|
112
|
+
ctx.setOpen(!ctx.open, { focus: "first" });
|
|
113
|
+
},
|
|
114
|
+
onKeyDown: (event) => {
|
|
115
|
+
childProps.onKeyDown?.(event);
|
|
116
|
+
if (event.defaultPrevented)
|
|
117
|
+
return;
|
|
118
|
+
// A button turns Enter / Space into a click (handled above); other elements need it here.
|
|
119
|
+
const activates = (event.key === "Enter" || event.key === " ") && event.currentTarget.tagName !== "BUTTON";
|
|
120
|
+
if (event.key === "ArrowDown" || activates) {
|
|
121
|
+
event.preventDefault();
|
|
122
|
+
ctx.setOpen(true, { focus: "first" });
|
|
123
|
+
}
|
|
124
|
+
else if (event.key === "ArrowUp") {
|
|
125
|
+
event.preventDefault();
|
|
126
|
+
ctx.setOpen(true, { focus: "last" });
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
onPointerEnter: (event) => {
|
|
130
|
+
childProps.onPointerEnter?.(event);
|
|
131
|
+
if (hoverMode && event.pointerType !== "touch")
|
|
132
|
+
ctx.setOpen(true);
|
|
133
|
+
},
|
|
134
|
+
onPointerLeave: (event) => {
|
|
135
|
+
childProps.onPointerLeave?.(event);
|
|
136
|
+
if (hoverMode && event.pointerType !== "touch")
|
|
137
|
+
ctx.scheduleClose();
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
function Content({ minWidth, className, style, onKeyDown, children, ...props }) {
|
|
142
|
+
const ctx = useMenuContext("Content");
|
|
143
|
+
const { open, setOpen, contentRef, triggerRef } = ctx;
|
|
144
|
+
const { isTop, zIndex } = useOverlayStack(open);
|
|
145
|
+
const [triggerWidth, setTriggerWidth] = useState();
|
|
146
|
+
const typeahead = useRef({ text: "", timer: undefined });
|
|
147
|
+
const closeAndReturn = useCallback(() => setOpen(false, { returnFocus: true }), [setOpen]);
|
|
148
|
+
useEscapeKey(closeAndReturn, open && isTop);
|
|
149
|
+
// Position, then move the focus in as asked by the opening gesture.
|
|
150
|
+
useLayoutEffect(() => {
|
|
151
|
+
const trigger = triggerRef.current;
|
|
152
|
+
const content = contentRef.current;
|
|
153
|
+
if (!open || !trigger || !content)
|
|
154
|
+
return;
|
|
155
|
+
setTriggerWidth(trigger.offsetWidth);
|
|
156
|
+
focusItem(content, ctx.focusIntent.current);
|
|
157
|
+
return autoUpdate(trigger, content, () => {
|
|
158
|
+
void computePosition(trigger, content, {
|
|
159
|
+
strategy: "fixed",
|
|
160
|
+
placement: ctx.placement,
|
|
161
|
+
middleware: [offset(4), flip(), shift({ padding: 8 })],
|
|
162
|
+
}).then(({ x, y, placement }) => {
|
|
163
|
+
content.style.left = `${x}px`;
|
|
164
|
+
content.style.top = `${y}px`;
|
|
165
|
+
content.dataset.placement = placement;
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
}, [open, triggerRef, contentRef, ctx.focusIntent, ctx.placement]);
|
|
169
|
+
// Pressing outside the menu and its trigger closes it, leaving the focus where it goes.
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
if (!open)
|
|
172
|
+
return;
|
|
173
|
+
const onPointerDown = (event) => {
|
|
174
|
+
const target = event.target;
|
|
175
|
+
if (!contentRef.current?.contains(target) && !triggerRef.current?.contains(target))
|
|
176
|
+
setOpen(false);
|
|
177
|
+
};
|
|
178
|
+
document.addEventListener("pointerdown", onPointerDown, true);
|
|
179
|
+
return () => document.removeEventListener("pointerdown", onPointerDown, true);
|
|
180
|
+
}, [open, setOpen, contentRef, triggerRef]);
|
|
181
|
+
useEffect(() => () => clearTimeout(typeahead.current.timer), []);
|
|
182
|
+
if (!open)
|
|
183
|
+
return null;
|
|
184
|
+
const handleKeyDown = (event) => {
|
|
185
|
+
onKeyDown?.(event);
|
|
186
|
+
if (event.defaultPrevented)
|
|
187
|
+
return;
|
|
188
|
+
const items = menuItems(contentRef.current);
|
|
189
|
+
const index = items.indexOf(document.activeElement);
|
|
190
|
+
const move = (next) => {
|
|
191
|
+
event.preventDefault();
|
|
192
|
+
items[next]?.focus();
|
|
193
|
+
};
|
|
194
|
+
switch (event.key) {
|
|
195
|
+
case "ArrowDown":
|
|
196
|
+
return move(index === -1 ? 0 : (index + 1) % items.length);
|
|
197
|
+
case "ArrowUp":
|
|
198
|
+
return move(index <= 0 ? items.length - 1 : index - 1);
|
|
199
|
+
case "Home":
|
|
200
|
+
return move(0);
|
|
201
|
+
case "End":
|
|
202
|
+
return move(items.length - 1);
|
|
203
|
+
case "Tab":
|
|
204
|
+
event.preventDefault();
|
|
205
|
+
return closeAndReturn();
|
|
206
|
+
case "Enter":
|
|
207
|
+
case " ":
|
|
208
|
+
if (index !== -1) {
|
|
209
|
+
event.preventDefault();
|
|
210
|
+
items[index].click();
|
|
211
|
+
}
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
// Typeahead: jump to the next item starting with the typed letters.
|
|
215
|
+
if (event.key.length !== 1 || event.ctrlKey || event.metaKey || event.altKey)
|
|
216
|
+
return;
|
|
217
|
+
const state = typeahead.current;
|
|
218
|
+
clearTimeout(state.timer);
|
|
219
|
+
state.text += event.key.toLowerCase();
|
|
220
|
+
state.timer = setTimeout(() => (state.text = ""), TYPEAHEAD_RESET);
|
|
221
|
+
const ordered = [...items.slice(index + 1), ...items.slice(0, index + 1)];
|
|
222
|
+
const match = ordered.find(item => (item.dataset.textValue ?? item.textContent ?? "").trim().toLowerCase().startsWith(state.text));
|
|
223
|
+
match?.focus();
|
|
224
|
+
};
|
|
225
|
+
const hoverMode = ctx.trigger === "hover";
|
|
226
|
+
return createPortal(_jsx("div", { role: "menu", id: ctx.menuId, "aria-labelledby": triggerRef.current?.id, tabIndex: -1, ...props, ref: contentRef, className: className ? `rui-menu ${className}` : "rui-menu", style: {
|
|
227
|
+
zIndex,
|
|
228
|
+
minWidth: toLength(minWidth) ?? triggerWidth,
|
|
229
|
+
"--rui-menu-accent": resolveColor(ctx.color),
|
|
230
|
+
...style,
|
|
231
|
+
}, onKeyDown: handleKeyDown, onPointerEnter: hoverMode ? ctx.cancelClose : undefined, onPointerLeave: hoverMode ? ctx.scheduleClose : undefined, children: children }), document.body);
|
|
232
|
+
}
|
|
233
|
+
/** Shared behaviour of every item: role, focus on hover, activation, closing. */
|
|
234
|
+
function useItemProps(role, { disabled = false, closeOnSelect = true, textValue }, onActivate, checked) {
|
|
235
|
+
const ctx = useMenuContext("Item");
|
|
236
|
+
return {
|
|
237
|
+
role,
|
|
238
|
+
tabIndex: -1,
|
|
239
|
+
"aria-disabled": disabled || undefined,
|
|
240
|
+
"aria-checked": checked,
|
|
241
|
+
"data-text-value": textValue,
|
|
242
|
+
onPointerMove: (event) => {
|
|
243
|
+
if (!disabled && document.activeElement !== event.currentTarget)
|
|
244
|
+
event.currentTarget.focus();
|
|
245
|
+
},
|
|
246
|
+
onClick: (event) => {
|
|
247
|
+
if (disabled) {
|
|
248
|
+
event.preventDefault();
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
onActivate(event);
|
|
252
|
+
if (closeOnSelect && !event.defaultPrevented)
|
|
253
|
+
ctx.setOpen(false, { returnFocus: true });
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function Item({ disabled, closeOnSelect, textValue, onSelect, asChild, className, children, ...props }) {
|
|
258
|
+
const itemProps = useItemProps("menuitem", { disabled, closeOnSelect, textValue }, event => onSelect?.(event));
|
|
259
|
+
if (asChild && isValidElement(children)) {
|
|
260
|
+
const childProps = children.props;
|
|
261
|
+
return cloneElement(children, {
|
|
262
|
+
...itemProps,
|
|
263
|
+
className: [childProps.className, "rui-menu-item", className].filter(Boolean).join(" "),
|
|
264
|
+
onClick: (event) => {
|
|
265
|
+
childProps.onClick?.(event);
|
|
266
|
+
itemProps.onClick(event);
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
return (_jsx("div", { ...props, ...itemProps, className: className ? `rui-menu-item ${className}` : "rui-menu-item", children: children }));
|
|
271
|
+
}
|
|
272
|
+
function CheckboxItem({ disabled, closeOnSelect, textValue, checked, onCheckedChange, className, children, ...props }) {
|
|
273
|
+
const itemProps = useItemProps("menuitemcheckbox", { disabled, closeOnSelect, textValue }, () => onCheckedChange(!checked), checked);
|
|
274
|
+
return (_jsxs("div", { ...props, ...itemProps, className: className ? `rui-menu-item ${className}` : "rui-menu-item", children: [_jsx("span", { className: "rui-menu-indicator", "aria-hidden": "true", children: checked ? "✓" : "" }), children] }));
|
|
275
|
+
}
|
|
276
|
+
const RadioGroupContext = createContext(null);
|
|
277
|
+
function RadioGroup({ value, onValueChange, ...props }) {
|
|
278
|
+
return (_jsx(RadioGroupContext.Provider, { value: { value, onValueChange }, children: _jsx("div", { role: "group", ...props }) }));
|
|
279
|
+
}
|
|
280
|
+
function RadioItem({ value, disabled, closeOnSelect, textValue, className, children, ...props }) {
|
|
281
|
+
const group = useContext(RadioGroupContext);
|
|
282
|
+
if (!group)
|
|
283
|
+
throw new Error("DropdownMenu.RadioItem must be used within DropdownMenu.RadioGroup");
|
|
284
|
+
const checked = group.value === value;
|
|
285
|
+
const itemProps = useItemProps("menuitemradio", { disabled, closeOnSelect, textValue }, () => group.onValueChange(value), checked);
|
|
286
|
+
return (_jsxs("div", { ...props, ...itemProps, className: className ? `rui-menu-item ${className}` : "rui-menu-item", children: [_jsx("span", { className: "rui-menu-indicator", "aria-hidden": "true", children: checked ? "●" : "" }), children] }));
|
|
287
|
+
}
|
|
288
|
+
/** Group title, not focusable. */
|
|
289
|
+
function Label({ className, ...props }) {
|
|
290
|
+
return _jsx("div", { role: "presentation", ...props, className: className ? `rui-menu-label ${className}` : "rui-menu-label" });
|
|
291
|
+
}
|
|
292
|
+
function Separator({ className, ...props }) {
|
|
293
|
+
return (_jsx("div", { role: "separator", ...props, className: className ? `rui-menu-separator ${className}` : "rui-menu-separator" }));
|
|
294
|
+
}
|
|
295
|
+
DropdownMenu.Trigger = Trigger;
|
|
296
|
+
DropdownMenu.Content = Content;
|
|
297
|
+
DropdownMenu.Item = Item;
|
|
298
|
+
DropdownMenu.CheckboxItem = CheckboxItem;
|
|
299
|
+
DropdownMenu.RadioGroup = RadioGroup;
|
|
300
|
+
DropdownMenu.RadioItem = RadioItem;
|
|
301
|
+
DropdownMenu.Label = Label;
|
|
302
|
+
DropdownMenu.Separator = Separator;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Modal.d.ts","sourceRoot":"","sources":["../../src/components/Modal.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"Modal.d.ts","sourceRoot":"","sources":["../../src/components/Modal.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAkBrD,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,QAGnC;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,QAEpC;AAED,KAAK,UAAU,GAAG;IAChB,EAAE,EAAE,MAAM,CAAA;IACV,0EAA0E;IAC1E,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,wEAAwE;IACxE,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IACpB,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACvB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACxB,QAAQ,EAAE,SAAS,CAAA;IACnB,2EAA2E;IAC3E,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,SAAS,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,aAAa,CAAA;IACrB,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB,CAAA;AAED,MAAM,CAAC,OAAO,UAAU,KAAK,CAAC,EAC5B,EAAE,EACF,IAAI,EACJ,OAAO,EACP,KAAK,EACL,MAAM,EACN,QAAQ,EACR,KAAK,EACL,MAAM,EACN,UAAoB,EACpB,SAAS,EACT,KAAK,EACL,YAAoB,EACpB,cAAc,EACd,eAAe,GAChB,EAAE,UAAU,sCA+FZ"}
|
package/dist/components/Modal.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
2
|
-
import { useEffect, useRef, useState } from "react";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
3
|
import { createPortal } from "react-dom";
|
|
4
|
+
import { useEscapeKey, useFocusTrap, useOverlayStack, useScrollLock } from "@-reddacted-/react-hooks";
|
|
5
|
+
import { useStyle } from "../style/useStyle.js";
|
|
4
6
|
const listeners = new Map();
|
|
5
7
|
function on(event, cb) {
|
|
6
8
|
if (!listeners.has(event))
|
|
@@ -18,11 +20,9 @@ export function openModal(id) {
|
|
|
18
20
|
export function closeModal(id) {
|
|
19
21
|
emit("modal:close", id);
|
|
20
22
|
}
|
|
21
|
-
const FOCUSABLE = `a[href], button, textarea, input, select, [tabindex]:not([tabindex="-1"])`;
|
|
22
23
|
export default function Modal({ id, open, onClose, width, height, children, title, footer, closeLabel = "Close", className, style, preventClose = false, ariaLabelledBy, ariaDescribedBy, }) {
|
|
23
24
|
const [busOpen, setBusOpen] = useState(false);
|
|
24
25
|
const contentRef = useRef(null);
|
|
25
|
-
const lastFocusedEl = useRef(null);
|
|
26
26
|
const portalTarget = useRef(null);
|
|
27
27
|
const controlled = open !== undefined;
|
|
28
28
|
const isOpen = controlled ? open : busOpen;
|
|
@@ -36,10 +36,8 @@ export default function Modal({ id, open, onClose, width, height, children, titl
|
|
|
36
36
|
return;
|
|
37
37
|
const setIsOpen = setBusOpen;
|
|
38
38
|
const offOpen = on("modal:open", (targetId) => {
|
|
39
|
-
if (targetId === id)
|
|
40
|
-
lastFocusedEl.current = document.activeElement ?? null;
|
|
39
|
+
if (targetId === id)
|
|
41
40
|
setIsOpen(true);
|
|
42
|
-
}
|
|
43
41
|
});
|
|
44
42
|
const offClose = on("modal:close", (targetId) => {
|
|
45
43
|
if (targetId === id)
|
|
@@ -61,135 +59,105 @@ export default function Modal({ id, open, onClose, width, height, children, titl
|
|
|
61
59
|
}
|
|
62
60
|
portalTarget.current = el;
|
|
63
61
|
}, []);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
document.body.style.overflow = "hidden";
|
|
73
|
-
const to = setTimeout(() => {
|
|
74
|
-
const container = contentRef.current;
|
|
75
|
-
if (!container)
|
|
76
|
-
return;
|
|
77
|
-
const focusables = container.querySelectorAll(FOCUSABLE);
|
|
78
|
-
(focusables[0] ?? container).focus();
|
|
79
|
-
}, 0);
|
|
80
|
-
const onKeyDown = (e) => {
|
|
81
|
-
if (e.key === "Escape" && !preventClose) {
|
|
82
|
-
dismissRef.current();
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
if (e.key !== "Tab")
|
|
86
|
-
return;
|
|
87
|
-
const container = contentRef.current;
|
|
88
|
-
if (!container)
|
|
89
|
-
return;
|
|
90
|
-
const focusables = Array.from(container.querySelectorAll(FOCUSABLE)).filter(el => !el.hasAttribute("disabled") && !el.getAttribute("aria-hidden"));
|
|
91
|
-
if (focusables.length === 0)
|
|
92
|
-
return;
|
|
93
|
-
const first = focusables[0];
|
|
94
|
-
const last = focusables[focusables.length - 1];
|
|
95
|
-
const active = document.activeElement;
|
|
96
|
-
if (e.shiftKey) {
|
|
97
|
-
if (active === first || !container.contains(active)) {
|
|
98
|
-
last.focus();
|
|
99
|
-
e.preventDefault();
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
else {
|
|
103
|
-
if (active === last || !container.contains(active)) {
|
|
104
|
-
first.focus();
|
|
105
|
-
e.preventDefault();
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
const onFocusIn = (e) => {
|
|
110
|
-
const container = contentRef.current;
|
|
111
|
-
if (!container)
|
|
112
|
-
return;
|
|
113
|
-
if (!container.contains(e.target))
|
|
114
|
-
container.focus();
|
|
115
|
-
};
|
|
116
|
-
document.addEventListener("keydown", onKeyDown);
|
|
117
|
-
document.addEventListener("focusin", onFocusIn);
|
|
118
|
-
return () => {
|
|
119
|
-
document.body.style.overflow = prevOverflow;
|
|
120
|
-
document.body.style.paddingRight = prevPadRight;
|
|
121
|
-
clearTimeout(to);
|
|
122
|
-
document.removeEventListener("keydown", onKeyDown);
|
|
123
|
-
document.removeEventListener("focusin", onFocusIn);
|
|
124
|
-
lastFocusedEl.current?.focus?.();
|
|
125
|
-
lastFocusedEl.current = null;
|
|
126
|
-
};
|
|
127
|
-
}, [isOpen, preventClose]);
|
|
62
|
+
useStyle("modal", modalCss);
|
|
63
|
+
const { isTop, zIndex } = useOverlayStack(isOpen);
|
|
64
|
+
useScrollLock(isOpen);
|
|
65
|
+
// Paused while a menu or drawer is open above: focus may leave the modal for it.
|
|
66
|
+
useFocusTrap(contentRef, isOpen, !isTop);
|
|
67
|
+
// Only the overlay on top of the stack closes on Escape.
|
|
68
|
+
const onEscape = useCallback(() => dismissRef.current(), []);
|
|
69
|
+
useEscapeKey(onEscape, isOpen && isTop && !preventClose);
|
|
128
70
|
if (!isOpen || !portalTarget.current)
|
|
129
71
|
return null;
|
|
130
|
-
const w = typeof width === "number" ? `${width}px` : (width ?? "min(540px, 95vw)");
|
|
131
|
-
const h = typeof height === "number" ? `${height}px` : height;
|
|
132
|
-
const prefersReducedMotion = typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches;
|
|
133
|
-
const scrimStyle = {
|
|
134
|
-
position: "fixed",
|
|
135
|
-
inset: 0,
|
|
136
|
-
zIndex: 9999,
|
|
137
|
-
display: "grid",
|
|
138
|
-
placeItems: "center",
|
|
139
|
-
padding: "1rem",
|
|
140
|
-
backdropFilter: "brightness(0.45) blur(2px)",
|
|
141
|
-
WebkitBackdropFilter: "brightness(0.45) blur(2px)",
|
|
142
|
-
animation: prefersReducedMotion ? "none" : "modalFade 150ms ease-out",
|
|
143
|
-
};
|
|
144
72
|
const cardStyle = {
|
|
145
|
-
width:
|
|
146
|
-
height:
|
|
147
|
-
|
|
148
|
-
maxHeight: "85vh",
|
|
149
|
-
display: "flex",
|
|
150
|
-
flexDirection: "column",
|
|
151
|
-
overflow: "hidden",
|
|
152
|
-
outline: "none",
|
|
153
|
-
backgroundColor: "var(--color-surface)",
|
|
154
|
-
border: "1px solid var(--color-border)",
|
|
155
|
-
borderRadius: "12px",
|
|
156
|
-
animation: prefersReducedMotion ? "none" : "modalPop 160ms ease-out",
|
|
73
|
+
width: typeof width === "number" ? `${width}px` : (width ?? "min(540px, 95vw)"),
|
|
74
|
+
height: typeof height === "number" ? `${height}px` : height,
|
|
75
|
+
...style,
|
|
157
76
|
};
|
|
158
|
-
const keyframes = (_jsx("style", { children: `
|
|
159
|
-
@keyframes modalFade { from { opacity: 0 } to { opacity: 1 } }
|
|
160
|
-
@keyframes modalPop {
|
|
161
|
-
from { opacity: 0; transform: translateY(4px) scale(0.98) }
|
|
162
|
-
to { opacity: 1; transform: translateY(0) scale(1) }
|
|
163
|
-
}
|
|
164
|
-
` }));
|
|
165
77
|
const framed = title !== undefined || footer !== undefined;
|
|
166
|
-
return createPortal(
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
78
|
+
return createPortal(_jsx("div", { className: "rui-modal-scrim", style: { zIndex }, onMouseDown: e => {
|
|
79
|
+
if (!preventClose && e.target === e.currentTarget)
|
|
80
|
+
dismissRef.current();
|
|
81
|
+
}, children: _jsxs("div", { ref: contentRef, role: "dialog", "aria-modal": "true", "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, tabIndex: -1, className: className ? `rui-modal ${className}` : "rui-modal", "data-framed": framed || undefined, style: cardStyle, onMouseDown: e => e.stopPropagation(), children: [title !== undefined && (_jsxs("div", { className: "rui-modal-header", children: [_jsx("span", { className: "rui-modal-title", children: title }), _jsx("button", { type: "button", className: "rui-modal-close", onClick: dismiss, "aria-label": closeLabel, children: "\u2715" })] })), _jsx("div", { className: "rui-modal-body", children: children }), footer && _jsx("div", { className: "rui-modal-footer", children: footer })] }) }), portalTarget.current);
|
|
82
|
+
}
|
|
83
|
+
// Every selector is wrapped in :where() so it has zero specificity:
|
|
84
|
+
// any class or style set by the consuming project wins without !important.
|
|
85
|
+
const modalCss = `
|
|
86
|
+
:where(.rui-modal-scrim) {
|
|
87
|
+
position: fixed;
|
|
88
|
+
inset: 0;
|
|
89
|
+
display: grid;
|
|
90
|
+
place-items: center;
|
|
91
|
+
padding: 1rem;
|
|
92
|
+
-webkit-backdrop-filter: brightness(0.45) blur(2px);
|
|
93
|
+
backdrop-filter: brightness(0.45) blur(2px);
|
|
94
|
+
animation: rui-modal-fade 150ms ease-out;
|
|
95
|
+
}
|
|
96
|
+
:where(.rui-modal) {
|
|
97
|
+
max-width: 95vw;
|
|
98
|
+
max-height: 85vh;
|
|
99
|
+
display: flex;
|
|
100
|
+
flex-direction: column;
|
|
101
|
+
overflow: hidden;
|
|
102
|
+
outline: none;
|
|
103
|
+
background-color: var(--color-surface);
|
|
104
|
+
border: 1px solid var(--color-border);
|
|
105
|
+
border-radius: 12px;
|
|
106
|
+
animation: rui-modal-pop 160ms ease-out;
|
|
107
|
+
}
|
|
108
|
+
:where(.rui-modal-header) {
|
|
109
|
+
display: flex;
|
|
110
|
+
align-items: center;
|
|
111
|
+
justify-content: space-between;
|
|
112
|
+
padding: 1rem 1.25rem;
|
|
113
|
+
border-bottom: 1px solid var(--color-border);
|
|
114
|
+
}
|
|
115
|
+
:where(.rui-modal-title) {
|
|
116
|
+
font-size: 0.95rem;
|
|
117
|
+
font-weight: 600;
|
|
118
|
+
color: var(--color-text);
|
|
119
|
+
}
|
|
120
|
+
:where(.rui-modal-close) {
|
|
121
|
+
border: none;
|
|
122
|
+
border-radius: 4px;
|
|
123
|
+
background: none;
|
|
124
|
+
cursor: pointer;
|
|
125
|
+
color: var(--color-text-muted);
|
|
126
|
+
font-size: 0.95rem;
|
|
127
|
+
line-height: 1;
|
|
128
|
+
}
|
|
129
|
+
:where(.rui-modal-close:hover) {
|
|
130
|
+
color: var(--color-text);
|
|
131
|
+
}
|
|
132
|
+
:where(.rui-modal-close:focus-visible) {
|
|
133
|
+
outline: 2px solid var(--color-primary);
|
|
134
|
+
outline-offset: 2px;
|
|
135
|
+
}
|
|
136
|
+
:where(.rui-modal-body) {
|
|
137
|
+
flex: 1;
|
|
138
|
+
overflow-y: auto;
|
|
139
|
+
}
|
|
140
|
+
:where(.rui-modal[data-framed] > .rui-modal-body) {
|
|
141
|
+
padding: 1.25rem;
|
|
142
|
+
}
|
|
143
|
+
:where(.rui-modal-footer) {
|
|
144
|
+
display: flex;
|
|
145
|
+
justify-content: flex-end;
|
|
146
|
+
gap: 0.75rem;
|
|
147
|
+
padding: 1rem 1.25rem;
|
|
148
|
+
border-top: 1px solid var(--color-border);
|
|
149
|
+
}
|
|
150
|
+
@media (prefers-reduced-motion: reduce) {
|
|
151
|
+
:where(.rui-modal-scrim, .rui-modal) {
|
|
152
|
+
animation: none;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
@keyframes rui-modal-fade {
|
|
156
|
+
from { opacity: 0; }
|
|
157
|
+
to { opacity: 1; }
|
|
158
|
+
}
|
|
159
|
+
@keyframes rui-modal-pop {
|
|
160
|
+
from { opacity: 0; transform: translateY(4px) scale(0.98); }
|
|
161
|
+
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
162
|
+
}
|
|
163
|
+
`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NavBar.d.ts","sourceRoot":"","sources":["../../src/components/NavBar.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"NavBar.d.ts","sourceRoot":"","sources":["../../src/components/NavBar.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGrD,KAAK,WAAW,GAAG;IACjB;;;;OAIG;IACH,QAAQ,EAAE,SAAS,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,aAAa,CAAA;CACtB,CAAA;AAED,wBAAgB,MAAM,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,WAAW,+BAOjE;yBAPe,MAAM"}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useStyle } from "../style/useStyle.js";
|
|
2
3
|
export function NavBar({ children, className, style }) {
|
|
3
|
-
|
|
4
|
+
useStyle("navbar", navBarCss);
|
|
5
|
+
return (_jsx("nav", { className: className ? `rui-navbar ${className}` : "rui-navbar", style: style, children: children }));
|
|
4
6
|
}
|
|
5
7
|
/** Takes the free space, pushing the following children to the right. */
|
|
6
8
|
function Spacer() {
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import type { ReactNode } from "react";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
type ErrorMessageProps = {
|
|
9
|
-
onRetry?: () => void;
|
|
10
|
-
label?: string;
|
|
11
|
-
retryLabel?: string;
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
2
|
+
type PageStatusProps = Omit<HTMLAttributes<HTMLDivElement>, "title"> & {
|
|
3
|
+
icon?: ReactNode;
|
|
4
|
+
title?: ReactNode;
|
|
5
|
+
description?: ReactNode;
|
|
6
|
+
/** Buttons or links, e.g. a retry. */
|
|
7
|
+
action?: ReactNode;
|
|
12
8
|
};
|
|
13
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Loading, empty, error or success state, centered horizontally. Every slot is optional and
|
|
11
|
+
* no text is provided by default. Pass `role="status"` (loading) or `role="alert"` (error).
|
|
12
|
+
*/
|
|
13
|
+
export declare function PageStatus({ icon, title, description, action, className, children, ...props }: PageStatusProps): import("react").JSX.Element;
|
|
14
14
|
export {};
|
|
15
15
|
//# sourceMappingURL=PageStatus.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PageStatus.d.ts","sourceRoot":"","sources":["../../src/components/PageStatus.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"PageStatus.d.ts","sourceRoot":"","sources":["../../src/components/PageStatus.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAItD,KAAK,eAAe,GAAG,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC,GAAG;IACrE,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,WAAW,CAAC,EAAE,SAAS,CAAA;IACvB,sCAAsC;IACtC,MAAM,CAAC,EAAE,SAAS,CAAA;CACnB,CAAA;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,KAAK,EAAE,EAAE,eAAe,+BAW9G"}
|