aplance-ui 1.2.1 → 1.3.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 +52 -13
- package/registry/components/primitives/popover/popover-context.ts +1 -0
- package/registry/components/primitives/popover/popover.tsx +13 -2
- 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/data-table/data-table.module.css +124 -6
- package/registry/components/ui/data-table/data-table.tsx +176 -88
- package/registry/components/ui/data-table/pagination-bar.tsx +174 -71
- package/registry/components/ui/data-table/types.ts +12 -3
- package/registry/components/ui/input-fields/common/input-field-native.module.css +52 -42
- package/registry/components/ui/input-fields/custom-select/custom-select.module.css +21 -1
- package/registry/components/ui/input-fields/custom-select/custom-select.tsx +90 -9
- package/registry/components/ui/input-fields/input/input.tsx +58 -29
- package/registry/components/ui/input-fields/media-input/media-input.module.css +29 -0
- package/registry/components/ui/input-fields/media-input/media-preview.tsx +25 -7
- package/registry/components/ui/tabs/tabs.module.css +22 -8
- package/registry/components/ui/tabs/tabs.tsx +5 -1
- package/registry/hooks/useFocusOutside.tsx +21 -6
- package/registry/hooks/useHasMounted.ts +22 -0
- package/registry/registry.json +21 -3
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";
|
|
@@ -86,6 +87,20 @@ const releaseScrollLock = () => {
|
|
|
86
87
|
}
|
|
87
88
|
};
|
|
88
89
|
|
|
90
|
+
// Module-level stack of open modal ids. Only the topmost modal reacts to
|
|
91
|
+
// Escape / backdrop-click - without this, dismissing a child modal (media
|
|
92
|
+
// picker, upload, etc.) also closes the parent it was opened from.
|
|
93
|
+
const _modalStack: string[] = [];
|
|
94
|
+
const pushModal = (id: string) => {
|
|
95
|
+
_modalStack.push(id);
|
|
96
|
+
};
|
|
97
|
+
const popModal = (id: string) => {
|
|
98
|
+
const index = _modalStack.lastIndexOf(id);
|
|
99
|
+
if (index >= 0) _modalStack.splice(index, 1);
|
|
100
|
+
};
|
|
101
|
+
const isTopModal = (id: string): boolean =>
|
|
102
|
+
_modalStack[_modalStack.length - 1] === id;
|
|
103
|
+
|
|
89
104
|
// ─── Shared button passthrough ────────────────────────────────────────────────
|
|
90
105
|
|
|
91
106
|
export type ButtonPassthroughProps = {
|
|
@@ -187,6 +202,8 @@ export const Modal: FC<ModalProps> = ({
|
|
|
187
202
|
// Always starts false so the element is in the DOM before the transition fires
|
|
188
203
|
const [isVisible, setIsVisible] = useState(false);
|
|
189
204
|
|
|
205
|
+
const mounted = useHasMounted();
|
|
206
|
+
|
|
190
207
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
191
208
|
const modalId = useId();
|
|
192
209
|
|
|
@@ -249,33 +266,39 @@ export const Modal: FC<ModalProps> = ({
|
|
|
249
266
|
// registers the closed CSS state before we flip to open - same trick
|
|
250
267
|
// Popover uses with isMeasured + ResizeObserver.
|
|
251
268
|
useLayoutEffect(() => {
|
|
252
|
-
if (shouldRender && contentRef.current) {
|
|
269
|
+
if (shouldRender && mounted && contentRef.current) {
|
|
253
270
|
void contentRef.current.offsetHeight; // flush reflow
|
|
254
271
|
setIsVisible(isOpen);
|
|
255
272
|
}
|
|
256
273
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
257
|
-
}, [shouldRender]); // only run when node mounts
|
|
274
|
+
}, [shouldRender, mounted]); // only run when node mounts or mounted state changes
|
|
258
275
|
|
|
259
276
|
useEffect(() => {
|
|
260
277
|
if (isOpen) onOpenRef.current?.();
|
|
261
278
|
}, [isOpen]);
|
|
262
279
|
|
|
263
|
-
// Body scroll lock - reference-counted
|
|
280
|
+
// Body scroll lock + open-modal stack registration (both reference-counted
|
|
281
|
+
// across stacked modals).
|
|
264
282
|
useEffect(() => {
|
|
265
283
|
if (!isOpen) return;
|
|
266
284
|
acquireScrollLock();
|
|
267
|
-
|
|
268
|
-
|
|
285
|
+
pushModal(modalId);
|
|
286
|
+
return () => {
|
|
287
|
+
releaseScrollLock();
|
|
288
|
+
popModal(modalId);
|
|
289
|
+
};
|
|
290
|
+
}, [isOpen, modalId]);
|
|
269
291
|
|
|
270
|
-
// ESC key
|
|
292
|
+
// ESC key - only the topmost modal closes, so Escape inside a child modal
|
|
293
|
+
// doesn't also dismiss its parent.
|
|
271
294
|
useEffect(() => {
|
|
272
295
|
if (!closeOnEsc || !isOpen) return;
|
|
273
296
|
const handler = (e: globalThis.KeyboardEvent) => {
|
|
274
|
-
if (e.key === "Escape") close();
|
|
297
|
+
if (e.key === "Escape" && isTopModal(modalId)) close();
|
|
275
298
|
};
|
|
276
299
|
window.addEventListener("keydown", handler);
|
|
277
300
|
return () => window.removeEventListener("keydown", handler);
|
|
278
|
-
}, [isOpen, closeOnEsc, close]);
|
|
301
|
+
}, [isOpen, closeOnEsc, close, modalId]);
|
|
279
302
|
|
|
280
303
|
// Focus trap
|
|
281
304
|
useLayoutEffect(() => {
|
|
@@ -308,13 +331,25 @@ export const Modal: FC<ModalProps> = ({
|
|
|
308
331
|
};
|
|
309
332
|
el.addEventListener("keydown", trap);
|
|
310
333
|
return () => el.removeEventListener("keydown", trap);
|
|
311
|
-
}, [isOpen]);
|
|
334
|
+
}, [isOpen, mounted]);
|
|
312
335
|
|
|
313
336
|
// Close when a click lands outside the dialog panel - mirrors Popover's
|
|
314
337
|
// outside-click pattern so the component family stays consistent.
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
338
|
+
// `ignoreSelector` exempts portaled popover menus (CustomSelect, DatePicker,
|
|
339
|
+
// RowActionsPopover, etc.): their menus portal to document.body so they're
|
|
340
|
+
// outside `contentRef`, but a click on a dropdown option must NOT dismiss
|
|
341
|
+
// the dialog it was opened from. Gated on being the topmost modal so a click
|
|
342
|
+
// that dismisses a child modal doesn't bubble into closing its parent.
|
|
343
|
+
useFocusOutside(
|
|
344
|
+
[contentRef],
|
|
345
|
+
() => {
|
|
346
|
+
if (isTopModal(modalId)) close();
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
enabled: closeOnOverlayClick && isOpen,
|
|
350
|
+
ignoreSelector: "[data-popover-menu]",
|
|
351
|
+
},
|
|
352
|
+
);
|
|
318
353
|
|
|
319
354
|
// ── Dialog panel ──────────────────────────────────────────────────────────
|
|
320
355
|
const dialog = shouldRender ? (
|
|
@@ -359,7 +394,11 @@ export const Modal: FC<ModalProps> = ({
|
|
|
359
394
|
) : null;
|
|
360
395
|
|
|
361
396
|
const portal =
|
|
362
|
-
renderOnPortal && dialog
|
|
397
|
+
renderOnPortal && dialog
|
|
398
|
+
? mounted
|
|
399
|
+
? createPortal(dialog, document.body)
|
|
400
|
+
: null
|
|
401
|
+
: dialog;
|
|
363
402
|
|
|
364
403
|
// Hoisted triggers sit outside the portal so they stay visible while closed;
|
|
365
404
|
// dialog content lives inside the portal.
|
|
@@ -15,6 +15,7 @@ export type PopoverContextType = {
|
|
|
15
15
|
position: Position;
|
|
16
16
|
hoverTimeoutRef: RefObject<ReturnType<typeof setTimeout> | null>;
|
|
17
17
|
renderOnPortal: boolean;
|
|
18
|
+
mounted: boolean;
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
export const PopoverContext = createContext<PopoverContextType | undefined>(
|
|
@@ -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";
|
|
@@ -82,6 +83,8 @@ export const Popover: FC<PopoverProps> = ({
|
|
|
82
83
|
// For hover trigger
|
|
83
84
|
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
84
85
|
|
|
86
|
+
const mounted = useHasMounted();
|
|
87
|
+
|
|
85
88
|
const [coords, setCoords] = useState<Coords>({ top: 0, left: 0 });
|
|
86
89
|
const [shouldRender, setShouldRender] = useState(false);
|
|
87
90
|
const [isMeasured, setIsMeasured] = useState(false);
|
|
@@ -178,7 +181,7 @@ export const Popover: FC<PopoverProps> = ({
|
|
|
178
181
|
setIsMeasured(false);
|
|
179
182
|
};
|
|
180
183
|
}
|
|
181
|
-
}, [shouldRender, calculatePosition]);
|
|
184
|
+
}, [shouldRender, mounted, calculatePosition]);
|
|
182
185
|
|
|
183
186
|
// Clear any pending hover-close timer when the Popover unmounts so the
|
|
184
187
|
// delayed setIsOpen never fires on a stale instance.
|
|
@@ -265,6 +268,7 @@ export const Popover: FC<PopoverProps> = ({
|
|
|
265
268
|
hoverTimeoutRef,
|
|
266
269
|
renderOnPortal,
|
|
267
270
|
position,
|
|
271
|
+
mounted,
|
|
268
272
|
}}
|
|
269
273
|
>
|
|
270
274
|
<div
|
|
@@ -387,6 +391,7 @@ export const PopoverMenu: FC<PopoverMenuProps> = ({
|
|
|
387
391
|
position,
|
|
388
392
|
hoverTimeoutRef,
|
|
389
393
|
renderOnPortal,
|
|
394
|
+
mounted,
|
|
390
395
|
} = usePopover();
|
|
391
396
|
|
|
392
397
|
const handleMouseEnter = () => {
|
|
@@ -453,6 +458,12 @@ export const PopoverMenu: FC<PopoverMenuProps> = ({
|
|
|
453
458
|
<div
|
|
454
459
|
ref={menuRef}
|
|
455
460
|
className={clsx(s.menu_wrapper, className)}
|
|
461
|
+
// Stable marker for portaled-layer detection. A `Popover` menu portals
|
|
462
|
+
// to `document.body`, escaping its host's DOM subtree - hosts with
|
|
463
|
+
// outside-click dismissal (Modal/Drawer) match this selector via
|
|
464
|
+
// `useFocusOutside({ ignoreSelector })` so clicking a menu option
|
|
465
|
+
// doesn't dismiss the host.
|
|
466
|
+
data-popover-menu=""
|
|
456
467
|
onMouseEnter={handleMouseEnter}
|
|
457
468
|
onMouseLeave={handleMouseLeave}
|
|
458
469
|
style={menuStyles}
|
|
@@ -461,5 +472,5 @@ export const PopoverMenu: FC<PopoverMenuProps> = ({
|
|
|
461
472
|
</div>
|
|
462
473
|
);
|
|
463
474
|
|
|
464
|
-
return renderOnPortal ? createPortal(menu, document.body) : menu;
|
|
475
|
+
return renderOnPortal ? (mounted ? createPortal(menu, document.body) : null) : menu;
|
|
465
476
|
};
|
|
@@ -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
|
}
|