@attrus-ds/core 1.0.20 → 1.2.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.
@@ -142,9 +142,11 @@
142
142
  .btn-primary:focus-visible { box-shadow: var(--shadow-focus); }
143
143
 
144
144
 
145
- /* SECONDARY — soft neutral fill + hairline border */
145
+ /* SECONDARY — hairline border, no resting fill. The fill arrives on hover:
146
+ a filled secondary at rest competed with the primary for weight, and on a
147
+ subtle surface it read as a disabled block rather than a button. */
146
148
  .btn-secondary {
147
- background: var(--color-background-subtle);
149
+ background: transparent;
148
150
  color: var(--color-foreground-default);
149
151
  border-color: var(--color-border-default);
150
152
  }
@@ -216,7 +218,7 @@
216
218
 
217
219
  /* DANGER OUTLINE — destructive contextual */
218
220
  .btn-danger-outline {
219
- background: var(--color-background-surface);
221
+ background: transparent;
220
222
  color: var(--color-danger-500);
221
223
  border-color: var(--color-feedback-danger-border);
222
224
  }
@@ -400,3 +402,54 @@
400
402
  .btn-group .btn-danger[aria-pressed="true"] + .btn-danger,
401
403
  .btn-group .btn-inverse[aria-pressed="true"] + .btn-inverse { box-shadow: none; }
402
404
 
405
+
406
+ /* ----------------------------------------------------------------
407
+ HOVER GLOW — coloured buttons emit light under the pointer
408
+ ----------------------------------------------------------------
409
+ Light, not shadow: three centred layers of the fill colour lifted toward
410
+ white (a tight core, a mid bloom, a wide falloff) plus a 1px top highlight.
411
+ Flat at rest, it switches on on hover and is the hover feedback; neutral
412
+ variants have no fill to glow and stay flat.
413
+
414
+ The primary glows in the ACCENT, not the brand: navy is near-black and cannot
415
+ emit light — its halo read as a dark smudge and vanished on inverse.
416
+
417
+ Motion: the rest rule carries the LEAVE curve (ease-in, 360ms, so it fades
418
+ rather than cuts); :hover carries the ENTER curve (ease-out, 280ms, so it
419
+ swells and settles). Rest declares the same four layers at zero so the
420
+ browser interpolates layer-by-layer instead of snapping.
421
+
422
+ Focus keeps its ring — hover + focus stacks the ring on top of the light.
423
+ Disabled and grouped segments never glow. Reduced motion drops the
424
+ transition only (handled by the .btn rule above).
425
+ ---------------------------------------------------------------- */
426
+ .btn-primary, .btn-danger, .btn-success {
427
+ box-shadow: inset 0 1px 0 transparent, 0 0 0 transparent, 0 0 0 transparent, 0 0 0 transparent;
428
+ transition:
429
+ background var(--transition-fast), border-color var(--transition-fast),
430
+ color var(--transition-fast), transform var(--transition-fast),
431
+ box-shadow 360ms cubic-bezier(.4, 0, 1, 1);
432
+ }
433
+ .btn-primary:hover, .btn-danger:hover, .btn-success:hover {
434
+ transition:
435
+ background var(--transition-fast), border-color var(--transition-fast),
436
+ color var(--transition-fast), transform var(--transition-fast),
437
+ box-shadow 280ms cubic-bezier(0, 0, .2, 1);
438
+ }
439
+ .btn-primary:hover { box-shadow: inset 0 1px 0 color-mix(in srgb, white 28%, transparent), 0 0 6px color-mix(in srgb, color-mix(in srgb, var(--color-accent-solid) 72%, white) 60%, transparent), 0 0 20px color-mix(in srgb, var(--color-accent-solid) 38%, transparent), 0 0 42px color-mix(in srgb, var(--color-accent-solid) 18%, transparent); }
440
+ .btn-danger:hover { box-shadow: inset 0 1px 0 color-mix(in srgb, white 28%, transparent), 0 0 6px color-mix(in srgb, color-mix(in srgb, var(--color-danger-500) 72%, white) 60%, transparent), 0 0 20px color-mix(in srgb, var(--color-danger-500) 38%, transparent), 0 0 42px color-mix(in srgb, var(--color-danger-500) 18%, transparent); }
441
+ .btn-success:hover { box-shadow: inset 0 1px 0 color-mix(in srgb, white 28%, transparent), 0 0 6px color-mix(in srgb, color-mix(in srgb, var(--color-success-500) 72%, white) 60%, transparent), 0 0 20px color-mix(in srgb, var(--color-success-500) 38%, transparent), 0 0 42px color-mix(in srgb, var(--color-success-500) 18%, transparent); }
442
+
443
+ .btn-primary:hover:focus-visible { box-shadow: var(--shadow-focus), inset 0 1px 0 color-mix(in srgb, white 28%, transparent), 0 0 6px color-mix(in srgb, color-mix(in srgb, var(--color-accent-solid) 72%, white) 60%, transparent), 0 0 20px color-mix(in srgb, var(--color-accent-solid) 38%, transparent), 0 0 42px color-mix(in srgb, var(--color-accent-solid) 18%, transparent); }
444
+ .btn-danger:hover:focus-visible { box-shadow: var(--shadow-focus-danger), inset 0 1px 0 color-mix(in srgb, white 28%, transparent), 0 0 6px color-mix(in srgb, color-mix(in srgb, var(--color-danger-500) 72%, white) 60%, transparent), 0 0 20px color-mix(in srgb, var(--color-danger-500) 38%, transparent), 0 0 42px color-mix(in srgb, var(--color-danger-500) 18%, transparent); }
445
+ .btn-success:hover:focus-visible { box-shadow: var(--shadow-focus-success), inset 0 1px 0 color-mix(in srgb, white 28%, transparent), 0 0 6px color-mix(in srgb, color-mix(in srgb, var(--color-success-500) 72%, white) 60%, transparent), 0 0 20px color-mix(in srgb, var(--color-success-500) 38%, transparent), 0 0 42px color-mix(in srgb, var(--color-success-500) 18%, transparent); }
446
+
447
+ /* Press: the light dims and settles under the finger */
448
+ .btn-primary:active, .btn-danger:active, .btn-success:active { transition-duration: var(--duration-fast, 120ms); }
449
+ .btn-primary:active { box-shadow: inset 0 1px 0 color-mix(in srgb, white 10%, transparent), 0 0 4px color-mix(in srgb, var(--color-accent-solid) 35%, transparent), 0 0 10px color-mix(in srgb, var(--color-accent-solid) 18%, transparent), 0 0 0 transparent; }
450
+ .btn-danger:active { box-shadow: inset 0 1px 0 color-mix(in srgb, white 10%, transparent), 0 0 4px color-mix(in srgb, var(--color-danger-500) 35%, transparent), 0 0 10px color-mix(in srgb, var(--color-danger-500) 18%, transparent), 0 0 0 transparent; }
451
+ .btn-success:active { box-shadow: inset 0 1px 0 color-mix(in srgb, white 10%, transparent), 0 0 4px color-mix(in srgb, var(--color-success-500) 35%, transparent), 0 0 10px color-mix(in srgb, var(--color-success-500) 18%, transparent), 0 0 0 transparent; }
452
+
453
+ .btn-primary[disabled], .btn-danger[disabled], .btn-success[disabled],
454
+ .btn-primary.is-disabled, .btn-danger.is-disabled, .btn-success.is-disabled,
455
+ .btn-group .btn-primary, .btn-group .btn-danger, .btn-group .btn-success { box-shadow: none; }
@@ -69,16 +69,22 @@
69
69
  .callout-icon {
70
70
  flex-shrink: 0;
71
71
  align-self: flex-start;
72
- /* Optical: pull the 18px glyph onto the cap line of the first row of text
73
- (line-height 15.6px), rather than centring it on the whole block. */
74
- margin-top: -1px;
75
72
  width: 18px;
76
73
  height: 18px;
77
- flex-shrink: 0;
78
- margin-top: 1px;
74
+ /* Centre the 18px glyph on the FIRST line of text, not on the block. The
75
+ offset derives from that line's own tokens, so it holds if the type scale
76
+ changes. (This rule used to declare margin-top twice — -1px, then 1px —
77
+ and the second won, leaving the icon 2.2px below the line.) */
78
+ margin-top: calc((var(--fs-sm) * var(--lh-snug) - 18px) / 2);
79
79
  color: var(--callout-fg);
80
80
  stroke-width: var(--icon-stroke);
81
81
  }
