@attrus-ds/core 1.0.13 → 1.0.17

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.
@@ -12,14 +12,14 @@
12
12
  [.dt-pagination] bottom — page nav + range
13
13
 
14
14
  Sizes:
15
- .dt--sm compact — 8 / 12 padding, fs-xs body, fs-xxs head
15
+ .dt-sm compact — 8 / 12 padding, fs-xs body, fs-xxs head
16
16
  default standard — 12 / 16 padding, fs-md body, fs-xxs head
17
- .dt--lg spacious — 16 / 20 padding, fs-md body, fs-xs head
17
+ .dt-lg spacious — 16 / 20 padding, fs-md body, fs-xs head
18
18
 
19
19
  Density helpers:
20
- .dt--zebra alternating row backgrounds
21
- .dt--sticky thead sticks to top while scrolling
22
- .dt--bordered vertical column borders
20
+ .dt-zebra alternating row backgrounds
21
+ .dt-sticky thead sticks to top while scrolling
22
+ .dt-bordered vertical column borders
23
23
 
24
24
  Cell helpers:
25
25
  .dt-cell-stack primary line + sub line
@@ -63,8 +63,11 @@
63
63
  display: flex;
64
64
  align-items: center;
65
65
  gap: var(--space-4);
66
- padding: var(--space-4) var(--space-6);
67
- min-height: 53px;
66
+ /* Box on the element, not on a descendant selector: the height must survive a
67
+ toolbar that is not a direct child of .dt-wrap, or the selecting state
68
+ renders 5px taller and the table jumps when the first row is ticked. */
69
+ height: 52px;
70
+ padding: 0 var(--space-5);
68
71
  box-sizing: border-box;
69
72
  border-bottom: 1px solid var(--color-border-subtle);
70
73
  background: var(--color-background-surface);
@@ -120,8 +123,8 @@
120
123
  display: inline-flex;
121
124
  align-items: center;
122
125
  justify-content: center;
123
- height: 28px;
124
- min-width: 28px;
126
+ height: var(--control-h-xs);
127
+ min-width: var(--control-h-xs);
125
128
  padding: 0 var(--space-3);
126
129
  border: 1px solid var(--color-border-default);
127
130
  background: var(--color-background-surface);
@@ -137,6 +140,19 @@
137
140
  border-color var(--transition-fast),
138
141
  color var(--transition-fast);
139
142
  }
143
+ /* The ellipsis is not a control: no box, no hover, no tab stop — a boxed gap
144
+ would read as a page you can go to. */
145
+ .dt-pagination .page-gap {
146
+ display: inline-flex;
147
+ align-items: center;
148
+ justify-content: center;
149
+ min-width: var(--space-5);
150
+ font-family: var(--font-mono);
151
+ font-size-adjust: var(--font-mono-adjust);
152
+ font-size: var(--fs-xs);
153
+ color: var(--color-foreground-muted);
154
+ user-select: none;
155
+ }
140
156
  .dt-pagination .page-btn:hover { background: var(--color-background-hover); color: var(--color-foreground-default); }
141
157
  .dt-pagination .page-btn:disabled { opacity: 0.4; cursor: not-allowed; }
