@paroicms/react-ui 0.5.0 → 0.5.1

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/Dialog.d.ts CHANGED
@@ -8,6 +8,6 @@ export interface DialogProps {
8
8
  children: ReactNode;
9
9
  className?: string;
10
10
  closable?: boolean;
11
- modal?: boolean;
11
+ size?: "sm" | "md" | "lg";
12
12
  }
13
- export declare function Dialog({ visible, onHide, header, footer, children, className, closable, modal, }: DialogProps): import("react").ReactPortal | null;
13
+ 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, }) {
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,5 @@ 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
+ return (_jsxs("dialog", { ref: dialogRef, className: clsx("PaDialog", size && `size-${size}`, 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
51
  }
@@ -13,5 +13,6 @@ export interface MultiSelectProps {
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,21 +3,31 @@ 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
33
  onChange(value.filter((v) => v !== optionValue));
@@ -30,21 +40,14 @@ export function MultiSelect({ value, onChange, options, label, error, className,
30
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
  }
@@ -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
  }
@@ -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;
@@ -2,26 +2,23 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import "../styles/SplitButton.css";
3
3
  import { clsx } from "clsx";
4
4
  import { ChevronDown } from "lucide-react";
5
- import { useEffect, useRef, useState } from "react";
5
+ import { useEffect, useRef } from "react";
6
6
  import { Button, } from "./Button.js";
7
+ import { computePopupPosition, setupPopoverPositioning } from "./popup-positioning.js";
7
8
  export function SplitButton(props) {
8
- const { items, className, ...buttonProps } = props;
9
- const [open, setOpen] = useState(false);
9
+ const { items, className, position, ...buttonProps } = props;
10
10
  const containerRef = useRef(null);
11
+ const menuRef = useRef(null);
12
+ const menuId = useRef(`pa-splitbtn-menu-${Math.random().toString(36).substring(2, 9)}`);
11
13
  const severity = props.severity ?? "primary";
12
14
  const disabled = props.disabled;
13
- // Close dropdown when clicking outside
14
15
  useEffect(() => {
15
- if (!open)
16
+ const menu = menuRef.current;
17
+ const container = containerRef.current;
18
+ if (!menu || !container)
16
19
  return;
17
- const handleClickOutside = (e) => {
18
- if (containerRef.current && !containerRef.current.contains(e.target)) {
19
- setOpen(false);
20
- }
21
- };
22
- document.addEventListener("mousedown", handleClickOutside);
23
- return () => document.removeEventListener("mousedown", handleClickOutside);
24
- }, [open]);
20
+ return setupPopoverPositioning(menu, () => computePopupPosition(container, menu, position ?? "auto", "corner"));
21
+ }, [position]);
25
22
  const handleItemClick = async (item) => {
26
23
  try {
27
24
  await item.command();
@@ -35,8 +32,8 @@ export function SplitButton(props) {
35
32
  }
36
33
  }
37
34
  finally {
38
- setOpen(false);
35
+ menuRef.current?.hidePopover();
39
36
  }
40
37
  };
41
- return (_jsxs("div", { ref: containerRef, className: clsx("PaSplitBtn", severity, open && "open", disabled && "disabled", className), children: [_jsx(Button, { ...buttonProps, className: "PaSplitBtn-main" }), _jsx("button", { type: "button", className: "PaSplitBtn-toggle", onClick: () => setOpen(!open), "aria-haspopup": "menu", "aria-expanded": open, children: _jsx(ChevronDown, { className: "PaSplitBtn-toggleIcon" }) }), open && (_jsx("div", { className: "PaSplitBtn-menu", role: "menu", children: items.map((item) => (_jsxs("button", { type: "button", className: clsx("PaSplitBtn-menuItem", item.danger && "danger", item.disabled && "disabled"), onClick: () => void handleItemClick(item), role: "menuitem", disabled: item.disabled, children: [item.icon && _jsx("span", { className: "PaSplitBtn-menuIcon", children: item.icon }), item.label] }, item.label))) }))] }));
38
+ return (_jsxs("div", { ref: containerRef, className: clsx("PaSplitBtn", severity, disabled && "disabled", className), children: [_jsx(Button, { ...buttonProps, className: "PaSplitBtn-main" }), _jsx("button", { type: "button", className: "PaSplitBtn-toggle", popoverTarget: menuId.current, popoverTargetAction: "toggle", "aria-haspopup": "menu", children: _jsx(ChevronDown, { className: "PaSplitBtn-toggleIcon" }) }), _jsx("div", { ref: menuRef, id: menuId.current, className: "PaSplitBtn-menu", role: "menu", popover: "auto", children: items.map((item) => (_jsxs("button", { type: "button", className: clsx("PaSplitBtn-menuItem", item.danger && "danger", item.disabled && "disabled"), onClick: () => void handleItemClick(item), role: "menuitem", disabled: item.disabled, children: [item.icon && _jsx("span", { className: "PaSplitBtn-menuIcon", children: item.icon }), item.label] }, item.label))) })] }));
42
39
  }
package/dist/Tooltip.d.ts CHANGED
@@ -2,7 +2,7 @@ import "../styles/Tooltip.css";
2
2
  import { type ReactNode } from "react";
3
3
  export interface TooltipProps {
4
4
  content: string;
5
- position?: "top" | "bottom" | "left" | "right";
5
+ position?: "auto" | "top" | "bottom" | "left" | "right";
6
6
  children: ReactNode;
7
7
  className?: string;
8
8
  delay?: number;
package/dist/Tooltip.js CHANGED
@@ -1,50 +1,29 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import "../styles/Tooltip.css";
3
3
  import { clsx } from "clsx";
4
- import { useEffect, useRef, useState } from "react";
5
- export function Tooltip({ content, position = "top", children, className, delay = 200, }) {
6
- const [visible, setVisible] = useState(false);
7
- const [coords, setCoords] = useState({ top: 0, left: 0 });
4
+ import { useEffect, useRef } from "react";
5
+ import { computePopupPosition, setupPopoverPositioning } from "./popup-positioning.js";
6
+ export function Tooltip({ content, position = "auto", children, className, delay = 200, }) {
8
7
  const triggerRef = useRef(null);
9
8
  const tooltipRef = useRef(null);
10
9
  const timeoutRef = useRef(undefined);
11
10
  const showTooltip = () => {
12
11
  timeoutRef.current = window.setTimeout(() => {
13
- setVisible(true);
12
+ tooltipRef.current?.showPopover();
14
13
  }, delay);
15
14
  };
16
15
  const hideTooltip = () => {
17
16
  if (timeoutRef.current) {
18
17
  clearTimeout(timeoutRef.current);
19
18
  }
20
- setVisible(false);
19
+ tooltipRef.current?.hidePopover();
21
20
  };
22
21
  useEffect(() => {
23
- if (!visible || !triggerRef.current)
22
+ if (!tooltipRef.current || !triggerRef.current)
24
23
  return;
25
- const trigger = triggerRef.current;
26
- const rect = trigger.getBoundingClientRect();
27
- let top = 0;
28
- let left = 0;
29
- switch (position) {
30
- case "top":
31
- top = rect.top - 8;
32
- left = rect.left + rect.width / 2;
33
- break;
34
- case "bottom":
35
- top = rect.bottom + 8;
36
- left = rect.left + rect.width / 2;
37
- break;
38
- case "left":
39
- top = rect.top + rect.height / 2;
40
- left = rect.left - 8;
41
- break;
42
- case "right":
43
- top = rect.top + rect.height / 2;
44
- left = rect.right + 8;
45
- break;
46
- }
47
- setCoords({ top, left });
48
- }, [visible, position]);
49
- return (_jsxs(_Fragment, { children: [_jsx("div", { ref: triggerRef, className: "PaTooltip-trigger", onMouseEnter: showTooltip, onMouseLeave: hideTooltip, onFocus: showTooltip, onBlur: hideTooltip, children: children }), visible && (_jsx("div", { ref: tooltipRef, className: clsx("PaTooltip", position, className), style: { top: coords.top, left: coords.left }, role: "tooltip", children: content }))] }));
24
+ const triggerEl = triggerRef.current;
25
+ const popupEl = tooltipRef.current;
26
+ return setupPopoverPositioning(popupEl, () => computePopupPosition(triggerEl, popupEl, position, "cardinal"));
27
+ }, [position]);
28
+ return (_jsxs(_Fragment, { children: [_jsx("div", { ref: triggerRef, className: "PaTooltip-trigger", onMouseEnter: showTooltip, onMouseLeave: hideTooltip, onFocus: showTooltip, onBlur: hideTooltip, children: children }), _jsx("div", { ref: tooltipRef, className: clsx("PaTooltip", className), role: "tooltip", popover: "hint", children: content })] }));
50
29
  }
@@ -14,5 +14,7 @@ interface AlertStackProviderProps {
14
14
  }
15
15
  export declare function AlertStackProvider({ children, ref }: AlertStackProviderProps): import("react/jsx-runtime").JSX.Element;
16
16
  export declare function useAlertStack(): AlertStackHandle;
17
- export declare function AlertStack(): import("react/jsx-runtime").JSX.Element | null;
17
+ export declare function AlertStack(props?: {
18
+ className?: string;
19
+ }): import("react/jsx-runtime").JSX.Element | null;
18
20
  export {};
@@ -47,7 +47,7 @@ export function useAlertStack() {
47
47
  }, [context.showAlert]);
48
48
  return { showAlert: context.showAlert, showError };
49
49
  }
50
- export function AlertStack() {
50
+ export function AlertStack(props) {
51
51
  const context = useContext(AlertStackContext);
52
52
  const config = useReactUIConfig();
53
53
  if (!context) {
@@ -68,5 +68,5 @@ export function AlertStack() {
68
68
  ]);
69
69
  if (!currentAlert)
70
70
  return null;
71
- return (_jsx(Alert, { severity: currentAlert.severity, onClose: dismissCurrent, children: currentAlert.message }));
71
+ return (_jsx(Alert, { className: props?.className, severity: currentAlert.severity, onClose: dismissCurrent, children: currentAlert.message }));
72
72
  }
@@ -0,0 +1,10 @@
1
+ export type CornerPosition = "topLeft" | "topRight" | "bottomLeft" | "bottomRight";
2
+ export type VerticalPosition = "top" | "bottom";
3
+ export type CardinalPosition = "top" | "bottom" | "left" | "right";
4
+ export interface PositionResult {
5
+ top: number;
6
+ left: number;
7
+ position: string;
8
+ }
9
+ export declare function setupPopoverPositioning(popupEl: HTMLElement, getPosition: () => PositionResult): () => void;
10
+ export declare function computePopupPosition(triggerEl: HTMLElement, popupEl: HTMLElement, preferredPosition: string, mode: "corner" | "vertical" | "cardinal"): PositionResult;
@@ -0,0 +1,160 @@
1
+ export function setupPopoverPositioning(popupEl, getPosition) {
2
+ const handleToggle = (e) => {
3
+ const newState = e.newState;
4
+ if (newState === "open") {
5
+ const position = getPosition();
6
+ popupEl.style.position = "absolute";
7
+ popupEl.style.top = `${position.top}px`;
8
+ popupEl.style.left = `${position.left}px`;
9
+ }
10
+ };
11
+ popupEl.addEventListener("toggle", handleToggle);
12
+ return () => popupEl.removeEventListener("toggle", handleToggle);
13
+ }
14
+ export function computePopupPosition(triggerEl, popupEl, preferredPosition, mode) {
15
+ const triggerRect = triggerEl.getBoundingClientRect();
16
+ const popupWidth = popupEl.offsetWidth;
17
+ const popupHeight = popupEl.offsetHeight;
18
+ const bounds = getConstrainingBounds(triggerEl);
19
+ let result;
20
+ switch (mode) {
21
+ case "corner":
22
+ result = computeCornerPosition(triggerRect, popupWidth, popupHeight, bounds, preferredPosition);
23
+ break;
24
+ case "vertical":
25
+ result = computeVerticalPosition(triggerRect, popupWidth, popupHeight, bounds, preferredPosition);
26
+ break;
27
+ case "cardinal":
28
+ result = computeCardinalPosition(triggerRect, popupWidth, popupHeight, bounds, preferredPosition);
29
+ break;
30
+ }
31
+ return {
32
+ position: result.position,
33
+ top: result.top + window.scrollY,
34
+ left: result.left + window.scrollX,
35
+ };
36
+ }
37
+ function getConstrainingBounds(triggerEl) {
38
+ const dialog = triggerEl.closest("dialog");
39
+ if (dialog) {
40
+ const rect = dialog.getBoundingClientRect();
41
+ return {
42
+ top: rect.top,
43
+ left: rect.left,
44
+ right: rect.right,
45
+ bottom: rect.bottom,
46
+ };
47
+ }
48
+ return {
49
+ top: 0,
50
+ left: 0,
51
+ right: window.innerWidth,
52
+ bottom: window.innerHeight,
53
+ };
54
+ }
55
+ function computeCornerPosition(triggerRect, popupWidth, popupHeight, bounds, preferredPosition) {
56
+ const spaceTop = triggerRect.top - bounds.top;
57
+ const spaceBottom = bounds.bottom - triggerRect.bottom;
58
+ const spaceLeft = triggerRect.left - bounds.left;
59
+ const spaceRight = bounds.right - triggerRect.right;
60
+ let position;
61
+ if (preferredPosition !== "auto") {
62
+ position = preferredPosition;
63
+ }
64
+ else {
65
+ // Pick corner with most space, prefer bottom-right > bottom-left > top-right > top-left
66
+ const canBottom = spaceBottom >= popupHeight;
67
+ const canTop = spaceTop >= popupHeight;
68
+ const canRight = spaceRight + triggerRect.width >= popupWidth;
69
+ const canLeft = spaceLeft + triggerRect.width >= popupWidth;
70
+ if (canBottom && canRight) {
71
+ position = "bottomRight";
72
+ }
73
+ else if (canBottom && canLeft) {
74
+ position = "bottomLeft";
75
+ }
76
+ else if (canTop && canRight) {
77
+ position = "topRight";
78
+ }
79
+ else if (canTop && canLeft) {
80
+ position = "topLeft";
81
+ }
82
+ else {
83
+ // Fallback: choose based on available space
84
+ const vertical = spaceBottom >= spaceTop ? "bottom" : "top";
85
+ const horizontal = spaceRight >= spaceLeft ? "Right" : "Left";
86
+ position = `${vertical}${horizontal}`;
87
+ }
88
+ }
89
+ let top;
90
+ let left;
91
+ if (position.startsWith("bottom")) {
92
+ top = triggerRect.bottom;
93
+ }
94
+ else {
95
+ top = triggerRect.top - popupHeight;
96
+ }
97
+ if (position.endsWith("Right")) {
98
+ left = triggerRect.left;
99
+ }
100
+ else {
101
+ left = triggerRect.right - popupWidth;
102
+ }
103
+ return { position, top, left };
104
+ }
105
+ function computeVerticalPosition(triggerRect, _popupWidth, popupHeight, bounds, preferredPosition) {
106
+ const spaceTop = triggerRect.top - bounds.top;
107
+ const spaceBottom = bounds.bottom - triggerRect.bottom;
108
+ let position;
109
+ if (preferredPosition !== "auto") {
110
+ position = preferredPosition;
111
+ }
112
+ else {
113
+ position = spaceBottom >= popupHeight || spaceBottom >= spaceTop ? "bottom" : "top";
114
+ }
115
+ const top = position === "bottom" ? triggerRect.bottom : triggerRect.top - popupHeight;
116
+ const left = triggerRect.left;
117
+ return { position, top, left };
118
+ }
119
+ function computeCardinalPosition(triggerRect, popupWidth, popupHeight, bounds, preferredPosition) {
120
+ const spaceTop = triggerRect.top - bounds.top;
121
+ const spaceBottom = bounds.bottom - triggerRect.bottom;
122
+ const spaceLeft = triggerRect.left - bounds.left;
123
+ const spaceRight = bounds.right - triggerRect.right;
124
+ let position;
125
+ if (preferredPosition !== "auto") {
126
+ position = preferredPosition;
127
+ }
128
+ else {
129
+ // Pick direction with most space
130
+ const spaces = [
131
+ { dir: "bottom", space: spaceBottom },
132
+ { dir: "right", space: spaceRight },
133
+ { dir: "top", space: spaceTop },
134
+ { dir: "left", space: spaceLeft },
135
+ ];
136
+ spaces.sort((a, b) => b.space - a.space);
137
+ position = spaces[0].dir;
138
+ }
139
+ let top;
140
+ let left;
141
+ switch (position) {
142
+ case "top":
143
+ top = triggerRect.top - popupHeight;
144
+ left = triggerRect.left + (triggerRect.width - popupWidth) / 2;
145
+ break;
146
+ case "bottom":
147
+ top = triggerRect.bottom;
148
+ left = triggerRect.left + (triggerRect.width - popupWidth) / 2;
149
+ break;
150
+ case "left":
151
+ top = triggerRect.top + (triggerRect.height - popupHeight) / 2;
152
+ left = triggerRect.left - popupWidth;
153
+ break;
154
+ case "right":
155
+ top = triggerRect.top + (triggerRect.height - popupHeight) / 2;
156
+ left = triggerRect.right;
157
+ break;
158
+ }
159
+ return { position, top, left };
160
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paroicms/react-ui",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "React UI toolkit for ParoiCMS.",
5
5
  "keywords": [
6
6
  "paroicms",
package/styles/Alert.css CHANGED
@@ -4,6 +4,7 @@
4
4
  .PaAlert {
5
5
  position: relative;
6
6
  padding: var(--space-4) var(--space-5);
7
+ margin: var(--space-4) 0;
7
8
  border-left: 4px solid;
8
9
  border-radius: var(--radius);
9
10
 
package/styles/Dialog.css CHANGED
@@ -1,32 +1,43 @@
1
1
  /* ========================================
2
2
  Dialog Component
3
3
  ======================================== */
4
- .PaDialog-overlay {
5
- position: fixed;
6
- inset: 0;
7
- z-index: var(--z-modal);
8
- display: flex;
9
- align-items: center;
10
- justify-content: center;
11
- }
12
-
13
- .PaDialog-backdrop {
14
- position: absolute;
15
- inset: 0;
16
- background: rgba(0, 0, 0, 0.5);
17
- }
18
-
19
4
  .PaDialog {
20
- position: relative;
21
- z-index: 1;
22
- display: flex;
23
- flex-direction: column;
24
- min-width: 320px;
25
5
  max-width: 90vw;
26
- max-height: 90vh;
6
+ padding: 0;
27
7
  background: var(--color-bg-elevated);
8
+ border: none;
28
9
  border-radius: var(--radius-lg);
29
10
  box-shadow: var(--shadow-lg);
11
+
12
+ &[open] {
13
+ display: flex;
14
+ flex-direction: column;
15
+ }
16
+
17
+ &:modal {
18
+ max-height: 90vh;
19
+ margin: auto;
20
+ }
21
+
22
+ &::backdrop {
23
+ background: rgba(0, 0, 0, 0.5);
24
+ }
25
+
26
+ /* Size variants */
27
+ &.size-sm {
28
+ width: 320px;
29
+ max-width: 100%;
30
+ }
31
+
32
+ &.size-md {
33
+ width: 480px;
34
+ max-width: 100%;
35
+ }
36
+
37
+ &.size-lg {
38
+ width: 640px;
39
+ max-width: 100%;
40
+ }
30
41
  }
31
42
 
32
43
  .PaDialog-header {
@@ -45,7 +45,7 @@
45
45
  border-color: var(--color-primary);
46
46
  }
47
47
 
48
- &.open {
48
+ &:has(+ .PaMultiSelect-dropdown:popover-open) {
49
49
  border-color: var(--color-primary);
50
50
  box-shadow: 0 0 0 3px var(--color-primary-light);
51
51
  }
@@ -77,7 +77,11 @@
77
77
  .PaMultiSelect-chipRemove {
78
78
  display: flex;
79
79
  align-items: center;
80
+ justify-content: center;
81
+ width: 25px;
82
+ height: 25px;
80
83
  padding: 0;
84
+ margin: calc(-1 * var(--space-1));
81
85
  color: var(--color-text-muted);
82
86
  cursor: pointer;
83
87
  background: transparent;
@@ -96,12 +100,11 @@
96
100
 
97
101
  .PaMultiSelect-dropdown {
98
102
  position: absolute;
99
- top: calc(100% + var(--space-1));
100
- right: 0;
101
- left: 0;
102
- z-index: var(--z-dropdown);
103
+ inset: auto;
104
+ z-index: var(--z-popover);
103
105
  max-height: 250px;
104
106
  padding: var(--space-2);
107
+ margin: 0;
105
108
  overflow-y: auto;
106
109
  background: var(--color-bg-elevated);
107
110
  border: 1px solid var(--color-border);
@@ -134,24 +137,6 @@
134
137
  }
135
138
  }
136
139
 
137
- .PaMultiSelect-checkbox {
138
- display: flex;
139
- align-items: center;
140
- justify-content: center;
141
- width: 16px;
142
- height: 16px;
143
- font-size: var(--text-xs);
144
- color: var(--color-primary);
145
- border: 1px solid var(--color-border);
146
- border-radius: var(--radius-sm);
147
-
148
- .PaMultiSelect-option.selected & {
149
- color: var(--color-text-inverse);
150
- background: var(--color-primary);
151
- border-color: var(--color-primary);
152
- }
153
- }
154
-
155
140
  .PaMultiSelect-error {
156
141
  font-size: var(--text-xs);
157
142
  color: var(--color-danger);
@@ -68,21 +68,15 @@
68
68
  }
69
69
 
70
70
  .PaSplitBtn-menu {
71
- position: absolute;
72
- top: calc(100% + var(--space-1));
73
- right: 0;
71
+ inset: auto;
74
72
  z-index: var(--z-dropdown);
75
- display: none;
76
73
  min-width: 180px;
77
74
  padding: var(--space-2);
75
+ margin: 0;
78
76
  background: var(--color-bg-elevated);
79
77
  border: 1px solid var(--color-border);
80
78
  border-radius: var(--radius-md);
81
79
  box-shadow: var(--shadow-md);
82
-
83
- .PaSplitBtn.open & {
84
- display: block;
85
- }
86
80
  }
87
81
 
88
82
  .PaSplitBtn-menuItem {
@@ -6,29 +6,15 @@
6
6
  }
7
7
 
8
8
  .PaTooltip {
9
- position: fixed;
10
- z-index: var(--z-tooltip);
9
+ inset: auto;
10
+ z-index: var(--z-popover);
11
11
  max-width: 200px;
12
12
  padding: var(--space-2) var(--space-3);
13
+ margin: 0;
13
14
  font-size: var(--text-xs);
14
15
  color: var(--color-text-inverse);
15
16
  pointer-events: none;
16
17
  background: var(--color-text);
18
+ border: none;
17
19
  border-radius: var(--radius);
18
-
19
- &.top {
20
- transform: translate(-50%, -100%);
21
- }
22
-
23
- &.bottom {
24
- transform: translateX(-50%);
25
- }
26
-
27
- &.left {
28
- transform: translate(-100%, -50%);
29
- }
30
-
31
- &.right {
32
- transform: translateY(-50%);
33
- }
34
20
  }
package/styles/Tree.css CHANGED
@@ -16,6 +16,7 @@
16
16
  }
17
17
 
18
18
  .PaTree-node {
19
+ position: relative;
19
20
  padding: var(--space-1) 0;
20
21
  list-style: none;
21
22
  }
@@ -141,7 +142,7 @@
141
142
  .PaTree-children .PaTree-node::before {
142
143
  position: absolute;
143
144
  top: 0;
144
- left: calc(-1 * var(--space-3));
145
+ left: calc(var(--space-2) + 11px - var(--space-6));
145
146
  height: 100%;
146
147
  content: "";
147
148
  border-left: 1.5px solid var(--color-border);
@@ -150,8 +151,8 @@
150
151
  .PaTree-children .PaTree-node::after {
151
152
  position: absolute;
152
153
  top: 1rem;
153
- left: calc(-1 * var(--space-3));
154
- width: var(--space-2);
154
+ left: calc(var(--space-2) + 11px - var(--space-6));
155
+ width: calc(var(--space-6));
155
156
  content: "";
156
157
  border-top: 1.5px solid var(--color-border);
157
158
  }