82
+ /* Rich callout: the first line is the title, which sits on its own scale */
83
+ .callout:has(.callout-title):not(.callout-sm):not(.callout-bar) > i,
84
+ .callout:has(.callout-title):not(.callout-sm):not(.callout-bar) > svg,
85
+ .callout:has(.callout-title):not(.callout-sm):not(.callout-bar) > .callout-icon {
86
+ margin-top: calc((var(--fs-md) * var(--lh-tight) - 18px) / 2);
87
+ }
82
88
 
83
89
  /* content column — wrap title + body + actions for rich callouts */
84
90
  .callout-content {
@@ -173,6 +179,17 @@
173
179
  }
174
180
  .callout-dismiss:hover { background: color-mix(in srgb, var(--callout-tint) 14%, transparent); color: var(--color-foreground-default); }
175
181
  .callout-dismiss > i, .callout-dismiss > svg { width: 16px; height: 16px; stroke-width: var(--icon-stroke); }
182
+ /* Centre the 26px dismiss on the first line (body, or title in a rich callout)
183
+ and hand the overhang back at the bottom, so it neither sits below the text
184
+ nor stretches a one-line callout. The bar and the small size own their own. */
185
+ .callout:not(.callout-bar):not(.callout-sm) .callout-dismiss {
186
+ margin-top: calc((var(--fs-sm) * var(--lh-snug) - 26px) / 2);
187
+ margin-bottom: calc((var(--fs-sm) * var(--lh-snug) - 26px) / 2);
188
+ }
189
+ .callout:has(.callout-title):not(.callout-bar):not(.callout-sm) .callout-dismiss {
190
+ margin-top: calc((var(--fs-md) * var(--lh-tight) - 26px) / 2);
191
+ margin-bottom: calc((var(--fs-md) * var(--lh-tight) - 26px) / 2);
192
+ }
176
193
 
177
194
  /* ---- tonal variants ---- */
178
195
  .callout-warn { --callout-tint: var(--color-warning-500); --callout-fg: var(--color-feedback-warning-foreground); }
@@ -191,6 +208,39 @@
191
208
  .callout-sm > i, .callout-sm > svg, .callout-sm .callout-icon { width: 16px; height: 16px; }
192
209
  .callout-sm .callout-body { font-size: var(--fs-xs); }
193
210
  .callout-sm .callout-title { font-size: var(--fs-sm); }
