/
githubmirror
/
zulip
Обзор
Документация
Войти
/
githubmirror
/
zulip
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
web/styles/inbox.css
957 строк
30 KB
Anders Kaseorg
dependencies: Upgrade JavaScript dependencies.
29 июл 2026, 02:35
29 июл 2026, 02:35
d257809
Код
Авторство
О чём код?
.inbox-container { display: flex; flex-direction: column; background: var(--color-background-inbox); padding: 0; min-height: 100vh; #inbox-pane { max-width: 100%; display: flex; flex-direction: column; margin: var(--navbar-fixed-height) 25px 0; a { color: var(--color-text-message-header); text-decoration: none; } .unread_count { opacity: 1; outline: 0 solid var(--color-background-unread-counter); } .search_group { position: sticky; top: var(--navbar-fixed-height); background: var(--color-background-inbox); display: flex; /* This padding-top value aligns the inbox filters with the filters in the left and right sidebars. */ padding: 10px 0; z-index: 1; } .button-inbox-selected { background-color: var(--color-background-button-inbox-selected); } #inbox-filters { .inbox-search-wrapper { flex-grow: 1; max-width: var(--width-inbox-search); } } .inbox-empty-text { display: none; } #inbox-loading-indicator { margin-top: 20px; margin-bottom: var(--max-unmaximized-compose-height); .searching-for-more-topics { margin-left: 0; } } #inbox-list { overflow: hidden; /* search box left border (1px) + search box right border (1px) + dropdown left border (1px) + dropdown right border (1px) = 4px at 16px em */ max-width: calc( var(--width-inbox-search) + var(--width-inbox-filters-dropdown) + 0.25em ); .inbox-channel-topic-list { margin: 0; } .inbox-focus-border { display: flex; min-height: 1.875em; /* 30px at 16px em */ /* Holds the horizontal layout that the removed `border: 2px solid transparent` was reserving; `.inbox-row`'s outline-based focus doesn't reserve space. Vertical padding omitted so the 30px icon wrappers fit in the 30px row height. */ padding-inline: 0.125em; /* 2px at 16px/1em */ box-sizing: border-box; justify-content: space-between; } .inbox-header { user-select: none; display: block; height: 1.875em; /* 30px at 16px em */ .inbox-focus-border { height: 1.875em; /* 30px at 16px em */ } .inbox-left-part { grid-template: auto / minmax(auto, min-content) auto min-content min-content; grid-template-areas: "header_name collapse_button unread_mention_info unread_count"; } .inbox-header-name { grid-area: header_name; display: flex; align-items: center; overflow: hidden; outline: 0; cursor: pointer; .inbox-header-name-text { margin: 0; padding: 1px 0; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; font-weight: 600; } } &:focus { outline: 0; } &:hover { .collapsible-button { visibility: visible; } } } .fa-lock { margin-right: 3px; } .stream-privacy.filter-icon { /* 0 5px at 16px/1em */ padding: 0 0.3125em; margin: 0; } .zulip-icon-user { position: relative; top: -1px; margin-right: 4px; } .collapsible-button { grid-area: collapse_button; &:hover { cursor: pointer; } .zulip-icon-chevron-down { /* We are using very specific vertical padding here * to avoid the inbox row grid overflow. */ padding: 0.125em 0.25em; /* 2px 4px at 16px em */ vertical-align: top; transform: rotate(180deg); } } .user-circle { /* 8.5328px at 16px/1em */ font-size: 0.5333em; /* TODO: Refactor inbox rows to use grid, to avoid the min-width here that holds the other rows to similar columns. */ /* 16px at 8.5328px/1em */ min-width: 1.8751em; top: 0; text-align: center; } .zulip-icon-bot, .conversation-partners-icon { opacity: 0.7; /* Required to align DM fullnames in user circle icon */ /* 2px at 16px / 1em */ margin-left: 0.125em; } .user_block .zulip-icon { /* 0 5px at 16px/1em */ padding: 0 0.3125em; &.user-circle { /* 5px at 16px / ~ 0.5em */ padding: 0 0.586em; } } .inbox-row { user-select: none; display: block; background-color: var(--color-background-inbox-row); &:hover { background: var(--color-background-inbox-row-hover); } .inbox-left-part { grid-template: auto / min-content minmax(0, 1fr) min-content min-content; grid-template-areas: "match_topic_and_dm_start recipient_info unread_mention_info unread_count"; } .inbox-topic-container .user-circle { grid-area: match_topic_and_dm_start; } .recipients_info, .inbox-topic-name { grid-area: recipient_info; } } .channel-visibility-policy-indicator, .visibility-policy-indicator, .inbox-action-button { display: flex; align-items: center; justify-content: center; /* Row-tall, row-square click area so there's room for the focus ring around the inner icon without clipping against the row's right edge. Centering the icon (vs. anchoring to an edge) keeps the focus ring symmetric around it. */ width: 1.875em; /* 30px — matches row height */ height: 1.875em; /* 30px — matches row height */ /* Whole 30×30 hit area is clickable; the inner icon itself only spans ~16×16. Without this, mouse hovers at the top/bottom fall back to the default arrow cursor. */ cursor: pointer; & > i, & > .zulip-icon, & > .recipient_bar_icon { /* Zero out per-icon padding; the outer button is the click area and the focus ring draws outside the icon via `outline-offset`. Overrides `.recipient_bar_icon`'s 6px side padding from message_header.css. */ padding: 0; border-radius: 0.3125em; /* 5px at 16px/1em */ } } .channel-visibility-policy-indicator, .visibility-policy-indicator { margin-left: 3px; } /* Normalize the inner icon's opacity to 1 so the outer wrapper is the single source of truth for opacity. Overrides `.recipient_bar_icon`'s 0.2 idle / 0.4 hover from message_header.css for every marker wrapper — follow, mute, unmute, and the inherit (default) placeholder that becomes visible on row hover. */ .channel-visibility-policy-indicator > .recipient_bar_icon, .visibility-policy-indicator > .recipient_bar_icon { opacity: 1; /* Beat `.recipient_bar_icon:hover`'s !important. */ &:hover { opacity: 1 !important; } } /* Explicit topic/channel state markers are secondary information: visible at a glance, quieter than primary content. 0.45 is the lowest opacity that meets WCAG 1.4.11's 3:1 non-text contrast against both the light (~3.4:1) and dark (~3.9:1) inbox row backgrounds. */ .channel-visibility-policy-indicator, .visibility-policy-indicator:not( .inbox-row-visibility-policy-inherit ) { opacity: 0.45; } /* Row-level interaction tiers for the icon-style wrapper types: action menu, channel-level marker, topic-level marker. The inherit subclass is reached via the topic-level marker since `.inbox-row-visibility-policy- inherit` is also a `.visibility-policy-indicator`. */ .inbox-row, .inbox-header { /* Mouse hover reveals/lifts wrappers to 0.45, matching the explicit-marker idle tier. */ &:hover { :is( .inbox-action-button, .channel-visibility-policy-indicator, .visibility-policy-indicator ) { opacity: 0.45; } } /* Keyboard selection alone (row focused but not mouse-hovered) reveals only the usually-hidden action buttons at a much quieter 0.2. Explicit state markers stay at their 0.45 idle — only the tertiary reveal needs this extra-quiet step. The `:not(:focus-visible)` exclusion ensures that when the button itself has keyboard focus (via arrow- right into the column), its own `:focus-visible` rule sets the direct-interaction tier instead. */ &:focus-within:not(:focus, :hover) { :is( .inbox-action-button, .inbox-row-visibility-policy-inherit ):not(:focus-visible) { opacity: 0.2; } } &:focus-visible:not(:hover) { :is( .inbox-action-button, .inbox-row-visibility-policy-inherit ) { opacity: 0.2; } } /* Direct hover on a wrapper lifts to 0.7. Compound selector beats the row-wide reveal on specificity. */ &:hover :is( .inbox-action-button, .channel-visibility-policy-indicator, .visibility-policy-indicator ):hover { opacity: 0.7; } /* Wrapper keyboard focus = full opacity so the focus ring renders at full blue saturation (WCAG 1.4.11 3:1 contrast). The inner icon dims to 0.7 separately via the rule under `&:focus-visible > i` above. */ &:focus-within :is( .inbox-action-button, .channel-visibility-policy-indicator, .visibility-policy-indicator ):focus-visible { opacity: 1; } } .unread-count-focus-outline { /* Because the inbox view font-size will never be smaller than the em-equivalent of 15px, we restate the base font-size here so that unreads match others in the UI at legacy size (14px). */ font-size: var(--base-font-size-px); grid-area: unread_count; display: flex; align-items: center; justify-content: center; border-radius: 3px; padding: 0 5px; /* Stretch to the row to keep unread count from affecting overall row size as test scales up. */ align-self: stretch; } /* Channels, topics, and direct messages mark their unread messages as read when their count is clicked. Folder headers (channel folders and the direct messages section) only show a combined count for everything inside them, so they don't get the clickable affordance. */ :is(.inbox-row, .inbox-header:not(.inbox-folder)) .unread-count-focus-outline { /* Whole 30×stretched hit area is clickable; surface the same pointer cursor users had on the smaller badge before the click target was expanded. */ cursor: pointer; /* Forward the badge's hover outline to the wrapper: the click handler now lives on the wrapper, so hovering anywhere in the wrapper (not just the small inner badge) should preview the click affordance. */ &:hover .unread_count { outline-width: var(--focus-ring-width-in-views); } } .unread_mention_info { grid-area: unread_mention_info; margin-right: 0; } .stream-privacy { display: flex; align-items: center; margin-right: 4px; margin-left: 17px; .zulip-icon { line-height: 0.875em; /* 14px at 16px em */ height: 1em; width: 1em; } } .inbox-topic-name { /* 16px channel icon width + 10px padding */ padding-left: 1.625em; /* 26x at 16px em */ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; & a { padding: 1px 0; white-space: pre; } } .inbox-left-part-wrapper { display: flex; width: 80%; } .inbox-left-part { width: 100%; display: grid; align-items: center; &:hover { cursor: pointer; } .inbox-group-or-bot-dm { position: relative; left: -3px; /* We don't display status emoji in group DMs, so prepare an ordinary inline layout... */ .user-status-microlayout { display: inline; white-space: collapse; } /* ...and hide the status emoji. */ .status-emoji { display: none; } } .recipients_info { display: flex; flex-wrap: wrap; column-gap: 10px; grid-area: recipient_info; .user_block { display: flex; align-items: center; } } } } #inbox-collapsed-note { display: none; overflow: hidden; max-width: calc( var(--width-inbox-search) + var(--width-inbox-filters-dropdown) + 0.25em ); .inbox-collapsed-note-and-button-wrapper { margin-top: 3px; padding-left: 8px; .inbox-collapsed-note-span { font-size: 1em; } #inbox-expand-all-button { display: block; margin-top: 5px; } } } .inbox-right-part-wrapper { flex-grow: 1; .inbox-right-part { display: flex; justify-content: space-between; flex-grow: 1; > .inbox-action-button:first-child { margin-left: auto; } } } .inbox-row-visibility-policy-inherit { opacity: 0; } /* Stickiness: while a marker or action button has its popover open, keep it in the direct-interaction tier (0.7) so the user can see which element the open menu belongs to. Substring match on `*-popover-visible` covers every class the popover JS toggles — `topic-popover-visible` (added by `topic_popover.ts` and `stream_popover.ts` on `.inbox-action-button`) and `visibility-policy-popover-visible` (added by `user_topic_popover.ts` on the visibility-policy wrappers) — without enumerating element/class combinations, and stays correct if a future popover follows the same naming convention. */ [class*="-popover-visible"] { opacity: 0.7; } .inbox-action-button { outline: none; opacity: 0; cursor: pointer; &.hide { display: none; } & i { /* Match the color used by `.recipient_bar_icon` on the state marker icons, so the 3-dots and the follow/mute/unmute markers render in the same hue as row-attention elevates both to visible. */ color: var(--color-message-header-icon-interactive); } } } .inbox-header-stream-archived { color: var(--color-text-message-header-archived); margin-left: 0.3125em; /* 5px at 16px em */ } } #inbox-view { display: none; position: relative; .inbox-folder { margin-bottom: 1px; background-color: transparent; .inbox-header-name-text, .unread_mention_info { color: var(--color-folder-header); opacity: 0.5; } .inbox-header-name-text { font-style: normal; font-weight: var(--font-weight-sidebar-heading); line-height: 112.5%; letter-spacing: var(--letter-spacing-sidebar-heading); text-transform: uppercase; outline: none; /* 16px at 16px / 1em */ font-size: 1em; /* Align the folder title with the channel privacy icons; 5px at 16px/1em. TODO: Reduce the depth of selectors in this file, so that the use of !important becomes unnecessary here. */ padding-left: 0.3125em !important; } &:hover { .inbox-header-name-text, .collapsible-button .zulip-icon, .unread_mention_info, .unread_count { opacity: 1; } } } .hidden_by_filters { display: none !important; } .channel-folders-inbox-menu-icon { display: grid; /* width excluding left & right margin */ width: calc(var(--left-sidebar-vdots-width) - 4px); cursor: pointer; place-items: center center; border-radius: 3px; margin: 2px; 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); } } } .inbox-container #inbox-pane #inbox-empty-without-search { margin-top: 70px; display: flex; flex-direction: column; align-items: center; } .inbox-empty-illustration { width: 54px; height: 54px; margin-bottom: 16px; background-color: var(--color-background-inbox-no-unreads-illustration); mask: url("../images/empty-view-illustrations/inbox-done.svg") no-repeat center; } .inbox-empty-title { /* Matches the font-size used for .empty-list-message */ font-size: 1.5em; color: var(--color-background-inbox-no-unreads-illustration); } .inbox-empty-action { font-size: 1.3em; padding-top: 0.5em; } .inbox-container #inbox-pane .inbox-empty-action-link { color: var(--color-text-url); &:hover { color: var(--color-text-url-hover); } } #inbox-filter_widget { margin-right: 0.3571em; /* 5px / 14px em */ max-width: var(--width-inbox-filters-dropdown); border: 1px solid var(--color-border-inbox-search); background-color: var(--color-background-inbox-search); gap: 0.3571em; /* 5px / 14px em */ /* 1.5px at 16px/1em and legacy 6px at 14px/1em. */ padding: 0.0937em 0.4285em; &:hover { background-color: var(--color-background-inbox-search-hover); border: 1px solid var(--color-border-inbox-search-hover); } &:focus { outline: none; } } #recent-view-filter_widget .dropdown_widget_value, #inbox-filter_widget .dropdown_widget_value { text-overflow: ellipsis; white-space: nowrap; overflow: hidden; width: 100%; text-align: left; } #recent-view-filter_widget .zulip-icon-chevron-down, #inbox-filter_widget .zulip-icon-chevron-down { color: var(--color-icons-inbox); opacity: 0.4; } .recent-view-filter-dropdown-list-container .dropdown-list-wrapper, .inbox-filter-dropdown-list-container .dropdown-list-wrapper, .new_channel_privacy-dropdown-list-container .dropdown-list-wrapper, .channel_privacy-dropdown-list-container .dropdown-list-wrapper { /* We want these dropdowns to open to fit their contents, which differ based on the language in use. */ width: 100%; /* We also don't want to set a min-width, again so that the content is in charge. This pushes back against more generic styles. */ min-width: unset; } .recent-view-filter-dropdown-list-container .dropdown-list .dropdown-list-item-common-styles, .inbox-filter-dropdown-list-container .dropdown-list .dropdown-list-item-common-styles, .new_channel_privacy-dropdown-list-container .dropdown-list .dropdown-list-item-common-styles, .channel_privacy-dropdown-list-container .dropdown-list .dropdown-list-item-common-styles { /* Parallel to the `width: 100%` set on .dropdown-list-wrapper above, we set min-width here to max-content so that the box opens to accommodate different lengths of text--and to ensure that hovered/selectable areas look correct. */ min-width: max-content; padding: 5px 10px; display: flex; flex-direction: column; /* For columnar flex items, we need to make set alignment to the start of the flex container. This pushes back against non-columnar flexboxes that require centered alignment. */ align-items: flex-start; } .recent-view-filter-dropdown-list-container .dropdown-list-item-name, .inbox-filter-dropdown-list-container .dropdown-list-item-name, .new_channel_privacy-dropdown-list-container .dropdown-list-item-name, .channel_privacy-dropdown-list-container .dropdown-list-item-name { white-space: nowrap; font-weight: 500; padding: 0; margin: 0; text-overflow: ellipsis; overflow: hidden; } .recent-view-filter-dropdown-list-container .dropdown-list-item-description, .inbox-filter-dropdown-list-container .dropdown-list-item-description, .channel_privacy-dropdown-list-container .dropdown-list-item-description, .new_channel_privacy-dropdown-list-container .dropdown-list-item-description { white-space: nowrap; font-weight: 400; font-size: 0.9285em; /* 13px at 14px em */ opacity: 0.8; padding: 0; text-overflow: ellipsis; overflow: hidden; } .inbox-container #inbox-pane .inbox-folder .unread_count { transition: none; display: none; } #inbox-pane #inbox-list .collapsible-button { visibility: hidden; } #inbox-pane #inbox-list .inbox-collapsed-state .collapsible-button { visibility: visible; .zulip-icon-chevron-down { transform: rotate(0deg); } } .folder-row-chevron { color: var(--color-folder-header); } .channel-row-chevron { color: var(--color-inbox-row-chevron); } .inbox-collapsed-state { .folder-row-chevron, .channel-row-chevron { opacity: 0.5; } } .inbox-container #inbox-pane .inbox-folder .unread_mention_info { display: none; } .inbox-container #inbox-pane .inbox-folder.inbox-collapsed-state .unread_mention_info, .inbox-container #inbox-pane .inbox-folder.inbox-collapsed-state .unread_count { display: inline; } .inbox-folder-components { border-radius: 5px; border: var(--inbox-view-folder-component-border-width) solid var(--color-border-inbox-folder-component); overflow: hidden; &:has(.inbox-row:not(.hidden_by_filters)), &:has(.inbox-header:not(.hidden_by_filters)) { margin-bottom: 0.5em; } } .inbox-folder.inbox-collapsed-state, .inbox-folder.hidden_by_filters { + .inbox-folder-components { border: 0; } } #inbox-pane #inbox-list .inbox-collapsed-state { + .inbox-folder-components, + .inbox-topic-container { display: none; } } #inbox-pane #inbox-list .inbox-streams-container .inbox-header .inbox-header-name { /* 5px at 16px / 1em */ padding: 1px 0.3125em 1px 0; } #inbox-view.no-visible-focus-outlines .inbox-container #inbox-pane [tabindex="0"] { /* Remove default focus outline from elements. */ &:focus-visible { outline: 0; } } #inbox-view:not(.no-visible-focus-outlines) .inbox-container #inbox-pane { /* Only show focus outlines when user uses keyboard. */ #inbox-filter_widget { &:focus-visible { outline: var(--focus-ring-outline); } } .inbox-row, .inbox-header { &:focus-visible { /* Flush outline on the row itself instead of the old 2px inner-border on `.inbox-focus-border`; matches `.recent-view-body-row`'s focus style, but inbox rows are tighter (30px) so the outline sits at the row edges instead of insetting 4px. */ outline: var(--focus-ring-outline); outline-offset: var(--focus-ring-outline-offset-in-views); border-radius: 0.3125em; /* 5px at 16px/1em */ } } .inbox-header:focus-visible { .collapsible-button > .zulip-icon { opacity: 1; } .collapsible-button { visibility: visible; } } /* Focus ring for the icon-style row actions. Outline lives on the wrapper (at full opacity) so the ring renders at full blue saturation and meets WCAG 1.4.11's 3:1 contrast requirement for focus indicators. Negative outline-offset pulls the 1.5px stroke inside the 30×30 wrapper bounds so it doesn't clip against the row's overflow:hidden edge, giving a 26×26 effective ring. The inner icon dims separately. */ .channel-visibility-policy-indicator, .visibility-policy-indicator, .inbox-action-button { &:focus-visible { outline: var(--focus-ring-outline); outline-offset: -0.125em; /* -2px at 16px/1em */ border-radius: 0.3125em; /* 5px at 16px/1em */ > i, > .zulip-icon, > .recipient_bar_icon { opacity: 0.7; } } } /* Focus ring drawn as a pseudo-element on the inner badge, with `5px` of breathing room outside the badge. A wrapper-sized outline (the previous implementation) leaves a wide gap between the ring and the badge fill that reads as a second concentric outline; the deliberate 5px gap here reads as padding around a single badge instead. */ .unread-count-focus-outline { &:focus-visible { outline: none; .unread_count { position: relative; /* Suppress the inner `.normal-count` `inset` `box-shadow` so it doesn't sit as a second, narrower outline inside the focus ring. */ box-shadow: none; &::after { content: ""; position: absolute; inset: -0.3125em; /* -5px at 16px/1em */ border: var(--focus-ring-outline); border-radius: var( --unread-count-focus-outline-radius-in-views ); pointer-events: none; } } } } .inbox-folder { &:focus-visible { .inbox-header-name-text, .collapsible-button .zulip-icon, .unread_mention_info, .unread_count { opacity: 1; } background: light-dark( transparent, var(--color-background-hover-popover-menu) ); } } } #inbox-view .inbox-container #inbox-pane #inbox-expand-all-button { --color-outline-button-focus: var(--color-outline-focus); --outline-offset-button-focus: var(--focus-ring-outline-offset-in-views); } #inbox-animation-extra-content-blocker { background: var(--color-background-inbox); }