Bug 1418602 - Allow theming sidebars. r=mconley draft
authorTim Nguyen <ntim.bugs@gmail.com>
Mon, 06 Aug 2018 17:46:43 +0100
changeset 830165 0270ec8c7dbb755893cae98611335a4361d78250
parent 830164 f4f56776a1122ff3df80203386c9a836f936bf6d
push id118817
push userbmo:ntim.bugs@gmail.com
push dateMon, 20 Aug 2018 09:00:50 +0000
reviewersmconley
bugs1418602
milestone63.0a1
Bug 1418602 - Allow theming sidebars. r=mconley MozReview-Commit-ID: 97zkU7raehV
browser/base/content/contentTheme.js
browser/components/places/content/bookmarksSidebar.js
browser/components/places/content/bookmarksSidebar.xul
browser/components/places/content/historySidebar.js
browser/components/places/content/historySidebar.xul
browser/modules/ThemeVariableMap.jsm
browser/themes/linux/places/places.css
browser/themes/osx/places/places.css
browser/themes/shared/places/places.inc.css
browser/themes/windows/places/places.css
toolkit/components/extensions/parent/ext-theme.js
toolkit/components/extensions/schemas/theme.json
toolkit/components/extensions/test/browser/browser.ini
toolkit/components/extensions/test/browser/browser_ext_themes_sidebars.js
toolkit/components/extensions/test/browser/head.js
--- a/browser/base/content/contentTheme.js
+++ b/browser/base/content/contentTheme.js
@@ -27,16 +27,53 @@ const inContentVariableMap = [
       const {r, g, b, a} = rgbaChannels;
       if (!_isTextColorDark(r, g, b)) {
         element.setAttribute("lwt-newtab-brighttext", "true");
       }
 
       return `rgba(${r}, ${g}, ${b}, ${a})`;
     },
   }],
