Merge m-c to inbound, a=merge
authorWes Kocher <wkocher@mozilla.com>
Fri, 21 Jul 2017 18:20:46 -0700
changeset 419067 66f0d5a2c077325dcd716a2a0bc6192bc4fc9fae
parent 419066 2ba59555d5299cc236923d91d8cf8c1e7e85f51a (current diff)
parent 418939 057d626fc5e1afbf4086eb9400ebb3718a4cabca (diff)
child 419068 a599289ac64ba1d52a1552e33150342746e3e61b
child 419084 da7ad37975f1595ac109c3be10af4c2ba6ecd8fa
push id7566
push usermtabara@mozilla.com
push dateWed, 02 Aug 2017 08:25:16 +0000
treeherdermozilla-beta@86913f512c3c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone56.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to inbound, a=merge MozReview-Commit-ID: Ah48RzFU8Mt
browser/extensions/activity-stream/test/mozinfo.json
devtools/client/themes/new-webconsole.css
dom/media/MediaDecoderReader.cpp
dom/media/MediaDecoderReader.h
dom/media/MediaFormatReader.cpp
js/xpconnect/public/nsAXPCNativeCallContext.h
layout/tables/nsTableFrame.cpp
modules/libpref/init/all.js
taskcluster/ci/test/test-platforms.yml
taskcluster/ci/test/test-sets.yml
taskcluster/ci/test/tests.yml
third_party/python/py/AUTHORS
third_party/python/py/LICENSE
third_party/python/py/MANIFEST.in
third_party/python/py/PKG-INFO
third_party/python/py/README.txt
third_party/python/py/setup.cfg
third_party/python/py/setup.py
third_party/python/pytest/.coveragerc
third_party/python/pytest/AUTHORS
third_party/python/pytest/LICENSE
third_party/python/pytest/MANIFEST.in
third_party/python/pytest/PKG-INFO
third_party/python/pytest/README.rst
third_party/python/pytest/_pytest/assertion/reinterpret.py
third_party/python/pytest/_pytest/cacheprovider.py
third_party/python/pytest/_pytest/genscript.py
third_party/python/pytest/_pytest/pdb.py
third_party/python/pytest/_pytest/standalonetemplate.py
third_party/python/pytest/_pytest/vendored_packages/README.md
third_party/python/pytest/_pytest/vendored_packages/pluggy-0.3.1.dist-info/DESCRIPTION.rst
third_party/python/pytest/_pytest/vendored_packages/pluggy-0.3.1.dist-info/METADATA
third_party/python/pytest/_pytest/vendored_packages/pluggy-0.3.1.dist-info/RECORD
third_party/python/pytest/_pytest/vendored_packages/pluggy-0.3.1.dist-info/WHEEL
third_party/python/pytest/_pytest/vendored_packages/pluggy-0.3.1.dist-info/metadata.json
third_party/python/pytest/_pytest/vendored_packages/pluggy-0.3.1.dist-info/pbr.json
third_party/python/pytest/_pytest/vendored_packages/pluggy-0.3.1.dist-info/top_level.txt
third_party/python/pytest/setup.cfg
third_party/python/pytest/setup.py
toolkit/components/telemetry/tests/marionette/harness/__init__.py
--- a/addon-sdk/source/test/addons/private-browsing-supported/sidebar/utils.js
+++ b/addon-sdk/source/test/addons/private-browsing-supported/sidebar/utils.js
@@ -39,17 +39,17 @@ function makeID(id) {
 }
 exports.makeID = makeID;
 
 function simulateCommand(ele) {
   let window = ele.ownerGlobal;
   let { document } = window;
   var evt = document.createEvent('XULCommandEvent');
   evt.initCommandEvent('command', true, true, window,
-    0, false, false, false, false, null);
+    0, false, false, false, false, null, 0);
   ele.dispatchEvent(evt);
 }
 exports.simulateCommand = simulateCommand;
 
 function simulateClick(ele) {
   let window = ele.ownerGlobal;
   let { document } = window;
   let evt = document.createEvent('MouseEvents');
--- a/addon-sdk/source/test/sidebar/utils.js
+++ b/addon-sdk/source/test/sidebar/utils.js
@@ -46,17 +46,17 @@ function makeID(id) {
 }
 exports.makeID = makeID;
 
 function simulateCommand(ele) {
   let window = ele.ownerGlobal;
   let { document } = window;
   var evt = document.createEvent('XULCommandEvent');
   evt.initCommandEvent('command', true, true, window,
-    0, false, false, false, false, null);
+    0, false, false, false, false, null, 0);
   ele.dispatchEvent(evt);
 }
 exports.simulateCommand = simulateCommand;
 
 function simulateClick(ele) {
   let window = ele.ownerGlobal;
   let { document } = window;
   let evt = document.createEvent('MouseEvents');
--- a/browser/base/content/browser-gestureSupport.js
+++ b/browser/base/content/browser-gestureSupport.js
@@ -329,17 +329,18 @@ var gGestureSupport = {
    */
   _doCommand: function GS__doCommand(aEvent, aCommand) {
     let node = document.getElementById(aCommand);
     if (node) {
       if (node.getAttribute("disabled") != "true") {
         let cmdEvent = document.createEvent("xulcommandevent");
         cmdEvent.initCommandEvent("command", true, true, window, 0,
                                   aEvent.ctrlKey, aEvent.altKey,
-                                  aEvent.shiftKey, aEvent.metaKey, aEvent);
+                                  aEvent.shiftKey, aEvent.metaKey,
+                                  aEvent, aEvent.mozInputSource);
         node.dispatchEvent(cmdEvent);
       }
 
     } else {
       goDoCommand(aCommand);
     }
   },
 
--- a/browser/base/content/browser-places.js
+++ b/browser/base/content/browser-places.js
@@ -1855,36 +1855,36 @@ var BookmarkingUI = {
       this.button.removeAttribute("notification");
 
       this.dropmarkerNotifier.style.transform = "";
       this.notifier.style.transform = "";
     }, 1000);
   },
 
   showSubView(anchor) {
-    this._showSubView(anchor);
+    this._showSubView(null, anchor);
   },
 
-  _showSubView(anchor = document.getElementById(this.BOOKMARK_BUTTON_ID)) {
+  _showSubView(event, anchor = document.getElementById(this.BOOKMARK_BUTTON_ID)) {
     let view = document.getElementById("PanelUI-bookmarks");
     view.addEventListener("ViewShowing", this);
     view.addEventListener("ViewHiding", this);
     anchor.setAttribute("closemenu", "none");
     PanelUI.showSubView("PanelUI-bookmarks", anchor,
-                        CustomizableUI.AREA_PANEL);
+                        CustomizableUI.AREA_PANEL, event);
   },
 
   onCommand: function BUI_onCommand(aEvent) {
     if (aEvent.target != aEvent.currentTarget) {
       return;
     }
 
     // Handle special case when the button is in the panel.
     if (this.button.getAttribute("cui-areatype") == CustomizableUI.TYPE_MENU_PANEL) {
-      this._showSubView();
+      this._showSubView(aEvent);
       return;
     }
     let widget = CustomizableUI.getWidget(this.BOOKMARK_BUTTON_ID)
                                .forWindow(window);
     if (widget.overflowed) {
       // Close the overflow panel because the Edit Bookmark panel will appear.
       widget.node.removeAttribute("closemenu");
     }
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -375,17 +375,17 @@ const gClickAndHoldListenersOnElement = 
   _clickHandler(aEvent) {
     if (aEvent.button == 0 &&
         aEvent.target == aEvent.currentTarget &&
         !aEvent.currentTarget.open &&
         !aEvent.currentTarget.disabled) {
       let cmdEvent = document.createEvent("xulcommandevent");
       cmdEvent.initCommandEvent("command", true, true, window, 0,
                                 aEvent.ctrlKey, aEvent.altKey, aEvent.shiftKey,
-                                aEvent.metaKey, null);
+                                aEvent.metaKey, null, aEvent.mozInputSource);
       aEvent.currentTarget.dispatchEvent(cmdEvent);
 
       // This is here to cancel the XUL default event
       // dom.click() triggers a command even if there is a click handler
       // however this can now be prevented with preventDefault().
       aEvent.preventDefault();
     }
   },
@@ -1626,17 +1626,17 @@ var gBrowserInit = {
 
     if (Win7Features)
       Win7Features.onOpenWindow();
 
     FullScreen.init();
     PointerLock.init();
 
     if (AppConstants.isPlatformAndVersionAtLeast("win", "10")) {
-      ContextMenuTouchModeObserver.init();
+      MenuTouchModeObserver.init();
     }
 
     // initialize the sync UI
     requestIdleCallback(() => {
       gSync.init();
     }, {timeout: 1000 * 5});
 
     if (AppConstants.MOZ_DATA_REPORTING)
@@ -1841,17 +1841,17 @@ var gBrowserInit = {
         Cu.reportError(ex);
       }
 
       if (this.gmpInstallManager) {
         this.gmpInstallManager.uninit();
       }
 
       if (AppConstants.isPlatformAndVersionAtLeast("win", "10")) {
-        ContextMenuTouchModeObserver.uninit();
+        MenuTouchModeObserver.uninit();
       }
       BrowserOffline.uninit();
       IndexedDBPromptHelper.uninit();
       PanelUI.uninit();
       AutoShowBookmarksToolbar.uninit();
     }
 
     // Final window teardown, do this last.
@@ -7955,17 +7955,20 @@ var gPageActionButton = {
     if ((event.type == "click" && event.button != 0) ||
         (event.type == "keypress" && event.charCode != KeyEvent.DOM_VK_SPACE &&
          event.keyCode != KeyEvent.DOM_VK_RETURN)) {
       return; // Left click, space or enter only
     }
 
     this._preparePanelToBeShown();
     this.panel.hidden = false;
-    this.panel.openPopup(this.button, "bottomcenter topright");
+    this.panel.openPopup(this.button, {
+      position: "bottomcenter topright",
+      triggerEvent: event,
+    });
   },
 
   _preparePanelToBeShown() {
     // Update the bookmark item's label.
     BookmarkingUI.updateBookmarkPageMenuItem();
 
     // Update the send-to-device item's disabled state.
     let browser = gBrowser.selectedBrowser;
@@ -8323,26 +8326,27 @@ var RestoreLastSessionObserver = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
                                          Ci.nsISupportsWeakReference])
 };
 
 function restoreLastSession() {
   SessionStore.restoreLastSession();
 }
 
-/* Observes context menus and adjusts their size for better
+/* Observes menus and adjusts their size for better
  * usability when opened via a touch screen. */
-var ContextMenuTouchModeObserver = {
+var MenuTouchModeObserver = {
   init() {
     window.addEventListener("popupshowing", this, true);
   },
 
   handleEvent(event) {
     let target = event.originalTarget;
-    if (target.localName != "menupopup") {
+    // Only resize non-context menus in Photon.
+    if (target.localName != "menupopup" && !gPhotonStructure) {
       return;
     }
 
     if (event.mozInputSource == MouseEvent.MOZ_SOURCE_TOUCH) {
       target.setAttribute("touchmode", "true");
     } else {
       target.removeAttribute("touchmode");
     }
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -1213,17 +1213,17 @@
 
       <toolbarbutton id="fullscreen-button" class="toolbarbutton-1 chromeclass-toolbar-additional"
                      observes="View:FullScreen"
                      type="checkbox"
                      label="&fullScreenCmd.label;"
                      tooltip="dynamic-shortcut-tooltip"/>
 #ifdef MOZ_PHOTON_THEME
       <toolbarbutton id="library-button" class="toolbarbutton-1 chromeclass-toolbar-additional"
-                     oncommand="PanelUI.showSubView('appMenu-libraryView', this, null, true);"
+                     oncommand="PanelUI.showSubView('appMenu-libraryView', this, null, event);"
                      closemenu="none"
                      label="&places.library.title;"/>
 #endif
     </toolbarpalette>
   </toolbox>
 
   <hbox id="fullscr-toggler" hidden="true"/>
 
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -1975,16 +1975,32 @@
               return this.updateBrowserRemoteness(aBrowser, remote, aOptions);
             }
 
             return false;
           ]]>
         </body>
       </method>
 
+      <method name="removePreloadedBrowser">
+        <body>
+          <![CDATA[
+            if (!this._isPreloadingEnabled()) {
+              return;
+            }
+
+            let browser = this._getPreloadedBrowser();
+
+            if (browser) {
+              browser.remove();
+            }
+          ]]>
+        </body>
+      </method>
+
       <field name="_preloadedBrowser">null</field>
       <method name="_getPreloadedBrowser">
         <body>
           <![CDATA[
             if (!this._isPreloadingEnabled()) {
               return null;
             }
 
@@ -5574,16 +5590,17 @@
             // Also support adding event listeners (forward to the tab container)
             addEventListener(a, b, c) { this.self.tabContainer.addEventListener(a, b, c); },
             removeEventListener(a, b, c) { this.self.tabContainer.removeEventListener(a, b, c); }
           });
         ]]>
         </getter>
       </property>
       <field name="_soundPlayingAttrRemovalTimer">0</field>
+      <field name="_hoverTabTimer">null</field>
     </implementation>
 
     <handlers>
       <handler event="DOMWindowClose" phase="capturing">
         <![CDATA[
           if (!event.isTrusted)
             return;
 
@@ -5689,19 +5706,17 @@
           if (!event.isTrusted)
             return;
 
           let browser = event.originalTarget;
 
           // Preloaded browsers do not actually have any tabs. If one crashes,
           // it should be released and removed.
           if (browser === this._preloadedBrowser) {
-            // Calling _getPreloadedBrowser is necessary to actually consume the preloaded browser
-            let preloaded = this._getPreloadedBrowser();
-            preloaded.remove();
+            this.removePreloadedBrowser();
             return;
           }
 
           let icon = browser.mIconURL;
           let tab = this.getTabForBrowser(browser);
 
           if (this.selectedBrowser == browser) {
             TabCrashHandler.onSelectedBrowserCrash(browser);
@@ -7422,16 +7437,21 @@
           if (val)
             this.setAttribute("visuallyselected", "true");
           else
             this.removeAttribute("visuallyselected");
           this.parentNode.tabbrowser._tabAttrModified(this, ["visuallyselected"]);
 
           this._setPositionAttributes(val);
 
+          // Tab becomes visible, it's not unselected anymore.
+          if (val) {
+            this.finishUnselectedTabHoverTimer();
+          }
+
           return val;
           ]]>
         </setter>
       </property>
 
       <property name="_selected">
         <setter>
           <![CDATA[
@@ -7565,32 +7585,85 @@
             let candidate = visibleTabs[tabIndex + 1];
             if (!candidate.selected) {
               tabContainer._afterHoveredTab = candidate;
               candidate.setAttribute("afterhovered", "true");
             }
           }
 
           tabContainer._hoveredTab = this;
+          if (this.linkedPanel && !this.selected) {
+            this.linkedBrowser.unselectedTabHover(true);
+            this.startUnselectedTabHoverTimer();
+          }
         ]]></body>
       </method>
 
       <method name="_mouseleave">
         <body><![CDATA[
           let tabContainer = this.parentNode;
           if (tabContainer._beforeHoveredTab) {
             tabContainer._beforeHoveredTab.removeAttribute("beforehovered");
             tabContainer._beforeHoveredTab = null;
           }
           if (tabContainer._afterHoveredTab) {
             tabContainer._afterHoveredTab.removeAttribute("afterhovered");
             tabContainer._afterHoveredTab = null;
           }
 
           tabContainer._hoveredTab = null;
+          if (this.linkedPanel && !this.selected) {
+            this.linkedBrowser.unselectedTabHover(false);
+            this.cancelUnselectedTabHoverTimer();
+          }
+        ]]></body>
+      </method>
+
+      <method name="startUnselectedTabHoverTimer">
+        <body><![CDATA[
+          // Only record data when we need to.
+          if (!this.linkedBrowser.shouldHandleUnselectedTabHover) {
+            return;
+          }
+
+          if (!TelemetryStopwatch.running("HOVER_UNTIL_UNSELECTED_TAB_OPENED", this)) {
+            TelemetryStopwatch.start("HOVER_UNTIL_UNSELECTED_TAB_OPENED", this);
+          }
+
+          if (this._hoverTabTimer) {
+            clearTimeout(this._hoverTabTimer);
+            this._hoverTabTimer = null;
+          }
+        ]]></body>
+      </method>
+
+      <method name="cancelUnselectedTabHoverTimer">
+        <body><![CDATA[
+          // Since we're listening "mouseout" event, instead of "mouseleave".
+          // Every time the cursor is moving from the tab to its child node (icon),
+          // it would dispatch "mouseout"(for tab) first and then dispatch
+          // "mouseover" (for icon, eg: close button, speaker icon) soon.
+          // It causes we would cancel present TelemetryStopwatch immediately
+          // when cursor is moving on the icon, and then start a new one.
+          // In order to avoid this situation, we could delay cancellation and
+          // remove it if we get "mouseover" within very short period.
+          this._hoverTabTimer = setTimeout(() => {
+            if (TelemetryStopwatch.running("HOVER_UNTIL_UNSELECTED_TAB_OPENED", this)) {
+              TelemetryStopwatch.cancel("HOVER_UNTIL_UNSELECTED_TAB_OPENED", this);
+            }
+          }, 100);
+        ]]></body>
+      </method>
+
+      <method name="finishUnselectedTabHoverTimer">
+        <body><![CDATA[
+          // Stop timer when the tab is opened.
+          if (TelemetryStopwatch.running("HOVER_UNTIL_UNSELECTED_TAB_OPENED", this)) {
+            TelemetryStopwatch.finish("HOVER_UNTIL_UNSELECTED_TAB_OPENED", this);
+          }
         ]]></body>
       </method>
 
       <method name="startMediaBlockTimer">
         <body><![CDATA[
           TelemetryStopwatch.start("TAB_MEDIA_BLOCKING_TIME_MS", this);
         ]]></body>
       </method>
--- a/browser/base/content/test/static/browser_all_files_referenced.js
+++ b/browser/base/content/test/static/browser_all_files_referenced.js
@@ -94,16 +94,19 @@ var whitelist = [
   // layout/mathml/nsMathMLChar.cpp
   {file: "resource://gre/res/fonts/mathfontSTIXGeneral.properties"},
   {file: "resource://gre/res/fonts/mathfontUnicode.properties"},
 
   // toolkit/components/places/ColorAnalyzer_worker.js
   {file: "resource://gre/modules/ClusterLib.js"},
   {file: "resource://gre/modules/ColorConversion.js"},
 
+  // List of built-in locales. See bug 1362617 for details.
+  {file: "resource://gre/res/multilocale.json"},
+
   // The l10n build system can't package string files only for some platforms.
   {file: "resource://gre/chrome/en-US/locale/en-US/global-platform/mac/accessible.properties",
    platforms: ["linux", "win"]},
   {file: "resource://gre/chrome/en-US/locale/en-US/global-platform/mac/intl.properties",
    platforms: ["linux", "win"]},
   {file: "resource://gre/chrome/en-US/locale/en-US/global-platform/mac/platformKeys.properties",
    platforms: ["linux", "win"]},
   {file: "resource://gre/chrome/en-US/locale/en-US/global-platform/unix/accessible.properties",
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/touch/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+  "extends": [
+    "plugin:mozilla/browser-test"
+  ]
+};
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/touch/browser.ini
@@ -0,0 +1,4 @@
+[DEFAULT]
+
+[browser_menu_touch.js]
+skip-if = !(os == 'win' && os_version == '10.0')
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/touch/browser_menu_touch.js
@@ -0,0 +1,109 @@
+/* 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/. */
+
+/* This test checks that toolbar menus are in touchmode
+ * when opened through a touch event. */
+
+async function openAndCheckMenu(menu, target) {
+  is(menu.state, "closed", "Menu panel is initally closed.");
+
+  let popupshown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+  EventUtils.synthesizeNativeTapAtCenter(target);
+  await popupshown;
+
+  is(menu.state, "open", "Menu panel is open.");
+  is(menu.getAttribute("touchmode"), "true", "Menu panel is in touchmode.");
+
+  menu.hidePopup();
+
+  popupshown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+  EventUtils.synthesizeMouseAtCenter(target, {});
+  await popupshown;
+
+  is(menu.state, "open", "Menu panel is open.");
+  ok(!menu.hasAttribute("touchmode"), "Menu panel is not in touchmode.");
+
+  menu.hidePopup();
+}
+
+// The customization UI menu is not attached to the document when it is
+// closed and hence requires special attention.
+async function openAndCheckCustomizationUIMenu(target) {
+  EventUtils.synthesizeNativeTapAtCenter(target);
+
+  await BrowserTestUtils.waitForCondition(() =>
+      document.getElementById("customizationui-widget-panel") != null);
+  let menu = document.getElementById("customizationui-widget-panel");
+
+  if (menu.state != "open") {
+    await BrowserTestUtils.waitForEvent(menu, "popupshown");
+    is(menu.state, "open", "Menu is open");
+  }
+
+  is(menu.getAttribute("touchmode"), "true", "Menu is in touchmode.");
+
+  menu.hidePopup();
+
+  EventUtils.synthesizeMouseAtCenter(target, {});
+
+  await BrowserTestUtils.waitForCondition(() =>
+      document.getElementById("customizationui-widget-panel") != null);
+  menu = document.getElementById("customizationui-widget-panel");
+
+  if (menu.state != "open") {
+    await BrowserTestUtils.waitForEvent(menu, "popupshown");
+    is(menu.state, "open", "Menu is open");
+  }
+
+  ok(!menu.hasAttribute("touchmode"), "Menu is not in touchmode.");
+
+  menu.hidePopup();
+}
+
+// Test main ("hamburger") menu.
+add_task(async function test_main_menu_touch() {
+  if (!gPhotonStructure) {
+    ok(true, "Skipping test because we're not in Photon mode");
+    return;
+  }
+
+  let mainMenu = document.getElementById("appMenu-popup");
+  let target = document.getElementById("PanelUI-menu-button");
+  await openAndCheckMenu(mainMenu, target);
+});
+
+// Test the page action menu.
+add_task(async function test_page_action_panel_touch() {
+  if (!gPhotonStructure) {
+    ok(true, "Skipping test because we're not in Photon mode");
+    return;
+  }
+
+  let pageActionPanel = document.getElementById("page-action-panel");
+  let target = document.getElementById("urlbar-page-action-button");
+  await openAndCheckMenu(pageActionPanel, target);
+});
+
+// Test the customizationUI panel, which is used for various menus
+// such as library, history, sync, developer and encoding.
+add_task(async function test_customizationui_panel_touch() {
+  if (!gPhotonStructure) {
+    ok(true, "Skipping test because we're not in Photon mode");
+    return;
+  }
+
+  CustomizableUI.addWidgetToArea("library-button", CustomizableUI.AREA_NAVBAR);
+  CustomizableUI.addWidgetToArea("history-panelmenu", CustomizableUI.AREA_NAVBAR);
+
+  await BrowserTestUtils.waitForCondition(() =>
+    CustomizableUI.getPlacementOfWidget("library-button").area == "nav-bar");
+
+  let target = document.getElementById("library-button");
+  await openAndCheckCustomizationUIMenu(target);
+
+  target = document.getElementById("history-panelmenu");
+  await openAndCheckCustomizationUIMenu(target);
+
+  CustomizableUI.reset();
+});
--- a/browser/base/moz.build
+++ b/browser/base/moz.build
@@ -32,16 +32,17 @@ BROWSER_CHROME_MANIFESTS += [
     'content/test/sidebar/browser.ini',
     'content/test/siteIdentity/browser.ini',
     'content/test/social/browser.ini',
     'content/test/static/browser.ini',
     'content/test/sync/browser.ini',
     'content/test/tabcrashed/browser.ini',
     'content/test/tabPrompts/browser.ini',
     'content/test/tabs/browser.ini',
+    'content/test/touch/browser.ini',
     'content/test/urlbar/browser.ini',
     'content/test/webextensions/browser.ini',
     'content/test/webrtc/browser.ini',
 ]
 
 DEFINES['MOZ_APP_VERSION'] = CONFIG['MOZ_APP_VERSION']
 DEFINES['MOZ_APP_VERSION_DISPLAY'] = CONFIG['MOZ_APP_VERSION_DISPLAY']
 
--- a/browser/components/contextualidentity/moz.build
+++ b/browser/components/contextualidentity/moz.build
@@ -6,9 +6,9 @@
 
 BROWSER_CHROME_MANIFESTS += [
     'test/browser/browser.ini',
 ]
 
 JAR_MANIFESTS += ['jar.mn']
 
 with Files('**'):
-    BUG_COMPONENT = ('DOM', 'Security')
+    BUG_COMPONENT = ('Core', 'DOM: Security')
--- a/browser/components/customizableui/CustomizableUI.jsm
+++ b/browser/components/customizableui/CustomizableUI.jsm
@@ -1516,16 +1516,24 @@ var CustomizableUIInternal = {
     }
 
     aTargetNode.setAttribute("shortcut", ShortcutUtils.prettifyShortcut(shortcut));
   },
 
   handleWidgetCommand(aWidget, aNode, aEvent) {
     log.debug("handleWidgetCommand");
 
+    if (aWidget.onBeforeCommand) {
+      try {
+        aWidget.onBeforeCommand.call(null, aEvent);
+      } catch (e) {
+        log.error(e);
+      }
+    }
+
     if (aWidget.type == "button") {
       if (aWidget.onCommand) {
         try {
           aWidget.onCommand.call(null, aEvent);
         } catch (e) {
           log.error(e);
         }
       } else {
@@ -1543,17 +1551,18 @@ var CustomizableUIInternal = {
         let wrapper = this.wrapWidget(aWidget.id).forWindow(ownerWindow);
 
         let hasMultiView = !!aNode.closest("photonpanelmultiview,panelmultiview");
         if (wrapper && !hasMultiView && wrapper.anchor) {
           this.hidePanelForNode(aNode);
           anchor = wrapper.anchor;
         }
       }
-      ownerWindow.PanelUI.showSubView(aWidget.viewId, anchor, area);
+
+      ownerWindow.PanelUI.showSubView(aWidget.viewId, anchor, area, aEvent);
     }
   },
 
   handleWidgetClick(aWidget, aNode, aEvent) {
     log.debug("handleWidgetClick");
     if (aWidget.onClick) {
       try {
         aWidget.onClick.call(null, aEvent);
@@ -2409,16 +2418,20 @@ var CustomizableUIInternal = {
       widget._introducedInVersion = aData.introducedInVersion || 0;
     }
 
     this.wrapWidgetEventHandler("onBeforeCreated", widget);
     this.wrapWidgetEventHandler("onClick", widget);
     this.wrapWidgetEventHandler("onCreated", widget);
     this.wrapWidgetEventHandler("onDestroyed", widget);
 
+    if (typeof aData.onBeforeCommand == "function") {
+      widget.onBeforeCommand = aData.onBeforeCommand;
+    }
+
     if (widget.type == "button") {
       widget.onCommand = typeof aData.onCommand == "function" ?
                            aData.onCommand :
                            null;
     } else if (widget.type == "view") {
       if (typeof aData.viewId != "string") {
         log.error("Expected a string for widget " + widget.id + " viewId, but got "
                   + aData.viewId);
@@ -3286,16 +3299,22 @@ this.CustomizableUI = {
    * - onCreated(aNode): Attached to all widgets; a function that will be invoked
    *                  whenever the widget has a DOM node constructed, passing the
    *                  constructed node as an argument.
    * - onDestroyed(aDoc): Attached to all non-custom widgets; a function that
    *                  will be invoked after the widget has a DOM node destroyed,
    *                  passing the document from which it was removed. This is
    *                  useful especially for 'view' type widgets that need to
    *                  cleanup after views that were constructed on the fly.
+   * - onBeforeCommand(aEvt): A function that will be invoked when the user
+   *                          activates the button but before the command
+   *                          is evaluated. Useful if code needs to run to
+   *                          change the button's icon in preparation to the
+   *                          pending command action. Called for both type=button
+   *                          and type=view.
    * - onCommand(aEvt): Only useful for button widgets; a function that will be
    *                    invoked when the user activates the button.
    * - onClick(aEvt): Attached to all widgets; a function that will be invoked
    *                  when the user clicks the widget.
    * - onViewShowing(aEvt): Only useful for views; a function that will be
    *                  invoked when a user shows your view. If any event
    *                  handler calls aEvt.preventDefault(), the view will
    *                  not be shown.
@@ -4237,17 +4256,16 @@ OverflowableToolbar.prototype = {
   },
 
   _onClickChevron(aEvent) {
     if (this._chevron.open) {
       this._panel.hidePopup();
       this._chevron.open = false;
     } else if (this._panel.state != "hiding") {
       this.show();
-      this._chevron.removeAttribute("animate");
     }
   },
 
   _onPanelHiding(aEvent) {
     this._chevron.open = false;
     this._panel.removeEventListener("dragover", this);
     this._panel.removeEventListener("dragend", this);
     let doc = aEvent.target.ownerDocument;
--- a/browser/components/customizableui/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/CustomizableWidgets.jsm
@@ -128,17 +128,17 @@ function fillSubviewFromMenuItems(aMenuI
       }
 
       if (!item.hasAttribute("oncommand")) {
         subviewItem.addEventListener("command", event => {
           let newEvent = doc.createEvent("XULCommandEvent");
           newEvent.initCommandEvent(
             event.type, event.bubbles, event.cancelable, event.view,
             event.detail, event.ctrlKey, event.altKey, event.shiftKey,
-            event.metaKey, event.sourceEvent);
+            event.metaKey, event.sourceEvent, 0);
           item.dispatchEvent(newEvent);
         });
       }
     } else {
       continue;
     }
     for (let attr of attrs) {
       let attrVal = menuChild.getAttribute(attr);
--- a/browser/components/customizableui/CustomizeMode.jsm
+++ b/browser/components/customizableui/CustomizeMode.jsm
@@ -814,16 +814,25 @@ CustomizeMode.prototype = {
     if (!this._customizing) {
       CustomizableUI.dispatchToolboxEvent("customizationchange");
     }
 
     if (AppConstants.MOZ_PHOTON_ANIMATIONS &&
         Services.prefs.getBoolPref("toolkit.cosmeticAnimations.enabled")) {
       let overflowButton = this.document.getElementById("nav-bar-overflow-button");
       overflowButton.setAttribute("animate", "true");
+      overflowButton.addEventListener("animationend", function onAnimationEnd(event) {
+        if (event.animationName.startsWith("overflow-animation")) {
+          this.setAttribute("fade", "true");
+        } else if (event.animationName == "overflow-fade") {
+          this.removeEventListener("animationend", onAnimationEnd);
+          this.removeAttribute("animate");
+          this.removeAttribute("fade");
+        }
+      });
     }
   },
 
   removeFromArea(aNode) {
     aNode = this._getCustomizableChildForNode(aNode);
     if (aNode.localName == "toolbarpaletteitem" && aNode.firstChild) {
       aNode = aNode.firstChild;
     }
--- a/browser/components/customizableui/PanelMultiView.jsm
+++ b/browser/components/customizableui/PanelMultiView.jsm
@@ -937,25 +937,35 @@ this.PanelMultiView = class {
         this.showMainView();
         if (this.panelViews) {
           if (this._transitionEndListener) {
             this._viewContainer.removeEventListener("transitionend", this._transitionEndListener);
             this._transitionEndListener = null;
           }
           for (let panelView of this._viewStack.children) {
             if (panelView.nodeName != "children") {
+              panelView.__lastKnownBoundingRect = null;
               panelView.style.removeProperty("min-width");
               panelView.style.removeProperty("max-width");
             }
           }
           this.window.removeEventListener("keydown", this);
           this._panel.removeEventListener("mousemove", this);
           this._resetKeyNavigation();
+
+          // Clear the main view size caches. The dimensions could be different
+          // when the popup is opened again, e.g. through touch mode sizing.
           this._mainViewHeight = 0;
+          this._mainViewWidth = 0;
+          this._viewContainer.style.removeProperty("min-height");
+          this._viewStack.style.removeProperty("max-height");
+          this._viewContainer.style.removeProperty("min-width");
+          this._viewContainer.style.removeProperty("max-width");
         }
+
         // Always try to layout the panel normally when reopening it. This is
         // also the layout that will be used in customize mode.
         if (this._mainView.hasAttribute("blockinboxworkaround")) {
           this._mainView.style.removeProperty("height");
           this._mainView.removeAttribute("exceeding");
         }
         break;
     }
--- a/browser/components/customizableui/content/panelUI.js
+++ b/browser/components/customizableui/content/panelUI.js
@@ -268,29 +268,31 @@ const PanelUI = {
 
         let personalBookmarksPlacement = CustomizableUI.getPlacementOfWidget("personal-bookmarks");
         if (personalBookmarksPlacement &&
             personalBookmarksPlacement.area == CustomizableUI.AREA_PANEL) {
           PlacesToolbarHelper.customizeChange();
         }
 
         let anchor;
+        let domEvent = null;
         if (!aEvent ||
             aEvent.type == "command") {
           anchor = this.menuButton;
         } else {
+          domEvent = aEvent;
           anchor = aEvent.target;
         }
 
         this.panel.addEventListener("popupshown", function() {
           resolve();
         }, {once: true});
 
         anchor = this._getPanelAnchor(anchor);
-        this.panel.openPopup(anchor);
+        this.panel.openPopup(anchor, { triggerEvent: domEvent });
       }, (reason) => {
         console.error("Error showing the PanelUI menu", reason);
       });
     });
   },
 
   /**
    * If the menu panel is being shown, hide it.
@@ -470,18 +472,32 @@ const PanelUI = {
   },
 
   /**
    * Shows a subview in the panel with a given ID.
    *
    * @param aViewId the ID of the subview to show.
    * @param aAnchor the element that spawned the subview.
    * @param aPlacementArea the CustomizableUI area that aAnchor is in.
+   * @param aEvent the event triggering the view showing.
    */
-  async showSubView(aViewId, aAnchor, aPlacementArea) {
+  async showSubView(aViewId, aAnchor, aPlacementArea, aEvent) {
+
+    let domEvent = null;
+    if (aEvent) {
+      if (aEvent.type == "command" && aEvent.inputSource != null) {
+        // Synthesize a new DOM mouse event to pass on the inputSource.
+        domEvent = document.createEvent("MouseEvent");
+        domEvent.initNSMouseEvent("click", true, true, null, 0, aEvent.screenX, aEvent.screenY,
+                                  0, 0, false, false, false, false, 0, aEvent.target, 0, aEvent.inputSource);
+      } else if (aEvent.mozInputSource != null) {
+        domEvent = aEvent;
+      }
+    }
+
     this._ensureEventListenersAdded();
     let viewNode = document.getElementById(aViewId);
     if (!viewNode) {
       Cu.reportError("Could not show panel subview with id: " + aViewId);
       return;
     }
 
     if (!aAnchor) {
@@ -579,17 +595,20 @@ const PanelUI = {
       tempPanel.addEventListener("popuphidden", panelRemover);
 
       let anchor = this._getPanelAnchor(aAnchor);
 
       if (aAnchor != anchor && aAnchor.id) {
         anchor.setAttribute("consumeanchor", aAnchor.id);
       }
 
-      tempPanel.openPopup(anchor, "bottomcenter topright");
+      tempPanel.openPopup(anchor, {
+        position: "bottomcenter topright",
+        triggerEvent: domEvent,
+      });
     }
   },
 
   /**
    * NB: The enable- and disableSingleSubviewPanelAnimations methods only
    * affect the hiding/showing animations of single-subview panels (tempPanel
    * in the showSubView method).
    */
--- a/browser/components/extensions/test/browser/browser_ext_devtools_inspectedWindow_eval_bindings.js
+++ b/browser/components/extensions/test/browser/browser_ext_devtools_inspectedWindow_eval_bindings.js
@@ -118,25 +118,29 @@ add_task(async function test_devtools_in
   info("Toolbox has been switched to the inspector as expected");
 
   info("Test inspectedWindow.eval inspect() binding called for a JS object");
 
   const splitPanelOpenedPromise = (async () => {
     await toolbox.once("split-console");
     let jsterm = toolbox.getPanel("webconsole").hud.jsterm;
 
-    const options = await new Promise(resolve => {
-      jsterm.once("variablesview-open", (evt, view, options) => resolve(options));
+    // Wait for the message to appear on the console.
+    const messageNode = await new Promise(resolve => {
+      jsterm.hud.on("new-messages", function onThisMessage(e, messages) {
+        for (let m of messages) {
+          resolve(m.node);
+          jsterm.hud.off("new-messages", onThisMessage);
+          return;
+        }
+      });
     });
 
-    const objectType = options.objectActor.type;
-    const objectPreviewProperties = options.objectActor.preview.ownProperties;
-    is(objectType, "object", "The inspected object has the expected type");
-    Assert.deepEqual(Object.keys(objectPreviewProperties), ["testkey"],
-                     "The inspected object has the expected preview properties");
+    let objectInspectors = [...messageNode.querySelectorAll(".tree")];
+    is(objectInspectors.length, 1, "There is the expected number of object inspectors");
   })();
 
   const inspectJSObjectPromise = extension.awaitMessage(`inspectedWindow-eval-result`);
   extension.sendMessage(`inspectedWindow-eval-request`, "inspect({testkey: 'testvalue'})");
   await inspectJSObjectPromise;
 
   info("Wait for the split console to be opened and the JS object inspected");
   await splitPanelOpenedPromise;
--- a/browser/components/preferences/in-content-new/tests/browser_applications_selection.js
+++ b/browser/components/preferences/in-content-new/tests/browser_applications_selection.js
@@ -33,17 +33,17 @@ add_task(async function selectInternalOp
   info("Got list after item was selected");
 
   // Find the "Add Live bookmarks option".
   let chooseItems = list.getElementsByAttribute("action", Ci.nsIHandlerInfo.handleInternally);
   Assert.equal(chooseItems.length, 1, "Should only be one action to handle internally");
 
   // Select the option.
   let cmdEvent = win.document.createEvent("xulcommandevent");
-  cmdEvent.initCommandEvent("command", true, true, win, 0, false, false, false, false, null);
+  cmdEvent.initCommandEvent("command", true, true, win, 0, false, false, false, false, null, 0);
   chooseItems[0].dispatchEvent(cmdEvent);
 
   // Check that we display the correct result.
   list = await waitForCondition(() =>
     win.document.getAnonymousElementByAttribute(feedItem, "class", "actionsMenu"));
   info("Got list after item was selected");
   Assert.ok(list.selectedItem, "Should have a selected item.");
   Assert.equal(list.selectedItem.getAttribute("action"),
--- a/browser/components/preferences/in-content-new/tests/browser_change_app_handler.js
+++ b/browser/components/preferences/in-content-new/tests/browser_change_app_handler.js
@@ -29,17 +29,17 @@ add_task(async function() {
   ok(ourItem.selected, "Should be able to select our item.");
 
   let list = await waitForCondition(() => win.document.getAnonymousElementByAttribute(ourItem, "class", "actionsMenu"));
   info("Got list after item was selected");
 
   let chooseItem = list.firstChild.querySelector(".choose-app-item");
   let dialogLoadedPromise = promiseLoadSubDialog("chrome://global/content/appPicker.xul");
   let cmdEvent = win.document.createEvent("xulcommandevent");
-  cmdEvent.initCommandEvent("command", true, true, win, 0, false, false, false, false, null);
+  cmdEvent.initCommandEvent("command", true, true, win, 0, false, false, false, false, null, 0);
   chooseItem.dispatchEvent(cmdEvent);
 
   let dialog = await dialogLoadedPromise;
   info("Dialog loaded");
 
   let dialogDoc = dialog.document;
   let dialogList = dialogDoc.getElementById("app-picker-listbox");
   dialogList.selectItem(dialogList.firstChild);
@@ -58,17 +58,17 @@ add_task(async function() {
      "App should be visible as preferred item.");
 
 
   // Now try to 'manage' this list:
   dialogLoadedPromise = promiseLoadSubDialog("chrome://browser/content/preferences/applicationManager.xul");
 
   let manageItem = list.firstChild.querySelector(".manage-app-item");
   cmdEvent = win.document.createEvent("xulcommandevent");
-  cmdEvent.initCommandEvent("command", true, true, win, 0, false, false, false, false, null);
+  cmdEvent.initCommandEvent("command", true, true, win, 0, false, false, false, false, null, 0);
   manageItem.dispatchEvent(cmdEvent);
 
   dialog = await dialogLoadedPromise;
   info("Dialog loaded the second time");
 
   dialogDoc = dialog.document;
   dialogList = dialogDoc.getElementById("appList");
   let itemToRemove = dialogList.querySelector('listitem[label="' + selectedApp.name + '"]');
--- a/browser/components/preferences/in-content/tests/browser_applications_selection.js
+++ b/browser/components/preferences/in-content/tests/browser_applications_selection.js
@@ -33,17 +33,17 @@ add_task(async function selectInternalOp
   info("Got list after item was selected");
 
   // Find the "Add Live bookmarks option".
   let chooseItems = list.getElementsByAttribute("action", Ci.nsIHandlerInfo.handleInternally);
   Assert.equal(chooseItems.length, 1, "Should only be one action to handle internally");
 
   // Select the option.
   let cmdEvent = win.document.createEvent("xulcommandevent");
-  cmdEvent.initCommandEvent("command", true, true, win, 0, false, false, false, false, null);
+  cmdEvent.initCommandEvent("command", true, true, win, 0, false, false, false, false, null, 0);
   chooseItems[0].dispatchEvent(cmdEvent);
 
   // Check that we display the correct result.
   list = await waitForCondition(() =>
     win.document.getAnonymousElementByAttribute(feedItem, "class", "actionsMenu"));
   info("Got list after item was selected");
   Assert.ok(list.selectedItem, "Should have a selected item.");
   Assert.equal(list.selectedItem.getAttribute("action"),
--- a/browser/components/preferences/in-content/tests/browser_change_app_handler.js
+++ b/browser/components/preferences/in-content/tests/browser_change_app_handler.js
@@ -28,17 +28,17 @@ add_task(async function() {
   ok(ourItem.selected, "Should be able to select our item.");
 
   let list = await waitForCondition(() => win.document.getAnonymousElementByAttribute(ourItem, "class", "actionsMenu"));
   info("Got list after item was selected");
 
   let chooseItem = list.firstChild.querySelector(".choose-app-item");
   let dialogLoadedPromise = promiseLoadSubDialog("chrome://global/content/appPicker.xul");
   let cmdEvent = win.document.createEvent("xulcommandevent");
-  cmdEvent.initCommandEvent("command", true, true, win, 0, false, false, false, false, null);
+  cmdEvent.initCommandEvent("command", true, true, win, 0, false, false, false, false, null, 0);
   chooseItem.dispatchEvent(cmdEvent);
 
   let dialog = await dialogLoadedPromise;
   info("Dialog loaded");
 
   let dialogDoc = dialog.document;
   let dialogList = dialogDoc.getElementById("app-picker-listbox");
   dialogList.selectItem(dialogList.firstChild);
@@ -57,17 +57,17 @@ add_task(async function() {
      "App should be visible as preferred item.");
 
 
   // Now try to 'manage' this list:
   dialogLoadedPromise = promiseLoadSubDialog("chrome://browser/content/preferences/applicationManager.xul");
 
   let manageItem = list.firstChild.querySelector(".manage-app-item");
   cmdEvent = win.document.createEvent("xulcommandevent");
-  cmdEvent.initCommandEvent("command", true, true, win, 0, false, false, false, false, null);
+  cmdEvent.initCommandEvent("command", true, true, win, 0, false, false, false, false, null, 0);
   manageItem.dispatchEvent(cmdEvent);
 
   dialog = await dialogLoadedPromise;
   info("Dialog loaded the second time");
 
   dialogDoc = dialog.document;
   dialogList = dialogDoc.getElementById("appList");
   let itemToRemove = dialogList.querySelector('listitem[label="' + selectedApp.name + '"]');
--- a/browser/components/sessionstore/test/browser_background_tab_crash.js
+++ b/browser/components/sessionstore/test/browser_background_tab_crash.js
@@ -229,20 +229,17 @@ add_task(async function test_preload_cra
 
   // Since new tab is only crashable for the activity-stream version,
   // we need to flip the pref
   await SpecialPowers.pushPrefEnv({
     set: [[ "browser.newtabpage.activity-stream.enabled", true ]]
   });
 
   // Release any existing preloaded browser
-  let preloaded = gBrowser._getPreloadedBrowser();
-  if (preloaded) {
-    preloaded.remove();
-  }
+  gBrowser.removePreloadedBrowser();
 
   // Create a fresh preloaded browser
   gBrowser._createPreloadBrowser();
 
   await BrowserTestUtils.crashBrowser(gBrowser._preloadedBrowser, false);
 
   Assert.ok(!gBrowser._preloadedBrowser);
 });
--- a/browser/config/mozconfigs/linux64/common-opt
+++ b/browser/config/mozconfigs/linux64/common-opt
@@ -1,13 +1,11 @@
 # This file is sourced by the nightly, beta, and release mozconfigs.
 
-# TODO remove once configure defaults to stylo once stylo enabled
-# on all platforms.
-ac_add_options --enable-stylo=build
+. $topsrcdir/build/mozconfig.stylo
 
 ac_add_options --enable-update-channel=${MOZ_UPDATE_CHANNEL}
 ac_add_options --with-google-api-keyfile=/builds/gapi.data
 ac_add_options --with-mozilla-api-keyfile=/builds/mozilla-desktop-geoloc-api.key
 
 . $topsrcdir/build/unix/mozconfig.linux
 
 # Needed to enable breakpad in application.ini
--- a/browser/config/mozconfigs/linux64/debug
+++ b/browser/config/mozconfigs/linux64/debug
@@ -1,14 +1,13 @@
 ac_add_options --enable-debug
 ac_add_options --enable-dmd
 ac_add_options --enable-verify-mar
 
-# TODO remove once configure defaults to stylo once stylo enabled
-ac_add_options --enable-stylo=build
+. $topsrcdir/build/mozconfig.stylo
 
 MOZ_AUTOMATION_L10N_CHECK=0
 
 . $topsrcdir/build/unix/mozconfig.linux
 
 # Needed to enable breakpad in application.ini
 export MOZILLA_OFFICIAL=1
 
--- a/browser/config/mozconfigs/linux64/debug-static-analysis-clang
+++ b/browser/config/mozconfigs/linux64/debug-static-analysis-clang
@@ -4,16 +4,17 @@ MOZ_AUTOMATION_L10N_CHECK=0
 
 . "$topsrcdir/build/mozconfig.common"
 
 ac_add_options --enable-debug
 ac_add_options --enable-dmd
 
 # Disable stylo until bug 1356926 is fixed and we have >= llvm39 on centos.
 ac_add_options --disable-stylo
+unset LLVM_CONFIG
 
 # Use Clang as specified in manifest
 export CC="$topsrcdir/clang/bin/clang"
 export CXX="$topsrcdir/clang/bin/clang++"
 
 # Add the static checker
 ac_add_options --enable-clang-plugin
 
--- a/browser/config/mozconfigs/linux64/nightly-asan
+++ b/browser/config/mozconfigs/linux64/nightly-asan
@@ -1,15 +1,13 @@
 # We still need to build with debug symbols
 ac_add_options --disable-debug
 ac_add_options --enable-optimize="-O2 -gline-tables-only"
 
-# TODO remove once configure defaults to stylo once stylo enabled
-# on all platforms.
-ac_add_options --enable-stylo=build
+. $topsrcdir/build/mozconfig.stylo
 
 # ASan specific options on Linux
 ac_add_options --enable-valgrind
 
 . $topsrcdir/build/unix/mozconfig.asan
 
 export PKG_CONFIG_LIBDIR=/usr/lib64/pkgconfig:/usr/share/pkgconfig
 . $topsrcdir/build/unix/mozconfig.gtk
--- a/browser/config/mozconfigs/linux64/nightly-fuzzing-asan
+++ b/browser/config/mozconfigs/linux64/nightly-fuzzing-asan
@@ -1,15 +1,13 @@
 # We still need to build with debug symbols
 ac_add_options --disable-debug
 ac_add_options --enable-optimize="-O2 -gline-tables-only"
 
-# TODO remove once configure defaults to stylo once stylo enabled
-# on all platforms.
-ac_add_options --enable-stylo=build
+. $topsrcdir/build/mozconfig.stylo
 
 # ASan specific options on Linux
 ac_add_options --enable-valgrind
 
 . $topsrcdir/build/unix/mozconfig.fuzzing
 
 ac_add_options --enable-fuzzing
 ac_add_options --disable-stdcxx-compat
--- a/browser/config/mozconfigs/linux64/opt-static-analysis-clang
+++ b/browser/config/mozconfigs/linux64/opt-static-analysis-clang
@@ -3,16 +3,17 @@ MOZ_AUTOMATION_PACKAGE_TESTS=0
 MOZ_AUTOMATION_L10N_CHECK=0
 
 . "$topsrcdir/build/mozconfig.common"
 
 ac_add_options --enable-dmd
 
 # Disable stylo until bug 1356926 is fixed and we have >= llvm39 on centos.
 ac_add_options --disable-stylo
+unset LLVM_CONFIG
 
 # Use Clang as specified in manifest
 CC="$topsrcdir/clang/bin/clang"
 CXX="$topsrcdir/clang/bin/clang++"
 
 # Add the static checker
 ac_add_options --enable-clang-plugin
 
--- a/browser/config/mozconfigs/linux64/valgrind
+++ b/browser/config/mozconfigs/linux64/valgrind
@@ -3,12 +3,13 @@
 ac_add_options --enable-valgrind
 ac_add_options --disable-jemalloc
 ac_add_options --disable-install-strip
 ac_add_options --disable-gtest-in-build
 
 # Rust code gives false positives that we have not entirely suppressed yet.
 # Bug 1365915 tracks fixing these.
 ac_add_options --disable-stylo
+unset LLVM_CONFIG
 
 # Include the override mozconfig again (even though the above includes it)
 # since it's supposed to override everything.
 . "$topsrcdir/build/mozconfig.common.override"
--- a/browser/config/mozconfigs/win32/common-opt
+++ b/browser/config/mozconfigs/win32/common-opt
@@ -1,13 +1,11 @@
 # This file is sourced by the nightly, beta, and release mozconfigs.
 
-# TODO remove once configure defaults to stylo once stylo enabled
-# on all platforms.
-ac_add_options --enable-stylo=build
+. "$topsrcdir/build/mozconfig.stylo"
 
 . "$topsrcdir/browser/config/mozconfigs/common"
 
 ac_add_options --enable-update-channel=${MOZ_UPDATE_CHANNEL}
 ac_add_options --enable-jemalloc
 
 if [ -f /c/builds/gapi.data ]; then
   _gapi_keyfile=c:/builds/gapi.data
--- a/browser/config/mozconfigs/win32/debug
+++ b/browser/config/mozconfigs/win32/debug
@@ -1,15 +1,13 @@
 . "$topsrcdir/build/mozconfig.win-common"
 MOZ_AUTOMATION_L10N_CHECK=0
 . "$topsrcdir/browser/config/mozconfigs/common"
 
-# TODO remove once configure defaults to stylo once stylo enabled
-# on all platforms.
-ac_add_options --enable-stylo=build
+. "$topsrcdir/build/mozconfig.stylo"
 
 ac_add_options --enable-debug
 ac_add_options --enable-dmd
 ac_add_options --enable-profiling  # needed for --enable-dmd to work on Windows
 ac_add_options --enable-verify-mar
 
 # Needed to enable breakpad in application.ini
 export MOZILLA_OFFICIAL=1
--- a/browser/config/mozconfigs/win64/common-opt
+++ b/browser/config/mozconfigs/win64/common-opt
@@ -1,13 +1,11 @@
 # This file is sourced by the nightly, beta, and release mozconfigs.
 
-# TODO remove once configure defaults to stylo once stylo enabled
-# on all platforms.
-ac_add_options --enable-stylo=build
+. "$topsrcdir/build/mozconfig.stylo"
 
 . "$topsrcdir/browser/config/mozconfigs/common"
 
 ac_add_options --enable-update-channel=${MOZ_UPDATE_CHANNEL}
 ac_add_options --enable-jemalloc
 if [ -f /c/builds/gapi.data ]; then
   _gapi_keyfile=c:/builds/gapi.data
 else
--- a/browser/config/mozconfigs/win64/debug
+++ b/browser/config/mozconfigs/win64/debug
@@ -1,18 +1,16 @@
 . "$topsrcdir/build/mozconfig.win-common"
 MOZ_AUTOMATION_L10N_CHECK=0
 . "$topsrcdir/browser/config/mozconfigs/common"
 
 ac_add_options --target=x86_64-pc-mingw32
 ac_add_options --host=x86_64-pc-mingw32
 
-# TODO remove once configure defaults to stylo once stylo enabled
-# on all platforms.
-ac_add_options --enable-stylo=build
+. "$topsrcdir/build/mozconfig.stylo"
 
 ac_add_options --enable-debug
 ac_add_options --enable-dmd
 ac_add_options --enable-profiling  # needed for --enable-dmd to work on Windows
 ac_add_options --enable-verify-mar
 
 # Needed to enable breakpad in application.ini
 export MOZILLA_OFFICIAL=1
--- a/browser/extensions/activity-stream/common/Actions.jsm
+++ b/browser/extensions/activity-stream/common/Actions.jsm
@@ -26,16 +26,17 @@ const actionTypes = {};
 for (const type of [
   "BLOCK_URL",
   "BOOKMARK_URL",
   "DELETE_BOOKMARK_BY_ID",
   "DELETE_HISTORY_URL",
   "DELETE_HISTORY_URL_CONFIRM",
   "DIALOG_CANCEL",
   "DIALOG_OPEN",
+  "FEED_INIT",
   "INIT",
   "LOCALE_UPDATED",
   "NEW_TAB_INITIAL_STATE",
   "NEW_TAB_LOAD",
   "NEW_TAB_UNLOAD",
   "NEW_TAB_VISIBLE",
   "OPEN_NEW_WINDOW",
   "OPEN_PRIVATE_WINDOW",
@@ -45,17 +46,23 @@ for (const type of [
   "PLACES_BOOKMARK_REMOVED",
   "PLACES_HISTORY_CLEARED",
   "PLACES_LINK_BLOCKED",
   "PLACES_LINK_DELETED",
   "PREFS_INITIAL_VALUES",
   "PREF_CHANGED",
   "SAVE_TO_POCKET",
   "SCREENSHOT_UPDATED",
+  "SECTION_DEREGISTER",
+  "SECTION_REGISTER",
+  "SECTION_ROWS_UPDATE",
   "SET_PREF",
+  "SNIPPETS_DATA",
+  "SNIPPETS_RESET",
+  "SYSTEM_TICK",
   "TELEMETRY_PERFORMANCE_EVENT",
   "TELEMETRY_UNDESIRED_EVENT",
   "TELEMETRY_USER_EVENT",
   "TOP_SITES_PIN",
   "TOP_SITES_UNPIN",
   "TOP_SITES_UPDATED",
   "UNINIT"
 ]) {
--- a/browser/extensions/activity-stream/common/Reducers.jsm
+++ b/browser/extensions/activity-stream/common/Reducers.jsm
@@ -11,30 +11,32 @@ const INITIAL_STATE = {
     initialized: false,
     // The locale of the browser
     locale: "",
     // Localized strings with defaults
     strings: {},
     // The version of the system-addon
     version: null
   },
+  Snippets: {initialized: false},
   TopSites: {
     // Have we received real data from history yet?
     initialized: false,
     // The history (and possibly default) links
     rows: []
   },
   Prefs: {
     initialized: false,
     values: {}
   },
   Dialog: {
     visible: false,
     data: {}
-  }
+  },
+  Sections: []
 };
 
 function App(prevState = INITIAL_STATE.App, action) {
   switch (action.type) {
     case at.INIT:
       return Object.assign({}, action.data || {}, {initialized: true});
     case at.LOCALE_UPDATED: {
       if (!action.data) {
@@ -100,25 +102,31 @@ function TopSites(prevState = INITIAL_ST
         if (row && row.url === action.data.url) {
           hasMatch = true;
           return Object.assign({}, row, {screenshot: action.data.screenshot});
         }
         return row;
       });
       return hasMatch ? Object.assign({}, prevState, {rows: newRows}) : prevState;
     case at.PLACES_BOOKMARK_ADDED:
+      if (!action.data) {
+        return prevState;
+      }
       newRows = prevState.rows.map(site => {
         if (site && site.url === action.data.url) {
           const {bookmarkGuid, bookmarkTitle, lastModified} = action.data;
           return Object.assign({}, site, {bookmarkGuid, bookmarkTitle, bookmarkDateCreated: lastModified});
         }
         return site;
       });
       return Object.assign({}, prevState, {rows: newRows});
     case at.PLACES_BOOKMARK_REMOVED:
+      if (!action.data) {
+        return prevState;
+      }
       newRows = prevState.rows.map(site => {
         if (site && site.url === action.data.url) {
           const newSite = Object.assign({}, site);
           delete newSite.bookmarkGuid;
           delete newSite.bookmarkTitle;
           delete newSite.bookmarkDateCreated;
           return newSite;
         }
@@ -160,13 +168,63 @@ function Prefs(prevState = INITIAL_STATE
       newValues = Object.assign({}, prevState.values);
       newValues[action.data.name] = action.data.value;
       return Object.assign({}, prevState, {values: newValues});
     default:
       return prevState;
   }
 }
 
+function Sections(prevState = INITIAL_STATE.Sections, action) {
+  let hasMatch;
+  let newState;
+  switch (action.type) {
+    case at.SECTION_DEREGISTER:
+      return prevState.filter(section => section.id !== action.data);
+    case at.SECTION_REGISTER:
+      // If section exists in prevState, update it
+      newState = prevState.map(section => {
+        if (section && section.id === action.data.id) {
+          hasMatch = true;
+          return Object.assign({}, section, action.data);
+        }
+        return section;
+      });
+      // If section doesn't exist in prevState, create a new section object and
+      // append it to the sections state
+      if (!hasMatch) {
+        const initialized = action.data.rows && action.data.rows.length > 0;
+        newState.push(Object.assign({title: "", initialized, rows: []}, action.data));
+      }
+      return newState;
+    case at.SECTION_ROWS_UPDATE:
+      return prevState.map(section => {
+        if (section && section.id === action.data.id) {
+          return Object.assign({}, section, action.data);
+        }
+        return section;
+      });
+    case at.PLACES_LINK_DELETED:
+    case at.PLACES_LINK_BLOCKED:
+      return prevState.map(section =>
+        Object.assign({}, section, {rows: section.rows.filter(site => site.url !== action.data.url)}));
+    default:
+      return prevState;
+  }
+}
+
+function Snippets(prevState = INITIAL_STATE.Snippets, action) {
+  switch (action.type) {
+    case at.SNIPPETS_DATA:
+      return Object.assign({}, prevState, {initialized: true}, action.data);
+    case at.SNIPPETS_RESET:
+      return INITIAL_STATE.Snippets;
+    default:
+      return prevState;
+  }
+}
+
 this.INITIAL_STATE = INITIAL_STATE;
-this.reducers = {TopSites, App, Prefs, Dialog};
+
+this.reducers = {TopSites, App, Snippets, Prefs, Dialog, Sections};
 this.insertPinned = insertPinned;
 
 this.EXPORTED_SYMBOLS = ["reducers", "INITIAL_STATE", "insertPinned"];
--- a/browser/extensions/activity-stream/data/content/activity-stream.bundle.js
+++ b/browser/extensions/activity-stream/data/content/activity-stream.bundle.js
@@ -1,78 +1,84 @@
 /******/ (function(modules) { // webpackBootstrap
 /******/ 	// The module cache
 /******/ 	var installedModules = {};
-/******/
+
 /******/ 	// The require function
 /******/ 	function __webpack_require__(moduleId) {
-/******/
+
 /******/ 		// Check if module is in cache
 /******/ 		if(installedModules[moduleId])
 /******/ 			return installedModules[moduleId].exports;
-/******/
+
 /******/ 		// Create a new module (and put it into the cache)
 /******/ 		var module = installedModules[moduleId] = {
 /******/ 			i: moduleId,
 /******/ 			l: false,
 /******/ 			exports: {}
 /******/ 		};
-/******/
+
 /******/ 		// Execute the module function
 /******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
-/******/
+
 /******/ 		// Flag the module as loaded
 /******/ 		module.l = true;
-/******/
+
 /******/ 		// Return the exports of the module
 /******/ 		return module.exports;
 /******/ 	}
-/******/
-/******/
+
+
 /******/ 	// expose the modules object (__webpack_modules__)
 /******/ 	__webpack_require__.m = modules;
-/******/
+
 /******/ 	// expose the module cache
 /******/ 	__webpack_require__.c = installedModules;
-/******/
+
 /******/ 	// identity function for calling harmony imports with the correct context
 /******/ 	__webpack_require__.i = function(value) { return value; };
-/******/
+
 /******/ 	// define getter function for harmony exports
 /******/ 	__webpack_require__.d = function(exports, name, getter) {
 /******/ 		if(!__webpack_require__.o(exports, name)) {
 /******/ 			Object.defineProperty(exports, name, {
 /******/ 				configurable: false,
 /******/ 				enumerable: true,
 /******/ 				get: getter
 /******/ 			});
 /******/ 		}
 /******/ 	};
-/******/
+
 /******/ 	// getDefaultExport function for compatibility with non-harmony modules
 /******/ 	__webpack_require__.n = function(module) {
 /******/ 		var getter = module && module.__esModule ?
 /******/ 			function getDefault() { return module['default']; } :
 /******/ 			function getModuleExports() { return module; };
 /******/ 		__webpack_require__.d(getter, 'a', getter);
 /******/ 		return getter;
 /******/ 	};
-/******/
+
 /******/ 	// Object.prototype.hasOwnProperty.call
 /******/ 	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
-/******/
+
 /******/ 	// __webpack_public_path__
 /******/ 	__webpack_require__.p = "";
-/******/
+
 /******/ 	// Load entry module and return exports
-/******/ 	return __webpack_require__(__webpack_require__.s = 19);
+/******/ 	return __webpack_require__(__webpack_require__.s = 25);
 /******/ })
 /************************************************************************/
 /******/ ([
 /* 0 */
+/***/ (function(module, exports) {
+
+module.exports = React;
+
+/***/ }),
+/* 1 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 /* 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/. */
 
 
@@ -92,17 +98,17 @@ const globalImportContext = typeof Windo
 
 
 // Create an object that avoids accidental differing key/value pairs:
 // {
 //   INIT: "INIT",
 //   UNINIT: "UNINIT"
 // }
 const actionTypes = {};
-for (const type of ["BLOCK_URL", "BOOKMARK_URL", "DELETE_BOOKMARK_BY_ID", "DELETE_HISTORY_URL", "DELETE_HISTORY_URL_CONFIRM", "DIALOG_CANCEL", "DIALOG_OPEN", "INIT", "LOCALE_UPDATED", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", "NEW_TAB_UNLOAD", "NEW_TAB_VISIBLE", "OPEN_NEW_WINDOW", "OPEN_PRIVATE_WINDOW", "PINNED_SITES_UPDATED", "PLACES_BOOKMARK_ADDED", "PLACES_BOOKMARK_CHANGED", "PLACES_BOOKMARK_REMOVED", "PLACES_HISTORY_CLEARED", "PLACES_LINK_BLOCKED", "PLACES_LINK_DELETED", "PREFS_INITIAL_VALUES", "PREF_CHANGED", "SAVE_TO_POCKET", "SCREENSHOT_UPDATED", "SET_PREF", "TELEMETRY_PERFORMANCE_EVENT", "TELEMETRY_UNDESIRED_EVENT", "TELEMETRY_USER_EVENT", "TOP_SITES_PIN", "TOP_SITES_UNPIN", "TOP_SITES_UPDATED", "UNINIT"]) {
+for (const type of ["BLOCK_URL", "BOOKMARK_URL", "DELETE_BOOKMARK_BY_ID", "DELETE_HISTORY_URL", "DELETE_HISTORY_URL_CONFIRM", "DIALOG_CANCEL", "DIALOG_OPEN", "FEED_INIT", "INIT", "LOCALE_UPDATED", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", "NEW_TAB_UNLOAD", "NEW_TAB_VISIBLE", "OPEN_NEW_WINDOW", "OPEN_PRIVATE_WINDOW", "PINNED_SITES_UPDATED", "PLACES_BOOKMARK_ADDED", "PLACES_BOOKMARK_CHANGED", "PLACES_BOOKMARK_REMOVED", "PLACES_HISTORY_CLEARED", "PLACES_LINK_BLOCKED", "PLACES_LINK_DELETED", "PREFS_INITIAL_VALUES", "PREF_CHANGED", "SAVE_TO_POCKET", "SCREENSHOT_UPDATED", "SECTION_DEREGISTER", "SECTION_REGISTER", "SECTION_ROWS_UPDATE", "SET_PREF", "SNIPPETS_DATA", "SNIPPETS_RESET", "SYSTEM_TICK", "TELEMETRY_PERFORMANCE_EVENT", "TELEMETRY_UNDESIRED_EVENT", "TELEMETRY_USER_EVENT", "TOP_SITES_PIN", "TOP_SITES_UNPIN", "TOP_SITES_UPDATED", "UNINIT"]) {
   actionTypes[type] = type;
 }
 
 // Helper function for creating routed actions between content and main
 // Not intended to be used by consumers
 function _RouteMessage(action, options) {
   const meta = action.meta ? Object.assign({}, action.meta) : {};
   if (!options || !options.from || !options.to) {
@@ -271,32 +277,26 @@ module.exports = {
   globalImportContext,
   UI_CODE,
   BACKGROUND_PROCESS,
   MAIN_MESSAGE_TYPE,
   CONTENT_MESSAGE_TYPE
 };
 
 /***/ }),
-/* 1 */
-/***/ (function(module, exports) {
-
-module.exports = React;
-
-/***/ }),
 /* 2 */
 /***/ (function(module, exports) {
 
-module.exports = ReactRedux;
+module.exports = ReactIntl;
 
 /***/ }),
 /* 3 */
 /***/ (function(module, exports) {
 
-module.exports = ReactIntl;
+module.exports = ReactRedux;
 
 /***/ }),
 /* 4 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
@@ -320,41 +320,116 @@ module.exports = function shortURL(link)
   }
   const eTLD = link.eTLD;
 
   const hostname = (link.hostname || new URL(link.url).hostname).replace(/^www\./i, "");
 
   // Remove the eTLD (e.g., com, net) and the preceding period from the hostname
   const eTLDLength = (eTLD || "").length || hostname.match(/\.com$/) && 3;
   const eTLDExtra = eTLDLength > 0 ? -(eTLDLength + 1) : Infinity;
-  return hostname.slice(0, eTLDExtra).toLowerCase() || hostname;
+  // If URL and hostname are not present fallback to page title.
+  return hostname.slice(0, eTLDExtra).toLowerCase() || hostname || link.title;
 };
 
 /***/ }),
 /* 5 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
-const React = __webpack_require__(1);
+const React = __webpack_require__(0);
 
 var _require = __webpack_require__(2);
 
+const injectIntl = _require.injectIntl;
+
+const ContextMenu = __webpack_require__(15);
+
+var _require2 = __webpack_require__(1);
+
+const ac = _require2.actionCreators;
+
+const linkMenuOptions = __webpack_require__(21);
+const DEFAULT_SITE_MENU_OPTIONS = ["CheckPinTopSite", "Separator", "OpenInNewWindow", "OpenInPrivateWindow"];
+
+class LinkMenu extends React.Component {
+  getOptions() {
+    const props = this.props;
+    const site = props.site,
+          index = props.index,
+          source = props.source;
+
+    // Handle special case of default site
+
+    const propOptions = !site.isDefault ? props.options : DEFAULT_SITE_MENU_OPTIONS;
+
+    const options = propOptions.map(o => linkMenuOptions[o](site, index)).map(option => {
+      const action = option.action,
+            id = option.id,
+            type = option.type,
+            userEvent = option.userEvent;
+
+      if (!type && id) {
+        option.label = props.intl.formatMessage(option);
+        option.onClick = () => {
+          props.dispatch(action);
+          if (userEvent) {
+            props.dispatch(ac.UserEvent({
+              event: userEvent,
+              source,
+              action_position: index
+            }));
+          }
+        };
+      }
+      return option;
+    });
+
+    // This is for accessibility to support making each item tabbable.
+    // We want to know which item is the first and which item
+    // is the last, so we can close the context menu accordingly.
+    options[0].first = true;
+    options[options.length - 1].last = true;
+    return options;
+  }
+  render() {
+    return React.createElement(ContextMenu, {
+      visible: this.props.visible,
+      onUpdate: this.props.onUpdate,
+      options: this.getOptions() });
+  }
+}
+
+module.exports = injectIntl(LinkMenu);
+module.exports._unconnected = LinkMenu;
+
+/***/ }),
+/* 6 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+const React = __webpack_require__(0);
+
+var _require = __webpack_require__(3);
+
 const connect = _require.connect;
 
-var _require2 = __webpack_require__(3);
+var _require2 = __webpack_require__(2);
 
 const addLocaleData = _require2.addLocaleData,
       IntlProvider = _require2.IntlProvider;
 
-const TopSites = __webpack_require__(15);
-const Search = __webpack_require__(14);
-const ConfirmDialog = __webpack_require__(10);
-const PreferencesPane = __webpack_require__(13);
+const TopSites = __webpack_require__(19);
+const Search = __webpack_require__(17);
+const ConfirmDialog = __webpack_require__(14);
+const PreferencesPane = __webpack_require__(16);
+const Sections = __webpack_require__(18);
 
 // Locales that should be displayed RTL
 const RTL_LIST = ["ar", "he", "fa", "ur"];
 
 // Add the locale data for pluralization and relative-time formatting for now,
 // this just uses english locale data. We can make this more sophisticated if
 // more features are needed.
 function addLocaleDataForReactIntl(_ref) {
@@ -404,38 +479,39 @@ class Base extends React.Component {
       React.createElement(
         "div",
         { className: "outer-wrapper" },
         React.createElement(
           "main",
           null,
           prefs.showSearch && React.createElement(Search, null),
           prefs.showTopSites && React.createElement(TopSites, null),
+          React.createElement(Sections, null),
           React.createElement(ConfirmDialog, null)
         ),
         React.createElement(PreferencesPane, null)
       )
     );
   }
 }
 
 module.exports = connect(state => ({ App: state.App, Prefs: state.Prefs }))(Base);
 
 /***/ }),
-/* 6 */
+/* 7 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
-var _require = __webpack_require__(0);
+var _require = __webpack_require__(1);
 
 const at = _require.actionTypes;
 
-var _require2 = __webpack_require__(17);
+var _require2 = __webpack_require__(22);
 
 const perfSvc = _require2.perfService;
 
 
 const VISIBLE = "visible";
 const VISIBILITY_CHANGE_EVENT = "visibilitychange";
 
 module.exports = class DetectUserSessionStart {
@@ -490,31 +566,31 @@ module.exports = class DetectUserSession
     if (this.document.visibilityState === VISIBLE) {
       this._sendEvent();
       this.document.removeEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
     }
   }
 };
 
 /***/ }),
-/* 7 */
+/* 8 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
 /* eslint-env mozilla/frame-script */
 
-var _require = __webpack_require__(18);
+var _require = __webpack_require__(24);
 
 const createStore = _require.createStore,
       combineReducers = _require.combineReducers,
       applyMiddleware = _require.applyMiddleware;
 
-var _require2 = __webpack_require__(0);
+var _require2 = __webpack_require__(1);
 
 const au = _require2.actionUtils;
 
 
 const MERGE_STORE_ACTION = "NEW_TAB_INITIAL_STATE";
 const OUTGOING_MESSAGE_NAME = "ActivityStream:ContentToMain";
 const INCOMING_MESSAGE_NAME = "ActivityStream:MainToContent";
 
@@ -559,66 +635,340 @@ const messageMiddleware = store => next 
  *
  * @param  {object} reducers An object containing Redux reducers
  * @return {object}          A redux store
  */
 module.exports = function initStore(reducers) {
   const store = createStore(mergeStateReducer(combineReducers(reducers)), applyMiddleware(messageMiddleware));
 
   addMessageListener(INCOMING_MESSAGE_NAME, msg => {
-    store.dispatch(msg.data);
+    try {
+      store.dispatch(msg.data);
+    } catch (ex) {
+      console.error("Content msg:", msg, "Dispatch error: ", ex); // eslint-disable-line no-console
+      dump(`Content msg: ${ JSON.stringify(msg) }\nDispatch error: ${ ex }\n${ ex.stack }`);
+    }
   });
 
   return store;
 };
 
 module.exports.MERGE_STORE_ACTION = MERGE_STORE_ACTION;
 module.exports.OUTGOING_MESSAGE_NAME = OUTGOING_MESSAGE_NAME;
 module.exports.INCOMING_MESSAGE_NAME = INCOMING_MESSAGE_NAME;
 
 /***/ }),
-/* 8 */
+/* 9 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/* WEBPACK VAR INJECTION */(function(global) {
+
+const DATABASE_NAME = "snippets_db";
+const DATABASE_VERSION = 1;
+const SNIPPETS_OBJECTSTORE_NAME = "snippets";
+const SNIPPETS_UPDATE_INTERVAL_MS = 14400000; // 4 hours.
+
+/**
+ * SnippetsMap - A utility for cacheing values related to the snippet. It has
+ *               the same interface as a Map, but is optionally backed by
+ *               indexedDB for persistent storage.
+ *               Call .connect() to open a database connection and restore any
+ *               previously cached data, if necessary.
+ *
+ */
+class SnippetsMap extends Map {
+  constructor() {
+    super(...arguments);
+    this._db = null;
+  }
+
+  set(key, value) {
+    super.set(key, value);
+    return this._dbTransaction(db => db.put(value, key));
+  }
+
+  delete(key, value) {
+    super.delete(key);
+    return this._dbTransaction(db => db.delete(key));
+  }
+
+  clear() {
+    super.clear();
+    return this._dbTransaction(db => db.clear());
+  }
+
+  /**
+   * connect - Attaches an indexedDB back-end to the Map so that any set values
+   *           are also cached in a store. It also restores any existing values
+   *           that are already stored in the indexedDB store.
+   *
+   * @return {type}  description
+   */
+  async connect() {
+    // Open the connection
+    const db = await this._openDB();
+
+    // Restore any existing values
+    await this._restoreFromDb(db);
+
+    // Attach a reference to the db
+    this._db = db;
+  }
+
+  /**
+   * _dbTransaction - Returns a db transaction wrapped with the given modifier
+   *                  function as a Promise. If the db has not been connected,
+   *                  it resolves immediately.
+   *
+   * @param  {func} modifier A function to call with the transaction
+   * @return {obj}           A Promise that resolves when the transaction has
+   *                         completed or errored
+   */
+  _dbTransaction(modifier) {
+    if (!this._db) {
+      return Promise.resolve();
+    }
+    return new Promise((resolve, reject) => {
+      const transaction = modifier(this._db.transaction(SNIPPETS_OBJECTSTORE_NAME, "readwrite").objectStore(SNIPPETS_OBJECTSTORE_NAME));
+      transaction.onsuccess = event => resolve();
+
+      /* istanbul ignore next */
+      transaction.onerror = event => reject(transaction.error);
+    });
+  }
+
+  _openDB() {
+    return new Promise((resolve, reject) => {
+      const openRequest = indexedDB.open(DATABASE_NAME, DATABASE_VERSION);
+
+      /* istanbul ignore next */
+      openRequest.onerror = event => {
+        // Try to delete the old database so that we can start this process over
+        // next time.
+        indexedDB.deleteDatabase(DATABASE_NAME);
+        reject(event);
+      };
+
+      openRequest.onupgradeneeded = event => {
+        const db = event.target.result;
+        if (!db.objectStoreNames.contains(SNIPPETS_OBJECTSTORE_NAME)) {
+          db.createObjectStore(SNIPPETS_OBJECTSTORE_NAME);
+        }
+      };
+
+      openRequest.onsuccess = event => {
+        let db = event.target.result;
+
+        /* istanbul ignore next */
+        db.onerror = err => console.error(err); // eslint-disable-line no-console
+        /* istanbul ignore next */
+        db.onversionchange = versionChangeEvent => versionChangeEvent.target.close();
+
+        resolve(db);
+      };
+    });
+  }
+
+  _restoreFromDb(db) {
+    return new Promise((resolve, reject) => {
+      let cursorRequest;
+      try {
+        cursorRequest = db.transaction(SNIPPETS_OBJECTSTORE_NAME).objectStore(SNIPPETS_OBJECTSTORE_NAME).openCursor();
+      } catch (err) {
+        // istanbul ignore next
+        reject(err);
+        // istanbul ignore next
+        return;
+      }
+
+      /* istanbul ignore next */
+      cursorRequest.onerror = event => reject(event);
+
+      cursorRequest.onsuccess = event => {
+        let cursor = event.target.result;
+        // Populate the cache from the persistent storage.
+        if (cursor) {
+          this.set(cursor.key, cursor.value);
+          cursor.continue();
+        } else {
+          // We are done.
+          resolve();
+        }
+      };
+    });
+  }
+}
+
+/**
+ * SnippetsProvider - Initializes a SnippetsMap and loads snippets from a
+ *                    remote location, or else default snippets if the remote
+ *                    snippets cannot be retrieved.
+ */
+class SnippetsProvider {
+  constructor() {
+    // Initialize the Snippets Map and attaches it to a global so that
+    // the snippet payload can interact with it.
+    global.gSnippetsMap = new SnippetsMap();
+  }
+
+  get snippetsMap() {
+    return global.gSnippetsMap;
+  }
+
+  async _refreshSnippets() {
+    // Check if the cached version of of the snippets in snippetsMap. If it's too
+    // old, blow away the entire snippetsMap.
+    const cachedVersion = this.snippetsMap.get("snippets-cached-version");
+    if (cachedVersion !== this.version) {
+      this.snippetsMap.clear();
+    }
+
+    // Has enough time passed for us to require an update?
+    const lastUpdate = this.snippetsMap.get("snippets-last-update");
+    const needsUpdate = !(lastUpdate >= 0) || Date.now() - lastUpdate > SNIPPETS_UPDATE_INTERVAL_MS;
+
+    if (needsUpdate && this.snippetsURL) {
+      this.snippetsMap.set("snippets-last-update", Date.now());
+      try {
+        // TODO: timeout?
+        const response = await fetch(this.snippetsURL);
+        if (response.status === 200) {
+          const payload = await response.text();
+
+          this.snippetsMap.set("snippets", payload);
+          this.snippetsMap.set("snippets-cached-version", this.version);
+        }
+      } catch (e) {
+        console.error(e); // eslint-disable-line no-console
+      }
+    }
+  }
+
+  _showDefaultSnippets() {
+    // TODO
+  }
+
+  _showRemoteSnippets() {
+    const snippetsEl = document.getElementById(this.elementId);
+    const containerEl = document.getElementById(this.containerElementId);
+    const payload = this.snippetsMap.get("snippets");
+
+    if (!snippetsEl) {
+      throw new Error(`No element was found with id '${ this.elementId }'.`);
+    }
+
+    // This could happen if fetching failed
+    if (!payload) {
+      throw new Error("No remote snippets were found in gSnippetsMap.");
+    }
+
+    // Note that injecting snippets can throw if they're invalid XML.
+    snippetsEl.innerHTML = payload;
+
+    // Scripts injected by innerHTML are inactive, so we have to relocate them
+    // through DOM manipulation to activate their contents.
+    for (const scriptEl of snippetsEl.getElementsByTagName("script")) {
+      const relocatedScript = document.createElement("script");
+      relocatedScript.text = scriptEl.text;
+      scriptEl.parentNode.replaceChild(relocatedScript, scriptEl);
+    }
+
+    // Unhide the container if everything went OK
+    if (containerEl) {
+      containerEl.style.display = "block";
+    }
+  }
+
+  /**
+   * init - Fetch the snippet payload and show snippets
+   *
+   * @param  {obj} options
+   * @param  {str} options.snippetsURL  The URL from which we fetch snippets
+   * @param  {int} options.version  The current snippets version
+   * @param  {str} options.elementId  The id of the element of the snippets container
+   */
+  async init(options) {
+    Object.assign(this, {
+      snippetsURL: "",
+      version: 0,
+      elementId: "snippets",
+      containerElementId: "snippets-container",
+      connect: true
+    }, options);
+
+    // TODO: Requires enabling indexedDB on newtab
+    // Restore the snippets map from indexedDB
+    if (this.connect) {
+      try {
+        await this.snippetsMap.connect();
+      } catch (e) {
+        console.error(e); // eslint-disable-line no-console
+      }
+    }
+
+    // Refresh snippets, if enough time has passed.
+    await this._refreshSnippets();
+
+    // Try showing remote snippets, falling back to defaults if necessary.
+    try {
+      this._showRemoteSnippets();
+    } catch (e) {
+      this._showDefaultSnippets(e);
+    }
+  }
+}
+
+module.exports.SnippetsMap = SnippetsMap;
+module.exports.SnippetsProvider = SnippetsProvider;
+module.exports.SNIPPETS_UPDATE_INTERVAL_MS = SNIPPETS_UPDATE_INTERVAL_MS;
+/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(23)))
+
+/***/ }),
+/* 10 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 /* 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/. */
 
 
-var _require = __webpack_require__(0);
+var _require = __webpack_require__(1);
 
 const at = _require.actionTypes;
 
 
 const INITIAL_STATE = {
   App: {
     // Have we received real data from the app yet?
     initialized: false,
     // The locale of the browser
     locale: "",
     // Localized strings with defaults
     strings: {},
     // The version of the system-addon
     version: null
   },
+  Snippets: { initialized: false },
   TopSites: {
     // Have we received real data from history yet?
     initialized: false,
     // The history (and possibly default) links
     rows: []
   },
   Prefs: {
     initialized: false,
     values: {}
   },
   Dialog: {
     visible: false,
     data: {}
-  }
+  },
+  Sections: []
 };
 
 function App() {
   let prevState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : INITIAL_STATE.App;
   let action = arguments[1];
 
   switch (action.type) {
     case at.INIT:
@@ -696,29 +1046,35 @@ function TopSites() {
         if (row && row.url === action.data.url) {
           hasMatch = true;
           return Object.assign({}, row, { screenshot: action.data.screenshot });
         }
         return row;
       });
       return hasMatch ? Object.assign({}, prevState, { rows: newRows }) : prevState;
     case at.PLACES_BOOKMARK_ADDED:
+      if (!action.data) {
+        return prevState;
+      }
       newRows = prevState.rows.map(site => {
         if (site && site.url === action.data.url) {
           var _action$data2 = action.data;
           const bookmarkGuid = _action$data2.bookmarkGuid,
                 bookmarkTitle = _action$data2.bookmarkTitle,
                 lastModified = _action$data2.lastModified;
 
           return Object.assign({}, site, { bookmarkGuid, bookmarkTitle, bookmarkDateCreated: lastModified });
         }
         return site;
       });
       return Object.assign({}, prevState, { rows: newRows });
     case at.PLACES_BOOKMARK_REMOVED:
+      if (!action.data) {
+        return prevState;
+      }
       newRows = prevState.rows.map(site => {
         if (site && site.url === action.data.url) {
           const newSite = Object.assign({}, site);
           delete newSite.bookmarkGuid;
           delete newSite.bookmarkTitle;
           delete newSite.bookmarkDateCreated;
           return newSite;
         }
@@ -766,47 +1122,247 @@ function Prefs() {
       newValues = Object.assign({}, prevState.values);
       newValues[action.data.name] = action.data.value;
       return Object.assign({}, prevState, { values: newValues });
     default:
       return prevState;
   }
 }
 
-var reducers = { TopSites, App, Prefs, Dialog };
+function Sections() {
+  let prevState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : INITIAL_STATE.Sections;
+  let action = arguments[1];
+
+  let hasMatch;
+  let newState;
+  switch (action.type) {
+    case at.SECTION_DEREGISTER:
+      return prevState.filter(section => section.id !== action.data);
+    case at.SECTION_REGISTER:
+      // If section exists in prevState, update it
+      newState = prevState.map(section => {
+        if (section && section.id === action.data.id) {
+          hasMatch = true;
+          return Object.assign({}, section, action.data);
+        }
+        return section;
+      });
+      // If section doesn't exist in prevState, create a new section object and
+      // append it to the sections state
+      if (!hasMatch) {
+        const initialized = action.data.rows && action.data.rows.length > 0;
+        newState.push(Object.assign({ title: "", initialized, rows: [] }, action.data));
+      }
+      return newState;
+    case at.SECTION_ROWS_UPDATE:
+      return prevState.map(section => {
+        if (section && section.id === action.data.id) {
+          return Object.assign({}, section, action.data);
+        }
+        return section;
+      });
+    case at.PLACES_LINK_DELETED:
+    case at.PLACES_LINK_BLOCKED:
+      return prevState.map(section => Object.assign({}, section, { rows: section.rows.filter(site => site.url !== action.data.url) }));
+    default:
+      return prevState;
+  }
+}
+
+function Snippets() {
+  let prevState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : INITIAL_STATE.Snippets;
+  let action = arguments[1];
+
+  switch (action.type) {
+    case at.SNIPPETS_DATA:
+      return Object.assign({}, prevState, { initialized: true }, action.data);
+    case at.SNIPPETS_RESET:
+      return INITIAL_STATE.Snippets;
+    default:
+      return prevState;
+  }
+}
+
+var reducers = { TopSites, App, Snippets, Prefs, Dialog, Sections };
 module.exports = {
   reducers,
   INITIAL_STATE,
   insertPinned
 };
 
 /***/ }),
-/* 9 */
+/* 11 */
 /***/ (function(module, exports) {
 
 module.exports = ReactDOM;
 
 /***/ }),
-/* 10 */
+/* 12 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
-const React = __webpack_require__(1);
+const React = __webpack_require__(0);
+const LinkMenu = __webpack_require__(5);
+const shortURL = __webpack_require__(4);
 
 var _require = __webpack_require__(2);
 
+const FormattedMessage = _require.FormattedMessage;
+
+const cardContextTypes = __webpack_require__(13);
+
+/**
+ * Card component.
+ * Cards are found within a Section component and contain information about a link such
+ * as preview image, page title, page description, and some context about if the page
+ * was visited, bookmarked, trending etc...
+ * Each Section can make an unordered list of Cards which will create one instane of
+ * this class. Each card will then get a context menu which reflects the actions that
+ * can be done on this Card.
+ */
+class Card extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = { showContextMenu: false, activeCard: null };
+  }
+  toggleContextMenu(event, index) {
+    this.setState({ showContextMenu: true, activeCard: index });
+  }
+  render() {
+    var _props = this.props;
+    const index = _props.index,
+          link = _props.link,
+          dispatch = _props.dispatch,
+          contextMenuOptions = _props.contextMenuOptions;
+
+    const isContextMenuOpen = this.state.showContextMenu && this.state.activeCard === index;
+    const hostname = shortURL(link);
+    var _cardContextTypes$lin = cardContextTypes[link.type];
+    const icon = _cardContextTypes$lin.icon,
+          intlID = _cardContextTypes$lin.intlID;
+
+
+    return React.createElement(
+      "li",
+      { className: `card-outer${ isContextMenuOpen ? " active" : "" }` },
+      React.createElement(
+        "a",
+        { href: link.url },
+        React.createElement(
+          "div",
+          { className: "card" },
+          link.image && React.createElement("div", { className: "card-preview-image", style: { backgroundImage: `url(${ link.image })` } }),
+          React.createElement(
+            "div",
+            { className: "card-details" },
+            React.createElement(
+              "div",
+              { className: "card-host-name" },
+              " ",
+              hostname,
+              " "
+            ),
+            React.createElement(
+              "div",
+              { className: `card-text${ link.image ? "" : " full-height" }` },
+              React.createElement(
+                "h4",
+                { className: "card-title" },
+                " ",
+                link.title,
+                " "
+              ),
+              React.createElement(
+                "p",
+                { className: "card-description" },
+                " ",
+                link.description,
+                " "
+              )
+            ),
+            React.createElement(
+              "div",
+              { className: "card-context" },
+              React.createElement("span", { className: `card-context-icon icon icon-${ icon }` }),
+              React.createElement(
+                "div",
+                { className: "card-context-label" },
+                React.createElement(FormattedMessage, { id: intlID, defaultMessage: "Visited" })
+              )
+            )
+          )
+        )
+      ),
+      React.createElement(
+        "button",
+        { className: "context-menu-button",
+          onClick: e => {
+            e.preventDefault();
+            this.toggleContextMenu(e, index);
+          } },
+        React.createElement(
+          "span",
+          { className: "sr-only" },
+          `Open context menu for ${ link.title }`
+        )
+      ),
+      React.createElement(LinkMenu, {
+        dispatch: dispatch,
+        visible: isContextMenuOpen,
+        onUpdate: val => this.setState({ showContextMenu: val }),
+        index: index,
+        site: link,
+        options: link.context_menu_options || contextMenuOptions })
+    );
+  }
+}
+module.exports = Card;
+
+/***/ }),
+/* 13 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+module.exports = {
+  history: {
+    intlID: "type_label_visited",
+    icon: "historyItem"
+  },
+  bookmark: {
+    intlID: "type_label_bookmarked",
+    icon: "bookmark"
+  },
+  trending: {
+    intlID: "type_label_recommended",
+    icon: "trending"
+  }
+};
+
+/***/ }),
+/* 14 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+const React = __webpack_require__(0);
+
+var _require = __webpack_require__(3);
+
 const connect = _require.connect;
 
-var _require2 = __webpack_require__(3);
+var _require2 = __webpack_require__(2);
 
 const FormattedMessage = _require2.FormattedMessage;
 
-var _require3 = __webpack_require__(0);
+var _require3 = __webpack_require__(1);
 
 const actionTypes = _require3.actionTypes,
       ac = _require3.actionCreators;
 
 /**
  * ConfirmDialog component.
  * One primary action button, one cancel button.
  *
@@ -899,23 +1455,23 @@ const ConfirmDialog = React.createClass(
   }
 });
 
 module.exports = connect(state => state.Dialog)(ConfirmDialog);
 module.exports._unconnected = ConfirmDialog;
 module.exports.Dialog = ConfirmDialog;
 
 /***/ }),
-/* 11 */
+/* 15 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
-const React = __webpack_require__(1);
+const React = __webpack_require__(0);
 
 class ContextMenu extends React.Component {
   constructor(props) {
     super(props);
     this.hideContext = this.hideContext.bind(this);
   }
   hideContext() {
     this.props.onUpdate(false);
@@ -969,120 +1525,47 @@ class ContextMenu extends React.Componen
             React.createElement(
               "a",
               { tabIndex: "0",
                 onKeyDown: e => this.onKeyDown(e, option),
                 onClick: () => {
                   this.hideContext();
                   option.onClick();
                 } },
-              option.icon && React.createElement("span", { className: `icon icon-spacer icon-${option.icon}` }),
+              option.icon && React.createElement("span", { className: `icon icon-spacer icon-${ option.icon }` }),
               option.label
             )
           );
         })
       )
     );
   }
 }
 
 module.exports = ContextMenu;
 
 /***/ }),
-/* 12 */
+/* 16 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
-const React = __webpack_require__(1);
+const React = __webpack_require__(0);
 
 var _require = __webpack_require__(3);
 
-const injectIntl = _require.injectIntl;
-
-const ContextMenu = __webpack_require__(11);
-
-var _require2 = __webpack_require__(0);
-
-const ac = _require2.actionCreators;
-
-const linkMenuOptions = __webpack_require__(16);
-const DEFAULT_SITE_MENU_OPTIONS = ["CheckPinTopSite", "Separator", "OpenInNewWindow", "OpenInPrivateWindow"];
-
-class LinkMenu extends React.Component {
-  getOptions() {
-    const props = this.props;
-    const site = props.site,
-          index = props.index,
-          source = props.source;
-
-    // Handle special case of default site
-
-    const propOptions = !site.isDefault ? props.options : DEFAULT_SITE_MENU_OPTIONS;
-
-    const options = propOptions.map(o => linkMenuOptions[o](site, index)).map(option => {
-      const action = option.action,
-            id = option.id,
-            type = option.type,
-            userEvent = option.userEvent;
-
-      if (!type && id) {
-        option.label = props.intl.formatMessage(option);
-        option.onClick = () => {
-          props.dispatch(action);
-          if (userEvent) {
-            props.dispatch(ac.UserEvent({
-              event: userEvent,
-              source,
-              action_position: index
-            }));
-          }
-        };
-      }
-      return option;
-    });
-
-    // This is for accessibility to support making each item tabbable.
-    // We want to know which item is the first and which item
-    // is the last, so we can close the context menu accordingly.
-    options[0].first = true;
-    options[options.length - 1].last = true;
-    return options;
-  }
-  render() {
-    return React.createElement(ContextMenu, {
-      visible: this.props.visible,
-      onUpdate: this.props.onUpdate,
-      options: this.getOptions() });
-  }
-}
-
-module.exports = injectIntl(LinkMenu);
-module.exports._unconnected = LinkMenu;
-
-/***/ }),
-/* 13 */
-/***/ (function(module, exports, __webpack_require__) {
-
-"use strict";
-
-
-const React = __webpack_require__(1);
-
-var _require = __webpack_require__(2);
-
 const connect = _require.connect;
 
-var _require2 = __webpack_require__(3);
+var _require2 = __webpack_require__(2);
 
 const injectIntl = _require2.injectIntl,
       FormattedMessage = _require2.FormattedMessage;
 
-var _require3 = __webpack_require__(0);
+var _require3 = __webpack_require__(1);
 
 const ac = _require3.actionCreators;
 
 
 const PreferencesInput = props => React.createElement(
   "section",
   null,
   React.createElement("input", { type: "checkbox", id: props.prefName, name: props.prefName, checked: props.value, onChange: props.onChange, className: props.className }),
@@ -1133,43 +1616,45 @@ class PreferencesPane extends React.Comp
     const isVisible = this.state.visible;
     return React.createElement(
       "div",
       { className: "prefs-pane-wrapper", ref: "wrapper" },
       React.createElement(
         "div",
         { className: "prefs-pane-button" },
         React.createElement("button", {
-          className: `prefs-button icon ${isVisible ? "icon-dismiss" : "icon-settings"}`,
+          className: `prefs-button icon ${ isVisible ? "icon-dismiss" : "icon-settings" }`,
           title: props.intl.formatMessage({ id: isVisible ? "settings_pane_done_button" : "settings_pane_button_label" }),
           onClick: this.togglePane })
       ),
       React.createElement(
         "div",
         { className: "prefs-pane" },
         React.createElement(
           "div",
-          { className: `sidebar ${isVisible ? "" : "hidden"}` },
+          { className: `sidebar ${ isVisible ? "" : "hidden" }` },
           React.createElement(
             "div",
             { className: "prefs-modal-inner-wrapper" },
             React.createElement(
               "h1",
               null,
               React.createElement(FormattedMessage, { id: "settings_pane_header" })
             ),
             React.createElement(
               "p",
               null,
               React.createElement(FormattedMessage, { id: "settings_pane_body" })
             ),
             React.createElement(PreferencesInput, { className: "showSearch", prefName: "showSearch", value: prefs.showSearch, onChange: this.handleChange,
               titleStringId: "settings_pane_search_header", descStringId: "settings_pane_search_body" }),
             React.createElement(PreferencesInput, { className: "showTopSites", prefName: "showTopSites", value: prefs.showTopSites, onChange: this.handleChange,
-              titleStringId: "settings_pane_topsites_header", descStringId: "settings_pane_topsites_body" })
+              titleStringId: "settings_pane_topsites_header", descStringId: "settings_pane_topsites_body" }),
+            React.createElement(PreferencesInput, { className: "showTopStories", prefName: "feeds.section.topstories", value: prefs["feeds.section.topstories"], onChange: this.handleChange,
+              titleStringId: "settings_pane_pocketstories_header", descStringId: "settings_pane_pocketstories_body" })
           ),
           React.createElement(
             "section",
             { className: "actions" },
             React.createElement(
               "button",
               { className: "done", onClick: this.togglePane },
               React.createElement(FormattedMessage, { id: "settings_pane_done_button" })
@@ -1181,35 +1666,35 @@ class PreferencesPane extends React.Comp
   }
 }
 
 module.exports = connect(state => ({ Prefs: state.Prefs }))(injectIntl(PreferencesPane));
 module.exports.PreferencesPane = PreferencesPane;
 module.exports.PreferencesInput = PreferencesInput;
 
 /***/ }),
-/* 14 */
+/* 17 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 /* globals ContentSearchUIController */
 
 
-const React = __webpack_require__(1);
+const React = __webpack_require__(0);
 
-var _require = __webpack_require__(2);
+var _require = __webpack_require__(3);
 
 const connect = _require.connect;
 
-var _require2 = __webpack_require__(3);
+var _require2 = __webpack_require__(2);
 
 const FormattedMessage = _require2.FormattedMessage,
       injectIntl = _require2.injectIntl;
 
-var _require3 = __webpack_require__(0);
+var _require3 = __webpack_require__(1);
 
 const ac = _require3.actionCreators;
 
 
 class Search extends React.Component {
   constructor(props) {
     super(props);
     this.onClick = this.onClick.bind(this);
@@ -1280,36 +1765,156 @@ class Search extends React.Component {
     );
   }
 }
 
 module.exports = connect()(injectIntl(Search));
 module.exports._unconnected = Search;
 
 /***/ }),
-/* 15 */
+/* 18 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
-const React = __webpack_require__(1);
+var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
 
-var _require = __webpack_require__(2);
+const React = __webpack_require__(0);
+
+var _require = __webpack_require__(3);
 
 const connect = _require.connect;
 
-var _require2 = __webpack_require__(3);
+var _require2 = __webpack_require__(2);
+
+const FormattedMessage = _require2.FormattedMessage;
+
+const Card = __webpack_require__(12);
+const Topics = __webpack_require__(20);
+
+class Section extends React.Component {
+  render() {
+    var _props = this.props;
+    const id = _props.id,
+          title = _props.title,
+          icon = _props.icon,
+          rows = _props.rows,
+          infoOption = _props.infoOption,
+          emptyState = _props.emptyState,
+          dispatch = _props.dispatch,
+          maxCards = _props.maxCards,
+          contextMenuOptions = _props.contextMenuOptions;
+
+    const initialized = rows && rows.length > 0;
+    const shouldShowTopics = id === "TopStories" && this.props.topics && this.props.read_more_endpoint;
+    // <Section> <-- React component
+    // <section> <-- HTML5 element
+    return React.createElement(
+      "section",
+      null,
+      React.createElement(
+        "div",
+        { className: "section-top-bar" },
+        React.createElement(
+          "h3",
+          { className: "section-title" },
+          React.createElement("span", { className: `icon icon-small-spacer icon-${ icon }` }),
+          React.createElement(FormattedMessage, title)
+        ),
+        infoOption && React.createElement(
+          "span",
+          { className: "section-info-option" },
+          React.createElement(
+            "span",
+            { className: "sr-only" },
+            React.createElement(FormattedMessage, { id: "section_info_option" })
+          ),
+          React.createElement("img", { className: "info-option-icon" }),
+          React.createElement(
+            "div",
+            { className: "info-option" },
+            infoOption.header && React.createElement(
+              "div",
+              { className: "info-option-header" },
+              React.createElement(FormattedMessage, infoOption.header)
+            ),
+            infoOption.body && React.createElement(
+              "p",
+              { className: "info-option-body" },
+              React.createElement(FormattedMessage, infoOption.body)
+            ),
+            infoOption.link && React.createElement(
+              "a",
+              { href: infoOption.link.href, target: "_blank", rel: "noopener noreferrer", className: "info-option-link" },
+              React.createElement(FormattedMessage, infoOption.link)
+            )
+          )
+        )
+      ),
+      React.createElement(
+        "ul",
+        { className: "section-list", style: { padding: 0 } },
+        rows.slice(0, maxCards).map((link, index) => link && React.createElement(Card, { index: index, dispatch: dispatch, link: link, contextMenuOptions: contextMenuOptions }))
+      ),
+      !initialized && React.createElement(
+        "div",
+        { className: "section-empty-state" },
+        React.createElement(
+          "div",
+          { className: "empty-state" },
+          React.createElement("img", { className: `empty-state-icon icon icon-${ emptyState.icon }` }),
+          React.createElement(
+            "p",
+            { className: "empty-state-message" },
+            React.createElement(FormattedMessage, emptyState.message)
+          )
+        )
+      ),
+      shouldShowTopics && React.createElement(Topics, { topics: this.props.topics, read_more_endpoint: this.props.read_more_endpoint })
+    );
+  }
+}
+
+class Sections extends React.Component {
+  render() {
+    const sections = this.props.Sections;
+    return React.createElement(
+      "div",
+      { className: "sections-list" },
+      sections.map(section => React.createElement(Section, _extends({ key: section.id }, section, { dispatch: this.props.dispatch })))
+    );
+  }
+}
+
+module.exports = connect(state => ({ Sections: state.Sections }))(Sections);
+module.exports._unconnected = Sections;
+module.exports.Section = Section;
+
+/***/ }),
+/* 19 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+const React = __webpack_require__(0);
+
+var _require = __webpack_require__(3);
+
+const connect = _require.connect;
+
+var _require2 = __webpack_require__(2);
 
 const FormattedMessage = _require2.FormattedMessage;
 
 const shortURL = __webpack_require__(4);
-const LinkMenu = __webpack_require__(12);
+const LinkMenu = __webpack_require__(5);
 
-var _require3 = __webpack_require__(0);
+var _require3 = __webpack_require__(1);
 
 const ac = _require3.actionCreators;
 
 const TOP_SITES_SOURCE = "TOP_SITES";
 const TOP_SITES_CONTEXT_MENU_OPTIONS = ["CheckPinTopSite", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", "DeleteUrl"];
 
 class TopSite extends React.Component {
   constructor(props) {
@@ -1329,38 +1934,38 @@ class TopSite extends React.Component {
   render() {
     var _props = this.props;
     const link = _props.link,
           index = _props.index,
           dispatch = _props.dispatch;
 
     const isContextMenuOpen = this.state.showContextMenu && this.state.activeTile === index;
     const title = link.pinTitle || shortURL(link);
-    const screenshotClassName = `screenshot${link.screenshot ? " active" : ""}`;
-    const topSiteOuterClassName = `top-site-outer${isContextMenuOpen ? " active" : ""}`;
-    const style = { backgroundImage: link.screenshot ? `url(${link.screenshot})` : "none" };
+    const screenshotClassName = `screenshot${ link.screenshot ? " active" : "" }`;
+    const topSiteOuterClassName = `top-site-outer${ isContextMenuOpen ? " active" : "" }`;
+    const style = { backgroundImage: link.screenshot ? `url(${ link.screenshot })` : "none" };
     return React.createElement(
       "li",
-      { className: topSiteOuterClassName, key: link.url },
+      { className: topSiteOuterClassName, key: link.guid || link.url },
       React.createElement(
         "a",
         { onClick: () => this.trackClick(), href: link.url },
         React.createElement(
           "div",
           { className: "tile", "aria-hidden": true },
           React.createElement(
             "span",
             { className: "letter-fallback" },
             title[0]
           ),
           React.createElement("div", { className: screenshotClassName, style: style })
         ),
         React.createElement(
           "div",
-          { className: `title ${link.isPinned ? "pinned" : ""}` },
+          { className: `title ${ link.isPinned ? "pinned" : "" }` },
           link.isPinned && React.createElement("div", { className: "icon icon-pin-small" }),
           React.createElement(
             "span",
             null,
             title
           )
         )
       ),
@@ -1369,17 +1974,17 @@ class TopSite extends React.Component {
         { className: "context-menu-button",
           onClick: e => {
             e.preventDefault();
             this.toggleContextMenu(e, index);
           } },
         React.createElement(
           "span",
           { className: "sr-only" },
-          `Open context menu for ${title}`
+          `Open context menu for ${ title }`
         )
       ),
       React.createElement(LinkMenu, {
         dispatch: dispatch,
         visible: isContextMenuOpen,
         onUpdate: val => this.setState({ showContextMenu: val }),
         site: link,
         index: index,
@@ -1396,35 +2001,100 @@ const TopSites = props => React.createEl
     "h3",
     { className: "section-title" },
     React.createElement(FormattedMessage, { id: "header_top_sites" })
   ),
   React.createElement(
     "ul",
     { className: "top-sites-list" },
     props.TopSites.rows.map((link, index) => link && React.createElement(TopSite, {
-      key: link.url,
+      key: link.guid || link.url,
       dispatch: props.dispatch,
       link: link,
       index: index }))
   )
 );
 
 module.exports = connect(state => ({ TopSites: state.TopSites }))(TopSites);
 module.exports._unconnected = TopSites;
 module.exports.TopSite = TopSite;
 
 /***/ }),
-/* 16 */
+/* 20 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
-var _require = __webpack_require__(0);
+const React = __webpack_require__(0);
+
+var _require = __webpack_require__(2);
+
+const FormattedMessage = _require.FormattedMessage;
+
+
+class Topic extends React.Component {
+  render() {
+    var _props = this.props;
+    const url = _props.url,
+          name = _props.name;
+
+    return React.createElement(
+      "li",
+      null,
+      React.createElement(
+        "a",
+        { key: name, className: "topic-link", href: url },
+        name
+      )
+    );
+  }
+}
+
+class Topics extends React.Component {
+  render() {
+    var _props2 = this.props;
+    const topics = _props2.topics,
+          read_more_endpoint = _props2.read_more_endpoint;
+
+    return React.createElement(
+      "div",
+      { className: "topic" },
+      React.createElement(
+        "span",
+        null,
+        React.createElement(FormattedMessage, { id: "pocket_read_more" })
+      ),
+      React.createElement(
+        "ul",
+        null,
+        topics.map(t => React.createElement(Topic, { key: t.name, url: t.url, name: t.name }))
+      ),
+      React.createElement(
+        "a",
+        { className: "topic-read-more", href: read_more_endpoint },
+        React.createElement(FormattedMessage, { id: "pocket_read_even_more" }),
+        React.createElement("span", { className: "topic-read-more-logo" })
+      )
+    );
+  }
+}
+
+module.exports = Topics;
+module.exports._unconnected = Topics;
+module.exports.Topic = Topic;
+
+/***/ }),
+/* 21 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+var _require = __webpack_require__(1);
 
 const at = _require.actionTypes,
       ac = _require.actionCreators;
 
 const shortURL = __webpack_require__(4);
 
 /**
  * List of functions that return items that can be included as menu options in a
@@ -1519,17 +2189,17 @@ module.exports = {
     userEvent: "SAVE_TO_POCKET"
   })
 };
 
 module.exports.CheckBookmark = site => site.bookmarkGuid ? module.exports.RemoveBookmark(site) : module.exports.AddBookmark(site);
 module.exports.CheckPinTopSite = (site, index) => site.isPinned ? module.exports.UnpinTopSite(site) : module.exports.PinTopSite(site, index);
 
 /***/ }),
-/* 17 */
+/* 22 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 /* globals Services */
 
 
 let usablePerfObj;
 
@@ -1609,63 +2279,108 @@ var _PerfService = function _PerfService
    * @return {Number}       the returned start time, as a DOMHighResTimeStamp
    *
    * @throws {Error}        "No Marks with the name ..." if none are available
    */
   getMostRecentAbsMarkStartByName(name) {
     let entries = this.getEntriesByName(name, "mark");
 
     if (!entries.length) {
-      throw new Error(`No marks with the name ${name}`);
+      throw new Error(`No marks with the name ${ name }`);
     }
 
     let mostRecentEntry = entries[entries.length - 1];
     return this._perf.timeOrigin + mostRecentEntry.startTime;
   }
 };
 
 var perfService = new _PerfService();
 module.exports = {
   _PerfService,
   perfService
 };
 
 /***/ }),
-/* 18 */
+/* 23 */
+/***/ (function(module, exports) {
+
+var g;
+
+// This works in non-strict mode
+g = (function() {
+	return this;
+})();
+
+try {
+	// This works if eval is allowed (see CSP)
+	g = g || Function("return this")() || (1,eval)("this");
+} catch(e) {
+	// This works if the window reference is available
+	if(typeof window === "object")
+		g = window;
+}
+
+// g can still be undefined, but nothing to do about it...
+// We return undefined, instead of nothing here, so it's
+// easier to handle this case. if(!global) { ...}
+
+module.exports = g;
+
+
+/***/ }),
+/* 24 */
 /***/ (function(module, exports) {
 
 module.exports = Redux;
 
 /***/ }),
-/* 19 */
+/* 25 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
-const React = __webpack_require__(1);
-const ReactDOM = __webpack_require__(9);
-const Base = __webpack_require__(5);
+const React = __webpack_require__(0);
+const ReactDOM = __webpack_require__(11);
+const Base = __webpack_require__(6);
 
-var _require = __webpack_require__(2);
+var _require = __webpack_require__(3);
 
 const Provider = _require.Provider;
 
-const initStore = __webpack_require__(7);
+const initStore = __webpack_require__(8);
 
-var _require2 = __webpack_require__(8);
+var _require2 = __webpack_require__(10);
 
 const reducers = _require2.reducers;
 
-const DetectUserSessionStart = __webpack_require__(6);
+const DetectUserSessionStart = __webpack_require__(7);
+
+var _require3 = __webpack_require__(9);
+
+const SnippetsProvider = _require3.SnippetsProvider;
+
 
 new DetectUserSessionStart().sendEventOrAddListener();
 
 const store = initStore(reducers);
 
 ReactDOM.render(React.createElement(
   Provider,
   { store: store },
   React.createElement(Base, null)
 ), document.getElementById("root"));
 
+// Trigger snippets when snippets data has been received.
+const snippets = new SnippetsProvider();
+const unsubscribe = store.subscribe(() => {
+  const state = store.getState();
+  if (state.Snippets.initialized) {
+    snippets.init({
+      snippetsURL: state.Snippets.snippetsURL,
+      version: state.Snippets.version
+    });
+    unsubscribe();
+  }
+});
+
 /***/ })
 /******/ ]);
\ No newline at end of file
--- a/browser/extensions/activity-stream/data/content/activity-stream.css
+++ b/browser/extensions/activity-stream/data/content/activity-stream.css
@@ -1,8 +1,9 @@
+@charset "UTF-8";
 html {
   box-sizing: border-box; }
 
 *,
 *::before,
 *::after {
   box-sizing: inherit; }
 
@@ -25,16 +26,18 @@ input {
   width: 16px;
   height: 16px;
   background-size: 16px;
   background-position: center center;
   background-repeat: no-repeat;
   vertical-align: middle; }
   .icon.icon-spacer {
     margin-inline-end: 8px; }
+  .icon.icon-small-spacer {
+    margin-inline-end: 6px; }
   .icon.icon-bookmark {
     background-image: url("assets/glyph-bookmark-16.svg"); }
   .icon.icon-bookmark-remove {
     background-image: url("assets/glyph-bookmark-remove-16.svg"); }
   .icon.icon-delete {
     background-image: url("assets/glyph-delete-16.svg"); }
   .icon.icon-dismiss {
     background-image: url("assets/glyph-dismiss-16.svg"); }
@@ -45,21 +48,29 @@ input {
   .icon.icon-settings {
     background-image: url("assets/glyph-settings-16.svg"); }
   .icon.icon-pin {
     background-image: url("assets/glyph-pin-16.svg"); }
   .icon.icon-unpin {
     background-image: url("assets/glyph-unpin-16.svg"); }
   .icon.icon-pocket {
     background-image: url("assets/glyph-pocket-16.svg"); }
+  .icon.icon-historyItem {
+    background-image: url("assets/glyph-historyItem-16.svg"); }
+  .icon.icon-trending {
+    background-image: url("assets/glyph-trending-16.svg"); }
+  .icon.icon-now {
+    background-image: url("assets/glyph-now-16.svg"); }
   .icon.icon-pin-small {
     background-image: url("assets/glyph-pin-12.svg");
     background-size: 12px;
     height: 12px;
     width: 12px; }
+  .icon.icon-check {
+    background-image: url("chrome://browser/skin/check.svg"); }
 
 html,
 body,
 #root {
   height: 100%; }
 
 body {
   background: #F6F6F8;
@@ -129,32 +140,45 @@ a {
       box-shadow: 0 0 0 5px rgba(0, 0, 0, 0.1);
       transition: box-shadow 150ms; }
     .actions button.done {
       background: #0695F9;
       border: solid 1px #1677CF;
       color: #FFF;
       margin-inline-start: auto; }
 
+#snippets-container {
+  display: none;
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  background: white;
+  height: 122px; }
+
+#snippets {
+  max-width: 736px;
+  margin: 0 auto; }
+
 .outer-wrapper {
   display: flex;
   flex-grow: 1;
   padding: 62px 32px 32px;
   height: 100%; }
 
 main {
   margin: auto; }
   @media (min-width: 672px) {
     main {
       width: 608px; } }
   @media (min-width: 800px) {
     main {
       width: 736px; } }
   main section {
-    margin-bottom: 41px; }
+    margin-bottom: 40px; }
 
 .section-title {
   color: #6E707E;
   font-size: 13px;
   font-weight: bold;
   text-transform: uppercase;
   margin: 0 0 18px; }
 
@@ -200,20 +224,20 @@ main {
       transform: scale(0.25);
       opacity: 0;
       transition-property: transform, opacity;
       transition-duration: 200ms;
       z-index: 399; }
       .top-sites-list .top-site-outer .context-menu-button:focus, .top-sites-list .top-site-outer .context-menu-button:active {
         transform: scale(1);
         opacity: 1; }
-    .top-sites-list .top-site-outer:hover .tile, .top-sites-list .top-site-outer:active .tile, .top-sites-list .top-site-outer:focus .tile, .top-sites-list .top-site-outer.active .tile {
+    .top-sites-list .top-site-outer:hover .tile, .top-sites-list .top-site-outer:focus .tile, .top-sites-list .top-site-outer.active .tile {
       box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1), 0 0 0 5px rgba(0, 0, 0, 0.1);
       transition: box-shadow 150ms; }
-    .top-sites-list .top-site-outer:hover .context-menu-button, .top-sites-list .top-site-outer:active .context-menu-button, .top-sites-list .top-site-outer:focus .context-menu-button, .top-sites-list .top-site-outer.active .context-menu-button {
+    .top-sites-list .top-site-outer:hover .context-menu-button, .top-sites-list .top-site-outer:focus .context-menu-button, .top-sites-list .top-site-outer.active .context-menu-button {
       transform: scale(1);
       opacity: 1; }
     .top-sites-list .top-site-outer .tile {
       position: relative;
       height: 96px;
       width: 96px;
       border-radius: 6px;
       box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
@@ -253,16 +277,127 @@ main {
       .top-sites-list .top-site-outer .title span {
         display: block;
         overflow: hidden;
         text-overflow: ellipsis;
         white-space: nowrap; }
       .top-sites-list .top-site-outer .title.pinned span {
         padding: 0 13px; }
 
+.sections-list .section-top-bar {
+  position: relative;
+  height: 16px;
+  margin-bottom: 18px; }
+  .sections-list .section-top-bar .section-title {
+    float: left; }
+  .sections-list .section-top-bar .section-info-option {
+    float: right; }
+  .sections-list .section-top-bar .info-option-icon {
+    background-image: url("assets/glyph-info-option-12.svg");
+    background-size: 12px 12px;
+    background-repeat: no-repeat;
+    background-position: center;
+    height: 16px;
+    width: 16px;
+    display: inline-block; }
+  .sections-list .section-top-bar .section-info-option div {
+    visibility: hidden;
+    opacity: 0;
+    transition: visibility 0.2s, opacity 0.2s ease-out;
+    transition-delay: 0.5s; }
+  .sections-list .section-top-bar .section-info-option:hover div {
+    visibility: visible;
+    opacity: 1;
+    transition: visibility 0.2s, opacity 0.2s ease-out; }
+  .sections-list .section-top-bar .info-option {
+    z-index: 9999;
+    position: absolute;
+    background: #FFF;
+    border: solid 1px rgba(0, 0, 0, 0.1);
+    border-radius: 3px;
+    font-size: 13px;
+    color: #0C0C0D;
+    line-height: 120%;
+    width: 320px;
+    right: 0;
+    top: 34px;
+    margin-top: -4px;
+    margin-right: -4px;
+    padding: 24px;
+    -moz-user-select: none; }
+  .sections-list .section-top-bar .info-option-header {
+    font-size: 15px;
+    font-weight: 600; }
+  .sections-list .section-top-bar .info-option-body {
+    margin: 0;
+    margin-top: 12px; }
+  .sections-list .section-top-bar .info-option-link {
+    display: block;
+    margin-top: 12px;
+    color: #0A84FF; }
+
+.sections-list .section-list {
+  width: 768px;
+  clear: both;
+  margin: 0; }
+
+.sections-list .section-empty-state {
+  width: 100%;
+  height: 266px;
+  display: flex;
+  border: solid 1px rgba(0, 0, 0, 0.1);
+  border-radius: 3px; }
+  .sections-list .section-empty-state .empty-state {
+    margin: auto;
+    max-width: 350px; }
+    .sections-list .section-empty-state .empty-state .empty-state-icon {
+      background-size: 50px 50px;
+      background-repeat: no-repeat;
+      background-position: center;
+      fill: rgba(160, 160, 160, 0.4);
+      -moz-context-properties: fill;
+      height: 50px;
+      width: 50px;
+      margin: 0 auto;
+      display: block; }
+    .sections-list .section-empty-state .empty-state .empty-state-message {
+      margin-bottom: 0;
+      font-size: 13px;
+      font-weight: 300;
+      color: #A0A0A0;
+      text-align: center; }
+
+.topic {
+  font-size: 13px;
+  color: #BFC0C7;
+  min-width: 780px; }
+  .topic ul {
+    display: inline;
+    padding-left: 12px; }
+  .topic ul li {
+    display: inline; }
+  .topic ul li::after {
+    content: '•';
+    padding-left: 8px;
+    padding-right: 8px; }
+  .topic ul li:last-child::after {
+    content: none; }
+  .topic .topic-link {
+    color: #008EA4; }
+  .topic .topic-read-more {
+    float: right;
+    margin-right: 40px;
+    color: #008EA4; }
+  .topic .topic-read-more-logo {
+    padding-right: 10px;
+    margin-left: 5px;
+    background-image: url("assets/topic-show-more-12.svg");
+    background-repeat: no-repeat;
+    background-position-y: 2px; }
+
 .search-wrapper {
   cursor: default;
   display: flex;
   position: relative;
   margin: 0 0 48px;
   width: 100%;
   height: 36px; }
   .search-wrapper input {
@@ -511,8 +646,114 @@ main {
   z-index: 11001; }
 
 .modal {
   background: #FFF;
   border: solid 1px rgba(0, 0, 0, 0.1);
   border-radius: 3px;
   font-size: 14px;
   z-index: 11002; }
+
+.card-outer {
+  background: #FFF;
+  display: inline-block;
+  margin-inline-end: 32px;
+  margin-bottom: 16px;
+  width: 224px;
+  border-radius: 3px;
+  border-color: rgba(0, 0, 0, 0.1);
+  height: 266px;
+  position: relative; }
+  .card-outer .context-menu-button {
+    cursor: pointer;
+    position: absolute;
+    top: -13.5px;
+    offset-inline-end: -13.5px;
+    width: 27px;
+    height: 27px;
+    background-color: #FFF;
+    background-image: url("assets/glyph-more-16.svg");
+    background-position: 65%;
+    background-repeat: no-repeat;
+    background-clip: padding-box;
+    border: 1px solid rgba(0, 0, 0, 0.2);
+    border-radius: 100%;
+    box-shadow: 0 2px 0 rgba(0, 0, 0, 0.1);
+    transform: scale(0.25);
+    opacity: 0;
+    transition-property: transform, opacity;
+    transition-duration: 200ms;
+    z-index: 399; }
+    .card-outer .context-menu-button:focus, .card-outer .context-menu-button:active {
+      transform: scale(1);
+      opacity: 1; }
+  .card-outer .card {
+    height: 100%;
+    border-radius: 3px; }
+  .card-outer > a {
+    display: block;
+    color: inherit;
+    height: 100%;
+    outline: none;
+    position: absolute; }
+    .card-outer > a.active .card, .card-outer > a:focus .card {
+      box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1), 0 0 0 5px rgba(0, 0, 0, 0.1);
+      transition: box-shadow 150ms; }
+  .card-outer:hover, .card-outer:focus, .card-outer.active {
+    outline: none;
+    box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1), 0 0 0 5px rgba(0, 0, 0, 0.1);
+    transition: box-shadow 150ms; }
+    .card-outer:hover .context-menu-button, .card-outer:focus .context-menu-button, .card-outer.active .context-menu-button {
+      transform: scale(1);
+      opacity: 1; }
+  .card-outer .card-preview-image {
+    position: relative;
+    background-size: cover;
+    background-position: center;
+    background-repeat: no-repeat;
+    height: 122px;
+    border-bottom-color: rgba(0, 0, 0, 0.1);
+    border-bottom-style: solid;
+    border-bottom-width: 1px;
+    border-radius: 3px 3px 0 0; }
+  .card-outer .card-details {
+    padding: 10px 16px 12px; }
+  .card-outer .card-text {
+    overflow: hidden;
+    max-height: 78px; }
+    .card-outer .card-text.full-height {
+      max-height: 200px; }
+  .card-outer .card-host-name {
+    color: #858585;
+    font-size: 10px;
+    padding-bottom: 4px;
+    text-transform: uppercase; }
+  .card-outer .card-title {
+    margin: 0 0 2px;
+    font-size: inherit;
+    word-wrap: break-word; }
+  .card-outer .card-description {
+    font-size: 12px;
+    margin: 0;
+    word-wrap: break-word;
+    overflow: hidden;
+    line-height: 18px;
+    max-height: 34px; }
+  .card-outer .card-context {
+    padding: 16px 16px 14px 14px;
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    color: #A0A0A0;
+    font-size: 11px;
+    display: flex;
+    align-items: center; }
+  .card-outer .card-context-icon {
+    opacity: 0.5;
+    font-size: 13px;
+    margin-inline-end: 6px;
+    display: block; }
+  .card-outer .card-context-label {
+    flex-grow: 1;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap; }
--- a/browser/extensions/activity-stream/data/content/activity-stream.html
+++ b/browser/extensions/activity-stream/data/content/activity-stream.html
@@ -3,16 +3,20 @@
   <head>
     <meta charset="utf-8">
     <title></title>
     <link rel="stylesheet" href="chrome://browser/content/contentSearchUI.css" />
     <link rel="stylesheet" href="resource://activity-stream/data/content/activity-stream.css" />
   </head>
   <body class="activity-stream">
     <div id="root"></div>
+    <div id="snippets-container">
+      <div id="topSection"></div> <!-- TODO: placeholder for v4 snippets. It should be removed when we switch to v5 -->
+      <div id="snippets"></div>
+    </div>
     <script src="chrome://browser/content/contentSearchUI.js"></script>
     <script src="resource://activity-stream/vendor/react.js"></script>
     <script src="resource://activity-stream/vendor/react-dom.js"></script>
     <script src="resource://activity-stream/vendor/react-intl.js"></script>
     <script src="resource://activity-stream/vendor/redux.js"></script>
     <script src="resource://activity-stream/vendor/react-redux.js"></script>
     <script src="resource://activity-stream/data/content/activity-stream.bundle.js"></script>
   </body>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/data/content/assets/glyph-historyItem-16.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+  <path fill="#4d4d4d" d="M365,190a4,4,0,1,1,4-4A4,4,0,0,1,365,190Zm0-6a2,2,0,1,0,2,2A2,2,0,0,0,365,184Z" transform="translate(-357 -178)"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/data/content/assets/glyph-info-option-12.svg
@@ -0,0 +1,1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12"><path fill="#999" d="M6 0a6 6 0 1 0 6 6 6 6 0 0 0-6-6zm.7 10.26a1.13 1.13 0 0 1-.78.28 1.13 1.13 0 0 1-.78-.28 1 1 0 0 1 0-1.42 1.13 1.13 0 0 1 .78-.28 1.13 1.13 0 0 1 .78.28 1 1 0 0 1 0 1.42zM8.55 5a3 3 0 0 1-.62.81l-.67.63a1.58 1.58 0 0 0-.4.57 2.24 2.24 0 0 0-.12.74H5.06a3.82 3.82 0 0 1 .19-1.35 2.11 2.11 0 0 1 .63-.86 4.17 4.17 0 0 0 .66-.67 1.09 1.09 0 0 0 .23-.67.73.73 0 0 0-.77-.86.71.71 0 0 0-.57.26 1.1 1.1 0 0 0-.23.7h-2A2.36 2.36 0 0 1 4 2.47a2.94 2.94 0 0 1 2-.65 3.06 3.06 0 0 1 2 .6 2.12 2.12 0 0 1 .72 1.72 2 2 0 0 1-.17.86z"/></svg>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/data/content/assets/glyph-now-16.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+  <path fill="#4d4d4d" d="M8 0a8 8 0 1 0 8 8 8.009 8.009 0 0 0-8-8zm0 14a6 6 0 1 1 6-6 6.007 6.007 0 0 1-6 6zm3.5-6H8V4.5a.5.5 0 0 0-1 0v4a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 0-1z"/>
+</svg>
--- a/browser/extensions/activity-stream/data/content/assets/glyph-pocket-16.svg
+++ b/browser/extensions/activity-stream/data/content/assets/glyph-pocket-16.svg
@@ -1,6 +1,6 @@
 <!-- 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/. -->
 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
-  <path fill="context-fill" d="M8 15a8 8 0 0 1-8-8V3a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v4a8 8 0 0 1-8 8zm3.985-10.032a.99.99 0 0 0-.725.319L7.978 8.57 4.755 5.336A.984.984 0 0 0 4 4.968a1 1 0 0 0-.714 1.7l-.016.011 3.293 3.306.707.707a1 1 0 0 0 1.414 0l.707-.707L12.7 6.679a1 1 0 0 0-.715-1.711z"/>
-</svg>
\ No newline at end of file
+  <path fill="#4d4d4d" d="M8 15a8 8 0 0 1-8-8V3a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v4a8 8 0 0 1-8 8zm3.985-10.032a.99.99 0 0 0-.725.319L7.978 8.57 4.755 5.336A.984.984 0 0 0 4 4.968a1 1 0 0 0-.714 1.7l-.016.011 3.293 3.306.707.707a1 1 0 0 0 1.414 0l.707-.707L12.7 6.679a1 1 0 0 0-.715-1.711z"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/data/content/assets/glyph-trending-16.svg
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="Context-/-Pocket-Trending" fill="#999999">
+            <path d="M12.164765,5.74981818 C12.4404792,5.74981818 12.5976221,6.06981818 12.4233364,6.28509091 C10.7404792,8.37236364 4.26619353,15.6829091 4.15905067,15.744 C5.70047924,12.3301818 7.1276221,8.976 7.1276221,8.976 L4.3276221,8.976 C4.09905067,8.976 3.9376221,8.74472727 4.02333638,8.52654545 C4.70047924,6.77672727 6.86190781,1.32945455 7.30476495,0.216727273 C7.35333638,0.0916363636 7.46190781,0.0174545455 7.59476495,0.016 C8.32476495,0.0130909091 10.7904792,0.00290909091 12.5790507,0 C12.844765,0 12.9976221,0.305454545 12.8433364,0.525090909 L9.17190781,5.74981818 L12.164765,5.74981818 Z" id="Fill-1"></path>
+        </g>
+    </g>
+</svg>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/data/content/assets/topic-show-more-12.svg
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="12px" height="12px" viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
+    <title>Icon / &gt;</title>
+    <desc>Created with Sketch.</desc>
+    <defs></defs>
+    <g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
+        <g id="Icon-/-&gt;" stroke-width="2" stroke="#008EA4">
+            <polyline id="Path-2" points="4 2 8 6 4 10"></polyline>
+        </g>
+    </g>
+</svg>
\ No newline at end of file
--- a/browser/extensions/activity-stream/data/locales.json
+++ b/browser/extensions/activity-stream/data/locales.json
@@ -1020,16 +1020,17 @@
   },
   "en-US": {
     "newtab_page_title": "New Tab",
     "default_label_loading": "Loading…",
     "header_top_sites": "Top Sites",
     "header_stories": "Top Stories",
     "header_visit_again": "Visit Again",
     "header_bookmarks": "Recent Bookmarks",
+    "header_recommended_by": "Recommended by {provider}",
     "header_bookmarks_placeholder": "You don’t have any bookmarks yet.",
     "header_stories_from": "from",
     "type_label_visited": "Visited",
     "type_label_bookmarked": "Bookmarked",
     "type_label_synced": "Synced from another device",
     "type_label_recommended": "Trending",
     "type_label_open": "Open",
     "type_label_topic": "Topic",
@@ -1046,16 +1047,17 @@
     "confirm_history_delete_p1": "Are you sure you want to delete every instance of this page from your history?",
     "confirm_history_delete_notice_p2": "This action cannot be undone.",
     "menu_action_save_to_pocket": "Save to Pocket",
     "search_for_something_with": "Search for {search_term} with:",
     "search_button": "Search",
     "search_header": "{search_engine_name} Search",
     "search_web_placeholder": "Search the Web",
     "search_settings": "Change Search Settings",
+    "section_info_option": "Info",
     "welcome_title": "Welcome to new tab",
     "welcome_body": "Firefox will use this space to show your most relevant bookmarks, articles, videos, and pages you’ve recently visited, so you can get back to them easily.",
     "welcome_label": "Identifying your Highlights",
     "time_label_less_than_minute": "<1m",
     "time_label_minute": "{number}m",
     "time_label_hour": "{number}h",
     "time_label_day": "{number}d",
     "settings_pane_button_label": "Customize your New Tab page",
@@ -1090,17 +1092,18 @@
     "topsites_form_add_button": "Add",
     "topsites_form_save_button": "Save",
     "topsites_form_cancel_button": "Cancel",
     "topsites_form_url_validation": "Valid URL required",
     "pocket_read_more": "Popular Topics:",
     "pocket_read_even_more": "View More Stories",
     "pocket_feedback_header": "The best of the web, curated by over 25 million people.",
     "pocket_feedback_body": "Pocket, a part of the Mozilla family, will help connect you to high-quality content that you may not have found otherwise.",
-    "pocket_send_feedback": "Send Feedback"
+    "pocket_send_feedback": "Send Feedback",
+    "empty_state_topstories": "You’ve caught up. Check back later for more top stories from Pocket. Can’t wait? Select a popular topic to find more great stories from around the web."
   },
   "en-ZA": {},
   "eo": {
     "newtab_page_title": "Nova legosigno",
     "default_label_loading": "Ŝargado…",
     "header_top_sites": "Plej vizititaj",
     "header_highlights": "Elstaraĵoj",
     "type_label_visited": "Vizititaj",
--- a/browser/extensions/activity-stream/lib/ActivityStream.jsm
+++ b/browser/extensions/activity-stream/lib/ActivityStream.jsm
@@ -9,21 +9,44 @@ const {utils: Cu} = Components;
 // common case to avoid the overhead of wrapping and detecting lazy loading.
 const {actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
 const {DefaultPrefs} = Cu.import("resource://activity-stream/lib/ActivityStreamPrefs.jsm", {});
 const {LocalizationFeed} = Cu.import("resource://activity-stream/lib/LocalizationFeed.jsm", {});
 const {NewTabInit} = Cu.import("resource://activity-stream/lib/NewTabInit.jsm", {});
 const {PlacesFeed} = Cu.import("resource://activity-stream/lib/PlacesFeed.jsm", {});
 const {PrefsFeed} = Cu.import("resource://activity-stream/lib/PrefsFeed.jsm", {});
 const {Store} = Cu.import("resource://activity-stream/lib/Store.jsm", {});
+const {SnippetsFeed} = Cu.import("resource://activity-stream/lib/SnippetsFeed.jsm", {});
+const {SystemTickFeed} = Cu.import("resource://activity-stream/lib/SystemTickFeed.jsm", {});
 const {TelemetryFeed} = Cu.import("resource://activity-stream/lib/TelemetryFeed.jsm", {});
 const {TopSitesFeed} = Cu.import("resource://activity-stream/lib/TopSitesFeed.jsm", {});
+const {TopStoriesFeed} = Cu.import("resource://activity-stream/lib/TopStoriesFeed.jsm", {});
 
 const REASON_ADDON_UNINSTALL = 6;
 
+// Sections, keyed by section id
+const SECTIONS = new Map([
+  ["topstories", {
+    feed: TopStoriesFeed,
+    prefTitle: "Fetches content recommendations from a configurable content provider",
+    showByDefault: false
+  }]
+]);
+
+const SECTION_FEEDS_CONFIG = Array.from(SECTIONS.entries()).map(entry => {
+  const id = entry[0];
+  const {feed: Feed, prefTitle, showByDefault: value} = entry[1];
+  return {
+    name: `section.${id}`,
+    factory: () => new Feed(),
+    title: prefTitle || `${id} section feed`,
+    value
+  };
+});
+
 const PREFS_CONFIG = new Map([
   ["default.sites", {
     title: "Comma-separated list of default top sites to fill in behind visited sites",
     value: "https://www.facebook.com/,https://www.youtube.com/,https://www.amazon.com/,https://www.yahoo.com/,https://www.ebay.com/,https://twitter.com/"
   }],
   ["showSearch", {
     title: "Show the Search bar on the New Tab page",
     value: true
@@ -40,21 +63,34 @@ const PREFS_CONFIG = new Map([
   ["telemetry.log", {
     title: "Log telemetry events in the console",
     value: false,
     value_local_dev: true
   }],
   ["telemetry.ping.endpoint", {
     title: "Telemetry server endpoint",
     value: "https://tiles.services.mozilla.com/v4/links/activity-stream"
+  }],
+  ["feeds.section.topstories.options", {
+    title: "Configuration options for top stories feed",
+    value: `{
+      "stories_endpoint": "https://getpocket.com/v3/firefox/global-recs?consumer_key=$apiKey",
+      "topics_endpoint": "https://getpocket.com/v3/firefox/trending-topics?consumer_key=$apiKey",
+      "read_more_endpoint": "https://getpocket.com/explore/trending?src=ff_new_tab",
+      "learn_more_endpoint": "https://getpocket.com/firefox_learnmore?src=ff_newtab",
+      "survey_link": "https://www.surveymonkey.com/r/newtabffx",
+      "api_key_pref": "extensions.pocket.oAuthConsumerKey",
+      "provider_name": "Pocket",
+      "provider_icon": "pocket"
+    }`
   }]
 ]);
 
 const FEEDS_CONFIG = new Map();
-for (const {name, factory, title, value} of [
+for (const {name, factory, title, value} of SECTION_FEEDS_CONFIG.concat([
   {
     name: "localization",
     factory: () => new LocalizationFeed(),
     title: "Initialize strings and detect locale for Activity Stream",
     value: true
   },
   {
     name: "newtabinit",
@@ -70,28 +106,40 @@ for (const {name, factory, title, value}
   },
   {
     name: "prefs",
     factory: () => new PrefsFeed(PREFS_CONFIG),
     title: "Preferences",
     value: true
   },
   {
+    name: "snippets",
+    factory: () => new SnippetsFeed(),
+    title: "Gets snippets data",
+    value: false
+  },
+  {
+    name: "systemtick",
+    factory: () => new SystemTickFeed(),
+    title: "Produces system tick events to periodically check for data expiry",
+    value: true
+  },
+  {
     name: "telemetry",
     factory: () => new TelemetryFeed(),
     title: "Relays telemetry-related actions to TelemetrySender",
     value: true
   },
   {
     name: "topsites",
     factory: () => new TopSitesFeed(),
     title: "Queries places and gets metadata for Top Sites section",
     value: true
   }
-]) {
+])) {
   const pref = `feeds.${name}`;
   FEEDS_CONFIG.set(pref, factory);
   PREFS_CONFIG.set(pref, {title, value});
 }
 
 this.ActivityStream = class ActivityStream {
 
   /**
@@ -130,9 +178,9 @@ this.ActivityStream = class ActivityStre
       // so we DON'T want to do this on an upgrade/downgrade, only on a
       // real uninstall
       this._defaultPrefs.reset();
     }
   }
 };
 
 this.PREFS_CONFIG = PREFS_CONFIG;
-this.EXPORTED_SYMBOLS = ["ActivityStream"];
+this.EXPORTED_SYMBOLS = ["ActivityStream", "SECTIONS"];
--- a/browser/extensions/activity-stream/lib/PlacesFeed.jsm
+++ b/browser/extensions/activity-stream/lib/PlacesFeed.jsm
@@ -8,16 +8,18 @@ Cu.import("resource://gre/modules/Servic
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 const {actionCreators: ac, actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
 
 XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
   "resource://gre/modules/NewTabUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
   "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Pocket",
+  "chrome://pocket/content/Pocket.jsm");
 
 const LINK_BLOCKED_EVENT = "newtab-linkBlocked";
 
 /**
  * Observer - a wrapper around history/bookmark observers to add the QueryInterface.
  */
 class Observer {
   constructor(dispatch, observerInterface) {
@@ -200,16 +202,19 @@ class PlacesFeed {
         NewTabUtils.activityStreamLinks.addBookmark(action.data);
         break;
       case at.DELETE_BOOKMARK_BY_ID:
         NewTabUtils.activityStreamLinks.deleteBookmark(action.data);
         break;
       case at.DELETE_HISTORY_URL:
         NewTabUtils.activityStreamLinks.deleteHistoryEntry(action.data);
         break;
+      case at.SAVE_TO_POCKET:
+        Pocket.savePage(action._target.browser, action.data.site.url, action.data.site.title);
+        break;
     }
   }
 }
 
 this.PlacesFeed = PlacesFeed;
 
 // Exported for testing only
 PlacesFeed.HistoryObserver = HistoryObserver;
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/lib/SnippetsFeed.jsm
@@ -0,0 +1,58 @@
+/* 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/. */
+"use strict";
+
+const {utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Console.jsm");
+const {actionTypes: at, actionCreators: ac} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
+
+// Url to fetch snippets, in the urlFormatter service format.
+const SNIPPETS_URL_PREF = "browser.aboutHomeSnippets.updateUrl";
+
+// Should be bumped up if the snippets content format changes.
+const STARTPAGE_VERSION = 4;
+
+this.SnippetsFeed = class SnippetsFeed {
+  constructor() {
+    this._onUrlChange = this._onUrlChange.bind(this);
+  }
+  get snippetsURL() {
+    const updateURL = Services
+      .prefs.getStringPref(SNIPPETS_URL_PREF)
+      .replace("%STARTPAGE_VERSION%", STARTPAGE_VERSION);
+    return Services.urlFormatter.formatURL(updateURL);
+  }
+  init() {
+    const data = {
+      snippetsURL: this.snippetsURL,
+      version: STARTPAGE_VERSION
+    };
+    this.store.dispatch(ac.BroadcastToContent({type: at.SNIPPETS_DATA, data}));
+    Services.prefs.addObserver(SNIPPETS_URL_PREF, this._onUrlChange);
+  }
+  uninit() {
+    this.store.dispatch({type: at.SNIPPETS_RESET});
+    Services.prefs.removeObserver(SNIPPETS_URL_PREF, this._onUrlChange);
+  }
+  _onUrlChange() {
+    this.store.dispatch(ac.BroadcastToContent({
+      type: at.SNIPPETS_DATA,
+      data: {snippetsURL: this.snippetsURL}
+    }));
+  }
+  onAction(action) {
+    switch (action.type) {
+      case at.INIT:
+        this.init();
+        break;
+      case at.FEED_INIT:
+        if (action.data === "feeds.snippets") { this.init(); }
+        break;
+    }
+  }
+};
+
+this.EXPORTED_SYMBOLS = ["SnippetsFeed"];
--- a/browser/extensions/activity-stream/lib/Store.jsm
+++ b/browser/extensions/activity-stream/lib/Store.jsm
@@ -4,16 +4,17 @@
 "use strict";
 
 const {utils: Cu} = Components;
 
 const {ActivityStreamMessageChannel} = Cu.import("resource://activity-stream/lib/ActivityStreamMessageChannel.jsm", {});
 const {Prefs} = Cu.import("resource://activity-stream/lib/ActivityStreamPrefs.jsm", {});
 const {reducers} = Cu.import("resource://activity-stream/common/Reducers.jsm", {});
 const {redux} = Cu.import("resource://activity-stream/vendor/Redux.jsm", {});
+const {actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
 
 /**
  * Store - This has a similar structure to a redux store, but includes some extra
  *         functionality to allow for routing of actions between the Main processes
  *         and child processes via a ActivityStreamMessageChannel.
  *         It also accepts an array of "Feeds" on inititalization, which
  *         can listen for any action that is dispatched through the store.
  */
@@ -86,16 +87,17 @@ this.Store = class Store {
 
   /**
    * onPrefChanged - Listener for handling feed changes.
    */
   onPrefChanged(name, value) {
     if (this._feedFactories.has(name)) {
       if (value) {
         this.initFeed(name);
+        this.dispatch({type: at.FEED_INIT, data: name});
       } else {
         this.uninitFeed(name);
       }
     }
   }
 
   /**
    * init - Initializes the ActivityStreamMessageChannel channel, and adds feeds.
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/lib/SystemTickFeed.jsm
@@ -0,0 +1,35 @@
+/* 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/. */
+"use strict";
+
+const {utils: Cu} = Components;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const {actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
+
+XPCOMUtils.defineLazyModuleGetter(this, "setInterval", "resource://gre/modules/Timer.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "clearInterval", "resource://gre/modules/Timer.jsm");
+
+// Frequency at which SYSTEM_TICK events are fired
+const SYSTEM_TICK_INTERVAL = 5 * 60 * 1000;
+
+this.SystemTickFeed = class SystemTickFeed {
+  init() {
+    this.intervalId = setInterval(() => this.store.dispatch({type: at.SYSTEM_TICK}), SYSTEM_TICK_INTERVAL);
+  }
+
+  onAction(action) {
+    switch (action.type) {
+      case at.INIT:
+        this.init();
+        break;
+      case at.UNINIT:
+        clearInterval(this.intervalId);
+        break;
+    }
+  }
+};
+
+this.SYSTEM_TICK_INTERVAL = SYSTEM_TICK_INTERVAL;
+this.EXPORTED_SYMBOLS = ["SystemTickFeed", "SYSTEM_TICK_INTERVAL"];
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/lib/TopStoriesFeed.jsm
@@ -0,0 +1,187 @@
+/* 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/. */
+"use strict";
+
+const {utils: Cu} = Components;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/NewTabUtils.jsm");
+Cu.importGlobalProperties(["fetch"]);
+
+const {actionCreators: ac, actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
+const {Prefs} = Cu.import("resource://activity-stream/lib/ActivityStreamPrefs.jsm", {});
+
+const STORIES_UPDATE_TIME = 30 * 60 * 1000; // 30 minutes
+const TOPICS_UPDATE_TIME = 3 * 60 * 60 * 1000; // 3 hours
+const SECTION_ID = "TopStories";
+
+this.TopStoriesFeed = class TopStoriesFeed {
+  constructor() {
+    this.storiesLastUpdated = 0;
+    this.topicsLastUpdated = 0;
+  }
+
+  init() {
+    try {
+      const prefs = new Prefs();
+      const options = JSON.parse(prefs.get("feeds.section.topstories.options"));
+      const apiKey = this._getApiKeyFromPref(options.api_key_pref);
+      this.stories_endpoint = this._produceUrlWithApiKey(options.stories_endpoint, apiKey);
+      this.topics_endpoint = this._produceUrlWithApiKey(options.topics_endpoint, apiKey);
+      this.read_more_endpoint = options.read_more_endpoint;
+
+      // TODO https://github.com/mozilla/activity-stream/issues/2902
+      const sectionOptions = {
+        id: SECTION_ID,
+        icon: options.provider_icon,
+        title: {id: "header_recommended_by", values: {provider: options.provider_name}},
+        rows: [],
+        maxCards: 3,
+        contextMenuOptions: ["SaveToPocket", "Separator", "CheckBookmark", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl"],
+        infoOption: {
+          header: {id: "pocket_feedback_header"},
+          body: {id: "pocket_feedback_body"},
+          link: {
+            href: options.survey_link,
+            id: "pocket_send_feedback"
+          }
+        },
+        emptyState: {
+          message: {id: "empty_state_topstories"},
+          icon: "check"
+        }
+      };
+      this.store.dispatch(ac.BroadcastToContent({type: at.SECTION_REGISTER, data: sectionOptions}));
+
+      this.fetchStories();
+      this.fetchTopics();
+    } catch (e) {
+      Cu.reportError(`Problem initializing top stories feed: ${e.message}`);
+    }
+  }
+
+  uninit() {
+    this.store.dispatch(ac.BroadcastToContent({type: at.SECTION_DEREGISTER, data: SECTION_ID}));
+  }
+
+  async fetchStories() {
+    if (this.stories_endpoint) {
+      const stories = await fetch(this.stories_endpoint)
+        .then(response => {
+          if (response.ok) {
+            return response.text();
+          }
+          throw new Error(`Stories endpoint returned unexpected status: ${response.status}`);
+        })
+        .then(body => {
+          let items = JSON.parse(body).list;
+          items = items
+            .filter(s => !NewTabUtils.blockedLinks.isBlocked(s.dedupe_url))
+            .map(s => ({
+              "guid": s.id,
+              "type": "trending",
+              "title": s.title,
+              "description": s.excerpt,
+              "image": this._normalizeUrl(s.image_src),
+              "url": s.dedupe_url,
+              "lastVisitDate": s.published_timestamp
+            }));
+          return items;
+        })
+        .catch(error => Cu.reportError(`Failed to fetch content: ${error.message}`));
+
+      if (stories) {
+        this.dispatchUpdateEvent(this.storiesLastUpdated,
+          {"type": at.SECTION_ROWS_UPDATE, "data": {"id": SECTION_ID, "rows": stories}});
+        this.storiesLastUpdated = Date.now();
+      }
+    }
+  }
+
+  async fetchTopics() {
+    if (this.topics_endpoint) {
+      const topics = await fetch(this.topics_endpoint)
+        .then(response => {
+          if (response.ok) {
+            return response.text();
+          }
+          throw new Error(`Topics endpoint returned unexpected status: ${response.status}`);
+        })
+        .then(body => JSON.parse(body).topics)
+        .catch(error => Cu.reportError(`Failed to fetch topics: ${error.message}`));
+
+      if (topics) {
+        this.dispatchUpdateEvent(this.topicsLastUpdated,
+          {"type": at.SECTION_ROWS_UPDATE, "data": {"id": SECTION_ID, "topics": topics, "read_more_endpoint": this.read_more_endpoint}});
+        this.topicsLastUpdated = Date.now();
+      }
+    }
+  }
+
+  dispatchUpdateEvent(lastUpdated, evt) {
+    if (lastUpdated === 0) {
+      this.store.dispatch(ac.BroadcastToContent(evt));
+    } else {
+      this.store.dispatch(evt);
+    }
+  }
+
+  _getApiKeyFromPref(apiKeyPref) {
+    if (!apiKeyPref) {
+      return apiKeyPref;
+    }
+
+    return new Prefs().get(apiKeyPref) || Services.prefs.getCharPref(apiKeyPref);
+  }
+
+  _produceUrlWithApiKey(url, apiKey) {
+    if (!url) {
+      return url;
+    }
+
+    if (url.includes("$apiKey") && !apiKey) {
+      throw new Error(`An API key was specified but none configured: ${url}`);
+    }
+
+    return url.replace("$apiKey", apiKey);
+  }
+
+  // Need to remove parenthesis from image URLs as React will otherwise
+  // fail to render them properly as part of the card template.
+  _normalizeUrl(url) {
+    if (url) {
+      return url.replace(/\(/g, "%28").replace(/\)/g, "%29");
+    }
+    return url;
+  }
+
+  onAction(action) {
+    switch (action.type) {
+      case at.INIT:
+        this.init();
+        break;
+      case at.SYSTEM_TICK:
+        if (Date.now() - this.storiesLastUpdated >= STORIES_UPDATE_TIME) {
+          this.fetchStories();
+        }
+        if (Date.now() - this.topicsLastUpdated >= TOPICS_UPDATE_TIME) {
+          this.fetchTopics();
+        }
+        break;
+      case at.UNINIT:
+        this.uninit();
+        break;
+      case at.FEED_INIT:
+        if (action.data === "feeds.section.topstories") {
+          this.init();
+        }
+        break;
+    }
+  }
+};
+
+this.STORIES_UPDATE_TIME = STORIES_UPDATE_TIME;
+this.TOPICS_UPDATE_TIME = TOPICS_UPDATE_TIME;
+this.SECTION_ID = SECTION_ID;
+this.EXPORTED_SYMBOLS = ["TopStoriesFeed", "STORIES_UPDATE_TIME", "TOPICS_UPDATE_TIME", "SECTION_ID"];
--- a/browser/extensions/activity-stream/test/functional/mochitest/browser.ini
+++ b/browser/extensions/activity-stream/test/functional/mochitest/browser.ini
@@ -1,8 +1,7 @@
 [DEFAULT]
-skip-if=!nightly_build
 support-files =
   blue_page.html
 
 [browser_as_load_location.js]
 [browser_getScreenshots.js]
 skip-if=true # issue 2851
deleted file mode 100644
--- a/browser/extensions/activity-stream/test/mozinfo.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
-  "activity_stream": true
-}
--- a/browser/extensions/activity-stream/test/unit/common/Reducers.test.js
+++ b/browser/extensions/activity-stream/test/unit/common/Reducers.test.js
@@ -1,10 +1,11 @@
 const {reducers, INITIAL_STATE, insertPinned} = require("common/Reducers.jsm");
-const {TopSites, App, Prefs, Dialog} = reducers;
+const {TopSites, App, Snippets, Prefs, Dialog, Sections} = reducers;
+
 const {actionTypes: at} = require("common/Actions.jsm");
 
 describe("Reducers", () => {
   describe("App", () => {
     it("should return the initial state", () => {
       const nextState = App(undefined, {type: "FOO"});
       assert.equal(nextState, INITIAL_STATE.App);
     });
@@ -72,16 +73,20 @@ describe("Reducers", () => {
       assert.equal(newRow.url, action.data.url);
       assert.equal(newRow.bookmarkGuid, action.data.bookmarkGuid);
       assert.equal(newRow.bookmarkTitle, action.data.bookmarkTitle);
       assert.equal(newRow.bookmarkDateCreated, action.data.lastModified);
 
       // old row is unchanged
       assert.equal(nextState.rows[0], oldState.rows[0]);
     });
+    it("should not update state for empty action.data on PLACES_BOOKMARK_ADDED", () => {
+      const nextState = TopSites(undefined, {type: at.PLACES_BOOKMARK_ADDED});
+      assert.equal(nextState, INITIAL_STATE.TopSites);
+    });
     it("should remove a bookmark on PLACES_BOOKMARK_REMOVED", () => {
       const oldState = {
         rows: [{url: "foo.com"}, {
           url: "bar.com",
           bookmarkGuid: "bookmark123",
           bookmarkTitle: "Title for bar.com",
           lastModified: 123456
         }]
@@ -93,16 +98,20 @@ describe("Reducers", () => {
       assert.equal(newRow.url, oldState.rows[1].url);
       assert.isUndefined(newRow.bookmarkGuid);
       assert.isUndefined(newRow.bookmarkTitle);
       assert.isUndefined(newRow.bookmarkDateCreated);
 
       // old row is unchanged
       assert.deepEqual(nextState.rows[0], oldState.rows[0]);
     });
+    it("should not update state for empty action.data on PLACES_BOOKMARK_REMOVED", () => {
+      const nextState = TopSites(undefined, {type: at.PLACES_BOOKMARK_REMOVED});
+      assert.equal(nextState, INITIAL_STATE.TopSites);
+    });
     it("should remove a link on PLACES_LINK_BLOCKED and PLACES_LINK_DELETED", () => {
       const events = [at.PLACES_LINK_BLOCKED, at.PLACES_LINK_DELETED];
       events.forEach(event => {
         const oldState = {rows: [{url: "foo.com"}, {url: "bar.com"}]};
         const action = {type: event, data: {url: "bar.com"}};
         const nextState = TopSites(oldState, action);
         assert.deepEqual(nextState.rows, [{url: "foo.com"}]);
       });
@@ -174,16 +183,80 @@ describe("Reducers", () => {
     });
     it("should return inital state on DELETE_HISTORY_URL", () => {
       const action = {type: at.DELETE_HISTORY_URL};
       const nextState = Dialog(INITIAL_STATE.Dialog, action);
 
       assert.deepEqual(INITIAL_STATE.Dialog, nextState);
     });
   });
+  describe("Sections", () => {
+    let oldState;
+
+    beforeEach(() => {
+      oldState = new Array(5).fill(null).map((v, i) => ({
+        id: `foo_bar_${i}`,
+        title: `Foo Bar ${i}`,
+        initialized: false,
+        rows: [{url: "www.foo.bar"}, {url: "www.other.url"}]
+      }));
+    });
+
+    it("should return INITIAL_STATE by default", () => {
+      assert.equal(INITIAL_STATE.Sections, Sections(undefined, {type: "non_existent"}));
+    });
+    it("should remove the correct section on SECTION_DEREGISTER", () => {
+      const newState = Sections(oldState, {type: at.SECTION_DEREGISTER, data: "foo_bar_2"});
+      assert.lengthOf(newState, 4);
+      const expectedNewState = oldState.splice(2, 1) && oldState;
+      assert.deepEqual(newState, expectedNewState);
+    });
+    it("should add a section on SECTION_REGISTER if it doesn't already exist", () => {
+      const action = {type: at.SECTION_REGISTER, data: {id: "foo_bar_5", title: "Foo Bar 5"}};
+      const newState = Sections(oldState, action);
+      assert.lengthOf(newState, 6);
+      const insertedSection = newState.find(section => section.id === "foo_bar_5");
+      assert.propertyVal(insertedSection, "title", action.data.title);
+    });
+    it("should set newSection.rows === [] if no rows are provided on SECTION_REGISTER", () => {
+      const action = {type: at.SECTION_REGISTER, data: {id: "foo_bar_5", title: "Foo Bar 5"}};
+      const newState = Sections(oldState, action);
+      const insertedSection = newState.find(section => section.id === "foo_bar_5");
+      assert.deepEqual(insertedSection.rows, []);
+    });
+    it("should update a section on SECTION_REGISTER if it already exists", () => {
+      const NEW_TITLE = "New Title";
+      const action = {type: at.SECTION_REGISTER, data: {id: "foo_bar_2", title: NEW_TITLE}};
+      const newState = Sections(oldState, action);
+      assert.lengthOf(newState, 5);
+      const updatedSection = newState.find(section => section.id === "foo_bar_2");
+      assert.ok(updatedSection && updatedSection.title === NEW_TITLE);
+    });
+    it("should have no effect on SECTION_ROWS_UPDATE if the id doesn't exist", () => {
+      const action = {type: at.SECTION_ROWS_UPDATE, data: {id: "fake_id", data: "fake_data"}};
+      const newState = Sections(oldState, action);
+      assert.deepEqual(oldState, newState);
+    });
+    it("should update the section rows with the correct data on SECTION_ROWS_UPDATE", () => {
+      const FAKE_DATA = ["some", "fake", "data"];
+      const action = {type: at.SECTION_ROWS_UPDATE, data: {id: "foo_bar_2", rows: FAKE_DATA}};
+      const newState = Sections(oldState, action);
+      const updatedSection = newState.find(section => section.id === "foo_bar_2");
+      assert.equal(updatedSection.rows, FAKE_DATA);
+    });
+    it("should remove blocked and deleted urls from all rows in all sections", () => {
+      const blockAction = {type: at.PLACES_LINK_BLOCKED, data: {url: "www.foo.bar"}};
+      const deleteAction = {type: at.PLACES_LINK_DELETED, data: {url: "www.foo.bar"}};
+      const newBlockState = Sections(oldState, blockAction);
+      const newDeleteState = Sections(oldState, deleteAction);
+      newBlockState.concat(newDeleteState).forEach(section => {
+        assert.deepEqual(section.rows, [{url: "www.other.url"}]);
+      });
+    });
+  });
   describe("#insertPinned", () => {
     let links;
 
     beforeEach(() => {
       links =  new Array(12).fill(null).map((v, i) => ({url: `site${i}.com`}));
     });
 
     it("should place pinned links where they belong", () => {
@@ -239,9 +312,28 @@ describe("Reducers", () => {
       assert.notProperty(result[2], "pinIndex");
     });
     it("should handle a link present in both the links and pinned list", () => {
       const pinned = [links[7]];
       const result = insertPinned(links, pinned);
       assert.equal(links.length, result.length);
     });
   });
+  describe("Snippets", () => {
+    it("should return INITIAL_STATE by default", () => {
+      assert.equal(Snippets(undefined, {type: "some_action"}), INITIAL_STATE.Snippets);
+    });
+    it("should set initialized to true on a SNIPPETS_DATA action", () => {
+      const state = Snippets(undefined, {type: at.SNIPPETS_DATA, data: {}});
+      assert.isTrue(state.initialized);
+    });
+    it("should set the snippet data on a SNIPPETS_DATA action", () => {
+      const data = {snippetsURL: "foo.com", version: 4};
+      const state = Snippets(undefined, {type: at.SNIPPETS_DATA, data});
+      assert.propertyVal(state, "snippetsURL", data.snippetsURL);
+      assert.propertyVal(state, "version", data.version);
+    });
+    it("should reset to the initial state on a SNIPPETS_RESET action", () => {
+      const state = Snippets({initalized: true, foo: "bar"}, {type: at.SNIPPETS_RESET});
+      assert.equal(state, INITIAL_STATE.Snippets);
+    });
+  });
 });
--- a/browser/extensions/activity-stream/test/unit/lib/ActivityStream.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/ActivityStream.test.js
@@ -1,27 +1,31 @@
 const injector = require("inject!lib/ActivityStream.jsm");
 
 const REASON_ADDON_UNINSTALL = 6;
 
 describe("ActivityStream", () => {
   let sandbox;
   let as;
   let ActivityStream;
+  let SECTIONS;
   function Fake() {}
 
   beforeEach(() => {
     sandbox = sinon.sandbox.create();
-    ({ActivityStream} = injector({
+    ({ActivityStream, SECTIONS} = injector({
       "lib/LocalizationFeed.jsm": {LocalizationFeed: Fake},
       "lib/NewTabInit.jsm": {NewTabInit: Fake},
       "lib/PlacesFeed.jsm": {PlacesFeed: Fake},
       "lib/TelemetryFeed.jsm": {TelemetryFeed: Fake},
       "lib/TopSitesFeed.jsm": {TopSitesFeed: Fake},
-      "lib/PrefsFeed.jsm": {PrefsFeed: Fake}
+      "lib/PrefsFeed.jsm": {PrefsFeed: Fake},
+      "lib/SnippetsFeed.jsm": {SnippetsFeed: Fake},
+      "lib/TopStoriesFeed.jsm": {TopStoriesFeed: Fake},
+      "lib/SystemTickFeed.jsm": {SystemTickFeed: Fake}
     }));
     as = new ActivityStream();
     sandbox.stub(as.store, "init");
     sandbox.stub(as.store, "uninit");
     sandbox.stub(as._defaultPrefs, "init");
     sandbox.stub(as._defaultPrefs, "reset");
   });
 
@@ -101,10 +105,26 @@ describe("ActivityStream", () => {
     it("should create a Telemetry feed", () => {
       const feed = as.feeds.get("feeds.telemetry")();
       assert.instanceOf(feed, Fake);
     });
     it("should create a Prefs feed", () => {
       const feed = as.feeds.get("feeds.prefs")();
       assert.instanceOf(feed, Fake);
     });
+    it("should create a section feed for each section in SECTIONS", () => {
+      // If new sections are added, their feeds will have to be added to the
+      // list of injected feeds above for this test to pass
+      SECTIONS.forEach((value, key) => {
+        const feed = as.feeds.get(`feeds.section.${key}`)();
+        assert.instanceOf(feed, Fake);
+      });
+    });
+    it("should create a Snippets feed", () => {
+      const feed = as.feeds.get("feeds.snippets")();
+      assert.instanceOf(feed, Fake);
+    });
+    it("should create a SystemTick feed", () => {
+      const feed = as.feeds.get("feeds.systemtick")();
+      assert.instanceOf(feed, Fake);
+    });
   });
 });
--- a/browser/extensions/activity-stream/test/unit/lib/PlacesFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/PlacesFeed.test.js
@@ -23,16 +23,17 @@ describe("PlacesFeed", () => {
         deleteHistoryEntry: sandbox.spy(),
         blockURL: sandbox.spy()
       }
     });
     globals.set("PlacesUtils", {
       history: {addObserver: sandbox.spy(), removeObserver: sandbox.spy()},
       bookmarks: {TYPE_BOOKMARK, addObserver: sandbox.spy(), removeObserver: sandbox.spy()}
     });
+    globals.set("Pocket", {savePage: sandbox.spy()});
     global.Components.classes["@mozilla.org/browser/nav-history-service;1"] = {
       getService() {
         return global.PlacesUtils.history;
       }
     };
     global.Components.classes["@mozilla.org/browser/nav-bookmarks-service;1"] = {
       getService() {
         return global.PlacesUtils.bookmarks;
@@ -93,16 +94,20 @@ describe("PlacesFeed", () => {
     it("should delete a bookmark on DELETE_BOOKMARK_BY_ID", () => {
       feed.onAction({type: at.DELETE_BOOKMARK_BY_ID, data: "g123kd"});
       assert.calledWith(global.NewTabUtils.activityStreamLinks.deleteBookmark, "g123kd");
     });
     it("should delete a history entry on DELETE_HISTORY_URL", () => {
       feed.onAction({type: at.DELETE_HISTORY_URL, data: "guava.com"});
       assert.calledWith(global.NewTabUtils.activityStreamLinks.deleteHistoryEntry, "guava.com");
     });
+    it("should save to Pocket on SAVE_TO_POCKET", () => {
+      feed.onAction({type: at.SAVE_TO_POCKET, data: {site: {url: "raspberry.com", title: "raspberry"}}, _target: {browser: {}}});
+      assert.calledWith(global.Pocket.savePage, {}, "raspberry.com", "raspberry");
+    });
   });
 
   describe("#observe", () => {
     it("should dispatch a PLACES_LINK_BLOCKED action with the url of the blocked link", () => {
       feed.observe(null, BLOCKED_EVENT, "foo123.com");
       assert.equal(feed.store.dispatch.firstCall.args[0].type, at.PLACES_LINK_BLOCKED);
       assert.deepEqual(feed.store.dispatch.firstCall.args[0].data, {url: "foo123.com"});
     });
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/lib/SnippetsFeed.test.js
@@ -0,0 +1,60 @@
+const {SnippetsFeed} = require("lib/SnippetsFeed.jsm");
+const {actionTypes: at, actionCreators: ac} = require("common/Actions.jsm");
+
+describe("SnippetsFeed", () => {
+  let sandbox;
+  beforeEach(() => {
+    sandbox = sinon.sandbox.create();
+  });
+  afterEach(() => {
+    sandbox.restore();
+  });
+  it("should dispatch the right version and snippetsURL on INIT", () => {
+    const url = "foo.com/%STARTPAGE_VERSION%";
+    sandbox.stub(global.Services.prefs, "getStringPref").returns(url);
+    const feed = new SnippetsFeed();
+    feed.store = {dispatch: sandbox.stub()};
+
+    feed.onAction({type: at.INIT});
+
+    assert.calledWith(feed.store.dispatch, ac.BroadcastToContent({
+      type: at.SNIPPETS_DATA,
+      data: {
+        snippetsURL: "foo.com/4",
+        version: 4
+      }
+    }));
+  });
+  it("should call .init when a FEED_INIT happens for feeds.snippets", () => {
+    const feed = new SnippetsFeed();
+    sandbox.stub(feed, "init");
+    feed.store = {dispatch: sandbox.stub()};
+
+    feed.onAction({type: at.FEED_INIT, data: "feeds.snippets"});
+
+    assert.calledOnce(feed.init);
+  });
+  it("should dispatch a SNIPPETS_RESET on uninit", () => {
+    const feed = new SnippetsFeed();
+    feed.store = {dispatch: sandbox.stub()};
+
+    feed.uninit();
+
+    assert.calledWith(feed.store.dispatch, {type: at.SNIPPETS_RESET});
+  });
+  describe("_onUrlChange", () => {
+    it("should dispatch a new snippetsURL", () => {
+      const url = "boo.com/%STARTPAGE_VERSION%";
+      sandbox.stub(global.Services.prefs, "getStringPref").returns(url);
+      const feed = new SnippetsFeed();
+      feed.store = {dispatch: sandbox.stub()};
+
+      feed._onUrlChange();
+
+      assert.calledWith(feed.store.dispatch, ac.BroadcastToContent({
+        type: at.SNIPPETS_DATA,
+        data: {snippetsURL: "boo.com/4"}
+      }));
+    });
+  });
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/lib/SystemTickFeed.test.js
@@ -0,0 +1,41 @@
+"use strict";
+const injector = require("inject!lib/SystemTickFeed.jsm");
+const {actionTypes: at} = require("common/Actions.jsm");
+
+describe("System Tick Feed", () => {
+  let SystemTickFeed;
+  let SYSTEM_TICK_INTERVAL;
+  let instance;
+  let clock;
+
+  beforeEach(() => {
+    clock = sinon.useFakeTimers();
+
+    ({SystemTickFeed, SYSTEM_TICK_INTERVAL} = injector({}));
+    instance = new SystemTickFeed();
+    instance.store = {getState() { return {}; }, dispatch() {}};
+  });
+  afterEach(() => {
+    clock.restore();
+  });
+  it("should create a SystemTickFeed", () => {
+    assert.instanceOf(instance, SystemTickFeed);
+  });
+  it("should fire SYSTEM_TICK events at configured interval", () => {
+    let expectation = sinon.mock(instance.store).expects("dispatch")
+      .twice()
+      .withExactArgs({type: at.SYSTEM_TICK});
+
+    instance.onAction({type: at.INIT});
+    clock.tick(SYSTEM_TICK_INTERVAL * 2);
+    expectation.verify();
+  });
+  it("should not fire SYSTEM_TICK events after UNINIT", () => {
+    let expectation = sinon.mock(instance.store).expects("dispatch")
+      .never();
+
+    instance.onAction({type: at.UNINIT});
+    clock.tick(SYSTEM_TICK_INTERVAL * 2);
+    expectation.verify();
+  });
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/lib/TopStoriesFeed.test.js
@@ -0,0 +1,257 @@
+"use strict";
+const injector = require("inject!lib/TopStoriesFeed.jsm");
+const {FakePrefs} = require("test/unit/utils");
+const {actionCreators: ac, actionTypes: at} = require("common/Actions.jsm");
+const {GlobalOverrider} = require("test/unit/utils");
+
+describe("Top Stories Feed", () => {
+  let TopStoriesFeed;
+  let STORIES_UPDATE_TIME;
+  let TOPICS_UPDATE_TIME;
+  let SECTION_ID;
+  let instance;
+  let clock;
+  let globals;
+
+  beforeEach(() => {
+    FakePrefs.prototype.prefs["feeds.section.topstories.options"] = `{
+      "stories_endpoint": "https://somedomain.org/stories?key=$apiKey",
+      "topics_endpoint": "https://somedomain.org/topics?key=$apiKey",
+      "survey_link": "https://www.surveymonkey.com/r/newtabffx",
+      "api_key_pref": "apiKeyPref",
+      "provider_name": "test-provider",
+      "provider_icon": "provider-icon"
+    }`;
+    FakePrefs.prototype.prefs.apiKeyPref = "test-api-key";
+
+    globals = new GlobalOverrider();
+    clock = sinon.useFakeTimers();
+
+    ({TopStoriesFeed, STORIES_UPDATE_TIME, TOPICS_UPDATE_TIME, SECTION_ID} = injector({"lib/ActivityStreamPrefs.jsm": {Prefs: FakePrefs}}));
+    instance = new TopStoriesFeed();
+    instance.store = {getState() { return {}; }, dispatch: sinon.spy()};
+  });
+  afterEach(() => {
+    globals.restore();
+    clock.restore();
+  });
+  describe("#init", () => {
+    it("should create a TopStoriesFeed", () => {
+      assert.instanceOf(instance, TopStoriesFeed);
+    });
+    it("should initialize endpoints based on prefs", () => {
+      instance.onAction({type: at.INIT});
+      assert.equal("https://somedomain.org/stories?key=test-api-key", instance.stories_endpoint);
+      assert.equal("https://somedomain.org/topics?key=test-api-key", instance.topics_endpoint);
+    });
+    it("should register section", () => {
+      const expectedSectionOptions = {
+        id: SECTION_ID,
+        icon: "provider-icon",
+        title: {id: "header_recommended_by", values: {provider: "test-provider"}},
+        rows: [],
+        maxCards: 3,
+        contextMenuOptions: ["SaveToPocket", "Separator", "CheckBookmark", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl"],
+        infoOption: {
+          header: {id: "pocket_feedback_header"},
+          body: {id: "pocket_feedback_body"},
+          link: {
+            href: "https://www.surveymonkey.com/r/newtabffx",
+            id: "pocket_send_feedback"
+          }
+        },
+        emptyState: {
+          message: {id: "empty_state_topstories"},
+          icon: "check"
+        }
+      };
+
+      instance.onAction({type: at.INIT});
+      assert.calledOnce(instance.store.dispatch);
+      assert.propertyVal(instance.store.dispatch.firstCall.args[0], "type", at.SECTION_REGISTER);
+      assert.calledWith(instance.store.dispatch, ac.BroadcastToContent({
+        type: at.SECTION_REGISTER,
+        data: expectedSectionOptions
+      }));
+    });
+    it("should fetch stories on init", () => {
+      instance.fetchStories = sinon.spy();
+      instance.fetchTopics = sinon.spy();
+      instance.onAction({type: at.INIT});
+      assert.calledOnce(instance.fetchStories);
+    });
+    it("should fetch topics on init", () => {
+      instance.fetchStories = sinon.spy();
+      instance.fetchTopics = sinon.spy();
+      instance.onAction({type: at.INIT});
+      assert.calledOnce(instance.fetchTopics);
+    });
+    it("should not fetch if endpoint not configured", () => {
+      let fetchStub = globals.sandbox.stub();
+      globals.set("fetch", fetchStub);
+      FakePrefs.prototype.prefs["feeds.section.topstories.options"] = "{}";
+      instance.init();
+      assert.notCalled(fetchStub);
+    });
+    it("should report error for invalid configuration", () => {
+      globals.sandbox.spy(global.Components.utils, "reportError");
+      FakePrefs.prototype.prefs["feeds.section.topstories.options"] = "invalid";
+      instance.init();
+
+      assert.called(Components.utils.reportError);
+    });
+    it("should report error for missing api key", () => {
+      let fakeServices = {prefs: {getCharPref: sinon.spy()}};
+      globals.set("Services", fakeServices);
+      globals.sandbox.spy(global.Components.utils, "reportError");
+      FakePrefs.prototype.prefs["feeds.section.topstories.options"] = `{
+        "stories_endpoint": "https://somedomain.org/stories?key=$apiKey",
+        "topics_endpoint": "https://somedomain.org/topics?key=$apiKey"
+      }`;
+      instance.init();
+
+      assert.called(Components.utils.reportError);
+    });
+    it("should deregister section", () => {
+      instance.onAction({type: at.UNINIT});
+      assert.calledOnce(instance.store.dispatch);
+      assert.calledWith(instance.store.dispatch, ac.BroadcastToContent({
+        type: at.SECTION_DEREGISTER,
+        data: SECTION_ID
+      }));
+    });
+    it("should initialize on FEED_INIT", () => {
+      instance.init = sinon.spy();
+      instance.onAction({type: at.FEED_INIT, data: "feeds.section.topstories"});
+      assert.calledOnce(instance.init);
+    });
+  });
+  describe("#fetch", () => {
+    it("should fetch stories and send event", async () => {
+      let fetchStub = globals.sandbox.stub();
+      globals.set("fetch", fetchStub);
+      globals.set("NewTabUtils", {blockedLinks: {isBlocked: globals.sandbox.spy()}});
+
+      const response = `{"list": [{"id" : "1",
+        "title": "title",
+        "excerpt": "description",
+        "image_src": "image-url",
+        "dedupe_url": "rec-url",
+        "published_timestamp" : "123"
+      }]}`;
+      const stories = [{
+        "guid": "1",
+        "type": "trending",
+        "title": "title",
+        "description": "description",
+        "image": "image-url",
+        "url": "rec-url",
+        "lastVisitDate": "123"
+      }];
+
+      instance.stories_endpoint = "stories-endpoint";
+      fetchStub.resolves({ok: true, status: 200, text: () => response});
+      await instance.fetchStories();
+
+      assert.calledOnce(fetchStub);
+      assert.calledWithExactly(fetchStub, instance.stories_endpoint);
+      assert.calledOnce(instance.store.dispatch);
+      assert.propertyVal(instance.store.dispatch.firstCall.args[0], "type", at.SECTION_ROWS_UPDATE);
+      assert.deepEqual(instance.store.dispatch.firstCall.args[0].data.id, SECTION_ID);
+      assert.deepEqual(instance.store.dispatch.firstCall.args[0].data.rows, stories);
+    });
+    it("should dispatch events", () => {
+      instance.dispatchUpdateEvent(123, {});
+      assert.calledOnce(instance.store.dispatch);
+    });
+    it("should report error for unexpected stories response", async () => {
+      let fetchStub = globals.sandbox.stub();
+      globals.set("fetch", fetchStub);
+      globals.sandbox.spy(global.Components.utils, "reportError");
+
+      instance.stories_endpoint = "stories-endpoint";
+      fetchStub.resolves({ok: false, status: 400});
+      await instance.fetchStories();
+
+      assert.calledOnce(fetchStub);
+      assert.calledWithExactly(fetchStub, instance.stories_endpoint);
+      assert.notCalled(instance.store.dispatch);
+      assert.called(Components.utils.reportError);
+    });
+    it("should exclude blocked (dismissed) URLs", async () => {
+      let fetchStub = globals.sandbox.stub();
+      globals.set("fetch", fetchStub);
+      globals.set("NewTabUtils", {blockedLinks: {isBlocked: url => url === "blocked"}});
+
+      const response = `{"list": [{"dedupe_url" : "blocked"}, {"dedupe_url" : "not_blocked"}]}`;
+      instance.stories_endpoint = "stories-endpoint";
+      fetchStub.resolves({ok: true, status: 200, text: () => response});
+      await instance.fetchStories();
+
+      assert.calledOnce(instance.store.dispatch);
+      assert.propertyVal(instance.store.dispatch.firstCall.args[0], "type", at.SECTION_ROWS_UPDATE);
+      assert.equal(instance.store.dispatch.firstCall.args[0].data.rows.length, 1);
+      assert.equal(instance.store.dispatch.firstCall.args[0].data.rows[0].url, "not_blocked");
+    });
+    it("should fetch topics and send event", async () => {
+      let fetchStub = globals.sandbox.stub();
+      globals.set("fetch", fetchStub);
+
+      const response = `{"topics": [{"name" : "topic1", "url" : "url-topic1"}, {"name" : "topic2", "url" : "url-topic2"}]}`;
+      const topics = [{
+        "name": "topic1",
+        "url": "url-topic1"
+      }, {
+        "name": "topic2",
+        "url": "url-topic2"
+      }];
+
+      instance.topics_endpoint = "topics-endpoint";
+      fetchStub.resolves({ok: true, status: 200, text: () => response});
+      await instance.fetchTopics();
+
+      assert.calledOnce(fetchStub);
+      assert.calledWithExactly(fetchStub, instance.topics_endpoint);
+      assert.calledOnce(instance.store.dispatch);
+      assert.propertyVal(instance.store.dispatch.firstCall.args[0], "type", at.SECTION_ROWS_UPDATE);
+      assert.deepEqual(instance.store.dispatch.firstCall.args[0].data.id, SECTION_ID);
+      assert.deepEqual(instance.store.dispatch.firstCall.args[0].data.topics, topics);
+    });
+    it("should report error for unexpected topics response", async () => {
+      let fetchStub = globals.sandbox.stub();
+      globals.set("fetch", fetchStub);
+      globals.sandbox.spy(global.Components.utils, "reportError");
+
+      instance.topics_endpoint = "topics-endpoint";
+      fetchStub.resolves({ok: false, status: 400});
+      await instance.fetchTopics();
+
+      assert.calledOnce(fetchStub);
+      assert.calledWithExactly(fetchStub, instance.topics_endpoint);
+      assert.notCalled(instance.store.dispatch);
+      assert.called(Components.utils.reportError);
+    });
+  });
+  describe("#update", () => {
+    it("should fetch stories after update interval", () => {
+      instance.fetchStories = sinon.spy();
+      instance.fetchTopics = sinon.spy();
+      instance.onAction({type: at.SYSTEM_TICK});
+      assert.notCalled(instance.fetchStories);
+
+      clock.tick(STORIES_UPDATE_TIME);
+      instance.onAction({type: at.SYSTEM_TICK});
+      assert.calledOnce(instance.fetchStories);
+    });
+    it("should fetch topics after update interval", () => {
+      instance.fetchStories = sinon.spy();
+      instance.fetchTopics = sinon.spy();
+      instance.onAction({type: at.SYSTEM_TICK});
+      assert.notCalled(instance.fetchTopics);
+
+      clock.tick(TOPICS_UPDATE_TIME);
+      instance.onAction({type: at.SYSTEM_TICK});
+      assert.calledOnce(instance.fetchTopics);
+    });
+  });
+});
--- a/browser/extensions/activity-stream/test/unit/lib/init-store.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/init-store.test.js
@@ -19,16 +19,26 @@ describe("initStore", () => {
   it("should add a listener for incoming actions", () => {
     assert.calledWith(global.addMessageListener, initStore.INCOMING_MESSAGE_NAME);
     const callback = global.addMessageListener.firstCall.args[1];
     globals.sandbox.spy(store, "dispatch");
     const message = {name: initStore.INCOMING_MESSAGE_NAME, data: {type: "FOO"}};
     callback(message);
     assert.calledWith(store.dispatch, message.data);
   });
+  it("should log errors from failed messages", () => {
+    const callback = global.addMessageListener.firstCall.args[1];
+    globals.sandbox.stub(global.console, "error");
+    globals.sandbox.stub(store, "dispatch").throws(Error("failed"));
+
+    const message = {name: initStore.INCOMING_MESSAGE_NAME, data: {type: "FOO"}};
+    callback(message);
+
+    assert.calledOnce(global.console.error);
+  });
   it("should replace the state if a MERGE_STORE_ACTION is dispatched", () => {
     store.dispatch({type: initStore.MERGE_STORE_ACTION, data: {number: 42}});
     assert.deepEqual(store.getState(), {number: 42});
   });
   it("should send out SendToMain ations", () => {
     const action = ac.SendToMain({type: "FOO"});
     store.dispatch(action);
     assert.calledWith(global.sendAsyncMessage, initStore.OUTGOING_MESSAGE_NAME, action);
--- a/browser/extensions/activity-stream/test/unit/unit-entry.js
+++ b/browser/extensions/activity-stream/test/unit/unit-entry.js
@@ -24,26 +24,29 @@ overrider.set({
   },
   // eslint-disable-next-line object-shorthand
   ContentSearchUIController: function() {}, // NB: This is a function/constructor
   dump() {},
   fetch() {},
   Preferences: FakePrefs,
   Services: {
     locale: {getRequestedLocale() {}},
+    urlFormatter: {formatURL: str => str},
     mm: {
       addMessageListener: (msg, cb) => cb(),
       removeMessageListener() {}
     },
     appShell: {hiddenDOMWindow: {performance: new FakePerformance()}},
     obs: {
       addObserver() {},
       removeObserver() {}
     },
     prefs: {
+      addObserver() {},
+      removeObserver() {},
       getStringPref() {},
       getDefaultBranch() {
         return {
           setBoolPref() {},
           setIntPref() {},
           setStringPref() {},
           clearUserPref() {}
         };
--- a/browser/extensions/onboarding/content/onboarding.css
+++ b/browser/extensions/onboarding/content/onboarding.css
@@ -531,16 +531,17 @@
 #onboarding-notification-action-btn {
   background: #0a84ff;
   /* With 1px transparent border, could see a border in the high-constrast mode */
   border: 1px solid transparent;
   border-radius: 0;
   padding: 10px 20px;
   font-size: 14px;
   color: #fff;
+  min-width: 130px;
 }
 
 @media all and (max-width: 960px) {
   #onboarding-notification-icon {
     display: none;
   }
 }
 @media all and (max-width: 720px) {
--- a/browser/installer/Makefile.in
+++ b/browser/installer/Makefile.in
@@ -7,16 +7,18 @@ DIST_SUBDIR := browser
 
 include $(topsrcdir)/config/rules.mk
 
 MOZ_PKG_REMOVALS = $(srcdir)/removed-files.in
 
 MOZ_PKG_MANIFEST = $(srcdir)/package-manifest.in
 MOZ_PKG_DUPEFLAGS = -f $(srcdir)/allowed-dupes.mn
 
+DEFINES += -DPKG_LOCALE_MANIFEST=$(topobjdir)/toolkit/locales/locale-manifest.in
+
 # Some files have been already bundled with xulrunner
 ifndef MOZ_MULET
 MOZ_PKG_FATAL_WARNINGS = 1
 else
 DEFINES += -DMOZ_MULET
 endif
 
 # When packaging an artifact build not all xpt files expected by the
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -42,18 +42,16 @@
 @RESPATH@/firefox.icns
 @RESPATH@/document.icns
 @RESPATH@/@LPROJ_ROOT@.lproj/*
 #endif
 
 [@AB_CD@]
 @RESPATH@/browser/chrome/@AB_CD@@JAREXT@
 @RESPATH@/browser/chrome/@AB_CD@.manifest
-@RESPATH@/chrome/@AB_CD@@JAREXT@
-@RESPATH@/chrome/@AB_CD@.manifest
 @RESPATH@/dictionaries/*
 #if defined(XP_WIN) || defined(XP_LINUX)
 @RESPATH@/fonts/*
 #endif
 @RESPATH@/hyphenation/*
 @RESPATH@/browser/@PREF_DIR@/firefox-l10n.js
 #ifdef HAVE_MAKENSISU
 @BINPATH@/uninstall/helper.exe
@@ -832,8 +830,12 @@ bin/libfreebl_32int64_3.so
 @RESPATH@/fix_stack_using_bpsyms.py
 #ifdef XP_MACOSX
 @RESPATH@/fix_macosx_stack.py
 #endif
 #ifdef XP_LINUX
 @RESPATH@/fix_linux_stack.py
 #endif
 #endif
+
+#ifdef PKG_LOCALE_MANIFEST
+#include @PKG_LOCALE_MANIFEST@
+#endif
--- a/browser/themes/shared/customizableui/panelUI.inc.css
+++ b/browser/themes/shared/customizableui/panelUI.inc.css
@@ -351,16 +351,21 @@ photonpanelmultiview panelview {
 }
 
 #appMenu-popup panelview,
 #customizationui-widget-multiview panelview:not([extension]) {
   min-width: @menuPanelWidth@;
   max-width: 30em;
 }
 
+/* Add 2 * 12px extra width for touch mode button padding. */
+#appMenu-popup[touchmode] panelview {
+  min-width: calc(@menuPanelWidth@ + 24px);
+}
+
 photonpanelmultiview .panel-subview-body {
   margin: 4px 0;
 }
 
 /* END photonpanelview adjustments */
 
 .cui-widget-panel.cui-widget-panelWithFooter > .panel-arrowcontainer > .panel-arrowcontent {
   padding-bottom: 0;
@@ -1293,16 +1298,20 @@ photonpanelmultiview .subviewbutton[chec
 }
 
 photonpanelmultiview .panel-banner-item > .toolbarbutton-multiline-text {
   font: menu;
   padding: 0;
   padding-inline-start: 8px; /* See '.subviewbutton-iconic > .toolbarbutton-text' rule above. */
 }
 
+photonpanelmultiview .subviewbutton-iconic > .toolbarbutton-icon {
+  width: 16px;
+}
+
 photonpanelmultiview .subviewbutton {
   -moz-context-properties: fill;
   fill: currentColor;
 }
 
 photonpanelmultiview .subviewbutton[checked="true"] {
   background: none;
   list-style-image: url(chrome://browser/skin/check.svg);
--- a/browser/themes/shared/toolbarbutton-icons.inc.css
+++ b/browser/themes/shared/toolbarbutton-icons.inc.css
@@ -344,16 +344,31 @@ toolbar:not([brighttext]) #bookmarks-men
     fill: #30A3FF;
   }
   to {
     transform: scaleX(-1) translateX(-1260px);
     fill: #30A3FF;
   }
 }
 
+/* The animation is supposed to show the blue fill color for 520ms, then the
+   fade to the toolbarbutton-fill color for the remaining 210ms. Thus with an
+   animation-duration of 730ms, 71% is the point where we start the fade out. */
+@keyframes overflow-fade {
+  from {
+    fill: #30A3FF;
+  }
+  71% {
+    fill: #30A3FF;
+  }
+  to {
+    fill: inherit;
+  }
+}
+
 #nav-bar-overflow-button > .toolbarbutton-animatable-box {
   position: fixed;
   overflow: hidden;
   /* The height of the sprite is 24px, which is 8px taller than
      the height of the icon. We need to move the sprite up 8px
      higher to counter for this. */
   margin-top: -8px;
   /* Since .toolbarbutton-icon uses a different width than the animatable box,
@@ -376,16 +391,22 @@ toolbar:not([brighttext]) #bookmarks-men
   animation-duration: 1.1s;
   background-image: url("chrome://browser/skin/chevron-animation.svg");
   width: 1278px;
 }
 
 #nav-bar-overflow-button[animate]:-moz-locale-dir(rtl) > .toolbarbutton-animatable-box > .toolbarbutton-animatable-image {
   animation-name: overflow-animation-rtl;
 }
+
+#nav-bar-overflow-button[animate][fade] > .toolbarbutton-animatable-box > .toolbarbutton-animatable-image {
+  animation-name: overflow-fade;
+  animation-timing-function: ease-out;
+  animation-duration: 730ms;
+}
 %endif
 
 #email-link-button@attributeSelectorForToolbar@ {
   list-style-image: url("chrome://browser/skin/mail.svg");
 }
 
 #sidebar-button@attributeSelectorForToolbar@ {
   list-style-image: url("chrome://browser/skin/sidebars-right.svg");
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -1956,23 +1956,30 @@ notification.pluginVulnerable > .notific
    */
   margin-left: -10px;
   margin-right: -10px;
   margin-bottom: -10px;
 }
 
 %include ../shared/contextmenu.inc.css
 
-/* Make context menu items larger when opened through touch. */
+/* Make menu items larger when opened through touch. */
+panel[touchmode] .PanelUI-subView .subviewbutton,
 menupopup[touchmode] menu,
 menupopup[touchmode] menuitem {
   padding-top: 12px;
   padding-bottom: 12px;
 }
 
+panel[touchmode] .PanelUI-subView #appMenu-edit-controls > .subviewbutton,
+panel[touchmode] .PanelUI-subView #appMenu-zoom-controls > .subviewbutton-iconic {
+  padding-inline-start: 12px;
+  padding-inline-end: 12px;
+}
+
 #contentAreaContextMenu[touchmode] > #context-navigation > menuitem {
   padding-top: 7px;
   padding-bottom: 7px;
 }
 
 #context-navigation {
   background-color: menu;
   padding-bottom: 4px;
--- a/build/macosx/mozconfig.common
+++ b/build/macosx/mozconfig.common
@@ -1,9 +1,7 @@
 if test `uname -s` = Linux; then
   . $topsrcdir/build/macosx/cross-mozconfig.common
 else
   . $topsrcdir/build/macosx/local-mozconfig.common
 fi
 
-# Enable stylo in automation builds.
-# Can be removed after bug 1375774 is resolved.
-ac_add_options --enable-stylo=build
+. $topsrcdir/build/mozconfig.stylo
--- a/build/moz.configure/checks.configure
+++ b/build/moz.configure/checks.configure
@@ -92,28 +92,28 @@ def checking(what, callback=None):
 # it can find. If PROG is already set from the environment or command line,
 # use that value instead.
 @template
 @imports(_from='mozbuild.shellutil', _import='quote')
 def check_prog(var, progs, what=None, input=None, allow_missing=False,
                paths=None, when=None):
     if input is not None:
         # Wrap input with type checking and normalization.
-        @depends(input)
+        @depends(input, when=when)
         def input(value):
             if not value:
                 return
             if isinstance(value, str):
                 return (value,)
             if isinstance(value, (tuple, list)) and len(value) == 1:
                 return value
             configure_error('input must resolve to a tuple or a list with a '
                             'single element, or a string')
     else:
-        option(env=var, nargs=1,
+        option(env=var, nargs=1, when=when,
                help='Path to %s' % (what or 'the %s program' % var.lower()))
         input = var
     what = what or var.lower()
 
     # Trick to make a @depends function out of an immediate value.
     progs = dependable(progs)
     paths = dependable(paths)
 
@@ -130,15 +130,15 @@ def check_prog(var, progs, what=None, in
             log.debug('%s: Trying %s', var.lower(), quote(prog))
             result = find_program(prog, paths)
             if result:
                 return result
 
         if not allow_missing or value:
             raise FatalCheckError('Cannot find %s' % what)
 
-    @depends_if(check, progs)
+    @depends_if(check, progs, when=when)
     def normalized_for_config(value, progs):
         return ':' if value is None else value
 
     set_config(var, normalized_for_config)
 
     return check
--- a/build/moz.configure/init.configure
+++ b/build/moz.configure/init.configure
@@ -304,16 +304,98 @@ def shell(value, mozillabuild):
     shell = 'sh'
     if mozillabuild:
         shell = mozillabuild[0] + '/msys/bin/sh'
     if sys.platform == 'win32':
         shell = shell + '.exe'
     return find_program(shell)
 
 
+# Source checkout and version control integration.
+# ================================================
+
+@depends(check_build_environment, 'MOZ_AUTOMATION', '--help')
+@checking('for vcs source checkout')
+@imports('os')
+def vcs_checkout_type(build_env, automation, _):
+    if os.path.exists(os.path.join(build_env.topsrcdir, '.hg')):
+        return 'hg'
+    elif os.path.exists(os.path.join(build_env.topsrcdir, '.git')):
+        return 'git'
+    elif automation:
+        raise FatalCheckError('unable to resolve VCS type; must run '
+                              'from a source checkout when MOZ_AUTOMATION '
+                              'is set')
+
+# Resolve VCS binary for detected repository type.
+
+# TODO remove hg.exe once bug 1382940 addresses ambiguous executables case.
+hg = check_prog('HG', ('hg.exe', 'hg',), allow_missing=True,
+                when=depends(vcs_checkout_type)(lambda x: x == 'hg'))
+git = check_prog('GIT', ('git',), allow_missing=True,
+                 when=depends(vcs_checkout_type)(lambda x: x == 'git'))
+
+@depends_if(hg)
+@checking('for Mercurial version')
+@imports('os')
+@imports('re')
+def hg_version(hg):
+    # HGPLAIN in Mercurial 1.5+ forces stable output, regardless of set
+    # locale or encoding.
+    env = dict(os.environ)
+    env['HGPLAIN'] = '1'
+
+    out = check_cmd_output(hg, '--version', env=env)
+
+    match = re.search(r'Mercurial Distributed SCM \(version ([^\)]+)', out)
+
+    if not match:
+        raise FatalCheckError('unable to determine Mercurial version: %s' % out)
+
+    # The version string may be "unknown" for Mercurial run out of its own
+    # source checkout or for bad builds. But LooseVersion handles it.
+
+    return Version(match.group(1))
+
+@depends_if(git)
+@checking('for Git version')
+@imports('re')
+def git_version(git):
+    out = check_cmd_output(git, '--version').rstrip()
+
+    match = re.search('git version (.*)$', out)
+
+    if not match:
+        raise FatalCheckError('unable to determine Git version: %s' % out)
+
+    return Version(match.group(1))
+
+# Only set VCS_CHECKOUT_TYPE if we resolved the VCS binary.
+# Require resolved VCS info when running in automation so automation's
+# environment is more well-defined.
+@depends(vcs_checkout_type, hg_version, git_version, 'MOZ_AUTOMATION')
+def exposed_vcs_checkout_type(vcs_checkout_type, hg, git, automation):
+    if vcs_checkout_type == 'hg':
+        if hg:
+            return 'hg'
+
+        if automation:
+            raise FatalCheckError('could not resolve Mercurial binary info')
+
+    elif vcs_checkout_type == 'git':
+        if git:
+            return 'git'
+
+        if automation:
+            raise FatalCheckError('could not resolve Git binary info')
+    elif vcs_checkout_type:
+        raise FatalCheckError('unhandled VCS type: %s' % vcs_checkout_type)
+
+set_config('VCS_CHECKOUT_TYPE', exposed_vcs_checkout_type)
+
 # Host and target systems
 # ==============================================================
 option('--host', nargs=1, help='Define the system type performing the build')
 
 option('--target', nargs=1,
        help='Define the system type where the resulting executables will be '
             'used')
 
--- a/build/moz.configure/toolchain.configure
+++ b/build/moz.configure/toolchain.configure
@@ -1100,28 +1100,29 @@ option('--enable-gold',
 @checking('for ld', lambda x: x.KIND)
 @imports('os')
 @imports('shutil')
 def enable_gold(enable_gold_option, c_compiler, developer_options, build_env):
     linker = None
     # Used to check the kind of linker
     version_check = ['-Wl,--version']
     cmd_base = c_compiler.wrapper + [c_compiler.compiler] + c_compiler.flags
-    if enable_gold_option or developer_options:
+
+    def resolve_gold():
         # Try to force the usage of gold
         targetDir = os.path.join(build_env.topobjdir, 'build', 'unix', 'gold')
 
         gold_detection_arg = '-print-prog-name=ld.gold'
         gold = check_cmd_output(c_compiler.compiler, gold_detection_arg).strip()
         if not gold:
-            die('Could not find gold')
+            return
 
         goldFullPath = find_program(gold)
         if goldFullPath is None:
-            die('Could not find gold')
+            return
 
         if os.path.exists(targetDir):
             shutil.rmtree(targetDir)
         os.makedirs(targetDir)
         os.symlink(goldFullPath, os.path.join(targetDir, 'ld'))
 
         linker = ['-B', targetDir]
         cmd = cmd_base + linker + version_check
@@ -1130,16 +1131,26 @@ def enable_gold(enable_gold_option, c_co
             return namespace(
                 KIND='gold',
                 LINKER_FLAG=linker,
             )
         else:
             # The -B trick didn't work, removing the directory
             shutil.rmtree(targetDir)
 
+    if enable_gold_option or developer_options:
+        result = resolve_gold()
+
+        if result:
+            return result
+        # gold is only required if --enable-gold is used.
+        elif enable_gold_option:
+            die('Could not find gold')
+        # Else fallthrough.
+
     cmd = cmd_base + version_check
     cmd_output = check_cmd_output(*cmd).decode('utf-8')
     # using decode because ld can be localized and python will
     # have problems with french accent for example
     if 'GNU ld' in cmd_output:
         # We are using the normal linker
         return namespace(
             KIND='bfd'
--- a/build/moz.configure/util.configure
+++ b/build/moz.configure/util.configure
@@ -17,27 +17,44 @@ def configure_error(message):
     Primarily for use in moz.configure templates to sanity check
     their inputs from moz.configure usage.'''
     raise ConfigureError(message)
 
 # A wrapper to obtain a process' output that returns the output generated
 # by running the given command if it exits normally, and streams that
 # output to log.debug and calls die or the given error callback if it
 # does not.
+@imports(_from='__builtin__', _import='unicode')
 @imports('subprocess')
 @imports('sys')
 @imports(_from='mozbuild.configure.util', _import='LineIO')
 @imports(_from='mozbuild.shellutil', _import='quote')
 def check_cmd_output(*args, **kwargs):
     onerror = kwargs.pop('onerror', None)
 
+    # subprocess on older Pythons can't handle unicode keys or values in
+    # environment dicts. Normalize automagically so callers don't have to
+    # deal with this.
+    if 'env' in kwargs:
+        normalized_env = {}
+        for k, v in kwargs['env'].items():
+            if isinstance(k, unicode):
+                k = k.encode('utf-8', 'strict')
+
+            if isinstance(v, unicode):
+                v = v.encode('utf-8', 'strict')
+
+            normalized_env[k] = v
+
+        kwargs['env'] = normalized_env
+
     with log.queue_debug():
         log.debug('Executing: `%s`', quote(*args))
         proc = subprocess.Popen(args, stdout=subprocess.PIPE,
-                                stderr=subprocess.PIPE)
+                                stderr=subprocess.PIPE, **kwargs)
         stdout, stderr = proc.communicate()
         retcode = proc.wait()
         if retcode == 0:
             return stdout
 
         log.debug('The command returned non-zero exit status %d.',
                   retcode)
         for out, desc in ((stdout, 'output'), (stderr, 'error output')):
--- a/build/mozconfig.common
+++ b/build/mozconfig.common
@@ -9,19 +9,16 @@
 # architectures, though note that if you want to override options set in
 # another mozconfig file, you'll need to use mozconfig.common.override instead
 # of this file.
 
 mk_add_options AUTOCLOBBER=1
 
 ac_add_options --enable-crashreporter
 
-# Tell the build system where to find llvm-config for builds on automation.
-export LLVM_CONFIG="${TOOLTOOL_DIR:-$topsrcdir}/clang/bin/llvm-config"
-
 # Enable checking that add-ons are signed by the trusted root
 MOZ_ADDON_SIGNING=${MOZ_ADDON_SIGNING-1}
 # Disable enforcing that add-ons are signed by the trusted root
 MOZ_REQUIRE_SIGNING=${MOZ_REQUIRE_SIGNING-0}
 
 ac_add_options --enable-js-shell
 
 . "$topsrcdir/build/mozconfig.automation"
--- a/build/mozconfig.no-compile
+++ b/build/mozconfig.no-compile
@@ -5,8 +5,9 @@ unset CC
 unset CXX
 unset HOST_CC
 unset HOST_CXX
 unset RUSTC
 unset CARGO
 unset MAKECAB
 unset TOOLCHAIN_PREFIX
 unset BINDGEN_CFLAGS
+unset LLVM_CONFIG
new file mode 100644
--- /dev/null
+++ b/build/mozconfig.stylo
@@ -0,0 +1,6 @@
+# Tell the build system where to find llvm-config for builds on automation.
+export LLVM_CONFIG="${TOOLTOOL_DIR:-$topsrcdir}/clang/bin/llvm-config"
+
+# TODO remove once configure defaults to stylo once stylo enabled
+# on all platforms.
+ac_add_options --enable-stylo=build
--- a/devtools/client/definitions.js
+++ b/devtools/client/definitions.js
@@ -88,26 +88,26 @@ Tools.inspector = {
   isTargetSupported: function (target) {
     return target.hasActor("inspector");
   },
 
   build: function (iframeWindow, toolbox) {
     return new InspectorPanel(iframeWindow, toolbox);
   }
 };
-
 Tools.webConsole = {
   id: "webconsole",
   key: l10n("cmd.commandkey"),
   accesskey: l10n("webConsoleCmd.accesskey"),
   modifiers: Services.appinfo.OS == "Darwin" ? "accel,alt" : "accel,shift",
   ordinal: 2,
+  oldWebConsoleURL: "chrome://devtools/content/webconsole/webconsole.xul",
+  newWebConsoleURL: "chrome://devtools/content/webconsole/webconsole.xhtml",
   icon: "chrome://devtools/skin/images/tool-webconsole.svg",
   invertIconForDarkTheme: true,
-  url: "chrome://devtools/content/webconsole/webconsole.xul",
   label: l10n("ToolboxTabWebconsole.label"),
   menuLabel: l10n("MenuWebconsole.label"),
   panelLabel: l10n("ToolboxWebConsole.panelLabel"),
   get tooltip() {
     return l10n("ToolboxWebconsole.tooltip2",
     (osString == "Darwin" ? "Cmd+Opt+" : "Ctrl+Shift+") + this.key);
   },
   inMenu: true,
@@ -121,21 +121,33 @@ Tools.webConsole = {
 
     panel.focusInput();
     return undefined;
   },
 
   isTargetSupported: function () {
     return true;
   },
-
   build: function (iframeWindow, toolbox) {
     return new WebConsolePanel(iframeWindow, toolbox);
   }
 };
+function switchWebconsole() {
+  if (Services.prefs.getBoolPref("devtools.webconsole.new-frontend-enabled")) {
+    Tools.webConsole.url = Tools.webConsole.newWebConsoleURL;
+  } else {
+    Tools.webConsole.url = Tools.webConsole.oldWebConsoleURL;
+  }
+}
+switchWebconsole();
+
+Services.prefs.addObserver(
+  "devtools.webconsole.new-frontend-enabled",
+  { observe: switchWebconsole }
+);
 
 Tools.jsdebugger = {
   id: "jsdebugger",
   key: l10n("debuggerMenu.commandkey"),
   accesskey: l10n("debuggerMenu.accesskey"),
   modifiers: osString == "Darwin" ? "accel,alt" : "accel,shift",
   ordinal: 3,
   icon: "chrome://devtools/skin/images/tool-debugger.svg",
--- a/devtools/client/framework/toolbox-options.xhtml
+++ b/devtools/client/framework/toolbox-options.xhtml
@@ -197,21 +197,16 @@
                    data-pref="devtools.chrome.enabled"/>
             <span>&options.enableChrome.label5;</span>
           </label>
           <label title="&options.enableRemote.tooltip2;">
             <input type="checkbox"
                    data-pref="devtools.debugger.remote-enabled"/>
             <span>&options.enableRemote.label3;</span>
           </label>
-          <label title="&options.enableWorkers.tooltip;">
-            <input type="checkbox"
-                   data-pref="devtools.debugger.workers"/>
-            <span>&options.enableWorkers.label;</span>
-          </label>
           <span class="options-citation-label theme-comment"
           >&options.context.triggersPageRefresh;</span>
       </fieldset>
     </div>
 
   </form>
   </body>
 </html>
--- a/devtools/client/framework/toolbox-process-window.js
+++ b/devtools/client/framework/toolbox-process-window.js
@@ -61,18 +61,18 @@ function setPrefDefaults() {
   Services.prefs.setBoolPref("devtools.inspector.showAllAnonymousContent", true);
   Services.prefs.setBoolPref("browser.dom.window.dump.enabled", true);
   Services.prefs.setBoolPref("devtools.command-button-noautohide.enabled", true);
   Services.prefs.setBoolPref("devtools.scratchpad.enabled", true);
   // Bug 1225160 - Using source maps with browser debugging can lead to a crash
   Services.prefs.setBoolPref("devtools.debugger.source-maps-enabled", false);
   Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", true);
   Services.prefs.setBoolPref("devtools.debugger.client-source-maps-enabled", true);
+  Services.prefs.setBoolPref("devtools.webconsole.new-frontend-enabled", false);
 }
-
 window.addEventListener("load", function () {
   let cmdClose = document.getElementById("toolbox-cmd-close");
   cmdClose.addEventListener("command", onCloseCommand);
   setPrefDefaults();
   connect().catch(e => {
     let errorMessageContainer = document.getElementById("error-message-container");
     let errorMessage = document.getElementById("error-message");
     errorMessage.value = e.message || e;
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -5,16 +5,17 @@
 devtools.jar:
 %   content devtools %content/
     content/shared/vendor/d3.js (shared/vendor/d3.js)
     content/shared/vendor/dagre-d3.js (shared/vendor/dagre-d3.js)
     content/shared/widgets/widgets.css (shared/widgets/widgets.css)
     content/netmonitor/src/assets/styles/netmonitor.css (netmonitor/src/assets/styles/netmonitor.css)
     content/shared/widgets/VariablesView.xul (shared/widgets/VariablesView.xul)
     content/netmonitor/index.html (netmonitor/index.html)
+    content/webconsole/webconsole.xhtml (webconsole/webconsole.xhtml)
     content/webconsole/webconsole.xul (webconsole/webconsole.xul)
     content/scratchpad/scratchpad.xul (scratchpad/scratchpad.xul)
     content/scratchpad/scratchpad.js (scratchpad/scratchpad.js)
     content/shared/splitview.css (shared/splitview.css)
     content/shared/theme-switching.js (shared/theme-switching.js)
     content/shared/frame-script-utils.js (shared/frame-script-utils.js)
     content/styleeditor/styleeditor.xul (styleeditor/styleeditor.xul)
     content/storage/storage.xul (storage/storage.xul)
--- a/devtools/client/locales/en-US/toolbox.dtd
+++ b/devtools/client/locales/en-US/toolbox.dtd
@@ -77,22 +77,16 @@
 <!ENTITY options.enableChrome.tooltip3  "Turning this option on will allow you to use various developer tools in browser context (via Tools > Web Developer > Browser Toolbox) and debug add-ons from the Add-ons Manager">
 
 <!-- LOCALIZATION NOTE (options.enableRemote.label3): This is the label for the
   -  checkbox that toggles remote debugging, i.e. devtools.debugger.remote-enabled
   -  boolean preference in about:config, in the options panel. -->
 <!ENTITY options.enableRemote.label3    "Enable remote debugging">
 <!ENTITY options.enableRemote.tooltip2  "Turning this option on will allow the developer tools to debug a remote instance like Firefox OS">
 
-<!-- LOCALIZATION NOTE (options.enableWorkers.label): This is the label for the
-  -  checkbox that toggles worker debugging, i.e. devtools.debugger.workers
-  -  boolean preference in about:config, in the options panel. -->
-<!ENTITY options.enableWorkers.label    "Enable worker debugging (in development)">
-<!ENTITY options.enableWorkers.tooltip  "Turning this option on will allow the developer tools to debug workers">
-
 <!-- LOCALIZATION NOTE (options.disableJavaScript.label,
   -  options.disableJavaScript.tooltip): This is the options panel label and
   -  tooltip for the checkbox that toggles JavaScript on or off. -->
 <!ENTITY options.disableJavaScript.label     "Disable JavaScript *">
 <!ENTITY options.disableJavaScript.tooltip   "Turning this option on will disable JavaScript for the current tab. If the tab or the toolbox is closed then this setting will be forgotten.">
 
 <!-- LOCALIZATION NOTE (options.disableHTTPCache.label,
   -  options.disableHTTPCache.tooltip): This is the options panel label and
--- a/devtools/client/locales/en-US/webConsole.dtd
+++ b/devtools/client/locales/en-US/webConsole.dtd
@@ -1,26 +1,21 @@
 <!-- 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/. -->
-
 <!-- LOCALIZATION NOTE : FILE The correct localization of this file might be to
   - keep it in English, or another language commonly spoken among web developers.
   - You want to make that choice consistent across the developer tools.
   - A good criteria is the language in which you'd find the best
   - documentation on web development on the web. -->
-
 <!ENTITY window.title "Web Console">
-<!ENTITY browserConsole.title "Browser Console">
-
 <!-- LOCALIZATION NOTE (openURL.label): You can see this string in the Web
    - Console context menu. -->
 <!ENTITY openURL.label     "Open URL in New Tab">
 <!ENTITY openURL.accesskey "T">
-
 <!-- LOCALIZATION NOTE (btnPageNet.label): This string is used for the menu
   -  button that allows users to toggle the network logging output.
   -  This string and the following strings toggle various kinds of output
   -  filters. -->
 <!ENTITY btnPageNet.label   "Net">
 <!ENTITY btnPageNet.tooltip "Log network access">
 <!ENTITY btnPageNet.accesskey "N">
 <!-- LOCALIZATION NOTE (btnPageNet.accesskeyMacOSX): This string is used as
--- a/devtools/client/locales/en-US/webconsole.properties
+++ b/devtools/client/locales/en-US/webconsole.properties
@@ -1,26 +1,24 @@
 # 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/.
-
 # LOCALIZATION NOTE
 # The correct localization of this file might be to keep it in
 # English, or another language commonly spoken among web developers.
 # You want to make that choice consistent across the developer tools.
 # A good criteria is the language in which you'd find the best
 # documentation on web development on the web.
-
-
+# LOCALIZATION NOTE (browserConsole.title): shown as the
+# title when opening the browser console popup
+browserConsole.title=Browser Console
 # LOCALIZATION NOTE (timestampFormat): %1$02S = hours (24-hour clock),
 # %2$02S = minutes, %3$02S = seconds, %4$03S = milliseconds.
 timestampFormat=%02S:%02S:%02S.%03S
-
 helperFuncUnsupportedTypeError=Can’t call pprint on this type of object.
-
 # LOCALIZATION NOTE (NetworkPanel.deltaDurationMS): this string is used to
 # show the duration between two network events (e.g request and response
 # header or response header and response body). Parameters: %S is the duration.
 NetworkPanel.durationMS=%Sms
 
 ConsoleAPIDisabled=The Web Console logging API (console.log, console.info, console.warn, console.error) has been disabled by a script on this page.
 
 # LOCALIZATION NOTE (webConsoleWindowTitleAndURL): the Web Console floating
--- a/devtools/client/memory/components/tree-map/canvas-utils.js
+++ b/devtools/client/memory/components/tree-map/canvas-utils.js
@@ -10,17 +10,17 @@
  * Create 2 canvases and contexts for drawing onto, 1 main canvas, and 1 zoom
  * canvas. The main canvas dimensions match the parent div, but the CSS can be
  * transformed to be zoomed and dragged around (potentially creating a blurry
  * canvas once zoomed in). The zoom canvas is a zoomed in section that matches
  * the parent div's dimensions and is kept in place through CSS. A zoomed in
  * view of the visualization is drawn onto this canvas, providing a crisp zoomed
  * in view of the tree map.
  */
-const { debounce } = require("sdk/lang/functional");
+const { debounce } = require("devtools/shared/debounce");
 const EventEmitter = require("devtools/shared/event-emitter");
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const FULLSCREEN_STYLE = {
   width: "100%",
   height: "100%",
   position: "absolute",
 };
--- a/devtools/client/memory/components/tree-map/drag-zoom.js
+++ b/devtools/client/memory/components/tree-map/drag-zoom.js
@@ -1,15 +1,15 @@
 /* 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/. */
 
 "use strict";
 
-const { debounce } = require("sdk/lang/functional");
+const { debounce } = require("devtools/shared/debounce");
 const { lerp } = require("devtools/client/memory/utils");
 const EventEmitter = require("devtools/shared/event-emitter");
 
 const LERP_SPEED = 0.5;
 const ZOOM_SPEED = 0.01;
 const TRANSLATE_EPSILON = 1;
 const ZOOM_EPSILON = 0.001;
 const LINE_SCROLL_MODE = 1;
--- a/devtools/client/performance/test/helpers/input-utils.js
+++ b/devtools/client/performance/test/helpers/input-utils.js
@@ -6,17 +6,17 @@ exports.HORIZONTAL_AXIS = 1;
 exports.VERTICAL_AXIS = 2;
 
 /**
  * Simulates a command event on an element.
  */
 exports.command = (node) => {
   let ev = node.ownerDocument.createEvent("XULCommandEvent");
   ev.initCommandEvent("command", true, true, node.ownerDocument.defaultView, 0, false,
-                      false, false, false, null);
+                      false, false, false, null, 0);
   node.dispatchEvent(ev);
 };
 
 /**
  * Simulates a click event on a devtools canvas graph.
  */
 exports.clickCanvasGraph = (graph, { x, y }) => {
   x = x || 0;
--- a/devtools/client/shared/vendor/react-dom.js
+++ b/devtools/client/shared/vendor/react-dom.js
@@ -146,20 +146,22 @@
     // This execution context doesn't know about XULDocuments, so don't get the toolbox.
     if (typeof XULDocument !== "function") {
       return null;
     }
 
     let doc = node.ownerDocument;
     const inspectorUrl = "chrome://devtools/content/inspector/inspector.xhtml";
     const netMonitorUrl = "chrome://devtools/content/netmonitor/netmonitor.xhtml";
+    const webConsoleUrl = "chrome://devtools/content/webconsole/webconsole.xhtml";
 
     while (doc instanceof XULDocument ||
            doc.location.href === inspectorUrl ||
-           doc.location.href === netMonitorUrl) {
+           doc.location.href === netMonitorUrl ||
+           doc.location.href === webConsoleUrl) {
       const {frameElement} = doc.defaultView;
 
       if (!frameElement) {
         // We're at the root element, and no toolbox was found.
         return null;
       }
 
       doc = frameElement.parentElement.ownerDocument;
--- a/devtools/client/shared/widgets/cubic-bezier.css
+++ b/devtools/client/shared/widgets/cubic-bezier.css
@@ -10,48 +10,48 @@
   width: 510px;
   height: 370px;
   flex-direction: row-reverse;
   overflow: hidden;
   padding: 5px;
   box-sizing: border-box;
 }
 
-.display-wrap {
+.cubic-bezier-container .display-wrap {
   width: 50%;
   height: 100%;
   text-align: center;
   overflow: hidden;
 }
 
 /* Coordinate Plane */
 
-.coordinate-plane {
+.cubic-bezier-container .coordinate-plane {
   width: 150px;
   height: 370px;
   margin: 0 auto;
   position: relative;
 }
 
-.control-point {
+.cubic-bezier-container .control-point {
   position: absolute;
   z-index: 1;
   height: 10px;
   width: 10px;
   border: 0;
   background: #666;
   display: block;
   margin: -5px 0 0 -5px;
   outline: none;
   border-radius: 5px;
   padding: 0;
   cursor: pointer;
 }
 
-.display-wrap {
+.cubic-bezier-container .display-wrap {
   background:
   repeating-linear-gradient(0deg,
     transparent,
     var(--bezier-grid-color) 0,
     var(--bezier-grid-color) 1px,
     transparent 1px,
     transparent 15px) no-repeat,
   repeating-linear-gradient(90deg,
@@ -61,65 +61,65 @@
     transparent 1px,
     transparent 15px) no-repeat;
   background-size: 100% 100%, 100% 100%;
   background-position: -2px 5px, -2px 5px;
 
   -moz-user-select: none;
 }
 
-canvas.curve {
+.cubic-bezier-container canvas.curve {
   background:
     linear-gradient(-45deg,
       transparent 49.7%,
       var(--bezier-diagonal-color) 49.7%,
       var(--bezier-diagonal-color) 50.3%,
       transparent 50.3%) center no-repeat;
   background-size: 100% 100%;
   background-position: 0 0;
 }
 
 /* Timing Function Preview Widget */
 
-.timing-function-preview {
+.cubic-bezier-container .timing-function-preview {
   position: absolute;
   bottom: 20px;
   right: 45px;
   width: 150px;
 }
 
-.timing-function-preview .scale {
+.cubic-bezier-container .timing-function-preview .scale {
   position: absolute;
   top: 6px;
   left: 0;
   z-index: 1;
 
   width: 150px;
   height: 1px;
 
   background: #ccc;
 }
 
-.timing-function-preview .dot {
+.cubic-bezier-container .timing-function-preview .dot {
   position: absolute;
   top: 0;
   left: -7px;
   z-index: 2;
 
   width: 10px;
   height: 10px;
 
   border-radius: 50%;
   border: 2px solid white;
   background: #4C9ED9;
 }
 
 /* Preset Widget */
 
-.preset-pane {
+.cubic-bezier-container .preset-pane {
   width: 50%;
   height: 100%;
   border-right: 1px solid var(--theme-splitter-color);
   padding-right: 4px; /* Visual balance for the panel-arrowcontent border on the left */
 }
 
 #preset-categories {
   display: flex;
@@ -129,88 +129,89 @@ canvas.curve {
   background-color: var(--theme-toolbar-background);
   margin: 3px auto 0 auto;
 }
 
 #preset-categories .category:last-child {
   border-right: none;
 }
 
-.category {
+.cubic-bezier-container .category {
   padding: 5px 0px;
   width: 33.33%;
   text-align: center;
   text-transform: capitalize;
   border-right: 1px solid var(--theme-splitter-color);
   cursor: default;
   color: var(--theme-body-color);
   text-overflow: ellipsis;
   overflow: hidden;
 }
 
-.category:hover {
+.cubic-bezier-container .category:hover {
   background-color: var(--theme-tab-toolbar-background);
 }
 
-.active-category {
+.cubic-bezier-container .active-category {
   background-color: var(--theme-selection-background);
   color: var(--theme-selection-color);
 }
 
-.active-category:hover {
+.cubic-bezier-container .active-category:hover {
   background-color: var(--theme-selection-background);
 }
 
 #preset-container {
   padding: 0px;
   width: 100%;
   height: 331px;
   overflow-y: auto;
 }
 
-.preset-list {
+.cubic-bezier-container .preset-list {
   display: none;
   padding-top: 6px;
 }
 
-.active-preset-list {
+.cubic-bezier-container .active-preset-list {
   display: flex;
   flex-wrap: wrap;
   justify-content: flex-start;
 }
 
-.preset {
+.cubic-bezier-container .preset {
   cursor: pointer;
   width: 33.33%;
   margin: 5px 0px;
   text-align: center;
 }
 
-.preset canvas {
+.cubic-bezier-container .preset canvas {
   display: block;
   border: 1px solid var(--theme-splitter-color);
   border-radius: 3px;
   background-color: var(--theme-body-background);
   margin: 0 auto;
 }
 
-.preset p {
+.cubic-bezier-container .preset p {
   font-size: 80%;
   margin: 2px auto 0px auto;
   color: var(--theme-body-color-alt);
   text-transform: capitalize;
   text-overflow: ellipsis;
   overflow: hidden;
 }
 
-.active-preset p, .active-preset:hover p {
+.cubic-bezier-container .active-preset p,
+.cubic-bezier-container .active-preset:hover p {
   color: var(--theme-body-color);
 }
 
-.preset:hover canvas {
+.cubic-bezier-container .preset:hover canvas {
   border-color: var(--theme-selection-background);
 }
 
-.active-preset canvas,
-.active-preset:hover canvas {
+.cubic-bezier-container .active-preset canvas,
+.cubic-bezier-container .active-preset:hover canvas {
   background-color: var(--theme-selection-background-semitransparent);
   border-color: var(--theme-selection-background);
 }
--- a/devtools/client/shared/widgets/filter-widget.css
+++ b/devtools/client/shared/widgets/filter-widget.css
@@ -14,31 +14,31 @@
   /* when opened in a xul:panel, a gray color is applied to text */
   color: var(--theme-body-color);
 }
 
 #filter-container.dragging {
   -moz-user-select: none;
 }
 
-.filters-list,
-.presets-list {
+#filter-container .filters-list,
+#filter-container .presets-list {
   display: flex;
   flex-direction: column;
   box-sizing: border-box;
 }
 
-.filters-list {
+#filter-container .filters-list {
   /* Allow the filters list to take the full width when the presets list is
      hidden */
   flex-grow: 1;
   padding: 0 6px;
 }
 
-.presets-list {
+#filter-container .presets-list {
   /* Make sure that when the presets list is shown, it has a fixed width */
   width: 200px;
   padding-left: 6px;
   transition: width .1s;
   flex-shrink: 0;
   border-left: 1px solid var(--theme-splitter-color);
 }
 
@@ -50,189 +50,190 @@
 
 #filter-container.show-presets .filters-list {
   width: 300px;
 }
 
 /* The list of filters and list of presets should push their footers to the
    bottom, so they can take as much space as there is */
 
-#filters,
-#presets {
+#filter-container #filters,
+#filter-container #presets {
   flex-grow: 1;
   /* Avoid pushing below the tooltip's area */
   overflow-y: auto;
 }
 
 /* The filters and presets list both have footers displayed at the bottom.
    These footers have some input (taking up as much space as possible) and an
    add button next */
 
-.footer {
+#filter-container .footer {
   display: flex;
   margin: 10px 3px;
   align-items: center;
 }
 
-.footer :not(button) {
+#filter-container .footer :not(button) {
   flex-grow: 1;
   margin-right: 3px;
 }
 
 /* Styles for 1 filter function item */
 
-.filter,
-.filter-name,
-.filter-value {
+#filter-container .filter,
+#filter-container .filter-name,
+#filter-container .filter-value {
   display: flex;
   align-items: center;
 }
 
-.filter {
+#filter-container .filter {
   margin: 5px 0;
 }
 
-.filter-name {
+#filter-container .filter-name {
   width: 120px;
   margin-right: 10px;
 }
 
-.filter-name label {
+#filter-container .filter-name label {
   -moz-user-select: none;
   flex-grow: 1;
 }
 
-.filter-name label.devtools-draglabel {
+#filter-container .filter-name label.devtools-draglabel {
   cursor: ew-resize;
 }
 
 /* drag/drop handle */
 
-.filter-name i {
+#filter-container .filter-name i {
   width: 10px;
   height: 10px;
   margin-right: 10px;
   cursor: grab;
   background: linear-gradient(to bottom,
                               currentColor 0,
                               currentcolor 1px,
                               transparent 1px,
                               transparent 2px);
   background-repeat: repeat-y;
   background-size: auto 4px;
   background-position: 0 1px;
 }
 
-.filter-value {
+#filter-container .filter-value {
   min-width: 150px;
   margin-right: 10px;
   flex: 1;
 }
 
-.filter-value input {
+#filter-container .filter-value input {
   flex-grow: 1;
 }
 
 /* Fix the size of inputs */
 /* Especially needed on Linux where input are bigger */
-input {
+#filter-container input {
   width: 8em;
 }
 
-.preset {
+#filter-container .preset {
   display: flex;
   margin-bottom: 10px;
   cursor: pointer;
   padding: 3px 5px;
 
   flex-direction: row;
   flex-wrap: wrap;
 }
 
-.preset label,
-.preset span {
+#filter-container .preset label,
+#filter-container .preset span {
   display: flex;
   align-items: center;
 }
 
-.preset label {
+#filter-container .preset label {
   flex: 1 0;
   cursor: pointer;
   color: var(--theme-body-color);
 }
 
-.preset:hover {
+#filter-container .preset:hover {
   background: var(--theme-selection-background);
 }
 
-.preset:hover label, .preset:hover span {
+#filter-container .preset:hover label,
+#filter-container .preset:hover span {
   color: var(--theme-selection-color);
 }
 
-.preset .remove-button {
+#filter-container .preset .remove-button {
   order: 2;
 }
 
-.preset span {
+#filter-container .preset span {
   flex: 2 100%;
   white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
   display: block;
   order: 3;
   color: var(--theme-body-color-alt);
 }
 
-.remove-button {
+#filter-container .remove-button {
   width: 16px;
   height: 16px;
   background: url(chrome://devtools/skin/images/close.svg);
   background-size: cover;
   font-size: 0;
   border: none;
   cursor: pointer;
 }
 
-.hidden {
+#filter-container .hidden {
   display: none !important;
 }
 
 #filter-container .dragging {
   position: relative;
   z-index: 10;
   cursor: grab;
 }
 
 /* message shown when there's no filter specified */
 #filter-container p {
   text-align: center;
   line-height: 20px;
 }
 
-.add,
+#filter-container .add,
 #toggle-presets {
   background-size: cover;
   border: none;
   width: 16px;
   height: 16px;
   font-size: 0;
   vertical-align: middle;
   cursor: pointer;
   margin: 0 5px;
 }
 
-.add {
+#filter-container .add {
   background: url(chrome://devtools/skin/images/add.svg);
 }
 
 #toggle-presets {
   background: url(chrome://devtools/skin/images/pseudo-class.svg);
 }
 
-.add,
-.remove-button,
+#filter-container .add,
+#filter-container .remove-button,
 #toggle-presets {
   filter: var(--icon-filter);
 }
 
 .show-presets #toggle-presets {
   filter: url(chrome://devtools/skin/images/filters.svg#checked-icon-state);
 }
--- a/devtools/client/shared/widgets/mdn-docs.css
+++ b/devtools/client/shared/widgets/mdn-docs.css
@@ -23,17 +23,17 @@
   overflow: auto;
   transition: opacity 400ms ease-in;
 }
 
 .mdn-syntax {
   margin-top: 1em;
 }
 
-.devtools-throbber {
+.mdn-container .devtools-throbber {
   align-self: center;
   opacity: 0;
 }
 
 .mdn-visit-page {
   display: inline-block;
   padding: 1em 0;
 }
--- a/devtools/client/shared/widgets/tooltip/CssDocsTooltip.js
+++ b/devtools/client/shared/widgets/tooltip/CssDocsTooltip.js
@@ -18,18 +18,17 @@ const TOOLTIP_HEIGHT = 308;
  * @param {Document} toolboxDoc
  *        The toolbox document to attach the CSS docs tooltip.
  */
 function CssDocsTooltip(toolboxDoc) {
   this.tooltip = new HTMLTooltip(toolboxDoc, {
     type: "arrow",
     consumeOutsideClicks: true,
     autofocus: true,
-    useXulWrapper: true,
-    stylesheet: "chrome://devtools/content/shared/widgets/mdn-docs.css",
+    useXulWrapper: true
   });
   this.widget = this.setMdnDocsContent();
   this._onVisitLink = this._onVisitLink.bind(this);
   this.widget.on("visitlink", this._onVisitLink);
 
   // Initialize keyboard shortcuts
   this.shortcuts = new KeyShortcuts({ window: this.tooltip.topWindow });
   this._onShortcut = this._onShortcut.bind(this);
--- a/devtools/client/shared/widgets/tooltip/HTMLTooltip.js
+++ b/devtools/client/shared/widgets/tooltip/HTMLTooltip.js
@@ -208,25 +208,22 @@ const getRelativeRect = function (node, 
  *        - {Boolean} autofocus
  *          Defaults to false. Should the tooltip be focused when opening it.
  *        - {Boolean} consumeOutsideClicks
  *          Defaults to true. The tooltip is closed when clicking outside.
  *          Should this event be stopped and consumed or not.
  *        - {Boolean} useXulWrapper
  *          Defaults to false. If the tooltip is hosted in a XUL document, use a XUL panel
  *          in order to use all the screen viewport available.
- *        - {String} stylesheet
- *          Style sheet URL to apply to the tooltip content.
  */
 function HTMLTooltip(toolboxDoc, {
     type = "normal",
     autofocus = false,
     consumeOutsideClicks = true,
     useXulWrapper = false,
-    stylesheet = "",
   } = {}) {
   EventEmitter.decorate(this);
 
   this.doc = toolboxDoc;
   this.type = type;
   this.autofocus = autofocus;
   this.consumeOutsideClicks = consumeOutsideClicks;
   this.useXulWrapper = this._isXUL() && useXulWrapper;
@@ -241,19 +238,16 @@ function HTMLTooltip(toolboxDoc, {
   this._onXulPanelHidden = this._onXulPanelHidden.bind(this);
 
   this._toggle = new TooltipToggle(this);
   this.startTogglingOnHover = this._toggle.start.bind(this._toggle);
   this.stopTogglingOnHover = this._toggle.stop.bind(this._toggle);
 
   this.container = this._createContainer();
 
-  if (stylesheet) {
-    this._applyStylesheet(stylesheet);
-  }
   if (this.useXulWrapper) {
     // When using a XUL panel as the wrapper, the actual markup for the tooltip is as
     // follows :
     // <panel> <!-- XUL panel used to position the tooltip anywhere on screen -->
     //   <div> <!-- div wrapper used to isolate the tooltip container -->
     //     <div> <! the actual tooltip.container element -->
     this.xulPanelWrapper = this._createXulPanelWrapper();
     let inner = this.doc.createElementNS(XHTML_NS, "div");
@@ -629,21 +623,9 @@ HTMLTooltip.prototype = {
    */
   _convertToScreenRect: function ({left, top, width, height}) {
     // mozInnerScreenX/Y are the coordinates of the top left corner of the window's
     // viewport, excluding chrome UI.
     left += this.doc.defaultView.mozInnerScreenX;
     top += this.doc.defaultView.mozInnerScreenY;
     return {top, right: left + width, bottom: top + height, left, width, height};
   },
-
-  /**
-   * Apply a scoped stylesheet to the container so that this css file only
-   * applies to it.
-   */
-  _applyStylesheet: function (url) {
-    let style = this.doc.createElementNS(XHTML_NS, "style");
-    style.setAttribute("scoped", "true");
-    url = url.replace(/"/g, "\\\"");
-    style.textContent = `@import url("${url}");`;
-    this.container.appendChild(style);
-  }
 };
--- a/devtools/client/shared/widgets/tooltip/SwatchBasedEditorTooltip.js
+++ b/devtools/client/shared/widgets/tooltip/SwatchBasedEditorTooltip.js
@@ -14,38 +14,35 @@ const INLINE_TOOLTIP_CLASS = "inline-too
 /**
  * Base class for all (color, gradient, ...)-swatch based value editors inside
  * tooltips
  *
  * @param {Document} document
  *        The document to attach the SwatchBasedEditorTooltip. This is either the toolbox
  *        document if the tooltip is a popup tooltip or the panel's document if it is an
  *        inline editor.
- * @param {String} stylesheet
- *        The stylesheet to be used for the HTMLTooltip.
  * @param {Boolean} useInline
  *        A boolean flag representing whether or not the InlineTooltip should be used.
  */
-function SwatchBasedEditorTooltip(document, stylesheet, useInline) {
+function SwatchBasedEditorTooltip(document, useInline) {
   EventEmitter.decorate(this);
 
   this.useInline = useInline;
 
   // Creating a tooltip instance
   if (useInline) {
     this.tooltip = new InlineTooltip(document);
   } else {
     // This one will consume outside clicks as it makes more sense to let the user
     // close the tooltip by clicking out
     // It will also close on <escape> and <enter>
     this.tooltip = new HTMLTooltip(document, {
       type: "arrow",
       consumeOutsideClicks: true,
       useXulWrapper: true,
-      stylesheet
     });
   }
 
   // By default, swatch-based editor tooltips revert value change on <esc> and
   // commit value change on <enter>
   this.shortcuts = new KeyShortcuts({
     window: this.tooltip.topWindow
   });
--- a/devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js
+++ b/devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js
@@ -33,20 +33,17 @@ const XHTML_NS = "http://www.w3.org/1999
  * @param {InspectorPanel} inspector
  *        The inspector panel, needed for the eyedropper.
  * @param {Function} supportsCssColor4ColorFunction
  *        A function for checking the supporting of css-color-4 color function.
  */
 function SwatchColorPickerTooltip(document,
                                   inspector,
                                   {supportsCssColor4ColorFunction}) {
-  let stylesheet = NEW_COLOR_WIDGET ?
-    "chrome://devtools/content/shared/widgets/color-widget.css" :
-    "chrome://devtools/content/shared/widgets/spectrum.css";
-  SwatchBasedEditorTooltip.call(this, document, stylesheet);
+  SwatchBasedEditorTooltip.call(this, document);
 
   this.inspector = inspector;
 
   // Creating a spectrum instance. this.spectrum will always be a promise that
   // resolves to the spectrum instance
   this.spectrum = this.setColorPickerContent([0, 0, 0, 1]);
   this._onSpectrumColorChange = this._onSpectrumColorChange.bind(this);
   this._openEyeDropper = this._openEyeDropper.bind(this);
--- a/devtools/client/shared/widgets/tooltip/SwatchCubicBezierTooltip.js
+++ b/devtools/client/shared/widgets/tooltip/SwatchCubicBezierTooltip.js
@@ -21,18 +21,17 @@ const XHTML_NS = "http://www.w3.org/1999
  * CubicBezierWidget.
  *
  * @param {Document} document
  *        The document to attach the SwatchCubicBezierTooltip. This is either the toolbox
  *        document if the tooltip is a popup tooltip or the panel's document if it is an
  *        inline editor.
  */
 function SwatchCubicBezierTooltip(document) {
-  let stylesheet = "chrome://devtools/content/shared/widgets/cubic-bezier.css";
-  SwatchBasedEditorTooltip.call(this, document, stylesheet);
+  SwatchBasedEditorTooltip.call(this, document);
 
   // Creating a cubic-bezier instance.
   // this.widget will always be a promise that resolves to the widget instance
   this.widget = this.setCubicBezierContent([0, 0, 1, 1]);
   this._onUpdate = this._onUpdate.bind(this);
 }
 
 SwatchCubicBezierTooltip.prototype = Heritage.extend(SwatchBasedEditorTooltip.prototype, {
--- a/devtools/client/shared/widgets/tooltip/SwatchFilterTooltip.js
+++ b/devtools/client/shared/widgets/tooltip/SwatchFilterTooltip.js
@@ -23,18 +23,17 @@ const XHTML_NS = "http://www.w3.org/1999
  *        The document to attach the SwatchFilterTooltip. This is either the toolbox
  *        document if the tooltip is a popup tooltip or the panel's document if it is an
  *        inline editor.
  * @param {function} cssIsValid
  *        A function to check that css declaration's name and values are valid together.
  *        This can be obtained from "shared/fronts/css-properties.js".
  */
 function SwatchFilterTooltip(document, cssIsValid) {
-  let stylesheet = "chrome://devtools/content/shared/widgets/filter-widget.css";
-  SwatchBasedEditorTooltip.call(this, document, stylesheet);
+  SwatchBasedEditorTooltip.call(this, document);
   this._cssIsValid = cssIsValid;
 
   // Creating a filter editor instance.
   this.widget = this.setFilterContent("none");
   this._onUpdate = this._onUpdate.bind(this);
 }
 
 SwatchFilterTooltip.prototype = Heritage.extend(SwatchBasedEditorTooltip.prototype, {
deleted file mode 100644
--- a/devtools/client/themes/new-webconsole.css
+++ /dev/null
@@ -1,600 +0,0 @@
-
-@import "chrome://devtools/skin/widgets.css";
-@import "resource://devtools/client/themes/light-theme.css";
-
-/* Webconsole specific theme variables */
-.theme-light,
-.theme-firebug {
-  --error-color: #FF0000;
-  --error-background-color: #FFEBEB;
-  --warning-background-color: #FFFFC8;
-}
-
-/* General output styles */
-
-a {
-  -moz-user-focus: normal;
-  -moz-user-input: enabled;
-  cursor: pointer;
-  text-decoration: underline;
-}
-
-/* Workaround for Bug 575675 - FindChildWithRules aRelevantLinkVisited
- * assertion when loading HTML page with links in XUL iframe */
-*:visited { }
-
-.webconsole-filterbar-wrapper {
-  flex-grow: 0;
-}
-
-.webconsole-filterbar-primary {
-  display: flex;
-}
-
-.devtools-toolbar.webconsole-filterbar-secondary {
-  height: initial;
-}
-
-.webconsole-filterbar-primary .devtools-plaininput {
-  flex: 1 1 100%;
-}
-
-.webconsole-output.hideTimestamps > .message > .timestamp {
-  display: none;
-}
-
-.message.startGroup .message-body > .objectBox-string,
-.message.startGroupCollapsed .message-body > .objectBox-string {
-  color: var(--theme-body-color);
-  font-weight: bold;
-}
-
-.webconsole-output-wrapper .message > .icon {
-  margin: 3px 0 0 0;
-  padding: 0 0 0 6px;
-}
-
-.message.error > .icon::before {
-  background-position: -12px -36px;
-}
-
-.message.warn > .icon::before {
-  background-position: -24px -36px;
-}
-
-.message.info > .icon::before {
-  background-position: -36px -36px;
-}
-
-.message.network .method {
-  margin-inline-end: 5px;
-}
-
-.network .message-flex-body > .message-body {
-  display: flex;
-}
-
-.webconsole-output-wrapper .message .indent {
-  display: inline-block;
-  border-inline-end: solid 1px var(--theme-splitter-color);
-}
-
-.message.startGroup .indent,
-.message.startGroupCollapsed .indent {
-  border-inline-end-color: transparent;
-  margin-inline-end: 5px;
-}
-
-.message.startGroup .icon,
-.message.startGroupCollapsed .icon {
-  display: none;
-}
-
-/* console.table() */
-.new-consoletable {
-  width: 100%;
-  border-collapse: collapse;
-  --consoletable-border: 1px solid var(--table-splitter-color);
-}
-
-.new-consoletable thead,
-.new-consoletable tbody {
-  background-color: var(--theme-body-background);
-}
-
-.new-consoletable th {
-  background-color: var(--theme-selection-background);
-  color: var(--theme-selection-color);
-  margin: 0;
-  padding: 5px 0 0;
-  font-weight: inherit;
-  border-inline-end: var(--consoletable-border);
-  border-bottom: var(--consoletable-border);
-}
-
-.new-consoletable tr:nth-of-type(even) {
-  background-color: var(--table-zebra-background);
-}
-
-.new-consoletable td {
-  padding: 3px 4px;
-  min-width: 100px;
-  -moz-user-focus: normal;
-  color: var(--theme-body-color);
-  border-inline-end: var(--consoletable-border);
-  height: 1.25em;
-  line-height: 1.25em;
-}
-
-
-/* Layout */
-.webconsole-output {
-  flex: 1;
-  direction: ltr;
-  overflow: auto;
-  -moz-user-select: text;
-  position: relative;
-}
-
-:root,
-body,
-#app-wrapper {
-  height: 100%;
-  margin: 0;
-  padding: 0;
-}
-
-body {
-  overflow: hidden;
-}
-
-#app-wrapper {
-  display: flex;
-  flex-direction: column;
-}
-
-:root, body {
-  margin: 0;
-  padding: 0;
-  height: 100%;
-}
-
-#app-wrapper {
-  height: 100%;
-  display: flex;
-  flex-direction: column;
-}
-#left-wrapper {
-  flex: 1;
-  display: flex;
-  flex-direction: column;
-}
-#output-container {
-  flex: 1;
-  overflow: hidden;
-}
-.webconsole-output-wrapper {
-  display: flex;
-  flex-direction: column;
-  height: 100%;
-}
-
-.message {
-  display: flex;
-  padding: 0 7px;
-  width: 100%;
-  box-sizing: border-box;
-}
-
-.message > .prefix,
-.message > .timestamp {
-  flex: none;
-  color: var(--theme-comment);
-  margin: 3px 6px 0 0;
-}
-
-.message > .indent {
-  flex: none;
-}
-
-.message > .icon {
-  flex: none;
-  margin: 3px 6px 0 0;
-  padding: 0 4px;
-  height: 1em;
-  align-self: flex-start;
-}
-
-.theme-firebug .message > .icon {
-  margin: 0;
-  margin-inline-end: 6px;
-}
-
-.theme-firebug .message[severity="error"],
-.theme-light .message.error,
-.theme-firebug .message.error {
-  color: var(--error-color);
-  background-color: var(--error-background-color);
-}
-
-.theme-firebug .message[severity="warn"],
-.theme-light .message.warn,
-.theme-firebug .message.warn {
-  background-color: var(--warning-background-color);
-}
-
-.message > .icon::before {
-  content: "";
-  background-image: url(chrome://devtools/skin/images/webconsole.svg);
-  background-position: 12px 12px;
-  background-repeat: no-repeat;
-  background-size: 72px 60px;
-  width: 12px;
-  height: 12px;
-  display: inline-block;
-}
-
-.theme-light .message > .icon::before {
-  background-image: url(chrome://devtools/skin/images/webconsole.svg#light-icons);
-}
-
-.message > .message-body-wrapper {
-  flex: auto;
-  min-width: 0px;
-  margin: 3px;
-}
-
-/* The red bubble that shows the number of times a message is repeated */
-.message-repeats {
-  -moz-user-select: none;
-  flex: none;
-  margin: 2px 6px;
-  padding: 0 6px;
-  height: 1.25em;
-  color: white;
-  background-color: red;
-  border-radius: 40px;
-  font: message-box;
-  font-size: 0.9em;
-  font-weight: 600;
-}
-
-.message-repeats[value="1"] {
-  display: none;
-}
-
-.message-location {
-  max-width: 40%;
-}
-
-.stack-trace {
-  /* The markup contains extra whitespace to improve formatting of clipboard text.
-     Make sure this whitespace doesn't affect the HTML rendering */
-  white-space: normal;
-}
-
-.stack-trace .frame-link-source,
-.message-location .frame-link-source {
-  /* Makes the file name truncated (and ellipsis shown) on the left side */
-  direction: rtl;
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
-}
-
-.stack-trace .frame-link-source-inner,
-.message-location .frame-link-source-inner {
-  /* Enforce LTR direction for the file name - fixes bug 1290056 */
-  direction: ltr;
-  unicode-bidi: embed;
-}
-
-.stack-trace .frame-link-function-display-name {
-  max-width: 50%;
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
-}
-
-.message-flex-body {
-  display: flex;
-}
-
-.message-body > * {
-  white-space: pre-wrap;
-  word-wrap: break-word;
-}
-
-.message-flex-body > .message-body {
-  display: block;
-  flex: auto;
-}
-#output-container.hideTimestamps > .message {
-  padding-inline-start: 0;
-  margin-inline-start: 7px;
-  width: calc(100% - 7px);
-}
-
-#output-container.hideTimestamps > .message > .timestamp {
-  display: none;
-}
-
-#output-container.hideTimestamps > .message > .indent {
-  background-color: var(--theme-body-background);
-}
-.message:hover {
-  background-color: var(--theme-selection-background-semitransparent) !important;
-}
-.theme-light .message.error {
-  background-color: rgba(255, 150, 150, 0.3);
-}
-
-.theme-dark .message.error {
-  background-color: rgba(235, 83, 104, 0.17);
-}
-
-.console-string {
-  color: var(--theme-highlight-lightorange);
-}
-.theme-selected .console-string,
-.theme-selected .cm-number,
-.theme-selected .cm-variable,
-.theme-selected .kind-ArrayLike {
-  color: #f5f7fa !important; /* Selection Text Color */
-}
-
-
-.message.network.error > .icon::before {
-  background-position: -12px 0;
-}
-.message.network > .message-body {
-  display: flex;
-  flex-wrap: wrap;
-}
-
-
-.message.network .method {
-  flex: none;
-}
-.message.network:not(.navigation-marker) .url {
-  flex: 1 1 auto;
-  /* Make sure the URL is very small initially, let flex change width as needed. */
-  width: 100px;
-  min-width: 5em;
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
-}
-.message.network .status {
-  flex: none;
-  margin-inline-start: 6px;
-}
-.message.network.mixed-content .url {
-  color: var(--theme-highlight-red);
-}
-
-.message .learn-more-link {
-  color: var(--theme-highlight-blue);
-  margin: 0 6px;
-}
-
-.message.network .xhr {
-  background-color: var(--theme-body-color-alt);
-  color: var(--theme-body-background);
-  border-radius: 3px;
-  font-weight: bold;
-  font-size: 10px;
-  padding: 2px;
-  line-height: 10px;
-  margin-inline-start: 3px;
-  margin-inline-end: 1ex;
-}
-.message.cssparser > .indent  {
-  border-inline-end: solid #00b6f0 6px;
-}
-.message.cssparser.error > .icon::before {
-  background-position: -12px -12px;
-}
-
-.message.cssparser.warn > .icon::before {
-  background-position: -24px -12px;
-}
-.message.exception > .indent {
-  border-inline-end: solid #fb9500 6px;
-}
-
-.message.exception.error > .icon::before {
-  background-position: -12px -24px;
-}
-.message.exception.warn > .icon::before {
-  background-position: -24px -24px;
-}
-.message.console-api > .indent {
-  border-inline-end: solid #cbcbcb 6px;
-}
-
-.message.server > .indent {
-  border-inline-end: solid #90B090 6px;
-}
-
-/* Input and output styles */
-.message.command > .indent,
-.message.result > .indent {
-  border-inline-end: solid #808080 6px;
-}
-
-.message.command > .icon::before {
-  background-position: -48px -36px;
-}
-
-.message.result > .icon::before {
-  background-position: -60px -36px;
-}
-
-
-
-
-/* JSTerm Styles */
-#jsterm-wrapper {
-  flex: 0;
-}
-.jsterm-input-container {
-  background-color: var(--theme-tab-toolbar-background);
-  border-top: 1px solid var(--theme-splitter-color);
-}
-
-.theme-light .jsterm-input-container {
-  /* For light theme use a white background for the input - it looks better
-     than off-white */
-  background-color: #fff;
-  border-top-color: #e0e0e0;
-}
-
-.theme-firebug .jsterm-input-container {
-  border-top: 1px solid #ccc;
-}
-
-.jsterm-input-node,
-.jsterm-complete-node {
-  border: none;
-  padding: 0;
-  padding-inline-start: 20px;
-  margin: 0;
-  -moz-appearance: none;
-  background-color: transparent;
-}
-
-.jsterm-input-node[focused="true"] {
-  background-image: var(--theme-command-line-image-focus);
-  box-shadow: none;
-}
-
-.jsterm-complete-node {
-  color: var(--theme-comment);
-}
-
-.jsterm-input-node-html {
-  width: 100%;
-}
-
-.jsterm-input-node {
-  /* Always allow scrolling on input - it auto expands in js by setting height,
-     but don't want it to get bigger than the window. 24px = toolbar height. */
-  max-height: calc(90vh - 24px);
-  background-image: var(--theme-command-line-image);
-  background-repeat: no-repeat;
-  background-size: 16px 16px;
-  background-position: 4px 50%;
-  color: var(--theme-content-color1);
-}
-
-:-moz-any(.jsterm-input-node,
-          .jsterm-complete-node) > .textbox-input-box > .textbox-textarea {
-  overflow-x: hidden;
-  /* Set padding for console input on textbox to make sure it is inlcuded in
-     scrollHeight that is used when resizing JSTerminal's input. Note: textbox
-     default style has important already */
-  padding: 4px 0 !important;
-}
-#webconsole-notificationbox,
-.jsterm-stack-node {
-  width: 100%;
-}
-
-.message.security > .indent {
-  border-inline-end: solid red 6px;
-}
-
-.message.security.error > .icon::before {
-  background-position: -12px -48px;
-}
-
-.message.security.warn > .icon::before {
-  background-position: -24px -48px;
-}
-
-.navigation-marker {
-  color: #aaa;
-  background: linear-gradient(#aaa, #aaa) no-repeat left 50%;
-  background-size: 100% 2px;
-  margin-top: 6px;
-  margin-bottom: 6px;
-  font-size: 0.9em;
-}
-
-.navigation-marker .url {
-  padding-inline-end: 9px;
-  text-decoration: none;
-  background: var(--theme-body-background);
-}
-
-.theme-light .navigation-marker .url {
-  background: #fff;
-}
-
-.stacktrace {
-  display: none;
-  padding: 5px 10px;
-  margin: 5px 0 0 0;
-  overflow-y: auto;
-  border: 1px solid var(--theme-splitter-color);
-  border-radius: 3px;
-}
-
-.theme-light .message.error .stacktrace {
-  background-color: rgba(255, 255, 255, 0.5);
-}
-
-.theme-dark .message.error .stacktrace {
-  background-color: rgba(0, 0, 0, 0.5);
-}
-
-.message.open .stacktrace {
-  display: block;
-}
-
-.message .theme-twisty {
-  display: inline-block;
-  vertical-align: middle;
-  margin: 3px 0 0 0;
-  flex-shrink: 0;
-}
-
-/*Do not mirror the twisty because container force to ltr */
-.message .theme-twisty:dir(rtl),
-.message .theme-twisty:-moz-locale-dir(rtl) {
-  transform: none;
-}
-
-.cm-s-mozilla a[class] {
-  font-style: italic;
-  text-decoration: none;
-}
-
-.cm-s-mozilla a[class]:hover,
-.cm-s-mozilla a[class]:focus {
-  text-decoration: underline;
-}
-
-a.learn-more-link.webconsole-learn-more-link {
-    font-style: normal;
-}
-
-/* Open DOMNode in inspector button */
-.open-inspector {
-  background: url(chrome://devtools/skin/images/vview-open-inspector.png) no-repeat 0 0;
-  padding-left: 16px;
-  margin-left: 5px;
-  cursor: pointer;
-}
-
-.elementNode:hover .open-inspector,
-.open-inspector:hover {
-  filter: url(images/filters.svg#checked-icon-state);
-}
-
-.elementNode:hover .open-inspector:active,
-.open-inspector:active {
-  filter: url(images/filters.svg#checked-icon-state) brightness(0.9);
-}
-
--- a/devtools/client/themes/tooltips.css
+++ b/devtools/client/themes/tooltips.css
@@ -1,13 +1,20 @@
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* 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/. */
 
+/* Import stylesheets for specific tooltip widgets */
+@import url(chrome://devtools/content/shared/widgets/color-widget.css);
+@import url(chrome://devtools/content/shared/widgets/cubic-bezier.css);
+@import url(chrome://devtools/content/shared/widgets/filter-widget.css);
+@import url(chrome://devtools/content/shared/widgets/mdn-docs.css);
+@import url(chrome://devtools/content/shared/widgets/spectrum.css);
+
 /* Tooltip specific theme variables */
 
 .theme-dark {
   --bezier-diagonal-color: #eee;
   --bezier-grid-color: rgba(0, 0, 0, 0.2);
 }
 
 .theme-light {
--- a/devtools/client/themes/webconsole.css
+++ b/devtools/client/themes/webconsole.css
@@ -397,16 +397,25 @@ a {
 }
 
 .message[category=output] > .icon::before,
 .message.result > .icon::before {
   background-position: -60px -36px;
 }
 
 /* JSTerm Styles */
+
+html #jsterm-wrapper,
+html .jsterm-stack-node,
+html .jsterm-input-node-html,
+html #webconsole-notificationbox {
+  flex: 0;
+  width: 100vw;
+}
+
 .jsterm-input-container {
   background-color: var(--theme-tab-toolbar-background);
   border-top: 1px solid var(--theme-splitter-color);
 }
 
 .theme-light .jsterm-input-container {
   /* For light theme use a white background for the input - it looks better
      than off-white */
@@ -855,8 +864,49 @@ a.learn-more-link.webconsole-learn-more-
   padding: 3px 4px;
   min-width: 100px;
   -moz-user-focus: normal;
   color: var(--theme-body-color);
   border-inline-end: var(--consoletable-border);
   height: 1.25em;
   line-height: 1.25em;
 }
+
+/* Layout */
+
+.webconsole-output {
+  flex: 1;
+  direction: ltr;
+  overflow: auto;
+  -moz-user-select: text;
+  position: relative;
+}
+
+html,
+body,
+#app-wrapper {
+  height: 100%;
+  margin: 0;
+  padding: 0;
+}
+
+body {
+  overflow: hidden;
+}
+
+#app-wrapper {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+
+body #output-container {
+  flex: 1;
+  overflow: hidden;
+}
+
+.webconsole-output-wrapper {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+}
+
+/* Object Inspector */
--- a/devtools/client/webaudioeditor/test/head.js
+++ b/devtools/client/webaudioeditor/test/head.js
@@ -358,17 +358,17 @@ function click(win, element) {
 }
 
 function mouseOver(win, element) {
   EventUtils.sendMouseEvent({ type: "mouseover" }, element, win);
 }
 
 function command(button) {
   let ev = button.ownerDocument.createEvent("XULCommandEvent");
-  ev.initCommandEvent("command", true, true, button.ownerDocument.defaultView, 0, false, false, false, false, null);
+  ev.initCommandEvent("command", true, true, button.ownerDocument.defaultView, 0, false, false, false, false, null, 0);
   button.dispatchEvent(ev);
 }
 
 function isVisible(element) {
   return !element.getAttribute("hidden");
 }
 
 /**
--- a/devtools/client/webaudioeditor/views/context.js
+++ b/devtools/client/webaudioeditor/views/context.js
@@ -1,16 +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/. */
 "use strict";
 
 /* import-globals-from ../includes.js */
 
-const { debounce } = require("sdk/lang/functional");
+const { debounce } = require("devtools/shared/debounce");
 const flags = require("devtools/shared/flags");
 
 // Globals for d3 stuff
 // Default properties of the graph on rerender
 const GRAPH_DEFAULTS = {
   translate: [20, 20],
   scale: 1
 };
--- a/devtools/client/webconsole/hudservice.js
+++ b/devtools/client/webconsole/hudservice.js
@@ -6,27 +6,25 @@
 
 var WebConsoleUtils = require("devtools/client/webconsole/utils").Utils;
 const {extend} = require("devtools/shared/extend");
 var {TargetFactory} = require("devtools/client/framework/target");
 var {Tools} = require("devtools/client/definitions");
 const { Task } = require("devtools/shared/task");
 var promise = require("promise");
 var Services = require("Services");
-
 loader.lazyRequireGetter(this, "Telemetry", "devtools/client/shared/telemetry");
 loader.lazyRequireGetter(this, "WebConsoleFrame", "devtools/client/webconsole/webconsole", true);
+loader.lazyRequireGetter(this, "NewWebConsoleFrame", "devtools/client/webconsole/new-webconsole", true);
 loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
 loader.lazyRequireGetter(this, "DebuggerServer", "devtools/server/main", true);
 loader.lazyRequireGetter(this, "DebuggerClient", "devtools/shared/client/main", true);
 loader.lazyRequireGetter(this, "showDoorhanger", "devtools/client/shared/doorhanger", true);
 loader.lazyRequireGetter(this, "viewSource", "devtools/client/shared/view-source");
-
 const l10n = require("devtools/client/webconsole/webconsole-l10n");
-
 const BROWSER_CONSOLE_WINDOW_FEATURES = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
 
 // The preference prefix for all of the Browser Console filters.
 const BROWSER_CONSOLE_FILTER_PREFS_PREFIX = "devtools.browserconsole.filter.";
 
 var gHudId = 0;
 
 // The HUD service
@@ -196,36 +194,31 @@ HUD_SERVICE.prototype =
         });
     }
 
     let target;
     function getTarget(aConnection)
     {
       return TargetFactory.forRemoteTab(aConnection);
     }
-
     function openWindow(aTarget)
     {
       target = aTarget;
-
       let deferred = promise.defer();
-
-      let win = Services.ww.openWindow(null, Tools.webConsole.url, "_blank",
+      // Using the old frontend for now in the browser console.  This can be switched to
+      // Tools.webConsole.url to use whatever is preffed on.
+      let url = Tools.webConsole.oldWebConsoleURL;
+      let win = Services.ww.openWindow(null, url, "_blank",
                                        BROWSER_CONSOLE_WINDOW_FEATURES, null);
       win.addEventListener("DOMContentLoaded", function () {
-        // Set the correct Browser Console title.
-        let root = win.document.documentElement;
-        root.setAttribute("title", root.getAttribute("browserConsoleTitle"));
-
+          win.document.title = l10n.getStr("browserConsole.title");
         deferred.resolve(win);
       }, {once: true});
-
       return deferred.promise;
     }
-
     connect().then(getTarget).then(openWindow).then((aWindow) => {
       return this.openBrowserConsole(target, aWindow, aWindow)
         .then((aBrowserConsole) => {
           this._browserConsoleDefer.resolve(aBrowserConsole);
           this._browserConsoleDefer = null;
         });
     }, console.error.bind(console));
 
@@ -279,27 +272,27 @@ HUD_SERVICE.prototype =
  *        The window of the web console owner.
  */
 function WebConsole(aTarget, aIframeWindow, aChromeWindow)
 {
   this.iframeWindow = aIframeWindow;
   this.chromeWindow = aChromeWindow;
   this.hudId = "hud_" + ++gHudId;
   this.target = aTarget;
-
   this.browserWindow = this.chromeWindow.top;
-
   let element = this.browserWindow.document.documentElement;
   if (element.getAttribute("windowtype") != gDevTools.chromeWindowType) {
     this.browserWindow = HUDService.currentContext();
   }
-
-  this.ui = new WebConsoleFrame(this);
+  if (aIframeWindow.location.href === Tools.webConsole.newWebConsoleURL) {
+    this.ui = new NewWebConsoleFrame(this);
+  } else {
+    this.ui = new WebConsoleFrame(this);
+  }
 }
-
 WebConsole.prototype = {
   iframeWindow: null,
   chromeWindow: null,
   browserWindow: null,
   hudId: null,
   target: null,
   ui: null,
   _browserConsole: false,
--- a/devtools/client/webconsole/jsterm.js
+++ b/devtools/client/webconsole/jsterm.js
@@ -251,20 +251,22 @@ JSTerm.prototype = {
     };
 
     let doc = this.hud.document;
     let toolbox = gDevTools.getToolbox(this.hud.owner.target);
     let tooltipDoc = toolbox ? toolbox.doc : doc;
     // The popup will be attached to the toolbox document or HUD document in the case
     // such as the browser console which doesn't have a toolbox.
     this.autocompletePopup = new AutocompletePopup(tooltipDoc, autocompleteOptions);
-
     let inputContainer = doc.querySelector(".jsterm-input-container");
     this.completeNode = doc.querySelector(".jsterm-complete-node");
     this.inputNode = doc.querySelector(".jsterm-input-node");
+    // Update the character width and height needed for the popup offset
+    // calculations.
+    this._updateCharSize();
 
     if (this.hud.isBrowserConsole &&
         !Services.prefs.getBoolPref("devtools.chrome.enabled")) {
       inputContainer.style.display = "none";
     } else {
       let okstring = l10n.getStr("selfxss.okstring");
       let msg = l10n.getFormatStr("selfxss.msg", [okstring]);
       this._onPaste = WebConsoleUtils.pasteHandlerGen(
@@ -337,19 +339,17 @@ JSTerm.prototype = {
       switch (helperResult.type) {
         case "clearOutput":
           this.clearOutput();
           break;
         case "clearHistory":
           this.clearHistory();
           break;
         case "inspectObject":
-          if (!this.hud.NEW_CONSOLE_OUTPUT_ENABLED) {
-            this.inspectObjectActor(helperResult.object);
-          }
+          this.inspectObjectActor(helperResult.object);
           break;
         case "error":
           try {
             errorMessage = l10n.getStr(helperResult.message);
           } catch (ex) {
             errorMessage = helperResult.message;
           }
           break;
@@ -358,24 +358,19 @@ JSTerm.prototype = {
           break;
         case "copyValueToClipboard":
           clipboardHelper.copyString(helperResult.value);
           break;
       }
     }
 
     // Hide undefined results coming from JSTerm helper functions.
-    if (!errorMessage
-        && result
-        && typeof result == "object"
-        && result.type == "undefined"
-        && helperResult
-        && !helperHasRawOutput
-        && !(this.hud.NEW_CONSOLE_OUTPUT_ENABLED && helperResult.type === "inspectObject")
-    ) {
+    if (!errorMessage && result && typeof result == "object" &&
+      result.type == "undefined" &&
+      helperResult && !helperHasRawOutput) {
       callback && callback();
       return;
     }
 
     if (this.hud.NEW_CONSOLE_OUTPUT_ENABLED) {
       this.hud.newConsoleOutput.dispatchMessageAdd(response, true).then(callback);
       return;
     }
@@ -404,16 +399,26 @@ JSTerm.prototype = {
     }
 
     if (WebConsoleUtils.isActorGrip(result)) {
       msg._objectActors.add(result.actor);
     }
   },
 
   inspectObjectActor: function (objectActor) {
+    if (this.hud.NEW_CONSOLE_OUTPUT_ENABLED) {
+      this.hud.newConsoleOutput.dispatchMessageAdd({
+        helperResult: {
+          type: "inspectObject",
+          object: objectActor
+        }
+      }, true);
+      return this.hud.newConsoleOutput;
+    }
+
     return this.openVariablesView({
       objectActor,
       label: VariablesView.getString(objectActor, {concise: true}),
     });
   },
 
   /**
    * Execute a string. Execution happens asynchronously in the content process.
@@ -584,16 +589,21 @@ JSTerm.prototype = {
    *        option is not used, then the variables view opens in the sidebar.
    *        - autofocus: optional boolean, |true| if you want to give focus to
    *        the variables view window after open, |false| otherwise.
    * @return object
    *         A promise object that is resolved when the variables view has
    *         opened. The new variables view instance is given to the callbacks.
    */
   openVariablesView: function (options) {
+    // Bail out if the side bar doesn't exist.
+    if (!this.hud.document.querySelector("#webconsole-sidebar")) {
+      return Promise.resolve(null);
+    }
+
     let onContainerReady = (window) => {
       let container = window.document.querySelector("#variables");
       let view = this._variablesView;
       if (!view || options.targetElement) {
         let viewOptions = {
           container: container,
           hideFilterInput: options.hideFilterInput,
         };
@@ -942,41 +952,39 @@ JSTerm.prototype = {
    * This method emits the "messages-cleared" notification.
    *
    * @param boolean clearStorage
    *        True if you want to clear the console messages storage associated to
    *        this Web Console.
    */
   clearOutput: function (clearStorage) {
     let hud = this.hud;
-    let outputNode = hud.outputNode;
-    let node;
-    while ((node = outputNode.firstChild)) {
-      hud.removeOutputMessage(node);
-    }
 
-    hud.groupDepth = 0;
-    hud._outputQueue.forEach(hud._destroyItem, hud);
-    hud._outputQueue = [];
+    if (hud.NEW_CONSOLE_OUTPUT_ENABLED) {
+      hud.newConsoleOutput.dispatchMessagesClear();
+    } else {
+      let outputNode = hud.outputNode;
+      let node;
+      while ((node = outputNode.firstChild)) {
+        hud.removeOutputMessage(node);
+      }
+
+      hud.groupDepth = 0;
+      hud._outputQueue.forEach(hud._destroyItem, hud);
+      hud._outputQueue = [];
+      hud._repeatNodes = {};
+    }
     this.webConsoleClient.clearNetworkRequests();
-    hud._repeatNodes = {};
-
     if (clearStorage) {
       this.webConsoleClient.clearMessagesCache();
     }
-
     this._sidebarDestroy();
-
-    if (hud.NEW_CONSOLE_OUTPUT_ENABLED) {
-      hud.newConsoleOutput.dispatchMessagesClear();
-    }
-
+    this.focus();
     this.emit("messages-cleared");
   },
-
   /**
    * Remove all of the private messages from the Web Console output.
    *
    * This method emits the "private-messages-cleared" notification.
    */
   clearPrivateMessages: function () {
     let nodes = this.hud.outputNode.querySelectorAll(".message[private]");
     for (let node of nodes) {
@@ -1586,28 +1594,26 @@ JSTerm.prototype = {
     let popup = this.autocompletePopup;
     popup.setItems(items);
 
     let completionType = this.lastCompletion.completionType;
     this.lastCompletion = {
       value: inputValue,
       matchProp: lastPart,
     };
-
     if (items.length > 1 && !popup.isOpen) {
       let str = this.getInputValue().substr(0, this.inputNode.selectionStart);
       let offset = str.length - (str.lastIndexOf("\n") + 1) - lastPart.length;
-      let x = offset * this.hud._inputCharWidth;
-      popup.openPopup(inputNode, x + this.hud._chevronWidth);
+      let x = offset * this._inputCharWidth;
+      popup.openPopup(inputNode, x + this._chevronWidth);
       this._autocompletePopupNavigated = false;
     } else if (items.length < 2 && popup.isOpen) {
       popup.hidePopup();
       this._autocompletePopupNavigated = false;
     }
-
     if (items.length == 1) {
       popup.selectedIndex = 0;
     }
 
     this.onAutocompleteSelect();
 
     if (completionType != this.COMPLETE_HINT_ONLY && popup.itemCount == 1) {
       this.acceptProposedCompletion();
@@ -1691,16 +1697,41 @@ JSTerm.prototype = {
    * @param string suffix
    *        The proposed suffix for the inputNode value.
    */
   updateCompleteNode: function (suffix) {
     // completion prefix = input, with non-control chars replaced by spaces
     let prefix = suffix ? this.getInputValue().replace(/[\S]/g, " ") : "";
     this.completeNode.value = prefix + suffix;
   },
+  /**
+   * Calculates the width and height of a single character of the input box.
+   * This will be used in opening the popup at the correct offset.
+   *
+   * @private
+   */
+  _updateCharSize: function () {
+    let doc = this.hud.document;
+    let tempLabel = doc.createElementNS(XHTML_NS, "span");
+    let style = tempLabel.style;
+    style.position = "fixed";
+    style.padding = "0";
+    style.margin = "0";
+    style.width = "auto";
+    style.color = "transparent";
+    WebConsoleUtils.copyTextStyles(this.inputNode, tempLabel);
+    tempLabel.textContent = "x";
+    doc.documentElement.appendChild(tempLabel);
+    this._inputCharWidth = tempLabel.offsetWidth;
+    tempLabel.remove();
+    // Calculate the width of the chevron placed at the beginning of the input
+    // box. Remove 4 more pixels to accomodate the padding of the popup.
+    this._chevronWidth = +doc.defaultView.getComputedStyle(this.inputNode)
+                             .paddingLeft.replace(/[^0-9.]/g, "") - 4;
+  },
 
   /**
    * Destroy the sidebar.
    * @private
    */
   _sidebarDestroy: function () {
     if (this._variablesView) {
       this._variablesView.controller.releaseActors();
--- a/devtools/client/webconsole/moz.build
+++ b/devtools/client/webconsole/moz.build
@@ -5,23 +5,22 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
 
 DIRS += [
     'net',
     'new-console-output',
 ]
-
 DevToolsModules(
     'console-commands.js',
     'console-output.js',
     'hudservice.js',
     'jsterm.js',
+    'new-webconsole.js',
     'panel.js',
     'utils.js',
     'webconsole-connection-proxy.js',
     'webconsole-l10n.js',
     'webconsole.js',
 )
-
 with Files('**'):
     BUG_COMPONENT = ('Firefox', 'Developer Tools: Console')
--- a/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
+++ b/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
@@ -29,22 +29,42 @@ function NewConsoleOutputWrapper(parentN
   this.toolbox = toolbox;
   this.owner = owner;
   this.document = document;
 
   this.init = this.init.bind(this);
 
   store = configureStore(this.jsterm.hud);
 }
-
 NewConsoleOutputWrapper.prototype = {
   init: function () {
     const attachRefToHud = (id, node) => {
       this.jsterm.hud[id] = node;
     };
+    // Focus the input line whenever the output area is clicked.
+    this.parentNode.addEventListener("click", (event) => {
+      // Do not focus on middle/right-click or 2+ clicks.
+      if (event.detail !== 1 || event.button !== 0) {
+        return;
+      }
+
+      // Do not focus if something is selected
+      let selection = this.document.defaultView.getSelection();
+      if (selection && !selection.isCollapsed) {
+        return;
+      }
+
+      // Do not focus if a link was clicked
+      if (event.target.nodeName.toLowerCase() === "a" ||
+          event.target.parentNode.nodeName.toLowerCase() === "a") {
+        return;
+      }
+
+      this.jsterm.focus();
+    });
 
     const serviceContainer = {
       attachRefToHud,
       emitNewMessage: (node, messageId) => {
         this.jsterm.hud.emit("new-messages", new Set([{
           node,
           messageId,
         }]));
@@ -135,24 +155,23 @@ NewConsoleOutputWrapper.prototype = {
     let provider = React.createElement(
       Provider,
       { store },
       React.DOM.div(
         {className: "webconsole-output-wrapper"},
         filterBar,
         childComponent
     ));
+    this.body = ReactDOM.render(provider, this.parentNode);
 
-    this.body = ReactDOM.render(provider, this.parentNode);
+    this.jsterm.focus();
   },
-
   dispatchMessageAdd: function (message, waitForResponse) {
     let action = actions.messageAdd(message);
     batchedMessageAdd(action);
-
     // Wait for the message to render to resolve with the DOM node.
     // This is just for backwards compatibility with old tests, and should
     // be removed once it's not needed anymore.
     // Can only wait for response if the action contains a valid message.
     if (waitForResponse && action.message) {
       let messageId = action.message.id;
       return new Promise(resolve => {
         let jsterm = this.jsterm;
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser_jsterm_inspect.js
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_jsterm_inspect.js
@@ -16,17 +16,17 @@ add_task(async function () {
   let jsterm = hud.jsterm;
 
   info("Test `inspect(window)`");
   // Add a global value so we can check it later.
   await jsterm.execute("testProp = 'testValue'");
   await jsterm.execute("inspect(window)");
 
   const inspectWindowNode = await waitFor(() =>
-    findInspectResultMessage(hud.ui.experimentalOutputNode, 1));
+    findInspectResultMessage(hud.ui.outputNode, 1));
 
   let objectInspectors = [...inspectWindowNode.querySelectorAll(".tree")];
   is(objectInspectors.length, 1, "There is the expected number of object inspectors");
 
   const [windowOi] = objectInspectors;
   let windowOiNodes = windowOi.querySelectorAll(".node");
 
   // The tree can be collapsed since the properties are fetched asynchronously.
@@ -48,15 +48,15 @@ add_task(async function () {
   is(testPropertyValueNode.textContent, '"testValue"',
     "The testProp property value is displayed as expected");
 
   /* Check that a primitive value can be inspected, too */
   info("Test `inspect(1)`");
   await jsterm.execute("inspect(1)");
 
   const inspectPrimitiveNode = await waitFor(() =>
-    findInspectResultMessage(hud.ui.experimentalOutputNode, 2));
+    findInspectResultMessage(hud.ui.outputNode, 2));
   is(inspectPrimitiveNode.textContent, 1, "The primitive is displayed as expected");
 });
 
 function findInspectResultMessage(node, index) {
   return node.querySelectorAll(".message.result")[index];
 }
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_console_dir.js
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_console_dir.js
@@ -25,25 +25,22 @@ add_task(async function () {
   });
 
   info("console.dir on an array");
   await ContentTask.spawn(gBrowser.selectedBrowser, null, function () {
     content.wrappedJSObject.console.dir(
       [1, 2, {a: "a", b: "b"}],
     );
   });
-
   let dirMessageNode = await waitFor(() =>
-    findConsoleDir(hud.ui.experimentalOutputNode, 0));
+    findConsoleDir(hud.ui.outputNode, 0));
   let objectInspectors = [...dirMessageNode.querySelectorAll(".tree")];
   is(objectInspectors.length, 1, "There is the expected number of object inspectors");
-
   const [arrayOi] = objectInspectors;
   let arrayOiNodes = arrayOi.querySelectorAll(".node");
-
   // The tree can be collapsed since the properties are fetched asynchronously.
   if (arrayOiNodes.length === 1) {
     // If this is the case, we wait for the properties to be fetched and displayed.
     await waitForNodeMutation(arrayOi, {
       childList: true
     });
     arrayOiNodes = arrayOi.querySelectorAll(".node");
   }
@@ -55,28 +52,24 @@ add_task(async function () {
   const arrayPropertiesNames = ["0", "1", "2", "length", "__proto__"];
   is(JSON.stringify(propertiesNodes), JSON.stringify(arrayPropertiesNames));
 
   info("console.dir on a long object");
   const obj = Array.from({length: 100}).reduce((res, _, i) => {
     res["item-" + (i + 1).toString().padStart(3, "0")] = i + 1;
     return res;
   }, {});
-
   await ContentTask.spawn(gBrowser.selectedBrowser, obj, function (data) {
     content.wrappedJSObject.console.dir(data);
   });
-
-  dirMessageNode = await waitFor(() => findConsoleDir(hud.ui.experimentalOutputNode, 1));
+  dirMessageNode = await waitFor(() => findConsoleDir(hud.ui.outputNode, 1));
   objectInspectors = [...dirMessageNode.querySelectorAll(".tree")];
   is(objectInspectors.length, 1, "There is the expected number of object inspectors");
-
   const [objectOi] = objectInspectors;
   let objectOiNodes = objectOi.querySelectorAll(".node");
-
   // The tree can be collapsed since the properties are fetched asynchronously.
   if (objectOiNodes.length === 1) {
     // If this is the case, we wait for the properties to be fetched and displayed.
     await waitForNodeMutation(objectOi, {
       childList: true
     });
     objectOiNodes = objectOi.querySelectorAll(".node");
   }
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_console_group.js
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_console_group.js
@@ -65,27 +65,23 @@ add_task(function* () {
   node = yield waitFor(() => findMessage(hud, "log-4"));
   testClass(node, "log");
   testIndent(node, 0);
 
   info("Test a collapsed group at root level");
   node = yield waitFor(() => findMessage(hud, "group-3"));
   testClass(node, "startGroupCollapsed");
   testIndent(node, 0);
-
   info("Test a message at root level, after closing a collapsed group");
   node = yield waitFor(() => findMessage(hud, "log-6"));
   testClass(node, "log");
   testIndent(node, 0);
-
-  let nodes = hud.ui.experimentalOutputNode.querySelectorAll(".message");
+  let nodes = hud.ui.outputNode.querySelectorAll(".message");
   is(nodes.length, 8, "expected number of messages are displayed");
 });
-
 function testClass(node, className) {
   ok(node.classList.contains(className), `message has the expected "${className}" class`);
 }
-
 function testIndent(node, indent) {
   indent = `${indent * INDENT_WIDTH}px`;
   is(node.querySelector(".indent").style.width, indent,
     "message has the expected level of indentation");
 }
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_console_table.js
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_console_table.js
@@ -117,34 +117,29 @@ add_task(function* () {
     }
   }];
 
   yield ContentTask.spawn(gBrowser.selectedBrowser, testCases, function (tests) {
     tests.forEach((test) => {
       content.wrappedJSObject.doConsoleTable(test.input, test.headers);
     });
   });
-
   let nodes = [];
   for (let testCase of testCases) {
     let node = yield waitFor(
-      () => findConsoleTable(hud.ui.experimentalOutputNode, testCases.indexOf(testCase))
+      () => findConsoleTable(hud.ui.outputNode, testCases.indexOf(testCase))
     );
     nodes.push(node);
   }
-
-  let consoleTableNodes = hud.ui.experimentalOutputNode.querySelectorAll(
+  let consoleTableNodes = hud.ui.outputNode.querySelectorAll(
     ".message .new-consoletable");
-
   is(consoleTableNodes.length, testCases.length,
     "console has the expected number of consoleTable items");
-
   testCases.forEach((testCase, index) => testItem(testCase, nodes[index]));
 });
-
 function testItem(testCase, node) {
   info(testCase.info);
 
   let columns = Array.from(node.querySelectorAll("thead th"));
   let rows = Array.from(node.querySelectorAll("tbody tr"));
 
   is(
     JSON.stringify(testCase.expected.columns),
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_filters.js
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_filters.js
@@ -1,30 +1,25 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests filters.
 
 "use strict";
-
 const { MESSAGE_LEVEL } = require("devtools/client/webconsole/new-console-output/constants");
-
 const TEST_URI = "http://example.com/browser/devtools/client/webconsole/new-console-output/test/mochitest/test-console-filters.html";
-
 add_task(function* () {
   let hud = yield openNewTabAndConsole(TEST_URI);
-  const outputNode = hud.ui.experimentalOutputNode;
-
+  const outputNode = hud.ui.outputNode;
   const toolbar = yield waitFor(() => {
     return outputNode.querySelector(".webconsole-filterbar-primary");
   });
   ok(toolbar, "Toolbar found");
-
   // Show the filter bar
   toolbar.querySelector(".devtools-filter-icon").click();
   const filterBar = yield waitFor(() => {
     return outputNode.querySelector(".webconsole-filterbar-secondary");
   });
   ok(filterBar, "Filter bar is shown when filter icon is clicked.");
 
   // Check defaults.
@@ -46,27 +41,24 @@ add_task(function* () {
   filterBar.querySelector(".error").click();
   yield waitFor(() => findMessages(hud, "").length == 4);
   ok(true, "When a filter is turned off, its messages are not shown.");
 
   // Check that the ui settings were persisted.
   yield closeTabAndToolbox();
   yield testFilterPersistence();
 });
-
 function filterIsEnabled(button) {
   return button.classList.contains("checked");
 }
-
 function* testFilterPersistence() {
   let hud = yield openNewTabAndConsole(TEST_URI);
-  const outputNode = hud.ui.experimentalOutputNode;
+  const outputNode = hud.ui.outputNode;
   const filterBar = yield waitFor(() => {
     return outputNode.querySelector(".webconsole-filterbar-secondary");
   });
   ok(filterBar, "Filter bar ui setting is persisted.");
-
   // Check that the filter settings were persisted.
   ok(!filterIsEnabled(filterBar.querySelector(".error")),
     "Filter button setting is persisted");
   ok(findMessages(hud, "").length == 4,
     "Messages of all levels shown when filters are on.");
 }
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_filters_persist.js
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_filters_persist.js
@@ -35,29 +35,25 @@ add_task(function* () {
   yield closeTabAndToolbox();
   hud = yield openNewTabAndConsole(TEST_URI);
 
   info("Check that all filters are enabled");
   filterButtons = yield getFilterButtons(hud);
   filterButtons.forEach(filterButton => {
     ok(filterIsEnabled(filterButton), "filter is enabled");
   });
-
   // Check that the ui settings were persisted.
   yield closeTabAndToolbox();
 });
-
 function* getFilterButtons(hud) {
-  const outputNode = hud.ui.experimentalOutputNode;
-
+  const outputNode = hud.ui.outputNode;
   info("Wait for console toolbar to appear");
   const toolbar = yield waitFor(() => {
     return outputNode.querySelector(".webconsole-filterbar-primary");
   });
-
   // Show the filter bar if it is hidden
   if (!outputNode.querySelector(".webconsole-filterbar-secondary")) {
     toolbar.querySelector(".devtools-filter-icon").click();
   }
 
   info("Wait for console filterbar to appear");
   const filterBar = yield waitFor(() => {
     return outputNode.querySelector(".webconsole-filterbar-secondary");
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_input_focus.js
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_input_focus.js
@@ -20,31 +20,26 @@ add_task(function* () {
   let inputNode = hud.jsterm.inputNode;
   ok(inputNode.getAttribute("focused"), "input node is focused after output is cleared");
 
   ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
     content.wrappedJSObject.console.log("console message 2");
   });
   let msg = yield waitFor(() => findMessage(hud, "console message 2"));
   let outputItem = msg.querySelector(".message-body");
-
   inputNode = hud.jsterm.inputNode;
   ok(inputNode.getAttribute("focused"), "input node is focused, first");
-
   yield waitForBlurredInput(inputNode);
-
   EventUtils.sendMouseEvent({type: "click"}, hud.outputNode);
+
   ok(inputNode.getAttribute("focused"), "input node is focused, second time");
-
   yield waitForBlurredInput(inputNode);
-
   info("Setting a text selection and making sure a click does not re-focus");
   let selection = hud.iframeWindow.getSelection();
   selection.selectAllChildren(outputItem);
-
   EventUtils.sendMouseEvent({type: "click"}, hud.outputNode);
   ok(!inputNode.getAttribute("focused"),
     "input node focused after text is selected");
 });
 
 function waitForBlurredInput(inputNode) {
   return new Promise(resolve => {
     let lostFocus = () => {
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_keyboard_accessibility.js
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_keyboard_accessibility.js
@@ -14,31 +14,25 @@ const TEST_URI =
       console.log("console message " + i);
     }
   </script>
   `;
 
 add_task(function* () {
   let hud = yield openNewTabAndConsole(TEST_URI);
   info("Web Console opened");
-
   const outputScroller = hud.ui.outputScroller;
-
   yield waitFor(() => findMessages(hud, "").length == 100);
-
   let currentPosition = outputScroller.scrollTop;
   const bottom = currentPosition;
-
-  EventUtils.sendMouseEvent({type: "click"}, hud.jsterm.inputNode);
-
+  hud.jsterm.inputNode.focus();
   // Page up.
   EventUtils.synthesizeKey("VK_PAGE_UP", {});
   isnot(outputScroller.scrollTop, currentPosition,
     "scroll position changed after page up");
-
   // Page down.
   currentPosition = outputScroller.scrollTop;
   EventUtils.synthesizeKey("VK_PAGE_DOWN", {});
   ok(outputScroller.scrollTop > currentPosition,
      "scroll position now at bottom");
 
   // Home
   EventUtils.synthesizeKey("VK_HOME", {});
--- a/devtools/client/webconsole/new-console-output/test/mochitest/head.js
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/head.js
@@ -131,24 +131,23 @@ function findMessage(hud, text, selector
  * @param object hud
  *        The web console.
  * @param string text
  *        A substring that can be found in the message.
  * @param selector [optional]
  *        The selector to use in finding the message.
  */
 function findMessages(hud, text, selector = ".message") {
-  const messages = hud.ui.experimentalOutputNode.querySelectorAll(selector);
+  const messages = hud.ui.outputNode.querySelectorAll(selector);
   const elements = Array.prototype.filter.call(
     messages,
     (el) => el.textContent.includes(text)
   );
   return elements;
 }
-
 /**
  * Simulate a context menu event on the provided element, and wait for the console context
  * menu to open. Returns a promise that resolves the menu popup element.
  *
  * @param object hud
  *        The web console.
  * @param element element
  *        The dom element on which the context menu event should be synthesized.
--- a/devtools/client/webconsole/new-webconsole.js
+++ b/devtools/client/webconsole/new-webconsole.js
@@ -9,18 +9,23 @@
 const {Utils: WebConsoleUtils} = require("devtools/client/webconsole/utils");
 const EventEmitter = require("devtools/shared/event-emitter");
 const promise = require("promise");
 const defer = require("devtools/shared/defer");
 const Services = require("Services");
 const { gDevTools } = require("devtools/client/framework/devtools");
 const { JSTerm } = require("devtools/client/webconsole/jsterm");
 const { WebConsoleConnectionProxy } = require("devtools/client/webconsole/webconsole-connection-proxy");
+const KeyShortcuts = require("devtools/client/shared/key-shortcuts");
+const { l10n } = require("devtools/client/webconsole/new-console-output/utils/messages");
+const system = require("devtools/shared/system");
+const { ZoomKeys } = require("devtools/client/shared/zoom-keys");
 
 const PREF_MESSAGE_TIMESTAMP = "devtools.webconsole.timestampMessages";
+const PREF_PERSISTLOG = "devtools.webconsole.persistlog";
 
 // XXX: This file is incomplete (see bug 1326937).
 // It's used when loading the webconsole with devtools-launchpad, but will ultimately be
 // the entry point for the new frontend
 
 /**
  * A WebConsoleFrame instance is an interactive console initialized *per target*
  * that displays console log data as well as provides an interactive terminal to
@@ -49,16 +54,29 @@ NewWebConsoleFrame.prototype = {
    * Getter for the debugger WebConsoleClient.
    * @type object
    */
   get webConsoleClient() {
     return this.proxy ? this.proxy.webConsoleClient : null;
   },
 
   /**
+   * Getter for the persistent logging preference.
+   * @type boolean
+   */
+  get persistLog() {
+    // For the browser console, we receive tab navigation
+    // when the original top level window we attached to is closed,
+    // but we don't want to reset console history and just switch to
+    // the next available window.
+    return this.isBrowserConsole ||
+           Services.prefs.getBoolPref(PREF_PERSISTLOG);
+  },
+
+  /**
    * Initialize the WebConsoleFrame instance.
    * @return object
    *         A promise object that resolves once the frame is ready to use.
    */
   init() {
     this._initUI();
     let connectionInited = this._initConnection();
 
@@ -77,26 +95,36 @@ NewWebConsoleFrame.prototype = {
         Services.obs.notifyObservers(id, "web-console-created");
       }
     };
     allReady.then(notifyObservers, notifyObservers)
             .then(this.newConsoleOutput.init);
 
     return allReady;
   },
-
   destroy() {
     if (this._destroyer) {
       return this._destroyer.promise;
     }
-
     this._destroyer = defer();
+    Services.prefs.removeObserver(PREF_MESSAGE_TIMESTAMP, this._onToolboxPrefChanged);
+    this.React = this.ReactDOM = this.FrameView = null;
+    if (this.jsterm) {
+      this.jsterm.off("sidebar-opened", this.resize);
+      this.jsterm.off("sidebar-closed", this.resize);
+      this.jsterm.destroy();
+      this.jsterm = null;
+    }
 
-    Services.prefs.addObserver(PREF_MESSAGE_TIMESTAMP, this._onToolboxPrefChanged);
-    this.React = this.ReactDOM = this.FrameView = null;
+    let toolbox = gDevTools.getToolbox(this.owner.target);
+    if (toolbox) {
+      toolbox.off("webconsole-selected", this._onPanelSelected);
+    }
+
+    this.window = this.owner = this.newConsoleOutput = null;
 
     let onDestroy = () => {
       this._destroyer.resolve(null);
     };
     if (this.proxy) {
       this.proxy.disconnect().then(onDestroy);
       this.proxy = null;
     } else {
@@ -194,22 +222,50 @@ NewWebConsoleFrame.prototype = {
     this.window.jsterm = this.jsterm;
     // @TODO Once the toolbox has been converted to React, see if passing
     // in JSTerm is still necessary.
 
     // Handle both launchpad and toolbox loading
     let Wrapper = this.owner.NewConsoleOutputWrapper || this.window.NewConsoleOutput;
     this.newConsoleOutput = new Wrapper(
       this.outputNode, this.jsterm, toolbox, this.owner, this.document);
-
     // Toggle the timestamp on preference change
     Services.prefs.addObserver(PREF_MESSAGE_TIMESTAMP, this._onToolboxPrefChanged);
     this._onToolboxPrefChanged();
+
+    this._initShortcuts();
   },
 
+  _initShortcuts: function () {
+    let shortcuts = new KeyShortcuts({
+      window: this.window
+    });
+
+    shortcuts.on(l10n.getStr("webconsole.find.key"),
+                 (name, event) => {
+                   this.filterBox.focus();
+                   event.preventDefault();
+                 });
+
+    let clearShortcut;
+    if (system.constants.platform === "macosx") {
+      clearShortcut = l10n.getStr("webconsole.clear.keyOSX");
+    } else {
+      clearShortcut = l10n.getStr("webconsole.clear.key");
+    }
+
+    shortcuts.on(clearShortcut, () => this.jsterm.clearOutput(true));
+
+    if (this.isBrowserConsole) {
+      shortcuts.on(l10n.getStr("webconsole.close.key"),
+                   this.window.close.bind(this.window));
+
+      ZoomKeys.register(this.window);
+    }
+  },
   /**
    * Handler for page location changes.
    *
    * @param string uri
    *        New page location.
    * @param string title
    *        New page title.
    */
--- a/devtools/client/webconsole/test/browser_console_open_or_focus.js
+++ b/devtools/client/webconsole/test/browser_console_open_or_focus.js
@@ -21,26 +21,20 @@ add_task(function* () {
 
   console.log("testmessage");
   yield waitForMessages({
     webconsole: hud,
     messages: [{
       text: "testmessage"
     }],
   });
-
   currWindow = Services.wm.getMostRecentWindow(null);
-  is(currWindow.document.documentURI, Tools.webConsole.url,
+  is(currWindow.document.documentURI, Tools.webConsole.oldWebConsoleURL,
      "The Browser Console is open and has focus");
-
   mainWindow.focus();
-
   yield HUDService.openBrowserConsoleOrFocus();
-
   currWindow = Services.wm.getMostRecentWindow(null);
-  is(currWindow.document.documentURI, Tools.webConsole.url,
+  is(currWindow.document.documentURI, Tools.webConsole.oldWebConsoleURL,
      "The Browser Console is open and has focus");
-
   yield HUDService.toggleBrowserConsole();
-
   hud = HUDService.getBrowserConsole();
   ok(!hud, "Browser Console has been closed");
 });
--- a/devtools/client/webconsole/test/browser_webconsole_bug_601667_filter_buttons.js
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_601667_filter_buttons.js
@@ -253,15 +253,15 @@ function clickButton(node) {
 
 function altClickButton(node) {
   EventUtils.sendMouseEvent({ type: "click", altKey: true }, node);
 }
 
 function chooseMenuItem(node) {
   let event = document.createEvent("XULCommandEvent");
   event.initCommandEvent("command", true, true, window, 0, false, false, false,
-                         false, null);
+                         false, null, 0);
   node.dispatchEvent(event);
 }
 
 function isChecked(node) {
   return node.getAttribute("checked") === "true";
 }
--- a/devtools/client/webconsole/webconsole-connection-proxy.js
+++ b/devtools/client/webconsole/webconsole-connection-proxy.js
@@ -282,145 +282,156 @@ WebConsoleConnectionProxy.prototype = {
    *
    * @private
    * @param string type
    *        Message type.
    * @param object packet
    *        The message received from the server.
    */
   _onPageError: function (type, packet) {
-    if (this.webConsoleFrame && packet.from == this._consoleActor) {
-      if (this.webConsoleFrame.NEW_CONSOLE_OUTPUT_ENABLED) {
-        this.dispatchMessageAdd(packet);
-        return;
-      }
+    if (!this.webConsoleFrame || packet.from != this._consoleActor) {
+      return;
+    }
+    if (this.webConsoleFrame.NEW_CONSOLE_OUTPUT_ENABLED) {
+      this.dispatchMessageAdd(packet);
+    } else {
       this.webConsoleFrame.handlePageError(packet.pageError);
     }
   },
-
   /**
    * The "logMessage" message type handler. We redirect any message to the UI
    * for displaying.
    *
    * @private
    * @param string type
    *        Message type.
    * @param object packet
    *        The message received from the server.
    */
   _onLogMessage: function (type, packet) {
     if (!this.webConsoleFrame || packet.from != this._consoleActor) {
       return;
     }
-
     if (this.webConsoleFrame.NEW_CONSOLE_OUTPUT_ENABLED) {
       this.dispatchMessageAdd(packet);
     } else {
       this.webConsoleFrame.handleLogMessage(packet);
     }
   },
-
   /**
    * The "consoleAPICall" message type handler. We redirect any message to
    * the UI for displaying.
    *
    * @private
    * @param string type
    *        Message type.
    * @param object packet
    *        The message received from the server.
    */
   _onConsoleAPICall: function (type, packet) {
-    if (this.webConsoleFrame && packet.from == this._consoleActor) {
-      if (this.webConsoleFrame.NEW_CONSOLE_OUTPUT_ENABLED) {
-        this.dispatchMessageAdd(packet);
-      } else {
-        this.webConsoleFrame.handleConsoleAPICall(packet.message);
-      }
+    if (!this.webConsoleFrame || packet.from != this._consoleActor) {
+      return;
+    }
+    if (this.webConsoleFrame.NEW_CONSOLE_OUTPUT_ENABLED) {
+      this.dispatchMessageAdd(packet);
+    } else {
+      this.webConsoleFrame.handleConsoleAPICall(packet.message);
     }
   },
-
   /**
    * The "networkEvent" message type handler. We redirect any message to
    * the UI for displaying.
    *
    * @private
    * @param string type
    *        Message type.
    * @param object networkInfo
    *        The network request information.
    */
   _onNetworkEvent: function (type, networkInfo) {
-    if (this.webConsoleFrame) {
-      if (this.webConsoleFrame.NEW_CONSOLE_OUTPUT_ENABLED) {
-        this.dispatchMessageAdd(networkInfo);
-      } else {
-        this.webConsoleFrame.handleNetworkEvent(networkInfo);
-      }
+    if (!this.webConsoleFrame) {
+      return;
+    }
+    if (this.webConsoleFrame.NEW_CONSOLE_OUTPUT_ENABLED) {
+      this.dispatchMessageAdd(networkInfo);
+    } else {
+      this.webConsoleFrame.handleNetworkEvent(networkInfo);
     }
   },
-
   /**
    * The "networkEventUpdate" message type handler. We redirect any message to
    * the UI for displaying.
    *
    * @private
    * @param string type
    *        Message type.
    * @param object response
    *        The update response received from the server.
    */
   _onNetworkEventUpdate: function (type, response) {
+    if (!this.webConsoleFrame) {
+      return;
+    }
     let { packet, networkInfo } = response;
-    if (this.webConsoleFrame) {
-      if (this.webConsoleFrame.NEW_CONSOLE_OUTPUT_ENABLED) {
-        this.dispatchMessageUpdate(networkInfo, response);
-      }
+    if (this.webConsoleFrame.NEW_CONSOLE_OUTPUT_ENABLED) {
+      this.dispatchMessageUpdate(networkInfo, response);
+    } else {
       this.webConsoleFrame.handleNetworkEventUpdate(networkInfo, packet);
     }
   },
-
   /**
    * The "fileActivity" message type handler. We redirect any message to
    * the UI for displaying.
    *
    * @private
    * @param string type
    *        Message type.
    * @param object packet
    *        The message received from the server.
    */
   _onFileActivity: function (type, packet) {
-    if (this.webConsoleFrame && packet.from == this._consoleActor) {
+    if (!this.webConsoleFrame || packet.from != this._consoleActor) {
+      return;
+    }
+    if (this.webConsoleFrame.NEW_CONSOLE_OUTPUT_ENABLED) {
+      // TODO: Implement for new console
+    } else {
       this.webConsoleFrame.handleFileActivity(packet.uri);
     }
   },
-
   _onReflowActivity: function (type, packet) {
-    if (this.webConsoleFrame && packet.from == this._consoleActor) {
+    if (!this.webConsoleFrame || packet.from != this._consoleActor) {
+      return;
+    }
+    if (this.webConsoleFrame.NEW_CONSOLE_OUTPUT_ENABLED) {
+      // TODO: Implement for new console
+    } else {
       this.webConsoleFrame.handleReflowActivity(packet);
     }
   },
-
   /**
    * The "serverLogCall" message type handler. We redirect any message to
    * the UI for displaying.
    *
    * @private
    * @param string type
    *        Message type.
    * @param object packet
    *        The message received from the server.
    */
   _onServerLogCall: function (type, packet) {
-    if (this.webConsoleFrame && packet.from == this._consoleActor) {
+    if (!this.webConsoleFrame || packet.from != this._consoleActor) {
+      return;
+    }
+    if (this.webConsoleFrame.NEW_CONSOLE_OUTPUT_ENABLED) {
+      // TODO: Implement for new console
+    } else {
       this.webConsoleFrame.handleConsoleAPICall(packet.message);
     }
   },
-
   /**
    * The "lastPrivateContextExited" message type handler. When this message is
    * received the Web Console UI is cleared.
    *
    * @private
    * @param string type
    *        Message type.
    * @param object packet
--- a/devtools/client/webconsole/webconsole.js
+++ b/devtools/client/webconsole/webconsole.js
@@ -485,24 +485,18 @@ WebConsoleFrame.prototype = {
     // This notification is only used in tests. Don't chain it onto
     // the returned promise because the console panel needs to be attached
     // to the toolbox before the web-console-created event is receieved.
     let notifyObservers = () => {
       let id = WebConsoleUtils.supportsString(this.hudId);
       Services.obs.notifyObservers(id, "web-console-created");
     };
     allReady.then(notifyObservers, notifyObservers);
-
-    if (this.NEW_CONSOLE_OUTPUT_ENABLED) {
-      allReady.then(this.newConsoleOutput.init);
-    }
-
     return allReady;
   },
-
   /**
    * Connect to the server using the remote debugging protocol.
    *
    * @private
    * @return object
    *         A promise object that is resolved/reject based on the connection
    *         result.
    */
@@ -522,92 +516,54 @@ WebConsoleFrame.prototype = {
       let node = this.createMessageNode(CATEGORY_JS, SEVERITY_ERROR,
                                         reason.error + ": " + reason.message);
       this.outputMessage(CATEGORY_JS, node, [reason]);
       this._initDefer.reject(reason);
     });
 
     return this._initDefer.promise;
   },
-
   /**
    * Find the Web Console UI elements and setup event listeners as needed.
    * @private
    */
   _initUI: function () {
     this.document = this.window.document;
     this.rootElement = this.document.documentElement;
-    this.NEW_CONSOLE_OUTPUT_ENABLED = !this.isBrowserConsole
-      && !this.owner.target.chrome
-      && Services.prefs.getBoolPref(PREF_NEW_FRONTEND_ENABLED);
-
     this.outputNode = this.document.getElementById("output-container");
     this.outputWrapper = this.document.getElementById("output-wrapper");
     this.completeNode = this.document.querySelector(".jsterm-complete-node");
     this.inputNode = this.document.querySelector(".jsterm-input-node");
-
     // In the old frontend, the area that scrolls is outputWrapper, but in the new
     // frontend this will be reassigned.
     this.outputScroller = this.outputWrapper;
-
-    // Update the character width and height needed for the popup offset
-    // calculations.
-    this._updateCharSize();
-
     this.jsterm = new JSTerm(this);
     this.jsterm.init();
-
     let toolbox = gDevTools.getToolbox(this.owner.target);
-
-    if (this.NEW_CONSOLE_OUTPUT_ENABLED) {
-      // @TODO Remove this once JSTerm is handled with React/Redux.
-      this.window.jsterm = this.jsterm;
-
-      // Remove context menu for now (see Bug 1307239).
-      this.outputWrapper.removeAttribute("context");
-
-      // XXX: We should actually stop output from happening on old output
-      // panel, but for now let's just hide it.
-      this.experimentalOutputNode = this.outputNode.cloneNode();
-      this.experimentalOutputNode.removeAttribute("tabindex");
-      this.outputNode.hidden = true;
-      this.outputNode.parentNode.appendChild(this.experimentalOutputNode);
-      // @TODO Once the toolbox has been converted to React, see if passing
-      // in JSTerm is still necessary.
-
-      this.newConsoleOutput = new this.window.NewConsoleOutput(
-        this.experimentalOutputNode, this.jsterm, toolbox, this.owner, this.document);
-
-      let filterToolbar = this.document.querySelector(".hud-console-filter-toolbar");
-      filterToolbar.hidden = true;
-    } else {
-      // Register the controller to handle "select all" properly.
-      this._commandController = new CommandController(this);
-      this.window.controllers.insertControllerAt(0, this._commandController);
-
-      this._contextMenuHandler = new ConsoleContextMenu(this);
-
-      this._initDefaultFilterPrefs();
-      this.filterBox = this.document.querySelector(".hud-filter-box");
-      this._setFilterTextBoxEvents();
-      this._initFilterButtons();
-      let clearButton =
-        this.document.getElementsByClassName("webconsole-clear-console-button")[0];
-      clearButton.addEventListener("command", () => {
-        this.owner._onClearButton();
-        this.jsterm.clearOutput(true);
-      });
-
-    }
-
+    // Register the controller to handle "select all" properly.
+    this._commandController = new CommandController(this);
+    this.window.controllers.insertControllerAt(0, this._commandController);
+
+    this._contextMenuHandler = new ConsoleContextMenu(this);
+
+    this._initDefaultFilterPrefs();
+    this.filterBox = this.document.querySelector(".hud-filter-box");
+    this._setFilterTextBoxEvents();
+    this._initFilterButtons();
+
+    let clearButton =
+      this.document.getElementsByClassName("webconsole-clear-console-button")[0];
+    clearButton.addEventListener("command", () => {
+      this.owner._onClearButton();
+      this.jsterm.clearOutput(true);
+    });
     this.resize();
     this.window.addEventListener("resize", this.resize, true);
     this.jsterm.on("sidebar-opened", this.resize);
     this.jsterm.on("sidebar-closed", this.resize);
-
     if (toolbox) {
       toolbox.on("webconsole-selected", this._onPanelSelected);
     }
 
     /*
      * Focus the input line whenever the output area is clicked.
      */
     this.outputWrapper.addEventListener("click", (event) => {
@@ -616,58 +572,40 @@ WebConsoleFrame.prototype = {
         return;
       }
 
       // Do not focus if something is selected
       let selection = this.window.getSelection();
       if (selection && !selection.isCollapsed) {
         return;
       }
-
       // Do not focus if a link was clicked
       if (event.target.nodeName.toLowerCase() === "a" ||
           event.target.parentNode.nodeName.toLowerCase() === "a") {
         return;
       }
-
-      // Do not focus if a search input was clicked on the new frontend
-      if (this.NEW_CONSOLE_OUTPUT_ENABLED &&
-          event.target.nodeName.toLowerCase() === "input" &&
-          event.target.getAttribute("type").toLowerCase() === "search") {
-        return;
-      }
-
       this.jsterm.focus();
     });
-
     // Toggle the timestamp on preference change
     this._prefObserver = new PrefObserver("");
     this._prefObserver.on(PREF_MESSAGE_TIMESTAMP, this._onToolboxPrefChanged);
     this._onToolboxPrefChanged();
-
     this._initShortcuts();
 
     // focus input node
     this.jsterm.focus();
   },
-
   /**
    * Resizes the output node to fit the output wrapped.
    * We need this because it makes the layout a lot faster than
    * using -moz-box-flex and 100% width.  See Bug 1237368.
    */
   resize: function () {
-    if (this.NEW_CONSOLE_OUTPUT_ENABLED) {
-      this.experimentalOutputNode.style.width =
-        this.outputWrapper.clientWidth + "px";
-    } else {
-      this.outputNode.style.width = this.outputWrapper.clientWidth + "px";
-    }
+    this.outputNode.style.width = this.outputWrapper.clientWidth + "px";
   },
-
   /**
    * Sets the focus to JavaScript input field when the web console tab is
    * selected or when there is a split console present.
    * @private
    */
   _onPanelSelected: function () {
     this.jsterm.focus();
   },
@@ -832,49 +770,21 @@ WebConsoleFrame.prototype = {
     if (Services.appinfo.OS == "Darwin") {
       let net = this.document.querySelector("toolbarbutton[category=net]");
       let accesskey = net.getAttribute("accesskeyMacOSX");
       net.setAttribute("accesskey", accesskey);
 
       let logging =
         this.document.querySelector("toolbarbutton[category=logging]");
       logging.removeAttribute("accesskey");
-
       let serverLogging =
         this.document.querySelector("toolbarbutton[category=server]");
       serverLogging.removeAttribute("accesskey");
     }
   },
-
-  /**
-   * Calculates the width and height of a single character of the input box.
-   * This will be used in opening the popup at the correct offset.
-   *
-   * @private
-   */
-  _updateCharSize: function () {
-    let doc = this.document;
-    let tempLabel = doc.createElementNS(XHTML_NS, "span");
-    let style = tempLabel.style;
-    style.position = "fixed";
-    style.padding = "0";
-    style.margin = "0";
-    style.width = "auto";
-    style.color = "transparent";
-    WebConsoleUtils.copyTextStyles(this.inputNode, tempLabel);
-    tempLabel.textContent = "x";
-    doc.documentElement.appendChild(tempLabel);
-    this._inputCharWidth = tempLabel.offsetWidth;
-    tempLabel.remove();
-    // Calculate the width of the chevron placed at the beginning of the input
-    // box. Remove 4 more pixels to accomodate the padding of the popup.
-    this._chevronWidth = +doc.defaultView.getComputedStyle(this.inputNode)
-                             .paddingLeft.replace(/[^0-9.]/g, "") - 4;
-  },
-
   /**
    * The event handler that is called whenever a user switches a filter on or
    * off.
    *
    * @private
    * @param nsIDOMEvent event
    *        The event that triggered the filter change.
    */
@@ -1969,29 +1879,22 @@ WebConsoleFrame.prototype = {
    * @param string event
    *        Event name.
    * @param object packet
    *        Notification packet received from the server.
    */
   handleTabNavigated: function (event, packet) {
     if (event == "will-navigate") {
       if (this.persistLog) {
-        if (this.NEW_CONSOLE_OUTPUT_ENABLED) {
-          // Add a _type to hit convertCachedPacket.
-          packet._type = true;
-          this.newConsoleOutput.dispatchMessageAdd(packet);
-        } else {
-          let marker = new Messages.NavigationMarker(packet, Date.now());
-          this.output.addMessage(marker);
-        }
+        let marker = new Messages.NavigationMarker(packet, Date.now());
+        this.output.addMessage(marker);
       } else {
         this.jsterm.clearOutput();
       }
     }
-
     if (packet.url) {
       this.onLocationChange(packet.url, packet.title);
     }
 
     if (event == "navigate" && !packet.nativeConsoleAPI) {
       this.logWarningAboutReplacedAPI();
     }
   },
@@ -2693,31 +2596,27 @@ WebConsoleFrame.prototype = {
         return;
       }
 
       this._startX = this._startY = undefined;
 
       callback.call(this, event);
     });
   },
-
   /**
    * Called when the message timestamp pref changes.
    */
   _onToolboxPrefChanged: function () {
     let newValue = Services.prefs.getBoolPref(PREF_MESSAGE_TIMESTAMP);
-    if (this.NEW_CONSOLE_OUTPUT_ENABLED) {
-      this.newConsoleOutput.dispatchTimestampsToggle(newValue);
-    } else if (newValue) {
+    if (newValue) {
       this.outputNode.classList.remove("hideTimestamps");
     } else {
       this.outputNode.classList.add("hideTimestamps");
     }
   },
-
   /**
    * Copies the selected items to the system clipboard.
    *
    * @param object options
    *        - linkOnly:
    *        An optional flag to copy only URL without other meta-information.
    *        Default is false.
    *        - contextmenu:
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/webconsole.xhtml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" dir="">
+<head>
+  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+    <link rel="stylesheet" href="chrome://devtools/skin/widgets.css"/>
+    <link rel="stylesheet" href="resource://devtools/client/themes/light-theme.css"/>
+    <link rel="stylesheet" href="chrome://devtools/skin/webconsole.css"/>
+    <link rel="stylesheet" href="resource://devtools/client/shared/components/reps/reps.css"/>
+    <script src="chrome://devtools/content/shared/theme-switching.js"></script>
+    <script type="application/javascript"
+            src="resource://devtools/client/webconsole/new-console-output/main.js"></script>
+  </head>
+  <body class="theme-sidebar" role="application">
+    <div id="app-wrapper" class="theme-body">
+      <div id="output-container" role="document" aria-live="polite"/>
+      <div id="jsterm-wrapper">
+        <xul:notificationbox id="webconsole-notificationbox">
+          <div class="jsterm-input-container" style="direction:ltr">
+            <xul:stack class="jsterm-stack-node" flex="1">
+              <xul:textbox class="jsterm-complete-node devtools-monospace"
+                       multiline="true" rows="1" tabindex="-1"/>
+              <xul:textbox class="jsterm-input-node devtools-monospace"
+                       multiline="true" rows="1" tabindex="0"
+                       aria-autocomplete="list"/>
+            </xul:stack>
+          </div>
+        </xul:notificationbox>
+      </div>
+    </div>
+  </body>
+</html>
--- a/devtools/client/webconsole/webconsole.xul
+++ b/devtools/client/webconsole/webconsole.xul
@@ -14,21 +14,19 @@
 <?xml-stylesheet href="chrome://devtools/skin/components-frame.css"
                  type="text/css"?>
 <?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
 <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         id="devtools-webconsole"
         macanimationtype="document"
         fullscreenbutton="true"
         title="&window.title;"
-        browserConsoleTitle="&browserConsole.title;"
         windowtype="devtools:webconsole"
         width="900" height="350"
         persist="screenX screenY width height sizemode">
-
   <script type="application/javascript"
           src="chrome://devtools/content/shared/theme-switching.js"/>
   <script type="application/javascript"
           src="resource://devtools/client/webconsole/new-console-output/main.js"/>
   <script type="text/javascript" src="chrome://global/content/globalOverlay.js"/>
   <script type="text/javascript" src="resource://devtools/client/webconsole/net/main.js"/>
   <script type="text/javascript"><![CDATA[
 function goUpdateConsoleCommands() {
--- a/devtools/server/actors/highlighters/box-model.js
+++ b/devtools/server/actors/highlighters/box-model.js
@@ -1,15 +1,14 @@
  /* 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/. */
 
 "use strict";
 
-const { extend } = require("sdk/core/heritage");
 const { AutoRefreshHighlighter } = require("./auto-refresh");
 const {
   CanvasFrameAnonymousContentHelper,
   createNode,
   createSVGNode,
   getBindingElementAndPseudo,
   hasPseudoClassLock,
   isNodeValid,
@@ -87,43 +86,41 @@ const PSEUDO_CLASSES = [":hover", ":acti
  *           <span class="box-model-infobar-pseudo-classes">:hover</span>
  *         </div>
  *       </div>
  *       <div class="box-model-infobar-arrow box-model-infobar-arrow-bottom"/>
  *     </div>
  *   </div>
  * </div>
  */
-function BoxModelHighlighter(highlighterEnv) {
-  AutoRefreshHighlighter.call(this, highlighterEnv);
-
-  this.markup = new CanvasFrameAnonymousContentHelper(this.highlighterEnv,
-    this._buildMarkup.bind(this));
+class BoxModelHighlighter extends AutoRefreshHighlighter {
+  constructor(highlighterEnv) {
+    super(highlighterEnv);
 
-  /**
-   * Optionally customize each region's fill color by adding an entry to the
-   * regionFill property: `highlighter.regionFill.margin = "red";
-   */
-  this.regionFill = {};
+    this.ID_CLASS_PREFIX = "box-model-";
+
+    this.markup = new CanvasFrameAnonymousContentHelper(this.highlighterEnv,
+      this._buildMarkup.bind(this));
 
-  this.onPageHide = this.onPageHide.bind(this);
-  this.onWillNavigate = this.onWillNavigate.bind(this);
-
-  this.highlighterEnv.on("will-navigate", this.onWillNavigate);
+    /**
+     * Optionally customize each region's fill color by adding an entry to the
+     * regionFill property: `highlighter.regionFill.margin = "red";
+     */
+    this.regionFill = {};
 
-  let { pageListenerTarget } = highlighterEnv;
-  pageListenerTarget.addEventListener("pagehide", this.onPageHide);
-}
+    this.onPageHide = this.onPageHide.bind(this);
+    this.onWillNavigate = this.onWillNavigate.bind(this);
+
+    this.highlighterEnv.on("will-navigate", this.onWillNavigate);
 
-BoxModelHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, {
-  typeName: "BoxModelHighlighter",
+    let { pageListenerTarget } = highlighterEnv;
+    pageListenerTarget.addEventListener("pagehide", this.onPageHide);
+  }
 
-  ID_CLASS_PREFIX: "box-model-",
-
-  _buildMarkup: function () {
+  _buildMarkup() {
     let doc = this.win.document;
 
     let highlighterContainer = doc.createElement("div");
     highlighterContainer.className = "highlighter-container box-model";
 
     // Build the root wrapper, used to adapt to the page zoom.
     let rootWrapper = createNode(this.win, {
       parent: highlighterContainer,
@@ -252,86 +249,86 @@ BoxModelHighlighter.prototype = extend(A
       attributes: {
         "class": "infobar-dimensions",
         "id": "infobar-dimensions"
       },
       prefix: this.ID_CLASS_PREFIX
     });
 
     return highlighterContainer;
-  },
+  }
 
   /**
    * Destroy the nodes. Remove listeners.
    */
-  destroy: function () {
+  destroy() {
     this.highlighterEnv.off("will-navigate", this.onWillNavigate);
 
     let { pageListenerTarget } = this.highlighterEnv;
     if (pageListenerTarget) {
       pageListenerTarget.removeEventListener("pagehide", this.onPageHide);
     }
 
     this.markup.destroy();
     AutoRefreshHighlighter.prototype.destroy.call(this);
-  },
+  }
 
-  getElement: function (id) {
+  getElement(id) {
     return this.markup.getElement(this.ID_CLASS_PREFIX + id);
-  },
+  }
 
   /**
    * Override the AutoRefreshHighlighter's _isNodeValid method to also return true for
    * text nodes since these can also be highlighted.
    * @param {DOMNode} node
    * @return {Boolean}
    */
-  _isNodeValid: function (node) {
+  _isNodeValid(node) {
     return node && (isNodeValid(node) || isNodeValid(node, nodeConstants.TEXT_NODE));
-  },
+  }
 
   /**
    * Show the highlighter on a given node
    */
-  _show: function () {
+  _show() {
     if (BOX_MODEL_REGIONS.indexOf(this.options.region) == -1) {
       this.options.region = "content";
     }
 
     let shown = this._update();
     this._trackMutations();
     this.emit("ready");
     return shown;
-  },
+  }
 
   /**
    * Track the current node markup mutations so that the node info bar can be
    * updated to reflects the node's attributes
    */
-  _trackMutations: function () {
+  _trackMutations() {
     if (isNodeValid(this.currentNode)) {
       let win = this.currentNode.ownerGlobal;
       this.currentNodeObserver = new win.MutationObserver(this.update);
       this.currentNodeObserver.observe(this.currentNode, {attributes: true});
     }
-  },
+  }
 
-  _untrackMutations: function () {
+  _untrackMutations() {
     if (isNodeValid(this.currentNode) && this.currentNodeObserver) {
       this.currentNodeObserver.disconnect();
       this.currentNodeObserver = null;
     }
-  },
+  }
 
   /**
    * Update the highlighter on the current highlighted node (the one that was
    * passed as an argument to show(node)).
    * Should be called whenever node size or attributes change
    */
-  _update: function () {
+  _update() {
     let shown = false;
     setIgnoreLayoutChanges(true);
 
     if (this._updateBoxModel()) {
       // Show the infobar only if configured to do so and the node is an element or a text
       // node.
       if (!this.options.hideInfoBar && (
           this.currentNode.nodeType === this.currentNode.ELEMENT_NODE ||
@@ -345,75 +342,75 @@ BoxModelHighlighter.prototype = extend(A
     } else {
       // Nothing to highlight (0px rectangle like a <script> tag for instance)
       this._hide();
     }
 
     setIgnoreLayoutChanges(false, this.highlighterEnv.window.document.documentElement);
 
     return shown;
-  },
+  }
 
-  _scrollUpdate: function () {
+  _scrollUpdate() {
     this._moveInfobar();
-  },
+  }
 
   /**
    * Hide the highlighter, the outline and the infobar.
    */
-  _hide: function () {
+  _hide() {
     setIgnoreLayoutChanges(true);
 
     this._untrackMutations();
     this._hideBoxModel();
     this._hideInfobar();
 
     setIgnoreLayoutChanges(false, this.highlighterEnv.window.document.documentElement);
-  },
+  }
 
   /**
    * Hide the infobar
    */
-  _hideInfobar: function () {
+  _hideInfobar() {
     this.getElement("infobar-container").setAttribute("hidden", "true");
-  },
+  }
 
   /**
    * Show the infobar
    */
-  _showInfobar: function () {
+  _showInfobar() {
     this.getElement("infobar-container").removeAttribute("hidden");
     this._updateInfobar();
-  },
+  }
 
   /**
    * Hide the box model
    */
-  _hideBoxModel: function () {
+  _hideBoxModel() {
     this.getElement("elements").setAttribute("hidden", "true");
-  },
+  }
 
   /**
    * Show the box model
    */
-  _showBoxModel: function () {
+  _showBoxModel() {
     this.getElement("elements").removeAttribute("hidden");
-  },
+  }
 
   /**
    * Calculate an outer quad based on the quads returned by getAdjustedQuads.
    * The BoxModelHighlighter may highlight more than one boxes, so in this case
    * create a new quad that "contains" all of these quads.
    * This is useful to position the guides and infobar.
    * This may happen if the BoxModelHighlighter is used to highlight an inline
    * element that spans line breaks.
    * @param {String} region The box-model region to get the outer quad for.
    * @return {Object} A quad-like object {p1,p2,p3,p4,bounds}
    */
-  _getOuterQuad: function (region) {
+  _getOuterQuad(region) {
     let quads = this.currentQuads[region];
     if (!quads.length) {
       return null;
     }
 
     let quad = {
       p1: {x: Infinity, y: Infinity},
       p2: {x: -Infinity, y: Infinity},
@@ -447,25 +444,25 @@ BoxModelHighlighter.prototype = extend(A
       quad.bounds.right = Math.max(quad.bounds.right, q.bounds.right);
     }
     quad.bounds.x = quad.bounds.left;
     quad.bounds.y = quad.bounds.top;
     quad.bounds.width = quad.bounds.right - quad.bounds.left;
     quad.bounds.height = quad.bounds.bottom - quad.bounds.top;
 
     return quad;
-  },
+  }
 
   /**
    * Update the box model as per the current node.
    *
    * @return {boolean}
    *         True if the current node has a box model to be highlighted
    */
-  _updateBoxModel: function () {
+  _updateBoxModel() {
     let options = this.options;
     options.region = options.region || "content";
 
     if (!this._nodeNeedsHighlighting()) {
       this._hideBoxModel();
       return false;
     }
 
@@ -511,19 +508,19 @@ BoxModelHighlighter.prototype = extend(A
       }
     }
 
     // Un-zoom the root wrapper if the page was zoomed.
     let rootId = this.ID_CLASS_PREFIX + "elements";
     this.markup.scaleRootElement(this.currentNode, rootId);
 
     return true;
-  },
+  }
 
-  _getBoxPathCoordinates: function (boxQuad, nextBoxQuad) {
+  _getBoxPathCoordinates(boxQuad, nextBoxQuad) {
     let {p1, p2, p3, p4} = boxQuad;
 
     let path;
     if (!nextBoxQuad || !this.options.onlyRegionArea) {
       // If this is the content box (inner-most box) or if we're not being asked
       // to highlight only region areas, then draw a simple rectangle.
       path = "M" + p1.x + "," + p1.y + " " +
              "L" + p2.x + "," + p2.y + " " +
@@ -540,30 +537,30 @@ BoxModelHighlighter.prototype = extend(A
              "L" + np1.x + "," + np1.y + " " +
              "L" + np4.x + "," + np4.y + " " +
              "L" + np3.x + "," + np3.y + " " +
              "L" + np2.x + "," + np2.y + " " +
              "L" + np1.x + "," + np1.y;
     }
 
     return path;
-  },
+  }
 
   /**
    * Can the current node be highlighted? Does it have quads.
    * @return {Boolean}
    */
-  _nodeNeedsHighlighting: function () {
+  _nodeNeedsHighlighting() {
     return this.currentQuads.margin.length ||
            this.currentQuads.border.length ||
            this.currentQuads.padding.length ||
            this.currentQuads.content.length;
-  },
+  }
 
-  _getOuterBounds: function () {
+  _getOuterBounds() {
     for (let region of ["margin", "border", "padding", "content"]) {
       let quad = this._getOuterQuad(region);
 
       if (!quad) {
         // Invisible element such as a script tag.
         break;
       }
 
@@ -579,24 +576,24 @@ BoxModelHighlighter.prototype = extend(A
       height: 0,
       left: 0,
       right: 0,
       top: 0,
       width: 0,
       x: 0,
       y: 0
     };
-  },
+  }
 
   /**
    * We only want to show guides for horizontal and vertical edges as this helps
    * to line them up. This method finds these edges and displays a guide there.
    * @param {String} region The region around which the guides should be shown.
    */
-  _showGuides: function (region) {
+  _showGuides(region) {
     let {p1, p2, p3, p4} = this._getOuterQuad(region);
 
     let allX = [p1.x, p2.x, p3.x, p4.x].sort((a, b) => a - b);
     let allY = [p1.y, p2.y, p3.y, p4.y].sort((a, b) => a - b);
     let toShowX = [];
     let toShowY = [];
 
     for (let arr of [allX, allY]) {
@@ -614,34 +611,34 @@ BoxModelHighlighter.prototype = extend(A
       }
     }
 
     // Move guide into place or hide it if no valid co-ordinate was found.
     this._updateGuide("top", Math.round(toShowY[0]));
     this._updateGuide("right", Math.round(toShowX[1]) - 1);
     this._updateGuide("bottom", Math.round(toShowY[1] - 1));
     this._updateGuide("left", Math.round(toShowX[0]));
-  },
+  }
 
-  _hideGuides: function () {
+  _hideGuides() {
     for (let side of BOX_MODEL_SIDES) {
       this.getElement("guide-" + side).setAttribute("hidden", "true");
     }
-  },
+  }
 
   /**
    * Move a guide to the appropriate position and display it. If no point is
    * passed then the guide is hidden.
    *
    * @param  {String} side
    *         The guide to update
    * @param  {Integer} point
    *         x or y co-ordinate. If this is undefined we hide the guide.
    */
-  _updateGuide: function (side, point = -1) {
+  _updateGuide(side, point = -1) {
     let guide = this.getElement("guide-" + side);
 
     if (point <= 0) {
       guide.setAttribute("hidden", "true");
       return false;
     }
 
     if (side === "top" || side === "bottom") {
@@ -654,22 +651,22 @@ BoxModelHighlighter.prototype = extend(A
       guide.setAttribute("y1", "0");
       guide.setAttribute("x2", point + "");
       guide.setAttribute("y2", "100%");
     }
 
     guide.removeAttribute("hidden");
 
     return true;
-  },
+  }
 
   /**
    * Update node information (displayName#id.class)
    */
-  _updateInfobar: function () {
+  _updateInfobar() {
     if (!this.currentNode) {
       return;
     }
 
     let {bindingElement: node, pseudo} =
         getBindingElementAndPseudo(this.currentNode);
 
     // Update the tag, id, classes, pseudo-classes and dimensions
@@ -698,44 +695,45 @@ BoxModelHighlighter.prototype = extend(A
 
     this.getElement("infobar-tagname").setTextContent(displayName);
     this.getElement("infobar-id").setTextContent(id);
     this.getElement("infobar-classes").setTextContent(classList);
     this.getElement("infobar-pseudo-classes").setTextContent(pseudos);
     this.getElement("infobar-dimensions").setTextContent(dim);
 
     this._moveInfobar();
-  },
+  }
 
-  _getPseudoClasses: function (node) {
+  _getPseudoClasses(node) {
     if (node.nodeType !== nodeConstants.ELEMENT_NODE) {
       // hasPseudoClassLock can only be used on Elements.
       return [];
     }
 
     return PSEUDO_CLASSES.filter(pseudo => hasPseudoClassLock(node, pseudo));
-  },
+  }
 
   /**
    * Move the Infobar to the right place in the highlighter.
    */
-  _moveInfobar: function () {
+  _moveInfobar() {
     let bounds = this._getOuterBounds();
     let container = this.getElement("infobar-container");
 
     moveInfobar(container, bounds, this.win);
-  },
+  }
 
-  onPageHide: function ({ target }) {
+  onPageHide({ target }) {
     // If a pagehide event is triggered for current window's highlighter, hide the
     // highlighter.
     if (target.defaultView === this.win) {
       this.hide();
     }
-  },
+  }
 
-  onWillNavigate: function ({ isTopLevel }) {
+  onWillNavigate({ isTopLevel }) {
     if (isTopLevel) {
       this.hide();
     }
   }
-});
+}
+
 exports.BoxModelHighlighter = BoxModelHighlighter;
--- a/devtools/server/actors/highlighters/css-grid.js
+++ b/devtools/server/actors/highlighters/css-grid.js
@@ -1,16 +1,15 @@
 /* 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/. */
 
 "use strict";
 
 const Services = require("Services");
-const { extend } = require("sdk/core/heritage");
 const { AutoRefreshHighlighter } = require("./auto-refresh");
 const {
   CanvasFrameAnonymousContentHelper,
   createNode,
   createSVGNode,
   moveInfobar,
 } = require("./utils/markup");
 const {
@@ -339,47 +338,45 @@ function drawRoundedRect(ctx, x, y, widt
  *       <div class="css-grid-infobar-text">
  *         <span class="css-grid-line-infobar-number">Grid Line Number</span>
  *         <span class="css-grid-line-infobar-names">Grid Line Names></span>
  *       </div>
  *     </div>
  *   </div>
  * </div>
  */
-function CssGridHighlighter(highlighterEnv) {
-  AutoRefreshHighlighter.call(this, highlighterEnv);
+class CssGridHighlighter extends AutoRefreshHighlighter {
+  constructor(highlighterEnv) {
+    super(highlighterEnv);
 
-  this.markup = new CanvasFrameAnonymousContentHelper(this.highlighterEnv,
-    this._buildMarkup.bind(this));
+    this.ID_CLASS_PREFIX = "css-grid-";
 
-  this.onNavigate = this.onNavigate.bind(this);
-  this.onPageHide = this.onPageHide.bind(this);
-  this.onWillNavigate = this.onWillNavigate.bind(this);
+    this.markup = new CanvasFrameAnonymousContentHelper(this.highlighterEnv,
+      this._buildMarkup.bind(this));
 
-  this.highlighterEnv.on("navigate", this.onNavigate);
-  this.highlighterEnv.on("will-navigate", this.onWillNavigate);
-
-  let { pageListenerTarget } = highlighterEnv;
-  pageListenerTarget.addEventListener("pagehide", this.onPageHide);
+    this.onNavigate = this.onNavigate.bind(this);
+    this.onPageHide = this.onPageHide.bind(this);
+    this.onWillNavigate = this.onWillNavigate.bind(this);
 
-  // Initialize the <canvas> position to the top left corner of the page
-  this._canvasPosition = {
-    x: 0,
-    y: 0
-  };
+    this.highlighterEnv.on("navigate", this.onNavigate);
+    this.highlighterEnv.on("will-navigate", this.onWillNavigate);
+
+    let { pageListenerTarget } = highlighterEnv;
+    pageListenerTarget.addEventListener("pagehide", this.onPageHide);
 
-  // Calling `calculateCanvasPosition` anyway since the highlighter could be initialized
-  // on a page that has scrolled already.
-  this.calculateCanvasPosition();
-}
+    // Initialize the <canvas> position to the top left corner of the page
+    this._canvasPosition = {
+      x: 0,
+      y: 0
+    };
 
-CssGridHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, {
-  typeName: "CssGridHighlighter",
-
-  ID_CLASS_PREFIX: "css-grid-",
+    // Calling `calculateCanvasPosition` anyway since the highlighter could be initialized
+    // on a page that has scrolled already.
+    this.calculateCanvasPosition();
+  }
 
   _buildMarkup() {
     let container = createNode(this.win, {
       attributes: {
         "class": "highlighter-container"
       }
     });
 
@@ -584,50 +581,50 @@ CssGridHighlighter.prototype = extend(Au
       attributes: {
         "class": "line-infobar-names",
         "id": "line-infobar-names"
       },
       prefix: this.ID_CLASS_PREFIX
     });
 
     return container;
-  },
+  }
 
   destroy() {
     let { highlighterEnv } = this;
     highlighterEnv.off("navigate", this.onNavigate);
     highlighterEnv.off("will-navigate", this.onWillNavigate);
 
     let { pageListenerTarget } = highlighterEnv;
     if (pageListenerTarget) {
       pageListenerTarget.removeEventListener("pagehide", this.onPageHide);
     }
 
     this.markup.destroy();
 
     // Clear the pattern cache to avoid dead object exceptions (Bug 1342051).
     this._clearCache();
     AutoRefreshHighlighter.prototype.destroy.call(this);
-  },
+  }
 
   getElement(id) {
     return this.markup.getElement(this.ID_CLASS_PREFIX + id);
-  },
+  }
 
   get ctx() {
     return this.canvas.getCanvasContext("2d");
-  },
+  }
 
   get canvas() {
     return this.getElement("canvas");
-  },
+  }
 
   get color() {
     return this.options.color || DEFAULT_GRID_COLOR;
-  },
+  }
 
   /**
    * Gets the grid gap pattern used to render the gap regions based on the device
    * pixel ratio given.
    *
    * @param {Number} devicePixelRatio
    *         The device pixel ratio we want the pattern for.
    * @param  {Object} dimension
@@ -673,156 +670,156 @@ CssGridHighlighter.prototype = extend(Au
     ctx.restore();
 
     let pattern = ctx.createPattern(canvas, "repeat");
 
     gridPatternMap.set(dimension, pattern);
     gCachedGridPattern.set(devicePixelRatio, gridPatternMap);
 
     return pattern;
-  },
+  }
 
   /**
    * Called when the page navigates. Used to clear the cached gap patterns and avoid
    * using DeadWrapper objects as gap patterns the next time.
    */
   onNavigate() {
     this._clearCache();
-  },
+  }
 
-  onPageHide: function ({ target }) {
+  onPageHide({ target }) {
     // If a page hide event is triggered for current window's highlighter, hide the
     // highlighter.
     if (target.defaultView === this.win) {
       this.hide();
     }
-  },
+  }
 
   onWillNavigate({ isTopLevel }) {
     if (isTopLevel) {
       this.hide();
     }
-  },
+  }
 
   _show() {
     if (Services.prefs.getBoolPref(CSS_GRID_ENABLED_PREF) && !this.isGrid()) {
       this.hide();
       return false;
     }
 
     // The grid pattern cache should be cleared in case the color changed.
     this._clearCache();
 
     // Hide the canvas, grid element highlights and infobar.
     this._hide();
 
     return this._update();
-  },
+  }
 
   _clearCache() {
     gCachedGridPattern.clear();
-  },
+  }
 
   /**
    * Shows the grid area highlight for the given area name.
    *
    * @param  {String} areaName
    *         Grid area name.
    */
   showGridArea(areaName) {
     this.renderGridArea(areaName);
-  },
+  }
 
   /**
    * Shows all the grid area highlights for the current grid.
    */
   showAllGridAreas() {
     this.renderGridArea();
-  },
+  }
 
   /**
    * Clear the grid area highlights.
    */
   clearGridAreas() {
     let areas = this.getElement("areas");
     areas.setAttribute("d", "");
-  },
+  }
 
   /**
    * Shows the grid cell highlight for the given grid cell options.
    *
    * @param  {Number} options.gridFragmentIndex
    *         Index of the grid fragment to render the grid cell highlight.
    * @param  {Number} options.rowNumber
    *         Row number of the grid cell to highlight.
    * @param  {Number} options.columnNumber
    *         Column number of the grid cell to highlight.
    */
   showGridCell({ gridFragmentIndex, rowNumber, columnNumber }) {
     this.renderGridCell(gridFragmentIndex, rowNumber, columnNumber);
-  },
+  }
 
   /**
    * Shows the grid line highlight for the given grid line options.
    *
    * @param  {Number} options.gridFragmentIndex
    *         Index of the grid fragment to render the grid line highlight.
    * @param  {Number} options.lineNumber
    *         Line number of the grid line to highlight.
    * @param  {String} options.type
    *         The dimension type of the grid line.
    */
   showGridLineNames({ gridFragmentIndex, lineNumber, type }) {
     this.renderGridLineNames(gridFragmentIndex, lineNumber, type);
-  },
+  }
 
   /**
    * Clear the grid cell highlights.
    */
   clearGridCell() {
     let cells = this.getElement("cells");
     cells.setAttribute("d", "");
-  },
+  }
 
   /**
    * Checks if the current node has a CSS Grid layout.
    *
    * @return  {Boolean} true if the current node has a CSS grid layout, false otherwise.
    */
   isGrid() {
     return this.currentNode.getGridFragments().length > 0;
-  },
+  }
 
   /**
    * Is a given grid fragment valid? i.e. does it actually have tracks? In some cases, we
    * may have a fragment that defines column tracks but doesn't have any rows (or vice
    * versa). In which case we do not want to draw anything for that fragment.
    *
    * @param {Object} fragment
    * @return {Boolean}
    */
   isValidFragment(fragment) {
     return fragment.cols.tracks.length && fragment.rows.tracks.length;
-  },
+  }
 
   /**
    * The AutoRefreshHighlighter's _hasMoved method returns true only if the
    * element's quads have changed. Override it so it also returns true if the
    * element's grid has changed (which can happen when you change the
    * grid-template-* CSS properties with the highlighter displayed).
    */
   _hasMoved() {
     let hasMoved = AutoRefreshHighlighter.prototype._hasMoved.call(this);
 
     let oldGridData = stringifyGridFragments(this.gridData);
     this.gridData = this.currentNode.getGridFragments();
     let newGridData = stringifyGridFragments(this.gridData);
 
     return hasMoved || oldGridData !== newGridData;
-  },
+  }
 
   /**
    * Update the highlighter on the current highlighted node (the one that was
    * passed as an argument to show(node)).
    * Should be called whenever node's geometry or grid changes.
    */
   _update() {
     setIgnoreLayoutChanges(true);
@@ -878,17 +875,17 @@ CssGridHighlighter.prototype = extend(Au
     this._showGrid();
     this._showGridElements();
 
     root.setAttribute("style",
       `position:absolute; width:${width}px;height:${height}px; overflow:hidden`);
 
     setIgnoreLayoutChanges(false, this.highlighterEnv.document.documentElement);
     return true;
-  },
+  }
 
   /**
    * Update the grid information displayed in the grid area info bar.
    *
    * @param  {GridArea} area
    *         The grid area object.
    * @param  {Object} bounds
    *          A DOMRect-like object represent the grid area rectangle.
@@ -902,17 +899,17 @@ CssGridHighlighter.prototype = extend(Au
     this.getElement("area-infobar-name").setTextContent(area.name);
     this.getElement("area-infobar-dimensions").setTextContent(dim);
 
     let container = this.getElement("area-infobar-container");
     moveInfobar(container, bounds, this.win, {
       position: "bottom",
       hideIfOffscreen: true
     });
-  },
+  }
 
   /**
    * Update the grid information displayed in the grid cell info bar.
    *
    * @param  {Number} rowNumber
    *         The grid cell's row number.
    * @param  {Number} columnNumber
    *         The grid cell's column number.
@@ -930,17 +927,17 @@ CssGridHighlighter.prototype = extend(Au
     this.getElement("cell-infobar-position").setTextContent(position);
     this.getElement("cell-infobar-dimensions").setTextContent(dim);
 
     let container = this.getElement("cell-infobar-container");
     moveInfobar(container, bounds, this.win, {
       position: "top",
       hideIfOffscreen: true
     });
-  },
+  }
 
   /**
    * Update the grid information displayed in the grid line info bar.
    *
    * @param  {String} gridLineNames
    *         Comma-separated string of names for the grid line.
    * @param  {Number} gridLineNumber
    *         The grid line number.
@@ -951,29 +948,29 @@ CssGridHighlighter.prototype = extend(Au
    */
   _updateGridLineInfobar(gridLineNames, gridLineNumber, x, y) {
     this.getElement("line-infobar-number").setTextContent(gridLineNumber);
     this.getElement("line-infobar-names").setTextContent(gridLineNames);
 
     let container = this.getElement("line-infobar-container");
     moveInfobar(container,
       getBoundsFromPoints([{x, y}, {x, y}, {x, y}, {x, y}]), this.win);
-  },
+  }
 
   /**
    * The <canvas>'s position needs to be updated if the page scrolls too much, in order
    * to give the illusion that it always covers the viewport.
    */
   _scrollUpdate() {
     let hasPositionChanged = this.calculateCanvasPosition();
 
     if (hasPositionChanged) {
       this._update();
     }
-  },
+  }
 
   /**
    * This method is responsible to do the math that updates the <canvas>'s position,
    * in accordance with the page's scroll, document's size, canvas size, and
    * viewport's size.
    * It's called when a page's scroll is detected.
    *
    * @return {Boolean} `true` if the <canvas> position was updated, `false` otherwise.
@@ -1023,34 +1020,34 @@ CssGridHighlighter.prototype = extend(Au
       this._canvasPosition.x = Math.min(leftThreshold, rightBoundary);
       hasUpdated = true;
     } else if (x > leftBoundary && x > leftThreshold) {
       this._canvasPosition.x = Math.max(rightThreshold, leftBoundary);
       hasUpdated = true;
     }
 
     return hasUpdated;
-  },
+  }
 
   /**
    * Updates the <canvas> element's style in accordance with the current window's
    * devicePixelRatio, and the position calculated in `calculateCanvasPosition`; it also
    * clears the drawing context.
    */
   updateCanvasElement() {
     let size = CANVAS_SIZE / this.win.devicePixelRatio;
     let { x, y } = this._canvasPosition;
 
     // Resize the canvas taking the dpr into account so as to have crisp lines, and
     // translating it to give the perception that it always covers the viewport.
     this.canvas.setAttribute("style",
       `width:${size}px;height:${size}px; transform: translate(${x}px, ${y}px);`);
 
     this.ctx.clearRect(0, 0, CANVAS_SIZE, CANVAS_SIZE);
-  },
+  }
 
   /**
    * Updates the current matrices for both canvas drawing and SVG, taking in account the
    * following transformations, in this order:
    *   1. The scale given by the display pixel ratio.
    *   2. The translation to the top left corner of the element.
    *   3. The scale given by the current zoom.
    *   4. The translation given by the top and left padding of the element.
@@ -1084,33 +1081,33 @@ CssGridHighlighter.prototype = extend(Au
       m = multiply(m, nodeMatrix);
       this.hasNodeTransformations = true;
     }
 
     // Finally, we translate the origin based on the node's padding and border values.
     m = multiply(m, translate(paddingLeft + borderLeft, paddingTop + borderTop));
 
     this.currentMatrix = m;
-  },
+  }
 
   getFirstRowLinePos(fragment) {
     return fragment.rows.lines[0].start;
-  },
+  }
 
   getLastRowLinePos(fragment) {
     return fragment.rows.lines[fragment.rows.lines.length - 1].start;
-  },
+  }
 
   getFirstColLinePos(fragment) {
     return fragment.cols.lines[0].start;
-  },
+  }
 
   getLastColLinePos(fragment) {
     return fragment.cols.lines[fragment.cols.lines.length - 1].start;
-  },
+  }
 
   /**
    * Get the GridLine index of the last edge of the explicit grid for a grid dimension.
    *
    * @param  {GridTracks} tracks
    *         The grid track of a given grid dimension.
    * @return {Number} index of the last edge of the explicit grid for a grid dimension.
    */
@@ -1119,17 +1116,17 @@ CssGridHighlighter.prototype = extend(Au
 
     // Traverse the grid track backwards until we find an explicit track.
     while (trackIndex >= 0 && tracks[trackIndex].type != "explicit") {
       trackIndex--;
     }
 
     // The grid line index is the grid track index + 1.
     return trackIndex + 1;
-  },
+  }
 
   renderFragment(fragment) {
     if (!this.isValidFragment(fragment)) {
       return;
     }
 
     this.renderLines(fragment.cols, COLUMNS, "left", "top", "height",
                      this.getFirstRowLinePos(fragment),
@@ -1144,17 +1141,17 @@ CssGridHighlighter.prototype = extend(Au
 
     // Line numbers are rendered in a 2nd step to avoid overlapping with existing lines.
     if (this.options.showGridLineNumbers) {
       this.renderLineNumbers(fragment.cols, COLUMNS, "left", "top",
                        this.getFirstRowLinePos(fragment));
       this.renderLineNumbers(fragment.rows, ROWS, "top", "left",
                        this.getFirstColLinePos(fragment));
     }
-  },
+  }
 
   /**
    * Renders the grid area overlay on the css grid highlighter canvas.
    */
   renderGridAreaOverlay() {
     let padding = 1;
 
     for (let i = 0; i < this.gridData.length; i++) {
@@ -1191,17 +1188,17 @@ CssGridHighlighter.prototype = extend(Au
                         areaColStartLinePos, areaColEnd.start,
                         ROWS, "areaEdge");
 
         this.renderGridAreaName(fragment, area);
       }
     }
 
     this.ctx.restore();
-  },
+  }
 
   /**
    * Render grid area name on the containing grid area cell.
    *
    * @param  {Object} fragment
    *         The grid fragment of the grid container.
    * @param  {Object} area
    *         The area overlay to render on the CSS highlighter canvas.
@@ -1265,17 +1262,17 @@ CssGridHighlighter.prototype = extend(Au
         drawRoundedRect(this.ctx, rectXPos, rectYPos, boxWidth, boxHeight, radius);
 
         this.ctx.fillStyle = this.color;
         this.ctx.fillText(area.name, x, y + padding);
       }
     }
 
     this.ctx.restore();
-  },
+  }
 
   /**
    * Render the grid lines given the grid dimension information of the
    * column or row lines.
    *
    * @param  {GridDimension} gridDimension
    *         Column or row grid dimension object.
    * @param  {Object} quad.bounds
@@ -1317,17 +1314,17 @@ CssGridHighlighter.prototype = extend(Au
       // Render a second line to illustrate the gutter for non-zero breadth.
       if (line.breadth > 0) {
         this.renderGridGap(linePos, lineStartPos, lineEndPos, line.breadth,
                            dimensionType);
         this.renderLine(linePos + line.breadth, lineStartPos, lineEndPos, dimensionType,
                         gridDimension.tracks[i].type);
       }
     }
-  },
+  }
 
   /**
    * Render the grid lines given the grid dimension information of the
    * column or row lines.
    *
    * see @param for renderLines.
    */
   renderLineNumbers(gridDimension, dimensionType, mainSide, crossSide,
@@ -1348,17 +1345,17 @@ CssGridHighlighter.prototype = extend(Au
       // For such lines the API returns always 0 as line's number.
       if (line.number === 0) {
         continue;
       }
 
       this.renderGridLineNumber(line.number, linePos, lineStartPos, line.breadth,
         dimensionType);
     }
-  },
+  }
 
   /**
    * Render the grid line on the css grid highlighter canvas.
    *
    * @param  {Number} linePos
    *         The line position along the x-axis for a column grid line and
    *         y-axis for a row grid line.
    * @param  {Number} startPos
@@ -1407,17 +1404,17 @@ CssGridHighlighter.prototype = extend(Au
     if (GRID_LINES_PROPERTIES[lineType].lineWidth) {
       this.ctx.lineWidth = GRID_LINES_PROPERTIES[lineType].lineWidth * devicePixelRatio;
     } else {
       this.ctx.lineWidth = lineWidth;
     }
 
     this.ctx.stroke();
     this.ctx.restore();
-  },
+  }
 
   /**
    * Render the grid line number on the css grid highlighter canvas.
    *
    * @param  {Number} lineNumber
    *         The grid line number.
    * @param  {Number} linePos
    *         The line position along the x-axis for a column grid line and
@@ -1492,17 +1489,17 @@ CssGridHighlighter.prototype = extend(Au
     let radius = 2 * displayPixelRatio;
     drawRoundedRect(this.ctx, x, y, boxWidth, boxHeight, radius);
 
     // Write the line number inside of the rectangle.
     this.ctx.fillStyle = "black";
     this.ctx.fillText(lineNumber, x + padding, y + textHeight + padding);
 
     this.ctx.restore();
-  },
+  }
 
   /**
    * Render the grid gap area on the css grid highlighter canvas.
    *
    * @param  {Number} linePos
    *         The line position along the x-axis for a column grid line and
    *         y-axis for a row grid line.
    * @param  {Number} startPos
@@ -1546,17 +1543,17 @@ CssGridHighlighter.prototype = extend(Au
         endPos = this._winDimensions.width;
         startPos = -endPos;
       }
       drawRect(this.ctx, startPos, linePos, endPos, linePos + breadth,
         this.currentMatrix);
     }
     this.ctx.fill();
     this.ctx.restore();
-  },
+  }
 
   /**
    * Render the grid area highlight for the given area name or for all the grid areas.
    *
    * @param  {String} areaName
    *         Name of the grid area to be highlighted. If no area name is provided, all
    *         the grid areas should be highlighted.
    */
@@ -1606,17 +1603,17 @@ CssGridHighlighter.prototype = extend(Au
           this._showGridAreaInfoBar();
           this._updateGridAreaInfobar(area, bounds);
         }
       }
     }
 
     let areas = this.getElement("areas");
     areas.setAttribute("d", paths.join(" "));
-  },
+  }
 
   /**
    * Render the grid cell highlight for the given grid fragment index, row and column
    * number.
    *
    * @param  {Number} gridFragmentIndex
    *         Index of the grid fragment to render the grid cell highlight.
    * @param  {Number} rowNumber
@@ -1661,17 +1658,17 @@ CssGridHighlighter.prototype = extend(Au
       y: Math.round(point.y / displayPixelRatio)
     })));
 
     let cells = this.getElement("cells");
     cells.setAttribute("d", getPathDescriptionFromPoints(svgPoints));
 
     this._showGridCellInfoBar();
     this._updateGridCellInfobar(rowNumber, columnNumber, bounds);
-  },
+  }
 
   /**
    * Render the grid line name highlight for the given grid fragment index, lineNumber,
    * and dimensionType.
    *
    * @param  {Number} gridFragmentIndex
    *         Index of the grid fragment to render the grid line highlight.
    * @param  {Number} lineNumber
@@ -1710,66 +1707,65 @@ CssGridHighlighter.prototype = extend(Au
       : colXPosition.start + (bounds.left / currentZoom);
 
     let y = dimensionType === ROWS
       ? linePos.start + (bounds.top / currentZoom)
       : rowYPosition.start + (bounds.top / currentZoom);
 
     this._showGridLineInfoBar();
     this._updateGridLineInfobar(names.join(", "), lineNumber, x, y);
-  },
+  }
 
   /**
    * Hide the highlighter, the canvas and the infobars.
    */
   _hide() {
     setIgnoreLayoutChanges(true);
     this._hideGrid();
     this._hideGridElements();
     this._hideGridAreaInfoBar();
     this._hideGridCellInfoBar();
     this._hideGridLineInfoBar();
     setIgnoreLayoutChanges(false, this.highlighterEnv.document.documentElement);
-  },
+  }
 
   _hideGrid() {
     this.getElement("canvas").setAttribute("hidden", "true");
-  },
+  }
 
   _showGrid() {
     this.getElement("canvas").removeAttribute("hidden");
-  },
+  }
 
   _hideGridElements() {
     this.getElement("elements").setAttribute("hidden", "true");
-  },
+  }
 
   _showGridElements() {
     this.getElement("elements").removeAttribute("hidden");
-  },
+  }
 
   _hideGridAreaInfoBar() {
     this.getElement("area-infobar-container").setAttribute("hidden", "true");
-  },
+  }
 
   _showGridAreaInfoBar() {
     this.getElement("area-infobar-container").removeAttribute("hidden");
-  },
+  }
 
   _hideGridCellInfoBar() {
     this.getElement("cell-infobar-container").setAttribute("hidden", "true");
-  },
+  }
 
   _showGridCellInfoBar() {
     this.getElement("cell-infobar-container").removeAttribute("hidden");
-  },
+  }
 
   _hideGridLineInfoBar() {
     this.getElement("line-infobar-container").setAttribute("hidden", "true");
-  },
+  }
 
   _showGridLineInfoBar() {
     this.getElement("line-infobar-container").removeAttribute("hidden");
-  },
-
-});
+  }
+}
 
 exports.CssGridHighlighter = CssGridHighlighter;
--- a/devtools/server/actors/highlighters/css-transform.js
+++ b/devtools/server/actors/highlighters/css-transform.js
@@ -1,15 +1,14 @@
 /* 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/. */
 
 "use strict";
 
-const { extend } = require("sdk/core/heritage");
 const { AutoRefreshHighlighter } = require("./auto-refresh");
 const {
   CanvasFrameAnonymousContentHelper, getComputedStyle,
   createSVGNode, createNode } = require("./utils/markup");
 const { setIgnoreLayoutChanges,
   getNodeBounds } = require("devtools/shared/layout/utils");
 
 // The minimum distance a line should be before it has an arrow marker-end
@@ -17,29 +16,27 @@ const ARROW_LINE_MIN_DISTANCE = 10;
 
 var MARKER_COUNTER = 1;
 
 /**
  * The CssTransformHighlighter is the class that draws an outline around a
  * transformed element and an outline around where it would be if untransformed
  * as well as arrows connecting the 2 outlines' corners.
  */
-function CssTransformHighlighter(highlighterEnv) {
-  AutoRefreshHighlighter.call(this, highlighterEnv);
+class CssTransformHighlighter extends AutoRefreshHighlighter {
+  constructor(highlighterEnv) {
+    super(highlighterEnv);
 
-  this.markup = new CanvasFrameAnonymousContentHelper(this.highlighterEnv,
-    this._buildMarkup.bind(this));
-}
+    this.ID_CLASS_PREFIX = "css-transform-";
 
-CssTransformHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, {
-  typeName: "CssTransformHighlighter",
+    this.markup = new CanvasFrameAnonymousContentHelper(this.highlighterEnv,
+      this._buildMarkup.bind(this));
+  }
 
-  ID_CLASS_PREFIX: "css-transform-",
-
-  _buildMarkup: function () {
+  _buildMarkup() {
     let container = createNode(this.win, {
       attributes: {
         "class": "highlighter-container"
       }
     });
 
     // The root wrapper is used to unzoom the highlighter when needed.
     let rootWrapper = createNode(this.win, {
@@ -125,79 +122,79 @@ CssTransformHighlighter.prototype = exte
           "class": "line",
           "marker-end": "url(#" + this.markerId + ")"
         },
         prefix: this.ID_CLASS_PREFIX
       });
     }
 
     return container;
-  },
+  }
 
   /**
    * Destroy the nodes. Remove listeners.
    */
-  destroy: function () {
+  destroy() {
     AutoRefreshHighlighter.prototype.destroy.call(this);
     this.markup.destroy();
-  },
+  }
 
-  getElement: function (id) {
+  getElement(id) {
     return this.markup.getElement(this.ID_CLASS_PREFIX + id);
-  },
+  }
 
   /**
    * Show the highlighter on a given node
    */
-  _show: function () {
+  _show() {
     if (!this._isTransformed(this.currentNode)) {
       this.hide();
       return false;
     }
 
     return this._update();
-  },
+  }
 
   /**
    * Checks if the supplied node is transformed and not inline
    */
-  _isTransformed: function (node) {
+  _isTransformed(node) {
     let style = getComputedStyle(node);
     return style && (style.transform !== "none" && style.display !== "inline");
-  },
+  }
 
-  _setPolygonPoints: function (quad, id) {
+  _setPolygonPoints(quad, id) {
     let points = [];
     for (let point of ["p1", "p2", "p3", "p4"]) {
       points.push(quad[point].x + "," + quad[point].y);
     }
     this.getElement(id).setAttribute("points", points.join(" "));
-  },
+  }
 
-  _setLinePoints: function (p1, p2, id) {
+  _setLinePoints(p1, p2, id) {
     let line = this.getElement(id);
     line.setAttribute("x1", p1.x);
     line.setAttribute("y1", p1.y);
     line.setAttribute("x2", p2.x);
     line.setAttribute("y2", p2.y);
 
     let dist = Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
     if (dist < ARROW_LINE_MIN_DISTANCE) {
       line.removeAttribute("marker-end");
     } else {
       line.setAttribute("marker-end", "url(#" + this.markerId + ")");
     }
-  },
+  }
 
   /**
    * Update the highlighter on the current highlighted node (the one that was
    * passed as an argument to show(node)).
    * Should be called whenever node size or attributes change
    */
-  _update: function () {
+  _update() {
     setIgnoreLayoutChanges(true);
 
     // Getting the points for the transformed shape
     let quads = this.currentQuads.border;
     if (!quads.length ||
         quads[0].bounds.width <= 0 || quads[0].bounds.height <= 0) {
       this._hideShapes();
       return false;
@@ -216,28 +213,29 @@ CssTransformHighlighter.prototype = exte
 
     // Adapt to the current zoom
     this.markup.scaleRootElement(this.currentNode, this.ID_CLASS_PREFIX + "root");
 
     this._showShapes();
 
     setIgnoreLayoutChanges(false, this.highlighterEnv.window.document.documentElement);
     return true;
-  },
+  }
 
   /**
    * Hide the highlighter, the outline and the infobar.
    */
-  _hide: function () {
+  _hide() {
     setIgnoreLayoutChanges(true);
     this._hideShapes();
     setIgnoreLayoutChanges(false, this.highlighterEnv.window.document.documentElement);
-  },
+  }
 
-  _hideShapes: function () {
+  _hideShapes() {
     this.getElement("elements").setAttribute("hidden", "true");
-  },
+  }
 
-  _showShapes: function () {
+  _showShapes() {
     this.getElement("elements").removeAttribute("hidden");
   }
-});
+}
+
 exports.CssTransformHighlighter = CssTransformHighlighter;
--- a/devtools/server/actors/highlighters/geometry-editor.js
+++ b/devtools/server/actors/highlighters/geometry-editor.js
@@ -1,15 +1,14 @@
 /* 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/. */
 
 "use strict";
 
-const { extend } = require("sdk/core/heritage");
 const { AutoRefreshHighlighter } = require("./auto-refresh");
 const { CanvasFrameAnonymousContentHelper, getCSSStyleRules, getComputedStyle,
         createSVGNode, createNode } = require("./utils/markup");
 const { setIgnoreLayoutChanges, getAdjustedQuads } = require("devtools/shared/layout/utils");
 
 const GEOMETRY_LABEL_SIZE = 6;
 
 // List of all DOM Events subscribed directly to the document from the
@@ -197,50 +196,48 @@ exports.getDefinedGeometryProperties = g
  * The highlighter displays lines and labels for each of the defined properties
  * in and around the element (relative to the offset parent when one exists).
  * The highlighter also highlights the element itself and its offset parent if
  * there is one.
  *
  * Note that the class name contains the word Editor because the aim is for the
  * handles to be draggable in content to make the geometry editable.
  */
-function GeometryEditorHighlighter(highlighterEnv) {
-  AutoRefreshHighlighter.call(this, highlighterEnv);
+class GeometryEditorHighlighter extends AutoRefreshHighlighter {
+  constructor(highlighterEnv) {
+    super(highlighterEnv);
+
+    this.ID_CLASS_PREFIX = "geometry-editor-";
 
-  // The list of element geometry properties that can be set.
-  this.definedProperties = new Map();
+    // The list of element geometry properties that can be set.
+    this.definedProperties = new Map();
 
-  this.markup = new CanvasFrameAnonymousContentHelper(highlighterEnv,
-    this._buildMarkup.bind(this));
+    this.markup = new CanvasFrameAnonymousContentHelper(highlighterEnv,
+      this._buildMarkup.bind(this));
 
-  let { pageListenerTarget } = this.highlighterEnv;
+    let { pageListenerTarget } = this.highlighterEnv;
 
-  // Register the geometry editor instance to all events we're interested in.
-  DOM_EVENTS.forEach(type => pageListenerTarget.addEventListener(type, this));
+    // Register the geometry editor instance to all events we're interested in.
+    DOM_EVENTS.forEach(type => pageListenerTarget.addEventListener(type, this));
+
+    // Register the mousedown event for each Geometry Editor's handler.
+    // Those events are automatically removed when the markup is destroyed.
+    let onMouseDown = this.handleEvent.bind(this);
 
-  // Register the mousedown event for each Geometry Editor's handler.
-  // Those events are automatically removed when the markup is destroyed.
-  let onMouseDown = this.handleEvent.bind(this);
+    for (let side of GeoProp.SIDES) {
+      this.getElement("handler-" + side)
+        .addEventListener("mousedown", onMouseDown);
+    }
 
-  for (let side of GeoProp.SIDES) {
-    this.getElement("handler-" + side)
-      .addEventListener("mousedown", onMouseDown);
+    this.onWillNavigate = this.onWillNavigate.bind(this);
+
+    this.highlighterEnv.on("will-navigate", this.onWillNavigate);
   }
 
-  this.onWillNavigate = this.onWillNavigate.bind(this);
-
-  this.highlighterEnv.on("will-navigate", this.onWillNavigate);
-}
-
-GeometryEditorHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, {
-  typeName: "GeometryEditorHighlighter",
-
-  ID_CLASS_PREFIX: "geometry-editor-",
-
-  _buildMarkup: function () {
+  _buildMarkup() {
     let container = createNode(this.win, {
       attributes: {"class": "highlighter-container"}
     });
 
     let root = createNode(this.win, {
       parent: container,
       attributes: {
         "id": "root",
@@ -356,19 +353,19 @@ GeometryEditorHighlighter.prototype = ex
           "x": GeoProp.isHorizontal(name) ? "30" : "35",
           "y": "10"
         },
         prefix: this.ID_CLASS_PREFIX
       });
     }
 
     return container;
-  },
+  }
 
-  destroy: function () {
+  destroy() {
     // Avoiding exceptions if `destroy` is called multiple times; and / or the
     // highlighter environment was already destroyed.
     if (!this.highlighterEnv) {
       return;
     }
 
     let { pageListenerTarget } = this.highlighterEnv;
 
@@ -378,19 +375,19 @@ GeometryEditorHighlighter.prototype = ex
     }
 
     AutoRefreshHighlighter.prototype.destroy.call(this);
 
     this.markup.destroy();
     this.definedProperties.clear();
     this.definedProperties = null;
     this.offsetParent = null;
-  },
+  }
 
-  handleEvent: function (event, id) {
+  handleEvent(event, id) {
     // No event handling if the highlighter is hidden
     if (this.getElement("root").hasAttribute("hidden")) {
       return;
     }
 
     const { target, type, pageX, pageY } = event;
 
     switch (type) {
@@ -471,23 +468,23 @@ GeometryEditorHighlighter.prototype = ex
         // it will override the inline style too. To ensure Geometry Editor
         // will always update the element, we have to add `!important` as
         // well.
         this.currentNode.style.setProperty(
           side, (value + delta) + unit, "important");
 
         break;
     }
-  },
+  }
 
-  getElement: function (id) {
+  getElement(id) {
     return this.markup.getElement(this.ID_CLASS_PREFIX + id);
-  },
+  }
 
-  _show: function () {
+  _show() {
     this.computedStyle = getComputedStyle(this.currentNode);
     let pos = this.computedStyle.position;
     // XXX: sticky positioning is ignored for now. To be implemented next.
     if (pos === "sticky") {
       this.hide();
       return false;
     }
 
@@ -495,19 +492,19 @@ GeometryEditorHighlighter.prototype = ex
     if (!hasUpdated) {
       this.hide();
       return false;
     }
 
     this.getElement("root").removeAttribute("hidden");
 
     return true;
-  },
+  }
 
-  _update: function () {
+  _update() {
     // At each update, the position or/and size may have changed, so get the
     // list of defined properties, and re-position the arrows and highlighters.
     this.definedProperties = getDefinedGeometryProperties(this.currentNode);
 
     if (!this.definedProperties.size) {
       console.warn("The element does not have editable geometry properties");
       return false;
     }
@@ -520,31 +517,31 @@ GeometryEditorHighlighter.prototype = ex
     this.updateArrows();
 
     // Avoid zooming the arrows when content is zoomed.
     let node = this.currentNode;
     this.markup.scaleRootElement(node, this.ID_CLASS_PREFIX + "root");
 
     setIgnoreLayoutChanges(false, this.highlighterEnv.document.documentElement);
     return true;
-  },
+  }
 
   /**
    * Update the offset parent rectangle.
    * There are 3 different cases covered here:
    * - the node is absolutely/fixed positioned, and an offsetParent is defined
    *   (i.e. it's not just positioned in the viewport): the offsetParent node
    *   is highlighted (i.e. the rectangle is shown),
    * - the node is relatively positioned: the rectangle is shown where the node
    *   would originally have been (because that's where the relative positioning
    *   is calculated from),
    * - the node has no offset parent at all: the offsetParent rectangle is
    *   hidden.
    */
-  updateOffsetParent: function () {
+  updateOffsetParent() {
     // Get the offsetParent, if any.
     this.offsetParent = getOffsetParent(this.currentNode);
     // And the offsetParent quads.
     this.parentQuads = getAdjustedQuads(
         this.win, this.offsetParent.element, "padding");
 
     let el = this.getElement("offset-parent");
 
@@ -575,51 +572,51 @@ GeometryEditorHighlighter.prototype = ex
       }
     }
 
     if (isHighlighted) {
       el.removeAttribute("hidden");
     } else {
       el.setAttribute("hidden", "true");
     }
-  },
+  }
 
-  updateCurrentNode: function () {
+  updateCurrentNode() {
     let box = this.getElement("current-node");
     let {p1, p2, p3, p4} = this.currentQuads.margin[0];
     let attr = p1.x + "," + p1.y + " " +
                p2.x + "," + p2.y + " " +
                p3.x + "," + p3.y + " " +
                p4.x + "," + p4.y;
     box.setAttribute("points", attr);
     box.removeAttribute("hidden");
-  },
+  }
 
-  _hide: function () {
+  _hide() {
     setIgnoreLayoutChanges(true);
 
     this.getElement("root").setAttribute("hidden", "true");
     this.getElement("current-node").setAttribute("hidden", "true");
     this.getElement("offset-parent").setAttribute("hidden", "true");
     this.hideArrows();
 
     this.definedProperties.clear();
 
     setIgnoreLayoutChanges(false, this.highlighterEnv.document.documentElement);
-  },
+  }
 
-  hideArrows: function () {
+  hideArrows() {
     for (let side of GeoProp.SIDES) {
       this.getElement("arrow-" + side).setAttribute("hidden", "true");
       this.getElement("label-" + side).setAttribute("hidden", "true");
       this.getElement("handler-" + side).setAttribute("hidden", "true");
     }
-  },
+  }
 
-  updateArrows: function () {
+  updateArrows() {
     this.hideArrows();
 
     // Position arrows always end at the node's margin box.
     let marginBox = this.currentQuads.margin[0].bounds;
 
     // Position the side arrows which need to be visible.
     // Arrows always start at the offsetParent edge, and end at the middle
     // position of the node's margin edge.
@@ -666,19 +663,19 @@ GeometryEditorHighlighter.prototype = ex
       let mainAxisStartPos = getSideArrowStartPos(side);
       let mainAxisEndPos = marginBox[side];
       let crossAxisPos = marginBox[GeoProp.crossAxisStart(side)] +
                          marginBox[GeoProp.crossAxisSize(side)] / 2;
 
       this.updateArrow(side, mainAxisStartPos, mainAxisEndPos, crossAxisPos,
                        sideProp.cssRule.style.getProper