summaryrefslogtreecommitdiffstats
path: root/browser/components/sidebar
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/sidebar')
-rw-r--r--browser/components/sidebar/browser-sidebar.js744
-rw-r--r--browser/components/sidebar/jar.mn9
-rw-r--r--browser/components/sidebar/sidebar-history.html34
-rw-r--r--browser/components/sidebar/sidebar-history.mjs201
-rw-r--r--browser/components/sidebar/sidebar-launcher.css34
-rw-r--r--browser/components/sidebar/sidebar-launcher.mjs169
-rw-r--r--browser/components/sidebar/sidebar-page.mjs45
-rw-r--r--browser/components/sidebar/sidebar-syncedtabs.html45
-rw-r--r--browser/components/sidebar/sidebar-syncedtabs.mjs191
-rw-r--r--browser/components/sidebar/sidebar.css27
-rw-r--r--browser/components/sidebar/sidebar.ftl26
11 files changed, 1525 insertions, 0 deletions
diff --git a/browser/components/sidebar/browser-sidebar.js b/browser/components/sidebar/browser-sidebar.js
new file mode 100644
index 0000000000..55664f8cfc
--- /dev/null
+++ b/browser/components/sidebar/browser-sidebar.js
@@ -0,0 +1,744 @@
+/* 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/. */
+
+/**
+ * SidebarUI controls showing and hiding the browser sidebar.
+ */
+var SidebarUI = {
+ get sidebars() {
+ if (this._sidebars) {
+ return this._sidebars;
+ }
+
+ function makeSidebar({ elementId, ...rest }) {
+ return {
+ get sourceL10nEl() {
+ return document.getElementById(elementId);
+ },
+ get title() {
+ return document.getElementById(elementId).getAttribute("label");
+ },
+ ...rest,
+ };
+ }
+
+ return (this._sidebars = new Map([
+ [
+ "viewBookmarksSidebar",
+ makeSidebar({
+ elementId: "sidebar-switcher-bookmarks",
+ url: "chrome://browser/content/places/bookmarksSidebar.xhtml",
+ menuId: "menu_bookmarksSidebar",
+ }),
+ ],
+ [
+ "viewHistorySidebar",
+ makeSidebar({
+ elementId: "sidebar-switcher-history",
+ url: this.sidebarRevampEnabled
+ ? "chrome://browser/content/sidebar/sidebar-history.html"
+ : "chrome://browser/content/places/historySidebar.xhtml",
+ menuId: "menu_historySidebar",
+ triggerButtonId: "appMenuViewHistorySidebar",
+ }),
+ ],
+ [
+ "viewTabsSidebar",
+ makeSidebar({
+ elementId: "sidebar-switcher-tabs",
+ url: this.sidebarRevampEnabled
+ ? "chrome://browser/content/sidebar/sidebar-syncedtabs.html"
+ : "chrome://browser/content/syncedtabs/sidebar.xhtml",
+ menuId: "menu_tabsSidebar",
+ }),
+ ],
+ [
+ "viewMegalistSidebar",
+ makeSidebar({
+ elementId: "sidebar-switcher-megalist",
+ url: "chrome://global/content/megalist/megalist.html",
+ menuId: "menu_megalistSidebar",
+ }),
+ ],
+ ]));
+ },
+
+ // Avoid getting the browser element from init() to avoid triggering the
+ // <browser> constructor during startup if the sidebar is hidden.
+ get browser() {
+ if (this._browser) {
+ return this._browser;
+ }
+ return (this._browser = document.getElementById("sidebar"));
+ },
+ POSITION_START_PREF: "sidebar.position_start",
+ DEFAULT_SIDEBAR_ID: "viewBookmarksSidebar",
+
+ // lastOpenedId is set in show() but unlike currentID it's not cleared out on hide
+ // and isn't persisted across windows
+ lastOpenedId: null,
+
+ _box: null,
+ // The constructor of this label accesses the browser element due to the
+ // control="sidebar" attribute, so avoid getting this label during startup.
+ get _title() {
+ if (this.__title) {
+ return this.__title;
+ }
+ return (this.__title = document.getElementById("sidebar-title"));
+ },
+ _splitter: null,
+ _reversePositionButton: null,
+ _switcherPanel: null,
+ _switcherTarget: null,
+ _switcherArrow: null,
+ _inited: false,
+
+ /**
+ * @type {MutationObserver | null}
+ */
+ _observer: null,
+
+ _initDeferred: Promise.withResolvers(),
+
+ get promiseInitialized() {
+ return this._initDeferred.promise;
+ },
+
+ get initialized() {
+ return this._inited;
+ },
+
+ async init() {
+ this._box = document.getElementById("sidebar-box");
+ this._splitter = document.getElementById("sidebar-splitter");
+ this._reversePositionButton = document.getElementById(
+ "sidebar-reverse-position"
+ );
+ this._switcherPanel = document.getElementById("sidebarMenu-popup");
+ this._switcherTarget = document.getElementById("sidebar-switcher-target");
+ this._switcherArrow = document.getElementById("sidebar-switcher-arrow");
+
+ if (this.sidebarRevampEnabled) {
+ await import("chrome://browser/content/sidebar/sidebar-launcher.mjs");
+ document.getElementById("sidebar-launcher").hidden = false;
+ document.getElementById("sidebar-header").hidden = true;
+ } else {
+ this._switcherTarget.addEventListener("command", () => {
+ this.toggleSwitcherPanel();
+ });
+ this._switcherTarget.addEventListener("keydown", event => {
+ this.handleKeydown(event);
+ });
+ }
+
+ this._inited = true;
+
+ Services.obs.addObserver(this, "intl:app-locales-changed");
+
+ this._initDeferred.resolve();
+ },
+
+ toggleMegalistItem() {
+ const sideMenuPopupItem = document.getElementById(
+ "sidebar-switcher-megalist"
+ );
+ sideMenuPopupItem.style.display = Services.prefs.getBoolPref(
+ "browser.megalist.enabled",
+ false
+ )
+ ? ""
+ : "none";
+ },
+
+ setMegalistMenubarVisibility(aEvent) {
+ const popup = aEvent.target;
+ if (popup != aEvent.currentTarget) {
+ return;
+ }
+
+ // Show the megalist item if enabled
+ const megalistItem = popup.querySelector("#menu_megalistSidebar");
+ megalistItem.hidden = !Services.prefs.getBoolPref(
+ "browser.megalist.enabled",
+ false
+ );
+ },
+
+ uninit() {
+ // If this is the last browser window, persist various values that should be
+ // remembered for after a restart / reopening a browser window.
+ let enumerator = Services.wm.getEnumerator("navigator:browser");
+ if (!enumerator.hasMoreElements()) {
+ let xulStore = Services.xulStore;
+ xulStore.persist(this._box, "sidebarcommand");
+
+ if (this._box.hasAttribute("positionend")) {
+ xulStore.persist(this._box, "positionend");
+ } else {
+ xulStore.removeValue(
+ document.documentURI,
+ "sidebar-box",
+ "positionend"
+ );
+ }
+ if (this._box.hasAttribute("checked")) {
+ xulStore.persist(this._box, "checked");
+ } else {
+ xulStore.removeValue(document.documentURI, "sidebar-box", "checked");
+ }
+
+ xulStore.persist(this._box, "style");
+ xulStore.persist(this._title, "value");
+ }
+
+ Services.obs.removeObserver(this, "intl:app-locales-changed");
+
+ if (this._observer) {
+ this._observer.disconnect();
+ this._observer = null;
+ }
+ },
+
+ /**
+ * The handler for Services.obs.addObserver.
+ */
+ observe(_subject, topic, _data) {
+ switch (topic) {
+ case "intl:app-locales-changed": {
+ if (this.isOpen) {
+ // The <tree> component used in history and bookmarks, but it does not
+ // support live switching the app locale. Reload the entire sidebar to
+ // invalidate any old text.
+ this.hide();
+ this.showInitially(this.lastOpenedId);
+ break;
+ }
+ }
+ }
+ },
+
+ /**
+ * Ensure the title stays in sync with the source element, which updates for
+ * l10n changes.
+ *
+ * @param {HTMLElement} [element]
+ */
+ observeTitleChanges(element) {
+ if (!element) {
+ return;
+ }
+ let observer = this._observer;
+ if (!observer) {
+ observer = new MutationObserver(() => {
+ this.title = this.sidebars.get(this.lastOpenedId).title;
+ });
+ // Re-use the observer.
+ this._observer = observer;
+ }
+ observer.disconnect();
+ observer.observe(element, {
+ attributes: true,
+ attributeFilter: ["label"],
+ });
+ },
+
+ /**
+ * Opens the switcher panel if it's closed, or closes it if it's open.
+ */
+ toggleSwitcherPanel() {
+ if (
+ this._switcherPanel.state == "open" ||
+ this._switcherPanel.state == "showing"
+ ) {
+ this.hideSwitcherPanel();
+ } else if (this._switcherPanel.state == "closed") {
+ this.showSwitcherPanel();
+ }
+ },
+
+ /**
+ * Handles keydown on the the switcherTarget button
+ *
+ * @param {Event} event
+ */
+ handleKeydown(event) {
+ switch (event.key) {
+ case "Enter":
+ case " ": {
+ this.toggleSwitcherPanel();
+ event.stopPropagation();
+ event.preventDefault();
+ break;
+ }
+ case "Escape": {
+ this.hideSwitcherPanel();
+ event.stopPropagation();
+ event.preventDefault();
+ break;
+ }
+ }
+ },
+
+ hideSwitcherPanel() {
+ this._switcherPanel.hidePopup();
+ },
+
+ showSwitcherPanel() {
+ this.toggleMegalistItem();
+ this._switcherPanel.addEventListener(
+ "popuphiding",
+ () => {
+ this._switcherTarget.classList.remove("active");
+ this._switcherTarget.setAttribute("aria-expanded", false);
+ },
+ { once: true }
+ );
+
+ // Combine start/end position with ltr/rtl to set the label in the popup appropriately.
+ let label =
+ this._positionStart == RTL_UI
+ ? gNavigatorBundle.getString("sidebar.moveToLeft")
+ : gNavigatorBundle.getString("sidebar.moveToRight");
+ this._reversePositionButton.setAttribute("label", label);
+
+ // Open the sidebar switcher popup, anchored off the switcher toggle
+ this._switcherPanel.hidden = false;
+ this._switcherPanel.openPopup(this._switcherTarget);
+
+ this._switcherTarget.classList.add("active");
+ this._switcherTarget.setAttribute("aria-expanded", true);
+ },
+
+ updateShortcut({ keyId }) {
+ let menuitem = this._switcherPanel?.querySelector(`[key="${keyId}"]`);
+ if (!menuitem) {
+ // If the menu item doesn't exist yet then the accel text will be set correctly
+ // upon creation so there's nothing to do now.
+ return;
+ }
+ menuitem.removeAttribute("acceltext");
+ },
+
+ /**
+ * Change the pref that will trigger a call to setPosition
+ */
+ reversePosition() {
+ Services.prefs.setBoolPref(this.POSITION_START_PREF, !this._positionStart);
+ },
+
+ /**
+ * Read the positioning pref and position the sidebar and the splitter
+ * appropriately within the browser container.
+ */
+ setPosition() {
+ // First reset all ordinals to match DOM ordering.
+ let browser = document.getElementById("browser");
+ [...browser.children].forEach((node, i) => {
+ node.style.order = i + 1;
+ });
+ let sidebarLauncher = document.querySelector("sidebar-launcher");
+
+ if (!this._positionStart) {
+ // DOM ordering is: sidebar-launcher | sidebar-box | splitter | appcontent |
+ // Want to display as: | appcontent | splitter | sidebar-box | sidebar-launcher
+ // So we just swap box and appcontent ordering and move sidebar-launcher to the end
+ let appcontent = document.getElementById("appcontent");
+ let boxOrdinal = this._box.style.order;
+ this._box.style.order = appcontent.style.order;
+
+ appcontent.style.order = boxOrdinal;
+ // the launcher should be on the right of the sidebar-box
+ sidebarLauncher.style.order = parseInt(this._box.style.order) + 1;
+ // Indicate we've switched ordering to the box
+ this._box.setAttribute("positionend", true);
+ sidebarLauncher.setAttribute("positionend", true);
+ } else {
+ this._box.removeAttribute("positionend");
+ sidebarLauncher.removeAttribute("positionend");
+ }
+
+ this.hideSwitcherPanel();
+
+ let content = SidebarUI.browser.contentWindow;
+ if (content && content.updatePosition) {
+ content.updatePosition();
+ }
+ },
+
+ /**
+ * Try and adopt the status of the sidebar from another window.
+ *
+ * @param {Window} sourceWindow - Window to use as a source for sidebar status.
+ * @returns {boolean} true if we adopted the state, or false if the caller should
+ * initialize the state itself.
+ */
+ adoptFromWindow(sourceWindow) {
+ // If the opener had a sidebar, open the same sidebar in our window.
+ // The opener can be the hidden window too, if we're coming from the state
+ // where no windows are open, and the hidden window has no sidebar box.
+ let sourceUI = sourceWindow.SidebarUI;
+ if (!sourceUI || !sourceUI._box) {
+ // no source UI or no _box means we also can't adopt the state.
+ return false;
+ }
+
+ // Set sidebar command even if hidden, so that we keep the same sidebar
+ // even if it's currently closed.
+ let commandID = sourceUI._box.getAttribute("sidebarcommand");
+ if (commandID) {
+ this._box.setAttribute("sidebarcommand", commandID);
+ }
+
+ if (sourceUI._box.hidden) {
+ // just hidden means we have adopted the hidden state.
+ return true;
+ }
+
+ // dynamically generated sidebars will fail this check, but we still
+ // consider it adopted.
+ if (!this.sidebars.has(commandID)) {
+ return true;
+ }
+
+ this._box.style.width = sourceUI._box.getBoundingClientRect().width + "px";
+ this.showInitially(commandID);
+
+ return true;
+ },
+
+ windowPrivacyMatches(w1, w2) {
+ return (
+ PrivateBrowsingUtils.isWindowPrivate(w1) ===
+ PrivateBrowsingUtils.isWindowPrivate(w2)
+ );
+ },
+
+ /**
+ * If loading a sidebar was delayed on startup, start the load now.
+ */
+ startDelayedLoad() {
+ let sourceWindow = window.opener;
+ // No source window means this is the initial window. If we're being
+ // opened from another window, check that it is one we might open a sidebar
+ // for.
+ if (sourceWindow) {
+ if (
+ sourceWindow.closed ||
+ sourceWindow.location.protocol != "chrome:" ||
+ !this.windowPrivacyMatches(sourceWindow, window)
+ ) {
+ return;
+ }
+ // Try to adopt the sidebar state from the source window
+ if (this.adoptFromWindow(sourceWindow)) {
+ return;
+ }
+ }
+
+ // If we're not adopting settings from a parent window, set them now.
+ let wasOpen = this._box.getAttribute("checked");
+ if (!wasOpen) {
+ return;
+ }
+
+ let commandID = this._box.getAttribute("sidebarcommand");
+ if (commandID && this.sidebars.has(commandID)) {
+ this.showInitially(commandID);
+ } else {
+ this._box.removeAttribute("checked");
+ // Remove the |sidebarcommand| attribute, because the element it
+ // refers to no longer exists, so we should assume this sidebar
+ // panel has been uninstalled. (249883)
+ // We use setAttribute rather than removeAttribute so it persists
+ // correctly.
+ this._box.setAttribute("sidebarcommand", "");
+ // On a startup in which the startup cache was invalidated (e.g. app update)
+ // extensions will not be started prior to delayedLoad, thus the
+ // sidebarcommand element will not exist yet. Store the commandID so
+ // extensions may reopen if necessary. A startup cache invalidation
+ // can be forced (for testing) by deleting compatibility.ini from the
+ // profile.
+ this.lastOpenedId = commandID;
+ }
+ },
+
+ /**
+ * Fire a "SidebarShown" event on the sidebar to give any interested parties
+ * a chance to update the button or whatever.
+ */
+ _fireShowEvent() {
+ let event = new CustomEvent("SidebarShown", { bubbles: true });
+ this._switcherTarget.dispatchEvent(event);
+ },
+
+ /**
+ * Fire a "SidebarFocused" event on the sidebar's |window| to give the sidebar
+ * a chance to adjust focus as needed. An additional event is needed, because
+ * we don't want to focus the sidebar when it's opened on startup or in a new
+ * window, only when the user opens the sidebar.
+ */
+ _fireFocusedEvent() {
+ let event = new CustomEvent("SidebarFocused", { bubbles: true });
+ this.browser.contentWindow.dispatchEvent(event);
+ },
+
+ /**
+ * True if the sidebar is currently open.
+ */
+ get isOpen() {
+ return !this._box.hidden;
+ },
+
+ /**
+ * The ID of the current sidebar.
+ */
+ get currentID() {
+ return this.isOpen ? this._box.getAttribute("sidebarcommand") : "";
+ },
+
+ get title() {
+ return this._title.value;
+ },
+
+ set title(value) {
+ this._title.value = value;
+ },
+
+ /**
+ * Toggle the visibility of the sidebar. If the sidebar is hidden or is open
+ * with a different commandID, then the sidebar will be opened using the
+ * specified commandID. Otherwise the sidebar will be hidden.
+ *
+ * @param {string} commandID ID of the sidebar.
+ * @param {DOMNode} [triggerNode] Node, usually a button, that triggered the
+ * visibility toggling of the sidebar.
+ * @returns {Promise}
+ */
+ toggle(commandID = this.lastOpenedId, triggerNode) {
+ if (
+ CustomizationHandler.isCustomizing() ||
+ CustomizationHandler.isExitingCustomizeMode
+ ) {
+ return Promise.resolve();
+ }
+ // First priority for a default value is this.lastOpenedId which is set during show()
+ // and not reset in hide(), unlike currentID. If show() hasn't been called and we don't
+ // have a persisted command either, or the command doesn't exist anymore, then
+ // fallback to a default sidebar.
+ if (!commandID) {
+ commandID = this._box.getAttribute("sidebarcommand");
+ }
+ if (!commandID || !this.sidebars.has(commandID)) {
+ commandID = this.DEFAULT_SIDEBAR_ID;
+ }
+
+ if (this.isOpen && commandID == this.currentID) {
+ this.hide(triggerNode);
+ return Promise.resolve();
+ }
+ return this.show(commandID, triggerNode);
+ },
+
+ _loadSidebarExtension(commandID) {
+ let sidebar = this.sidebars.get(commandID);
+ let { extensionId } = sidebar;
+ if (extensionId) {
+ SidebarUI.browser.contentWindow.loadPanel(
+ extensionId,
+ sidebar.panel,
+ sidebar.browserStyle
+ );
+ }
+ },
+
+ /**
+ * Show the sidebar.
+ *
+ * This wraps the internal method, including a ping to telemetry.
+ *
+ * @param {string} commandID ID of the sidebar to use.
+ * @param {DOMNode} [triggerNode] Node, usually a button, that triggered the
+ * showing of the sidebar.
+ * @returns {Promise<boolean>}
+ */
+ async show(commandID, triggerNode) {
+ let panelType = commandID.substring(4, commandID.length - 7);
+ Services.telemetry.keyedScalarAdd("sidebar.opened", panelType, 1);
+
+ // Extensions without private window access wont be in the
+ // sidebars map.
+ if (!this.sidebars.has(commandID)) {
+ return false;
+ }
+ return this._show(commandID).then(() => {
+ this._loadSidebarExtension(commandID);
+
+ if (triggerNode) {
+ updateToggleControlLabel(triggerNode);
+ }
+
+ this._fireFocusedEvent();
+ return true;
+ });
+ },
+
+ /**
+ * Show the sidebar, without firing the focused event or logging telemetry.
+ * This is intended to be used when the sidebar is opened automatically
+ * when a window opens (not triggered by user interaction).
+ *
+ * @param {string} commandID ID of the sidebar.
+ * @returns {Promise<boolean>}
+ */
+ async showInitially(commandID) {
+ let panelType = commandID.substring(4, commandID.length - 7);
+ Services.telemetry.keyedScalarAdd("sidebar.opened", panelType, 1);
+
+ // Extensions without private window access wont be in the
+ // sidebars map.
+ if (!this.sidebars.has(commandID)) {
+ return false;
+ }
+ return this._show(commandID).then(() => {
+ this._loadSidebarExtension(commandID);
+ return true;
+ });
+ },
+
+ /**
+ * Implementation for show. Also used internally for sidebars that are shown
+ * when a window is opened and we don't want to ping telemetry.
+ *
+ * @param {string} commandID ID of the sidebar.
+ * @returns {Promise<void>}
+ */
+ _show(commandID) {
+ return new Promise(resolve => {
+ if (this.sidebarRevampEnabled) {
+ this._box.dispatchEvent(
+ new CustomEvent("sidebar-show", { detail: { viewId: commandID } })
+ );
+ } else {
+ this.hideSwitcherPanel();
+ }
+
+ this.selectMenuItem(commandID);
+ this._box.hidden = this._splitter.hidden = false;
+ // sets the sidebar to the left or right, based on a pref
+ this.setPosition();
+
+ this._box.setAttribute("checked", "true");
+ this._box.setAttribute("sidebarcommand", commandID);
+
+ let { url, title, sourceL10nEl } = this.sidebars.get(commandID);
+
+ // use to live update <tree> elements if the locale changes
+ this.lastOpenedId = commandID;
+ this.title = title;
+ // Keep the title element in the switcher in sync with any l10n changes.
+ this.observeTitleChanges(sourceL10nEl);
+
+ this.browser.setAttribute("src", url); // kick off async load
+
+ if (this.browser.contentDocument.location.href != url) {
+ this.browser.addEventListener(
+ "load",
+ () => {
+ // We're handling the 'load' event before it bubbles up to the usual
+ // (non-capturing) event handlers. Let it bubble up before resolving.
+ setTimeout(() => {
+ resolve();
+
+ // Now that the currentId is updated, fire a show event.
+ this._fireShowEvent();
+ }, 0);
+ },
+ { capture: true, once: true }
+ );
+ } else {
+ resolve();
+
+ // Now that the currentId is updated, fire a show event.
+ this._fireShowEvent();
+ }
+ });
+ },
+
+ /**
+ * Hide the sidebar.
+ *
+ * @param {DOMNode} [triggerNode] Node, usually a button, that triggered the
+ * hiding of the sidebar.
+ */
+ hide(triggerNode) {
+ if (!this.isOpen) {
+ return;
+ }
+
+ this.hideSwitcherPanel();
+ if (this.sidebarRevampEnabled) {
+ this._box.dispatchEvent(new CustomEvent("sidebar-hide"));
+ }
+ this.selectMenuItem("");
+
+ // Replace the document currently displayed in the sidebar with about:blank
+ // so that we can free memory by unloading the page. We need to explicitly
+ // create a new content viewer because the old one doesn't get destroyed
+ // until about:blank has loaded (which does not happen as long as the
+ // element is hidden).
+ this.browser.setAttribute("src", "about:blank");
+ this.browser.docShell.createAboutBlankDocumentViewer(null, null);
+
+ this._box.removeAttribute("checked");
+ this._box.hidden = this._splitter.hidden = true;
+
+ let selBrowser = gBrowser.selectedBrowser;
+ selBrowser.focus();
+ if (triggerNode) {
+ updateToggleControlLabel(triggerNode);
+ }
+ },
+
+ /**
+ * Sets the checked state only on the menu items of the specified sidebar, or
+ * none if the argument is an empty string.
+ */
+ selectMenuItem(commandID) {
+ for (let [id, { menuId, triggerButtonId }] of this.sidebars) {
+ let menu = document.getElementById(menuId);
+ let triggerbutton =
+ triggerButtonId && document.getElementById(triggerButtonId);
+ if (id == commandID) {
+ menu.setAttribute("checked", "true");
+ if (triggerbutton) {
+ triggerbutton.setAttribute("checked", "true");
+ updateToggleControlLabel(triggerbutton);
+ }
+ } else {
+ menu.removeAttribute("checked");
+ if (triggerbutton) {
+ triggerbutton.removeAttribute("checked");
+ updateToggleControlLabel(triggerbutton);
+ }
+ }
+ }
+ },
+};
+
+// Add getters related to the position here, since we will want them
+// available for both startDelayedLoad and init.
+XPCOMUtils.defineLazyPreferenceGetter(
+ SidebarUI,
+ "_positionStart",
+ SidebarUI.POSITION_START_PREF,
+ true,
+ SidebarUI.setPosition.bind(SidebarUI)
+);
+XPCOMUtils.defineLazyPreferenceGetter(
+ SidebarUI,
+ "sidebarRevampEnabled",
+ "sidebar.revamp",
+ false
+);
diff --git a/browser/components/sidebar/jar.mn b/browser/components/sidebar/jar.mn
index c3d7f0cbcf..8a7071ca72 100644
--- a/browser/components/sidebar/jar.mn
+++ b/browser/components/sidebar/jar.mn
@@ -3,3 +3,12 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
browser.jar:
+ content/browser/sidebar/browser-sidebar.js
+ content/browser/sidebar/sidebar-launcher.css
+ content/browser/sidebar/sidebar-launcher.mjs
+ content/browser/sidebar/sidebar-history.html
+ content/browser/sidebar/sidebar-history.mjs
+ content/browser/sidebar/sidebar-page.mjs
+ content/browser/sidebar/sidebar-syncedtabs.html
+ content/browser/sidebar/sidebar-syncedtabs.mjs
+ content/browser/sidebar/sidebar.css
diff --git a/browser/components/sidebar/sidebar-history.html b/browser/components/sidebar/sidebar-history.html
new file mode 100644
index 0000000000..f1df5c507a
--- /dev/null
+++ b/browser/components/sidebar/sidebar-history.html
@@ -0,0 +1,34 @@
+<!-- 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>
+ <head>
+ <meta charset="utf-8" />
+ <meta
+ http-equiv="Content-Security-Policy"
+ content="default-src resource: chrome:; object-src 'none'; img-src data: chrome:;"
+ />
+ <meta name="color-scheme" content="light dark" />
+ <title data-l10n-id="firefoxview-page-title"></title>
+ <link rel="localization" href="branding/brand.ftl" />
+ <link rel="localization" href="toolkit/branding/accounts.ftl" />
+ <link rel="localization" href="browser/firefoxView.ftl" />
+ <link rel="localization" href="preview/sidebar.ftl" />
+ <link rel="localization" href="toolkit/branding/brandings.ftl" />
+ <link
+ rel="stylesheet"
+ href="chrome://browser/content/sidebar/sidebar.css"
+ />
+ <script
+ type="module"
+ src="chrome://browser/content/sidebar/sidebar-history.mjs"
+ ></script>
+ <script src="chrome://browser/content/contentTheme.js"></script>
+ </head>
+
+ <body>
+ <sidebar-history />
+ </body>
+</html>
diff --git a/browser/components/sidebar/sidebar-history.mjs b/browser/components/sidebar/sidebar-history.mjs
new file mode 100644
index 0000000000..6c662b2c6f
--- /dev/null
+++ b/browser/components/sidebar/sidebar-history.mjs
@@ -0,0 +1,201 @@
+/* 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 { html, when } from "chrome://global/content/vendor/lit.all.mjs";
+
+import { SidebarPage } from "./sidebar-page.mjs";
+
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://browser/content/firefoxview/fxview-search-textbox.mjs";
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://browser/content/firefoxview/fxview-tab-list.mjs";
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://global/content/elements/moz-card.mjs";
+import { HistoryController } from "chrome://browser/content/firefoxview/HistoryController.mjs";
+import { navigateToLink } from "chrome://browser/content/firefoxview/helpers.mjs";
+
+const NEVER_REMEMBER_HISTORY_PREF = "browser.privatebrowsing.autostart";
+
+export class SidebarHistory extends SidebarPage {
+ constructor() {
+ super();
+ this._started = false;
+ // Setting maxTabsLength to -1 for no max
+ this.maxTabsLength = -1;
+ }
+
+ controller = new HistoryController(this, {
+ component: "sidebar",
+ });
+
+ connectedCallback() {
+ super.connectedCallback();
+ this.controller.updateAllHistoryItems();
+ }
+
+ onPrimaryAction(e) {
+ navigateToLink(e);
+ }
+
+ deleteFromHistory() {
+ this.controller.deleteFromHistory();
+ }
+
+ /**
+ * The template to use for cards-container.
+ */
+ get cardsTemplate() {
+ if (this.controller.searchResults) {
+ return this.#searchResultsTemplate();
+ } else if (this.controller.allHistoryItems.size) {
+ return this.#historyCardsTemplate();
+ }
+ return this.#emptyMessageTemplate();
+ }
+
+ #historyCardsTemplate() {
+ let cardsTemplate = [];
+ this.controller.historyMapByDate.forEach(historyItem => {
+ if (historyItem.items.length) {
+ let dateArg = JSON.stringify({ date: historyItem.items[0].time });
+ cardsTemplate.push(html`<moz-card
+ type="accordion"
+ data-l10n-attrs="heading"
+ data-l10n-id=${historyItem.l10nId}
+ data-l10n-args=${dateArg}
+ >
+ <div>
+ <fxview-tab-list
+ compactRows
+ class="with-context-menu"
+ maxTabsLength=${this.maxTabsLength}
+ .tabItems=${this.getTabItems(historyItem.items)}
+ @fxview-tab-list-primary-action=${this.onPrimaryAction}
+ .updatesPaused=${false}
+ >
+ </fxview-tab-list>
+ </div>
+ </moz-card>`);
+ }
+ });
+ return cardsTemplate;
+ }
+
+ #emptyMessageTemplate() {
+ let descriptionHeader;
+ let descriptionLabels;
+ let descriptionLink;
+ if (Services.prefs.getBoolPref(NEVER_REMEMBER_HISTORY_PREF, false)) {
+ // History pref set to never remember history
+ descriptionHeader = "firefoxview-dont-remember-history-empty-header";
+ descriptionLabels = [
+ "firefoxview-dont-remember-history-empty-description",
+ "firefoxview-dont-remember-history-empty-description-two",
+ ];
+ descriptionLink = {
+ url: "about:preferences#privacy",
+ name: "history-settings-url-two",
+ };
+ } else {
+ descriptionHeader = "firefoxview-history-empty-header";
+ descriptionLabels = [
+ "firefoxview-history-empty-description",
+ "firefoxview-history-empty-description-two",
+ ];
+ descriptionLink = {
+ url: "about:preferences#privacy",
+ name: "history-settings-url",
+ };
+ }
+ return html`
+ <fxview-empty-state
+ headerLabel=${descriptionHeader}
+ .descriptionLabels=${descriptionLabels}
+ .descriptionLink=${descriptionLink}
+ class="empty-state history"
+ ?isSelectedTab=${this.selectedTab}
+ mainImageUrl="chrome://browser/content/firefoxview/history-empty.svg"
+ >
+ </fxview-empty-state>
+ `;
+ }
+
+ #searchResultsTemplate() {
+ return html` <moz-card
+ data-l10n-attrs="heading"
+ data-l10n-id="sidebar-search-results-header"
+ data-l10n-args=${JSON.stringify({
+ query: this.controller.searchQuery,
+ })}
+ >
+ <div>
+ ${when(
+ this.controller.searchResults.length,
+ () =>
+ html`<h3
+ slot="secondary-header"
+ data-l10n-id="firefoxview-search-results-count"
+ data-l10n-args="${JSON.stringify({
+ count: this.controller.searchResults.length,
+ })}"
+ ></h3>`
+ )}
+ <fxview-tab-list
+ compactRows
+ maxTabsLength="-1"
+ .searchQuery=${this.controller.searchQuery}
+ .tabItems=${this.getTabItems(this.controller.searchResults)}
+ @fxview-tab-list-primary-action=${this.onPrimaryAction}
+ .updatesPaused=${false}
+ >
+ </fxview-tab-list>
+ </div>
+ </moz-card>`;
+ }
+
+ async onChangeSortOption(e) {
+ await this.controller.onChangeSortOption(e);
+ }
+
+ async onSearchQuery(e) {
+ await this.controller.onSearchQuery(e);
+ }
+
+ getTabItems(items) {
+ return items.map(item => ({
+ ...item,
+ secondaryL10nId: null,
+ secondaryL10nArgs: null,
+ }));
+ }
+
+ render() {
+ return html`
+ ${this.stylesheet()}
+ <div class="container">
+ <div class="history-sort-option">
+ <div class="history-sort-option">
+ <fxview-search-textbox
+ data-l10n-id="firefoxview-search-text-box-history"
+ data-l10n-attrs="placeholder"
+ @fxview-search-textbox-query=${this.onSearchQuery}
+ .size=${15}
+ ></fxview-search-textbox>
+ </div>
+ </div>
+ ${this.cardsTemplate}
+ </div>
+ `;
+ }
+
+ willUpdate() {
+ if (this.controller.allHistoryItems.size) {
+ // onChangeSortOption() will update history data once it has been fetched
+ // from the API.
+ this.controller.createHistoryMaps();
+ }
+ }
+}
+
+customElements.define("sidebar-history", SidebarHistory);
diff --git a/browser/components/sidebar/sidebar-launcher.css b/browser/components/sidebar/sidebar-launcher.css
new file mode 100644
index 0000000000..b033a650b3
--- /dev/null
+++ b/browser/components/sidebar/sidebar-launcher.css
@@ -0,0 +1,34 @@
+/* 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 https://mozilla.org/MPL/2.0/. */
+
+.wrapper {
+ display: grid;
+ grid-template-rows: auto 1fr;
+ box-sizing: border-box;
+ height: 100%;
+ padding: var(--space-medium);
+ border-inline-end: 1px solid var(--chrome-content-separator-color);
+ background-color: var(--sidebar-background-color);
+ color: var(--sidebar-text-color);
+ :host([positionend]) & {
+ border-inline-start: 1px solid var(--chrome-content-separator-color);
+ border-inline-end: none;;
+ }
+}
+
+:host([positionend]) {
+ .wrapper {
+ border-inline-start: 1px solid var(--chrome-content-separator-color);
+ }
+}
+
+.actions-list {
+ display: flex;
+ flex-direction: column;
+ justify-content: end;
+}
+
+.icon-button::part(button) {
+ background-image: var(--action-icon);
+}
diff --git a/browser/components/sidebar/sidebar-launcher.mjs b/browser/components/sidebar/sidebar-launcher.mjs
new file mode 100644
index 0000000000..85eb94b6ca
--- /dev/null
+++ b/browser/components/sidebar/sidebar-launcher.mjs
@@ -0,0 +1,169 @@
+/* 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 {
+ html,
+ ifDefined,
+ styleMap,
+} from "chrome://global/content/vendor/lit.all.mjs";
+import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
+
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://global/content/elements/moz-button.mjs";
+
+/**
+ * Vertical strip attached to the launcher that provides an entry point
+ * to various sidebar panels.
+ *
+ */
+export default class SidebarLauncher extends MozLitElement {
+ static properties = {
+ topActions: { type: Array },
+ bottomActions: { type: Array },
+ selectedView: { type: String },
+ open: { type: Boolean },
+ };
+
+ constructor() {
+ super();
+ this.topActions = [
+ {
+ icon: `url("chrome://browser/skin/insights.svg")`,
+ view: null,
+ l10nId: "sidebar-launcher-insights",
+ },
+ ];
+
+ this.bottomActions = [
+ {
+ l10nId: "sidebar-menu-history",
+ icon: `url("chrome://browser/content/firefoxview/view-history.svg")`,
+ view: "viewHistorySidebar",
+ },
+ {
+ l10nId: "sidebar-menu-bookmarks",
+ icon: `url("chrome://browser/skin/bookmark-hollow.svg")`,
+ view: "viewBookmarksSidebar",
+ },
+ {
+ l10nId: "sidebar-menu-synced-tabs",
+ icon: `url("chrome://browser/skin/device-phone.svg")`,
+ view: "viewTabsSidebar",
+ },
+ ];
+
+ this.selectedView = window.SidebarUI.currentID;
+ this.open = window.SidebarUI.isOpen;
+ this.menuMutationObserver = new MutationObserver(() =>
+ this.#setExtensionItems()
+ );
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ this._sidebarBox = document.getElementById("sidebar-box");
+ this._sidebarBox.addEventListener("sidebar-show", this);
+ this._sidebarBox.addEventListener("sidebar-hide", this);
+ this._sidebarMenu = document.getElementById("viewSidebarMenu");
+
+ this.menuMutationObserver.observe(this._sidebarMenu, {
+ childList: true,
+ subtree: true,
+ });
+ this.#setExtensionItems();
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ this._sidebarBox.removeEventListener("sidebar-show", this);
+ this._sidebarBox.removeEventListener("sidebar-hide", this);
+ this.menuMutationObserver.disconnect();
+ }
+
+ getImageUrl(icon, targetURI) {
+ if (window.IS_STORYBOOK) {
+ return `chrome://global/skin/icons/defaultFavicon.svg`;
+ }
+ if (!icon) {
+ if (targetURI?.startsWith("moz-extension")) {
+ return "chrome://mozapps/skin/extensions/extension.svg";
+ }
+ return `chrome://global/skin/icons/defaultFavicon.svg`;
+ }
+ // If the icon is not for website (doesn't begin with http), we
+ // display it directly. Otherwise we go through the page-icon
+ // protocol to try to get a cached version. We don't load
+ // favicons directly.
+ if (icon.startsWith("http")) {
+ return `page-icon:${targetURI}`;
+ }
+ return icon;
+ }
+
+ #setExtensionItems() {
+ for (let item of this._sidebarMenu.children) {
+ if (item.id.endsWith("-sidebar-action")) {
+ this.topActions.push({
+ tooltiptext: item.label,
+ icon: item.style.getPropertyValue("--webextension-menuitem-image"),
+ view: item.id.slice("menubar_menu_".length),
+ });
+ }
+ }
+ }
+
+ handleEvent(e) {
+ switch (e.type) {
+ case "sidebar-show":
+ this.selectedView = e.detail.viewId;
+ this.open = true;
+ break;
+ case "sidebar-hide":
+ this.open = false;
+ break;
+ }
+ }
+
+ showView(e) {
+ let view = e.target.getAttribute("view");
+ window.SidebarUI.toggle(view);
+ }
+
+ buttonType(action) {
+ return this.open && action.view == this.selectedView
+ ? "icon"
+ : "icon ghost";
+ }
+
+ entrypointTemplate(action) {
+ return html`<moz-button
+ class="icon-button"
+ type=${this.buttonType(action)}
+ view=${action.view}
+ @click=${action.view ? this.showView : null}
+ title=${ifDefined(action.tooltiptext)}
+ data-l10n-id=${ifDefined(action.l10nId)}
+ style=${styleMap({ "--action-icon": action.icon })}
+ >
+ </moz-button>`;
+ }
+
+ render() {
+ return html`
+ <link
+ rel="stylesheet"
+ href="chrome://browser/content/sidebar/sidebar-launcher.css"
+ />
+ <div class="wrapper">
+ <div class="top-actions actions-list">
+ ${this.topActions.map(action => this.entrypointTemplate(action))}
+ </div>
+ <div class="bottom-actions actions-list">
+ ${this.bottomActions.map(action => this.entrypointTemplate(action))}
+ </div>
+ </div>
+ `;
+ }
+}
+customElements.define("sidebar-launcher", SidebarLauncher);
diff --git a/browser/components/sidebar/sidebar-page.mjs b/browser/components/sidebar/sidebar-page.mjs
new file mode 100644
index 0000000000..157298a561
--- /dev/null
+++ b/browser/components/sidebar/sidebar-page.mjs
@@ -0,0 +1,45 @@
+/* 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 { MozLitElement } from "chrome://global/content/lit-utils.mjs";
+import { html } from "chrome://global/content/vendor/lit.all.mjs";
+
+export class SidebarPage extends MozLitElement {
+ constructor() {
+ super();
+ this.clearDocument = this.clearDocument.bind(this);
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ this.ownerGlobal.addEventListener("beforeunload", this.clearDocument);
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ this.ownerGlobal.removeEventListener("beforeunload", this.clearDocument);
+ }
+
+ /**
+ * Clear out the document so the disconnectedCallback() will trigger properly
+ * and all of the custom elements can cleanup.
+ */
+ clearDocument() {
+ this.ownerGlobal.document.body.textContent = "";
+ }
+
+ /**
+ * The common stylesheet for all sidebar pages.
+ *
+ * @returns {TemplateResult}
+ */
+ stylesheet() {
+ return html`
+ <link
+ rel="stylesheet"
+ href="chrome://browser/content/sidebar/sidebar.css"
+ />
+ `;
+ }
+}
diff --git a/browser/components/sidebar/sidebar-syncedtabs.html b/browser/components/sidebar/sidebar-syncedtabs.html
new file mode 100644
index 0000000000..6c4874b9ea
--- /dev/null
+++ b/browser/components/sidebar/sidebar-syncedtabs.html
@@ -0,0 +1,45 @@
+<!-- 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>
+ <head>
+ <meta charset="utf-8" />
+ <meta
+ http-equiv="Content-Security-Policy"
+ content="default-src resource: chrome:; object-src 'none'; img-src data: chrome:;"
+ />
+ <meta name="color-scheme" content="light dark" />
+ <link rel="localization" href="branding/brand.ftl" />
+ <link rel="localization" href="browser/firefoxView.ftl" />
+ <link rel="localization" href="toolkit/branding/accounts.ftl" />
+ <link rel="localization" href="toolkit/branding/brandings.ftl" />
+ <link rel="stylesheet" href="chrome://global/skin/global.css" />
+ <script
+ type="module"
+ src="chrome://global/content/elements/moz-card.mjs"
+ ></script>
+ <script
+ type="module"
+ src="chrome://browser/content/firefoxview/fxview-empty-state.mjs"
+ ></script>
+ <script
+ type="module"
+ src="chrome://browser/content/firefoxview/fxview-search-textbox.mjs"
+ ></script>
+ <script
+ type="module"
+ src="chrome://browser/content/firefoxview/fxview-tab-list.mjs"
+ ></script>
+ <script
+ type="module"
+ src="chrome://browser/content/sidebar/sidebar-syncedtabs.mjs"
+ ></script>
+ <script src="chrome://browser/content/contentTheme.js"></script>
+ </head>
+
+ <body>
+ <sidebar-syncedtabs />
+ </body>
+</html>
diff --git a/browser/components/sidebar/sidebar-syncedtabs.mjs b/browser/components/sidebar/sidebar-syncedtabs.mjs
new file mode 100644
index 0000000000..4c3bd9dc46
--- /dev/null
+++ b/browser/components/sidebar/sidebar-syncedtabs.mjs
@@ -0,0 +1,191 @@
+/* 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/. */
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ SyncedTabsController: "resource:///modules/SyncedTabsController.sys.mjs",
+});
+
+import { html, ifDefined } from "chrome://global/content/vendor/lit.all.mjs";
+import {
+ escapeHtmlEntities,
+ navigateToLink,
+} from "chrome://browser/content/firefoxview/helpers.mjs";
+
+import { SidebarPage } from "./sidebar-page.mjs";
+
+class SyncedTabsInSidebar extends SidebarPage {
+ controller = new lazy.SyncedTabsController(this);
+
+ constructor() {
+ super();
+ this.onSearchQuery = this.onSearchQuery.bind(this);
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ this.controller.addSyncObservers();
+ this.controller.updateStates();
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ this.controller.removeSyncObservers();
+ }
+
+ /**
+ * The template shown when the list of synced devices is currently
+ * unavailable.
+ *
+ * @param {object} options
+ * @param {string} options.action
+ * @param {string} options.buttonLabel
+ * @param {string[]} options.descriptionArray
+ * @param {string} options.descriptionLink
+ * @param {boolean} options.error
+ * @param {string} options.header
+ * @param {string} options.headerIconUrl
+ * @param {string} options.mainImageUrl
+ * @returns {TemplateResult}
+ */
+ messageCardTemplate({
+ action,
+ buttonLabel,
+ descriptionArray,
+ descriptionLink,
+ error,
+ header,
+ headerIconUrl,
+ mainImageUrl,
+ }) {
+ return html`
+ <fxview-empty-state
+ headerLabel=${header}
+ .descriptionLabels=${descriptionArray}
+ .descriptionLink=${ifDefined(descriptionLink)}
+ class="empty-state synced-tabs error"
+ isSelectedTab
+ mainImageUrl="${ifDefined(mainImageUrl)}"
+ ?errorGrayscale=${error}
+ headerIconUrl="${ifDefined(headerIconUrl)}"
+ id="empty-container"
+ >
+ <button
+ class="primary"
+ slot="primary-action"
+ ?hidden=${!buttonLabel}
+ data-l10n-id="${ifDefined(buttonLabel)}"
+ data-action="${action}"
+ @click=${e => this.controller.handleEvent(e)}
+ aria-details="empty-container"
+ ></button>
+ </fxview-empty-state>
+ `;
+ }
+
+ /**
+ * The template shown for a device that has tabs.
+ *
+ * @param {string} deviceName
+ * @param {string} deviceType
+ * @param {Array} tabItems
+ * @returns {TemplateResult}
+ */
+ deviceTemplate(deviceName, deviceType, tabItems) {
+ return html`<moz-card
+ type="accordion"
+ .heading=${deviceName}
+ icon
+ class=${deviceType}
+ >
+ <fxview-tab-list
+ compactRows
+ .tabItems=${ifDefined(tabItems)}
+ .updatesPaused=${false}
+ .searchQuery=${this.controller.searchQuery}
+ @fxview-tab-list-primary-action=${navigateToLink}
+ />
+ </moz-card>`;
+ }
+
+ /**
+ * The template shown for a device that has no tabs.
+ *
+ * @param {string} deviceName
+ * @param {string} deviceType
+ * @returns {TemplateResult}
+ */
+ noDeviceTabsTemplate(deviceName, deviceType) {
+ return html`<moz-card
+ .heading=${deviceName}
+ icon
+ class=${deviceType}
+ data-l10n-id="firefoxview-syncedtabs-device-notabs"
+ >
+ </moz-card>`;
+ }
+
+ /**
+ * The template shown for a device that has tabs, but no tabs that match the
+ * current search query.
+ *
+ * @param {string} deviceName
+ * @param {string} deviceType
+ * @returns {TemplateResult}
+ */
+ noSearchResultsTemplate(deviceName, deviceType) {
+ return html`<moz-card
+ .heading=${deviceName}
+ icon
+ class=${deviceType}
+ data-l10n-id="firefoxview-search-results-empty"
+ data-l10n-args=${JSON.stringify({
+ query: escapeHtmlEntities(this.controller.searchQuery),
+ })}
+ >
+ </moz-card>`;
+ }
+
+ /**
+ * The template shown for the list of synced devices.
+ *
+ * @returns {TemplateResult[]}
+ */
+ deviceListTemplate() {
+ return Object.values(this.controller.getRenderInfo()).map(
+ ({ name: deviceName, deviceType, tabItems, tabs }) => {
+ if (tabItems.length) {
+ return this.deviceTemplate(deviceName, deviceType, tabItems);
+ } else if (tabs.length) {
+ return this.noSearchResultsTemplate(deviceName, deviceType);
+ }
+ return this.noDeviceTabsTemplate(deviceName, deviceType);
+ }
+ );
+ }
+
+ render() {
+ const messageCard = this.controller.getMessageCard();
+ if (messageCard) {
+ return [this.stylesheet(), this.messageCardTemplate(messageCard)];
+ }
+ return html`
+ ${this.stylesheet()}
+ <fxview-search-textbox
+ data-l10n-id="firefoxview-search-text-box-syncedtabs"
+ data-l10n-attrs="placeholder"
+ @fxview-search-textbox-query=${this.onSearchQuery}
+ size="15"
+ ></fxview-search-textbox>
+ ${this.deviceListTemplate()}
+ `;
+ }
+
+ onSearchQuery(e) {
+ this.controller.searchQuery = e.detail.query;
+ this.requestUpdate();
+ }
+}
+
+customElements.define("sidebar-syncedtabs", SyncedTabsInSidebar);
diff --git a/browser/components/sidebar/sidebar.css b/browser/components/sidebar/sidebar.css
new file mode 100644
index 0000000000..34d43aa850
--- /dev/null
+++ b/browser/components/sidebar/sidebar.css
@@ -0,0 +1,27 @@
+/* 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 url("chrome://global/skin/global.css");
+
+:root {
+ background-color: var(--lwt-sidebar-background-color);
+ color: var(--lwt-sidebar-text-color);
+}
+
+moz-card {
+ margin-block-start: var(--space-medium);
+
+ &.phone::part(icon),
+ &.mobile::part(icon) {
+ background-image: url('chrome://browser/skin/device-phone.svg');
+ }
+
+ &.desktop::part(icon) {
+ background-image: url('chrome://browser/skin/device-desktop.svg');
+ }
+
+ &.tablet::part(icon) {
+ background-image: url('chrome://browser/skin/device-tablet.svg');
+ }
+}
diff --git a/browser/components/sidebar/sidebar.ftl b/browser/components/sidebar/sidebar.ftl
new file mode 100644
index 0000000000..2a5ef75d83
--- /dev/null
+++ b/browser/components/sidebar/sidebar.ftl
@@ -0,0 +1,26 @@
+# 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/.
+
+sidebar-launcher-insights =
+ .title = Insights
+
+## Variables:
+## $date (string) - Date to be formatted based on locale
+
+sidebar-history-date-today =
+ .heading = Today — { DATETIME($date, dateStyle: "full") }
+sidebar-history-date-yesterday =
+ .heading = Yesterday — { DATETIME($date, dateStyle: "full") }
+sidebar-history-date-this-month =
+ .heading = { DATETIME($date, dateStyle: "full") }
+sidebar-history-date-prev-month =
+ .heading = { DATETIME($date, month: "long", year: "numeric") }
+
+##
+
+# "Search" is a noun (as in "Results of the search for")
+# Variables:
+# $query (String) - The search query used for searching through browser history.
+sidebar-search-results-header =
+ .heading = Search results for “{ $query }”