/
githubmirror
/
zulip
Обзор
Документация
Войти
/
githubmirror
/
zulip
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
web/styles/recent_view.css
1 046 строк
32 KB
Ujjwal Verma
css: Remove dead .zulip-icon-user selector.
31 июл 2026, 22:04
31 июл 2026, 22:04
09ff82e
Код
Авторство
О чём код?
.recent_view_container { padding: 0; overflow: hidden; display: flex; flex-direction: column; position: sticky; top: var(--navbar-fixed-height); z-index: 1; } .recent_view_container #recent_view_table { max-width: 100%; overflow: hidden !important; display: flex; flex-direction: column; border: 0; background-color: var(--color-background); } #recent_view_table .table, #recent-view-content-table { /* To keep border properties to the thead th. */ border-collapse: separate; table-layout: fixed; border-spacing: 0; width: 100%; } #recent-view-content-table { border: var(--recent-view-body-border-width) solid var(--color-border-recent-view-table); border-top: none; border-radius: 0 0 5px 5px; overflow: hidden; } #recent_view { display: none; padding-top: var(--navbar-fixed-height); /* Add bottom padding equal to `#bottom-whitespace`. This helps us keep #compose visible at its max-height without overlapping with any visible topics. */ padding-bottom: var(--max-unmaximized-compose-height); & td { box-sizing: border-box; vertical-align: middle; padding: 0.1875em 0.5em; /* 3px 8px at 16px/1em */ } /* Suppress focus rings and the focus-driven vdots reveal when focus was set programmatically (e.g., on page load or sidebar click). The class is removed on the first keyboard navigation, restoring :focus outlines and the icon reveals below. See focus_outline_util.ts. The icon-opacity rules mirror the selectors they override (in `.recent_view_focusable` below) so each wins on specificity from the extra class. */ &.no-visible-focus-outlines .recent_view_focusable { &:focus { outline: 0; } &:focus > .change_visibility_policy :is( .recent-view-row-topic-menu, .visibility-status-icon[data-vdots-original-icon-class] ) { opacity: 0; } &.recent-view-body-row:focus:not(:hover) .change_visibility_policy :is( .recent-view-row-topic-menu, .visibility-status-icon[data-vdots-original-icon-class] ) { opacity: 0; } } .recent_view_focusable { & > .zulip-icon { outline: 0; } &:focus { outline: var(--focus-ring-outline); /* Match both the natively-rendered vdots icon and the swapped vdots (signaled by the data attribute set in `recent_view_ui.update_visibility_icon_swap_state`) so the wrapper-focus tier reads the same regardless of whether the row has a status marker. */ > .change_visibility_policy :is( .recent-view-row-topic-menu, .visibility-status-icon[data-vdots-original-icon-class] ) { /* Wrapper focus = direct interaction on the menu button; top of the tier. */ opacity: 0.7; } } /* Stickiness: while the 3-dots topic menu has its popover open, keep the icon in the direct-interaction tier (0.7) so the user can see which row the open menu belongs to. */ &.topic-popover-visible > .change_visibility_policy .visibility-status-icon { opacity: 0.7; } /* Row keyboard-selected but not mouse-hovered: extra-quiet reveal of the default-state 3-dots icon, so keyboard navigation doesn't loudly advertise an action the user rarely needs. Mouse hover's 0.4 rule below overrides whenever the mouse is on the row. */ &.recent-view-body-row:focus:not(:hover) .change_visibility_policy :is( .recent-view-row-topic-menu, .visibility-status-icon[data-vdots-original-icon-class] ) { opacity: 0.2; } &.recent-view-topic-visibility { /* 3px (vs. 5px on `.recent-view-body-row`) keeps a consistent perceived corner-rounding ratio: 5px on a wide row reads as a subtle round, but on a small 22×22 ring (icon-natural wrapper + 3px outset) it reads as nearly circular. */ border-radius: 0.1875em; /* 3px at 16px/1em */ /* 3px rather than 5px: the wrapper sits hard against the recent-view table's right edge (the table has overflow:hidden), so a 5px outset left the outline's right side clipped. 3px gives comfortable clearance while still matching the inbox's 22×22 effective ring size around a 16×16 icon. */ outline-offset: 0.1875em; /* 3px at 16px/1em */ } > .change_visibility_policy { display: flex; } > .change_visibility_policy .recent-view-row-topic-menu { /* Tertiary action: hidden at rest, revealed on row hover / focus, brightened on direct interaction. */ opacity: 0; cursor: pointer; &:hover { opacity: 0.7; } &:not(.visibility-status-icon) { display: none; } } } & a { color: var(--color-recent-view-link); text-decoration: none; } .empty-table-message { background-color: var(--color-background); padding: 3em 1em; } .recent-view-empty-load-more { margin-top: 10px; display: flex; flex-direction: column; align-items: center; gap: 10px; /* Reset the 1.5em font-size inherited from .empty-table-message so the load-more text and button match normal UI text size. */ font-size: var(--base-font-size-px); } .fa-check-square-o, .fa-square-o { width: 0.7142em; /* Legacy 10px size at 14px/1em. */ padding-right: 0.3571em; /* Legacy 5px size at 14px/1em. */ padding-left: 0.1428em; /* Legacy 2px size at 14px/1em. */ } .table_fix_head { padding: 0 !important; border: 1px solid var(--color-border-recent-view-table); border-radius: 5px 5px 0 0; overflow: hidden; } .recent-view-load-more-container { margin: 20px 10px; align-items: center; } .fetch-messages-button { display: grid; place-items: center; /* Stack the loading indicator and button label in the same grid cell so the label stays vertically centered when the indicator is empty/hidden. */ > * { grid-row: 1; grid-column: 1; } /* Override the inline height: 0 set by loading.make_indicator so the spinner participates in grid alignment. */ .loading-indicator { height: auto !important; } .loading_indicator_spinner { height: 20px; width: 20px; } path { fill: var(--color-recent-view-loading-spinner); } } #recent_view_filter_buttons { /* This padding-top value aligns the recents filters with the filters in the left and right sidebars. TODO: As part of the recents redesign, these filters should match the height of the sidebar filters. */ padding: 10px 0 0; display: flex; gap: var(--recent-topics-filters-gap); /* Search box has no height without this in safari. */ flex: 0 0 auto; flex-wrap: wrap; justify-content: flex-start; background: var(--color-background); } #recent_filters_group { display: flex; flex-flow: row wrap; gap: var(--recent-topics-filters-gap); } #recent-view-search-wrapper { flex-grow: 1; margin-bottom: var(--recent-topics-filters-gap); /* Prevent the search input outline from being clipped by the overflow:hidden ancestor at fractional browser zoom levels. Applied on both sides because this element can wrap to its own line at narrow widths. 2px because 1px can round to 0 device pixels at certain fractional zoom levels (e.g., 80%). */ margin-inline: 2px; } .button-recent-filters { color: var(--color-text-default); background-color: var(--color-background-zulip-button); border: 1px solid var(--color-border-zulip-button); border-radius: 40px; /* Matching to the height of the filter search box. */ height: var(--recent-topics-filters-height); /* Legacy 12px at 14px/1em. */ padding: 0 0.8571em; flex: 0 0 auto; display: flex; align-items: center; font-family: inherit; font-size: inherit; &:hover { background-color: var(--color-background-zulip-button-hover); } &:active { background-color: var(--color-background-zulip-button-active); } &:focus-visible { background-color: var( --color-background-recent-filters-button-focus ); outline: 0; } &.fake_disabled_button { cursor: not-allowed; opacity: 0.5; &:hover { background-color: var( --color-background-recent-filters-button-disabled ); border-color: var( --color-border-recent-filters-button-disabled ); } } } .button-recent-selected { color: var(--color-text-brand-subtle-action-button); background-color: var(--color-background-brand-subtle-action-button); &:hover { background-color: var( --color-background-brand-subtle-action-button-hover ); } &:active { background-color: var( --color-background-brand-subtle-action-button-active ); } } .unread_count { /* Focus underline can only occupy the total length of the unread count */ margin-right: 1px; margin-left: 1px; align-self: center; opacity: 1; outline: 0 solid var(--color-background-unread-counter); &:hover { outline-width: var(--focus-ring-width-in-views); } } .unread_mention_info:not(:empty) { /* Zero out right margin from left sidebar presentation. */ margin-right: 0; /* Match with its font-size. */ line-height: 14px; /* Present a default/arrow cursor */ cursor: default; } .unread_hidden { display: none; } .flex_container { display: flex; align-items: center; } .inline-flex-container { display: inline-flex; align-items: center; } .recent_topic_actions { /* Add spacing between mention marker, unread count and mute icon */ margin-left: 0.3125em; /* 5px at 16px/1em */ display: flex; flex-flow: row nowrap; } .recent-view-unread-mention-and-count-wrapper .recent_topic_actions { /* Unset margin; gap on the wrapper handles spacing. */ margin-left: 0; margin-right: 0; } .recent-view-unread-mention-and-count-wrapper .unread_count { /* Unset margin; gap on the wrapper handles spacing. */ margin: 0; } .mention_in_unread { opacity: 0.7; } .recent_topic_actions.dummy_action_button { visibility: hidden; } .recent_topic_actions .recipient_bar_icon { /* Zero out padding used in recipient bar. */ padding-right: 0; padding-left: 0; } /* Topic state markers (follow/mute/unmute) are secondary information: visible at a glance, but quieter than primary content. Overrides `.recipient_bar_icon`'s 0.2 idle / 0.4 hover from message_header.css. */ .recent_topic_actions .visibility-status-icon:not(.recent-view-row-topic-menu) { /* 0.45 is the lowest opacity that meets WCAG 1.4.11's 3:1 non-text contrast against both light and dark row backgrounds. */ opacity: 0.45; &:hover { /* Beat `.recipient_bar_icon:hover`'s !important. */ opacity: 0.7 !important; } } .recent_view_participants { display: inline-flex; /* Causes LI items to display in row. */ /* Keep avatars centered on the line. */ vertical-align: middle; list-style-type: none; margin: auto; /* Centers vertically / horizontally in flex container. */ /* 24px at 16px/1em */ height: 1.5em; /* 4px at 16px/1em */ padding: 0.25em 0; border-radius: 6px; overflow: hidden; /* By using the row-reverse layout, the visual ordering will be opposite of the DOM ordering. This will allows us to stack the items in the opposite direction of the natural stacking order without having to mess with the zIndex value. The MAJOR DOWNSIDE is that the HTML itself now reads backwards, which super janky. */ flex-direction: row-reverse; &:first-child { /* Remove padding of first child to align with header text. */ padding-left: 0; } } .recent_view_participant_item { /* 24px at 16px/1em */ height: 1.5em; margin: 0; padding: 0 var(--recent-view-participant-item-padding-x); position: relative; width: var(--recent-view-participant-item-width); cursor: pointer; /* Can't use last-child due to tippy element being appended */ &:last-of-type { /* Remove right padding of last item to align with header text. */ padding-left: 0; } &:first-of-type { padding-right: 0; } .fa-user { opacity: 0.7; } } .recent_view_participant_avatar, .recent_view_participant_overflow { border: 0; /* Keep the rounded corners from ballooning to a circle at smaller font sizes. 6px at 16px/1em */ border-radius: 0.375em; color: var(--color-recent-view-participant-overflow-text); display: block; height: 100%; text-align: center; background-color: var( --color-background-recent-view-participant-overflow ); } .recent_view_participant_avatar { background-color: transparent; } .recent_view_participant_overflow { /* 24px at 16px/1em */ line-height: 1.5; background-color: transparent; color: var(--color-text-recent-view-last-msg-time); text-align: left; } & tr { background-color: var(--color-background-recent-view-row); /* Avoid annoying, confusing hover-state when scrolling on touch devices like iPad that treat a tap-and-hold/swipe as a hover. */ @media (hover: hover) and (pointer: fine) { &:hover { background-color: var(--color-background-recent-view-row-hover); .change_visibility_policy .recent-view-row-topic-menu { /* Tertiary action reveal tier (row hover). */ opacity: 0.45; } } } } & tr .recent_topic_name { /* Extra left padding to make room for the unread marker bar alongside the keyboard selection outline, applied to all rows so content aligns between read and unread rows. */ padding-left: 0.75em; /* 12px at 16px/1em */ } .unread_topic { & a { color: var(--color-recent-view-link-unread); font-weight: 500; } .recent_topic_name { position: relative; &::before { content: ""; position: absolute; top: 0; left: 0; width: 2px; height: 100%; background: linear-gradient( 90deg, var(--color-unread-marker) 30%, hsl(217deg 64% 59% / 0%) ); border-radius: 2px 0 0 2px; } } } /* When consecutive rows are both unread, make the blue marker bar continuous with no gap between them. */ .unread_topic + .unread_topic .recent_topic_name::before { border-top-left-radius: 0; } /* Also remove the bottom radius on the preceding unread row when followed by another unread row. We use :has() to look ahead at the next sibling. */ .unread_topic:has(+ .unread_topic) .recent_topic_name::before { border-bottom-left-radius: 0; } /* Hide the unread marker bar when the "Unread" filter is active, since every visible row is already unread. */ &.recent-view-filtered-by-unread .unread_topic .recent_topic_name::before { display: none; } .last_msg_time { float: left; margin-right: 5px; .last-msg-link { /* Prevent time strings from collapsing over multiple lines. */ display: block; min-width: max-content; color: var(--color-text-recent-view-last-msg-time); } } .unread_topic .last-msg-link { color: var(--color-text-recent-view-last-msg-time-unread); } & thead th { box-sizing: border-box; text-align: left; padding: 0.25em 0 0.25em 0.5em; /* 4px at 16px/1em */ background-color: var(--color-background-recent-view-table-thead-th); overflow: hidden; color: var(--color-text-recent-view-table-thead-th); text-transform: uppercase; font-weight: 600; line-height: 1.25em; /* 20px at 16px/1em */ z-index: 1; .recent-view-header-wrapper { display: flex; align-items: center; } .table-sortable-arrow { align-self: center; font-size: 1em; /* 16px at 16px/1em */ transform: rotate(180deg); opacity: 0; transition: opacity 100ms ease-out; color: var(--color-arrow-recent-view-table-thead-th); } &.descend .table-sortable-arrow { transform: rotate(0deg); } &:not(.active)[data-sort]:hover .table-sortable-arrow { opacity: 0.7; } &.active .table-sortable-arrow { opacity: 1; } &.active { opacity: 1; transition: opacity 100ms ease-out; } &[data-sort]:hover { cursor: pointer; background-color: var( --color-background-recent-view-table-thead-sort-header ); transition: background-color 100ms ease-in-out; } .zulip-icon-chevron-right { font-size: 0.9375em; /* 15px at 16px/1em */ align-self: center; color: hsl(0deg 0% 60%); padding: 0 0.125em; /* 2px at 16px/1em */ } } #recent-view-table-headers:hover .recent-view-unread-sort-header .table-sortable-arrow { opacity: 0.7; } .recent-view-topic-header { /* Match left padding with row td (.recent_topic_name), including the extra space for the unread marker bar. */ padding: 0 0.5em 0 0; /* 8px 12px at 16px/1em */ /* th no longer has data-sort; the individual sort spans handle cursor and hover. */ cursor: default; .recent-view-header-wrapper { /* 0.75em left padding, 1px header width */ --recent-view-channel-row-header-minus-body: calc( 0.75em + var(--recent-view-body-border-width) - 1px ); /* Match the row grid so "Channel" and "Conversation" sort spans align with channel and topic text in rows. Sort arrows are inside the spans, so 3 columns suffice. 1em = 16px at 16px/1em, matching the 1em (16px at 16px/1em) privacy icon width in data rows. */ display: grid; grid-template-columns: calc( min( calc( var(--longest-subscribed-channel-name-width) + 1em + 8px ), calc( calc( 100% - var( --recent-view-channel-row-header-minus-body ) ) / 3 ) ) + var(--recent-view-channel-row-header-minus-body) ) auto 1fr auto; align-items: stretch; } .recent-view-channel-sort-header, .recent-view-conversation-sort-header, .recent-view-unread-sort-header { display: flex; align-items: center; gap: 4px; cursor: pointer; /* Negative margin compensates the padding so the pill hover highlight doesn't push adjacent elements out of alignment. */ padding: 0.25em; /* 4px at 16px/1em */ margin: 0; border-radius: 3px; transition: background-color 100ms ease-in-out; &:hover { background-color: var( --color-background-recent-view-table-thead-sort-header ); .table-sortable-arrow { opacity: 0.7; } } &.active .table-sortable-arrow { opacity: 1; } &.descend .table-sortable-arrow { transform: rotate(0deg); } } .recent-view-channel-sort-header { padding-left: 1em; margin-left: 0; } .recent-view-conversation-sort-header { /* Prevent hover pill highlight from extending into the adjacent unread sort column. */ margin-right: 0.3125em; /* 5px at 16px/1em */ } .recent-view-unread-sort-header { /* Use width that reasonably covers 2-3 magnitudes of count. */ width: 1.5em; margin-right: -0.1563em; /* 2.5px at 16px/1em */ justify-self: end; justify-content: center; } } .recent_topic_users, .recent-view-participants-header { padding-right: 0; padding-left: var(--recent-view-participants-padding-x-start); width: calc( var(--recent-view-participants-padding-x-start) + (var(--recent-view-max-avatars) + 1) * ( var(--recent-view-participant-item-width) + 2 * var(--recent-view-participant-item-padding-x) ) - /* First item start padding is --recent-view-participants-padding-x-start. Last item doesn't have end padding. */ 2 * var(--recent-view-participant-item-padding-x) ); } &.recent-view-participants-hidden { td { padding: 0.375em 0.5em; /* 6px 8px at 16px/1em */ } .recent_topic_users, .recent-view-participants-header { display: none; } } .recent-view-participants-header .recent-view-header-wrapper { display: block; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; } .recent_topic_timestamp, .recent-view-last-msg-time-header { /* Time text width is measured from JS and set as --recent-view-time-text-width. Add the visibility icon width (1em), cell padding (2 × 0.5em), and space for the icon's focus outline (0.5em). Falls back to 25% before JS has set the variable. */ width: 25%; width: calc(var(--recent-view-time-text-width) + 2.5em); } .recent_topic_name { .recent-view-conversation-with-unread-wrapper { display: grid; grid-template-areas: "conversation-type chevron-right conversation-name unread-mention-and-count" ". . conversation-name ."; width: 100%; /* We add 1em for the privacy icon and 8px for the flexbox gap between the icon and the channel name. */ grid-template-columns: min( calc( var(--longest-subscribed-channel-name-width) + 1em + 8px ), calc(100% / 3) ) auto 1fr auto; align-items: center; } .recent-view-conversation-type-wrapper { grid-area: conversation-type; display: flex; align-items: center; gap: 8px; .stream-privacy { display: flex; } } .recent-view-conversation-link { grid-area: conversation-name; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .recent-view-unread-mention-and-count-wrapper { grid-area: unread-mention-and-count; /* Reserve space so a focused unread badge's `-5px` outset focus ring (`.recent-view-table-unread-count`'s `::before`) does not paint over the topic-name's ellipsis when the topic is long enough to truncate. */ padding-left: 0.4375em; /* 7px at 16px/1em */ } .zulip-icon-chevron-right { grid-area: chevron-right; font-size: 0.9375em; /* 15px at 16px/1em */ color: hsl(0deg 0% 60%); padding: 0 0.125em; /* 2px at 16px/1em */ } .recent-view-dm-name, .recent-view-channel-name { opacity: 0.8; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } } & thead .last_msg_time_header { /* The responsive table of bootstrap somehow ignores the width of ::after element. This ensures it is always visible. 20px = space occupied by ::after (icon) + some extra padding. */ padding-right: 20px; } @media (width < $md_min) { /* Hide last message time on smaller screens. */ &.recent-view-participants-hidden { .recent_topic_timestamp, thead .last_msg_time_header { display: none; } } .recent_topic_actions { margin-right: 5px; font-size: 1.0714em; /* 15px at 14px / em */ } } .private_conversation_row { .pm_status_icon { display: flex; justify-content: center; align-items: center; .zulip-icon.conversation-partners-icon, .zulip-icon.zulip-icon-bot { opacity: 0.6; } .user-circle { min-width: var( --length-user-status-circle-recent-conversations ); font-size: 0.625em; /* 10px at 16px/1em */ float: left; position: unset; /* Make the width 16px to match other icons. */ padding: 0 0.3em; /* 3px at 10/1em */ } } .recent-view-dm-group { /* We don't display status emoji in group DMs, so prepare an ordinary inline layout... */ .user-status-microlayout { display: inline; white-space: nowrap; } /* ...and hide the status emoji. */ .status-emoji { display: none; } } } } #recent_view_bottom_whitespace { #recent_view_loading_messages_indicator, .bottom-messages-logo { display: block; position: absolute; top: 200px; left: 0; right: 0; margin: auto; .loading_indicator_spinner { position: relative; top: -7px; } } } #recent_view_folder_filter_container { display: flex; align-items: center; height: var(--recent-topics-filters-height); } #recent_view_folder_filter_button { font-size: 1.5625em; /* 25px at 16px/1em */ padding: 0; &:focus-visible { outline: var(--focus-ring-outline); } } #recent-view-filter_widget { display: inline-flex; width: 10.7142em; /* 150px at 14px em */ /* Prevent the outline from being clipped by the overflow:hidden ancestor at fractional browser zoom levels. 2px because 1px can round to 0 device pixels at certain zoom levels (e.g., 80%). */ margin-left: 2px; /* 1.5px 6px at 16px/1em */ padding: 0 0.375em; /* Matching to the height of the filter search box. */ height: var(--recent-topics-filters-height); /* Matching to the input styles otherwise defined on .input-element in inputs.css */ color: var(--color-text-input); background: var(--color-background-input); border: none; outline: 1px solid var(--color-outline-input); outline-offset: -1px; &:hover { outline-color: var(--color-outline-input-hover); } &:focus { background-color: var(--color-background-input-focus); outline-color: var(--color-outline-input-focus); box-shadow: 0 0 5px var(--color-box-shadow-input-focus); } } #recent_view .recent_topic_timestamp .flex_container { /* Push the visibility icon to the right edge of the cell. */ justify-content: space-between; } .recent-view-unread-mention-and-count-wrapper { display: flex; align-items: center; /* Stretch to the full row height so the unread count is a larger click target for "Mark as read". */ align-self: stretch; margin-left: auto; flex-direction: row; /* Match the left sidebar gap between @ marker and unread count. */ gap: 5px; .unread_mention_info { /* Unset margin in favor of flex gap. */ margin: 0; } .recent-view-unread-count-wrapper { display: flex; align-items: center; align-self: stretch; } } .recent-view-body-row { outline-offset: -0.25em; /* -4px at 16px/1em */ /* 0.4688em (vs. inbox's 0.3125em / 5px) — the larger negative offset on this view shrinks the ring's effective corner radius (`border-radius` minus `|outline-offset|`), so we bump the base radius so the ring's painted corners read with the same curvature as `.inbox-row`'s 3.5px effective radius. Em-based so both offset and radius scale together with the user's font size, keeping the perceived corner-rounding ratio stable. */ border-radius: 0.4688em; /* 7.5px at 16px/1em */ } .recent-view-table-unread-count { border-radius: 0.3125em; /* 5px at 16px/1em */ position: relative; } /* Focus ring drawn as a pseudo-element so the ring's corner radius can be set independently of the badge's, and so the wrapper-level `:focus` outline rule on `.recent_view_focusable` (which the badge also matches) does not stack a second concentric outline on top. Scoped via `#recent_view:not(.no-visible-focus-outlines)` so it stays suppressed alongside the wrapper outline until the first keyboard navigation key removes the class (see focus_outline_util.ts), and so the rule has high enough specificity to beat the wrapper rule. Inset `-5px` matches the breathing room the inbox wrapper-extending outline gets from its `padding: 0 5px`. */ #recent_view:not(.no-visible-focus-outlines) .recent-view-table-unread-count:focus { outline: none; &::before { 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; } }