@paroicms/react-ui 0.5.0 → 0.5.2

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.
Files changed (50) hide show
  1. package/dist/Button.js +7 -1
  2. package/dist/Checkbox.d.ts +4 -3
  3. package/dist/Checkbox.js +5 -7
  4. package/dist/DateInput.d.ts +5 -3
  5. package/dist/DateInput.js +7 -4
  6. package/dist/Dialog.d.ts +4 -2
  7. package/dist/Dialog.js +30 -27
  8. package/dist/InputNumber.d.ts +2 -2
  9. package/dist/InputNumber.js +5 -5
  10. package/dist/InputText.d.ts +3 -4
  11. package/dist/InputText.js +7 -6
  12. package/dist/MultiSelect.d.ts +3 -2
  13. package/dist/MultiSelect.js +27 -24
  14. package/dist/PasswordInput.d.ts +5 -3
  15. package/dist/PasswordInput.js +7 -4
  16. package/dist/PopupMenu.d.ts +3 -4
  17. package/dist/PopupMenu.js +13 -51
  18. package/dist/RadioButton.d.ts +4 -4
  19. package/dist/RadioButton.js +5 -5
  20. package/dist/Select.d.ts +3 -4
  21. package/dist/Select.js +5 -5
  22. package/dist/SplitButton.d.ts +1 -0
  23. package/dist/SplitButton.js +12 -15
  24. package/dist/Switch.d.ts +2 -2
  25. package/dist/Switch.js +1 -1
  26. package/dist/Textarea.d.ts +5 -3
  27. package/dist/Textarea.js +7 -4
  28. package/dist/Tooltip.d.ts +1 -1
  29. package/dist/Tooltip.js +11 -32
  30. package/dist/alert-stack.d.ts +3 -1
  31. package/dist/alert-stack.js +2 -2
  32. package/dist/popup-positioning.d.ts +10 -0
  33. package/dist/popup-positioning.js +160 -0
  34. package/package.json +6 -6
  35. package/styles/Alert.css +1 -0
  36. package/styles/Checkbox.css +7 -6
  37. package/styles/DateInput.css +2 -25
  38. package/styles/Dialog.css +35 -22
  39. package/styles/InputNumber.css +2 -25
  40. package/styles/InputText.css +16 -31
  41. package/styles/MultiSelect.css +8 -23
  42. package/styles/PasswordInput.css +7 -18
  43. package/styles/RadioButton.css +6 -5
  44. package/styles/Select.css +12 -27
  45. package/styles/SplitButton.css +2 -8
  46. package/styles/Textarea.css +1 -24
  47. package/styles/Tooltip.css +4 -18
  48. package/styles/Tree.css +4 -3
  49. package/styles/theme/field.css +29 -0
  50. package/styles/theme/index.css +1 -0
package/dist/Button.js CHANGED
@@ -2,13 +2,19 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import "../styles/Button.css";
3
3
  import { clsx } from "clsx";
4
4
  import { Check, Loader2 } from "lucide-react";
5
- import { useCallback, useState, } from "react";
5
+ import { useCallback, useEffect, useState, } from "react";
6
6
  import { useReactUIConfig } from "./react-ui-provider.js";