211
+ /* Optical alignment for the small size. The base offsets were tuned for an 18px
212
+ glyph on a ~15.6px line; at sm the line is ~14.3px, so they left the icon 2px
213
+ and the X 4px below the text, and the 26px dismiss stretched a one-line
214
+ callout. Each is re-centred on the FIRST line (still flex-start, so a wrapping
215
+ message keeps them on line one, not the block's middle):
216
+ the small line is 11px × 1.3 = 14.3px (measured — not --lh-normal)
217
+ icon: (14.3 − 16) / 2 → −0.85px
218
+ X: a 20px box, (14.3 − 20)/2 → −2.85px top
219
+ The dismiss keeps a 26px hit area through ::before, so the target does not
220
+ shrink — only its layout footprint does. */
221
+ .callout-sm > i, .callout-sm > svg, .callout-sm .callout-icon {
222
+ margin-top: calc((var(--fs-xs) * var(--lh-snug) - 16px) / 2);
223
+ }
224
+ .callout-sm .callout-dismiss {
225
+ position: relative;
226
+ width: 20px;
227
+ height: 20px;
228
+ margin: calc((var(--fs-xs) * var(--lh-snug) - 20px) / 2) 0 calc((var(--fs-xs) * var(--lh-snug) - 20px) / 2) var(--space-2);
229
+ }
230
+ .callout-sm .callout-dismiss::before {
231
+ content: "";
232
+ position: absolute;
233
+ inset: -3px;
234
+ border-radius: var(--radius-sm);
235
+ }
236
+ .callout-sm .callout-dismiss > i, .callout-sm .callout-dismiss > svg { width: 14px; height: 14px; }
237
+ .callout-sm:has(.callout-title) > i, .callout-sm:has(.callout-title) > svg, .callout-sm:has(.callout-title) .callout-icon {
238
+ margin-top: calc((var(--fs-sm) * var(--lh-tight) - 16px) / 2);
239
+ }
240
+ .callout-sm:has(.callout-title) .callout-dismiss {
241
+ margin-top: calc((var(--fs-sm) * var(--lh-tight) - 20px) / 2);
242
+ margin-bottom: calc((var(--fs-sm) * var(--lh-tight) - 20px) / 2);
243
+ }
194
244
 
195
245
  /* ---- alert bar — full-width banner, items centered, actions right ---- */
196
246
  .callout-bar {
@@ -333,3 +333,21 @@
333
333
  .combobox-chevron { transition: none; }
334
334
  .combobox-option { transition: none; }
335
335
  }
