diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b451c68..84b2d3c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -6,28 +6,21 @@ on:
pull_request:
jobs:
- css:
+ test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
- ruby: ['3.1', '3.4']
+ ruby: ['3.3', '3.4']
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- - run: bundle exec rake css
-
- # Capybara integration test for the utility-nav server clock. Needs headless
- # Chrome (preinstalled on ubuntu-latest) and Rails 8 (Ruby >= 3.2).
- test:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - uses: ruby/setup-ruby@v1
+ - uses: actions/setup-node@v4
with:
- ruby-version: '3.4'
- bundler-cache: true
+ node-version: 22
+ cache: npm
+ cache-dependency-path: test/dummy/package-lock.json
- run: bundle exec rake test
diff --git a/.gitignore b/.gitignore
index e15aa1e..4bef1b3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,5 +3,7 @@
/src/
/test/dummy/tmp/
/test/dummy/log/
-/test/dummy/app/assets/builds/
/test/dummy/node_modules/
+/test/dummy/app/assets/builds/*
+!/test/dummy/app/assets/builds/.keep
+/tmp/
diff --git a/Gemfile b/Gemfile
index 49e2c88..ef09166 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,26 +1,36 @@
source 'https://rubygems.org'
# Specify your gem's dependencies in active_admin_theme.gemspec
+
+# Run against an unreleased ActiveAdmin, such as a branch with the layout hooks:
+# ACTIVEADMIN_PATH=../activeadmin bundle exec rake test
+# ACTIVEADMIN_BRANCH=extensible-layout bundle exec rake test
+if ENV["ACTIVEADMIN_PATH"]
+ gem "activeadmin", path: ENV["ACTIVEADMIN_PATH"]
+elsif ENV["ACTIVEADMIN_BRANCH"]
+ gem "activeadmin", github: ENV.fetch("ACTIVEADMIN_REPO", "senid231/activeadmin"), branch: ENV["ACTIVEADMIN_BRANCH"]
+end
gemspec
-group :development, :test do
- gem 'rake'
- gem 'sassc'
+# Run against an unreleased ActiveAdmin, such as a branch with the layout hooks:
+# ACTIVEADMIN_PATH=../activeadmin bundle exec rake test
+# ACTIVEADMIN_BRANCH=extensible-layout bundle exec rake test
+if ENV["ACTIVEADMIN_PATH"]
+ gem "activeadmin", path: ENV["ACTIVEADMIN_PATH"]
+elsif ENV["ACTIVEADMIN_BRANCH"]
+ gem "activeadmin", github: ENV.fetch("ACTIVEADMIN_REPO", "senid231/activeadmin"), branch: ENV["ACTIVEADMIN_BRANCH"]
end
-# Dummy ActiveAdmin app (test/dummy) that hosts the Capybara integration test
-# (`rake test`) and sources the neutral README screenshot. Sprockets +
-# sassc-rails is the pipeline AA 3.x targets (it emits `//= require` directives
-# and SCSS). Rails 8 needs Ruby >= 3.2, so keep these out of the 3.1 bundle —
-# `rake css` still runs on 3.1 without them.
-if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.2')
- group :development, :test do
- gem 'rails', '~> 8.0'
- gem 'sprockets-rails'
- gem 'sassc-rails'
- gem 'sqlite3'
- gem 'puma'
- gem 'capybara'
- gem 'cuprite'
- end
+# Dummy ActiveAdmin 4 app (test/dummy): importmap + propshaft, Tailwind CSS
+# built with the npm CLI. It hosts the Capybara tests and the screenshot
+# scripts.
+group :development, :test do
+ gem 'rake'
+ gem 'rails', '~> 8.0'
+ gem 'propshaft'
+ gem 'importmap-rails'
+ gem 'sqlite3'
+ gem 'puma'
+ gem 'capybara'
+ gem 'cuprite'
end
diff --git a/README.md b/README.md
index 67806d0..474eb25 100644
--- a/README.md
+++ b/README.md
@@ -1,486 +1,338 @@
# ActiveAdminTheme
-Custom ActiveAdmin templates
+A dense desktop theme and a set of shared components for [ActiveAdmin](https://github.com/activeadmin/activeadmin) 4.
+ActiveAdmin 4 keeps its menu in a left sidebar and gives breadcrumbs and titles a lot of room. That works on mobile but wastes width on a laptop with a large table and many filters. This theme keeps the compact desktop layout of theme 3 and keeps ActiveAdmin 4's mobile support:
-## Installation
-As active_skin is the css theme for the [activeadmin](https://github.com/activeadmin/activeadmin) administration framework - you have to install if first.
+- **Desktop** (768px and wider): a horizontal top menu, compact breadcrumbs and title, filters left of the table (collapsible).
+- **Mobile** (narrower than 768px): the menu opens in a drawer, filters sit above the table, forms and show pages use the full width.
+- Index tables scroll sideways when they are wider than the screen, and cells stay on one line.
+- Theme 3 colors, density, and typography on top of ActiveAdmin 4's own dark mode.
+
+Theme 4 needs ActiveAdmin 4 (`>= 4.0.0.beta23`) and Ruby 3.3 or newer. For ActiveAdmin 3, use theme 3.x (branch `3.x`). See [docs/migrating-from-v3.md](docs/migrating-from-v3.md).
-#### As a Gem
-Having active admin installed add the following line to your application's Gemfile:
+## Installation
+Add the gem and install it:
```ruby
-gem 'active_admin_theme'
+gem "active_admin_theme"
```
-And then execute:
+The gem requires `active_admin` first, so its views and partials win over ActiveAdmin's own. Do not reorder that.
- $ bundle
+### CSS
-Or install it yourself as:
+ActiveAdmin 4 builds its CSS with Tailwind CSS 4. Your Tailwind entry file (generated by `rails g active_admin:assets`, usually `app/assets/stylesheets/active_admin.css`) must import the theme CSS after Tailwind. Pick one route.
- $ gem install active_admin_theme
+**From the installed gem.** The gem's location differs between machines (a laptop, a Docker image, CI), so the path must not be committed. The task writes it into a generated file next to your entry file, and the entry file only imports that file by a fixed name:
-#### As a NPM module (Yarn package)
-Execute:
+```sh
+bin/rails active_admin_theme:css
+```
- $ npm i @activeadmin-plugins/active_admin_theme
+The entry file gets this block, which is the same on every machine, so commit it:
-Or
+```css
+@import "tailwindcss";
+@config "../../../tailwind-active_admin.config.js";
- $ yarn add @activeadmin-plugins/active_admin_theme
+/* active_admin_theme:begin */
+@import "./active_admin_theme.generated.css";
+/* active_admin_theme:end */
+```
-Or add manually to `package.json`:
+The task writes `active_admin_theme.generated.css`, which holds the machine-specific path. Add it to `.gitignore`:
```
-"dependencies": {
- "@activeadmin-plugins/active_admin_theme": "^3.1.1"
-}
+/app/assets/stylesheets/active_admin_theme.generated.css
```
-and execute:
- $ yarn
+Run the task before every Tailwind build, because the generated file does not exist on a fresh checkout and its path changes when the gem upgrades. For example, in the build script that runs Tailwind:
-## Usage
+```sh
+bin/rails active_admin_theme:css && npx tailwindcss -i app/assets/stylesheets/active_admin.css -o app/assets/builds/active_admin.css
+```
-In your base stylesheet entry point `active_admin.scss` (as example), add line:
+If that is awkward for your build, use the npm route.
+
+**From the npm package.** This avoids a machine-specific path and a build step, and fits hosts that already install `@activeadmin/activeadmin` from npm:
+
+```sh
+npm install @activeadmin-plugins/active_admin_theme
+```
-#### As a Gem via Sprockets
```css
-@import 'wigu/active_admin_theme';
+@import "tailwindcss";
+@config "../../../tailwind-active_admin.config.js";
+@import "@activeadmin-plugins/active_admin_theme/css";
```
-#### As a NPM module (Yarn package) via Webpacker or any other assets bundler
+Keep the npm package and the gem on the same version.
-```css
-@import '@activeadmin-plugins/active_admin_theme';
+Tailwind only generates the classes it finds in files it scans. In `tailwind-active_admin.config.js`, add the gem's files to `content`:
+
+```js
+const themePath = execSync('bundle show active_admin_theme', { encoding: 'utf-8' }).trim().split(/\r?\n/).pop();
+
+export default {
+ content: [
+ // ActiveAdmin's entries stay as they are
+ `${themePath}/app/**/*.{arb,erb,html,rb,js}`,
+ `${themePath}/lib/**/*.rb`,
+ // ...
+ ],
+ // ...
+}
```
-## Customising
+### JavaScript
-Set any of the variables below *above* the import line:
+ActiveAdmin 4 renders its own import map, `ActiveAdmin.importmap`, not the import map in your `config/importmap.rb`. A `pin` in your own `config/importmap.rb` loads nothing on ActiveAdmin pages.
+
+The theme's engine pins its modules into `ActiveAdmin.importmap`, so importmap hosts need no setup. To load one of your own scripts on ActiveAdmin pages, pin it into the same map, for example in `config/initializers/active_admin.rb`:
+
+```ruby
+ActiveAdmin.importmap.pin "my_admin_script", to: "my_admin_script.js"
+```
-```scss
-$skinMainFirstColor: #A5A7AA;
-$skinMainSecondColor: #0066CC;
-$skinBorderWindowColor: #B8BABE;
+ActiveAdmin pages import the modules listed in `ActiveAdminTheme.javascripts`. With an ActiveAdmin that has the `javascript_modules` setting ([activeadmin/activeadmin#9148](https://github.com/activeadmin/activeadmin/pull/9148)), the theme adds them to that setting after your initializers run. With an older ActiveAdmin, the theme's own `_html_head` imports them. The default list is:
-@import 'wigu/active_admin_theme';
+```ruby
+%w[
+ active_admin_theme/layout
+ active_admin_theme/server_clock
+ active_admin_theme/table_crisscross
+]
```
-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';
+Change the list in an initializer to load fewer scripts or to add the optional ones:
+
+```ruby
+ActiveAdminTheme.javascripts -= %w[active_admin_theme/table_crisscross]
+ActiveAdminTheme.javascripts += %w[active_admin_theme/secret_reveal]
```
-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.
+**jsbundling hosts.** The npm package exports each module (`@activeadmin-plugins/active_admin_theme/layout`, `.../server_clock`, and so on). Import the ones you want from your `active_admin` entry file, and override `app/views/active_admin/_html_head.html.erb` to load your bundle.
+
+## Overriding
+
+A host partial with the same path overrides the theme's. Copy the partial from the gem into your app's `app/views/active_admin/` and edit it. The theme's partials are:
-### Switching themes
+- `_header_leading`: the menu button and the site title.
+- `_header_navigation`: the top menu.
+- `_main_navigation`: the mobile drawer menu.
+- `_header_actions`: the server clock, the dark mode switch, the current user, and the logout icon.
+- `_page_heading`, `_breadcrumbs`, `_page_title`: the sidebar toggle, the breadcrumbs, and the page title.
+- `_page_content`: the grid that holds the sidebar and the page.
+- `_sidebar`.
-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:
+ActiveAdmin renders these partials from its own layout once it has the layout hooks ([activeadmin/activeadmin#9149](https://github.com/activeadmin/activeadmin/pull/9149) to [#9152](https://github.com/activeadmin/activeadmin/pull/9152)). Until then, the theme also ships copies of the layout, `_site_header`, `_page_header`, and `_html_head` in `app/views_compat` that render the same partials. The engine adds that directory to the view paths only when the installed ActiveAdmin lacks the hooks, so override the small partials above rather than those copies.
-```scss
-// app/assets/javascripts/active_admin.js
-//= require wigu/theme_toggle
+A host Arbre component with the same builder method name replaces the theme's, as long as it loads after the theme's. Components in `app/admin_components` (or any file loaded in a `to_prepare` block) do.
+
+## Colors
+
+All colors are CSS custom properties with a light value on `:root` and a dark value under `.dark`. Override any of them in your own CSS:
+
+```css
+:root {
+ --aat-header-bg: #1b3a57;
+ --aat-button: #1f6f4a;
+}
+.dark {
+ --aat-surface: #20242b;
+}
```
-```js
-// or, as an npm module
-import "@activeadmin-plugins/active_admin_theme/src/theme_toggle";
+| Property | Used for |
+| --- | --- |
+| `--aat-page-bg`, `--aat-surface`, `--aat-surface-2`, `--aat-surface-hover`, `--aat-selected-row` | Page, panels, table headers, hover, selected rows |
+| `--aat-text`, `--aat-text-muted`, `--aat-border` | Text and borders |
+| `--aat-input-bg`, `--aat-input-border` | Form controls |
+| `--aat-accent`, `--aat-link` | Accent and links |
+| `--aat-button`, `--aat-button-text` | Buttons and action items |
+| `--aat-header-bg`, `--aat-header-text`, `--aat-menu-*` | Header and top menu |
+
+## DSL
+
+### `columns`
+
+ActiveAdmin 4 removed `columns` and `column`. The theme restores them, with separate control for mobile and desktop.
+
+```ruby
+columns "1:3" do # desktop: 1/4 + 3/4; mobile: stacked
+ column { ... }
+ column { ... }
+end
+
+columns "1:1", mobile: "1:1" do # side by side on mobile too
+ ...
+end
+
+columns do # no ratio: equal widths, any number of columns
+ ...
+end
```
-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.
+- The ratio string lists one weight per column. Pixel widths are not supported.
+- On mobile, columns stack unless `mobile:` sets a ratio.
+- A column has a minimum width of `--aat-col-min` (14rem). When the row has no room for it, the column moves to the next line instead of clipping its content.
+- `column span: N` from ActiveAdmin 3 still works and counts as the column's weight.
-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:
+### `tabs`
```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
+tabs do
+ tab("Details") { ... }
+ tab("History", id: "history") { ... }
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.
+Tabs use Flowbite's tabs, which ActiveAdmin already loads.
-[](./img/switch.png)
+### `panel`
-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.
+ActiveAdmin 4 still has `panel`, but no longer wraps sidebar content or the show page's `attributes_table` in one. The theme wraps both again: sidebar sections in a panel titled with the section name, and `attributes_table` on a show page in a "Model Details" panel. `Panel#header_action` is not restored.
-`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`. The third state is there so that following the system stays
-reachable: a two-state toggle writes a preference on the first click and has no
-way back short of clearing storage by hand.
+## Components
-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.
+Every component is opt-in: nothing changes for an app that does not call it or load its script. In a `content do` block, wrap the helpers that return strings (`with_tooltip`, `pre_wrap`) in `text_node`.
-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.
+### `status_tag`
-### Disabling dark mode
+`ok`, `warning`, `error`, `info`, and `empty` have theme colors in light and dark mode. For any other value, define a CSS custom property named `--aat-status-` in your CSS. For a one-off color, pass `color:` (a hex, named, `rgb()`, or `hsl()` color; anything else raises `ArgumentError`):
-To ship light-only, two manual steps (there is no build flag):
+```ruby
+status_tag "Shipped" # uses --aat-status-shipped, if you define it
+status_tag "Late", color: "#b91c1c"
+```
-- Don't include `wigu/theme_toggle` in your JS manifest. That removes the switch
- and the automatic `data-theme` switching it drives.
-- Pin the document to light: add `data-theme="light"` to the `` element
- (e.g. by overriding the ActiveAdmin layout), so the
- `@media (prefers-color-scheme: dark)` styles never apply.
+### `truncate_text`
-### Server clock
+One line of text with an ellipsis and the full value in the `title` attribute:
-The `#utility_nav` now carries a ticking server clock — two lines, time + zone
-over the date — rendered by the theme's `server_clock.js` and injected per
-namespace by its Ruby backend (`ActiveAdminTheme.install!`), with the logout
-button beside it.
+```ruby
+truncate_text(order.note, length: 40, max_width: "12rem")
+```
-The backend injects only the markup; load the script the same way as the
-theme switch, or the nav shows a static one-line label and never ticks:
+### `with_tooltip`
-```js
-// app/assets/javascripts/active_admin.js
-//= require wigu/server_clock
+A plain `title` hint by default. With `html: true`, the tooltip is sanitized HTML shown by ActiveAdmin's Flowbite tooltip. The HTML tooltip is positioned inside its container, so a wrapper with `overflow` (a scrolling table) can clip it.
-// or, as an npm module
-import "@activeadmin-plugins/active_admin_theme/src/server_clock";
+```ruby
+text_node with_tooltip("42", "Total orders")
+text_node with_tooltip("42", "Total orders", html: true)
```
-[](./img/clock.png)
+### `pre_wrap`
-The theme also keeps a logout item in the nav. If your app has no logout route
-(custom auth) or you deliberately run a logout-free nav, use ActiveAdmin's own
-per-namespace switch — the theme honours it:
+Preformatted text that wraps long lines:
```ruby
-# config/initializers/active_admin.rb
-config.logout_link_path = nil
+text_node pre_wrap(response.body)
```
-The restored current-user item has a theme switch (ActiveAdmin offers none
-short of unsetting `current_user_method`, which breaks authorization):
+### `secret_reveal`
+
+A masked value that is fetched and shown on click. The URL must return `{"value": "..."}`. Needs the `active_admin_theme/secret_reveal` script.
```ruby
-ActiveAdminTheme.add_current_user = false
+secret_reveal url: secret_admin_account_path(account), mask: "••••••••"
```
-### Crisscross table highlighting
+### `confirm_dialog`
-Clicking a cell in an index table highlights its whole row and column — the
-cells get class `selected`, which the theme paints with `--aa-selected`, so the
-clicked value sits at the centre of a cross, with an accent outline on the cell
-itself. Handy for tracing a value to its row and column in a wide table.
+A confirmation dialog with optional form fields, built on the Flowbite modal that ActiveAdmin uses for batch actions. It submits a real form with the CSRF token. Needs the `active_admin_theme/confirm_dialog` script for focus handling and field validation. Do not place it inside another form.
-The `selected` fill is always in the theme (it also covers ActiveAdmin's own
-batch-action row selection); the click behaviour is opt-in — load the script to
-enable it, leave it out to keep tables static:
+```ruby
+confirm_dialog "Reject", title: "Reject order", message: "The buyer is notified.",
+ url: reject_admin_order_path(order), method: :post,
+ fields: [
+ {name: :reason, label: "Reason", as: :text, required: true},
+ {name: :kind, as: :select, collection: [["Fraud", "fraud"], ["Other", "other"]]},
+ {name: :notify, label: "Notify the buyer", as: :boolean, value: true}
+ ]
+```
-```js
-// app/assets/javascripts/active_admin.js
-//= require wigu/table_crisscross
+Field types are `:string`, `:text`, `:select`, and `:boolean`.
+
+### Searchable select
+
+A select built on [tom-select](https://tom-select.js.org) that searches over AJAX, loads more options as you scroll, and can depend on another select. It works in forms and filters, and on mobile. The gem vendors a pinned tom-select and pins it into `ActiveAdmin.importmap`; an app that does not load the script does not load tom-select. npm hosts add `tom-select` to their own dependencies. Add the script:
+
+```ruby
+ActiveAdminTheme.javascripts += %w[active_admin_theme/searchable_select]
+```
+
+If your app already defines its own `searchable_select_options` in an initializer, yours wins: the theme includes its version earlier.
-// or, as an npm module
-import "@activeadmin-plugins/active_admin_theme/src/table_crisscross";
+Serve the options from the resource that holds them:
+
+```ruby
+ActiveAdmin.register Customer do
+ searchable_select_options label: :name, search: :name_cont, per_page: 25
+end
+```
+
+`searchable_select_options(name = :all, scope: nil, label: nil, search: nil, value: :id, order: nil, per_page: 25)` adds a collection action that returns `{results: [{id:, text:}], has_more: true}` for `?term=` and `?page=`. It applies the authorization adapter's read scope. Use the input in a form or a filter:
+
+```ruby
+f.input :customer, as: :searchable_select
+f.input :city, as: :searchable_select, depends_on: :country_id, depends_placeholder: "Select a country first"
+filter :customer, as: :searchable_select
```
-It uses jQuery, which ActiveAdmin already loads.
+A dependent select stays disabled until its parent has a value, and reloads when the parent changes. Other options: `url:`, `resource:`, `options_name:`, `placeholder:`, `depends_param:`.
-[](./img/crisscross-light.png)
+## Optional scripts
-### Pinned bars and sticky table headers
+| Script | Default | What it does |
+| --- | --- | --- |
+| `active_admin_theme/layout` | on | Collapsible sidebar |
+| `active_admin_theme/server_clock` | on | Header clock |
+| `active_admin_theme/table_crisscross` | on | Row and column highlight on a cell click |
+| `active_admin_theme/confirm_dialog` | off | Focus and validation for `confirm_dialog` |
+| `active_admin_theme/secret_reveal` | off | `secret_reveal` |
+| `active_admin_theme/searchable_select` | off | Searchable select |
-On pages with a table wider than the viewport the whole page scrolls sideways;
-the theme pins `#header`, `#title_bar` (title and action buttons) and the flash
-bar to the visible viewport so they stay put. The bars are sized with `100vw`,
-which includes the vertical scrollbar gutter — load the shipped
-`scrollbar_width.js` to subtract it, or the bars sit ~15px past the visible
-viewport at the far-right scroll position:
+## Nested menus
-```js
-// app/assets/javascripts/active_admin.js
-//= require wigu/scrollbar_width
+ActiveAdmin 4 supports one submenu level and raises `TypeError` for a parent chain. The theme brings back the chain from ActiveAdmin 3, so menus can be as deep as you need:
-// or, as an npm module
-import "@activeadmin-plugins/active_admin_theme/src/scrollbar_width";
+```ruby
+menu parent: ["Customer", "Referral Program"], priority: 30
```
-Without the script the CSS falls back to plain `100vw` — nothing breaks, you
-just keep the small far-right shift.
+In the top menu each deeper level flies out beside its item, and in the drawer it expands below its item. A top item is marked current when the page is anywhere below it.
+
+## Header
-Index-table column headers also stick to the viewport top while a long listing
-scrolls vertically. This is on by default for ActiveAdmin index tables. AA 3.x
-hardcodes the index table's classes (`index class:` is ignored), so opt out
-app-wide with one line in your stylesheet:
+The header shows the site title, the top menu, a server clock, the dark mode switch, the current user, and a logout icon. They come from the signed-in user (`current_active_admin_user?`) and `config.logout_link_path`. Without a current user the header shows only the title, the menu, and the dark mode switch. To change the header, override `_header_actions.html.erb`, `_header_leading.html.erb`, or `_header_navigation.html.erb`.
-```scss
-.paginated_collection table.index_table thead tr th { position: static; }
+## Development
+
+```sh
+bundle install
+bundle exec rake test # builds the dummy app's Tailwind CSS, runs smoke and browser tests (needs Chrome)
+bundle exec ruby test/screenshot.rb # PAGES, WIDTHS, MODES, OUT environment variables
+```
+
+`test/dummy` is an ActiveAdmin 4 app (importmap, propshaft, Tailwind through the npm CLI). Run `npm run build` in `test/dummy` to rebuild its CSS.
+
+To run the tests against an unreleased ActiveAdmin, such as a branch with the layout hooks, point Bundler at it:
+
+```sh
+ACTIVEADMIN_PATH=../activeadmin bundle exec rake test
+ACTIVEADMIN_BRANCH=extensible-layout bundle exec rake test # from ACTIVEADMIN_REPO, default senid231/activeadmin
```
-For tables whose markup you render yourself, `skip-sticky-headers` on an
-`index_table` opts out, and `with-sticky-headers` opts any hand-built table in
-(single-row theads: only the first header row pins).
-
-`//= require wigu/all` pulls all four shipped scripts at once; each stays
-individually requirable.
-
-### Trix / ActionText editor
-
-Trix ships with ActionText (a Rails built-in), so any ActiveAdmin app can have a
-rich-text field. The theme styles the editor chrome — toolbar, editor surface,
-link dialog, content `pre`/`blockquote` — off the theme variables in both light
-and dark, and inverts Trix's black toolbar icons in dark mode so they stay
-legible. Nothing to opt into: your app still imports Trix's own base CSS
-(`trix/dist/trix`), the theme only restyles it, and the rules are inert when no
-editor is on the page.
-
-[](./img/trix-light.png)
-[](./img/trix-dark.png)
-
-### Upgrading
-
-#### 3.2.0
-
-The variable contract only grows — `$skinTitleBarHeight` joins it and nothing
-that worked against 3.1.1 stops working — but several defaults now look
-different, and two of them take layout away from the host app:
-
-* `#header`, `#title_bar` and the flash bar are pinned to the visible viewport,
- so they stay put while a table wider than the window scrolls the page
- sideways. Apps that pinned those bars with their own stylesheet should drop
- those rules: the theme lays the title bar out with flexbox and resets
- `#titlebar_right` to `position: static`, and a leftover `position:
- absolute/sticky` fights it. Load `wigu/scrollbar_width` to subtract the
- scrollbar gutter — see *Pinned bars and sticky table headers*.
-* Index-table column headers stick to the viewport top on every ActiveAdmin
- index table. One line in your stylesheet opts out app-wide:
- `.paginated_collection table.index_table thead tr th { position: static; }`.
-* The title bar owns its own metrics now: the record title is 16px, on one line
- with an ellipsis, instead of ActiveAdmin's large wrapping heading; action
- items are spaced; the band is `$skinTitleBarHeight` (37px) tall, with
- `$skinTitleBarButtonPaddingY` / `$skinTitleBarButtonPaddingX` on the buttons.
- Title-bar dropdown panels right-align under their own button.
-* The utility nav carries the server clock, the theme switch and a logout drawn
- as a sign-out glyph rather than the word *Logout*. `ActiveAdminTheme
- .add_current_user = false` drops the added current-user item, and
- ActiveAdmin's own `config.logout_link_path = nil` drops the logout one.
-
-New and opt-in, nothing to change on upgrade: the Trix / ActionText editor is
-themed in both modes, crisscross cell highlighting (`wigu/table_crisscross`),
-`--aa-datetimepicker-*` bridge variables for the companion datetimepicker, dark
-styling for select2 and tom-select, and `//= require wigu/all` to pull every
-shipped script at once.
-
-#### 3.1.0
-
-* Status tag fills are darker, so the label can be white and still clear 4.5:1 —
- it was black on mid-tone fills before. Each fill is a variable now
- (`$skinStatusTagOkColor` and the four beside it), and
- `$skinStatusTagTextColor: #000000;` restores the old label with the fills
- you choose.
-* Index-table column headings take `$skinTextColor` rather than a muted grey,
- so a heading reads as strongly as the column under it.
-
-#### 3.0.0
-
-Three things changed shape 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 |
-| `$skinErrorTextColor` / `$skinErrorTextColorDark` |  `#932419` /  `#f49b9b` | validation messages |
-| `$skinErrorBgColor` / `$skinErrorBgColorDark` |  `#fae6e4` /  `#3a2224` | error summary above a form |
-
-#### 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 |
-| `$skinTitleBarHeight` | `37px` | minimum bar height; grows to fit content |
-
-#### 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` | `$skinTextColor` / `$skinTextColorDark` | index-table column header text; the body text colour, so headings read as strongly as the rows |
-| `$skinStatusTagTextColor` |  `#ffffff` | label inside a filled status tag; `empty` / `unknown` / `none` have no fill and keep `$skinTextMutedColor` |
-| `$skinStatusTagNeutralColor` |  `#707681` | unclassified tags: `No`, protocol tags |
-| `$skinStatusTagOkColor` |  `#5e7e63` | `ok` `published` `complete` `completed` `green` `yes` |
-| `$skinStatusTagNoticeColor` |  `#3874d2` | `notice` `blue` |
-| `$skinStatusTagWarnColor` |  `#9e6c15` | `warn` `warning` `orange` |
-| `$skinStatusTagErrorColor` |  `#ce483b` | `error` `errored` `red` |
-| `$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
-
-1. Fork it ( https://github.com/activeadmin-plugins/active_admin_theme/fork )
-2. Create your feature branch (`git checkout -b my-new-feature`)
-3. Commit your changes (`git commit -am 'Add some feature'`)
-4. Push to the branch (`git push origin my-new-feature`)
-5. Create a new Pull Request
+The theme replaces some of ActiveAdmin's own view files with its copies. `test/aa_overrides_test.rb` fails when the installed ActiveAdmin changes one of them. It records the originals for both modes, with and without the layout hooks, so run it against both. After you raise the ActiveAdmin version, diff each reported file against the theme's copy in `app/views` or `app/views_compat`, port what applies, then refresh the record:
+
+```sh
+UPDATE_AA_DIGESTS=1 bundle exec ruby test/aa_overrides_test.rb
+```
+
+## License
+
+MIT
diff --git a/Rakefile b/Rakefile
index b090a1d..22f613c 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1,21 +1,23 @@
require "bundler/gem_tasks"
-desc "Compile the theme against ActiveAdmin and check the result"
-task :css do
- ruby "test/css_check.rb"
+desc "Build the dummy app's Tailwind CSS (npm CLI)"
+task :assets do
+ Dir.chdir("test/dummy") do
+ unless Dir.exist?("node_modules")
+ sh "npm #{ENV['CI'] ? 'ci' : 'install'} --no-audit --no-fund"
+ end
+ sh "npm run build"
+ end
end
-desc "Capybara integration test: the server clock renders + ticks (needs Chrome)"
-task :test do
- ruby "test/clock_test.rb"
+desc "Smoke test: every demo page renders with the theme (builds CSS first)"
+task test: :assets do
+ Dir["test/*_test.rb"].sort.each { |file| ruby file }
end
-desc "Regenerate the neutral README screenshot img/clock.png (needs Chrome)"
+desc "Regenerate the README screenshot img/clock.png (needs Chrome)"
task :screenshot do
ruby "test/screenshot.rb"
end
-# css only: :test needs the Rails/Capybara stack the Gemfile excludes on
-# Ruby 3.1 (the gem's own minimum) plus a local Chrome. Run `rake test`
-# explicitly where both are available; CI names its tasks per job anyway.
-task default: :css
+task default: :test
diff --git a/active_admin_theme.gemspec b/active_admin_theme.gemspec
index a41ff9a..c9b9155 100644
--- a/active_admin_theme.gemspec
+++ b/active_admin_theme.gemspec
@@ -8,12 +8,12 @@ Gem::Specification.new do |spec|
spec.version = ActiveAdminTheme::VERSION
spec.authors = ["Igor Fedoronchuk", "Alex Sikorskiy"]
spec.email = ["igor.f@didww.com", "alex.s@didww.com"]
- spec.summary = %q{Flat design for ActiveAdmin}
- spec.description = %q{Flat design for activeadmin gem }
+ spec.summary = %q{Dense desktop theme and shared components for ActiveAdmin 4}
+ spec.description = %q{A compact top-menu layout, theme colors, and opt-in DSL and UI components for ActiveAdmin 4 (Tailwind CSS).}
spec.homepage = "https://github.com/activeadmin-plugins/active_admin_theme"
spec.license = "MIT"
- spec.required_ruby_version = '>= 3.1.0'
+ spec.required_ruby_version = '>= 3.3.0'
# Whitelist, not a reject list: a new directory in the repo does not
# reach consumers until it is named here. The reject form needs a new
@@ -25,5 +25,5 @@ Gem::Specification.new do |spec|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
- spec.add_dependency "activeadmin", ">= 3.0", "< 4.0.0.a"
+ spec.add_dependency "activeadmin", ">= 4.0.0.beta23", "< 5.0"
end
diff --git a/app/assets/javascripts/wigu/all.js b/app/assets/javascripts/wigu/all.js
deleted file mode 100644
index d613862..0000000
--- a/app/assets/javascripts/wigu/all.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// Umbrella require for every script the theme ships, so a Sprockets manifest
-// carries one line instead of a hand-maintained list that silently drifts:
-// theme CSS can depend on a JS-set property (scrollbar_width feeds the pinned
-// bars' width), and a missed require degrades visually with no error. Each
-// script stays individually requirable for apps that want a subset; see the
-// README for what each one does and how to opt out.
-//= require wigu/theme_toggle
-//= require wigu/server_clock
-//= require wigu/scrollbar_width
-//= require wigu/table_crisscross
diff --git a/app/assets/javascripts/wigu/scrollbar_width.js b/app/assets/javascripts/wigu/scrollbar_width.js
deleted file mode 100644
index 74bb762..0000000
--- a/app/assets/javascripts/wigu/scrollbar_width.js
+++ /dev/null
@@ -1,72 +0,0 @@
-// Expose the vertical scrollbar width as a CSS custom property
-// (--scrollbar-width) on the pinned bars. The sticky #header, #title_bar and
-// .flashes are pinned to the viewport's left edge and sized with 100vw, but
-// 100vw INCLUDES the vertical scrollbar gutter — so when a page has a vertical
-// scrollbar the bars are ~15px wider than the visible viewport and, clamped
-// inside their full-content-width containing block, get pushed left by that
-// amount at the far-right horizontal scroll position. Subtracting this
-// variable (0 when there is no scrollbar) sizes them to the visible viewport
-// instead, so they stay put. Optional: the CSS falls back to
-// `var(--scrollbar-width, 0px)` = plain 100vw when this script is absent.
-//
-// The value is written on the three consuming bars, NOT on : a custom
-// property changed on the root invalidates style for the entire document —
-// one full recalc of a 500-row index table per write — while var() resolves
-// from the element itself just as well, confining invalidation to three tiny
-// subtrees.
-//
-// The property name is deliberately the bare --scrollbar-width, not --aa-*:
-// consumer apps that already set the same variable with the same semantics
-// from their own legacy script keep working before their manifest is updated
-// (a root-level legacy value still reaches the bars through inheritance when
-// this script is absent). The trade-off is a shared name — load one writer,
-// not both.
-(function () {
- "use strict";
-
- var BARS = "#header, #title_bar, .flashes";
- var last = null;
-
- // Idempotent and cheap: reads the inline style (no layout) and writes only
- // real changes, so it is safe to call on every observer delivery — which is
- // what picks up bars parsed after the value was first computed, and bars
- // replaced by a Turbo/Turbolinks body swap.
- function apply() {
- if (last === null) return;
- var val = last + "px";
- var bars = document.querySelectorAll(BARS);
- for (var i = 0; i < bars.length; i++) {
- if (bars[i].style.getPropertyValue("--scrollbar-width") !== val) {
- bars[i].style.setProperty("--scrollbar-width", val);
- }
- }
- }
-
- function update() {
- var w = window.innerWidth - document.documentElement.clientWidth;
- if (w < 0) w = 0;
- last = w;
- apply();
- }
-
- if (window.ResizeObserver) {
- // The observer alone is enough: it delivers once right after observe()
- // (post-layout, pre-paint, so the read forces nothing) and again whenever
- // the root's client box changes — which covers window resizes AND content
- // growth toggling the scrollbar. A resize listener on top of it would
- // force a synchronous style+layout per event during a window drag (the
- // clientWidth read lands before the frame's layout) for no extra
- // information. documentElement exists at parse time and survives body
- // swaps, so there is no DOMContentLoaded gate and nothing goes stale.
- new ResizeObserver(update).observe(document.documentElement);
- } else {
- // No ResizeObserver: fall back to resize events plus one eager read.
- window.addEventListener("resize", update);
- update();
- }
- // A body swap replaces the bars the value was written on without
- // necessarily resizing the root; re-apply the cached value to the fresh
- // elements.
- document.addEventListener("turbo:load", apply);
- document.addEventListener("turbolinks:load", apply);
-})();
diff --git a/app/assets/javascripts/wigu/server_clock.js b/app/assets/javascripts/wigu/server_clock.js
deleted file mode 100644
index b7af4a5..0000000
--- a/app/assets/javascripts/wigu/server_clock.js
+++ /dev/null
@@ -1,171 +0,0 @@
-// Server clock for the utility navigation. Shows the server's time ticking once
-// a second in li#servertime inside #utility_nav, where the stylesheet styles it.
-//
-// The instant comes from a [data-servertime] element the host app renders — the
-// body in some apps, a footer element in others — holding
-// "YYYY MM DD HH MM SS ZONE" (space-separated, exactly what Ruby's
-// Time#strftime('%Y %m %d %H %M %S %Z') produces). The clock keeps that server
-// instant and advances it by the client's own elapsed time, so a skewed browser
-// clock does not throw it off. With no such element it renders nothing: a
-// fabricated client clock would bypass every server-side suppression (anonymous
-// visitors, hosts without current_user, consumer opt-outs) and show browser
-// time masquerading as the server clock.
-//
-// Vanilla, no deps, mounts itself into #utility_nav. Turbo/Turbolinks swap the
-// body without firing DOMContentLoaded, so the clock re-reads the fresh
-// [data-servertime] on their load events and re-queries the DOM every tick
-// instead of holding node references — a held reference goes detached on the
-// first navigation and the visible clock freezes.
-(function () {
- "use strict";
-
- var serverTime, clientTime, zone, fmt;
- var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
- "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
-
- function pad2(n) {
- return (n < 10 ? "0" : "") + n;
- }
-
- function ready(fn) {
- if (document.readyState !== "loading") fn();
- else document.addEventListener("DOMContentLoaded", fn);
- }
-
- // (Re)read the server instant from the current document.
- function sync() {
- // Prefer the theme's own span inside the nav: a host mid-migration may
- // still render its old body[data-servertime] (possibly in another format),
- // which would win on first-match-in-tree-order and shadow the fresh
- // per-request instant. Fall back to anywhere in the document for hosts
- // that only render the attribute on the body or a footer element.
- var nav = document.getElementById("utility_nav");
- var source = (nav && nav.querySelector("[data-servertime]")) ||
- document.querySelector("[data-servertime]");
-
- // Preferred source: an absolute instant plus the server's IANA zone
- // (rendered by the gem's Ruby backend). Intl re-applies the zone's offset
- // on every tick, so a long-lived page follows the server through its own
- // DST transitions — the wall-clock path below keeps the offset and label
- // frozen from page load.
- fmt = null;
- serverTime = undefined; // no source on this page: tick() renders nothing
- var epoch = source && source.getAttribute("data-servertime-epoch");
- var tz = source && source.getAttribute("data-servertime-zone");
- if (epoch && tz && window.Intl &&
- Intl.DateTimeFormat.prototype.formatToParts) {
- try {
- fmt = new Intl.DateTimeFormat("en-US", {
- timeZone: tz, hour12: false, hourCycle: "h23",
- year: "numeric", month: "short", day: "2-digit",
- hour: "2-digit", minute: "2-digit", second: "2-digit",
- timeZoneName: "short"
- });
- serverTime = epoch * 1000;
- // A malformed epoch is truthy but NaN; formatToParts(new Date(NaN))
- // would throw on every tick. Drop to the wall-clock path instead.
- if (!isFinite(serverTime)) fmt = null;
- } catch (e) {
- fmt = null; // unknown zone name: fall through to the wall-clock path
- }
- }
-
- var raw = !fmt && source && source.getAttribute("data-servertime");
-
- if (raw) {
- var bits = raw.split(" ");
- // A fixed epoch via Date.UTC, NOT new Date(y,m,d,...): local-zone
- // parsing shifts a server instant that falls into the client's DST gap,
- // and a tab left open across a client transition jumps the display an
- // hour while the server had none. The zone label is carried verbatim.
- serverTime = Date.UTC(bits[0], bits[1] - 1, bits[2], bits[3], bits[4], bits[5]);
- // No fabricated label: a missing %Z just renders no zone.
- zone = bits[6] || "";
- }
- clientTime = Date.now();
- tick();
- }
-
- // The two display lines for this li, created on first use and reused every
- // later tick. The server-rendered inner span [data-servertime] is the data
- // source for later sync() calls (turbo:load, bfcache restores) — never
- // overwrite it; hide it and append the lines beside it. textContent only,
- // no innerHTML: the zone token of a host-rendered attribute is untrusted
- // text and must not be able to become markup — and skipping the per-second
- // innerHTML re-parse stops the node churn too.
- function lines(li) {
- var time = li.querySelector(".clock-time");
- if (time) return { time: time, date: li.querySelector(".clock-date") };
- var data = li.querySelector("[data-servertime]");
- // Append target: the outer Arbre span when the gem rendered the item, the
- // li's own span otherwise. No inline colour — the span inherits the nav's
- // text colour from the stylesheet cascade.
- var host = data ? data.parentNode : li.querySelector("span");
- if (!host) {
- host = document.createElement("span");
- li.appendChild(host);
- }
- if (data) data.style.display = "none";
- time = document.createElement("span");
- time.className = "clock-time";
- var date = document.createElement("span");
- date.className = "clock-date";
- host.appendChild(time);
- host.appendChild(date);
- return { time: time, date: date };
- }
-
- function tick() {
- var nav = document.getElementById("utility_nav");
- // isFinite, not an undefined check: a host-rendered data-servertime in an
- // unexpected format parses to NaN, which must render nothing — not a
- // ticking "NaN:NaN:NaN".
- if (!nav || !isFinite(serverTime)) return;
-
- // Reuse an existing li (idempotent across re-renders); never duplicate.
- var li = nav.querySelector("#servertime");
- if (!li) {
- li = document.createElement("li");
- li.id = "servertime";
- // Mark it as ours, so the ownership check below keeps passing.
- li.setAttribute("data-theme-clock", "");
- nav.appendChild(li);
- } else if (!li.hasAttribute("data-theme-clock") &&
- !li.querySelector("[data-servertime]")) {
- // The li belongs to the host — their own servertime menu item, e.g.
- // rendered as a plain link. Appending display lines would show the
- // host's label and a second clock side by side in one item.
- return;
- }
- var out = lines(li);
-
- // Two lines, as in yeti-web: time + timezone on top (larger), year month
- // day below (smaller). The stylesheet sizes .clock-time / .clock-date.
- var now = new Date(serverTime + (Date.now() - clientTime));
- var time, date;
- if (fmt) {
- // serverTime is an absolute instant; Intl renders it in the server's
- // zone, offset and abbreviation recomputed per tick.
- var p = {};
- fmt.formatToParts(now).forEach(function (part) { p[part.type] = part.value; });
- time = p.hour + ":" + p.minute + ":" + p.second + " " + p.timeZoneName;
- date = p.year + " " + p.month + " " + p.day;
- } else {
- // UTC getters only — serverTime is a fixed epoch holding a wall clock,
- // so the client's zone and DST rules never touch the math.
- time = pad2(now.getUTCHours()) + ":" + pad2(now.getUTCMinutes()) + ":" +
- pad2(now.getUTCSeconds()) + (zone ? " " + zone : "");
- date = now.getUTCFullYear() + " " + MONTHS[now.getUTCMonth()] + " " +
- pad2(now.getUTCDate());
- }
- out.time.textContent = time;
- out.date.textContent = date;
- }
-
- ready(function () {
- sync();
- setInterval(tick, 1000);
- });
- document.addEventListener("turbo:load", sync);
- document.addEventListener("turbolinks:load", sync);
-})();
diff --git a/app/assets/javascripts/wigu/table_crisscross.js b/app/assets/javascripts/wigu/table_crisscross.js
deleted file mode 100644
index e885b4d..0000000
--- a/app/assets/javascripts/wigu/table_crisscross.js
+++ /dev/null
@@ -1,49 +0,0 @@
-// Crisscross highlighting for ActiveAdmin index tables. Clicking a cell marks
-// its whole row and column with class "selected", which the stylesheet paints
-// with var(--aa-selected) — so the clicked cell sits at the centre of a
-// highlighted cross, making it easy to trace a value to its row and column in a
-// wide table.
-//
-// Uses jQuery, which ActiveAdmin loads before this file (its own
-// checkbox-toggler is jQuery too); the rest of the theme JS is dependency-free,
-// but this behaviour lives entirely in AA's jQuery-driven index table.
-//
-// AA's table-checkbox-toggler selects the row when a cell is clicked; we undo
-// that (re-click the checkbox) so crisscross highlighting never leaves rows
-// ticked for batch actions.
-$(document).ready(function () {
- var marker = 'selected';
-
- // "Select all" clears every row marker left by AA's toggler.
- $('table.index_table .toggle_all').click(function () {
- $(this).closest('table').find('tr.' + marker).removeClass(marker);
- });
-
- // Clicking the row checkbox itself must not bubble into the cell handler.
- $('table.index_table').find(':checkbox').click(function (e) {
- e.stopPropagation();
- $(this).closest('tr.' + marker).removeClass(marker);
- });
-
- $('table.index_table').on('click', 'td', function (e) {
- var tdIndex = e.currentTarget.cellIndex,
- $cell = $(e.currentTarget),
- $table = $cell.closest('table'),
- $row = $cell.closest('tr'),
- $columnTds = $table.find('td:nth-child(' + (tdIndex + 1) + ')'),
- $rowTds = $row.find('td');
-
- // AA ticks the row checkbox on a cell click; click it again to revert.
- $row.find(':checkbox').click();
-
- if (!$cell.hasClass('col-selectable')) {
- $table.find('td.' + marker).removeClass(marker);
- $table.find('td.selected-cell').removeClass('selected-cell');
- $columnTds.addClass(marker);
- $rowTds.addClass(marker);
- // Mark the clicked cell itself so the stylesheet can set it apart as the
- // centre of the cross.
- $cell.addClass('selected-cell');
- }
- });
-});
diff --git a/app/assets/javascripts/wigu/theme_toggle.js b/app/assets/javascripts/wigu/theme_toggle.js
deleted file mode 100644
index d96b516..0000000
--- a/app/assets/javascripts/wigu/theme_toggle.js
+++ /dev/null
@@ -1,140 +0,0 @@
-// 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,
-// so the control can live anywhere and survive a re-render. If neither exists
-// it inserts its own entry into 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
deleted file mode 100644
index 7d1984f..0000000
--- a/app/assets/stylesheets/wigu/active_admin_theme.scss
+++ /dev/null
@@ -1,2216 +0,0 @@
-//VAR-----------------------------------------------------------------------------------------------------------------//
-
-$skinMainFirstColor: #23282f!default;
-$skinMainSecondColor: #5ea3d3!default;
-$skinBorderRadius: 4px!default;
-$skinBorderWindowColor: #e6e9ee!default;
-$skinTablePadding: 10px!default;
-
-// 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
-$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: #343c46!default;
-$skinTitleBarBorderColor: $skinMainSecondColor!default;
-$skinTitleBarBorderWidth: 0!default;
-$skinTitleBarButtonPaddingY: 6px!default; // action button vertical padding
-$skinTitleBarButtonPaddingX: 10px!default; // action button horizontal padding
-// Minimum bar height — raise it alongside the button paddings above if your
-// buttons outgrow the default band (the bar still grows to fit its content).
-$skinTitleBarHeight: 37px!default;
-
-// 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;
-// Validation errors: message text and summary block fill.
-$skinErrorTextColor: #932419!default;
-$skinErrorTextColorDark: #f49b9b!default;
-$skinErrorBgColor: #fae6e4!default;
-$skinErrorBgColorDark: #3a2224!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: $skinTextColor!default;
-$skinTableHeaderTextColorDark: $skinTextColorDark!default;
-
-// Status tags. The label is the same on every filled tag in both modes. The
-// fills below are dark enough to carry a white one: every one of the five is
-// at least 4.53 against it. The mid-tone fills this palette replaces ran from
-// 2.35 to 3.78, under the 4.5 WCAG AA asks of text this small, which is why
-// they moved rather than the label.
-$skinStatusTagTextColor: #ffffff!default;
-$skinStatusTagNeutralColor: #707681!default; // "No", protocol tags, anything unclassified
-$skinStatusTagOkColor: #5e7e63!default; // ok / published / complete / green / yes
-$skinStatusTagNoticeColor: #3874d2!default; // notice / blue
-$skinStatusTagWarnColor: #9e6c15!default; // warn / warning / orange
-$skinStatusTagErrorColor: #ce483b!default; // error / errored / red
-
-//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};
- --aa-error: #{$skinErrorTextColorDark};
- --aa-error-bg: #{$skinErrorBgColorDark};
-}
-: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};
- --aa-error: #{$skinErrorTextColor};
- --aa-error-bg: #{$skinErrorBgColor};
-
- // Bridge for the companion active_admin_datetimepicker plugin: feed its
- // --aa-datetimepicker-* inputs from this theme's palette, so the xdsoft picker
- // is themed (light + dark) automatically with no per-app override. These point
- // at the --aa-* vars, which are themselves redefined per mode, so a single set
- // here covers both. Harmless no-op when the plugin isn't installed (nothing
- // reads these). A host can still override any of them downstream.
- --aa-datetimepicker-accent: var(--aa-accent);
- --aa-datetimepicker-surface: var(--aa-surface);
- --aa-datetimepicker-surface-2: var(--aa-surface-2);
- --aa-datetimepicker-text: var(--aa-text);
- --aa-datetimepicker-text-muted: var(--aa-text-muted);
- --aa-datetimepicker-border: var(--aa-border);
- --aa-datetimepicker-hover: var(--aa-surface-hover);
-}
-// 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; }
-}
-
-// Sort markers. Three shapes rather than one at two opacities: ActiveAdmin's
-// sprite draws a double arrow for sortable-but-unsorted, and reusing the
-// descending triangle for it leaves the two states indistinguishable on a
-// freshly loaded index. Masked, like the theme switch icons, so they take
-// currentColor and follow the heading into dark mode.
-$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;
-$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;
-$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;
-
-// Theme switch icons. Inline, because the gem ships no image files, and as a
-// mask rather than a coloured SVG: a mask takes currentColor and so follows
-// $skinMenuTextColor on its own. Half circle = auto, sun = light, moon = 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;
-
-// Logout drawn as a sign-out glyph instead of the word "Logout", so it matches
-// the icon toggle beside it. Masked like the theme icons above, so it takes
-// currentColor and follows $skinMenuTextColor; a consumer can override the var.
-$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;
-
-// A wrong-typed override is legal SassScript, so it compiles without a word and
-// emits CSS the browser drops — `border-bottom: none solid #5ea3d3`,
-// `padding: 10 12px`. The rule silently vanishes and ActiveAdmin's own value
-// reappears. Fail the build with a readable message instead.
-@each $name, $value in (
- skinBorderRadius: $skinBorderRadius,
- skinTablePadding: $skinTablePadding,
- skinMenuFontSize: $skinMenuFontSize,
- skinMenuItemPaddingY: $skinMenuItemPaddingY,
- skinMenuPanelMaxWidth: $skinMenuPanelMaxWidth,
- skinHeaderPaddingTop: $skinHeaderPaddingTop,
- skinHeaderPaddingBottom: $skinHeaderPaddingBottom,
- skinTitleBarBorderWidth: $skinTitleBarBorderWidth,
- skinTitleBarButtonPaddingY: $skinTitleBarButtonPaddingY,
- skinTitleBarButtonPaddingX: $skinTitleBarButtonPaddingX
-) {
- @if type-of($value) != number or (unitless($value) and $value != 0) {
- @error "$#{$name} must be a length with a unit (e.g. 10px), got `#{$value}`.";
- }
-}
-@each $name, $value in (
- skinMainFirstColor: $skinMainFirstColor,
- skinMainSecondColor: $skinMainSecondColor,
- skinBorderWindowColor: $skinBorderWindowColor,
- skinMenuPillColor: $skinMenuPillColor,
- skinMenuPillTextColor: $skinMenuPillTextColor,
- skinMenuPanelColor: $skinMenuPanelColor,
- skinMenuItemHoverColor: $skinMenuItemHoverColor,
- skinMenuTextColor: $skinMenuTextColor,
- skinTitleBarColor: $skinTitleBarColor,
- 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,
- skinErrorTextColor: $skinErrorTextColor,
- skinErrorTextColorDark: $skinErrorTextColorDark,
- skinErrorBgColor: $skinErrorBgColor,
- skinErrorBgColorDark: $skinErrorBgColorDark,
- skinTabInactiveColor: $skinTabInactiveColor,
- skinTabInactiveColorDark: $skinTabInactiveColorDark,
- skinActiveTabTextColor: $skinActiveTabTextColor,
- skinActiveTabTextColorDark: $skinActiveTabTextColorDark,
- skinInactiveTabTextColor: $skinInactiveTabTextColor,
- skinInactiveTabTextColorDark: $skinInactiveTabTextColorDark,
- skinTableHeaderTextColor: $skinTableHeaderTextColor,
- skinTableHeaderTextColorDark: $skinTableHeaderTextColorDark,
- skinStatusTagTextColor: $skinStatusTagTextColor,
- skinStatusTagNeutralColor: $skinStatusTagNeutralColor,
- skinStatusTagOkColor: $skinStatusTagOkColor,
- skinStatusTagNoticeColor: $skinStatusTagNoticeColor,
- skinStatusTagWarnColor: $skinStatusTagWarnColor,
- skinStatusTagErrorColor: $skinStatusTagErrorColor
-) {
- @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}`.";
- }
-}
-// A cell's inner corner is 1px tighter than the outer one, so it does not
-// show from under the table border. Subtracting px directly will not do: the
-// radius is a project's to set, and 0.25rem fails the whole compile with
-// "Incompatible units". calc() defers the subtraction to the browser, and a
-// unitless 0 is handled separately because calc(0 - 1px) is invalid.
-@function inner-radius($radius) {
- @if unitless($radius) { @return 0; }
- @return calc(#{$radius} - 1px);
-}
-
-//HTML----------------------------------------------------------------------------------------------------------------//
-
-html {
- background: var(--aa-page-bg);
-}
-
-// Pin a bar (#header, #title_bar) to the viewport's left edge at the visible
-// viewport width while a wide index table scrolls the whole page sideways
-// (#wrapper is display:table, min-width:100%). width = 100vw minus the vertical
-// scrollbar — the JS-set --scrollbar-width var (0 when wigu/scrollbar_width is
-// absent, and then max-width:100% keeps 100vw's scrollbar allowance from adding a
-// stray horizontal scrollbar). It's a plain max-width, not min(…, 100%):
-// sassc/libsass evaluates the Sass min() function and rejects calc() as a
-// non-number. No `top`, so the bar still scrolls vertically.
-@mixin viewport-pinned-bar {
- position: sticky;
- left: 0;
- width: calc(100vw - var(--scrollbar-width, 0px));
- max-width: 100%;
- box-sizing: border-box;
-}
-
-body.active_admin {
- background: var(--aa-page-bg);
-}
-#wrapper {
- // ActiveAdmin lays #wrapper out as `display: table`, which sizes to its
- // content: a wide index table therefore scrolls the WHOLE page horizontally
- // (the master / pre-theme behaviour admins are used to). Only guarantee the
- // layout fills the viewport when the content is narrow. The utility nav
- // (clock / theme toggle / logout) is pinned `sticky` (see #utility_nav below)
- // so it stays reachable while the page scrolls sideways.
- min-width: 100%;
-
- div.tabs {
- > ul > li > a {
- background-image: none;
- box-shadow: none;
- text-shadow: none;
- box-shadow: none;
- margin-bottom: 11px;
- // 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: $skinTabPaddingY $skinTabPaddingX;
-
- -webkit-border-top-left-radius: $skinBorderRadius;
- -webkit-border-top-right-radius: $skinBorderRadius;
- -moz-border-radius-topleft: $skinBorderRadius;
- -moz-border-radius-topright: $skinBorderRadius;
- border-top-left-radius: $skinBorderRadius;
- border-top-right-radius: $skinBorderRadius;
-
- -webkit-border-bottom-left-radius: 0;
- -moz-border-radius-bottomleft: 0;
- border-bottom-left-radius: 0;
-
- -webkit-border-bottom-right-radius: 0;
- -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: 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: 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 {
- background-color: $skinMainFirstColor;
- border-bottom: 1px solid lighten( $skinMainFirstColor, 15% )!important;
- padding-top: $skinHeaderPaddingTop;
- padding-bottom: $skinHeaderPaddingBottom;
- border: none;
- text-shadow: none;
- background-image: none;
-
- // A wide index table scrolls the WHOLE page sideways. Pin the header on
- // screen at the viewport width so it doesn't stretch to the table's width
- // (which spread the menu out under the utility nav and overlapped it); it
- // still grows to fit a wrapped menu, ActiveAdmin's natural layout.
- @include viewport-pinned-bar;
- h1.site_title {
- color: $skinMainSecondColor;
- font-size: 16px;
- img {
- max-height: $skinHeaderLogoMaxHeight;
- }
- }
- a, a:link {
- color: #ffffff;
- }
- ul.tabs > li > a {
- // On the anchor, not the li, so the dropdown subtree does not inherit it.
- font-size: $skinMenuFontSize;
- @include rounded($skinBorderRadius);
- padding: 6px 10px 6px 10px;
- }
- ul.tabs > li.current > a, ul.tabs > li:hover > a {
- 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 {
- // Sits at the top-right of the header; the header itself is pinned on screen
- // (sticky, see #header above) so the clock / theme toggle / logout stay
- // reachable while a wide table scrolls the page sideways.
- z-index: 2;
- white-space: nowrap;
-
- > li > a { padding: 0 5px; }
- > li:hover > a, > li.current > a { background-color: transparent; }
- > li#servertime:hover > a { color: #ffffff; }
-
- // Server clock (server_clock.js), rendered as two compact lines like
- // yeti-web: time + timezone (larger) over year/month/day (smaller). A
- // right-aligned flex column sized to the nav band (~24px) so both lines
- // fit the header height unchanged. Monospace + tabular figures keep the
- // ticking time a constant width, so the adjacent logout doesn't jitter.
- > li#servertime > span {
- display: inline-flex;
- flex-direction: column;
- justify-content: center;
- align-items: flex-end;
- vertical-align: middle;
- height: 24px;
- font-family: 'SFMono-Regular', Menlo, Consolas, 'Liberation Mono', monospace;
- font-variant-numeric: tabular-nums;
- }
- > li#servertime .clock-time { font-size: 12px; line-height: 1.1; }
- > li#servertime .clock-date { font-size: 9px; line-height: 1.1; }
-
- // 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. inline-flex, not flex: ActiveAdmin
- // lays the utility nav out as `li { display: inline }`, and a block-level
- // item breaks the row, dropping the items after it onto new lines.
- > li#theme_toggle { display: inline-flex; align-items: center; vertical-align: middle; }
- > 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;
- }
-
- // Logout as an icon ($logout-icon), to match the toggle beside it.
- // font-size:0 hides the "Logout" text visually; the accessible name is
- // the aria-label the gem's Ruby puts on the anchor, because zero-sized
- // text is pruned from the accessibility tree by some screen readers
- // (the toggle gets its aria-label from theme_toggle.js the same way).
- // The masked glyph follows the bar's text colour. The real logout
- // link/method from add_logout_button_to_menu is untouched.
- // Scoped to a.wigu-logout — the class the gem's Ruby puts on the item
- // it adds — so a consumer-rendered logout keeps its own label instead
- // of collapsing to an unlabeled glyph.
- > li#logout:has(> a.wigu-logout) { display: inline-flex; align-items: center; vertical-align: middle; }
- > li#logout > a.wigu-logout {
- display: flex;
- align-items: center;
- font-size: 0;
- opacity: 0.75;
-
- &:before {
- content: "";
- display: block;
- width: 16px;
- height: 16px;
- background-color: currentColor;
- -webkit-mask: #{$logout-icon} center / contain no-repeat;
- mask: #{$logout-icon} center / contain no-repeat;
- }
- &:hover, &:focus { opacity: 1; }
- }
- }
- ul.tabs {
- li.has_nested {
- // `> a` only: leaf items need no room for the submenu arrow.
- > a {
- padding-right: 20px;
- }
- ul {
- background-color: $skinMenuPanelColor;
- @include rounded-all(0, $skinBorderRadius, $skinBorderRadius, $skinBorderRadius);
- // Size to content instead of the base's max-width: calc(100% + 20px),
- // which wraps long labels onto a second line — but keep a ceiling, or
- // a long label pushes the panel out of the viewport where the :hover
- // chain cannot follow it. Past the ceiling, labels wrap as before.
- // Known limit: a depth-2 flyout under a right-edge tab can still leave
- // a 1280px viewport — it sits at left:100% of an already-offset panel,
- // and CSS cannot see the distance to the viewport edge. ActiveAdmin's
- // own 175px panels already land within 5px of the edge there, so any
- // widening inherits this. A full fix needs JS that flips the flyout to
- // `right:100%`, or CSS anchor positioning once it is baseline.
- width: max-content;
- max-width: $skinMenuPanelMaxWidth;
- overflow-wrap: break-word;
- li > a {
- padding-left: 15px;
- color: $skinMenuTextColor;
- line-height: $skinMenuItemLineHeight;
- padding-top: $skinMenuItemPaddingY;
- padding-bottom: $skinMenuItemPaddingY;
- }
- 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: '';
- display: block;
- position: absolute;
- top: 50%;
- left: 4px;
- // currentColor, so the marker follows $skinMenuTextColor instead of
- // a fixed-palette PNG that only reads against the default panel.
- border: 3px solid transparent;
- border-left-color: currentColor;
- margin-top: -3px;
- }
- }
- }
- // `> li.has_nested` + `> a` so the "bridge" border is added only to a
- // hovered TOP-LEVEL menu item (bridging its pill to the dropdown). Without
- // the `>`, nested has_nested items (e.g. System > Components) also got the
- // border, which pushed the next dropdown item down when their submenu
- // opened. 5px to match the base's `ul { margin-top: 5px }` gap, and square
- // off the corners that face the panel so the header does not show through.
- > li.has_nested:hover {
- > a {
- @include rounded-top($skinBorderRadius);
- border-bottom: 5px solid $skinMenuPanelColor;
- }
- }
- }
- }
- #title_bar {
- background-image: none;
- border: none;
- box-shadow: none;
- background-color: $skinTitleBarColor;
- border: none;
- border-bottom: $skinTitleBarBorderWidth solid $skinTitleBarBorderColor;
- // Pin the whole bar to the viewport like #header, so the title AND the action
- // buttons stay visible while a wide index table scrolls the page sideways.
- @include viewport-pinned-bar;
- // Lay the title (left) and action items (right) on one compact row. Without
- // this, resetting #titlebar_right to static (below) stacks it under the
- // title and the bar grows tall. min-height, NOT height: with a fixed height
- // the bar cannot contain its content — action items that wrap on a narrow
- // window would overflow it and paint over the page — while min-height still
- // makes breadcrumb (detail) pages match plain index pages. Drop the base
- // clearfix — in a flex container it becomes a stray third item that
- // space-between strands right.
- display: flex;
- align-items: center;
- justify-content: space-between;
- min-height: $skinTitleBarHeight;
- &::after { display: none; }
- #titlebar_left {
- display: flex;
- align-items: center;
- gap: 6px;
- height: auto;
- // Let the title shrink below its min-content width: flex items refuse to
- // otherwise, and a long nowrap record title would push the action items
- // out of the pinned bar — unreachable past the viewport on wide pages.
- min-width: 0;
- span.breadcrumb {
- font-size: 12px;
- text-shadow: none;
- margin-bottom: 0;
- a, span {
- color: lighten( $skinMainFirstColor, 30% )!important;
- }
- }
- // Own a sensible title size so consumer apps don't each shrink AA's large
- // default (which towered over the breadcrumb). margin:0 + the flex parent
- // centre it on the breadcrumb's line; nowrap + ellipsis keep a long
- // record title (show pages) on one line without pushing the action
- // items out of the bar — min-width: 0 lets it actually shrink.
- h2 {
- color: #ffffff;
- text-shadow: none;
- font-size: 16px;
- margin: 0;
- min-width: 0;
- // Screen only: AA registers the stylesheet with media "all", and paper
- // has no horizontal scroll — a nowrap/ellipsis title would print
- // truncated at the page edge where it used to wrap.
- @media screen {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- }
- }
- #titlebar_right {
- // Horizontal pinning is handled by #title_bar (sticky + viewport width),
- // so the action items just sit at the right of the bar. Reset any
- // consumer `position: absolute/sticky` (each app used to pin the buttons
- // itself via local stylesheet overrides).
- position: static;
- // Shrink to the action-items height (some apps/base leave it ~50px, taller
- // than the 37px bar, so centering pushed it over the top edge).
- height: auto;
- // Space multiple action items apart (e.g. a Menu dropdown next to a New
- // button); the base leaves them flush on this side.
- .action_items > .action_item:not(:last-child):not(:empty) { margin-right: 10px; }
- // The form selectors cover button_to action items: Rails renders them as
- // form > input[type=submit] (or button), which the anchor selectors
- // miss — consumer apps were re-hardcoding the 6px/10px padding to
- // compensate, desyncing from the $skin* knobs on every change.
- // `> 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.
- // A button_to action item renders as a block