aplance-ui 1.3.0 → 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.
@@ -1,710 +1,743 @@
1
- /* ─────────────────────────────────────────────
2
- Root
3
- Fills its flex parent. Stacks filter bar (fixed) + content wrap (grows).
4
- ───────────────────────────────────────────── */
5
- .root {
6
- width: 100%;
7
- flex: 1;
8
- min-height: 0;
9
- display: flex;
10
- flex-direction: column;
11
- gap: var(--spacing-6);
12
- }
13
-
14
- /* ─────────────────────────────────────────────
15
- Filter bar
16
- CSS grid - always one row, never wraps.
17
- gridTemplateColumns is set inline from FilterBar.
18
- ───────────────────────────────────────────── */
19
- .filter_bar {
20
- display: grid;
21
- align-items: center;
22
- place-items: center;
23
- gap: var(--spacing-6);
24
- width: 100%;
25
- flex-shrink: 0;
26
-
27
- > * {
28
- min-width: 0;
29
- }
30
- }
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
-
44
- /* ─────────────────────────────────────────────
45
- Content wrap
46
- Grows to fill all space below the filter bar.
47
- Flex column: table shell (grows) + pagination (pinned to bottom via margin-top:auto).
48
- ───────────────────────────────────────────── */
49
- .content_wrap {
50
- flex: 1;
51
- min-height: 0;
52
- display: flex;
53
- flex-direction: column;
54
- }
55
-
56
- /* ─────────────────────────────────────────────
57
- Table shell
58
- Grows to fill space above pagination. Header pill + scrollable body.
59
- ───────────────────────────────────────────── */
60
- .table_shell {
61
- flex: 1;
62
- min-height: 0;
63
- display: flex;
64
- flex-direction: column;
65
- gap: var(--spacing-6);
66
- padding-bottom: var(--spacing-6);
67
- }
68
-
69
- /* no bottom padding when pagination is not shown */
70
- .table_shell_no_pagination {
71
- padding-bottom: 0;
72
- }
73
-
74
- /* ─────────────────────────────────────────────
75
- Detached header pill
76
- Rounded standalone bar. scrollLeft synced to body via JS so pinned
77
- columns stay perfectly aligned with their body counterparts.
78
- ───────────────────────────────────────────── */
79
- .table_header_container {
80
- position: relative;
81
- flex-shrink: 0;
82
- width: 100%;
83
- }
84
-
85
- .table_header_wrap {
86
- width: 100%;
87
- overflow-x: auto;
88
- overflow-y: hidden;
89
- border-radius: var(--radius-md);
90
- background: var(--color-muted);
91
- border: var(--border-1) solid var(--color-border);
92
- scrollbar-width: none;
93
- }
94
-
95
- .table_header_wrap::-webkit-scrollbar {
96
- display: none;
97
- }
98
-
99
- .header_padding_mask {
100
- position: absolute;
101
- top: 0;
102
- right: 0;
103
- bottom: 0;
104
- width: var(--header-padding-right, 0px);
105
- background: var(--color-muted);
106
- border-radius: 0 var(--radius-md) var(--radius-md) 0;
107
- border-top: var(--border-1) solid var(--color-border);
108
- border-right: var(--border-1) solid var(--color-border);
109
- border-bottom: var(--border-1) solid var(--color-border);
110
- pointer-events: none;
111
- z-index: 10;
112
- visibility: hidden;
113
- }
114
-
115
- .table_header_container[data-has-scroll="true"] .header_padding_mask {
116
- visibility: visible;
117
- }
118
-
119
- /* ─────────────────────────────────────────────
120
- Scrollable body
121
- flex:1 + min-height:0 = fills all space, then scrolls.
122
- ───────────────────────────────────────────── */
123
- .table_body_wrap {
124
- flex: 1;
125
- min-height: 0;
126
- overflow-x: auto;
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;
133
- border: var(--border-1) solid var(--color-border);
134
- border-radius: var(--radius-md);
135
- background: var(--color-background);
136
- position: relative;
137
- }
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
-
147
- /* ─────────────────────────────────────────────
148
- Shared <table> - header wrap + body wrap.
149
- table-layout:fixed + identical widths = perfect column alignment.
150
- ───────────────────────────────────────────── */
151
- .table {
152
- --data-table-selection-control-size: var(--spacing-9);
153
-
154
- border-collapse: separate;
155
- border-spacing: 0;
156
- table-layout: fixed;
157
-
158
- thead th {
159
- position: relative;
160
- height: var(--spacing-11);
161
- padding: 0 var(--spacing-6);
162
- font-size: var(--font-size-xs);
163
- font-weight: var(--font-weight-semibold);
164
- color: var(--color-foreground);
165
- white-space: nowrap;
166
- /* explicit background so sticky pinned cells mask scrolled content */
167
- background: var(--color-muted);
168
- /* visible: header checkbox focus ring not clipped */
169
- overflow: visible;
170
- }
171
-
172
- tbody tr {
173
- transition: background var(--transition-fast);
174
-
175
- &:hover td {
176
- background: var(--color-secondary);
177
- }
178
-
179
- &.row_selected td {
180
- background: var(--color-muted);
181
- }
182
-
183
- &.row_group td {
184
- background: var(--color-secondary);
185
- font-weight: var(--font-weight-medium);
186
- }
187
-
188
- &.row_editing {
189
- /* hover should not flicker the background of a row the user is editing */
190
- &:hover td {
191
- background: var(--color-background);
192
- }
193
-
194
- td {
195
- background: var(--color-background);
196
-
197
- .td_inner {
198
- /* let inline-edit focus rings + dropdown shadows escape the cell */
199
- overflow: visible;
200
- text-overflow: clip;
201
- }
202
- }
203
- }
204
-
205
- &.row_dragging td {
206
- opacity: 0.5;
207
- background: var(--color-muted);
208
- }
209
-
210
- &.row_drop_target td {
211
- box-shadow: inset 0 2px 0 0 var(--color-primary);
212
- }
213
-
214
- td {
215
- padding: var(--spacing-5) var(--spacing-6);
216
- font-size: var(--font-size-sm);
217
- font-weight: var(--font-weight-normal);
218
- color: var(--color-foreground);
219
- vertical-align: middle;
220
- border-bottom: var(--border-1) solid var(--color-border);
221
- /* explicit background so sticky pinned cells mask scrolled content */
222
- background: var(--color-background);
223
- /* visible: focus rings + dropdowns escape the cell */
224
- overflow: visible;
225
- }
226
- }
227
- }
228
-
229
- /* ─────────────────────────────────────────────
230
- Vertical column borders (showVerticalBorders prop)
231
- Drawn on the LEFT of every cell except the first, so we never get a
232
- double border at the table's outer edge.
233
- ───────────────────────────────────────────── */
234
- .table_vertical_borders {
235
- /* Only add vertical border if the previous cell is not the last left-pinned cell. Prevents double border with pinned columns. */
236
- thead th:not(.col_pinned_last.col_pinned_left) + th,
237
- tbody td:not(.col_pinned_last.col_pinned_left) + td {
238
- border-left: var(--border-1) solid var(--color-border);
239
- }
240
- }
241
-
242
- /* ─────────────────────────────────────────────
243
- Header inner - flex container that hugs its content.
244
- No `flex: 1` on the label; icons sit immediately next to the text and
245
- `justify-content` (set by `.align_*`) determines start/end alignment.
246
- ───────────────────────────────────────────── */
247
- .th_inner {
248
- width: 100%;
249
- display: flex;
250
- align-items: center;
251
- min-height: var(--spacing-11);
252
- gap: var(--spacing-2);
253
- margin: 0;
254
- padding: 0;
255
- overflow: hidden;
256
- }
257
-
258
- .th_label {
259
- flex: 0 1 auto;
260
- min-width: 0;
261
- max-width: 100%;
262
- overflow: hidden;
263
- text-overflow: ellipsis;
264
- white-space: nowrap;
265
- }
266
-
267
- .th_actions {
268
- display: inline-flex;
269
- align-items: center;
270
- gap: var(--spacing-1);
271
- flex-shrink: 0;
272
- }
273
-
274
- .drag_handle {
275
- width: 1rem;
276
- height: 1rem;
277
- display: inline-flex;
278
- align-items: center;
279
- justify-content: center;
280
- color: var(--color-muted-foreground);
281
- cursor: grab;
282
- flex-shrink: 0;
283
-
284
- &:active {
285
- cursor: grabbing;
286
- }
287
- }
288
-
289
- /* ─────────────────────────────────────────────
290
- Compact icon-only button used for sort / group / pin in the header.
291
- ───────────────────────────────────────────── */
292
- .icon_btn {
293
- /* !important needed: --button-icon is also set by :is(.ghost, .tone_ghost) in
294
- button.module.css at equal specificity - CSS module load order is not guaranteed,
295
- so !important on the var is the only reliable way to win without hardcoding svg color. */
296
- --button-icon: var(--color-muted-foreground) !important;
297
-
298
- &:hover:not(.disabled) {
299
- --button-icon: var(--color-foreground) !important;
300
- background-color: var(--color-border) !important;
301
- }
302
- }
303
-
304
- /* ─────────────────────────────────────────────
305
- Body cell inner - default truncates with ellipsis.
306
- Override per-column with `meta.truncate = false` for cells hosting
307
- inputs/buttons so focus rings and dropdowns aren't clipped.
308
- ───────────────────────────────────────────── */
309
- .td_inner {
310
- overflow: hidden;
311
- text-overflow: ellipsis;
312
- white-space: nowrap;
313
- /* outset breathing room mirrors the header's */
314
- margin: calc(var(--spacing-2) * -1);
315
- padding: var(--spacing-2);
316
- }
317
-
318
- .td_inner_no_truncate {
319
- overflow: visible;
320
- text-overflow: clip;
321
- white-space: normal;
322
- }
323
-
324
- .selection_checkbox {
325
- --native-control-size: var(--data-table-selection-control-size);
326
-
327
- width: fit-content;
328
- min-width: fit-content;
329
- display: inline-flex;
330
- align-items: center;
331
- justify-content: center;
332
- padding: 0;
333
- margin: 0;
334
-
335
- > * {
336
- width: fit-content;
337
- min-width: fit-content;
338
- }
339
-
340
- > * > * {
341
- width: fit-content;
342
- min-width: fit-content;
343
- }
344
- }
345
-
346
- .col_select {
347
- padding-left: var(--spacing-5) !important;
348
- padding-right: var(--spacing-5) !important;
349
-
350
- .th_inner,
351
- .td_inner {
352
- margin: 0;
353
- padding: 0;
354
- display: flex;
355
- align-items: center;
356
- justify-content: center;
357
- gap: 0;
358
- overflow: visible;
359
- }
360
- }
361
-
362
- .group_anchor_cell {
363
- text-align: left !important;
364
-
365
- .td_inner {
366
- display: flex;
367
- align-items: center;
368
- justify-content: flex-start;
369
- }
370
- }
371
-
372
- /* ─────────────────────────────────────────────
373
- Cell alignment via `column.meta.align`
374
- - text-align handles inline text (truncate=true case)
375
- - flex justify-content handles non-truncated content (action buttons, inputs)
376
- - header is always a flex container so we always set justify-content there
377
- ───────────────────────────────────────────── */
378
- .align_left {
379
- text-align: left;
380
-
381
- .th_inner {
382
- justify-content: flex-start;
383
- }
384
-
385
- .td_inner_no_truncate {
386
- display: flex;
387
- align-items: center;
388
- justify-content: flex-start;
389
- }
390
- }
391
-
392
- .align_center {
393
- text-align: center;
394
-
395
- .th_inner {
396
- justify-content: center;
397
- }
398
-
399
- .td_inner_no_truncate {
400
- display: flex;
401
- align-items: center;
402
- justify-content: center;
403
- }
404
- }
405
-
406
- .align_right {
407
- text-align: right;
408
-
409
- .th_inner {
410
- justify-content: flex-end;
411
- }
412
-
413
- .td_inner_no_truncate {
414
- display: flex;
415
- align-items: center;
416
- justify-content: flex-end;
417
- }
418
- }
419
-
420
- /* ─────────────────────────────────────────────
421
- Column pinning - sticky offsets are written inline by JS
422
- The hairline divider lives only on the LAST column of each pinning group
423
- (`col_pinned_last`) so adjacent pinned columns don't get double dividers.
424
- ───────────────────────────────────────────── */
425
- .table th.col_pinned_left,
426
- .table th.col_pinned_right,
427
- .table td.col_pinned_left,
428
- .table td.col_pinned_right {
429
- position: sticky;
430
- z-index: 1;
431
-
432
- /* To ensure pinned columns are visible above other content */
433
- background-color: var(--color-muted);
434
- }
435
-
436
- .table thead th.col_pinned_left,
437
- .table thead th.col_pinned_right {
438
- z-index: 2;
439
- }
440
-
441
- /* Pinned left columns: add a right border only to the last pinned cell to act as a divider. */
442
- .col_pinned_last.col_pinned_left {
443
- border-right: var(--border-1) solid var(--color-border);
444
- }
445
-
446
- /* Pinned right columns: add a left border only to the last pinned cell to act as a divider. */
447
- .col_pinned_last.col_pinned_right {
448
- border-left: var(--border-1) solid var(--color-border);
449
- }
450
-
451
- /* Keep interactive cell popovers above sticky neighbors when they are rendered
452
- in-place (non-portal mode). */
453
- .table tbody td:focus-within {
454
- position: relative;
455
- z-index: 3;
456
- }
457
-
458
- /* ─────────────────────────────────────────────
459
- Drag-and-drop column reordering
460
- ───────────────────────────────────────────── */
461
- .col_dragging {
462
- opacity: 0.5;
463
- }
464
-
465
- .col_drop_target {
466
- /* a slim vertical bar on the leading edge of the drop target */
467
- outline: 2px solid var(--color-primary);
468
- outline-offset: -2px;
469
- }
470
-
471
- /* ─────────────────────────────────────────────
472
- Column resize handle - narrow vertical strip on the cell's right edge
473
- ───────────────────────────────────────────── */
474
- .resize_handle {
475
- position: absolute;
476
- top: 0;
477
- right: 0;
478
- bottom: 0;
479
- width: 5px;
480
- cursor: col-resize;
481
- user-select: none;
482
- touch-action: none;
483
- background: transparent;
484
- transition: background var(--transition-fast);
485
-
486
- &:hover,
487
- &.resize_handle_active {
488
- background: var(--color-primary);
489
- }
490
- }
491
-
492
- /* ─────────────────────────────────────────────
493
- Group rows + toggle
494
- ───────────────────────────────────────────── */
495
- .group_toggle {
496
- display: inline-flex;
497
- align-items: center;
498
- gap: var(--spacing-3);
499
- padding: 0;
500
- border: none;
501
- background: transparent;
502
- color: var(--color-foreground);
503
- font: inherit;
504
- cursor: pointer;
505
- }
506
-
507
- .group_chevron_icon {
508
- color: var(--color-muted-foreground);
509
- }
510
-
511
- .group_chevron {
512
- transition: transform var(--transition-fast);
513
- flex-shrink: 0;
514
- }
515
-
516
- .group_chevron_open {
517
- transform: rotate(90deg);
518
- }
519
-
520
- .group_value {
521
- font-weight: var(--font-weight-semibold);
522
- }
523
-
524
- .group_count {
525
- color: var(--color-muted-foreground);
526
- font-size: var(--font-size-xs);
527
- }
528
-
529
- .cell_aggregated {
530
- font-weight: var(--font-weight-semibold);
531
- }
532
-
533
- /* ─────────────────────────────────────────────
534
- Loading / empty states
535
- ───────────────────────────────────────────── */
536
- .state_overlay {
537
- width: 100%;
538
- padding: var(--spacing-14) var(--spacing-8);
539
- display: flex;
540
- align-items: center;
541
- justify-content: center;
542
- }
543
-
544
- .empty_msg {
545
- font-size: var(--font-size-sm);
546
- color: var(--color-muted-foreground);
547
- }
548
-
549
- /* ─────────────────────────────────────────────
550
- Pagination bar
551
- margin-top:auto pushes it to the absolute bottom of content_wrap
552
- no matter how few rows are visible.
553
- flex-shrink:0 stops it compressing when space is tight.
554
- ───────────────────────────────────────────── */
555
- .pagination_bar {
556
- flex-shrink: 0;
557
- margin-top: auto;
558
- width: 100%;
559
- display: flex;
560
- align-items: center;
561
- justify-content: space-between;
562
- gap: var(--spacing-8);
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
- }
584
- }
585
-
586
- .pagination_bar_loading {
587
- flex-wrap: wrap;
588
- row-gap: var(--spacing-4);
589
- }
590
-
591
- .loading_select_skeleton {
592
- border-radius: var(--radius-sm);
593
- flex-shrink: 0;
594
- }
595
-
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
-
607
- .pagination_count {
608
- font-size: var(--font-size-sm);
609
- color: var(--color-muted-foreground);
610
- white-space: nowrap;
611
-
612
- span {
613
- font-weight: var(--font-weight-medium);
614
- color: var(--color-foreground);
615
- }
616
- }
617
-
618
- .pagination_controls {
619
- display: flex;
620
- align-items: center;
621
- gap: var(--spacing-8);
622
- min-width: 0;
623
- }
624
-
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
- }
645
-
646
- .pagination_text_skeleton {
647
- height: 1em;
648
- min-width: 0;
649
- max-width: 100%;
650
- flex: 0 1 auto;
651
- }
652
-
653
- .pagination_select_skeleton,
654
- .pagination_button_skeleton {
655
- width: var(--spacing-11);
656
- height: var(--spacing-11);
657
- min-width: 0;
658
- }
659
-
660
- .pagination_select_skeleton {
661
- flex: 0 1 auto;
662
- }
663
-
664
- .pagination_button_skeleton {
665
- flex: 0 0 auto;
666
- }
667
-
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
- }
676
-
677
- .page_size_label {
678
- font-size: var(--font-size-sm);
679
- color: var(--color-muted-foreground);
680
- white-space: nowrap;
681
- }
682
-
683
- .page_indicator {
684
- font-size: var(--font-size-sm);
685
- color: var(--color-muted-foreground);
686
- white-space: nowrap;
687
- min-width: 6rem;
688
- text-align: center;
689
-
690
- span {
691
- font-weight: var(--font-weight-medium);
692
- color: var(--color-foreground);
693
- }
694
- }
695
-
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
- }
1
+ /* ─────────────────────────────────────────────
2
+ Root
3
+ Fills its flex parent. Stacks filter bar (fixed) + content wrap (grows).
4
+ ───────────────────────────────────────────── */
5
+ .root {
6
+ width: 100%;
7
+ flex: 1;
8
+ min-height: 0;
9
+ display: flex;
10
+ flex-direction: column;
11
+ gap: var(--spacing-6);
12
+ }
13
+
14
+ /* ─────────────────────────────────────────────
15
+ Filter bar
16
+ CSS grid - always one row, never wraps.
17
+ gridTemplateColumns is set inline from FilterBar.
18
+ ───────────────────────────────────────────── */
19
+ .filter_bar {
20
+ display: grid;
21
+ align-items: center;
22
+ place-items: center;
23
+ gap: var(--spacing-6);
24
+ width: 100%;
25
+ flex-shrink: 0;
26
+
27
+ > * {
28
+ min-width: 0;
29
+ }
30
+ }
31
+
32
+ /* ─────────────────────────────────────────────
33
+ Compact filters (≤ --md)
34
+ The inline row can't fit its fields below this width, and stacking them all
35
+ full-height buries the table on phones - so the DataTable swaps <FilterBar>
36
+ for <FilterModal>: a single trigger button that opens the same fields,
37
+ stacked, inside the Modal primitive (a bottom sheet on phones).
38
+ ───────────────────────────────────────────── */
39
+ .filter_modal_bar {
40
+ display: flex;
41
+ flex-shrink: 0;
42
+ }
43
+
44
+ .filter_trigger {
45
+ display: inline-flex;
46
+ align-items: center;
47
+ gap: var(--spacing-2);
48
+ }
49
+
50
+ /* Active-filter count pill on the trigger button. */
51
+ .filter_count {
52
+ display: inline-flex;
53
+ align-items: center;
54
+ justify-content: center;
55
+ min-width: var(--spacing-9);
56
+ height: var(--spacing-9);
57
+ padding: 0 var(--spacing-1);
58
+ border-radius: var(--radius-full);
59
+ background: var(--color-primary);
60
+ color: var(--color-primary-foreground);
61
+ font-size: var(--font-size-xs);
62
+ font-weight: var(--font-weight-semibold);
63
+ line-height: 1;
64
+ }
65
+
66
+ /* Fields stacked full width inside the modal; each control fills its row. */
67
+ .filter_modal_fields {
68
+ display: flex;
69
+ flex-direction: column;
70
+ gap: var(--spacing-5);
71
+ }
72
+
73
+ .filter_modal_field {
74
+ width: 100%;
75
+ }
76
+
77
+ .filter_modal_field > * {
78
+ width: 100%;
79
+ }
80
+
81
+ /* ─────────────────────────────────────────────
82
+ Content wrap
83
+ Grows to fill all space below the filter bar.
84
+ Flex column: table shell (grows) + pagination (pinned to bottom via margin-top:auto).
85
+ ───────────────────────────────────────────── */
86
+ .content_wrap {
87
+ flex: 1;
88
+ min-height: 0;
89
+ display: flex;
90
+ flex-direction: column;
91
+ }
92
+
93
+ /* ─────────────────────────────────────────────
94
+ Table shell
95
+ Grows to fill space above pagination. Header pill + scrollable body.
96
+ ───────────────────────────────────────────── */
97
+ .table_shell {
98
+ flex: 1;
99
+ min-height: 0;
100
+ display: flex;
101
+ flex-direction: column;
102
+ gap: var(--spacing-6);
103
+ padding-bottom: var(--spacing-6);
104
+ }
105
+
106
+ /* no bottom padding when pagination is not shown */
107
+ .table_shell_no_pagination {
108
+ padding-bottom: 0;
109
+ }
110
+
111
+ /* ─────────────────────────────────────────────
112
+ Detached header pill
113
+ Rounded standalone bar. scrollLeft synced to body via JS so pinned
114
+ columns stay perfectly aligned with their body counterparts.
115
+ ───────────────────────────────────────────── */
116
+ .table_header_container {
117
+ position: relative;
118
+ flex-shrink: 0;
119
+ width: 100%;
120
+ }
121
+
122
+ .table_header_wrap {
123
+ width: 100%;
124
+ overflow-x: auto;
125
+ overflow-y: hidden;
126
+ border-radius: var(--radius-md);
127
+ background: var(--color-muted);
128
+ border: var(--border-1) solid var(--color-border);
129
+ scrollbar-width: none;
130
+ }
131
+
132
+ .table_header_wrap::-webkit-scrollbar {
133
+ display: none;
134
+ }
135
+
136
+ .header_padding_mask {
137
+ position: absolute;
138
+ top: 0;
139
+ right: 0;
140
+ bottom: 0;
141
+ width: var(--header-padding-right, 0px);
142
+ background: var(--color-muted);
143
+ border-radius: 0 var(--radius-md) var(--radius-md) 0;
144
+ border-top: var(--border-1) solid var(--color-border);
145
+ border-right: var(--border-1) solid var(--color-border);
146
+ border-bottom: var(--border-1) solid var(--color-border);
147
+ pointer-events: none;
148
+ z-index: 10;
149
+ visibility: hidden;
150
+ }
151
+
152
+ .table_header_container[data-has-scroll="true"] .header_padding_mask {
153
+ visibility: visible;
154
+ }
155
+
156
+ /* ─────────────────────────────────────────────
157
+ Scrollable body
158
+ flex:1 + min-height:0 = fills all space, then scrolls.
159
+ ───────────────────────────────────────────── */
160
+ .table_body_wrap {
161
+ flex: 1;
162
+ min-height: 0;
163
+ overflow-x: auto;
164
+ overflow-y: auto;
165
+ /* Reserve the scrollbar gutter by DEFAULT so clientWidth is stable: the
166
+ vertical scrollbar (when rows overflow) sits in the reserved gutter instead
167
+ of stealing ~15px and triggering an unexpected horizontal scrollbar. This
168
+ default also covers the first paint before JS measures. */
169
+ scrollbar-gutter: stable;
170
+ border: var(--border-1) solid var(--color-border);
171
+ border-radius: var(--radius-md);
172
+ background: var(--color-background);
173
+ position: relative;
174
+ }
175
+
176
+ /* Once JS confirms the body does NOT overflow vertically (no scrollbar to make
177
+ room for), release the gutter so the content fills to the border - otherwise
178
+ a few-row table shows a blank ~15px strip on the right. `data-has-scroll` is
179
+ set from `scrollHeight > clientHeight` in syncHeaderMetrics. */
180
+ .table_body_wrap[data-has-scroll="false"] {
181
+ scrollbar-gutter: auto;
182
+ }
183
+
184
+ /* ─────────────────────────────────────────────
185
+ Shared <table> - header wrap + body wrap.
186
+ table-layout:fixed + identical widths = perfect column alignment.
187
+ ───────────────────────────────────────────── */
188
+ .table {
189
+ --data-table-selection-control-size: var(--spacing-9);
190
+
191
+ border-collapse: separate;
192
+ border-spacing: 0;
193
+ table-layout: fixed;
194
+
195
+ thead th {
196
+ position: relative;
197
+ height: var(--spacing-11);
198
+ padding: 0 var(--spacing-6);
199
+ font-size: var(--font-size-xs);
200
+ font-weight: var(--font-weight-semibold);
201
+ color: var(--color-foreground);
202
+ white-space: nowrap;
203
+ /* explicit background so sticky pinned cells mask scrolled content */
204
+ background: var(--color-muted);
205
+ /* visible: header checkbox focus ring not clipped */
206
+ overflow: visible;
207
+ }
208
+
209
+ tbody tr {
210
+ transition: background var(--transition-fast);
211
+
212
+ &:hover td {
213
+ background: var(--color-secondary);
214
+ }
215
+
216
+ &.row_selected td {
217
+ background: var(--color-muted);
218
+ }
219
+
220
+ &.row_group td {
221
+ background: var(--color-secondary);
222
+ font-weight: var(--font-weight-medium);
223
+ }
224
+
225
+ &.row_editing {
226
+ /* hover should not flicker the background of a row the user is editing */
227
+ &:hover td {
228
+ background: var(--color-background);
229
+ }
230
+
231
+ td {
232
+ background: var(--color-background);
233
+
234
+ .td_inner {
235
+ /* let inline-edit focus rings + dropdown shadows escape the cell */
236
+ overflow: visible;
237
+ text-overflow: clip;
238
+ }
239
+ }
240
+ }
241
+
242
+ &.row_dragging td {
243
+ opacity: 0.5;
244
+ background: var(--color-muted);
245
+ }
246
+
247
+ &.row_drop_target td {
248
+ box-shadow: inset 0 2px 0 0 var(--color-primary);
249
+ }
250
+
251
+ td {
252
+ padding: var(--spacing-5) var(--spacing-6);
253
+ font-size: var(--font-size-sm);
254
+ font-weight: var(--font-weight-normal);
255
+ color: var(--color-foreground);
256
+ vertical-align: middle;
257
+ border-bottom: var(--border-1) solid var(--color-border);
258
+ /* explicit background so sticky pinned cells mask scrolled content */
259
+ background: var(--color-background);
260
+ /* visible: focus rings + dropdowns escape the cell */
261
+ overflow: visible;
262
+ }
263
+ }
264
+ }
265
+
266
+ /* ─────────────────────────────────────────────
267
+ Vertical column borders (showVerticalBorders prop)
268
+ Drawn on the LEFT of every cell except the first, so we never get a
269
+ double border at the table's outer edge.
270
+ ───────────────────────────────────────────── */
271
+ .table_vertical_borders {
272
+ /* Only add vertical border if the previous cell is not the last left-pinned cell. Prevents double border with pinned columns. */
273
+ thead th:not(.col_pinned_last.col_pinned_left) + th,
274
+ tbody td:not(.col_pinned_last.col_pinned_left) + td {
275
+ border-left: var(--border-1) solid var(--color-border);
276
+ }
277
+ }
278
+
279
+ /* ─────────────────────────────────────────────
280
+ Header inner - flex container that hugs its content.
281
+ No `flex: 1` on the label; icons sit immediately next to the text and
282
+ `justify-content` (set by `.align_*`) determines start/end alignment.
283
+ ───────────────────────────────────────────── */
284
+ .th_inner {
285
+ width: 100%;
286
+ display: flex;
287
+ align-items: center;
288
+ min-height: var(--spacing-11);
289
+ gap: var(--spacing-2);
290
+ margin: 0;
291
+ padding: 0;
292
+ overflow: hidden;
293
+ }
294
+
295
+ .th_label {
296
+ flex: 0 1 auto;
297
+ min-width: 0;
298
+ max-width: 100%;
299
+ overflow: hidden;
300
+ text-overflow: ellipsis;
301
+ white-space: nowrap;
302
+ }
303
+
304
+ .th_actions {
305
+ display: inline-flex;
306
+ align-items: center;
307
+ gap: var(--spacing-1);
308
+ flex-shrink: 0;
309
+ }
310
+
311
+ .drag_handle {
312
+ width: 1rem;
313
+ height: 1rem;
314
+ display: inline-flex;
315
+ align-items: center;
316
+ justify-content: center;
317
+ color: var(--color-muted-foreground);
318
+ cursor: grab;
319
+ flex-shrink: 0;
320
+
321
+ &:active {
322
+ cursor: grabbing;
323
+ }
324
+ }
325
+
326
+ /* ─────────────────────────────────────────────
327
+ Compact icon-only button used for sort / group / pin in the header.
328
+ ───────────────────────────────────────────── */
329
+ .icon_btn {
330
+ /* !important needed: --button-icon is also set by :is(.ghost, .tone_ghost) in
331
+ button.module.css at equal specificity - CSS module load order is not guaranteed,
332
+ so !important on the var is the only reliable way to win without hardcoding svg color. */
333
+ --button-icon: var(--color-muted-foreground) !important;
334
+
335
+ &:hover:not(.disabled) {
336
+ --button-icon: var(--color-foreground) !important;
337
+ background-color: var(--color-border) !important;
338
+ }
339
+ }
340
+
341
+ /* ─────────────────────────────────────────────
342
+ Body cell inner - default truncates with ellipsis.
343
+ Override per-column with `meta.truncate = false` for cells hosting
344
+ inputs/buttons so focus rings and dropdowns aren't clipped.
345
+ ───────────────────────────────────────────── */
346
+ .td_inner {
347
+ overflow: hidden;
348
+ text-overflow: ellipsis;
349
+ white-space: nowrap;
350
+ /* outset breathing room mirrors the header's */
351
+ margin: calc(var(--spacing-2) * -1);
352
+ padding: var(--spacing-2);
353
+ }
354
+
355
+ .td_inner_no_truncate {
356
+ overflow: visible;
357
+ text-overflow: clip;
358
+ white-space: normal;
359
+ }
360
+
361
+ .selection_checkbox {
362
+ --native-control-size: var(--data-table-selection-control-size);
363
+
364
+ width: fit-content;
365
+ min-width: fit-content;
366
+ display: inline-flex;
367
+ align-items: center;
368
+ justify-content: center;
369
+ padding: 0;
370
+ margin: 0;
371
+
372
+ > * {
373
+ width: fit-content;
374
+ min-width: fit-content;
375
+ }
376
+
377
+ > * > * {
378
+ width: fit-content;
379
+ min-width: fit-content;
380
+ }
381
+ }
382
+
383
+ .col_select {
384
+ padding-left: var(--spacing-5) !important;
385
+ padding-right: var(--spacing-5) !important;
386
+
387
+ .th_inner,
388
+ .td_inner {
389
+ margin: 0;
390
+ padding: 0;
391
+ display: flex;
392
+ align-items: center;
393
+ justify-content: center;
394
+ gap: 0;
395
+ overflow: visible;
396
+ }
397
+ }
398
+
399
+ .group_anchor_cell {
400
+ text-align: left !important;
401
+
402
+ .td_inner {
403
+ display: flex;
404
+ align-items: center;
405
+ justify-content: flex-start;
406
+ }
407
+ }
408
+
409
+ /* ─────────────────────────────────────────────
410
+ Cell alignment via `column.meta.align`
411
+ - text-align handles inline text (truncate=true case)
412
+ - flex justify-content handles non-truncated content (action buttons, inputs)
413
+ - header is always a flex container so we always set justify-content there
414
+ ───────────────────────────────────────────── */
415
+ .align_left {
416
+ text-align: left;
417
+
418
+ .th_inner {
419
+ justify-content: flex-start;
420
+ }
421
+
422
+ .td_inner_no_truncate {
423
+ display: flex;
424
+ align-items: center;
425
+ justify-content: flex-start;
426
+ }
427
+ }
428
+
429
+ .align_center {
430
+ text-align: center;
431
+
432
+ .th_inner {
433
+ justify-content: center;
434
+ }
435
+
436
+ .td_inner_no_truncate {
437
+ display: flex;
438
+ align-items: center;
439
+ justify-content: center;
440
+ }
441
+ }
442
+
443
+ .align_right {
444
+ text-align: right;
445
+
446
+ .th_inner {
447
+ justify-content: flex-end;
448
+ }
449
+
450
+ .td_inner_no_truncate {
451
+ display: flex;
452
+ align-items: center;
453
+ justify-content: flex-end;
454
+ }
455
+ }
456
+
457
+ /* ─────────────────────────────────────────────
458
+ Column pinning - sticky offsets are written inline by JS
459
+ The hairline divider lives only on the LAST column of each pinning group
460
+ (`col_pinned_last`) so adjacent pinned columns don't get double dividers.
461
+ ───────────────────────────────────────────── */
462
+ .table th.col_pinned_left,
463
+ .table th.col_pinned_right,
464
+ .table td.col_pinned_left,
465
+ .table td.col_pinned_right {
466
+ position: sticky;
467
+ z-index: 1;
468
+
469
+ /* To ensure pinned columns are visible above other content */
470
+ background-color: var(--color-muted);
471
+ }
472
+
473
+ .table thead th.col_pinned_left,
474
+ .table thead th.col_pinned_right {
475
+ z-index: 2;
476
+ }
477
+
478
+ /* Pinned left columns: add a right border only to the last pinned cell to act as a divider. */
479
+ .col_pinned_last.col_pinned_left {
480
+ border-right: var(--border-1) solid var(--color-border);
481
+ }
482
+
483
+ /* Pinned right columns: add a left border only to the last pinned cell to act as a divider. */
484
+ .col_pinned_last.col_pinned_right {
485
+ border-left: var(--border-1) solid var(--color-border);
486
+ }
487
+
488
+ /* Keep interactive cell popovers above sticky neighbors when they are rendered
489
+ in-place (non-portal mode). */
490
+ .table tbody td:focus-within {
491
+ position: relative;
492
+ z-index: 3;
493
+ }
494
+
495
+ /* ─────────────────────────────────────────────
496
+ Drag-and-drop column reordering
497
+ ───────────────────────────────────────────── */
498
+ .col_dragging {
499
+ opacity: 0.5;
500
+ }
501
+
502
+ .col_drop_target {
503
+ /* a slim vertical bar on the leading edge of the drop target */
504
+ outline: 2px solid var(--color-primary);
505
+ outline-offset: -2px;
506
+ }
507
+
508
+ /* ─────────────────────────────────────────────
509
+ Column resize handle - narrow vertical strip on the cell's right edge
510
+ ───────────────────────────────────────────── */
511
+ .resize_handle {
512
+ position: absolute;
513
+ top: 0;
514
+ right: 0;
515
+ bottom: 0;
516
+ width: 5px;
517
+ cursor: col-resize;
518
+ user-select: none;
519
+ touch-action: none;
520
+ background: transparent;
521
+ transition: background var(--transition-fast);
522
+
523
+ &:hover,
524
+ &.resize_handle_active {
525
+ background: var(--color-primary);
526
+ }
527
+ }
528
+
529
+ /* ─────────────────────────────────────────────
530
+ Group rows + toggle
531
+ ───────────────────────────────────────────── */
532
+ .group_toggle {
533
+ display: inline-flex;
534
+ align-items: center;
535
+ gap: var(--spacing-3);
536
+ padding: 0;
537
+ border: none;
538
+ background: transparent;
539
+ color: var(--color-foreground);
540
+ font: inherit;
541
+ cursor: pointer;
542
+ }
543
+
544
+ .group_chevron {
545
+ transition: transform var(--transition-fast);
546
+ flex-shrink: 0;
547
+ }
548
+
549
+ .group_chevron_open {
550
+ transform: rotate(90deg);
551
+ }
552
+
553
+ .group_value {
554
+ font-weight: var(--font-weight-semibold);
555
+ }
556
+
557
+ .group_count {
558
+ color: var(--color-muted-foreground);
559
+ font-size: var(--font-size-xs);
560
+ }
561
+
562
+ .cell_aggregated {
563
+ font-weight: var(--font-weight-semibold);
564
+ }
565
+
566
+ /* ─────────────────────────────────────────────
567
+ Loading / empty states
568
+ ───────────────────────────────────────────── */
569
+ .state_overlay {
570
+ width: 100%;
571
+ padding: var(--spacing-14) var(--spacing-8);
572
+ display: flex;
573
+ align-items: center;
574
+ justify-content: center;
575
+ }
576
+
577
+ .empty_msg {
578
+ font-size: var(--font-size-sm);
579
+ color: var(--color-muted-foreground);
580
+ }
581
+
582
+ /* ─────────────────────────────────────────────
583
+ Pagination bar
584
+ margin-top:auto pushes it to the absolute bottom of content_wrap
585
+ no matter how few rows are visible.
586
+ flex-shrink:0 stops it compressing when space is tight.
587
+ ───────────────────────────────────────────── */
588
+ .pagination_bar {
589
+ flex-shrink: 0;
590
+ margin-top: auto;
591
+ width: 100%;
592
+ display: flex;
593
+ align-items: center;
594
+ justify-content: space-between;
595
+ gap: var(--spacing-8);
596
+ min-width: 0;
597
+ /* When the count + controls can't fit, the WHOLE bar scrolls horizontally as
598
+ one strip rather than wrapping or clipping. The outlet hides X overflow, so
599
+ letting a cluster shrink would just clip the nav buttons - scroll the bar
600
+ instead. `space-between` still spreads them when there IS room. The vertical
601
+ padding gives the buttons' focus rings room so `overflow-x: auto` (which also
602
+ clips the Y axis) doesn't crop them. */
603
+ overflow-x: auto;
604
+ scrollbar-width: thin;
605
+ scrollbar-color: var(--color-border) transparent;
606
+ padding-block: var(--spacing-2);
607
+
608
+ > * {
609
+ flex-shrink: 0;
610
+ }
611
+ }
612
+
613
+ @media (--sm) {
614
+ .pagination_bar {
615
+ gap: var(--spacing-5);
616
+ }
617
+ }
618
+
619
+ .pagination_bar_loading {
620
+ flex-wrap: wrap;
621
+ row-gap: var(--spacing-4);
622
+ }
623
+
624
+ .loading_select_skeleton {
625
+ border-radius: var(--radius-sm);
626
+ flex-shrink: 0;
627
+ }
628
+
629
+ .pagination_left {
630
+ display: flex;
631
+ align-items: center;
632
+ gap: var(--spacing-8);
633
+ min-width: 0;
634
+ }
635
+
636
+ .pagination_left_loading {
637
+ overflow: hidden;
638
+ }
639
+
640
+ .pagination_count {
641
+ font-size: var(--font-size-sm);
642
+ color: var(--color-muted-foreground);
643
+ white-space: nowrap;
644
+
645
+ span {
646
+ font-weight: var(--font-weight-medium);
647
+ color: var(--color-foreground);
648
+ }
649
+ }
650
+
651
+ .pagination_controls {
652
+ display: flex;
653
+ align-items: center;
654
+ gap: var(--spacing-8);
655
+ min-width: 0;
656
+ }
657
+
658
+ @media (--sm) {
659
+ .pagination_controls {
660
+ gap: var(--spacing-5);
661
+ }
662
+ }
663
+
664
+ .pagination_controls_loading {
665
+ flex-direction: row;
666
+ flex-wrap: wrap;
667
+ align-items: center;
668
+ justify-content: flex-end;
669
+ max-width: 100%;
670
+ row-gap: var(--spacing-4);
671
+ }
672
+
673
+ .pagination_controls_loading > * {
674
+ flex: 0 1 auto;
675
+ min-width: 0;
676
+ max-width: 100%;
677
+ }
678
+
679
+ .pagination_text_skeleton {
680
+ height: 1em;
681
+ min-width: 0;
682
+ max-width: 100%;
683
+ flex: 0 1 auto;
684
+ }
685
+
686
+ .pagination_select_skeleton,
687
+ .pagination_button_skeleton {
688
+ width: var(--spacing-11);
689
+ height: var(--spacing-11);
690
+ min-width: 0;
691
+ }
692
+
693
+ .pagination_select_skeleton {
694
+ flex: 0 1 auto;
695
+ }
696
+
697
+ .pagination_button_skeleton {
698
+ flex: 0 0 auto;
699
+ }
700
+
701
+ .page_size_wrap {
702
+ display: flex;
703
+ align-items: center;
704
+ gap: var(--spacing-5);
705
+ min-width: 0;
706
+ flex-shrink: 1;
707
+ max-width: 100%;
708
+ }
709
+
710
+ .page_size_label {
711
+ font-size: var(--font-size-sm);
712
+ color: var(--color-muted-foreground);
713
+ white-space: nowrap;
714
+ }
715
+
716
+ .page_indicator {
717
+ font-size: var(--font-size-sm);
718
+ color: var(--color-muted-foreground);
719
+ white-space: nowrap;
720
+ min-width: 6rem;
721
+ text-align: center;
722
+
723
+ span {
724
+ font-weight: var(--font-weight-medium);
725
+ color: var(--color-foreground);
726
+ }
727
+ }
728
+
729
+ .page_nav {
730
+ display: flex;
731
+ align-items: center;
732
+ gap: var(--spacing-2);
733
+ flex-shrink: 0;
734
+ }
735
+
736
+ .page_nav_loading {
737
+ flex-wrap: wrap;
738
+ justify-content: flex-end;
739
+ row-gap: var(--spacing-2);
740
+ min-width: 0;
741
+ max-width: 100%;
742
+ flex-shrink: 1;
743
+ }