336
+
337
+ /* ----------------------------------------------------------------
338
+ SIZE + ICON-ONLY
339
+ ---------------------------------------------------------------- */
340
+ /* sm — the 32px trigger that sits beside sm buttons in a table toolbar. */
341
+ .combobox-sm .combobox-trigger { height: var(--control-h-sm); padding: 0 var(--space-2) 0 var(--space-3); gap: var(--space-2); font-size: var(--fs-sm); }
342
+ /* icon-only — the glyph IS the value (a sort order, a view mode). The label is
343
+ kept as sr-only so the trigger still announces what is selected. */
344
+ .combobox.is-icon-only { display: inline-block; width: auto; align-self: flex-start; }
345
+ .combobox.is-icon-only .combobox-trigger { width: auto; padding: 0 var(--space-2) 0 var(--space-2-5); gap: var(--space-1); }
346
+ .combobox.is-icon-only .combobox-trigger-icon { color: var(--color-foreground-default); }
347
+ /* Option glyph in the list (always shown when the option carries one) */
348
+ .combobox-option-icon { flex-shrink: 0; display: inline-flex; color: var(--color-foreground-secondary); }
349
+ .combobox-option-icon > svg, .combobox-option-icon > i { width: var(--icon-sm); height: var(--icon-sm); stroke-width: var(--icon-stroke); }
350
+ /* The trigger icon rule sizes the span; an svg/i passed as the icon kept its own
351
+ 24px and spilled out. Size the glyph itself, like .combobox-option-icon. */
352
+ .combobox-trigger-icon { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; }
353
+ .combobox-trigger-icon > svg, .combobox-trigger-icon > i { width: var(--icon-sm); height: var(--icon-sm); stroke-width: var(--icon-stroke); }
@@ -0,0 +1,123 @@
1
+ /* ============================================================
2
+ TABLE FILTERS — components/filters.css
3
+ DTFilters (apply | live) + DateRangePicker + the apply hint.
4
+ Sits in DTToolbar's filters zone. Status tabs live OUTSIDE the
5
+ table and always apply at once; this bar owns search + period.
6
+ ============================================================ */
7
+
8
+ .dt-filters { display: flex; flex-direction: column; flex: 1 1 auto; min-width: 0; }
9
+ .dt-filters-row { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-3); min-width: 0; }
10
+
11
+ /* "Search by [Name | ID | Value]" + the field that opens beside it */
12
+ .dt-filters-scope { display: inline-flex; align-items: center; gap: var(--space-2); min-width: 0; }
13
+ .dt-filters-scope-label { font-size: var(--fs-sm); color: var(--color-foreground-secondary); white-space: nowrap; }
14
+ /* The field animates its WIDTH open (max-width 0 → 320) instead of popping in.
15
+ The 4px padding and matching negative margin keep the focus ring inside the
16
+ clip while it is closed. */
17
+ .dt-filters-field {
18
+ max-width: 0; opacity: 0; overflow: hidden; padding: 4px; margin: -4px;
19
+ transition: max-width var(--duration-base) var(--ease-entrance), opacity var(--duration-fast) var(--ease-standard);
20
+ }
21
+ .dt-filters-field.is-open { max-width: 320px; opacity: 1; }
22
+ .dt-filters-field > .input { width: 300px; max-width: 100%; }
23
+
24
+ /* Second row (apply mode) — opens while something is pending or applied.
25
+ grid 0fr → 1fr animates to the row's own height, not a guessed one. */
26
+ .dt-filters-apply {
27
+ display: grid; grid-template-rows: 0fr; opacity: 0; overflow: hidden;
28
+ transition: grid-template-rows var(--duration-base) var(--ease-entrance), opacity var(--duration-base) var(--ease-standard);
29
+ }
30
+ .dt-filters-apply.is-open { grid-template-rows: 1fr; opacity: 1; }
31
+ .dt-filters-apply-inner {
32
+ min-height: 0; display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2);
33
+ transition: margin-top var(--duration-base) var(--ease-entrance), padding-top var(--duration-base) var(--ease-entrance);
34
+ }
35
+ /* the hairline between the controls and this row */
36
+ .dt-filters-apply.is-open .dt-filters-apply-inner {
37
+ margin-top: var(--space-3); padding-top: var(--space-3);
38
+ border-top: var(--border-hairline) solid var(--color-border-subtle);
39
+ }
40
+ /* the clip must lift once open, or the hint balloon is cut */
41
+ .dt-filters-apply.is-open, .dt-filters-apply.is-open .dt-filters-apply-inner { animation: dt-filters-unclip 0s var(--duration-base) forwards; }
42
+ @keyframes dt-filters-unclip { to { overflow: visible; } }
43
+ .dt-filters-vsep { align-self: stretch; width: var(--border-hairline); min-height: 20px; background: var(--color-border-subtle); margin: 0 var(--space-1); }
44
+
45
+ /* An applied filter: a 32px pill, the height of the buttons beside it. The ✕ /
46
+ undo is its own button, so the pill is not one large hit area. */
47
+ .dt-filter-pill {
48
+ display: inline-flex; align-items: center; gap: var(--space-2); box-sizing: border-box;
49
+ height: var(--control-h-sm); padding: 0 var(--space-1) 0 var(--space-3);
50
+ border: var(--border-hairline) solid var(--color-border-default); border-radius: var(--radius-pill);
51
+ background: var(--color-background-surface); color: var(--color-foreground-secondary);
52
+ font-size: var(--fs-sm); line-height: 1; white-space: nowrap;
53
+ }
54
+ .dt-filter-pill-label { max-width: 32ch; overflow: hidden; text-overflow: ellipsis; }
55
+ .dt-filter-pill-action {
56
+ appearance: none; display: inline-flex; align-items: center; justify-content: center;
57
+ width: 24px; height: 24px; padding: 0; border: 0; border-radius: var(--radius-pill);
58
+ background: none; color: inherit; font: inherit; cursor: pointer;
59
+ }
60
+ .dt-filter-pill-action > svg { width: var(--icon-xs); height: var(--icon-xs); }
61
+ .dt-filter-pill-action:hover { background: var(--color-background-hover); color: var(--color-foreground-default); }
62
+ .dt-filter-pill-action:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
63
+ /* Marked for removal — still applied until the next Apply. */
64
+ .dt-filter-pill.is-pending { border-style: dashed; color: var(--color-foreground-muted); }
65
+ .dt-filter-pill.is-pending .dt-filter-pill-label { text-decoration: line-through; }
66
+
67
+ /* Presence — items enter and leave the apply row the same way */
68
+ .dt-filters-item { transition: opacity 180ms var(--ease-standard), transform 180ms var(--ease-entrance); }
69
+ .dt-filters-item.is-entering, .dt-filters-item.is-leaving { opacity: 0; transform: translateX(-6px); }
70
+
71
+ /* Apply with a hint balloon while anything is pending (and on hover) */
72
+ .dt-filters-applywrap { position: relative; display: inline-flex; }
73
+ .dt-filters-hint {
74
+ position: absolute; left: 0; bottom: calc(100% + var(--space-2)); z-index: 8000;
75
+ width: max-content; max-width: 260px; padding: var(--space-2) var(--space-3);
76
+ background: var(--color-background-inverse); color: var(--color-foreground-inverse-default);
77
+ border-radius: var(--radius-sm); font-size: var(--fs-xs); line-height: var(--lh-snug);
78
+ box-shadow: var(--shadow-md); pointer-events: none;
79
+ opacity: 0; transform: translateY(2px);
80
+ transition: opacity var(--duration-fast) var(--ease-standard), transform var(--duration-fast) var(--ease-standard);
81
+ }
82
+ .dt-filters-hint::after {
83
+ content: ""; position: absolute; left: var(--space-4); top: 100%;
84
+ border: 5px solid transparent; border-top-color: var(--color-background-inverse);
85
+ }
86
+ .dt-filters-hint.is-visible, .dt-filters-applywrap:hover .dt-filters-hint { opacity: 1; transform: none; }
87
+
88
+ /* ------------------------------------------------------------
89
+ DATE RANGE PICKER
90
+ ------------------------------------------------------------ */
91
+ .date-range { position: relative; display: inline-flex; }
92
+ .date-range > .date-trigger { width: 300px; max-width: 100%; }
93
+ .date-range-label { display: inline-flex; align-items: center; gap: var(--space-2-5); min-width: 0; }
94
+ .date-range-label > svg, .date-range-label > i { width: var(--icon-sm); height: var(--icon-sm); flex-shrink: 0; stroke-width: var(--icon-stroke); color: var(--color-foreground-muted); }
95
+ .date-range-label > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
96
+ .date-range-sm > .date-trigger { height: var(--control-h-sm); font-size: var(--fs-sm); }
97
+ .date-range-panel {
98
+ z-index: 7000; display: flex; gap: var(--space-4); box-sizing: border-box;
99
+ padding: var(--space-4); width: max-content; max-width: calc(100vw - var(--space-6));
100
+ background: var(--color-background-surface); color: var(--color-foreground-default);
101
+ border: var(--border-hairline) solid var(--color-border-default); border-radius: var(--shape-overlay, var(--radius-lg));
102
+ box-shadow: var(--shadow-lg);
103
+ }
104
+ .date-range:not(.is-portal) .date-range-panel { position: absolute; top: calc(100% + var(--space-2)); left: 0; }
105
+ .date-range-presets { display: flex; flex-direction: column; gap: var(--space-1); min-width: 140px; padding-right: var(--space-4); border-right: var(--border-hairline) solid var(--color-border-subtle); }
106
+ .date-range-preset {
107
+ appearance: none; text-align: left; padding: var(--space-2) var(--space-3); border: 0; border-radius: var(--radius-sm);
108
+ background: none; font: inherit; font-size: var(--fs-sm); color: var(--color-foreground-secondary); cursor: pointer;
109
+ }
110
+ .date-range-preset:hover { background: var(--color-background-hover); color: var(--color-foreground-default); }
111
+ .date-range-preset.is-active { background: var(--color-background-selected); color: var(--color-foreground-brand); }
112
+ .date-range-preset:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
113
+ .date-range-fields { display: flex; flex-direction: column; gap: var(--space-3); }
114
+ .date-range-field { display: flex; flex-direction: column; gap: var(--space-1); font-size: var(--fs-xs); color: var(--color-foreground-muted); }
115
+ .date-range-actions { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: auto; }
116
+ @media (max-width: 640px) {
117
+ .date-range-panel { flex-direction: column; }
118
+ .date-range-presets { flex-direction: row; flex-wrap: wrap; border-right: 0; padding-right: 0; border-bottom: var(--border-hairline) solid var(--color-border-subtle); padding-bottom: var(--space-3); }
119
+ }
120
+
121
+ @media (prefers-reduced-motion: reduce) {
122
+ .dt-filters-field, .dt-filters-apply, .dt-filters-apply-inner, .dt-filters-item, .dt-filters-hint { transition: none; }
123
+ }
@@ -248,6 +248,36 @@ textarea.input.input-lg { height: auto; min-height: 120px; padding: var(--space-
248
248
  border-color var(--transition-fast),
249
249
  box-shadow var(--transition-fast);
250
250
  }
251
+ /* Small field — the toolbar size (32px), for a search with its own action */
252
+ .group.group-sm { height: var(--control-h-sm); box-sizing: border-box; }
253
+ .group.group-sm > input { font-size: var(--fs-sm); padding: 0 var(--space-3); }
254
+ /* The inset button is tuned for the 40px field; at 32px it keeps a 24px box of
255
+ its own (4px clear of each edge) and centres its label with line-height 1,
256
+ instead of inheriting a height that left the text low. */
257
+ .group.group-sm > .fix-btn {
258
+ height: 24px;
259
+ align-self: center;
260
+ margin-top: 0;
261
+ margin-bottom: 0;
262
+ /* 1px of bottom padding lifts the label 0.5px: Lato at 11px sits low by its
263
+ lowercase mass in a box this small, the same correction as the sm pill. */
264
+ padding: 0 var(--space-3) 1px;
265
+ font-size: var(--fs-xs);
266
+ line-height: 1;
267
+ display: inline-flex;
268
+ align-items: center;
269
+ box-sizing: border-box;
270
+ }
271
+ .group.group-sm > .fix-btn:last-child { margin-right: 3px; }
272
+ .group.group-sm > .fix-btn:first-child { margin-left: 3px; }
273
+ /* field-in-group focus */
274
+ /* A field marked .input inside a group still matched .input:focus and drew its
275
+ own 4px halo — clipped by the group, it showed as an inner band that stopped
276
+ short of the inset button, notched by the field's own rounded corners. The
277
+ group owns the ring; the field inside it draws none. */
278
+ .group > .input,
279
+ .group > .input:focus,
280
+ .group > .input:focus-visible { box-shadow: none; border-radius: 0; }
251
281
  .group:focus-within {
252
282
  border-color: var(--color-border-focus);
253
283
  box-shadow: var(--shadow-focus);
@@ -1420,3 +1450,25 @@ textarea.input.input-lg { height: auto; min-height: 120px; padding: var(--space-
1420
1450
  .group.underline > .fix-btn.is-danger { background: var(--color-danger-500); border-color: transparent; color: var(--color-foreground-on-solid); }
1421
1451
  .group.underline > .fix-btn.is-danger-outline { background: var(--color-background-surface); border-color: var(--color-feedback-danger-border); color: var(--color-danger-500); }
1422
1452
  .group.underline > .fix-btn.is-success { background: var(--color-success-500); border-color: transparent; color: var(--color-foreground-on-solid); }
1453
+
1454
+ /* Small search pill — 32px, with the same 24px inset button as .group-sm.
1455
+ Appended last so it outranks the inset-button block, which is tuned for 40px. */
1456
+ .pill-search.pill-search-sm { box-sizing: border-box; }
1457
+ .pill-search.pill-search-sm:has(> .fix-btn:last-child) { padding-right: 0; }
1458
+ .pill-search.pill-search-sm > .fix-btn {
1459
+ height: 24px; align-self: center; margin-top: 0; margin-bottom: 0;
1460
+ padding: 0 var(--space-3) 1px; font-size: var(--fs-xs); line-height: 1;
1461
+ display: inline-flex; align-items: center; box-sizing: border-box;
1462
+ }
1463
+ .pill-search.pill-search-sm > .fix-btn:last-child { margin-right: 4px; }
1464
+
1465
+ /* Disabled wins over every variant — the variant rules above are more specific
1466
+ than the base :disabled, so without this a disabled tertiary kept its fill and
1467
+ still read as clickable. */
1468
+ :is(.group, .pill-search) > .fix-btn:disabled,
1469
+ :is(.group, .pill-search) > .fix-btn[class*="is-"]:disabled {
1470
+ background: transparent;
1471
+ border-color: transparent;
1472
+ color: var(--color-foreground-disabled);
1473
+ cursor: not-allowed;
1474
+ }
@@ -226,3 +226,11 @@
226
226
  @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
227
227
  .sb.is-glass { background: var(--color-background-surface); }
228
228
  }
229
+
230
+ /* Optical centring for small labels. A badge that inherits a tall line-height
231
+ (15px on 10px type) is centred on its LINE BOX, and JetBrains Mono carries its
232
+ ink high in that box — the caps sat 0.7px above centre. The pill model fixes it:
233
+ line-height 1 inside a fixed-height flex box, which centres the glyph's em box.
234
+ Heights are unchanged. */
235
+ .sb-count { display: inline-flex; align-items: center; line-height: 1; height: 17px; padding-top: 0; padding-bottom: 0; box-sizing: border-box; }
236
+ .sb-item .badge { display: inline-flex; align-items: center; line-height: 1; height: 15px; padding-top: 0; padding-bottom: 0; box-sizing: border-box; }
@@ -42,12 +42,19 @@
42
42
  align-self: center;
43
43
  }
44
44
 
45
- /* Label wrapper — runtime-injected; nudges text up 0.5px to compensate for
46
- Lato's optical descender offset, leaving the dot at geometric centre. */
45
+ /* Label lift — moves the text only, so dot, icon and remove stay centred on the
46
+ box. Sentence-case Lato reads by its lowercase mass, which a box that centres
47
+ the cap band leaves ~1.2px low; the token lands the label between the cap and
48
+ x-height bands, where the eye places it. */
47
49
  .pill > .pill-label {
48
50
  display: inline-block;
49
- transform: translateY(-0.5px);
51
+ transform: translateY(calc(-1 * var(--optical-lift-sans, 0em)));
50
52
  }
53
+ /* All caps reads by the cap band, which is already level with the dot. */
54
+ .pill > .pill-label.is-caps,
55
+ .pill.is-caps > .pill-label,
56
+ .pill[style*="uppercase"] > .pill-label,
57
+ .pill > .pill-label[style*="uppercase"] { transform: translateY(calc(-1 * var(--optical-lift-caps, 0em))); }
51
58
 
52
59
  /* Trailing icon (e.g. clock, arrow) */
53
60
  .pill > i, .pill > svg,
@@ -205,3 +212,15 @@
205
212
  cursor: not-allowed;
206
213
  pointer-events: none;
207
214
  }
215
+
216
+ /* Optical centre for mixed-case labels. Glyphs were centred on the CAP height
217
+ (geometric error 0.06px), but labels are sentence case — "Approved",
218
+ "Pending" — and the eye weighs the x-height, ~0.11em lower, so the label read
219
+ low against the dot. Glyphs drop half that gap (0.055em ≈ 0.6px at 11px),
220
+ landing between cap and x-height. In em, so it scales with every pill size. */
221
+ .pill > .dot, .pill > svg, .pill > i, .pill > .pill-x, .pill > button { transform: translateY(0.055em); }
222
+
223
+ /* sm only: the 10px baseline snaps down a pixel in a 20px box, so the label —
224
+ in either case — gets its own lift. */
225
+ .pill.sm > .pill-label,
226
+ .pill.sm > .pill-label.is-caps { transform: translateY(calc(-1 * var(--optical-lift-sm, 0.06em))); }
@@ -270,7 +270,22 @@
270
270
  ---------------------------------------------------------------- */
271
271
  .dt tbody tr {
272
272
  border-bottom: 1px solid var(--color-border-subtle);
273
- transition: background var(--transition-fast);
273
+ }
274
+ /* Row hover fades in and out. Hover is painted on the <tr> by some rules and on
275
+ its cells by others (subrow groups, detail rows, actionable rows), so both
276
+ carry the transition — a fade on the row alone left those snapping. In is
277
+ quick so the row answers the pointer; out is slower so moving down a column
278
+ reads as one continuous wash instead of a flicker. */
279
+ .dt tbody tr,
280
+ .dt tbody tr > td {
281
+ transition: background-color var(--duration-slower) var(--ease-standard);
282
+ }
283
+ .dt tbody tr:hover,
284
+ .dt tbody tr:hover > td {
285
+ transition-duration: var(--duration-base);
286
+ }
287
+ @media (prefers-reduced-motion: reduce) {
288
+ .dt tbody tr, .dt tbody tr > td { transition: none; }
274
289
  }
275
290
  .dt tbody tr:last-child { border-bottom: 0; }
276
291
 
@@ -935,11 +950,9 @@
935
950
  opacity: 1;
936
951
  }
