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

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.
@@ -33,10 +33,10 @@ const button_theme_button = tv({
33
33
  text: 'bg-transparent min-w-0 w-auto h-auto px-0'
34
34
  },
35
35
  size: {
36
- xs: 'gap-1 rounded-sm !text-caption1 min-w-min font-medium [&>svg]:size-[14px]',
37
- sm: 'gap-1 rounded-md !text-body3 min-w-min font-medium [&>svg]:size-4',
38
- md: 'gap-1 rounded-md text-tiny min-w-min font-medium [&>svg]:size-[18px]',
39
- lg: 'gap-1 rounded-lg text-sm min-w-min font-medium [&>svg]:size-5',
36
+ xs: 'gap-1 rounded-[4px] !text-caption1 min-w-min font-medium [&>svg]:size-[14px]',
37
+ sm: 'gap-1 rounded-[6px] !text-body3 min-w-min font-medium [&>svg]:size-4',
38
+ md: 'gap-1 rounded-[6px] text-tiny min-w-min font-medium [&>svg]:size-[18px]',
39
+ lg: 'gap-1 rounded-[8px] text-sm min-w-min font-medium [&>svg]:size-5',
40
40
  xl: 'gap-1 rounded-[10px] text-medium min-w-min font-medium [&>svg]:size-6',
41
41
  auto: 'min-w-min rounded-[10px]'
42
42
  },
@@ -492,22 +492,22 @@ const button_theme_button = tv({
492
492
  {
493
493
  isInGroup: true,
494
494
  size: 'xs',
495
- class: 'rounded-none first:rounded-s-sm last:rounded-e-sm'
495
+ class: 'rounded-none first:rounded-s-[4px] last:rounded-e-[4px]'
496
496
  },
497
497
  {
498
498
  isInGroup: true,
499
499
  size: 'sm',
500
- class: 'rounded-none first:rounded-s-md last:rounded-e-md'
500
+ class: 'rounded-none first:rounded-s-[6px] last:rounded-e-[6px]'
501
501
  },
502
502
  {
503
503
  isInGroup: true,
504
504
  size: 'md',
505
- class: 'rounded-none first:rounded-s-md last:rounded-e-md'
505
+ class: 'rounded-none first:rounded-s-[6px] last:rounded-e-[6px]'
506
506
  },
507
507
  {
508
508
  isInGroup: true,
509
509
  size: 'lg',
510
- class: 'rounded-none first:rounded-s-lg last:rounded-e-lg'
510
+ class: 'rounded-none first:rounded-s-[8px] last:rounded-e-[8px]'
511
511
  },
512
512
  {
513
513
  isInGroup: true,
@@ -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.16",
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",
@@ -58,8 +58,8 @@
58
58
  "tailwind-variants": "^3.2.2",
59
59
  "values.js": "^2.1.1",
60
60
  "zustand": "^5.0.8",
61
- "@particle-network/icons": "0.7.0-beta.5",
62
- "@particle-network/ui-shared": "0.5.0"
61
+ "@particle-network/ui-shared": "0.6.0-beta.0",
62
+ "@particle-network/icons": "0.7.0-beta.6"
63
63
  },
64
64
  "scripts": {
65
65
  "build": "rslib build",