aplance-ui 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/registry/components/primitives/popover/popover-context.ts +33 -29
- package/registry/components/primitives/popover/popover.tsx +138 -24
- package/registry/components/ui/copy-button/copy-button.tsx +5 -0
- package/registry/components/ui/data-table/data-table.module.css +743 -710
- package/registry/components/ui/data-table/data-table.tsx +26 -8
- package/registry/components/ui/data-table/filter-modal.tsx +86 -0
- package/registry/components/ui/data-table/types.ts +8 -0
- package/registry/components/ui/data-table/useDataTable.tsx +1 -0
- package/registry/components/ui/data-table/utils.ts +26 -1
- package/registry/components/ui/input-fields/custom-select/custom-select.module.css +11 -0
- package/registry/components/ui/input-fields/custom-select/custom-select.tsx +500 -484
- package/registry/components/ui/input-fields/date-picker/date-picker.tsx +940 -933
- package/registry/components/ui/input-fields/date-picker/types.ts +2 -0
- package/registry/components/ui/input-fields/emoji-picker/emoji-picker.tsx +8 -0
- package/registry/components/ui/tabs/tabs.tsx +29 -16
- package/registry/hooks/useMediaQuery.ts +38 -0
- package/registry/registry.json +22 -2
package/package.json
CHANGED
|
@@ -1,29 +1,33 @@
|
|
|
1
|
-
import { createContext, useContext, type RefObject } from "react";
|
|
2
|
-
import type { Coords, Position, Trigger } from "./popover";
|
|
3
|
-
|
|
4
|
-
export type PopoverContextType = {
|
|
5
|
-
isOpen: boolean;
|
|
6
|
-
setIsOpen: (open: boolean) => void;
|
|
7
|
-
popoverRef: RefObject<HTMLDivElement | null>;
|
|
8
|
-
menuRef: RefObject<HTMLDivElement | null>;
|
|
9
|
-
coords: Coords;
|
|
10
|
-
isMeasured: boolean;
|
|
11
|
-
shouldRender: boolean;
|
|
12
|
-
animationDuration?: number;
|
|
13
|
-
triggerWidth: number;
|
|
14
|
-
trigger: Trigger;
|
|
15
|
-
position: Position;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
export const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
1
|
+
import { createContext, useContext, type RefObject } from "react";
|
|
2
|
+
import type { Coords, Position, Trigger } from "./popover";
|
|
3
|
+
|
|
4
|
+
export type PopoverContextType = {
|
|
5
|
+
isOpen: boolean;
|
|
6
|
+
setIsOpen: (open: boolean) => void;
|
|
7
|
+
popoverRef: RefObject<HTMLDivElement | null>;
|
|
8
|
+
menuRef: RefObject<HTMLDivElement | null>;
|
|
9
|
+
coords: Coords;
|
|
10
|
+
isMeasured: boolean;
|
|
11
|
+
shouldRender: boolean;
|
|
12
|
+
animationDuration?: number;
|
|
13
|
+
triggerWidth: number;
|
|
14
|
+
trigger: Trigger;
|
|
15
|
+
position: Position;
|
|
16
|
+
resolvedPosition: Position;
|
|
17
|
+
availableWidth?: number;
|
|
18
|
+
availableHeight?: number;
|
|
19
|
+
autoPosition: boolean;
|
|
20
|
+
hoverTimeoutRef: RefObject<ReturnType<typeof setTimeout> | null>;
|
|
21
|
+
renderOnPortal: boolean;
|
|
22
|
+
mounted: boolean;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const PopoverContext = createContext<PopoverContextType | undefined>(
|
|
26
|
+
undefined,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
export const usePopover = () => {
|
|
30
|
+
const ctx = useContext(PopoverContext);
|
|
31
|
+
if (!ctx) throw new Error("usePopover must be used within Popover");
|
|
32
|
+
return ctx;
|
|
33
|
+
};
|
|
@@ -26,6 +26,24 @@ export type Coords = {
|
|
|
26
26
|
left: number;
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
+
const DEFAULT_COLLISION_PADDING = 8;
|
|
30
|
+
|
|
31
|
+
const oppositePosition: Record<Position, Position> = {
|
|
32
|
+
top: "bottom",
|
|
33
|
+
bottom: "top",
|
|
34
|
+
left: "right",
|
|
35
|
+
right: "left",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const useIsomorphicLayoutEffect =
|
|
39
|
+
typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
40
|
+
|
|
41
|
+
const isVerticalPosition = (position: Position): boolean =>
|
|
42
|
+
position === "top" || position === "bottom";
|
|
43
|
+
|
|
44
|
+
const clamp = (value: number, min: number, max: number): number =>
|
|
45
|
+
Math.min(Math.max(value, min), Math.max(min, max));
|
|
46
|
+
|
|
29
47
|
// Popover Component
|
|
30
48
|
export type PopoverProps = {
|
|
31
49
|
children?: ReactNode;
|
|
@@ -44,6 +62,10 @@ export type PopoverProps = {
|
|
|
44
62
|
trigger?: Trigger;
|
|
45
63
|
// Useful when using in isolated environments like Iframe
|
|
46
64
|
renderOnPortal?: boolean;
|
|
65
|
+
/** Auto-position the menu so it stays within the viewport. Default: true. */
|
|
66
|
+
autoPosition?: boolean;
|
|
67
|
+
/** Viewport padding used by auto positioning. Default: 8. */
|
|
68
|
+
collisionPadding?: number;
|
|
47
69
|
};
|
|
48
70
|
|
|
49
71
|
export const Popover: FC<PopoverProps> = ({
|
|
@@ -62,6 +84,8 @@ export const Popover: FC<PopoverProps> = ({
|
|
|
62
84
|
onClose,
|
|
63
85
|
trigger = "click",
|
|
64
86
|
renderOnPortal = true,
|
|
87
|
+
autoPosition,
|
|
88
|
+
collisionPadding = DEFAULT_COLLISION_PADDING,
|
|
65
89
|
}) => {
|
|
66
90
|
const readLocationKey = () =>
|
|
67
91
|
`${window.location.pathname}${window.location.search}${window.location.hash}`;
|
|
@@ -84,11 +108,18 @@ export const Popover: FC<PopoverProps> = ({
|
|
|
84
108
|
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
85
109
|
|
|
86
110
|
const mounted = useHasMounted();
|
|
111
|
+
const shouldAutoPosition = autoPosition ?? true;
|
|
87
112
|
|
|
88
113
|
const [coords, setCoords] = useState<Coords>({ top: 0, left: 0 });
|
|
89
114
|
const [shouldRender, setShouldRender] = useState(false);
|
|
90
115
|
const [isMeasured, setIsMeasured] = useState(false);
|
|
91
116
|
const [triggerWidth, setTriggerWidth] = useState(0);
|
|
117
|
+
const [resolvedPosition, setResolvedPosition] =
|
|
118
|
+
useState<Position>(position);
|
|
119
|
+
const [availableSize, setAvailableSize] = useState<{
|
|
120
|
+
width?: number;
|
|
121
|
+
height?: number;
|
|
122
|
+
}>({});
|
|
92
123
|
|
|
93
124
|
// Calculate Menu Position
|
|
94
125
|
const calculatePosition = useCallback(() => {
|
|
@@ -97,21 +128,66 @@ export const Popover: FC<PopoverProps> = ({
|
|
|
97
128
|
const triggerRect = popoverRef.current.getBoundingClientRect();
|
|
98
129
|
const menuRect = menuRef.current.getBoundingClientRect();
|
|
99
130
|
const { innerWidth, innerHeight } = window;
|
|
131
|
+
const padding = Number.isFinite(collisionPadding)
|
|
132
|
+
? Math.max(0, collisionPadding)
|
|
133
|
+
: DEFAULT_COLLISION_PADDING;
|
|
100
134
|
|
|
101
135
|
let top = 0;
|
|
102
136
|
let left = 0;
|
|
137
|
+
let nextPosition = position;
|
|
138
|
+
|
|
139
|
+
const availableByPosition: Record<Position, number> = {
|
|
140
|
+
top: Math.max(0, triggerRect.top - padding - offset),
|
|
141
|
+
bottom: Math.max(0, innerHeight - triggerRect.bottom - padding - offset),
|
|
142
|
+
left: Math.max(0, triggerRect.left - padding - offset),
|
|
143
|
+
right: Math.max(0, innerWidth - triggerRect.right - padding - offset),
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
if (shouldAutoPosition) {
|
|
147
|
+
const preferredSpace = availableByPosition[position];
|
|
148
|
+
const opposite = oppositePosition[position];
|
|
149
|
+
const oppositeSpace = availableByPosition[opposite];
|
|
150
|
+
const requiredSpace = isVerticalPosition(position)
|
|
151
|
+
? menuRect.height
|
|
152
|
+
: menuRect.width;
|
|
153
|
+
|
|
154
|
+
if (preferredSpace < requiredSpace && oppositeSpace > preferredSpace) {
|
|
155
|
+
nextPosition = opposite;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const viewportWidth = Math.max(0, innerWidth - padding * 2);
|
|
160
|
+
const viewportHeight = Math.max(0, innerHeight - padding * 2);
|
|
161
|
+
const availableWidth = shouldAutoPosition
|
|
162
|
+
? isVerticalPosition(nextPosition)
|
|
163
|
+
? viewportWidth
|
|
164
|
+
: availableByPosition[nextPosition]
|
|
165
|
+
: undefined;
|
|
166
|
+
const availableHeight = shouldAutoPosition
|
|
167
|
+
? isVerticalPosition(nextPosition)
|
|
168
|
+
? availableByPosition[nextPosition]
|
|
169
|
+
: viewportHeight
|
|
170
|
+
: undefined;
|
|
171
|
+
const measuredWidth =
|
|
172
|
+
availableWidth === undefined
|
|
173
|
+
? menuRect.width
|
|
174
|
+
: Math.min(menuRect.width, availableWidth);
|
|
175
|
+
const measuredHeight =
|
|
176
|
+
availableHeight === undefined
|
|
177
|
+
? menuRect.height
|
|
178
|
+
: Math.min(menuRect.height, availableHeight);
|
|
103
179
|
|
|
104
180
|
// Main axis positioning (offset applies here)
|
|
105
|
-
switch (
|
|
181
|
+
switch (nextPosition) {
|
|
106
182
|
case "top":
|
|
107
|
-
top = triggerRect.top -
|
|
183
|
+
top = triggerRect.top - measuredHeight - offset;
|
|
108
184
|
|
|
109
185
|
break;
|
|
110
186
|
case "bottom":
|
|
111
187
|
top = triggerRect.bottom + offset;
|
|
112
188
|
break;
|
|
113
189
|
case "left":
|
|
114
|
-
left = triggerRect.left -
|
|
190
|
+
left = triggerRect.left - measuredWidth - offset;
|
|
115
191
|
break;
|
|
116
192
|
case "right":
|
|
117
193
|
left = triggerRect.right + offset;
|
|
@@ -119,37 +195,37 @@ export const Popover: FC<PopoverProps> = ({
|
|
|
119
195
|
}
|
|
120
196
|
|
|
121
197
|
// Cross axis positioning (align applies here)
|
|
122
|
-
switch (
|
|
198
|
+
switch (nextPosition) {
|
|
123
199
|
case "top":
|
|
124
200
|
case "bottom":
|
|
125
201
|
if (align === "start") left = triggerRect.left;
|
|
126
|
-
else if (align === "end") left = triggerRect.right -
|
|
127
|
-
else left = triggerRect.left + (triggerRect.width -
|
|
202
|
+
else if (align === "end") left = triggerRect.right - measuredWidth;
|
|
203
|
+
else left = triggerRect.left + (triggerRect.width - measuredWidth) / 2;
|
|
128
204
|
break;
|
|
129
205
|
case "left":
|
|
130
206
|
case "right":
|
|
131
207
|
if (align === "start") top = triggerRect.top;
|
|
132
|
-
else if (align === "end") top = triggerRect.bottom -
|
|
208
|
+
else if (align === "end") top = triggerRect.bottom - measuredHeight;
|
|
133
209
|
else
|
|
134
|
-
top = triggerRect.top + triggerRect.height / 2 -
|
|
210
|
+
top = triggerRect.top + triggerRect.height / 2 - measuredHeight / 2;
|
|
135
211
|
break;
|
|
136
212
|
}
|
|
137
213
|
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
);
|
|
144
|
-
left = Math.max(
|
|
145
|
-
PADDING,
|
|
146
|
-
Math.min(left, innerWidth - menuRect.width - PADDING),
|
|
147
|
-
);
|
|
214
|
+
// Keep the resolved menu position inside the viewport padding.
|
|
215
|
+
if (shouldAutoPosition) {
|
|
216
|
+
top = clamp(top, padding, innerHeight - measuredHeight - padding);
|
|
217
|
+
left = clamp(left, padding, innerWidth - measuredWidth - padding);
|
|
218
|
+
}
|
|
148
219
|
|
|
149
220
|
setCoords({ top, left });
|
|
150
221
|
setTriggerWidth(triggerRect.width);
|
|
222
|
+
setResolvedPosition(nextPosition);
|
|
223
|
+
setAvailableSize({
|
|
224
|
+
width: availableWidth,
|
|
225
|
+
height: availableHeight,
|
|
226
|
+
});
|
|
151
227
|
setIsMeasured(true);
|
|
152
|
-
}, [position, align, offset]);
|
|
228
|
+
}, [position, align, offset, shouldAutoPosition, collisionPadding]);
|
|
153
229
|
|
|
154
230
|
// Open / Close Handling
|
|
155
231
|
// Visibility (opacity / pointer-events) is derived from `isOpen && isMeasured`
|
|
@@ -170,12 +246,17 @@ export const Popover: FC<PopoverProps> = ({
|
|
|
170
246
|
|
|
171
247
|
// Measure after menu is rendered. Cleanup resets isMeasured so the next mount
|
|
172
248
|
// re-runs the entry animation from a known starting state.
|
|
173
|
-
|
|
249
|
+
useIsomorphicLayoutEffect(() => {
|
|
174
250
|
if (shouldRender && menuRef.current) {
|
|
175
251
|
calculatePosition();
|
|
176
252
|
|
|
253
|
+
if (typeof ResizeObserver === "undefined") {
|
|
254
|
+
return () => setIsMeasured(false);
|
|
255
|
+
}
|
|
256
|
+
|
|
177
257
|
const observer = new ResizeObserver(() => calculatePosition());
|
|
178
258
|
observer.observe(menuRef.current);
|
|
259
|
+
if (popoverRef.current) observer.observe(popoverRef.current);
|
|
179
260
|
return () => {
|
|
180
261
|
observer.disconnect();
|
|
181
262
|
setIsMeasured(false);
|
|
@@ -268,6 +349,10 @@ export const Popover: FC<PopoverProps> = ({
|
|
|
268
349
|
hoverTimeoutRef,
|
|
269
350
|
renderOnPortal,
|
|
270
351
|
position,
|
|
352
|
+
resolvedPosition,
|
|
353
|
+
availableWidth: availableSize.width,
|
|
354
|
+
availableHeight: availableSize.height,
|
|
355
|
+
autoPosition: shouldAutoPosition,
|
|
271
356
|
mounted,
|
|
272
357
|
}}
|
|
273
358
|
>
|
|
@@ -372,6 +457,11 @@ export type PopoverMenuProps = {
|
|
|
372
457
|
matchTriggerWidth?: boolean;
|
|
373
458
|
};
|
|
374
459
|
|
|
460
|
+
type PopoverMenuStyle = CSSProperties & {
|
|
461
|
+
"--popover-available-width"?: string;
|
|
462
|
+
"--popover-available-height"?: string;
|
|
463
|
+
};
|
|
464
|
+
|
|
375
465
|
export const PopoverMenu: FC<PopoverMenuProps> = ({
|
|
376
466
|
children,
|
|
377
467
|
className,
|
|
@@ -388,10 +478,13 @@ export const PopoverMenu: FC<PopoverMenuProps> = ({
|
|
|
388
478
|
animationDuration,
|
|
389
479
|
trigger,
|
|
390
480
|
setIsOpen,
|
|
391
|
-
|
|
481
|
+
resolvedPosition,
|
|
392
482
|
hoverTimeoutRef,
|
|
393
483
|
renderOnPortal,
|
|
394
484
|
mounted,
|
|
485
|
+
availableWidth,
|
|
486
|
+
availableHeight,
|
|
487
|
+
autoPosition,
|
|
395
488
|
} = usePopover();
|
|
396
489
|
|
|
397
490
|
const handleMouseEnter = () => {
|
|
@@ -439,14 +532,34 @@ export const PopoverMenu: FC<PopoverMenuProps> = ({
|
|
|
439
532
|
return transforms[position];
|
|
440
533
|
};
|
|
441
534
|
|
|
442
|
-
const
|
|
535
|
+
const constrainedMaxWidth =
|
|
536
|
+
autoPosition && availableWidth !== undefined
|
|
537
|
+
? matchTriggerWidth && triggerWidth > 0
|
|
538
|
+
? Math.min(availableWidth, triggerWidth)
|
|
539
|
+
: availableWidth
|
|
540
|
+
: matchTriggerWidth && triggerWidth > 0
|
|
541
|
+
? triggerWidth
|
|
542
|
+
: undefined;
|
|
543
|
+
|
|
544
|
+
const menuStyles: PopoverMenuStyle = {
|
|
443
545
|
position: "fixed",
|
|
444
546
|
top: coords.top,
|
|
445
547
|
left: coords.left,
|
|
446
548
|
zIndex: "var(--z-index-popover)",
|
|
447
|
-
...(
|
|
549
|
+
...(constrainedMaxWidth !== undefined && { maxWidth: constrainedMaxWidth }),
|
|
550
|
+
...(autoPosition &&
|
|
551
|
+
availableHeight !== undefined && { maxHeight: availableHeight }),
|
|
552
|
+
...(autoPosition &&
|
|
553
|
+
availableWidth !== undefined && {
|
|
554
|
+
"--popover-available-width": `${availableWidth}px`,
|
|
555
|
+
}),
|
|
556
|
+
...(autoPosition &&
|
|
557
|
+
availableHeight !== undefined && {
|
|
558
|
+
"--popover-available-height": `${availableHeight}px`,
|
|
559
|
+
}),
|
|
560
|
+
...(autoPosition && { overflow: "auto" }),
|
|
448
561
|
opacity: isVisible ? 1 : 0,
|
|
449
|
-
transform: getTransform(
|
|
562
|
+
transform: getTransform(resolvedPosition, isMeasured, isOpen),
|
|
450
563
|
transition: animationDuration
|
|
451
564
|
? `opacity ${animationDuration}ms cubic-bezier(0.4, 0, 0.2, 1), transform ${animationDuration}ms cubic-bezier(0.4, 0, 0.2, 1)`
|
|
452
565
|
: "opacity var(--transition-base), transform var(--transition-base)",
|
|
@@ -464,6 +577,7 @@ export const PopoverMenu: FC<PopoverMenuProps> = ({
|
|
|
464
577
|
// `useFocusOutside({ ignoreSelector })` so clicking a menu option
|
|
465
578
|
// doesn't dismiss the host.
|
|
466
579
|
data-popover-menu=""
|
|
580
|
+
data-position={resolvedPosition}
|
|
467
581
|
onMouseEnter={handleMouseEnter}
|
|
468
582
|
onMouseLeave={handleMouseLeave}
|
|
469
583
|
style={menuStyles}
|
|
@@ -18,6 +18,9 @@ export type CopyButtonProps = {
|
|
|
18
18
|
layout?: ButtonLayout;
|
|
19
19
|
preview?: CopyPreview;
|
|
20
20
|
previewMaxLength?: number;
|
|
21
|
+
/** Called with the value after a successful copy (e.g. to toast, or clear a
|
|
22
|
+
* one-time reveal). Not called if the copy fails. */
|
|
23
|
+
onCopy?: (value: string) => void;
|
|
21
24
|
};
|
|
22
25
|
|
|
23
26
|
const maskToken = (raw: string): string => {
|
|
@@ -44,6 +47,7 @@ export function CopyButton({
|
|
|
44
47
|
layout,
|
|
45
48
|
preview = "none",
|
|
46
49
|
previewMaxLength = 24,
|
|
50
|
+
onCopy,
|
|
47
51
|
}: CopyButtonProps) {
|
|
48
52
|
const [copied, setCopied] = useState(false);
|
|
49
53
|
const timeoutRef = useRef<number | null>(null);
|
|
@@ -82,6 +86,7 @@ export function CopyButton({
|
|
|
82
86
|
const success = await copyWithFallback(value);
|
|
83
87
|
if (!success) return;
|
|
84
88
|
|
|
89
|
+
onCopy?.(value);
|
|
85
90
|
setCopied(true);
|
|
86
91
|
if (timeoutRef.current !== null) {
|
|
87
92
|
window.clearTimeout(timeoutRef.current);
|