diff --git a/README.md b/README.md
index 6411ab7..6b5f016 100644
--- a/README.md
+++ b/README.md
@@ -57,22 +57,255 @@ In your base stylesheet entry point `active_admin.scss` (as example), add line:
@import '@activeadmin-plugins/active_admin_theme';
```
-You can change basic colors of the theme by setting some variable above active_admin_theme import line in active_admin.css.scss
+## Customising
-```css
-...
-$skinMainFirstColor: #A5A7AA!default;
-$skinMainSecondColor: #0066CC!default;
-$skinBorderWindowColor: #B8BABE!default;
+Set any of the variables below *above* the import line:
+
+```scss
+$skinMainFirstColor: #A5A7AA;
+$skinMainSecondColor: #0066CC;
+$skinBorderWindowColor: #B8BABE;
+
+@import 'wigu/active_admin_theme';
+```
+
+Variables are typed. A value of the wrong kind — `none` where a colour is
+expected, or a length without its unit — fails the build with a message
+naming the variable, instead of silently emitting CSS the browser discards.
+
+### Dark mode
+
+The theme follows the operating system via `prefers-color-scheme`, and can be
+pinned per page with `data-theme="light"` or `data-theme="dark"` on ``.
+Every colour below that has a `…Dark` twin is what dark mode uses; each twin
+defaults to its light counterpart unless noted, so a project that only sets
+the light value keeps one consistent colour in both modes.
+
+### A worked example
+
+The defaults are the configuration this theme is run with in
+[yeti-web](https://github.com/yeti-switch/yeti-web). To go back to the blue
+header the theme shipped before:
+
+```scss
+$skinMenuPillColor: $skinMainSecondColor;
+$skinMenuPanelColor: $skinMainSecondColor;
+$skinMenuTextColor: #ffffff;
+$skinMenuItemHoverColor: transparent;
+$skinMenuItemHoverTextColor: #ffffff;
+$skinMenuFontSize: 1em;
+$skinHeaderPaddingY: 7px; // one value top and bottom
+$skinTitleBarColor: lighten($skinMainFirstColor, 8%);
+$skinTitleBarBorderWidth: 3px;
+$skinPanelHeaderColor: $skinMainSecondColor;
+$skinPanelHeaderTextColor: #ffffff;
+$skinTabInactiveColor: $skinMainSecondColor;
+$skinInactiveTabTextColor: #ffffff;
+$skinLinkColor: $skinMainSecondColor;
@import 'wigu/active_admin_theme';
-...
```
+Note `var(--aa-page-bg)` works as a variable value: a custom property follows
+the mode on its own, so one line covers both themes.
+
+### Switching themes
+
+The stylesheet follows the operating system on its own and honours
+`data-theme="light"` or `data-theme="dark"` on ``. A switch is optional
+and ships with the gem:
+
+```scss
+// app/assets/javascripts/active_admin.js
+//= require wigu/theme_toggle
+```
+
+```js
+// or, as an npm module
+import "@activeadmin-plugins/active_admin_theme/src/theme_toggle";
+```
+
+A gem cannot add a menu item: ActiveAdmin builds the utility navigation from
+the host application's initializer, and nothing in a stylesheet or an asset
+runs at that point. So with nothing else to do, the script injects its own
+`li#theme_toggle` into `#utility_nav` on load. That works, but the item is
+inserted before the server-rendered ones and is not yours to order or hide.
+
+Declaring it yourself costs four lines and puts it under your control — this is
+how [yeti-web](https://github.com/yeti-switch/yeti-web) does it:
+
+```ruby
+# config/initializers/active_admin.rb
+config.namespace :admin do |admin|
+ admin.build_menu :utility_navigation do |menu|
+ # A real url, not "#": ActiveAdmin drops a blank utility item.
+ menu.add id: "theme_toggle", label: "", url: "#theme",
+ priority: 9_999_998, html_options: { role: "button" }
+ end
+end
+```
+
+The script finds `#theme_toggle` or anything carrying `.dark-mode-toggle`,
+binds by delegation — so the control survives a re-render — and writes nothing
+to the page but `data-mode`. Everything visible comes from the stylesheet.
+
+[](./img/switch.png)
+
+Below the two pages: the three states at rest — half circle for **auto**, sun
+for **light**, moon for **dark** — then the last two hovered. A click moves to
+the next state, so the control costs the width of one icon in a header that is
+usually already full. The title says where that click goes, since one icon
+cannot show both.
+
+`auto` removes the attribute, so the media query decides and the page follows
+the operating system live; the other two pin the choice in `localStorage` under
+`aa-theme`. ActiveAdmin 4's own toggle writes `light` or `dark` on the first
+click and never writes `auto` back, so a user there cannot return to following
+the system without clearing storage by hand — hence the third state.
+
+The glyphs are inline SVG used as a CSS `mask`, so the gem still ships no image
+files, there is nothing for a host application's CSP to allow, and the icon
+takes `currentColor` — `$skinMenuTextColor` like the rest of the bar, and the
+hover colour on hover. To use your own icon font instead, override
+`$theme-icon-auto` / `$theme-icon-light` / `$theme-icon-dark`, or restyle
+`#theme_toggle > a:before` outright.
+
+The control is a link with no destination, so Tab reaches it and
+Enter and Space operate it. Changing the theme in one tab
+applies it in the others.
+
+### Upgrading
+
+Two things changed shape in this release and are worth knowing if you already
+set variables:
+
+* Dropdown panels — the title-bar menu, the batch-actions menu and the
+ table-tools menus — now follow the surface palette (`$skinSurfaceColor` and
+ friends) instead of `$skinMainFirstColor` / `$skinMainSecondColor`. That is
+ what lets them work in both modes. If you branded those panels through the
+ two main colours, point `$skinSurfaceColor` and `$skinSurfaceHoverColor` at
+ the same values.
+* The primary button fill is darker (`darken($skinMainSecondColor, 20%)` rather
+ than the accent itself). White on the accent is 2.74:1, under the 4.5:1 small
+ text needs, and dark mode was already using this tone — the button is now one
+ colour in both modes. Set `$skinButtonColor: $skinMainSecondColor;` for the
+ old look.
+* The default content link colour is darker (`#1f5f8d` rather than the accent).
+ The accent is a fill colour and failed WCAG AA as body text. Set
+ `$skinLinkColor` back to `$skinMainSecondColor` if you prefer the old look.
+
+### Variables
+
+#### Core
+
+| Variable | Default (light / dark) | |
+|---|---|---|
+| `$skinMainFirstColor` | `#23282f` | |
+| `$skinMainSecondColor` | `#5ea3d3` | |
+| `$skinBorderRadius` | `4px` | |
+| `$skinBorderWindowColor` | `#e6e9ee` | |
+| `$skinTablePadding` | `10px` | |
+
+#### Surfaces, text and borders
+
+| Variable | Default (light / dark) | |
+|---|---|---|
+| `$skinPageBgColor` / `$skinPageBgColorDark` | `#f7f9fb` / `#161a1e` | page background |
+| `$skinSurfaceColor` / `$skinSurfaceColorDark` | `#ffffff` / `#25292f` | panels / cards / content |
+| `$skinSurface2Color` / `$skinSurface2ColorDark` | `#f0f2f5` / `#2c3137` | table headers / striping / subtle fills |
+| `$skinSurfaceHoverColor` / `$skinSurfaceHoverColorDark` | `#f5f7fa` / `#3f454d` | row / item hover |
+| `$skinSelectedRowColor` / `$skinSelectedRowColorDark` | `#d9e4ec` / `#304457` | checked table row |
+| `$skinElevatedColor` / `$skinElevatedColorDark` | `$skinSurfaceColor` / `#363c43` | tool buttons and dropdown panels floating above the page |
+| `$skinTextColor` / `$skinTextColorDark` | `#323537` / `#dde2e8` | body text |
+| `$skinTextMutedColor` / `$skinTextMutedColorDark` | `#6b7177` / `#b0b8c2` | secondary text / axis labels |
+| `$skinBorderColor` / `$skinBorderColorDark` | `#e0e4e9` / `#404750` | borders / grid lines |
+| `$skinInputBgColor` / `$skinInputBgColorDark` | `#ffffff` / `#1e2227` | form control background |
+| `$skinInputBorderColor` / `$skinInputBorderColorDark` | `#c9ced4` / `#4d555f` | form control border |
+
+#### Header menu
+
+| Variable | Default (light / dark) | |
+|---|---|---|
+| `$skinMenuPillColor` | `#2e3236` | top-level current/hover pill |
+| `$skinMenuPillTextColor` | `#6cb0de` | text on that pill; follows the dropdown text so a |
+| `$skinMenuPanelColor` | `#2e3236` | dropdown panel bg + hover "bridge" border |
+| `$skinMenuTextColor` | `#dfe2e6` | dropdown item text (was: inherited #fff) |
+| `$skinMenuItemHoverColor` | `#3f454c` | dropdown item hover/current bg (was: none) |
+| `$skinMenuItemHoverTextColor` | `#6cb0de` | hover/current dropdown item text, same reason |
+| `$skinMenuFontSize` | `13px` | header menu text size |
+| `$skinMenuItemPaddingY` | `5px` | dropdown item top/bottom padding (was 6px/4px + a 7px border) |
+| `$skinMenuItemLineHeight` | `1.35` | dropdown item line-height |
+| `$skinMenuPanelMaxWidth` | `260px` | dropdown panel ceiling; longer labels wrap instead of leaving the viewport |
+| `$skinHeaderPaddingY` | `null` | sets both halves at once |
+| `$skinHeaderPaddingTop` | `4.5px` | header top padding (base value, kept so the header does not shift) |
+| `$skinHeaderPaddingBottom` | `4.5px` | header bottom padding |
+| `$skinHeaderLogoMaxHeight` | `none` | cap the site_title logo image height |
+
+#### Title bar
+
+| Variable | Default (light / dark) | |
+|---|---|---|
+| `$skinTitleBarColor` | `#343c46` | |
+| `$skinTitleBarBorderColor` | `$skinMainSecondColor` | |
+| `$skinTitleBarBorderWidth` | `0` | |
+| `$skinTitleBarButtonPaddingY` | `6px` | action button vertical padding |
+| `$skinTitleBarButtonPaddingX` | `10px` | action button horizontal padding |
+
+#### Panels, tabs and labels
+
+| Variable | Default (light / dark) | |
+|---|---|---|
+| `$skinPanelHeaderColor` / `$skinPanelHeaderColorDark` | `var(--aa-page-bg)` / `var(--aa-page-bg)` | |
+| `$skinPanelHeaderTextColor` / `$skinPanelHeaderTextColorDark` | `var(--aa-inactive-tab-text)` / `var(--aa-inactive-tab-text)` | |
+| `$skinPanelHeaderPaddingY` | `5px` | panel + sidebar header height |
+| `$skinLabelColor` / `$skinLabelColorDark` | `#8494a8` / `$skinTextColorDark` | |
+| `$skinTabInactiveColor` / `$skinTabInactiveColorDark` | `#f7f9fb` / `#161a1e` | inactive tab fill |
+| `$skinActiveTabTextColor` / `$skinActiveTabTextColorDark` | `$skinMainSecondColor` / `#7cc0ec` | selected tab label |
+| `$skinInactiveTabTextColor` / `$skinInactiveTabTextColorDark` | `#5e6469` / `#b0b8c2` | inactive tab label |
+| `$skinTableHeaderTextColor` / `$skinTableHeaderTextColorDark` | `#5e6469` / `#dde2e8` | index-table column header text |
+| `$skinTabPaddingY` | `8px` | tab height |
+| `$skinTabPaddingX` | `15px` | tab label horizontal padding (text → border) |
+
+#### Buttons and table tools
+
+| Variable | Default (light / dark) | |
+|---|---|---|
+| `$skinButtonColor` / `$skinButtonColorDark` | `darken($skinMainSecondColor, 20%)` / `$skinButtonColor` | one tone in both modes; white on it is 5.35:1 |
+| `$skinButtonTextColor` / `$skinButtonTextColorDark` | `#ffffff` / `$skinButtonTextColor` | label on those buttons |
+| `$skinTableToolsHeight` | `30px` | |
+| `$skinTableToolsPaddingX` | `$skinTableToolsHeight * 0.4` | 12px at 30px |
+
+#### Links
+
+| Variable | Default (light / dark) | |
+|---|---|---|
+| `$skinAccentColor` / `$skinAccentColorDark` | `$skinMainSecondColor` / `$skinAccentColor` | focus ring / accent outline |
+| `$skinLinkColor` / `$skinLinkColorDark` | `#38678b` / `#7cc0ec` | |
+| `$skinDeleteLinkColor` / `$skinDeleteLinkColorDark` | `$skinLinkColor` / `#f49b9b` | |
+
## Screen
-
+Index with filters, show page, nested `has_many` form, an open batch-actions
+menu and the datepicker — the same admin in both modes. The theme follows the
+operating system and can be pinned per page with `data-theme`.
+
+#### Light
+
+[](./img/light.png)
+
+#### Dark
+
+[](./img/dark.png)
+
+#### Form and filter controls
+
+Shown at full size, because the two shots above scale the controls down past
+the point where you can tell what colour they are. Inputs, selects and
+textareas take `$skinInputBgColor` / `$skinInputBorderColor` — white on
+`#c9ced4` in light mode, a recessed `#1e2227` well on `#4d555f` in dark — and
+the focused field (`Name`, `Title`) carries `$skinMainSecondColor`.
+[](./img/inputs.png)
## Contributing
diff --git a/app/assets/javascripts/wigu/theme_toggle.js b/app/assets/javascripts/wigu/theme_toggle.js
new file mode 100644
index 0000000..9a7f45e
--- /dev/null
+++ b/app/assets/javascripts/wigu/theme_toggle.js
@@ -0,0 +1,140 @@
+// Theme switch for active_admin_theme. Optional — the stylesheet works without
+// it, following the operating system.
+//
+// Cycles auto -> light -> dark -> auto:
+// auto no stored choice; follows prefers-color-scheme, live
+// light/dark pins html[data-theme] and remembers it
+//
+// The only thing this writes to the page is data-mode on the control; the
+// stylesheet draws the icon from it. That keeps appearance in the theme, where
+// a project can restyle it, instead of in a script it would have to fork.
+//
+// Binds by delegation to anything carrying .dark-mode-toggle or #theme_toggle,
+// the way ActiveAdmin 4 does, so the control can live anywhere and survive a
+// re-render. If neither exists it appends its own entry to the utility
+// navigation. No jQuery and no ujs, so it does not care how the admin is built.
+(function () {
+ "use strict";
+
+ var KEY = "aa-theme";
+ var ORDER = { auto: "light", light: "dark", dark: "auto" };
+ var LABEL = {
+ auto: "Theme: auto (follows the system) — click for light",
+ light: "Theme: light — click for dark",
+ dark: "Theme: dark — click for auto",
+ };
+ var root = document.documentElement;
+
+ // localStorage throws in private mode in some browsers, and is absent in a few
+ // embedded webviews. Losing the preference is acceptable; breaking the admin
+ // is not.
+ function stored() {
+ try { return localStorage.getItem(KEY); } catch (e) { return null; }
+ }
+ function store(value) {
+ try { value === null ? localStorage.removeItem(KEY) : localStorage.setItem(KEY, value); } catch (e) {}
+ }
+
+ // The choice is held here and storage is best-effort persistence. Reading it
+ // back instead would make the switch a no-op wherever localStorage throws —
+ // private mode in some browsers — because the write is swallowed and the next
+ // read returns the old value.
+ var current = null;
+
+ function mode() {
+ if (current === null) current = normalise(stored());
+ return current;
+ }
+
+ function normalise(value) {
+ return value === "light" || value === "dark" ? value : "auto";
+ }
+
+ function apply() {
+ // auto drops the attribute entirely so the media query decides.
+ if (mode() === "auto") root.removeAttribute("data-theme");
+ else root.setAttribute("data-theme", mode());
+ }
+
+ // Before DOMContentLoaded on purpose: applied later, the page paints in the
+ // other theme first and flashes.
+ apply();
+
+ function ready(fn) {
+ if (document.readyState !== "loading") fn();
+ else document.addEventListener("DOMContentLoaded", fn);
+ }
+
+ var SELECTOR = ".dark-mode-toggle, #theme_toggle";
+
+ function controls() {
+ return document.querySelectorAll(SELECTOR);
+ }
+
+ function refresh() {
+ var active = mode();
+ Array.prototype.forEach.call(controls(), function (host) {
+ // The host itself may be the anchor (a menu item) or wrap one (our own li).
+ var link = host.tagName === "A" ? host : host.querySelector("a") || host;
+ host.setAttribute("data-mode", active);
+ link.setAttribute("title", LABEL[active]);
+ link.setAttribute("aria-label", LABEL[active]);
+ });
+ }
+
+ function cycle() {
+ var next = ORDER[mode()];
+ current = next;
+ store(next === "auto" ? null : next);
+ apply();
+ refresh();
+ }
+
+ // Delegated, so a control added later — or replaced by a Turbo render — still
+ // works without rebinding.
+ document.addEventListener("click", function (event) {
+ if (!event.target.closest || !event.target.closest(SELECTOR)) return;
+ event.preventDefault();
+ cycle();
+ });
+
+ // The control is a link with no destination, so it is reachable by Tab; that
+ // makes Enter and Space its keyboard contract.
+ document.addEventListener("keydown", function (event) {
+ if (event.key !== "Enter" && event.key !== " ") return;
+ if (!event.target.closest || !event.target.closest(SELECTOR)) return;
+ event.preventDefault();
+ cycle();
+ });
+
+ // Another tab changed the preference.
+ window.addEventListener("storage", function (event) {
+ if (event.key !== KEY) return;
+ current = normalise(event.newValue);
+ apply();
+ refresh();
+ });
+
+ // In auto mode, follow the operating system while the page is open.
+ if (window.matchMedia) {
+ var query = window.matchMedia("(prefers-color-scheme: dark)");
+ var onChange = function () { if (mode() === "auto") { apply(); refresh(); } };
+ if (query.addEventListener) query.addEventListener("change", onChange);
+ else if (query.addListener) query.addListener(onChange);
+ }
+
+ ready(function () {
+ if (controls().length === 0) {
+ var nav = document.getElementById("utility_nav");
+ if (!nav) return;
+ var host = document.createElement("li");
+ host.id = "theme_toggle";
+ var link = host.appendChild(document.createElement("a"));
+ // Not "#": ActiveAdmin treats a bare hash as a blank menu item.
+ link.setAttribute("href", "#theme");
+ link.setAttribute("role", "button");
+ nav.insertBefore(host, nav.firstChild);
+ }
+ refresh();
+ });
+})();
diff --git a/app/assets/stylesheets/wigu/active_admin_theme.scss b/app/assets/stylesheets/wigu/active_admin_theme.scss
index c7df61e..d4ee1aa 100644
--- a/app/assets/stylesheets/wigu/active_admin_theme.scss
+++ b/app/assets/stylesheets/wigu/active_admin_theme.scss
@@ -6,34 +6,227 @@ $skinBorderRadius: 4px!default;
$skinBorderWindowColor: #e6e9ee!default;
$skinTablePadding: 10px!default;
-// Header menu colors. Defaults follow $skinMainSecondColor, so a project that
-// overrides it still gets the menu following that accent. Every colour here has
-// a matching text colour, otherwise a light value would be unreadable.
-$skinMenuPillColor: $skinMainSecondColor!default; // top-level current/hover pill
-$skinMenuTextColor: #ffffff!default; // dropdown item text, hover/current included
-// Follows the dropdown text rather than defaulting to white on its own: a
-// project that light-themes the menu sets one variable, not two.
-$skinMenuPillTextColor: $skinMenuTextColor!default; // text on that pill
-$skinMenuPanelColor: $skinMainSecondColor!default; // dropdown panel bg + hover "bridge" border
-$skinMenuItemHoverColor: transparent!default; // dropdown item hover/current bg
-
-// Header / menu sizing. NOTE: not a no-op refactor — dropdown item height and
-// dropdown width change for every project, configured or not. Header padding
-// keeps its previous asymmetric values, so the header itself does not move.
-$skinMenuFontSize: 1em!default; // top-level tab text size (does not cascade into dropdowns)
-$skinMenuItemPaddingY: 8px!default; // dropdown item top/bottom padding (was 6px/4px + a 7px border)
-$skinMenuItemLineHeight: 1.5!default; // dropdown item line-height
+// Content palette — the surfaces, text and borders the UI is built from, per
+// mode. Each has a light value (the existing look) and a *Dark value; they feed
+// the --aa-* custom properties below. Defaults reproduce the current colors, so
+// non-configuring projects are unchanged.
+$skinPageBgColor: #f7f9fb!default; $skinPageBgColorDark: #161a1e!default; // page background
+$skinSurfaceColor: #ffffff!default; $skinSurfaceColorDark: #25292f!default; // panels / cards / content
+$skinSurface2Color: #f0f2f5!default; $skinSurface2ColorDark: #2c3137!default; // table headers / striping / subtle fills
+$skinSurfaceHoverColor: #f5f7fa!default; $skinSurfaceHoverColorDark:#3f454d!default; // row / item hover
+$skinSelectedRowColor: #d9e4ec!default; $skinSelectedRowColorDark: #304457!default; // checked table row
+// Things that float above the page — tool-row buttons and dropdown panels.
+// On light they match the content surface; on dark they have to sit a step
+// lighter than the page or they read as holes in it.
+$skinElevatedColor: $skinSurfaceColor!default; $skinElevatedColorDark: #363c43!default;
+$skinTextColor: #323537!default; $skinTextColorDark: #dde2e8!default; // body text
+$skinTextMutedColor: #6b7177!default; $skinTextMutedColorDark: #b0b8c2!default; // secondary text / axis labels
+$skinBorderColor: #e0e4e9!default; $skinBorderColorDark: #404750!default; // borders / grid lines
+$skinInputBgColor: #ffffff!default; $skinInputBgColorDark: #1e2227!default; // form control background
+$skinInputBorderColor: #c9ced4!default; $skinInputBorderColorDark: #4d555f!default; // form control border
+
+// Header menu colors. Each defaults to the previous value, so projects that do
+// not set these compile to identical CSS (and a project that overrides
+// $skinMainSecondColor still gets the menu following that accent).
+$skinMenuPillColor: #2e3236!default; // top-level current/hover pill
+$skinMenuTextColor: #dfe2e6!default; // dropdown item text (was: inherited #fff)
+$skinMenuPillTextColor: #6cb0de!default; // text on that pill; follows the dropdown text so a
+ // light menu is one variable to set, not two
+$skinMenuPanelColor: #2e3236!default; // dropdown panel bg + hover "bridge" border
+$skinMenuItemHoverColor: #3f454c!default; // dropdown item hover/current bg (was: none)
+$skinMenuItemHoverTextColor: #6cb0de!default; // hover/current dropdown item text, same reason
+
+// Header / menu sizing. Defaults reproduce (or keep the same total as) the
+// previous hard-coded values, so non-configuring projects are unchanged.
+$skinMenuFontSize: 13px!default; // header menu text size
+$skinMenuItemPaddingY: 5px!default; // dropdown item top/bottom padding (was 6px/4px + a 7px border)
$skinMenuPanelMaxWidth: 260px!default; // dropdown panel ceiling; longer labels wrap instead of leaving the viewport
-$skinHeaderPaddingTop: 5px!default; // header top padding (base value, kept so the header does not shift)
-$skinHeaderPaddingBottom: 9px!default; // header bottom padding
+$skinMenuItemLineHeight: 1.35!default; // dropdown item line-height
+// Header padding. Set $skinHeaderPaddingY for the same value top and bottom,
+// or the two halves separately — the halves win, since they are read last.
+$skinHeaderPaddingY: null!default;
+$skinHeaderPaddingTop: if($skinHeaderPaddingY == null, 4.5px, $skinHeaderPaddingY)!default;
+$skinHeaderPaddingBottom: if($skinHeaderPaddingY == null, 4.5px, $skinHeaderPaddingY)!default;
$skinHeaderLogoMaxHeight: none!default; // cap the site_title logo image height
// Title bar
-$skinTitleBarColor: lighten($skinMainFirstColor, 8%)!default;
+$skinTitleBarColor: #343c46!default;
$skinTitleBarBorderColor: $skinMainSecondColor!default;
-$skinTitleBarBorderWidth: 3px!default;
-$skinTitleBarButtonPaddingY: 10px!default; // action button vertical padding
-$skinTitleBarButtonPaddingX: 20px!default; // action button horizontal padding
+$skinTitleBarBorderWidth: 0!default;
+$skinTitleBarButtonPaddingY: 6px!default; // action button vertical padding
+$skinTitleBarButtonPaddingX: 10px!default; // action button horizontal padding
+
+// Panel/section headers + tabs accent (the blue bar on panel headers and the
+// inactive tabs). $skinPanelHeaderColor is the base/light value;
+// $skinPanelHeaderColorDark lets a project use a different color in dark mode.
+$skinPanelHeaderColor: var(--aa-page-bg)!default;
+$skinPanelHeaderColorDark: var(--aa-page-bg)!default;
+// Panel/section header TEXT color (the label on the colored header bar), per mode.
+$skinPanelHeaderTextColor: var(--aa-inactive-tab-text)!default;
+$skinPanelHeaderTextColorDark: var(--aa-inactive-tab-text)!default;
+
+// Form label / legend text — a muted blue-grey in light; the regular text color
+// in dark (where the muted blue-grey would be too dim).
+$skinLabelColor: #8494a8!default;
+$skinLabelColorDark: $skinTextColorDark!default;
+
+// Buttons (form submit, title-bar action buttons, batch actions). Separate from
+// the accent so buttons can be a non-blue color, with a dark-mode override.
+// Darker than the accent because the label is white: the accent itself gives
+// 2.74:1, under the 4.5:1 small text needs. At -20% it is 5.35:1, and it is the
+// tone dark mode already used, so the button is now one colour in both modes.
+$skinButtonColor: darken($skinMainSecondColor, 20%)!default;
+// Label on those buttons. A project is invited above to pick a non-blue fill;
+// without this the label stays white and a light fill is unreadable.
+$skinButtonTextColor: #ffffff!default;
+$skinButtonTextColorDark: $skinButtonTextColor!default;
+$skinButtonColorDark: $skinButtonColor!default;
+
+// Vertical padding controlling the height of panel/section headers and tabs.
+$skinPanelHeaderPaddingY: 5px!default; // panel + sidebar header height
+$skinTabPaddingY: 8px!default; // tab height
+$skinTabPaddingX: 15px!default; // tab label horizontal padding (text → border)
+// Height of every table-tools button (scopes, batch actions, view switcher,
+// custom tools) — matched to the panel-header height.
+$skinTableToolsHeight: 30px!default;
+// Horizontal text padding inside each table-tools button (controls button width).
+// Derived from the button height so the two scale together.
+$skinTableToolsPaddingX: $skinTableToolsHeight * 0.4 !default; // 12px at 30px
+// Per-group table-tools button fills. All default to the surface palette so the
+// whole row shares one neutral look; override any group independently (both modes).
+
+// Index-table column header text (both the sortable links and plain headers use
+// one color, configurable per mode).
+
+// Inactive tab background. Defaults to the panel-header color so tabs stay
+// consistent, but can be set separately (e.g. a more subdued tab bar).
+
+// Tab text colors — the active (selected) tab and the inactive tabs, per mode.
+
+// Content link color (generic links inside the admin content), per mode.
+// Focus ring / accent outline. Follows the theme accent, so a project that
+// rebrands $skinMainSecondColor does not keep a stray blue focus border.
+$skinAccentColor: $skinMainSecondColor!default;
+$skinAccentColorDark: $skinAccentColor!default;
+// Darker than the theme accent on purpose: the accent is a fill colour and
+// fails WCAG AA as body text — #5ea3d3 is 2.74 on white, where 4.5 is the bar
+// for text this size — and links also sit on zebra rows, table headers and
+// selected rows, so the worst of those is what has to clear it. #1f5f8d
+// keeps the hue and gives 5.28 against the darkest of them.
+$skinLinkColor: #38678b!default;
+$skinLinkColorDark: #7cc0ec!default;
+
+// Delete / destroy links. Light follows the regular link color (so action links
+// match); dark uses a lightened red, since the default red is too dark there.
+$skinDeleteLinkColor: $skinLinkColor!default;
+$skinDeleteLinkColorDark: #f49b9b!default;
+// Tabs: fill and label colours per mode. yeti-web sets all four to make the
+// inactive tabs recede to the page background and the active one read as raised.
+$skinTabInactiveColor: #f7f9fb!default;
+$skinTabInactiveColorDark: #161a1e!default;
+$skinActiveTabTextColor: $skinMainSecondColor!default;
+$skinActiveTabTextColorDark: #7cc0ec!default;
+$skinInactiveTabTextColor: #5e6469!default;
+$skinInactiveTabTextColorDark: #b0b8c2!default;
+// Index-table column header text, one colour for sortable and plain headers.
+$skinTableHeaderTextColor: #5e6469!default;
+$skinTableHeaderTextColorDark: #dde2e8!default;
+
+//DARK-MODE PALETTE---------------------------------------------------------------------------------------------------//
+// Semantic CSS custom properties for runtime light/dark switching. Light values
+// (= the existing look) live on :root; the dark values are applied either by the
+// OS preference (unless the user forced light) or by an explicit html[data-theme].
+// Phase 0: defined only — surfaces are repointed to these vars in later phases,
+// so there is no visual change yet.
+@mixin aa-dark-palette {
+ // Render native controls (checkboxes, radios, native selects, scrollbars) dark.
+ color-scheme: dark;
+ --aa-page-bg: #{$skinPageBgColorDark};
+ --aa-surface: #{$skinSurfaceColorDark};
+ --aa-surface-2: #{$skinSurface2ColorDark};
+ // Not $skinSurface2ColorDark: that is the zebra fill, and feeding both from
+ // one variable left the header band and every other row byte-identical.
+ // Light mode never had that problem — $skinBorderWindowColor gives the
+ // header its own step there.
+ --aa-table-header: #{$skinElevatedColorDark};
+ --aa-surface-hover: #{$skinSurfaceHoverColorDark};
+ --aa-selected: #{$skinSelectedRowColorDark};
+ --aa-elevated: #{$skinElevatedColorDark};
+ --aa-text: #{$skinTextColorDark};
+ --aa-text-muted: #{$skinTextMutedColorDark};
+ --aa-border: #{$skinBorderColorDark};
+ --aa-link: #{$skinLinkColorDark};
+ --aa-accent: #{$skinAccentColorDark};
+ --aa-input-bg: #{$skinInputBgColorDark};
+ --aa-input-border: #{$skinInputBorderColorDark};
+ --aa-shadow: rgba(0, 0, 0, 0.45);
+ --aa-panel-header: #{$skinPanelHeaderColorDark};
+ --aa-panel-header-text: #{$skinPanelHeaderTextColorDark};
+ --aa-label-text: #{$skinLabelColorDark};
+ --aa-button: #{$skinButtonColorDark};
+ --aa-button-text: #{$skinButtonTextColorDark};
+ --aa-button-hover: #{darken($skinButtonColorDark, 8%)};
+ --aa-table-header-text: #{$skinTableHeaderTextColorDark};
+ --aa-tab-inactive: #{$skinTabInactiveColorDark};
+ --aa-active-tab-text: #{$skinActiveTabTextColorDark};
+ --aa-inactive-tab-text: #{$skinInactiveTabTextColorDark};
+ --aa-delete-link: #{$skinDeleteLinkColorDark};
+}
+:root {
+ color-scheme: light;
+ --aa-page-bg: #{$skinPageBgColor};
+ --aa-surface: #{$skinSurfaceColor};
+ --aa-surface-2: #{$skinSurface2Color};
+ // Index-table headers keep following $skinBorderWindowColor, the knob the
+ // README has documented for years, instead of the new surface scale.
+ --aa-table-header: #{$skinBorderWindowColor};
+ --aa-surface-hover: #{$skinSurfaceHoverColor};
+ --aa-selected: #{$skinSelectedRowColor};
+ --aa-elevated: #{$skinElevatedColor};
+ --aa-text: #{$skinTextColor};
+ --aa-text-muted: #{$skinTextMutedColor};
+ --aa-border: #{$skinBorderColor};
+ --aa-link: #{$skinLinkColor};
+ --aa-accent: #{$skinAccentColor};
+ --aa-input-bg: #{$skinInputBgColor};
+ --aa-input-border: #{$skinInputBorderColor};
+ --aa-shadow: rgba(0, 0, 0, 0.08);
+ --aa-panel-header: #{$skinPanelHeaderColor};
+ --aa-panel-header-text: #{$skinPanelHeaderTextColor};
+ --aa-label-text: #{$skinLabelColor};
+ --aa-button: #{$skinButtonColor};
+ --aa-button-text: #{$skinButtonTextColor};
+ --aa-button-hover: #{darken($skinButtonColor, 8%)};
+ --aa-table-header-text: #{$skinTableHeaderTextColor};
+ --aa-tab-inactive: #{$skinTabInactiveColor};
+ --aa-active-tab-text: #{$skinActiveTabTextColor};
+ --aa-inactive-tab-text: #{$skinInactiveTabTextColor};
+ --aa-delete-link: #{$skinDeleteLinkColor};
+}
+// OS preference, unless the user explicitly chose light
+@media (prefers-color-scheme: dark) {
+ html:not([data-theme="light"]) { @include aa-dark-palette; }
+}
+// Explicit user choice always wins
+html[data-theme="dark"] { @include aa-dark-palette; }
+
+// Public helper so a host app can scope its own (project-specific) overrides to
+// dark mode using the same triggers. Use at the top level and put full selectors
+// inside; the --aa-* custom properties are available:
+// @include aa-dark { .my-widget { background: var(--aa-surface); } }
+@mixin aa-dark {
+ @media (prefers-color-scheme: dark) {
+ html:not([data-theme="light"]) { @content; }
+ }
+ html[data-theme="dark"] { @content; }
+}
+
+// Иконки переключателя тем. Инлайном, потому что гем не возит картинок, и
+// маской, а не цветным SVG: маска красится currentColor и сама идёт за
+// $skinMenuTextColor. Половинка круга — auto, солнце — light, месяц — dark.
+$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;
+$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;
+$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;
// A wrong-typed override is legal SassScript, so it compiles without a word and
// emits CSS the browser drops — `border-bottom: none solid #5ea3d3`,
@@ -65,19 +258,84 @@ $skinTitleBarButtonPaddingX: 20px!default; // action button horizontal
skinMenuItemHoverColor: $skinMenuItemHoverColor,
skinMenuTextColor: $skinMenuTextColor,
skinTitleBarColor: $skinTitleBarColor,
- skinTitleBarBorderColor: $skinTitleBarBorderColor
+ skinTitleBarBorderColor: $skinTitleBarBorderColor,
+ skinSelectedRowColor: $skinSelectedRowColor,
+ skinSelectedRowColorDark: $skinSelectedRowColorDark,
+ skinAccentColor: $skinAccentColor,
+ skinAccentColorDark: $skinAccentColorDark,
+ skinButtonTextColor: $skinButtonTextColor,
+ skinButtonTextColorDark: $skinButtonTextColorDark,
+ skinPageBgColor: $skinPageBgColor,
+ skinPageBgColorDark: $skinPageBgColorDark,
+ skinSurfaceColor: $skinSurfaceColor,
+ skinSurfaceColorDark: $skinSurfaceColorDark,
+ skinSurface2Color: $skinSurface2Color,
+ skinSurface2ColorDark: $skinSurface2ColorDark,
+ skinSurfaceHoverColor: $skinSurfaceHoverColor,
+ skinSurfaceHoverColorDark: $skinSurfaceHoverColorDark,
+ skinElevatedColor: $skinElevatedColor,
+ skinElevatedColorDark: $skinElevatedColorDark,
+ skinTextColor: $skinTextColor,
+ skinTextColorDark: $skinTextColorDark,
+ skinTextMutedColor: $skinTextMutedColor,
+ skinTextMutedColorDark: $skinTextMutedColorDark,
+ skinBorderColor: $skinBorderColor,
+ skinBorderColorDark: $skinBorderColorDark,
+ skinInputBgColor: $skinInputBgColor,
+ skinInputBgColorDark: $skinInputBgColorDark,
+ skinInputBorderColor: $skinInputBorderColor,
+ skinInputBorderColorDark: $skinInputBorderColorDark,
+ skinMenuItemHoverTextColor: $skinMenuItemHoverTextColor,
+ skinLabelColor: $skinLabelColor,
+ skinLabelColorDark: $skinLabelColorDark,
+ skinButtonColor: $skinButtonColor,
+ skinButtonColorDark: $skinButtonColorDark,
+ skinLinkColor: $skinLinkColor,
+ skinLinkColorDark: $skinLinkColorDark,
+ skinDeleteLinkColor: $skinDeleteLinkColor,
+ skinDeleteLinkColorDark: $skinDeleteLinkColorDark,
+ skinTabInactiveColor: $skinTabInactiveColor,
+ skinTabInactiveColorDark: $skinTabInactiveColorDark,
+ skinActiveTabTextColor: $skinActiveTabTextColor,
+ skinActiveTabTextColorDark: $skinActiveTabTextColorDark,
+ skinInactiveTabTextColor: $skinInactiveTabTextColor,
+ skinInactiveTabTextColorDark: $skinInactiveTabTextColorDark,
+ skinTableHeaderTextColor: $skinTableHeaderTextColor,
+ skinTableHeaderTextColorDark: $skinTableHeaderTextColorDark
) {
@if type-of($value) != color {
@error "$#{$name} must be a color (use `transparent`, not `none`), got `#{$value}`.";
}
}
+// The panel header pair defaults to a custom property and is documented as
+// accepting one, so a plain `type-of != color` would reject the shipped value.
+@each $name, $value in (
+ skinPanelHeaderColor: $skinPanelHeaderColor,
+ skinPanelHeaderColorDark: $skinPanelHeaderColorDark,
+ skinPanelHeaderTextColor: $skinPanelHeaderTextColor,
+ skinPanelHeaderTextColorDark: $skinPanelHeaderTextColorDark
+) {
+ @if type-of($value) != color and str-index("#{$value}", "var(") != 1 {
+ @error "$#{$name} must be a color or a CSS custom property (e.g. var(--aa-page-bg)), got `#{$value}`.";
+ }
+}
+// Радиус внутреннего угла ячейки — на 1px меньше внешнего, чтобы не торчал
+// из-под рамки таблицы. Вычитать px напрямую нельзя: $skinBorderRadius задаёт
+// проект, и 0.25rem роняет всю компиляцию с «Incompatible units». calc()
+// откладывает вычитание до браузера, а безразмерный 0 обрабатываем отдельно —
+// calc(0 - 1px) невалиден.
+@function inner-radius($radius) {
+ @if unitless($radius) { @return 0; }
+ @return calc(#{$radius} - 1px);
+}
+
//HTML----------------------------------------------------------------------------------------------------------------//
html {
- background: #f7f9fb;
+ background: var(--aa-page-bg);
}
body.active_admin {
- background: #f7f9fb;
+ background: var(--aa-page-bg);
}
#wrapper {
div.tabs {
@@ -87,11 +345,15 @@ body.active_admin {
text-shadow: none;
box-shadow: none;
margin-bottom: 11px;
- border: none;
- background-color: $skinMainSecondColor;
- color: #ffffff;
+ // Top/side outline only; no bottom outline (the content panel's own top
+ // border is the baseline, drawn over the tabs via z-index below).
+ border: 1px solid var(--aa-border);
+ border-bottom: 0;
+ vertical-align: top;
+ background-color: var(--aa-tab-inactive);
+ color: var(--aa-inactive-tab-text);
margin-right: 5px;
- padding: 10px 20px;
+ padding: $skinTabPaddingY $skinTabPaddingX;
-webkit-border-top-left-radius: $skinBorderRadius;
-webkit-border-top-right-radius: $skinBorderRadius;
@@ -108,17 +370,26 @@ body.active_admin {
-moz-border-radius-bottomright: 0;
border-bottom-right-radius: 0;
}
+ // Active tab sits above the content panel (z-index: 2) and is filled with the
+ // content color, so it covers the panel's top border there and merges in.
ul > li.ui-state-active > a {
- background-color: #ffffff;
- border: 1px solid $skinBorderWindowColor;
- border-bottom: 0;
- color: $skinMainSecondColor;
+ background-color: var(--aa-surface);
+ border: 1px solid var(--aa-border);
+ border-bottom-color: var(--aa-surface);
+ color: var(--aa-active-tab-text);
+ position: relative;
+ z-index: 2;
+ vertical-align: top;
}
div.tab-content {
- background-color: #ffffff;
- border: 1px solid $skinBorderWindowColor;
- border-bottom: 2px solid $skinBorderWindowColor;
+ background-color: var(--aa-surface);
+ // Uniform 1px border (no thicker bottom) so the rounded corners are even.
+ border: 1px solid var(--aa-border);
@include rounded($skinBorderRadius);
+ // Paint above the inactive tabs so the panel's own top border is the
+ // baseline drawn over them (the active tab paints above this, z-index: 2).
+ position: relative;
+ z-index: 1;
}
}
#header {
@@ -149,6 +420,55 @@ body.active_admin {
background-color: $skinMenuPillColor;
color: $skinMenuPillTextColor;
}
+ // ActiveAdmin swaps in its "dark" submenu arrow on the current/hovered item,
+ // which assumes the highlight is lighter than the bar. The default pill is
+ // not, so keep the light arrow there.
+ ul.tabs > li.has_nested.current > a, ul.tabs > li.has_nested:hover > a {
+ background-image: url($menu-arrow-light-icon-url);
+ }
+ // The utility nav (username, clock, theme switcher, logout) are not navigation
+ // tabs: drop the pill padding, and on hover highlight by text colour only — no
+ // background block. The clock has no click action, so it gets no hover at all.
+ #utility_nav {
+ > li > a { padding: 0 5px; }
+ > li:hover > a, > li.current > a { background-color: transparent; }
+ > li#servertime:hover > a { color: #ffffff; }
+
+ // The theme switch (app/assets/javascripts/wigu/theme_toggle.js). The
+ // script only writes data-mode; the glyph is drawn here, so a project can
+ // swap it for its own icon font without forking the script. Masked rather
+ // than embedded as a coloured SVG so it takes currentColor and follows
+ // $skinMenuTextColor like every other item in the bar.
+ // On the li, not only the anchor: font-size 0 collapses the anchor to the
+ // icon's own 16px, which then sits at the top of the item instead of on
+ // the line the rest of the bar shares.
+ > li#theme_toggle { display: flex; align-items: center; }
+ > li#theme_toggle > a {
+ display: flex;
+ align-items: center;
+ font-size: 0;
+ opacity: 0.75;
+
+ &:before {
+ content: "";
+ display: block;
+ width: 16px;
+ height: 16px;
+ background-color: currentColor;
+ -webkit-mask: #{$theme-icon-auto} center / contain no-repeat;
+ mask: #{$theme-icon-auto} center / contain no-repeat;
+ }
+ &:hover, &:focus { opacity: 1; }
+ }
+ > li#theme_toggle[data-mode="light"] > a:before {
+ -webkit-mask-image: $theme-icon-light;
+ mask-image: $theme-icon-light;
+ }
+ > li#theme_toggle[data-mode="dark"] > a:before {
+ -webkit-mask-image: $theme-icon-dark;
+ mask-image: $theme-icon-dark;
+ }
+ }
ul.tabs {
li.has_nested {
// `> a` only: leaf items need no room for the submenu arrow.
@@ -180,6 +500,7 @@ body.active_admin {
}
li:hover > a, li.current > a, li > a:focus {
background-color: $skinMenuItemHoverColor;
+ color: $skinMenuItemHoverTextColor;
}
li.current > a::before, li:hover > a::before, li > a:focus::before {
content: '';
@@ -231,41 +552,55 @@ body.active_admin {
}
}
#titlebar_right {
- .action_item a {
+ // `> a` and `> .dropdown_menu > a`: an action item may hold a dropdown, and
+ // a descendant selector here paints its menu entries with the button fill
+ // too — with !important, so the entry rules below cannot undo it.
+ .action_item > a,
+ .action_item > .dropdown_menu > a {
background-image: none;
border: none;
box-shadow: none;
text-shadow: none;
@include rounded($skinBorderRadius);
- background-color: $skinMainSecondColor;
+ background-color: var(--aa-button);
border: none;
padding: $skinTitleBarButtonPaddingY $skinTitleBarButtonPaddingX;
- color: #ffffff;
+ color: var(--aa-button-text);
font-weight: 600;
&:hover {
- background-color: lighten( $skinMainSecondColor, 5% )!important;
+ background-color: var(--aa-button-hover)!important;
}
}
div.dropdown_menu {
div.dropdown_menu_list_wrapper {
- background-color: $skinMainFirstColor;
- border: none;
+ background-color: var(--aa-elevated);
+ border: 1px solid var(--aa-border);
background-image: none;
- border: none;
box-shadow: none;
- div.dropdown_menu_nipple, div.dropdown_menu_nipple:after, div.dropdown_menu_nipple:before {
- border-color: $skinMainFirstColor transparent;
+ // ActiveAdmin draws the nipple as three stacked triangles: the element
+ // itself is the outline, :before the inner shadow, :after the fill.
+ // Painting all three the same colour collapses it into a solid arrow
+ // that is invisible whenever the panel fill is close to the page —
+ // which is exactly what a light surface is. Outline it like the panel.
+ div.dropdown_menu_nipple {
+ border-color: var(--aa-border) transparent;
+ &:before, &:after { border-color: var(--aa-elevated) transparent; }
}
ul.dropdown_menu_list {
box-shadow: none;
li {
a {
- background-color: #ffffff;
- color: $skinMainFirstColor;
+ background-color: var(--aa-surface);
+ color: var(--aa-text);
border-radius: 0;
&:hover {
- background-color: $skinMainSecondColor;
- color: #ffffff;
+ // background-image, not just background-color: ActiveAdmin
+ // paints this hover with a linear-gradient, and a gradient
+ // sits on top of the background colour — leaving it in place
+ // made every colour set here invisible.
+ background-image: none;
+ background-color: var(--aa-selected);
+ color: var(--aa-text);
}
}
}
@@ -296,32 +631,77 @@ body.active_admin {
}
#active_admin_content {
span.blank_slate {
- border: 2px solid lighten( $skinMainSecondColor, 30% );
- color: #8494a8;
+ // Was a hardcoded lightened blue (too bright in dark mode); use the palette
+ // border + muted text so it adapts to light/dark.
+ border: 2px solid var(--aa-border);
+ color: var(--aa-text-muted);
@include rounded($skinBorderRadius);
}
div.table_tools {
- margin-bottom: 35px;
+ margin-bottom: 14px; // match the gap between sidebar sections
+ // The gem shrinks the scope / batch / tool buttons to 0.9em; reset to 1em so
+ // they match the surrounding base text — the same size the panel headers use.
+ .scopes a, a.table_tools_button, .dropdown_menu_button { font-size: 1em; }
+ // Contain the floated buttons so the margin sits below them. The collapsible-
+ // sidebar gem sets `.table_tools:after { clear: none; padding-bottom: 16px }`,
+ // which disables the float clearfix and otherwise leaves the box ~16px tall
+ // with the 29px buttons overflowing into the margin.
+ display: flow-root;
+ &::after { padding-bottom: 0; }
+ // Gap between the batch-actions button and the scopes — only rendered when
+ // the button is present, so scope-only pages stay flush with the table edge.
+ div.batch_actions_selector { margin-right: 10px; }
+ // Baseline for any table_tools button (the index-list view switcher, custom
+ // tools like "Visible columns", etc.). The base AA style is a light gradient;
+ // theme it via the --aa-* vars. Scopes override the fill + segmented-control
+ // geometry below (they reset border-radius and re-round only the ends).
+ // .dropdown_menu_button is the same control with a different class (the
+ // "Visible columns" style tools); without it here it keeps ActiveAdmin's
+ // white gradient pill at 22px next to the themed 30px buttons.
+ a.table_tools_button, a.dropdown_menu_button {
+ box-sizing: border-box;
+ height: $skinTableToolsHeight;
+ line-height: $skinTableToolsHeight; // = button height: nudges the text to the optical centre (the font otherwise sits high in the line box)
+ background: var(--aa-elevated);
+ background-image: none;
+ border: 1px solid var(--aa-border);
+ color: var(--aa-text);
+ @include rounded($skinBorderRadius);
+ padding: 0 $skinTableToolsPaddingX;
+ box-shadow: none;
+ text-shadow: none;
+ &:hover { background: var(--aa-surface-hover); }
+ }
div.dropdown_menu {
div.dropdown_menu_list_wrapper {
- background-color: $skinMainFirstColor;
- border: none;
+ background-color: var(--aa-elevated);
+ border: 1px solid var(--aa-border);
background-image: none;
- border: none;
box-shadow: none;
- div.dropdown_menu_nipple, div.dropdown_menu_nipple:after, div.dropdown_menu_nipple:before {
- border-color: $skinMainFirstColor transparent;
+ // ActiveAdmin draws the nipple as three stacked triangles: the element
+ // itself is the outline, :before the inner shadow, :after the fill.
+ // Painting all three the same colour collapses it into a solid arrow
+ // that is invisible whenever the panel fill is close to the page —
+ // which is exactly what a light surface is. Outline it like the panel.
+ div.dropdown_menu_nipple {
+ border-color: var(--aa-border) transparent;
+ &:before, &:after { border-color: var(--aa-elevated) transparent; }
}
ul.dropdown_menu_list {
box-shadow: none;
li {
a {
- background-color: #ffffff;
- color: $skinMainFirstColor;
+ background-color: var(--aa-surface);
+ color: var(--aa-text);
border-radius: 0;
&:hover {
- background-color: $skinMainSecondColor;
- color: #ffffff;
+ // background-image, not just background-color: ActiveAdmin
+ // paints this hover with a linear-gradient, and a gradient
+ // sits on top of the background colour — leaving it in place
+ // made every colour set here invisible.
+ background-image: none;
+ background-color: var(--aa-selected);
+ color: var(--aa-text);
}
}
}
@@ -329,40 +709,53 @@ body.active_admin {
}
}
div.batch_actions_selector {
- a {
+ > a {
+ box-sizing: border-box;
+ height: $skinTableToolsHeight;
+ line-height: $skinTableToolsHeight;
background-image: none;
- border: none;
box-shadow: none;
- background-color: $skinMainSecondColor;
+ background-color: var(--aa-elevated);
@include rounded($skinBorderRadius);
- border: none;
- padding: 10px 20px;
- color: #ffffff;
+ border: 1px solid var(--aa-border);
+ padding: 0 $skinTableToolsPaddingX;
+ color: var(--aa-text);
&:hover {
- background-color: lighten( $skinMainSecondColor, 5% );
+ background-color: var(--aa-surface-hover);
}
}
}
div.dropdown_menu {
div.dropdown_menu_list_wrapper {
- background-color: $skinMainFirstColor;
- border: none;
+ background-color: var(--aa-elevated);
+ border: 1px solid var(--aa-border);
background-image: none;
- div.dropdown_menu_nipple, div.dropdown_menu_nipple:after, div.dropdown_menu_nipple:before {
- border-color: $skinMainFirstColor transparent;
+ // ActiveAdmin draws the nipple as three stacked triangles: the element
+ // itself is the outline, :before the inner shadow, :after the fill.
+ // Painting all three the same colour collapses it into a solid arrow
+ // that is invisible whenever the panel fill is close to the page —
+ // which is exactly what a light surface is. Outline it like the panel.
+ div.dropdown_menu_nipple {
+ border-color: var(--aa-border) transparent;
+ &:before, &:after { border-color: var(--aa-elevated) transparent; }
}
ul.dropdown_menu_list {
box-shadow: none;
li {
a {
- background-color: #ffffff;
+ background-color: var(--aa-surface);
box-shadow: none;
- color: $skinMainFirstColor;
+ color: var(--aa-text);
border-radius: 0;
text-shadow: none;
&:hover {
- background-color: $skinMainSecondColor;
- color: #ffffff;
+ // background-image, not just background-color: ActiveAdmin
+ // paints this hover with a linear-gradient, and a gradient
+ // sits on top of the background colour — leaving it in place
+ // made every colour set here invisible.
+ background-image: none;
+ background-color: var(--aa-selected);
+ color: var(--aa-text);
}
}
}
@@ -370,41 +763,61 @@ body.active_admin {
}
}
div.batch_actions_selector {
- a {
+ > a {
+ box-sizing: border-box;
+ height: $skinTableToolsHeight;
+ line-height: $skinTableToolsHeight;
background-image: none;
- background-color: $skinMainSecondColor;
+ background-color: var(--aa-elevated);
@include rounded($skinBorderRadius);
- border: none;
- padding: 10px 20px;
- color: #ffffff;
+ border: 1px solid var(--aa-border);
+ padding: 0 $skinTableToolsPaddingX;
+ color: var(--aa-text);
text-shadow: none;
box-shadow: none;
&:hover {
- background-color: lighten( $skinMainSecondColor, 5% );
+ background-color: var(--aa-surface-hover);
box-shadow: none;
}
}
}
- ul.scopes, div.scopes > ul {
+ // ActiveAdmin renders the index view switcher with the same classes as
+ // the scope bar — ul.indexes.table_tools_segmented_control, anchors
+ // carrying .table_tools_button — so it needs the same segmented
+ // geometry. Without it the switcher keeps the generic button radius on
+ // every segment, the shared borders double up, and the selected view is
+ // indistinguishable from the others.
+ ul.scopes, div.scopes > ul, ul.indexes {
li {
a {
background-image: none;
- border: none;
+ // Reset the generic .table_tools_button radius — scopes are a segmented
+ // control, so only the first/last child are re-rounded (below).
+ @include rounded(0);
+ // Outline the segmented control with the border color (was a white
+ // right-divider, which is invisible when the scope bar matches the
+ // page background). Adjacent borders are collapsed below.
+ border: 1px solid var(--aa-border);
box-shadow: none;
- background-color: $skinMainSecondColor;
- padding: 10px 20px;
- color: #ffffff;
+ background-color: var(--aa-surface);
+ padding: 0 $skinTableToolsPaddingX;
+ color: var(--aa-text);
text-shadow: none;
- border-right: 1px solid #ffffff;
span {
- color: #ffffff;
+ color: var(--aa-text);
font-size: 11px;
font-weight: bold;
}
}
}
+ li + li a { margin-left: -1px; } // collapse the shared border between scopes
li:first-child {
a {
+ // Override AA's base `.scopes li:first-child a { margin-left: 10px }`.
+ // That gap belongs on the batch-actions button (margin-right below), so
+ // the scope bar aligns with the table's left edge when there are no
+ // batch actions.
+ margin-left: 0;
-webkit-border-top-left-radius: $skinBorderRadius;
-webkit-border-bottom-left-radius: $skinBorderRadius;
-moz-border-radius-topleft: $skinBorderRadius;
@@ -425,16 +838,24 @@ body.active_admin {
}
li.selected {
a {
- background-color: lighten( $skinMainSecondColor, 15% );
+ // Selected scope: a touch lighter than the neutral fill, plus an accent
+ // text colour so the active scope's label stands out.
+ background-color: var(--aa-surface-hover);
+ color: var(--aa-link);
+ span { color: var(--aa-link); }
}
}
}
}
}
table.index_table {
- background-color: #ffffff;
- border: 1px solid $skinBorderWindowColor;
- border-bottom: 2px solid $skinBorderWindowColor;
+ background-color: var(--aa-surface);
+ // Uniform 1px border (no thicker bottom) so the rounded corners are even.
+ border: 1px solid var(--aa-border);
+ // The right edge is owned by the last-column cells (below) rather than the
+ // table itself, so a last-column cell's own border-right doesn't stack
+ // against the table border 1px further out (a doubled right line).
+ border-right: 0;
@include rounded($skinBorderRadius);
th {
background-image: none;
@@ -445,16 +866,46 @@ body.active_admin {
font-weight: bold;
background-color: $skinBorderWindowColor;
padding: $skinTablePadding 12px $skinTablePadding 12px;
- border-bottom: 3px solid darken( $skinBorderWindowColor, 5% );
+ // 1px header underline (was 3px) so it matches the other 1px borders.
+ border-bottom: 1px solid darken( $skinBorderWindowColor, 5% );
border-right: 1px solid darken( $skinBorderWindowColor, 5% );
a {
font-size: 1.0em;
font-weight: bold!important;
}
}
+ // Right edge = a single 1px line on the last-column cells (header th, body
+ // td, footer cells) coloured like the table border, since the table itself
+ // no longer draws a right border.
+ thead th:last-child,
+ tbody td:last-child,
+ tfoot tr > :last-child {
+ border-right: 1px solid var(--aa-border);
+ }
+ // Drop the bottom row's cell border: with border-collapse: separate it sits
+ // just inside the table's own bottom border, so the two stack and the bottom
+ // edge looks thicker than the other sides. The bottom row is the footer's
+ // last row when there's a totals footer, otherwise the body's last row.
+ tfoot tr:last-child > td,
+ tfoot tr:last-child > th { border-bottom: 0; }
+ &:not(:has(tfoot)) tbody tr:last-child > td { border-bottom: 0; }
+ // Round the corner cells so their square backgrounds follow the table's
+ // rounded border instead of cutting square notches into it (separate borders
+ // don't round cell backgrounds). Nest 1px inside the table's outer radius.
+ thead th:first-child { border-top-left-radius: inner-radius($skinBorderRadius); }
+ thead th:last-child { border-top-right-radius: inner-radius($skinBorderRadius); }
+ tfoot tr:last-child > :first-child { border-bottom-left-radius: inner-radius($skinBorderRadius); }
+ tfoot tr:last-child > :last-child { border-bottom-right-radius: inner-radius($skinBorderRadius); }
+ &:not(:has(tfoot)) tbody tr:last-child > td:first-child { border-bottom-left-radius: inner-radius($skinBorderRadius); }
+ &:not(:has(tfoot)) tbody tr:last-child > td:last-child { border-bottom-right-radius: inner-radius($skinBorderRadius); }
th.col-selectable {
width: 12px;
}
+ // Center the batch-actions checkboxes (header select-all + per-row): they are
+ // baseline-aligned and otherwise ride a few px high in the cell.
+ .col-selectable input[type="checkbox"] {
+ vertical-align: middle;
+ }
th.sorted-desc {
background-color: $skinBorderWindowColor;
}
@@ -474,18 +925,19 @@ body.active_admin {
.sidebar_section.panel {
background-image: none;
box-shadow: none;
- background-color: #ffffff;
- border: 1px solid $skinBorderWindowColor;
- border-bottom: 2px solid $skinBorderWindowColor;
+ background-color: var(--aa-surface);
+ // Uniform 1px border (no thicker bottom) so the rounded corners are even.
+ border: 1px solid var(--aa-border);
& > h3 {
background-image: none;
box-shadow: none;
text-shadow: none;
- background-color: $skinMainSecondColor;
- color: #ffffff;
+ background-color: var(--aa-panel-header);
+ color: var(--aa-panel-header-text);
border: none;
- padding: 8px 15px;
- font-weight: 400;
+ border-bottom: 1px solid var(--aa-border);
+ padding: $skinPanelHeaderPaddingY 15px;
+ font-weight: 700; // match the tab label weight
-webkit-border-top-left-radius: $skinBorderRadius;
-webkit-border-top-right-radius: $skinBorderRadius;
@@ -496,11 +948,11 @@ body.active_admin {
}
.filter_form_field {
label {
- color: #8494a8;
+ color: var(--aa-label-text);
}
input {
- border: 1px solid #e6e9ee;
- background-color: #fdfefe;
+ border: 1px solid var(--aa-input-border);
+ background-color: var(--aa-input-bg);
}
input:focus {
border-color: lighten( $skinMainSecondColor, 20% ) ;
@@ -519,14 +971,14 @@ form input[type=submit], form input[type=button], form button, a.button, a:link.
background-image: none;
box-shadow: none;
text-shadow: none;
- background-color: $skinMainSecondColor;
+ background-color: var(--aa-button);
@include rounded($skinBorderRadius);
border: none;
padding: 10px 20px;
- color: #ffffff;
+ color: var(--aa-button-text);
&:hover {
background-image: none!important;
- background-color: lighten( $skinMainSecondColor, 5% )!important;
+ background-color: var(--aa-button-hover)!important;
}
}
form fieldset.buttons li.cancel a, form fieldset.actions li.cancel a, form.filter_form a.clear_filters_btn {
@@ -547,20 +999,21 @@ form fieldset.inputs {
box-shadow: none;
text-shadow: none;
box-shadow: none;
- background-color: #ffffff;
- border: 1px solid #e6e9ee;
- border-bottom: 2px solid #e6e9ee;
+ background-color: var(--aa-surface);
+ // Uniform 1px border (no thicker bottom) so the rounded corners stay even.
+ border: 1px solid var(--aa-input-border);
legend {
span {
background-image: none;
box-shadow: none;
text-shadow: none;
box-shadow: none;
- background-color: $skinMainSecondColor;
- color: #ffffff;
+ background-color: var(--aa-panel-header);
+ color: var(--aa-panel-header-text);
border: none;
- padding: 8px 15px;
- font-weight: 400;
+ border-bottom: 1px solid var(--aa-border);
+ padding: $skinPanelHeaderPaddingY 15px;
+ font-weight: 700; // match the tab label weight
-webkit-border-top-left-radius: $skinBorderRadius;
-webkit-border-top-right-radius: $skinBorderRadius;
@@ -572,11 +1025,11 @@ form fieldset.inputs {
}
ol > li {
label {
- color: #8494a8;
+ color: var(--aa-label-text);
}
input, textarea {
- border: 1px solid #e6e9ee;
- background-color: #fdfefe;
+ border: 1px solid var(--aa-input-border);
+ background-color: var(--aa-input-bg);
}
input:focus, textarea:focus {
border-color: lighten( $skinMainSecondColor, 20% ) ;
@@ -607,7 +1060,7 @@ form fieldset.inputs {
border-bottom: 10px solid $skinMainSecondColor;
}
table.ui-datepicker-calendar {
- background-color: #ffffff;
+ background-color: var(--aa-surface);
border-color: $skinMainSecondColor;
thead {
tr {
@@ -637,20 +1090,24 @@ form fieldset.inputs {
box-shadow: none;
text-shadow: none;
box-shadow: none;
- background-color: #ffffff;
- border: 1px solid $skinBorderWindowColor;
- border-bottom: 2px solid $skinBorderWindowColor;
+ background-color: var(--aa-surface);
+ // Uniform border width on all sides: a thicker (2px) bottom border combined
+ // with border-radius renders a distorted, non-uniform bottom corner (the outer
+ // edge curves but the border's inner edge looks square). 1px all round keeps
+ // the rounded corners symmetric.
+ border: 1px solid var(--aa-border);
> h3 {
background-image: none;
box-shadow: none;
text-shadow: none;
box-shadow: none;
- background-color: $skinMainSecondColor;
- color: #ffffff;
+ background-color: var(--aa-panel-header);
+ color: var(--aa-panel-header-text);
border: none;
- padding: 8px 15px;
+ border-bottom: 1px solid var(--aa-border);
+ padding: $skinPanelHeaderPaddingY 15px;
margin-top: 0;
- font-weight: 400;
+ font-weight: 700; // match the tab label weight
-webkit-border-top-left-radius: $skinBorderRadius;
-webkit-border-top-right-radius: $skinBorderRadius;
@@ -667,11 +1124,46 @@ form fieldset.inputs {
padding: 10px 0;
}
.attributes_table table th {
- color: #8494a8;
+ color: var(--aa-label-text);
+ // base AA gives attribute labels an emboss text-shadow, which reads as a
+ // blurry halo. Drop it (the index table headers are already shadow-free).
+ text-shadow: none;
+}
+// Index-table column headers: one color for both the sortable links and the
+// plain headers (configurable via --aa-table-header-text). !important overrides
+// the dark-mode content-link recolor that would otherwise turn sortable headers
+// blue.
+table.index_table thead th,
+table.index_table thead th a {
+ color: var(--aa-table-header-text) !important;
}
+
+// Form section header: a