]> git.draconx.ca Git - homepage.git/commitdiff
Automatically adjust colour scheme for "dark mode".
authorNick Bowler <nbowler@draconx.ca>
Tue, 10 May 2022 03:35:57 +0000 (23:35 -0400)
committerNick Bowler <nbowler@draconx.ca>
Wed, 11 May 2022 00:27:54 +0000 (20:27 -0400)
Playing around a bit, let's try to respect "dark" mode in firefox and
support automatically switching colour scheme accordingly.

We convert all colour references in the stylesheet to new SASS mixins
that will generate rules to adapt to the correct colour scheme.  The
selection is done using CSS variables that should gracefully degrade
to the original (light) colours.

Rules
content/style.scss
content/weblog/responsive-tables.md
layouts/default.xml
lib/colourmap.scss [new file with mode: 0644]
lib/scss-var.rb

diff --git a/Rules b/Rules
index 365e83a6fcb3d31fa7c07bcb282de5896e837fcf..b8538d86ea359e6e5246f6b01bc131c8b02eed29 100644 (file)
--- a/Rules
+++ b/Rules
@@ -210,7 +210,7 @@ compile '/images/*.jpg', rep: :info do
 end
 
 compile '/**/*.scss' do
-    filter :sass, syntax: :scss
+    filter :sass, syntax: :scss, load_paths: ["."]
     filter :css_source, uribase: \
         "https://git.draconx.ca/gitweb/homepage.git/blob/" +
         @item[:gitrev] + ":"
index d53717d9d0f692a02d356d0cb5c27574b981d1a8..5f2ef9686cff45de2ac8899ff196ddd7aa5ee193 100644 (file)
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-// colour definitions
-$backgroundcolour:  #ffffff;
-$foregroundcolour:  #000000;
+@import "lib/colourmap.scss";
 
-$linkdefaultcolour: #0000cd;
-$linkactivecolour:  #ff0000;
-$linkvisitedcolour: #800080;
+@include defcolours
+    ( $background:  #ffffff #000000
+    , $foreground:  #000000 #ffffff
 
-$ruledefaultcolour: #d3d3d3;
-$rulestrongcolour:  #696969;
+    , $linkdefault: #0000cd #a3aaff
+    , $linkactive:  #ff0000
+    , $linkvisited: #800080 #e493f7
+    , $focusring:   #628cb2
 
-$annotationcolour:  #708090;
+    , $annotation:  #708090
+    , $tableshade:  #f5f5f5 #101010
 
-$tableshadecolour:  #f5f5f5;
-
-$focusringcolour: #628cb2;
+    , $ruledefault: #d3d3d3 #494949
+    , $rulestrong:  #696969 #939393
+    );
 
 @mixin header_size($maxwidth, $fontsize) {
     font-size: $fontsize;
@@ -40,19 +41,28 @@ $focusringcolour: #628cb2;
 }
 
 body {
-    background-color: $backgroundcolour;
-    color: $foregroundcolour;
     font-family: sans-serif;
     margin: 1em;
+
+    @include usecolours
+        ( $background-color: background
+        , $color: foreground
+        );
 }
 
-a:link { color: $linkdefaultcolour; border-color: $linkdefaultcolour; }
-a:visited { color: $linkvisitedcolour; border-color: $linkvisitedcolour; }
-a:active { color: $linkactivecolour; border-color: $linkactivecolour; }
+a:link {
+    @include usecolours($color: linkdefault, $border-color: linkdefault);
+}
+a:visited {
+    @include usecolours($color: linkvisited, $border-color: linkvisited);
+}
+a:active {
+    @include usecolours($color: linkactive, $border-color: linkactive);
+}
 
 @supports (outline-style: auto) {
     a:link { border-width: 0; }
-    a:focus { outline: auto $focusringcolour; }
+    a:focus { @include usecolour(outline, focusring, auto); }
     li, td, dt { &>a:link { border: solid 1px transparent; } }
 }
 
