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
1073 lines
34 KiB
CSS
1073 lines
34 KiB
CSS
.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 {
|
||
--recent-view-header-channel-column-adjustment: calc(
|
||
var(--recent-view-row-left-padding) +
|
||
var(--recent-view-body-border-width) -
|
||
var(--recent-view-header-border-width)
|
||
);
|
||
|
||
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: var(--recent-view-header-border-width) 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: var(--recent-view-row-left-padding);
|
||
}
|
||
|
||
.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 {
|
||
/* 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.
|
||
|
||
The first column is floored to the "Channel" header's
|
||
JS-measured width so the label never overlaps the next
|
||
column when channel names are short or absent. */
|
||
display: grid;
|
||
grid-template-columns:
|
||
max(
|
||
var(--recent-view-header-channel-column-min-width, 0px),
|
||
calc(
|
||
min(
|
||
calc(
|
||
var(
|
||
--longest-subscribed-channel-name-width
|
||
) +
|
||
1em + 8px
|
||
),
|
||
calc(
|
||
calc(
|
||
100% -
|
||
var(
|
||
--recent-view-header-channel-column-adjustment
|
||
)
|
||
) /
|
||
3
|
||
)
|
||
) +
|
||
var(--recent-view-header-channel-column-adjustment)
|
||
)
|
||
)
|
||
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;
|
||
/* Padding gives the hover pill its size. */
|
||
padding: 0.25em; /* 4px at 16px/1em */
|
||
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;
|
||
}
|
||
|
||
.recent-view-conversation-sort-header {
|
||
/* Cancel the pill padding so the label aligns with topic
|
||
text in rows. */
|
||
margin-left: -0.25em; /* -4px at 16px/1em */
|
||
/* 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 {
|
||
--recent-view-row-left-padding: 0.5em; /* 8px at 16px/1em */
|
||
|
||
td {
|
||
/* 6px 8px at 16px/1em */
|
||
padding: 0.375em 0.5em 0.375em var(--recent-view-row-left-padding);
|
||
}
|
||
|
||
.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. Floored to
|
||
match the header grid's "Channel" floor. */
|
||
grid-template-columns:
|
||
max(
|
||
calc(
|
||
var(
|
||
--recent-view-header-channel-column-min-width,
|
||
0px
|
||
) -
|
||
var(--recent-view-header-channel-column-adjustment)
|
||
),
|
||
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;
|
||
}
|
||
}
|