+  ["--lwt-sidebar-background-color", {
+    lwtProperty: "sidebar",
+  }],
+  ["--lwt-sidebar-text-color", {
+    lwtProperty: "sidebar_text",
+    processColor(rgbaChannels, element) {
+      if (!rgbaChannels) {
+        element.removeAttribute("lwt-sidebar");
+        element.removeAttribute("lwt-sidebar-brighttext");
+        return null;
+      }
+
+      element.setAttribute("lwt-sidebar", "true");
+      const {r, g, b, a} = rgbaChannels;
+      if (!_isTextColorDark(r, g, b)) {
+        element.setAttribute("lwt-sidebar-brighttext", "true");
+      }
+
+      return `rgba(${r}, ${g}, ${b}, ${a})`;
+    },
+  }],
+  ["--lwt-sidebar-highlight-background-color", {
+    lwtProperty: "sidebar_highlight",
+  }],
+  ["--lwt-sidebar-highlight-text-color", {
+    lwtProperty: "sidebar_highlight_text",
+    processColor(rgbaChannels, element) {
+      if (!rgbaChannels) {
+        element.removeAttribute("lwt-sidebar-highlight");
+        return null;
+      }
+      element.setAttribute("lwt-sidebar-highlight", "true");
+
+      const {r, g, b, a} = rgbaChannels;
+      return `rgba(${r}, ${g}, ${b}, ${a})`;
+    },
+  }],
 ];
 
 /**
  * ContentThemeController handles theme updates sent by the frame script.
  * To be able to use ContentThemeController, you must add your page to the whitelist
  * in LightweightThemeChildListener.jsm
  */
 const ContentThemeController = {
@@ -53,17 +90,19 @@ const ContentThemeController = {
    * @param {Object} event object containing the theme update.
    */
   handleEvent({ type, detail }) {
     if (type == "LightweightTheme:Set") {
       let {data} = detail;
       if (!data) {
         data = {};
       }
-      this._setProperties(document.body, data);
+      // XUL documents don't have a body
+      const element = document.body ? document.body : document.documentElement;
+      this._setProperties(element, data);
     }
   },
 
   /**
    * Set a CSS variable to a given value
    * @param {Element} elem The element where the CSS variable should be added.
    * @param {string} variableName The CSS variable to set.
    * @param {string} value The new value of the CSS variable.
@@ -85,17 +124,17 @@ const ContentThemeController = {
     for (let [cssVarName, definition] of inContentVariableMap) {
       const {
         lwtProperty,
         processColor,
       } = definition;
       let value = themeData[lwtProperty];
 
       if (processColor) {
-        value = processColor(value, document.body);
+        value = processColor(value, elem);
       } else if (value) {
         const {r, g, b, a} = value;
         value = `rgba(${r}, ${g}, ${b}, ${a})`;
       }
 
       this._setProperty(elem, cssVarName, value);
     }
   },
--- a/browser/components/places/content/bookmarksSidebar.js
+++ b/browser/components/places/content/bookmarksSidebar.js
@@ -2,16 +2,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* Shared Places Import - change other consumers if you change this: */
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 XPCOMUtils.defineLazyModuleGetters(this, {
+  LightweightThemeChild: "resource:///actors/LightweightThemeChild.jsm",
   PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
   PlacesUIUtils: "resource:///modules/PlacesUIUtils.jsm",
   PlacesTransactions: "resource://gre/modules/PlacesTransactions.jsm",
   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
 });
 XPCOMUtils.defineLazyScriptGetter(this, "PlacesTreeView",
                                   "chrome://browser/content/places/treeView.js");
 XPCOMUtils.defineLazyScriptGetter(this, ["PlacesInsertionPoint", "PlacesController",
@@ -20,16 +21,22 @@ XPCOMUtils.defineLazyScriptGetter(this, 
 /* End Shared Places Import */
 
 function init() {
   let uidensity = window.top.document.documentElement.getAttribute("uidensity");
   if (uidensity) {
     document.documentElement.setAttribute("uidensity", uidensity);
   }
 
+  /* Listen for sidebar theme changes */
+  new LightweightThemeChild({
+    content: window,
+    chromeOuterWindowID: window.top.windowUtils.outerWindowID,
+  });
+
   document.getElementById("bookmarks-view").place =
     "place:type=" + Ci.nsINavHistoryQueryOptions.RESULTS_AS_ROOTS_QUERY;
 }
 
 function searchBookmarks(aSearchString) {
   var tree = document.getElementById("bookmarks-view");
   if (!aSearchString) {
     // eslint-disable-next-line no-self-assign
--- a/browser/components/places/content/bookmarksSidebar.xul
+++ b/browser/components/places/content/bookmarksSidebar.xul
@@ -25,16 +25,18 @@
       aria-label="&bookmarksButton.label;">
 
   <script type="application/javascript"
           src="chrome://browser/content/places/bookmarksSidebar.js"/>
   <script type="application/javascript"
           src="chrome://global/content/globalOverlay.js"/>
   <script type="application/javascript"
           src="chrome://browser/content/utilityOverlay.js"/>
+  <script type="application/javascript"
+          src="chrome://browser/content/contentTheme.js"/>
 
 #include placesCommands.inc.xul
 #include ../../../../toolkit/content/editMenuCommands.inc.xul
 #include placesContextMenu.inc.xul
 #include bookmarksHistoryTooltip.inc.xul
 
   <hbox id="sidebar-search-container" align="center">
     <textbox id="search-box" flex="1" type="search"
--- a/browser/components/places/content/historySidebar.js
+++ b/browser/components/places/content/historySidebar.js
@@ -2,16 +2,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* Shared Places Import - change other consumers if you change this: */
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 XPCOMUtils.defineLazyModuleGetters(this, {
+  LightweightThemeChild: "resource:///actors/LightweightThemeChild.jsm",
   PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
   PlacesUIUtils: "resource:///modules/PlacesUIUtils.jsm",
   PlacesTransactions: "resource://gre/modules/PlacesTransactions.jsm",
   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
 });
 XPCOMUtils.defineLazyScriptGetter(this, "PlacesTreeView",
                                   "chrome://browser/content/places/treeView.js");
 XPCOMUtils.defineLazyScriptGetter(this, ["PlacesInsertionPoint", "PlacesController",
@@ -27,16 +28,22 @@ var gHistoryGrouping = "";
 var gSearching = false;
 
 function HistorySidebarInit() {
   let uidensity = window.top.document.documentElement.getAttribute("uidensity");
   if (uidensity) {
     document.documentElement.setAttribute("uidensity", uidensity);
   }
 
+  /* Listen for sidebar theme changes */
+  new LightweightThemeChild({
+    content: window,
+    chromeOuterWindowID: window.top.windowUtils.outerWindowID,
+  });
+
   gHistoryTree = document.getElementById("historyTree");
   gSearchBox = document.getElementById("search-box");
 
   gHistoryGrouping = document.getElementById("viewButton").
                               getAttribute("selectedsort");
 
   if (gHistoryGrouping == "site")
     document.getElementById("bysite").setAttribute("checked", "true");
--- a/browser/components/places/content/historySidebar.xul
+++ b/browser/components/places/content/historySidebar.xul
@@ -25,16 +25,18 @@
       aria-label="&historyButton.label;">
 
   <script type="application/javascript"
           src="chrome://browser/content/places/historySidebar.js"/>
   <script type="application/javascript"
           src="chrome://global/content/globalOverlay.js"/>
   <script type="application/javascript"
           src="chrome://browser/content/utilityOverlay.js"/>
+  <script type="application/javascript"
+          src="chrome://browser/content/contentTheme.js"/>
 
 #include ../../../../toolkit/content/editMenuCommands.inc.xul
 
 #include placesCommands.inc.xul
 
 #include ../../../../toolkit/content/editMenuKeys.inc.xul
 #ifdef XP_MACOSX
   <keyset id="editMenuKeysExtra">
--- a/browser/modules/ThemeVariableMap.jsm
+++ b/browser/modules/ThemeVariableMap.jsm
@@ -79,9 +79,13 @@ const ThemeVariableMap = [
   ["--autocomplete-popup-highlight-color", {
     lwtProperty: "popup_highlight_text"
   }],
 ];
 
 const ThemeContentPropertyList = [
   "ntp_background",
   "ntp_text",
+  "sidebar",
+  "sidebar_highlight",
+  "sidebar_highlight_text",
+  "sidebar_text",
 ];
--- a/browser/themes/linux/places/places.css
+++ b/browser/themes/linux/places/places.css
@@ -18,23 +18,16 @@
   margin: 1px 0;
   margin-inline-start: 4px;
 }
 
 #viewButton:-moz-focusring:not(:hover):not([open]) {
   outline: 1px dotted -moz-DialogText;
 }
 
-.sidebar-placesTree {
-  margin: 0;
-  color: inherit;
-  -moz-appearance: none;
-  background: transparent;
-}
-
 :root[uidensity=touch] #search-box,
 :root[uidensity=touch] .sidebar-placesTreechildren::-moz-tree-row {
   min-height: 32px;
 }
 
 .sidebar-placesTreechildren::-moz-tree-cell(leaf) ,
 .sidebar-placesTreechildren::-moz-tree-image(leaf) {
   cursor: pointer;
--- a/browser/themes/osx/places/places.css
+++ b/browser/themes/osx/places/places.css
@@ -5,16 +5,19 @@
 /* Sidebars */
 
 %include ../../shared/places/places.inc.css
 
 .sidebar-placesTree {
   margin: 0;
   /* Default font size is 11px on mac, so this is 12px */
   font-size: 1.0909rem;
+}
+
+.sidebar-panel:not([lwt-sidebar]) .sidebar-placesTree {
   -moz-appearance: -moz-mac-source-list;
   -moz-font-smoothing-background-color: -moz-mac-source-list;
 }
 
 :root[uidensity=touch] .sidebar-placesTreechildren::-moz-tree-row {
   min-height: 32px;
 }
 
--- a/browser/themes/shared/places/places.inc.css
+++ b/browser/themes/shared/places/places.inc.css
@@ -2,16 +2,61 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 .sidebar-panel {
   -moz-appearance: none;
   background-color: transparent;
 }
 
+/* Themed sidebars */
+
+.sidebar-panel[lwt-sidebar] {
+  background-color: var(--lwt-sidebar-background-color);
+  color: var(--lwt-sidebar-text-color);
+}
+
+.sidebar-panel[lwt-sidebar] .sidebar-placesTreechildren::-moz-tree-row(selected) {
+  background-color: hsla(0,0%,80%,.3);
+}
+
+.sidebar-panel[lwt-sidebar-brighttext] .sidebar-placesTreechildren::-moz-tree-row(selected) {
+  -moz-appearance: none;
+  background-color: rgba(249,249,250,.1);
+}
+
+.sidebar-panel[lwt-sidebar-brighttext] .sidebar-placesTreechildren::-moz-tree-image(selected),
+.sidebar-panel[lwt-sidebar-brighttext] .sidebar-placesTreechildren::-moz-tree-twisty(selected),
+.sidebar-panel[lwt-sidebar-brighttext] .sidebar-placesTreechildren::-moz-tree-cell-text(selected) {
+  color: var(--lwt-sidebar-text-color);
+}
+
+.sidebar-panel[lwt-sidebar-highlight] .sidebar-placesTreechildren::-moz-tree-row(selected,focus) {
+  -moz-appearance: none;
+  background-color: var(--lwt-sidebar-highlight-background-color);
+}
+
+.sidebar-panel[lwt-sidebar-highlight] .sidebar-placesTreechildren::-moz-tree-image(selected, focus),
+.sidebar-panel[lwt-sidebar-highlight] .sidebar-placesTreechildren::-moz-tree-twisty(selected, focus),
+.sidebar-panel[lwt-sidebar-highlight] .sidebar-placesTreechildren::-moz-tree-cell-text(selected, focus) {
+  color: var(--lwt-sidebar-highlight-text-color);
+}
+
+/* Sidebar tree */
+
+.sidebar-placesTree {
+  -moz-appearance: none;
+  background-color: transparent;
+  color: inherit;
+  border: 0;
+  margin: 0;
+}
+
+/* View button */
+
 #viewButton {
   -moz-appearance: none;
   border-radius: 4px;
   padding: 2px 4px;
   color: inherit;
 }
 
 #viewButton:hover {
--- a/browser/themes/windows/places/places.css
+++ b/browser/themes/windows/places/places.css
@@ -1,24 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* Sidebars */
 
 %include ../../shared/places/places.inc.css
 
-.sidebar-placesTree {
-  -moz-appearance: none;
-  background-color: transparent;
-  color: inherit;
-  border: 0;
-  margin: 0;
-}
-
 :root[uidensity=touch] #search-box,
 :root[uidensity=touch] .sidebar-placesTreechildren::-moz-tree-row {
   min-height: 32px;
 }
 
 .sidebar-placesTreechildren::-moz-tree-cell,
 .sidebar-placesTreechildren::-moz-tree-twisty {
   padding: 0 4px;
--- a/toolkit/components/extensions/parent/ext-theme.js
+++ b/toolkit/components/extensions/parent/ext-theme.js
@@ -179,16 +179,20 @@ class Theme {
         case "button_background_active":
         case "popup":
         case "popup_text":
         case "popup_border":
         case "popup_highlight":
         case "popup_highlight_text":
         case "ntp_background":
         case "ntp_text":
+        case "sidebar":
+        case "sidebar_text":
+        case "sidebar_highlight":
+        case "sidebar_highlight_text":
           this.lwtStyles[color] = cssColor;
           break;
         default:
           if (this.experiment && this.experiment.colors && color in this.experiment.colors) {
             this.lwtStyles.experimental.colors[color] = cssColor;
           } else {
             const {logger} = this.extension;
             logger.warn(`Unrecognized theme property found: colors.${color}`);
--- a/toolkit/components/extensions/schemas/theme.json
+++ b/toolkit/components/extensions/schemas/theme.json
@@ -231,16 +231,32 @@
               },
               "ntp_background": {
                 "$ref": "ThemeColor",
                 "optional": true
               },
               "ntp_text": {
                 "$ref": "ThemeColor",
                 "optional": true
+              },
+              "sidebar": {
+                "$ref": "ThemeColor",
+                "optional": true
+              },
+              "sidebar_text": {
+                "$ref": "ThemeColor",
+                "optional": true
+              },
+              "sidebar_highlight": {
+                "$ref": "ThemeColor",
+                "optional": true
+              },
+              "sidebar_highlight_text": {
+                "$ref": "ThemeColor",
+                "optional": true
               }
             },
             "additionalProperties": { "$ref": "ThemeColor" }
           },
           "icons": {
             "type": "object",
             "optional": true,
             "properties": {
--- a/toolkit/components/extensions/test/browser/browser.ini
+++ b/toolkit/components/extensions/test/browser/browser.ini
@@ -27,10 +27,11 @@ skip-if = verify
 [browser_ext_themes_toolbars.js]
 [browser_ext_themes_toolbarbutton_icons.js]
 [browser_ext_themes_toolbarbutton_colors.js]
 [browser_ext_themes_theme_transition.js]
 [browser_ext_themes_arrowpanels.js]
 [browser_ext_themes_tab_selected.js]
 [browser_ext_themes_autocomplete_popup.js]
 [browser_ext_themes_sanitization.js]
+[browser_ext_themes_sidebars.js]
 [browser_ext_themes_findbar.js]
 [browser_ext_themes_warnings.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_sidebars.js
@@ -0,0 +1,133 @@
+"use strict";
+
+// This test checks whether the sidebar color properties work.
+
+/**
+ * Test whether the selected browser has the sidebar theme applied
+ * @param {Object} theme that is applied
+ * @param {boolean} isBrightText whether the brighttext attribute should be set
+ */
+async function test_sidebar_theme(theme, isBrightText) {
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      theme,
+    },
+  });
+
+  const content = SidebarUI.browser.contentWindow;
+  const root = content.document.documentElement;
+
+  ok(!root.hasAttribute("lwt-sidebar"),
+     "Sidebar should not have lwt-sidebar attribute");
+  ok(!root.hasAttribute("lwt-sidebar-brighttext"),
+     "Sidebar should not have lwt-sidebar-brighttext attribute");
+  ok(!root.hasAttribute("lwt-sidebar-highlight"),
+     "Sidebar should not have lwt-sidebar-highlight attribute");
+
+  const rootCS = content.getComputedStyle(root);
+  const originalBackground = rootCS.backgroundColor;
+  const originalColor = rootCS.color;
+
+  const treeChildren = content.document.querySelector(".sidebar-placesTreechildren");
+  // ::-moz-tree-row(selected, focus) computed style can't be accessed, so we create a fake one.
+  const highlightCS = {
+    get backgroundColor() {
+      // Standardize to rgb like other computed style.
+      let color = rootCS.getPropertyValue("--lwt-sidebar-highlight-background-color");
+      let [r, g, b, a] = color.replace("rgba(", "").split(",").map(channel => parseInt(channel));
+      return `rgb(${r}, ${g}, ${b})`;
+    },
+
+    get color() {
+      let color = rootCS.getPropertyValue("--lwt-sidebar-highlight-text-color");
+      let [r, g, b, a] = color.replace("rgba(", "").split(",").map(channel => parseInt(channel));
+      return `rgb(${r}, ${g}, ${b})`;
+    }
+  };
+  const originalHighlightBackground = highlightCS.backgroundColor;
+  const originalHighlightColor = highlightCS.color;
+
+  await extension.startup();
+
+  Services.ppmm.sharedData.flush();
+
+  const actualBackground = hexToCSS(theme.colors.sidebar) || originalBackground;
+  const actualColor = hexToCSS(theme.colors.sidebar_text) || originalColor;
+  const actualHighlightBackground = hexToCSS(theme.colors.sidebar_highlight) || originalHighlightBackground;
+  const actualHighlightColor = hexToCSS(theme.colors.sidebar_highlight_text) || originalHighlightColor;
+  const isCustomHighlight = !!theme.colors.sidebar_highlight_text;
+  const isCustomSidebar = !!theme.colors.sidebar_text;
+
+  is(root.hasAttribute("lwt-sidebar"), isCustomSidebar,
+     `Sidebar should${!isCustomSidebar ? " not" : ""} have lwt-sidebar attribute`);
+  is(root.hasAttribute("lwt-sidebar-brighttext"), isBrightText,
+     `Sidebar should${!isBrightText ? " not" : ""} have lwt-sidebar-brighttext attribute`);
+  is(root.hasAttribute("lwt-sidebar-highlight"), isCustomHighlight,
+     `Sidebar should${!isCustomHighlight ? " not" : ""} have lwt-sidebar-highlight attribute`);
+
+  is(rootCS.backgroundColor, actualBackground, "Sidebar background should be set.");
+  is(rootCS.color, actualColor, "Sidebar text color should be set.");
+
+  is(highlightCS.backgroundColor, actualHighlightBackground,
+     "Sidebar highlight background color should be set.");
+  is(highlightCS.color, actualHighlightColor,
+     "Sidebar highlight text color should be set.");
+
+  await extension.unload();
+
+  Services.ppmm.sharedData.flush();
+
+  ok(!root.hasAttribute("lwt-sidebar"),
+     "Sidebar should not have lwt-sidebar attribute");
+  ok(!root.hasAttribute("lwt-sidebar-brighttext"),
+     "Sidebar should not have lwt-sidebar-brighttext attribute");
+  ok(!root.hasAttribute("lwt-sidebar-highlight"),
+     "Sidebar should not have lwt-sidebar-highlight attribute");
+
+  is(rootCS.backgroundColor, originalBackground,
+     "Sidebar background should be reset.");
+  is(rootCS.color, originalColor,
+     "Sidebar text color should be reset.");
+  is(highlightCS.backgroundColor, originalHighlightBackground,
+     "Sidebar highlight background color should be reset.");
+  is(highlightCS.color, originalHighlightColor,
+     "Sidebar highlight text color should be reset.");
+}
+
+add_task(async function test_support_sidebar_colors() {
+  for (let command of ["viewBookmarksSidebar", "viewHistorySidebar"]) {
+    info("Executing command: " + command);
+
+    await SidebarUI.show(command);
+
+    await test_sidebar_theme({
+      colors: {
+        sidebar: "#fafad2", // lightgoldenrodyellow
+        sidebar_text: "#2f4f4f", // darkslategrey
+      },
+    }, false);
+
+    await test_sidebar_theme({
+      colors: {
+        sidebar: "#8b4513", // saddlebrown
+        sidebar_text: "#ffa07a" // lightsalmon
+      },
+    }, true);
+
+    await test_sidebar_theme({
+      colors: {
+        sidebar: "#fffafa", // snow
+        sidebar_text: "#663399", // rebeccapurple
+        sidebar_highlight: "#7cfc00", // lawngreen
+        sidebar_highlight_text: "#ffefd5", // papayawhip
+      },
+    }, false);
+
+    await test_sidebar_theme({
+      colors: {
+        sidebar_highlight: "#a0522d", // sienna
+        sidebar_highlight_text: "#fff5ee", // seashell
+      },
+    }, false);
+  }
+});
--- a/toolkit/components/extensions/test/browser/head.js
+++ b/toolkit/components/extensions/test/browser/head.js
@@ -33,25 +33,31 @@ const ENCODED_IMAGE_DATA = "iVBORw0KGgoA
   "eG+mgfrcLHh3C79bx6wttGEqERiH/AjPohWMouv2ZAAAAAElFTkSuQmCC";
 const ACCENT_COLOR = "#a14040";
 const TEXT_COLOR = "#fac96e";
 // For testing aliases of the colors above:
 const FRAME_COLOR = [71, 105, 91];
 const TAB_BACKGROUND_TEXT_COLOR = [207, 221, 192, .9];
 
 function hexToRGB(hex) {
+  if (!hex) {
+    return null;
+  }
   hex = parseInt((hex.indexOf("#") > -1 ? hex.substring(1) : hex), 16);
   return [hex >> 16, (hex & 0x00FF00) >> 8, (hex & 0x0000FF)];
 }
 
 function rgbToCSS(rgb) {
   return `rgb(${rgb.join(", ")})`;
 }
 
 function hexToCSS(hex) {
+  if (!hex) {
+    return null;
+  }
   return rgbToCSS(hexToRGB(hex));
 }
 
 function imageBufferFromDataURI(encodedImageData) {
   let decodedImageData = atob(encodedImageData);
   return Uint8Array.from(decodedImageData, byte => byte.charCodeAt(0)).buffer;
 }