aplance-ui 1.2.1 → 1.2.2

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.1",
3
+ "version": "1.2.2",
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",
@@ -86,6 +86,20 @@ const releaseScrollLock = () => {
86
86
  }
87
87
  };
88
88
 
89
+ // Module-level stack of open modal ids. Only the topmost modal reacts to
90
+ // Escape / backdrop-click - without this, dismissing a child modal (media
91
+ // picker, upload, etc.) also closes the parent it was opened from.
92
+ const _modalStack: string[] = [];
93
+ const pushModal = (id: string) => {
94
+ _modalStack.push(id);
95
+ };
96
+ const popModal = (id: string) => {
97
+ const index = _modalStack.lastIndexOf(id);
98
+ if (index >= 0) _modalStack.splice(index, 1);
99
+ };
100
+ const isTopModal = (id: string): boolean =>
101
+ _modalStack[_modalStack.length - 1] === id;
102
+
89
103
  // ─── Shared button passthrough ────────────────────────────────────────────────
90
104
 
91
105
  export type ButtonPassthroughProps = {
@@ -124,14 +138,14 @@ export type ModalProps = {
124
138
  */
125
139
  placement?: ModalPlacement;
126
140
  /**
127
- * Placement override below the `--md` breakpoint (≤ 900px).
141
+ * Placement override below the `--md` breakpoint (900px and below).
128
142
  * Defaults to `bottom` so modals become bottom sheets on phones.
129
143
  * Pass the same value as `placement` (or any placement) to opt out.
130
144
  */
131
145
  mobilePlacement?: ModalPlacement;
132
146
  /**
133
147
  * Entrance/exit animation. Defaults based on `placement`:
134
- * center→scale, top/bottom/left/right→slide from that edge, full→fade.
148
+ * center -> scale, top/bottom/left/right -> slide from that edge, full -> fade.
135
149
  */
136
150
  animation?: ModalAnimation;
137
151
  scrollBehavior?: ModalScrollBehavior;
@@ -229,8 +243,8 @@ export const Modal: FC<ModalProps> = ({
229
243
  const close = useCallback(() => setIsOpen(false), [setIsOpen]);
230
244
  const toggle = useCallback(() => setIsOpen(!isOpen), [isOpen, setIsOpen]);
231
245
 
232
- // Open: setShouldRender(true) → node mounts → useLayoutEffect below fires → isVisible=true → open transition
233
- // Close: setIsVisible(false) immediately → close transition plays → setTimeout unmounts after duration
246
+ // Open: setShouldRender(true) -> node mounts -> useLayoutEffect below fires -> isVisible=true -> open transition.
247
+ // Close: setIsVisible(false) -> close transition plays -> setTimeout unmounts after duration.
234
248
  useEffect(() => {
235
249
  if (isOpen) {
236
250
  setShouldRender(true);
@@ -260,22 +274,28 @@ export const Modal: FC<ModalProps> = ({
260
274
  if (isOpen) onOpenRef.current?.();
261
275
  }, [isOpen]);
262
276
 
263
- // Body scroll lock - reference-counted across stacked modals
277
+ // Body scroll lock + open-modal stack registration (both reference-counted
278
+ // across stacked modals).
264
279
  useEffect(() => {
265
280
  if (!isOpen) return;
266
281
  acquireScrollLock();
267
- return releaseScrollLock;
268
- }, [isOpen]);
282
+ pushModal(modalId);
283
+ return () => {
284
+ releaseScrollLock();
285
+ popModal(modalId);
286
+ };
287
+ }, [isOpen, modalId]);
269
288
 
270
- // ESC key
289
+ // ESC key - only the topmost modal closes, so Escape inside a child modal
290
+ // doesn't also dismiss its parent.
271
291
  useEffect(() => {
272
292
  if (!closeOnEsc || !isOpen) return;
273
293
  const handler = (e: globalThis.KeyboardEvent) => {
274
- if (e.key === "Escape") close();
294
+ if (e.key === "Escape" && isTopModal(modalId)) close();
275
295
  };
276
296
  window.addEventListener("keydown", handler);
277
297
  return () => window.removeEventListener("keydown", handler);
278
- }, [isOpen, closeOnEsc, close]);
298
+ }, [isOpen, closeOnEsc, close, modalId]);
279
299
 
280
300
  // Focus trap
281
301
  useLayoutEffect(() => {
@@ -312,9 +332,22 @@ export const Modal: FC<ModalProps> = ({
312
332
 
313
333
  // Close when a click lands outside the dialog panel - mirrors Popover's
314
334
  // outside-click pattern so the component family stays consistent.
315
- useFocusOutside([contentRef], close, {
316
- enabled: closeOnOverlayClick && isOpen,
317
- });
335
+ // `ignoreSelector` exempts portaled popover menus (CustomSelect, DatePicker,
336
+ // RowActionsPopover, etc.): their menus portal to document.body so they're
337
+ // 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
340
+ // that dismisses a child modal doesn't bubble into closing its parent.
341
+ useFocusOutside(
342
+ [contentRef],
343
+ () => {
344
+ if (isTopModal(modalId)) close();
345
+ },
346
+ {
347
+ enabled: closeOnOverlayClick && isOpen,
348
+ ignoreSelector: "[data-popover-menu]",
349
+ },
350
+ );
318
351
 
319
352
  // ── Dialog panel ──────────────────────────────────────────────────────────
320
353
  const dialog = shouldRender ? (
@@ -453,6 +453,12 @@ export const PopoverMenu: FC<PopoverMenuProps> = ({
453
453
  <div
454
454
  ref={menuRef}
455
455
  className={clsx(s.menu_wrapper, className)}
456
+ // Stable marker for portaled-layer detection. A `Popover` menu portals
457
+ // to `document.body`, escaping its host's DOM subtree - hosts with
458
+ // outside-click dismissal (Modal/Drawer) match this selector via
459
+ // `useFocusOutside({ ignoreSelector })` so clicking a menu option
460
+ // doesn't dismiss the host.
461
+ data-popover-menu=""
456
462
  onMouseEnter={handleMouseEnter}
457
463
  onMouseLeave={handleMouseLeave}
458
464
  style={menuStyles}
@@ -114,16 +114,18 @@
114
114
  overflow-x: auto;
115
115
  overflow-y: auto;
116
116
  border: var(--border-1) solid var(--color-border);
117
- border-radius: var(--radius-md);
118
- background: var(--color-background);
119
- position: relative;
120
- }
117
+ border-radius: var(--radius-md);
118
+ background: var(--color-background);
119
+ position: relative;
120
+ }
121
121
 
122
122
  /* ─────────────────────────────────────────────
123
123
  Shared <table> - header wrap + body wrap.
124
124
  table-layout:fixed + identical widths = perfect column alignment.
125
125
  ───────────────────────────────────────────── */
126
126
  .table {
127
+ --data-table-selection-control-size: var(--spacing-9);
128
+
127
129
  border-collapse: separate;
128
130
  border-spacing: 0;
129
131
  table-layout: fixed;
@@ -295,6 +297,8 @@
295
297
  }
296
298
 
297
299
  .selection_checkbox {
300
+ --native-control-size: var(--data-table-selection-control-size);
301
+
298
302
  width: fit-content;
299
303
  min-width: fit-content;
300
304
  display: inline-flex;
@@ -534,14 +538,28 @@
534
538
  min-width: 0;
535
539
  }
536
540
 
537
- .pagination_left {
538
- display: flex;
539
- align-items: center;
540
- gap: var(--spacing-8);
541
- min-width: 0;
542
- flex-shrink: 1;
541
+ .pagination_bar_loading {
542
+ flex-wrap: wrap;
543
+ row-gap: var(--spacing-4);
543
544
  }
544
545
 
546
+ .loading_select_skeleton {
547
+ border-radius: var(--radius-sm);
548
+ flex-shrink: 0;
549
+ }
550
+
551
+ .pagination_left {
552
+ display: flex;
553
+ align-items: center;
554
+ gap: var(--spacing-8);
555
+ min-width: 0;
556
+ flex-shrink: 1;
557
+ }
558
+
559
+ .pagination_left_loading {
560
+ overflow: hidden;
561
+ }
562
+
545
563
  .pagination_count {
546
564
  font-size: var(--font-size-sm);
547
565
  color: var(--color-muted-foreground);
@@ -557,15 +575,56 @@
557
575
  display: flex;
558
576
  align-items: center;
559
577
  gap: var(--spacing-8);
560
- flex-shrink: 0;
578
+ min-width: 0;
579
+ flex-shrink: 1;
561
580
  }
562
581
 
563
- .page_size_wrap {
564
- display: flex;
565
- align-items: center;
566
- gap: var(--spacing-5);
582
+ .pagination_controls_loading {
583
+ flex-direction: row;
584
+ flex-wrap: wrap;
585
+ align-items: center;
586
+ justify-content: flex-end;
587
+ max-width: 100%;
588
+ row-gap: var(--spacing-4);
589
+ }
590
+
591
+ .pagination_controls_loading > * {
592
+ flex: 0 1 auto;
593
+ min-width: 0;
594
+ max-width: 100%;
595
+ }
596
+
597
+ .pagination_text_skeleton {
598
+ height: 1em;
599
+ min-width: 0;
600
+ max-width: 100%;
601
+ flex: 0 1 auto;
602
+ }
603
+
604
+ .pagination_select_skeleton,
605
+ .pagination_button_skeleton {
606
+ width: var(--spacing-11);
607
+ height: var(--spacing-11);
608
+ min-width: 0;
609
+ }
610
+
611
+ .pagination_select_skeleton {
612
+ flex: 0 1 auto;
567
613
  }
568
614
 
615
+ .pagination_button_skeleton {
616
+ flex: 0 0 auto;
617
+ }
618
+
619
+ .page_size_wrap {
620
+ display: flex;
621
+ align-items: center;
622
+ gap: var(--spacing-5);
623
+ min-width: 0;
624
+ flex-shrink: 1;
625
+ max-width: 100%;
626
+ }
627
+
569
628
  .page_size_label {
570
629
  font-size: var(--font-size-sm);
571
630
  color: var(--color-muted-foreground);
@@ -585,8 +644,18 @@
585
644
  }
586
645
  }
587
646
 
588
- .page_nav {
589
- display: flex;
590
- align-items: center;
591
- gap: var(--spacing-2);
592
- }
647
+ .page_nav {
648
+ display: flex;
649
+ align-items: center;
650
+ gap: var(--spacing-2);
651
+ flex-shrink: 0;
652
+ }
653
+
654
+ .page_nav_loading {
655
+ flex-wrap: wrap;
656
+ justify-content: flex-end;
657
+ row-gap: var(--spacing-2);
658
+ min-width: 0;
659
+ max-width: 100%;
660
+ flex-shrink: 1;
661
+ }