@@ -88,7 +98,7 @@ p.img {
     }
 
     small {
-        color: $foregroundcolour;
+        @include usecolours($color: foreground);
         text-align: justify;
         @media (max-width: 24em) { text-align: left; }
         padding: 0.5ex;
@@ -123,7 +133,7 @@ hr {
     clear: both;
     margin: 0.5em 0;
     border: 0;
-    border-top: 1px solid $ruledefaultcolour;
+    @include usecolour(border-top, ruledefault, 1px solid);
 }
 
 kbd {
@@ -141,16 +151,17 @@ kbd {
 }
 
 .permalink {
+    @include usecolours($color: annotation);
     font-size: small;
-    color: $annotationcolour;
 
     a:link, a:visited { color: inherit; }
+    a:active { @include usecolours($color: linkactive); }
     @media not screen { visibility: hidden; }
 }
 
 // General table styles.
 table {
-    border: 1px solid $ruledefaultcolour;
+    @include usecolour(border-top, ruledefault, 1px solid);
     border-collapse: collapse;
     width: 100%;
 }
@@ -171,18 +182,20 @@ td, th {
     margin: 0;
 }
 
-thead>tr, tbody>tr { border: solid $ruledefaultcolour; }
-th, thead>tr { border-bottom: 1px solid $rulestrongcolour; }
-tbody+tbody { border-bottom: 1px solid $ruledefaultcolour; }
+thead>tr, tbody>tr { @include usecolour(border, ruledefault, solid); }
+th, thead>tr { @include usecolour(border-bottom, rulestrong, 1px solid); }
+tbody+tbody { @include usecolour(border-bottom, ruledefault, 1px solid); }
 *>table, *>th { border: none; }
 thead>tr { border-width: 1px; }
 tbody>tr { border-width: 0 1px; }
 
-td + td { box-shadow: -1px 0 $backgroundcolour; }
+td + td {
+    @include usecolour(box-shadow, background, -1px 0);
+}
 
 tbody>tr {
-    &:nth-of-type(even) { background-color: $tableshadecolour; }
-    &:last-child { border-bottom: solid 1px $ruledefaultcolour; }
+    &:nth-of-type(even) { @include usecolours($background-color: tableshade); }
+    &:last-child { @include usecolour(border-bottom, ruledefault, solid 1px); }
 }
 
 // Specific table styles
@@ -313,10 +326,10 @@ $clickynames: name, date, size;
     #{"input.clicky-#{$col+$focuslabel}"}>span:first-child
     , #{"input.clicky-#{$col}-rev#{$focuslabel}"} .svg
     {
-        border-color: $foregroundcolour;
+        @include usecolours($border-color: foreground);
         @at-root { @supports (outline-style: auto) { & {
+            @include usecolour(outline, focusring, auto);
             border-color: transparent;
-            outline: auto $focusringcolour;
         }}}
     }
 }
@@ -340,12 +353,12 @@ thead.clicky label {
         padding-right: 2px;
     }
 
-    &:active { color: $linkactivecolour; }
+    &:active { @include usecolours($color: linkactive); }
     &:first-child:active>span, &~label:active>.svg {
-        border-color: $linkactivecolour;
+        @include usecolours($border-color: linkactive);
         @at-root { @supports (outline-style: auto) { & {
+            @include usecolour(outline, focusring, auto);
             border-color: transparent;
-            outline: auto $focusringcolour;
         }}}
     }
 
@@ -414,7 +427,7 @@ table.filelist {
     }
 }
 
-#footer p { color: $annotationcolour; }
+#footer p { @include usecolours($color: annotation); }
 #article-info p { font-style: italic; }
 
 .wbr:after { content: "\200b"; }
@@ -440,3 +453,18 @@ ul.ordered > {
     #sitetitle * { float: none; }
     #footer { padding: 0 1em; }
 }
+
+// page-specific dark mode styles
+@media (min-width: 35em) {
+    #page_weblog_responsive_tables {
+        @each $tN in t6 t7 t8 {
+            ##{$tN}>tbody>tr.#{$tN}-split {
+                @include usecolour_var_(border-bottom, ruledefault);
+
+                &:nth-of-type(odd) ~ tr:nth-of-type(odd) {
+                    @include usecolour_var_(background-color, tableshade);
+                }
+            }
+        }
+    }
+}
index 2063484f7b266bdc1d1934167a90963d10c3edf2..a668e163b60269c569b667e4687ac387bcb7b882 100644 (file)
@@ -272,12 +272,12 @@ are pretty easy to fix in the stylesheet.
     #t6>thead, #t6>tbody { display: grid; }
 
     #t6>tbody>tr.t6-split { border-bottom: 1px solid <%=
