active_admin_theme 3.1.0 → 3.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.
@@ -55,6 +55,9 @@ $skinTitleBarBorderColor: $skinMainSecondColor!default;
55
55
  $skinTitleBarBorderWidth: 0!default;
56
56
  $skinTitleBarButtonPaddingY: 6px!default; // action button vertical padding
57
57
  $skinTitleBarButtonPaddingX: 10px!default; // action button horizontal padding
58
+ // Minimum bar height — raise it alongside the button paddings above if your
59
+ // buttons outgrow the default band (the bar still grows to fit its content).
60
+ $skinTitleBarHeight: 37px!default;
58
61
 
59
62
  // Panel/section headers + tabs accent (the blue bar on panel headers and the
60
63
  // inactive tabs). $skinPanelHeaderColor is the base/light value;
@@ -130,7 +133,7 @@ $skinInactiveTabTextColor: #5e6469!default;
130
133
  $skinInactiveTabTextColorDark: #b0b8c2!default;
131
134
  // Index-table column header text, one colour for sortable and plain headers.
132
135
  $skinTableHeaderTextColor: $skinTextColor!default;
133
- $skinTableHeaderTextColorDark: #dde2e8!default;
136
+ $skinTableHeaderTextColorDark: $skinTextColorDark!default;
134
137
 
135
138
  // Status tags. The label is the same on every filled tag in both modes. The
136
139
  // fills below are dark enough to carry a white one: every one of the five is
@@ -214,6 +217,20 @@ $skinStatusTagErrorColor: #ce483b!default; // error / errored / red
214
217
  --aa-active-tab-text: #{$skinActiveTabTextColor};
215
218
  --aa-inactive-tab-text: #{$skinInactiveTabTextColor};
216
219
  --aa-delete-link: #{$skinDeleteLinkColor};
220
+
221
+ // Bridge for the companion active_admin_datetimepicker plugin: feed its
222
+ // --aa-datetimepicker-* inputs from this theme's palette, so the xdsoft picker
223
+ // is themed (light + dark) automatically with no per-app override. These point
224
+ // at the --aa-* vars, which are themselves redefined per mode, so a single set
225
+ // here covers both. Harmless no-op when the plugin isn't installed (nothing
226
+ // reads these). A host can still override any of them downstream.
227
+ --aa-datetimepicker-accent: var(--aa-accent);
228
+ --aa-datetimepicker-surface: var(--aa-surface);
229
+ --aa-datetimepicker-surface-2: var(--aa-surface-2);
230
+ --aa-datetimepicker-text: var(--aa-text);
231
+ --aa-datetimepicker-text-muted: var(--aa-text-muted);
232
+ --aa-datetimepicker-border: var(--aa-border);
233
+ --aa-datetimepicker-hover: var(--aa-surface-hover);
217
234
  }
218
235
  // OS preference, unless the user explicitly chose light
219
236
  @media (prefers-color-scheme: dark) {
@@ -233,13 +250,27 @@ html[data-theme="dark"] { @include aa-dark-palette; }
233
250
  html[data-theme="dark"] { @content; }
234
251
  }
235
252
 
236
- // Иконки переключателя тем. Инлайном, потому что гем не возит картинок, и
237
- // маской, а не цветным SVG: маска красится currentColor и сама идёт за
238
- // $skinMenuTextColor. Половинка круга — auto, солнце — light, месяц — dark.
253
+ // Sort markers. Three shapes rather than one at two opacities: ActiveAdmin's
254
+ // sprite draws a double arrow for sortable-but-unsorted, and reusing the
255
+ // descending triangle for it leaves the two states indistinguishable on a
256
+ // freshly loaded index. Masked, like the theme switch icons, so they take
257
+ // currentColor and follow the heading into dark mode.
258
+ $sort-icon-none: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 9 12'%3E%3Cpath d='M4.5 0 9 5H0z'/%3E%3Cpath d='M4.5 12 0 7h9z'/%3E%3C/svg%3E")!default;
259
+ $sort-icon-asc: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 9 12'%3E%3Cpath d='M4.5 2 9 8H0z'/%3E%3C/svg%3E")!default;
260
+ $sort-icon-desc: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 9 12'%3E%3Cpath d='M4.5 10 0 4h9z'/%3E%3C/svg%3E")!default;
261
+
262
+ // Theme switch icons. Inline, because the gem ships no image files, and as a
263
+ // mask rather than a coloured SVG: a mask takes currentColor and so follows
264
+ // $skinMenuTextColor on its own. Half circle = auto, sun = light, moon = dark.
239
265
  $theme-icon-auto: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='6.6' fill='none' stroke='black' stroke-width='1.4'/%3E%3Cpath d='M8 2.6a5.4 5.4 0 0 1 0 10.8z'/%3E%3C/svg%3E")!default;
240
266
  $theme-icon-light: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.4' stroke-linecap='round'%3E%3Ccircle cx='8' cy='8' r='3.1'/%3E%3Cpath d='M8 .9v1.8M8 13.3v1.8M.9 8h1.8M13.3 8h1.8M2.9 2.9l1.3 1.3M11.8 11.8l1.3 1.3M13.1 2.9l-1.3 1.3M4.2 11.8l-1.3 1.3'/%3E%3C/svg%3E")!default;
241
267
  $theme-icon-dark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M13.4 9.6A5.9 5.9 0 0 1 6.4 2.6a5.9 5.9 0 1 0 7 7z'/%3E%3C/svg%3E")!default;
242
268
 
