@particle-network/ui-react 0.7.0-beta.14 → 0.7.0-beta.15

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.
@@ -1 +1,2 @@
1
+ export * from './provider';
1
2
  export * from './simple-popover';
@@ -1 +1,2 @@
1
+ export * from "./provider.js";
1
2
  export * from "./simple-popover.js";
@@ -0,0 +1,22 @@
1
+ import React from 'react';
2
+ import type { SimplePopoverPlacement, SimplePopoverSize } from './simple-popover';
3
+ export interface TriggerOptions {
4
+ placement?: SimplePopoverPlacement;
5
+ size?: SimplePopoverSize;
6
+ offset?: number;
7
+ delay?: number;
8
+ closeDelay?: number;
9
+ contentClassName?: string;
10
+ }
11
+ interface SimplePopoverCtx {
12
+ show: (anchor: HTMLElement, content: React.ReactNode, options?: TriggerOptions) => void;
13
+ hide: (closeDelay?: number) => void;
14
+ prepare: (anchor: HTMLElement, content: React.ReactNode, options?: TriggerOptions) => void;
15
+ toggle: () => void;
16
+ }
17
+ export declare function useSimplePopoverContext(): SimplePopoverCtx | null;
18
+ export interface UXSimplePopoverProviderProps {
19
+ children: React.ReactNode;
20
+ }
21
+ export declare const UXSimplePopoverProvider: React.FC<UXSimplePopoverProviderProps>;
22
+ export {};
@@ -0,0 +1,197 @@
1
+ 'use client';
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import react, { createContext, useCallback, useContext, useEffect, useId, useRef, useState } from "react";
4
+ import { tv } from "@heroui/theme";
5
+ import { cn } from "../../utils/index.js";
6
+ const popoverVariants = tv({
7
+ base: 'bg-content1 text-foreground-300 shadow-box text-tiny leading-1.4 rounded-medium max-w-[300px] break-words wrap-break-word antialiased',
8
+ variants: {
9
+ size: {
10
+ sm: 'p-1.5',
11
+ md: 'p-2.5',
12
+ lg: 'p-3.5'
13
+ }
14
+ },
15
+ defaultVariants: {
16
+ size: 'md'
17
+ }
18
+ });
19
+ const PopoverContext = /*#__PURE__*/ createContext(null);
20
+ function useSimplePopoverContext() {
21
+ return useContext(PopoverContext);
22
+ }
23
+ const OFFSET_MARGIN = {
24
+ top: {
25
+ marginBottom: 8
26
+ },
27
+ bottom: {
28
+ marginTop: 8
29
+ },
30
+ left: {
31
+ marginRight: 8
32
+ },
33
+ right: {
34
+ marginLeft: 8
35
+ }
36
+ };
37
+ const MARGIN_SIDE_MAP = {
38
+ top: 'Bottom',
39
+ bottom: 'Top',
40
+ left: 'Right',
41
+ right: 'Left'
42
+ };
43
+ const UXSimplePopoverProvider = ({ children })=>{
44
+ const uid = useId().replace(/:/g, '');
45
+ const popoverRef = useRef(null);
46
+ const openTimerRef = useRef(null);
47
+ const closeTimerRef = useRef(null);
48
+ const triggerModeRef = useRef('hover');
49
+ const currentAnchorRef = useRef(null);
50
+ const [content, setContent] = useState(null);
51
+ const [anchorStyle, setAnchorStyle] = useState({});
52
+ const [contentClassName, setContentClassName] = useState(void 0);
53
+ const [size, setSize] = useState('md');
54
+ const clearTimers = useCallback(()=>{
55
+ if (null !== openTimerRef.current) {
56
+ clearTimeout(openTimerRef.current);
57
+ openTimerRef.current = null;
58
+ }
59
+ if (null !== closeTimerRef.current) {
60
+ clearTimeout(closeTimerRef.current);
61
+ closeTimerRef.current = null;
62
+ }
63
+ }, []);
64
+ const resolveAnchorName = useCallback((anchor)=>{
65
+ if (!anchor.style.getPropertyValue('anchor-name')) anchor.style.setProperty('anchor-name', `--sp-${uid}-${Math.random().toString(36).slice(2, 8)}`);
66
+ return anchor.style.getPropertyValue('anchor-name');
67
+ }, [
68
+ uid
69
+ ]);
70
+ const computeAnchorStyle = useCallback((anchorName, options)=>{
71
+ const { placement = 'top', offset = 8 } = options;
72
+ const primarySide = placement.split('-')[0];
73
+ const marginSide = MARGIN_SIDE_MAP[primarySide] ?? 'Bottom';
74
+ const offsetStyle = 8 !== offset ? {
75
+ [`margin${marginSide}`]: offset
76
+ } : OFFSET_MARGIN[primarySide];
77
+ return {
78
+ positionAnchor: anchorName,
79
+ positionArea: placement.replace(/-/g, ' '),
80
+ positionTryFallbacks: 'flip-block, flip-inline, flip-block flip-inline',
81
+ ...offsetStyle
82
+ };
83
+ }, []);
84
+ const show = useCallback((anchor, node, options)=>{
85
+ const opts = options ?? {};
86
+ const delay = opts.delay ?? 300;
87
+ clearTimers();
88
+ triggerModeRef.current = 'hover';
89
+ const anchorName = resolveAnchorName(anchor);
90
+ setContent(node);
91
+ setContentClassName(opts.contentClassName);
92
+ setSize(opts.size ?? 'md');
93
+ setAnchorStyle(computeAnchorStyle(anchorName, opts));
94
+ openTimerRef.current = setTimeout(()=>{
95
+ openTimerRef.current = null;
96
+ popoverRef.current?.showPopover();
97
+ }, delay);
98
+ }, [
99
+ clearTimers,
100
+ resolveAnchorName,
101
+ computeAnchorStyle
102
+ ]);
103
+ const hide = useCallback((closeDelay)=>{
104
+ const cd = closeDelay ?? 100;
105
+ clearTimers();
106
+ closeTimerRef.current = setTimeout(()=>{
107
+ closeTimerRef.current = null;
108
+ popoverRef.current?.hidePopover();
109
+ }, cd);
110
+ }, [
111
+ clearTimers
112
+ ]);
113
+ const prepare = useCallback((anchor, node, options)=>{
114
+ const opts = options ?? {};
115
+ clearTimers();
116
+ triggerModeRef.current = 'click';
117
+ currentAnchorRef.current = anchor;
118
+ const anchorName = resolveAnchorName(anchor);
119
+ setContent(node);
120
+ setContentClassName(opts.contentClassName);
121
+ setSize(opts.size ?? 'md');
122
+ setAnchorStyle(computeAnchorStyle(anchorName, opts));
123
+ }, [
124
+ clearTimers,
125
+ resolveAnchorName,
126
+ computeAnchorStyle
127
+ ]);
128
+ const toggle = useCallback(()=>{
129
+ const el = popoverRef.current;
130
+ if (!el) return;
131
+ try {
132
+ if (el.matches(':popover-open')) el.hidePopover();
133
+ else el.showPopover();
134
+ } catch {}
135
+ }, []);
136
+ useEffect(()=>{
137
+ const handlePointerDown = (e)=>{
138
+ const el = popoverRef.current;
139
+ if (!el || 'click' !== triggerModeRef.current) return;
140
+ try {
141
+ if (!el.matches(':popover-open')) return;
142
+ } catch {
143
+ return;
144
+ }
145
+ const target = e.target;
146
+ if (!el.contains(target) && !currentAnchorRef.current?.contains(target)) el.hidePopover();
147
+ };
148
+ document.addEventListener('pointerdown', handlePointerDown);
149
+ return ()=>document.removeEventListener('pointerdown', handlePointerDown);
150
+ }, []);
151
+ useEffect(()=>()=>{
152
+ clearTimers();
153
+ }, [
154
+ clearTimers
155
+ ]);
156
+ const ctx = react.useMemo(()=>({
157
+ show,
158
+ hide,
159
+ prepare,
160
+ toggle
161
+ }), [
162
+ show,
163
+ hide,
164
+ prepare,
165
+ toggle
166
+ ]);
167
+ const handlePopoverMouseEnter = useCallback(()=>{
168
+ if ('hover' === triggerModeRef.current) clearTimers();
169
+ }, [
170
+ clearTimers
171
+ ]);
172
+ const handlePopoverMouseLeave = useCallback(()=>{
173
+ if ('hover' === triggerModeRef.current) hide();
174
+ }, [
175
+ hide
176
+ ]);
177
+ return /*#__PURE__*/ jsxs(PopoverContext.Provider, {
178
+ value: ctx,
179
+ children: [
180
+ children,
181
+ /*#__PURE__*/ jsx("div", {
182
+ ref: popoverRef,
183
+ id: `ux-sp-${uid}`,
184
+ popover: "manual",
185
+ className: cn(popoverVariants({
186
+ size
187
+ }), contentClassName),
188
+ style: anchorStyle,
189
+ onMouseEnter: handlePopoverMouseEnter,
190
+ onMouseLeave: handlePopoverMouseLeave,
191
+ children: content
192
+ })
193
+ ]
194
+ });
195
+ };
196
+ UXSimplePopoverProvider.displayName = 'UX.SimplePopoverProvider';
197
+ export { UXSimplePopoverProvider, useSimplePopoverContext };
@@ -1,22 +1,25 @@
1
1
  import React from 'react';