-      scss_get_var(:ruledefaultcolour) %>; }
+      scss_get_colour(:ruledefault) %>; }
     #t6>tbody>tr.t6-split:nth-of-type(odd) ~ tr:nth-of-type(even) {
       background-color: initial;
     }
     #t6>tbody>tr.t6-split:nth-of-type(odd) ~ tr:nth-of-type(odd) {
-      background-color: <%= scss_get_var(:tableshadecolour) %>;
+      background-color: <%= scss_get_colour(:tableshade) %>;
     }
   }
 }
@@ -325,12 +325,12 @@ these techniques to [table 1](#t1).
     #t7>thead, #t7>tbody { display: grid; }
 
     #t7>tbody>tr.t7-split { border-bottom: 1px solid <%=
-      scss_get_var(:ruledefaultcolour) %>; }
+      scss_get_colour(:ruledefault) %>; }
     #t7>tbody>tr.t7-split:nth-of-type(odd) ~ tr:nth-of-type(even) {
       background-color: initial;
     }
     #t7>tbody>tr.t7-split:nth-of-type(odd) ~ tr:nth-of-type(odd) {
-      background-color: <%= scss_get_var(:tableshadecolour) %>;
+      background-color: <%= scss_get_colour(:tableshade) %>;
     }
   }
 }
@@ -386,12 +386,12 @@ in a stylesheet.  We will do it with more grids.
     #t8>thead, #t8>tbody { display: grid; }
 
     #t8>tbody>tr.t8-split { border-bottom: 1px solid <%=
-      scss_get_var(:ruledefaultcolour) %>; }
+      scss_get_colour(:ruledefault) %>; }
     #t8>tbody>tr.t8-split:nth-of-type(odd) ~ tr:nth-of-type(even) {
       background-color: initial;
     }
     #t8>tbody>tr.t8-split:nth-of-type(odd) ~ tr:nth-of-type(odd) {
-      background-color: <%= scss_get_var(:tableshadecolour) %>;
+      background-color: <%= scss_get_colour(:tableshade) %>;
     }
   }
 
index 2e003aefeb975fd077edf9e3b8848782b06c07a8..11aeec1e955d0a273d856a37d8d45b67b03eb48f 100644 (file)
@@ -2,7 +2,7 @@
 <!--
   Nick's web site: Intermediate document structure.
 
-  Copyright © 2016-2020 Nick Bowler
+  Copyright © 2016-2022 Nick Bowler
 
   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
@@ -66,7 +66,9 @@ end %>
 "    <published>%Y-%m-%d</published>\n" if @item[:published]
 %>  </article>
 <% end
-%>  <html xmlns="<%= Xmlns['xhtml'] %>">
+%>  <html xmlns="<%= Xmlns['xhtml'] %>" id="<%=
+  "page" + @item.identifier.without_ext.gsub(/[^[:alnum:]]/, "_")
+%>">
 <% if !doc_header then
 %>    <h1><%= @item.fetch(:header, @item[:title]) %></h1>
 <% end %><%= doc_str
