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.
Files changed (26) hide show
  1. package/package.json +1 -1
  2. package/registry/components/primitives/modal/modal.tsx +16 -10
  3. package/registry/components/primitives/popover/popover-context.ts +33 -28
  4. package/registry/components/primitives/popover/popover.tsx +145 -26
  5. package/registry/components/primitives/slider/slider.module.css +62 -5
  6. package/registry/components/primitives/slider/slider.tsx +192 -34
  7. package/registry/components/primitives/slider/types.ts +56 -3
  8. package/registry/components/primitives/slider/utils.ts +9 -0
  9. package/registry/components/ui/copy-button/copy-button.tsx +5 -0
  10. package/registry/components/ui/data-table/data-table.module.css +695 -613
  11. package/registry/components/ui/data-table/data-table.tsx +136 -112
  12. package/registry/components/ui/data-table/filter-modal.tsx +86 -0
  13. package/registry/components/ui/data-table/types.ts +8 -0
  14. package/registry/components/ui/data-table/useDataTable.tsx +1 -0
  15. package/registry/components/ui/data-table/utils.ts +26 -1
  16. package/registry/components/ui/input-fields/custom-select/custom-select.module.css +32 -1
  17. package/registry/components/ui/input-fields/custom-select/custom-select.tsx +500 -469
  18. package/registry/components/ui/input-fields/date-picker/date-picker.tsx +940 -933
  19. package/registry/components/ui/input-fields/date-picker/types.ts +2 -0
  20. package/registry/components/ui/input-fields/emoji-picker/emoji-picker.tsx +8 -0
  21. package/registry/components/ui/input-fields/input/input.tsx +80 -69
  22. package/registry/components/ui/tabs/tabs.module.css +22 -8
  23. package/registry/components/ui/tabs/tabs.tsx +29 -12
  24. package/registry/hooks/useHasMounted.ts +22 -0
  25. package/registry/hooks/useMediaQuery.ts +38 -0
  26. package/registry/registry.json +41 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aplance-ui",
3
- "version": "1.2.2",
3
+ "version": "1.4.0",
4
4
  "description": "The aplance-ui CLI - scaffold the design system and copy components into your project as source.",
5
5
  "keywords": [
6
6
  "react",
@@ -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 and below).
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 -> scale, top/bottom/left/right -> slide from that edge, full -> fade.
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: setShouldRender(true) -> node mounts -> useLayoutEffect below fires -> isVisible=true -> open transition.
247
- // Close: setIsVisible(false) -> close transition plays -> setTimeout unmounts after duration.
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, not on every isOpen change
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 ? createPortal(dialog, document.body) : 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
- hoverTimeoutRef: RefObject<ReturnType<typeof setTimeout> | null>;
17
- renderOnPortal: boolean;
18
- };
19
-
20
- export const PopoverContext = createContext<PopoverContextType | undefined>(
21
- undefined,
22
- );
23
-
24
- export const usePopover = () => {
25
- const ctx = useContext(PopoverContext);
26
- if (!ctx) throw new Error("usePopover must be used within Popover");
27
- return ctx;
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 (position) {
181
+ switch (nextPosition) {
103
182
  case "top":
104
- top = triggerRect.top - menuRect.height - offset;
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 - menuRect.width - offset;
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 (position) {
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 - menuRect.width;
124
- else left = triggerRect.left + (triggerRect.width - menuRect.width) / 2;
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 - menuRect.height;
208
+ else if (align === "end") top = triggerRect.bottom - measuredHeight;
130
209
  else
131
- top = triggerRect.top + triggerRect.height / 2 - menuRect.height / 2;
210
+ top = triggerRect.top + triggerRect.height / 2 - measuredHeight / 2;
132
211
  break;
133
212
  }
134
213
 
135
- // Collision detection
136
- const PADDING = 8;
137
- top = Math.max(
138
- PADDING,
139
- Math.min(top, innerHeight - menuRect.height - PADDING),
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
- useLayoutEffect(() => {
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
- position,
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 menuStyles: CSSProperties = {
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
- ...(matchTriggerWidth && { maxWidth: triggerWidth }),
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(position, isMeasured, isOpen),
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-12);
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-6);
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-base),
85
- transform var(--transition-base);
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
  }