Restyle Tag Selector

* Render a colored dot for colored tags
* Improve spacing and positioning, especially on 2x screens
* Add scss map for tag colors in light/dark scheme
* Add support for compact/comfortable in tag selector
* Restyle filter box, add a placeholder and a new icon
This commit is contained in:
Tom Najdek 2023-10-18 14:16:08 +02:00 committed by Dan Stillman
parent ff115b0873
commit 9ff76d2dd9
21 changed files with 334 additions and 131 deletions

View file

@ -27,7 +27,7 @@
const React = require('react') const React = require('react')
const { PureComponent, createElement: create } = React const { PureComponent, createElement: create } = React
const { IconDownChevron } = require('./icons') const { CSSIcon } = require('./icons')
const cx = require('classnames') const cx = require('classnames')
const { const {
bool, element, func, node, number, oneOf, string bool, element, func, node, number, oneOf, string
@ -77,7 +77,7 @@ class Button extends PureComponent {
if (!Zotero.isNode && Zotero.isLinux) { if (!Zotero.isNode && Zotero.isLinux) {
return this.props.isMenu && <span className="menu-marker"/> return this.props.isMenu && <span className="menu-marker"/>
} }
return this.props.isMenu && <IconDownChevron className="menu-marker"/> return this.props.isMenu && <CSSIcon name="chevron-6" className="menu-marker icon-8"/>
} }
get attributes() { get attributes() {

View file

@ -29,7 +29,7 @@ const React = require('react');
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
const TagList = require('./tagSelector/tagSelectorList'); const TagList = require('./tagSelector/tagSelectorList');
const { Button } = require('./button'); const { Button } = require('./button');
const { IconTagSelectorMenu } = require('./icons'); const { CSSIcon } = require('./icons');
const Search = require('./search'); const Search = require('./search');
class TagSelector extends React.PureComponent { class TagSelector extends React.PureComponent {
@ -46,21 +46,26 @@ class TagSelector extends React.PureComponent {
width={this.props.width} width={this.props.width}
height={this.props.height} height={this.props.height}
fontSize={this.props.fontSize} fontSize={this.props.fontSize}
lineHeight={this.props.lineHeight}
uiDensity={this.props.uiDensity}
/> />
<div className="tag-selector-filter-container"> <div className="tag-selector-filter-pane">
<Search <div className="tag-selector-filter-container">
ref={this.props.searchBoxRef} <Search
value={this.props.searchString} ref={this.props.searchBoxRef}
onSearch={this.props.onSearch} value={this.props.searchString}
className="tag-selector-filter" onSearch={this.props.onSearch}
/> className="tag-selector-filter"
<Button data-l10n-id="tagselector-search"
icon={<IconTagSelectorMenu />} />
title="zotero.toolbar.actions.label" <Button
className="tag-selector-actions" icon={<CSSIcon name="filter" className="icon-16" />}
isMenu title="zotero.toolbar.actions.label"
onMouseDown={ev => this.props.onSettings(ev)} className="tag-selector-actions"
/> isMenu
onMouseDown={ev => this.props.onSettings(ev)}
/>
</div>
</div> </div>
</div> </div>
); );
@ -88,6 +93,8 @@ TagSelector.propTypes = {
width: PropTypes.number.isRequired, width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired, height: PropTypes.number.isRequired,
fontSize: PropTypes.number.isRequired, fontSize: PropTypes.number.isRequired,
lineHeight: PropTypes.number.isRequired,
uiDensity: PropTypes.string.isRequired,
// Search // Search
searchBoxRef: PropTypes.object, searchBoxRef: PropTypes.object,

View file

@ -26,21 +26,21 @@
const React = require('react'); const React = require('react');
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
var { Collection } = require('react-virtualized'); var { Collection } = require('react-virtualized');
const { props } = require("bluebird");
// See also .tag-selector-item in _tag-selector.scss // See also .tag-selector-item in _tag-selector.scss
var filterBarHeight = 32; var filterBarHeight = 36;
var tagPaddingTop = 4; var tagPaddingLeft = 4;
var tagPaddingLeft = 2; var tagPaddingRight = 4;
var tagPaddingRight = 2; var tagSpaceBetweenX = 2;
var tagPaddingBottom = 4;
var tagSpaceBetweenX = 7;
var tagSpaceBetweenY = 4; var tagSpaceBetweenY = 4;
var panePaddingTop = 2; var panePaddingTop = 8;
var panePaddingLeft = 2; var panePaddingLeft = 8;
var panePaddingRight = 25; var panePaddingRight = 2; // + scrollbar width
//var panePaddingBottom = 2; //var panePaddingBottom = 2;
var minHorizontalPadding = panePaddingLeft + tagPaddingLeft + tagPaddingRight + panePaddingRight; var minHorizontalPadding = panePaddingLeft + tagPaddingLeft + tagPaddingRight + panePaddingRight;
class TagList extends React.PureComponent { class TagList extends React.PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
@ -53,11 +53,13 @@ class TagList extends React.PureComponent {
// Redraw all tags on every refresh // Redraw all tags on every refresh
if (this.collectionRef && this.collectionRef.current) { if (this.collectionRef && this.collectionRef.current) {
// If width or height changed, recompute positions. It seems like this should happen // If width or height changed, recompute positions. It seems like this should happen
// automatically, but it doesn't as of 9.21.0. // automatically, but it doesn't as of 9.21.0. Also check for density change.
if (prevProps.height != this.props.height if (prevProps.height != this.props.height
|| prevProps.width != this.props.width || prevProps.width != this.props.width
|| prevProps.fontSize != this.props.fontSize || prevProps.lineHeight != this.props.lineHeight
|| prevProps.tags != this.props.tags) { || prevProps.tags != this.props.tags
|| prevProps.uiDensity !== this.props.uiDensity) {
this.collectionRef.current.recomputeCellSizesAndPositions(); this.collectionRef.current.recomputeCellSizesAndPositions();
} }
// If dimensions didn't change, just redraw at current positions. Without this, clicking // If dimensions didn't change, just redraw at current positions. Without this, clicking
@ -94,8 +96,12 @@ class TagList extends React.PureComponent {
* Calculate the x,y coordinates of all tags * Calculate the x,y coordinates of all tags
*/ */
updatePositions() { updatePositions() {
var tagMaxWidth = this.props.width - minHorizontalPadding; const tagPaddingTop = this.props.uiDensity === 'comfortable' ? 2 : 1;
var rowHeight = tagPaddingTop + this.props.fontSize + tagPaddingBottom + tagSpaceBetweenY; const tagPaddingBottom = tagPaddingTop;
this.scrollbarWidth = Zotero.Utilities.Internal.getScrollbarWidth();
var tagMaxWidth = this.props.width - minHorizontalPadding - this.scrollbarWidth;
var rowHeight = tagPaddingTop + this.props.lineHeight + tagPaddingBottom + tagSpaceBetweenY;
var positions = []; var positions = [];
var row = 0; var row = 0;
let rowX = panePaddingLeft; let rowX = panePaddingLeft;
@ -113,9 +119,11 @@ class TagList extends React.PureComponent {
shouldAddSeparator = true; shouldAddSeparator = true;
forceNewLine = true; forceNewLine = true;
} }
let tagWidth = tagPaddingLeft + Math.min(tag.width, tagMaxWidth) + tagPaddingRight; // size of the colored dot + space between the dot and the tag name always sums up to fontSize (e.g., 8px + 3px at 11px fontSize)
const tagColorWidth = (tag.color && !Zotero.Utilities.Internal.isOnlyEmoji(tag.name)) ? this.props.fontSize : 0;
let tagWidth = tagPaddingLeft + Math.min(tag.width, tagMaxWidth) + tagPaddingRight + tagColorWidth;
// If first row or cell fits, add to current row // If first row or cell fits, add to current row
if (!forceNewLine && (i == 0 || ((rowX + tagWidth) < (this.props.width - panePaddingLeft - panePaddingRight)))) { if (!forceNewLine && (i == 0 || ((rowX + tagWidth) < (this.props.width - panePaddingRight - this.scrollbarWidth)))) {
positions[i] = [rowX, panePaddingTop + (row * rowHeight)]; positions[i] = [rowX, panePaddingTop + (row * rowHeight)];
} }
// Otherwise, start new row // Otherwise, start new row
@ -135,10 +143,10 @@ class TagList extends React.PureComponent {
} }
cellSizeAndPositionGetter = ({ index }) => { cellSizeAndPositionGetter = ({ index }) => {
var tagMaxWidth = this.props.width - minHorizontalPadding; var tagMaxWidth = this.props.width - minHorizontalPadding - this.scrollbarWidth;
return { return {
width: Math.min(this.props.tags[index].width, tagMaxWidth), width: Math.min(this.props.tags[index].width, tagMaxWidth),
height: this.props.fontSize, height: this.props.lineHeight,
x: this.positions[index][0], x: this.positions[index][0],
y: this.positions[index][1] y: this.positions[index][1]
}; };
@ -149,7 +157,7 @@ class TagList extends React.PureComponent {
const { onDragOver, onDragExit, onDrop } = this.props.dragObserver; const { onDragOver, onDragExit, onDrop } = this.props.dragObserver;
var className = 'tag-selector-item zotero-clicky'; var className = 'tag-selector-item';
if (tag.selected) { if (tag.selected) {
className += ' selected'; className += ' selected';
} }
@ -159,6 +167,9 @@ class TagList extends React.PureComponent {
if (tag.disabled) { if (tag.disabled) {
className += ' disabled'; className += ' disabled';
} }
if (Zotero.Utilities.Internal.isOnlyEmoji(tag.name)) {
className += ' emoji';
}
let props = { let props = {
className, className,
@ -176,7 +187,7 @@ class TagList extends React.PureComponent {
// Don't specify explicit width unless we're truncating, because for some reason the width // Don't specify explicit width unless we're truncating, because for some reason the width
// from canvas can sometimes be slightly smaller than the actual width, resulting in an // from canvas can sometimes be slightly smaller than the actual width, resulting in an
// unnecessary ellipsis. // unnecessary ellipsis.
var tagMaxWidth = this.props.width - minHorizontalPadding; var tagMaxWidth = this.props.width - minHorizontalPadding - this.scrollbarWidth;
if (props.style.width < tagMaxWidth) { if (props.style.width < tagMaxWidth) {
delete props.style.width; delete props.style.width;
} }
@ -190,11 +201,12 @@ class TagList extends React.PureComponent {
if (tag.color) { if (tag.color) {
props.style.color = tag.color; props.style.color = tag.color;
props['data-color'] = tag.color.toLowerCase();
} }
return ( return (
<div key={tag.name} {...props}> <div key={tag.name} {...props}>
{tag.name} <span>{tag.name}</span>
</div> </div>
); );
} }
@ -265,6 +277,8 @@ class TagList extends React.PureComponent {
width: PropTypes.number.isRequired, width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired, height: PropTypes.number.isRequired,
fontSize: PropTypes.number.isRequired, fontSize: PropTypes.number.isRequired,
lineHeight: PropTypes.number.isRequired,
uiDensity: PropTypes.string.isRequired
}; };
} }

View file

@ -42,6 +42,9 @@ const defaults = {
}; };
const { Cc, Ci } = require('chrome'); const { Cc, Ci } = require('chrome');
// first n tags will be measured using DOM method for more accurate measurment (at the cost of performance)
const FORCE_DOM_TAGS_FOR_COUNT = 200;
Zotero.TagSelector = class TagSelectorContainer extends React.PureComponent { Zotero.TagSelector = class TagSelectorContainer extends React.PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
@ -50,7 +53,10 @@ Zotero.TagSelector = class TagSelectorContainer extends React.PureComponent {
['collection-item', 'item', 'item-tag', 'tag', 'setting'], ['collection-item', 'item', 'item-tag', 'tag', 'setting'],
'tagSelector' 'tagSelector'
); );
this._prefObserverID = Zotero.Prefs.registerObserver('fontSize', this.handleFontChange.bind(this)); this._prefObserverID = Zotero.Prefs.registerObserver('fontSize', this.handleUIPropertiesChange.bind(this));
this._prefObserverID = Zotero.Prefs.registerObserver('uiDensity', this.handleUIPropertiesChange.bind(this));
this._mediaQueryList = window.matchMedia("(min-resolution: 1.5dppx)");
this._mediaQueryList.addEventListener("change", this.handleUIPropertiesChange.bind(this));
this.tagListRef = React.createRef(); this.tagListRef = React.createRef();
this.searchBoxRef = React.createRef(); this.searchBoxRef = React.createRef();
@ -66,7 +72,8 @@ Zotero.TagSelector = class TagSelectorContainer extends React.PureComponent {
this.state = { this.state = {
...defaults, ...defaults,
...this.getContainerDimensions(), ...this.getContainerDimensions(),
...this.getFontInfo() ...this.getFontInfo(),
isHighDensity: this._mediaQueryList.matches
}; };
} }
@ -357,6 +364,7 @@ Zotero.TagSelector = class TagSelectorContainer extends React.PureComponent {
container.appendChild(elem); container.appendChild(elem);
var style = window.getComputedStyle(elem); var style = window.getComputedStyle(elem);
var props = { var props = {
lineHeight: style.getPropertyValue('line-height'),
fontSize: style.getPropertyValue('font-size'), fontSize: style.getPropertyValue('font-size'),
fontFamily: style.getPropertyValue('font-family') fontFamily: style.getPropertyValue('font-family')
}; };
@ -365,36 +373,61 @@ Zotero.TagSelector = class TagSelectorContainer extends React.PureComponent {
} }
/** /**
* Recompute tag widths based on the current font settings * Recompute tag widths when either font, UI density or pixel density changes
*/ */
handleFontChange() { handleUIPropertiesChange(ev) {
this.widths.clear(); this.widths.clear();
this.widthsBold.clear(); this.widthsBold.clear();
const isHighDensity = ev.target instanceof MediaQueryList ? ev.matches : this.state.isHighDensity;
this.setState({ this.setState({
...this.getFontInfo() ...this.getFontInfo(),
uiDensity: Zotero.Prefs.get('uiDensity'),
isHighDensity
}); });
} }
/** /**
* Uses canvas.measureText to compute and return the width of the given text of given font in pixels. * Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
* Except for emoji tags, where, on high-density screens, we use actual DOM element for more accurate
* measurement (which is 4-5x slower) because canvas method can be off by enough to cause visible artifacts.
* It's possible to force use of DOM method for other tags using forceUseDOM parameter.
* *
* @param {String} text The text to be rendered. * @param {String} text The text to be rendered.
* @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana"). * @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana").
* @param {String} forceUseDOM Force use of DOM method for measuring text width
* *
* @see https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393 * @see https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
*/ */
getTextWidth(text, font) { getTextWidth(text, font, forceUseDOM = false) {
// re-use canvas object for better performance let width;
var canvas = this.canvas || (this.canvas = document.createElement("canvas")); const useDOM = forceUseDOM || (this.state.isHighDensity && Zotero.Utilities.Internal.includesEmoji(text));
var context = canvas.getContext("2d"); if (useDOM) {
context.font = font; if (!this.divMeasure) {
// Add a little more to make sure we don't crop this.divMeasure = document.createElement('div');
var metrics = context.measureText(text); this.divMeasure.style.position = 'absolute';
return Math.ceil(metrics.width); this.divMeasure.style.top = '-9999px';
this.divMeasure.whiteSpace = 'nowrap';
document.querySelector('#zotero-tag-selector').appendChild(this.divMeasure);
}
this.divMeasure.style.font = font;
this.divMeasure.textContent = text;
width = this.divMeasure.clientWidth;
this.divMeasure.textContent = '';
}
else {
// re-use canvas object for better performance
var canvas = this.canvas || (this.canvas = document.createElement("canvas"));
var context = canvas.getContext("2d");
context.font = font;
var metrics = context.measureText(text);
width = metrics.width;
}
return width;
} }
getWidth(name) { getWidth(name, forceUseDOM = false) {
var num = 0;
var font = this.state.fontSize + ' ' + this.state.fontFamily; var font = this.state.fontSize + ' ' + this.state.fontFamily;
// Colored tags are shown in bold, which results in a different width // Colored tags are shown in bold, which results in a different width
var fontBold = 'bold ' + font; var fontBold = 'bold ' + font;
@ -402,8 +435,8 @@ Zotero.TagSelector = class TagSelectorContainer extends React.PureComponent {
let widths = hasColor ? this.widthsBold : this.widths; let widths = hasColor ? this.widthsBold : this.widths;
let width = widths.get(name); let width = widths.get(name);
if (width === undefined) { if (width === undefined) {
width = this.getTextWidth(name, hasColor ? fontBold : font); width = this.getTextWidth(name, hasColor ? fontBold : font, forceUseDOM);
//Zotero.debug(`Calculated ${hasColor ? 'bold ' : ''}width of ${width} for tag '${name}'`); // Zotero.debug(`Calculated ${hasColor ? 'bold ' : ''}width of ${width} for tag '${name}' using ${forceUseDOM ? 'DOM' : 'hybrid'} method`);
widths.set(name, width); widths.set(name, width);
} }
return width; return width;
@ -460,7 +493,7 @@ Zotero.TagSelector = class TagSelectorContainer extends React.PureComponent {
// Prepare tag objects for list component // Prepare tag objects for list component
//var d = new Date(); //var d = new Date();
var inTagColors = true; var inTagColors = true;
tags = tags.map((tag) => { tags = tags.map((tag, i) => {
let name = tag.tag; let name = tag.tag;
tag = { tag = {
name, name,
@ -480,10 +513,14 @@ Zotero.TagSelector = class TagSelectorContainer extends React.PureComponent {
if ((this.displayAllTags || inTagColors) && !this.state.scope.has(name)) { if ((this.displayAllTags || inTagColors) && !this.state.scope.has(name)) {
tag.disabled = true; tag.disabled = true;
} }
tag.width = this.getWidth(name); const forceUseDOM = this.state.isHighDensity && i < FORCE_DOM_TAGS_FOR_COUNT;
tag.width = this.getWidth(name, forceUseDOM);
return tag; return tag;
}); });
//Zotero.debug(`Prepared tags in ${new Date() - d} ms`); // clean up divMeasure, which might have been used for measuring emoji tags
this.divMeasure?.parentNode?.removeChild?.(this.divMeasure);
this.divMeasure = null;
// Zotero.debug(`Prepared ${tags.length} tags in ${new Date() - d} ms`);
return <TagSelector return <TagSelector
tags={tags} tags={tags}
searchBoxRef={this.searchBoxRef} searchBoxRef={this.searchBoxRef}
@ -498,6 +535,8 @@ Zotero.TagSelector = class TagSelectorContainer extends React.PureComponent {
width={this.state.width} width={this.state.width}
height={this.state.height} height={this.state.height}
fontSize={parseInt(this.state.fontSize.replace('px', ''))} fontSize={parseInt(this.state.fontSize.replace('px', ''))}
lineHeight={parseInt(this.state.lineHeight.replace('px', ''))}
uiDensity={Zotero.Prefs.get('uiDensity')}
/>; />;
} }

View file

@ -417,6 +417,12 @@ Zotero.Utilities.Internal = {
return !str.replace(re, ''); return !str.replace(re, '');
}, },
includesEmoji: function (str) {
// Remove emoji, Zero Width Joiner, and Variation Selector-16 and compare lengths
const re = /\p{Extended_Pictographic}|\u200D|\uFE0F/gu;
return str.replace(re, '').length !== str.length;
},
/** /**
* Display a prompt from an error with custom buttons and a callback * Display a prompt from an error with custom buttons and a callback
*/ */
@ -2304,6 +2310,21 @@ Zotero.Utilities.Internal = {
} }
return false; return false;
},
getScrollbarWidth() {
let document = Zotero.getMainWindow().document;
let scrollDiv = document.createElement('div');
scrollDiv.style.position = 'absolute';
scrollDiv.style.top = '-9999px';
scrollDiv.style.width = '50px';
scrollDiv.style.height = '50px';
scrollDiv.style.overflow = 'scroll';
document.documentElement.appendChild(scrollDiv);
const scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth;
document.documentElement.removeChild(scrollDiv);
return scrollbarWidth;
} }
} }

View file

@ -240,3 +240,6 @@ sidenav-related =
abstract-field = abstract-field =
.label = Add abstract… .label = Add abstract…
tagselector-search =
.placeholder = Filter Tags

View file

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.99998 1.70711C1.37001 1.07714 1.81618 0 2.70708 0H14.2929C15.1838 0 15.6299 1.07714 15 1.70711L9.99998 6.70711V12.7071L6.99998 15.7071V6.70711L1.99998 1.70711ZM14.2929 1L2.70708 1L7.99998 6.29289V13.2929L8.99998 12.2929V6.29289L14.2929 1Z" fill="#FFFFFF8C" />
</svg>

After

Width:  |  Height:  |  Size: 423 B

View file

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.99998 1.70711C1.37001 1.07714 1.81618 0 2.70708 0H14.2929C15.1838 0 15.6299 1.07714 15 1.70711L9.99998 6.70711V12.7071L6.99998 15.7071V6.70711L1.99998 1.70711ZM14.2929 1L2.70708 1L7.99998 6.29289V13.2929L8.99998 12.2929V6.29289L14.2929 1Z" fill="#00000080" />
</svg>

After

Width:  |  Height:  |  Size: 423 B

View file

@ -0,0 +1,3 @@
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.70711 2L1 2.70711L4 5.70711L7 2.70711L6.29289 2L4 4.29289L1.70711 2Z" fill="#FFFFFF8C"/>
</svg>

After

Width:  |  Height:  |  Size: 200 B

View file

@ -0,0 +1,3 @@
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.70711 2L1 2.70711L4 5.70711L7 2.70711L6.29289 2L4 4.29289L1.70711 2Z" fill="#00000080"/>
</svg>

After

Width:  |  Height:  |  Size: 200 B

View file

@ -88,3 +88,15 @@ $item-pane-sections: (
"tags": var(--accent-orange), "tags": var(--accent-orange),
"related": var(--accent-wood), "related": var(--accent-wood),
); );
$tagColorsLookup: (
'#ff6666': --tag-red,
'#ff8c19': --tag-orange,
'#999999': --tag-gray,
'#5fb236': --tag-green,
'#009980': --tag-teal,
'#2ea8e5': --tag-blue,
'#576dd9': --tag-indigo,
'#a28ae5': --tag-purple,
'#a6507b': --tag-plum,
);

View file

@ -8,7 +8,7 @@
font-style: normal; font-style: normal;
} }
#zotero-collections-pane { #zotero-collections-pane, #zotero-item-pane {
background: var(--material-sidepane); background: var(--material-sidepane);
} }

View file

@ -3,37 +3,31 @@
// -------------------------------------------------- // --------------------------------------------------
.btn { .btn {
font: { font: {
family: inherit; family: inherit;
size: inherit; size: inherit;
} }
line-height: inherit; line-height: inherit;
color: inherit; color: inherit;
text-align: center; text-align: center;
-moz-appearance: toolbarbutton; -moz-appearance: toolbarbutton;
&[disabled], &[disabled],
&.disabled { &.disabled {
opacity: $btn-disabled-opacity; opacity: $btn-disabled-opacity;
} }
} }
.btn-icon { .btn-icon {
.icon { .icon {
&:first-child { svg, img {
margin-left: -5px; vertical-align: middle;
} }
&:last-child { }
margin-right: -5px; span.menu-marker {
} -moz-appearance: toolbarbutton-dropdown;
svg, img { display: inline-block;
vertical-align: middle; margin-left: 4px;
} }
}
span.menu-marker {
-moz-appearance: toolbarbutton-dropdown;
display: inline-block;
margin-right: -5px;
}
} }

View file

@ -1,26 +1,49 @@
.icon > svg, .icon > img { .icon > svg, .icon > img {
width: 16px; width: 16px;
} }
.icon-bg { .icon-bg {
width: 16px; width: 16px;
height: 16px; height: 16px;
display: inline-block; display: inline-block;
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: contain; background-size: contain;
background-position: center; background-position: center;
vertical-align: middle; vertical-align: middle;
} }
.icon-css { .icon-css {
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
} }
.icon.icon-downchevron { .icon.icon-downchevron {
width: 7px !important; width: 7px !important;
} }
.icon { .icon {
-moz-appearance: none !important; -moz-appearance: none !important;
}
.icon-16 {
width: 16px;
height: 16px;
}
.icon-8 {
width: 8px;
height: 8px;
}
$-icons: (
filter: 16,
chevron-6: 8
);
@each $icon, $size in $-icons {
.icon-#{$icon} {
@include color-scheme using($color) {
@include svgicon($icon, $color, $size);
}
}
} }

View file

@ -136,11 +136,10 @@
.zotero-toolbar { .zotero-toolbar {
-moz-appearance: none; -moz-appearance: none;
}
#zotero-layout-switcher .zotero-toolbar {
background: var(--material-tabbar); background: var(--material-tabbar);
border-bottom: var(--material-panedivider); border-bottom: var(--material-panedivider);
} }
#zotero-collections-tree > div, #zotero-item-pane {
background: var(--material-sidepane);
}
} }

View file

@ -4,10 +4,25 @@
} }
.search input { .search input {
background: var(--material-background);
border-radius: 5px;
border: var(--material-border-quinary);
color: var(--fill-primary);
flex: 1 0; flex: 1 0;
font-size: 1em; margin: 6px 4px;
min-width: 40px; min-width: 40px;
padding-left: 4px; padding: 3px 7px;
&::placeholder {
color: var(--fill-tertiary);
opacity: 1.0;
}
&:focus {
outline: none;
border-color: SelectedItem;
box-shadow: 0 0 0 2px SelectedItem;
}
} }
.search .search-cancel-button { .search .search-cancel-button {

View file

@ -28,7 +28,6 @@
display: flex; display: flex;
overflow: hidden; overflow: hidden;
height: 100px; height: 100px;
background: var(--material-sidepane);
} }
.tag-selector-list-container > div { .tag-selector-list-container > div {
@ -43,18 +42,22 @@
} }
.tag-selector-list { .tag-selector-list {
list-style: none;
display: inline-block; display: inline-block;
list-style: none;
margin: 0; margin: 0;
padding: 0; padding: 0;
scrollbar-color: var(--color-scrollbar) var(--color-scrollbar-background);
}
.tag-selector-filter-pane {
padding: 0 8px 0;
} }
.tag-selector-filter-container { .tag-selector-filter-container {
height: 30px; border-top: var(--material-panedivider);
flex: 0 0 1em;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
padding: 0.125em 0 0.125em 0.5em; flex: 0 0 1em;
} }
.tag-selector-filter-container .search { .tag-selector-filter-container .search {
@ -63,21 +66,79 @@
} }
.tag-selector-actions { .tag-selector-actions {
flex: 0 1; align-self: center;
display: block;
white-space: nowrap;
background-color: inherit; background-color: inherit;
border: 0;
display: block;
flex: 0 1;
padding: 3px 4px;
white-space: nowrap;
} }
.tag-selector-item { .tag-selector-item {
border-radius: 4px;
cursor: pointer; cursor: pointer;
font-size: 0.916666667em;
line-height: 1.272727273;
overflow: hidden; overflow: hidden;
padding: 1px 4px;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: pre; white-space: pre;
padding: 1px 4px 3px; // See also TagSelectorList.jsx
@include comfortable {
padding: 2px 4px;
}
&:hover {
background-color: var(--fill-quinary);
}
&.selected {
background-color: var(--fill-secondary);
&:hover {
opacity: .75;
}
}
&.colored { &.colored {
font-weight: bold; font-weight: bold;
&.selected {
background-color: currentcolor;
@each $colorHex, $colorVar in $tagColorsLookup {
@include state('.tag-selector-item[data-color="#{$colorHex}"]') {
background-color: var($colorVar);
}
}
}
&:not(.emoji) {
&::before {
content: " ";
display: inline-block;
width: 0.636363636em; // 7px (+ 1px border = 8px)
height: 0.636363636em;
margin-right: .272727273em;
border-radius: 50%;
background-color: currentcolor; // fallback for non-standard colors
vertical-align: -0.363636364em; // -4px
border: var(--material-border-transparent);
@include state('.tag-selector-item.selected') {
border-color: var(--color-background);
}
}
@each $colorHex, $colorVar in $tagColorsLookup {
@include state('.tag-selector-item[data-color="#{$colorHex}"]') {
&::before {
background-color: var($colorVar);
}
}
}
}
} }
&.disabled { &.disabled {
@ -89,4 +150,12 @@
color: var(--color-background); color: var(--color-background);
background-color: var(--fill-secondary); background-color: var(--fill-secondary);
} }
span {
color: var(--fill-primary);
@include state('.tag-selector-item.selected') {
color: var(--color-background);
}
}
} }

