Files
upstream-zulip/web/styles/left_sidebar.css
upstream-mirror 64898b2c79
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
2026-09-15 15:05:30 +05:00

2745 lines
80 KiB
CSS

.left-sidebar-title {
color: var(--color-text-sidebar-heading);
opacity: var(--opacity-sidebar-heading);
transition: opacity 140ms linear;
font-size: inherit;
font-weight: var(--font-weight-sidebar-heading);
letter-spacing: var(--letter-spacing-sidebar-heading);
line-height: 20px;
/* Override heading margin from Bootstrap. */
margin: 0;
/* Show an ellipsis on a heading when
it won't sit adjacent other icons
or controls in the row. */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#left-sidebar .input-element-wrapper:not(.left-sidebar-search-section),
#user-profile-modal .stream-list-container .input-element-wrapper,
#user-profile-modal .group-list-container .input-element-wrapper {
/* We normalize this based on Firefox's 31px-tall input,
raising Chrome's (28px) and Safari's (30px) to all
match, so that `top:` and other offset values are
identical relative to input elements. This value
is taken up by the grid-template definition in
inputs.css. We don't do this on the sidebar search, though,
so that it matches the height of the filter at the top of the
right sidebar. Normalizing the height isn't essential here,
as there are no sticky headers offset from the filter input. */
--input-element-row-height: 1.9375em;
.input-element {
/* And to keep the visual space of the input identical,
we stretch to the height of the grid row above. */
align-self: stretch;
}
}
#left-sidebar .unread_count {
user-select: none;
}
.sidebar-topic-check,
.topic-markers-and-unreads {
cursor: pointer;
}
.visibility-policy-icon {
display: flex;
align-items: center;
&:focus {
outline: 0;
}
&:focus-visible {
outline: 2px solid var(--color-outline-focus);
outline-offset: 1px;
}
}
#left-sidebar-navigation-list .filter-icon i {
color: var(--color-left-sidebar-navigation-icon);
}
#stream_filters,
#left-sidebar-navigation-list {
margin-right: var(--left-sidebar-right-margin);
}
.add-stream-icon-container {
grid-area: add-channel;
display: grid;
margin: 2px;
border-radius: 3px;
color: var(--color-sidebar-action);
/* The section-header variant is an <a>, so without this rule the
generic `a:focus` rule in zulip.css would shift the icon to the
interactive link color when focused. Re-state the default color
here so it stays consistent with the <span>/<div> sibling
buttons. */
&:focus,
&:focus-visible {
color: var(--color-sidebar-action);
}
&:hover {
color: var(--color-sidebar-action-hover);
background-color: var(--color-background-sidebar-action-hover);
cursor: pointer;
}
&:focus-visible {
outline: 2px solid var(--color-outline-focus);
outline-offset: -2px;
/* `!important` overrides the `.dark-theme:root
a:not(:active):focus` rule in dark_theme.css, which would
otherwise set this anchor's focus outline to grey. */
outline-color: var(--color-outline-focus) !important;
}
.add_stream_icon {
/* We display as flex to center the icon while
keeping the entire highlighted area clickable. */
display: flex;
place-items: center center;
&::before {
/* To horizontally center the icon within the
flex, though, we need this margin adjustment. */
margin: 0 auto;
}
}
}
.masked_unread_count {
/* 8px at 16px/14em */
font-size: 0.5em;
display: none;
/* Masked unreads display as flex when revealed. */
align-items: center;
justify-content: center;
color: var(--color-unread-counter-muted);
width: var(--left-sidebar-single-digit-unread-width);
}
#left-sidebar-navigation-list .selected-home-view {
&.hide-unread-messages-count {
.masked_unread_count {
display: flex;
grid-area: markers-and-unreads;
/* Adding margin-right aligns the .masked_unread_count */
/* with the rest of the masked unread counts. */
margin-right: var(--left-sidebar-unread-offset);
justify-self: end;
visibility: visible;
}
.unread_count {
visibility: hidden;
}
/* When message summary count is 0, we dont want to show unread_count or masked_unread_count. */
.unread_count.hide + .masked_unread_count {
visibility: hidden;
}
/* Suppress the unread-count dot and show
the unread count on hover. */
&:hover {
.masked_unread_count {
visibility: hidden;
}
.unread_count:not(:empty) {
visibility: visible;
}
}
}
}
#stream_filters {
overflow: visible;
margin-bottom: 5px;
padding: 0;
font-weight: normal;
.topic_search_section {
margin: 3px 0;
}
& li {
& .sidebar-topic-name:hover {
text-decoration: none;
}
& ul {
margin-left: 0;
&.topic-list li {
padding: 0;
}
}
}
.stream-with-count.hide_unread_counts,
.stream-list-subsection-header.hide_unread_counts,
.show-inactive-or-muted-channels.hide_unread_counts {
.masked_unread_count {
display: flex;
}
.unread_count {
display: none;
}
/* When count is 0, we dont want to show unread_count or masked_unread_count. */
.unread_count.hide + .masked_unread_count {
display: none;
}
&:hover {
.masked_unread_count {
display: none;
}
.unread_count:not(.hide) {
display: inline;
}
}
}
.narrow-filter
> .bottom_left_row:hover
> .stream-with-count.hide_unread_counts {
.masked_unread_count {
display: none;
}
.unread_count {
display: inline;
}
}
.stream-expanded {
.channel-new-topic-button,
.channel-search-topics-button {
display: flex;
}
.subscription_block .sidebar-menu-icon {
display: flex;
color: var(--color-vdots-visible);
}
.stream-with-count.hide_unread_counts {
.masked_unread_count {
display: none;
}
.unread_count {
display: inline;
}
}
}
.toggle_stream_mute {
margin-right: 3px;
opacity: 0.5;
&:hover {
opacity: 1;
}
}
}
.left-sidebar-navigation-icon-container,
.left-sidebar-navigation-label-container {
&:hover {
text-decoration: none;
}
}
.direct-message-section-bottom-divider-container {
display: block;
margin-right: var(--left-sidebar-right-margin);
padding: var(--left-sidebar-direct-messages-bottom-divider-padding) 0;
position: sticky;
top: var(--line-height-sidebar-row-prominent);
background: var(--color-background);
z-index: 3;
}
.direct-message-section-bottom-divider {
opacity: 1 !important; /* override .dark-theme:root hr */
border-color: var(--color-left-sidebar-direct-messages-bottom-divider);
border-width: 0.5px;
margin: 0;
}
#left-sidebar-modal-content #direct-messages-modal,
#left-sidebar-modal-content #more-topics-modal {
display: grid;
grid-template:
"section-header" minmax(0, max-content)
"conversation-list" minmax(0, 1fr) / minmax(0, 1fr);
max-height: 100%;
}
#left-sidebar-modal-content #direct-messages-modal {
#direct-messages-modal-section-header,
.dm-list {
margin-right: var(--left-sidebar-right-margin);
}
.dm-list {
margin-bottom: var(--left-sidebar-bottom-scrolling-buffer);
}
&.no-display {
display: none;
}
}
#direct-messages-modal-section-header,
#direct-messages-section-header {
grid-template-columns:
0 var(--left-sidebar-header-icon-toggle-width) 0 minmax(0, 1fr)
minmax(0, max-content) minmax(0, max-content) var(
--left-sidebar-vdots-width
)
0;
cursor: pointer;
white-space: nowrap;
position: sticky;
top: 0;
z-index: 3;
transition: box-shadow 150ms ease-out;
/* The active direct messages section has a different background color */
&:not(.active-direct-messages-section) {
background-color: var(--color-background);
}
&.sidebar-header-drop-shadow {
box-shadow: var(--box-shadow-sidebar-sticky-header-drop);
}
/* Combine hover border with drop shadow since CSS box-shadow doesn't stack. */
&:not(.zoom-in):hover.sidebar-header-drop-shadow {
box-shadow:
inset 0 0 0 1px var(--color-shadow-sidebar-row-hover),
var(--box-shadow-sidebar-sticky-header-drop);
}
#toggle-direct-messages-section-icon {
grid-area: starting-anchor-element;
/* Horizontally center the icon in its allotted grid area. */
justify-self: center;
}
.left-sidebar-title {
grid-area: row-content;
}
.heading-markers-and-unreads {
grid-area: markers-and-unreads;
display: flex;
gap: 5px;
align-items: center;
/* Extra margin for unreads. */
margin-right: var(--left-sidebar-unread-offset);
&:has(.unread_count:empty) {
margin-right: 0;
}
}
.compose-new-direct-message,
.show-all-direct-messages {
color: var(--color-sidebar-action);
display: flex;
align-items: center;
justify-content: center;
text-decoration: none;
margin: 2px 0;
border-radius: 3px;
grid-row: 1 / 1;
&:hover {
color: var(--color-sidebar-action-hover);
background-color: var(--color-background-sidebar-action-hover);
}
}
}
#direct-messages-section-header {
display: grid;
align-items: center;
/* This extends the general pattern of left sidebar rows, but includes a
second grid row for placing filter boxes. */
grid-template-areas:
"starting-offset starting-anchor-element icon-content-gap row-content controls markers-and-unreads ending-anchor-element ending-offset"
"filter-container filter-container filter-container filter-container filter-container filter-container filter-container filter-container";
grid-template-rows: var(--line-height-sidebar-row-prominent) minmax(
0,
max-content
);
&:hover,
&.highlighted_row {
border-radius: 4px;
}
&:hover {
background-color: var(--color-background-hover-narrow-filter);
box-shadow: inset 0 0 0 1px var(--color-shadow-sidebar-row-hover);
.left-sidebar-title,
.sidebar-heading-icon {
opacity: var(--opacity-sidebar-heading-hover);
}
}
.compose-new-direct-message,
.show-all-direct-messages {
display: none;
@media (hover: none) {
display: flex;
}
}
&.hover-over-dm-section,
&:hover {
.compose-new-direct-message,
.show-all-direct-messages {
display: flex;
}
}
}
#direct-messages-modal-section-header {
display: grid;
align-items: center;
/* This is a simplified version of left sidebar rows, and includes a
second grid row for placing filter boxes. */
grid-template:
"starting-offset row-content controls markers-and-unreads ending-offset" var(
--line-height-sidebar-row-prominent
)
"filter-container filter-container filter-container filter-container filter-container"
minmax(0, max-content)
/ 0.6em minmax(0, 1fr) minmax(0, max-content) minmax(0, max-content)
var(--left-sidebar-vdots-width);
}
.spectator-view #left-sidebar-search {
grid-template-columns: minmax(0, 1fr);
}
#left-sidebar-search {
display: grid;
grid-template-areas: "filter-container add-channel channel-folders-option";
grid-template-columns:
minmax(0, 1fr) var(--left-sidebar-header-icon-width)
auto;
position: sticky;
top: 0;
background: var(--color-background);
/* Must be padding not margin so that the sticky headers don't show behind it */
padding: 0 var(--left-sidebar-right-margin)
var(--sidebar-filter-bottom-spacing) 5px;
&.no-display {
display: none;
}
.channel-folders-sidebar-menu-icon {
grid-area: channel-folders-option;
display: grid;
/* width excluding left & right margin */
width: calc(var(--left-sidebar-vdots-width) - 3px);
cursor: pointer;
place-items: center center;
border-radius: 3px;
margin: 2px 2px 2px 1px;
color: var(--color-vdots-visible);
& i.zulip-icon {
font-size: 1.0625em;
}
&:hover {
color: var(--color-vdots-hover);
background-color: var(--color-background-sidebar-action-hover);
}
&:focus-visible {
outline: 2px solid var(--color-outline-focus);
outline-offset: -2px;
}
}
}
.stream-list-subsection-header {
display: grid;
align-items: center;
/* There's no three-dot menu here, but we still want the markers to line-up with
markers in other rows that do have three-dot menus. */
grid-template:
"arrow row-content add-channel markers-and-unreads three-dot-placeholder" var(
--line-height-sidebar-row-prominent
)
/ var(--right-sidebar-header-icon-toggle-width) minmax(0, 1fr) minmax(
0,
max-content
)
minmax(0, max-content) var(--left-sidebar-vdots-width);
cursor: pointer;
background-color: var(--color-background);
position: sticky;
/* height of DM header + 1px divider line and its top/bottom padding */
top: calc(
var(--line-height-sidebar-row-prominent) + 1px + 2 *
var(--left-sidebar-direct-messages-bottom-divider-padding)
);
/* Must be more than .sidebar-topic-check and less than #left-sidebar-search
and #direct-messages-section-header */
z-index: 2;
color: var(--color-text-default);
/* There seems to be a bug where when the header returns to normal position after
being sticky, it's 0.5px below its actual position and thus overlaps the channel
below. This extra margin prevents that overlap. */
margin-bottom: 1px;
transition: box-shadow 150ms ease-out;
&.sidebar-header-drop-shadow {
box-shadow: var(--box-shadow-sidebar-sticky-header-drop);
}
.markers-and-unreads {
display: flex;
align-items: center;
}
.unread_count,
.masked_unread_count {
margin-right: var(--left-sidebar-unread-offset);
}
.add-stream-icon-container {
text-decoration: none;
grid-auto-columns: var(--left-sidebar-header-icon-width);
/* -4px for the 2px top and bottom padding in .add-stream-icon-container */
grid-template-rows: calc(
var(--line-height-sidebar-row-prominent) - 4px
);
}
.add_stream_icon {
visibility: hidden;
}
.add-stream-icon-container:focus-visible .add_stream_icon,
&:focus-within .add_stream_icon,
&.highlighted_row .add_stream_icon {
visibility: visible;
}
/* We only set the border radius on the hover/popover/highlighted states,
so as to prevent the background on highlighted channels
from bleeding through. */
&:hover,
&:has(.left_sidebar_menu_icon_visible),
&.highlighted_row {
border-radius: 4px;
}
&:hover {
background-color: var(--color-background-hover-narrow-filter);
box-shadow: inset 0 0 0 1px var(--color-shadow-sidebar-row-hover);
.left-sidebar-title,
.stream-list-section-toggle {
opacity: var(--opacity-sidebar-heading-hover);
}
.add_stream_icon {
visibility: visible;
}
/* Combine hover border with drop shadow since CSS box-shadow doesn't stack. */
&.sidebar-header-drop-shadow {
box-shadow:
inset 0 0 0 1px var(--color-shadow-sidebar-row-hover),
var(--box-shadow-sidebar-sticky-header-drop);
}
}
&:has(.left_sidebar_menu_icon_visible) {
box-shadow: inset 0 0 0 1px var(--color-shadow-sidebar-row-hover);
background-color: var(--color-background-hover-narrow-filter);
.left-sidebar-title,
.stream-list-section-toggle {
opacity: var(--opacity-sidebar-heading-hover);
}
}
.stream-list-section-toggle {
color: var(--color-text-sidebar-heading);
opacity: var(--opacity-sidebar-heading-icon);
justify-self: center;
}
}
.stream-list-toggle-inactive-or-muted-channels {
padding-left: var(--left-sidebar-toggle-width-offset);
.show-inactive-or-muted-channels,
.hide-inactive-or-muted-channels {
/* Override the action heading font size, so that em measurements
on child elements are sized properly */
font-size: var(--base-font-size-px);
/* We also want the count text to be the normal font style. */
font-variant: inherit;
padding: 0;
.stream-list-toggle-inactive-or-muted-channels-text {
font-size: var(--font-size-sidebar-action-heading);
font-variant: var(--font-variant-sidebar-action-heading);
overflow-x: hidden;
text-overflow: ellipsis;
}
}
}
.stream-list-section-container:not(.showing-inactive-or-muted) {
.inactive-or-muted-in-channel-folder:not(
.active-filter,
:has(.active-sub-filter)
) {
display: none;
}
.stream-list-toggle-inactive-or-muted-channels {
.hide-inactive-or-muted-channels {
display: none;
}
}
}
#streams_list.is_searching {
.show-inactive-or-muted-channels,
.hide-inactive-or-muted-channels {
display: none;
}
.inactive-or-muted-in-channel-folder {
display: block;
}
}
.stream-list-section-container.showing-inactive-or-muted {
.stream-list-toggle-inactive-or-muted-channels {
.show-inactive-or-muted-channels {
display: none;
}
}
}
/* Sections with inactive/muted channels have a button to toggle
showing them. If there's only one inactive/muted channel, and
it's currently highlighted (.stream-expanded), then we want to
always show it, which means there aren't any inactive/muted
channels left to toggle visibility for. So we should hide the
toggle button.
(Checking for any .inactive-or-muted-in-channel-folder without
.stream-expanded in a .stream-list-section-container, and if none
then hide .stream-list-toggle-inactive-or-muted-channels). */
.stream-list-section-container:not(
:has(.inactive-or-muted-in-channel-folder:not(.stream-expanded))
) {
.stream-list-toggle-inactive-or-muted-channels {
display: none;
}
}
.show-inactive-or-muted-channels,
.hide-inactive-or-muted-channels {
display: grid;
grid-template:
"icon content markers-and-unreads three-dot-placeholder" auto
/ 1.5em minmax(0, 1fr) minmax(0, max-content) var(
--left-sidebar-vdots-width
);
align-items: center;
.markers-and-unreads {
margin-right: var(--left-sidebar-unread-offset);
}
.zulip-icon-collapse,
.zulip-icon-expand {
grid-area: icon;
/* 2px at 16px/1em to properly vertically center it */
margin-top: 0.125em;
}
.stream-list-toggle-inactive-or-muted-channels-text {
grid-area: content;
}
}
.stream-list-section {
margin: 0;
}
.stream-list-section-container.no-display {
display: none;
}
#streams_list .stream-list-section-container.collapsed {
.narrow-filter:not(.stream-expanded),
.stream-list-toggle-inactive-or-muted-channels,
&.hide-topic-bracket ul.topic-list.topic-list-has-topics::before,
&.hide-topic-bracket ul.topic-list.topic-list-has-topics::after {
display: none;
}
}
/* Only show section header unread counts when the section is collapsed,
when the header is being hovered over, when keyboard focus is inside
the header, or when the header is the current arrow-key target. */
#streams_list .stream-list-section-container:not(.collapsed) {
.stream-list-subsection-header:not(
:hover,
:focus-within,
.highlighted_row
) {
.unread_count,
.masked_unread_count {
visibility: hidden;
}
}
.stream-list-subsection-header:not(:hover, :focus-within, .highlighted_row)
.unread_mention_info {
display: none;
}
}
.direct-messages-container.zoomed-out {
/* Properly offset all the grid rows
in the DM section. */
margin-right: var(--left-sidebar-right-margin);
}
.direct-messages-container,
#left-sidebar-modal-content {
& ul.dm-list {
list-style-type: none;
font-weight: 400;
margin-left: 0;
margin-bottom: 0;
line-height: var(--line-height-sidebar-row-prominent);
& li.dm-list-item {
.dm-box,
.dm-name {
text-decoration: none;
color: inherit;
outline: none;
}
.zulip-icon:not(.user-circle) {
color: var(--color-left-sidebar-dm-partners-icon);
vertical-align: -0.2em;
}
}
& li#show-more-direct-messages {
color: var(--color-text-sidebar-action-heading);
cursor: pointer;
/* The 'more conversations' line has no icons,
so vertically align the text with the unread
count, when one appears there. */
align-items: baseline;
.dm-name {
font-size: var(--font-size-sidebar-action-heading);
font-weight: var(--font-weight-sidebar-action-heading);
font-variant: var(--font-variant-sidebar-action-heading);
text-transform: var(--text-transform-sidebar-action-heading);
}
&:hover {
background-color: var(
--color-background-sidebar-action-heading-hover
);
.dm-name {
color: var(--color-text-sidebar-action-heading-hover);
}
}
.unread_count {
margin-top: 2px;
}
}
}
}
.zulip-icon-heading-triangle-right {
transition:
opacity 140ms linear,
rotate 140ms linear;
}
.zulip-icon-heading-triangle-right.rotate-icon-down {
rotate: 90deg;
}
.zulip-icon-heading-triangle-right.rotate-icon-right {
rotate: 0deg;
}
#toggle-direct-messages-section-icon,
#toggle-top-left-navigation-area-icon {
color: var(--color-text-sidebar-heading);
opacity: var(--opacity-sidebar-heading-icon);
&:focus {
outline: 0;
}
/* This renders an outline when the caret is reached
with the keyboard, although that is not at present
easily accomplished. */
&:focus-visible {
outline: 2px solid var(--color-outline-focus);
}
}
.stream-list-section-toggle {
&:focus {
outline: 0;
}
&:focus-visible {
outline: 2px solid var(--color-outline-focus);
}
}
/* Arrow-key sidebar navigation focuses the toggle icon inside a
section header row while also applying .highlighted_row to the row
itself. Suppress the inner outline in that case to avoid showing
two nested focus rings on the same row. The IDs are needed to win
over the ID-scoped :focus-visible rules on the Views and DM
toggle icons above. */
.highlighted_row #toggle-direct-messages-section-icon:focus-visible,
.highlighted_row #toggle-top-left-navigation-area-icon:focus-visible,
.highlighted_row .stream-list-section-toggle:focus-visible {
outline: 0;
}
.active-direct-messages-section {
background-color: var(--color-background-active-narrow-filter);
&#direct-messages-section-header {
border-radius: 4px 4px 0 0;
}
#direct-messages-list {
border-radius: 0 0 4px 4px;
}
}
.top_left_row,
.bottom_left_row {
/* Ensure a border radius on any interactive
state that might show a highlight. */
border-radius: 4px;
/* Always show the box-shadow outline on hover and
an open left-sidebar menu (vdots) */
&:hover,
&:has(.left_sidebar_menu_icon_visible) {
box-shadow: inset 0 0 0 1px var(--color-shadow-sidebar-row-hover);
}
/* However, only adjust the background colors outside of active
filters, whose colors are more intense in light and dark mode
than the hover-narrow-filter color values. */
&:not(.active-sub-filter) {
&:hover,
&:has(.left_sidebar_menu_icon_visible) {
background-color: var(--color-background-hover-narrow-filter);
.sidebar-topic-check {
background-color: var(--color-background-hover-narrow-filter);
}
}
}
&:has(.zoomed-new-topic):hover {
background: var(--color-background-sidebar-action-heading-hover);
}
&:has(.sidebar-topic-action-heading):hover {
background-color: var(--color-background-sidebar-action-heading-hover);
}
&:has(a.left-sidebar-navigation-label-container:focus-visible) {
outline: 2px solid var(--color-outline-focus);
outline-offset: -2px;
background-color: var(--color-background-hover-narrow-filter);
}
.left-sidebar-navigation-label-container:focus {
outline: none;
}
&.active-filter,
&.top-left-active-filter,
&.active-sub-filter {
&:has(.left_sidebar_menu_icon_visible) {
background-color: var(--color-background-active-narrow-filter);
}
}
}
.stream-list-toggle-inactive-or-muted-channels.highlighted_row,
.stream-list-toggle-inactive-or-muted-channels:focus-visible {
outline: 2px solid var(--color-outline-focus);
outline-offset: -2px;
background-color: var(--color-background-sidebar-action-heading-hover);
}
#stream_filters .narrow-filter:has(a.subscription_block:focus-visible) {
& > .bottom_left_row {
outline: 2px solid var(--color-outline-focus);
outline-offset: -2px;
}
&.active-filter .topic-list .bottom_left_row {
background-color: var(--color-background-active-narrow-filter);
.sidebar-topic-check {
background-color: var(--color-background-active-narrow-filter);
}
}
&:not(.active-filter) {
background-color: var(--color-background-hover-narrow-filter);
border-radius: 4px;
.sidebar-topic-check {
background-color: var(--color-background-hover-narrow-filter);
}
}
}
#views-label-container,
#direct-messages-section-header,
#direct-messages-modal-section-header,
.stream-list-subsection-header,
.top_left_row,
.bottom_left_row {
&.highlighted_row {
outline: 2px solid var(--color-outline-focus);
outline-offset: -2px;
}
}
#stream_filters
.narrow-filter
.topic-list
.bottom_left_row:has(a.topic-box:focus-visible),
#more-topics-modal .bottom_left_row:has(a.topic-box:focus-visible),
#direct-messages-list .dm-list .bottom_left_row:has(a.dm-box:focus-visible),
#modal-direct-messages-list .bottom_left_row:has(a.dm-box:focus-visible),
#show-more-direct-messages:has(a.dm-name:focus-visible),
#subscribe-to-more-streams:has(.subscribe-more-link:focus-visible),
#login-to-more-streams:has(.subscribe-more-link:focus-visible) {
outline: 2px solid var(--color-outline-focus);
outline-offset: -2px;
background-color: var(--color-background-hover-narrow-filter);
.sidebar-topic-check {
/* 2px of the original 4px padding is now the outline, don't draw over that */
padding-left: 2px;
background-color: var(--color-background-hover-narrow-filter);
}
}
#left-sidebar-topic-search-hint {
/* Match the outer spacing used by #subscribe-to-more-streams so
icon and label align with that row's icon and label. */
margin-right: var(--left-sidebar-right-margin);
}
.left-sidebar-topic-search-hint-link {
/* Padding on the link (which fills the row) keeps the focus ring
spanning the full row width. */
padding-left: var(--left-sidebar-toggle-width-offset);
display: grid;
grid-template:
"icon . text" minmax(var(--line-height-sidebar-row-prominent), auto)
/ var(--left-sidebar-icon-column-width)
var(--left-sidebar-icon-content-gap)
minmax(0, 1fr);
align-items: center;
border-radius: 4px;
cursor: pointer;
/* Override the global a:hover / a:focus rule in zulip.css that
adds underline and changes the color to the generic link color. */
&,
&:hover,
&:focus {
color: var(--color-text-sidebar-row);
text-decoration: none;
}
&:hover {
background-color: var(--color-background-hover-narrow-filter);
}
/* Match the inset outline used by .highlighted_row so Tab and
arrow key focus look identical and don't clip outside the row. */
&:focus-visible {
outline: 2px solid var(--color-outline-focus);
outline-offset: -2px;
}
.zulip-icon {
grid-area: icon;
justify-self: center;
opacity: 0.7;
}
.left-sidebar-topic-search-hint-label {
grid-area: text;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
#stream_filters .narrow-filter,
#direct-messages-list .bottom_left_row {
a.stream-name:focus,
a.sidebar-topic-name:focus,
a.conversation-partners:focus,
a.dm-box:focus,
a.sidebar-topic-action-heading:focus {
outline: none;
text-decoration: none;
}
}
#subscribe-to-more-streams,
#login-to-more-streams {
/* --left-sidebar-bottom-scrolling-buffer is to prevent the row from
being cut off, and --sidebar-bottom-spacing is extra padding. */
margin: 5px var(--left-sidebar-right-margin)
calc(
var(--left-sidebar-bottom-scrolling-buffer) +
var(--sidebar-bottom-spacing)
)
0;
padding-left: var(--left-sidebar-toggle-width-offset);
border-radius: 4px;
text-decoration: none;
&:hover {
background: var(--color-background-sidebar-action-heading-hover);
box-shadow: inset 0 0 0 1px var(--color-shadow-sidebar-row-hover);
color: var(--color-text-sidebar-action-heading-hover);
}
.subscribe-more-link {
display: grid;
grid-template:
"plus-icon . row-text" minmax(
var(--line-height-sidebar-row-prominent),
auto
)
/ var(--left-sidebar-icon-column-width) var(
--left-sidebar-icon-content-gap
)
minmax(0, 1fr);
align-items: center;
line-height: 1;
color: var(--color-text-sidebar-action-heading);
text-decoration: none;
outline: none;
}
.subscribe-more-label {
grid-area: row-text;
/* Because the label is run in small caps, we do a font-metrics-based
adjustment of its top margin to keep the text vertically aligned
with the square [+] icon (whose shape leaves absolutely no room
for error when it comes to centering). Going back to the font metrics
outlined in web/src/information_density.ts, we can offset against the
space reserved for descenders, and take half that value to pull the
text up: 400 / (400 + 1025) ~= 0.28; half of 0.28 is 0.14, so
`margin-top: -0.14em`. However, it looks as though a slight adjustment
to -0.12em produces a slightly more pleasing result, which makes sense
because mathematical centering and the perception of center don't
always match. */
margin-top: -0.12em;
padding: 3px 0;
font-size: var(--font-size-sidebar-action-heading);
font-weight: var(--font-weight-sidebar-action-heading);
font-variant: var(--font-variant-sidebar-action-heading);
font-feature-settings: var(
--font-feature-settings-sidebar-action-heading
);
text-transform: var(--text-transform-sidebar-action-heading);
}
.subscribe-more-icon {
/* Scale the icon with the action-heading font-size
for better vertical centering. */
font-size: var(--font-size-sidebar-action-heading);
}
}
ul.filters {
list-style-type: none;
margin-left: 0;
line-height: var(--line-height-sidebar-row-prominent);
.sidebar-topic-name,
.left-sidebar-navigation-label-container {
color: var(--color-text-sidebar-row);
&:hover {
/* Push back against a:hover color in dark_theme.css */
color: var(--color-text-sidebar-row) !important;
}
&:focus {
text-decoration: none;
}
}
}
#more-topics-modal .topic-list,
ul.filters {
.sidebar-topic-action-heading {
&:focus {
color: var(--color-text-sidebar-action-heading);
}
}
& hr {
margin-top: 10px;
margin-bottom: 10px;
}
.has-only-muted-unreads {
.unread_count,
.masked_unread_count {
opacity: var(--opacity-left-sidebar-muted);
}
&:hover .unread_count {
opacity: var(--opacity-left-sidebar-muted-hover);
}
}
.has-only-muted-mentions {
.unread_mention_info {
opacity: var(--opacity-left-sidebar-muted-mentions);
}
&:hover .unread_mention_info {
opacity: var(--opacity-left-sidebar-muted-hover);
}
}
/* This is a noop in the current design, because unread counts for
muted streams have the same opacity, but the logic is here to
be explicit and because the design may change in the future. */
.more_topic_unreads_muted_only .unread_count {
opacity: var(--opacity-left-sidebar-muted);
}
.zulip-icon-follow {
opacity: 0.5;
&:hover {
opacity: 1;
color: var(--color-left-sidebar-follow-icon-hover);
}
}
& li.muted_topic {
.sidebar-topic-check,
.sidebar-topic-name,
.unread_count {
opacity: var(--opacity-left-sidebar-muted);
}
&:hover {
.sidebar-topic-check,
.sidebar-topic-name,
.unread_count {
opacity: var(--opacity-left-sidebar-muted-hover);
}
}
}
& li.out_of_home_view {
.stream-privacy,
.stream-name,
.channel-new-topic-button,
.unread_count,
.masked_unread_count,
.sidebar-menu-icon {
opacity: var(--opacity-left-sidebar-muted);
}
.unread_mention_info {
/* Default opacity for this icon is too close to
--opacity-left-sidebar-muted */
opacity: var(--opacity-left-sidebar-muted-mentions);
}
&:hover {
.stream-privacy,
.stream-name,
.channel-new-topic-button,
.unread_count,
.masked_unread_count,
.sidebar-menu-icon {
opacity: var(--opacity-left-sidebar-muted-hover);
}
}
.has-unmuted-unreads {
.unread_count {
opacity: 1;
}
}
& li.unmuted_or_followed_topic {
color: var(--color-unmuted-or-followed-topic-list-item);
.unread_count {
opacity: 1;
}
}
}
}
li.active-filter,
li.top-left-active-filter,
li.active-sub-filter {
font-weight: 600 !important;
position: relative;
border-radius: 4px;
color: var(--color-text-active-narrow-filter);
background-color: var(--color-background-active-narrow-filter);
/* These hover styles effectively push back on non-active-filter
styles, which would otherwise present the less-intense
--color-background-hover-narrow-filter colors. */
&:hover {
background-color: var(--color-background-active-narrow-filter);
.sidebar-topic-check {
background-color: var(--color-background-active-narrow-filter);
}
}
.sidebar-topic-check {
background-color: var(--color-background-active-narrow-filter);
}
.sidebar-topic-name-inner {
color: var(--color-text-active-narrow-filter);
}
}
.top_left_mentions,
#direct-messages-section-header,
#direct-messages-modal-section-header,
.dm-list-item {
/* To highlight the background when a new unread arrives,
we set the background on the .new-unread class. */
&.new-unread {
background-color: var(--color-background-active-narrow-filter);
}
/* The background-color transition is maintained by this class,
which ensures an immediate opening transition, and lingers
some time longer beyond the CSS Animation on the unread count,
ensuring a smooth transition back to the normal background
color on the sidebar row that's been targeted. */
&.new-unread-highlight {
transition: background-color 1s ease-in-out;
}
}
.top_left_mentions #stream_filters .narrow-filter.active-filter {
.topic-list .filter-topics,
> .bottom_left_row {
background-color: var(--color-background-active-narrow-filter);
border-radius: 4px;
&:hover {
background-color: var(--color-background-hover-narrow-filter);
}
}
}
#left-sidebar-navigation-list-condensed {
display: flex;
justify-content: center;
.left-sidebar-navigation-condensed-item {
/* 26px minimum width gives a bit more
breathing room for unread markers when
unreads are displayed.
However, we want to permit growing but
not shrinking to keep icons reasonably
spaced across different information-density
settings. So we convert this value to ems:
26px at 16px/1em. */
flex: 1 0 1.625em;
/* Unset padding from individual top_left items */
padding: 0;
border-radius: 4px;
/* Set a positioning context for the unread dot. */
position: relative;
&:not(.top_left_condensed_unread_marker) {
.unread_count {
position: absolute;
}
/* Show the same styles when each item is
hovered or, via the keyboard, the `<a>`
element within receives focus. */
&:hover,
&:focus-within {
background: var(--color-background-navigation-item-hover);
.unread_count {
/* 6px at 12px/1em */
top: -0.5em;
right: -0.5em;
background: var(--color-background-unread-counter-no-alpha);
}
}
&:not(:hover) .unread_count {
/* .unread_count has its based font-size set to 12px at 14px/em. */
/* 2px, 6px at 12px/1em */
top: 0.1667em;
/* With positioning, we center the dot over the icon
by subtracting half its width from the 50% mark. */
right: calc(50% - 0.25em);
/* But then we shift it left the em equivalent of half
the icon width (15px/2) plus 2px. This keeps the
unread dot in correct proximity to the view icon,
no matter how the row flexes or what information-
density values a user has set. 9.5px at 12px/1em.
We set this and its scaled counterpart as variables
for use in the pulse-unread animation to maintain the
dot's correct position relative to the view icon. */
--unread-dot-x-offset: 0.7917em;
/* To make sure the unread dot appears to scale from the
offset dot's center in collapsed Views, we need to
adjust the translateX offset by the same amount we
scale. calc() does not play nicely with animations,
so it's critical that we calculate this value here,
well before its needed. */
--unread-dot-x-offset-scaled: calc(
var(--unread-dot-x-offset) / var(--pulse-unread-scale-max)
);
transform: translateX(var(--unread-dot-x-offset));
width: 0.5em;
height: 0.5em;
padding: 0;
color: transparent;
background-color: var(--color-background-unread-counter-dot);
}
}
/* Hide the unread dot, assuming that message
counts are not hidden. */
&.selected-home-view .unread_count {
display: none;
}
&.top_left_starred_messages .unread_count {
display: none;
}
&.top_left_condensed_unread_marker {
/* Flex to just the unread's width, to keep the
unreads aligned on the righthand side with others
up and down the left sidebar. */
flex: 0 0 auto;
display: flex;
align-content: center;
&:has(.unread_count:empty) {
/* When the unread count is empty (no unreads),
we flex shut to 0 across the board so the
unread count takes up no space. */
flex: 0 0 0;
}
.unread_count:not(:empty) {
margin: 0 var(--left-sidebar-unread-offset);
align-self: center;
}
}
}
&.hide-unread-messages-count {
/* Restore the unread dot when message counts
are hidden, unless there are no unreads,
of course. */
.selected-home-view .unread_count:not(:empty) {
display: block;
}
/* We suppress the entire unread area
for users who prefer masked unreads. */
.top_left_condensed_unread_marker {
display: none;
}
}
.left-sidebar-navigation-icon-container {
/* Unset margin from full nav list anchor elements. */
margin: 0;
/* Horizontally center icons within their boxes. */
text-align: center;
&:focus {
/* Unset inherited :focus outline. */
outline: 0;
}
}
.top-left-active-filter {
/* Don't display a background on condensed icons. */
background: unset;
}
.filter-icon {
display: flex;
align-items: center;
justify-content: center;
/* Enlarge icons slightly in condensed views. */
/* 15px at 16px/1em */
font-size: 0.9375em;
/* 24px at 15px/1em */
height: 1.6em;
color: var(--color-left-sidebar-navigation-icon);
}
}
#left-sidebar-navigation-list {
margin-bottom: 0;
display: grid;
line-height: var(--line-height-sidebar-row);
/* Explicitly ensure parity with the line-height
for the sake of low-resolution screens, whose
font-rendering and rounding may cause icons
to appear out of alignment. This grid feature should
only apply in the expanded-navigation view. */
grid-auto-rows: var(--line-height-sidebar-row);
.left-sidebar-navigation-label-container {
.left-sidebar-navigation-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
/* Don't show unread counts on views... */
.top_left_my_reactions,
.top_left_inbox,
.top_left_recent_view,
.top_left_all_messages {
.unread_count {
visibility: hidden;
}
/* ...unless it's the selected home view. */
&.selected-home-view .unread_count {
visibility: visible;
}
}
.sidebar-menu-icon.hide {
visibility: hidden;
}
.left-sidebar-navigation-label-container {
grid-area: row-content;
/* The label container itself is also a grid,
for laying out the items that are its
children. Same template areas, different
column widths. */
grid-template-columns:
var(--left-sidebar-toggle-width-offset) var(
--left-sidebar-icon-column-width
)
var(--left-sidebar-icon-content-gap) minmax(0, 1fr) minmax(
0,
max-content
)
minmax(0, max-content) 0 0;
.filter-icon {
grid-area: starting-anchor-element;
/* Use a flex container to handle
icon centering within the grid area. */
display: flex;
justify-content: center;
}
.left-sidebar-navigation-label {
grid-area: row-content;
padding-right: var(--left-sidebar-before-unread-count-padding);
@media screen and (resolution <= 1x) {
/* For the sake of low-resolution screens,
we'll let the actual label take 1 as a line-height
value, and allow grid to handle the alignment. */
line-height: 1;
}
}
.unread_count {
grid-area: markers-and-unreads;
/* Extra margin for unreads. */
margin-right: var(--left-sidebar-unread-offset);
&:empty {
margin-right: 0;
}
}
}
/* Low-attention unreads have no bounding box,
so their counters should be aligned on the
same baseline as the navigation label. */
.top_left_starred_messages,
.top_left_drafts,
.top_left_scheduled_messages {
.left-sidebar-navigation-label-container {
align-items: baseline;
}
.left-sidebar-navigation-label {
@media screen and (resolution <= 1x) {
/* Owing to the baseline alignment in this
area, we don't need the low-res line-height
adjustment. */
line-height: inherit;
}
}
.filter-icon {
align-self: center;
}
}
.top_left_starred_messages {
&.hide_starred_message_count {
.masked_unread_count {
display: flex;
grid-area: markers-and-unreads;
/* Extra margin for unreads. */
margin-right: var(--left-sidebar-unread-offset);
}
.unread_count {
display: none;
}
/* When starred message count is 0, we dont want to show unread_count or masked_unread_count. */
.unread_count.hide + .masked_unread_count {
display: none;
}
}
}
.top_left_starred_messages:hover {
&.hide_starred_message_count {
.masked_unread_count {
display: none;
}
.unread_count {
display: inline;
}
.unread_count.hide {
display: none;
}
}
}
.top_left_starred_messages.top-left-active-filter {
&.hide_starred_message_count {
.masked_unread_count {
display: none;
}
.unread_count {
display: inline;
}
.unread_count.hide {
display: none;
}
}
}
.conversation-partners {
grid-area: row-content;
overflow: hidden;
text-overflow: ellipsis;
}
.conversation-partners .status-emoji {
/* Prevent status emoji from colliding
with unread counts. */
margin-right: 3px;
/* To make status emoji look good with
multiline usernames, we need to fall
back to inline-block display here. */
display: inline-block;
vertical-align: -0.25em;
}
/* New grid definitions here. */
#views-label-container,
.top_left_row,
.left-sidebar-navigation-label-container,
.dm-box,
.subscription_block,
.searching-for-more-topics {
display: grid;
align-items: center;
/* This general pattern of elements applies to every single row in the left
sidebar, to some degree or another. Eventually, these template areas
could be applied to all rows, with different `grid-template-column`
values applied as needed (and shared as needed). For example, an element
with no "starting-offset" sets that area to `0`; so too with other non-
existent elements.
The offsets themselves are meant to greedily assign all of the available
horizontal space to the content area of the row. That space can then be
modified or reassigned as needed, without running up against `padding`
(which alters the box size) or `margin` (which notoriously bleeds outside
of the element it's defined on). */
grid-template-areas: "starting-offset starting-anchor-element icon-content-gap row-content controls markers-and-unreads ending-anchor-element ending-offset";
}
.top_left_row {
/* We stretch the items on the overall
nav row, so there's no unclickable
gaps between nav rows. */
align-items: stretch;
/* The row grid for the outer .top_left_row
is chiefly for lefthand spacing and placing
the inner row content and vdots. */
grid-template-columns:
0 0 0 minmax(0, 1fr) 0 0 var(--left-sidebar-vdots-width)
0;
.sidebar-menu-icon {
grid-area: ending-anchor-element;
}
}
.left-sidebar-filter-input-container {
display: block;
margin-bottom: 5px;
grid-area: filter-container;
.filter-input,
.filter-topics {
grid-area: filter-input;
}
/* Keep topic filters to a single line, and show
an ellipsis when the input loses focus. */
#left-sidebar-filter-topic-input {
flex-wrap: nowrap;
.pill {
flex-shrink: 0;
}
.input {
white-space: nowrap;
overflow: hidden;
}
}
}
#left-sidebar-navigation-area:has(.selected-home-view.top-left-active-filter)
.top_left_condensed_unread_marker {
/* Hide the condensed unread marker when the home view is active,
since the home view row already shows the unread count. */
display: none;
}
#views-label-container.showing-condensed-navigation,
.left-sidebar.zoom-in #views-label-container {
/* Use a next-sibling combinator (+) to use CSS to show and hide
filter rows as needed, based on the narrow. */
+ #left-sidebar-navigation-list {
/* In the condensed state, we don't want to generate
auto rows, or there will be a footprint where the
expanded nav sits. */
grid-auto-rows: unset;
/* When the navigation area is condensed, hide all
the rows in the full navigation list... */
& .top_left_row {
display: none;
}
/* ...except when there is an active filter in place:
that row should still be shown. */
& .top_left_row.top-left-active-filter {
display: grid;
/* In the absence of auto rows in the condensed state,
we set an explicit height on the active filter. */
height: var(--line-height-sidebar-row);
}
}
.zulip-icon-heading-triangle-right {
rotate: 0deg;
}
}
#views-label-container {
margin-right: var(--left-sidebar-right-margin);
grid-template-columns:
0 var(--left-sidebar-header-icon-toggle-width) 0 minmax(0, 0.5fr)
0 minmax(0, 1fr)
var(--left-sidebar-vdots-width) 0;
/* 28px at 16px/1em */
grid-template-rows: 1.75em;
cursor: pointer;
border-radius: 4px;
&:not(.remove-pointer-for-spectator):hover,
&:has(.left-sidebar-navigation-menu-icon[aria-expanded="true"]) {
background-color: var(--color-background-hover-narrow-filter);
box-shadow: inset 0 0 0 1px var(--color-shadow-sidebar-row-hover);
.left-sidebar-title,
.sidebar-heading-icon {
opacity: var(--opacity-sidebar-heading-hover);
}
}
/* Remove the cursor: pointer property of Views label for the spectators. */
&.remove-pointer-for-spectator {
cursor: default;
}
#toggle-top-left-navigation-area-icon {
grid-area: starting-anchor-element;
/* Horizontally center the icon in its allotted grid area. */
justify-self: center;
}
.left-sidebar-title {
grid-area: row-content;
}
#left-sidebar-navigation-list-condensed {
margin: 0;
grid-area: markers-and-unreads;
}
.left-sidebar-navigation-menu-icon {
grid-area: ending-anchor-element;
/* Horizontally center vdots. */
justify-self: stretch;
/* Properly size vdots. */
/* 17px at 16px/1em */
font-size: 1.0625em;
/* Occupy same clickable height as
other condensed-view icons */
/* 24px at 17px/1em */
height: 1.4118em;
/* Vertically center dots with
flexbox. */
display: flex;
align-items: center;
justify-content: center;
/* These margin and padding values should be
coordinated with those on .sidebar-menu-icon. */
margin: 2px 2px 2px 1px;
padding-left: 1px;
border-radius: 3px;
color: var(--color-vdots-visible);
&:hover {
color: var(--color-vdots-hover);
background-color: var(--color-background-sidebar-action-hover);
}
}
}
.left-sidebar:not(.zoom-in) #views-label-container.showing-expanded-navigation {
/* When the expanded navigation is visible,
hide the condensed navigation's controls. */
#left-sidebar-navigation-list-condensed,
.left-sidebar-navigation-menu-icon {
display: none;
}
/* Give the sidebar title through the end of the markers
area, if needed. */
.left-sidebar-title {
grid-column: row-content-start / markers-and-unreads-end;
}
}
/* When we collapsed the views section because we're zoomed in, hide
the condensed navigation's controls (except the unread counter). */
.left-sidebar.zoom-in #views-label-container {
grid-template-columns:
0 var(--left-sidebar-header-icon-toggle-width)
0 minmax(0, 1fr) 0 minmax(0, max-content) var(
--left-sidebar-vdots-width
)
0;
.left-sidebar-navigation-condensed-item:not(
.top_left_condensed_unread_marker
),
.left-sidebar-navigation-menu-icon {
display: none;
}
}
.subscription_block {
user-select: text;
grid-template-columns:
var(--left-sidebar-toggle-width-offset) var(
--left-sidebar-icon-column-width
)
var(--left-sidebar-icon-content-gap) minmax(0, 1fr) minmax(
0,
max-content
)
minmax(0, max-content)
var(--left-sidebar-vdots-width) 0;
white-space: nowrap;
text-decoration: none;
color: inherit;
&:hover,
&:active {
opacity: 1;
text-decoration: none;
color: inherit;
}
&:not(:active):focus {
text-decoration: none;
border: none;
outline: none;
color: inherit;
}
.stream-privacy {
grid-area: starting-anchor-element;
display: flex;
place-content: center center;
.zulip-icon.zulip-icon-globe {
/* 12px at 14px/1em */
font-size: 0.8571em;
}
.zulip-icon.zulip-icon-hashtag {
/* 13px at 14px/1em */
font-size: 0.9286em;
}
.zulip-icon.zulip-icon-lock {
/* 13px at 14px/1em */
font-size: 0.9286em;
}
}
.stream-name {
grid-area: row-content;
color: inherit;
&:hover {
text-decoration: none;
}
}
}
.left-sidebar-filter-row {
display: grid;
/* The final 2px column keeps the filter box from going
to the edge of the highlighted channel box, and also
matches the right edge to the right edge of the vdots
on the channel row. (The vdots take a 2px right margin.) */
grid-template-columns:
[filter-box-start] minmax(0, 1fr)
[clear-button-start] var(--line-height-sidebar-row-prominent)
[clear-button-end filter-box-end] 2px;
grid-template-rows: [filter-box-start clear-button-start] auto [clear-button-end filter-box-end];
align-content: center;
.clear_search_button {
grid-area: clear-button;
}
}
.topic-box,
.searching-for-more-topics {
display: grid;
grid-template:
"starting-offset starting-anchor-element icon-content-gap row-content controls markers-and-unreads ending-anchor-element ending-offset" var(
--line-height-sidebar-row-prominent
)
". . . row-content . . . . " auto / 0 var(
--left-sidebar-icon-column-width
)
var(--left-sidebar-icon-content-gap)
minmax(0, 1fr) minmax(0, max-content) minmax(0, max-content)
var(--left-sidebar-vdots-width) 0;
text-decoration: none;
color: inherit;
&:hover,
&:active {
opacity: 1;
text-decoration: none;
color: inherit;
}
&:not(:active):focus {
text-decoration: none;
border: none;
outline: none;
color: inherit;
}
}
.topic-box {
user-select: text;
}
.searching-for-more-topics {
margin-left: var(--left-sidebar-toggle-width-offset);
height: var(--line-height-sidebar-row-prominent);
}
.topic-box .zero_count {
display: none;
}
.sidebar-topic-name {
cursor: pointer;
grid-area: row-content;
padding: 0 var(--left-sidebar-before-unread-count-padding) 0 0;
&:hover {
color: inherit;
}
&:focus {
text-decoration: none;
}
.sidebar-topic-name-inner {
/* Clamp multi-line topics to two lines. */
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
/* Break overflowing words as necessary. */
overflow-wrap: break-word;
line-height: var(--line-height-sidebar-topic-inner);
/* To avoid clipping the tops of certain characters,
we set the top space with padding. But to make the
line-clamp effect work, we set the bottom space
with margin. */
padding-top: var(--spacing-top-bottom-sidebar-topic-inner);
margin: 0 0 var(--spacing-top-bottom-sidebar-topic-inner) 0;
}
}
.sidebar-topic-action-heading {
color: var(--color-text-sidebar-action-heading);
font-size: var(--font-size-sidebar-action-heading);
font-weight: var(--font-weight-sidebar-action-heading);
font-variant: var(--font-variant-sidebar-action-heading);
font-feature-settings: var(--font-feature-settings-sidebar-action-heading);
text-transform: var(--text-transform-sidebar-action-heading);
cursor: pointer;
grid-area: row-content;
padding: 0 var(--left-sidebar-before-unread-count-padding) 0 0;
/* TODO: Consolidate these styles with conversation partners and stream name
once grid rewrite is complete on all sidebar rows.
Also: note that these styles will be moot for topic names once we allow
for multiline topics. If we hold multiline topics to a certain number
of lines, we'll likely need a JavaScript-based solution like Clamp.js
to display an ellipsis on the final visible line. */
white-space: nowrap;
/* Both `hidden` and `clip` are shown for the sake
of older browsers that do not support `clip`. */
overflow-x: hidden;
overflow-x: clip;
text-overflow: ellipsis;
&:focus {
text-decoration: none;
}
&:hover {
text-decoration: none;
/* Push back against a:hover color in dark_theme.css. */
color: var(--color-text-sidebar-action-heading-hover) !important;
}
}
.filter-topics {
font-weight: initial;
}
.searching-for-more-topics img {
height: 16px;
grid-area: row-content;
}
.sidebar-topic-check {
grid-area: starting-anchor-element;
place-self: center end;
/* 15px at 14px/1em */
font-size: 1.0714em;
/* Use background to mask part of grouping bracket. */
padding-left: 3px;
/* Keep background from affecting rounded corners on
.active-sub-filter by reducing the checkbox
line-height to match its font size. */
line-height: 1;
/* As a grid item, adjust the checkmark's z-index here so
that the background color appears above the grouping
bracket's bottom line. Its value must less than
the z-index set on .stream-list-subsection-header */
z-index: 1;
/* To correct an edge case where a resolved topic is
the last in a list of topics, we set the app background
color on it to keep the closing topic bracket from
getting too close to the checkmark. */
background-color: var(--color-background);
}
.left-sidebar-controls {
grid-area: controls;
display: grid;
/* We won't know in advance how many controls a given
row has, but this allows grid to generate as many
as needed, sized to a shared icon width. */
grid-auto-flow: column;
grid-auto-columns: var(--left-sidebar-header-icon-width);
grid-template-rows: var(--line-height-sidebar-row-prominent);
place-content: stretch stretch;
margin-right: 1px;
}
.dm-markers-and-unreads,
.stream-markers-and-unreads,
.topic-markers-and-unreads {
grid-area: markers-and-unreads;
display: flex;
/* Present a uniform space between icons */
gap: 5px;
align-items: center;
justify-content: center;
/* Extra margin for unreads. */
margin-right: var(--left-sidebar-unread-offset);
&:has(.unread_count.hide) {
margin-right: 0;
}
.unread_mention_info {
/* Unset margin in favor of flex gap. */
margin: 0;
}
.unread_count {
/* Height is set here by the flexbox; this
decouples .unread_count from the app-wide
definition. */
height: auto;
}
}
.dm-markers-and-unreads,
.conversation-partners {
align-self: baseline;
}
#more-topics-modal .topic-box {
grid-template:
"starting-offset starting-anchor-element icon-content-gap row-content controls markers-and-unreads ending-anchor-element ending-offset"
var(--line-height-sidebar-row-prominent)
". . . row-content . . . . "
auto
/ var(--input-icon-starting-offset) var(
--left-sidebar-icon-column-width
)
var(--left-sidebar-icon-content-gap) minmax(0, 1fr) minmax(
0,
max-content
)
minmax(0, max-content) var(--left-sidebar-vdots-width) 0;
}
.zoomed-new-topic {
display: grid;
grid-template:
"starting-offset starting-anchor-element icon-content-gap row-content ending-offset"
var(--line-height-sidebar-row-prominent)
". . . row-content ."
auto
/ var(--input-icon-starting-offset) var(
--left-sidebar-icon-column-width
)
var(--left-sidebar-icon-content-gap)
minmax(0, 1fr)
0;
color: var(--color-text-sidebar-action-heading);
.topic-list-new-topic-icon {
grid-area: starting-anchor-element;
place-self: center end;
}
.new-topic-label {
grid-area: row-content;
margin-top: -0.06em;
font-size: var(--font-size-sidebar-action-heading);
font-weight: var(--font-weight-sidebar-action-heading);
font-variant: var(--font-variant-sidebar-action-heading);
font-feature-settings: var(
--font-feature-settings-sidebar-action-heading
);
text-transform: var(--text-transform-sidebar-action-heading);
}
&:not(:active):focus {
text-decoration: none;
border: none;
outline: none;
}
&:hover,
&:focus-visible {
border-radius: 4px;
text-decoration: none;
color: var(--color-text-sidebar-action-heading);
}
}
.channel-new-topic-button,
.channel-search-topics-button {
/* display: flex; is set on visible channels and
channel-row hovers. */
display: none;
align-items: center;
justify-content: center;
color: var(--color-sidebar-action);
margin: 2px 0;
border-radius: 3px;
&:hover {
color: var(--color-sidebar-action-hover);
background-color: var(--color-background-sidebar-action-hover);
}
}
.narrow-filter:hover {
.channel-new-topic-button {
display: flex;
}
}
.bottom_left_row .sidebar-menu-icon {
grid-area: ending-anchor-element;
}
.stream-list-subsection-header .sidebar-menu-icon {
grid-area: three-dot-placeholder;
}
.stream-name {
white-space: nowrap;
overflow-x: hidden;
overflow-x: clip;
text-overflow: ellipsis;
padding: 0 var(--left-sidebar-before-unread-count-padding) 0 0;
}
.conversation-partners-list {
/* Clamp multi-line DMs to two lines. */
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
/* Break overflowing usernames as necessary. */
overflow-wrap: break-word;
line-height: var(--line-height-sidebar-topic-inner);
/* To avoid clipping the tops of certain characters,
we set the top space with padding. But to make the
line-clamp effect work, we set the bottom space
with margin. */
padding: var(--spacing-top-bottom-sidebar-topic-inner)
var(--left-sidebar-before-unread-count-padding) 0 0;
margin-bottom: var(--spacing-top-bottom-sidebar-topic-inner);
}
/*
All of our left sidebar handlers use absolute
positioning. We should fix that.
*/
.bottom_left_row .sidebar-menu-icon,
.top_left_row .sidebar-menu-icon,
.stream-list-section-container .sidebar-menu-icon {
display: none;
cursor: pointer;
/* Use a flex container to handle
icon centering within the grid area.
:hover actually sets the `display: flex`,
so it remains hidden otherwise. */
justify-content: center;
align-items: center;
text-align: center;
/* Ensure icons are vertically aligned, in
case they appear in a grid definition,
like the nav rows, that use a different
centering regime for the row. */
align-self: stretch;
border-radius: 3px;
/* These margin and padding values should be
coordinated with those on
.left-sidebar-navigation-menu-icon. */
margin: 2px 2px 2px 1px;
/* This helps horizontally align the vdots,
given the reduced margin-left above. */
padding-left: 1px;
/* Set the icon size, which will be inherited
by .zulip-icon */
/* 17px at 16px/1em */
font-size: 1.0625em;
/*
If you hover directly over the ellipsis itself,
show it in black.
*/
&:hover {
color: var(--color-vdots-hover);
}
/*
Hover does not work for touch-based devices like mobile phones.
Hence the icons does not appear, making the user unaware of its
presence on such devices. The following media property displays the
icon by default for such behaviour.
*/
@media (hover: none) {
display: flex;
/* Show dots on touchscreens in a less distracting,
lighter shade. */
color: var(--color-vdots-hint);
}
}
.bottom_left_row .sidebar-menu-icon,
.bottom_left_row .channel-new-topic-button,
.top_left_row .sidebar-menu-icon,
.stream-list-section-container .sidebar-menu-icon {
&:focus-visible {
outline: 2px solid var(--color-outline-focus);
}
}
/*
When you hover over or keyboard-highlight a list
item's menu icon, we give it a purple background.
The stream icon should always display when
any topic is hovered/highlighted, which is why
it gets a more specific selector here.
*/
#stream_filters
li:is(:hover, :focus-within, :has(.highlighted_row))
.stream-sidebar-menu-icon,
.top_left_row:is(:hover, :focus-within, .highlighted_row) .sidebar-menu-icon,
.bottom_left_row:is(:hover, :focus-within, .highlighted_row) .sidebar-menu-icon,
#stream_filters
.stream-list-subsection-header:is(:hover, :focus-within, .highlighted_row)
.sidebar-menu-icon,
.app-main .column-left .left-sidebar .left_sidebar_menu_icon_visible {
/* We push against `display: none` with
`display: flex` because the sidebar vdots
all expect to be displayed as flex items
when visible. Their vertical alignment
depends on it, too. */
display: flex;
color: var(--color-vdots-visible);
&:hover {
color: var(--color-vdots-hover);
background-color: var(--color-background-sidebar-action-hover);
}
}
ul.topic-list {
line-height: var(--line-height-sidebar-row-prominent);
list-style-type: none;
font-weight: normal;
}
ul.topic-list.topic-list-has-topics::before {
content: " ";
display: block;
position: absolute;
/* 12px at 16px/1em */
top: 0.75em;
bottom: 0.75em;
left: 9px;
border: 1px solid var(--color-topic-indent-border);
border-right: 0;
border-radius: 9px 0 0 9px;
width: 6px;
pointer-events: none;
}
ul.topic-list.topic-list-has-topics::after {
content: " ";
display: block;
position: absolute;
/* -14px at 16px/1em */
top: -0.875em;
/* 12px at 16px/1em */
bottom: 0.75em;
left: 16px;
width: 4px;
border-bottom: 1px solid var(--color-topic-indent-border);
pointer-events: none;
}
ul.topic-list:has(.show-more-topics)::after {
/* When the show all topics control is displayed,
extend the bottom bracket. */
width: 18px;
}
#stream_filters .narrow-filter,
#more-topics-modal {
.topic-list
.bottom_left_row:has(
a.sidebar-topic-action-heading:focus-visible,
a.zoomed-new-topic:focus-visible
) {
outline: 2px solid var(--color-outline-focus);
outline-offset: -2px;
background-color: var(--color-background-hover-narrow-filter);
}
}
/* The grouping border should not be shown
in the more topics modal. */
#more-topics-modal .topic-list.topic-list-has-topics::before,
#more-topics-modal .topic-list.topic-list-has-topics::after {
border: 0;
}
li.topic-list-item {
position: relative;
padding-right: 5px;
margin-left: var(--left-sidebar-toggle-width-offset);
&:has(.zoomed-new-topic) {
/* We set a margin equal to starting-offset of filter-input to match
its alignment */
margin-left: calc(
var(--left-sidebar-toggle-width-offset) -
var(--input-icon-starting-offset)
);
}
}
.dm-box {
user-select: text;
grid-template-columns:
var(--left-sidebar-toggle-width-offset) [action-heading-start] var(
--left-sidebar-icon-column-width
)
var(--left-sidebar-icon-content-gap) minmax(0, 1fr)
[action-heading-end] 0 minmax(0, max-content)
var(--left-sidebar-vdots-width) 0;
grid-template-rows: [action-heading-start] auto [action-heading-end];
.conversation-partners-icon {
grid-area: starting-anchor-element;
place-self: center;
&:not(.user-circle) {
place-self: start center;
}
}
.dm-name {
grid-area: action-heading;
}
.user-circle {
/* User circles are approximately 8px at 15px/1em. */
font-size: 0.5333em;
align-self: start;
}
.unread_count {
grid-area: markers-and-unreads;
/* Use flexbox to vertically center
the 12px-high text node within the
16px-high unread box. */
display: flex;
align-items: center;
&:empty {
margin-right: 0;
}
}
}
.zero_count {
visibility: hidden;
}
.zero-topic-unreads.show-more-topics .topic-box {
margin-right: 30px;
}
#more-topics-modal > .channel-header {
position: sticky;
top: 0;
z-index: 2;
padding-bottom: 1px;
background-color: var(--color-background);
&:hover {
/* Prevent hover styles set on other rows. */
box-shadow: none;
background-color: var(--color-background);
}
/* We avoid putting the box-shadow around both
the channel row and the filter input it contains,
as there is no hover effect on channel rows when
zoomed in, making a preserve-the-hover-outline
effect here moot. */
&:has(.left_sidebar_menu_icon_visible) {
box-shadow: none;
}
.subscription_block:focus-visible {
outline: 2px solid var(--color-outline-focus);
border-radius: 4px;
background-color: var(--color-background-hover-narrow-filter);
}
}
#more-topics-modal .bottom_left_row:last-of-type {
/* Needed for the scroll container to not cut off the hover border */
margin-bottom: 5px;
}
.top_left_row.hidden-by-filters {
display: none !important;
}
#left-sidebar-navigation-area.hidden-by-filters {
display: none;
}
#left-sidebar.direct-messages-hidden-by-filters,
.spectator-view {
#direct-messages-section-header {
display: none;
}
.direct-message-section-bottom-divider-container {
display: none;
}
.stream-list-subsection-header {
top: 0;
}
}
#left-sidebar.zoom-in {
#left_sidebar_scroll_container {
/* Hidden instead of display: none so that we scroll to the
correct height more easily as it becomes visible again */
visibility: hidden;
}
#left-sidebar-search {
display: none;
}
}
/* Show the DM header when zoomed into topics, visible behind
the back button area */
#left-sidebar.zoom-in-topics #direct-messages-section-header {
visibility: visible;
}
#left-sidebar-modal {
/* Hidden instead of display: none so that we scroll to the
correct height more easily as it becomes visible again */
visibility: hidden;
}
/* The two modals have different top paddings */
#left-sidebar.zoom-in-conversations #left-sidebar-modal,
#left-sidebar.direct-messages-hidden-by-filters #left-sidebar-modal,
.spectator-view #left-sidebar-modal {
.left-sidebar-modal-close-area {
padding-top: var(--left-sidebar-modal-close-area-padding-top);
}
#left-sidebar-modal-content {
height: calc(
100dvh - var(--navbar-fixed-height) -
var(--left-sidebar-modal-close-area-padding-top) -
var(--left-sidebar-modal-close-area-height)
);
box-shadow:
0 -0.2em 0.75em 0 var(--color-left-sidebar-inner-box-shadow),
0 -2.5em 1em 0 var(--color-left-sidebar-middle-box-shadow),
0 -4em 1em 0 var(--color-left-sidebar-outer-box-shadow);
}
.left-sidebar-modal-close-area:focus-visible + #left-sidebar-modal-content {
box-shadow:
0 -0.2em 0.75em 0 var(--color-left-sidebar-inner-box-shadow),
0 -2.5em 1em 0 var(--color-left-sidebar-middle-box-shadow-hover),
0 -4em 1em 0 var(--color-left-sidebar-outer-box-shadow-hover);
}
/* Don't show hover style on touch screens */
@media (hover: hover) and (pointer: fine) {
.left-sidebar-modal-close-area:hover + #left-sidebar-modal-content {
box-shadow:
0 -0.2em 0.75em 0 var(--color-left-sidebar-inner-box-shadow),
0 -2.5em 1em 0 var(--color-left-sidebar-middle-box-shadow-hover),
0 -4em 1em 0 var(--color-left-sidebar-outer-box-shadow-hover);
}
}
}
/* The topic menu leaves space to see the DM section (if it's visible) by
padding the sidebar row height to the close section's upper padding. */
body:not(.spectator-view)
#left-sidebar:not(.direct-messages-hidden-by-filters).zoom-in-topics
#left-sidebar-modal {
.left-sidebar-modal-close-area {
/* Avoid annoying flash of gray in the close area
on iOS/iPadOS */
-webkit-tap-highlight-color: transparent;
padding-top: calc(
var(--left-sidebar-modal-close-area-padding-top) +
var(--line-height-sidebar-row-prominent)
);
}
#left-sidebar-modal-content {
height: calc(
100dvh - var(--navbar-fixed-height) -
var(--left-sidebar-modal-close-area-padding-top) -
var(--left-sidebar-modal-close-area-height) -
var(--line-height-sidebar-row-prominent)
);
box-shadow:
0 -0.2em 0.75em 0 var(--color-left-sidebar-inner-box-shadow),
0 -2.5em 1em 0 var(--color-left-sidebar-middle-box-shadow),
0 -5.5em 1em 0 var(--color-left-sidebar-outer-box-shadow);
}
.left-sidebar-modal-close-area:focus-visible + #left-sidebar-modal-content {
box-shadow:
0 -0.2em 0.75em 0 var(--color-left-sidebar-inner-box-shadow),
0 -2.5em 1em 0 var(--color-left-sidebar-middle-box-shadow-hover),
0 -5.5em 1em 0 var(--color-left-sidebar-outer-box-shadow-hover);
}
/* Don't show hover style on touch screens */
@media (hover: hover) and (pointer: fine) {
.left-sidebar-modal-close-area:hover + #left-sidebar-modal-content {
box-shadow:
0 -0.2em 0.75em 0 var(--color-left-sidebar-inner-box-shadow),
0 -2.5em 1em 0 var(--color-left-sidebar-middle-box-shadow-hover),
0 -5.5em 1em 0 var(--color-left-sidebar-outer-box-shadow-hover);
}
}
}
@container app (width <= $cq_ml_min) {
.left-sidebar-modal-close-area .zulip-icon-close {
margin-right: 5px;
}
}
#left-sidebar.zoom-in-topics #left-sidebar-modal,
#left-sidebar.zoom-in-conversations #left-sidebar-modal {
position: absolute;
visibility: visible;
left: 0;
top: var(--navbar-fixed-height);
width: var(--left-sidebar-width);
.left-sidebar-modal-close-area {
position: relative;
z-index: 1;
height: var(--left-sidebar-modal-close-area-height);
display: flex;
margin: 0 10px;
justify-content: space-between;
align-items: center;
font-size: 0.9375em; /* 15px at 16px/em */
font-weight: 600;
letter-spacing: 0.75px;
&:hover {
cursor: pointer;
}
.left-sidebar-modal-back {
display: flex;
align-items: center;
}
}
#direct-messages-modal-section-header,
.channel-header {
margin-top: 5px;
grid-area: section-header;
}
.modal-direct-messages-list,
.topic-list-scroll-container {
grid-area: conversation-list;
}
.channel-header {
/* Override the usual hover behavior for the first row (topic name and search bar)
and its popover. */
box-shadow: none;
background-color: var(--color-background);
/* For the scrollbar */
margin-right: var(--left-sidebar-right-margin);
}
#left-sidebar-modal-content {
padding: 0 0 5px 5px;
background: var(--color-background);
border-radius: 6px 6px 0 0;
}
.left-sidebar-modal-close-area:focus-visible {
outline: none;
}
/* Remove extra left spacing */
.subscription_block {
grid-template-columns:
0.5em var(--left-sidebar-icon-column-width) var(
--left-sidebar-icon-content-gap
)
minmax(0, 1fr) minmax(0, max-content) minmax(0, max-content) var(
--left-sidebar-vdots-width
)
0;
}
.topic-list {
margin-bottom: var(--left-sidebar-bottom-scrolling-buffer);
}
.topic-list-item {
padding-right: 0;
margin-right: var(--left-sidebar-right-margin);
}
/* Override default margin when in the modal. */
.topic-list,
.topic-list-item {
margin-left: 0;
}
/* Always show the new topic button and header menu icon for the modal view. */
.channel-new-topic-button {
display: flex;
}
.channel-header .sidebar-menu-icon {
display: flex;
color: var(--color-vdots-visible);
}
}
#left-sidebar-menu-popover {
/* No hover effect on the label */
.display-style-selector-header:hover {
background: inherit;
cursor: default;
}
.popover-menu-link:not(.display-style-selector-header) {
display: grid;
grid-template: "left-icon row-content" auto / 1.125em minmax(
max-content,
1fr
);
align-items: center;
.popover-menu-label {
grid-area: row-content;
}
}
}