aplance-ui 1.2.2 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aplance-ui",
3
- "version": "1.2.2",
3
+ "version": "1.3.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.
@@ -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 = () => {
@@ -467,5 +472,5 @@ export const PopoverMenu: FC<PopoverMenuProps> = ({
467
472
  </div>
468
473
  );
469
474
 
470
- return renderOnPortal ? createPortal(menu, document.body) : menu;
475
+ return renderOnPortal ? (mounted ? createPortal(menu, document.body) : null) : menu;
471
476
  };
@@ -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
  }