234 lines
8.5 KiB
TypeScript
234 lines
8.5 KiB
TypeScript
import { globalStyle, style } from '@vanilla-extract/css';
|
|
import { color, config, toRem } from 'folds';
|
|
|
|
// 36px circular avatar — a notch above folds `Avatar size="200"` (32px)
|
|
// for visual weight matching the channels mockup. Consumers override
|
|
// the folds preset via inline style; the shared `CHANNEL_AVATAR_PX`
|
|
// constant keeps the CSS slot width and the inline override in sync.
|
|
export const CHANNEL_AVATAR_PX = 36;
|
|
const ChannelAvatarWidth = toRem(CHANNEL_AVATAR_PX);
|
|
|
|
export const ChannelRow = style({
|
|
display: 'flex',
|
|
alignItems: 'flex-start',
|
|
gap: config.space.S300,
|
|
// S200 (8px) sits inside MessageBase's S400 (16px) left pad, so the
|
|
// avatar's left edge lands ~24px from the screen edge — tighter than
|
|
// the previous 32px (S400+S400) without crowding the bubble cluster.
|
|
// Mirror on the right for symmetric breathing room.
|
|
paddingLeft: config.space.S200,
|
|
paddingRight: config.space.S200,
|
|
paddingTop: config.space.S100,
|
|
paddingBottom: config.space.S100,
|
|
minWidth: 0,
|
|
// Hover bg subtle so adjacent rows still read as distinct units even
|
|
// without bubble borders. `@media (hover: hover)` keeps this inert on
|
|
// touch where there's no pointer to follow.
|
|
'@media': {
|
|
'(hover: hover) and (pointer: fine)': {
|
|
selectors: {
|
|
'&:hover': {
|
|
backgroundColor: color.SurfaceVariant.Container,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
// Fixed-width slot keeps the body column aligned across collapsed rows
|
|
// (where `avatar` is `undefined` — the slot still occupies `ChannelAvatarWidth`,
|
|
// so the body's left edge stays put).
|
|
export const ChannelAvatarSlot = style({
|
|
width: ChannelAvatarWidth,
|
|
flexShrink: 0,
|
|
display: 'flex',
|
|
justifyContent: 'center',
|
|
paddingTop: toRem(2),
|
|
});
|
|
|
|
export const ChannelBody = style({
|
|
flex: 1,
|
|
minWidth: 0,
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: config.space.S100,
|
|
});
|
|
|
|
export const ChannelHeader = style({
|
|
display: 'flex',
|
|
alignItems: 'baseline',
|
|
gap: config.space.S200,
|
|
minWidth: 0,
|
|
});
|
|
|
|
export const ChannelMessageBody = style({
|
|
minWidth: 0,
|
|
});
|
|
|
|
export const ChannelReactions = style({
|
|
minWidth: 0,
|
|
marginTop: config.space.S100,
|
|
});
|
|
|
|
export const ChannelThreadSummary = style({
|
|
minWidth: 0,
|
|
marginTop: config.space.S100,
|
|
});
|
|
|
|
// Horizontal line + centered label. Spans the full row width including
|
|
// the avatar slot so the line reads as a section break, not a per-message
|
|
// chrome element.
|
|
export const ChannelDayDividerRoot = style({
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: config.space.S300,
|
|
paddingLeft: config.space.S400,
|
|
paddingRight: config.space.S400,
|
|
paddingTop: config.space.S400,
|
|
paddingBottom: config.space.S400,
|
|
});
|
|
|
|
export const ChannelDayDividerLine = style({
|
|
flex: 1,
|
|
height: '1px',
|
|
backgroundColor: color.SurfaceVariant.ContainerLine,
|
|
});
|
|
|
|
export const ChannelDayDividerLabel = style({
|
|
fontSize: toRem(11),
|
|
fontWeight: 600,
|
|
letterSpacing: '0.06em',
|
|
textTransform: 'uppercase',
|
|
color: color.SurfaceVariant.OnContainer,
|
|
opacity: 0.7,
|
|
flexShrink: 0,
|
|
});
|
|
|
|
// Sysline (membership / room.create / pinned-events). Compact single
|
|
// row aligned with the body gutter — the sysline sits where messages'
|
|
// body would, indented past the avatar slot, so the column reads
|
|
// continuous.
|
|
export const ChannelSysline = style({
|
|
// Indent past the avatar gutter so the sysline body aligns with the
|
|
// body column of the surrounding message rows (avatar + gap + row pad).
|
|
// Tracks `ChannelRow.paddingLeft/Right` (S200) in lockstep.
|
|
paddingLeft: `calc(${ChannelAvatarWidth} + ${config.space.S300} + ${config.space.S200})`,
|
|
paddingRight: config.space.S200,
|
|
paddingTop: config.space.S100,
|
|
paddingBottom: config.space.S100,
|
|
color: color.SurfaceVariant.OnContainer,
|
|
});
|
|
|
|
export const ChannelSyslineIcon = style({
|
|
flexShrink: 0,
|
|
opacity: 0.5,
|
|
});
|
|
|
|
export const ChannelSyslineBody = style({
|
|
minWidth: 0,
|
|
flex: 1,
|
|
});
|
|
|
|
// Bubble chrome applied when `ChannelLayout` is invoked with
|
|
// `headerInBubble` (thread drawer and channels main timeline pass it).
|
|
// Mirrors `StreamBubble` from the DM timeline so a channel row reads
|
|
// like a chat-bubble cluster: dark `Surface.Container` card with an
|
|
// asymmetric notch corner per `data-own`, sized `fit-content` so short
|
|
// bubbles shrink-wrap instead of stretching across the column.
|
|
// Reactions and the thread-summary card live as siblings of the body
|
|
// in `ChannelLayout`, so they stay OUTSIDE the bubble — identical
|
|
// composition to Stream. The `[data-bubble="true"]` row marker keeps
|
|
// the un-bubbled channel/sysline layout (pre-redesign callers) opt-in
|
|
// rather than forcing the look on every consumer.
|
|
globalStyle(`${ChannelRow}[data-bubble="true"] ${ChannelMessageBody}`, {
|
|
backgroundColor: color.Surface.Container,
|
|
color: color.SurfaceVariant.OnContainer,
|
|
border: `1px solid ${color.Surface.ContainerLine}`,
|
|
paddingTop: config.space.S200,
|
|
paddingBottom: config.space.S200,
|
|
paddingLeft: toRem(15),
|
|
paddingRight: toRem(15),
|
|
display: 'inline-block',
|
|
width: 'fit-content',
|
|
maxWidth: '100%',
|
|
minWidth: 0,
|
|
position: 'relative',
|
|
zIndex: 1,
|
|
// Clips the thread-summary footer's hover bg against the bubble's
|
|
// rounded BR/BL corners — without it the rectangular hover paint
|
|
// punches past the curve. No outflow content lives inside the bubble
|
|
// (option bar, reactions are siblings on the row) so clipping is
|
|
// safe.
|
|
overflow: 'hidden',
|
|
});
|
|
|
|
// Asymmetric corner per `data-own` — own messages flatten BOTTOM-LEFT
|
|
// (4px), incoming messages flatten TOP-LEFT. Same pattern as
|
|
// `StreamBubble.own`/`StreamBubble.others`.
|
|
globalStyle(`${ChannelRow}[data-bubble="true"][data-own="true"] ${ChannelMessageBody}`, {
|
|
borderRadius: `${toRem(16)} ${toRem(16)} ${toRem(16)} ${toRem(4)}`,
|
|
});
|
|
|
|
globalStyle(`${ChannelRow}[data-bubble="true"][data-own="false"] ${ChannelMessageBody}`, {
|
|
borderRadius: `${toRem(4)} ${toRem(16)} ${toRem(16)} ${toRem(16)}`,
|
|
// Peer (not-own) bubble bg for the channel layout — its own var. (The 1-1
|
|
// Stream layout's incoming bubble instead binds to color.Surface.Container,
|
|
// the composer surface.) Covers channels main timeline AND thread drawer
|
|
// (both pass `headerInBubble`, so `data-bubble="true"` fires).
|
|
backgroundColor: 'var(--vojo-peer-bubble-bg)',
|
|
});
|
|
|
|
// Small gap so the in-bubble header (username + time) doesn't sit flush
|
|
// against the first line of message text. Matches the Stream layout's
|
|
// `StreamName` 2px marginBottom.
|
|
globalStyle(`${ChannelRow}[data-bubble="true"] ${ChannelHeader}[data-in-bubble="true"]`, {
|
|
marginBottom: toRem(2),
|
|
});
|
|
|
|
// Thread-summary footer rendered INSIDE the bubble (rather than as a
|
|
// separate pill below). Negative L/R margin (matches the bubble's
|
|
// `paddingLeft/Right: 15px`) stretches the wrapper to the bubble's
|
|
// inner border edge so the 1px top rule reads as a section divider
|
|
// spanning the whole bubble. Negative `marginBottom` cancels the
|
|
// bubble's S200 bottom pad so the footer flushes against the bubble's
|
|
// rounded bottom edge — bubble + summary read as one card with a
|
|
// horizontal rule splitting them.
|
|
//
|
|
// The footer body keeps no own border/radius — it inherits the bubble's
|
|
// bottom corners via clipping (`ChannelMessageBody` itself doesn't
|
|
// `overflow: hidden`, but the rounded bottom of the bubble visually
|
|
// caps the footer anyway because the divider line never reaches the
|
|
// curved corner pixel).
|
|
export const ChannelBubbleThreadSummary = style({
|
|
marginTop: config.space.S200,
|
|
marginLeft: toRem(-15),
|
|
marginRight: toRem(-15),
|
|
marginBottom: `calc(-1 * ${config.space.S200})`,
|
|
borderTop: `1px solid ${color.Surface.ContainerLine}`,
|
|
});
|
|
|
|
// Footer button — strip the original ThreadSummaryCard pill chrome
|
|
// (own bg, radius, padding, max-width) so it reads as a flush bubble
|
|
// footer. Click target expands to the full footer width. Hover paints
|
|
// a subtle `SurfaceVariant.Container` shade that contrasts against
|
|
// the bubble's `Surface.Container` bg, signalling tappable footer
|
|
// without the pill silhouette returning.
|
|
globalStyle(`${ChannelBubbleThreadSummary} > button`, {
|
|
display: 'flex',
|
|
width: '100%',
|
|
maxWidth: 'none',
|
|
borderRadius: 0,
|
|
backgroundColor: 'transparent',
|
|
padding: `${config.space.S200} ${toRem(15)}`,
|
|
});
|
|
|
|
globalStyle(`${ChannelBubbleThreadSummary} > button:hover`, {
|
|
backgroundColor: color.SurfaceVariant.Container,
|
|
});
|
|
|
|
globalStyle(`${ChannelBubbleThreadSummary} > button:focus-visible`, {
|
|
// Inset the focus ring slightly so it doesn't punch through the
|
|
// bubble's rounded bottom corners on the BR/BL when the row is
|
|
// either own or incoming.
|
|
outlineOffset: toRem(-2),
|
|
});
|