937
952
 
938
- /* A closed group must not glue to the parent — the parent keeps its own border
939
- until the group opens, otherwise two collapsed rows read as one. */
940
- .dt tbody tr.has-subrows.is-collapsible:not(.is-open) > td {
941
- border-bottom: 1px solid var(--color-border-default);
942
- }
953
+ /* A closed group keeps only the row's own hairline — the chevron already says
954
+ there is more. A second border on the cells stacked on top of it and read as
955
+ a heavier underline under every expandable row. */
943
956
 
944
957
  /* Disclosure control — sits in the parent's lead cell */
945
958
  .dt-subrow-toggle {
@@ -1218,7 +1231,11 @@ a.dt-detail-card:focus-visible { outline: none; box-shadow: var(--shadow-focus);
1218
1231
  .dt-toolbar.dt-toolbar-sm { min-height: calc(var(--control-h-sm) + 2 * var(--space-4) + var(--border-hairline)); padding: var(--space-4) var(--space-4); }
1219
1232
  .dt-toolbar-lead { display: inline-flex; align-items: baseline; gap: var(--space-2); min-width: 0; }
1220
1233
  .dt-toolbar-filters { display: inline-flex; align-items: center; gap: var(--space-2); min-width: 0; flex: 1 1 auto; }
1221
- .dt-toolbar-actions { margin-left: auto; display: inline-flex; align-items: center; gap: var(--space-2); flex-shrink: 0; }
1234
+ .dt-toolbar-actions { margin-left: auto; display: inline-flex; align-items: center; gap: var(--space-2); flex-shrink: 0;
1235
+ /* Pinned to the FIRST row: when the filters grow a second row (an apply bar,
1236
+ a wrapped chip set) the actions must not drift down to the middle. Order is
1237
+ sort | expand/collapse | exports. */
1238
+ align-self: flex-start; }
1222
1239
  /* Selection mode keeps the toolbar on the surface colour. The tint marks WHAT
1223
1240
  is selected; repeating it on the chrome dilutes that signal and the bar stops
1224
1241
  reading as chrome at all. The state is carried by content instead — the count
@@ -1380,3 +1397,100 @@ a.dt-detail-card:focus-visible { outline: none; box-shadow: var(--shadow-focus);
1380
1397
  .dt-outline-set { display: inline-flex; flex-wrap: wrap; gap: var(--space-2); }
1381
1398
  .dt.on-inverse .dt-outline,
1382
1399
  .dark .dt .dt-outline { border-color: var(--color-border-inverse); color: var(--color-foreground-inverse-default); }
1400
+
1401
+ /* Optical centring for small labels. A badge that inherits a tall line-height
1402
+ (15px on 10px type) is centred on its LINE BOX, and JetBrains Mono carries its
1403
+ ink high in that box — the caps sat 0.7px above centre. The pill model fixes it:
1404
+ line-height 1 inside a fixed-height flex box, which centres the glyph's em box.
1405
+ Heights are unchanged. */
1406
+ .dt-subrow-count { display: inline-flex; align-items: center; line-height: 1; height: 17px; padding-top: 0; padding-bottom: 0; box-sizing: border-box; }
1407
+ /* Optical nudge. Lato's cap centre sits ~0.3px below its em centre at 11px and
1408
+ pixel-snapping doubled it, so the bare label read 0.6px below the flag. The
1409
+ label is bare text and cannot be targeted, so the row lifts by half a pixel
1410
+ (1px off the bottom of a fixed-height box) and the flag / icon are handed that
1411
+ half-pixel back — only the text moves. */
1412
+ .dt-outline { line-height: 1; padding-bottom: 1px; }
1413
+ .dt-outline > .flag, .dt-outline > svg, .dt-outline > i, .dt-outline > img { margin-top: 1px; }
1414
+
1415
+ /* ----------------------------------------------------------------
1416
+ GROUP DIVIDER META — the summary on the right of a day/section divider
1417
+ ---------------------------------------------------------------- */
1418
+ /* Shipped in DTGroupDivider since 1.0.13 with no rule, so the meta ran straight
1419
+ into the label. Floated right (a <td> cannot be a flex row without breaking the
1420
+ table), muted, tabular so day totals line up down the ledger. */
1421
+ .dt-row-divider-meta {
1422
+ float: right;
1423
+ margin-left: var(--space-4);
1424
+ color: var(--color-foreground-muted);
1425
+ font-variant-numeric: tabular-nums;
1426
+ }
1427
+
1428
+ /* ----------------------------------------------------------------
1429
+ ROW ACTION "OPEN" — the row opens something; only the chevron toggles
1430
+ ---------------------------------------------------------------- */
1431
+ .dt tbody tr.has-subrows.is-actionable,
1432
+ .dt tbody tr.has-detail.is-actionable { cursor: pointer; }
1433
+ .dt tbody tr.is-actionable:hover > td { background: var(--color-background-hover); }
1434
+ /* The chevron is a live control here (unlike rowAction="toggle"). */
1435
+ .dt tbody tr.is-actionable .dt-subrow-toggle { pointer-events: auto; }
1436
+ /* The name as a button: looks like the text it replaces, owns the focus ring. */
1437
+ .dt-row-button {
1438
+ appearance: none; background: none; border: 0; padding: 0; margin: 0;
1439
+ font: inherit; color: inherit; text-align: inherit; letter-spacing: inherit;
1440
+ cursor: pointer; border-radius: var(--radius-xs);
1441
+ }
1442
+ .dt-row-button:hover { text-decoration: underline; text-underline-offset: 2px; }
1443
+ .dt-row-button:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
1444
+
1445
+ /* ----------------------------------------------------------------
1446
+ TOGGLE SPACER — the chevron's footprint on a row without children
1447
+ ---------------------------------------------------------------- */
1448
+ .dt-subrow-toggle.is-spacer { display: inline-block; width: 20px; height: 20px; flex-shrink: 0; pointer-events: none; }
1449
+
1450
+ /* ----------------------------------------------------------------
1451
+ LEDGER PATTERN — running balance column
1452
+ ---------------------------------------------------------------- */
1453
+ /* Any open-action row (with or without children) takes the pointer; the
1454
+ has-subrows-only rule above covers the parent of a group. */
1455
+ .dt tbody tr.is-actionable { cursor: pointer; }
1456
+ /* The balance after the entry: the same mono figure as the amount, demoted to
1457
+ regular weight and muted — it is context, the amount is the fact. An entry
1458
+ that has not settled moved no money, so it shows "—" (.is-empty). */
1459
+ .dt td.dt-num.dt-balance { font-weight: var(--w-regular); color: var(--color-foreground-muted); }
1460
+ .dt td.dt-num.dt-balance.is-empty { color: var(--color-foreground-disabled); }
1461
+ /* A void row's balance is not struck: there is nothing to annul, just no value. */
1462
+ .dt tbody tr.is-muted td.dt-num.dt-balance { text-decoration: none; }
1463
+
1464
+ /* ----------------------------------------------------------------
1465
+ FILTERED BAND — "Showing n of total", above the toolbar
1466
+ ----------------------------------------------------------------
1467
+ A thin strip inside .dt-wrap, above DTToolbar, that exists only while a filter
1468
+ is applied (a status tab other than All counts). Neutral on purpose: it is a
1469
+ state note, not an alert. grid 0fr → 1fr so it takes no space when closed.
1470
+ Use it when the count does not need a fixed place in the toolbar lead; keep
1471
+ the lead count when the total is always worth reading. */
1472
+ .dt-filtered {
1473
+ display: grid; grid-template-rows: 0fr;
1474
+ transition: grid-template-rows var(--duration-base) var(--ease-entrance);
1475
+ }
1476
+ .dt-filtered.is-open { grid-template-rows: 1fr; }
1477
+ .dt-filtered-inner {
1478
+ min-height: 0; overflow: hidden;
1479
+ display: flex; align-items: center; gap: var(--space-2);
1480
+ background: var(--color-background-subtle);
1481
+ color: var(--color-foreground-secondary);
1482
+ font-size: var(--fs-xs); line-height: 1; font-variant-numeric: tabular-nums;
1483
+ }
1484
+ .dt-filtered.is-open .dt-filtered-inner {
1485
+ padding: var(--space-2) var(--space-5);
1486
+ border-bottom: var(--border-hairline) solid var(--color-border-subtle);
1487
+ }
1488
+ .dt-filtered-inner > svg { width: 13px; height: 13px; flex-shrink: 0; stroke-width: var(--icon-stroke); }
1489
+ @media (prefers-reduced-motion: reduce) { .dt-filtered { transition: none; } }
1490
+
1491
+ /* A closed subrow is a 1px-tall row that still carried its own hairlines (the
1492
+ row's, and the group-closing one on .is-last), so they stacked under the
1493
+ parent's and read as a heavier underline. Closed, it draws nothing; the
1494
+ parent's hairline is the only separator until the group opens. */
1495
+ .dt tbody tr.dt-subrow.is-collapsible:not(.is-open),
1496
+ .dt tbody tr.dt-subrow.is-collapsible:not(.is-open) > td { border-bottom: 0; border-top: 0; }
@@ -135,3 +135,12 @@
135
135
  flex-wrap: wrap;
136
136
  gap: var(--space-2);
137
137
  }
138
+
139
+ /* The label is bare text in the flex row and inherited the page line-height,
140
+ which put it 0.2–0.6px below centre. line-height 1 centres its em box. */
141
+ .toggle-chip { line-height: 1; } /* label */
142
+ /* Same optical nudge as .dt-outline: 11px Lato in a 24px box snaps its caps
143
+ 0.6px low. Lift the row half a pixel and hand it back to the glyphs, so only
144
+ the bare label moves. */
145
+ .toggle-chip-sm { padding-bottom: 1px; }
146
+ .toggle-chip-sm > svg, .toggle-chip-sm > i, .toggle-chip-sm > .toggle-chip-count { margin-top: 1px; }
@@ -12,6 +12,7 @@
12
12
  @import "./components/cards.css";
13
13
  @import "./components/combobox.css";
14
14
  @import "./components/empty-states.css";
15
+ @import "./components/filters.css";
15
16
  @import "./components/inputs.css";
16
17
  @import "./components/number-input.css";
17
18
  @import "./components/overlays.css";
@@ -673,6 +673,13 @@
673
673
  Swapping the mono family = change the two lines above/below (and a product
674
674
  theme may override the pair). Set to `none` to disable the whole mechanism. */
675
675
  --font-mono-adjust: 0.5065; /* @kind font */
676
+ /* Optical lift for short labels in a fixed-height box (pills, tags, badges).
677
+ Tuned by eye: at md and lg the label already reads centred on its dot, so
678
+ both are 0. The sm pill is the exception — at 10px Lato's baseline lands on
679
+ a fractional pixel and snaps down, leaving the text ~1px low in a 20px box. */
680
+ --optical-lift-sans: 0em; /* @kind font */
681
+ --optical-lift-caps: 0em; /* @kind font */
682
+ --optical-lift-sm: 0.06em; /* @kind font */ /* 0.6px at 10px — tuned by eye */
676
683
  --font-display: 'Lato', -apple-system, BlinkMacSystemFont, sans-serif;
677
684
 
678
685
  /* Weights (per manual) */
@@ -13,6 +13,7 @@ export interface ComboOption {
13
13
  disabled?: boolean;
14
14
  /** Text used by the search filter when label isn't a plain string. */
15
15
  searchText?: string;
16
+ icon?: React.ReactNode;
16
17
  }
17
18
 
18
19
  export type ComboboxStatus = 'default' | 'error';
@@ -50,6 +51,8 @@ export interface ComboboxProps {
50
51
  needs a direction: 'start' grows right (default), 'end' grows left. Use
51
52
  'end' for a compact control near the right edge. */
52
53
  align?: 'start' | 'end';
54
+ size?: 'sm' | 'md';
55
+ iconOnly?: boolean;
53
56
  className?: string;
54
57
  style?: React.CSSProperties;
55
58
  'aria-label'?: string;