654 lines
21 KiB
TypeScript
654 lines
21 KiB
TypeScript
import { createVar, globalStyle, keyframes, style, styleVariants } from '@vanilla-extract/css';
|
||
import { recipe, RecipeVariants } from '@vanilla-extract/recipes';
|
||
import { DefaultReset, color, config, toRem } from 'folds';
|
||
|
||
const SpacingVar = createVar();
|
||
const SpacingVariant = styleVariants({
|
||
'0': {
|
||
vars: {
|
||
[SpacingVar]: config.space.S0,
|
||
},
|
||
},
|
||
'100': {
|
||
vars: {
|
||
[SpacingVar]: config.space.S100,
|
||
},
|
||
},
|
||
'200': {
|
||
vars: {
|
||
[SpacingVar]: config.space.S200,
|
||
},
|
||
},
|
||
'300': {
|
||
vars: {
|
||
[SpacingVar]: config.space.S300,
|
||
},
|
||
},
|
||
'400': {
|
||
vars: {
|
||
[SpacingVar]: config.space.S400,
|
||
},
|
||
},
|
||
'500': {
|
||
vars: {
|
||
[SpacingVar]: config.space.S500,
|
||
},
|
||
},
|
||
});
|
||
|
||
const highlightAnime = keyframes({
|
||
'0%': {
|
||
backgroundColor: color.Primary.Container,
|
||
},
|
||
'25%': {
|
||
backgroundColor: color.Primary.ContainerActive,
|
||
},
|
||
'50%': {
|
||
backgroundColor: color.Primary.Container,
|
||
},
|
||
'75%': {
|
||
backgroundColor: color.Primary.ContainerActive,
|
||
},
|
||
'100%': {
|
||
backgroundColor: color.Primary.Container,
|
||
},
|
||
});
|
||
const HighlightVariant = styleVariants({
|
||
true: {
|
||
animation: `${highlightAnime} 2000ms ease-in-out`,
|
||
animationIterationCount: 'infinite',
|
||
},
|
||
});
|
||
|
||
const SelectedVariant = styleVariants({
|
||
true: {
|
||
backgroundColor: color.Surface.ContainerActive,
|
||
},
|
||
});
|
||
|
||
const AutoCollapse = style({
|
||
selectors: {
|
||
[`&+&`]: {
|
||
marginTop: 0,
|
||
},
|
||
},
|
||
});
|
||
|
||
export const MessageBase = recipe({
|
||
base: [
|
||
DefaultReset,
|
||
{
|
||
marginTop: SpacingVar,
|
||
padding: `${config.space.S100} ${config.space.S200} ${config.space.S100} ${config.space.S400}`,
|
||
borderRadius: `0 ${config.radii.R400} ${config.radii.R400} 0`,
|
||
},
|
||
],
|
||
variants: {
|
||
space: SpacingVariant,
|
||
collapse: {
|
||
true: {
|
||
marginTop: 0,
|
||
},
|
||
},
|
||
autoCollapse: {
|
||
true: AutoCollapse,
|
||
},
|
||
highlight: HighlightVariant,
|
||
selected: SelectedVariant,
|
||
},
|
||
defaultVariants: {
|
||
space: '400',
|
||
},
|
||
});
|
||
|
||
export type MessageBaseVariants = RecipeVariants<typeof MessageBase>;
|
||
|
||
export const AvatarBase = style({
|
||
paddingTop: toRem(4),
|
||
transition: 'transform 200ms cubic-bezier(0, 0.8, 0.67, 0.97)',
|
||
display: 'flex',
|
||
alignSelf: 'start',
|
||
|
||
selectors: {
|
||
'&:hover': {
|
||
transform: `translateY(${toRem(-2)})`,
|
||
},
|
||
},
|
||
});
|
||
|
||
export const ModernBefore = style({
|
||
minWidth: toRem(36),
|
||
});
|
||
|
||
export const Username = style({
|
||
overflow: 'hidden',
|
||
whiteSpace: 'nowrap',
|
||
textOverflow: 'ellipsis',
|
||
selectors: {
|
||
'button&': {
|
||
cursor: 'pointer',
|
||
},
|
||
'button&:hover, button&:focus-visible': {
|
||
textDecoration: 'underline',
|
||
},
|
||
},
|
||
});
|
||
|
||
export const UsernameBold = style({
|
||
fontWeight: 550,
|
||
});
|
||
|
||
// Stream layout (DM redesign — see docs/plans/dm_1x1_redesign.md §6.5b).
|
||
//
|
||
// Symmetric three-gap layout, expressed as a 3-track CSS grid:
|
||
//
|
||
// ┌─G─┬─time─┬─G─┬─dot─┬─G─┬───── bubble (1fr) ─────────┐
|
||
// row row
|
||
// left right
|
||
//
|
||
// where G = `column-gap` = `padding-left` of the row root. Both inset
|
||
// values come from the same custom property, so the gap from the row's
|
||
// left edge to the timestamp text equals the gap between time and dot
|
||
// equals the gap between dot and bubble — symmetric by construction, no
|
||
// per-side magic constants.
|
||
//
|
||
// Track 1 is `auto`, so the browser sizes it to the rendered timestamp
|
||
// width — no JS measurement, no locale branching. 24h "16:58" auto-sizes
|
||
// to ~33 px and the whole row block contracts left; AM/PM "11:30 PM"
|
||
// auto-sizes to ~52 px and the block expands right. Both keep the same
|
||
// `G` gaps; the dot and bubble shift together as one unit.
|
||
//
|
||
// Two gap dimensions, set per-breakpoint via the `compact` recipe
|
||
// variant on the row roots:
|
||
//
|
||
// - StreamRowPadVar → screen→time gap (the row's `padding-left`)
|
||
// - StreamRowGapVar → time→dot and dot→bubble gaps (the `column-gap`)
|
||
//
|
||
// Splitting these lets the inter-element gaps tighten on desktop and
|
||
// loosen on mobile without disturbing the screen-edge anchor (which the
|
||
// user dialled in earlier and asked to keep).
|
||
//
|
||
// Mobile: pad = S100 (minimal screen-edge anchor); gap = 2 × S100 = S200
|
||
// (the user asked to double the inter-element gap on native).
|
||
// Desktop: pad = S500 (≈ 0.5 cm PageNav clearance, unchanged); gap =
|
||
// S500 / 1.1 ≈ 18.2 px (the user asked to shrink the desktop inter-element
|
||
// gap by 1.1× — keeps the layout tighter without dropping a whole token).
|
||
const StreamRowPadVar = createVar();
|
||
const StreamRowGapVar = createVar();
|
||
const StreamRowPadMobile = config.space.S100;
|
||
const StreamRowPadDesktop = config.space.S500;
|
||
const StreamRowGapMobile = config.space.S200;
|
||
const StreamRowGapDesktop = `calc(${config.space.S500} / 1.1)`;
|
||
|
||
const StreamDotSize = toRem(9);
|
||
const StreamSyslineDotSize = toRem(6);
|
||
// Slightly larger than message dots so day markers read as the visual
|
||
// anchor of the `─── Сегодня ───` line.
|
||
const StreamDayDotSize = toRem(12.1);
|
||
const StreamRailLineWidth = '1px';
|
||
const StreamBubbleBorderWidth = '1px';
|
||
const StreamTimeLineHeight = toRem(13);
|
||
const StreamRailBridgeY = config.space.S400;
|
||
|
||
// Vertical centre of the bubble's header text from the row's content-area
|
||
// top. = bubble.borderTop (1) + bubble.paddingTop (S200) + line.T200 / 2.
|
||
// Used to push the timestamp and the dot down in their grid cells so
|
||
// they read on the same baseline as the Username component inside the
|
||
// bubble. Rail-end's height also adds `S100` back to account for the
|
||
// rail's negative `top` offset (it starts above the row outer edge).
|
||
const StreamHeaderInnerCenterY = `calc(${StreamBubbleBorderWidth} + ${config.space.S200} + (${config.lineHeight.T200} / 2))`;
|
||
|
||
export const StreamRoot = recipe({
|
||
base: {
|
||
position: 'relative',
|
||
display: 'grid',
|
||
// Three tracks: time (auto = formatted-timestamp width) | dot column
|
||
// (auto = dot diameter) | bubble (1fr). The grid auto-sizes track 1
|
||
// to the rendered timestamp width — locale-agnostic, no JS measure.
|
||
gridTemplateColumns: 'auto auto 1fr',
|
||
columnGap: StreamRowGapVar,
|
||
paddingLeft: StreamRowPadVar,
|
||
alignItems: 'flex-start',
|
||
paddingTop: config.space.S100,
|
||
paddingBottom: config.space.S100,
|
||
paddingRight: config.space.S400,
|
||
},
|
||
variants: {
|
||
compact: {
|
||
false: {
|
||
vars: {
|
||
[StreamRowPadVar]: StreamRowPadDesktop,
|
||
[StreamRowGapVar]: StreamRowGapDesktop,
|
||
},
|
||
},
|
||
// Mobile: cancel MessageBase's S400/S200 horizontal padding so the
|
||
// row spans edge-to-edge, then padding-left alone provides the
|
||
// minimal screen→time anchor inside the row.
|
||
true: {
|
||
vars: {
|
||
[StreamRowPadVar]: StreamRowPadMobile,
|
||
[StreamRowGapVar]: StreamRowGapMobile,
|
||
},
|
||
marginLeft: `calc(-1 * ${config.space.S400})`,
|
||
marginRight: `calc(-1 * ${config.space.S200})`,
|
||
paddingRight: 0,
|
||
},
|
||
},
|
||
},
|
||
defaultVariants: { compact: false },
|
||
});
|
||
|
||
// Sysline timestamp. Now a regular grid item in track 1 — sized to its
|
||
// content width like the message-row timestamp, just inheriting the
|
||
// row's `align-items: center` so the single-line sysline reads on a
|
||
// shared centreline with the rest of the row.
|
||
export const StreamTimeColumn = style({
|
||
fontSize: toRem(11),
|
||
fontVariantNumeric: 'tabular-nums',
|
||
color: color.Surface.OnContainer,
|
||
opacity: 0.55,
|
||
whiteSpace: 'nowrap',
|
||
fontFamily:
|
||
'"JetBrains Mono Variable", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
|
||
});
|
||
|
||
globalStyle(`${StreamTimeColumn} time`, {
|
||
display: 'block',
|
||
fontFamily: 'inherit',
|
||
fontSize: toRem(11),
|
||
lineHeight: StreamTimeLineHeight,
|
||
});
|
||
|
||
// DotColumn — grid track 2. Always StreamDotSize wide regardless of row
|
||
// type so message / sysline / day-divider rows all hit the same vertical
|
||
// X for the rail line — otherwise the rail would visibly kink at row
|
||
// boundaries with different dot sizes. Stretches to row content height
|
||
// so the rail abs-child can extend through the row.
|
||
export const StreamDotColumn = style({
|
||
position: 'relative',
|
||
alignSelf: 'stretch',
|
||
width: StreamDotSize,
|
||
});
|
||
|
||
// Per-row rail segment. Lives inside DotColumn and is horizontally
|
||
// centred on the dot column (= the dot's centre). Top/bottom extend
|
||
// `bridge + row.paddingTop/Bottom` past the DotColumn bounds so consecutive
|
||
// rails meet across the row's vertical padding plus MessageBase's marginTop.
|
||
// DotColumn stretches to the row content area, so its top/bottom = row
|
||
// content top/bottom; we extend past those to reach row outer ± bridge.
|
||
export const StreamRail = style({
|
||
position: 'absolute',
|
||
top: `calc(-1 * (${StreamRailBridgeY} + ${config.space.S100}))`,
|
||
bottom: `calc(-1 * (${StreamRailBridgeY} + ${config.space.S100}))`,
|
||
left: '50%',
|
||
transform: 'translateX(-50%)',
|
||
width: StreamRailLineWidth,
|
||
background: color.Surface.ContainerLine,
|
||
pointerEvents: 'none',
|
||
zIndex: 0,
|
||
});
|
||
|
||
// Rail-end / rail-start variants — stop the rail at the dot rather than
|
||
// extending past it. `StreamHeaderInnerCenterY` is the Y from DotColumn
|
||
// top to the dot centre (DotColumn top = row content top = excludes
|
||
// row.paddingTop); subtract the dot radius so the rail meets the dot edge.
|
||
export const StreamRailEnd = style({
|
||
bottom: 'auto',
|
||
// Top stays at the negative bridge offset; height takes the rail down
|
||
// to the dot's centre line.
|
||
height: `calc(${StreamRailBridgeY} + ${config.space.S100} + ${StreamHeaderInnerCenterY})`,
|
||
});
|
||
|
||
export const StreamRailStart = style({
|
||
top: StreamHeaderInnerCenterY,
|
||
});
|
||
|
||
export const StreamRailSingle = style({
|
||
display: 'none',
|
||
});
|
||
|
||
// Two-layer span: outer Halo paints a solid bg disk + ring that masks the
|
||
// rail line behind the dot regardless of opacity; inner Fill carries the
|
||
// author colour and the read-state opacity.
|
||
//
|
||
// Halo is a regular in-flow grid-cell child (no `position: absolute`)
|
||
// that sits in DotColumn. Its block-axis position inside the column is
|
||
// controlled per-row-type by composed classes below (Header* for message
|
||
// rows, Sysline* for syslines). `display: block` is REQUIRED — the
|
||
// rendered element is a `<span>`, and `width` / `height` / `margin-top`
|
||
// are silently ignored on inline-display elements (so the dot would
|
||
// collapse to zero size without it). Sysline / day-row variants override
|
||
// to `position: absolute`, which auto-blockifies; message rows rely on
|
||
// this declaration.
|
||
export const StreamDotHalo = style({
|
||
display: 'block',
|
||
width: StreamDotSize,
|
||
height: StreamDotSize,
|
||
borderRadius: '50%',
|
||
background: color.Surface.Container,
|
||
boxShadow: `0 0 0 3px ${color.Surface.Container}`,
|
||
pointerEvents: 'none',
|
||
position: 'relative',
|
||
zIndex: 2,
|
||
});
|
||
|
||
// Sysline dot — smaller, abs-positioned at DotColumn centre so it
|
||
// vertically centres on the sysline row regardless of DotColumn height.
|
||
export const StreamSyslineDotHalo = style({
|
||
position: 'absolute',
|
||
top: '50%',
|
||
left: '50%',
|
||
transform: 'translate(-50%, -50%)',
|
||
width: StreamSyslineDotSize,
|
||
height: StreamSyslineDotSize,
|
||
});
|
||
|
||
// Sysline rail-end / rail-start — sysline DotColumn doesn't stretch, so
|
||
// the rail extends from the dot centre by bridge + half DotColumn height.
|
||
// We can't read DotColumn height at compile time, but `50%` of the rail's
|
||
// own absolute-positioned containing block (= DotColumn) gives the same
|
||
// effect.
|
||
// Sysline rail-end / rail-start variants. The rail's containing block is
|
||
// DotColumn (which stretches via `align-self: stretch` regardless of the
|
||
// parent's `align-items`). Heights/tops are measured from DotColumn's
|
||
// padding-edge top — `top: -(bridge + S100)` lifts the rail's top-zero
|
||
// position by `bridge + paddingTop` past the row outer edge, so the
|
||
// terminating end must add `S100` back when capping at the dot centre
|
||
// (which is at `50%` of DotColumn under `align-items: center`).
|
||
export const StreamSyslineRailEnd = style({
|
||
bottom: 'auto',
|
||
height: `calc(${StreamRailBridgeY} + ${config.space.S100} + 50%)`,
|
||
});
|
||
|
||
export const StreamSyslineRailStart = style({
|
||
top: '50%',
|
||
});
|
||
|
||
// Message-row dot in DotColumn. Pushes the dot down by the same Y the
|
||
// bubble's header text occupies, so the dot centres on the Username line.
|
||
// Set as marginTop because Halo is in-flow.
|
||
export const StreamHeaderDotHalo = style({
|
||
marginTop: `calc(${StreamHeaderInnerCenterY} - (${StreamDotSize} / 2))`,
|
||
});
|
||
|
||
export const StreamDotFill = style({
|
||
position: 'absolute',
|
||
inset: 0,
|
||
borderRadius: '50%',
|
||
pointerEvents: 'none',
|
||
// Smooth read↔unread fade — without it the state flip reads as a hard step.
|
||
transition: 'opacity 220ms ease, filter 220ms ease',
|
||
});
|
||
|
||
// Wrapper for grid track 3 — holds the bubble and the reactions row stacked.
|
||
// `min-width: 0` lets fit-content bubbles shrink below their content's natural
|
||
// size when the grid track is narrower than the bubble (otherwise they'd
|
||
// overflow).
|
||
export const StreamColumn = style({
|
||
gridColumn: 3,
|
||
minWidth: 0,
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
alignItems: 'flex-start',
|
||
});
|
||
|
||
// Reactions row sits below the bubble, outside its bg/border, so reactions
|
||
// read as floating chips. Aligns to the bubble's left edge by inheriting
|
||
// `align-items: flex-start` from StreamColumn. The inline marginTop on the
|
||
// `<Reactions>` callsites in RoomTimeline supplies the spacing — flex
|
||
// columns don't collapse margins so a wrapper margin would stack.
|
||
export const StreamReactions = style({
|
||
maxWidth: '100%',
|
||
minWidth: 0,
|
||
});
|
||
|
||
export const StreamBubble = recipe({
|
||
base: {
|
||
backgroundColor: color.SurfaceVariant.Container,
|
||
color: color.SurfaceVariant.OnContainer,
|
||
border: `${StreamBubbleBorderWidth} solid ${color.SurfaceVariant.ContainerLine}`,
|
||
paddingTop: config.space.S200,
|
||
paddingBottom: config.space.S200,
|
||
minWidth: 0,
|
||
maxWidth: toRem(720),
|
||
position: 'relative',
|
||
zIndex: 1,
|
||
},
|
||
variants: {
|
||
// Asymmetric notch — own: top-left flat, three corners R500.
|
||
// Incoming: mirrored.
|
||
own: {
|
||
true: {
|
||
borderRadius: `${toRem(4)} ${config.radii.R500} ${config.radii.R500} ${config.radii.R500}`,
|
||
},
|
||
false: {
|
||
borderRadius: `${config.radii.R500} ${config.radii.R500} ${config.radii.R500} ${toRem(4)}`,
|
||
},
|
||
},
|
||
// Mobile fills the message column (block 100%); desktop fits content
|
||
// (inline-block fit-content). Branched via useScreenSizeContext, not
|
||
// CSS media queries — see docs/plans/dm_1x1_redesign.md §5.5.
|
||
compact: {
|
||
true: {
|
||
display: 'block',
|
||
width: '100%',
|
||
paddingLeft: config.space.S300,
|
||
paddingRight: config.space.S300,
|
||
},
|
||
false: {
|
||
display: 'inline-block',
|
||
width: 'fit-content',
|
||
maxWidth: '100%',
|
||
paddingLeft: toRem(15),
|
||
paddingRight: toRem(15),
|
||
},
|
||
},
|
||
// Image messages: bubble becomes a transparent shell so the
|
||
// StreamMediaImage child supplies the visible chrome instead.
|
||
// `display: block, width: 100%` (NOT fit-content) so the bubble has a
|
||
// definite width inherited from StreamColumn — required for the
|
||
// child's `max-width: 100%` to clamp the image. With fit-content the
|
||
// chain becomes circular (parent shrinks to child, child grows to
|
||
// its explicit pixel width), and the image overflows past the
|
||
// viewport on narrow screens.
|
||
mediaMode: {
|
||
true: {
|
||
backgroundColor: 'transparent',
|
||
border: 'none',
|
||
borderRadius: 0,
|
||
padding: 0,
|
||
display: 'block',
|
||
width: '100%',
|
||
},
|
||
},
|
||
},
|
||
defaultVariants: {
|
||
own: false,
|
||
compact: false,
|
||
mediaMode: false,
|
||
},
|
||
});
|
||
|
||
export const StreamBubbleHeader = style({
|
||
position: 'relative',
|
||
marginBottom: toRem(2),
|
||
fontSize: toRem(11),
|
||
lineHeight: config.lineHeight.T200,
|
||
minHeight: config.lineHeight.T200,
|
||
fontWeight: 600,
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: toRem(6),
|
||
flexWrap: 'nowrap',
|
||
});
|
||
|
||
// Message-row timestamp — grid item in track 1, content-sized. Pushed
|
||
// down by the same Y as StreamHeaderDotHalo so timestamp / dot / Username
|
||
// share one baseline.
|
||
export const StreamHeaderTime = style({
|
||
paddingTop: `calc(${StreamHeaderInnerCenterY} - (${StreamTimeLineHeight} / 2))`,
|
||
fontSize: toRem(11),
|
||
lineHeight: StreamTimeLineHeight,
|
||
fontVariantNumeric: 'tabular-nums',
|
||
color: color.Surface.OnContainer,
|
||
opacity: 0.55,
|
||
whiteSpace: 'nowrap',
|
||
fontFamily:
|
||
'"JetBrains Mono Variable", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
|
||
});
|
||
|
||
globalStyle(`${StreamHeaderTime} time`, {
|
||
display: 'block',
|
||
fontFamily: 'inherit',
|
||
fontSize: toRem(11),
|
||
lineHeight: StreamTimeLineHeight,
|
||
});
|
||
|
||
// Sysline — thin single-line state-event row inside Stream layout.
|
||
// Composes with StreamRoot so the time / dot / content tracks line up
|
||
// vertically with message rows above and below. Override align-items to
|
||
// center because the sysline content is one line tall and reads best
|
||
// vertically centred.
|
||
export const StreamSysline = style({
|
||
alignItems: 'center',
|
||
paddingTop: toRem(2),
|
||
paddingBottom: toRem(2),
|
||
});
|
||
|
||
|
||
export const StreamSyslineBody = style({
|
||
fontSize: toRem(11.5),
|
||
color: color.Surface.OnContainer,
|
||
opacity: 0.55,
|
||
fontFamily:
|
||
'"JetBrains Mono Variable", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
|
||
whiteSpace: 'nowrap',
|
||
overflow: 'hidden',
|
||
textOverflow: 'ellipsis',
|
||
minWidth: 0,
|
||
});
|
||
|
||
// Day divider row — uses the SAME 3-track grid as message rows so its
|
||
// dot and rail land on the exact same X as the dots/rails above and below
|
||
// it (otherwise the rail would visibly kink at the day boundary). Track 1
|
||
// holds an invisible `<Time>` placeholder rendered by Stream.tsx so the
|
||
// `auto` track sizes to the same width as the surrounding message rows.
|
||
export const StreamDayRoot = recipe({
|
||
base: {
|
||
position: 'relative',
|
||
display: 'grid',
|
||
gridTemplateColumns: 'auto auto 1fr',
|
||
columnGap: StreamRowGapVar,
|
||
paddingLeft: StreamRowPadVar,
|
||
alignItems: 'center',
|
||
paddingTop: toRem(10),
|
||
paddingBottom: toRem(10),
|
||
paddingRight: config.space.S400,
|
||
},
|
||
variants: {
|
||
compact: {
|
||
false: {
|
||
vars: {
|
||
[StreamRowPadVar]: StreamRowPadDesktop,
|
||
[StreamRowGapVar]: StreamRowGapDesktop,
|
||
},
|
||
},
|
||
true: {
|
||
vars: {
|
||
[StreamRowPadVar]: StreamRowPadMobile,
|
||
[StreamRowGapVar]: StreamRowGapMobile,
|
||
},
|
||
marginLeft: `calc(-1 * ${config.space.S400})`,
|
||
marginRight: `calc(-1 * ${config.space.S200})`,
|
||
paddingRight: 0,
|
||
},
|
||
},
|
||
},
|
||
defaultVariants: { compact: false },
|
||
});
|
||
|
||
// Off-screen placeholder timestamp inside the day divider's track 1 —
|
||
// invisible, but takes the same auto-width as the live timestamps in
|
||
// surrounding message rows so the grid columns align. Override the
|
||
// header time's paddingTop because the placeholder needs no header-line
|
||
// alignment (the day divider is already vertically centred on the row).
|
||
export const StreamDayTimePlaceholder = style({
|
||
visibility: 'hidden',
|
||
pointerEvents: 'none',
|
||
paddingTop: 0,
|
||
});
|
||
|
||
export const StreamDayLabel = style({
|
||
fontSize: toRem(11),
|
||
textTransform: 'uppercase',
|
||
letterSpacing: toRem(1),
|
||
fontWeight: 600,
|
||
color: color.Surface.OnContainer,
|
||
opacity: 0.55,
|
||
whiteSpace: 'nowrap',
|
||
flexShrink: 0,
|
||
fontFamily:
|
||
'"JetBrains Mono Variable", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
|
||
});
|
||
|
||
// Day-divider dot — abs-positioned in DotColumn so it centres on the
|
||
// (StreamDotSize-wide) column regardless of its own larger size; the
|
||
// extra 3.1 px (= dayDot 12.1 - StreamDot 9) overflows symmetrically into
|
||
// the columnGap on each side.
|
||
export const StreamDayDot = style({
|
||
position: 'absolute',
|
||
top: '50%',
|
||
left: '50%',
|
||
transform: 'translate(-50%, -50%)',
|
||
width: StreamDayDotSize,
|
||
height: StreamDayDotSize,
|
||
borderRadius: '50%',
|
||
background: color.Primary.MainHover,
|
||
boxShadow: `0 0 0 4px ${color.Surface.Container}`,
|
||
pointerEvents: 'none',
|
||
zIndex: 2,
|
||
});
|
||
|
||
// `─── Сегодня ───` line — grid item in track 3. Flex row with two flex:1
|
||
// hairlines and the label centered between them. The dot's overflow into
|
||
// the columnGap means the leftmost line segment naturally starts past
|
||
// the dot's right edge.
|
||
export const StreamDayLineWrap = style({
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: config.space.S300,
|
||
});
|
||
|
||
export const StreamDayLineSegment = style({
|
||
flex: 1,
|
||
height: 1,
|
||
background: color.Surface.ContainerLine,
|
||
minWidth: toRem(8),
|
||
});
|
||
|
||
export const MessageTextBody = recipe({
|
||
base: {
|
||
wordBreak: 'break-word',
|
||
},
|
||
variants: {
|
||
preWrap: {
|
||
true: {
|
||
whiteSpace: 'pre-wrap',
|
||
},
|
||
},
|
||
jumboEmoji: {
|
||
true: {
|
||
fontSize: '1.504em',
|
||
lineHeight: '1.4962em',
|
||
},
|
||
},
|
||
emote: {
|
||
true: {
|
||
color: color.Success.Main,
|
||
fontStyle: 'italic',
|
||
},
|
||
},
|
||
},
|
||
});
|
||
|
||
export type MessageTextBodyVariants = RecipeVariants<typeof MessageTextBody>;
|