Files
ThreadNet-Web/packages/shared-components/src/room/timeline/event-tile/EventTileView/EventTileView.module.css
T
rbondessonandGitHub 72d8d9f60f Move EventTile to shared components - #4b (#34565)
* Define the shared EventTile shell contract

* Create a functional shared shell skeleton

* Implement the thread rendering branch

* Implement notification and threads-list branches

* Implement file and default timeline branches

* Add shared stories and complete shell tests

* Adding state flags, stories and styling

* Correct DOM order for IRC branch

* Refine EventTileView Storybook shell and fixtures

* Fix the room story for modern layout

* Fix Bubble layout

* Fix IRC layout

* Fix ThreadsList and Thread

* Fix File and Notification

* Cleanup of stories and styling

* Fix lint errors

* Added baseline screenshots

* Rename to avoid knip errors

* Avoid the knip error

* Fix test fail on click

* Use separate div wrappers for slots instead of relying on cloning components.

* Renamed names for slot classes

* Changed to conditional state classes

* Change layout and shape variants to classes

* Clean up and reorder for better readability

* Fix Sonar issues

* Fix Sonar issues
2026-08-12 06:53:40 +00:00

784 lines
19 KiB
CSS

/*
* Copyright 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
/* Shared root and slot-boundary baseline. */
.root {
/* Group geometry. */
--event-tile-group-gutter: 64px;
--event-tile-group-line-spacing-block-start: 1px;
--event-tile-group-line-spacing-block-end: 3px;
--event-tile-group-line-height: 22px;
/* IRC geometry. */
--event-tile-irc-line-height: 18px;
--event-tile-irc-name-width: 80px;
--event-tile-irc-icon-width: 14px;
--event-tile-irc-timestamp-width: 48px;
--event-tile-irc-right-padding: 5px;
/* Bubble geometry. */
--event-tile-bubble-corner-radius: 12px;
--event-tile-bubble-gap: 5px;
--event-tile-bubble-gutter: 11px;
--event-tile-bubble-other-background: var(--cpd-color-gray-300, #e9edf2);
--event-tile-bubble-self-background: var(--cpd-color-green-300, #d8f5ed);
/* Shared preview, action-bar, bubble, and interaction values. */
--event-tile-preview-offset: 48px;
--event-tile-action-bar-size: 28px;
--event-tile-action-bar-margin: 3px;
--event-tile-action-bar-offset: 8px;
--event-tile-state-hover-background: var(--cpd-color-bg-subtle-secondary, #e9edf2);
--event-tile-state-highlight-background: var(--cpd-color-yellow-200, #fff8e3);
--event-tile-state-selected-background: var(--cpd-color-bg-subtle-secondary, #e9edf2);
--event-tile-state-marker-width: 4px;
--event-tile-state-marker-color: var(--cpd-color-bg-action-primary-rest, #0dbd8b);
position: relative;
flex-shrink: 0;
max-width: 100%;
clear: both;
padding-top: 18px;
&.stateContinuation {
padding-top: 0;
}
}
.line {
position: relative;
min-width: 0;
}
/* Slot wrappers are the public styling boundary. The supplied content may be
a component, fragment, or multiple elements and does not need to forward
layout props. */
.slotActionBar,
.slotAvatar,
.slotBody,
.slotContextMenu,
.slotFooter,
.slotNotificationBadge,
.slotNotificationRoomLabel,
.slotPadlock,
.slotReceipt,
.slotReplyChain,
.slotSender,
.slotThreadInfo,
.slotTimestamp {
min-width: 0;
}
.slotContextMenu {
z-index: 1;
}
/* Shared structural containers. */
.senderDetails {
display: flex;
align-items: flex-start;
}
.details {
display: flex;
width: 100%;
align-items: center;
justify-content: space-between;
gap: var(--cpd-space-2x, 8px);
}
/* The shell owns the action-bar boundary. The action-bar contents own their controls.
Presence of the slot is the render/show signal supplied by the host. */
.slotActionBar {
position: absolute;
z-index: 10;
inset-block-start: calc(
-1 *
(var(--event-tile-action-bar-size) + 2 *
(var(--event-tile-action-bar-margin) + var(--cpd-border-width-1, 1px)))
);
inset-inline-end: var(--event-tile-action-bar-offset);
user-select: none;
}
/* Group layout. */
.root.layoutGroup {
&:hover .line,
&:focus-within .line {
background-color: var(--event-tile-state-hover-background);
}
&.stateHighlighted .line {
background-color: var(--event-tile-state-highlight-background);
}
&.stateSelected .line,
&.stateEditing .line {
background-color: var(--event-tile-state-selected-background);
}
&.stateSelected .line::before,
&.stateEditing .line::before {
content: "";
position: absolute;
z-index: 0;
inset-block: 0;
inset-inline-start: 0;
width: var(--event-tile-state-marker-width);
border-start-start-radius: inherit;
border-end-start-radius: inherit;
background: var(--event-tile-state-marker-color);
pointer-events: none;
}
.line {
padding-block: var(--event-tile-group-line-spacing-block-start) var(--event-tile-group-line-spacing-block-end);
padding-inline-start: var(--event-tile-group-gutter);
margin-inline-end: 80px;
min-height: 14px;
border-radius: 8px;
line-height: var(--event-tile-group-line-height);
}
.slotSender {
display: flex;
align-items: center;
max-width: calc(100% - var(--event-tile-group-gutter));
margin-inline-start: var(--event-tile-group-gutter);
min-height: 26px;
line-height: 20px;
}
.slotAvatar {
position: absolute;
z-index: 9;
inset-block-start: 14px;
inset-inline-start: var(--cpd-space-2x, 8px);
}
.slotTimestamp {
position: absolute;
inset-inline-start: 0;
width: var(--MessageTimestamp-width, 46px);
text-align: center;
}
.slotPadlock {
position: absolute;
inset-block-start: 0;
inset-inline-start: 44px;
height: var(--event-tile-group-line-height);
margin: 1px;
}
.slotThreadInfo {
margin-inline-start: var(--event-tile-group-gutter);
}
.slotReplyChain {
margin-inline-start: 0;
margin-bottom: var(--cpd-space-2x, 8px);
}
.slotFooter {
margin: var(--cpd-space-1x, 4px) 0 var(--cpd-space-1x, 4px) var(--event-tile-group-gutter);
}
.slotReceipt {
margin-inline-end: 10px;
float: inline-end;
position: relative;
inset-block-start: calc(-1 * (var(--event-tile-group-line-height) + 3px));
width: 90px;
height: 1px;
text-align: end;
overflow: visible;
}
&.stateContinuation .line {
clear: both;
}
}
/* IRC layout. */
.root.layoutIrc {
display: flex;
align-items: flex-start;
padding-top: 0;
line-height: var(--event-tile-irc-line-height);
&:hover .line,
&:focus-within .line {
background-color: var(--event-tile-state-hover-background);
}
&.stateHighlighted .line {
background-color: var(--event-tile-state-highlight-background);
}
&.stateSelected .line,
&.stateEditing .line {
background-color: var(--event-tile-state-selected-background);
}
&.stateSelected .line::before,
&.stateEditing .line::before {
content: "";
position: absolute;
z-index: 0;
inset-block: 0;
inset-inline-start: 0;
width: var(--event-tile-state-marker-width);
background: var(--event-tile-state-marker-color);
pointer-events: none;
}
> .slotActionBar,
> .slotAvatar,
> .slotBody,
> .slotContextMenu,
> .slotFooter,
> .slotNotificationBadge,
> .slotNotificationRoomLabel,
> .slotPadlock,
> .slotReceipt,
> .slotReplyChain,
> .slotSender,
> .slotThreadInfo,
> .slotTimestamp,
> .line {
margin-inline-end: var(--event-tile-irc-right-padding);
}
.slotTimestamp {
min-width: var(--event-tile-irc-timestamp-width);
flex-shrink: 0;
text-align: end;
}
.slotAvatar {
display: flex;
align-items: center;
flex: 0 0 var(--event-tile-irc-icon-width);
position: relative;
width: var(--event-tile-irc-icon-width);
height: var(--event-tile-irc-line-height);
}
.slotSender {
flex-shrink: 0;
width: var(--event-tile-irc-name-width);
margin-inline-end: 0;
}
.slotPadlock,
.slotFooter,
.slotThreadInfo,
.slotReceipt {
flex-shrink: 0;
}
.slotPadlock {
position: absolute;
}
.line {
display: flex;
flex-direction: column;
flex-grow: 1;
flex-shrink: 1;
padding-block: 1px;
}
.slotReplyChain {
margin: 0;
}
.slotFooter {
margin-block: var(--cpd-space-1-5x, 6px);
}
}
/* Bubble layout. */
.root.layoutBubble {
max-width: unset;
margin-inline: var(--EventTile_bubble-margin-inline-start, 49px) var(--EventTile_bubble-margin-inline-end, 60px);
padding-top: 0;
&::before {
content: "";
position: absolute;
z-index: -1;
inset: -1px calc(-1 * var(--EventTile_bubble-margin-inline-end, 60px)) -1px
calc(-1 * var(--EventTile_bubble-margin-inline-start, 49px));
border-radius: 4px;
pointer-events: none;
}
/* Bubble geometry and slot placement. */
.line {
display: flex;
flex-direction: column;
gap: var(--event-tile-bubble-gap);
width: fit-content;
max-width: 70%;
margin-block: 0;
margin-inline: -9px -12px;
border-radius: var(--event-tile-bubble-corner-radius) var(--event-tile-bubble-corner-radius) 0 0;
line-height: 18px;
padding: calc(var(--event-tile-bubble-gutter) - 1px);
padding-inline-end: 60px;
background: var(--event-tile-bubble-other-background);
border-end-end-radius: var(--event-tile-bubble-corner-radius);
}
.slotAvatar {
position: absolute;
z-index: 9;
inset-block-start: 6px;
inset-inline-start: -36px;
line-height: 0;
border: 4px solid var(--cpd-color-bg-canvas-default, #fff);
border-radius: 50%;
}
.slotAvatar > * {
border-radius: 50%;
box-shadow: 0 0 0 3px var(--cpd-color-bg-canvas-default, #fff);
}
.slotSender,
.line {
width: fit-content;
max-width: 70%;
line-height: 18px;
}
.slotSender {
position: relative;
inset-block-start: -2px;
inset-inline-start: 2px;
font-size: 15px;
white-space: normal;
}
.slotTimestamp {
position: absolute;
z-index: 3;
inset-block-end: 0;
inset-inline-end: 0;
padding: 4px 8px;
}
> .slotReceipt {
position: absolute;
z-index: 1;
inset-block-end: -3px;
inset-inline-end: -78px;
}
.slotPadlock {
flex-shrink: 0;
height: calc(18px + 2px);
}
.slotActionBar {
inset-block-start: -28px;
inset-inline-start: calc(
100% - var(--event-tile-action-bar-size) - 2 * (var(--event-tile-action-bar-margin) + 1px)
);
inset-inline-end: auto;
}
.slotFooter {
display: flex;
align-items: center;
margin-block: var(--cpd-space-1-5x, 6px);
margin-inline: -9px -12px;
}
.slotThreadInfo {
margin-inline-start: -11px;
margin-inline-end: auto;
}
.slotReplyChain,
.slotThreadInfo {
clear: both;
}
/* Own-event modifiers. */
&.stateOwnEvent .slotSender {
display: none;
}
&.stateOwnEvent .line {
border-end-start-radius: var(--event-tile-bubble-corner-radius);
border-end-end-radius: 0;
background: var(--event-tile-bubble-self-background);
margin-inline-start: auto;
}
&.stateOwnEvent .slotAvatar {
inset-block-start: -19px;
inset-inline-start: auto;
inset-inline-end: -38px;
}
&.stateOwnEvent .slotActionBar {
inset-block-start: -28px;
inset-inline-start: auto;
inset-inline-end: 0;
}
&.stateOwnEvent .slotFooter {
justify-content: flex-end;
}
&.stateOwnEvent .slotThreadInfo {
margin-inline-start: auto;
margin-inline-end: -11px;
}
/* Interaction state. */
&.stateSelected .line::before {
content: "";
position: absolute;
z-index: 0;
inset-block: 0;
inset-inline-start: 0;
width: var(--event-tile-state-marker-width);
border-start-start-radius: inherit;
border-end-start-radius: inherit;
background: var(--event-tile-state-marker-color);
pointer-events: none;
}
&:hover::before,
&.stateSelected::before {
background: var(--event-tile-state-hover-background);
}
&.stateHighlighted::before {
background: var(--event-tile-state-highlight-background);
}
&:hover .slotAvatar {
border-color: var(--event-tile-state-hover-background);
}
&.stateHighlighted .slotAvatar {
border-color: var(--event-tile-state-highlight-background);
}
&:hover .slotAvatar > *,
&.stateSelected .slotAvatar > * {
box-shadow: 0 0 0 3px var(--event-tile-state-hover-background);
}
&.stateHighlighted .slotAvatar > * {
box-shadow: 0 0 0 3px var(--event-tile-state-highlight-background);
}
/* Section state. */
&.stateContinuation {
margin-top: 2px;
}
&.stateContinuation .line {
border-start-start-radius: 0;
}
&.stateContinuation.stateOwnEvent .line {
border-start-start-radius: var(--event-tile-bubble-corner-radius);
border-start-end-radius: 0;
}
&.stateLastInSection {
margin-bottom: 24px;
}
&.stateLastInSection .line {
border-end-start-radius: var(--event-tile-bubble-corner-radius);
}
&.stateLastInSection.stateOwnEvent .line {
border-end-start-radius: 0;
border-end-end-radius: var(--event-tile-bubble-corner-radius);
}
}
/* ThreadView adds a narrower group timeline around EventTile. */
.root.shapeThread {
display: flex;
flex-direction: column;
width: 100%;
padding-top: 0;
&.layoutGroup {
.senderDetails {
align-items: center;
gap: var(--cpd-space-4x, 16px);
padding-inline-start: var(--cpd-space-2x, 8px);
}
.senderDetails .slotAvatar {
/* ThreadView keeps the avatar in the sender-details flex row. */
position: relative;
inset: auto;
}
.senderDetails .slotSender {
margin-inline-start: 0;
max-width: 100%;
min-height: 0;
line-height: normal;
}
.line {
padding-block: 2px;
padding-inline-start: 56px;
margin-inline-end: 8px;
}
.slotTimestamp {
/* ThreadView aligns this with the event content, not the bottom of
the line's flex/content flow. */
inset-block-start: 2px;
}
.slotFooter {
margin-inline-start: 56px;
margin-inline-end: 8px;
}
}
}
/* ThreadsList layout. */
.root.shapeThreadsList {
display: flex;
flex-flow: row wrap;
align-items: center;
margin-block: 13px;
padding: 8px;
border-radius: 8px;
&:hover {
background-color: var(--cpd-color-bg-subtle-primary, #f0f1f2);
}
&::after {
content: "";
position: absolute;
inset: auto -8px -13px 56px;
height: 1px;
}
&:last-child::after {
content: unset;
}
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
.details {
width: calc(100% - var(--event-tile-preview-offset));
margin-inline-start: var(--event-tile-preview-offset);
overflow: hidden;
}
.slotSender {
/* In preview tiles the sender is a metadata-row profile, not the
group-layout sender above the event line. */
display: inline-flex;
align-items: center;
flex: 1;
max-width: 100%;
min-height: 0;
margin-inline-start: 0;
line-height: normal;
}
.slotAvatar {
position: absolute;
inset: 8px auto auto 8px;
}
.line {
width: 100%;
box-sizing: border-box;
padding-inline-start: var(--event-tile-preview-offset);
padding-bottom: 0;
}
.slotThreadInfo {
/* The preview is inside the padded line in this mode; it does not use
the group-layout thread-summary gutter. */
margin-inline-start: 0;
}
.slotTimestamp {
display: block;
font-size: 12px;
overflow: hidden;
margin-inline-start: auto;
align-self: flex-start;
position: initial;
inset: auto;
width: unset;
text-align: initial;
text-overflow: ellipsis;
}
&.layoutGroup:hover .line {
background-color: inherit;
box-shadow: none;
}
}
/* Notification layout. */
.root.shapeNotification {
display: flex;
flex-flow: row wrap;
align-items: center;
margin-block: 13px;
padding: 8px;
border-radius: 8px;
&:hover {
background-color: var(--cpd-color-bg-subtle-primary, #f0f1f2);
}
&::after {
content: "";
position: absolute;
inset: auto -8px -13px 56px;
height: 1px;
}
&:last-child::after {
content: unset;
}
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
.details {
width: calc(100% - var(--event-tile-preview-offset));
margin-inline-start: var(--event-tile-preview-offset);
overflow: hidden;
}
.slotSender {
/* In preview tiles the sender is a metadata-row profile, not the
group-layout sender above the event line. */
display: inline-flex;
align-items: center;
flex: 1;
max-width: 100%;
min-height: 0;
margin-inline-start: 0;
line-height: normal;
}
.slotAvatar {
position: absolute;
inset: 8px auto auto 8px;
}
.line {
width: 100%;
box-sizing: border-box;
padding-inline-start: var(--event-tile-preview-offset);
padding-bottom: 0;
}
.slotThreadInfo {
/* The preview is inside the padded line in these modes; it does not use
the group-layout thread-summary gutter. */
margin-inline-start: 0;
}
.slotTimestamp {
display: block;
font-size: 12px;
overflow: hidden;
margin-inline-start: auto;
align-self: flex-start;
position: initial;
inset: auto;
width: unset;
text-align: initial;
text-overflow: ellipsis;
}
&.layoutGroup:hover .line {
background-color: inherit;
box-shadow: none;
}
}
/* File panel layout. The application File branch does not apply a timeline
layout class, so explicitly reset the group-layout defaults owned by this
shared shell. */
.root.shapeFile {
padding-top: 0;
overflow-wrap: anywhere;
.senderDetailsLink {
display: block;
margin-bottom: var(--cpd-space-1x, 4px);
text-decoration: none;
}
.senderDetails {
display: flex;
align-items: center;
gap: var(--cpd-space-2x, 8px);
margin-top: -2px;
}
.senderDetails .slotAvatar {
position: relative;
inset: auto;
flex: 0 0 auto;
}
.senderDetails .slotSender {
display: inline-flex;
align-items: center;
max-width: 100%;
min-height: 0;
margin-inline-start: 0;
line-height: normal;
}
.senderDetails .slotTimestamp {
position: initial;
inset: auto;
width: unset;
margin-inline-start: auto;
text-align: initial;
}
.line {
margin-inline-end: 0;
padding: 0;
border-radius: 0;
}
}