diff --git a/README.md b/README.md
index 6bad8d1..67806d0 100644
--- a/README.md
+++ b/README.md
@@ -382,6 +382,8 @@ Three things changed shape and are worth knowing if you already set variables:
| `$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
diff --git a/app/assets/stylesheets/wigu/active_admin_theme.scss b/app/assets/stylesheets/wigu/active_admin_theme.scss
index 7928648..7d1984f 100644
--- a/app/assets/stylesheets/wigu/active_admin_theme.scss
+++ b/app/assets/stylesheets/wigu/active_admin_theme.scss
@@ -123,6 +123,11 @@ $skinLinkColorDark: #7cc0ec!default;
// 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;
@@ -186,6 +191,8 @@ $skinStatusTagErrorColor: #ce483b!default; // error / errored / red
--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;
@@ -217,6 +224,8 @@ $skinStatusTagErrorColor: #ce483b!default; // error / errored / red
--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
@@ -337,6 +346,10 @@ $logout-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'
skinLinkColorDark: $skinLinkColorDark,
skinDeleteLinkColor: $skinDeleteLinkColor,
skinDeleteLinkColorDark: $skinDeleteLinkColorDark,
+ skinErrorTextColor: $skinErrorTextColor,
+ skinErrorTextColorDark: $skinErrorTextColorDark,
+ skinErrorBgColor: $skinErrorBgColor,
+ skinErrorBgColorDark: $skinErrorBgColorDark,
skinTabInactiveColor: $skinTabInactiveColor,
skinTabInactiveColorDark: $skinTabInactiveColorDark,
skinActiveTabTextColor: $skinActiveTabTextColor,
@@ -1242,8 +1255,16 @@ body.active_admin {
form fieldset > ol > li fieldset.fragments ol {
margin-left: 20%;
}
-form fieldset ol > li.has_many_container h3 {
- color: #8494a8;
+form .has_many_container h3 {
+ color: var(--aa-text-muted);
+}
+form fieldset > ol > li p.inline-errors,
+form fieldset > ol > li ul.errors,
+form ul.errors {
+ color: var(--aa-error);
+}
+form ul.errors {
+ background: var(--aa-error-bg);
}
form input[type=submit], form input[type=button], form button, a.button, a:link.button, a:visited.button {
background-image: none;
diff --git a/docs/swatches/3a2224.svg b/docs/swatches/3a2224.svg
new file mode 100644
index 0000000..bc35653
--- /dev/null
+++ b/docs/swatches/3a2224.svg
@@ -0,0 +1 @@
+
diff --git a/docs/swatches/932419.svg b/docs/swatches/932419.svg
new file mode 100644
index 0000000..84b4577
--- /dev/null
+++ b/docs/swatches/932419.svg
@@ -0,0 +1 @@
+
diff --git a/docs/swatches/fae6e4.svg b/docs/swatches/fae6e4.svg
new file mode 100644
index 0000000..ad19344
--- /dev/null
+++ b/docs/swatches/fae6e4.svg
@@ -0,0 +1 @@
+
diff --git a/test/css_check.rb b/test/css_check.rb
index 4afdbac..c1f0f84 100644
--- a/test/css_check.rb
+++ b/test/css_check.rb
@@ -61,6 +61,8 @@ module CssCheck
"$skinStatusTagWarnColor: none" => '$skinStatusTagWarnColor: none;',
"$skinStatusTagErrorColor: none" => '$skinStatusTagErrorColor: none;',
"$skinStatusTagTextColor as a length" => '$skinStatusTagTextColor: 10px;',
+ "$skinErrorTextColor: none" => '$skinErrorTextColor: none;',
+ "$skinErrorBgColorDark: none" => '$skinErrorBgColorDark: none;',
}.freeze
# The variables table in the README is the public contract people configure
@@ -242,6 +244,37 @@ def self.status_tag_labels_are_readable
problems
end
+ # The same bar for the other text the theme paints on a fill it also owns.
+ def self.message_pairs
+ {
+ "error_on_panel" => ["$skinErrorTextColor", "$skinSurfaceColor"],
+ "error_on_panel_dark" => ["$skinErrorTextColorDark", "$skinSurfaceColorDark"],
+ "error_summary" => ["$skinErrorTextColor", "$skinErrorBgColor"],
+ "error_summary_dark" => ["$skinErrorTextColorDark", "$skinErrorBgColorDark"],
+ "has_many_heading" => ["$skinTextMutedColor", "$skinPageBgColor"],
+ "has_many_heading_dark" => ["$skinTextMutedColorDark", "$skinPageBgColorDark"]
+ }
+ end
+
+ def self.messages_are_readable
+ pairs = message_pairs
+ probe = pairs.flat_map { |name, (text, fill)| [["#{name}_text", text], ["#{name}_fill", fill]] }.map do |name, colour|
+ ".css-check-#{name} { r: red(#{colour}); g: green(#{colour}); b: blue(#{colour}); a: alpha(#{colour}); }"
+ end
+ colours = compile("", probe.join("\n"))
+ .scan(/\.css-check-(\w+)\s*\{\s*r:\s*([\d.]+);\s*g:\s*([\d.]+);\s*b:\s*([\d.]+);\s*a:\s*([\d.]+);?\s*\}/)
+ .to_h { |name, r, g, b, a| [name, { rgb: [r, g, b].map { |v| v.to_f.round }, alpha: a.to_f }] }
+
+ pairs.keys.filter_map do |name|
+ text, fill = colours.fetch("#{name}_text"), colours.fetch("#{name}_fill")
+ next "#{name}: translucent, so the ratio cannot be measured" if text[:alpha] < 1 || fill[:alpha] < 1
+
+ ratio = contrast(text[:rgb], fill[:rgb])
+ next if ratio >= LABEL_MINIMUM
+ "#{name}: #{hex(text[:rgb])} on #{hex(fill[:rgb])} is #{format("%.3f", ratio)}:1, under #{LABEL_MINIMUM}"
+ end
+ end
+
def self.hex(rgb)
format("#%02x%02x%02x", *rgb)
end
@@ -317,6 +350,7 @@ def self.run
# before the second ran — so a run could report one problem while holding
# three, and each fix revealed the next.
failures.concat(status_tag_labels_are_readable)
+ failures.concat(messages_are_readable)
failures.concat(readme_table_matches_declarations)
unless failures.empty?