aplance-ui 1.2.2 → 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/modal/modal.tsx +16 -10
- package/registry/components/primitives/popover/popover-context.ts +33 -28
- package/registry/components/primitives/popover/popover.tsx +145 -26
- package/registry/components/primitives/slider/slider.module.css +62 -5
- package/registry/components/primitives/slider/slider.tsx +192 -34
- package/registry/components/primitives/slider/types.ts +56 -3
- package/registry/components/primitives/slider/utils.ts +9 -0
- package/registry/components/ui/copy-button/copy-button.tsx +5 -0
- package/registry/components/ui/data-table/data-table.module.css +695 -613
- package/registry/components/ui/data-table/data-table.tsx +136 -112
- 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 +32 -1
- package/registry/components/ui/input-fields/custom-select/custom-select.tsx +500 -469
- 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/input-fields/input/input.tsx +80 -69
- package/registry/components/ui/tabs/tabs.module.css +22 -8
- package/registry/components/ui/tabs/tabs.tsx +29 -12
- package/registry/hooks/useHasMounted.ts +22 -0
- package/registry/hooks/useMediaQuery.ts +38 -0
- package/registry/registry.json +41 -4
package/package.json
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
type ButtonLayout,
|
|
23
23
|
} from "../../ui/button/button";
|
|
24
24
|
import { useFocusOutside } from "../../../hooks/useFocusOutside";
|
|
25
|
+
import { useHasMounted } from "../../../hooks/useHasMounted";
|
|
25
26
|
import { ModalContext, useModal } from "./modal-context";
|
|
26
27
|
import s from "./modal.module.css";
|
|
27
28
|
import { Icon } from "../icon/icon";
|
|
@@ -138,14 +139,14 @@ export type ModalProps = {
|
|
|
138
139
|
*/
|
|
139
140
|
placement?: ModalPlacement;
|
|
140
141
|
/**
|
|
141
|
-
* Placement override below the `--md` breakpoint (900px
|
|
142
|
+
* Placement override below the `--md` breakpoint (≤ 900px).
|
|
142
143
|
* Defaults to `bottom` so modals become bottom sheets on phones.
|
|
143
144
|
* Pass the same value as `placement` (or any placement) to opt out.
|
|
144
145
|
*/
|
|
145
146
|
mobilePlacement?: ModalPlacement;
|
|
146
147
|
/**
|
|
147
148
|
* Entrance/exit animation. Defaults based on `placement`:
|
|
148
|
-
* center
|
|
149
|
+
* center→scale, top/bottom/left/right→slide from that edge, full→fade.
|
|
149
150
|
*/
|
|
150
151
|
animation?: ModalAnimation;
|
|
151
152
|
scrollBehavior?: ModalScrollBehavior;
|
|
@@ -201,6 +202,8 @@ export const Modal: FC<ModalProps> = ({
|
|
|
201
202
|
// Always starts false so the element is in the DOM before the transition fires
|
|
202
203
|
const [isVisible, setIsVisible] = useState(false);
|
|
203
204
|
|
|
205
|
+
const mounted = useHasMounted();
|
|
206
|
+
|
|
204
207
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
205
208
|
const modalId = useId();
|
|
206
209
|
|
|
@@ -243,8 +246,8 @@ export const Modal: FC<ModalProps> = ({
|
|
|
243
246
|
const close = useCallback(() => setIsOpen(false), [setIsOpen]);
|
|
244
247
|
const toggle = useCallback(() => setIsOpen(!isOpen), [isOpen, setIsOpen]);
|
|
245
248
|
|
|
246
|
-
// Open:
|
|
247
|
-
// Close: setIsVisible(false)
|
|
249
|
+
// Open: setShouldRender(true) → node mounts → useLayoutEffect below fires → isVisible=true → open transition
|
|
250
|
+
// Close: setIsVisible(false) immediately → close transition plays → setTimeout unmounts after duration
|
|
248
251
|
useEffect(() => {
|
|
249
252
|
if (isOpen) {
|
|
250
253
|
setShouldRender(true);
|
|
@@ -263,12 +266,12 @@ export const Modal: FC<ModalProps> = ({
|
|
|
263
266
|
// registers the closed CSS state before we flip to open - same trick
|
|
264
267
|
// Popover uses with isMeasured + ResizeObserver.
|
|
265
268
|
useLayoutEffect(() => {
|
|
266
|
-
if (shouldRender && contentRef.current) {
|
|
269
|
+
if (shouldRender && mounted && contentRef.current) {
|
|
267
270
|
void contentRef.current.offsetHeight; // flush reflow
|
|
268
271
|
setIsVisible(isOpen);
|
|
269
272
|
}
|
|
270
273
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
271
|
-
}, [shouldRender]); // only run when node mounts
|
|
274
|
+
}, [shouldRender, mounted]); // only run when node mounts or mounted state changes
|
|
272
275
|
|
|
273
276
|
useEffect(() => {
|
|
274
277
|
if (isOpen) onOpenRef.current?.();
|
|
@@ -328,15 +331,14 @@ export const Modal: FC<ModalProps> = ({
|
|
|
328
331
|
};
|
|
329
332
|
el.addEventListener("keydown", trap);
|
|
330
333
|
return () => el.removeEventListener("keydown", trap);
|
|
331
|
-
}, [isOpen]);
|
|
334
|
+
}, [isOpen, mounted]);
|
|
332
335
|
|
|
333
336
|
// Close when a click lands outside the dialog panel - mirrors Popover's
|
|
334
337
|
// outside-click pattern so the component family stays consistent.
|
|
335
338
|
// `ignoreSelector` exempts portaled popover menus (CustomSelect, DatePicker,
|
|
336
339
|
// RowActionsPopover, etc.): their menus portal to document.body so they're
|
|
337
340
|
// outside `contentRef`, but a click on a dropdown option must NOT dismiss
|
|
338
|
-
// the dialog it was opened from.
|
|
339
|
-
// Backdrop / outside click - gated on being the topmost modal so a click
|
|
341
|
+
// the dialog it was opened from. Gated on being the topmost modal so a click
|
|
340
342
|
// that dismisses a child modal doesn't bubble into closing its parent.
|
|
341
343
|
useFocusOutside(
|
|
342
344
|
[contentRef],
|
|
@@ -392,7 +394,11 @@ export const Modal: FC<ModalProps> = ({
|
|
|
392
394
|
) : null;
|
|
393
395
|
|
|
394
396
|
const portal =
|
|
395
|
-
renderOnPortal && dialog
|
|
397
|
+
renderOnPortal && dialog
|
|
398
|
+
? mounted
|
|
399
|
+
? createPortal(dialog, document.body)
|
|
400
|
+
: null
|
|
401
|
+
: dialog;
|
|
396
402
|
|
|
397
403
|
// Hoisted triggers sit outside the portal so they stay visible while closed;
|
|
398
404
|
// dialog content lives inside the portal.
|
|
@@ -1,28 +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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
+
};
|
|
@@ -14,6 +14,7 @@ import { createPortal } from "react-dom";
|
|
|
14
14
|
import s from "./popover.module.css";
|
|
15
15
|
import { PopoverContext, usePopover } from "./popover-context";
|
|
16
16
|
import { useFocusOutside } from "../../../hooks/useFocusOutside";
|
|
17
|
+
import { useHasMounted } from "../../../hooks/useHasMounted";
|
|
17
18
|
|
|
18
19
|
// Types
|
|
19
20
|
export type Position = "top" | "bottom" | "left" | "right";
|
|
@@ -25,6 +26,24 @@ export type Coords = {
|
|
|
25
26
|
left: number;
|
|
26
27
|
};
|
|
27
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
|
+
|
|
28
47
|
// Popover Component
|
|
29
48
|
export type PopoverProps = {
|
|
30
49
|
children?: ReactNode;
|
|
@@ -43,6 +62,10 @@ export type PopoverProps = {
|
|
|
43
62
|
trigger?: Trigger;
|
|
44
63
|
// Useful when using in isolated environments like Iframe
|
|
45
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;
|
|
46
69
|
};
|
|
47
70
|
|
|
48
71
|
export const Popover: FC<PopoverProps> = ({
|
|
@@ -61,6 +84,8 @@ export const Popover: FC<PopoverProps> = ({
|
|
|
61
84
|
onClose,
|
|
62
85
|
trigger = "click",
|
|
63
86
|
renderOnPortal = true,
|
|
87
|
+
autoPosition,
|
|
88
|
+
collisionPadding = DEFAULT_COLLISION_PADDING,
|
|
64
89
|
}) => {
|
|
65
90
|
const readLocationKey = () =>
|
|
66
91
|
`${window.location.pathname}${window.location.search}${window.location.hash}`;
|
|
@@ -82,10 +107,19 @@ export const Popover: FC<PopoverProps> = ({
|
|
|
82
107
|
// For hover trigger
|
|
83
108
|
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
84
109
|
|
|
110
|
+
const mounted = useHasMounted();
|
|
111
|
+
const shouldAutoPosition = autoPosition ?? true;
|
|
112
|
+
|
|
85
113
|
const [coords, setCoords] = useState<Coords>({ top: 0, left: 0 });
|
|
86
114
|
const [shouldRender, setShouldRender] = useState(false);
|
|
87
115
|
const [isMeasured, setIsMeasured] = useState(false);
|
|
88
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
|
+
}>({});
|
|
89
123
|
|
|
90
124
|
// Calculate Menu Position
|
|
91
125
|
const calculatePosition = useCallback(() => {
|
|
@@ -94,21 +128,66 @@ export const Popover: FC<PopoverProps> = ({
|
|
|
94
128
|
const triggerRect = popoverRef.current.getBoundingClientRect();
|
|
95
129
|
const menuRect = menuRef.current.getBoundingClientRect();
|
|
96
130
|
const { innerWidth, innerHeight } = window;
|
|
131
|
+
const padding = Number.isFinite(collisionPadding)
|
|
132
|
+
? Math.max(0, collisionPadding)
|
|
133
|
+
: DEFAULT_COLLISION_PADDING;
|
|
97
134
|
|
|
98
135
|
let top = 0;
|
|
99
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);
|
|
100
179
|
|
|
101
180
|
// Main axis positioning (offset applies here)
|
|
102
|
-
switch (
|
|
181
|
+
switch (nextPosition) {
|
|
103
182
|
case "top":
|
|
104
|
-
top = triggerRect.top -
|
|
183
|
+
top = triggerRect.top - measuredHeight - offset;
|
|
105
184
|
|
|
106
185
|
break;
|
|
107
186
|
case "bottom":
|
|
108
187
|
top = triggerRect.bottom + offset;
|
|
109
188
|
break;
|
|
110
189
|
case "left":
|
|
111
|
-
left = triggerRect.left -
|
|
190
|
+
left = triggerRect.left - measuredWidth - offset;
|
|
112
191
|
break;
|
|
113
192
|
case "right":
|
|
114
193
|
left = triggerRect.right + offset;
|
|
@@ -116,37 +195,37 @@ export const Popover: FC<PopoverProps> = ({
|
|
|
116
195
|
}
|
|
117
196
|
|
|
118
197
|
// Cross axis positioning (align applies here)
|
|
119
|
-
switch (
|
|
198
|
+
switch (nextPosition) {
|
|
120
199
|
case "top":
|
|
121
200
|
case "bottom":
|
|
122
201
|
if (align === "start") left = triggerRect.left;
|
|
123
|
-
else if (align === "end") left = triggerRect.right -
|
|
124
|
-
else left = triggerRect.left + (triggerRect.width -
|
|
202
|
+
else if (align === "end") left = triggerRect.right - measuredWidth;
|
|
203
|
+
else left = triggerRect.left + (triggerRect.width - measuredWidth) / 2;
|
|
125
204
|
break;
|
|
126
205
|
case "left":
|
|
127
206
|
case "right":
|
|
128
207
|
if (align === "start") top = triggerRect.top;
|
|
129
|
-
else if (align === "end") top = triggerRect.bottom -
|
|
208
|
+
else if (align === "end") top = triggerRect.bottom - measuredHeight;
|
|
130
209
|
else
|
|
131
|
-
top = triggerRect.top + triggerRect.height / 2 -
|
|
210
|
+
top = triggerRect.top + triggerRect.height / 2 - measuredHeight / 2;
|
|
132
211
|
break;
|
|
133
212
|
}
|
|
134
213
|
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
);
|
|
141
|
-
left = Math.max(
|
|
142
|
-
PADDING,
|
|
143
|
-
Math.min(left, innerWidth - menuRect.width - PADDING),
|
|
144
|
-
);
|
|
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
|
+
}
|
|
145
219
|
|
|
146
220
|
setCoords({ top, left });
|
|
147
221
|
setTriggerWidth(triggerRect.width);
|
|
222
|
+
setResolvedPosition(nextPosition);
|
|
223
|
+
setAvailableSize({
|
|
224
|
+
width: availableWidth,
|
|
225
|
+
height: availableHeight,
|
|
226
|
+
});
|
|
148
227
|
setIsMeasured(true);
|
|
149
|
-
}, [position, align, offset]);
|
|
228
|
+
}, [position, align, offset, shouldAutoPosition, collisionPadding]);
|
|
150
229
|
|
|
151
230
|
// Open / Close Handling
|
|
152
231
|
// Visibility (opacity / pointer-events) is derived from `isOpen && isMeasured`
|
|
@@ -167,18 +246,23 @@ export const Popover: FC<PopoverProps> = ({
|
|
|
167
246
|
|
|
168
247
|
// Measure after menu is rendered. Cleanup resets isMeasured so the next mount
|
|
169
248
|
// re-runs the entry animation from a known starting state.
|
|
170
|
-
|
|
249
|
+
useIsomorphicLayoutEffect(() => {
|
|
171
250
|
if (shouldRender && menuRef.current) {
|
|
172
251
|
calculatePosition();
|
|
173
252
|
|
|
253
|
+
if (typeof ResizeObserver === "undefined") {
|
|
254
|
+
return () => setIsMeasured(false);
|
|
255
|
+
}
|
|
256
|
+
|
|
174
257
|
const observer = new ResizeObserver(() => calculatePosition());
|
|
175
258
|
observer.observe(menuRef.current);
|
|
259
|
+
if (popoverRef.current) observer.observe(popoverRef.current);
|
|
176
260
|
return () => {
|
|
177
261
|
observer.disconnect();
|
|
178
262
|
setIsMeasured(false);
|
|
179
263
|
};
|
|
180
264
|
}
|
|
181
|
-
}, [shouldRender, calculatePosition]);
|
|
265
|
+
}, [shouldRender, mounted, calculatePosition]);
|
|
182
266
|
|
|
183
267
|
// Clear any pending hover-close timer when the Popover unmounts so the
|
|
184
268
|
// delayed setIsOpen never fires on a stale instance.
|
|
@@ -265,6 +349,11 @@ export const Popover: FC<PopoverProps> = ({
|
|
|
265
349
|
hoverTimeoutRef,
|
|
266
350
|
renderOnPortal,
|
|
267
351
|
position,
|
|
352
|
+
resolvedPosition,
|
|
353
|
+
availableWidth: availableSize.width,
|
|
354
|
+
availableHeight: availableSize.height,
|
|
355
|
+
autoPosition: shouldAutoPosition,
|
|
356
|
+
mounted,
|
|
268
357
|
}}
|
|
269
358
|
>
|
|
270
359
|
<div
|
|
@@ -368,6 +457,11 @@ export type PopoverMenuProps = {
|
|
|
368
457
|
matchTriggerWidth?: boolean;
|
|
369
458
|
};
|
|
370
459
|
|
|
460
|
+
type PopoverMenuStyle = CSSProperties & {
|
|
461
|
+
"--popover-available-width"?: string;
|
|
462
|
+
"--popover-available-height"?: string;
|
|
463
|
+
};
|
|
464
|
+
|
|
371
465
|
export const PopoverMenu: FC<PopoverMenuProps> = ({
|
|
372
466
|
children,
|
|
373
467
|
className,
|
|
@@ -384,9 +478,13 @@ export const PopoverMenu: FC<PopoverMenuProps> = ({
|
|
|
384
478
|
animationDuration,
|
|
385
479
|
trigger,
|
|
386
480
|
setIsOpen,
|
|
387
|
-
|
|
481
|
+
resolvedPosition,
|
|
388
482
|
hoverTimeoutRef,
|
|
389
483
|
renderOnPortal,
|
|
484
|
+
mounted,
|
|
485
|
+
availableWidth,
|
|
486
|
+
availableHeight,
|
|
487
|
+
autoPosition,
|
|
390
488
|
} = usePopover();
|
|
391
489
|
|
|
392
490
|
const handleMouseEnter = () => {
|
|
@@ -434,14 +532,34 @@ export const PopoverMenu: FC<PopoverMenuProps> = ({
|
|
|
434
532
|
return transforms[position];
|
|
435
533
|
};
|
|
436
534
|
|
|
437
|
-
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 = {
|
|
438
545
|
position: "fixed",
|
|
439
546
|
top: coords.top,
|
|
440
547
|
left: coords.left,
|
|
441
548
|
zIndex: "var(--z-index-popover)",
|
|
442
|
-
...(
|
|
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" }),
|
|
443
561
|
opacity: isVisible ? 1 : 0,
|
|
444
|
-
transform: getTransform(
|
|
562
|
+
transform: getTransform(resolvedPosition, isMeasured, isOpen),
|
|
445
563
|
transition: animationDuration
|
|
446
564
|
? `opacity ${animationDuration}ms cubic-bezier(0.4, 0, 0.2, 1), transform ${animationDuration}ms cubic-bezier(0.4, 0, 0.2, 1)`
|
|
447
565
|
: "opacity var(--transition-base), transform var(--transition-base)",
|
|
@@ -459,6 +577,7 @@ export const PopoverMenu: FC<PopoverMenuProps> = ({
|
|
|
459
577
|
// `useFocusOutside({ ignoreSelector })` so clicking a menu option
|
|
460
578
|
// doesn't dismiss the host.
|
|
461
579
|
data-popover-menu=""
|
|
580
|
+
data-position={resolvedPosition}
|
|
462
581
|
onMouseEnter={handleMouseEnter}
|
|
463
582
|
onMouseLeave={handleMouseLeave}
|
|
464
583
|
style={menuStyles}
|
|
@@ -467,5 +586,5 @@ export const PopoverMenu: FC<PopoverMenuProps> = ({
|
|
|
467
586
|
</div>
|
|
468
587
|
);
|
|
469
588
|
|
|
470
|
-
return renderOnPortal ? createPortal(menu, document.body) : menu;
|
|
589
|
+
return renderOnPortal ? (mounted ? createPortal(menu, document.body) : null) : menu;
|
|
471
590
|
};
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
─────────────────────────────────────────────────────────────────────────── */
|
|
5
5
|
|
|
6
6
|
.container {
|
|
7
|
-
--slider-nav-size: var(--spacing-
|
|
7
|
+
--slider-nav-size: var(--spacing-11);
|
|
8
8
|
--slider-nav-bg: color-mix(in srgb, var(--color-foreground) 56%, transparent);
|
|
9
9
|
--slider-nav-bg-hover: color-mix(in srgb, var(--color-foreground) 78%, transparent);
|
|
10
10
|
--slider-nav-fg: var(--color-background);
|
|
11
|
-
--slider-dot-size: var(--spacing-
|
|
11
|
+
--slider-dot-size: var(--spacing-5);
|
|
12
12
|
--slider-dot-bg: color-mix(in srgb, var(--color-foreground) 28%, transparent);
|
|
13
13
|
--slider-dot-bg-active: var(--color-primary);
|
|
14
14
|
|
|
@@ -39,6 +39,10 @@
|
|
|
39
39
|
transition: height var(--transition-base);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
.viewport_coverflow {
|
|
43
|
+
perspective: 72rem;
|
|
44
|
+
}
|
|
45
|
+
|
|
42
46
|
.track {
|
|
43
47
|
display: flex;
|
|
44
48
|
min-width: 0;
|
|
@@ -48,11 +52,21 @@
|
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
.track_fade,
|
|
51
|
-
.track_scale
|
|
55
|
+
.track_scale,
|
|
56
|
+
.track_flip {
|
|
52
57
|
display: grid;
|
|
53
58
|
transform: none;
|
|
54
59
|
}
|
|
55
60
|
|
|
61
|
+
.track_flip {
|
|
62
|
+
perspective: 72rem;
|
|
63
|
+
transform-style: preserve-3d;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.track_coverflow {
|
|
67
|
+
transform-style: preserve-3d;
|
|
68
|
+
}
|
|
69
|
+
|
|
56
70
|
.slide {
|
|
57
71
|
min-width: 0;
|
|
58
72
|
flex-grow: 0;
|
|
@@ -75,14 +89,45 @@
|
|
|
75
89
|
}
|
|
76
90
|
}
|
|
77
91
|
|
|
92
|
+
.slide_cards,
|
|
93
|
+
.slide_coverflow {
|
|
94
|
+
will-change: transform, opacity;
|
|
95
|
+
transition:
|
|
96
|
+
opacity var(--slider-transition-duration) var(--slider-transition-easing),
|
|
97
|
+
transform var(--slider-transition-duration) var(--slider-transition-easing);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.slide_cards {
|
|
101
|
+
transform:
|
|
102
|
+
translateY(var(--slider-slide-offset))
|
|
103
|
+
scale(var(--slider-slide-scale));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.slide_coverflow {
|
|
107
|
+
transform:
|
|
108
|
+
translateY(var(--slider-slide-offset))
|
|
109
|
+
translateZ(var(--slider-slide-depth))
|
|
110
|
+
rotateY(var(--slider-slide-rotate))
|
|
111
|
+
scale(var(--slider-slide-scale));
|
|
112
|
+
transform-style: preserve-3d;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.container_dragging,
|
|
116
|
+
.container_no_transition {
|
|
117
|
+
.slide_cards,
|
|
118
|
+
.slide_coverflow {
|
|
119
|
+
transition: none;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
78
123
|
.slide_overlay {
|
|
79
124
|
grid-area: 1 / 1;
|
|
80
125
|
width: 100%;
|
|
81
126
|
opacity: 0;
|
|
82
127
|
pointer-events: none;
|
|
83
128
|
transition:
|
|
84
|
-
opacity var(--transition-
|
|
85
|
-
transform var(--transition-
|
|
129
|
+
opacity var(--slider-transition-duration) var(--slider-transition-easing),
|
|
130
|
+
transform var(--slider-transition-duration) var(--slider-transition-easing);
|
|
86
131
|
}
|
|
87
132
|
|
|
88
133
|
.track_fade .slide_overlay {
|
|
@@ -93,12 +138,22 @@
|
|
|
93
138
|
transform: scale(0.96);
|
|
94
139
|
}
|
|
95
140
|
|
|
141
|
+
.track_flip .slide_overlay {
|
|
142
|
+
backface-visibility: hidden;
|
|
143
|
+
transform: rotateY(90deg) scale(0.96);
|
|
144
|
+
transform-style: preserve-3d;
|
|
145
|
+
}
|
|
146
|
+
|
|
96
147
|
.slide_active {
|
|
97
148
|
opacity: 1;
|
|
98
149
|
pointer-events: auto;
|
|
99
150
|
transform: scale(1);
|
|
100
151
|
}
|
|
101
152
|
|
|
153
|
+
.slide_overlay.slide_active {
|
|
154
|
+
transform: scale(1);
|
|
155
|
+
}
|
|
156
|
+
|
|
102
157
|
/* ─── Built-in nav arrows ───────────────────────────────────────────────────
|
|
103
158
|
`data-slider-no-drag` is set in TSX so `dragIgnoreSelector` does not start
|
|
104
159
|
a track drag when the user clicks the chevron.
|
|
@@ -294,6 +349,8 @@
|
|
|
294
349
|
@media (--motion-reduce) {
|
|
295
350
|
.track,
|
|
296
351
|
.slide_overlay,
|
|
352
|
+
.slide_cards,
|
|
353
|
+
.slide_coverflow,
|
|
297
354
|
.progress_bar {
|
|
298
355
|
transition: none;
|
|
299
356
|
}
|