diff --git a/lib/colourmap.scss b/lib/colourmap.scss
new file mode 100644 (file)
index 0000000..62468d9
--- /dev/null
@@ -0,0 +1,118 @@
+// Nick's web site: SCSS colourmap helpers for automatic light/dark styles.
+//
+// Copyright © 2022 Nick Bowler
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+// database of colour names
+$colourmap: ();
+
+// database of combination CSS properties that should be translated to
+// colour-only properties (e.g., border-top -> border-top-color).
+$_colourpropmap:
+    ( border-top: border-top-color
+    , border-bottom: border-bottom-color
+    , border-left: border-left-color
+    , border-right: border-right-color
+    , border: border-color
+    );
+
+// Define a named set of colours.  Pass keyword arguments with each keyword
+// being a colour name and the argument is a list of one or two colours.
+//
+// When two colours are specified, the first should be for "light" backgrounds
+// and the second for "dark".
+//
+// For example:
+//
+//   @include defcolours($bg: white black, $fg: black white);
+@mixin defcolours($args...) {
+    :root {
+        @each $colour, $list in keywords($args) {
+            $colourmap: map-merge($colourmap, ($colour: $list)) !global;
+            @if length($list) > 1 {
+                #{--colour- + $colour}: nth($list, 1);
+            }
+        }
+    }
+    @media (prefers-color-scheme: dark) {
+        :root {
+            @each $colour, $list in keywords($args) {
+                @if length($list) > 1 {
+                    #{--colour- + $colour}: nth($list, 2);
+                }
+            }
+        }
+    }
+}
+
+// For the given previously-defined colour name, returns its value from
+// primary (light) colour scheme.
+//
+// The $pre and $post keyword arguments may be used to supplement the
+// result with additional tokens either before or after the colour
+// value, respectively, as might be used for combined properties
+// such as border, outline, etc.
+@function getcolour($colour, $pre: (), $post: ()) {
+    @return join(append($pre, nth(map-get($colourmap, $colour), 1)), $post);
+}
+
+@mixin usecolour_var_($prop, $colour, $pre: (), $post: ()) {
+    @if (length(map-get($colourmap, $colour)) > 1) {
+        $transprop: map-get($_colourpropmap, $prop);
+        @if $transprop {
+            #{$transprop}: var(--colour- + $colour)
+        } @else {
+            #{$prop}: join(append($pre, var(--colour- + $colour)), $post);
+        }
+    }
+}
+
+// Sets the given CSS property to the specified colour name.  The $pre
+// and $post keyword arguments may be used to supplement the property
+// value, as with the getcolour function.
+//
+// For two-value colours, the property is set using CSS variables to
+// adapt the colour based on the user's preference for a light or dark
+// background.  The rules will fall back to the static (light) colour
+// if CSS variables are not supported.
+//
+// For example:
+//
+//   @include usecolour(border, fg, $pre: solid 1px);
+@mixin usecolour($prop, $colour, $pre: (), $post: ()) {
+    #{$prop}: getcolour($colour, $pre, $post);
+    @at-root & { @include usecolour_var_($prop, $colour, $pre, $post); }
+}
+
+// Convenience helper to assign multiple colour properties at once, for
+// the common case where the $pre and $post arguments to usecolour are
+// not required.
+//
+// Takes any number of keyword arguments with the keyword being the
+// property name and the value being the colour name.
+//
+// For example:
+//
+//   @include usecolours($background-color: bg, $color: fg);
+@mixin usecolours($args...) {
+    @each $prop, $colour in keywords($args) {
+        #{$prop}: getcolour($colour);
+    }
+    @at-root & {
+        @each $prop, $colour in keywords($args) {
+            @include usecolour_var_($prop, $colour);
+        }
+    }
+}
index c6cf65464f415e750460e41c0b130bb1422112e7..b15680f6787deae166c5572b06e2a34f3368920e 100644 (file)
@@ -1,6 +1,6 @@
 # Nick's web site: Helper to retrieve global style variables in ruby.
 #
-# Copyright © 2020 Nick Bowler
+# Copyright © 2020, 2022 Nick Bowler
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -22,11 +22,9 @@ class GetSCSSGlobals < Sass::Tree::Visitors::Perform
         x = new(nil)
         x.send(:visit, root)
         result = x.instance_variable_get(:@globals)
-        if name.nil?
-            return result.freeze
-        else
-            return result[name]
-        end
+        return result.freeze if name.nil?
+
+        x.send(:rubify_result, result[name])
     end
 
     protected
@@ -40,15 +38,35 @@ class GetSCSSGlobals < Sass::Tree::Visitors::Perform
 
     def visit_variable(node)
         super
-        
+
         x = @environment.global_env.var(node.name)
-        if !x.nil?
-            @globals[node.name] = x
+        @globals[node.name] = x unless x.nil?
+    end
+
+    # Convert SASS maps and lists to ruby equivalents that are actually usable.
+    def rubify_result(val)
+        case val
+        when Sass::Script::Value::Map
+            val.to_h.each_with_object({}) do |(k, v), h|
+                h[k.to_s.to_sym] = rubify_result(v)
+            end
+        when Sass::Script::Value::List
+            val.to_a.map { |v| rubify_result(v) }
+        else
+            val
         end
     end
 end
 
 def scss_get_var(variable, item = @items["/style.scss"])
-    engine = Sass::Engine.for_file(item.raw_filename, { :syntax => :scss })
+    engine = Sass::Engine.for_file(item.raw_filename, {
+        :syntax => :scss,
+        :load_paths => ["."],
+    })
     return GetSCSSGlobals.visit(engine.to_tree, variable)
 end
+
+def scss_get_colour(colour, index = 0, item = @items["/style.scss"])
+    cmap = scss_get_var(:colourmap, item);
+    [cmap[colour]].flatten[index]
+end