2
2
  export type SimplePopoverPlacement = 'top' | 'top-left' | 'top-right' | 'bottom' | 'bottom-left' | 'bottom-right' | 'left' | 'left-top' | 'left-bottom' | 'right' | 'right-top' | 'right-bottom';
3
3
  export type SimplePopoverTriggerType = 'click' | 'hover';
4
+ export type SimplePopoverSize = 'sm' | 'md' | 'lg';
4
5
  export interface UXSimplePopoverProps {
5
6
  /** Popover content */
6
7
  content: React.ReactNode;
7
8
  /** Trigger element(s) */
8
9
  children: React.ReactNode;
9
- /** How to open: click (uses popovertarget) or hover (uses showPopover/hidePopover) */
10
+ /** How to open: click or hover */
10
11
  triggerType?: SimplePopoverTriggerType;
11
12
  /** Placement relative to trigger; uses CSS Anchor Positioning when supported */
12
13
  placement?: SimplePopoverPlacement;
14
+ /** Popover padding size */
15
+ size?: SimplePopoverSize;
13
16
  /** Hover only: delay before opening (ms) */
14
17
  delay?: number;
15
18
  /** Hover only: delay before closing (ms) */
16
19
  closeDelay?: number;
17
20
  /** Distance between popover and trigger (px) */
18
21
  offset?: number;
19
- /** ClassName for the trigger wrapper (when cloning a single child, applied to that child) */
22
+ /** ClassName for the trigger wrapper and popover content */
20
23
  classNames?: {
21
24
  trigger?: string;
22
25
  content?: string;
@@ -1,132 +1,84 @@
1
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
- import react, { cloneElement, isValidElement, useCallback, useId, useRef } from "react";
3
- import { cn } from "../../utils/index.js";
4
- const UXSimplePopover = (props)=>{
5
- const { content, children, triggerType = 'hover', placement = 'top', offset = 8, delay = 300, closeDelay = 100, classNames = {
6
- trigger: '',
7
- content: ''
8
- } } = props;
9
- const id = useId().replace(/:/g, '');
10
- const popoverId = `simple-popover-${id}`;
11
- const anchorName = `--simple-popover-${id}`;
12
- const popoverRef = useRef(null);
13
- const openTimerRef = useRef(null);
14
- const closeTimerRef = useRef(null);
15
- const clearOpenTimer = useCallback(()=>{
16
- if (null !== openTimerRef.current) {
17
- clearTimeout(openTimerRef.current);
18
- openTimerRef.current = null;
19
- }
20
- }, []);
21
- const clearCloseTimer = useCallback(()=>{
22
- if (null !== closeTimerRef.current) {
23
- clearTimeout(closeTimerRef.current);
24
- closeTimerRef.current = null;
25
- }
26
- }, []);
27
- const showPopover = useCallback(()=>{
28
- clearCloseTimer();
29
- openTimerRef.current = setTimeout(()=>{
30
- openTimerRef.current = null;
31
- popoverRef.current?.showPopover();
32
- }, delay);
33
- }, [
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { useCallback, useRef } from "react";
3
+ import { useSimplePopoverContext } from "./provider.js";
4
+ const warnedRef = {
5
+ current: false
6
+ };
7
+ const UXSimplePopover = ({ content, children, triggerType = 'hover', placement = 'top', size = 'md', offset = 8, delay = 300, closeDelay = 100, classNames })=>{
8
+ const ctx = useSimplePopoverContext();
9
+ const ref = useRef(null);
10
+ const contentRef = useRef(content);
11
+ contentRef.current = content;
12
+ const optionsRef = useRef({
13
+ placement,
14
+ size,
15
+ offset,
16
+ delay,
17
+ closeDelay,
18
+ contentClassName: classNames?.content
19
+ });
20
+ optionsRef.current = {
21
+ placement,
22
+ size,
23
+ offset,
34
24
  delay,
35
- clearCloseTimer
25
+ closeDelay,
26
+ contentClassName: classNames?.content
27
+ };
28
+ const isClick = 'click' === triggerType;
29
+ const handleEnter = useCallback(()=>{
30
+ if (ref.current && ctx && !isClick) ctx.show(ref.current, contentRef.current, optionsRef.current);
31
+ }, [
32
+ ctx,
33
+ isClick
36
34
  ]);
37
- const hidePopover = useCallback(()=>{
38
- clearOpenTimer();
39
- closeTimerRef.current = setTimeout(()=>{
40
- closeTimerRef.current = null;
41
- popoverRef.current?.hidePopover();
42
- }, closeDelay);
35
+ const handleLeave = useCallback(()=>{
36
+ if (ctx && !isClick) ctx.hide(optionsRef.current.closeDelay);
43
37
  }, [
44
- closeDelay,
45
- clearOpenTimer
38
+ ctx,
39
+ isClick
46
40
  ]);
47
- react.useEffect(()=>()=>{
48
- clearOpenTimer();
49
- clearCloseTimer();
50
- }, [
51
- clearOpenTimer,
52
- clearCloseTimer
41
+ const handlePointerDown = useCallback(()=>{
42
+ if (ref.current && ctx && isClick) ctx.prepare(ref.current, contentRef.current, optionsRef.current);
43
+ }, [
44
+ ctx,
45
+ isClick
53
46
  ]);
54
- const triggerAnchorStyle = {
55
- anchorName
56
- };
57
- const offsetMargin = {
58
- top: {
59
- marginBottom: offset
60
- },
61
- bottom: {
62
- marginTop: offset
63
- },
64
- left: {
65
- marginRight: offset
66
- },
67
- right: {
68
- marginLeft: offset
69
- }
70
- };
71
- const primarySide = placement.split('-')[0];
72
- const popoverAnchorStyle = {
73
- positionAnchor: anchorName,
74
- positionArea: placement.replace(/-/g, ' '),
75
- positionTryFallbacks: 'flip-block, flip-inline, flip-block flip-inline',
76
- ...offsetMargin[primarySide]
77
- };
78
- const isClick = 'click' === triggerType;
79
- const trigger = isClick ? /*#__PURE__*/ jsx("button", {
80
- type: "button",
81
- popoverTarget: popoverId,
82
- popoverTargetAction: "toggle",
83
- className: classNames.trigger,
84
- style: {
85
- margin: 0,
86
- padding: 0,
87
- border: 'none',
88
- font: 'inherit',
89
- color: 'inherit',
90
- backgroundColor: 'transparent',
91
- cursor: 'pointer',
92
- ...triggerAnchorStyle
93
- },
94
- children: children
95
- }) : (()=>{
96
- const childElement = /*#__PURE__*/ isValidElement(children) && 1 === react.Children.count(children) ? children : null;
97
- return childElement ? /*#__PURE__*/ cloneElement(childElement, {
98
- onMouseEnter: showPopover,
99
- onMouseLeave: hidePopover,
100
- className: [
101
- classNames.trigger,
102
- childElement.props?.className
103
- ].filter(Boolean).join(' '),
104
- style: {
105
- ...childElement.props?.style,
106
- ...triggerAnchorStyle
47
+ const handleClick = useCallback(()=>{
48
+ if (ctx && isClick) ctx.toggle();
49
+ }, [
50
+ ctx,
51
+ isClick
52
+ ]);
53
+ const handleKeyDown = useCallback((e)=>{
54
+ if (isClick && ('Enter' === e.key || ' ' === e.key)) {
55
+ e.preventDefault();
56
+ if (ref.current && ctx) {
57
+ ctx.prepare(ref.current, contentRef.current, optionsRef.current);
58
+ ctx.toggle();
107
59
  }
108
- }) : /*#__PURE__*/ jsx("span", {
109
- className: classNames.trigger,
110
- style: triggerAnchorStyle,
111
- role: "button",
60
+ }
61
+ }, [
62
+ ctx,
63
+ isClick
64
+ ]);
65
+ if (!ctx && !warnedRef.current) {
66
+ warnedRef.current = true;
67
+ console.warn('[UXSimplePopover] must be used inside <UXSimplePopoverProvider>. Popover will not work.');
68
+ }
69
+ return /*#__PURE__*/ jsx("span", {
70
+ ref: ref,
71
+ className: classNames?.trigger,
72
+ onMouseEnter: handleEnter,
73
+ onMouseLeave: handleLeave,
74
+ ...isClick ? {
75
+ role: 'button',
112
76
  tabIndex: 0,
113
- onMouseEnter: showPopover,
114
- onMouseLeave: hidePopover,
115
- children: children
116
- });
117
- })();
118
- return /*#__PURE__*/ jsxs(Fragment, {
119
- children: [
120
- trigger,
121
- /*#__PURE__*/ jsx("div", {
122
- ref: popoverRef,
123
- id: popoverId,
124
- popover: isClick ? 'auto' : 'manual',
125
- className: cn('bg-content1 text-foreground-300 shadow-box text-tiny leading-1.4 rounded-medium max-w-[300px] p-2.5 break-words wrap-break-word antialiased', classNames.content),
126
- style: popoverAnchorStyle,
127
- children: content
128
- })
129
- ]
77
+ onPointerDownCapture: handlePointerDown,
78
+ onClickCapture: handleClick,
79
+ onKeyDown: handleKeyDown
80
+ } : {},
81
+ children: children
130
82
  });
131
83
  };
132
84
  UXSimplePopover.displayName = 'UXSimplePopover';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@particle-network/ui-react",
3
- "version": "0.7.0-beta.14",
3
+ "version": "0.7.0-beta.15",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -37,8 +37,8 @@
37
37
  "@rslib/core": "^0.12.3",
38
38
  "@types/react": "^19.1.10",
39
39
  "react": "^19.1.0",
40
- "@particle-network/lintstaged-config": "0.1.0",
41
- "@particle-network/eslint-config": "0.3.0"
40
+ "@particle-network/eslint-config": "0.3.0",
41
+ "@particle-network/lintstaged-config": "0.1.0"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "react": ">=16.9.0",