@firecms/ui 3.0.1 → 3.1.0-canary.02232f4
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 +9 -7
- package/dist/components/BooleanSwitchWithLabel.d.ts +2 -1
- package/dist/components/Card.d.ts +1 -1
- package/dist/components/Chip.d.ts +1 -1
- package/dist/components/ColorPicker.d.ts +30 -0
- package/dist/components/DateTimeField.d.ts +7 -0
- package/dist/components/Dialog.d.ts +2 -1
- package/dist/components/FileUpload.d.ts +1 -1
- package/dist/components/Menu.d.ts +2 -1
- package/dist/components/Menubar.d.ts +2 -1
- package/dist/components/MultiSelect.d.ts +2 -1
- package/dist/components/ResizablePanels.d.ts +16 -0
- package/dist/components/SearchBar.d.ts +11 -1
- package/dist/components/SearchableSelect.d.ts +48 -0
- package/dist/components/Select.d.ts +2 -1
- package/dist/components/Sheet.d.ts +1 -0
- package/dist/components/Tabs.d.ts +8 -1
- package/dist/components/ToggleButtonGroup.d.ts +30 -0
- package/dist/components/Tooltip.d.ts +18 -2
- package/dist/components/index.d.ts +4 -0
- package/dist/hooks/PortalContainerContext.d.ts +31 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/useOutsideAlerter.d.ts +1 -1
- package/dist/icons/FirestoreIcon.d.ts +6 -0
- package/dist/icons/components/DatabaseIcon.d.ts +6 -0
- package/dist/icons/index.d.ts +2 -0
- package/dist/index.css +57 -6
- package/dist/index.es.js +2846 -1165
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +2846 -1165
- package/dist/index.umd.js.map +1 -1
- package/dist/styles.d.ts +11 -11
- package/package.json +7 -7
- package/src/components/BooleanSwitch.tsx +3 -3
- package/src/components/BooleanSwitchWithLabel.tsx +4 -0
- package/src/components/Button.tsx +6 -5
- package/src/components/Card.tsx +7 -7
- package/src/components/Checkbox.tsx +1 -1
- package/src/components/Chip.tsx +4 -3
- package/src/components/ColorPicker.tsx +134 -0
- package/src/components/DateTimeField.tsx +129 -35
- package/src/components/DebouncedTextField.tsx +3 -3
- package/src/components/Dialog.tsx +25 -16
- package/src/components/DialogActions.tsx +1 -1
- package/src/components/ExpandablePanel.tsx +1 -1
- package/src/components/FileUpload.tsx +25 -24
- package/src/components/IconButton.tsx +3 -2
- package/src/components/Menu.tsx +44 -30
- package/src/components/Menubar.tsx +14 -3
- package/src/components/MultiSelect.tsx +113 -77
- package/src/components/Popover.tsx +11 -3
- package/src/components/ResizablePanels.tsx +181 -0
- package/src/components/SearchBar.tsx +37 -19
- package/src/components/SearchableSelect.tsx +335 -0
- package/src/components/Select.tsx +86 -73
- package/src/components/Separator.tsx +2 -2
- package/src/components/Sheet.tsx +12 -3
- package/src/components/Skeleton.tsx +4 -2
- package/src/components/Slider.tsx +4 -4
- package/src/components/Table.tsx +1 -1
- package/src/components/Tabs.tsx +150 -37
- package/src/components/TextField.tsx +19 -8
- package/src/components/TextareaAutosize.tsx +77 -212
- package/src/components/ToggleButtonGroup.tsx +67 -0
- package/src/components/Tooltip.tsx +16 -8
- package/src/components/index.tsx +4 -0
- package/src/hooks/PortalContainerContext.tsx +48 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useInjectStyles.tsx +12 -3
- package/src/hooks/useOutsideAlerter.tsx +1 -1
- package/src/icons/FirestoreIcon.tsx +47 -0
- package/src/icons/components/DatabaseIcon.tsx +10 -0
- package/src/icons/index.ts +2 -0
- package/src/index.css +57 -6
- package/src/styles.ts +11 -11
- package/src/util/cls.ts +1 -1
- package/tailwind.config.js +2 -3
|
@@ -3,6 +3,7 @@ import React, { useEffect, useState } from "react";
|
|
|
3
3
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
4
4
|
import { paperMixin } from "../styles";
|
|
5
5
|
import { cls } from "../util";
|
|
6
|
+
import { usePortalContainer } from "../hooks/PortalContainerContext";
|
|
6
7
|
|
|
7
8
|
export type DialogProps = {
|
|
8
9
|
open?: boolean;
|
|
@@ -24,21 +25,22 @@ export type DialogProps = {
|
|
|
24
25
|
* If `true`, the dialog will not focus the first focusable element when opened.
|
|
25
26
|
*/
|
|
26
27
|
disableInitialFocus?: boolean;
|
|
28
|
+
portalContainer?: HTMLElement | null;
|
|
27
29
|
};
|
|
28
30
|
|
|
29
31
|
const widthClasses = {
|
|
30
|
-
xs: "max-w-xs
|
|
31
|
-
sm: "max-w-sm
|
|
32
|
-
md: "max-w-md
|
|
33
|
-
lg: "max-w-lg
|
|
34
|
-
xl: "max-w-xl
|
|
35
|
-
"2xl": "max-w-2xl
|
|
36
|
-
"3xl": "max-w-3xl
|
|
37
|
-
"4xl": "max-w-4xl
|
|
38
|
-
"5xl": "max-w-5xl
|
|
39
|
-
"6xl": "max-w-6xl
|
|
40
|
-
"7xl": "max-w-7xl
|
|
41
|
-
full: "max-w-full
|
|
32
|
+
xs: "max-w-xs w-xs",
|
|
33
|
+
sm: "max-w-sm w-sm",
|
|
34
|
+
md: "max-w-md w-md",
|
|
35
|
+
lg: "max-w-lg w-lg",
|
|
36
|
+
xl: "max-w-xl w-xl",
|
|
37
|
+
"2xl": "max-w-2xl w-2xl",
|
|
38
|
+
"3xl": "max-w-3xl w-3xl",
|
|
39
|
+
"4xl": "max-w-4xl w-4xl",
|
|
40
|
+
"5xl": "max-w-5xl w-5xl",
|
|
41
|
+
"6xl": "max-w-6xl w-6xl",
|
|
42
|
+
"7xl": "max-w-7xl w-7xl",
|
|
43
|
+
full: "max-w-full w-full"
|
|
42
44
|
};
|
|
43
45
|
|
|
44
46
|
export const Dialog = ({
|
|
@@ -57,15 +59,22 @@ export const Dialog = ({
|
|
|
57
59
|
onEscapeKeyDown,
|
|
58
60
|
onPointerDownOutside,
|
|
59
61
|
onInteractOutside,
|
|
60
|
-
disableInitialFocus = true
|
|
62
|
+
disableInitialFocus = true,
|
|
63
|
+
portalContainer
|
|
61
64
|
}: DialogProps) => {
|
|
62
65
|
const [displayed, setDisplayed] = useState(false);
|
|
63
66
|
|
|
67
|
+
// Get the portal container from context
|
|
68
|
+
const contextContainer = usePortalContainer();
|
|
69
|
+
|
|
70
|
+
// Prioritize manual prop, fallback to context container
|
|
71
|
+
const finalContainer = (portalContainer ?? contextContainer ?? undefined) as HTMLElement | undefined;
|
|
72
|
+
|
|
64
73
|
useEffect(() => {
|
|
65
74
|
if (!open) {
|
|
66
75
|
const timeout = setTimeout(() => {
|
|
67
76
|
setDisplayed(false);
|
|
68
|
-
},
|
|
77
|
+
}, 100);
|
|
69
78
|
return () => clearTimeout(timeout);
|
|
70
79
|
} else {
|
|
71
80
|
setDisplayed(true);
|
|
@@ -78,12 +87,12 @@ export const Dialog = ({
|
|
|
78
87
|
<DialogPrimitive.Root open={displayed || open}
|
|
79
88
|
modal={modal}
|
|
80
89
|
onOpenChange={onOpenChange}>
|
|
81
|
-
<DialogPrimitive.Portal>
|
|
90
|
+
<DialogPrimitive.Portal container={finalContainer}>
|
|
82
91
|
|
|
83
92
|
<div className={cls("fixed inset-0 z-30", containerClassName)}>
|
|
84
93
|
|
|
85
94
|
<DialogPrimitive.Overlay
|
|
86
|
-
className={cls("fixed inset-0 transition-opacity z-20 ease-in-out duration-200 bg-black bg-opacity-
|
|
95
|
+
className={cls("fixed inset-0 transition-opacity z-20 ease-in-out duration-200 bg-black dark:bg-opacity-60 dark:bg-black/60 bg-opacity-50 bg-black/50 dark: bg-black/60 backdrop-blur-sm ",
|
|
87
96
|
displayed && open ? "opacity-100" : "opacity-0",
|
|
88
97
|
"z-20 fixed top-0 left-0 w-full h-full flex justify-center items-center"
|
|
89
98
|
)}
|
|
@@ -19,7 +19,7 @@ export function DialogActions({
|
|
|
19
19
|
defaultBorderMixin,
|
|
20
20
|
"pt-2 pb-4 px-4 border-t flex flex-row items-center justify-end bottom-0 right-0 left-0 text-right z-2 gap-2",
|
|
21
21
|
position,
|
|
22
|
-
"bg-white bg-opacity-60 dark:bg-surface-900 dark:bg-opacity-60",
|
|
22
|
+
"bg-white bg-opacity-60 bg-white/60 dark:bg-surface-900 dark:bg-opacity-60 dark:bg-surface-900/60",
|
|
23
23
|
translucent ? "backdrop-blur-sm" : "",
|
|
24
24
|
className)}>
|
|
25
25
|
{children}
|
|
@@ -95,7 +95,7 @@ export function ExpandablePanel({
|
|
|
95
95
|
<div
|
|
96
96
|
className={cls(
|
|
97
97
|
"rounded-t flex items-center justify-between w-full min-h-[52px]",
|
|
98
|
-
"hover:bg-surface-accent-200 hover:bg-opacity-40 dark:hover:bg-surface-800 dark:hover:bg-opacity-40",
|
|
98
|
+
"hover:bg-surface-accent-200 hover:bg-opacity-40 hover:bg-surface-accent-200/40 dark:hover:bg-surface-800 dark:hover:bg-opacity-40 dark:hover:bg-surface-800/40",
|
|
99
99
|
invisible ? "border-b px-2" : "p-4",
|
|
100
100
|
open ? "py-6" : "py-4",
|
|
101
101
|
"transition-all duration-200",
|
|
@@ -28,22 +28,22 @@ export type FileUploadProps = {
|
|
|
28
28
|
title?: React.ReactNode;
|
|
29
29
|
uploadDescription?: React.ReactNode;
|
|
30
30
|
preventDropOnDocument?: boolean;
|
|
31
|
-
size?: "medium" | "large";
|
|
31
|
+
size?: "small" | "medium" | "large";
|
|
32
32
|
};
|
|
33
33
|
|
|
34
34
|
export function FileUpload({
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
35
|
+
accept,
|
|
36
|
+
onFilesAdded,
|
|
37
|
+
onFilesRejected,
|
|
38
|
+
maxSize,
|
|
39
|
+
disabled,
|
|
40
|
+
maxFiles,
|
|
41
|
+
title,
|
|
42
|
+
uploadDescription,
|
|
43
|
+
children,
|
|
44
|
+
preventDropOnDocument = true,
|
|
45
|
+
size
|
|
46
|
+
}: React.PropsWithChildren<FileUploadProps>) {
|
|
47
47
|
|
|
48
48
|
const {
|
|
49
49
|
getRootProps,
|
|
@@ -52,15 +52,15 @@ export function FileUpload({
|
|
|
52
52
|
isDragAccept,
|
|
53
53
|
isDragReject
|
|
54
54
|
} = useDropzone({
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
55
|
+
accept,
|
|
56
|
+
noDragEventsBubbling: true,
|
|
57
|
+
maxSize,
|
|
58
|
+
onDrop: onFilesAdded,
|
|
59
|
+
onDropRejected: onFilesRejected,
|
|
60
|
+
disabled,
|
|
61
|
+
maxFiles,
|
|
62
|
+
preventDropOnDocument
|
|
63
|
+
}
|
|
64
64
|
);
|
|
65
65
|
return <div
|
|
66
66
|
{...getRootProps()}
|
|
@@ -71,6 +71,7 @@ export function FileUpload({
|
|
|
71
71
|
{
|
|
72
72
|
"h-44": size === "large",
|
|
73
73
|
"h-28": size === "medium",
|
|
74
|
+
"h-16": size === "small",
|
|
74
75
|
"cursor-pointer": !disabled,
|
|
75
76
|
[fieldBackgroundHoverMixin]: !isDragActive,
|
|
76
77
|
"transition-colors duration-200 ease-[cubic-bezier(0,0,0.2,1)] border-red-500": isDragReject,
|
|
@@ -89,8 +90,8 @@ export function FileUpload({
|
|
|
89
90
|
<div
|
|
90
91
|
className="flex-grow h-28 box-border flex flex-col items-center justify-center text-center">
|
|
91
92
|
<Typography align={"center"}
|
|
92
|
-
|
|
93
|
-
|
|
93
|
+
variant={"label"}
|
|
94
|
+
className={"flex flex-row gap-2 justify-center"}>
|
|
94
95
|
{uploadDescription}
|
|
95
96
|
</Typography>
|
|
96
97
|
</div>
|
|
@@ -14,7 +14,7 @@ export type IconButtonProps<C extends React.ElementType> =
|
|
|
14
14
|
onClick?: React.MouseEventHandler<any>
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
const buttonClasses = "hover:bg-surface-accent-200 hover:bg-opacity-75 dark:hover:bg-surface-accent-800 hover:scale-105 transition-transform";
|
|
17
|
+
const buttonClasses = "hover:bg-surface-accent-200 hover:bg-opacity-75 hover:bg-surface-accent-200/75 dark:hover:bg-surface-accent-800 hover:scale-105 transition-transform";
|
|
18
18
|
const baseClasses = "inline-flex items-center justify-center p-2 text-sm font-medium focus:outline-none transition-colors ease-in-out duration-150";
|
|
19
19
|
const colorClasses = "text-surface-accent-600 visited:text-surface-accent-600 dark:text-surface-accent-300 dark:visited:text-surface-300";
|
|
20
20
|
const sizeClasses = {
|
|
@@ -40,7 +40,7 @@ const IconButtonInner = <C extends React.ElementType = "button">({
|
|
|
40
40
|
...props
|
|
41
41
|
}: IconButtonProps<C>, ref: React.ForwardedRef<HTMLButtonElement>) => {
|
|
42
42
|
|
|
43
|
-
const bgClasses = variant === "ghost" ? "bg-transparent" : "bg-surface-accent-200 bg-opacity-50 dark:bg-surface-950 dark:bg-opacity-50";
|
|
43
|
+
const bgClasses = variant === "ghost" ? "bg-transparent" : "bg-surface-accent-200 bg-opacity-50 bg-surface-accent-200/50 dark:bg-surface-950 dark:bg-opacity-50 dark:bg-surface-950/50";
|
|
44
44
|
const Component: React.ElementType<any> = component || "button";
|
|
45
45
|
return (
|
|
46
46
|
<Component
|
|
@@ -50,6 +50,7 @@ const IconButtonInner = <C extends React.ElementType = "button">({
|
|
|
50
50
|
className={cls(
|
|
51
51
|
disabled ? "opacity-50 pointer-events-none" : "cursor-pointer",
|
|
52
52
|
toggled ? "outline outline-2 outline-primary" : "",
|
|
53
|
+
"text-inherit dark:text-inherit",
|
|
53
54
|
colorClasses,
|
|
54
55
|
bgClasses,
|
|
55
56
|
baseClasses,
|
package/src/components/Menu.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import React from "react";
|
|
|
3
3
|
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
|
4
4
|
import { focusedDisabled, paperMixin } from "../styles";
|
|
5
5
|
import { cls } from "../util";
|
|
6
|
+
import { usePortalContainer } from "../hooks/PortalContainerContext";
|
|
6
7
|
|
|
7
8
|
export type MenuProps = {
|
|
8
9
|
children: React.ReactNode;
|
|
@@ -33,28 +34,36 @@ const Menu = React.forwardRef<
|
|
|
33
34
|
onOpenChange,
|
|
34
35
|
portalContainer,
|
|
35
36
|
sideOffset = 4,
|
|
36
|
-
|
|
37
|
-
}, ref) =>
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
37
|
+
className
|
|
38
|
+
}, ref) => {
|
|
39
|
+
// Get the portal container from context
|
|
40
|
+
const contextContainer = usePortalContainer();
|
|
41
|
+
|
|
42
|
+
// Prioritize manual prop, fallback to context container
|
|
43
|
+
const finalContainer = (portalContainer ?? contextContainer ?? undefined) as HTMLElement | undefined;
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<DropdownMenu.Root
|
|
47
|
+
open={open}
|
|
48
|
+
defaultOpen={defaultOpen}
|
|
49
|
+
onOpenChange={onOpenChange}>
|
|
50
|
+
<DropdownMenu.Trigger
|
|
51
|
+
ref={ref}
|
|
52
|
+
asChild>
|
|
53
|
+
{trigger}
|
|
54
|
+
</DropdownMenu.Trigger>
|
|
55
|
+
<DropdownMenu.Portal container={finalContainer}>
|
|
56
|
+
<DropdownMenu.Content
|
|
57
|
+
side={side}
|
|
58
|
+
sideOffset={sideOffset}
|
|
59
|
+
align={align}
|
|
60
|
+
className={cls(paperMixin, focusedDisabled, "py-2 z-30", className)}>
|
|
61
|
+
{children}
|
|
62
|
+
</DropdownMenu.Content>
|
|
63
|
+
</DropdownMenu.Portal>
|
|
64
|
+
</DropdownMenu.Root>
|
|
65
|
+
);
|
|
66
|
+
})
|
|
58
67
|
Menu.displayName = "Menu"
|
|
59
68
|
|
|
60
69
|
export { Menu }
|
|
@@ -62,20 +71,24 @@ export { Menu }
|
|
|
62
71
|
export type MenuItemProps = {
|
|
63
72
|
children: React.ReactNode;
|
|
64
73
|
dense?: boolean;
|
|
74
|
+
disabled?: boolean;
|
|
65
75
|
onClick?: (event: React.MouseEvent) => void;
|
|
66
76
|
className?: string;
|
|
67
77
|
};
|
|
68
78
|
|
|
69
79
|
export const MenuItem = React.memo(({
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
80
|
+
children,
|
|
81
|
+
dense = false, // Default value is false if not provided
|
|
82
|
+
disabled = false,
|
|
83
|
+
onClick,
|
|
84
|
+
className
|
|
85
|
+
}: MenuItemProps) => {
|
|
75
86
|
// Dynamically adjusting the class based on the "dense" prop
|
|
76
87
|
const classNames = cls(
|
|
77
|
-
onClick && "cursor-pointer",
|
|
78
|
-
|
|
88
|
+
onClick && !disabled && "cursor-pointer",
|
|
89
|
+
disabled && "opacity-50 cursor-not-allowed",
|
|
90
|
+
"rounded-md text-sm font-medium text-surface-accent-700 dark:text-surface-accent-300 flex items-center gap-4",
|
|
91
|
+
!disabled && "hover:bg-surface-accent-100 dark:hover:bg-surface-accent-900",
|
|
79
92
|
dense ? "px-4 py-1.5" : "px-4 py-2",
|
|
80
93
|
className
|
|
81
94
|
);
|
|
@@ -83,7 +96,8 @@ export const MenuItem = React.memo(({
|
|
|
83
96
|
return (
|
|
84
97
|
<DropdownMenu.Item
|
|
85
98
|
className={classNames}
|
|
86
|
-
|
|
99
|
+
disabled={disabled}
|
|
100
|
+
onClick={disabled ? undefined : onClick}>
|
|
87
101
|
{children}
|
|
88
102
|
</DropdownMenu.Item>
|
|
89
103
|
);
|
|
@@ -3,6 +3,7 @@ import * as React from "react";
|
|
|
3
3
|
import * as MenubarPrimitive from "@radix-ui/react-menubar";
|
|
4
4
|
import { cls } from "../util";
|
|
5
5
|
import { CheckIcon, ChevronRightIcon } from "../icons";
|
|
6
|
+
import { usePortalContainer } from "../hooks/PortalContainerContext";
|
|
6
7
|
|
|
7
8
|
export function Menubar({
|
|
8
9
|
children,
|
|
@@ -44,7 +45,7 @@ export function MenubarTrigger({
|
|
|
44
45
|
return (
|
|
45
46
|
<MenubarPrimitive.Trigger
|
|
46
47
|
onSelect={onSelect}
|
|
47
|
-
className={cls("py-2 px-3 outline-none select-none font-medium leading-none rounded text-text-primary dark:text-text-primary-dark text-[13px] flex items-center justify-between gap-[2px] data-[highlighted]:bg-surface-accent-100 data-[highlighted]:dark:bg-surface-800 data-[state=open]:bg-surface-accent-100 data-[state=open]:dark:bg-surface-800 hover:bg-surface-accent-200 hover:bg-opacity-75 dark:hover:bg-surface-accent-800",
|
|
48
|
+
className={cls("py-2 px-3 outline-none select-none font-medium leading-none rounded text-text-primary dark:text-text-primary-dark text-[13px] flex items-center justify-between gap-[2px] data-[highlighted]:bg-surface-accent-100 data-[highlighted]:dark:bg-surface-800 data-[state=open]:bg-surface-accent-100 data-[state=open]:dark:bg-surface-800 hover:bg-surface-accent-200 hover:bg-opacity-75 hover:bg-surface-accent-200/75 dark:hover:bg-surface-accent-800",
|
|
48
49
|
className)}>
|
|
49
50
|
{children}
|
|
50
51
|
</MenubarPrimitive.Trigger>
|
|
@@ -53,9 +54,19 @@ export function MenubarTrigger({
|
|
|
53
54
|
|
|
54
55
|
export function MenubarPortal({
|
|
55
56
|
children,
|
|
56
|
-
|
|
57
|
+
portalContainer,
|
|
58
|
+
}: {
|
|
59
|
+
children: React.ReactNode;
|
|
60
|
+
portalContainer?: HTMLElement | null;
|
|
61
|
+
}) {
|
|
62
|
+
// Get the portal container from context
|
|
63
|
+
const contextContainer = usePortalContainer();
|
|
64
|
+
|
|
65
|
+
// Prioritize manual prop, fallback to context container
|
|
66
|
+
const finalContainer = (portalContainer ?? contextContainer ?? undefined) as HTMLElement | undefined;
|
|
67
|
+
|
|
57
68
|
return (
|
|
58
|
-
<MenubarPrimitive.Portal>
|
|
69
|
+
<MenubarPrimitive.Portal container={finalContainer}>
|
|
59
70
|
{children}
|
|
60
71
|
</MenubarPrimitive.Portal>
|
|
61
72
|
)
|