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 +1 -1
- package/registry/components/primitives/modal/modal.tsx +16 -10
- package/registry/components/primitives/popover/popover-context.ts +1 -0
- package/registry/components/primitives/popover/popover.tsx +7 -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 +99 -50
- package/registry/components/ui/data-table/data-table.tsx +110 -104
- 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 +29 -14
- 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 +5 -1
- package/registry/hooks/useHasMounted.ts +22 -0
- package/registry/registry.json +20 -3
|
@@ -29,6 +29,18 @@
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/* On tablet/phone the configured multi-column row can't fit. Stack every field
|
|
33
|
+
full width instead of overflowing the page or scrolling (a scroll container
|
|
34
|
+
would clip the inputs' focus rings). `!important` beats the inline
|
|
35
|
+
grid-template-columns the FilterBar sets per consumer; `stretch` makes each
|
|
36
|
+
field fill the row. */
|
|
37
|
+
@media (--md) {
|
|
38
|
+
.filter_bar {
|
|
39
|
+
grid-template-columns: 1fr !important;
|
|
40
|
+
justify-items: stretch;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
32
44
|
/* ─────────────────────────────────────────────
|
|
33
45
|
Content wrap
|
|
34
46
|
Grows to fill all space below the filter bar.
|
|
@@ -113,12 +125,25 @@
|
|
|
113
125
|
min-height: 0;
|
|
114
126
|
overflow-x: auto;
|
|
115
127
|
overflow-y: auto;
|
|
128
|
+
/* Reserve the scrollbar gutter by DEFAULT so clientWidth is stable: the
|
|
129
|
+
vertical scrollbar (when rows overflow) sits in the reserved gutter instead
|
|
130
|
+
of stealing ~15px and triggering an unexpected horizontal scrollbar. This
|
|
131
|
+
default also covers the first paint before JS measures. */
|
|
132
|
+
scrollbar-gutter: stable;
|
|
116
133
|
border: var(--border-1) solid var(--color-border);
|
|
117
134
|
border-radius: var(--radius-md);
|
|
118
135
|
background: var(--color-background);
|
|
119
136
|
position: relative;
|
|
120
137
|
}
|
|
121
138
|
|
|
139
|
+
/* Once JS confirms the body does NOT overflow vertically (no scrollbar to make
|
|
140
|
+
room for), release the gutter so the content fills to the border - otherwise
|
|
141
|
+
a few-row table shows a blank ~15px strip on the right. `data-has-scroll` is
|
|
142
|
+
set from `scrollHeight > clientHeight` in syncHeaderMetrics. */
|
|
143
|
+
.table_body_wrap[data-has-scroll="false"] {
|
|
144
|
+
scrollbar-gutter: auto;
|
|
145
|
+
}
|
|
146
|
+
|
|
122
147
|
/* ─────────────────────────────────────────────
|
|
123
148
|
Shared <table> - header wrap + body wrap.
|
|
124
149
|
table-layout:fixed + identical widths = perfect column alignment.
|
|
@@ -536,6 +561,26 @@
|
|
|
536
561
|
justify-content: space-between;
|
|
537
562
|
gap: var(--spacing-8);
|
|
538
563
|
min-width: 0;
|
|
564
|
+
/* When the count + controls can't fit, the WHOLE bar scrolls horizontally as
|
|
565
|
+
one strip rather than wrapping or clipping. The outlet hides X overflow, so
|
|
566
|
+
letting a cluster shrink would just clip the nav buttons - scroll the bar
|
|
567
|
+
instead. `space-between` still spreads them when there IS room. The vertical
|
|
568
|
+
padding gives the buttons' focus rings room so `overflow-x: auto` (which also
|
|
569
|
+
clips the Y axis) doesn't crop them. */
|
|
570
|
+
overflow-x: auto;
|
|
571
|
+
scrollbar-width: thin;
|
|
572
|
+
scrollbar-color: var(--color-border) transparent;
|
|
573
|
+
padding-block: var(--spacing-2);
|
|
574
|
+
|
|
575
|
+
> * {
|
|
576
|
+
flex-shrink: 0;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
@media (--sm) {
|
|
581
|
+
.pagination_bar {
|
|
582
|
+
gap: var(--spacing-5);
|
|
583
|
+
}
|
|
539
584
|
}
|
|
540
585
|
|
|
541
586
|
.pagination_bar_loading {
|
|
@@ -548,18 +593,17 @@
|
|
|
548
593
|
flex-shrink: 0;
|
|
549
594
|
}
|
|
550
595
|
|
|
551
|
-
.pagination_left {
|
|
552
|
-
display: flex;
|
|
553
|
-
align-items: center;
|
|
554
|
-
gap: var(--spacing-8);
|
|
555
|
-
min-width: 0;
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
596
|
+
.pagination_left {
|
|
597
|
+
display: flex;
|
|
598
|
+
align-items: center;
|
|
599
|
+
gap: var(--spacing-8);
|
|
600
|
+
min-width: 0;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
.pagination_left_loading {
|
|
604
|
+
overflow: hidden;
|
|
605
|
+
}
|
|
606
|
+
|
|
563
607
|
.pagination_count {
|
|
564
608
|
font-size: var(--font-size-sm);
|
|
565
609
|
color: var(--color-muted-foreground);
|
|
@@ -576,23 +620,28 @@
|
|
|
576
620
|
align-items: center;
|
|
577
621
|
gap: var(--spacing-8);
|
|
578
622
|
min-width: 0;
|
|
579
|
-
flex-shrink: 1;
|
|
580
623
|
}
|
|
581
624
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
}
|
|
625
|
+
@media (--sm) {
|
|
626
|
+
.pagination_controls {
|
|
627
|
+
gap: var(--spacing-5);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
.pagination_controls_loading {
|
|
632
|
+
flex-direction: row;
|
|
633
|
+
flex-wrap: wrap;
|
|
634
|
+
align-items: center;
|
|
635
|
+
justify-content: flex-end;
|
|
636
|
+
max-width: 100%;
|
|
637
|
+
row-gap: var(--spacing-4);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
.pagination_controls_loading > * {
|
|
641
|
+
flex: 0 1 auto;
|
|
642
|
+
min-width: 0;
|
|
643
|
+
max-width: 100%;
|
|
644
|
+
}
|
|
596
645
|
|
|
597
646
|
.pagination_text_skeleton {
|
|
598
647
|
height: 1em;
|
|
@@ -616,14 +665,14 @@
|
|
|
616
665
|
flex: 0 0 auto;
|
|
617
666
|
}
|
|
618
667
|
|
|
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
|
-
}
|
|
668
|
+
.page_size_wrap {
|
|
669
|
+
display: flex;
|
|
670
|
+
align-items: center;
|
|
671
|
+
gap: var(--spacing-5);
|
|
672
|
+
min-width: 0;
|
|
673
|
+
flex-shrink: 1;
|
|
674
|
+
max-width: 100%;
|
|
675
|
+
}
|
|
627
676
|
|
|
628
677
|
.page_size_label {
|
|
629
678
|
font-size: var(--font-size-sm);
|
|
@@ -644,18 +693,18 @@
|
|
|
644
693
|
}
|
|
645
694
|
}
|
|
646
695
|
|
|
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
|
-
}
|
|
696
|
+
.page_nav {
|
|
697
|
+
display: flex;
|
|
698
|
+
align-items: center;
|
|
699
|
+
gap: var(--spacing-2);
|
|
700
|
+
flex-shrink: 0;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
.page_nav_loading {
|
|
704
|
+
flex-wrap: wrap;
|
|
705
|
+
justify-content: flex-end;
|
|
706
|
+
row-gap: var(--spacing-2);
|
|
707
|
+
min-width: 0;
|
|
708
|
+
max-width: 100%;
|
|
709
|
+
flex-shrink: 1;
|
|
710
|
+
}
|
|
@@ -345,11 +345,11 @@ export const DataTable = <TData extends { id: string }>({
|
|
|
345
345
|
// ── Sync header/body scrolling ──────────────────────────────────────────────
|
|
346
346
|
// The header lives in its own `overflow:hidden` container; we drive its
|
|
347
347
|
// scrollLeft from the body's scrollLeft on every scroll event so pinned
|
|
348
|
-
// columns stay perfectly aligned with their body counterparts.
|
|
349
|
-
const headerRef = useRef<HTMLDivElement>(null);
|
|
350
|
-
const bodyRef = useRef<HTMLDivElement>(null);
|
|
351
|
-
const bodyTableRef = useRef<HTMLTableElement>(null);
|
|
352
|
-
const skeletonRowRef = useRef<HTMLTableRowElement>(null);
|
|
348
|
+
// columns stay perfectly aligned with their body counterparts.
|
|
349
|
+
const headerRef = useRef<HTMLDivElement>(null);
|
|
350
|
+
const bodyRef = useRef<HTMLDivElement>(null);
|
|
351
|
+
const bodyTableRef = useRef<HTMLTableElement>(null);
|
|
352
|
+
const skeletonRowRef = useRef<HTMLTableRowElement>(null);
|
|
353
353
|
const rows = table.getRowModel().rows;
|
|
354
354
|
const headerCount = table.getFlatHeaders().length;
|
|
355
355
|
const totalWidth = table.getTotalSize();
|
|
@@ -402,6 +402,9 @@ export const DataTable = <TData extends { id: string }>({
|
|
|
402
402
|
paddingRight,
|
|
403
403
|
);
|
|
404
404
|
header.parentElement?.setAttribute("data-has-scroll", hasScroll.toString());
|
|
405
|
+
// Mirror onto the body wrap so CSS can release the reserved scrollbar gutter
|
|
406
|
+
// when there's no vertical overflow (no blank strip on a few-row table).
|
|
407
|
+
body.setAttribute("data-has-scroll", hasScroll.toString());
|
|
405
408
|
|
|
406
409
|
// Re-sync horizontal scroll position after width changes so pinned
|
|
407
410
|
// columns stay perfectly aligned with their body counterparts.
|
|
@@ -465,7 +468,7 @@ export const DataTable = <TData extends { id: string }>({
|
|
|
465
468
|
}, []);
|
|
466
469
|
|
|
467
470
|
// ── Derived UI state ────────────────────────────────────────────────────────
|
|
468
|
-
// Defer the empty state by a tick after a loading-to-idle transition so the
|
|
471
|
+
// Defer the empty state by a tick after a loading-to-idle transition so the
|
|
469
472
|
// loader doesn't flicker into "no results" for one paint before rows land.
|
|
470
473
|
// Genuinely empty results (no in-flight load) commit immediately on mount.
|
|
471
474
|
const isRowsEmpty = rows.length === 0;
|
|
@@ -474,6 +477,9 @@ export const DataTable = <TData extends { id: string }>({
|
|
|
474
477
|
);
|
|
475
478
|
const wasLoadingRef = useRef(isLoading);
|
|
476
479
|
|
|
480
|
+
// wasLoadingRef is intentionally omitted from the dep array: it's a ref used
|
|
481
|
+
// to track the previous value of isLoading across renders without triggering
|
|
482
|
+
// re-runs. React guarantees ref objects are stable across the component lifetime.
|
|
477
483
|
useEffect(() => {
|
|
478
484
|
const wasLoading = wasLoadingRef.current;
|
|
479
485
|
wasLoadingRef.current = isLoading;
|
|
@@ -482,18 +488,19 @@ export const DataTable = <TData extends { id: string }>({
|
|
|
482
488
|
setShowEmptyState(false);
|
|
483
489
|
return;
|
|
484
490
|
}
|
|
485
|
-
if (wasLoading) {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
EMPTY_STATE_GRACE_MS,
|
|
489
|
-
);
|
|
490
|
-
return () => window.clearTimeout(id);
|
|
491
|
+
if (!wasLoading) {
|
|
492
|
+
setShowEmptyState(true);
|
|
493
|
+
return;
|
|
491
494
|
}
|
|
492
|
-
|
|
495
|
+
const id = window.setTimeout(
|
|
496
|
+
() => setShowEmptyState(true),
|
|
497
|
+
EMPTY_STATE_GRACE_MS,
|
|
498
|
+
);
|
|
499
|
+
return () => window.clearTimeout(id);
|
|
493
500
|
}, [isLoading, isRowsEmpty]);
|
|
494
501
|
|
|
495
502
|
// Single tab-like discriminator drives the body - loader, empty, or content.
|
|
496
|
-
// While loading OR while bridging the loading-to-empty grace window, the
|
|
503
|
+
// While loading OR while bridging the loading-to-empty grace window, the
|
|
497
504
|
// loader stays visible to avoid the brief "no results" flash.
|
|
498
505
|
const view: "loader" | "empty" | "content" =
|
|
499
506
|
isLoading || (isRowsEmpty && !showEmptyState)
|
|
@@ -525,20 +532,20 @@ export const DataTable = <TData extends { id: string }>({
|
|
|
525
532
|
const selectedCount = rowsSelectable
|
|
526
533
|
? table.getSelectedRowModel().flatRows.length
|
|
527
534
|
: 0;
|
|
528
|
-
const headerGroups = table.getHeaderGroups();
|
|
529
|
-
const visibleLeafColumns = table.getVisibleLeafColumns();
|
|
530
|
-
|
|
531
|
-
const skeletonRowCount =
|
|
532
|
-
bodyViewportHeight > 0 && skeletonRowHeight > 0
|
|
533
|
-
? Math.max(
|
|
535
|
+
const headerGroups = table.getHeaderGroups();
|
|
536
|
+
const visibleLeafColumns = table.getVisibleLeafColumns();
|
|
537
|
+
|
|
538
|
+
const skeletonRowCount =
|
|
539
|
+
bodyViewportHeight > 0 && skeletonRowHeight > 0
|
|
540
|
+
? Math.max(
|
|
534
541
|
MIN_SKELETON_ROWS,
|
|
535
542
|
Math.floor(bodyViewportHeight / skeletonRowHeight),
|
|
536
543
|
)
|
|
537
544
|
: MIN_SKELETON_ROWS;
|
|
538
545
|
|
|
539
|
-
const columnExtraWidths = useMemo<Record<string, number>>(() => {
|
|
540
|
-
const extraSpace = Math.max(0, bodyViewportWidth - totalWidth);
|
|
541
|
-
if (extraSpace <= 0) return EMPTY_COLUMN_EXTRA_WIDTHS;
|
|
546
|
+
const columnExtraWidths = useMemo<Record<string, number>>(() => {
|
|
547
|
+
const extraSpace = Math.max(0, bodyViewportWidth - totalWidth);
|
|
548
|
+
if (extraSpace <= 0) return EMPTY_COLUMN_EXTRA_WIDTHS;
|
|
542
549
|
|
|
543
550
|
// Never add responsive width onto pinned columns because tanstack computes
|
|
544
551
|
// sticky offsets from base sizes; changing pinned visual width would desync
|
|
@@ -561,27 +568,27 @@ export const DataTable = <TData extends { id: string }>({
|
|
|
561
568
|
remainder -= 1;
|
|
562
569
|
}
|
|
563
570
|
}
|
|
564
|
-
|
|
565
|
-
return next;
|
|
566
|
-
}, [bodyViewportWidth, totalWidth, visibleLeafColumns]);
|
|
567
|
-
|
|
568
|
-
const tableWidth = Math.max(totalWidth, bodyViewportWidth);
|
|
569
|
-
|
|
570
|
-
// Match the table element width to the measured column widths. This keeps
|
|
571
|
-
// header, body, and skeleton rows on the same sizing contract during resize.
|
|
572
|
-
const tableSizingStyle: CSSProperties = useMemo(
|
|
573
|
-
() => ({ width: `${tableWidth}px`, minWidth: `${totalWidth}px` }),
|
|
574
|
-
[tableWidth, totalWidth],
|
|
575
|
-
);
|
|
576
|
-
|
|
577
|
-
const bodySkeletonStyle: CSSProperties = {
|
|
578
|
-
height: "var(--spacing-9)",
|
|
579
|
-
};
|
|
580
|
-
|
|
581
|
-
const selectSkeletonStyle: CSSProperties = {
|
|
582
|
-
width: "var(--data-table-selection-control-size, var(--spacing-9))",
|
|
583
|
-
height: "var(--data-table-selection-control-size, var(--spacing-9))",
|
|
584
|
-
};
|
|
571
|
+
|
|
572
|
+
return next;
|
|
573
|
+
}, [bodyViewportWidth, totalWidth, visibleLeafColumns]);
|
|
574
|
+
|
|
575
|
+
const tableWidth = Math.max(totalWidth, bodyViewportWidth);
|
|
576
|
+
|
|
577
|
+
// Match the table element width to the measured column widths. This keeps
|
|
578
|
+
// header, body, and skeleton rows on the same sizing contract during resize.
|
|
579
|
+
const tableSizingStyle: CSSProperties = useMemo(
|
|
580
|
+
() => ({ width: `${tableWidth}px`, minWidth: `${totalWidth}px` }),
|
|
581
|
+
[tableWidth, totalWidth],
|
|
582
|
+
);
|
|
583
|
+
|
|
584
|
+
const bodySkeletonStyle: CSSProperties = {
|
|
585
|
+
height: "var(--spacing-9)",
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
const selectSkeletonStyle: CSSProperties = {
|
|
589
|
+
width: "var(--data-table-selection-control-size, var(--spacing-9))",
|
|
590
|
+
height: "var(--data-table-selection-control-size, var(--spacing-9))",
|
|
591
|
+
};
|
|
585
592
|
|
|
586
593
|
const tableClass = clsx(
|
|
587
594
|
s.table,
|
|
@@ -644,66 +651,65 @@ export const DataTable = <TData extends { id: string }>({
|
|
|
644
651
|
{/* Scrollable body - only this element scrolls. The body is a
|
|
645
652
|
single switch over `view`: at most one branch ever renders, the
|
|
646
653
|
other two are unmounted entirely. */}
|
|
647
|
-
<div
|
|
648
|
-
className={s.table_body_wrap}
|
|
649
|
-
style={{ pointerEvents: isLoading ? "none" : undefined }}
|
|
650
|
-
ref={bodyRef}
|
|
651
|
-
>
|
|
652
|
-
{view === "loader" && (
|
|
653
|
-
<table className={tableClass} style={tableSizingStyle}>
|
|
654
|
-
<tbody>
|
|
655
|
-
{Array.from({ length: skeletonRowCount }).map(
|
|
656
|
-
(_, rowIndex) => (
|
|
657
|
-
<tr
|
|
658
|
-
key={`skeleton-${rowIndex}`}
|
|
659
|
-
ref={rowIndex === 0 ? skeletonRowRef : undefined}
|
|
660
|
-
>
|
|
661
|
-
{visibleLeafColumns.map((column) => {
|
|
662
|
-
const isPinned = column.getIsPinned();
|
|
663
|
-
const isLastPinnedCol =
|
|
664
|
-
isPinned && isLastPinned(column);
|
|
665
|
-
const isSelectColumn =
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
isPinned === "
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
column,
|
|
679
|
-
column.
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
)}
|
|
654
|
+
<div
|
|
655
|
+
className={s.table_body_wrap}
|
|
656
|
+
style={{ pointerEvents: isLoading ? "none" : undefined }}
|
|
657
|
+
ref={bodyRef}
|
|
658
|
+
>
|
|
659
|
+
{view === "loader" && (
|
|
660
|
+
<table className={tableClass} style={tableSizingStyle}>
|
|
661
|
+
<tbody>
|
|
662
|
+
{Array.from({ length: skeletonRowCount }).map(
|
|
663
|
+
(_, rowIndex) => (
|
|
664
|
+
<tr
|
|
665
|
+
key={`skeleton-${rowIndex}`}
|
|
666
|
+
ref={rowIndex === 0 ? skeletonRowRef : undefined}
|
|
667
|
+
>
|
|
668
|
+
{visibleLeafColumns.map((column) => {
|
|
669
|
+
const isPinned = column.getIsPinned();
|
|
670
|
+
const isLastPinnedCol =
|
|
671
|
+
isPinned && isLastPinned(column);
|
|
672
|
+
const isSelectColumn = column.id === SELECT_COLUMN_ID;
|
|
673
|
+
|
|
674
|
+
return (
|
|
675
|
+
<td
|
|
676
|
+
key={column.id}
|
|
677
|
+
className={clsx(
|
|
678
|
+
isSelectColumn && s.col_select,
|
|
679
|
+
isPinned === "left" && s.col_pinned_left,
|
|
680
|
+
isPinned === "right" && s.col_pinned_right,
|
|
681
|
+
isLastPinnedCol && s.col_pinned_last,
|
|
682
|
+
)}
|
|
683
|
+
style={getColumnStyle(
|
|
684
|
+
column,
|
|
685
|
+
column.getSize(),
|
|
686
|
+
columnExtraWidths[column.id] ?? 0,
|
|
687
|
+
)}
|
|
688
|
+
>
|
|
689
|
+
<div className={s.td_inner}>
|
|
690
|
+
<Loader
|
|
691
|
+
variant="skeleton"
|
|
692
|
+
className={
|
|
693
|
+
isSelectColumn
|
|
694
|
+
? s.loading_select_skeleton
|
|
695
|
+
: undefined
|
|
696
|
+
}
|
|
697
|
+
style={
|
|
698
|
+
isSelectColumn
|
|
699
|
+
? selectSkeletonStyle
|
|
700
|
+
: bodySkeletonStyle
|
|
701
|
+
}
|
|
702
|
+
/>
|
|
703
|
+
</div>
|
|
704
|
+
</td>
|
|
705
|
+
);
|
|
706
|
+
})}
|
|
707
|
+
</tr>
|
|
708
|
+
),
|
|
709
|
+
)}
|
|
710
|
+
</tbody>
|
|
711
|
+
</table>
|
|
712
|
+
)}
|
|
707
713
|
{view === "empty" && (
|
|
708
714
|
<div className={s.state_overlay}>
|
|
709
715
|
<p className={s.empty_msg}>{emptyMessage}</p>
|
|
@@ -34,6 +34,9 @@
|
|
|
34
34
|
/* Option button */
|
|
35
35
|
.option {
|
|
36
36
|
width: 100%;
|
|
37
|
+
/* button base is min-width: fit-content - a long label must truncate
|
|
38
|
+
inside the row, never widen it past the menu */
|
|
39
|
+
min-width: 0;
|
|
37
40
|
/* button has justify-content: space-between */
|
|
38
41
|
justify-content: flex-start;
|
|
39
42
|
/* modifying padding of actual button variant for consistency */
|
|
@@ -42,8 +45,14 @@
|
|
|
42
45
|
|
|
43
46
|
/* > icon */
|
|
44
47
|
|
|
48
|
+
/* The label owns the row's slack space so trailing affordances
|
|
49
|
+
(.option_delete) always sit at the row's end - layout-driven, no
|
|
50
|
+
auto-margins (those tie with the Icon primitive's own margin reset
|
|
51
|
+
and the winner would depend on CSS bundle order). */
|
|
45
52
|
.option_label {
|
|
46
|
-
|
|
53
|
+
flex: 1 1 auto;
|
|
54
|
+
min-width: 0;
|
|
55
|
+
text-align: start;
|
|
47
56
|
}
|
|
48
57
|
}
|
|
49
58
|
}
|
|
@@ -53,4 +62,15 @@
|
|
|
53
62
|
max-width: 100% !important;
|
|
54
63
|
}
|
|
55
64
|
|
|
65
|
+
/* Trailing per-option delete affordance (option.onDelete). The growing
|
|
66
|
+
.option_label pushes it to the row's end; muted until hovered, then
|
|
67
|
+
error-toned. Click is stopPropagation'd in the component so it never
|
|
68
|
+
selects the option. */
|
|
69
|
+
.option_delete {
|
|
70
|
+
flex-shrink: 0;
|
|
71
|
+
--icon-color: var(--color-muted-foreground);
|
|
56
72
|
|
|
73
|
+
&:hover {
|
|
74
|
+
--icon-color: var(--color-error-foreground);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
useState,
|
|
10
10
|
} from "react";
|
|
11
11
|
import clsx from "clsx";
|
|
12
|
-
import { IconPlus } from "@tabler/icons-react";
|
|
12
|
+
import { IconPlus, IconTrash } from "@tabler/icons-react";
|
|
13
13
|
import { Input } from "../input/input";
|
|
14
14
|
import {
|
|
15
15
|
Popover,
|
|
@@ -22,10 +22,7 @@ import sPickerCommon from "../common/picker-popover-common.module.css";
|
|
|
22
22
|
import { Button, type ButtonVariant } from "../../button/button";
|
|
23
23
|
import s from "./custom-select.module.css";
|
|
24
24
|
import { FieldMessage } from "../common/field-message/field-message";
|
|
25
|
-
import {
|
|
26
|
-
SelectButton,
|
|
27
|
-
type SelectButtonProps,
|
|
28
|
-
} from "../common/select-button/select-button";
|
|
25
|
+
import { SelectButton, type SelectButtonProps } from "../common/select-button/select-button";
|
|
29
26
|
import { Icon } from "../../../primitives/icon/icon";
|
|
30
27
|
import type { BaseInputFieldProps } from "../common/types";
|
|
31
28
|
|
|
@@ -38,6 +35,10 @@ export type CustomSelectOption = {
|
|
|
38
35
|
optionButtonVariant?: ButtonVariant;
|
|
39
36
|
applyToSelector?: boolean;
|
|
40
37
|
disabled?: boolean;
|
|
38
|
+
/** Renders a trailing delete affordance on the option row. Clicking it does
|
|
39
|
+
* NOT select the option - the consumer typically opens a confirm dialog and
|
|
40
|
+
* performs the deletion (e.g. removing a folder/tag from its pool). */
|
|
41
|
+
onDelete?: () => void;
|
|
41
42
|
};
|
|
42
43
|
|
|
43
44
|
export type CustomSelectProps = Omit<
|
|
@@ -161,7 +162,7 @@ export const CustomSelect = ({
|
|
|
161
162
|
[actualValue],
|
|
162
163
|
);
|
|
163
164
|
|
|
164
|
-
// A selected value may not exist in `options
|
|
165
|
+
// A selected value may not exist in `options` - e.g. when `allowCreate` is
|
|
165
166
|
// on and the consumer just stores the value without pushing it back into
|
|
166
167
|
// the options list. Synthesize a "ghost" entry so the trigger displays it,
|
|
167
168
|
// the menu shows it (multi-select can uncheck), and the "Create" check
|
|
@@ -229,13 +230,7 @@ export const CustomSelect = ({
|
|
|
229
230
|
? selectedOption.optionStyle
|
|
230
231
|
: undefined,
|
|
231
232
|
};
|
|
232
|
-
}, [
|
|
233
|
-
multiple,
|
|
234
|
-
optionsWithSelected,
|
|
235
|
-
placeholder,
|
|
236
|
-
selectedValues,
|
|
237
|
-
showDisplayIcon,
|
|
238
|
-
]);
|
|
233
|
+
}, [multiple, optionsWithSelected, placeholder, selectedValues, showDisplayIcon]);
|
|
239
234
|
|
|
240
235
|
const popoverOnClose = popover?.onClose;
|
|
241
236
|
const handlePopoverClose = useCallback(() => {
|
|
@@ -422,7 +417,9 @@ export const CustomSelect = ({
|
|
|
422
417
|
<Icon size="sm" aria-hidden="true">
|
|
423
418
|
<IconPlus />
|
|
424
419
|
</Icon>
|
|
425
|
-
<span
|
|
420
|
+
<span
|
|
421
|
+
className={clsx(s.option_label, sCommon.truncate_text)}
|
|
422
|
+
>
|
|
426
423
|
Create “{trimmedTerm}”
|
|
427
424
|
</span>
|
|
428
425
|
</Button>
|
|
@@ -455,6 +452,24 @@ export const CustomSelect = ({
|
|
|
455
452
|
>
|
|
456
453
|
{option.label}
|
|
457
454
|
</span>
|
|
455
|
+
{option.onDelete && interactive ? (
|
|
456
|
+
<Icon
|
|
457
|
+
size="sm"
|
|
458
|
+
clickable
|
|
459
|
+
className={s.option_delete}
|
|
460
|
+
aria-label={`Delete ${option.label}`}
|
|
461
|
+
onClick={(e) => {
|
|
462
|
+
e.stopPropagation();
|
|
463
|
+
// Close the popover BEFORE handing off - the consumer
|
|
464
|
+
// opens a confirm dialog, which must not sit under
|
|
465
|
+
// the still-open popover in the stacking order.
|
|
466
|
+
setIsOpen(false);
|
|
467
|
+
option.onDelete?.();
|
|
468
|
+
}}
|
|
469
|
+
>
|
|
470
|
+
<IconTrash />
|
|
471
|
+
</Icon>
|
|
472
|
+
) : null}
|
|
458
473
|
</Button>
|
|
459
474
|
))
|
|
460
475
|
)}
|