@paroicms/react-ui 0.1.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 +9 -0
- package/dist/PuAccordion.d.ts +9 -0
- package/dist/PuAccordion.jsx +66 -0
- package/dist/PuButton.d.ts +14 -0
- package/dist/PuButton.jsx +15 -0
- package/dist/PuCheckbox.d.ts +8 -0
- package/dist/PuCheckbox.jsx +13 -0
- package/dist/PuInput.d.ts +10 -0
- package/dist/PuInput.jsx +13 -0
- package/dist/PuMenuItem.d.ts +7 -0
- package/dist/PuMenuItem.jsx +33 -0
- package/dist/PuPopupMenu.d.ts +14 -0
- package/dist/PuPopupMenu.jsx +135 -0
- package/dist/PuSelect.d.ts +17 -0
- package/dist/PuSelect.jsx +24 -0
- package/dist/PuSideMenu.d.ts +4 -0
- package/dist/PuSideMenu.jsx +15 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +9 -0
- package/dist/paroi-ui-lib-types.d.ts +47 -0
- package/dist/paroi-ui-lib-types.js +1 -0
- package/dist/svg-icons.d.ts +5 -0
- package/dist/svg-icons.jsx +30 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# @paroicms/react-ui
|
|
2
|
+
|
|
3
|
+
React UI toolkit for ParoiCMS.
|
|
4
|
+
|
|
5
|
+
This package is part of [ParoiCMS](https://www.npmjs.com/package/@paroicms/server).
|
|
6
|
+
|
|
7
|
+
## License
|
|
8
|
+
|
|
9
|
+
Released under the [MIT license](https://gitlab.com/paroi/opensource/paroicms/-/blob/main/LICENSE.md).
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { PuMenuOption } from "./paroi-ui-lib-types.js";
|
|
3
|
+
export interface PuAccordionProps {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
header: PuMenuOption;
|
|
6
|
+
/** Initial value */
|
|
7
|
+
expanded?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function PuAccordion({ children, header, expanded }: PuAccordionProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { useAsyncEffect } from "@paroi/use-async-effect";
|
|
2
|
+
import { useRef, useState } from "react";
|
|
3
|
+
import { PuMenuItem } from "./PuMenuItem.jsx";
|
|
4
|
+
export function PuAccordion({ children, header, expanded }) {
|
|
5
|
+
const [isExpanded, setIsExpanded] = useState(expanded ?? false);
|
|
6
|
+
const contentRef = useRef(null);
|
|
7
|
+
useAsyncEffect(async () => {
|
|
8
|
+
const content = contentRef.current;
|
|
9
|
+
if (!content)
|
|
10
|
+
return;
|
|
11
|
+
if (content.classList.contains("initializing")) {
|
|
12
|
+
content.classList.remove("initializing");
|
|
13
|
+
content.classList.add(isExpanded ? "show" : "hide");
|
|
14
|
+
if (!isExpanded) {
|
|
15
|
+
content.style.maxHeight = "0";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
await startTransition(content, isExpanded);
|
|
20
|
+
}
|
|
21
|
+
}, [isExpanded]);
|
|
22
|
+
return (<div className="PuAccordion">
|
|
23
|
+
<PuMenuItem item={header} expanded={isExpanded} onToggle={setIsExpanded}/>
|
|
24
|
+
<div className="PuAccordion-content initializing" ref={contentRef}>
|
|
25
|
+
{children}
|
|
26
|
+
</div>
|
|
27
|
+
</div>);
|
|
28
|
+
}
|
|
29
|
+
function startTransition(content, isOpen) {
|
|
30
|
+
if (isOpen) {
|
|
31
|
+
if (content.classList.contains("show"))
|
|
32
|
+
return Promise.resolve();
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
if (content.classList.contains("hide"))
|
|
36
|
+
return Promise.resolve();
|
|
37
|
+
}
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
let ended = false;
|
|
40
|
+
const timeoutId = setTimeout(endNow, 300);
|
|
41
|
+
content.classList.remove("show", "hide");
|
|
42
|
+
content.classList.add("transition");
|
|
43
|
+
content.style.maxHeight = isOpen ? "0" : `${content.scrollHeight}px`;
|
|
44
|
+
const transitionEnd = () => {
|
|
45
|
+
clearTimeout(timeoutId);
|
|
46
|
+
endNow();
|
|
47
|
+
};
|
|
48
|
+
content.addEventListener("transitionend", transitionEnd);
|
|
49
|
+
requestAnimationFrame(() => {
|
|
50
|
+
content.scrollHeight; // force reflow
|
|
51
|
+
content.style.maxHeight = isOpen ? `${content.scrollHeight}px` : "0";
|
|
52
|
+
});
|
|
53
|
+
function endNow() {
|
|
54
|
+
if (ended)
|
|
55
|
+
return;
|
|
56
|
+
ended = true;
|
|
57
|
+
content.classList.remove("transition");
|
|
58
|
+
content.classList.add(isOpen ? "show" : "hide");
|
|
59
|
+
if (isOpen) {
|
|
60
|
+
content.style.maxHeight = "";
|
|
61
|
+
}
|
|
62
|
+
content.removeEventListener("transitionend", transitionEnd);
|
|
63
|
+
resolve();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type MouseEvent, type ReactNode } from "react";
|
|
2
|
+
export interface PuButtonProps {
|
|
3
|
+
children: ReactNode;
|
|
4
|
+
className?: string;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
outlined?: boolean;
|
|
7
|
+
severity?: "primary" | "secondary" | "success" | "info" | "warning" | "danger";
|
|
8
|
+
onClick?: (e: MouseEvent<HTMLButtonElement>) => void;
|
|
9
|
+
"aria-label"?: string;
|
|
10
|
+
"aria-controls"?: string;
|
|
11
|
+
"aria-haspopup"?: boolean;
|
|
12
|
+
type?: "button" | "submit" | "reset";
|
|
13
|
+
}
|
|
14
|
+
export declare const PuButton: import("react").ForwardRefExoticComponent<PuButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
export const PuButton = forwardRef(({ children, className, disabled, outlined, severity = "primary", onClick, type = "button", ...rest }, ref) => {
|
|
3
|
+
const classes = [
|
|
4
|
+
"PuButton",
|
|
5
|
+
outlined ? "outlined" : "",
|
|
6
|
+
severity ? severity : "",
|
|
7
|
+
className || "",
|
|
8
|
+
disabled ? "disabled" : "",
|
|
9
|
+
]
|
|
10
|
+
.filter(Boolean)
|
|
11
|
+
.join(" ");
|
|
12
|
+
return (<button ref={ref} className={classes} disabled={disabled} onClick={onClick} type={type} {...rest}>
|
|
13
|
+
{children}
|
|
14
|
+
</button>);
|
|
15
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { InputHTMLAttributes } from "react";
|
|
2
|
+
export interface PuCheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "onChange" | "type"> {
|
|
3
|
+
checked: boolean;
|
|
4
|
+
onChange: (checked: boolean) => void;
|
|
5
|
+
label?: string;
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function PuCheckbox({ checked, onChange, label, className, ...rest }: PuCheckboxProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function PuCheckbox({ checked, onChange, label, className = "", ...rest }) {
|
|
2
|
+
const handleChange = (e) => {
|
|
3
|
+
onChange(e.target.checked);
|
|
4
|
+
};
|
|
5
|
+
const checkboxClasses = ["PuCheckbox", className].filter(Boolean).join(" ");
|
|
6
|
+
return (<div className={checkboxClasses}>
|
|
7
|
+
<label className="PuCheckbox-container">
|
|
8
|
+
<input className="PuCheckbox-input" type="checkbox" checked={checked} onChange={handleChange} {...rest}/>
|
|
9
|
+
<span className="PuCheckbox-checkmark"/>
|
|
10
|
+
{label && <span className="PuCheckbox-label">{label}</span>}
|
|
11
|
+
</label>
|
|
12
|
+
</div>);
|
|
13
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { InputHTMLAttributes } from "react";
|
|
2
|
+
export interface PuInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "onChange"> {
|
|
3
|
+
value: string;
|
|
4
|
+
onChange: (value: string) => void;
|
|
5
|
+
label?: string;
|
|
6
|
+
error?: string;
|
|
7
|
+
className?: string;
|
|
8
|
+
fullWidth?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function PuInput({ value, onChange, label, error, className, fullWidth, ...rest }: PuInputProps): import("react").JSX.Element;
|
package/dist/PuInput.jsx
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function PuInput({ value, onChange, label, error, className = "", fullWidth = false, ...rest }) {
|
|
2
|
+
const handleChange = (e) => {
|
|
3
|
+
onChange(e.target.value);
|
|
4
|
+
};
|
|
5
|
+
const inputClasses = ["PuInput", error ? "error" : "", fullWidth ? "fullWidth" : "", className]
|
|
6
|
+
.filter(Boolean)
|
|
7
|
+
.join(" ");
|
|
8
|
+
return (<div className={inputClasses}>
|
|
9
|
+
{label && <label className="PuInput-label">{label}</label>}
|
|
10
|
+
<input className="PuInput-field" value={value} onChange={handleChange} {...rest}/>
|
|
11
|
+
{error && <div className="PuInput-error">{error}</div>}
|
|
12
|
+
</div>);
|
|
13
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PuMenuOption } from "./paroi-ui-lib-types.js";
|
|
2
|
+
export interface PuMenuItemProps {
|
|
3
|
+
item: PuMenuOption;
|
|
4
|
+
expanded?: boolean;
|
|
5
|
+
onToggle?: (expanded: boolean) => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function PuMenuItem({ item, expanded, onToggle }: PuMenuItemProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { PuPopupMenu } from "./PuPopupMenu.jsx";
|
|
2
|
+
import { SubMenuIconBottom, SubMenuIconLeft } from "./svg-icons.jsx";
|
|
3
|
+
export function PuMenuItem({ item, expanded, onToggle }) {
|
|
4
|
+
let { key: id, label, icon, url, command, popupMenu, className, style, active, disabled } = item;
|
|
5
|
+
if (disabled) {
|
|
6
|
+
url = undefined;
|
|
7
|
+
command = undefined;
|
|
8
|
+
}
|
|
9
|
+
const itemClassNames = [
|
|
10
|
+
"PuMenuItem",
|
|
11
|
+
className || "",
|
|
12
|
+
active ? "active" : "",
|
|
13
|
+
disabled ? "disabled" : "",
|
|
14
|
+
]
|
|
15
|
+
.filter(Boolean)
|
|
16
|
+
.join(" ");
|
|
17
|
+
return (<div className={itemClassNames} style={style} key={id}>
|
|
18
|
+
{expanded !== undefined && onToggle ? (<button className="PuMenuItem-left toggle" onClick={() => onToggle(!expanded)} type="button">
|
|
19
|
+
{expanded ? <SubMenuIconBottom /> : <SubMenuIconLeft />}
|
|
20
|
+
</button>) : icon ? (<div className="PuMenuItem-left icon">{icon}</div>) : undefined}
|
|
21
|
+
{url ? (<a className="PuMenuItem-label" href={url} onClick={url && command
|
|
22
|
+
? (ev) => {
|
|
23
|
+
ev.preventDefault();
|
|
24
|
+
command?.();
|
|
25
|
+
}
|
|
26
|
+
: undefined}>
|
|
27
|
+
{label}
|
|
28
|
+
</a>) : command ? (<button className="PuMenuItem-label" onClick={command} type="button">
|
|
29
|
+
{label}
|
|
30
|
+
</button>) : (<span className="PuMenuItem-label">{label}</span>)}
|
|
31
|
+
{popupMenu && <PuPopupMenu className="PuMenuItem-popup" items={popupMenu}/>}
|
|
32
|
+
</div>);
|
|
33
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type MouseEvent } from "react";
|
|
2
|
+
import type { PuMenuNode } from "./paroi-ui-lib-types.js";
|
|
3
|
+
export interface PuPopupMenuProps {
|
|
4
|
+
items: PuMenuNode[];
|
|
5
|
+
className?: string;
|
|
6
|
+
id?: string;
|
|
7
|
+
autoPosition?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface PopupMenuRef {
|
|
10
|
+
toggle: (event: MouseEvent) => void;
|
|
11
|
+
show: (event: MouseEvent) => void;
|
|
12
|
+
hide: () => void;
|
|
13
|
+
}
|
|
14
|
+
export declare const PuPopupMenu: import("react").ForwardRefExoticComponent<PuPopupMenuProps & import("react").RefAttributes<PopupMenuRef>>;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { forwardRef, useEffect, useImperativeHandle, useRef } from "react";
|
|
2
|
+
import { PuButton } from "./PuButton.jsx";
|
|
3
|
+
import { PuMenuItem } from "./PuMenuItem.jsx";
|
|
4
|
+
import { PopupMenuBtnSvg, SubMenuIconLeft } from "./svg-icons.jsx";
|
|
5
|
+
const isPopoverSupported = typeof HTMLElement !== "undefined" && "showPopover" in HTMLElement.prototype;
|
|
6
|
+
if (!isPopoverSupported) {
|
|
7
|
+
console?.error("PuPopupMenu: Popover API is not supported in this browser.");
|
|
8
|
+
}
|
|
9
|
+
let seq = 0;
|
|
10
|
+
/**
|
|
11
|
+
* Utility function to configure popup positioning
|
|
12
|
+
*/
|
|
13
|
+
function setupPopoverPositioning(element, getPosition) {
|
|
14
|
+
const handleToggle = (e) => {
|
|
15
|
+
const newState = e.newState;
|
|
16
|
+
if (newState === "open") {
|
|
17
|
+
const position = getPosition();
|
|
18
|
+
element.style.position = "absolute";
|
|
19
|
+
element.style.top = `${position.top}px`;
|
|
20
|
+
element.style.left = `${position.left}px`;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
element.addEventListener("toggle", handleToggle);
|
|
24
|
+
return () => element.removeEventListener("toggle", handleToggle);
|
|
25
|
+
}
|
|
26
|
+
export const PuPopupMenu = forwardRef(function PuPopupMenu({ items, className = "", id, autoPosition = true }, ref) {
|
|
27
|
+
const menuRef = useRef(null);
|
|
28
|
+
const buttonRef = useRef(null);
|
|
29
|
+
const menuIdRef = useRef(undefined);
|
|
30
|
+
if (menuIdRef.current === undefined) {
|
|
31
|
+
menuIdRef.current = id ?? `pu-popup-menu-${++seq}`;
|
|
32
|
+
}
|
|
33
|
+
const toggle = (event) => {
|
|
34
|
+
if (menuRef.current) {
|
|
35
|
+
menuRef.current.togglePopover();
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const show = (event) => {
|
|
39
|
+
if (menuRef.current) {
|
|
40
|
+
menuRef.current.showPopover();
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const hide = () => {
|
|
44
|
+
if (menuRef.current) {
|
|
45
|
+
menuRef.current.hidePopover();
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
useImperativeHandle(ref, () => ({
|
|
49
|
+
toggle,
|
|
50
|
+
show,
|
|
51
|
+
hide,
|
|
52
|
+
}));
|
|
53
|
+
// Position popover when it opens
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!autoPosition || !menuRef.current || !buttonRef.current)
|
|
56
|
+
return;
|
|
57
|
+
const menuEl = menuRef.current;
|
|
58
|
+
return setupPopoverPositioning(menuEl, () => {
|
|
59
|
+
const rect = buttonRef.current?.getBoundingClientRect();
|
|
60
|
+
return {
|
|
61
|
+
top: rect ? rect.bottom + window.scrollY : 0,
|
|
62
|
+
left: rect ? rect.left + window.scrollX : 0,
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
}, [autoPosition]);
|
|
66
|
+
const buttonProps = {
|
|
67
|
+
popoverTarget: menuIdRef.current,
|
|
68
|
+
popoverTargetAction: "toggle",
|
|
69
|
+
};
|
|
70
|
+
return (<>
|
|
71
|
+
<PuButton ref={buttonRef} className={className} outlined severity="secondary" aria-label="unfold" // should be localized
|
|
72
|
+
aria-controls={menuIdRef.current} aria-haspopup {...buttonProps}>
|
|
73
|
+
<PopupMenuBtnSvg />
|
|
74
|
+
</PuButton>
|
|
75
|
+
|
|
76
|
+
<div className="PuPopupMenu" ref={menuRef} id={menuIdRef.current} popover="auto">
|
|
77
|
+
<div className="PuPopupMenu-content">
|
|
78
|
+
{items.map((item) => (<PopupMenuItemWrapper key={item.key} item={item} onHide={hide}/>))}
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</>);
|
|
82
|
+
});
|
|
83
|
+
function PopupMenuItemWrapper({ item, onHide }) {
|
|
84
|
+
const subMenuRef = useRef(null);
|
|
85
|
+
const subMenuId = useRef(`pu-submenu-${item.key}-${Math.random().toString(36).substring(2, 9)}`);
|
|
86
|
+
// For items with submenu, override the command to toggle the submenu
|
|
87
|
+
const itemWithSubmenuHandling = { ...item };
|
|
88
|
+
if (item.subMenu && item.subMenu.length > 0) {
|
|
89
|
+
// Create submenu trigger icon
|
|
90
|
+
const originalIcon = item.icon;
|
|
91
|
+
itemWithSubmenuHandling.icon = originalIcon || <SubMenuIconLeft />;
|
|
92
|
+
// Create a command that opens the submenu
|
|
93
|
+
const originalCommand = item.command;
|
|
94
|
+
itemWithSubmenuHandling.command = () => {
|
|
95
|
+
if (originalCommand) {
|
|
96
|
+
originalCommand();
|
|
97
|
+
}
|
|
98
|
+
// Will be handled by popovertarget attribute
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
// For regular items, add onHide to the command
|
|
103
|
+
const originalCommand = item.command;
|
|
104
|
+
itemWithSubmenuHandling.command = () => {
|
|
105
|
+
if (originalCommand) {
|
|
106
|
+
originalCommand();
|
|
107
|
+
}
|
|
108
|
+
onHide();
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
// Handle submenu positioning with Popover API
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
if (!subMenuRef.current || !item.subMenu)
|
|
114
|
+
return;
|
|
115
|
+
const subMenuEl = subMenuRef.current;
|
|
116
|
+
return setupPopoverPositioning(subMenuEl, () => {
|
|
117
|
+
const parentEl = subMenuEl.parentElement;
|
|
118
|
+
if (parentEl) {
|
|
119
|
+
const rect = parentEl.getBoundingClientRect();
|
|
120
|
+
return {
|
|
121
|
+
top: rect.top,
|
|
122
|
+
left: rect.right + 5, // 5px gap
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return { top: 0, left: 0 };
|
|
126
|
+
});
|
|
127
|
+
}, [item.subMenu]);
|
|
128
|
+
return (<div className="PuPopupMenu-item" popoverTargetAction="toggle" popoverTarget={subMenuId.current}>
|
|
129
|
+
<PuMenuItem item={itemWithSubmenuHandling}/>
|
|
130
|
+
|
|
131
|
+
{item.subMenu && (<div className="PuPopupMenu-submenu" popover="auto" id={subMenuId.current} ref={subMenuRef}>
|
|
132
|
+
{item.subMenu.map((subItem) => (<PopupMenuItemWrapper key={subItem.key} item={subItem} onHide={onHide}/>))}
|
|
133
|
+
</div>)}
|
|
134
|
+
</div>);
|
|
135
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { SelectHTMLAttributes } from "react";
|
|
2
|
+
export interface PuSelectOption {
|
|
3
|
+
value: string;
|
|
4
|
+
label: string;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface PuSelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, "onChange"> {
|
|
8
|
+
value: string;
|
|
9
|
+
onChange: (value: string) => void;
|
|
10
|
+
options: PuSelectOption[];
|
|
11
|
+
label?: string;
|
|
12
|
+
error?: string;
|
|
13
|
+
className?: string;
|
|
14
|
+
fullWidth?: boolean;
|
|
15
|
+
placeholder?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function PuSelect({ value, onChange, options, label, error, className, fullWidth, placeholder, ...rest }: PuSelectProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ChevronDownIcon } from "./svg-icons.jsx";
|
|
2
|
+
export function PuSelect({ value, onChange, options, label, error, className = "", fullWidth = false, placeholder, ...rest }) {
|
|
3
|
+
const handleChange = (e) => {
|
|
4
|
+
onChange(e.target.value);
|
|
5
|
+
};
|
|
6
|
+
const selectClasses = ["PuSelect", error ? "error" : "", fullWidth ? "fullWidth" : "", className]
|
|
7
|
+
.filter(Boolean)
|
|
8
|
+
.join(" ");
|
|
9
|
+
return (<div className={selectClasses}>
|
|
10
|
+
{label && <label className="PuSelect-label">{label}</label>}
|
|
11
|
+
<div className="PuSelect-wrapper">
|
|
12
|
+
<select className="PuSelect-field" value={value} onChange={handleChange} {...rest}>
|
|
13
|
+
{placeholder && (<option value="" disabled>
|
|
14
|
+
{placeholder}
|
|
15
|
+
</option>)}
|
|
16
|
+
{options.map((option) => (<option key={option.value} value={option.value} disabled={option.disabled}>
|
|
17
|
+
{option.label}
|
|
18
|
+
</option>))}
|
|
19
|
+
</select>
|
|
20
|
+
<ChevronDownIcon />
|
|
21
|
+
</div>
|
|
22
|
+
{error && <div className="PuSelect-error">{error}</div>}
|
|
23
|
+
</div>);
|
|
24
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { PuAccordion } from "./PuAccordion.jsx";
|
|
2
|
+
import { PuMenuItem } from "./PuMenuItem.jsx";
|
|
3
|
+
export function PuSideMenu({ menu }) {
|
|
4
|
+
return (<>
|
|
5
|
+
{menu.map((child) => (<PuAccordionOrMenuItem item={child} key={child.key}/>))}
|
|
6
|
+
</>);
|
|
7
|
+
}
|
|
8
|
+
function PuAccordionOrMenuItem({ item }) {
|
|
9
|
+
if (item.subMenu) {
|
|
10
|
+
return (<PuAccordion header={item} expanded={item.expanded} key={item.key}>
|
|
11
|
+
{item.subMenu.map((child) => (<PuAccordionOrMenuItem item={child} key={child.key}/>))}
|
|
12
|
+
</PuAccordion>);
|
|
13
|
+
}
|
|
14
|
+
return <PuMenuItem item={item}/>;
|
|
15
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import "../styles/index.scss";
|
|
2
|
+
export type * from "./paroi-ui-lib-types.js";
|
|
3
|
+
export * from "./PuAccordion.jsx";
|
|
4
|
+
export * from "./PuButton.jsx";
|
|
5
|
+
export * from "./PuCheckbox.jsx";
|
|
6
|
+
export * from "./PuInput.jsx";
|
|
7
|
+
export * from "./PuMenuItem.jsx";
|
|
8
|
+
export * from "./PuPopupMenu.jsx";
|
|
9
|
+
export * from "./PuSelect.jsx";
|
|
10
|
+
export * from "./PuSideMenu.jsx";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import "../styles/index.scss";
|
|
2
|
+
export * from "./PuAccordion.jsx";
|
|
3
|
+
export * from "./PuButton.jsx";
|
|
4
|
+
export * from "./PuCheckbox.jsx";
|
|
5
|
+
export * from "./PuInput.jsx";
|
|
6
|
+
export * from "./PuMenuItem.jsx";
|
|
7
|
+
export * from "./PuPopupMenu.jsx";
|
|
8
|
+
export * from "./PuSelect.jsx";
|
|
9
|
+
export * from "./PuSideMenu.jsx";
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
export interface PuMenuOption {
|
|
3
|
+
/**
|
|
4
|
+
* Unique identifier of the menuitem.
|
|
5
|
+
*/
|
|
6
|
+
key: string;
|
|
7
|
+
/**
|
|
8
|
+
* Text of the menuitem.
|
|
9
|
+
*/
|
|
10
|
+
label: string | ReactNode;
|
|
11
|
+
/**
|
|
12
|
+
* Icon of the item. It can be a string, ReactNode or method.
|
|
13
|
+
*/
|
|
14
|
+
icon?: ReactNode;
|
|
15
|
+
/**
|
|
16
|
+
* When set as true, disables the menuitem.
|
|
17
|
+
*/
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Inline style of the menuitem.
|
|
21
|
+
*/
|
|
22
|
+
style?: CSSProperties;
|
|
23
|
+
/**
|
|
24
|
+
* Style class of the menuitem.
|
|
25
|
+
*/
|
|
26
|
+
className?: string;
|
|
27
|
+
/**
|
|
28
|
+
* External link to navigate when item is clicked.
|
|
29
|
+
*/
|
|
30
|
+
url?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Callback to execute when item is clicked.
|
|
33
|
+
*/
|
|
34
|
+
command?(): void;
|
|
35
|
+
popupMenu?: PuMenuNode[];
|
|
36
|
+
active?: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface PuMenuNode extends PuMenuOption {
|
|
39
|
+
/**
|
|
40
|
+
* An array of child menu items.
|
|
41
|
+
*/
|
|
42
|
+
subMenu?: PuMenuNode[];
|
|
43
|
+
/**
|
|
44
|
+
* Initial visibility of submenu.
|
|
45
|
+
*/
|
|
46
|
+
expanded?: boolean;
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function PopupMenuBtnSvg(): import("react").JSX.Element;
|
|
2
|
+
export declare function SubMenuIconLeft(): import("react").JSX.Element;
|
|
3
|
+
export declare function SubMenuIconBottom(): import("react").JSX.Element;
|
|
4
|
+
export declare function ChevronDownIcon(): import("react").JSX.Element;
|
|
5
|
+
export declare function SubMenuIcon(): import("react").JSX.Element;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export function PopupMenuBtnSvg() {
|
|
2
|
+
return (<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
|
3
|
+
<title>⇓</title>
|
|
4
|
+
<path fill="currentColor" d="M12.146 7.146a.5.5 0 0 1 .708.708l-4.5 4.5a.5.5 0 0 1-.708 0l-4.5-4.5a.5.5 0 1 1 .708-.708L8 11.293zm0-4a.5.5 0 0 1 .708.708l-4.5 4.5a.5.5 0 0 1-.708 0l-4.5-4.5a.5.5 0 1 1 .708-.708L8 7.293z"/>
|
|
5
|
+
</svg>);
|
|
6
|
+
}
|
|
7
|
+
export function SubMenuIconLeft() {
|
|
8
|
+
return (<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg" className="submenu-icon">
|
|
9
|
+
<title>→</title>
|
|
10
|
+
<path d="M4.38708 13C4.28408 13.0005 4.18203 12.9804 4.08691 12.9409C3.99178 12.9014 3.9055 12.8433 3.83313 12.7701C3.68634 12.6231 3.60388 12.4238 3.60388 12.2161C3.60388 12.0084 3.68634 11.8091 3.83313 11.6622L8.50507 6.99022L3.83313 2.31827C3.69467 2.16968 3.61928 1.97313 3.62287 1.77005C3.62645 1.56698 3.70872 1.37322 3.85234 1.22959C3.99596 1.08597 4.18972 1.00371 4.3928 1.00012C4.59588 0.996539 4.79242 1.07192 4.94102 1.21039L10.1669 6.43628C10.3137 6.58325 10.3962 6.78249 10.3962 6.99022C10.3962 7.19795 10.3137 7.39718 10.1669 7.54416L4.94102 12.7701C4.86865 12.8433 4.78237 12.9014 4.68724 12.9409C4.59212 12.9804 4.49007 13.0005 4.38708 13Z" fill="currentColor"/>
|
|
11
|
+
</svg>);
|
|
12
|
+
}
|
|
13
|
+
export function SubMenuIconBottom() {
|
|
14
|
+
return (<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg" className="submenu-icon">
|
|
15
|
+
<title>↓</title>
|
|
16
|
+
<path d="M7.01744 10.398C6.91269 10.3985 6.8089 10.378 6.71215 10.3379C6.61541 10.2977 6.52766 10.2386 6.45405 10.1641L1.13907 4.84913C1.03306 4.69404 0.985221 4.5065 1.00399 4.31958C1.02276 4.13266 1.10693 3.95838 1.24166 3.82747C1.37639 3.69655 1.55301 3.61742 1.74039 3.60402C1.92777 3.59062 2.11386 3.64382 2.26584 3.75424L7.01744 8.47394L11.769 3.75424C11.9189 3.65709 12.097 3.61306 12.2748 3.62921C12.4527 3.64535 12.6199 3.72073 12.7498 3.84328C12.8797 3.96582 12.9647 4.12842 12.9912 4.30502C13.0177 4.48162 12.9841 4.662 12.8958 4.81724L7.58083 10.1322C7.50996 10.2125 7.42344 10.2775 7.32656 10.3232C7.22968 10.3689 7.12449 10.3944 7.01744 10.398Z" fill="currentColor"/>
|
|
17
|
+
</svg>);
|
|
18
|
+
}
|
|
19
|
+
export function ChevronDownIcon() {
|
|
20
|
+
return (<svg className="PuSelect-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
21
|
+
<title>DOWN</title>
|
|
22
|
+
<path d="M7.01744 10.398C6.91269 10.3985 6.8089 10.378 6.71215 10.3379C6.61541 10.2977 6.52766 10.2386 6.45405 10.1641L1.13907 4.84913C1.03306 4.69404 0.985221 4.5065 1.00399 4.31958C1.02276 4.13266 1.10693 3.95838 1.24166 3.82747C1.37639 3.69655 1.55301 3.61742 1.74039 3.60402C1.92777 3.59062 2.11386 3.64382 2.26584 3.75424L7.01744 8.47394L11.769 3.75424C11.9189 3.65709 12.097 3.61306 12.2748 3.62921C12.4527 3.64535 12.6199 3.72073 12.7498 3.84328C12.8797 3.96582 12.9647 4.12842 12.9912 4.30502C13.0177 4.48162 12.9841 4.662 12.8958 4.81724L7.58083 10.1322C7.50996 10.2125 7.42344 10.2775 7.32656 10.3232C7.22968 10.3689 7.12449 10.3944 7.01744 10.398Z" fill="currentColor"/>
|
|
23
|
+
</svg>);
|
|
24
|
+
}
|
|
25
|
+
export function SubMenuIcon() {
|
|
26
|
+
return (<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
27
|
+
<title>...</title>
|
|
28
|
+
<path d="M4.38708 13C4.28408 13.0005 4.18203 12.9804 4.08691 12.9409C3.99178 12.9014 3.9055 12.8433 3.83313 12.7701C3.68634 12.6231 3.60388 12.4238 3.60388 12.2161C3.60388 12.0084 3.68634 11.8091 3.83313 11.6622L8.50507 6.99022L3.83313 2.31827C3.69467 2.16968 3.61928 1.97313 3.62287 1.77005C3.62645 1.56698 3.70872 1.37322 3.85234 1.22959C3.99596 1.08597 4.18972 1.00371 4.3928 1.00012C4.59588 0.996539 4.79242 1.07192 4.94102 1.21039L10.1669 6.43628C10.3137 6.58325 10.3962 6.78249 10.3962 6.99022C10.3962 7.19795 10.3137 7.39718 10.1669 7.54416L4.94102 12.7701C4.86865 12.8433 4.78237 12.9014 4.68724 12.9409C4.59212 12.9804 4.49007 13.0005 4.38708 13Z" fill="currentColor"/>
|
|
29
|
+
</svg>);
|
|
30
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@paroicms/react-ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React UI toolkit for ParoiCMS.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"paroicms",
|
|
7
|
+
"react",
|
|
8
|
+
"ui",
|
|
9
|
+
"components"
|
|
10
|
+
],
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://gitlab.com/paroi/opensource/paroicms.git",
|
|
14
|
+
"directory": "packages/react-ui"
|
|
15
|
+
},
|
|
16
|
+
"author": "Paroi Team",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"type": "module",
|
|
19
|
+
"module": "dist/index.js",
|
|
20
|
+
"typings": "dist/index.d.ts",
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc",
|
|
23
|
+
"clear": "rimraf dist/*",
|
|
24
|
+
"dev": "tsc --watch --preserveWatchOutput"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/react": "~19.1.1",
|
|
28
|
+
"@types/react-dom": "~19.1.2",
|
|
29
|
+
"react": "~19.1.0",
|
|
30
|
+
"react-dom": "~19.1.0",
|
|
31
|
+
"rimraf": "~6.0.1",
|
|
32
|
+
"typescript": "~5.8.3"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist"
|
|
36
|
+
]
|
|
37
|
+
}
|