zotero/scss/components/_item-tree.scss
Dan Stillman d54327a045
Some checks failed
CI / Test (shard 1) (push) Has been cancelled
CI / Test (shard 2) (push) Has been cancelled
CI / Test (shard 3) (push) Has been cancelled
CI / Test (shard 4) (push) Has been cancelled
CI / Utilities Tests (push) Has been cancelled
CI / Build, Upload (push) Has been cancelled
Fix retracted-item icon spacing in items list
The gap after the icon came from whitespace baked into the legacy
cross.png, so it disappeared when the icon switched to the
edge-to-edge cross.svg and the title abutted the icon.
2026-08-01 10:42:29 -04:00

497 lines
12 KiB
SCSS

@use "sass:map";
#zotero-items-pane-container {
flex-grow: 1;
flex-shrink: 1;
min-width: $min-width-items-pane;
height: 150px;
min-height: $min-height-items-pane;
}
#zotero-items-pane {
min-width: $min-width-items-pane;
}
#zotero-items-tree {
.virtualized-table-body, .drag-image-container {
padding: 4px 8px 8px;
}
// With sticky section headers, the body can't have top padding: overflow clips at the
// padding box, so rows would scroll up into the padding and show above the pinned header
// (which sticks to the content-box top). Drop the top padding so rows clip exactly where the
// header pins, and keep the gap below the column header as a margin -- outside the scroll
// area, where rows can't reach it.
.virtualized-table-body.has-sticky-section-headers {
padding-top: 0;
margin-top: 4px;
}
.virtualized-table-body {
scrollbar-color: var(--color-scrollbar) var(--color-scrollbar-background);
}
.virtualized-table-header {
padding-inline-start: 8px;
padding-inline-end: calc(8px + var(--scrollbar-width, 0px));
box-sizing: border-box;
// A clear divider below the column headers (the default faint border is lost
// against the white section-header/spacer rows below it). The header already
// carries a second 1px line via ::after, so drop that one to avoid doubling.
border-bottom: var(--material-panedivider);
&::after {
border-bottom: none;
}
.cell.hasAttachment,
.cell.numNotes {
padding: 0;
text-align: center;
}
.cell:first-child {
&::before {
content: "";
display: inline-block;
width: 8px;
height: 100%;
background: transparent;
position: absolute;
inset-inline-start: -8px;
@include state(".cell:hover") {
background: var(--material-mix-quinary);
}
@include state(".cell.dragging") {
background: var(--material-mix-quarternary);
}
}
}
.cell:last-child {
&::after {
content: "";
display: inline-block;
width: calc(var(--scrollbar-width, 0px) + 8px);
height: 100%;
background: transparent;
position: absolute;
inset-inline-end: calc(-1 * var(--scrollbar-width, 0px) - 8px);
@include state(".cell:hover") {
background: var(--material-mix-quinary);
}
@include state(".cell.dragging") {
background: var(--material-mix-quarternary);
}
}
}
}
.virtualized-table, .drag-image-container {
.row {
// Header and spacer rows are always the pane background, never striped
&.odd:not(.selected):not(.library-header-row):not(.spacer-row) {
background-color: var(--material-stripe);
}
&.even:not(.selected) {
background-color: var(--material-background);
}
&.selected {
border-radius: 0;
&.first-selected {
border-top-left-radius: 5px;
border-top-right-radius: 5px;
}
&.last-selected {
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
}
}
.colored-tag-swatches {
display: flex;
:root:-moz-locale-dir(rtl) & {
transform: scaleX(-1);
flex-direction: row-reverse;
}
}
.icon-item-type + .tag-swatch,
.icon-item-type + .colored-tag-swatches,
.annotation-icon + .tag-swatch,
.annotation-icon + .colored-tag-swatches,
.retracted + .tag-swatch,
.retracted + .colored-tag-swatches {
margin-inline-end: 4px;
}
.emoji + .emoji,
.emoji + .colored-tag-swatches,
.colored-tag-swatches + .emoji {
margin-inline-end: 4px;
}
.tag-swatch {
display: inline-block;
&.colored {
-moz-context-properties: fill, fill-opacity, stroke, stroke-opacity;
width: 0.92307692em;
height: 0.92307692em;
background:
url("chrome://zotero/skin/tag-circle.svg") no-repeat center/100%,
url("chrome://zotero/skin/tag-circle-border.svg") no-repeat center/0;
fill: currentcolor;
@each $colorHex, $colorVar in $tagColorsLookup {
&[data-color="#{$colorHex}"] {
fill: var($colorVar);
}
}
@include focus-states('.row.selected', '.virtualized-table:focus-within') using($color) {
@if $color =="white" {
background: url("chrome://zotero/skin/tag-circle-border.svg") no-repeat center/contain;
stroke: var(--accent-white);
}
}
}
&.colored+.colored {
background:
url("chrome://zotero/skin/tag-crescent.svg") no-repeat center/100%,
url("chrome://zotero/skin/tag-crescent-border.svg") no-repeat center/0;
// RTL: Parent is flipped, so margin can always be on logical left
margin-left: -0.38461538em;
@include focus-states('.row.selected', '.virtualized-table:focus-within') using($color) {
@if $color =="white" {
background: url("chrome://zotero/skin/tag-crescent-border.svg") no-repeat center/contain;
}
}
}
}
.attachment-progress {
appearance: none;
background-color: transparent;
background-image: conic-gradient(var(--color-accent) var(--progress), var(--fill-quarternary) var(--progress), var(--fill-quarternary) 100%);
mask-image: radial-gradient(circle at center, transparent 5px, black 6px);
border-radius: 8px;
border: none;
display: block;
height: 16px;
width: 16px;
@include state(".row.selected") {
background-image: conic-gradient(var(--color-accent-text) var(--progress), transparent var(--progress));
background-image: conic-gradient(var(--accent-white) var(--progress), #ffffff33 var(--progress), #ffffff33 100%);
@include state(".virtualized-table:not(:focus-within)") {
background-image: conic-gradient(var(--color-accent) var(--progress), var(--fill-quarternary) var(--progress), var(--fill-quarternary) 100%);
}
}
&::-moz-progress-bar {
appearance: none;
background: transparent;
}
}
}
.cell.hasAttachment {
height: 100%;
// Don't show ellipsis
text-overflow: unset;
}
.cell.primary {
.retracted {
width: 12px;
margin-inline-start: 3px;
margin-inline-end: 4px;
}
}
.cell.hasAttachment {
text-overflow: unset;
align-items: center;
display: flex;
justify-content: center;
.icon-treeitemattachmentpdf {
background-size: 10px
}
.icon-missing-file {
opacity: 0.4;
}
}
// Section header above each library's items in a cross-library selection. These
// rows are pinned to the top while their section is scrolled (see the sticky
// section header support in virtualized-table), so they're a uniform height with
// top-aligned content. The background matches the pane so the items below stripe
// against it; square corners keep the on-scroll hairline from curving at the edges.
.library-header-row {
background-color: var(--material-background);
border-radius: 0;
.cell.library-header {
display: flex;
align-items: center;
padding-bottom: 3px;
font-weight: 600;
color: var(--fill-secondary);
.icon-item-type {
margin-inline-end: 4px;
opacity: 0.8;
}
// Stands in for a missing icon (single-library summary header) so the
// label still aligns with the item titles below it
.library-header-icon-spacer {
width: 16px;
margin-inline-end: 4px;
flex-shrink: 0;
}
}
}
// The divider under a section header shows only while its section is scrolled
// under the pinned copy (see the "stuck" state in virtualized-table). Drawn as a
// box-shadow rather than a border so toggling it doesn't shift the row's content.
.virtualized-table-sticky-section-header.stuck .library-header-row {
box-shadow: inset 0 -1px 0 var(--color-panedivider);
}
// Blank whitespace row above each library header (except the first); matches the
// pane background so it reads as empty space rather than a striped row
.spacer-row {
background-color: var(--material-background);
pointer-events: none;
}
.annotation-row {
.cell {
font-size: $font-size-small;
max-width: fit-content;
// Do not italicize CJK characters
font-synthesis: none;
&.title {
flex-grow: 1;
flex-basis: 0;
max-width: fit-content;
.cell-text::before {
content: attr(q-mark-open)
}
&::after {
content: attr(q-mark-close)
}
.italics {
font-style: italic;
}
}
&.annotation-comment {
flex-grow: 2;
flex-basis: 0;
}
}
&.tight .cell {
padding: 0 2px 0 0;
}
}
.cell:not(.hasAttachment) .item-icon {
margin-inline-end: 4px;
}
.cell .annotation-icon {
-moz-context-properties: fill;
}
.numNotes, .hasAttachment {
text-align: center;
.cell-text {
text-align: center;
}
}
}
}
item-tree-menu-bar[inactive] {
max-height: 0 !important;
overflow: hidden;
}
$-itemTypesIcons: (
artwork,
attachment-epub,
attachment-epub-link,
attachment-file, // mapped to document below
attachment-link,
attachment-pdf-link,
attachment-pdf,
attachment-snapshot,
attachment-web-link,
attachment-image,
attachment-image-link,
attachment-video,
attachment-video-link,
audio-recording,
bill,
blog-post,
book-section,
book,
case,
computer-program,
conference-paper,
dataset,
dictionary-entry,
document,
email,
encyclopedia-article,
epub,
film,
forum-post,
hearing,
instant-message,
interview,
journal-article,
letter,
magazine-article,
manuscript,
map,
newspaper-article,
note,
patent,
podcast,
preprint,
presentation,
radio-broadcast,
report,
standard,
statute,
thesis,
tv-broadcast,
video-recording,
webpage
);
$-itemTypesMap: (
"attachment-file": "document"
);
$-attachmentIcons: (
attachment-epub,
attachment-epub-link,
attachment-link,
attachment-pdf-link,
attachment-pdf,
attachment-snapshot,
attachment-web-link,
attachment-file,
attachment-image,
attachment-video
);
// Non-item objects that can appear in the trash
$-trashableObjectIcons: (
"collection",
"search"
);
// Few icons that appear in color in the item tree
$-coloredIcons: (
cross: --accent-red,
tick: --accent-green,
refresh: --fill-secondary,
plus-circle: --fill-secondary,
minus-circle: --fill-secondary
);
.icon-item-type {
width: 16px;
height: 16px;
}
.icon-attachment-type {
width: 12px;
height: 12px;
padding: 1px;
box-sizing: content-box;
}
// Due to quirks of the state() mixin, we need two sets of .icon-item-type rules:
// one when the icon is within a virtualized-table, and one when it isn't. We declare
// a mixin here to avoid duplication.
@mixin -icon-item-type-rules($color) {
.icon-item-type {
// default icon, for known item types more specific selectors below will apply
@include svgicon("document", $color, "16", "item-type", true);
}
@each $itemTypeIcon in $-itemTypesIcons {
$itemType: camelCase(str-replace(str-replace($itemTypeIcon, "pdf", "PDF"), "epub", "EPUB"));
@if map.has-key($-itemTypesMap, $itemTypeIcon) {
$itemTypeIcon: map.get($-itemTypesMap, $itemTypeIcon);
}
.icon-item-type[data-item-type=#{$itemType}] {
@include svgicon($itemTypeIcon, $color, "16", "item-type", true);
}
}
}
// Universal icon rules
@include focus-states using($color) {
@include -icon-item-type-rules($color);
}
// Selection and focus aware icon rules
.focus-states-target .row {
@include focus-states using($color) {
@include -icon-item-type-rules($color);
@each $itemAttachmentIcon in $-attachmentIcons {
$itemType: camelCase(str-replace(str-replace($itemAttachmentIcon, "pdf", "PDF"), "epub", "EPUB"));
.icon-attachment-type[data-item-type=#{$itemType}] {
@include svgicon($itemAttachmentIcon, $color, "12", "item-type", false);
background-origin: content-box;
}
}
@each $icon in $-trashableObjectIcons {
.icon-css.icon-#{$icon} {
@include svgicon($icon, $color, "16", "collection-tree");
}
}
@each $icon, $colorVar in $-coloredIcons {
.icon-#{$icon} {
@include svgicon($icon, "universal", "16");
@if $color =="white" {
fill: var(--accent-white);
}
@else {
fill: var($colorVar);
}
}
}
}
}