@coldsmirk/ledger-mantine 0.1.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.
@@ -0,0 +1,904 @@
1
+ /*
2
+ * @coldsmirk/ledger-mantine — layered stylesheet.
3
+ *
4
+ * Contract (docs/styling.md):
5
+ * - every class is kebab-case under the `ledger-` prefix, mapped 1:1 from a camelCase
6
+ * Styles API selector (root -> .ledger-root, headerCell -> .ledger-header-cell);
7
+ * - state is expressed through data-attributes (data-selected, data-pinned="start"),
8
+ * never through state classes;
9
+ * - custom properties use the `--ledger-*` prefix;
10
+ * - colors and metrics consume Mantine CSS variables only, so dark mode and RTL follow
11
+ * the host theme with no rules of our own.
12
+ * Everything lives in the `ledger` layer; cross-library layer order is the app's call.
13
+ */
14
+
15
+ @layer ledger {
16
+ /* ------------------------------------------------------------------------------------------
17
+ * Adaptive sizing (docs/sizing.md): the root fills whatever the parent gives it and
18
+ * degrades to content height when the parent is indefinite. min-width/min-height: 0
19
+ * pre-defuse the flex/grid min-size:auto shrink trap; overflow only ever appears on the
20
+ * internal scroller.
21
+ * ------------------------------------------------------------------------------------------ */
22
+ .ledger-root {
23
+ position: relative;
24
+ display: flex;
25
+ flex-direction: column;
26
+ width: 100%;
27
+ min-width: 0;
28
+ height: 100%;
29
+ min-height: 0;
30
+ overflow: hidden;
31
+ }
32
+
33
+ /* The frame around header + body — the elastic middle; carries the outer table border
34
+ (painted in the Borders section: three real sides + an inset overlay bottom). */
35
+ .ledger-main {
36
+ position: relative;
37
+ display: flex;
38
+ flex: 1 1 auto;
39
+ flex-direction: column;
40
+ min-width: 0;
41
+ min-height: 0;
42
+ }
43
+
44
+ /* The header viewport never scrolls by itself; its scrollLeft mirrors the body scroller. */
45
+ .ledger-header {
46
+ flex: none;
47
+ overflow: hidden;
48
+ }
49
+
50
+ /* The one elastic region — the Mantine ScrollArea hosting the body table. Its bottom seam
51
+ line is an inset overlay (Borders section), so it needs a positioning context. */
52
+ .ledger-scroller {
53
+ position: relative;
54
+ flex: 1 1 auto;
55
+ min-height: 0;
56
+ }
57
+
58
+ /* An empty (or errored-out) body has no rows to give the region height — keep a readable
59
+ floor under indefinite parents; the overlaid state block itself adds none. */
60
+ .ledger-root[data-empty] .ledger-scroller,
61
+ .ledger-root[data-error] .ledger-scroller {
62
+ min-height: 16rem;
63
+ }
64
+
65
+ /* Scrollbars (the ScrollArea children carrying data-orientation) are transient chrome and
66
+ float ABOVE pinned cells/rows (z-index 2), matching native overlay scrollbars and EP
67
+ (fixed columns +1, scrollbar bar +2). Mantine gives them no z-index of their own, so a
68
+ covered track stretch would be unreachable — the pinned cell intercepts the pointer — and
69
+ the thumb would vanish under a pinned column exactly at the scroll extremes. */
70
+ .ledger-scroller > [data-orientation] {
71
+ z-index: 3;
72
+ }
73
+
74
+ /* With the active row enabled the body viewport is a keyboard focus stop — show it. */
75
+ .ledger-scroller > [tabindex="0"]:focus-visible {
76
+ outline: 2px solid var(--mantine-primary-color-filled);
77
+ outline-offset: -2px;
78
+ }
79
+
80
+ /* The footer viewport (totals): always visible below the scroller, scrollLeft-mirrored.
81
+ Its separators live in the Borders section — the top seam belongs to the scroller. */
82
+ .ledger-footer {
83
+ flex: none;
84
+ overflow: hidden;
85
+ }
86
+
87
+ .ledger-table {
88
+ width: 100%;
89
+ }
90
+
91
+ /* ------------------------------------------------------------------------------------------
92
+ * Header: rendered outside the body scroller (never occludes its scrollbar, never bounces
93
+ * with overscroll); cells only stick horizontally when pinned. Actions reveal on hover/focus.
94
+ * ------------------------------------------------------------------------------------------ */
95
+ .ledger-header-cell {
96
+ position: relative;
97
+ vertical-align: middle;
98
+ background-color: var(--ledger-header-bg, var(--mantine-color-body));
99
+ }
100
+
101
+ .ledger-header-cell[data-pinned] {
102
+ position: sticky;
103
+ z-index: 2;
104
+ }
105
+
106
+ .ledger-header-cell[data-align="center"],
107
+ .ledger-cell[data-align="center"],
108
+ .ledger-footer-cell[data-align="center"] {
109
+ text-align: center;
110
+ }
111
+
112
+ .ledger-header-cell[data-align="end"],
113
+ .ledger-cell[data-align="end"],
114
+ .ledger-footer-cell[data-align="end"] {
115
+ text-align: end;
116
+ }
117
+
118
+ .ledger-header-label {
119
+ display: inline-flex;
120
+ gap: 6px;
121
+ align-items: center;
122
+ max-width: 100%;
123
+ font: inherit;
124
+ color: inherit;
125
+ text-align: inherit;
126
+ }
127
+
128
+ /* The sort control is a NATIVE button (matched via its type attribute) reset here: layered
129
+ author rules always beat UA button styles, whereas Mantine's UnstyledButton carries an
130
+ unlayered font-size that would defeat the layered `font: inherit` above (the unlayered-host
131
+ rule, docs/styling.md). */
132
+ .ledger-header-label[type="button"] {
133
+ padding: 0;
134
+ appearance: none;
135
+ cursor: pointer;
136
+ background: none;
137
+ border: 0;
138
+ }
139
+
140
+ .ledger-header-label[data-align="center"] {
141
+ justify-content: center;
142
+ width: 100%;
143
+ }
144
+
145
+ .ledger-header-label[data-align="end"] {
146
+ justify-content: flex-end;
147
+ width: 100%;
148
+ }
149
+
150
+ .ledger-header-label:focus-visible {
151
+ outline: 2px solid var(--mantine-primary-color-filled);
152
+ outline-offset: -2px;
153
+ }
154
+
155
+ .ledger-header-label [data-truncate],
156
+ .ledger-cell [data-truncate] {
157
+ display: block;
158
+ min-width: 0;
159
+ overflow: hidden;
160
+ text-overflow: ellipsis;
161
+ white-space: nowrap;
162
+ }
163
+
164
+ /* Sort affordance: invisible until hover on unsorted columns, always visible once sorted. */
165
+ .ledger-sort-indicator {
166
+ display: inline-flex;
167
+ align-items: center;
168
+ color: var(--mantine-color-dimmed);
169
+ opacity: 0;
170
+ }
171
+
172
+ .ledger-header-cell:hover .ledger-sort-indicator,
173
+ .ledger-header-label:focus-visible .ledger-sort-indicator {
174
+ opacity: 0.6;
175
+ }
176
+
177
+ .ledger-sort-indicator[data-sorted] {
178
+ color: inherit;
179
+ opacity: 1;
180
+ }
181
+
182
+ .ledger-sort-indicator sup {
183
+ font-size: 0.7em;
184
+ font-weight: 600;
185
+ }
186
+
187
+ /* Actions overlay the trailing edge of the header cell and reveal on interaction. */
188
+ .ledger-header-actions {
189
+ position: absolute;
190
+ inset-inline-end: 4px;
191
+ top: 50%;
192
+ display: flex;
193
+ gap: 2px;
194
+ align-items: center;
195
+ padding-inline-start: 4px;
196
+ background-color: inherit;
197
+ opacity: 0;
198
+ translate: 0 -50%;
199
+ }
200
+
201
+ .ledger-header-actions:has([data-active]),
202
+ .ledger-header-actions:has([aria-expanded="true"]),
203
+ .ledger-header-cell:hover .ledger-header-actions,
204
+ .ledger-header-cell:focus-within .ledger-header-actions {
205
+ opacity: 1;
206
+ }
207
+
208
+ /* The resizer owns the cell's trailing strip (8px, z-index above the actions), so actions
209
+ clear out of it entirely: without this the filter button's outer pixels LOOK clickable but
210
+ hit the invisible resizer, and the button crowds within 1px of the drag line. */
211
+ .ledger-header-cell:has(.ledger-resizer) .ledger-header-actions {
212
+ inset-inline-end: 12px;
213
+ }
214
+
215
+ /* Resize handle on the trailing edge; the visible line appears on hover or while dragging. */
216
+ .ledger-resizer {
217
+ position: absolute;
218
+ inset-inline-end: 0;
219
+ top: 0;
220
+ z-index: 1;
221
+ width: 8px;
222
+ height: 100%;
223
+ touch-action: none;
224
+ cursor: col-resize;
225
+ user-select: none;
226
+ }
227
+
228
+ .ledger-resizer::after {
229
+ position: absolute;
230
+ inset-block: 15%;
231
+ inset-inline-end: 3px;
232
+ width: 2px;
233
+ content: "";
234
+ background-color: transparent;
235
+ border-radius: 1px;
236
+ }
237
+
238
+ /* Resting tick, only where the handle would otherwise be invisible: a borderless table marks
239
+ each resizable boundary with a faint mid-height line (precedent: MUI DataGrid's header
240
+ separators), so finding the handle takes no guessing. Column borders already say where
241
+ columns end, so bordered tables skip it; hover and drag swap in the taller primary line
242
+ either way — a two-step tell, like the Tooltip's delay. */
243
+ :where(.ledger-root:not([data-with-column-borders])) .ledger-resizer::after {
244
+ inset-block: 30%;
245
+ background-color: var(--mantine-color-default-border);
246
+ }
247
+
248
+ .ledger-resizer:hover::after,
249
+ .ledger-resizer[data-resizing]::after {
250
+ inset-block: 15%;
251
+ background-color: var(--mantine-primary-color-filled);
252
+ }
253
+
254
+ /* Drag-reorder feedback. */
255
+ .ledger-header-cell[data-dragging] {
256
+ opacity: 0.4;
257
+ }
258
+
259
+ .ledger-header-cell[data-drop-side]::before {
260
+ position: absolute;
261
+ inset-block: 0;
262
+ z-index: 3;
263
+ width: 2px;
264
+ content: "";
265
+ background-color: var(--mantine-primary-color-filled);
266
+ }
267
+
268
+ .ledger-header-cell[data-drop-side="before"]::before {
269
+ inset-inline-start: 0;
270
+ }
271
+
272
+ .ledger-header-cell[data-drop-side="after"]::before {
273
+ inset-inline-end: 0;
274
+ }
275
+
276
+ /* ------------------------------------------------------------------------------------------
277
+ * Rows: every background flows through --ledger-row-bg, and pinned cells reference the same
278
+ * pipeline — a pinned column that fails to cover stripes/hover is unrepresentable.
279
+ * The paint site is the CELL, never the <tr>: Mantine ships an unlayered
280
+ * `tr { background-color: transparent }`, and unlayered author styles defeat any layered
281
+ * row-level paint regardless of specificity.
282
+ * Ancestry and extra state in these selectors is wrapped in :where() so every rule stays at
283
+ * (0,2,0) — SOURCE ORDER alone is the precedence ladder: stripes < hover < selected/active <
284
+ * their hover step < the cursor inside a selection < pinned < the editing row (below).
285
+ * ------------------------------------------------------------------------------------------ */
286
+ .ledger-row {
287
+ --ledger-pinned-bg: var(--ledger-row-bg, var(--mantine-color-body));
288
+ }
289
+
290
+ .ledger-row > td {
291
+ background-color: var(--ledger-row-bg, transparent);
292
+ }
293
+
294
+ :where(.ledger-root[data-striped="odd"] .ledger-tbody) .ledger-row[data-parity="odd"],
295
+ :where(.ledger-root[data-striped="even"] .ledger-tbody) .ledger-row[data-parity="even"] {
296
+ --ledger-row-bg: var(--ledger-striped-color, var(--mantine-color-default-hover));
297
+ }
298
+
299
+ :where(.ledger-root[data-highlight-on-hover] .ledger-tbody) .ledger-row:hover {
300
+ --ledger-row-bg: var(--ledger-hover-color, var(--mantine-color-default-hover));
301
+ }
302
+
303
+ /* Selection and the active (keyboard-cursor) row rest at Mantine's calm light step; hovering
304
+ either takes the light-hover step — hover deepens a state, it never replaces it. */
305
+ .ledger-row[data-selected] {
306
+ --ledger-row-bg: var(--mantine-primary-color-light);
307
+ }
308
+
309
+ .ledger-row[data-active] {
310
+ --ledger-row-bg: var(--mantine-primary-color-light);
311
+ }
312
+
313
+ :where(.ledger-root[data-highlight-on-hover] .ledger-tbody) .ledger-row:where([data-selected], [data-active]):hover {
314
+ --ledger-row-bg: var(--mantine-primary-color-light-hover);
315
+ }
316
+
317
+ /* The cursor inside a selected block rests one step deeper — exactly one row is ever current,
318
+ and it has to stay findable there without an accent bar. */
319
+ .ledger-row[data-active]:where([data-selected]) {
320
+ --ledger-row-bg: var(--mantine-primary-color-light-hover);
321
+ }
322
+
323
+ .ledger-row[data-clickable] {
324
+ cursor: pointer;
325
+ }
326
+
327
+ /* Sticky pinned rows sit at the body scroller's edges, above the flow (the header lives
328
+ outside the scroller, so top offsets start at 0). */
329
+ .ledger-row[data-pinned-row] {
330
+ --ledger-row-bg: var(--mantine-color-body);
331
+ position: sticky;
332
+ z-index: 2;
333
+ }
334
+
335
+ .ledger-row[data-pinned-row="top"] {
336
+ top: 0;
337
+ }
338
+
339
+ .ledger-row[data-pinned-row="bottom"] {
340
+ bottom: 0;
341
+ }
342
+
343
+ /* Virtualization spacers carry geometry only — no borders, no background. */
344
+ .ledger-table tr[data-ledger-spacer] {
345
+ background: none;
346
+ border: 0;
347
+ }
348
+
349
+ /* ------------------------------------------------------------------------------------------
350
+ * Cells
351
+ * ------------------------------------------------------------------------------------------ */
352
+ .ledger-cell[data-pinned],
353
+ .ledger-selection-cell[data-pinned],
354
+ .ledger-expander-cell[data-pinned],
355
+ .ledger-row-drag-cell[data-pinned],
356
+ .ledger-footer-cell[data-pinned] {
357
+ position: sticky;
358
+ z-index: 2;
359
+ background-color: var(--ledger-pinned-bg, var(--mantine-color-body));
360
+ }
361
+
362
+ /* Pinned-edge scroll shadows, visible only while content is actually scrolled past. */
363
+ [data-pinned-edge]::after {
364
+ position: absolute;
365
+ inset-block: 0;
366
+ width: 8px;
367
+ pointer-events: none;
368
+ content: "";
369
+ opacity: 0;
370
+ }
371
+
372
+ [data-pinned-edge="start"]::after {
373
+ inset-inline-end: 0;
374
+ background: linear-gradient(to right, rgb(0 0 0 / 10%), transparent);
375
+ translate: 100% 0;
376
+ }
377
+
378
+ [data-pinned-edge="end"]::after {
379
+ inset-inline-start: 0;
380
+ background: linear-gradient(to left, rgb(0 0 0 / 10%), transparent);
381
+ translate: -100% 0;
382
+ }
383
+
384
+ [dir="rtl"] [data-pinned-edge="start"]::after {
385
+ background: linear-gradient(to left, rgb(0 0 0 / 10%), transparent);
386
+ translate: -100% 0;
387
+ }
388
+
389
+ [dir="rtl"] [data-pinned-edge="end"]::after {
390
+ background: linear-gradient(to right, rgb(0 0 0 / 10%), transparent);
391
+ translate: 100% 0;
392
+ }
393
+
394
+ .ledger-root[data-scrolled-start] [data-pinned-edge="start"]::after,
395
+ .ledger-root[data-scrolled-end] [data-pinned-edge="end"]::after {
396
+ opacity: 1;
397
+ }
398
+
399
+ .ledger-selection-cell,
400
+ .ledger-expander-cell,
401
+ .ledger-row-drag-cell {
402
+ vertical-align: middle;
403
+ text-align: center;
404
+ }
405
+
406
+ /* The reorder handle: grab affordance while live, quiet dim while something else controls
407
+ the order (the tooltip says what — labels.rowOrderingUnavailable). */
408
+ .ledger-row-drag-cell button {
409
+ vertical-align: middle;
410
+ cursor: grab;
411
+ }
412
+
413
+ .ledger-row-drag-cell button[aria-disabled] {
414
+ cursor: default;
415
+ opacity: 0.4;
416
+ }
417
+
418
+ /* An icon-only ActionIcon is an inline-flex box whose baseline is its bottom edge, so the
419
+ default `vertical-align: baseline` rides it a few pixels high of the middle-aligned cell's
420
+ center — the row chevron and the tree header's expand-all both need pinning to the middle. */
421
+ .ledger-expander-cell button,
422
+ .ledger-header-cell[data-ledger-column-id="ledger:expander"] button {
423
+ vertical-align: middle;
424
+ }
425
+
426
+ /* Expander chevrons rotate open (rows, groups, expand-all). */
427
+ .ledger-expander-cell button svg,
428
+ [data-group-cell] button svg,
429
+ .ledger-header-cell button[data-expanded] svg,
430
+ .ledger-expander-cell button[data-expanded] svg {
431
+ transition: rotate 150ms ease;
432
+ }
433
+
434
+ .ledger-expander-cell button[data-expanded] svg,
435
+ [data-group-cell] button[data-expanded] svg,
436
+ .ledger-header-cell button[data-expanded] svg {
437
+ rotate: 90deg;
438
+ }
439
+
440
+ [data-group-cell] {
441
+ display: inline-flex;
442
+ gap: 6px;
443
+ align-items: center;
444
+ font-weight: 500;
445
+ }
446
+
447
+ [data-group-count] {
448
+ font-size: var(--mantine-font-size-xs);
449
+ color: var(--mantine-color-dimmed);
450
+ }
451
+
452
+ .ledger-cell[data-editing] {
453
+ padding-block: 2px;
454
+ }
455
+
456
+ .ledger-cell-editor {
457
+ display: flex;
458
+ gap: 4px;
459
+ align-items: center;
460
+ width: 100%;
461
+ }
462
+
463
+ .ledger-cell-editor[data-pending] {
464
+ opacity: 0.7;
465
+ }
466
+
467
+ /* Instant columns: the live control keeps its place while its write is out — the pending
468
+ spinner hangs off the host, outside the flow the cell centers. */
469
+ .ledger-instant-cell {
470
+ position: relative;
471
+ display: inline-flex;
472
+ gap: 4px;
473
+ align-items: center;
474
+ vertical-align: middle;
475
+ }
476
+
477
+ .ledger-instant-spinner {
478
+ position: absolute;
479
+ inset-block-start: 50%;
480
+ inset-inline-start: calc(100% + 6px);
481
+ transform: translateY(-50%);
482
+ }
483
+
484
+ /* Row-mode editing: the whole row reads as one open form — a quiet primary wash plus a
485
+ hairline above and below drawn by the cells, so pinned cells carry the frame too. */
486
+ .ledger-row[data-editing-row] {
487
+ --ledger-row-bg: var(--mantine-primary-color-light);
488
+ }
489
+
490
+ .ledger-row[data-editing-row] > * {
491
+ box-shadow:
492
+ inset 0 1px 0 0 var(--mantine-primary-color-filled),
493
+ inset 0 -1px 0 0 var(--mantine-primary-color-filled);
494
+ }
495
+
496
+ /* Row reordering (docs/rows.md#row-ordering): the lifted row dims in place, and the target
497
+ row draws the drop edge — on its cells, so pinned cells carry the line too. */
498
+ .ledger-row[data-dragging] {
499
+ opacity: 0.4;
500
+ }
501
+
502
+ .ledger-row[data-drop-side="before"] > * {
503
+ box-shadow: inset 0 2px 0 0 var(--mantine-primary-color-filled);
504
+ }
505
+
506
+ .ledger-row[data-drop-side="after"] > * {
507
+ box-shadow: inset 0 -2px 0 0 var(--mantine-primary-color-filled);
508
+ }
509
+
510
+ /* The pointer ghost: a snapshot of the whole lifted row (row-reorder.tsx clones it under
511
+ per-column pixel widths), positioned imperatively and portaled to the body — never part
512
+ of the table's flow. */
513
+ .ledger-row-drag-overlay {
514
+ position: fixed;
515
+ inset-block-start: 0;
516
+ inset-inline-start: 0;
517
+ z-index: var(--mantine-z-index-popover);
518
+ overflow: hidden;
519
+ pointer-events: none;
520
+ background-color: var(--mantine-color-body);
521
+ border: 1px solid var(--mantine-color-default-border);
522
+ border-radius: var(--mantine-radius-default);
523
+ box-shadow: var(--mantine-shadow-md);
524
+ }
525
+
526
+ /* ------------------------------------------------------------------------------------------
527
+ * Detail panels, footer, states, chrome
528
+ * ------------------------------------------------------------------------------------------ */
529
+ .ledger-detail-panel {
530
+ padding: var(--mantine-spacing-sm) var(--mantine-spacing-md);
531
+ }
532
+
533
+ .ledger-footer-cell {
534
+ font-weight: 600;
535
+ background-color: var(--mantine-color-body);
536
+ }
537
+
538
+ /* Overlaid on the whole body region (the scroller is its positioning context): centered in
539
+ the VISIBLE area, and immune to horizontal scrolling of the empty table underneath.
540
+ The EmptyState variables soften Mantine's stock indicator (gray-1 disc, dimmed icon) —
541
+ inside a bare table body it reads heavy; both substitutes are color-scheme-adaptive. */
542
+ .ledger-empty {
543
+ --empty-state-indicator-bg: var(--mantine-color-default-hover);
544
+ --empty-state-indicator-color: var(--mantine-color-placeholder);
545
+ position: absolute;
546
+ inset: 0;
547
+ display: flex;
548
+ align-items: center;
549
+ justify-content: center;
550
+ padding: var(--mantine-spacing-xl) var(--mantine-spacing-md);
551
+ }
552
+
553
+ /* The error panel keeps the quiet disc but tints it with the light error pair, both
554
+ color-scheme-adaptive. */
555
+ .ledger-empty[data-variant="error"] {
556
+ --empty-state-indicator-bg: var(--mantine-color-red-light);
557
+ --empty-state-indicator-color: var(--mantine-color-red-light-color);
558
+ }
559
+
560
+ /* Error over stale rows: a body-tinted scrim. Weighted for the message, not the rows — the
561
+ panel's own text sits on top of them, so the scrim has to win that contrast outright; the
562
+ rows survive as a ghost that says "still here, not current". */
563
+ .ledger-empty[data-over-rows] {
564
+ z-index: 2;
565
+ background-color: color-mix(in srgb, var(--mantine-color-body) 92%, transparent);
566
+ }
567
+
568
+ .ledger-loader-row td {
569
+ padding-block: var(--mantine-spacing-sm);
570
+ color: var(--mantine-color-dimmed);
571
+ border-bottom: 0;
572
+ }
573
+
574
+ /* The cell holds a centred pair — spinner + text, or message + retry — so it is laid out as
575
+ one flex line rather than as inline boxes. Inline layout aligns them on the BASELINE, which
576
+ leaves a text run and a button a pixel or two apart and drifts further whenever either
577
+ changes height (adding the retry icon moved it again). The cell itself cannot be the flex
578
+ container: that would strip the `<td>` of its table-cell box and with it the colSpan that
579
+ makes this row span the table. */
580
+ .ledger-loader-row-content {
581
+ display: flex;
582
+ gap: 8px;
583
+ align-items: center;
584
+ justify-content: center;
585
+ font-size: var(--mantine-font-size-sm);
586
+ }
587
+
588
+ /* The retry button carries its own horizontal padding, so the pair needs less between them. */
589
+ .ledger-loader-row[data-error] .ledger-loader-row-content {
590
+ gap: 4px;
591
+ }
592
+
593
+ .ledger-pagination-bar {
594
+ flex: none;
595
+ }
596
+
597
+ .ledger-root > .ledger-pagination-bar {
598
+ padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm);
599
+ border-block-start: 1px solid var(--mantine-color-default-border);
600
+ }
601
+
602
+ /* The seam above the bar is owned by the table's own bottom edge (frame overlay, footer
603
+ trailing border, or scroller seam — Borders section); the bar draws its own line only in
604
+ the fully borderless look, where no other edge exists. */
605
+ .ledger-root[data-with-row-borders] > .ledger-pagination-bar,
606
+ .ledger-root[data-with-table-border] > .ledger-pagination-bar {
607
+ border-block-start: 0;
608
+ }
609
+
610
+ .ledger-selection-bar {
611
+ padding: var(--mantine-spacing-xs) 0;
612
+ }
613
+
614
+ /* The columns panel (docs/columns.md) is bare content whose host owns its box: it fills a
615
+ definite one (a drawer) and degrades to content height in an indefinite one (a popover) —
616
+ the same adaptive contract the table root follows. The list is the only elastic region, so
617
+ a long column set scrolls inside the panel instead of overflowing it.
618
+
619
+ Presentation contract: at rest a row is identity — checkbox and name — plus dimmed marks
620
+ where the layout deviates from default (captions over pinned zones, an overridden width, a
621
+ grouped column, a dimmed hidden name). The machinery — handle, width field, pin segment,
622
+ group toggle — reveals on the hovered or keyboard-focused row as an overlaid toolbar, so
623
+ resting names keep the full row width. Where hover does not exist the toolbar sits inline
624
+ (the hover-none block after the base panel rules). */
625
+ .ledger-columns-panel {
626
+ display: flex;
627
+ flex-direction: column;
628
+ width: 100%;
629
+ min-width: 280px;
630
+ max-height: 100%;
631
+ padding: var(--mantine-spacing-xs);
632
+ }
633
+
634
+ /* No inline padding: the title and the reset button align to the zone slabs' edges below,
635
+ which are the panel's visual frame — the rows' own 8px inset is interior to that frame. */
636
+
637
+ /* The gap under the hairline repeats the panel's own padding token, so the content block
638
+ sits at the same distance from all four of its frame edges. */
639
+ .ledger-columns-panel-header {
640
+ padding: 4px 0 10px;
641
+ margin-block-end: var(--mantine-spacing-xs);
642
+ border-block-end: 1px solid var(--mantine-color-default-border);
643
+ }
644
+
645
+ .ledger-columns-panel-list {
646
+ display: flex;
647
+ flex: 1;
648
+ flex-direction: column;
649
+ min-height: 0;
650
+ overflow-y: auto;
651
+ }
652
+
653
+ .ledger-columns-panel-zone {
654
+ display: flex;
655
+ flex-direction: column;
656
+ gap: 2px;
657
+ }
658
+
659
+ /* Pinned zones are shelves: the tinted slab marks the drag boundary (a column sorts only
660
+ among its own kind) the way the pinned columns' own opaque background marks them in the
661
+ table. Nothing pinned means one flat list and zero chrome. Block padding only — inline
662
+ padding would shift the rows' handle/checkbox/name columns off the center zone's. */
663
+ .ledger-columns-panel-zone[data-pinned] {
664
+ padding-block: 4px;
665
+ background-color: var(--mantine-color-gray-light);
666
+ border-radius: var(--mantine-radius-default);
667
+ }
668
+
669
+ .ledger-columns-panel-zone + .ledger-columns-panel-zone {
670
+ margin-block-start: 8px;
671
+ }
672
+
673
+ /* The caption pairs the zone's pin glyph with its name, tying the shelf to the same vocabulary
674
+ the row's pin segments speak. It is a ledger-owned div (Mantine's unlayered Text reset would
675
+ zero a padding set here), so its type and color live here too; line-height 1 collapses the
676
+ text's line box onto the glyph's 12px so flex centering aligns their midlines exactly. */
677
+ .ledger-columns-panel-zone-label {
678
+ display: flex;
679
+ gap: 4px;
680
+ align-items: center;
681
+ padding: 4px 8px;
682
+ font-size: var(--mantine-font-size-xs);
683
+ font-weight: 500;
684
+ line-height: 1;
685
+ color: var(--mantine-color-dimmed);
686
+ }
687
+
688
+ /* Radii across the panel — the tinted shelves, this hover pill, and the toolbar backdrop —
689
+ follow the theme's default radius, the same token the toolbar's Mantine controls resolve
690
+ with no radius prop, so the whole cluster rounds as one under any host theme. */
691
+ .ledger-columns-panel-item {
692
+ position: relative;
693
+ min-height: 36px;
694
+ padding: 2px 8px;
695
+ border-radius: var(--mantine-radius-default);
696
+ }
697
+
698
+ /* focus-within matches :hover because the toolbar below reveals on both. */
699
+ .ledger-columns-panel-item:hover,
700
+ .ledger-columns-panel-item:focus-within {
701
+ background-color: var(--mantine-color-default-hover);
702
+ }
703
+
704
+ /* The row keeps its seat, greyed, while dnd-kit's clone follows the pointer. */
705
+ .ledger-columns-panel-item[data-dragging] {
706
+ opacity: 0.4;
707
+ }
708
+
709
+ /* A translucent grip is enough of a "this list reorders" hint; the hovered row sharpens it. */
710
+ .ledger-columns-panel-handle {
711
+ cursor: grab;
712
+ opacity: 0.3;
713
+ transition: opacity 120ms ease;
714
+ }
715
+
716
+ .ledger-columns-panel-item:hover .ledger-columns-panel-handle,
717
+ .ledger-columns-panel-item:focus-within .ledger-columns-panel-handle {
718
+ opacity: 1;
719
+ }
720
+
721
+ .ledger-columns-panel-item[data-dragging] .ledger-columns-panel-handle {
722
+ cursor: grabbing;
723
+ }
724
+
725
+ /* The checkbox's label wrapper. Mantine already clears min-width on the body between them, so
726
+ this is the last link in the chain that must be allowed to shrink; without it the nowrap
727
+ title below sets an unshrinkable floor and shoves the rest of the row out of alignment. */
728
+ .ledger-columns-panel-label {
729
+ min-width: 0;
730
+ }
731
+
732
+ /* Long titles ellipse rather than wrap — a two-line row among one-line rows reads as a break
733
+ in the list. */
734
+ .ledger-columns-panel-label > label {
735
+ display: block;
736
+ overflow: hidden;
737
+ text-overflow: ellipsis;
738
+ white-space: nowrap;
739
+ }
740
+
741
+ /* Hidden columns stay listed — bringing them back is the point — but read as absent. The
742
+ color sits on OUR wrapper, not on Mantine's label: that label declares `color: inherit` in
743
+ a stylesheet the host may load unlayered, where it would beat anything the ledger layer
744
+ says — but inheritance it can only obey. */
745
+ .ledger-columns-panel-item[data-hidden] .ledger-columns-panel-label {
746
+ color: var(--mantine-color-dimmed);
747
+ }
748
+
749
+ .ledger-columns-panel-indicators {
750
+ display: flex;
751
+ flex: none;
752
+ gap: 6px;
753
+ align-items: center;
754
+ font-variant-numeric: tabular-nums;
755
+ color: var(--mantine-color-dimmed);
756
+ white-space: nowrap;
757
+ }
758
+
759
+ /* The revealed toolbar. Overlaid rather than inline so resting rows spend the space on the
760
+ name; its own backdrop matches the hovered row and fades in over the name's tail. */
761
+ .ledger-columns-panel-controls {
762
+ position: absolute;
763
+ inset-block: 0;
764
+ inset-inline-end: 0;
765
+ display: flex;
766
+ gap: 6px;
767
+ align-items: center;
768
+ padding-inline: 8px;
769
+ pointer-events: none;
770
+ background-color: var(--mantine-color-default-hover);
771
+ border-start-end-radius: var(--mantine-radius-default);
772
+ border-end-end-radius: var(--mantine-radius-default);
773
+ opacity: 0;
774
+ transition: opacity 120ms ease;
775
+ }
776
+
777
+ .ledger-columns-panel-controls::before {
778
+ position: absolute;
779
+ inset-block: 0;
780
+ inset-inline-start: -24px;
781
+ width: 24px;
782
+ content: "";
783
+ background: linear-gradient(to right, transparent, var(--mantine-color-default-hover));
784
+ }
785
+
786
+ [dir="rtl"] .ledger-columns-panel-controls::before {
787
+ background: linear-gradient(to left, transparent, var(--mantine-color-default-hover));
788
+ }
789
+
790
+ .ledger-columns-panel-item:hover .ledger-columns-panel-controls,
791
+ .ledger-columns-panel-item:focus-within .ledger-columns-panel-controls {
792
+ pointer-events: auto;
793
+ opacity: 1;
794
+ }
795
+
796
+ /* Widths are numbers the table's own cells would show right-aligned and tabular. */
797
+ .ledger-columns-panel-width-input {
798
+ font-variant-numeric: tabular-nums;
799
+ text-align: end;
800
+ }
801
+
802
+ /* ------------------------------------------------------------------------------------------
803
+ * Borders — ledger-owned at cell level. The host paints row borders on <tr> under
804
+ * border-collapse: collapse, and Chrome does not repaint collapsed borders at stuck sticky
805
+ * positions; our tables are border-collapse: separate (inline), so every cell carries its
806
+ * border wherever it sticks.
807
+ * ------------------------------------------------------------------------------------------ */
808
+ .ledger-root[data-with-row-borders] .ledger-thead th,
809
+ .ledger-root[data-with-row-borders] .ledger-tbody .ledger-row > td {
810
+ border-block-end: 1px solid var(--ledger-border-color, var(--mantine-color-default-border));
811
+ }
812
+
813
+ /* Header and footer rows always render every position, so DOM order IS column order there.
814
+ Body rows do not: `spanRows` drops the cells a merged run covers, which promotes whatever
815
+ renders next to `:first-child` and would strip the border off a mid-table column. The cell
816
+ carries `data-leading` for its real (pinning-aware) display position instead. */
817
+ .ledger-root[data-with-column-borders] .ledger-table th:not(:first-child),
818
+ .ledger-root[data-with-column-borders] .ledger-table td:not([data-leading]) {
819
+ border-inline-start: 1px solid var(--ledger-border-color, var(--mantine-color-default-border));
820
+ }
821
+
822
+ /* Seam ownership below the body (EP's inner-wrapper overlay technique): at scroll end the
823
+ last row's border occupies the viewport's LAST pixel, so any line stacked outside the
824
+ scroller would read as one thick border. Seam lines therefore paint as inset overlays that
825
+ COINCIDE with that pixel — never as leading borders on the next region. */
826
+
827
+ /* z-index 2 = the pinned-cell tier: the overlay is generated after every cell, so it paints
828
+ over scrolling pinned cells (their opaque backgrounds would erase the line mid-scroll)
829
+ while staying under the scrollbars (3). */
830
+ .ledger-root[data-with-row-borders] .ledger-scroller::after {
831
+ position: absolute;
832
+ inset-block-end: 0;
833
+ inset-inline: 0;
834
+ z-index: 2;
835
+ height: 1px;
836
+ pointer-events: none;
837
+ content: "";
838
+ background-color: var(--ledger-border-color, var(--mantine-color-default-border));
839
+ }
840
+
841
+ /* The footer separates with its TRAILING edge only (the top seam is the scroller overlay);
842
+ under the frame this pixel coincides with the frame's bottom overlay. */
843
+ .ledger-root[data-with-row-borders] .ledger-footer {
844
+ border-block-end: 1px solid var(--ledger-border-color, var(--mantine-color-default-border));
845
+ }
846
+
847
+ /* The frame: three real sides; the bottom edge is an inset overlay for the same coincidence
848
+ reason — it lands on the scroller's (or footer's) last pixel instead of adding one below. */
849
+ .ledger-root[data-with-table-border] .ledger-main {
850
+ border: 1px solid var(--ledger-border-color, var(--mantine-color-default-border));
851
+ border-block-end: 0;
852
+ }
853
+
854
+ .ledger-root[data-with-table-border] .ledger-main::after {
855
+ position: absolute;
856
+ inset-block-end: 0;
857
+ inset-inline: 0;
858
+ z-index: 2;
859
+ height: 1px;
860
+ pointer-events: none;
861
+ content: "";
862
+ background-color: var(--ledger-border-color, var(--mantine-color-default-border));
863
+ }
864
+ }
865
+
866
+ /* Interaction overrides reopen the same layer after every base rule, so equal-specificity base
867
+ declarations cannot win through source order. */
868
+ @layer ledger {
869
+ /* No hover, no reveal: the toolbar lives inline and the rest-state marks stand down — the
870
+ controls sitting right there already say the same thing. */
871
+ @media (hover: none) {
872
+ .ledger-columns-panel-handle {
873
+ opacity: 1;
874
+ }
875
+
876
+ .ledger-columns-panel-indicators {
877
+ display: none;
878
+ }
879
+
880
+ .ledger-columns-panel-controls {
881
+ position: static;
882
+ flex: none;
883
+ padding-inline: 0;
884
+ pointer-events: auto;
885
+ background-color: transparent;
886
+ opacity: 1;
887
+ }
888
+
889
+ .ledger-columns-panel-controls::before {
890
+ content: none;
891
+ }
892
+ }
893
+
894
+ @media (prefers-reduced-motion: reduce) {
895
+ .ledger-expander-cell button svg,
896
+ [data-group-cell] button svg,
897
+ .ledger-expander-cell button[data-expanded] svg,
898
+ .ledger-header-cell button[data-expanded] svg,
899
+ .ledger-columns-panel-handle,
900
+ .ledger-columns-panel-controls {
901
+ transition: none;
902
+ }
903
+ }
904
+ }