7
7
  export function Button(props) {
8
8
  const { children, className, disabled, outlined, icon, severity = "primary", ref, ...rest } = props;
9
9
  const { autoDismissInsideFeedbackDurationMs } = useReactUIConfig();
10
10
  const [loading, setLoading] = useState(false);
11
11
  const [showFeedback, setShowFeedback] = useState(false);
12
+ // Cancel feedback when button becomes enabled
13
+ useEffect(() => {
14
+ if (!disabled && showFeedback) {
15
+ setShowFeedback(false);
16
+ }
17
+ }, [disabled, showFeedback]);
12
18
  const classNames = clsx("PaBtn", severity, outlined && "outlined", disabled && "disabled", showFeedback && "feedback", className);
13
19
  const handleAsyncClick = useCallback(async (e) => {
14
20
  if (props.as !== "async")
@@ -1,9 +1,10 @@
1
1
  import "../styles/Checkbox.css";
2
- import type { InputHTMLAttributes } from "react";
2
+ import type { ChangeEvent, InputHTMLAttributes } from "react";
3
3
  export interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "onChange" | "type"> {
4
4
  checked: boolean;
5
- onChange: (checked: boolean) => void;
5
+ onChange?: (checked: boolean, ev: ChangeEvent<HTMLInputElement>) => void;
6
6
  label?: string;
7
+ inputLabel?: string;
7
8
  className?: string;
8
9
  }
9
- export declare function Checkbox({ checked, onChange, label, className, ...rest }: CheckboxProps): import("react/jsx-runtime").JSX.Element;
10
+ export declare function Checkbox({ checked, onChange, label, inputLabel, className, ...rest }: CheckboxProps): import("react/jsx-runtime").JSX.Element;
package/dist/Checkbox.js CHANGED
@@ -1,12 +1,10 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import "../styles/Checkbox.css";
3
3
  import { clsx } from "clsx";
4
- export function Checkbox({ checked, onChange, label, className, ...rest }) {
4
+ export function Checkbox({ checked, onChange, label, inputLabel, className, ...rest }) {
5
5
  const handleChange = (e) => {
6
- onChange(e.target.checked);
6
+ onChange?.(e.target.checked, e);
7
7
  };
8
- if (label) {
9
- return (_jsxs("label", { className: clsx("PaCheckbox", className), children: [_jsx("input", { className: "PaCheckbox-input", type: "checkbox", checked: checked, onChange: handleChange, ...rest }), _jsx("span", { className: "PaCheckbox-label", children: label })] }));
10
- }
11
- return (_jsx("input", { className: clsx("PaCheckbox-input", className), type: "checkbox", checked: checked, onChange: handleChange, ...rest }));
8
+ const fieldContent = inputLabel ? (_jsxs("label", { className: clsx("PaCheckboxField", className), children: [_jsx("input", { className: "PaCheckboxInput", type: "checkbox", checked: checked, onChange: handleChange, ...rest }), _jsx("span", { className: "PaCheckboxField-label", children: inputLabel })] })) : (_jsx("input", { className: clsx("PaCheckboxInput", className), type: "checkbox", checked: checked, onChange: handleChange, ...rest }));
9
+ return (_jsx(_Fragment, { children: label ? (_jsxs("label", { className: clsx("PaField", className), children: [_jsx("span", { className: "PaField-label", children: label }), fieldContent] })) : (fieldContent) }));
12
10
  }
@@ -1,9 +1,11 @@
1
1
  import "../styles/DateInput.css";
2
- import type { InputHTMLAttributes, Ref } from "react";
3
- export interface DateInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type"> {
2
+ import type { ChangeEvent, InputHTMLAttributes, Ref } from "react";
3
+ export interface DateInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "onChange"> {
4
4
  type: "date" | "datetime-local" | "time";
5
+ value?: string;
6
+ onChange?: (value: string, ev: ChangeEvent<HTMLInputElement>) => void;
5
7
  label?: string;
6
8
  error?: string;
7
9
  ref?: Ref<HTMLInputElement>;
8
10
  }
9
- export declare function DateInput({ className, label, error, type, ref, ...props }: DateInputProps): import("react/jsx-runtime").JSX.Element;
11
+ export declare function DateInput({ className, label, error, type, value, onChange, ref, ...props }: DateInputProps): import("react/jsx-runtime").JSX.Element;
package/dist/DateInput.js CHANGED
@@ -1,7 +1,10 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import "../styles/DateInput.css";
3
3
  import { clsx } from "clsx";
4
- export function DateInput({ className, label, error, type, ref, ...props }) {
5
- const inputElement = (_jsx("input", { ref: ref, type: type, className: clsx("PaDateInput-field", error && "error"), ...props }));
6
- return (_jsxs("span", { className: clsx("PaDateInput", className), children: [label ? (_jsxs("label", { className: "PaDateInput-wrapper", children: [_jsx("span", { className: "PaDateInput-label", children: label }), inputElement] })) : (inputElement), error && _jsx("span", { className: "PaDateInput-error", children: error })] }));
4
+ export function DateInput({ className, label, error, type, value, onChange, ref, ...props }) {
5
+ const handleChange = (e) => {
6
+ onChange?.(e.target.value, e);
7
+ };
8
+ const inputElement = (_jsx("input", { ref: ref, type: type, className: clsx("PaDateTimeInput", error && "error", !label && className), value: value, onChange: handleChange, ...props }));
9
+ return (_jsxs(_Fragment, { children: [label ? (_jsxs("label", { className: clsx("PaField", error && "error", className), children: [_jsx("span", { className: "PaField-label", children: label }), inputElement] })) : (inputElement), error && _jsx("span", { className: "PaFieldError", children: error })] }));
7
10
  }
package/dist/Dialog.d.ts CHANGED
@@ -8,6 +8,8 @@ export interface DialogProps {
8
8
  children: ReactNode;
9
9
  className?: string;
10
10
  closable?: boolean;
11
- modal?: boolean;
11
+ /** Default size is "md" */
12
+ size?: DialogSize;
12
13
  }
13
- export declare function Dialog({ visible, onHide, header, footer, children, className, closable, modal, }: DialogProps): import("react").ReactPortal | null;
14
+ export type DialogSize = "sm" | "md" | "lg" | "innerContent";
15
+ export declare function Dialog({ visible, onHide, header, footer, children, className, closable, size, }: DialogProps): import("react/jsx-runtime").JSX.Element;
package/dist/Dialog.js CHANGED
@@ -3,39 +3,38 @@ import "../styles/Dialog.css";
3
3
  import { clsx } from "clsx";
4
4
  import { X } from "lucide-react";
5
5
  import { useEffect, useRef } from "react";
6
- import { createPortal } from "react-dom";
7
- /** Track dialog stack for ESC key handling - only topmost dialog should respond */
8
- let dialogStackCounter = 0;
9
- export function Dialog({ visible, onHide, header, footer, children, className, closable = true, modal = true, }) {
10
- // Track this dialog's position in the stack for ESC key handling
11
- const dialogIdRef = useRef(undefined);
6
+ export function Dialog({ visible, onHide, header, footer, children, className, closable = true, size = "md", }) {
7
+ const dialogRef = useRef(null);
8
+ // Control open/close with native methods
12
9
  useEffect(() => {
10
+ const dialog = dialogRef.current;
11
+ if (!dialog)
12
+ return;
13
13
  if (visible) {
14
- dialogIdRef.current = ++dialogStackCounter;
14
+ if (!dialog.open) {
15
+ dialog.showModal();
16
+ }
15
17
  }
16
- return () => {
17
- if (dialogIdRef.current !== undefined) {
18
- // Decrement only if this was the topmost dialog
19
- if (dialogIdRef.current === dialogStackCounter) {
20
- dialogStackCounter--;
21
- }
22
- dialogIdRef.current = undefined;
18
+ else {
19
+ if (dialog.open) {
20
+ dialog.close();
23
21
  }
24
- };
22
+ }
25
23
  }, [visible]);
26
- // Handle escape key - only the topmost dialog should respond
24
+ // Handle ESC key via native cancel event
27
25
  useEffect(() => {
28
- if (!visible || !closable)
26
+ const dialog = dialogRef.current;
27
+ if (!dialog)
29
28
  return;
30
- const handleKeyDown = (e) => {
31
- // Only handle ESC if this is the topmost dialog
32
- if (e.key === "Escape" && dialogIdRef.current === dialogStackCounter) {
29
+ const handleCancel = (e) => {
30
+ e.preventDefault(); // Prevent default close
31
+ if (closable) {
33
32
  onHide();
34
33
  }
35
34
  };
36
- document.addEventListener("keydown", handleKeyDown);
37
- return () => document.removeEventListener("keydown", handleKeyDown);
38
- }, [visible, closable, onHide]);
35
+ dialog.addEventListener("cancel", handleCancel);
36
+ return () => dialog.removeEventListener("cancel", handleCancel);
37
+ }, [closable, onHide]);
39
38
  // Prevent body scroll when open
40
39
  useEffect(() => {
41
40
  if (visible) {
@@ -48,8 +47,12 @@ export function Dialog({ visible, onHide, header, footer, children, className, c
48
47
  document.body.style.overflow = "";
49
48
  };
50
49
  }, [visible]);
51
- if (!visible)
52
- return null;
53
- const dialog = (_jsxs("div", { className: "PaDialog-overlay", children: [modal && _jsx("div", { className: "PaDialog-backdrop", onClick: closable ? onHide : undefined }), _jsxs("div", { className: clsx("PaDialog", className), role: "dialog", "aria-modal": modal, children: [(header || closable) && (_jsxs("div", { className: "PaDialog-header", children: [header && _jsx("div", { className: "PaDialog-title", children: header }), closable && (_jsx("button", { type: "button", className: "PaDialog-close", onClick: onHide, "aria-label": "Close", children: _jsx(X, { size: 18 }) }))] })), _jsx("div", { className: "PaDialog-content", children: children }), footer && _jsx("div", { className: "PaDialog-footer", children: footer })] })] }));
54
- return createPortal(dialog, document.body);
50
+ const sizeClass = size === "innerContent"
51
+ ? undefined
52
+ : size === "sm"
53
+ ? "sizeSm"
54
+ : size === "lg"
55
+ ? "sizeLg"
56
+ : "sizeMd";
57
+ return (_jsxs("dialog", { ref: dialogRef, className: clsx("PaDialog", sizeClass, className), children: [(header || closable) && (_jsxs("div", { className: "PaDialog-header", children: [header && _jsx("div", { className: "PaDialog-title", children: header }), closable && (_jsx("button", { type: "button", className: "PaDialog-close", onClick: onHide, "aria-label": "Close", children: _jsx(X, { size: 18 }) }))] })), _jsx("div", { className: "PaDialog-content", children: children }), footer && _jsx("div", { className: "PaDialog-footer", children: footer })] }));
55
58
  }
@@ -1,8 +1,8 @@
1
- import type { InputHTMLAttributes, Ref } from "react";
1
+ import type { ChangeEvent, InputHTMLAttributes, Ref } from "react";
2
2
  import "../styles/InputNumber.css";
3
3
  export interface InputNumberProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "onChange"> {
4
4
  value: number | undefined;
5
- onChange: (value: number | undefined) => void;
5
+ onChange?: (value: number | undefined, ev: ChangeEvent<HTMLInputElement>) => void;
6
6
  label?: string;
7
7
  error?: string;
8
8
  ref?: Ref<HTMLInputElement>;
@@ -1,19 +1,19 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { clsx } from "clsx";
3
3
  import "../styles/InputNumber.css";
4
4
  export function InputNumber({ className, label, error, value, onChange, min, max, step, ref, ...props }) {
5
5
  const handleChange = (e) => {
6
6
  const val = e.target.value;
7
7
  if (val === "") {
8
- onChange(undefined);
8
+ onChange?.(undefined, e);
9
9
  }
10
10
  else {
11
11
  const num = Number(val);
12
12
  if (!Number.isNaN(num)) {
13
- onChange(num);
13
+ onChange?.(num, e);
14
14
  }
15
15
  }
16
16
  };
17
- const inputElement = (_jsx("input", { ref: ref, type: "number", className: clsx("PaInputNumber-field", error && "error"), value: value ?? "", onChange: handleChange, min: min, max: max, step: step, ...props }));
18
- return (_jsxs("span", { className: clsx("PaInputNumber", className), children: [label ? (_jsxs("label", { className: "PaInputNumber-wrapper", children: [_jsx("span", { className: "PaInputNumber-label", children: label }), inputElement] })) : (inputElement), error && _jsx("span", { className: "PaInputNumber-error", children: error })] }));
17
+ const inputElement = (_jsx("input", { ref: ref, type: "number", className: clsx("PaNumberInput", error && "error", !label && className), value: value ?? "", onChange: handleChange, min: min, max: max, step: step, ...props }));
18
+ return (_jsxs(_Fragment, { children: [label ? (_jsxs("label", { className: clsx("PaField", error && "error", className), children: [_jsx("span", { className: "PaField-label", children: label }), inputElement] })) : (inputElement), error && _jsx("span", { className: "PaFieldError", children: error })] }));
19
19
  }
@@ -1,14 +1,13 @@
1
- import type { InputHTMLAttributes, ReactNode } from "react";
1
+ import type { ChangeEvent, InputHTMLAttributes, ReactNode } from "react";
2
2
  import "../styles/InputText.css";
3
3
  export interface InputTextProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "onChange"> {
4
4
  value: string;
5
- onChange: (value: string) => void;
5
+ onChange?: (value: string, ev: ChangeEvent<HTMLInputElement>) => void;
6
6
  label?: string;
7
7
  error?: string;
8
8
  className?: string;
9
- fullWidth?: boolean;
10
9
  endElement?: ReactNode;
11
10
  icon?: ReactNode;
12
11
  iconPosition?: "left" | "right";
13
12
  }
14
- export declare function InputText({ value, onChange, label, error, className, fullWidth, endElement, icon, iconPosition, ...rest }: InputTextProps): import("react/jsx-runtime").JSX.Element;
13
+ export declare function InputText({ value, onChange, label, error, className, endElement, icon, iconPosition, ...rest }: InputTextProps): import("react/jsx-runtime").JSX.Element;
package/dist/InputText.js CHANGED
@@ -1,11 +1,12 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { clsx } from "clsx";
3
3
  import "../styles/InputText.css";
4
- export function InputText({ value, onChange, label, error, className, fullWidth = false, endElement, icon, iconPosition = "left", ...rest }) {
4
+ export function InputText({ value, onChange, label, error, className, endElement, icon, iconPosition = "left", ...rest }) {
5
5
  const handleChange = (e) => {
6
- onChange(e.target.value);
6
+ onChange?.(e.target.value, e);
7
7
  };
8
- const inputElement = (_jsx("input", { className: "PaInputText-field", value: value, onChange: handleChange, ...rest }));
9
- const fieldContent = endElement ? (_jsxs("span", { className: "PaInputText-row", children: [inputElement, endElement] })) : icon ? (_jsxs("span", { className: clsx("PaInputText-iconWrapper", iconPosition === "right" && "iconRight"), children: [_jsx("span", { className: "PaInputText-icon", children: icon }), inputElement] })) : (inputElement);
10
- return (_jsxs("span", { className: clsx("PaInputText", error && "error", fullWidth && "fullWidth", className), children: [label ? (_jsxs("label", { className: "PaInputText-wrapper", children: [_jsx("span", { className: "PaInputText-label", children: label }), fieldContent] })) : (fieldContent), error && _jsx("span", { className: "PaInputText-error", children: error })] }));
8
+ const inputElement = (_jsx("input", { className: "PaTextInput", value: value, onChange: handleChange, ...rest }));
9
+ const hasWrapper = Boolean(endElement || icon);
10
+ const fieldContent = endElement ? (_jsxs("span", { className: "PaTextField-row", children: [inputElement, endElement] })) : icon ? (_jsxs("span", { className: clsx("PaTextField-iconWrapper", iconPosition === "right" && "iconRight"), children: [_jsx("span", { className: "PaTextField-icon", children: icon }), inputElement] })) : (inputElement);
11
+ return (_jsxs(_Fragment, { children: [label ? (_jsxs("label", { className: clsx("PaTextField PaField", error && "error", className), children: [_jsx("span", { className: "PaField-label", children: label }), fieldContent] })) : hasWrapper ? (_jsx("span", { className: clsx("PaTextField", error && "error", className), children: fieldContent })) : (_jsx("input", { className: clsx("PaTextInput", className), value: value, onChange: handleChange, ...rest })), error && _jsx("span", { className: "PaFieldError", children: error })] }));
11
12
  }
@@ -6,12 +6,13 @@ export interface MultiSelectOption {
6
6
  }
7
7
  export interface MultiSelectProps {
8
8
  value: string[];
9
- onChange: (value: string[]) => void;
9
+ onChange?: (value: string[]) => void;
10
10
  options: MultiSelectOption[];
11
11
  label?: string;
12
12
  error?: string;
13
13
  className?: string;
14
14
  placeholder?: string;
15
15
  disabled?: boolean;
16
+ position?: "auto" | "top" | "bottom";
16
17
  }
17
- export declare function MultiSelect({ value, onChange, options, label, error, className, placeholder, disabled, }: MultiSelectProps): import("react/jsx-runtime").JSX.Element;
18
+ export declare function MultiSelect({ value, onChange, options, label, error, className, placeholder, disabled, position, }: MultiSelectProps): import("react/jsx-runtime").JSX.Element;
@@ -3,48 +3,51 @@ import "../styles/MultiSelect.css";
3
3
  import { clsx } from "clsx";
4
4
  import { ChevronDown, X } from "lucide-react";
5
5
  import { useEffect, useRef, useState } from "react";
6
- export function MultiSelect({ value, onChange, options, label, error, className, placeholder, disabled, }) {
6
+ import { Checkbox } from "./Checkbox.js";
7
+ import { computePopupPosition, setupPopoverPositioning } from "./popup-positioning.js";
8
+ export function MultiSelect({ value, onChange, options, label, error, className, placeholder, disabled, position, }) {
7
9
  const [open, setOpen] = useState(false);
8
10
  const containerRef = useRef(null);
9
- // Close dropdown when clicking outside
11
+ const dropdownRef = useRef(null);
12
+ const dropdownId = useRef(`pa-multiselect-${Math.random().toString(36).substring(2, 9)}`);
10
13
  useEffect(() => {
11
- if (!open)
14
+ const dropdown = dropdownRef.current;
15
+ const container = containerRef.current;
16
+ if (!dropdown || !container)
12
17
  return;
13
- const handleClickOutside = (e) => {
14
- if (containerRef.current && !containerRef.current.contains(e.target)) {
15
- setOpen(false);
16
- }
18
+ const handleToggle = (e) => {
19
+ setOpen(e.newState === "open");
17
20
  };
18
- document.addEventListener("mousedown", handleClickOutside);
19
- return () => document.removeEventListener("mousedown", handleClickOutside);
20
- }, [open]);
21
+ dropdown.addEventListener("toggle", handleToggle);
22
+ const cleanupPositioning = setupPopoverPositioning(dropdown, () => {
23
+ dropdown.style.width = `${container.offsetWidth}px`;
24
+ return computePopupPosition(container, dropdown, position ?? "auto", "vertical");
25
+ });
26
+ return () => {
27
+ dropdown.removeEventListener("toggle", handleToggle);
28
+ cleanupPositioning();
29
+ };
30
+ }, [position]);
21
31
  const toggleOption = (optionValue) => {
22
32
  if (value.includes(optionValue)) {
23
- onChange(value.filter((v) => v !== optionValue));
33
+ onChange?.(value.filter((v) => v !== optionValue));
24
34
  }
25
35
  else {
26
- onChange([...value, optionValue]);
36
+ onChange?.([...value, optionValue]);
27
37
  }
28
38
  };
29
39
  const removeValue = (optionValue) => {
30
- onChange(value.filter((v) => v !== optionValue));
40
+ onChange?.(value.filter((v) => v !== optionValue));
31
41
  };
32
42
  const selectedOptions = options.filter((opt) => value.includes(opt.value));
33
- const controlElement = (_jsxs(_Fragment, { children: [_jsxs("span", { className: clsx("PaMultiSelect-control", open && "open"), onClick: () => !disabled && setOpen(!open), onKeyDown: (e) => {
34
- if (e.key === "Enter" || e.key === " ") {
43
+ const controlElement = (_jsxs(_Fragment, { children: [_jsxs("span", { className: "PaMultiSelect-control", onClick: () => !disabled && dropdownRef.current?.togglePopover(), onKeyDown: (e) => {
44
+ if ((e.key === "Enter" || e.key === " ") && !disabled) {
35
45
  e.preventDefault();
36
- if (!disabled)
37
- setOpen(!open);
46
+ dropdownRef.current?.togglePopover();
38
47
  }
39
48
  }, tabIndex: disabled ? -1 : 0, role: "combobox", "aria-expanded": open, "aria-haspopup": "listbox", children: [_jsx("span", { className: "PaMultiSelect-values", children: selectedOptions.length > 0 ? (selectedOptions.map((opt) => (_jsxs("span", { className: "PaMultiSelect-chip", children: [opt.label, _jsx("button", { type: "button", className: "PaMultiSelect-chipRemove", onClick: (e) => {
40
49
  e.stopPropagation();
41
50
  removeValue(opt.value);
42
- }, "aria-label": `Remove ${opt.label}`, children: _jsx(X, { size: 12 }) })] }, opt.value)))) : (_jsx("span", { className: "PaMultiSelect-placeholder", children: placeholder })) }), _jsx(ChevronDown, { className: "PaMultiSelect-icon", size: 16 })] }), open && (_jsx("span", { className: "PaMultiSelect-dropdown", role: "listbox", children: options.map((option) => (_jsxs("span", { className: clsx("PaMultiSelect-option", value.includes(option.value) && "selected", option.disabled && "disabled"), onClick: () => !option.disabled && toggleOption(option.value), onKeyDown: (e) => {
43
- if (e.key === "Enter" || e.key === " ") {
44
- e.preventDefault();
45
- if (!option.disabled)
46
- toggleOption(option.value);
47
- }
48
- }, tabIndex: option.disabled ? -1 : 0, role: "option", "aria-selected": value.includes(option.value), children: [_jsx("span", { className: "PaMultiSelect-checkbox", children: value.includes(option.value) && "✓" }), option.label] }, option.value))) }))] }));
51
+ }, "aria-label": `Remove ${opt.label}`, children: _jsx(X, { size: 12 }) })] }, opt.value)))) : (_jsx("span", { className: "PaMultiSelect-placeholder", children: placeholder })) }), _jsx(ChevronDown, { className: "PaMultiSelect-icon", size: 16 })] }), _jsx("span", { ref: dropdownRef, id: dropdownId.current, className: "PaMultiSelect-dropdown", role: "listbox", popover: "auto", children: options.map((option) => (_jsxs("label", { className: clsx("PaMultiSelect-option", value.includes(option.value) && "selected", option.disabled && "disabled"), children: [_jsx(Checkbox, { checked: value.includes(option.value), onChange: () => toggleOption(option.value), disabled: option.disabled }), option.label] }, option.value))) })] }));
49
52
  return (_jsxs("span", { ref: containerRef, className: clsx("PaMultiSelect", error && "error", disabled && "disabled", className), children: [label ? (_jsxs("label", { className: "PaMultiSelect-wrapper", children: [_jsx("span", { className: "PaMultiSelect-label", children: label }), controlElement] })) : (controlElement), error && _jsx("span", { className: "PaMultiSelect-error", children: error })] }));
50
53
  }
@@ -1,8 +1,10 @@
1
1
  import "../styles/PasswordInput.css";
2
- import { type InputHTMLAttributes, type Ref } from "react";
3
- export interface PasswordInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type"> {
2
+ import { type ChangeEvent, type InputHTMLAttributes, type Ref } from "react";
3
+ export interface PasswordInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "onChange"> {
4
+ value?: string;
5
+ onChange?: (value: string, ev: ChangeEvent<HTMLInputElement>) => void;
4
6
  label?: string;
5
7
  error?: string;
6
8
  ref?: Ref<HTMLInputElement>;
7
9
  }
8
- export declare function PasswordInput({ className, label, error, ref, ...props }: PasswordInputProps): import("react/jsx-runtime").JSX.Element;
10
+ export declare function PasswordInput({ className, label, error, value, onChange, ref, ...props }: PasswordInputProps): import("react/jsx-runtime").JSX.Element;
@@ -1,10 +1,13 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import "../styles/PasswordInput.css";
3
3
  import { clsx } from "clsx";
4
4
  import { Eye, EyeOff } from "lucide-react";
5
5
  import { useState } from "react";
6
- export function PasswordInput({ className, label, error, ref, ...props }) {
6
+ export function PasswordInput({ className, label, error, value, onChange, ref, ...props }) {
7
7
  const [visible, setVisible] = useState(false);
8
- const inputWithToggle = (_jsxs("span", { className: clsx("PaPasswordInput-inputWrapper", error && "error"), children: [_jsx("input", { ref: ref, type: visible ? "text" : "password", className: "PaPasswordInput-input", ...props }), _jsx("button", { type: "button", className: "PaPasswordInput-toggle", onClick: () => setVisible(!visible), tabIndex: -1, "aria-label": visible ? "Hide password" : "Show password", children: visible ? _jsx(EyeOff, { size: 16 }) : _jsx(Eye, { size: 16 }) })] }));
9
- return (_jsxs("span", { className: clsx("PaPasswordInput", className), children: [label ? (_jsxs("label", { className: "PaPasswordInput-wrapper", children: [_jsx("span", { className: "PaPasswordInput-label", children: label }), inputWithToggle] })) : (inputWithToggle), error && _jsx("span", { className: "PaPasswordInput-error", children: error })] }));
8
+ const handleChange = (e) => {
9
+ onChange?.(e.target.value, e);
10
+ };
11
+ const inputWithToggle = (_jsxs("span", { className: clsx("PaPasswordInput-inputWrapper", error && "error"), children: [_jsx("input", { ref: ref, type: visible ? "text" : "password", className: "PaPasswordInput-input", value: value, onChange: handleChange, ...props }), _jsx("button", { type: "button", className: "PaPasswordInput-toggle", onClick: () => setVisible(!visible), tabIndex: -1, "aria-label": visible ? "Hide password" : "Show password", children: visible ? _jsx(EyeOff, { size: 16 }) : _jsx(Eye, { size: 16 }) })] }));
12
+ return (_jsxs(_Fragment, { children: [label ? (_jsxs("label", { className: clsx("PaPasswordInput PaField", error && "error", className), children: [_jsx("span", { className: "PaField-label", children: label }), inputWithToggle] })) : (inputWithToggle), error && _jsx("span", { className: "PaFieldError", children: error })] }));
10
13
  }
@@ -5,9 +5,8 @@ export interface PopupMenuProps {
5
5
  items: MenuNode[];
6
6
  className?: string;
7
7
  id?: string;
8
- autoPosition?: boolean;
9
- /** Position of the popup relative to the trigger: 'bottom' (default) or 'top' */
10
- position?: "bottom" | "top";
8
+ /** Position of the popup relative to the trigger. Default is 'auto'. */
9
+ position?: "auto" | "topLeft" | "topRight" | "bottomLeft" | "bottomRight";
11
10
  /** Optional custom trigger element. If provided, it will be used instead of the default button. */
12
11
  children?: ReactNode;
13
12
  ref?: Ref<PopupMenuRef>;
@@ -17,4 +16,4 @@ export interface PopupMenuRef {
17
16
  show: (event: MouseEvent) => void;
18
17
  hide: () => void;
19
18
  }
20
- export declare function PopupMenu({ items, className, id, autoPosition, position, children, ref, }: PopupMenuProps): import("react/jsx-runtime").JSX.Element;
19
+ export declare function PopupMenu({ items, className, id, position, children, ref, }: PopupMenuProps): import("react/jsx-runtime").JSX.Element;
package/dist/PopupMenu.js CHANGED
@@ -5,28 +5,13 @@ import { ChevronRight, ChevronsDown } from "lucide-react";
5
5
  import { cloneElement, isValidElement, useEffect, useImperativeHandle, useRef, } from "react";
6
6
  import { Button } from "./Button.js";
7
7
  import { MenuItem } from "./MenuItem.js";
8
+ import { computePopupPosition, setupPopoverPositioning } from "./popup-positioning.js";
8
9
  const isPopoverSupported = typeof HTMLElement !== "undefined" && "showPopover" in HTMLElement.prototype;
9
10
  if (!isPopoverSupported) {
10
11
  console?.error("PopupMenu: Popover API is not supported in this browser.");
11
12
  }
12
13
  let seq = 0;
13
- /**
14
- * Utility function to configure popup positioning
15
- */
16
- function setupPopoverPositioning(element, getPosition) {
17
- const handleToggle = (e) => {
18
- const newState = e.newState;
19
- if (newState === "open") {
20
- const position = getPosition();
21
- element.style.position = "absolute";
22
- element.style.top = `${position.top}px`;
23
- element.style.left = `${position.left}px`;
24
- }
25
- };
26
- element.addEventListener("toggle", handleToggle);
27
- return () => element.removeEventListener("toggle", handleToggle);
28
- }
29
- export function PopupMenu({ items, className, id, autoPosition = true, position = "bottom", children, ref, }) {
14
+ export function PopupMenu({ items, className, id, position = "auto", children, ref, }) {
30
15
  const menuRef = useRef(null);
31
16
  const triggerRef = useRef(null);
32
17
  const menuIdRef = useRef(undefined);
@@ -55,28 +40,12 @@ export function PopupMenu({ items, className, id, autoPosition = true, position
55
40
  }));
56
41
  // Position popover when it opens
57
42
  useEffect(() => {
58
- if (!autoPosition || !menuRef.current || !triggerRef.current)
59
- return;
60
43
  const menuEl = menuRef.current;
61
- return setupPopoverPositioning(menuEl, () => {
62
- const rect = triggerRef.current?.getBoundingClientRect();
63
- if (!rect)
64
- return { top: 0, left: 0 };
65
- if (position === "top") {
66
- // Position above the trigger
67
- const menuHeight = menuEl.offsetHeight || 150; // Fallback height
68
- return {
69
- top: rect.top + window.scrollY - menuHeight - 8,
70
- left: rect.left + window.scrollX,
71
- };
72
- }
73
- // Default: position below
74
- return {
75
- top: rect.bottom + window.scrollY,
76
- left: rect.left + window.scrollX,
77
- };
78
- });
79
- }, [autoPosition, position]);
44
+ const triggerEl = triggerRef.current;
45
+ if (!menuEl || !triggerEl)
46
+ return;
47
+ return setupPopoverPositioning(menuEl, () => computePopupPosition(triggerEl, menuEl, position, "corner"));
48
+ }, [position]);
80
49
  const triggerProps = {
81
50
  popoverTarget: menuIdRef.current,
82
51
  popoverTargetAction: "toggle",
@@ -125,20 +94,13 @@ function PopupMenuItemWrapper({ item, onHide }) {
125
94
  }
126
95
  // Handle submenu positioning with Popover API
127
96
  useEffect(() => {
128
- if (!subMenuRef.current || !item.subMenu)
129
- return;
130
97
  const subMenuEl = subMenuRef.current;
131
- return setupPopoverPositioning(subMenuEl, () => {
132
- const parentEl = subMenuEl.parentElement;
133
- if (parentEl) {
134
- const rect = parentEl.getBoundingClientRect();
135
- return {
136
- top: rect.top,
137
- left: rect.right + 5, // 5px gap
138
- };
139
- }
140
- return { top: 0, left: 0 };
141
- });
98
+ if (!subMenuEl || !item.subMenu)
99
+ return;
100
+ const parentEl = subMenuEl.parentElement;
101
+ if (!parentEl)
102
+ return;
103
+ return setupPopoverPositioning(subMenuEl, () => computePopupPosition(parentEl, subMenuEl, "right", "cardinal"));
142
104
  }, [item.subMenu]);
143
105
  return (_jsxs("div", { className: "PaPopupMenu-item", popoverTargetAction: "toggle", popoverTarget: subMenuId.current, children: [_jsx(MenuItem, { item: itemWithSubmenuHandling }), item.subMenu && (_jsx("div", { className: "PaPopupMenu-submenu", popover: "auto", id: subMenuId.current, ref: subMenuRef, children: item.subMenu.map((subItem) => (_jsx(PopupMenuItemWrapper, { item: subItem, onHide: onHide }, subItem.key))) }))] }));
144
106
  }
@@ -1,9 +1,9 @@
1
- import type { InputHTMLAttributes } from "react";
1
+ import type { ChangeEvent, InputHTMLAttributes } from "react";
2
2
  import "../styles/RadioButton.css";
3
3
  export interface RadioButtonProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "onChange"> {
4
4
  checked: boolean;
5
- onChange: (checked: boolean) => void;
6
- label?: string;
5
+ onChange?: (checked: boolean, ev: ChangeEvent<HTMLInputElement>) => void;
6
+ inputLabel?: string;
7
7
  className?: string;
8
8
  }
9
- export declare function RadioButton({ checked, onChange, label, className, ...rest }: RadioButtonProps): import("react/jsx-runtime").JSX.Element;
9
+ export declare function RadioButton({ checked, onChange, inputLabel, className, ...rest }: RadioButtonProps): import("react/jsx-runtime").JSX.Element;
@@ -1,12 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { clsx } from "clsx";
3
3
  import "../styles/RadioButton.css";
4
- export function RadioButton({ checked, onChange, label, className, ...rest }) {
4
+ export function RadioButton({ checked, onChange, inputLabel, className, ...rest }) {
5
5
  const handleChange = (e) => {
6
- onChange(e.target.checked);
6
+ onChange?.(e.target.checked, e);
7
7
  };
8
- if (label) {
9
- return (_jsxs("label", { className: clsx("PaRadioButton", className), children: [_jsx("input", { className: "PaRadioButton-input", type: "radio", checked: checked, onChange: handleChange, ...rest }), _jsx("span", { className: "PaRadioButton-label", children: label })] }));
8
+ if (inputLabel) {
9
+ return (_jsxs("label", { className: clsx("PaRadioButton", className), children: [_jsx("input", { className: "PaRadioInput", type: "radio", checked: checked, onChange: handleChange, ...rest }), _jsx("span", { className: "PaRadioButton-label", children: inputLabel })] }));
10
10
  }
11
- return (_jsx("input", { className: clsx("PaRadioButton-input", className), type: "radio", checked: checked, onChange: handleChange, ...rest }));
11
+ return (_jsx("input", { className: clsx("PaRadioInput", className), type: "radio", checked: checked, onChange: handleChange, ...rest }));
12
12
  }
package/dist/Select.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import "../styles/Select.css";
2
- import type { SelectHTMLAttributes } from "react";
2
+ import type { ChangeEvent, SelectHTMLAttributes } from "react";
3
3
  export interface SelectOption {
4
4
  value: string;
5
5
  label: string;
@@ -7,12 +7,11 @@ export interface SelectOption {
7
7
  }
8
8
  export interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, "onChange"> {
9
9
  value: string;
10
- onChange: (value: string) => void;
10
+ onChange?: (value: string, ev: ChangeEvent<HTMLSelectElement>) => void;
11
11
  options: SelectOption[];
12
12
  label?: string;
13
13
  error?: string;
14
14
  className?: string;
15
- fullWidth?: boolean;
16
15
  placeholder?: string;
17
16
  }
18
- export declare function Select({ value, onChange, options, label, error, className, fullWidth, placeholder, ...rest }: SelectProps): import("react/jsx-runtime").JSX.Element;
17
+ export declare function Select({ value, onChange, options, label, error, className, placeholder, ...rest }: SelectProps): import("react/jsx-runtime").JSX.Element;
package/dist/Select.js CHANGED
@@ -1,11 +1,11 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import "../styles/Select.css";
3
3
  import { clsx } from "clsx";
4
4
  import { ChevronDown } from "lucide-react";
5
- export function Select({ value, onChange, options, label, error, className, fullWidth = false, placeholder, ...rest }) {
5
+ export function Select({ value, onChange, options, label, error, className, placeholder, ...rest }) {
6
6
  const handleChange = (e) => {
7
- onChange(e.target.value);
7
+ onChange?.(e.target.value, e);
8
8
  };
9
- const selectWithIcon = (_jsxs("span", { className: "PaSelect-fieldWrapper", children: [_jsxs("select", { className: "PaSelect-field", value: value, onChange: handleChange, ...rest, children: [placeholder && (_jsx("option", { value: "", disabled: true, children: placeholder })), options.map((option) => (_jsx("option", { value: option.value, disabled: option.disabled, children: option.label }, option.value)))] }), _jsx(ChevronDown, { className: "PaSelect-icon", size: 16 })] }));
10
- return (_jsxs("span", { className: clsx("PaSelect", error && "error", fullWidth && "fullWidth", className), children: [label ? (_jsxs("label", { className: "PaSelect-wrapper", children: [_jsx("span", { className: "PaSelect-label", children: label }), selectWithIcon] })) : (selectWithIcon), error && _jsx("span", { className: "PaSelect-error", children: error })] }));
9
+ const selectWithIcon = (_jsxs("span", { className: "PaSelectField-wrapper", children: [_jsxs("select", { className: "PaSelect", value: value, onChange: handleChange, ...rest, children: [placeholder && (_jsx("option", { value: "", disabled: true, children: placeholder })), options.map((option) => (_jsx("option", { value: option.value, disabled: option.disabled, children: option.label }, option.value)))] }), _jsx(ChevronDown, { className: "PaSelectField-icon", size: 16 })] }));
10
+ return (_jsxs(_Fragment, { children: [label ? (_jsxs("label", { className: clsx("PaSelectField PaField", error && "error", className), children: [_jsx("span", { className: "PaField-label", children: label }), selectWithIcon] })) : (_jsx("span", { className: clsx("PaSelectField", error && "error", className), children: selectWithIcon })), error && _jsx("span", { className: "PaFieldError", children: error })] }));
11
11
  }
@@ -11,6 +11,7 @@ export interface SplitAnchorElProps extends SplitButtonBaseProps, Omit<AnchorElP
11
11
  interface SplitButtonBaseProps {
12
12
  items: SplitButtonItem[];
13
13
  className?: string;
14
+ position?: "auto" | "topLeft" | "topRight" | "bottomLeft" | "bottomRight";
14
15
  }
15
16
  export interface SplitButtonItem {
16
17
  label: string;