style(message): tighten channel layout and channels list for mobile touch and rework the jump-to-message highlight
This commit is contained in:
parent
67a275561d
commit
53515ea427
4 changed files with 114 additions and 30 deletions
|
|
@ -1,24 +1,39 @@
|
|||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
import { createVar, globalStyle, style } from '@vanilla-extract/css';
|
||||
import { color, config, toRem } from 'folds';
|
||||
|
||||
// 40px circular avatar — Discord's cozy-mode avatar size. 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 = 40;
|
||||
const ChannelAvatarWidth = toRem(CHANNEL_AVATAR_PX);
|
||||
// Avatar diameter — Discord's cozy-mode 40px on desktop, compacted to 32px
|
||||
// on narrow/native viewports (the 40px slot + 16px gutters wasted ~a third
|
||||
// of a 360px screen on chrome — user request, redesign item 11). Exposed as
|
||||
// a CSS var scoped on ChannelRow so the inline `<Avatar>` size override in
|
||||
// Channel.tsx tracks the same breakpoint as the slot width without JS
|
||||
// media-query plumbing.
|
||||
export const CHANNEL_AVATAR_SIZE_VAR = createVar();
|
||||
const ChannelAvatarWidth = CHANNEL_AVATAR_SIZE_VAR;
|
||||
|
||||
// Discord cozy-mode geometry: avatar 16px from the list edge, 16px gap to the
|
||||
// content, so the message column starts at 16 + 40 + 16 = 72px.
|
||||
// Mirrors BubbleTimelineBand's desktop breakpoint (RoomTimeline.css.ts) —
|
||||
// keep the two media queries in sync.
|
||||
const MOBILE_MEDIA = 'screen and (max-width: 599px)';
|
||||
|
||||
// Discord cozy-mode geometry on desktop: avatar 16px from the list edge,
|
||||
// 16px gap to the content, so the message column starts at 16 + 40 + 16 =
|
||||
// 72px. On mobile everything tightens: 6px edge, 32px avatar, 10px gap →
|
||||
// body column at 48px from the band edge (60px from the screen edge with
|
||||
// the band's 12px native gutter), vs the old 84px.
|
||||
const ChannelEdgePad = toRem(16);
|
||||
const ChannelAvatarGap = toRem(16);
|
||||
const ChannelEdgePadMobile = toRem(6);
|
||||
const ChannelAvatarGapMobile = toRem(10);
|
||||
|
||||
export const ChannelRow = style({
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: ChannelAvatarGap,
|
||||
vars: {
|
||||
[CHANNEL_AVATAR_SIZE_VAR]: toRem(40),
|
||||
},
|
||||
// Span the full message-column width so the hover highlight runs edge-to-edge
|
||||
// like Discord: cancel MessageBase's S400/S200 horizontal padding with negative
|
||||
// margins, then re-add the 16px avatar gutter as paddingLeft (so the avatar's
|
||||
// margins, then re-add the avatar gutter as paddingLeft (so the avatar's
|
||||
// left edge lands 16px from the column edge — Discord cozy). NB: the column is
|
||||
// the centred BubbleTimelineBand, so that edge is the band's content edge, not
|
||||
// the screen edge (the band adds 12px native / 40px desktop outside this).
|
||||
|
|
@ -31,9 +46,9 @@ export const ChannelRow = style({
|
|||
paddingTop: toRem(2),
|
||||
paddingBottom: toRem(2),
|
||||
minWidth: 0,
|
||||
// Hover bg subtle so adjacent rows still read as distinct units. `@media
|
||||
// (hover: hover)` keeps this inert on touch where there's no pointer.
|
||||
'@media': {
|
||||
// Hover bg subtle so adjacent rows still read as distinct units. `@media
|
||||
// (hover: hover)` keeps this inert on touch where there's no pointer.
|
||||
'(hover: hover) and (pointer: fine)': {
|
||||
selectors: {
|
||||
'&:hover': {
|
||||
|
|
@ -41,12 +56,20 @@ export const ChannelRow = style({
|
|||
},
|
||||
},
|
||||
},
|
||||
[MOBILE_MEDIA]: {
|
||||
vars: {
|
||||
[CHANNEL_AVATAR_SIZE_VAR]: toRem(32),
|
||||
},
|
||||
gap: ChannelAvatarGapMobile,
|
||||
paddingLeft: ChannelEdgePadMobile,
|
||||
paddingRight: toRem(8),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 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).
|
||||
// (where `avatar` is `undefined` — the slot still occupies the avatar
|
||||
// diameter, so the body's left edge stays put).
|
||||
export const ChannelAvatarSlot = style({
|
||||
width: ChannelAvatarWidth,
|
||||
flexShrink: 0,
|
||||
|
|
@ -90,14 +113,23 @@ export const ChannelThreadSummary = style({
|
|||
// continuous.
|
||||
export const ChannelSysline = style({
|
||||
// Indent past the avatar gutter so the sysline body aligns with the message
|
||||
// body column (72px). The sysline sits inside MessageBase's S400 (16px) left
|
||||
// pad (it has no edge-to-edge negative margin), so paddingLeft = avatar (40)
|
||||
// + gap (16) = 56 lands the content at 16 + 56 = 72px.
|
||||
paddingLeft: `calc(${ChannelAvatarWidth} + ${ChannelAvatarGap})`,
|
||||
// body column (72px desktop). The sysline sits inside MessageBase's S400
|
||||
// (16px) left pad (it has no edge-to-edge negative margin), so paddingLeft
|
||||
// = avatar (40) + gap (16) = 56 lands the content at 16 + 56 = 72px.
|
||||
// ChannelRow's avatar-size var is out of scope here (sysline rows render
|
||||
// standalone), so the mobile compaction repeats the literals: 32 + 10 = 42,
|
||||
// minus the 10px edge-pad difference (16 − 6) the message rows gained,
|
||||
// = 32px — keeps the sysline body on the message-body column line.
|
||||
paddingLeft: `calc(${toRem(40)} + ${ChannelAvatarGap})`,
|
||||
paddingRight: config.space.S200,
|
||||
paddingTop: config.space.S100,
|
||||
paddingBottom: config.space.S100,
|
||||
color: color.SurfaceVariant.OnContainer,
|
||||
'@media': {
|
||||
[MOBILE_MEDIA]: {
|
||||
paddingLeft: toRem(32),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const ChannelSyslineIcon = style({
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { Avatar, Box, Icon, Icons, type IconSrc, as } from 'folds';
|
|||
import { type Room } from 'matrix-js-sdk';
|
||||
|
||||
import * as css from './Channel.css';
|
||||
import { CHANNEL_AVATAR_PX } from './Channel.css';
|
||||
import { CHANNEL_AVATAR_SIZE_VAR } from './Channel.css';
|
||||
import { UserAvatar } from '../../user-avatar';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
|
||||
|
|
@ -137,7 +137,16 @@ export function ChannelMessageAvatar({
|
|||
? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined
|
||||
: undefined;
|
||||
return (
|
||||
<Avatar size="300" style={{ width: CHANNEL_AVATAR_PX, height: CHANNEL_AVATAR_PX }}>
|
||||
// Size from the ChannelRow-scoped CSS var (40px desktop / 32px mobile) so
|
||||
// the avatar tracks the slot's breakpoint while still out-ranking the
|
||||
// folds size preset via inline style.
|
||||
<Avatar
|
||||
size="300"
|
||||
style={{
|
||||
width: `var(${CHANNEL_AVATAR_SIZE_VAR})`,
|
||||
height: `var(${CHANNEL_AVATAR_SIZE_VAR})`,
|
||||
}}
|
||||
>
|
||||
<UserAvatar
|
||||
userId={senderId}
|
||||
src={avatarUrl}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { createVar, globalStyle, keyframes, style, styleVariants } from '@vanilla-extract/css';
|
||||
import { recipe, RecipeVariants } from '@vanilla-extract/recipes';
|
||||
import { DefaultReset, color, config, toRem } from 'folds';
|
||||
import { VOJO_HORSESHOE_GAP_PX } from '../../../styles/horseshoe';
|
||||
|
||||
const SpacingVar = createVar();
|
||||
const SpacingVariant = styleVariants({
|
||||
|
|
@ -53,10 +54,36 @@ const highlightAnime = keyframes({
|
|||
backgroundColor: color.Primary.Container,
|
||||
},
|
||||
});
|
||||
// Jump-to-message highlight — a clean FULL-WIDTH band: flat rectangle
|
||||
// (overrides the base's rounded-right radius, which read as «square left /
|
||||
// rounded right» — wrong shape for a row spotlight) that bleeds through the
|
||||
// timeline band's horizontal gutters out to the screen edge on native and to
|
||||
// the centred band's edges on web. The bleed is transparent borders +
|
||||
// cancelling negative margins: border and margin sum to zero per side, so
|
||||
// the row's content layout doesn't shift when the highlight engages/clears,
|
||||
// while the animated background paints under the transparent borders
|
||||
// (default border-box clip). Gutter widths mirror BubbleTimelineBand
|
||||
// (RoomTimeline.css.ts): VOJO_HORSESHOE_GAP_PX native, 40px ≥600px — keep
|
||||
// in sync with that media query.
|
||||
const HighlightGutterMobile = toRem(VOJO_HORSESHOE_GAP_PX);
|
||||
const HighlightGutterDesktop = toRem(40);
|
||||
const HighlightVariant = styleVariants({
|
||||
true: {
|
||||
animation: `${highlightAnime} 2000ms ease-in-out`,
|
||||
animationIterationCount: 'infinite',
|
||||
borderRadius: 0,
|
||||
borderLeft: `${HighlightGutterMobile} solid transparent`,
|
||||
borderRight: `${HighlightGutterMobile} solid transparent`,
|
||||
marginLeft: `calc(-1 * ${HighlightGutterMobile})`,
|
||||
marginRight: `calc(-1 * ${HighlightGutterMobile})`,
|
||||
'@media': {
|
||||
'screen and (min-width: 600px)': {
|
||||
borderLeft: `${HighlightGutterDesktop} solid transparent`,
|
||||
borderRight: `${HighlightGutterDesktop} solid transparent`,
|
||||
marginLeft: `calc(-1 * ${HighlightGutterDesktop})`,
|
||||
marginRight: `calc(-1 * ${HighlightGutterDesktop})`,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { recipe } from '@vanilla-extract/recipes';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
import { color, config, toRem } from 'folds';
|
||||
|
||||
// Category headers in the channels pane, Dawn-canon take two: the list rows
|
||||
|
|
@ -7,6 +7,10 @@ import { color, config, toRem } from 'folds';
|
|||
// pill-shaped tabular badges — the headers borrow exactly that vocabulary
|
||||
// (icon chip + regular app font + count pill + rotating chevron) instead of
|
||||
// the folds Chip pill. Typography stays the app default on purpose.
|
||||
//
|
||||
// Generous vertical padding (8px on top of the 26px icon ≈ 42px row) keeps
|
||||
// the collapse target comfortably tappable on touch — the previous 4px row
|
||||
// read as a thin label, not a button.
|
||||
export const SectionHeader = style({
|
||||
appearance: 'none',
|
||||
WebkitAppearance: 'none',
|
||||
|
|
@ -15,24 +19,36 @@ export const SectionHeader = style({
|
|||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: toRem(8),
|
||||
padding: `${toRem(4)} ${toRem(8)}`,
|
||||
borderRadius: toRem(8),
|
||||
gap: toRem(10),
|
||||
padding: `${toRem(8)} ${toRem(8)}`,
|
||||
borderRadius: toRem(10),
|
||||
cursor: 'pointer',
|
||||
font: 'inherit',
|
||||
color: color.Background.OnContainer,
|
||||
transition: 'background-color 120ms ease-out',
|
||||
selectors: {
|
||||
'&:hover': {
|
||||
// Touch press feedback — `:active` clears on touchend, so it can't
|
||||
// stick the way the WebView's synthesised `:hover` does.
|
||||
'&:active': {
|
||||
backgroundColor: color.Background.ContainerHover,
|
||||
},
|
||||
'&:focus-visible': {
|
||||
outline: `2px solid ${color.Primary.Main}`,
|
||||
outlineOffset: toRem(-2),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Mouse-only hover + focus ring. Android WebView synthesises sticky
|
||||
// `:hover` AND `:focus-visible` on the tapped element and never clears
|
||||
// them until the next interaction elsewhere — ungated rules here left the
|
||||
// section header recoloured/outlined after every tap. Same `data-input`
|
||||
// gate as JumpToLatestFab / ChatComposer (matchMedia interaction queries
|
||||
// lie on Android WebView — see the input-mode detector in src/index.tsx).
|
||||
globalStyle(`:root[data-input="mouse"] ${SectionHeader}:hover`, {
|
||||
backgroundColor: color.Background.ContainerHover,
|
||||
});
|
||||
globalStyle(`:root[data-input="mouse"] ${SectionHeader}:focus-visible`, {
|
||||
outline: `2px solid ${color.Primary.Main}`,
|
||||
outlineOffset: toRem(-2),
|
||||
});
|
||||
|
||||
// Tinted square glyph — same idiom as the mock's letter-avatars in the bots
|
||||
// sidebar (rounded square, accent-on-tint). One accent per section kind.
|
||||
export const SectionIcon = recipe({
|
||||
|
|
@ -57,8 +73,8 @@ export const SectionIcon = recipe({
|
|||
});
|
||||
|
||||
export const SectionLabel = style({
|
||||
fontSize: toRem(13.5),
|
||||
lineHeight: toRem(18),
|
||||
fontSize: toRem(14),
|
||||
lineHeight: toRem(19),
|
||||
fontWeight: 600,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue