Skip to content

Commit 51bab85

Browse files
committed
3.1.1: a sort marker you can read, and guards that actually guard
A review of 3.1.0 found that several of the checks added alongside it were quiet rather than wrong, and that one visual change lost information. Six fixes, each reproduced before and after. ## Unsorted and descending looked the same ActiveAdmin's sprite had three glyphs: a double arrow for sortable-but-unsorted, up for ascending, down for descending. The replacement drew the same down triangle for unsorted and descending, separated only by 0.6 against 1.0 opacity on an 8x4 shape — so a freshly loaded index showed every heading pointing down. Three shapes again, drawn as masks in currentColor like the theme switch icons, so they still follow the heading colour into dark mode. ## The type guards for the new palette were never tested Removing all five $skinStatusTag*Color entries from the colour guard left `rake css` green: the one BAD fixture passed because `none` crashes mix() inside the theme, not because the guard fired. A rejection whose message names gem internals is exactly what the guards exist to replace. The BAD check now requires the message to name the variable the fixture set, and there is a fixture per variable rather than one for five. With the guards deleted it fails five times over, naming each. ## An example that failed the contract beside it GOOD["status tags recoloured"] paired $skinStatusTagTextColor: #f5f5f5 with fills left at their defaults, giving 4.17 to 4.19:1 on four of the five — under the 4.5:1 the guard two screens away enforces. The contrast guard only ever compiles the defaults, so it could not see its own neighbour. ## Checks that hid each other The contrast and README checks sat behind `if failures.empty?`, and the first aborted before the second ran. A run could report one problem while holding three, and each fix revealed the next. They are collected and reported together now. For the same reason a single translucent fill no longer returns early and hides the other four, and the label is no longer reported as a tag named "label". ## Two lists that drift TAG_COLOURS was five names typed out by hand — the drift DECLARED_ROWS was deleted for. It is read from the stylesheet, so a sixth tag colour cannot be silently exempt. And the !default scanner had no comment awareness, so a note like `// was: $skinStatusTagOkColor: #8daa92!default;` failed the build twice over, blaming the live declaration. ## Smaller * ActiveAdmin's sprite is cleared from every anchor in a sortable heading, not only the one the theme marks. An application's own link kept the PNG and the 13px indent it needs. * $skinTableHeaderTextColorDark derives from $skinTextColorDark, matching its light twin and the sentence the README already carried. It was a literal that happens to equal it today.
1 parent 654a4c1 commit 51bab85

9 files changed

Lines changed: 107 additions & 62 deletions

File tree

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Or add manually to `package.json`:
3535

3636
```
3737
"dependencies": {
38-
"@activeadmin-plugins/active_admin_theme": "^3.1.0"
38+
"@activeadmin-plugins/active_admin_theme": "^3.1.1"
3939
}
4040
```
4141
and execute:
@@ -273,7 +273,7 @@ Three things changed shape and are worth knowing if you already set variables:
273273
| `$skinTabInactiveColor` / `$skinTabInactiveColorDark` | `#f7f9fb` / `#161a1e` | inactive tab fill |
274274
| `$skinActiveTabTextColor` / `$skinActiveTabTextColorDark` | `$skinMainSecondColor` / `#7cc0ec` | selected tab label |
275275
| `$skinInactiveTabTextColor` / `$skinInactiveTabTextColorDark` | `#5e6469` / `#b0b8c2` | inactive tab label |
276-
| `$skinTableHeaderTextColor` / `$skinTableHeaderTextColorDark` | `$skinTextColor` / `#dde2e8` | index-table column header text; the body text colour, so headings read as strongly as the rows |
276+
| `$skinTableHeaderTextColor` / `$skinTableHeaderTextColorDark` | `$skinTextColor` / `$skinTextColorDark` | index-table column header text; the body text colour, so headings read as strongly as the rows |
277277
| `$skinStatusTagTextColor` | `#ffffff` | label inside a filled status tag; `empty` / `unknown` / `none` have no fill and keep `$skinTextMutedColor` |
278278
| `$skinStatusTagNeutralColor` | `#707681` | unclassified tags: `No`, protocol tags |
279279
| `$skinStatusTagOkColor` | `#5e7e63` | `ok` `published` `complete` `completed` `green` `yes` |

‎app/assets/stylesheets/wigu/active_admin_theme.scss‎

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ $skinInactiveTabTextColor: #5e6469!default;
130130
$skinInactiveTabTextColorDark: #b0b8c2!default;
131131
// Index-table column header text, one colour for sortable and plain headers.
132132
$skinTableHeaderTextColor: $skinTextColor!default;
133-
$skinTableHeaderTextColorDark: #dde2e8!default;
133+
$skinTableHeaderTextColorDark: $skinTextColorDark!default;
134134

135135
// Status tags. The label is the same on every filled tag in both modes. The
136136
// fills below are dark enough to carry a white one: every one of the five is
@@ -233,6 +233,15 @@ html[data-theme="dark"] { @include aa-dark-palette; }
233233
html[data-theme="dark"] { @content; }
234234
}
235235

236+
// Маркеры сортировки. Три разные формы, а не одна с разной прозрачностью:
237+
// у спрайта ActiveAdmin «не отсортировано» — двойная стрелка, и если заменить
238+
// её тем же треугольником, что у «по убыванию», состояния становятся
239+
// неразличимы на свежей странице. Маской, как иконки переключателя, — чтобы
240+
// красились currentColor и шли за цветом заголовка в обоих режимах.
241+
$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;
242+
$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;
243+
$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;
244+
236245
// Иконки переключателя тем. Инлайном, потому что гем не возит картинок, и
237246
// маской, а не цветным SVG: маска красится currentColor и сама идёт за
238247
// $skinMenuTextColor. Половинка круга — auto, солнце — light, месяц — dark.
@@ -905,27 +914,27 @@ body.active_admin {
905914
// link keeps its full width so the whole cell stays clickable, and the
906915
// marker takes currentColor — the stock sprite is a fixed grey PNG that
907916
// cannot follow the text into dark mode.
908-
// `a[href*="order="]`, not every anchor in the header: an application can
909-
// put its own link in there — yeti-web adds a persistent-sort toggle — and
910-
// it would otherwise get a sort marker of its own. ActiveAdmin's heading
911-
// link always carries the order parameter.
912-
th.sortable > a[href*="order="] {
917+
// The sprite goes from every anchor in a sortable heading, because
918+
// ActiveAdmin sets it on every one — an application link in there would
919+
// otherwise keep the low-res PNG and the 13px indent it needs.
920+
th.sortable a {
913921
padding-left: 0;
914922
background-image: none;
923+
}
924+
// The marker, though, only on ActiveAdmin's own heading link, which always
925+
// carries the order parameter: an application's link is its own business.
926+
th.sortable > a[href*="order="] {
915927

916928
&:after {
917929
content: "";
918930
display: inline-block;
931+
width: 9px;
932+
height: 12px;
919933
margin-left: 6px;
920934
vertical-align: middle;
921-
// The unused side has no width rather than a transparent one, so the
922-
// box is exactly as tall as the triangle in it. Keeping all four sides
923-
// and nudging with a margin instead puts the two states at different
924-
// heights, because `vertical-align: middle` centres the box and the
925-
// visible half then sits off-centre within it.
926-
border: 4px solid transparent;
927-
border-bottom-width: 0;
928-
border-top-color: currentColor;
935+
background-color: currentColor;
936+
-webkit-mask: #{$sort-icon-none} center / contain no-repeat;
937+
mask: #{$sort-icon-none} center / contain no-repeat;
929938
// 0.6, not lower: the marker is the only thing separating a sortable
930939
// heading from a plain one, so WCAG 1.4.11 asks 3:1 of it. Against the
931940
// header fill it gives 3.40 light and 4.22 dark; at 0.4 it was 2.13
@@ -934,13 +943,15 @@ body.active_admin {
934943
}
935944
}
936945
th.sorted-asc > a[href*="order="]:after {
937-
border-top-width: 0;
938-
border-bottom-width: 4px;
939-
border-top-color: transparent;
940-
border-bottom-color: currentColor;
946+
-webkit-mask-image: $sort-icon-asc;
947+
mask-image: $sort-icon-asc;
948+
opacity: 1;
949+
}
950+
th.sorted-desc > a[href*="order="]:after {
951+
-webkit-mask-image: $sort-icon-desc;
952+
mask-image: $sort-icon-desc;
941953
opacity: 1;
942954
}
943-
th.sorted-desc > a[href*="order="]:after { opacity: 1; }
944955
// Right edge = a single 1px line on the last-column cells (header th, body
945956
// td, footer cells) coloured like the table border, since the table itself
946957
// no longer draws a right border.

‎img/dark.png‎

-109 Bytes
Loading

‎img/inputs.png‎

0 Bytes
Loading

‎img/light.png‎

-1.25 KB
Loading

‎img/switch.png‎

-1.56 KB
Loading

‎lib/active_admin_theme/version.rb‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module ActiveAdminTheme
2-
VERSION = "3.1.0"
2+
VERSION = "3.1.1"
33
end

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@activeadmin-plugins/active_admin_theme",
3-
"version": "3.1.0",
3+
"version": "3.1.1",
44
"description": "Flat design for ActiveAdmin",
55
"main": "src/active_admin_theme.scss",
66
"author": "Igor Fedoronchuk <igor.f@didww.com>",

‎test/css_check.rb‎

Lines changed: 73 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,14 @@ module CssCheck
2929
"black status tag labels" => '$skinStatusTagTextColor: #000000;',
3030
"repainted palette" => '$skinPageBgColor: #fafafa; $skinSurfaceColor: #ffffff;
3131
$skinTextColor: #202020; $skinLinkColor: #0b5;',
32-
"status tags recoloured" => '$skinStatusTagOkColor: #1f7a3a; $skinStatusTagTextColor: #f5f5f5;',
32+
# Every colour here must clear LABEL_MINIMUM against the label, because the
33+
# contrast guard measures the shipped palette: an example that fails the
34+
# contract documents the wrong thing.
35+
"status tags recoloured" => '$skinStatusTagOkColor: #1f7a3a;
36+
$skinStatusTagNeutralColor: #55595f;
37+
$skinStatusTagNoticeColor: #2f62b4;
38+
$skinStatusTagWarnColor: #875c12;
39+
$skinStatusTagErrorColor: #b03a2e;',
3340
}.freeze
3441

3542
# Wrong-typed overrides. All of these are legal SassScript, so without the
@@ -49,14 +56,19 @@ module CssCheck
4956
"$skinPanelHeaderColor as a length" => '$skinPanelHeaderColor: 10px;',
5057
"$skinStatusTagTextColor: none" => '$skinStatusTagTextColor: none;',
5158
"$skinStatusTagOkColor: none" => '$skinStatusTagOkColor: none;',
59+
"$skinStatusTagNeutralColor: none" => '$skinStatusTagNeutralColor: none;',
60+
"$skinStatusTagNoticeColor: none" => '$skinStatusTagNoticeColor: none;',
61+
"$skinStatusTagWarnColor: none" => '$skinStatusTagWarnColor: none;',
62+
"$skinStatusTagErrorColor: none" => '$skinStatusTagErrorColor: none;',
63+
"$skinStatusTagTextColor as a length" => '$skinStatusTagTextColor: 10px;',
5264
}.freeze
5365

5466
# The variables table in the README is the public contract people configure
5567
# against, and it had drifted from the declarations in 30 of 52 rows after the
5668
# defaults moved to yeti-web's configuration. Nothing noticed, because nothing
5769
# was comparing them.
5870
def self.readme_table_matches_declarations
59-
scss = File.read(File.join(STYLESHEETS, "wigu/active_admin_theme.scss"))
71+
scss = strip_comments(File.read(File.join(STYLESHEETS, "wigu/active_admin_theme.scss")))
6072
declared = {}
6173
duplicates = []
6274
scss.scan(/(\$skin[A-Za-z0-9]+)\s*:\s*(.+?)!default/) do |name, value|
@@ -156,16 +168,29 @@ def self.contrast(one, two)
156168
# every one; sassc normalises most of them to hex but emits names as names, so
157169
# a regex over the stylesheet silently skipped `darkseagreen` and crashed on a
158170
# four-digit hex. Asking Sass for the channels removes the question.
159-
TAG_COLOURS = {
160-
"neutral" => "$skinStatusTagNeutralColor",
161-
"ok" => "$skinStatusTagOkColor",
162-
"notice" => "$skinStatusTagNoticeColor",
163-
"warn" => "$skinStatusTagWarnColor",
164-
"error" => "$skinStatusTagErrorColor",
165-
}.freeze
171+
# Read from the stylesheet, not typed out here. A hand-kept list is the same
172+
# drift this file removed when DECLARED_ROWS went: add a sixth tag colour and
173+
# it would be silently exempt from the contrast check for ever.
174+
# Sass ignores a commented-out declaration; this file used to count one, and
175+
# with the duplicate and mismatch checks in place that turned a note like
176+
# `// was: $skinStatusTagOkColor: #8daa92!default;` into a red build blaming
177+
# the live declaration.
178+
def self.strip_comments(scss)
179+
scss.gsub(%r{/\*.*?\*/}m, "").gsub(%r{//[^\n]*}, "")
180+
end
181+
182+
def self.tag_colours
183+
@tag_colours ||= begin
184+
scss = strip_comments(File.read(File.join(STYLESHEETS, "wigu/active_admin_theme.scss")))
185+
names = scss.scan(/\$skinStatusTag([A-Za-z0-9]+)Color\s*:[^;]*!default/).flatten
186+
names.reject! { |name| name == "Text" }
187+
raise "css_check: no $skinStatusTag*Color declarations found" if names.empty?
188+
names.uniq.to_h { |name| [name.downcase, "$skinStatusTag#{name}Color"] }
189+
end
190+
end
166191

167192
def self.status_tag_palette
168-
probe = TAG_COLOURS.merge("label" => "$skinStatusTagTextColor").map do |name, variable|
193+
probe = tag_colours.merge("label" => "$skinStatusTagTextColor").map do |name, variable|
169194
".css-check-#{name} { r: red(#{variable}); g: green(#{variable}); " \
170195
"b: blue(#{variable}); a: alpha(#{variable}); }"
171196
end
@@ -177,7 +202,7 @@ def self.status_tag_palette
177202
found = channels.to_h do |name, r, g, b, a|
178203
[name, { rgb: [r, g, b].map { |v| v.to_f.round }, alpha: a.to_f }]
179204
end
180-
missing = (TAG_COLOURS.keys + ["label"]) - found.keys
205+
missing = (tag_colours.keys + ["label"]) - found.keys
181206
raise "css_check: the status tag probe returned nothing for #{missing.join(", ")}" unless missing.empty?
182207
found
183208
end
@@ -191,20 +216,26 @@ def self.status_tag_palette
191216
def self.status_tag_labels_are_readable
192217
palette = status_tag_palette
193218
label = palette.fetch("label")
194-
translucent = palette.select { |_, colour| colour[:alpha] < 1 }.keys
195-
unless translucent.empty?
196-
return translucent.map do |name|
197-
"status tag #{name}: translucent, so the label ratio cannot be measured"
198-
end
199-
end
219+
problems = []
200220

201-
TAG_COLOURS.keys.filter_map do |name|
202-
fill = palette.fetch(name)[:rgb]
203-
ratio = contrast(fill, label[:rgb])
221+
# The label is not a tag, and reporting it as one sent a reader looking for
222+
# a `label` status class that does not exist.
223+
problems << "$skinStatusTagTextColor is translucent, so no tag ratio can be measured" if label[:alpha] < 1
224+
225+
tag_colours.each_key do |name|
226+
fill = palette.fetch(name)
227+
# Reported, not skipped, and without abandoning the other four: a single
228+
# translucent fill used to return early and hide every failure behind it.
229+
if fill[:alpha] < 1 || label[:alpha] < 1
230+
problems << "status tag #{name}: translucent, so the label ratio cannot be measured"
231+
next
232+
end
233+
ratio = contrast(fill[:rgb], label[:rgb])
204234
next if ratio >= LABEL_MINIMUM
205-
"status tag #{name}: label #{hex(label[:rgb])} on #{hex(fill)} is " \
206-
"#{format("%.2f", ratio)}:1, under #{LABEL_MINIMUM}"
235+
problems << "status tag #{name}: label #{hex(label[:rgb])} on #{hex(fill[:rgb])} is " \
236+
"#{format("%.3f", ratio)}:1, under #{LABEL_MINIMUM}"
207237
end
238+
problems
208239
end
209240

210241
def self.hex(rgb)
@@ -239,8 +270,16 @@ def self.run
239270
BAD.each do |name, overrides|
240271
compile(overrides)
241272
failures << "#{name}: should be rejected with @error, but compiled silently"
242-
rescue SassC::SyntaxError
243-
# expected — the theme's type guards caught it
273+
rescue SassC::SyntaxError => e
274+
# Rejected is not enough: the point of the guards is that the message
275+
# names the variable the host set. Without this, a fixture passes when
276+
# the wrong value merely crashes something downstream — `none` reaching
277+
# mix() inside the theme reads as a rejection while naming gem internals,
278+
# and the guard it was written to prove can be deleted unnoticed.
279+
variable = name[/\$skin[A-Za-z0-9]+/]
280+
next if variable.nil? || e.message.include?(variable)
281+
failures << "#{name}: rejected, but the message does not name #{variable} — " \
282+
"#{e.message.lines.first.to_s.strip}"
244283
end
245284

246285
# The header menu's text colours must follow the variables. A hard-coded
@@ -269,25 +308,20 @@ def self.run
269308
"#{blocky.map { |rule| rule[/\A[^{]*/].strip }.join(", ")}"
270309
end
271310

272-
if failures.empty?
273-
unreadable = status_tag_labels_are_readable
274-
unless unreadable.empty?
275-
unreadable.each { |line| warn "css_check: #{line}" }
276-
abort "css_check: #{unreadable.size} status tag(s) fail the label contrast minimum"
277-
end
311+
# One list, reported together. Behind `if failures.empty?` these two were
312+
# invisible whenever anything else failed, and the first of them aborted
313+
# before the second ran — so a run could report one problem while holding
314+
# three, and each fix revealed the next.
315+
failures.concat(status_tag_labels_are_readable)
316+
failures.concat(readme_table_matches_declarations)
278317

279-
drift = readme_table_matches_declarations
280-
unless drift.empty?
281-
drift.each { |line| warn "css_check: #{line}" }
282-
abort "css_check: the README variables table is out of sync in #{drift.size} place(s)"
283-
end
284-
285-
puts "css_check: #{GOOD.size} overrides compile clean, #{BAD.size} bad ones rejected, " \
286-
"README table matches #{compared_declarations} declarations"
287-
else
318+
unless failures.empty?
288319
failures.each { |failure| warn "css_check: #{failure}" }
289320
abort "css_check: #{failures.size} problem(s)"
290321
end
322+
323+
puts "css_check: #{GOOD.size} overrides compile clean, #{BAD.size} bad ones rejected, " \
324+
"README table matches #{compared_declarations} declarations"
291325
end
292326
end
293327

0 commit comments

Comments
 (0)