diff --git a/chrome/content/zotero/components/tabBar.jsx b/chrome/content/zotero/components/tabBar.jsx
new file mode 100644
index 0000000000..e88ecabc76
--- /dev/null
+++ b/chrome/content/zotero/components/tabBar.jsx
@@ -0,0 +1,110 @@
+/*
+ ***** BEGIN LICENSE BLOCK *****
+
+ Copyright © 2020 Corporation for Digital Scholarship
+ Vienna, Virginia, USA
+ https://www.zotero.org
+
+ This file is part of Zotero.
+
+ Zotero is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Zotero is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with Zotero. If not, see .
+
+ ***** END LICENSE BLOCK *****
+*/
+
+"use strict";
+
+import React, { forwardRef, useState, useImperativeHandle, useEffect } from 'react';
+import cx from 'classnames';
+
+const TabBar = forwardRef(function (props, ref) {
+ const [tabs, setTabs] = useState(props.initialTabs);
+ const [selectedIndex, setSelectedIndex] = useState(0);
+
+ useImperativeHandle(ref, () => ({
+ addTab({ title, type }) {
+ var newTabs = [...tabs];
+ newTabs.push({ title, type });
+ setTabs(newTabs);
+ setSelectedIndex(newTabs.length - 1);
+ },
+
+ renameTab(title, index) {
+ var newTabs = tabs.map((tab, currentIndex) => {
+ let newTab = Object.assign({}, tab);
+ if (index == currentIndex) {
+ newTab.title = title;
+ }
+ return newTab;
+ });
+ setTabs(newTabs);
+ }
+ }));
+
+ useEffect(() => {
+ if (props.onTabSelected) {
+ props.onTabSelected(selectedIndex);
+ }
+ }, [selectedIndex]);
+
+
+ function removeTab(index) {
+ var newTabs = [...tabs];
+ newTabs.splice(index, 1);
+ setTabs(newTabs);
+ setSelectedIndex(selectedIndex - 1);
+ if (props.onTabClosed) {
+ props.onTabClosed(index);
+ }
+ }
+
+ function handleTabClick(event, index) {
+ setSelectedIndex(index);
+ event.stopPropagation();
+ }
+
+ function handleTabClose(event, index) {
+ removeTab(index);
+ if (props.onTabClose) {
+ props.onTabClose(index);
+ }
+ event.stopPropagation();
+ }
+
+ function renderTab(tab, index) {
+ return (
+
handleTabClick(event, index)}
+ >
+
{tab.title}
+
handleTabClose(event, index)}
+ >
+ X
+
+
+ );
+ }
+
+ return (
+
+ { tabs.map((tab, index) => renderTab(tab, index)) }
+
+ );
+});
+
+export default TabBar;
\ No newline at end of file
diff --git a/chrome/content/zotero/standalone/standalone.xul b/chrome/content/zotero/standalone/standalone.xul
index d6f748d1d4..712511567c 100644
--- a/chrome/content/zotero/standalone/standalone.xul
+++ b/chrome/content/zotero/standalone/standalone.xul
@@ -43,9 +43,13 @@
@@ -111,7 +115,16 @@
accesskey="&selectAllCmd.accesskey;"
command="cmd_selectAll"/>
-
+
+
+
+
+
+
+
+
+
+
.
+
+ ***** END LICENSE BLOCK *****
+*/
+
+"use strict";
+
+// Using 'import' breaks hooks
+var React = require('react');
+var ReactDOM = require('react-dom');
+import TabBar from 'components/tabBar';
+
+var Zotero_Tabs = new function () {
+ const HTML_NS = 'http://www.w3.org/1999/xhtml';
+
+ Object.defineProperty(this, 'deck', {
+ get: () => document.getElementById('tabs-deck')
+ });
+
+ this._tabBarRef = {};
+ this._tabs = [
+ {
+ title: "",
+ type: 'library'
+ }
+ ];
+
+ this.init = function () {
+ ReactDOM.render(
+ ,
+ document.getElementById('tab-bar-container')
+ );
+
+ };
+
+
+ /**
+ * @return {Element} - The element created in the deck
+ */
+ this.add = function ({ title, type, url, index }) {
+ this._tabBarRef.current.addTab({ title, type });
+
+ var elem;
+ if (url) {
+ elem = document.createElement('iframe');
+ elem.setAttribute('type', 'content');
+ elem.setAttribute('src', url);
+ }
+ else {
+ elem = document.createElementNS(HTML_NS, 'div');
+ elem.textContent = title;
+ }
+
+ var deck = this.deck;
+ deck.insertBefore(elem, index === undefined ? null : deck.childNodes[index]);
+
+ return elem;
+ };
+
+
+ this.rename = function (title, index) {
+ if (index === undefined) {
+ index = this.deck.selectedIndex;
+ }
+ this._tabs[index].title = title;
+ this._tabBarRef.current.renameTab(title, index);
+ };
+
+
+ this._onTabSelected = function (index) {
+ this.deck.selectedIndex = index;
+ };
+
+
+ this._onTabClosed = function (index) {
+ this._tabs.splice(index, 1);
+ var deck = this.deck;
+ deck.removeChild(deck.childNodes[index]);
+ };
+};
\ No newline at end of file
diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js
index 449efc7c7e..62039e4892 100644
--- a/chrome/content/zotero/zoteroPane.js
+++ b/chrome/content/zotero/zoteroPane.js
@@ -144,6 +144,7 @@ var ZoteroPane = new function()
Zotero.hiDPI = window.devicePixelRatio > 1;
Zotero.hiDPISuffix = Zotero.hiDPI ? "@2x" : "";
+ Zotero_Tabs.init();
ZoteroPane_Local.setItemsPaneMessage(Zotero.getString('pane.items.loading'));
// Add a default progress window
@@ -1166,6 +1167,10 @@ var ZoteroPane = new function()
return;
}
+ // Rename tab
+ // TODO: What if PDF is in front?
+ Zotero_Tabs.rename(collectionTreeRow.getName());
+
// Clear quick search and tag selector when switching views
document.getElementById('zotero-tb-search').value = "";
if (ZoteroPane.tagSelector) {
diff --git a/chrome/content/zotero/zoteroPane.xul b/chrome/content/zotero/zoteroPane.xul
index b43c55c473..323ec585e2 100644
--- a/chrome/content/zotero/zoteroPane.xul
+++ b/chrome/content/zotero/zoteroPane.xul
@@ -41,6 +41,7 @@
xmlns:html="http://www.w3.org/1999/xhtml">
+
@@ -73,506 +74,508 @@
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- &zotero.lookup.description;
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ &zotero.lookup.description;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/scss/_zotero-react-client.scss b/scss/_zotero-react-client.scss
index c99c1f191f..0e40965eb6 100644
--- a/scss/_zotero-react-client.scss
+++ b/scss/_zotero-react-client.scss
@@ -25,8 +25,10 @@
@import "components/createParent";
@import "components/editable";
@import "components/icons";
+@import "components/mainWindow";
@import "components/progressMeter";
@import "components/search";
@import "components/syncButtonTooltip";
+@import "components/tabsBar";
@import "components/tagsBox";
@import "components/tagSelector";
diff --git a/scss/components/_mainWindow.scss b/scss/components/_mainWindow.scss
new file mode 100644
index 0000000000..3aa8e23067
--- /dev/null
+++ b/scss/components/_mainWindow.scss
@@ -0,0 +1,149 @@
+:root {
+ --tab-min-height: 30px;
+ --tabs-border-color: rgba(0,0,0,.3);
+ --tabline-color: #0a84ff;
+
+ --toolbar-non-lwt-bgcolor: #f9f9fa;
+ --toolbar-non-lwt-textcolor: #0c0c0d;
+ --toolbar-non-lwt-bgimage: none;
+ --toolbar-bgcolor: var(--toolbar-non-lwt-bgcolor);
+ --toolbar-bgimage: var(--toolbar-non-lwt-bgimage);
+ --chrome-content-separator-color: hsl(0, 0%, 60%);
+
+ --toolbarbutton-border-radius: 3px;
+ --toolbarbutton-icon-fill-opacity: .85;
+ --toolbarbutton-hover-background: hsla(0, 0%, 100%, .1)
+ linear-gradient(hsla(0, 0%, 100%, .3),
+ hsla(0, 0%, 100%, .1)) no-repeat;
+ --toolbarbutton-hover-bordercolor: hsla(0, 0%, 0%, .2);
+ --toolbarbutton-header-bordercolor: hsla(0, 0%, 0%, .2);
+ --toolbarbutton-hover-boxshadow: 0 1px 0 hsla(0, 0%, 100%, .5),
+ 0 1px 0 hsla(0, 0%, 100%, .5) inset;
+
+ --toolbarbutton-active-background: hsla(0, 0%, 0%, .02)
+ linear-gradient(hsla(0, 0%, 0%, .12),
+ transparent) border-box;
+ --toolbarbutton-active-bordercolor: hsla(0, 0%, 0%, .3);
+ --toolbarbutton-active-boxshadow: 0 1px 0 hsla(0, 0%, 100%, .5),
+ 0 1px 0 hsla(0, 0%, 0%, .05) inset,
+ 0 1px 1px hsla(0, 0%, 0%, .2) inset;
+ --toolbarbutton-inactive-bordercolor: rgba(0, 0, 0, 0.1);
+ --toolbarbutton-inactive-boxshadow: 0 1px 0 hsla(0, 0%, 0%, .05) inset;
+ --toolbarbutton-checkedhover-backgroundcolor: hsla(0, 0%, 0%, .09);
+ --toolbarbutton-icon-fill-attention: var(--lwt-toolbarbutton-icon-fill-attention, #0a84ff);
+
+ --lwt-header-image: none;
+ --row-grouped-header-bg-color: #d5d5d5;
+ --row-grouped-header-bg-color-selected: #3874d1;
+ --panel-separator-color: hsla(210, 4%, 10%, .14);
+ --arrowpanel-dimmed: hsla(0, 0%, 80%, .3);
+ --arrowpanel-dimmed-further: hsla(0, 0%, 80%, .45);
+ --splitter-color: #bdbdbd;
+ --urlbar-popup-url-color: hsl(210, 77%, 47%);
+ --urlbar-popup-action-color: hsl(178, 100%, 28%);
+}
+
+:root {
+ --autocomplete-popup-background: -moz-field;
+ --autocomplete-popup-color: -moz-fieldtext;
+ --autocomplete-popup-border-color: ThreeDShadow;
+ --autocomplete-popup-highlight-background: Highlight;
+ --autocomplete-popup-highlight-color: HighlightText;
+ /* Note: Setting this to 0 (without px) breaks CSS calculations for OSX. */
+ --space-above-tabbar: 0px;
+}
+
+:root:-moz-window-inactive {
+ --toolbar-bgcolor: -moz-mac-chrome-inactive;
+}
+
+
+
+
+#titlebar {
+ margin-bottom: -30px;
+ -moz-box-pack: center;
+}
+
+#personal-bookmarks {
+ -moz-window-dragging: inherit;
+}
+
+toolbarpaletteitem {
+ -moz-window-dragging: no-drag;
+ -moz-box-pack: start;
+}
+
+.titlebar-buttonbox-container {
+ -moz-box-ordinal-group: 1000;
+}
+
+#titlebar-fullscreen-button {
+ -moz-appearance: -moz-mac-fullscreen-button;
+}
+
+
+
+
+
+
+
+/* ::::: Tabs in Titlebar :::::: */
+
+/**
+ * For tabs in titlebar on OS X, we stretch the titlebar down so that the
+ * tabstrip can overlap it.
+ */
+#main-window[tabsintitlebar] > #titlebar {
+ min-height: calc(var(--tab-min-height) + var(--space-above-tabbar));
+}
+
+#main-window[tabsintitlebar="true"]:not(:-moz-lwtheme) > #titlebar {
+ -moz-appearance: -moz-window-titlebar;
+}
+
+#main-window:not([tabsintitlebar]) .titlebar-placeholder {
+ visibility: collapse;
+}
+
+/* NB: these would be margin-inline-start/end if it wasn't for the fact that OS X
+ * doesn't reverse the order of the items in the titlebar in RTL mode. */
+.titlebar-placeholder[type="caption-buttons"],
+#titlebar-buttonbox {
+ margin-right: 12px;
+ margin-left: 12px;
+}
+
+.titlebar-placeholder[type="fullscreen-button"],
+#titlebar-fullscreen-button {
+ margin-right: 7px;
+ margin-left: 7px;
+}
+
+#titlebar-fullscreen-button {
+ -moz-appearance: -moz-mac-fullscreen-button;
+}
+
+#titlebar-buttonbox {
+ -moz-appearance: -moz-window-button-box;
+}
+
+/* Fullscreen and caption buttons don't move with RTL on OS X so override the automatic ordering. */
+#titlebar-fullscreen-button:-moz-locale-dir(ltr),
+#titlebar-buttonbox-container:-moz-locale-dir(rtl),
+.titlebar-placeholder[type="fullscreen-button"]:-moz-locale-dir(ltr),
+.titlebar-placeholder[type="caption-buttons"]:-moz-locale-dir(rtl) {
+ -moz-box-ordinal-group: 1000;
+}
+
+#titlebar-fullscreen-button:-moz-locale-dir(rtl),
+#titlebar-buttonbox-container:-moz-locale-dir(ltr),
+.titlebar-placeholder[type="caption-buttons"]:-moz-locale-dir(ltr),
+.titlebar-placeholder[type="fullscreen-button"]:-moz-locale-dir(rtl) {
+ -moz-box-ordinal-group: 0;
+}
+
+#main-window[sizemode="fullscreen"] .titlebar-placeholder[type="fullscreen-button"],
+#main-window[sizemode="fullscreen"] .titlebar-placeholder[type="caption-buttons"] {
+ display: none;
+}
diff --git a/scss/components/_tabsBar.scss b/scss/components/_tabsBar.scss
new file mode 100644
index 0000000000..3fe4450e23
--- /dev/null
+++ b/scss/components/_tabsBar.scss
@@ -0,0 +1,42 @@
+#tab-bar-container {
+ min-height: 30px;
+}
+
+.tabs {
+ display: flex;
+}
+
+.tab {
+ -moz-appearance: toolbar;
+ width: 200px;
+ height: 30px;
+ line-height: 30px;
+ display: flex;
+ justify-content: center;
+ align-content: center;
+ position: relative;
+ background: #f9f9f9;
+ border: 1px solid black;
+ margin-right: -1px;
+ border-bottom: 1px solid transparent;
+ color: #000;
+
+ .tab-name {
+ line-height: 30px;
+ }
+
+ .tab-close {
+ position: absolute;
+ left: 6px;
+ top: 7px;
+ width: 16px;
+ height: 16px;
+ text-align: center;
+ line-height: 16px;
+ display: none;
+ }
+
+ &:hover:not(:first-child) .tab-close {
+ display: block;
+ }
+}
\ No newline at end of file
diff --git a/scss/mac/_tabsBar.scss b/scss/mac/_tabsBar.scss
new file mode 100644
index 0000000000..ed7310c942
--- /dev/null
+++ b/scss/mac/_tabsBar.scss
@@ -0,0 +1,9 @@
+.tabs {
+ -moz-window-dragging: drag;
+ margin-left: 78px;
+}
+
+.tab {
+ -moz-window-dragging: no-drag;
+
+}
\ No newline at end of file
diff --git a/scss/zotero-react-client-mac.scss b/scss/zotero-react-client-mac.scss
index 2678caa9c9..3db5c8b167 100644
--- a/scss/zotero-react-client-mac.scss
+++ b/scss/zotero-react-client-mac.scss
@@ -8,4 +8,5 @@
@import "mac/createParent";
@import "mac/editable";
@import "mac/search";
+@import "mac/tabsBar";
@import "mac/tag-selector";