142
158
  .dt-pagination .page-btn.is-current {
@@ -185,23 +201,65 @@
185
201
 
186
202
  /* Sortable header */
187
203
  .dt-th-sort {
204
+ /* A sortable header must be keyboard-operable, so it is a <button> — which
205
+ arrives with the UA's Arial, grey fill and 2px border. Reset those and
206
+ inherit the <th>'s own typography, so the control looks like the header it
207
+ replaces rather than a button parked inside one. */
208
+ appearance: none;
209
+ background: none;
210
+ border: 0;
211
+ padding: 0;
212
+ margin: 0;
213
+ font: inherit;
214
+ letter-spacing: inherit;
215
+ text-transform: inherit;
216
+ color: inherit;
217
+ text-align: inherit;
218
+ display: inline-flex;
219
+ align-items: center;
188
220
  cursor: pointer;
189
221
  user-select: none;
190
222
  transition: color var(--transition-fast);
191
223
  }
224
+ .dt-th-sort:focus-visible { outline: none; box-shadow: var(--shadow-focus); border-radius: var(--radius-xs); }
192
225
  .dt-th-sort:hover { color: var(--color-foreground-default); }
226
+ .dt-th-sort { gap: var(--space-2); }
227
+ /* The chevron is a CLIPPED SLOT holding both arrows stacked, and the track
228
+ slides between them. Rotating a single chevron 180° also reads as a direction
229
+ change, but it spins through horizontal — a moment where the glyph points at
230
+ neither value. Sliding keeps an arrow pointing the whole way: the outgoing one
231
+ leaves in the direction it means, the incoming one arrives from the opposite
232
+ edge, so the motion itself carries "ascending" or "descending".
233
+ Slot height must equal each arrow's height, or the track lands off-centre. */
193
234
  .dt-th-sort .chev {
194
- display: inline-flex;
195
- width: 10px;
196
- height: 10px;
235
+ display: block;
236
+ width: var(--sort-chev, 10px);
237
+ height: var(--sort-chev, 10px);
197
238
  margin-left: var(--space-2);
198
- vertical-align: -1px;
199
- color: var(--color-foreground-placeholder);
200
- stroke-width: var(--icon-stroke-bold);
201
- transition: color var(--transition-fast), transform var(--transition-fast);
239
+ flex-shrink: 0;
240
+ overflow: hidden;
241
+ color: var(--color-foreground-muted);
242
+ transition: color var(--transition-fast);
243
+ }
244
+ .dt-th-sort .chev-track {
245
+ display: block;
246
+ /* Unsorted rests on the down arrow — the conventional "click to sort" hint. */
247
+ transform: translateY(calc(-1 * var(--sort-chev, 10px)));
248
+ transition: transform var(--duration-base) var(--ease-standard);
202
249
  }
203
- .dt-th-sort.is-asc .chev { color: var(--color-foreground-accent); transform: rotate(180deg); }
250
+ .dt-th-sort .chev-track > svg,
251
+ .dt-th-sort .chev-track > i {
252
+ display: block;
253
+ width: var(--sort-chev, 10px);
254
+ height: var(--sort-chev, 10px);
255
+ }
256
+ .dt-th-sort.is-asc .chev-track { transform: translateY(0); }
257
+ .dt-th-sort.is-desc .chev-track { transform: translateY(calc(-1 * var(--sort-chev, 10px))); }
258
+ .dt-th-sort.is-asc .chev,
204
259
  .dt-th-sort.is-desc .chev { color: var(--color-foreground-accent); }
260
+ @media (prefers-reduced-motion: reduce) {
261
+ .dt-th-sort .chev-track { transition: none; }
262
+ }
205
263
 
206
264
  /* ----------------------------------------------------------------
207
265
  4. BODY ROWS
@@ -305,8 +363,30 @@
305
363
  .dt-cell-lead > .ic-round > i, .dt-cell-lead > .ic-round > svg,
306
364
  .dt-cell-lead > .ic-round > svg { width: var(--icon-sm); height: var(--icon-sm); stroke-width: var(--icon-stroke-bold); }
307
365
 
366
+ /* Trailing round icon — same badge, after the label.
367
+ Leading answers "what kind of row is this" and is scanned down the column;
368
+ trailing annotates the label itself ("verified", "has a note"), so it must
369
+ not enter the vertical scan. Hence margin-left:auto is NOT used: the badge
370
+ hugs the text instead of floating to the cell edge, where it would read as
371
+ a separate column. */
372
+ .dt-cell-lead > .ic-round.is-trailing { order: 2; margin-left: var(--space-1); }
373
+ .dt-cell-lead > .ic-round.is-sm { width: 20px; height: 20px; }
374
+ .dt-cell-lead > .ic-round.is-sm > i,
375
+ .dt-cell-lead > .ic-round.is-sm > svg { width: 11px; height: 11px; }
376
+ /* The label must keep the shrink so a long name truncates before the badge */
377
+ .dt-cell-lead > .ic-round.is-trailing ~ * { min-width: 0; }
378
+
308
379
  /* Numbers — explicit class wins over per-cell .right alignment */
309
380
  .dt-num {
381
+ /* Amounts are what the eye hunts for in a ledger, so they carry the emphasis:
382
+ mono ExtraBold (--w-mono-emphasis). A monospaced face cannot widen — it must
383
+ keep its cell — so weight there buys ink, not width: which is why 800 reads
384
+ as emphasis without shouting.
385
+ The currency chip and any sub line stay regular — see below — so the pair
386
+ gives one strong figure against quiet context instead of a bold block. */
387
+ font-family: var(--font-mono);
388
+ font-size-adjust: var(--font-mono-adjust);
389
+ font-weight: var(--w-mono-emphasis);
310
390
  font-variant-numeric: tabular-nums;
311
391
  font-feature-settings: 'tnum' on, 'lnum' on;
312
392
  text-align: right;
@@ -317,13 +397,16 @@
317
397
  by specificity); stacked cells in a numeric column align flex to the right. */
318
398
  .dt thead th.dt-num { text-align: right; }
319
399
  .dt td.dt-num .dt-cell-stack { align-items: flex-end; }
400
+ /* Context under an amount stays regular, or the whole cell reads as emphasis */
401
+ .dt td.dt-num .dt-cell-stack .sub { font-weight: var(--w-regular); }
320
402
  .dt-num .ccy {
321
403
  color: var(--color-foreground-muted);
322
404
  font-weight: var(--w-regular);
323
405
  margin-right: var(--space-2);
324
406
  font-size: var(--fs-sm);
325
407
  }
326
- .dt-num.bold { font-weight: var(--w-bold); font-size: var(--fs-base); }
408
+ /* .bold now only steps the SIZE up — weight is already bold by default */
409
+ .dt-num.bold { font-size: var(--fs-base); }
327
410
  .dt-num.delta-pos { color: var(--color-feedback-success-foreground); }
328
411
  .dt-num.delta-neg { color: var(--color-feedback-danger-foreground); }
329
412
 
@@ -367,30 +450,30 @@
367
450
  /* ----------------------------------------------------------------
368
451
  6. SIZES
369
452
  ---------------------------------------------------------------- */
370
- .dt--sm thead th { padding: var(--space-2) var(--space-4); }
371
- .dt--sm tbody td { padding: var(--space-3) var(--space-4); font-size: var(--fs-sm); }
372
- .dt--sm .dt-cell-stack > .pri { font-size: var(--fs-sm); }
373
- .dt--sm .dt-cell-stack > .sub { font-size: var(--fs-xxs); }
374
- .dt--sm .dt-cell-lead > .ic-tile { width: 28px; height: 28px; border-radius: var(--radius-sm); }
375
- .dt--sm .dt-cell-lead > .ic-tile > i, .dt--sm .dt-cell-lead > .ic-tile > svg,
376
- .dt--sm .dt-cell-lead > .ic-tile > svg { width: var(--icon-sm); height: var(--icon-sm); }
453
+ .dt-sm thead th { padding: var(--space-2) var(--space-4); }
454
+ .dt-sm tbody td { padding: var(--space-3) var(--space-4); font-size: var(--fs-sm); }
455
+ .dt-sm .dt-cell-stack > .pri { font-size: var(--fs-sm); }
456
+ .dt-sm .dt-cell-stack > .sub { font-size: var(--fs-xxs); }
457
+ .dt-sm .dt-cell-lead > .ic-tile { width: 28px; height: var(--control-h-xs); border-radius: var(--radius-sm); }
458
+ .dt-sm .dt-cell-lead > .ic-tile > i, .dt-sm .dt-cell-lead > .ic-tile > svg,
459
+ .dt-sm .dt-cell-lead > .ic-tile > svg { width: var(--icon-sm); height: var(--icon-sm); }
377
460
 
378
- .dt--lg thead th { padding: var(--space-4) var(--space-6); font-size: var(--fs-xs); }
379
- .dt--lg tbody td { padding: var(--space-5) var(--space-6); font-size: var(--fs-base); }
380
- .dt--lg .dt-cell-stack > .pri { font-size: var(--fs-base); }
381
- .dt--lg .dt-cell-lead > .ic-tile { width: 40px; height: 40px; }
461
+ .dt-lg thead th { padding: var(--space-4) var(--space-6); font-size: var(--fs-xs); }
462
+ .dt-lg tbody td { padding: var(--space-5) var(--space-6); font-size: var(--fs-base); }
463
+ .dt-lg .dt-cell-stack > .pri { font-size: var(--fs-base); }
464
+ .dt-lg .dt-cell-lead > .ic-tile { width: 40px; height: 40px; }
382
465
 
383
466
 
384
467
  /* ----------------------------------------------------------------
385
468
  7. DENSITY HELPERS
386
469
  ---------------------------------------------------------------- */
387
470
  /* Zebra — alternating bg */
388
- .dt--zebra tbody tr:nth-child(even):not(.is-selected):not(:hover) {
471
+ .dt-zebra tbody tr:nth-child(even):not(.is-selected):not(:hover) {
389
472
  background: color-mix(in srgb, var(--color-background-subtle) 55%, var(--color-background-surface));
390
473
  }
391
474
 
392
475
  /* Sticky head */
393
- .dt--sticky thead th {
476
+ .dt-sticky thead th {
394
477
  position: sticky;
395
478
  top: 0;
396
479
  z-index: 1;
@@ -399,8 +482,8 @@
399
482
  }
400
483
 
401
484
  /* Vertical column borders */
402
- .dt--bordered th + th,
403
- .dt--bordered td + td {
485
+ .dt-bordered th + th,
486
+ .dt-bordered td + td {
404
487
  border-left: 1px solid var(--color-border-subtle);
405
488
  }
406
489
 
@@ -547,15 +630,24 @@
547
630
  .dt-responsive .dt td { display: block; width: 100%; box-sizing: border-box; }
548
631
 
549
632
  /* Each row becomes a card */
633
+ .dt-responsive .dt tbody { padding-top: var(--space-3); }
550
634
  .dt-responsive .dt tbody tr {
551
635
  border: 1px solid var(--color-border-default);
552
636
  border-radius: var(--radius-md);
553
637
  background: var(--color-background-surface);
554
638
  padding: var(--space-2) var(--space-4);
555
- margin-bottom: var(--space-3);
639
+ /* Inset horizontally, not just vertically. A card flush against the wrapper
640
+ edge reads as a table row that lost its table, and its own border lands
641
+ exactly on the wrapper's — two hairlines touching. The gutter is what makes
642
+ the stack read as cards. */
643
+ margin: 0 var(--space-3) var(--space-3);
644
+ /* Not width:100% — the block rule above sets it, and with side margins the
645
+ card would measure 100% PLUS the gutters and hang off the right edge.
646
+ auto lets it shrink to the space the margins leave. */
647
+ width: auto;
556
648
  box-shadow: var(--shadow-xs);
557
649
  }
558
- .dt-responsive .dt tbody tr:last-child { margin-bottom: 0; border-bottom: 1px solid var(--color-border-default); }
650
+ .dt-responsive .dt tbody tr:last-child { margin-bottom: var(--space-3); border-bottom: 1px solid var(--color-border-default); }
559
651
 
560
652
  /* Each cell becomes a label : value line */
561
653
  .dt-responsive .dt tbody td {
@@ -752,7 +844,9 @@
752
844
  visibly hanging off its parent card. */
753
845
  @media (max-width: 900px) {
754
846
  .dt-responsive .dt tbody tr.dt-subrow {
755
- margin: calc(-1 * var(--space-3)) var(--space-3) var(--space-3);
847
+ /* Pulls up under its parent card (which now carries a bottom gutter) and
848
+ keeps the same side inset, so parent and child read as one stack. */
849
+ margin: calc(-1 * var(--space-3)) var(--space-5) var(--space-3);
756
850
  border-radius: var(--shape-control);
757
851
  border: 1px solid var(--color-border-default);
758
852
  border-top: 0;
@@ -761,3 +855,475 @@
761
855
  .dt-responsive .dt tbody tr.dt-subrow > td { border-bottom: 0; padding-left: var(--space-4); }
762
856
  .dt-responsive .dt tbody tr.dt-subrow > td::before { display: none; }
763
857
  }
858
+
859
+ /* ----------------------------------------------------------------
860
+ 12b. COLLAPSIBLE SUBROWS — disclosure + animated reveal
861
+ ----------------------------------------------------------------
862
+ Same subrow vocabulary as §12, but the group starts closed and the parent
863
+ carries a disclosure control.
864
+
865
+ Structure:
866
+ <tr class="has-subrows is-collapsible is-open">
867
+ <td>… <button class="dt-subrow-toggle" aria-expanded="true"
868
+ aria-controls="grp-1">chevron</button> …
869
+ <tr class="dt-subrow is-collapsible" id="grp-1">
870
+ <td><div class="dt-subrow-inner"><div class="dt-subrow-cell">…</div></div></td>
871
+
872
+ WHY THE DOUBLE WRAPPER: a <tr> cannot be animated. height/max-height on a row
873
+ is ignored by table layout, and display:grid on a <td> destroys column
874
+ alignment. So the animation lives on a wrapper INSIDE the cell:
875
+ .dt-subrow-inner is a grid animating grid-template-rows 0fr -> 1fr, and its
876
+ single child clips the overflow. That is the one height transition that needs
877
+ no magic pixel value, so a subrow can hold two lines on mobile and still
878
+ animate correctly.
879
+
880
+ Cell padding also lives on .dt-subrow-cell rather than the <td>: padding on
881
+ the cell would keep the row 24px tall when closed, leaving a visible gap.
882
+
883
+ Support: grid-template-rows interpolation needs Chrome 117+, Safari 17.4+,
884
+ Firefox 127+. Older engines skip the tween and snap open/closed — the
885
+ disclosure still works, it just does not animate.
886
+ ---------------------------------------------------------------- */
887
+
888
+ /* Closed by default; .is-open on the SUBROW drives the reveal */
889
+ .dt tbody tr.dt-subrow.is-collapsible > td {
890
+ padding-top: 0;
891
+ padding-bottom: 0;
892
+ }
893
+ /* The reveal animates max-height on a clipped wrapper.
894
+ The modern `grid-template-rows: 0fr -> 1fr` pattern was tried first and does
895
+ NOT work inside a table cell: a <td> has no definite height, so 1fr has
896
+ nothing to resolve against and computes to 0px — the group stays shut. That is
897
+ a property of table layout, not a bug in the pattern.
898
+ The cost of max-height is a cap: --subrow-max is the tallest a subrow may grow
899
+ before it starts clipping. It is generous enough for two wrapped lines, and it
900
+ is a token so a product with taller subrows can raise it. */
901
+ .dt-subrow-inner {
902
+ overflow: hidden;
903
+ max-height: 0;
904
+ transition: max-height var(--duration-slow) var(--ease-standard);
905
+ }
906
+ .dt tbody tr.dt-subrow.is-open .dt-subrow-inner { max-height: 120px; } /* literal on purpose: a token here breaks until the cached colors_and_type.css refreshes */
907
+
908
+ /* The padding the <td> gave up */
909
+ .dt-subrow-cell {
910
+ /* Padding starts at zero and arrives with the reveal. A grid item's padding
911
+ counts toward its automatic minimum size, so 8px of vertical padding floors
912
+ the 0fr track at 16px and leaves a gap under a closed group — min-height:0
913
+ does not remove it. Transitioning the padding alongside the track is what
914
+ lets the row reach a true zero. */
915
+ padding: 0;
916
+ display: flex;
917
+ align-items: center;
918
+ gap: var(--space-3);
919
+ min-width: 0;
920
+ /* Fade trails the height so the text does not appear before it has room */
921
+ opacity: 0;
922
+ /* Only opacity transitions. Animating padding too made the JS height read
923
+ race it: scrollHeight was sampled while padding was still ~0, so the target
924
+ undershot by the padding and clipped the last line. A padding snap is
925
+ invisible inside a clipped box. */
926
+ transition: opacity var(--duration-fast) var(--ease-standard);
927
+ }
928
+ .dt tbody tr.dt-subrow.is-open .dt-subrow-cell {
929
+ padding: var(--space-3) 0;
930
+ opacity: 1;
931
+ }
932
+
933
+ /* A closed group must not glue to the parent — the parent keeps its own border
934
+ until the group opens, otherwise two collapsed rows read as one. */
935
+ .dt tbody tr.has-subrows.is-collapsible:not(.is-open) > td {
936
+ border-bottom: 1px solid var(--color-border-default);
937
+ }
938
+
939
+ /* Disclosure control — sits in the parent's lead cell */
940
+ .dt-subrow-toggle {
941
+ flex-shrink: 0;
942
+ width: 20px; height: 20px;
943
+ display: inline-flex; align-items: center; justify-content: center;
944
+ padding: 0;
945
+ border: 0;
946
+ border-radius: var(--radius-2xs);
947
+ background: transparent;
948
+ color: var(--color-foreground-muted);
949
+ cursor: pointer;
950
+ transition: background var(--transition-fast), color var(--transition-fast);
951
+ }
952
+ .dt-subrow-toggle:hover { background: var(--color-background-hover); color: var(--color-foreground-default); }
953
+ .dt-subrow-toggle:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
954
+ .dt-subrow-toggle > i,
955
+ .dt-subrow-toggle > svg {
956
+ width: 14px; height: 14px;
957
+ stroke-width: var(--icon-stroke);
958
+ transition: transform var(--duration-fast) var(--ease-standard);
959
+ }
960
+ .dt-subrow-toggle[aria-expanded="true"] > i,
961
+ .dt-subrow-toggle[aria-expanded="true"] > svg { transform: rotate(90deg); }
962
+
963
+ /* Count badge beside the toggle — tells the user what is hidden before opening */
964
+ .dt-subrow-count {
965
+ font-family: var(--font-mono); font-size-adjust: var(--font-mono-adjust);
966
+ font-size: var(--fs-xxs);
967
+ color: var(--color-foreground-muted);
968
+ background: var(--color-background-subtle);
969
+ border: 1px solid var(--color-border-default);
970
+ border-radius: var(--radius-pill);
971
+ padding: 0 var(--space-2);
972
+ flex-shrink: 0;
973
+ }
974
+
975
+ .dt.on-inverse .dt-subrow-toggle,
976
+ .dark .dt .dt-subrow-toggle { color: var(--color-foreground-inverse-muted); }
977
+ .dt.on-inverse .dt-subrow-toggle:hover,
978
+ .dark .dt .dt-subrow-toggle:hover {
979
+ background: color-mix(in srgb, var(--color-pure-white) 8%, transparent);
980
+ color: var(--color-foreground-inverse-default);
981
+ }
982
+ .dt.on-inverse .dt-subrow-toggle:focus-visible,
983
+ .dark .dt .dt-subrow-toggle:focus-visible { box-shadow: var(--shadow-focus-inverse); }
984
+ .dt.on-inverse .dt-subrow-count,
985
+ .dark .dt .dt-subrow-count {
986
+ background: transparent;
987
+ border-color: var(--color-border-inverse);
988
+ color: var(--color-foreground-inverse-muted);
989
+ }
990
+
991
+ @media (prefers-reduced-motion: reduce) {
992
+ .dt-subrow-inner,
993
+ .dt-subrow-cell,
994
+ .dt-subrow-toggle > i,
995
+ .dt-subrow-toggle > svg { transition: none; }
996
+ }
997
+
998
+ /* Mobile: the animation wrapper survives the card layout, but the inner cell
999
+ stacks instead of sitting on one line. */
1000
+ @media (max-width: 900px) {
1001
+ .dt-responsive .dt tbody tr.dt-subrow.is-collapsible > td { padding-top: 0; padding-bottom: 0; }
1002
+ .dt-responsive .dt-subrow-cell { flex-wrap: wrap; padding: var(--space-3) 0; }
1003
+ }
1004
+
1005
+ /* ----------------------------------------------------------------
1006
+ 12c. ROW AS CONTROL — the whole parent row toggles the group
1007
+ ----------------------------------------------------------------
1008
+ With role="button" + tabindex on the <tr>, the row needs what a button has:
1009
+ a pointer, and a visible focus ring. The chevron inside is aria-hidden and
1010
+ pointer-events:none — otherwise one action would take two tab stops and the
1011
+ click would land twice.
1012
+ ---------------------------------------------------------------- */
1013
+ .dt tbody tr.has-subrows.is-clickable { cursor: pointer; }
1014
+ .dt tbody tr.has-subrows.is-clickable:focus-visible { outline: none; }
1015
+ .dt tbody tr.has-subrows.is-clickable:focus-visible > td { background: var(--color-background-hover); }
1016
+ .dt tbody tr.has-subrows.is-clickable:focus-visible > td:first-child { box-shadow: inset 3px 0 0 0 var(--color-border-focus); }
1017
+ .dt tbody tr.has-subrows.is-clickable .dt-subrow-toggle { pointer-events: none; }
1018
+ .dt tbody tr.has-subrows.is-clickable[aria-expanded="true"] .dt-subrow-toggle > svg { transform: rotate(90deg); }
1019
+
1020
+ /* ----------------------------------------------------------------
1021
+ 12d. DETAIL PANEL — a child that is a DIFFERENT KIND of thing
1022
+ ----------------------------------------------------------------
1023
+ The other half of the hierarchy story. Use §12/§12b (.dt-subrow) when the
1024
+ child shares the parent's columns because it is the same kind of thing — a
1025
+ fee under a payment, a leg under a transfer: you want to scan the Amount
1026
+ column and see both. Use THIS when the child has its own attributes and the
1027
+ parent's headers would be lies: an audit trail (timestamp / actor / action),
1028
+ KYC documents, delivery steps.
1029
+
1030
+ Structure:
1031
+ <tr class="has-detail is-clickable" role="button" tabindex="0"
1032
+ aria-expanded="false" aria-controls="d1">
1033
+ <tr class="dt-detail" id="d1">
1034
+ <td colspan="5">
1035
+ <div class="dt-subrow-inner"><div class="dt-detail-body"> … </div></div>
1036
+
1037
+ The reveal mechanism is shared with §12b (the wrapper + max-height written by
1038
+ the handler), so this costs no new animation code. What changes is the cell:
1039
+ one colspan cell instead of one per column, which frees the layout entirely.
1040
+
1041
+ Inside, pick one:
1042
+ .dt-detail-grid key/value pairs — the default for record detail
1043
+ .dt table a nested table WITH ITS OWN <thead>, when children are
1044
+ many and need real column semantics
1045
+ free markup timeline, form, chart
1046
+ ---------------------------------------------------------------- */
1047
+
1048
+ .dt tbody tr.dt-detail > td {
1049
+ padding: 0 0 0 calc(var(--space-5) + var(--subrow-indent));
1050
+ background: var(--color-background-subtle);
1051
+ border-bottom: 1px solid var(--color-border-default);
1052
+ }
1053
+ /* Glued to its parent, like a subrow: the pair reads as one block */
1054
+ .dt tbody tr.has-detail.is-open > td { border-bottom: 0; }
1055
+
1056
+ .dt-detail-body {
1057
+ padding: var(--space-4) var(--space-5) var(--space-5) 0;
1058
+ /* Padding starts at zero and arrives with the reveal — same reason as
1059
+ .dt-subrow-cell: a wrapper child's padding floors the collapsed height. */
1060
+ padding: 0;
1061
+ opacity: 0;
1062
+ transition: opacity var(--duration-base) var(--ease-standard);
1063
+ }
1064
+ .dt tbody tr.dt-detail.is-open .dt-detail-body {
1065
+ padding: var(--space-4) var(--space-5) var(--space-5) 0;
1066
+ opacity: 1;
1067
+ }
1068
+
1069
+ /* Optional heading above the panel content */
1070
+ .dt-detail-title {
1071
+ font-family: var(--font-mono);
1072
+ font-size-adjust: var(--font-mono-adjust);
1073
+ font-size: var(--fs-xxs);
1074
+ font-weight: var(--w-regular);
1075
+ letter-spacing: var(--ls-caps);
1076
+ text-transform: uppercase;
1077
+ color: var(--color-foreground-muted);
1078
+ margin-bottom: var(--space-3);
1079
+ }
1080
+
1081
+ /* Key/value grid — the default panel content */
1082
+ .dt-detail-grid {
1083
+ display: grid;
1084
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
1085
+ gap: var(--space-4) var(--space-6);
1086
+ }
1087
+ .dt-detail-grid > div { min-width: 0; }
1088
+ .dt-detail-grid dt {
1089
+ font-family: var(--font-mono);
1090
+ font-size-adjust: var(--font-mono-adjust);
1091
+ font-size: var(--fs-xxs);
1092
+ letter-spacing: var(--ls-caps);
1093
+ text-transform: uppercase;
1094
+ color: var(--color-foreground-muted);
1095
+ margin-bottom: 2px;
1096
+ }
1097
+ .dt-detail-grid dd {
1098
+ margin: 0;
1099
+ font-size: var(--fs-sm);
1100
+ color: var(--color-foreground-default);
1101
+ }
1102
+
1103
+ /* A nested table inside the panel keeps its own headers — that is the whole
1104
+ point of reaching for a panel instead of a subrow. */
1105
+ .dt-detail-body > .dt-wrap { margin: 0; }
1106
+ .dt-detail-body .dt { background: var(--color-background-surface); }
1107
+
1108
+ /* Dark surfaces */
1109
+ .dt.on-inverse tbody tr.dt-detail > td,
1110
+ .dark .dt tbody tr.dt-detail > td {
1111
+ background: color-mix(in srgb, var(--color-pure-white) 4%, transparent);
1112
+ border-bottom-color: var(--color-border-inverse);
1113
+ }
1114
+ .dt.on-inverse .dt-detail-grid dd,
1115
+ .dark .dt .dt-detail-grid dd { color: var(--color-foreground-inverse-default); }
1116
+ .dt.on-inverse .dt-detail-grid dt,
1117
+ .dark .dt .dt-detail-grid dt,
1118
+ .dt.on-inverse .dt-detail-title,
1119
+ .dark .dt .dt-detail-title { color: var(--color-foreground-inverse-muted); }
1120
+
1121
+ /* Row-as-control affordances, shared with §12c */
1122
+ .dt tbody tr.has-detail.is-clickable { cursor: pointer; }
1123
+ .dt tbody tr.has-detail.is-clickable:hover > td { background: var(--color-background-hover); }
1124
+ .dt tbody tr.has-detail.is-clickable:focus-visible { outline: none; }
1125
+ .dt tbody tr.has-detail.is-clickable:focus-visible > td { background: var(--color-background-hover); }
1126
+ .dt tbody tr.has-detail.is-clickable:focus-visible > td:first-child { box-shadow: inset 3px 0 0 0 var(--color-border-focus); }
1127
+ .dt tbody tr.has-detail.is-clickable .dt-subrow-toggle { pointer-events: none; }
1128
+ .dt tbody tr.has-detail.is-clickable[aria-expanded="true"] .dt-subrow-toggle > svg { transform: rotate(90deg); }
1129
+
1130
+ @media (max-width: 900px) {
1131
+ .dt tbody tr.dt-detail > td { padding-left: var(--space-4); }
1132
+ .dt-detail-grid { grid-template-columns: 1fr; }
1133
+ }
1134
+
1135
+ .demo-card-canvas .dt-wrap.dt-responsive { border-radius: var(--shape-container); overflow: hidden; }
1136
+
1137
+ /* A table demo sits on a rounded card, so its wrapper echoes that radius —
1138
+ square corners inside a rounded card read as a box in a box. Last in the
1139
+ sheet so it wins over the base .dt-wrap declaration. */
1140
+ .demo-card-canvas .dt-wrap,
1141
+ .demo-card-canvas .dt-wrap.dt-responsive {
1142
+ border-radius: var(--shape-container);
1143
+ overflow: hidden;
1144
+ }
1145
+
1146
+ /* ----------------------------------------------------------------
1147
+ DETAIL CARD — a clickable surface for a key-value detail panel
1148
+ ----------------------------------------------------------------
1149
+ When the panel holds attributes of one record (not a list), the child is a
1150
+ single object and should look like one: a card, not loose text on the row's
1151
+ background. Clickable when the object has a destination of its own.
1152
+ ---------------------------------------------------------------- */
1153
+ .dt-detail-card {
1154
+ display: block;
1155
+ width: 100%;
1156
+ text-align: left;
1157
+ background: var(--color-background-surface);
1158
+ border: var(--border-hairline) solid var(--color-border-default);
1159
+ border-radius: var(--shape-container);
1160
+ padding: var(--space-4) var(--space-5);
1161
+ font: inherit;
1162
+ color: inherit;
1163
+ }
1164
+ button.dt-detail-card,
1165
+ a.dt-detail-card { cursor: pointer; transition: border-color var(--transition-fast), background var(--transition-fast); }
1166
+ button.dt-detail-card:hover,
1167
+ a.dt-detail-card:hover { border-color: var(--color-border-strong); background: var(--color-background-hover); }
1168
+ button.dt-detail-card:focus-visible,
1169
+ a.dt-detail-card:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
1170
+
1171
+ /* ----------------------------------------------------------------
1172
+ TOOLBAR & PAGINATION SEAM
1173
+ ----------------------------------------------------------------
1174
+ The toolbar and the pager are chrome for the SAME surface as the table, not
1175
+ separate blocks stacked on it. A rule under the toolbar (or over the pager)
1176
+ competes with the header row's own bottom border and with the last row's, so
1177
+ the table reads as a third thing floating between two bars. The thead border
1178
+ and the last row's border already mark where data starts and ends.
1179
+ ---------------------------------------------------------------- */
1180
+
1181
+ /* ----------------------------------------------------------------
1182
+ TOOLBAR & PAGINATION — the standard
1183
+ ----------------------------------------------------------------
1184
+ Both are chrome for the SAME surface as the table and live INSIDE .dt-wrap.
1185
+ Parked as a canvas sibling they float above the table with a dangling border.
1186
+
1187
+ Toolbar, three zones:
1188
+ .dt-toolbar-lead identity — title + count. State whether the count is
1189
+ total or filtered ("12 of 248"), or it lies under filter.
1190
+ .dt-toolbar-filters search + chips; takes the middle
1191
+ .dt-toolbar-actions margin-left:auto — export, columns, density.
1192
+ Never a flex:1 spacer: a spacer competing with the
1193
+ lead collapses it (same bug as the ActionBar).
1194
+
1195
+ Selection mode (.is-selecting) is the same bar in another state, not a new
1196
+ bar: the lead becomes "3 selected" and the actions become bulk actions. The
1197
+ height is preserved so the table does not jump when a row is ticked.
1198
+
1199
+ Footer is pagination only. Rows-per-page sits with the range, not in the
1200
+ toolbar: it describes the page, and the toolbar describes the data.
1201
+ ---------------------------------------------------------------- */
1202
+ .dt-wrap > .dt-toolbar {
1203
+ display: flex;
1204
+ align-items: center;
1205
+ gap: var(--space-4);
1206
+ height: 52px;
1207
+ padding: 0 var(--space-5);
1208
+ box-sizing: border-box;
1209
+ }
1210
+ .dt-toolbar.dt-toolbar-sm { height: 44px; padding: 0 var(--space-4); }
1211
+ .dt-toolbar-lead { display: inline-flex; align-items: baseline; gap: var(--space-2); min-width: 0; }
1212
+ .dt-toolbar-filters { display: inline-flex; align-items: center; gap: var(--space-2); min-width: 0; flex: 1 1 auto; }
1213
+ .dt-toolbar-actions { margin-left: auto; display: inline-flex; align-items: center; gap: var(--space-2); flex-shrink: 0; }
1214
+ /* Selection mode keeps the toolbar on the surface colour. The tint marks WHAT
1215
+ is selected; repeating it on the chrome dilutes that signal and the bar stops
1216
+ reading as chrome at all. The state is carried by content instead — the count
1217
+ in brand, and bulk actions replacing the table actions. */
1218
+ .dt-toolbar.is-selecting .dt-toolbar-lead {
1219
+ color: var(--color-foreground-brand);
1220
+ font-weight: var(--w-bold);
1221
+ }
1222
+
1223
+ .dt-wrap > .dt-pagination {
1224
+ display: flex;
1225
+ align-items: center;
1226
+ gap: var(--space-4);
1227
+ height: 44px;
1228
+ padding: 0 var(--space-5);
1229
+ box-sizing: border-box;
1230
+ }
1231
+ .dt-pagination-range { color: var(--color-foreground-muted); }
1232
+ .dt-pagination-size { display: inline-flex; align-items: center; gap: var(--space-2); }
1233
+ /* RIGHT BY DEFAULT — the PAGER goes to the edge; the range and the rows-per-page
1234
+ control stay together on the left, which is the reading order: what you are
1235
+ looking at, then how much of it, then where to go next.
1236
+ The earlier version pushed the last child whatever it was, so a footer with no
1237
+ pager sent its own range to the right. Anchoring on the nav keeps the intent,
1238
+ and the fallback below covers a caller who writes the pager with own markup. */
1239
+ .dt-pagination > nav:last-child,
1240
+ .dt-pagination > .pager:last-child { margin-left: auto; }
1241
+ .dt-pagination-nav { margin-left: auto; display: inline-flex; align-items: center; gap: var(--space-1); }
1242
+
1243
+ /* .scroll-x creates its own scroll context, so the wrapper's overflow:hidden
1244
+ cannot clip it and the wrapper's rounded corners show THROUGH the table.
1245
+ Give the scroller the same radius on the corners it touches: top corners when
1246
+ it follows a toolbar, bottom when a pager follows it, all four when alone. */
1247
+ .dt-wrap > .scroll-x { border-radius: inherit; }
1248
+ .dt-wrap > .dt-toolbar + .scroll-x { border-top-left-radius: 0; border-top-right-radius: 0; }
1249
+ .dt-wrap > .scroll-x:has(+ .dt-pagination) { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
1250
+
1251
+ /* Vertical scroller: the wrapper cannot clip a descendant that scrolls, so its
1252
+ rounded corners vanish behind the scrollbar gutter. The scroller carries the
1253
+ radius on the edges it owns instead. */
1254
+ .dt-wrap > .dt-scroll-y,
1255
+ .dt-wrap > .scroll-x { border-radius: inherit; }
1256
+ .dt-wrap > .scroll-x { overflow-x: auto; overflow-y: hidden; }
1257
+ .dt-wrap > .dt-scroll-y { overflow-y: auto; overflow-x: hidden; }
1258
+ /* Any hand-rolled vertical scroller in a wrap gets the same treatment */
1259
+ .dt-wrap > [style*="overflow-y"] { border-radius: inherit; }
1260
+ .dt-wrap > .dt-toolbar + [style*="overflow-y"] { border-top-left-radius: 0; border-top-right-radius: 0; }
1261
+ .dt-wrap > [style*="overflow-y"]:has(+ .dt-pagination) { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
1262
+ .dt-wrap > .dt-toolbar + .dt-scroll-y { border-top-left-radius: 0; border-top-right-radius: 0; }
1263
+ .dt-wrap > .dt-scroll-y:has(+ .dt-pagination) { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
1264
+
1265
+ /* ----------------------------------------------------------------
1266
+ FRAME OWNERSHIP — the wrap only draws a border when nothing else bounds it
1267
+ ----------------------------------------------------------------
1268
+ .dt-wrap's border says "the table starts and ends here". A toolbar closes the
1269
+ top, a pager closes the bottom, and a card/drawer closes all four — in those
1270
+ cases the border is a second contour around a boundary that already reads.
1271
+ Radius stays either way: it is what clips the thead corners.
1272
+ ---------------------------------------------------------------- */
1273
+ .dt-wrap:has(> .dt-toolbar),
1274
+ .dt-wrap:has(> .dt-pagination) { border: 0; }
1275
+ /* Chrome carries the edge instead */
1276
+ .dt-wrap:has(> .dt-toolbar) > .dt-toolbar { border-top: var(--border-hairline) solid var(--color-border-default); }
1277
+ .dt-wrap:has(> .dt-pagination) > .dt-pagination { border-bottom: var(--border-hairline) solid var(--color-border-default); }
1278
+ /* Inside a card, drawer or modal the container is the frame */
1279
+ .demo-card-canvas .dt-wrap,
1280
+ .drawer-body .dt-wrap,
1281
+ .modal-body .dt-wrap { border: 0; }
1282
+
1283
+ /* Checkbox column — fixed narrow gutter so the first data column starts at the
1284
+ same x whether or not selection is on. */
1285
+ .dt .dt-check { width: 40px; padding-right: 0; }
1286
+ .dt .dt-check .check { margin: 0; }
1287
+
1288
+ /* Size modifiers — the row scale. Restored after the dt-sm/-md/-lg alias cleanup
1289
+ removed the only block that declared them. */
1290
+ .dt.dt-sm { font-size: var(--fs-xs); }
1291
+ .dt.dt-md { font-size: var(--fs-sm); }
1292
+ .dt.dt-lg { font-size: var(--fs-base); }
1293
+
1294
+ /* ----------------------------------------------------------------
1295
+ VOID FIGURES — a number that no longer counts
1296
+ ----------------------------------------------------------------
1297
+ A cancelled, reversed or voided amount is still worth showing: the reader
1298
+ needs to know the transaction existed. Struck through says "this figure does
1299
+ not count" in a way dimming alone cannot — muted grey reads as "secondary",
1300
+ not as "annulled", and in a money column that difference matters.
1301
+
1302
+ Only figures are struck. Running the strike across a whole row would cross
1303
+ out names and references too, which are still true; it is the VALUE that was
1304
+ voided.
1305
+
1306
+ Applies automatically inside a muted row (the state that already means
1307
+ cancelled or returned), and .is-void marks a single cell when the row itself
1308
+ is still live.
1309
+ ---------------------------------------------------------------- */
1310
+ .dt tbody tr.is-muted td.dt-num,
1311
+ .dt tbody td.dt-num.is-void {
1312
+ text-decoration: line-through;
1313
+ /* Hairline: the strike marks the figure as annulled, it does not cross it out.
1314
+ At 1.5px over tabular mono bold the rule competes with the digits and the
1315
+ amount stops being readable — which defeats the point of still showing it. */
1316
+ text-decoration-thickness: var(--border-hairline);
1317
+ text-decoration-color: var(--color-foreground-muted);
1318
+ color: var(--color-foreground-secondary);
1319
+ }
1320
+ /* The currency chip is not part of the figure — striking it looks like damage */
1321
+ .dt tbody tr.is-muted td.dt-num .ccy,
1322
+ .dt tbody td.dt-num.is-void .ccy { text-decoration: none; }
1323
+ .dt.on-inverse tbody tr.is-muted td.dt-num,
1324
+ .dt.on-inverse tbody td.dt-num.is-void,
1325
+ .dark .dt tbody tr.is-muted td.dt-num,
1326
+ .dark .dt tbody td.dt-num.is-void {
1327
+ text-decoration-color: var(--color-foreground-inverse-muted);
1328
+ color: var(--color-foreground-inverse-secondary);
1329
+ }