269
+ // Logout drawn as a sign-out glyph instead of the word "Logout", so it matches
270
+ // the icon toggle beside it. Masked like the theme icons above, so it takes
271
+ // currentColor and follows $skinMenuTextColor; a consumer can override the var.
272
+ $logout-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6.5 2.5H3.2v11h3.3'/%3E%3Cpath d='M12.8 8H6'/%3E%3Cpath d='M9.5 5 12.8 8l-3.3 3'/%3E%3C/svg%3E")!default;
273
+
243
274
  // A wrong-typed override is legal SassScript, so it compiles without a word and
244
275
  // emits CSS the browser drops — `border-bottom: none solid #5ea3d3`,
245
276
  // `padding: 10 12px`. The rule silently vanishes and ActiveAdmin's own value
@@ -337,11 +368,11 @@ $theme-icon-dark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/s
337
368
  @error "$#{$name} must be a color or a CSS custom property (e.g. var(--aa-page-bg)), got `#{$value}`.";
338
369
  }
339
370
  }
340
- // Радиус внутреннего угла ячейки — на 1px меньше внешнего, чтобы не торчал
341
- // из-под рамки таблицы. Вычитать px напрямую нельзя: $skinBorderRadius задаёт
342
- // проект, и 0.25rem роняет всю компиляцию с «Incompatible units». calc()
343
- // откладывает вычитание до браузера, а безразмерный 0 обрабатываем отдельно —
344
- // calc(0 - 1px) невалиден.
371
+ // A cell's inner corner is 1px tighter than the outer one, so it does not
372
+ // show from under the table border. Subtracting px directly will not do: the
373
+ // radius is a project's to set, and 0.25rem fails the whole compile with
374
+ // "Incompatible units". calc() defers the subtraction to the browser, and a
375
+ // unitless 0 is handled separately because calc(0 - 1px) is invalid.
345
376
  @function inner-radius($radius) {
346
377
  @if unitless($radius) { @return 0; }
347
378
  @return calc(#{$radius} - 1px);
@@ -352,10 +383,35 @@ $theme-icon-dark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/s
352
383
  html {
353
384
  background: var(--aa-page-bg);
354
385
  }
386
+
387
+ // Pin a bar (#header, #title_bar) to the viewport's left edge at the visible
388
+ // viewport width while a wide index table scrolls the whole page sideways
389
+ // (#wrapper is display:table, min-width:100%). width = 100vw minus the vertical
390
+ // scrollbar — the JS-set --scrollbar-width var (0 when wigu/scrollbar_width is
391
+ // absent, and then max-width:100% keeps 100vw's scrollbar allowance from adding a
392
+ // stray horizontal scrollbar). It's a plain max-width, not min(…, 100%):
393
+ // sassc/libsass evaluates the Sass min() function and rejects calc() as a
394
+ // non-number. No `top`, so the bar still scrolls vertically.
395
+ @mixin viewport-pinned-bar {
396
+ position: sticky;
397
+ left: 0;
398
+ width: calc(100vw - var(--scrollbar-width, 0px));
399
+ max-width: 100%;
400
+ box-sizing: border-box;
401
+ }
402
+
355
403
  body.active_admin {
356
404
  background: var(--aa-page-bg);
357
405
  }
358
406
  #wrapper {
407
+ // ActiveAdmin lays #wrapper out as `display: table`, which sizes to its
408
+ // content: a wide index table therefore scrolls the WHOLE page horizontally
409
+ // (the master / pre-theme behaviour admins are used to). Only guarantee the
410
+ // layout fills the viewport when the content is narrow. The utility nav
411
+ // (clock / theme toggle / logout) is pinned `sticky` (see #utility_nav below)
412
+ // so it stays reachable while the page scrolls sideways.
413
+ min-width: 100%;
414
+
359
415
  div.tabs {
360
416
  > ul > li > a {
361
417
  background-image: none;
@@ -418,6 +474,12 @@ body.active_admin {
418
474
  border: none;
419
475
  text-shadow: none;
420
476
  background-image: none;
477
+
478
+ // A wide index table scrolls the WHOLE page sideways. Pin the header on
479
+ // screen at the viewport width so it doesn't stretch to the table's width
480
+ // (which spread the menu out under the utility nav and overlapped it); it
481
+ // still grows to fit a wrapped menu, ActiveAdmin's natural layout.
482
+ @include viewport-pinned-bar;
421
483
  h1.site_title {
422
484
  color: $skinMainSecondColor;
423
485
  font-size: 16px;
@@ -448,10 +510,34 @@ body.active_admin {
448
510
  // tabs: drop the pill padding, and on hover highlight by text colour only — no
449
511
  // background block. The clock has no click action, so it gets no hover at all.
450
512
  #utility_nav {
513
+ // Sits at the top-right of the header; the header itself is pinned on screen
514
+ // (sticky, see #header above) so the clock / theme toggle / logout stay
515
+ // reachable while a wide table scrolls the page sideways.
516
+ z-index: 2;
517
+ white-space: nowrap;
518
+
451
519
  > li > a { padding: 0 5px; }
452
520
  > li:hover > a, > li.current > a { background-color: transparent; }
453
521
  > li#servertime:hover > a { color: #ffffff; }
454
522
 
523
+ // Server clock (server_clock.js), rendered as two compact lines like
524
+ // yeti-web: time + timezone (larger) over year/month/day (smaller). A
525
+ // right-aligned flex column sized to the nav band (~24px) so both lines
526
+ // fit the header height unchanged. Monospace + tabular figures keep the
527
+ // ticking time a constant width, so the adjacent logout doesn't jitter.
528
+ > li#servertime > span {
529
+ display: inline-flex;
530
+ flex-direction: column;
531
+ justify-content: center;
532
+ align-items: flex-end;
533
+ vertical-align: middle;
534
+ height: 24px;
535
+ font-family: 'SFMono-Regular', Menlo, Consolas, 'Liberation Mono', monospace;
536
+ font-variant-numeric: tabular-nums;
537
+ }
538
+ > li#servertime .clock-time { font-size: 12px; line-height: 1.1; }
539
+ > li#servertime .clock-date { font-size: 9px; line-height: 1.1; }
540
+
455
541
  // The theme switch (app/assets/javascripts/wigu/theme_toggle.js). The
456
542
  // script only writes data-mode; the glyph is drawn here, so a project can
457
543
  // swap it for its own icon font without forking the script. Masked rather
@@ -488,6 +574,35 @@ body.active_admin {
488
574
  -webkit-mask-image: $theme-icon-dark;
489
575
  mask-image: $theme-icon-dark;
490
576
  }
577
+
578
+ // Logout as an icon ($logout-icon), to match the toggle beside it.
579
+ // font-size:0 hides the "Logout" text visually; the accessible name is
580
+ // the aria-label the gem's Ruby puts on the anchor, because zero-sized
581
+ // text is pruned from the accessibility tree by some screen readers
582
+ // (the toggle gets its aria-label from theme_toggle.js the same way).
583
+ // The masked glyph follows the bar's text colour. The real logout
584
+ // link/method from add_logout_button_to_menu is untouched.
585
+ // Scoped to a.wigu-logout — the class the gem's Ruby puts on the item
586
+ // it adds — so a consumer-rendered logout keeps its own label instead
587
+ // of collapsing to an unlabeled glyph.
588
+ > li#logout:has(> a.wigu-logout) { display: inline-flex; align-items: center; vertical-align: middle; }
589
+ > li#logout > a.wigu-logout {
590
+ display: flex;
591
+ align-items: center;
592
+ font-size: 0;
593
+ opacity: 0.75;
594
+
595
+ &:before {
596
+ content: "";
597
+ display: block;
598
+ width: 16px;
599
+ height: 16px;
600
+ background-color: currentColor;
601
+ -webkit-mask: #{$logout-icon} center / contain no-repeat;
602
+ mask: #{$logout-icon} center / contain no-repeat;
603
+ }
604
+ &:hover, &:focus { opacity: 1; }
605
+ }
491
606
  }
492
607
  ul.tabs {
493
608
  li.has_nested {
@@ -557,26 +672,87 @@ body.active_admin {
557
672
  background-color: $skinTitleBarColor;
558
673
  border: none;
559
674
  border-bottom: $skinTitleBarBorderWidth solid $skinTitleBarBorderColor;
675
+ // Pin the whole bar to the viewport like #header, so the title AND the action
676
+ // buttons stay visible while a wide index table scrolls the page sideways.
677
+ @include viewport-pinned-bar;
678
+ // Lay the title (left) and action items (right) on one compact row. Without
679
+ // this, resetting #titlebar_right to static (below) stacks it under the
680
+ // title and the bar grows tall. min-height, NOT height: with a fixed height
681
+ // the bar cannot contain its content — action items that wrap on a narrow
682
+ // window would overflow it and paint over the page — while min-height still
683
+ // makes breadcrumb (detail) pages match plain index pages. Drop the base
684
+ // clearfix — in a flex container it becomes a stray third item that
685
+ // space-between strands right.
686
+ display: flex;
687
+ align-items: center;
688
+ justify-content: space-between;
689
+ min-height: $skinTitleBarHeight;
690
+ &::after { display: none; }
560
691
  #titlebar_left {
692
+ display: flex;
693
+ align-items: center;
694
+ gap: 6px;
695
+ height: auto;
696
+ // Let the title shrink below its min-content width: flex items refuse to
697
+ // otherwise, and a long nowrap record title would push the action items
698
+ // out of the pinned bar — unreachable past the viewport on wide pages.
699
+ min-width: 0;
561
700
  span.breadcrumb {
562
701
  font-size: 12px;
563
702
  text-shadow: none;
564
- margin-bottom: 8px;
703
+ margin-bottom: 0;
565
704
  a, span {
566
- color: lighten( $skinMainFirstColor, 30% )!important; ;
705
+ color: lighten( $skinMainFirstColor, 30% )!important;
567
706
  }
568
707
  }
708
+ // Own a sensible title size so consumer apps don't each shrink AA's large
709
+ // default (which towered over the breadcrumb). margin:0 + the flex parent
710
+ // centre it on the breadcrumb's line; nowrap + ellipsis keep a long
711
+ // record title (show pages) on one line without pushing the action
712
+ // items out of the bar — min-width: 0 lets it actually shrink.
569
713
  h2 {
570
714
  color: #ffffff;
571
715
  text-shadow: none;
716
+ font-size: 16px;
717
+ margin: 0;
718
+ min-width: 0;
719
+ // Screen only: AA registers the stylesheet with media "all", and paper
720
+ // has no horizontal scroll — a nowrap/ellipsis title would print
721
+ // truncated at the page edge where it used to wrap.
722
+ @media screen {
723
+ white-space: nowrap;
724
+ overflow: hidden;
725
+ text-overflow: ellipsis;
726
+ }
572
727
  }
573
728
  }
574
729
  #titlebar_right {
730
+ // Horizontal pinning is handled by #title_bar (sticky + viewport width),
731
+ // so the action items just sit at the right of the bar. Reset any
732
+ // consumer `position: absolute/sticky` (each app used to pin the buttons
733
+ // itself via local stylesheet overrides).
734
+ position: static;
735
+ // Shrink to the action-items height (some apps/base leave it ~50px, taller
736
+ // than the 37px bar, so centering pushed it over the top edge).
737
+ height: auto;
738
+ // Space multiple action items apart (e.g. a Menu dropdown next to a New
739
+ // button); the base leaves them flush on this side.
740
+ .action_items > .action_item:not(:last-child):not(:empty) { margin-right: 10px; }
741
+ // The form selectors cover button_to action items: Rails renders them as
742
+ // form > input[type=submit] (or button), which the anchor selectors
743
+ // miss — consumer apps were re-hardcoding the 6px/10px padding to
744
+ // compensate, desyncing from the $skin* knobs on every change.
575
745
  // `> a` and `> .dropdown_menu > a`: an action item may hold a dropdown, and
576
746
  // a descendant selector here paints its menu entries with the button fill
577
747
  // too — with !important, so the entry rules below cannot undo it.
748
+ // A button_to action item renders as a block <form>; inline-block
749
+ // keeps it on the bar's single row (consumers used to float it, which
750
+ // collapsed the flex bar's height instead).
751
+ .action_item > form { display: inline-block; }
578
752
  .action_item > a,
579
- .action_item > .dropdown_menu > a {
753
+ .action_item > .dropdown_menu > a,
754
+ .action_item > form > input[type="submit"],
755
+ .action_item > form > button {
580
756
  background-image: none;
581
757
  border: none;
582
758
  box-shadow: none;
@@ -592,7 +768,28 @@ body.active_admin {
592
768
  }
593
769
  }
594
770
  div.dropdown_menu {
771
+ // Containing block for the panel below, so `right: 0` means "the
772
+ // trigger's right edge". AA leaves this element unpositioned, which
773
+ // makes the panel resolve against #title_bar (sticky) and park at the
774
+ // bar's far right no matter which action item was clicked. inline-block
775
+ // over AA's `display: inline`: an inline box's containing block spans
776
+ // its text fragments, so the markup's whitespace around the button
777
+ // pushed the panel a few px past the button's edge.
778
+ position: relative;
779
+ display: inline-block;
595
780
  div.dropdown_menu_list_wrapper {
781
+ // Always right-align title-bar dropdowns under their own button,
782
+ // overriding AA's inline positioning. AA 3.x's _position() sets
783
+ // inline `left` OR `right` per open depending on a window-geometry
784
+ // branch and never clears the other, and the wrapper persists across
785
+ // opens — after a resize between opens both offsets accumulate and
786
+ // the width:auto wrapper stretches from the stale left coordinate to
787
+ // the right edge. Its `right` branch is wrong here anyway: it offsets
788
+ // from the window, not from the bar. Consumer apps used to carry
789
+ // exactly this pair as a local hotfix; the theme owns the bar now, so
790
+ // it owns the guard.
791
+ left: auto !important;
792
+ right: 0 !important;
596
793
  background-color: var(--aa-elevated);
597
794
  border: 1px solid var(--aa-border);
598
795
  background-image: none;
@@ -603,6 +800,12 @@ body.active_admin {
603
800
  // that is invisible whenever the panel fill is close to the page —
604
801
  // which is exactly what a light surface is. Outline it like the panel.
605
802
  div.dropdown_menu_nipple {
803
+ // The panel is pinned right (above) while AA still offsets the
804
+ // arrow from the panel's LEFT edge by the button's centre, which
805
+ // lands it on empty bar. Pin it right too, centred on the button's
806
+ // own caret (AA draws that 6px wide at right: 12px).
807
+ left: auto !important;
808
+ right: 9px !important;
606
809
  border-color: var(--aa-border) transparent;
607
810
  &:before, &:after { border-color: var(--aa-elevated) transparent; }
608
811
  }
@@ -630,6 +833,12 @@ body.active_admin {
630
833
  }
631
834
  }
632
835
  .flashes {
836
+ // Pin like #header/#title_bar: this is the one full-width bar between the
837
+ // two pinned ones, and on a wide page scrolled right its left-aligned text
838
+ // would leave the viewport, leaving an unreadable colored stripe. AA puts
839
+ // all padding/background on the inner .flash, so the always-rendered empty
840
+ // wrapper stays zero-height and inert on flash-less pages.
841
+ @include viewport-pinned-bar;
633
842
  .flash {
634
843
  color: #ffffff;
635
844
  background-image: none;
@@ -905,27 +1114,27 @@ body.active_admin {
905
1114
  // link keeps its full width so the whole cell stays clickable, and the
906
1115
  // marker takes currentColor — the stock sprite is a fixed grey PNG that
907
1116
  // cannot follow the text into dark mode.
908
- // `a[href*="order="]`, not every anchor in the header: an application can
909
- // put its own link in there — yeti-web adds a persistent-sort toggle — and
910
- // it would otherwise get a sort marker of its own. ActiveAdmin's heading
911
- // link always carries the order parameter.
912
- th.sortable > a[href*="order="] {
1117
+ // The sprite goes from every anchor in a sortable heading, because
1118
+ // ActiveAdmin sets it on every one — an application link in there would
1119
+ // otherwise keep the low-res PNG and the 13px indent it needs.
1120
+ th.sortable a {
913
1121
  padding-left: 0;
914
1122
  background-image: none;
1123
+ }
1124
+ // The marker, though, only on ActiveAdmin's own heading link, which always
1125
+ // carries the order parameter: an application's link is its own business.
1126
+ th.sortable > a[href*="order="] {
915
1127
 
916
1128
  &:after {
917
1129
  content: "";
918
1130
  display: inline-block;
1131
+ width: 9px;
1132
+ height: 12px;
919
1133
  margin-left: 6px;
920
1134
  vertical-align: middle;
921
- // The unused side has no width rather than a transparent one, so the
922
- // box is exactly as tall as the triangle in it. Keeping all four sides
923
- // and nudging with a margin instead puts the two states at different
924
- // heights, because `vertical-align: middle` centres the box and the
925
- // visible half then sits off-centre within it.
926
- border: 4px solid transparent;
927
- border-bottom-width: 0;
928
- border-top-color: currentColor;
1135
+ background-color: currentColor;
1136
+ -webkit-mask: #{$sort-icon-none} center / contain no-repeat;
1137
+ mask: #{$sort-icon-none} center / contain no-repeat;
929
1138
  // 0.6, not lower: the marker is the only thing separating a sortable
930
1139
  // heading from a plain one, so WCAG 1.4.11 asks 3:1 of it. Against the
931
1140
  // header fill it gives 3.40 light and 4.22 dark; at 0.4 it was 2.13
@@ -934,13 +1143,15 @@ body.active_admin {
934
1143
  }
935
1144
  }
936
1145
  th.sorted-asc > a[href*="order="]:after {
937
- border-top-width: 0;
938
- border-bottom-width: 4px;
939
- border-top-color: transparent;
940
- border-bottom-color: currentColor;
1146
+ -webkit-mask-image: $sort-icon-asc;
1147
+ mask-image: $sort-icon-asc;
1148
+ opacity: 1;
1149
+ }
1150
+ th.sorted-desc > a[href*="order="]:after {
1151
+ -webkit-mask-image: $sort-icon-desc;
1152
+ mask-image: $sort-icon-desc;
941
1153
  opacity: 1;
942
1154
  }
943
- th.sorted-desc > a[href*="order="]:after { opacity: 1; }
944
1155
  // Right edge = a single 1px line on the last-column cells (header th, body
945
1156
  // td, footer cells) coloured like the table border, since the table itself
946
1157
  // no longer draws a right border.
@@ -1205,6 +1416,38 @@ table.index_table thead th a {
1205
1416
  color: var(--aa-table-header-text) !important;
1206
1417
  }
1207
1418
 
1419
+ // Sticky table headers: keep the column labels visible while a long index table
1420
+ // scrolls vertically. The header/title bar pin only horizontally (no `top`), so
1421
+ // they scroll away on vertical scroll and this thead pins to the viewport top —
1422
+ // no overlap. Scoped to .paginated_collection so it only hits real AA index
1423
+ // listings, not custom report/dashboard tables.
1424
+ //
1425
+ // Opting out: AA 3.x hardcodes the index table's class attribute and ignores
1426
+ // `index class:` options, so .skip-sticky-headers is reachable only on tables
1427
+ // whose markup you render yourself. For real AA listings, opt out app-wide
1428
+ // with one CSS line in your own stylesheet:
1429
+ // .paginated_collection table.index_table thead tr th { position: static; }
1430
+ // Opt a hand-built table IN with .with-sticky-headers.
1431
+ // tr:first-child only: with a multi-row thead (group header over column
1432
+ // header) every row would stick at the same offset and paint over each other —
1433
+ // per-row offsets need measurements CSS alone can't express. The first row
1434
+ // pins; any further header rows scroll away normally.
1435
+ .paginated_collection table.index_table:not(.skip-sticky-headers) thead tr:first-child th,
1436
+ table.with-sticky-headers thead tr:first-child th {
1437
+ position: sticky;
1438
+ top: -1px;
1439
+ z-index: 98;
1440
+ }
1441
+ // Square the corner cells of sticky theads: their inner-radius top corners are
1442
+ // transparent, so once the header detaches from the table's rounded border the
1443
+ // rows scrolling beneath flicker through the ~3px notches. At rest the square
1444
+ // background overpaints the border's rounding by the same ~3px — the lesser,
1445
+ // static artifact (CSS cannot tell "stuck" from "at rest").
1446
+ .paginated_collection table.index_table:not(.skip-sticky-headers) thead tr:first-child th:first-child,
1447
+ table.with-sticky-headers thead tr:first-child th:first-child { border-top-left-radius: 0; }
1448
+ .paginated_collection table.index_table:not(.skip-sticky-headers) thead tr:first-child th:last-child,
1449
+ table.with-sticky-headers thead tr:first-child th:last-child { border-top-right-radius: 0; }
1450
+
1208
1451
  // Form section header: a <legend> interrupts the fieldset's top border, so the
1209
1452
  // header bar has no border around it (unlike a panel header). Float the legend
1210
1453
  // so it flows as a normal block and the fieldset border wraps around it.
@@ -1349,7 +1592,47 @@ form fieldset > ol > li fieldset ol li {
1349
1592
  border-color: var(--aa-border);
1350
1593
  }
1351
1594
  .dropdown_menu .dropdown_menu_list_wrapper .dropdown_menu_list li {
1352
- border-bottom-color: var(--aa-border);
1595
+ // Was repainted to --aa-border, but a hairline under every row makes the
1596
+ // flyout read as a table of cells rather than a menu. Drop it — the hover
1597
+ // highlight is enough separation. (Same depth as the base rule, later wins.)
1598
+ border-bottom: none;
1599
+ }
1600
+
1601
+ // Dropdown-button caret. ActiveAdmin draws it with two stacked triangles on the
1602
+ // button: :before is a #FFF "shadow", :after the #777 arrow — both hard-coded.
1603
+ // On the themed blue title-bar action button and on the dark elevated
1604
+ // table-tools buttons the #777 arrow is ~invisible. Recolor it to the button's
1605
+ // own text colour and hide the light shadow into the fill.
1606
+ #titlebar_right .dropdown_menu .dropdown_menu_button {
1607
+ &:before { border-top-color: var(--aa-button); }
1608
+ &:after { border-top-color: var(--aa-button-text); }
1609
+ }
1610
+ .table_tools .dropdown_menu .dropdown_menu_button,
1611
+ .batch_actions_selector .dropdown_menu_button {
1612
+ &:before { border-top-color: var(--aa-elevated); }
1613
+ &:after { border-top-color: var(--aa-text); }
1614
+ }
1615
+
1616
+ // Disabled form controls. The theme recolors inputs/selects via --aa-*, but a
1617
+ // disabled one looked identical to an enabled one (especially in dark). Mute
1618
+ // the fill, text and cursor so "disabled" reads in both modes. select2 sets its
1619
+ // control bg with !important, so the disabled override needs it too.
1620
+ #active_admin_content {
1621
+ input:disabled, textarea:disabled, select:disabled {
1622
+ background-color: var(--aa-surface-2) !important;
1623
+ color: var(--aa-text-muted) !important;
1624
+ opacity: 0.6;
1625
+ cursor: not-allowed !important;
1626
+ }
1627
+ }
1628
+ // Both container classes (--default --disabled) so this out-specifies the
1629
+ // enabled control rule's `--default` fill (which is !important) regardless of
1630
+ // source order.
1631
+ .select2-container--default.select2-container--disabled .select2-selection {
1632
+ background-color: var(--aa-surface-2) !important;
1633
+ opacity: 0.6;
1634
+ cursor: not-allowed !important;
1635
+ .select2-selection__rendered { color: var(--aa-text-muted) !important; }
1353
1636
  }
1354
1637
 
1355
1638
  .ui-dialog {
@@ -1359,6 +1642,11 @@ form fieldset > ol > li fieldset ol li {
1359
1642
  background-color: var(--aa-surface);
1360
1643
  color: var(--aa-text);
1361
1644
  border-bottom: 1px solid var(--aa-border);
1645
+ // jQuery-UI's default `minHeight: 150` option is applied inline to the content
1646
+ // box (min-height:~54px), so a one-line confirm leaves a big empty area
1647
+ // between the message and the buttons. Let the content size to its text
1648
+ // (the inline min-height needs !important to beat).
1649
+ .ui-dialog-content { min-height: 0 !important; }
1362
1650
  .ui-dialog-titlebar {
1363
1651
  background-image: none;
1364
1652
  box-shadow: none;
@@ -1454,6 +1742,28 @@ form.filter_form .filter_form_field.filter_date_range .seperator {
1454
1742
  width: 10px;
1455
1743
  }
1456
1744
 
1745
+ // Date-only range filters (jQuery-UI datepicker, input.datepicker) get no width
1746
+ // from the base, so the two inputs fall back to their size="12" intrinsic width
1747
+ // and line up side by side only by luck; the label's float:right "Set range"
1748
+ // link then leaks into the input band on a longer label and bumps the "To"
1749
+ // input onto a second line. Lay the field out with flexbox: the label takes a
1750
+ // full row, the two inputs share the next, immune to the float.
1751
+ form.filter_form .filter_form_field.filter_date_range {
1752
+ display: flex;
1753
+ flex-wrap: wrap;
1754
+
1755
+ > label { flex: 0 0 100%; }
1756
+
1757
+ input.datepicker {
1758
+ box-sizing: border-box;
1759
+ flex: 1 1 0;
1760
+ min-width: 0;
1761
+ margin: 0;
1762
+
1763
+ & + input.datepicker { margin-left: 6px; }
1764
+ }
1765
+ }
1766
+
1457
1767
  //filter select box border radius
1458
1768
  .filter_form_field select {
1459
1769
  border-radius: 3px;
@@ -1538,8 +1848,10 @@ input[type='radio'] {
1538
1848
  // the seven :not()s are worth seven class-level points, and that beat every
1539
1849
  // component rule declared later — pagination links and the Clear Filters
1540
1850
  // button came out --aa-link instead of --aa-text.
1541
- a:where(:not(.button):not(.action_item):not(.table_tools_button):not(.dropdown_menu_button):not(.ui-tabs-anchor):not(.delete_link):not([data-method="delete"])) { color: var(--aa-link); }
1542
- a[data-method="delete"], a.delete_link { color: var(--aa-delete-link); }
1851
+ a:where(:not(.button):not(.action_item):not(.table_tools_button):not(.dropdown_menu_button):not(.ui-tabs-anchor):not(.delete_link):not(.delete-link):not([data-method="delete"])) { color: var(--aa-link); }
1852
+ // `.delete-link` (hyphen) as well as AA's own `.delete_link`: some apps name
1853
+ // their soft-delete links with the hyphen.
1854
+ a[data-method="delete"], a.delete_link, a.delete-link { color: var(--aa-delete-link); }
1543
1855
 
1544
1856
  // Pagination — non-current page chips use a light-grey bg in the theme
1545
1857
  .pagination span > a { background-color: var(--aa-surface-2); color: var(--aa-text); }
@@ -1549,7 +1861,10 @@ input[type='radio'] {
1549
1861
  // cannot be out-specified by anything sane here, which is why this needs
1550
1862
  // !important, like the accent fill one line over in the base pagination
1551
1863
  // block. Without it the digit renders at 1.00:1 on its own background.
1552
- .pagination span > a:hover { color: $skinMenuPillTextColor !important; }
1864
+ // The hover fill is the accent ($skinMainSecondColor); $skinMenuPillTextColor
1865
+ // is a light blue that sits on it at ~1.16:1 (invisible). White reads on the
1866
+ // accent (matches the base pagination block's own hover colour one file over).
1867
+ .pagination span > a:hover { color: #fff !important; }
1553
1868
 
1554
1869
  // Batch-actions / table-tools dropdown menu items are white by default
1555
1870
  .table_tools .dropdown_menu_list li a {
@@ -1621,12 +1936,25 @@ input[type='radio'] {
1621
1936
  table.index_table tbody tr:nth-child(even) > td,
1622
1937
  table.index_table tbody tr.even > td { background: var(--aa-surface-2); }
1623
1938
  table.index_table tbody tr:hover > td { background: var(--aa-surface-hover); }
1624
- // Checked rows: the zebra and hover rules above outrank ActiveAdmin's own
1625
- // `tr.selected td`, so without this a ticked row looks exactly like an
1626
- // unticked one. The :hover variant keeps the selection visible while the
1627
- // pointer is over the row.
1939
+ // Selected cells: the zebra and hover rules above outrank ActiveAdmin's own
1940
+ // `tr.selected td` / `td.selected`, so without this a ticked row (batch
1941
+ // actions) or a click-highlighted row + column (crisscross) looks unselected.
1942
+ // The `tr >` in the td.selected selector is load-bearing: the even-stripe
1943
+ // rule is `...tbody tr:nth-child(even) > td` (specificity 0,0,2,4); a plain
1944
+ // `tbody td.selected` is only (0,0,2,3) and loses on even rows, breaking the
1945
+ // cross every other row. Matching the `tr >` ties the specificity, so source
1946
+ // order wins. The :hover variants keep the selection visible under the pointer.
1628
1947
  table.index_table tbody tr.selected > td,
1629
- table.index_table tbody tr.selected:hover > td { background: var(--aa-selected); }
1948
+ table.index_table tbody tr.selected:hover > td,
1949
+ table.index_table tbody tr > td.selected,
1950
+ table.index_table tbody tr:hover > td.selected { background: var(--aa-selected); }
1951
+ // The clicked cell sits at the centre of the cross; an accent outline marks
1952
+ // it apart from the row/column tint without a competing fill (outset inward
1953
+ // so it adds no layout shift).
1954
+ table.index_table tbody tr > td.selected-cell {
1955
+ outline: 2px solid var(--aa-accent);
1956
+ outline-offset: -2px;
1957
+ }
1630
1958
  // Plain content tables (e.g. the CDR show "Attempts" table) are striped by the
1631
1959
  // base CSS with light fills; map the odd/even rows onto the dark surfaces too.
1632
1960
  table tbody tr.odd > td, table tbody tr.odd > th { background-color: var(--aa-surface); }
@@ -1643,11 +1971,23 @@ input[type='radio'] {
1643
1971
  color: var(--aa-text);
1644
1972
  }
1645
1973
 
1974
+ // Give the content column its own block-formatting context so #index_footer's
1975
+ // `clear: both` (below) only clears the pagination floats INSIDE the table
1976
+ // area — not the floated #sidebar. Without this, a tall filter sidebar pushes
1977
+ // the pagination far below a short table (it clears the sidebar's float too).
1978
+ #main_content { display: flow-root; }
1979
+
1646
1980
  // Index footer (per-page selector, pagination, "displaying…" info, download
1647
1981
  // links): lay them out in one left-packed, vertically-centred row. The gem
1648
1982
  // floats them, so they sat top-aligned and, having different heights, looked
1649
1983
  // misaligned. A slightly larger font than the gem default (0.85em) too.
1650
1984
  #index_footer {
1985
+ // ActiveAdmin floats .pagination_top and .paginated_collection_contents
1986
+ // left; the footer has no clear, so it rode up beside them and rendered
1987
+ // ABOVE the table (per-page + page numbers + "Displaying…" duplicated on
1988
+ // top, nothing below). Clear the floats so it sits under the table — one
1989
+ // pagination on top, one on the bottom, like master.
1990
+ clear: both;
1651
1991
  text-align: left;
1652
1992
  font-size: 0.95em;
1653
1993
  .pagination_per_page,
@@ -1700,6 +2040,83 @@ input[type='radio'] {
1700
2040
  table.ui-datepicker-calendar tbody td span { color: var(--aa-text-muted); }
1701
2041
  .ui-state-hover, .ui-state-active { background: var(--aa-surface-hover); }
1702
2042
  }
2043
+
2044
+ // select2 (used by most ActiveAdmin apps). It ships its own light chrome and
2045
+ // appends the dropdown to <body>, so it is styled here at the mixin level
2046
+ // like the datepicker. Drive the control, the dropdown panel, the results
2047
+ // list and the search field from the --aa-* tokens so it follows both modes;
2048
+ // without this the dropdown stays a white panel with white/near-invisible
2049
+ // text in dark mode. !important is required because select2's own stylesheet
2050
+ // sets these to white with equal specificity and loads after this in the
2051
+ // bundle — same reason the datetime picker overrides use it.
2052
+ .select2-container--default {
2053
+ .select2-selection--single,
2054
+ .select2-selection--multiple {
2055
+ background-color: var(--aa-input-bg) !important;
2056
+ border-color: var(--aa-input-border) !important;
2057
+ color: var(--aa-text);
2058
+ }
2059
+ .select2-selection__rendered { color: var(--aa-text) !important; }
2060
+ .select2-selection__placeholder { color: var(--aa-text-muted) !important; }
2061
+ // Clear "×": keep it a small, muted inline glyph that brightens on hover.
2062
+ // select2 4.1 renders it as a <button> that takes a large square hover fill
2063
+ // (the "big blue square"); neutralize that and the 4.0 span the same way so
2064
+ // the affordance is just the colour change, correctly aligned.
2065
+ .select2-selection__clear {
2066
+ color: var(--aa-text-muted) !important;
2067
+ background: transparent !important;
2068
+ border: none !important;
2069
+ box-shadow: none !important;
2070
+ padding: 0 4px !important;
2071
+ &:hover, &:focus { color: var(--aa-text) !important; background: transparent !important; }
2072
+ }
2073
+ .select2-selection--multiple .select2-selection__choice {
2074
+ background-color: var(--aa-surface-2) !important;
2075
+ border-color: var(--aa-border) !important;
2076
+ color: var(--aa-text) !important;
2077
+ }
2078
+ .select2-search__field { color: var(--aa-text) !important; }
2079
+ }
2080
+ .select2-dropdown {
2081
+ background-color: var(--aa-surface) !important;
2082
+ border-color: var(--aa-border) !important;
2083
+ color: var(--aa-text);
2084
+ }
2085
+ .select2-results__option { color: var(--aa-text) !important; }
2086
+ .select2-results__option--highlighted[aria-selected] {
2087
+ background-color: var(--aa-surface-hover) !important;
2088
+ color: var(--aa-text) !important;
2089
+ }
2090
+ .select2-results__option[aria-selected="true"] {
2091
+ background-color: var(--aa-selected) !important;
2092
+ color: var(--aa-text) !important;
2093
+ }
2094
+
2095
+ // tom-select (used by e.g. yeti). Same situation as select2: its own light
2096
+ // chrome, dropdown rendered in place; recolor from the --aa-* tokens.
2097
+ .ts-wrapper {
2098
+ .ts-control {
2099
+ background: var(--aa-input-bg) !important;
2100
+ border-color: var(--aa-input-border) !important;
2101
+ color: var(--aa-text);
2102
+ input, input::placeholder { color: var(--aa-text) !important; }
2103
+ .item {
2104
+ background: var(--aa-surface-2) !important;
2105
+ border-color: var(--aa-border) !important;
2106
+ color: var(--aa-text) !important;
2107
+ }
2108
+ }
2109
+ }
2110
+ .ts-dropdown {
2111
+ background: var(--aa-surface) !important;
2112
+ border-color: var(--aa-border) !important;
2113
+ color: var(--aa-text);
2114
+ .optgroup-header { background: var(--aa-surface-2) !important; color: var(--aa-text-muted) !important; }
2115
+ .option { color: var(--aa-text) !important; }
2116
+ .option.active,
2117
+ .active { background: var(--aa-surface-hover) !important; color: var(--aa-text) !important; }
2118
+ .option[aria-selected="true"] { background: var(--aa-selected) !important; color: var(--aa-text) !important; }
2119
+ }
1703
2120
  }
1704
2121
 
1705
2122
  // DARK-ONLY content overrides — genuinely dark-specific values with no light
@@ -1718,3 +2135,58 @@ input[type='radio'] {
1718
2135
  html:not([data-theme="light"]) { @include aa-dark-content; }
1719
2136
  }
1720
2137
  html[data-theme="dark"] { @include aa-dark-content; }
2138
+
2139
+ // Trix / ActionText editor. Trix ships with ActionText (a Rails built-in), so
2140
+ // its chrome is themed here for every consumer; the app still imports Trix's
2141
+ // own base CSS (trix/dist/trix). All rules key off the --aa-* tokens, so they
2142
+ // follow the active palette in both light and dark, and are inert when no
2143
+ // editor is on the page.
2144
+ trix-toolbar {
2145
+ .trix-button-group { border-color: var(--aa-border); }
2146
+ .trix-button {
2147
+ background: var(--aa-surface);
2148
+ color: var(--aa-text);
2149
+ border-bottom-color: var(--aa-border);
2150
+ &:not(:first-child) { border-left-color: var(--aa-border); }
2151
+ &:not(:disabled):hover { background: var(--aa-surface-hover); }
2152
+ &.trix-active { background: var(--aa-selected); }
2153
+ }
2154
+ // Trix dims idle icons to opacity 0.6 and disabled ones to 0.125 — both read
2155
+ // as faint in either palette, the 0.125 so faint the block-format buttons
2156
+ // (disabled until the editor is focused) look broken. Lift idle to 0.8 and
2157
+ // disabled to 0.4: still clearly dimmer than idle, but legible. Trix keeps the
2158
+ // active icon at 1 via a higher-specificity rule.
2159
+ .trix-button--icon::before { opacity: 0.8; }
2160
+ .trix-button--icon:disabled::before { opacity: 0.4; }
2161
+ }
2162
+
2163
+ trix-editor {
2164
+ background: var(--aa-input-bg);
2165
+ border-color: var(--aa-input-border);
2166
+ color: var(--aa-text);
2167
+ &:empty:not(:focus)::before { color: var(--aa-text-muted); } // placeholder
2168
+ }
2169
+
2170
+ .trix-content {
2171
+ pre { background: var(--aa-surface-2); }
2172
+ blockquote { border-left: 0.3em solid var(--aa-border); }
2173
+ }
2174
+
2175
+ .trix-dialog {
2176
+ background: var(--aa-surface);
2177
+ color: var(--aa-text);
2178
+ box-shadow: 0 2px 8px var(--aa-shadow);
2179
+ input[type="text"], input[type="url"] {
2180
+ background: var(--aa-input-bg);
2181
+ border-color: var(--aa-input-border);
2182
+ color: var(--aa-text);
2183
+ }
2184
+ .trix-button { background: var(--aa-accent); color: #fff; }
2185
+ }
2186
+
2187
+ // Trix bakes its toolbar icons into the base CSS as black SVG background images
2188
+ // on ::before; invert them in dark mode so they read on the dark toolbar (the
2189
+ // idle/disabled opacity lift above applies in both modes).
2190
+ @include aa-dark {
2191
+ trix-toolbar .trix-button--icon::before { filter: invert(1); }
2192
+ }