Tabs redesign

This commit is contained in:
Tom Najdek 2023-12-14 09:43:11 +01:00 committed by Dan Stillman
parent a2e034c921
commit bd067df4f6
13 changed files with 213 additions and 172 deletions

View file

@ -1293,6 +1293,45 @@ var CollectionTree = class CollectionTree extends LibraryTree {
return false; return false;
} }
getIconName(index) {
const treeRow = this.getRow(index);
let collectionType = treeRow.type;
let icon = collectionType;
switch (collectionType) {
case 'group':
icon = 'library-group';
break;
case 'feed':
// Better alternative needed: https://github.com/zotero/zotero/pull/902#issuecomment-183185973
/*
if (treeRow.ref.updating) {
collectionType += 'Updating';
} else */if (treeRow.ref.lastCheckError) {
icon += '-error';
}
break;
case 'trash':
if (this._trashNotEmpty[treeRow.ref.libraryID]) {
icon += '-full';
}
break;
case 'feeds':
icon = 'feed-library';
break;
case 'header':
if (treeRow.ref.id == 'group-libraries-header') {
icon = 'groups';
}
break;
case 'separator':
return null;
}
return icon;
}
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
/// ///
@ -2234,47 +2273,11 @@ var CollectionTree = class CollectionTree extends LibraryTree {
} }
_getIcon(index) { _getIcon(index) {
const treeRow = this.getRow(index); let iconName = this.getIconName(index);
var collectionType = treeRow.type;
if (collectionType == 'separator') {
return document.createElement('span');
}
let icon = collectionType; return iconName
? getCSSIcon(iconName)
switch (collectionType) { : document.createElement('span');
case 'group':
icon = 'library-group';
break;
case 'feed':
// Better alternative needed: https://github.com/zotero/zotero/pull/902#issuecomment-183185973
/*
if (treeRow.ref.updating) {
collectionType += 'Updating';
} else */if (treeRow.ref.lastCheckError) {
icon += '-error';
}
break;
case 'trash':
if (this._trashNotEmpty[treeRow.ref.libraryID]) {
icon += '-full';
}
break;
case 'feeds':
icon = 'feed-library';
break;
case 'header':
if (treeRow.ref.id == 'group-libraries-header') {
icon = 'groups';
}
break;
}
return getCSSIcon(icon);
} }
/** /**

View file

@ -109,22 +109,6 @@ i('Twisty', (
<path d="M8 13.4c-.5 0-.9-.2-1.2-.6L.4 5.2C0 4.7-.1 4.3.2 3.7S1 3 1.6 3h12.8c.6 0 1.2.1 1.4.7.3.6.2 1.1-.2 1.6l-6.4 7.6c-.3.4-.7.5-1.2.5z"/> <path d="M8 13.4c-.5 0-.9-.2-1.2-.6L.4 5.2C0 4.7-.1 4.3.2 3.7S1 3 1.6 3h12.8c.6 0 1.2.1 1.4.7.3.6.2 1.1-.2 1.6l-6.4 7.6c-.3.4-.7.5-1.2.5z"/>
</svg> </svg>
)); ));
i('ArrowLeft', (
/* 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" viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
<path d="m5.001 8.352 5.465 5.466a.626.626 0 0 0 .884-.886L6.416 7.999l4.933-4.932a.626.626 0 0 0-.885-.885L5 7.647l.001.705z"/>
</svg>
));
i('ArrowRight', (
/* 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" viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
<path d="m10.999 8.352-5.465 5.466a.626.626 0 0 1-.884-.886l4.935-4.934-4.934-4.931a.626.626 0 0 1 .885-.885L11 7.647l-.001.705z"/>
</svg>
));
i('Cross', "chrome://zotero/skin/cross.png"); i('Cross', "chrome://zotero/skin/cross.png");
i('Tick', "chrome://zotero/skin/tick.png"); i('Tick', "chrome://zotero/skin/tick.png");
i('ArrowRefresh', "chrome://zotero/skin/arrow_refresh.png"); i('ArrowRefresh', "chrome://zotero/skin/arrow_refresh.png");
@ -253,14 +237,14 @@ module.exports.getCSSIcon = function (key) {
let iconEl = document.createElement('span'); let iconEl = document.createElement('span');
iconEl.classList.add('icon'); iconEl.classList.add('icon');
iconEl.classList.add('icon-css'); iconEl.classList.add('icon-css');
iconEl.classList.add(key); iconEl.classList.add(`icon-${key}`);
cssIconsCache.set(key, iconEl); cssIconsCache.set(key, iconEl);
} }
return cssIconsCache.get(key).cloneNode(true); return cssIconsCache.get(key).cloneNode(true);
}; };
module.exports.getCSSItemTypeIcon = function (itemType, key = 'icon-item-type') { module.exports.getCSSItemTypeIcon = function (itemType, key = 'item-type') {
let icon = module.exports.getCSSIcon(key); let icon = module.exports.getCSSIcon(key);
icon.dataset.itemType = itemType; icon.dataset.itemType = itemType;
return icon; return icon;

View file

@ -27,7 +27,7 @@
import React, { forwardRef, useState, useRef, useImperativeHandle, useEffect, useLayoutEffect } from 'react'; import React, { forwardRef, useState, useRef, useImperativeHandle, useEffect, useLayoutEffect } from 'react';
import cx from 'classnames'; import cx from 'classnames';
const { IconXmark, IconArrowLeft, IconArrowRight } = require('./icons'); const { CSSIcon } = require('./icons');
const SCROLL_ARROW_SCROLL_BY = 222; const SCROLL_ARROW_SCROLL_BY = 222;
@ -312,7 +312,7 @@ const TabBar = forwardRef(function (props, ref) {
} }
} }
function renderTab({ id, title, selected, iconBackgroundImage }, index) { function renderTab({ id, title, selected, icon }, index) {
return ( return (
<div <div
key={id} key={id}
@ -328,18 +328,18 @@ const TabBar = forwardRef(function (props, ref) {
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
tabIndex="-1" tabIndex="-1"
> >
<div className="tab-name" dir="auto">{iconBackgroundImage && {icon}
<span className="icon-bg" style={{ backgroundImage: iconBackgroundImage }}/>}{title}</div> <div className="tab-name" dir="auto">{title}</div>
<div <div
className="tab-close" className="tab-close"
onClick={(event) => handleTabClose(event, id)} onClick={(event) => handleTabClose(event, id)}
> >
<IconXmark/> <CSSIcon name="x-8" className="icon-16" />
</div> </div>
</div> </div>
); );
} }
return ( return (
<div> <div>
<div <div
@ -359,9 +359,14 @@ const TabBar = forwardRef(function (props, ref) {
ref={startArrowRef} ref={startArrowRef}
className="scroll-start-arrow" className="scroll-start-arrow"
style={{ transform: Zotero.rtl ? 'scaleX(-1)' : undefined }} style={{ transform: Zotero.rtl ? 'scaleX(-1)' : undefined }}
onClick={handleClickScrollStart} >
onDoubleClick={handleScrollArrowDoubleClick} <button
><IconArrowLeft/></div> onClick={handleClickScrollStart}
onDoubleClick={handleScrollArrowDoubleClick}
>
<CSSIcon name="chevron-tabs" className="icon-20" />
</button>
</div>
<div className="tabs-wrapper"> <div className="tabs-wrapper">
<div <div
ref={tabsRef} ref={tabsRef}
@ -378,12 +383,19 @@ const TabBar = forwardRef(function (props, ref) {
ref={endArrowRef} ref={endArrowRef}
className="scroll-end-arrow" className="scroll-end-arrow"
style={{ transform: Zotero.rtl ? 'scaleX(-1)' : undefined }} style={{ transform: Zotero.rtl ? 'scaleX(-1)' : undefined }}
onClick={handleClickScrollEnd} >
onDoubleClick={handleScrollArrowDoubleClick} <button
><IconArrowRight/></div> onClick={handleClickScrollEnd}
onDoubleClick={handleScrollArrowDoubleClick}
>
<CSSIcon name="chevron-tabs" className="icon-20" />
</button>
</div>
</div> </div>
</div> </div>
); );
}); });
TabBar.displayName = 'TabBar';
export default TabBar; export default TabBar;

View file

@ -2744,19 +2744,19 @@ var ItemTree = class ItemTree extends LibraryTree {
// If the item has a child attachment // If the item has a child attachment
if (type !== null && type != 'none') { if (type !== null && type != 'none') {
if (type == 'pdf') { if (type == 'pdf') {
icon = getCSSItemTypeIcon('attachmentPDF', 'icon-attachment-type'); icon = getCSSItemTypeIcon('attachmentPDF', 'attachment-type');
ariaLabel = Zotero.getString('pane.item.attachments.hasPDF'); ariaLabel = Zotero.getString('pane.item.attachments.hasPDF');
} }
else if (type == 'snapshot') { else if (type == 'snapshot') {
icon = getCSSItemTypeIcon('attachmentSnapshot', 'icon-attachment-type'); icon = getCSSItemTypeIcon('attachmentSnapshot', 'attachment-type');
ariaLabel = Zotero.getString('pane.item.attachments.hasSnapshot'); ariaLabel = Zotero.getString('pane.item.attachments.hasSnapshot');
} }
else if (type == 'epub') { else if (type == 'epub') {
icon = getCSSItemTypeIcon('attachmentEPUB', 'icon-attachment-type'); icon = getCSSItemTypeIcon('attachmentEPUB', 'attachment-type');
ariaLabel = Zotero.getString('pane.item.attachments.hasEPUB'); ariaLabel = Zotero.getString('pane.item.attachments.hasEPUB');
} }
else { else {
icon = getCSSItemTypeIcon('attachmentFile', 'icon-attachment-type'); icon = getCSSItemTypeIcon('attachmentFile', 'attachment-type');
ariaLabel = Zotero.getString('pane.item.attachments.has'); ariaLabel = Zotero.getString('pane.item.attachments.has');
} }

View file

@ -29,6 +29,7 @@
var React = require('react'); var React = require('react');
var ReactDOM = require('react-dom'); var ReactDOM = require('react-dom');
import TabBar from 'components/tabBar'; import TabBar from 'components/tabBar';
import { CSSIcon, CSSItemTypeIcon } from 'components/icons';
// Reduce loaded tabs limit if the system has 8 GB or less memory. // Reduce loaded tabs limit if the system has 8 GB or less memory.
// TODO: Revise this after upgrading to Zotero 7 // TODO: Revise this after upgrading to Zotero 7
@ -81,13 +82,33 @@ var Zotero_Tabs = new function () {
}; };
this._update = function () { this._update = function () {
this._tabBarRef.current.setTabs(this._tabs.map(tab => ({ this._tabBarRef.current.setTabs(this._tabs.map((tab) => {
id: tab.id, let icon = null;
type: tab.type, if (tab.id === 'zotero-pane') {
title: tab.title, let index = ZoteroPane.collectionsView?.selection?.focused;
selected: tab.id == this._selectedID, if (typeof index !== 'undefined' && ZoteroPane.collectionsView.getRow(index)) {
iconBackgroundImage: tab.iconBackgroundImage let iconName = ZoteroPane.collectionsView.getIconName(index);
}))); icon = <CSSIcon name={iconName} className="tab-icon" />;
}
}
else if (tab.data?.itemID) {
try {
let item = Zotero.Items.get(tab.data.itemID);
icon = <CSSItemTypeIcon itemType={item.getItemTypeIconName()} className="tab-icon" />;
}
catch (e) {
// item might not yet be loaded, we will get the icon on the next update
}
}
return {
id: tab.id,
type: tab.type,
title: tab.title,
selected: tab.id == this._selectedID,
icon,
};
}));
// Disable File > Close menuitem if multiple tabs are open // Disable File > Close menuitem if multiple tabs are open
const multipleTabsOpen = this._tabs.length > 1; const multipleTabsOpen = this._tabs.length > 1;
document.getElementById('cmd_close').setAttribute('disabled', multipleTabsOpen); document.getElementById('cmd_close').setAttribute('disabled', multipleTabsOpen);
@ -243,23 +264,9 @@ var Zotero_Tabs = new function () {
return; return;
} }
tab.title = title; tab.title = title;
Zotero_Tabs.updateLibraryTabIcon();
this._update(); this._update();
}; };
this.updateLibraryTabIcon = () => {
let index = ZoteroPane.collectionsView.selection.focused;
if (!ZoteroPane.collectionsView.getRow(index)) {
return;
}
let icon = ZoteroPane.collectionsView._getIcon(index);
var { tab } = this._getTab('zotero-pane');
if (!tab || !icon.style.backgroundImage) {
return;
}
tab.iconBackgroundImage = icon.style.backgroundImage;
};
/** /**
* Close tabs * Close tabs
* *

View file

@ -754,7 +754,7 @@
</toolbar> </toolbar>
</toolbox> </toolbox>
<!-- Keep in sync with Zotero.test conditional block in overlay.js --> <!-- Keep in sync with Zotero.test conditional block in overlay.js -->
<hbox> <hbox id="zotero-title-bar">
<div xmlns="http://www.w3.org/1999/xhtml" id="tab-bar-container" style="-moz-box-flex: 1;"/> <div xmlns="http://www.w3.org/1999/xhtml" id="tab-bar-container" style="-moz-box-flex: 1;"/>
<hbox id="zotero-tabs-toolbar" align="center"> <hbox id="zotero-tabs-toolbar" align="center">
<toolbarbutton id="zotero-tb-opened-tabs" class="zotero-tb-button" tabindex="-1" data-l10n-id="zotero-toolbar-opened-tabs-menu" type="panel" /> <toolbarbutton id="zotero-tb-opened-tabs" class="zotero-tb-button" tabindex="-1" data-l10n-id="zotero-toolbar-opened-tabs-menu" type="panel" />

View file

@ -0,0 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon">
<path id="Vector" d="M13.8839 2.88388L13 2L5 10L13 18L13.8839 17.1161L6.76777 10L13.8839 2.88388Z" fill="context-fill"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 243 B

View file

@ -19,3 +19,7 @@
width: 290px; width: 290px;
background: var(--material-background); background: var(--material-background);
} }
#zotero-title-bar {
border-bottom: var(--material-panedivider);
}

View file

@ -3,6 +3,14 @@ $icons: (
library-group, publications, trash-full, trash, unfiled, retracted, search, library-group, publications, trash-full, trash, unfiled, retracted, search,
); );
@each $icon in $icons {
.icon-css.icon-#{$icon} {
@include focus-states using ($color) {
@include svgicon($icon, $color, "16", "collection-tree");
}
}
}
#zotero-collections-tree-container { #zotero-collections-tree-container {
height: 5.2em; height: 5.2em;
} }
@ -22,7 +30,7 @@ $icons: (
} }
@each $icon in $icons { @each $icon in $icons {
.icon-css.#{$icon} { .icon-css.icon-#{$icon} {
@include focus-states using ($color) { @include focus-states using ($color) {
@include svgicon($icon, $color, "16", "collection-tree"); @include svgicon($icon, $color, "16", "collection-tree");
} }

View file

@ -25,6 +25,11 @@
-moz-appearance: none !important; -moz-appearance: none !important;
} }
.icon-20 {
width: 20px;
height: 20px;
}
.icon-16 { .icon-16 {
width: 16px; width: 16px;
height: 16px; height: 16px;
@ -40,6 +45,8 @@ $-icons: (
chevron-6: 8, chevron-6: 8,
filter: 16, filter: 16,
note: 16, note: 16,
x-8: 16,
chevron-tabs: 20
); );
@each $icon, $size in $-icons { @each $icon, $size in $-icons {

View file

@ -1,12 +1,7 @@
// Styling for displaying tabs in the title bar // Styling for displaying tabs in the title bar
:root:not([legacytoolbar="true"]) { :root:not([legacytoolbar="true"]) {
& { & {
@media (-moz-platform: windows) { --tab-min-height: 36px;
--tab-min-height: 36px;
}
@media not (-moz-platform: windows) {
--tab-min-height: 30px;
}
--tabs-border-color: rgba(0,0,0,.3); --tabs-border-color: rgba(0,0,0,.3);
--tabline-color: #0a84ff; --tabline-color: #0a84ff;
@ -138,14 +133,6 @@
background: var(--material-tabbar); background: var(--material-tabbar);
-moz-window-dragging: drag; -moz-window-dragging: drag;
} }
#tab-bar-container .tab {
background: transparent;
}
#tab-bar-container .tab.selected {
background: var(--material-tabbar)
}
.zotero-toolbar { .zotero-toolbar {
-moz-appearance: none; -moz-appearance: none;

View file

@ -4,7 +4,7 @@
--safe-area-end: 0; --safe-area-end: 0;
--tab-border: .5px solid var(--tab-border); --tab-border: .5px solid var(--tab-border);
min-height: 30px; min-height: var(--tab-min-height);
&:not([hidden]) { &:not([hidden]) {
// display: flex overrides the hidden attribute // display: flex overrides the hidden attribute
@ -39,51 +39,77 @@
} }
.scroll-start-arrow, .scroll-end-arrow { .scroll-start-arrow, .scroll-end-arrow {
height: 30px; padding: 0 4px;
padding: 0 3px;
align-items: center; align-items: center;
z-index: 1; color: var(--fill-secondary);
color: var(--fill-tertiary);
display: none; display: none;
box-shadow: none; box-shadow: none;
// Add bottom border to scroll arrows, z-index: 1;
// and negate the extra vertical pixel added by the border with a negative margin
border-bottom: var(--tab-border); > button {
margin-bottom: -1px; width: auto;
box-sizing: border-box; padding: 0;
border: 0;
height: 28px;
border-radius: 5px;
background: transparent;
}
.icon { .icon {
display: flex; display: flex;
fill: var(--fill-tertiary);
} }
&.active { &.active {
color: var(--fill-secondary); button {
&:hover { &:hover {
background-color: var(--fill-quinary); background-color: var(--fill-quinary);
}
&:active {
background-color: var(--fill-quarternary);
}
}
.icon {
fill: var(--fill-secondary);
} }
} }
} }
.scroll-start-arrow { .scroll-start-arrow {
border-right: var(--material-border-transparent); border-right: var(--material-border-transparent);
border-left: var(--material-border-quarternary);
&.active { &.active {
border-right: var(--material-panedivider); border-right: var(--material-panedivider);
box-shadow: 3px 0 3px -3px rgba(0, 0, 0, .3); box-shadow: 0.5px 0.5px 0px -0.5px rgba(0, 0, 0, 0.05), 3px 0px 3px -3px rgba(0, 0, 0, .3)
} }
} }
.scroll-end-arrow { .scroll-end-arrow {
border-left: var(--material-border-transparent); border-left: var(--material-border-transparent);
&.active { &.active {
border-left: var(--material-panedivider); border-left: var(--material-panedivider);
box-shadow: -3px 0 3px -3px rgba(0, 0, 0, .3) box-shadow: -0.5px 0.5px 0px -0.5px rgba(0, 0, 0, 0.05), -3px 0 3px -3px rgba(0, 0, 0, .3)
}
.icon {
transform: rotate(180deg);
} }
} }
.pinned-tabs { .pinned-tabs {
display: none; display: none;
padding-right: 8px;
.tabs {
overflow: unset;
}
.tab { .tab {
max-width: 110px; max-width: 100px;
} }
} }
@ -108,74 +134,76 @@
overflow: hidden; overflow: hidden;
left: 0; left: 0;
right: 0; right: 0;
padding: 4px 1px;
column-gap: 4px;
&:before { .pinned-tabs & {
content: ""; padding: 4px 0;
width: 0;
min-width: 0;
border-inline-end: var(--tab-border);
}
&:after {
content: "";
flex: 1 0 0;
width: 100%;
min-width: 0;
// Add bottom border to the space between the last tab and the spacer in #tab-bar-container::after
border-bottom: var(--tab-border);
border-inline-start: var(--tab-border);
} }
} }
.tab { .tab {
box-sizing: border-box; box-sizing: border-box;
-moz-appearance: none;
max-width: 200px; max-width: 200px;
flex: 1 1 200px; flex: 1 1 200px;
height: 30px;
line-height: 30px;
position: relative; position: relative;
color: var(--fill-primary); color: var(--fill-primary);
text-align: center; text-align: center;
padding: 0 22px; min-width: 100px;
min-width: 110px; height: 28px;
line-height: 16px;
&:not(:last-child) { padding: 6px 22px 6px 6px;
border-inline-end: var(--tab-border); display: flex;
} border-radius: 5px;
transition: background-color 0.1s ease-out;
&.selected { &.selected {
background: var(--material-button); background: var(--material-button);
box-shadow: 0px 0px 0px 0.5px rgba(0, 0, 0, 0.05), 0px 0.5px 2.5px 0px rgba(0, 0, 0, 0.30); box-shadow: 0px 0px 0px 0.5px rgba(0, 0, 0, 0.05), 0px 0.5px 2.5px 0px rgba(0, 0, 0, 0.30);
} }
&.dragging { &.dragging {
border-inline-start: var(--tab-border);
z-index: 1; z-index: 1;
} }
&.dragging + & { &:not(:last-child) {
border-inline-start: var(--tab-border); border-inline-end: var(--tab-border);
}
&:not(.selected):hover {
background-color: var(--fill-quinary);
}
&:first-child {
padding: 6px; // no 22px padding for X button because pinned tab cannot be closed
.tab-close {
display: none;
}
} }
.tab-name { .tab-name {
line-height: 30px; background: linear-gradient(to left, transparent 0px, var(--fill-primary) 20px) text;
overflow-x: hidden; flex: 1 1 100%;
text-overflow: ellipsis; line-height: 16px;
display: -moz-box; margin-inline-start: 4px;
-moz-box-pack: center; overflow: hidden;
position: relative; text-align: left;
top: -2px; -webkit-text-fill-color: transparent;
overflow-y: hidden; white-space: nowrap;
.icon-bg { @include state('.tab:first-child') {
margin-inline-end: 7px; background: linear-gradient(to left, transparent 0px, var(--fill-primary) 10px) text;
margin-top: -2px;
} }
} }
.tab-icon {
flex: 0 0 16px;
}
.tab-close { .tab-close {
position: absolute; position: absolute;
transition: background-color 0.1s ease-out;
&:dir(ltr) { &:dir(ltr) {
right: 6px; right: 6px;
@ -205,10 +233,6 @@
} }
} }
&:first-child .tab-close {
display: none;
}
&:not(.selected) { &:not(.selected) {
border-bottom: var(--tab-border); border-bottom: var(--tab-border);
} }

View file

@ -1,5 +1,5 @@
#tab-bar-container { #tab-bar-container {
--safe-area-start: 78px; --safe-area-start: 73px;
--safe-area-end: 20px; --safe-area-end: 20px;
} }