View file

@ -1,6 +1,5 @@
.search input { .search input {
-moz-appearance: searchfield; -moz-appearance: searchfield;
height: 24px;
} }
.search .search-cancel-button { .search .search-cancel-button {

View file

@ -2,18 +2,4 @@
// Tag selector // Tag selector
// -------------------------------------------------- // --------------------------------------------------
.tag-selector-filter-container {
padding: 0.25em 0 0.25em 0.5em;
border-top: var(--material-panedivider);
}
.tag-selector-item {
padding-bottom: .3em;
}
.tag-selector-actions {
flex: none;
border: 0;
margin-right: 3px;
padding: 1px 6px 0;
}

View file

@ -31,6 +31,9 @@
--color-sidepane: #303030; --color-sidepane: #303030;
--color-tabbar: #1e1e1e; --color-tabbar: #1e1e1e;
--color-toolbar: #272727; --color-toolbar: #272727;
--color-scrollbar: rgb(117, 117, 117);
--color-scrollbar-hover: rgb(158, 158, 158);
--color-scrollbar-background: transparent;
--tag-blue: #55a6dfd9; --tag-blue: #55a6dfd9;
--tag-gray: #aaac; --tag-gray: #aaac;
--tag-green: #74b04ad9; --tag-green: #74b04ad9;
@ -59,5 +62,6 @@
--material-border: 1px solid var(--color-border); --material-border: 1px solid var(--color-border);
--material-border50: 1px solid var(--color-border50); --material-border50: 1px solid var(--color-border50);
--material-panedivider: 1px solid var(--color-panedivider); --material-panedivider: 1px solid var(--color-panedivider);
--material-border-quinary: 1px solid var(--fill-quinary);
} }
} }

View file

@ -31,6 +31,9 @@
--color-sidepane: #f2f2f2; --color-sidepane: #f2f2f2;
--color-tabbar: #f2f2f2; --color-tabbar: #f2f2f2;
--color-toolbar: #f9f9f9; --color-toolbar: #f9f9f9;
--color-scrollbar: rgb(194, 194, 194);
--color-scrollbar-hover: rgb(125, 125, 125);
--color-scrollbar-background: transparent;
--tag-blue: #55a6df; --tag-blue: #55a6df;
--tag-gray: #aaa; --tag-gray: #aaa;
--tag-green: #74b04a; --tag-green: #74b04a;
@ -59,5 +62,6 @@
--material-border: 1px solid var(--color-border); --material-border: 1px solid var(--color-border);
--material-border50: 1px solid var(--color-border50); --material-border50: 1px solid var(--color-border50);
--material-panedivider: 1px solid var(--color-panedivider); --material-panedivider: 1px solid var(--color-panedivider);
--material-border-quinary: 1px solid var(--fill-quinary);
} }
} }